mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-09 17:19:02 +00:00
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)
This commit is contained in:
parent
01323fda75
commit
058bde70f9
4 changed files with 34 additions and 11 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue