@swarmclawai/swarmclaw 1.5.62 → 1.5.64
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -10
- package/package.json +2 -2
- package/src/app/api/chatrooms/[id]/chat/route.test.ts +111 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +47 -28
- package/src/app/api/chatrooms/[id]/members/route.ts +0 -8
- package/src/app/api/chatrooms/[id]/route.ts +0 -11
- package/src/app/api/chatrooms/route.ts +0 -10
- package/src/app/api/chats/[id]/clear/route.ts +7 -3
- package/src/app/api/chats/[id]/clear/undo/route.ts +23 -0
- package/src/app/api/chats/[id]/compact/route.ts +72 -0
- package/src/app/api/chats/[id]/context-status/route.ts +21 -0
- package/src/app/api/chats/clear-route.test.ts +121 -0
- package/src/app/api/chats/compact-route.test.ts +70 -0
- package/src/app/api/chats/context-status-route.test.ts +68 -0
- package/src/app/api/mcp-servers/[id]/route.ts +5 -0
- package/src/app/api/mcp-servers/[id]/test/route.ts +5 -0
- package/src/app/api/mcp-servers/[id]/tools-info/route.ts +75 -0
- package/src/cli/index.js +5 -1
- package/src/cli/spec.js +4 -1
- package/src/components/chat/chat-area.tsx +62 -6
- package/src/components/chat/chat-header.tsx +13 -1
- package/src/components/chat/context-meter-badge.tsx +227 -0
- package/src/components/chatrooms/chatroom-sheet.tsx +1 -4
- package/src/components/mcp-servers/mcp-server-list.tsx +56 -0
- package/src/components/mcp-servers/mcp-server-sheet.tsx +202 -1
- package/src/components/mcp-servers/registry-browser.tsx +224 -0
- package/src/lib/chat/chats.ts +37 -1
- package/src/lib/server/chats/chat-session-service.ts +75 -0
- package/src/lib/server/chats/clear-undo-snapshots.test.ts +107 -0
- package/src/lib/server/chats/clear-undo-snapshots.ts +92 -0
- package/src/lib/server/mcp-connection-pool.test.ts +98 -0
- package/src/lib/server/mcp-connection-pool.ts +134 -0
- package/src/lib/server/mcp-gateway-runtime.test.ts +177 -0
- package/src/lib/server/mcp-gateway-runtime.ts +138 -0
- package/src/lib/server/session-tools/chatroom.ts +0 -11
- package/src/lib/server/session-tools/index.ts +83 -15
- package/src/lib/server/storage-normalization.ts +11 -0
- package/src/types/agent.ts +1 -0
- package/src/types/misc.ts +7 -0
package/README.md
CHANGED
|
@@ -399,6 +399,31 @@ Operational docs: https://swarmclaw.ai/docs/observability
|
|
|
399
399
|
|
|
400
400
|
## Releases
|
|
401
401
|
|
|
402
|
+
### v1.5.64 Highlights
|
|
403
|
+
|
|
404
|
+
Two themes this release. First, **context-window management reaches the chat UI**: a live token-usage meter in every chat header, a one-click LLM-backed compaction that keeps the session alive without nuking history, and a redesigned clear flow with a 30-second undo that restores both transcripts and CLI resume IDs. Second, **MCP token spend is now controllable**: per-server `alwaysExpose` policy, per-agent eager-tool overrides, an in-session `mcp_tool_search` promoter, a long-lived connection pool, a token-cost endpoint per server, and a built-in browser for the public SwarmDock MCP registry.
|
|
405
|
+
|
|
406
|
+
- **Context meter in the chat header.** New `ContextMeterBadge` (`src/components/chat/context-meter-badge.tsx`) renders a live chip showing `N% · Mk` next to the chat title, driven by `GET /api/chats/:id/context-status`. Color thresholds at 70% (amber) and 90% (red). Clicking the chip opens a popover with the full breakdown (used / remaining / messages) plus Compact and Clear buttons. The button row explicitly states: *"Long-term memory, skills, and facts are preserved. Clear only affects this chat transcript."* — so users stop fearing Clear.
|
|
407
|
+
- **User-invokable `/compact` via the popover.** New `POST /api/chats/:id/compact` runs `summarizeAndCompact` with the session's own provider/model via `buildChatModel` as the summarizer. The existing hierarchical-summary pipeline in `context-manager.ts` does the work: tool failures, file ops, and adaptive chunking are all preserved. Accepts `keepLastN` in the body (2-200, default 10). Returns `status: 'no_action' | 'compacted'` plus counts. The popover gates the button below 3 messages so users don't waste LLM calls on trivially short transcripts.
|
|
408
|
+
- **Clear with 30-second undo.** `POST /api/chats/:id/clear` now returns `{ cleared, undoToken, expiresAt }`, and a new `POST /api/chats/:id/clear/undo` restores the snapshot. The undo snapshot (messages + every CLI session ID including `claudeSessionId`, `codexThreadId`, `opencodeSessionId`, `opencodeWebSessionId`, `geminiSessionId`, `copilotSessionId`, `droidSessionId`, `cursorSessionId`, `qwenSessionId`, `acpSessionId`, and `delegateResumeIds`) lives in an HMR-safe in-memory store (`src/lib/server/chats/clear-undo-snapshots.ts`) with a 30-second TTL, 200-entry cap, session-scoped lookups, and single-use tokens. The chat UI wires this to a sonner toast with an Undo action; restoring fires a "Chat restored." confirmation toast.
|
|
409
|
+
- **`alwaysExpose` policy for MCP servers** (`McpServerConfig.alwaysExpose: boolean | string[]`, default `true` for back-compat). Set `false` on a chatty server (e.g. a Playwright MCP with 40 tools that cost thousands of tokens per turn) and the agent binds nothing up front — it can still discover and promote specific tools via the new `mcp_tool_search` meta-tool. Set an allowlist `['query_resources', 'fetch_url']` to eagerly bind a curated subset.
|
|
410
|
+
- **Per-agent `mcpEagerTools` override** (`Agent.mcpEagerTools?: string[]`) lets you force-expose specific tool names for a specific agent regardless of the server's `alwaysExpose`. Precedence: per-agent allowlist > server `alwaysExpose` > session promotions.
|
|
411
|
+
- **`mcp_tool_search` meta-tool** (`src/lib/server/mcp-gateway-runtime.ts`). When a server's tools are lazy, the agent gets a single `mcp_tool_search({ query, limit? })` tool that searches the process-wide discovery cache (bare name substring + description keywords) and promotes matches for the current session. The next turn binds the promoted names for real. `SessionToolPromoter` state is keyed by session ID and HMR-safe. Behavior mirrors `@swarmclawai/mcp-gateway`'s router so users who split MCP fan-out across SwarmClaw and the gateway get consistent semantics.
|
|
412
|
+
- **Long-lived MCP connection pool** (`src/lib/server/mcp-connection-pool.ts`). A single client/transport per server lives for the process lifetime instead of reconnecting every turn. Config-fingerprint tracking rotates stale entries automatically; the `/test` endpoint evicts explicitly so a config change takes effect immediately. Saves ~100-500ms × (servers × turns) per chat. HMR-safe via `hmrSingleton` so dev reloads don't leak child processes.
|
|
413
|
+
- **Token-cost discovery endpoint** (`GET /api/mcp-servers/:id/tools-info`). Connects, lists tools, and reports per-tool schema tokens plus aggregates — using the same `chars / 3.5` formula as `@swarmclawai/mcp-gateway` so numbers line up side by side. Surfaces inside `mcp-server-list.tsx` so you can see which server is the costliest before an agent even runs.
|
|
414
|
+
- **SwarmDock MCP Registry browser** (`src/components/mcp-servers/registry-browser.tsx`). Opens from the New MCP Server sheet and browses the public registry at `https://swarmdock-api.onrender.com/api/v1/mcp/servers`. Selecting a server populates the form with its recommended install command — one-click discovery without leaving SwarmClaw. A new `MCP Gateway (local)` preset is also bundled so users can bootstrap `@swarmclawai/mcp-gateway` in one tap.
|
|
415
|
+
- **4 new CLI commands.** `swarmclaw chats context-status <id>`, `swarmclaw chats compact <id>`, `swarmclaw chats clear-undo <id>`, and the existing `chats clear` now returns the undo token so CLI scripts can build their own clear+undo workflows.
|
|
416
|
+
- **Back-compat normalization.** Existing MCP servers load with `alwaysExpose: true` (historical behavior — every tool bound every turn) via `storage-normalization.ts`. No user action required to upgrade.
|
|
417
|
+
- **Full regression coverage.** New tests: `clear-undo-snapshots.test.ts` (5 cases — TTL, single-use, session isolation, CLI-id preservation, expiry sweep), `clear-route.test.ts` (clear → undo → double-undo 404 → missing-session 404 round-trip), `compact-route.test.ts` (no-action path + 404), `context-status-route.test.ts`, plus `mcp-connection-pool.test.ts` and `mcp-gateway-runtime.test.ts`. `test:runtime` runs 100 tests across 13 suites.
|
|
418
|
+
|
|
419
|
+
### v1.5.63 Highlights
|
|
420
|
+
|
|
421
|
+
Chatroom fix from @borislavnnikolov: CLI-backed agents (codex-cli, copilot-cli, gemini-cli, and the rest of the `NON_LANGGRAPH_PROVIDER_IDS` set) now work correctly as chatroom members instead of falling through a LangGraph path they cannot run. With the execution path fixed, the worker-only membership blocks are lifted too, so any non-trashed agent can be added to a room.
|
|
422
|
+
|
|
423
|
+
- **Direct provider runtime for CLI chatroom turns.** `src/app/api/chatrooms/[id]/chat/route.ts` now branches on `NON_LANGGRAPH_PROVIDER_IDS` and calls `provider.handler.streamChat()` directly for CLI-backed agents while keeping the LangGraph `streamAgentChat` path for everything else. Streaming, tool events, and persisted messages all flow through unchanged.
|
|
424
|
+
- **Full member selection.** The create, update, members, session-tool, and UI layers (`src/app/api/chatrooms/route.ts`, `src/app/api/chatrooms/[id]/route.ts`, `src/app/api/chatrooms/[id]/members/route.ts`, `src/lib/server/session-tools/chatroom.ts`, `src/components/chatrooms/chatroom-sheet.tsx`) no longer reject or hide worker-only agents. Any non-trashed agent is eligible.
|
|
425
|
+
- **Regression test.** `src/app/api/chatrooms/[id]/chat/route.test.ts` proves a `codex-cli`-backed chatroom turn bypasses `streamAgentChat`, streams a response through the provider handler, and persists one assistant reply.
|
|
426
|
+
|
|
402
427
|
### v1.5.62 Highlights
|
|
403
428
|
|
|
404
429
|
Hardens parallel sub-agent dispatch with a concurrency cap, a quorum join policy, and a cycle check — so a fan-out can't accidentally saturate providers, melt a mission budget, or wedge the runtime on a delegation loop.
|
|
@@ -432,16 +457,6 @@ Viral-loop release. Adds public share links for missions, skills, and sessions,
|
|
|
432
457
|
- **Share-link-based skill install.** `POST /api/skills/import` already accepts an http(s) URL; pointing it at `https://<your-host>/api/s/<token>/raw` now installs a shared skill from another SwarmClaw instance without auth handshakes. Pairs naturally with existing `swarmclaw skills import` CLI.
|
|
433
458
|
- **Share-link repository tests.** `share-link-repository.test.ts` covers mint / list / revoke / lookup-by-token round-trip plus expiry handling against a temporary data dir.
|
|
434
459
|
|
|
435
|
-
### v1.5.58 Highlights
|
|
436
|
-
|
|
437
|
-
This release broadens the built-in evaluation harness so SwarmClaw runs can be benchmarked against named suites, adds two targeted starter kits, exposes live per-session cost data, tightens auto-skill drafting, and ships a zero-setup demo mission template.
|
|
438
|
-
|
|
439
|
-
- **Benchmark-style eval suites.** New `SWEBENCH_LITE_SCENARIOS` and `GAIA_L1_SCENARIOS` in `src/lib/server/eval/scenarios-swebench.ts` and `scenarios-gaia.ts` — curated parallels (not the upstream datasets) sized for a single-agent harness run. The shared `EvalScenario` type now carries an optional `suite: 'core' | 'swe-bench-lite' | 'gaia-l1' | 'tool-use' | 'code-action'` tag. `POST /api/eval/suite` accepts `{ suite: "swe-bench-lite" }` to scope a run. New `GET /api/eval/suites` lists every suite with scenario count, max score, and categories. CLI commands: `swarmclaw eval suites`, and `swarmclaw eval suite` still takes a JSON body now including `suite`. Useful for advertising verifiable numbers against a named benchmark instead of a bespoke scoring rubric.
|
|
440
|
-
- **Two additional starter kits.** `inbox_triage` (single Triager agent over email + memory + documents) and `data_analyst` (single Analyst agent over shell + files + web + documents) join the existing seven kits in `src/lib/setup-defaults.ts`. Both are surfaced on the intent-driven setup path alongside Personal Assistant, Research Copilot, Builder Studio, and Delegate Team.
|
|
441
|
-
- **Live per-session usage API.** New `GET /api/usage/live?sessionId=...` returns a lightweight snapshot — records, tokens in/out, estimated cost, firstAt/lastAt, wallclockMs, turns — so frontends can surface a live cost meter without pulling the full aggregated `/api/usage` payload. Without a `sessionId` the route returns the ten most recently active sessions. Registered in the CLI as `swarmclaw usage live`.
|
|
442
|
-
- **Auto-skill drafting is stricter and rate-limited.** `shouldAutoDraftSkillSuggestion` in `chat-turn-finalization.ts` now requires at least 3 tool events in the completed turn (was 1), and a new per-agent daily cap limits automatic drafts to 3 per day per agent to prevent suggestion-inbox spam. Both thresholds are named constants (`AUTO_DRAFT_MIN_TOOL_EVENTS`, `AUTO_DRAFT_DAILY_LIMIT`). Agents with `autoDraftSkillSuggestions = false` are unaffected (auto-drafting remains opt-in per agent).
|
|
443
|
-
- **Hello World demo mission template.** New `hello-world-demo` entry in `BUILT_IN_MISSION_TEMPLATES` — a bounded, zero-setup mission that reads three files in the working directory and writes a one-paragraph markdown summary to `hello-world-report.md`. Budgets (USD 0.25, 20k tokens, 30 turns, 15 min) are small enough to run on a local Ollama model without cost. Intended as the first thing a new user watches an agent complete end to end.
|
|
444
|
-
|
|
445
460
|
Older releases: https://swarmclaw.ai/docs/release-notes
|
|
446
461
|
|
|
447
462
|
- GitHub releases: https://github.com/swarmclawai/swarmclaw/releases
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.64",
|
|
4
4
|
"description": "Build and run autonomous AI agents with OpenClaw, Hermes, multiple model providers, orchestration, delegation, memory, skills, schedules, and chat connectors.",
|
|
5
5
|
"main": "electron-dist/main.js",
|
|
6
6
|
"license": "MIT",
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"test:cli": "node --test src/cli/*.test.js bin/*.test.js scripts/postinstall.test.mjs scripts/run-next-build.test.mjs scripts/run-next-typegen.test.mjs",
|
|
81
81
|
"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",
|
|
82
82
|
"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/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/openclaw-exports.test.ts src/app/api/openclaw/dashboard-url/route.test.ts",
|
|
83
|
-
"test:runtime": "tsx --test src/lib/server/knowledge-sources.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/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.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/chats/chat-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/tts/route.test.ts",
|
|
83
|
+
"test:runtime": "tsx --test 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/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/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/observability/otel-config.test.ts src/lib/server/safe-parse-body.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/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/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/tts/route.test.ts",
|
|
84
84
|
"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",
|
|
85
85
|
"test:e2e": "tsx .workbench/browser-e2e/run.ts",
|
|
86
86
|
"test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",
|
|
@@ -297,3 +297,114 @@ test('chatroom route forwards tool activity and records one reply per participat
|
|
|
297
297
|
assert.deepEqual(output.assistantCounts, { alpha: 1, beta: 1 })
|
|
298
298
|
assert.deepEqual([...new Set(output.agentOrder)].sort(), ['alpha', 'beta'])
|
|
299
299
|
})
|
|
300
|
+
|
|
301
|
+
test('chatroom route uses direct provider runtime for CLI providers', () => {
|
|
302
|
+
const output = runWithTempDataDir<{
|
|
303
|
+
errors: string[]
|
|
304
|
+
streamedText: string
|
|
305
|
+
assistantTexts: string[]
|
|
306
|
+
}>(`
|
|
307
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
308
|
+
const providersMod = await import('@/lib/providers')
|
|
309
|
+
const routeMod = await import('./src/app/api/chatrooms/[id]/chat/route')
|
|
310
|
+
const streamMod = await import('@/lib/server/chat-execution/stream-agent-chat')
|
|
311
|
+
const storage = storageMod.default || storageMod
|
|
312
|
+
const providers = providersMod.default || providersMod
|
|
313
|
+
const route = routeMod.default || routeMod
|
|
314
|
+
const stream = streamMod.default || streamMod
|
|
315
|
+
|
|
316
|
+
const originalHandler = providers.PROVIDERS['codex-cli'].handler
|
|
317
|
+
|
|
318
|
+
const now = Date.now()
|
|
319
|
+
storage.saveAgents({
|
|
320
|
+
alpha: {
|
|
321
|
+
id: 'alpha',
|
|
322
|
+
name: 'Alpha',
|
|
323
|
+
provider: 'codex-cli',
|
|
324
|
+
model: 'gpt-5.3-codex',
|
|
325
|
+
extensions: [],
|
|
326
|
+
createdAt: now,
|
|
327
|
+
updatedAt: now,
|
|
328
|
+
},
|
|
329
|
+
})
|
|
330
|
+
storage.saveChatrooms({
|
|
331
|
+
room_1: {
|
|
332
|
+
id: 'room_1',
|
|
333
|
+
name: 'CLI Room',
|
|
334
|
+
agentIds: ['alpha'],
|
|
335
|
+
messages: [],
|
|
336
|
+
createdAt: now,
|
|
337
|
+
updatedAt: now,
|
|
338
|
+
chatMode: 'sequential',
|
|
339
|
+
autoAddress: true,
|
|
340
|
+
},
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
async function readSse(response) {
|
|
344
|
+
const reader = response.body.getReader()
|
|
345
|
+
const decoder = new TextDecoder()
|
|
346
|
+
let buffer = ''
|
|
347
|
+
const events = []
|
|
348
|
+
while (true) {
|
|
349
|
+
const { done, value } = await reader.read()
|
|
350
|
+
if (done) break
|
|
351
|
+
buffer += decoder.decode(value, { stream: true })
|
|
352
|
+
let idx = buffer.indexOf('\\n\\n')
|
|
353
|
+
while (idx !== -1) {
|
|
354
|
+
const chunk = buffer.slice(0, idx)
|
|
355
|
+
buffer = buffer.slice(idx + 2)
|
|
356
|
+
const line = chunk
|
|
357
|
+
.split('\\n')
|
|
358
|
+
.map((entry) => entry.trim())
|
|
359
|
+
.find((entry) => entry.startsWith('data: '))
|
|
360
|
+
if (line) {
|
|
361
|
+
events.push(JSON.parse(line.slice(6)))
|
|
362
|
+
}
|
|
363
|
+
idx = buffer.indexOf('\\n\\n')
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return events
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
stream.setStreamAgentChatForTest(async () => {
|
|
370
|
+
throw new Error('streamAgentChat should not be called for codex-cli chatroom turns')
|
|
371
|
+
})
|
|
372
|
+
providers.PROVIDERS['codex-cli'].handler = {
|
|
373
|
+
streamChat: async (opts) => {
|
|
374
|
+
const reply = 'Codex CLI answered from direct provider runtime.'
|
|
375
|
+
opts.write('data: ' + JSON.stringify({ t: 'd', text: reply }) + '\\n')
|
|
376
|
+
return reply
|
|
377
|
+
},
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
const response = await route.POST(
|
|
382
|
+
new Request('http://local/api/chatrooms/room_1/chat', {
|
|
383
|
+
method: 'POST',
|
|
384
|
+
headers: { 'content-type': 'application/json' },
|
|
385
|
+
body: JSON.stringify({ senderId: 'user', text: 'Say hello to the room.' }),
|
|
386
|
+
}),
|
|
387
|
+
{ params: Promise.resolve({ id: 'room_1' }) },
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
const events = await readSse(response)
|
|
391
|
+
const chatroom = storage.loadChatrooms().room_1
|
|
392
|
+
const assistantTexts = chatroom.messages
|
|
393
|
+
.filter((entry) => entry.role === 'assistant')
|
|
394
|
+
.map((entry) => entry.text)
|
|
395
|
+
|
|
396
|
+
console.log(JSON.stringify({
|
|
397
|
+
errors: events.filter((entry) => entry.t === 'err').map((entry) => entry.text),
|
|
398
|
+
streamedText: events.filter((entry) => entry.t === 'd').map((entry) => entry.text).join(''),
|
|
399
|
+
assistantTexts,
|
|
400
|
+
}))
|
|
401
|
+
} finally {
|
|
402
|
+
providers.PROVIDERS['codex-cli'].handler = originalHandler
|
|
403
|
+
stream.setStreamAgentChatForTest(null)
|
|
404
|
+
}
|
|
405
|
+
`, { prefix: 'swarmclaw-chatroom-route-cli-provider-' })
|
|
406
|
+
|
|
407
|
+
assert.equal(output.errors.some((text) => /streamAgentChat should not be called/i.test(text)), false)
|
|
408
|
+
assert.equal(output.streamedText.includes('Codex CLI answered from direct provider runtime.'), true)
|
|
409
|
+
assert.equal(output.assistantTexts.some((text) => text.includes('Codex CLI answered from direct provider runtime.')), true)
|
|
410
|
+
})
|
|
@@ -6,6 +6,7 @@ import { notFound } from '@/lib/server/collection-helpers'
|
|
|
6
6
|
import { safeParseBody } from '@/lib/server/safe-parse-body'
|
|
7
7
|
import { streamAgentChat } from '@/lib/server/chat-execution/stream-agent-chat'
|
|
8
8
|
import { getProvider } from '@/lib/providers'
|
|
9
|
+
import { NON_LANGGRAPH_PROVIDER_IDS } from '@/lib/provider-sets'
|
|
9
10
|
import {
|
|
10
11
|
resolveApiKey,
|
|
11
12
|
parseMentions,
|
|
@@ -231,37 +232,55 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
231
232
|
|
|
232
233
|
let fullText = ''
|
|
233
234
|
let agentError = ''
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
fullText += parsed.text
|
|
249
|
-
writeEvent({ t: 'd', text: parsed.text, agentId: agent.id, agentName: agent.name })
|
|
250
|
-
} else if (parsed.t === 'tool_call' || parsed.t === 'tool_result') {
|
|
251
|
-
writeEvent({ ...parsed, agentId: agent.id, agentName: agent.name })
|
|
252
|
-
} else if (parsed.t === 'err' && parsed.text) {
|
|
253
|
-
agentError = parsed.text
|
|
254
|
-
writeEvent({ t: 'err', text: parsed.text, agentId: agent.id, agentName: agent.name })
|
|
255
|
-
}
|
|
256
|
-
} catch {
|
|
257
|
-
// skip malformed lines
|
|
235
|
+
const forwardProviderEvents = (raw: string) => {
|
|
236
|
+
const lines = raw.split('\n').filter(Boolean)
|
|
237
|
+
for (const line of lines) {
|
|
238
|
+
if (!line.startsWith('data: ')) continue
|
|
239
|
+
try {
|
|
240
|
+
const parsed = JSON.parse(line.slice(6).trim())
|
|
241
|
+
if (parsed.t === 'd' && parsed.text) {
|
|
242
|
+
fullText += parsed.text
|
|
243
|
+
writeEvent({ t: 'd', text: parsed.text, agentId: agent.id, agentName: agent.name })
|
|
244
|
+
} else if (parsed.t === 'tool_call' || parsed.t === 'tool_result') {
|
|
245
|
+
writeEvent({ ...parsed, agentId: agent.id, agentName: agent.name })
|
|
246
|
+
} else if (parsed.t === 'err' && parsed.text) {
|
|
247
|
+
agentError = parsed.text
|
|
248
|
+
writeEvent({ t: 'err', text: parsed.text, agentId: agent.id, agentName: agent.name })
|
|
258
249
|
}
|
|
250
|
+
} catch {
|
|
251
|
+
// skip malformed lines
|
|
259
252
|
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
})
|
|
253
|
+
}
|
|
254
|
+
}
|
|
263
255
|
|
|
264
|
-
|
|
256
|
+
let rawResponseText = ''
|
|
257
|
+
if (NON_LANGGRAPH_PROVIDER_IDS.has(syntheticSession.provider)) {
|
|
258
|
+
const provider = getProvider(syntheticSession.provider)
|
|
259
|
+
if (!provider) throw new Error(`Unknown provider: ${syntheticSession.provider}`)
|
|
260
|
+
rawResponseText = await provider.handler.streamChat({
|
|
261
|
+
session: syntheticSession,
|
|
262
|
+
message: messageForAgent,
|
|
263
|
+
imagePath,
|
|
264
|
+
apiKey,
|
|
265
|
+
systemPrompt: fullSystemPrompt,
|
|
266
|
+
write: forwardProviderEvents,
|
|
267
|
+
active: new Map<string, unknown>(),
|
|
268
|
+
loadHistory: () => history,
|
|
269
|
+
})
|
|
270
|
+
if (!rawResponseText) rawResponseText = fullText
|
|
271
|
+
} else {
|
|
272
|
+
const result = await streamAgentChat({
|
|
273
|
+
session: syntheticSession,
|
|
274
|
+
message: messageForAgent,
|
|
275
|
+
imagePath,
|
|
276
|
+
attachedFiles,
|
|
277
|
+
apiKey,
|
|
278
|
+
systemPrompt: fullSystemPrompt,
|
|
279
|
+
write: forwardProviderEvents,
|
|
280
|
+
history,
|
|
281
|
+
})
|
|
282
|
+
rawResponseText = result.finalResponse || result.fullText || fullText
|
|
283
|
+
}
|
|
265
284
|
const responseText = stripAgentReactionTokens(stripHiddenControlTokens(rawResponseText))
|
|
266
285
|
|
|
267
286
|
// Don't persist empty or error-only messages — they pollute chat history
|
|
@@ -4,7 +4,6 @@ import { notify } from '@/lib/server/ws-hub'
|
|
|
4
4
|
import { notFound } from '@/lib/server/collection-helpers'
|
|
5
5
|
import { safeParseBody } from '@/lib/server/safe-parse-body'
|
|
6
6
|
import { genId } from '@/lib/id'
|
|
7
|
-
import { isWorkerOnlyAgent, buildWorkerOnlyAgentMessage } from '@/lib/server/agents/agent-availability'
|
|
8
7
|
|
|
9
8
|
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
10
9
|
const { id } = await params
|
|
@@ -18,13 +17,6 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
18
17
|
if (!agentId) return NextResponse.json({ error: 'agentId is required' }, { status: 400 })
|
|
19
18
|
|
|
20
19
|
const agents = loadAgents()
|
|
21
|
-
if (isWorkerOnlyAgent(agents[agentId])) {
|
|
22
|
-
return NextResponse.json(
|
|
23
|
-
{ error: buildWorkerOnlyAgentMessage(agents[agentId], 'join chatrooms') },
|
|
24
|
-
{ status: 400 },
|
|
25
|
-
)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
20
|
if (!chatroom.agentIds.includes(agentId)) {
|
|
29
21
|
chatroom.agentIds.push(agentId)
|
|
30
22
|
|
|
@@ -4,7 +4,6 @@ import { notify } from '@/lib/server/ws-hub'
|
|
|
4
4
|
import { notFound } from '@/lib/server/collection-helpers'
|
|
5
5
|
import { safeParseBody } from '@/lib/server/safe-parse-body'
|
|
6
6
|
import { genId } from '@/lib/id'
|
|
7
|
-
import { isWorkerOnlyAgent } from '@/lib/server/agents/agent-availability'
|
|
8
7
|
import {
|
|
9
8
|
ensureChatroomRoutingGuidance,
|
|
10
9
|
synthesizeRoutingGuidanceFromRules,
|
|
@@ -78,16 +77,6 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
78
77
|
{ status: 400 },
|
|
79
78
|
)
|
|
80
79
|
}
|
|
81
|
-
const cliAgentNames = agentIds
|
|
82
|
-
.filter((agentId) => isWorkerOnlyAgent(agents[agentId]))
|
|
83
|
-
.map((agentId) => agents[agentId]?.name || agentId)
|
|
84
|
-
if (cliAgentNames.length > 0) {
|
|
85
|
-
return NextResponse.json(
|
|
86
|
-
{ error: `CLI-based agents cannot join chatrooms: ${cliAgentNames.join(', ')}. They can only be used for direct chats and delegation.` },
|
|
87
|
-
{ status: 400 },
|
|
88
|
-
)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
80
|
const oldIds = new Set(chatroom.agentIds)
|
|
92
81
|
const newIds = new Set(agentIds)
|
|
93
82
|
const added = agentIds.filter((aid: string) => !oldIds.has(aid))
|
|
@@ -6,7 +6,6 @@ import { ChatroomCreateSchema, formatZodError } from '@/lib/validation/schemas'
|
|
|
6
6
|
import { safeParseBody } from '@/lib/server/safe-parse-body'
|
|
7
7
|
import { z } from 'zod'
|
|
8
8
|
import type { Chatroom, ChatroomMessage } from '@/types'
|
|
9
|
-
import { isWorkerOnlyAgent } from '@/lib/server/agents/agent-availability'
|
|
10
9
|
import {
|
|
11
10
|
ensureChatroomRoutingGuidance,
|
|
12
11
|
synthesizeRoutingGuidanceFromRules,
|
|
@@ -61,15 +60,6 @@ export async function POST(req: Request) {
|
|
|
61
60
|
{ status: 400 },
|
|
62
61
|
)
|
|
63
62
|
}
|
|
64
|
-
const cliAgentNames = requestedAgentIds
|
|
65
|
-
.filter((agentId) => isWorkerOnlyAgent(knownAgents[agentId]))
|
|
66
|
-
.map((agentId) => knownAgents[agentId]?.name || agentId)
|
|
67
|
-
if (cliAgentNames.length > 0) {
|
|
68
|
-
return NextResponse.json(
|
|
69
|
-
{ error: `CLI-based agents cannot join chatrooms: ${cliAgentNames.join(', ')}. They can only be used for direct chats and delegation.` },
|
|
70
|
-
{ status: 400 },
|
|
71
|
-
)
|
|
72
|
-
}
|
|
73
63
|
const agentIds: string[] = requestedAgentIds
|
|
74
64
|
const chatMode = body.chatMode === 'parallel' ? 'parallel' : 'sequential'
|
|
75
65
|
const autoAddress = Boolean(body.autoAddress)
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
-
import {
|
|
2
|
+
import { clearChatMessagesWithUndo } from '@/lib/server/chats/chat-session-service'
|
|
3
3
|
import { notFound } from '@/lib/server/collection-helpers'
|
|
4
4
|
|
|
5
5
|
export async function POST(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
6
6
|
const { id } = await params
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
const result = clearChatMessagesWithUndo(id)
|
|
8
|
+
if (!result.ok) {
|
|
9
|
+
if (result.status === 404) return notFound()
|
|
10
|
+
return NextResponse.json(result.payload, { status: result.status })
|
|
11
|
+
}
|
|
12
|
+
return NextResponse.json(result.payload)
|
|
9
13
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { restoreChatFromUndoToken } from '@/lib/server/chats/chat-session-service'
|
|
4
|
+
import { badRequest, notFound } from '@/lib/server/collection-helpers'
|
|
5
|
+
import { safeParseBody } from '@/lib/server/safe-parse-body'
|
|
6
|
+
|
|
7
|
+
const BodySchema = z.object({
|
|
8
|
+
undoToken: z.string().min(1),
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
12
|
+
const { id } = await params
|
|
13
|
+
const parsed = await safeParseBody(req, BodySchema)
|
|
14
|
+
if (parsed.error) return parsed.error
|
|
15
|
+
const token = parsed.data.undoToken.trim()
|
|
16
|
+
if (!token) return badRequest('undoToken is required')
|
|
17
|
+
const result = restoreChatFromUndoToken(id, token)
|
|
18
|
+
if (!result.ok) {
|
|
19
|
+
if (result.status === 404) return notFound(result.payload.error)
|
|
20
|
+
return NextResponse.json(result.payload, { status: result.status })
|
|
21
|
+
}
|
|
22
|
+
return NextResponse.json(result.payload)
|
|
23
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { HumanMessage } from '@langchain/core/messages'
|
|
4
|
+
import { getSession } from '@/lib/server/sessions/session-repository'
|
|
5
|
+
import { getMessages, replaceAllMessages } from '@/lib/server/messages/message-repository'
|
|
6
|
+
import { summarizeAndCompact, type LLMSummarizer } from '@/lib/server/context-manager'
|
|
7
|
+
import { buildChatModel } from '@/lib/server/build-llm'
|
|
8
|
+
import { notFound } from '@/lib/server/collection-helpers'
|
|
9
|
+
import { safeParseBody } from '@/lib/server/safe-parse-body'
|
|
10
|
+
import { errorMessage } from '@/lib/shared-utils'
|
|
11
|
+
|
|
12
|
+
const BodySchema = z.object({
|
|
13
|
+
keepLastN: z.number().int().min(2).max(200).optional(),
|
|
14
|
+
}).partial()
|
|
15
|
+
|
|
16
|
+
const DEFAULT_KEEP_LAST_N = 10
|
|
17
|
+
|
|
18
|
+
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
19
|
+
const { id } = await params
|
|
20
|
+
const session = getSession(id)
|
|
21
|
+
if (!session) return notFound()
|
|
22
|
+
|
|
23
|
+
const parsed = await safeParseBody(req, BodySchema)
|
|
24
|
+
if (parsed.error) return parsed.error
|
|
25
|
+
const keepLastN = Math.max(2, Math.min(parsed.data.keepLastN ?? DEFAULT_KEEP_LAST_N, 200))
|
|
26
|
+
|
|
27
|
+
const messages = getMessages(id)
|
|
28
|
+
if (messages.length <= keepLastN) {
|
|
29
|
+
return NextResponse.json({
|
|
30
|
+
status: 'no_action',
|
|
31
|
+
messageCount: messages.length,
|
|
32
|
+
keepLastN,
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const generateSummary: LLMSummarizer = async (prompt) => {
|
|
37
|
+
const llm = buildChatModel({
|
|
38
|
+
provider: session.provider,
|
|
39
|
+
model: session.model,
|
|
40
|
+
apiKey: null,
|
|
41
|
+
credentialId: session.credentialId ?? null,
|
|
42
|
+
apiEndpoint: session.apiEndpoint ?? null,
|
|
43
|
+
})
|
|
44
|
+
const res = await llm.invoke([new HumanMessage(prompt)])
|
|
45
|
+
return typeof res.content === 'string' ? res.content : ''
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const result = await summarizeAndCompact({
|
|
50
|
+
messages,
|
|
51
|
+
keepLastN,
|
|
52
|
+
agentId: session.agentId ?? null,
|
|
53
|
+
sessionId: id,
|
|
54
|
+
provider: session.provider as string,
|
|
55
|
+
model: session.model as string,
|
|
56
|
+
generateSummary,
|
|
57
|
+
})
|
|
58
|
+
replaceAllMessages(id, result.messages)
|
|
59
|
+
return NextResponse.json({
|
|
60
|
+
status: 'compacted',
|
|
61
|
+
prunedCount: result.prunedCount,
|
|
62
|
+
memoriesStored: result.memoriesStored,
|
|
63
|
+
summaryAdded: result.summaryAdded,
|
|
64
|
+
messageCount: result.messages.length,
|
|
65
|
+
})
|
|
66
|
+
} catch (err: unknown) {
|
|
67
|
+
return NextResponse.json(
|
|
68
|
+
{ error: `Compaction failed: ${errorMessage(err)}` },
|
|
69
|
+
{ status: 500 },
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { getSession } from '@/lib/server/sessions/session-repository'
|
|
3
|
+
import { getMessages } from '@/lib/server/messages/message-repository'
|
|
4
|
+
import { getContextStatus } from '@/lib/server/context-manager'
|
|
5
|
+
import { notFound } from '@/lib/server/collection-helpers'
|
|
6
|
+
|
|
7
|
+
const SYSTEM_PROMPT_TOKEN_ESTIMATE = 2000
|
|
8
|
+
|
|
9
|
+
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
10
|
+
const { id } = await params
|
|
11
|
+
const session = getSession(id)
|
|
12
|
+
if (!session) return notFound()
|
|
13
|
+
const messages = getMessages(id)
|
|
14
|
+
const status = getContextStatus(
|
|
15
|
+
messages,
|
|
16
|
+
SYSTEM_PROMPT_TOKEN_ESTIMATE,
|
|
17
|
+
session.provider as string,
|
|
18
|
+
session.model as string,
|
|
19
|
+
)
|
|
20
|
+
return NextResponse.json(status)
|
|
21
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
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('chat clear + undo round-trip restores messages and CLI session IDs', () => {
|
|
7
|
+
const output = runWithTempDataDir<{
|
|
8
|
+
clearStatus: number
|
|
9
|
+
clearPayload: { cleared: number; undoToken: string }
|
|
10
|
+
postClearCount: number
|
|
11
|
+
postClearClaudeSessionId: string | null
|
|
12
|
+
postClearOpencodeWebSessionId: string | null
|
|
13
|
+
undoStatus: number
|
|
14
|
+
undoPayload: { restored: number }
|
|
15
|
+
postUndoCount: number
|
|
16
|
+
postUndoClaudeSessionId: string | null
|
|
17
|
+
postUndoOpencodeWebSessionId: string | null
|
|
18
|
+
undoTwiceStatus: number
|
|
19
|
+
undoTwicePayload: { error?: string }
|
|
20
|
+
missingSessionStatus: number
|
|
21
|
+
}>(`
|
|
22
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
23
|
+
const repoMod = await import('@/lib/server/messages/message-repository')
|
|
24
|
+
const clearRouteMod = await import('./src/app/api/chats/[id]/clear/route')
|
|
25
|
+
const undoRouteMod = await import('./src/app/api/chats/[id]/clear/undo/route')
|
|
26
|
+
const storage = storageMod.default || storageMod
|
|
27
|
+
const repo = repoMod.default || repoMod
|
|
28
|
+
const clearRoute = clearRouteMod.default || clearRouteMod
|
|
29
|
+
const undoRoute = undoRouteMod.default || undoRouteMod
|
|
30
|
+
|
|
31
|
+
const now = Date.now()
|
|
32
|
+
storage.saveSessions({
|
|
33
|
+
sess_clear_1: {
|
|
34
|
+
id: 'sess_clear_1',
|
|
35
|
+
name: 'Clear test',
|
|
36
|
+
cwd: process.env.WORKSPACE_DIR,
|
|
37
|
+
user: 'tester',
|
|
38
|
+
provider: 'openai',
|
|
39
|
+
model: 'gpt-5',
|
|
40
|
+
claudeSessionId: 'cs_preserved_abc',
|
|
41
|
+
codexThreadId: null,
|
|
42
|
+
opencodeWebSessionId: 'owb_preserved_xyz',
|
|
43
|
+
messages: [],
|
|
44
|
+
createdAt: now,
|
|
45
|
+
lastActiveAt: now,
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
repo.appendMessage('sess_clear_1', { role: 'user', text: 'first', time: now })
|
|
50
|
+
repo.appendMessage('sess_clear_1', { role: 'assistant', text: 'reply', time: now + 1 })
|
|
51
|
+
repo.appendMessage('sess_clear_1', { role: 'user', text: 'second', time: now + 2 })
|
|
52
|
+
|
|
53
|
+
const clearResponse = await clearRoute.POST(
|
|
54
|
+
new Request('http://local/api/chats/sess_clear_1/clear', { method: 'POST' }),
|
|
55
|
+
{ params: Promise.resolve({ id: 'sess_clear_1' }) },
|
|
56
|
+
)
|
|
57
|
+
const clearPayload = await clearResponse.json()
|
|
58
|
+
|
|
59
|
+
const postClearCount = repo.getMessages('sess_clear_1').length
|
|
60
|
+
const postClearSession = storage.loadSession('sess_clear_1')
|
|
61
|
+
|
|
62
|
+
const undoResponse = await undoRoute.POST(
|
|
63
|
+
new Request('http://local/api/chats/sess_clear_1/clear/undo', {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: { 'content-type': 'application/json' },
|
|
66
|
+
body: JSON.stringify({ undoToken: clearPayload.undoToken }),
|
|
67
|
+
}),
|
|
68
|
+
{ params: Promise.resolve({ id: 'sess_clear_1' }) },
|
|
69
|
+
)
|
|
70
|
+
const undoPayload = await undoResponse.json()
|
|
71
|
+
|
|
72
|
+
const postUndoCount = repo.getMessages('sess_clear_1').length
|
|
73
|
+
const postUndoSession = storage.loadSession('sess_clear_1')
|
|
74
|
+
|
|
75
|
+
const undoTwiceResponse = await undoRoute.POST(
|
|
76
|
+
new Request('http://local/api/chats/sess_clear_1/clear/undo', {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers: { 'content-type': 'application/json' },
|
|
79
|
+
body: JSON.stringify({ undoToken: clearPayload.undoToken }),
|
|
80
|
+
}),
|
|
81
|
+
{ params: Promise.resolve({ id: 'sess_clear_1' }) },
|
|
82
|
+
)
|
|
83
|
+
const undoTwicePayload = await undoTwiceResponse.json()
|
|
84
|
+
|
|
85
|
+
const missingResponse = await clearRoute.POST(
|
|
86
|
+
new Request('http://local/api/chats/not_a_real_session/clear', { method: 'POST' }),
|
|
87
|
+
{ params: Promise.resolve({ id: 'not_a_real_session' }) },
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
console.log(JSON.stringify({
|
|
91
|
+
clearStatus: clearResponse.status,
|
|
92
|
+
clearPayload: { cleared: clearPayload.cleared, undoToken: clearPayload.undoToken },
|
|
93
|
+
postClearCount,
|
|
94
|
+
postClearClaudeSessionId: postClearSession?.claudeSessionId ?? null,
|
|
95
|
+
postClearOpencodeWebSessionId: postClearSession?.opencodeWebSessionId ?? null,
|
|
96
|
+
undoStatus: undoResponse.status,
|
|
97
|
+
undoPayload,
|
|
98
|
+
postUndoCount,
|
|
99
|
+
postUndoClaudeSessionId: postUndoSession?.claudeSessionId ?? null,
|
|
100
|
+
postUndoOpencodeWebSessionId: postUndoSession?.opencodeWebSessionId ?? null,
|
|
101
|
+
undoTwiceStatus: undoTwiceResponse.status,
|
|
102
|
+
undoTwicePayload,
|
|
103
|
+
missingSessionStatus: missingResponse.status,
|
|
104
|
+
}))
|
|
105
|
+
`, { prefix: 'swarmclaw-clear-undo-route-' })
|
|
106
|
+
|
|
107
|
+
assert.equal(output.clearStatus, 200)
|
|
108
|
+
assert.equal(output.clearPayload.cleared, 3)
|
|
109
|
+
assert.match(output.clearPayload.undoToken, /^undo_/)
|
|
110
|
+
assert.equal(output.postClearCount, 0)
|
|
111
|
+
assert.equal(output.postClearClaudeSessionId, null, 'CLI session should be nulled after clear')
|
|
112
|
+
assert.equal(output.postClearOpencodeWebSessionId, null, 'opencode-web session should be nulled after clear')
|
|
113
|
+
assert.equal(output.undoStatus, 200)
|
|
114
|
+
assert.equal(output.undoPayload.restored, 3)
|
|
115
|
+
assert.equal(output.postUndoCount, 3)
|
|
116
|
+
assert.equal(output.postUndoClaudeSessionId, 'cs_preserved_abc', 'CLI session ID should be restored by undo')
|
|
117
|
+
assert.equal(output.postUndoOpencodeWebSessionId, 'owb_preserved_xyz', 'opencode-web session ID should be restored by undo')
|
|
118
|
+
assert.equal(output.undoTwiceStatus, 404, 'undo token should be single-use')
|
|
119
|
+
assert.ok(output.undoTwicePayload.error)
|
|
120
|
+
assert.equal(output.missingSessionStatus, 404)
|
|
121
|
+
})
|