feat(serve): add pollable daemon turn status - #9080
Conversation
Add GET /session/:id/turns/current and GET /session/:id/turns/:promptId so external callers can poll a turn's lifecycle state (queued / running / completed / cancelled / error) and result instead of holding the SSE stream for the whole turn lifetime. - Live state comes from the bridge's pending prompt queue; settled outcomes from persisted turn_result transcript records, so results survive daemon restarts and the daemon keeps no per-turn memory - Each prompt captures its own recording and settles exactly that one, so overlapping turns (DAEMON-003 deadline overlap) can never misattribute one turn's outcome to another promptId - Enforces the same client authorization as POST /session/:id/prompt Refs QwenLM#8680
E2E test reportTested the final bundled CLI on macOS with
Additional verification on the final source:
Known boundary: this report does not claim crash/shutdown backfill, deleted-JSONL recovery, offline Session lookup, or permanent result retention. Those are explicitly outside this PR. |
|
Thanks for the PR — and for the disciplined scope reduction compared to PR 8682. Template looks good ✓ Problem: real and grounded. Since Direction: aligned. Issue #8680 was accepted for exploration in triage; a polling surface is the direct complement of the merged non-blocking admission — read-only and additive, no behavior change for existing clients. CHANGELOG has no direct turn-status precedent, but the background-automation direction is active and this fits it. Size: core paths are touched (core services/utils + cli serve/acp-integration + acp-bridge, cross-package). Roughly 1,076 production lines vs ~1,931 test lines vs ~46 doc lines. That crosses both the 500-line maintainer-awareness bar and the 1,000-line large-PR advisory, so this is flagged for maintainer attention. Mitigating context: this is the deliberately minimal rebuild after PR 8682 grew to ~7,900 lines, with crash durability and permanent result storage explicitly cut. Splitting further doesn't look natural — the Session settle hooks, bridge overlay, and routes form one contract. Approach: matches what I'd propose independently — capability flag, two GET routes scoped to the live owning runtime with the same client-id auth as /prompt, live queue plus a bounded 64-entry terminal overlay in the bridge, best-effort bounded backward transcript scan for settled turns, and final answer = last tool-free parent-model response block capped at 32,768 UTF-16 code units. Two housekeeping points: (1) PR 8682 is still open — it should be closed in favor of this one; (2) the Risk: Stage 1e matches the Moving on to code review. 🔍 中文说明感谢贡献——也感谢相比 PR 8682 所做的严格的范围收敛。 模板完整 ✓ 问题:真实且有依据。自 方向:对齐。issue #8680 在 triage 中已"接受探索";轮询接口是已合入的非阻塞 admission 的直接补充——只读、增量,不改变现有客户端行为。CHANGELOG 没有完全对应的先例,但 background-automation 方向是活跃的,本 PR 契合该方向。 规模:触及核心路径(core services/utils + cli serve/acp-integration + acp-bridge,跨包)。约 1,076 生产行,对比约 1,931 测试行、约 46 文档行。同时越过 500 行"维护者关注"线与 1,000 行大 PR 建议线,因此标记请维护者关注。缓解背景:本 PR 是在 PR 8682 膨胀到约 7,900 行之后刻意做的最小重建,crash durability 与永久结果存储已被明确砍掉。进一步拆分看起来不自然——Session settle 钩子、bridge overlay 与路由共同构成一个契约。 方案:与我独立的设想一致——能力点、两个仅面向 live owning runtime 且复用 /prompt client-id 鉴权的 GET 路由、bridge 内实时队列 + 有界 64 条终态 overlay、对已落盘终态做有界的 transcript 回扫,以及"最后一个不含工具调用的父模型响应块"作为最终回答、上限 32,768 个 UTF-16 code units。两个事务性提醒:(1) PR 8682 仍处于 open 状态——应关闭它以让位给本 PR;(2) 风险:Stage 1e 命中 进入代码审查 🔍 — Qwen Code · qwen3.8-max Reviewed at |
Code reviewI formed an independent proposal before reading the diff (capability flag, two live-runtime-scoped GET routes, live queue + bounded terminal overlay, bounded transcript scan for settled turns, last-tool-free-block final answer). The PR matches it, and goes further than I would have on the hard parts: the bridge re-reads the overlay after every awaited child read — including failed ones — so a terminal published mid-lookup can never regress to No critical blockers found. What I verified against the surrounding code:
Two observations worth a maintainer's eye, neither blocking:
The flow being added, for reviewers navigating the diff: sequenceDiagram
participant P1 as Client
participant P2 as serve route
participant P3 as Bridge
participant P4 as ACP child
participant P5 as Transcript JSONL
P1->>P2: GET turns by promptId or current
P2->>P3: getSessionTurnStatus (client-id auth)
P3->>P3: check live queue and 64-entry terminal overlay
alt not resolved live
P3->>P4: ext sessionTurnStatus
P4->>P5: bounded backward scan (10 pages of 500)
P5-->>P4: turn_result record or null
P4-->>P3: payload
end
P3->>P3: re-check overlay, concurrent terminal wins
P3-->>P2: status or not found
P2-->>P1: 200 status or 404 prompt_not_found
Files changed (25 of 25 shown)
Test evidenceThe PR's own CI at the reviewed commit, fetched via API (per the static-review rule I did not build or run any PR code; the live-behavior lane is the sandboxed trigger below). Final CI results for
One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。 The E2E report in this thread (capability discovery, tool-boundary final answer, truncation bound) is the author's self-reported result on macOS — attributed as a claim, not re-run here. Sandboxed verification would settle this: 中文说明代码审查读 diff 之前我先独立写了方案(能力点、两个仅限 live runtime 的 GET 路由、实时队列 + 有界终态 overlay、有界 transcript 回扫、"最后一个无工具调用响应块"作为最终回答)。PR 与之吻合,而且在最难的地方做得更细:bridge 在每次 await 子进程读取之后(包括读取失败时)重新检查 overlay,因此 lookup 中途出现的终态不会回退成 未发现关键阻塞项。对照周边代码核实过的点:
两点提请维护者留意,均不阻塞:
新增流程(对应英文版时序图):客户端 → serve 路由 → bridge:先查实时队列与 64 条终态 overlay;未命中则经 ext 方法调 ACP 子进程做有界回扫(10 页、每页 500 条);读取返回后重查 overlay(并发终态优先),最后返回状态或 404。 测试证据以上为被审提交在 CI 上的真实状态(API 拉取;按静态审查规则未构建/运行任何 PR 代码)。 线程中的 E2E 报告(能力点发现、工具边界最终回答、截断上限)是作者自述的 macOS 结果——作为声明引用,未在此复核。 沙箱验证可以定案: — Qwen Code · qwen3.8-max Reviewed at |
|
Confidence: 3/5 — clean review and unusually thorough tests, but this is a 1,000+ production-line feature across core session/recording internals, so the Stage 0 maintainer-awareness escalation caps auto-approval; it needs a human sign-off, not bot doubt. Stepping back: this is the version of PR 8682 that should have landed. The rebuild restored scope discipline — 7.9k lines down to 3k, explicit non-goals instead of teardown persistence and crash backfill creeping in — and the result is a contract a reviewer can actually hold in their head. The approach matches my independent proposal and exceeds it exactly where daemon code usually hurts: the races between polling, settlement, and transcript visibility are handled deliberately and tested individually. If I were maintaining this in six months I'd thank the author, not curse them — the design doc, the bounded everything, and the first-writer-wins publication are the kind of care that ages well. Why not approve, then:
⏸️ Deferring to @wenshao — the review itself found no blockers (Stage 2), but a core-surface feature of this size warrants a maintainer's sign-off on the contract before merge. Two housekeeping asks for @BenGuanRan: close PR 8682 in favor of this one, and consider running 中文说明置信度:3/5 —— review 干净、测试异常充分,但这是一个横跨 core session/recording 内部、1,000+ 生产行的 feature,Stage 0 的"维护者关注"升级决定了不能自动批准;这是流程要求,而非 review 存疑。 整体来看:这才是 PR 8682 本该落地的形态。重建恢复了范围纪律——从 7.9k 行收敛到 3k 行,用明确的 non-goals 取代了逐步膨胀的 teardown 持久化与 crash 回填——最终契约是评审者能完整把握的。方案与我的独立设想一致,并且恰好在 daemon 代码最容易出问题的地方做得更好:轮询、settle 与 transcript 可见性之间的竞态被刻意处理并逐一测试。半年后维护这段代码,只会感谢作者——设计文档、处处有界、first-writer-wins 发布,都是经得起时间的细致。 为什么不直接批准:
⏸️ 转交 @wenshao —— review 本身未发现阻塞项(见 Stage 2),但此规模的核心面 feature 在合入前应有 maintainer 对契约的确认。请 @BenGuanRan 处理两件事务:关闭 PR 8682 让位给本 PR;考虑在当前 head 上运行 — Qwen Code · qwen3.8-max Reviewed at |
🩺 serve daemon A/BBuilt the PR base vs this PR head
|
| field | PR base (before) | this PR (after) |
|---|---|---|
features[] |
— | "session_turn_status" |
— Qwen Code · serve A/B
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite (integration-tests/cli, incl. the new turn-status E2Es) did not run locally.
Not explored to full depth (tool budget reached): "This PR adds pollable daemon turn-status routes to qwen…": none — all checks above completed within budget.; "This PR adds pollable daemon turn-status routes to qwen…": none — all checks above completed within budget.; "This PR adds pollable daemon turn-status routes to qwen…": did not benchmark the record-count at which finding 1's collapse becomes reachable in a real session transcript (mechanism verified by code trace only).; "This PR adds pollable daemon turn-status routes to qwen…": I did not benchmark cold index build time against the 10s budget on a real transcript (no node_modules in the worktree / out of budget) — this is why Finding 1 …; "This PR adds pollable daemon turn-status routes to qwen…": did not benchmark cold buildIndex wall-time against the 10s budget on a real large transcript (worktree has no installed deps) — this is why the finding below…, and 21 more.
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite (integration-tests/cli, incl. the new turn-status E2Es) did not run locally。
未探索到全部深度(达到工具调用预算):"This PR adds pollable daemon turn-status routes to qwen…":none — all checks above completed within budget.;"This PR adds pollable daemon turn-status routes to qwen…":none — all checks above completed within budget.;"This PR adds pollable daemon turn-status routes to qwen…":did not benchmark the record-count at which finding 1's collapse becomes reachable in a real session transcript (mechanism verified by code trace only).;"This PR adds pollable daemon turn-status routes to qwen…":I did not benchmark cold index build time against the 10s budget on a real transcript (no node_modules in the worktree / out of budget) — this is why Finding 1 …;"This PR adds pollable daemon turn-status routes to qwen…":did not benchmark cold buildIndex wall-time against the 10s budget on a real large transcript (worktree has no installed deps) — this is why the finding below…,另有 21 条。
— qwen3.8-max via Qwen Code /review (v0.21.11)
| if (terminal && persisted) { | ||
| return enrichTerminalTurnStatus(terminal, persisted); | ||
| } |
There was a problem hiding this comment.
[Critical] R1-4: the overlay/persisted merge assumes both sources agree on a turn's terminal state, but the prompt-deadline path guarantees disagreement: the bridge latches state:'error'/prompt_deadline_exceeded in the overlay (first-writer-wins), while the child — with zero deadline awareness (no deadlineMs references in acp-integration) — persists its own completed/cancelled record for the same promptId. — Failure scenario: probe-reproduced — a wedged agent exceeds deadlineMs; the bridge latches the error terminal and releases the FIFO; the agent later completes and the child persists completed. During the overlay window the poll returns state:'error' enriched with the completed answer's resultText (a self-contradictory payload); after 64-entry eviction or daemon restart, the identical poll returns state:'completed'. One promptId flips error→completed purely as a function of poll timing; the "not an exactly-once store" disclaimer answers loss (404), not contradiction.
Suggested direction: reconcile the deadline case explicitly — prefer the persisted state once it exists and surface the deadline as metadata (or document that error/prompt_deadline_exceeded is superseded once the transcript settles). At minimum, never emit state:'error' enriched with a successful resultText.
中文说明
overlay/持久化的合并假设两个来源对某轮终态一致,但 prompt-deadline 路径必然造成不一致:bridge 以 first-writer-wins 在 overlay 中锁定 state:'error'/prompt_deadline_exceeded,而子进程完全无 deadline 概念(acp-integration 中没有任何 deadlineMs 引用),会为同一个 promptId 落盘自己的 completed/cancelled 记录。
失败场景:已探针复现——卡死的 agent 超过 deadlineMs;bridge 锁定 error 终态并释放 FIFO;agent 随后完成,子进程落盘 completed。在 overlay 窗口内轮询返回 state:'error' 却被 enrich 上成功回答的 resultText(自相矛盾的载荷);64 条驱逐或 daemon 重启之后,同样的轮询返回 state:'completed'。同一个 promptId 仅因轮询时机不同就在 error→completed 之间翻转;"非 exactly-once 存储" 的免责声明只覆盖丢失(404),不覆盖矛盾。
建议方向:显式调和 deadline 场景——持久化记录存在时以其状态为准、把 deadline 作为元数据呈现(或在文档中声明 transcript 落盘后 error/prompt_deadline_exceeded 被取代)。至少不要输出带成功 resultText 的 state:'error'。
— qwen3.8-max via Qwen Code /review (v0.21.11)
There was a problem hiding this comment.
Verified as a real fault-path disagreement, not treated as fixed. The PR description now explicitly states that the caller-facing deadline terminal is authoritative while retained in the 64-entry live overlay, while a child that settles later may persist a different execution outcome; after overlay eviction or restart the bounded transcript may expose that child outcome. Eliminating this would require durable terminal reconciliation or a permanent task-result ledger, which is outside this bounded polling contract. Leaving this thread open for maintainer confirmation of that contract.
There was a problem hiding this comment.
Verified as a real fault-path disagreement, not treated as fixed. The PR description now explicitly states that the caller-facing deadline terminal is authoritative while retained in the 64-entry live overlay, while a child that settles later may persist a different execution outcome; after overlay eviction or restart the bounded transcript may expose that child outcome. Eliminating this would require durable terminal reconciliation or a permanent task-result ledger, which is outside this bounded polling contract. Leaving this thread open for maintainer confirmation of that contract.
|
@qwen-code /resolve |
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
Qwen Code resolved the merge conflicts and pushed the branch update. Merge resolution for PR #9080Root causeMain's Textual or semanticSemantic in the import list: both sides edited the same four lines with opposing intent (main deleted two lines the PR kept as context; the PR added a line between them). Resolution — keep the PR's import, drop the two whose only users #9055 deleted: encodeSessionTranscriptCursor,
isTurnResultRecordPayload,
subagentGenerator,Dropping the pair is mandatory: no usage of either remains in the merged file, so keeping them would fail What is load-bearing
What I could not verifyNo build, typecheck, or tests were run here. Both #9055 and this PR modify 中文说明冲突根因:main 上的 语义冲突及解决:保留 PR 的新导入( 关键点:合并后的 未能验证:本次未运行构建或测试。#9055 与本 PR 同时修改了 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Not explored to full depth (tool budget reached): "PR 9080 reverse audit round 5 (cap round): hunt only gaps…": none — finished within budget, no check left incomplete.; "PR 9080 reverse audit round 5 (cap round): hunt only gaps…": none — all checks above completed within budget.; "PR 9080 reverse audit round 5 (cap round): hunt only gaps…": none — finished within budget.; "Second-round reverse audit of PR 9080 (pollable daemon…": none — all checks above completed within budget.; "PR 9080 reverse audit round 3: hunt only gaps all prior…": did not quantify real-world throw rates of #settleGoalTurn / releaseTurn / refreshSystemInstruction beyond confirming they are unguarded awaits with throwing …, and 19 more.
中文说明
未探索到全部深度(达到工具调用预算):"PR 9080 reverse audit round 5 (cap round): hunt only gaps…":none — finished within budget, no check left incomplete.;"PR 9080 reverse audit round 5 (cap round): hunt only gaps…":none — all checks above completed within budget.;"PR 9080 reverse audit round 5 (cap round): hunt only gaps…":none — finished within budget.;"Second-round reverse audit of PR 9080 (pollable daemon…":none — all checks above completed within budget.;"PR 9080 reverse audit round 3: hunt only gaps all prior…":did not quantify real-world throw rates of #settleGoalTurn / releaseTurn / refreshSystemInstruction beyond confirming they are unguarded awaits with throwing …,另有 19 条。
— qwen3.8-max via Qwen Code /review (v0.21.11)
| function enrichTerminalTurnStatus( | ||
| terminal: BridgeTurnStatus, | ||
| persisted: BridgeTurnStatus, | ||
| ): BridgeTurnStatus { |
There was a problem hiding this comment.
[Critical] R1-4: the overlay/persisted merge assumes both sources agree on a turn's terminal state, but the prompt-deadline path guarantees disagreement: the bridge latches state:'error'/prompt_deadline_exceeded in the overlay (first-writer-wins, publishPromptTerminal ~7585-7591), while the child — with zero deadline awareness (no deadlineMs references anywhere in packages/cli/src/acp-integration) — persists its own completed/cancelled record for the same promptId. The merge keeps the overlay's state while enriching with the persisted resultText. Still stands at head 24e98dd (re-checked; no fix since the round-1 report). — Failure scenario: probe-reproduced — a wedged agent exceeds deadlineMs; the bridge latches the error terminal and releases the FIFO; the agent later completes and the child persists completed with the answer. During the overlay window the poll returns state:'error' enriched with the completed answer's resultText (a self-contradictory payload); after 64-entry eviction or daemon restart, the identical poll returns state:'completed'. One promptId flips error→completed purely as a function of poll timing; the "not an exactly-once store" disclaimer answers loss (404), not contradiction.
Suggested direction: reconcile the deadline case explicitly — prefer the persisted state once it exists and surface the deadline as metadata (or document that error/prompt_deadline_exceeded is superseded once the transcript settles). At minimum, never emit state:'error' enriched with a successful resultText.
中文说明
overlay/持久化的合并假设两个来源对某轮终态一致,但 prompt-deadline 路径必然造成不一致:bridge 以 first-writer-wins 在 overlay 中锁定 state:'error'/prompt_deadline_exceeded(publishPromptTerminal 约 7585-7591 行),而子进程完全无 deadline 概念(packages/cli/src/acp-integration 中没有任何 deadlineMs 引用),会为同一个 promptId 落盘自己的 completed/cancelled 记录。合并时保留 overlay 的 state,却用持久化的 resultText 去 enrich。
失败场景:已探针复现——卡死的 agent 超过 deadlineMs;bridge 锁定 error 终态并释放 FIFO;agent 随后完成,子进程落盘 completed 及答案。在 overlay 窗口内轮询返回 state:'error' 却被 enrich 上成功回答的 resultText(自相矛盾的载荷);64 条驱逐或 daemon 重启之后,同样的轮询返回 state:'completed'。同一个 promptId 仅因轮询时机不同就在 error→completed 之间翻转;"非 exactly-once 存储" 的免责声明只覆盖丢失(404),不覆盖矛盾。
建议方向:显式调和 deadline 场景——持久化记录存在时以其状态为准、把 deadline 作为元数据呈现(或在文档中声明 transcript 落盘后 error/prompt_deadline_exceeded 被取代)。至少不要输出带成功 resultText 的 state:'error'。
— qwen3.8-max via Qwen Code /review (v0.21.11)
There was a problem hiding this comment.
Verified as a real fault-path disagreement, not treated as fixed. The PR description now explicitly states that the caller-facing deadline terminal is authoritative while retained in the 64-entry live overlay, while a child that settles later may persist a different execution outcome; after overlay eviction or restart the bounded transcript may expose that child outcome. Eliminating this would require durable terminal reconciliation or a permanent task-result ledger, which is outside this bounded polling contract. Leaving this thread open for maintainer confirmation of that contract.
There was a problem hiding this comment.
Verified as a real fault-path disagreement, not treated as fixed. The PR description now explicitly states that the caller-facing deadline terminal is authoritative while retained in the 64-entry live overlay, while a child that settles later may persist a different execution outcome; after overlay eviction or restart the bounded transcript may expose that child outcome. Eliminating this would require durable terminal reconciliation or a permanent task-result ledger, which is outside this bounded polling contract. Leaving this thread open for maintainer confirmation of that contract.
|
Updated this existing PR to Post-merge verification on the exact pushed head: Session 631/631, ACP bridge 661/661, recording service 80/80, Session service 139/139; workspace build, typecheck, lint, bundle, and diff check passed. Bundled daemon E2E passed 3/3 for final parent answer after a tool boundary, stable truncation status, and normal Session reload lookup; capability E2E passed 1/1. The supported restart boundary remains explicit: recording must be enabled, append must succeed, the result must remain on the active branch and within the bounded scan window, and the Session must be loaded live again. Deleted JSONL, disabled/failed recording, unexpected process crash, daemon shutdown, or results outside the window may return |
Exact-head E2E test reportTested commit: Environment: macOS, Node.js 22-compatible repository toolchain,
The generic |
Maintainer handoffCurrent head:
Under the documented bounded, best-effort contract, there is no known remaining production-code blocker. Please confirm whether the deadline boundary is acceptable for this PR; if stronger cross-restart terminal consistency is required, it should be designed as a separate durable task-result subsystem rather than extending this polling diff. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite (integration-tests/cli, incl. the new turn-status E2Es) did not run locally.
Not explored to full depth (tool budget reached): chunk 4: none — all checks I needed completed within budget.; "You are review agent reverse-audit — Reverse audit agent…": none — all checks I started were completed within budget.; "You are review agent reverse-audit — Reverse audit agent…": none — all checks I started completed within budget.; "You are review agent reverse-audit — Reverse audit agent…": none — all checks above completed within budget.; "You are review agent reverse-audit — Reverse audit agent…": none — all checks I started were completed within budget., and 8 more.
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite (integration-tests/cli, incl. the new turn-status E2Es) did not run locally。
未探索到全部深度(达到工具调用预算):chunk 4:none — all checks I needed completed within budget.;"You are review agent reverse-audit — Reverse audit agent…":none — all checks I started were completed within budget.;"You are review agent reverse-audit — Reverse audit agent…":none — all checks I started completed within budget.;"You are review agent reverse-audit — Reverse audit agent…":none — all checks above completed within budget.;"You are review agent reverse-audit — Reverse audit agent…":none — all checks I started were completed within budget.,另有 8 条。
— qwen3.8-max via Qwen Code /review (v0.21.11)
| if (promptId !== undefined) { | ||
| if (terminal && persisted) { | ||
| return enrichTerminalTurnStatus(terminal, persisted); | ||
| } |
There was a problem hiding this comment.
[Critical] R1-4: the overlay/persisted merge assumes both sources agree on a turn's terminal state, but the prompt-deadline path guarantees disagreement: the bridge latches state:'error'/prompt_deadline_exceeded in the overlay (first-writer-wins, publishPromptTerminal → rememberTerminalTurnStatus), while the child — with zero deadline awareness (no deadlineMs references anywhere in packages/cli/src/acp-integration) — persists its own completed record for the same promptId. The merge keeps the overlay's state while enriching with the persisted resultText. Unfixed since the round-1 report; git diff 24e98dd..bbf2443 changed nothing here. — Failure scenario: probe-reproduced end-to-end at this head — a wedged agent exceeds deadlineMs; the bridge latches the error terminal and releases the FIFO; the agent later completes and the child persists completed with the answer. During the overlay window the poll returns state:'error' enriched with the successful answer's resultText (a self-contradictory payload); after 64-entry eviction or daemon restart, the identical poll returns state:'completed'. One promptId flips error→completed purely as a function of poll timing.
Suggested direction: reconcile the deadline case explicitly — prefer the persisted state once it exists and surface the deadline as metadata (or document that error/prompt_deadline_exceeded is superseded once the transcript settles). The minimal variant below takes the persisted record when it disagrees with an error terminal; at minimum, never emit state:'error' enriched with a successful resultText.
| if (promptId !== undefined) { | |
| if (terminal && persisted) { | |
| return enrichTerminalTurnStatus(terminal, persisted); | |
| } | |
| if (promptId !== undefined) { | |
| if (terminal && persisted) { | |
| if (terminal.state === 'error' && persisted.state !== 'error') { | |
| return persisted; | |
| } | |
| return enrichTerminalTurnStatus(terminal, persisted); | |
| } |
中文说明
overlay/持久化的合并假设两个来源对某轮终态一致,但 prompt-deadline 路径必然造成不一致:bridge 以 first-writer-wins 在 overlay 中锁定 state:'error'/prompt_deadline_exceeded(publishPromptTerminal → rememberTerminalTurnStatus),而子进程完全无 deadline 概念(packages/cli/src/acp-integration 中没有任何 deadlineMs 引用),会为同一个 promptId 落盘自己的 completed 记录。合并时保留 overlay 的 state,却用持久化的 resultText 去 enrich。自第 1 轮报告以来未修复;git diff 24e98dd..bbf2443 在此处没有任何变化。
失败场景:已在当前 head 端到端探针复现——卡死的 agent 超过 deadlineMs;bridge 锁定 error 终态并释放 FIFO;agent 随后完成,子进程落盘 completed 及答案。在 overlay 窗口内轮询返回 state:'error' 却被 enrich 上成功回答的 resultText(自相矛盾的载荷);64 条驱逐或 daemon 重启之后,同样的轮询返回 state:'completed'。同一个 promptId 仅因轮询时机不同就在 error→completed 之间翻转。
建议方向:显式调和 deadline 场景——持久化记录存在时以其状态为准、把 deadline 作为元数据呈现(或在文档中声明 transcript 落盘后 error/prompt_deadline_exceeded 被取代)。下方的最小变体在 error 终态与持久化记录不一致时采用持久化记录;至少不要输出带成功 resultText 的 state:'error'。
— qwen3.8-max via Qwen Code /review (v0.21.11)
| pending: PendingPromptEntry, | ||
| terminal: PromptTerminal, | ||
| ): void { | ||
| const promptText = truncateTurnText(pending.text); |
There was a problem hiding this comment.
[Suggestion] R2-2: the settle-time truncation in rememberTerminalTurnStatus is still never exercised by any endpoint test (no reply, no fix since the round-2 report). The new truncation E2E covers settled resultText (persisted side) and the bridge unit 'caps live promptText at TURN_RESULT_TEXT_MAX_CHARS' covers the live overlay, but no test drives an oversized prompt through the overlay-terminal path, so this truncateTurnText(pending.text) call has no pin. — Concrete cost: dropping or bypassing this call ships green — during the overlay window the settled status for a >32,768-char prompt carries an unbounded promptText (and no promptTextTruncated flag), diverging from the live path's capped projection for the same promptId; nothing turns red.
Suggested fix: extend 'caps live promptText…' (or add a sibling) to settle the prompt and assert the overlay-terminal status carries promptText capped at TURN_RESULT_TEXT_MAX_CHARS with promptTextTruncated: true.
中文说明
rememberTerminalTurnStatus 中 settle 时的截断仍然没有任何接口测试覆盖(第 2 轮报告后无回复、无修复)。新的截断 E2E 覆盖了 settled 的 resultText(持久化侧),bridge 单测 'caps live promptText at TURN_RESULT_TEXT_MAX_CHARS' 覆盖了实时 overlay,但没有测试用超长 prompt 走 overlay 终态路径,因此这个 truncateTurnText(pending.text) 调用没有钉住。
具体代价:删除或绕过该调用可以绿灯上线——overlay 窗口内,超长 prompt 的 settled 状态会带无上限的 promptText(且没有 promptTextTruncated 标志),与同一个 promptId 实时路径的有上限投影互相矛盾;没有任何测试变红。
建议修复:扩展 'caps live promptText…'(或新增一个同款测试),让 prompt settle 后断言 overlay 终态的 promptText 被截到 TURN_RESULT_TEXT_MAX_CHARS 且带 promptTextTruncated: true。
— qwen3.8-max via Qwen Code /review (v0.21.11)
| onPromptAdmitted: () => { | ||
| expect(handle.agent.promptCalls).toHaveLength(0); | ||
| admittedStatus = bridge.getSessionTurnStatus( |
There was a problem hiding this comment.
[Suggestion] R2-3: the dispatch-boundary test's load-bearing assertion still runs INSIDE the onPromptAdmitted callback, and the bridge still swallows callback exceptions into an optional diagnostic sink (try { context?.onPromptAdmitted?.(); } catch { … onDiagnosticLine … }, bridge.ts ~7804-7811). No reply, no fix since the round-2 report. — Failure scenario: if a future change dispatches to the channel before admission fires, toHaveLength(0) throws inside the callback, the bridge swallows it, admittedStatus is never assigned, and the test then fails only via expect(undefined).resolves with vitest's "received value must be a Promise" — pointing the debugger at an async-contract phantom instead of the real ordering regression. The regression is still caught (never a silent pass), but through a misleading failure.
| onPromptAdmitted: () => { | |
| expect(handle.agent.promptCalls).toHaveLength(0); | |
| admittedStatus = bridge.getSessionTurnStatus( | |
| onPromptAdmitted: () => { | |
| admittedPromptCallCount = handle.agent.promptCalls.length; | |
| admittedStatus = bridge.getSessionTurnStatus( |
(capture in the callback, then assert expect(admittedPromptCallCount).toBe(0) outside, after the admittedStatus expectations)
中文说明
dispatch 边界测试的关键断言仍然写在 onPromptAdmitted 回调内部,而 bridge 仍然把回调异常吞进可选的 diagnostic sink(try { context?.onPromptAdmitted?.(); } catch { … onDiagnosticLine … },bridge.ts 约 7804-7811 行)。第 2 轮报告后无回复、无修复。
失败场景:未来若有改动让 channel 在 admission 触发前就被 dispatch,toHaveLength(0) 会在回调内抛错、被 bridge 吞掉,admittedStatus 永远不会被赋值,测试最终只会经由 expect(undefined).resolves 以 vitest 的 "received value must be a Promise" 失败——把调试者引向一个 async 契约的假象,而不是真正的顺序回退。回退仍会被捕获(不会静默通过),但失败信息具有误导性。
建议在回调内只做采集(admittedPromptCallCount = handle.agent.promptCalls.length;),在回调外、admittedStatus 断言之后再断言 expect(admittedPromptCallCount).toBe(0)。
— qwen3.8-max via Qwen Code /review (v0.21.11)
| it('keeps overlapping turn records attributed to their own promptIds', async () => { | ||
| // DAEMON-003 overlap: the bridge releases the FIFO on deadline |
There was a problem hiding this comment.
[Suggestion] R2-4: this DAEMON-003 overlap test still steers the predecessor down a path the real overlap never takes — the hand-rolled stream ignores the abort signal and completes cleanly, so the cancelled settle is asserted via the iteration-start abort check rather than the throw-from-model-await path that real supersession produces. No reply, no fix since the round-2 report. (The R1-3 fix removed the behavioral divergence between the two abort landings — both now settle cancelled — so what remains is the test-realism gap: the throw variant of the overlap is still unexercised.) — Failure scenario: a future regression affecting only the abort-throws-from-model-await path (the common one — the deadline supersede fires while the predecessor is wedged in a model await) keeps this test green, because the test's predecessor never throws.
Suggested fix: add an overlap variant whose predecessor stream rejects with an AbortError when the successor's install-time abort lands, asserting the predecessor still settles state:'cancelled' under its own promptId.
中文说明
这个 DAEMON-003 重叠测试仍然把前序轮引到真实重叠不会走的路径上——手写 stream 忽略 abort 信号并干净地完成,因此 cancelled 的 settle 是经由每轮迭代开头的中止检查断言的,而不是真实顶替产生的 model await 抛错路径。第 2 轮报告后无回复、无修复。(R1-3 的修复已消除两种中止落点的行为分歧——现在两者都 settle 为 cancelled——因此剩下的是测试真实性缺口:重叠的抛错变体仍未被演练。)
失败场景:未来某个只影响 model await 抛错路径的回退(这才是常见情形——deadline 顶替恰好发生在前一轮卡死在 model await 期间)不会让这个测试变红,因为测试里的前序轮从不抛错。
建议修复:新增一个重叠变体,让前序轮 stream 在 successor 安装时中止落地时以 AbortError 拒绝,并断言前序轮仍在自己的 promptId 下 settle 为 state:'cancelled'。
— qwen3.8-max via Qwen Code /review (v0.21.11)
| const live = entry.pendingPromptList.filter( | ||
| (pending) => | ||
| !pending.terminalPublished && | ||
| (!pending.removed || pending.state === 'running'), | ||
| ); |
There was a problem hiding this comment.
[Suggestion] the documented projection "Removed queued entries become terminal and are no longer projected as queued" has zero test coverage — the new getSessionTurnStatus describe block covers the removed-RUNNING case ('keeps a removed running prompt visible until it settles') but never removes a QUEUED prompt and polls its status. Verification probed the current behavior: it is correct (removePendingPrompt splices the queued entry and publishes cancelled via the DAEMON-004 path) — this is a pure pin gap. — Failure scenario: if rememberTerminalTurnStatus were skipped for the queued-removal terminal path (or the overlay lookup bypassed), GET /session/:id/turns/:promptId for a prompt the daemon just cancelled via removePendingPrompt would return 404 prompt_not_found instead of cancelled; no test turns red.
Suggested fix: add a bridge test: two prompts (one running, one queued), removePendingPrompt the queued one, then assert getSessionTurnStatus(sessionId, undefined, queuedId) resolves to { state: 'cancelled' } and getSessionTurnStatus(sessionId) does not report it as queued.
中文说明
文档承诺的投影"被删除的 queued 条目变为终态、不再投影为 queued"完全没有测试覆盖——新的 getSessionTurnStatus 测试块覆盖了 removed-RUNNING 情形('keeps a removed running prompt visible until it settles'),但从未删除一个 QUEUED prompt 再轮询其状态。验证阶段已用探针确认当前行为是正确的(removePendingPrompt 会 splice 掉 queued 条目并经 DAEMON-004 路径发布 cancelled)——这是一个纯粹的钉住缺口。
失败场景:若 queued 删除的终态路径跳过 rememberTerminalTurnStatus(或 overlay 查找被绕过),对刚被 removePendingPrompt 取消的 prompt 调 GET /session/:id/turns/:promptId 会返回 404 prompt_not_found 而不是 cancelled;没有任何测试变红。
建议修复:新增 bridge 测试——两个 prompt(一个 running、一个 queued),对 queued 的执行 removePendingPrompt,然后断言 getSessionTurnStatus(sessionId, undefined, queuedId) 解析为 { state: 'cancelled' },且 getSessionTurnStatus(sessionId) 不再把它报告为 queued。
— qwen3.8-max via Qwen Code /review (v0.21.11)
| !optionalString('stopReason') || | ||
| !optionalTimestamp('startedAt') || | ||
| !optionalString('promptText', TURN_RESULT_TEXT_MAX_CHARS) || |
There was a problem hiding this comment.
[Suggestion] R3-3 (pattern, 2 locations — this validator-cap side and the writer-type side at ~670-672): the read-side bounded contract caps promptText/resultText (32,768), error.message (4,096) and error.code (256), but leaves promptId, stopReason, and originatorClientId unbounded (optionalString without maxChars; promptId only checked non-empty at ~692), and settledTurnStatus (bridge.ts ~2068-2100) forwards all three verbatim into the GET /session/:id/turns/:promptId response. Probe-verified: the validator accepts 4,000,000-char values in all three fields while rejecting oversized resultText/promptText. The trigger is the validator's own untrusted-disk threat model (a corrupted or hand-edited transcript; production writers cannot produce such values, and reads are bounded by the 4 MiB page cap). — Failure scenario: a corrupted transcript carrying multi-megabyte values in any of those three fields passes validation and is echoed unbounded through the endpoint — the exact payload bloat the cap constants in this same diff were built to prevent, through the fields the contract forgot.
| !optionalString('stopReason') || | |
| !optionalTimestamp('startedAt') || | |
| !optionalString('promptText', TURN_RESULT_TEXT_MAX_CHARS) || | |
| !optionalString('stopReason', TURN_RESULT_ERROR_CODE_MAX_CHARS) || | |
| !optionalTimestamp('startedAt') || | |
| !optionalString('promptText', TURN_RESULT_TEXT_MAX_CHARS) || |
(apply the same treatment to promptId at ~692 and originatorClientId at ~721, with a small dedicated cap)
中文说明
R3-3(模式,2 处——这里是校验器上限侧,另一处写入类型侧在约 670-672 行):读取侧的有界契约给 promptText/resultText(32,768)、error.message(4,096)、error.code(256)设了上限,却让 promptId、stopReason、originatorClientId 无上限(optionalString 不带 maxChars;promptId 在约 692 行只检查非空),而 settledTurnStatus(bridge.ts 约 2068-2100 行)会把这三个字段原样转发进 GET /session/:id/turns/:promptId 响应。已用探针验证:校验器接受这三个字段的 4,000,000 字符值,同时拒绝超长的 resultText/promptText。触发条件正是校验器自己的不可信磁盘威胁模型(损坏或手工编辑的 transcript;生产写入方不可能产生这种值,且读取受 4 MiB 页上限约束)。
失败场景:损坏的 transcript 在这三个字段中携带数 MB 的值,通过校验后被接口无上限地回显——恰恰是同一个 diff 里那些上限常量本想防止的载荷膨胀,只是从契约遗漏的字段漏了进来。
建议对约 692 行的 promptId 与约 721 行的 originatorClientId 做同样处理(用一个小的专用上限)。
— qwen3.8-max via Qwen Code /review (v0.21.11)
| data: 'aGVsbG8=', | ||
| }, | ||
| ], | ||
| _meta: { 'qwen.daemon.promptDisplayText': '[image]' }, |
There was a problem hiding this comment.
[Suggestion] 'records the image placeholder used by the live turn status' supplies the placeholder via the qwen.daemon.promptDisplayText meta override, so the new extractTurnPromptText fallback branches it is named after (image-only → '[image]', no-text → '') are never exercised by any test. The meta is not always present in production (routes/session.ts forwards it only when the channel worker is authorized; the bridge sets it only for channel sessions), so an image-only daemon prompt without it falls through to extractTurnPromptText (Session.ts ~5904-5913). The bridge-side structurally-identical extractPromptText (bridge.ts:2008) drives the live status; only it is pinned. — Failure scenario: a future edit to extractTurnPromptText silently changes persisted turn_result.promptText — the field served by GET /session/:id/turns/:promptId after overlay eviction or restart — and makes it diverge from the live status the client saw for that promptId. No test turns red; the divergence surfaces only as an empty prompt text for an image turn after a restart.
Suggested fix: add cases sending an image-only prompt (and a text-less prompt) WITHOUT the meta, asserting promptText: '[image]' (resp. ''); ideally assert parity between the session-side extractor and the bridge's extractPromptText, or share one implementation.
中文说明
'records the image placeholder used by the live turn status' 是通过 qwen.daemon.promptDisplayText meta 覆盖来提供占位符的,因此它以之命名的新 extractTurnPromptText 兜底分支(仅图片 → '[image]'、无文本 → '')没有任何测试演练。生产中该 meta 并非总存在(routes/session.ts 仅在 channel worker 已授权时转发;bridge 仅对 channel 会话设置它),因此不带 meta 的仅图片 daemon prompt 会落到 extractTurnPromptText(Session.ts 约 5904-5913 行)。bridge 侧结构相同的 extractPromptText(bridge.ts:2008)驱动实时状态;只有它被钉住了。
失败场景:未来对 extractTurnPromptText 的修改会悄悄改变持久化的 turn_result.promptText——overlay 驱逐或重启之后 GET /session/:id/turns/:promptId 提供的正是该字段——使其与客户端就该 promptId 看到的实时状态产生分歧。没有任何测试变红;分歧只会在重启之后以图片轮次的 prompt 文本为空的形式暴露。
建议修复:新增不带 meta 的仅图片 prompt(以及无文本 prompt)用例,断言 promptText: '[image]'(分别为 '');理想情况下断言 session 侧提取器与 bridge 的 extractPromptText 一致性,或共用一个实现。
— qwen3.8-max via Qwen Code /review (v0.21.11)
| expect(mockChatRecordingService.recordTurnResult).toHaveBeenCalledWith( | ||
| expect.objectContaining({ | ||
| promptId: 'daemon-prompt-id', | ||
| state: 'error', | ||
| error: { message: 'model exploded' }, |
There was a problem hiding this comment.
[Suggestion] post-start failure turns never pin startedAt presence. The suite deliberately pins its ABSENCE for pre-start failures ('records admission errors without a startedAt timestamp' asserts expect(payload.startedAt).toBeUndefined()), but this test — a turn that started and then failed — asserts only state/error. startedAt is set at Session.ts:3743 after admission and forwarded to the turn-status API at bridge.ts:2080, so the started/failed distinction is intentional semantics with no guard here. Mutation-tested during verification: moving the assignment off the failure path leaves all 15 existing tests green; only a probe assertion catches it. — Failure scenario: a refactor relocating turnRecording.startedAt = Date.now() off the failure path makes every settled-after-start error record lose startedAt; after overlay eviction or daemon restart, GET /session/:id/turns/:promptId for a failed turn silently loses its start time and the pre-start/post-start failure distinction; no test turns red.
| expect(mockChatRecordingService.recordTurnResult).toHaveBeenCalledWith( | |
| expect.objectContaining({ | |
| promptId: 'daemon-prompt-id', | |
| state: 'error', | |
| error: { message: 'model exploded' }, | |
| expect(mockChatRecordingService.recordTurnResult).toHaveBeenCalledWith( | |
| expect.objectContaining({ | |
| promptId: 'daemon-prompt-id', | |
| state: 'error', | |
| error: { message: 'model exploded' }, | |
| }), | |
| ); | |
| const errorPayload = | |
| mockChatRecordingService.recordTurnResult.mock.calls[0][0]; | |
| expect(typeof errorPayload.startedAt).toBe('number'); | |
| expect(errorPayload.startedAt!).toBeLessThanOrEqual( | |
| errorPayload.endedAt!, | |
| ); |
中文说明
启动后失败的轮次从未钉住 startedAt 的存在性。测试套件刻意钉住了启动前失败时它的缺失('records admission errors without a startedAt timestamp' 断言 expect(payload.startedAt).toBeUndefined()),但本测试——一个已启动随后失败的轮次——只断言 state/error。startedAt 在 Session.ts:3743 的 admission 之后设置、在 bridge.ts:2080 转发进 turn-status 接口,因此 started/failed 的区分是有意的语义,这里却没有防护。验证阶段做了变异测试:把赋值挪出失败路径后,现有 15 个测试全部保持绿色;只有探针断言能捕获。
失败场景:把 turnRecording.startedAt = Date.now() 挪出失败路径的重构,会让所有启动后 settle 的 error 记录丢失 startedAt;overlay 驱逐或 daemon 重启之后,失败轮次的 GET /session/:id/turns/:promptId 会悄悄丢失启动时间以及启动前/启动后失败的区分;没有任何测试变红。
— qwen3.8-max via Qwen Code /review (v0.21.11)
| mockChat.sendMessageStream = vi.fn().mockResolvedValue( | ||
| createFailingStream('Request was aborted.', () => { | ||
| void session.cancelPendingPrompt(); | ||
| }), | ||
| ); |
There was a problem hiding this comment.
[Suggestion] the cancel × error-shape discrimination matrix is pinned in only one of its informative cells, and this test's title masks the hole. The title says 'user cancel races a NON-ABORT stream error' but the body uses the abort-shaped message 'Request was aborted.' — the genuine non-abort-message + concurrent-cancel cell has zero coverage, and the complementary negative cell (abort-shaped error, NO cancel → state:'error') has none either (the plain-error test uses the non-abort-shaped 'model exploded'). The settle mapping is flag-based today (Session.ts:3494-3506 never inspects the error message). Mutation-tested during verification: OR-ing message-based cancel detection into controlledAbort leaves all 15 existing tests green; only a probe catches it. Note 'Request was aborted.' is the real APIUserAbortError message, and message-based cancel detection is an existing pattern in sibling code (desktop SessionManager, vscode SessionMessageHandler) — the drift is a live pattern, not an idle hypothetical. — Failure scenario: a future change adding message-based cancel detection would settle genuine provider/network aborts with no user cancel as state:'cancelled' — the turn-status API reporting real API failures as user cancellations — and user cancels racing plain provider failures as state:'error'; no test turns red in either direction.
Suggested fix: rename this test to match its body (cancel racing an abort-shaped error), and add both missing cells: (a) createFailingStream('Request was aborted.') with no cancel hook → assert { state: 'error' }; (b) createFailingStream('model exploded', () => { void session.cancelPendingPrompt(); }) → assert { state: 'cancelled', stopReason: 'cancelled' }.
中文说明
取消 × 错误形状的判别矩阵只在其一个有信息量的格子上被钉住,而且这个测试的标题掩盖了缺口。标题写的是 'user cancel races a NON-ABORT stream error',但测试体使用的是 abort 形状的消息 'Request was aborted.'——真正的非 abort 消息 + 并发取消的格子零覆盖,互补的负向格子(abort 形状错误、无取消 → state:'error')也没有覆盖(纯错误测试用的是非 abort 形状的 'model exploded')。当前的 settle 映射是基于标志的(Session.ts:3494-3506 从不检查错误消息)。验证阶段做了变异测试:把基于消息的取消检测并入 controlledAbort 后,现有 15 个测试全部保持绿色;只有探针能捕获。注意 'Request was aborted.' 正是真实 APIUserAbortError 的消息,而且基于消息的取消检测在兄弟代码中已是既有模式(desktop SessionManager、vscode SessionMessageHandler)——这种漂移是现实中活着的模式,不是空想的假设。
失败场景:未来加入基于消息的取消检测后,真正的 provider/network 中止(无用户取消)会被 settle 为 state:'cancelled'——turn-status 接口把真实的 API 失败报告成用户取消——而与纯 provider 失败竞态的用户取消会被 settle 为 state:'error';两个方向都没有测试变红。
建议修复:把本测试改名为与测试体一致(取消与 abort 形状错误竞态),并补上缺失的两个格子:(a) 不带取消钩子的 createFailingStream('Request was aborted.') → 断言 { state: 'error' };(b) createFailingStream('model exploded', () => { void session.cancelPendingPrompt(); }) → 断言 { state: 'cancelled', stopReason: 'cancelled' }。
— qwen3.8-max via Qwen Code /review (v0.21.11)
| promptText: 'settled prompt', | ||
| resultText: 'settled answer', | ||
| originatorClientId: 'client-9', |
There was a problem hiding this comment.
[Suggestion] no persisted record in any test carries resultTruncated: true, so settledTurnStatus' defaulting branch (resultCode: record.resultCode ?? TURN_RESULT_CODE_TEXT_TRUNCATED, bridge.ts:2095) is unreached by the suite — grep for resultTruncated/resultCode in bridge.test.ts returns zero matches; the writer always emits both fields as a pair, so the ?? default never executes even in the E2E. The branch is reachable, not dead defense: the validator only fails resultCode when it is present and inconsistent, so a {resultTruncated: true} record with no resultCode passes validation and flows through the child response into settledTurnStatus. — Failure scenario: a refactor removing the ?? TURN_RESULT_CODE_TEXT_TRUNCATED default makes validator-valid truncated records surface with resultTruncated: true but no resultCode, silently dropping the truncation marker clients key on; no test turns red.
Suggested fix: extend one persisted-record test (e.g. this fixture) with a variant { …, resultTruncated: true } (no resultCode) and assert resultCode === TURN_RESULT_CODE_TEXT_TRUNCATED in the returned status.
中文说明
所有测试中的持久化记录都不带 resultTruncated: true,因此 settledTurnStatus 的默认值分支(resultCode: record.resultCode ?? TURN_RESULT_CODE_TEXT_TRUNCATED,bridge.ts:2095)从未被测试套件走到——在 bridge.test.ts 中 grep resultTruncated/resultCode 均为零匹配;写入方总是成对写出两个字段,所以即使在 E2E 中 ?? 默认值也从不执行。该分支是可达的,不是死防御:校验器只在 resultCode 存在且不一致时才判失败,因此一条不带 resultCode 的 {resultTruncated: true} 记录可以通过校验、经子进程响应流入 settledTurnStatus。
失败场景:删除 ?? TURN_RESULT_CODE_TEXT_TRUNCATED 默认值的重构,会让校验器合法的截断记录以 resultTruncated: true 但没有 resultCode 的形式出现,悄悄丢掉客户端赖以判断的截断标记;没有任何测试变红。
建议修复:在某个持久化记录测试(例如本 fixture)中加一个变体 { …, resultTruncated: true }(不带 resultCode),断言返回状态中 resultCode === TURN_RESULT_CODE_TEXT_TRUNCATED。
— qwen3.8-max via Qwen Code /review (v0.21.11)
What this PR does
This PR adds an always-on
session_turn_statuscapability and two read-only, live-Session routes:GET /session/:id/turns/currentandGET /session/:id/turns/:promptId. Callers can pollidle,queued,running,completed,cancelled, orerrorwithout maintaining an SSE subscription.The exact prompt route returns the raw final parent-model answer from the last tool-free response block. Text before a tool call, tool output, thought text, subagent updates, diagnostics, background output, and optional rewritten presentation are not reported as
resultText. Results are bounded at 32,768 UTF-16 code units and exposeresultTruncated: trueplusRESULT_TEXT_TRUNCATEDwhen that bound is reached.Live state comes from the owning bridge. Recent terminals use a fixed 64-entry in-process overlay while settled turns are appended once, best-effort, by the Session recorder and read from a bounded active-transcript window. A successful rewind clears the overlay, failed rewind keeps it, and forks do not inherit source prompt identities.
Why it's needed
Automation clients such as AgentRun receive a
promptIdfrom non-blocking prompt admission but currently need to keep an SSE stream open to learn the final state and main answer. This makes short-lived or reconnecting callers unnecessarily complex. The polling surface lets them recover the result for a live Session while preserving workspace ownership and client authorization.This PR intentionally supersedes #8682 instead of extending it. That PR validated the problem and several important correctness cases, but after many review rounds its diff grew into strict teardown persistence, crash/shutdown transcript backfill, rewind indexing, rewrite-pipeline changes, and repeated conflict resolution. Those changes exceeded the requested polling contract and made review convergence harder. This is a clean rebuild from the latest
main: it retains the validated API, final-answer semantics, bounded live/persisted lookup, and critical race fixes, while explicitly leaving crash durability and permanent result storage out of scope.Reviewer Test Plan
How to verify
/capabilitiescontainssession_turn_status.promptId; expect queued/running while live and a settled terminal afterward.resultTextto contain only the answer after the tool boundary.resultTruncated: true, andresultCode: "RESULT_TEXT_TRUNCATED".404 prompt_not_found. Confirm this is a bounded not-found result, not proof that the prompt never existed.Evidence (Before & After)
Before: the installed global
qwen0.18.5 does not advertisesession_turn_statusand has no turn polling route.After: a built daemon backed by the repository's fake OpenAI server passed capability discovery, the tool-boundary final-answer E2E, and the 32,768-code-unit truncation E2E. Full changed-file suites passed: Session 611/611, ACP agent 400/400, ACP bridge 593/593, core recording/session service 215/215, conversation branches 21/21, and serve server 938/938. Repository build, bundle, lint, and workspace typecheck also passed.
Tested on
Environment (optional)
macOS, Node.js 22-compatible repository toolchain, no sandbox, real
qwen serveprocess with the local fake OpenAI-compatible server.Risk & Scope
Linked Issues
Related to #8680
Supersedes #8682
中文说明
本 PR 做了什么
本 PR 新增默认开启的
session_turn_status能力点,以及两个只读、仅面向存活 Session 的接口:GET /session/:id/turns/current和GET /session/:id/turns/:promptId。调用方无需保持 SSE 订阅,即可轮询idle、queued、running、completed、cancelled或error状态。精确 prompt 接口返回父模型最后一个不含工具调用的响应块中的原始最终回答。工具调用前的文本、工具输出、思考文本、subagent 更新、诊断信息、后台输出以及可选的改写展示文本都不会进入
resultText。结果上限为 32,768 个 UTF-16 code units,达到上限时返回resultTruncated: true和RESULT_TEXT_TRUNCATED。实时状态来自 Session 所属的 bridge。最近的终态使用固定 64 条的进程内 overlay;已结束的 turn 由 Session recorder 单次、best-effort 写入,并从有界的 active transcript 窗口读取。rewind 成功后清空 overlay,rewind 失败时保留;fork 不会继承源 Session 的 prompt 身份。
为什么需要
AgentRun 等自动化调用方从非阻塞 prompt admission 获得
promptId后,目前必须持续保持 SSE 才能获知最终状态和主回答,这让短生命周期或需要重连的调用方承担了不必要的复杂度。新增轮询接口让它们可以在 live Session 范围内恢复结果,同时保持工作空间归属与 client 授权边界。本 PR 有意替代 #8682,而不是继续扩展它。#8682 已验证问题和多项重要正确性场景,但经过多轮评审后,其 diff 逐步扩展到了严格 teardown 持久化、crash/shutdown transcript 回填、rewind 索引、rewrite pipeline 改造以及反复的冲突处理。这些内容已经超出本次轮询契约,并提高了评审收敛难度。因此本 PR 基于最新
main重新最小实现:保留已验证的 API、最终回答语义、有界实时/持久化查询和关键竞态修复,同时明确不处理 crash durability 和永久结果存储。Reviewer 测试计划
如何验证
/capabilities包含session_turn_status。promptId轮询;执行期间应看到 queued/running,结束后应看到终态。resultText应只包含工具边界之后的回答。resultTruncated: true和resultCode: "RESULT_TEXT_TRUNCATED"。404 prompt_not_found。该结果仅表示在有界范围内未找到,并不证明 prompt 从未存在。证据(Before & After)
Before:本机全局安装的
qwen0.18.5 不会发布session_turn_status,也没有 turn 轮询接口。After:构建后的真实 daemon 使用仓库内 fake OpenAI server,已通过能力点发现、“工具边界后最终回答”E2E 和“32,768 code units 截断状态”E2E。完整变更文件测试均通过:Session 611/611、ACP agent 400/400、ACP bridge 593/593、core recording/session service 215/215、conversation branches 21/21、serve server 938/938。仓库 build、bundle、lint 和 workspace typecheck 也全部通过。
测试系统
环境(可选)
macOS,兼容 Node.js 22 的仓库工具链,无 sandbox,真实
qwen serve进程配合本地 fake OpenAI-compatible server。风险与范围
关联 Issue
Related to #8680
Supersedes #8682