Skip to content

Commit aac8eac

Browse files
committed
feat: add session context packs
1 parent 6a05534 commit aac8eac

16 files changed

Lines changed: 765 additions & 22 deletions

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,15 @@ Operational docs: https://swarmclaw.ai/docs/observability
399399

400400
## Releases
401401

402+
### v1.9.14 Highlights
403+
404+
Session context-pack release: SwarmClaw now turns a live chat into a concise handoff packet with session metadata, recent visible turns, linked tasks, attachments, resume handles, and next actions.
405+
406+
- **Context-pack API.** `GET /api/chats/:id/context-pack` returns structured handoff JSON, and `?format=markdown` returns copyable markdown.
407+
- **Chat header copy action.** Active chats with messages expose a context-pack button for quick handoff to another operator or backend.
408+
- **CLI access.** `swarmclaw chats context-pack <chatId> --query format=markdown` exposes the same packet for scripts and release automation.
409+
- **Smoke coverage.** Runtime tests and the browser smoke gate now verify the context-pack route and markdown response.
410+
402411
### v1.9.13 Highlights
403412

404413
Architecture health release: SwarmClaw now turns runtime ownership, dispatch, memory, startup, and quality evidence into a scored operator report.

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@swarmclawai/swarmclaw",
3-
"version": "1.9.13",
3+
"version": "1.9.14",
44
"description": "Build and run autonomous AI agents with OpenClaw, Hermes, multiple model providers, orchestration, delegation, memory, skills, schedules, and chat connectors.",
55
"main": "electron-dist/main.js",
66
"license": "MIT",
@@ -88,7 +88,7 @@
8888
"test:cli": "node --test src/cli/*.test.js bin/*.test.js scripts/electron-after-pack.test.mjs scripts/ensure-sandbox-browser-image.test.mjs scripts/postinstall.test.mjs scripts/run-next-build.test.mjs scripts/run-next-typegen.test.mjs",
8989
"test:setup": "tsx --test src/app/api/setup/check-provider/route.test.ts src/lib/server/provider-model-discovery.test.ts src/components/auth/setup-wizard/utils.test.ts src/components/auth/setup-wizard/types.test.ts src/hooks/setup-done-detection.test.ts src/lib/setup-defaults.test.ts src/lib/server/storage-auth.test.ts src/lib/server/storage-auth-docker.test.ts",
9090
"test:openclaw": "tsx --test src/lib/openclaw/openclaw-agent-id.test.ts src/lib/openclaw/openclaw-endpoint.test.ts src/lib/server/agents/agent-runtime-config.test.ts src/lib/server/build-llm.test.ts src/lib/server/connectors/connector-routing.test.ts src/lib/server/connectors/openclaw.test.ts src/lib/server/connectors/swarmdock.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/gateways/gateway-topology.test.ts src/lib/server/llm-response-cache.test.ts src/lib/server/mcp-conformance.test.ts src/lib/server/openclaw/agent-resolver.test.ts src/lib/server/openclaw/deploy.test.ts src/lib/server/openclaw/skills-normalize.test.ts src/lib/server/session-tools/openclaw-nodes.test.ts src/lib/server/session-tools/swarmdock.test.ts src/lib/server/tasks/task-quality-gate.test.ts src/lib/server/tasks/task-validation.test.ts src/lib/server/tool-capability-policy.test.ts src/lib/providers/openai.test.ts src/lib/providers/openclaw-exports.test.ts src/app/api/gateways/topology-route.test.ts src/app/api/openclaw/dashboard-url/route.test.ts",
91-
"test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/agents/delegation-advisory.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/extension-managed-resources.test.ts src/lib/server/eval/baseline.test.ts src/lib/server/eval/environment-plan.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chat-execution/reasoning-tag-scrubber.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/runtime/queue-retry-policy.test.ts src/lib/server/runs/run-brief.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/schedules/schedule-history.test.ts src/lib/quality/release-readiness.test.ts src/lib/quality/architecture-health.test.ts src/lib/server/artifacts/artifact-resolver.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/server/tasks/task-execution-workspace.test.ts src/lib/server/tasks/task-execution-policy.test.ts src/lib/server/tasks/task-handoff.test.ts src/lib/server/tasks/task-service.test.ts src/lib/server/session-tools/execute.test.ts src/lib/server/session-tools/manage-tasks.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/tasks/task-workspace-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/extensions/managed-resources/route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/portability/export/route.test.ts src/app/api/portability/import/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/schedules/schedule-history-route.test.ts src/app/api/tts/route.test.ts",
91+
"test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/agents/delegation-advisory.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/extension-managed-resources.test.ts src/lib/server/eval/baseline.test.ts src/lib/server/eval/environment-plan.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chat-execution/reasoning-tag-scrubber.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/chats/session-context-pack.test.ts src/lib/server/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/runtime/queue-retry-policy.test.ts src/lib/server/runs/run-brief.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/schedules/schedule-history.test.ts src/lib/quality/release-readiness.test.ts src/lib/quality/architecture-health.test.ts src/lib/server/artifacts/artifact-resolver.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/server/tasks/task-execution-workspace.test.ts src/lib/server/tasks/task-execution-policy.test.ts src/lib/server/tasks/task-handoff.test.ts src/lib/server/tasks/task-service.test.ts src/lib/server/session-tools/execute.test.ts src/lib/server/session-tools/manage-tasks.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/tasks/task-workspace-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-pack-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/extensions/managed-resources/route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/portability/export/route.test.ts src/app/api/portability/import/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/schedules/schedule-history-route.test.ts src/app/api/tts/route.test.ts",
9292
"test:builder": "tsx --test src/features/protocols/builder/utils/nodes-to-template.test.ts src/features/protocols/builder/utils/template-to-nodes.test.ts src/features/protocols/builder/validators/dag-validator.test.ts",
9393
"test:e2e": "node --import tsx scripts/browser-e2e-smoke.ts",
9494
"test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",

scripts/browser-e2e-smoke.ts

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ interface AuthSession {
2424
cookieHeader: string
2525
}
2626

27+
function authHeaders(auth: AuthSession, contentType = true): Record<string, string> {
28+
return {
29+
...(contentType ? { 'Content-Type': 'application/json' } : {}),
30+
'X-Access-Key': auth.accessKey,
31+
...(auth.cookieHeader ? { Cookie: auth.cookieHeader } : {}),
32+
}
33+
}
34+
2735
function printHelp(): void {
2836
console.log(`SwarmClaw browser smoke runner
2937
@@ -363,11 +371,7 @@ async function runBrowserSmoke(baseUrl: string): Promise<void> {
363371

364372
const taskRes = await fetchWithTimeout(new URL('/api/tasks', baseUrl).toString(), {
365373
method: 'POST',
366-
headers: {
367-
'Content-Type': 'application/json',
368-
'X-Access-Key': auth.accessKey,
369-
...(auth.cookieHeader ? { Cookie: auth.cookieHeader } : {}),
370-
},
374+
headers: authHeaders(auth),
371375
body: JSON.stringify({
372376
title: 'E2E task workspace',
373377
description: 'Verify task-scoped workspace and preview metadata render in the task board.',
@@ -380,6 +384,49 @@ async function runBrowserSmoke(baseUrl: string): Promise<void> {
380384
throw new Error(`Could not create task workspace smoke record: ${taskRes.status} ${await taskRes.text().catch(() => '')}`)
381385
}
382386

387+
const chatRes = await fetchWithTimeout(new URL('/api/chats', baseUrl).toString(), {
388+
method: 'POST',
389+
headers: authHeaders(auth),
390+
body: JSON.stringify({
391+
name: 'E2E context pack',
392+
provider: 'claude-cli',
393+
model: 'sonnet',
394+
messages: [
395+
{ role: 'user', text: 'Prepare a handoff pack.', time: Date.now(), attachedFiles: ['/tmp/e2e-context.md'] },
396+
{ role: 'assistant', text: 'The handoff pack is ready.', time: Date.now() + 1 },
397+
],
398+
}),
399+
}, 10_000)
400+
if (!chatRes.ok) {
401+
throw new Error(`Could not create context pack smoke chat: ${chatRes.status} ${await chatRes.text().catch(() => '')}`)
402+
}
403+
const chatPayload = await chatRes.json().catch(() => null) as { id?: unknown } | null
404+
if (typeof chatPayload?.id !== 'string' || !chatPayload.id) {
405+
throw new Error(`Context pack smoke chat returned an unexpected payload: ${JSON.stringify(chatPayload)}`)
406+
}
407+
408+
const contextPackRes = await fetchWithTimeout(new URL(`/api/chats/${chatPayload.id}/context-pack`, baseUrl).toString(), {
409+
headers: authHeaders(auth),
410+
}, 10_000)
411+
if (!contextPackRes.ok) {
412+
throw new Error(`Context pack JSON route returned ${contextPackRes.status}: ${await contextPackRes.text().catch(() => '')}`)
413+
}
414+
const contextPack = await contextPackRes.json().catch(() => null) as { schemaVersion?: unknown; session?: { id?: unknown }; recentMessages?: unknown[] } | null
415+
if (contextPack?.schemaVersion !== 1 || contextPack.session?.id !== chatPayload.id || !Array.isArray(contextPack.recentMessages)) {
416+
throw new Error(`Context pack JSON returned an unexpected payload: ${JSON.stringify(contextPack)}`)
417+
}
418+
419+
const contextPackMarkdownRes = await fetchWithTimeout(new URL(`/api/chats/${chatPayload.id}/context-pack?format=markdown`, baseUrl).toString(), {
420+
headers: authHeaders(auth),
421+
}, 10_000)
422+
if (!contextPackMarkdownRes.ok) {
423+
throw new Error(`Context pack markdown route returned ${contextPackMarkdownRes.status}: ${await contextPackMarkdownRes.text().catch(() => '')}`)
424+
}
425+
const contextPackMarkdown = await contextPackMarkdownRes.text()
426+
if (!contextPackMarkdown.includes('# Session Context Pack: E2E context pack') || !contextPackMarkdown.includes('Recent Turns')) {
427+
throw new Error(`Context pack markdown returned an unexpected body: ${contextPackMarkdown.slice(0, 240)}`)
428+
}
429+
383430
page = await newSmokePage()
384431
await smokeStep('task board shows created workspace task', () => waitForPageText(page, '/tasks?taskView=all', {
385432
anyText: ['E2E task workspace'],
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { NextResponse } from 'next/server'
2+
3+
import { buildSessionContextPack, formatSessionContextPackMarkdown } from '@/lib/server/chats/session-context-pack'
4+
import { notFound } from '@/lib/server/collection-helpers'
5+
import { getMessages } from '@/lib/server/messages/message-repository'
6+
import { getSession } from '@/lib/server/sessions/session-repository'
7+
import { listTasks } from '@/lib/server/tasks/task-repository'
8+
9+
const DEFAULT_RECENT_MESSAGES = 12
10+
const MAX_RECENT_MESSAGES = 40
11+
12+
function parseMaxRecentMessages(req: Request): number {
13+
const url = new URL(req.url)
14+
const raw = url.searchParams.get('messages') || url.searchParams.get('maxRecentMessages')
15+
const parsed = Number.parseInt(raw || '', 10)
16+
if (!Number.isFinite(parsed)) return DEFAULT_RECENT_MESSAGES
17+
return Math.max(1, Math.min(MAX_RECENT_MESSAGES, parsed))
18+
}
19+
20+
export async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) {
21+
const { id } = await params
22+
const session = getSession(id)
23+
if (!session) return notFound()
24+
25+
const url = new URL(req.url)
26+
const format = url.searchParams.get('format')
27+
const pack = buildSessionContextPack({
28+
session,
29+
messages: getMessages(id),
30+
tasks: listTasks(),
31+
maxRecentMessages: parseMaxRecentMessages(req),
32+
})
33+
34+
if (format === 'markdown') {
35+
return new Response(formatSessionContextPackMarkdown(pack), {
36+
headers: {
37+
'content-type': 'text/markdown; charset=utf-8',
38+
},
39+
})
40+
}
41+
42+
return NextResponse.json(pack)
43+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import assert from 'node:assert/strict'
2+
import test from 'node:test'
3+
4+
import { runWithTempDataDir } from '@/lib/server/test-utils/run-with-temp-data-dir'
5+
6+
test('GET /api/chats/[id]/context-pack returns structured and markdown handoff context', () => {
7+
const output = runWithTempDataDir<{
8+
status: number
9+
markdownStatus: number
10+
missingStatus: number
11+
schemaVersion: number
12+
linkedTaskCount: number
13+
recentMessageCount: number
14+
markdownContentType: string
15+
markdownIncludesTitle: boolean
16+
markdownIncludesTask: boolean
17+
}>(`
18+
const storageMod = await import('./src/lib/server/storage')
19+
const repoMod = await import('@/lib/server/messages/message-repository')
20+
const routeMod = await import('./src/app/api/chats/[id]/context-pack/route')
21+
const storage = storageMod.default || storageMod
22+
const repo = repoMod.default || repoMod
23+
const route = routeMod.default || routeMod
24+
25+
const now = Date.now()
26+
storage.saveSessions({
27+
sess_pack_1: {
28+
id: 'sess_pack_1',
29+
name: 'Context pack test',
30+
cwd: process.env.WORKSPACE_DIR,
31+
user: 'tester',
32+
provider: 'openai',
33+
model: 'gpt-4o-mini',
34+
claudeSessionId: null,
35+
codexThreadId: 'codex-pack-thread',
36+
messages: [],
37+
createdAt: now,
38+
lastActiveAt: now,
39+
runContext: {
40+
objective: 'Hand off a release session.',
41+
constraints: ['Keep the pack short.'],
42+
keyFacts: ['The current tag is local only.'],
43+
discoveries: [],
44+
failedApproaches: [],
45+
currentPlan: ['Run checks'],
46+
completedSteps: [],
47+
blockers: [],
48+
parentContext: null,
49+
updatedAt: now,
50+
version: 1,
51+
},
52+
},
53+
})
54+
storage.saveTasks({
55+
task_pack_1: {
56+
id: 'task_pack_1',
57+
title: 'Verify context pack',
58+
description: 'Exercise API route.',
59+
status: 'running',
60+
sessionId: 'sess_pack_1',
61+
agentId: null,
62+
createdAt: now,
63+
updatedAt: now + 3,
64+
},
65+
})
66+
67+
repo.appendMessage('sess_pack_1', { role: 'user', text: 'Prepare handoff.', time: now, attachedFiles: ['/tmp/handoff.md'] })
68+
repo.appendMessage('sess_pack_1', { role: 'assistant', text: 'Ready.', time: now + 1 })
69+
70+
const response = await route.GET(
71+
new Request('http://local/api/chats/sess_pack_1/context-pack?messages=1'),
72+
{ params: Promise.resolve({ id: 'sess_pack_1' }) },
73+
)
74+
const payload = await response.json()
75+
76+
const markdownResponse = await route.GET(
77+
new Request('http://local/api/chats/sess_pack_1/context-pack?format=markdown'),
78+
{ params: Promise.resolve({ id: 'sess_pack_1' }) },
79+
)
80+
const markdown = await markdownResponse.text()
81+
82+
const missingResponse = await route.GET(
83+
new Request('http://local/api/chats/missing/context-pack'),
84+
{ params: Promise.resolve({ id: 'missing' }) },
85+
)
86+
87+
console.log(JSON.stringify({
88+
status: response.status,
89+
markdownStatus: markdownResponse.status,
90+
missingStatus: missingResponse.status,
91+
schemaVersion: payload.schemaVersion,
92+
linkedTaskCount: payload.linkedTasks.length,
93+
recentMessageCount: payload.recentMessages.length,
94+
markdownContentType: markdownResponse.headers.get('content-type') || '',
95+
markdownIncludesTitle: markdown.includes('# Session Context Pack: Context pack test'),
96+
markdownIncludesTask: markdown.includes('Verify context pack'),
97+
}))
98+
`, { prefix: 'swarmclaw-context-pack-route-' })
99+
100+
assert.equal(output.status, 200)
101+
assert.equal(output.markdownStatus, 200)
102+
assert.equal(output.missingStatus, 404)
103+
assert.equal(output.schemaVersion, 1)
104+
assert.equal(output.linkedTaskCount, 1)
105+
assert.equal(output.recentMessageCount, 1)
106+
assert.match(output.markdownContentType, /text\/markdown/)
107+
assert.equal(output.markdownIncludesTitle, true)
108+
assert.equal(output.markdownIncludesTask, true)
109+
})

src/cli/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,7 @@ const COMMAND_GROUPS = [
627627
cmd('clear-undo', 'POST', '/chats/:id/clear/undo', 'Restore a cleared chat via its undoToken', { expectsJsonBody: true }),
628628
cmd('compact', 'POST', '/chats/:id/compact', 'Summarize and compact chat history (accepts optional keepLastN)', { expectsJsonBody: true }),
629629
cmd('context-status', 'GET', '/chats/:id/context-status', 'Report token usage and context-window status for a chat'),
630+
cmd('context-pack', 'GET', '/chats/:id/context-pack', 'Get a copyable session context pack for handoff'),
630631
cmd('browser-status', 'GET', '/chats/:id/browser', 'Check browser status'),
631632
cmd('browser-close', 'DELETE', '/chats/:id/browser', 'Close browser'),
632633
cmd('mailbox', 'GET', '/chats/:id/mailbox', 'List chat mailbox envelopes'),

0 commit comments

Comments
 (0)