From 058bde70f9e7b448731e8de2932bc9bb71adb16d Mon Sep 17 00:00:00 2001 From: doudouOUC Date: Mon, 25 May 2026 20:41:19 +0800 Subject: [PATCH] docs(serve): reconcile recap cancellation docs with actual v1 behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../session-recap/session-recap-design.md | 18 +++++++++++---- docs/developers/qwen-serve-protocol.md | 2 +- docs/users/qwen-serve.md | 2 ++ packages/cli/src/serve/server.ts | 23 +++++++++++++++---- 4 files changed, 34 insertions(+), 11 deletions(-) diff --git a/docs/design/session-recap/session-recap-design.md b/docs/design/session-recap/session-recap-design.md index 0c1d9b36be..03fdce7b65 100644 --- a/docs/design/session-recap/session-recap-design.md +++ b/docs/design/session-recap/session-recap-design.md @@ -44,11 +44,19 @@ helpers: `DaemonClient.recapSession(sessionId, opts)` and `docs/developers/qwen-serve-protocol.md` ยง `POST /session/:id/recap` for the wire contract and error envelope. -Cancellation is best-effort at v1: client disconnect aborts the -bridge-side wait, but the LLM call in the ACP child runs to completion -(recap is short โ€” single-attempt, ~1โ€“5s typical). 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 needed. +Cancellation is **absent in v1**. The route does not listen for HTTP +client disconnect, no `AbortSignal` is threaded into +`bridge.generateSessionRecap`, and the ACP child handler passes a +never-aborting `AbortController().signal` to the core helper (no +cross-process abort plumbing yet). The only ceilings are the bridge's +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 โ€” the child-side LLM call would still run +to completion, so e2e cancel is not achievable without the cross- +process abort piece. This is acceptable for v1 because recap is short +(single-attempt side-query, `maxOutputTokens: 300`, ~1โ€“5s typical). +A future request-id-based cancel ext-method can plumb full end-to-end +cancellation if/when the bandwidth cost justifies it. ## Architecture diff --git a/docs/developers/qwen-serve-protocol.md b/docs/developers/qwen-serve-protocol.md index 1b2e2c8933..97b202371a 100644 --- a/docs/developers/qwen-serve-protocol.md +++ b/docs/developers/qwen-serve-protocol.md @@ -1135,7 +1135,7 @@ Errors: - `400 {code: 'invalid_client_id'}` โ€” malformed `X-Qwen-Client-Id` header. - `404` โ€” session unknown. -Cancellation: client disconnect aborts the bridge-side wait, but the LLM call in the ACP child runs to completion (recap is short โ€” single-attempt, ~1โ€“5s typical). A 60s backstop timeout guards against a wedged ACP channel. +Cancellation: **none in v1**. The route does not listen for HTTP client disconnect, no `AbortSignal` is plumbed into the bridge, and the ACP child runs the side-query to completion regardless of whether the caller has disconnected. The only ceilings are the bridge's 60s backstop timeout (`SESSION_RECAP_TIMEOUT_MS`) and the transport-closed race against ACP channel death. This is acceptable because recap is short (single-attempt, `maxOutputTokens: 300`, ~1โ€“5s typical); a request-id-based cancel ext-method can plumb full end-to-end cancellation in a future release if the bandwidth cost ever justifies it. ### Mutation: approval, tools, init, MCP restart diff --git a/docs/users/qwen-serve.md b/docs/users/qwen-serve.md index 88752093a0..92f76e07b4 100644 --- a/docs/users/qwen-serve.md +++ b/docs/users/qwen-serve.md @@ -16,6 +16,8 @@ Run Qwen Code as a local HTTP daemon so multiple clients (IDE plugins, web UIs, - **One daemon, one workspace** โ€” each `qwen serve` process binds to exactly one workspace at boot (per [#3803](https://github.com/QwenLM/qwen-code/issues/3803) ยง02). Multi-workspace deployments run one daemon per workspace on separate ports (or behind an orchestrator). - **Remote runtime control** ([#4175](https://github.com/QwenLM/qwen-code/issues/4175) PR 17) โ€” change a session's approval mode (`POST /session/:id/approval-mode`), toggle a tool per workspace (`POST /workspace/tools/:name/enable`), scaffold an empty `QWEN.md` (`POST /workspace/init`, mechanical only โ€” does NOT call the model; for AI-fill, follow up with `POST /session/:id/prompt`), or restart a single MCP server with a budget pre-check (`POST /workspace/mcp/:server/restart`). All four are strict-gated โ€” configure `--token` first. - **Session recap** ([#4175](https://github.com/QwenLM/qwen-code/issues/4175) follow-up) โ€” fetch a one-sentence "where did I leave off" summary of an active session (`POST /session/:id/recap`). Wraps core's `generateSessionRecap` as a side-query against the fast model; pollutes neither the main chat history nor the SSE stream. Non-strict gate (same posture as `/prompt`); SDK helper `client.recapSession(sessionId)`. + - **Known limit โ€” token-cost amplification:** the route is a pure-cost endpoint (each call is an LLM side-query, no state benefit) and the daemon has no per-route rate limit in v1. On a no-token loopback default a buggy or malicious local client can spam it to burn tokens. Configure `--token` (and optionally `--require-auth`) on shared dev hosts before exposing the daemon. + - **Concurrent recap safety:** two simultaneous `/recap` calls on the same session run two independent side-queries. `generateSessionRecap` reads a snapshot of the chat history via `GeminiClient.getChat().getHistory()` and feeds it to a separate `BaseLlmClient.generateText` call (via `runSideQuery`); it never appends to or mutates the session's `GeminiChat`. Safe to call from multiple clients without coordination. ## v0.16-alpha known limits diff --git a/packages/cli/src/serve/server.ts b/packages/cli/src/serve/server.ts index d3f7596943..1e74ec3c2f 100644 --- a/packages/cli/src/serve/server.ts +++ b/packages/cli/src/serve/server.ts @@ -1474,11 +1474,24 @@ export function createServeApp( // #4175 follow-up. 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. Posture mirrors `/session/:id/prompt`: - // non-strict gate (token cost, not state mutation), and disconnect - // is detected via `res.once('close')` for the bridge-side - // cancellation. Best-effort โ€” `recap: null` on short history or - // transient model failure is a normal 200, not an error. + // a full prompt turn. Non-strict gate (token cost, not state + // mutation), matching `/session/:id/prompt`'s posture. + // + // v1 cancellation: NONE on the route side. There is intentionally no + // `res.once('close')` listener and no `AbortSignal` plumbed into + // `bridge.generateSessionRecap`. The only ceilings are the bridge's + // 60s `SESSION_RECAP_TIMEOUT_MS` backstop and the + // `getTransportClosedReject` race against ACP transport death. This + // matches the ACP child's `acpAgent.ts` handler, which also passes + // a never-aborting `AbortController().signal` to the core helper + // because there is no cross-process abort plumbing yet. Wiring an + // HTTP-side AbortController would be cosmetic โ€” the child-side LLM + // call would still run to completion, so e2e cancel is not + // achievable in v1. Recap is short (single-attempt side-query, + // ~1โ€“5s typical, `maxOutputTokens: 300`), so this is acceptable. + // + // Best-effort โ€” `recap: null` on short history or transient model + // failure is a normal 200, not an error. const sessionId = req.params['id']; if (!sessionId) { res