feat(serve): add POST /session/:id/recap#4504
Conversation
Wraps generateSessionRecap (core/services/sessionRecap.ts) so daemon clients can fetch a one-sentence "where did I leave off" summary without driving the agent through a full prompt turn. Mirrors the ext-method roundtrip used by /session/:id/approval-mode — bridge forwards `qwen/control/session/recap` to the ACP child, which calls the existing core helper against the per-session GeminiClient history. - Route: non-strict mutation gate (parity with /prompt — costs tokens but mutates no state) - Capability tag: `session_recap` - SDK: `client.recapSession(sessionId, opts)` + `session.recap(opts)` convenience wrapper - 60s bridge-side backstop timeout; client-disconnect aborts the HTTP wait (LLM call in the child still completes — recap is short) - Recap is best-effort: short history / transient model failure surfaces as 200 with `recap: null`, not an error Tests cover the route (200 happy path, 200 null recap, client-id context, 404 on unknown session, malformed client-id, non-strict gate posture), the bridge ext-method roundtrip (success, null recap, SessionNotFoundError), the SDK client + session-client wrappers (URL encoding, body, headers, signal propagation, 404 throw), and a public-surface type lock for `DaemonSessionRecapResult`. Closes part of QwenLM#4175 (Top 5 ROI port QwenLM#1 from the daemon coverage gap inventory). Targets daemon_mode_b_main integration branch. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
📋 Review SummaryThis PR exposes the existing 🔍 General Feedback
🎯 Specific Feedback🟢 Medium
🔵 Low
✅ Highlights
|
|
Thanks for the review — won't take these in this PR. Medium 1 (explicit Medium 2 (configurable Medium 3 (SDK Low 1 (rename to Low 2 (file follow-up issue for request-id-based cancel): already listed in the PR description's "Out of scope" section; tracking issue inflation isn't needed. Low 3 (expand JSDoc on Highlights noted, thanks. |
There was a problem hiding this comment.
Pull request overview
Adds a daemon-accessible session recap feature by exposing core’s generateSessionRecap through the serve HTTP API, ACP control ext-methods, the ACP bridge, and the TypeScript SDK (with docs + tests to lock the contract).
Changes:
- Add
POST /session/:id/recaptoqwen serve, advertise it via the newsession_recapcapability, and plumb it through ACP (qwen/control/session/recap) +HttpAcpBridge. - Add SDK surface:
DaemonClient.recapSession(...),DaemonSessionClient.recap(...), and theDaemonSessionRecapResulttype (re-exported from the top-level barrel). - Add/extend unit tests and protocol/user/design documentation for the recap route, response shape, and best-effort semantics.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| packages/sdk-typescript/test/unit/DaemonSessionClient.test.ts | Tests session-bound wrapper forwards clientId + AbortSignal. |
| packages/sdk-typescript/test/unit/DaemonClient.test.ts | Tests new recapSession request shape, encoding, headers, signal forwarding, and error handling. |
| packages/sdk-typescript/test/unit/daemon-public-surface.test.ts | Type-lock to ensure DaemonSessionRecapResult remains publicly exported. |
| packages/sdk-typescript/src/index.ts | Re-export DaemonSessionRecapResult from the SDK root barrel. |
| packages/sdk-typescript/src/daemon/types.ts | Define DaemonSessionRecapResult and document best-effort recap: null contract. |
| packages/sdk-typescript/src/daemon/index.ts | Re-export DaemonSessionRecapResult from the daemon sub-barrel. |
| packages/sdk-typescript/src/daemon/DaemonSessionClient.ts | Add recap() wrapper that delegates to DaemonClient.recapSession. |
| packages/sdk-typescript/src/daemon/DaemonClient.ts | Implement recapSession(sessionId, opts?) calling POST /session/:id/recap. |
| packages/cli/src/serve/server.ts | Add the POST /session/:id/recap HTTP route calling bridge.generateSessionRecap. |
| packages/cli/src/serve/server.test.ts | Add route tests + ensure capability list includes session_recap. |
| packages/cli/src/serve/capabilities.ts | Add session_recap to SERVE_CAPABILITY_REGISTRY. |
| packages/cli/src/acp-integration/acpAgent.ts | Handle ACP ext-method qwen/control/session/recap by calling generateSessionRecap. |
| packages/acp-bridge/src/status.ts | Add SERVE_CONTROL_EXT_METHODS.sessionRecap constant. |
| packages/acp-bridge/src/bridgeTypes.ts | Extend HttpAcpBridge interface with generateSessionRecap(...). |
| packages/acp-bridge/src/bridge.ts | Implement generateSessionRecap with a 60s backstop timeout and ACP ext-method forwarding. |
| packages/acp-bridge/src/bridge.test.ts | Add bridge tests for forwarding, null recap preservation, and missing-session errors. |
| docs/users/qwen-serve.md | Document the new “Session recap” feature at a user level. |
| docs/developers/qwen-serve-protocol.md | Specify the wire contract for POST /session/:id/recap (capability, response, errors, cancellation semantics). |
| docs/design/session-recap/session-recap-design.md | Update triggers/design to include the daemon HTTP path and its posture/cancellation notes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const clientId = parseClientIdHeader(req, res); | ||
| if (clientId === null) return; | ||
| try { | ||
| const response = await bridge.generateSessionRecap( | ||
| sessionId, | ||
| clientId !== undefined ? { clientId } : undefined, | ||
| ); | ||
| res.status(200).json(response); | ||
| } catch (err) { | ||
| sendBridgeError(res, err, { | ||
| route: 'POST /session/:id/recap', | ||
| sessionId, | ||
| }); | ||
| } | ||
| }); |
There was a problem hiding this comment.
Thanks — addressed in commit 058bde70f per chiga0's option 1: reconciled the route comment + protocol doc + design doc to match actual v1 behavior (no res.once('close'), no AbortSignal threaded through the bridge, only the 60s SESSION_RECAP_TIMEOUT_MS backstop + getTransportClosedReject race). Going with the docs-match-reality path rather than wiring the AbortController, because the ACP child handler also passes a never-aborting AbortController().signal to generateSessionRecap — an HTTP-side controller in isolation would be cosmetic, the LLM call still runs to completion either way. A request-id-based cancel ext-method can plumb full e2e cancellation in a follow-up PR.
| const current = config.getApprovalMode(); | ||
| return { previous, current }; | ||
| } | ||
| case SERVE_CONTROL_EXT_METHODS.sessionRecap: { |
There was a problem hiding this comment.
[Suggestion] The ~30-line sessionRecap ext-method handler has no test coverage. acpAgent.test.ts tests other ext-method handlers (rewindSession, renameSession, workspaceMcp, etc.) but sessionRecap is absent. The uncovered paths include: RequestError.invalidParams on missing/invalid sessionId, sessionOrThrow for unknown sessions, generateSessionRecap(config, signal) return value wrapping, and recap: null pass-through.
Consider adding a describe block mirroring the existing renameSession test pattern to pin the routing, parameter validation, and response shape.
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Thanks — agreed, will add. Deferring to a follow-up PR to keep this one focused on chiga0's doc-only gate. The follow-up will add a describe('SERVE_CONTROL_EXT_METHODS.sessionRecap') block in acpAgent.test.ts mirroring the renameSession pattern, covering: success path, recap: null pass-through, RequestError.invalidParams on missing/invalid sessionId, SESSION_ID_RE rejection (will also be added — sibling handlers enforce it but recap currently doesn't), and sessionOrThrow on unknown session.
| sessionId: string, | ||
| opts?: { signal?: AbortSignal; clientId?: string }, | ||
| ): Promise<DaemonSessionRecapResult> { | ||
| const res = await this._fetch( |
There was a problem hiding this comment.
[Suggestion] recapSession calls this._fetch() directly, bypassing fetchWithTimeout. Every other non-streaming method (heartbeat, cancel, restartMcpServer, initWorkspace, setWorkspaceToolEnabled) uses fetchWithTimeout. When the caller omits signal (the common path), the SDK request has no client-side timeout — a wedged daemon causes await client.recapSession(id) to hang indefinitely.
Consider using fetchWithTimeout with a per-call timeout above the bridge's 60s backstop (e.g. 90s), similar to how restartMcpServer handles its long-running nature:
return await this.fetchWithTimeout(
`${this.baseUrl}/session/${encodeURIComponent(sessionId)}/recap`,
{ method: 'POST', headers: ..., body: '{}', signal: opts?.signal },
async (res) => {
if (!res.ok) throw await this.failOnError(res, 'POST /session/:id/recap');
return (await res.json()) as DaemonSessionRecapResult;
},
RECAP_DEFAULT_TIMEOUT_MS, // e.g. 90_000
);— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Thanks — agreed, will fix in the follow-up PR. The bridge's 60s SESSION_RECAP_TIMEOUT_MS is a server-side backstop and doesn't protect against TCP half-open / DNS stall on the client. Switching to fetchWithTimeout with a RECAP_DEFAULT_TIMEOUT_MS=90_000 (slightly above the bridge ceiling so a healthy slow recap isn't cut off client-side) gives the SDK consumer the same network-level safety net every other non-streaming method has. JSDoc will be updated to drop the "bypasses fetchTimeoutMs" claim — that wasn't even accurate, raw _fetch just has no timeout at all. Holding for the follow-up to keep this PR minimal-doc-only per chiga0's gate.
Qwen Code Review (DEEP)CI-safe profile adapted from bundled Correctness / Security
Needs Verification
Validation EvidencePRESENT — PR body includes exact
Test Coverage
Needs Verification
Validation EvidencePRESENT — The PR body includes exact
Maintainability / Performance
Needs Verification
Undirected Audit
Needs Verification
Validation EvidencePRESENT — PR body includes a manual end-to-end smoke test with exact
Qwen Code |
本地验证报告(maintainer review)把本 PR 的单 commit 验证矩阵(merged 状态)
E2E smoke test(真机:boot daemon + curl)启动
实际响应样例(已隐去具体 sessionId): {"sessionId":"…","recap":"Setting up context for the Qwen Code project in /tmp/pr4504-merged. Next: specify your development task or question to begin."}代码评审要点
风险与遗留
结论建议合并到 验证环境:macOS Darwin 25.4.0,Node v22.17.0,npm 11.8.0。tmux 多 worktree 并行(PR-as-is + cherry-picked-on-daemon_mode_b_main 对照),单 commit cherry-pick 无冲突。 |
Review:
|
Per chiga0's review on QwenLM#4504 (option 1 — match docs to reality rather than wire up cosmetic AbortController plumbing). The route, design doc, and protocol reference all claimed "client disconnect aborts the bridge-side wait" via `res.once('close')`, but the route has no such listener and the bridge accepts no `AbortSignal`. The only ceilings are the 60s `SESSION_RECAP_TIMEOUT_MS` backstop and the transport- closed race against ACP channel death. Wiring an HTTP-side AbortController in isolation would be cosmetic because the ACP child handler also passes a never-aborting `AbortController().signal` to the core helper (no cross-process abort plumbing yet) — e2e cancel needs both layers. Recap is short (~1–5s, `maxOutputTokens: 300`), so the absent cancellation is acceptable for v1; a request-id-based cancel ext-method can land in a follow-up. Also adds two known-limit bullets to the user guide per chiga0's other minor notes: token-cost amplification on no-token loopback (no per-route rate limit) and concurrent-recap safety (side-query reads chat history via `GeminiClient.getChat().getHistory()` snapshot and runs through a separate `BaseLlmClient`, never mutating the session's `GeminiChat`). 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
|
Thanks @chiga0 — went with option 1 in commit Reconciled three locations to match actual v1 behavior:
Also picked up your two minor doc requests in
Left as-is for follow-up (per your "not blocking" labels):
Tests + typecheck + lint all still green after the doc edits. |
|
Thanks @wenshao — appreciate the cherry-pick validation. Just landed commit For your two earlier inline
Holding both for the follow-up PR to keep this one minimal-doc-fix-only per chiga0's gate. |
|
Thanks for the deep review — partially taken in commit Taken (commit
Deferred to a small follow-up PR after this merges:
Not taken:
Tests + typecheck + lint all still green. |
Qwen Code Review (DEEP)CI-safe profile adapted from bundled I need to examine the actual code to verify correctness and security claims. Let me read the key files. Now let me verify the Correctness / Security
Needs Verification
Validation EvidencePRESENT — PR body includes manual curl smoke test (exact commands, expected JSON output for happy path, empty-history null, and capability advertisement), plus per-package unit test counts (643/643, 180/180, 261/261). Sufficient for an API-addition PR; no TUI/visual surface to validate.
Test Coverage
Needs Verification
Validation Evidence
The P1 finding maps to a specific diff line. Posting inline comment: Test Coverage
Needs Verification
Validation Evidence
Maintainability / Performance
Needs Verification
Validation EvidencePRESENT — PR body includes an end-to-end manual smoke test transcript (curl commands against a running daemon showing 200 with recap text, 200 with
Undirected Audit
Needs Verification
Validation Evidence
Qwen Code |
Qwen Code Review (DEEP)CI-safe profile adapted from bundled Correctness / SecurityNo correctness or security issues found. The implementation is well-structured and follows established codebase patterns at every layer:
Needs VerificationNo concerns requiring verification outside the supplied context. Validation Evidence
Test Coverage
Needs Verification
Validation Evidence
Maintainability / PerformanceNo maintainability or performance issues found. The material concerns in this diff — The code follows established codebase conventions: bare Needs VerificationNo concerns requiring additional context. Validation EvidencePRESENT — the PR body includes an end-to-end smoke transcript (
Undirected Audit
No additional undirected-audit issues found. Needs VerificationNo concerns requiring code outside the supplied context. Validation Evidence
Qwen Code |
| } | ||
| const session = this.sessionOrThrow(sessionId); | ||
| const config = session.getConfig(); | ||
| // v1: no cross-process abort plumbing. Client disconnect aborts |
There was a problem hiding this comment.
[Suggestion] The reconciliation in commit 058bde70f corrects the cancellation narrative in 3 docs + the server.ts route comment, but this inline comment still says "Client disconnect aborts the bridge-side wait" — the exact false claim the PR disproves everywhere else.
The new server.ts comment explicitly says "This matches the ACP child's acpAgent.ts handler, which also passes a never-aborting AbortController().signal" — pointing readers here for corroboration, but the comment they land on says the opposite.
| // v1: no cross-process abort plumbing. Client disconnect aborts | |
| // v1: no cross-process abort plumbing. The bridge does not listen | |
| // for HTTP client disconnect and no AbortSignal is threaded through | |
| // the ext-method, so the LLM call in this child always runs to | |
| // completion. The only ceilings are the bridge's 60s | |
| // `SESSION_RECAP_TIMEOUT_MS` backstop and the transport-closed race | |
| // against ACP channel death. Acceptable because recap is short | |
| // (single-attempt side-query, `maxOutputTokens: 300`). A future | |
| // request-id-based cancel ext-method can plumb a real signal | |
| // end-to-end if the bandwidth cost ever becomes an issue. |
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Thanks — good catch, applied verbatim in commit 40c23eed8. You're right that 058bde7's server.ts comment pointed here for corroboration but landed readers on the opposite claim. The new comment now mirrors the route's wording ("no AbortSignal threaded through the ext-method, only 60s SESSION_RECAP_TIMEOUT_MS backstop + transport-closed race") so the cross-reference is consistent end-to-end.
…-method The previous commit (058bde7) reconciled the cancellation narrative in 3 doc files + the route comment in server.ts, but missed the inline comment inside the ACP child's `SERVE_CONTROL_EXT_METHODS.sessionRecap` handler. That comment still claimed "Client disconnect aborts the bridge-side wait" — the exact false statement 058bde7 was meant to remove from the codebase. Worse, the new server.ts comment from 058bde7 points readers at this handler for corroboration ("This matches the ACP child's `acpAgent.ts` handler ..."), so a reader following that crumb would land on a comment saying the opposite. Per @wenshao's `[Suggestion]` review on QwenLM#4504, applying his suggested replacement verbatim. Comment-only change; no behavior delta. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
|
Thanks — all three items in this review are already tracked and deferred to the follow-up PR after this merges. See the breakdown in #issuecomment-4534337349:
For |
|
Thanks — clean bill on correctness/security noted. The P1 test coverage finding (acpAgent Appreciate the explicit confirmation that |
| * provider hiccups without inheriting the 10s `initTimeoutMs` default | ||
| * (which would false-fire on any GPT-style slow start). The race is a | ||
| * safety net against a wedged ACP channel — actual cancellation on | ||
| * client disconnect is handled at the HTTP route layer. |
There was a problem hiding this comment.
[Suggestion] The JSDoc for SESSION_RECAP_TIMEOUT_MS states "actual cancellation on client disconnect is handled at the HTTP route layer," but the server.ts route comment (line 1486) explicitly says the opposite: "v1 cancellation: NONE on the route side. There is intentionally no res.once('close') listener and no AbortSignal plumbed into bridge.generateSessionRecap."
The reconciliation in commits 058bde70f and 40c23eed8 corrected 3 doc files, the server.ts route comment, and the acpAgent.ts ext-method handler comment — but this bridge-layer JSDoc was missed. A reader following the server.ts cross-reference would land on a claim that contradicts the route's own documentation.
| * client disconnect is handled at the HTTP route layer. | |
| * safety net against a wedged ACP channel — there is no HTTP-side | |
| * disconnect cancellation in v1 (see server.ts route comment). |
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Thanks — applied verbatim in commit 0a1931113. Did a wider sweep this time and caught two more stale "cancellation via signal" claims in the SDK JSDocs (DaemonClient.recapSession + DaemonSessionClient.recap); both now spell out that the signal aborts only the local HTTP fetch and the daemon/child layers ignore it. Hopefully the cancellation narrative is now consistent end-to-end across all six locations.
… SDK JSDocs Third pass on the same task. wenshao caught one more spot in `bridge.ts:330` (JSDoc for `SESSION_RECAP_TIMEOUT_MS` claimed "actual cancellation on client disconnect is handled at the HTTP route layer" — the exact opposite of what the route comment + protocol doc + design doc + acpAgent comment all now say). Pre-empting another round-trip by sweeping the rest of the codebase and fixing the two remaining misleading SDK JSDocs in the same go: - `DaemonClient.recapSession`: previously said "cancellation is via the optional signal" without qualifying that the signal aborts ONLY the local HTTP fetch. The daemon-side wait + the child-side LLM call both ignore it. Spelled out the layered reality: signal → fetch cancellation only; bridge → 60s backstop; ACP child → always runs to completion. Also corrected the "bypasses fetchTimeoutMs" claim — the raw `_fetch` simply doesn't go through that wrapper at all. - `DaemonSessionClient.recap`: same clarification on the wrapper that delegates to `recapSession`. Comment-only changes; no behavior delta. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
wenshao
left a comment
There was a problem hiding this comment.
Round 4 at 0a1931113: R3 Suggestion (bridge.ts:330 comment contradiction) addressed, plus two additional stale SDK cancellation JSDoc claims fixed. Full re-review of the 19-file diff (9 agents, 2 reverse audit rounds) found 0 new issues. Build clean, 1083/1084 tests pass (1 pre-existing flaky test outside PR scope). All prior suggestions resolved. — qwen3.7-max via Qwen Code /review
维护者本地真实测试验证报告 (PR #4504)把 PR HEAD 合并 / 构建
文件级单元测试PR 描述里给出的就是按文件计数,这里也按相同口径复核(全部
为了确认本 PR 没拖累相邻代码,顺便跑了三个 package 各自的全量套件:
端到端冒烟(真实
|
| 检查项 | 期望 | 实测 | ✓ |
|---|---|---|---|
features 包含 session_recap |
true | true | ✓ |
session_recap 紧邻 workspace_mcp_restart |
true | true | ✓ |
POST /session/:id/recap 200 + {sessionId, recap} shape |
✓ | ✓ | ✓ |
| Unknown session → 404 | 404 | 404 | ✓ |
Malformed X-Qwen-Client-Id → 400 invalid_client_id |
400 | 400 | ✓ |
| Non-strict gate(未知 client 不被拒) | 200 | 200 | ✓ |
| 路由 → bridge → ACP child → LLM 真实闭环 | LLM 真发出 | LLM 返回真实 recap,~3s | ✓ |
bridge.test.ts 文件级测试 |
180 / 180 | 186 / 186 | ✓ |
server.test.ts 文件级测试 |
261 / 261 | 261 / 261 | ✓ |
复现
git fetch origin pull/4504/head:pr-4504-test
git fetch origin daemon_mode_b_main
git worktree add -b pr4504-merge-test /tmp/pr4504-merged origin/daemon_mode_b_main
cd /tmp/pr4504-merged && git merge pr-4504-test --no-edit
npm ci && npm run build
# 单元测试
(cd packages/sdk-typescript && npx vitest run --coverage=false \
test/unit/DaemonClient.test.ts \
test/unit/DaemonSessionClient.test.ts \
test/unit/daemon-public-surface.test.ts)
(cd packages/acp-bridge && npx vitest run --coverage=false src/bridge.test.ts)
(cd packages/cli && npx vitest run --coverage=false src/serve/server.test.ts)
# E2E:启动 + curl
node dist/cli.js serve --port 0 --hostname 127.0.0.1
# 在另一个 shell 拿 URL 后跑 curl(见上文具体命令)结论
- ✅ 与 latest base 干净合并,无冲突
- ✅ build / typecheck / lint 全绿
- ✅ 文件级单元测试与 PR 描述完全吻合(其中
server.test.ts: 261/261精确匹配,bridge.test.ts: 186略多于 PR 时点的 180,但全部通过) - ✅ 真实
qwen servedaemon 端到端冒烟:capability / happy path / 404 / 400 / non-strict gate 全部符合 PR 描述 - ✅ recap 真实 LLM 调用闭环走通(~3s,单 attempt),证明不只是 mock 通过
- 同意合并。
Squashed feature work from daemon_mode_b_main branch, rebased onto latest main to establish proper merge-base and clean PR diff. Original commits: - perf(core): F2 cleanup PR A — R9/W11/W12/R10 (post-merge follow-ups) (#4411) - refactor(acp-bridge): F1 test split — lift bridge.test.ts (6861 LOC) to acp-bridge (#4445) - fix(core): F2 cleanup PR B — self-heal observability (W133-a + W134) (#4460) - feat(sdk/daemon-ui): unified completeness follow-up to #4328 (#4353) - docs(serve): v0.16-alpha known limits + SDK QWEN_SERVER_TOKEN env fallback (PR 27) (#4473) - docs(deploy): local launch templates for v0.16-alpha (PR 30a) (#4483) - feat(daemon+sdk): cross-client real-time sync completeness (#4484) - feat(serve): add POST /session/:id/recap (#4504) - feat(daemon): add voterClientId to permission_resolved (A4) (#4539) - feat(serve): --allow-origin <pattern> CORS allowlist (T2.4 #4514) (#4527) - feat(daemon): in-session model switch reaches the bus (A1) (#4546) - feat(serve): prompt absolute deadline + SSE writer idle timeout (#4514 T2.9) (#4530) - Feat/daemon react cli (#4380)
* feat(serve): add POST /session/:id/recap Wraps generateSessionRecap (core/services/sessionRecap.ts) so daemon clients can fetch a one-sentence "where did I leave off" summary without driving the agent through a full prompt turn. Mirrors the ext-method roundtrip used by /session/:id/approval-mode — bridge forwards `qwen/control/session/recap` to the ACP child, which calls the existing core helper against the per-session GeminiClient history. - Route: non-strict mutation gate (parity with /prompt — costs tokens but mutates no state) - Capability tag: `session_recap` - SDK: `client.recapSession(sessionId, opts)` + `session.recap(opts)` convenience wrapper - 60s bridge-side backstop timeout; client-disconnect aborts the HTTP wait (LLM call in the child still completes — recap is short) - Recap is best-effort: short history / transient model failure surfaces as 200 with `recap: null`, not an error Tests cover the route (200 happy path, 200 null recap, client-id context, 404 on unknown session, malformed client-id, non-strict gate posture), the bridge ext-method roundtrip (success, null recap, SessionNotFoundError), the SDK client + session-client wrappers (URL encoding, body, headers, signal propagation, 404 throw), and a public-surface type lock for `DaemonSessionRecapResult`. Closes part of #4175 (Top 5 ROI port #1 from the daemon coverage gap inventory). Targets daemon_mode_b_main integration branch. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * docs(serve): reconcile recap cancellation docs with actual v1 behavior Per chiga0's review on #4504 (option 1 — match docs to reality rather than wire up cosmetic AbortController plumbing). The route, design doc, and protocol reference all claimed "client disconnect aborts the bridge-side wait" via `res.once('close')`, but the route has no such listener and the bridge accepts no `AbortSignal`. The only ceilings are the 60s `SESSION_RECAP_TIMEOUT_MS` backstop and the transport- closed race against ACP channel death. Wiring an HTTP-side AbortController in isolation would be cosmetic because the ACP child handler also passes a never-aborting `AbortController().signal` to the core helper (no cross-process abort plumbing yet) — e2e cancel needs both layers. Recap is short (~1–5s, `maxOutputTokens: 300`), so the absent cancellation is acceptable for v1; a request-id-based cancel ext-method can land in a follow-up. Also adds two known-limit bullets to the user guide per chiga0's other minor notes: token-cost amplification on no-token loopback (no per-route rate limit) and concurrent-recap safety (side-query reads chat history via `GeminiClient.getChat().getHistory()` snapshot and runs through a separate `BaseLlmClient`, never mutating the session's `GeminiChat`). 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * docs(serve): finish recap cancellation reconciliation in acpAgent ext-method The previous commit (058bde7) reconciled the cancellation narrative in 3 doc files + the route comment in server.ts, but missed the inline comment inside the ACP child's `SERVE_CONTROL_EXT_METHODS.sessionRecap` handler. That comment still claimed "Client disconnect aborts the bridge-side wait" — the exact false statement 058bde7 was meant to remove from the codebase. Worse, the new server.ts comment from 058bde7 points readers at this handler for corroboration ("This matches the ACP child's `acpAgent.ts` handler ..."), so a reader following that crumb would land on a comment saying the opposite. Per @wenshao's `[Suggestion]` review on #4504, applying his suggested replacement verbatim. Comment-only change; no behavior delta. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * docs(serve): finish recap cancellation reconciliation across bridge + SDK JSDocs Third pass on the same task. wenshao caught one more spot in `bridge.ts:330` (JSDoc for `SESSION_RECAP_TIMEOUT_MS` claimed "actual cancellation on client disconnect is handled at the HTTP route layer" — the exact opposite of what the route comment + protocol doc + design doc + acpAgent comment all now say). Pre-empting another round-trip by sweeping the rest of the codebase and fixing the two remaining misleading SDK JSDocs in the same go: - `DaemonClient.recapSession`: previously said "cancellation is via the optional signal" without qualifying that the signal aborts ONLY the local HTTP fetch. The daemon-side wait + the child-side LLM call both ignore it. Spelled out the layered reality: signal → fetch cancellation only; bridge → 60s backstop; ACP child → always runs to completion. Also corrected the "bypasses fetchTimeoutMs" claim — the raw `_fetch` simply doesn't go through that wrapper at all. - `DaemonSessionClient.recap`: same clarification on the wrapper that delegates to `recapSession`. Comment-only changes; no behavior delta. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
Summary
generateSessionRecap(packages/core/src/services/sessionRecap.ts) to daemon clients via a newPOST /session/:id/recaproute, so SDK / web UI / IDE-plugin callers can fetch a one-sentence "where did I leave off" summary without driving the agent through a full prompt turn./recapis already used by the TUI anduseAwaySummary, but daemon clients had no access path./stats//exportports) are tracked separately.What's added
POST /session/:id/recap— non-strict gate (parity with/prompt),X-Qwen-Client-Idhonored, no body requiredsession_recap(always-on, slotted next toworkspace_mcp_restart)qwen/control/session/recapHttpAcpBridge.generateSessionRecap(sessionId, context?)DaemonClient.recapSession(sessionId, opts?)+DaemonSessionClient.recap(opts?)+DaemonSessionRecapResulttype re-exported from the top-level barrelResult shape:
{ "sessionId": "sess:42", "recap": "Debugging the auth retry race. Next: add deterministic timing." }recapisnull(a 200, not an error) when history is too short or the side-query fails — preserving core's documented best-effort contract.Architecture
Reuses the existing ext-method roundtrip pattern from
setSessionApprovalMode(so no new infrastructure):Non-strict gate — same posture as
/session/:id/prompt: the call costs tokens but mutates no state. Operators that want it locked down configure--token, which gates everything.Cancellation is best-effort at v1: client disconnect aborts the bridge-side wait, but the LLM call in the child runs to completion (recap is short — single-attempt side-query, ~1–5s typical,
maxOutputTokens: 300). A 60s backstop timeout guards a wedged ACP channel. A future request-id-based cancel ext-method can plumb full end-to-end cancellation if it ever becomes worth the bandwidth cost.Test plan
packages/sdk-typescript—DaemonClient.test.ts(+6: empty-body POST, null-recap passthrough, URL encoding,X-Qwen-Client-Idheader,AbortSignalpropagation, 404 throw);DaemonSessionClient.test.ts(+1: session-bound wrapper);daemon-public-surface.test.ts(+1: type-lock forDaemonSessionRecapResult). 643 / 643 ✓packages/acp-bridge—bridge.test.ts(+3: ext-method forwarding, null-recap preservation,SessionNotFoundErroron unknown id). 180 / 180 ✓packages/cli—server.test.ts(+6: 200 happy path, 200 null-recap, client-id context, 404, malformed client-id, non-strict-gate posture pin) plus capability registry ordering update. 261 / 261 ✓npm run buildclean (0 errors)End-to-end smoke (manual, against a freshly built daemon):
Out of scope (follow-ups)
chat.mjsdemo/recapcommand — separate commit, three lines.prompt_suggestionSSE event (NES) and other Top-5 ROI ports (/statsstructured,/exportstructured,/goalCRUD) — same proposal(serve): Mode B feature-priority roadmap toward v0.16 production-ready #4175 sub-goal, separate PRs.Docs
docs/developers/qwen-serve-protocol.md— new### POST /session/:id/recapsection (request/response, capability tag, error envelope, cancellation semantics).docs/users/qwen-serve.md— feature-list bullet pointing at the SDK helper.docs/design/session-recap/session-recap-design.md— added the daemon HTTP path to the triggers table + a "Daemon access path" subsection covering the non-strict gate decision and v1 cancellation caveat.🤖 Generated with Qwen Code