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)
|
@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)
|
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)
When the prompt-deadline path latches an error terminal in the overlay and the child later settles and persists a non-error turn_result for the same promptId, the poll surface previously kept the overlay error while enriching it with the successful resultText, and flipped to completed only after overlay eviction or restart. Merge via mergeTerminalWithPersisted at the two enrich call sites so the persisted outcome supersedes a bridge-synthesized error terminal once it exists; the exactly-once turn_error event publication and FIFO release are unchanged. The different-promptId endedAt tie-break is intentionally untouched.
Update: R1-4 deadline-consistency fix pushedNew head: This supersedes the handoff's "maintainer confirmation of the bounded contract" ask for the R1-4 deadline boundary: instead of leaving the disagreement disclosed, the merge now prefers the child's persisted outcome on the poll surface once it exists and is non-error, so polls never emit an error state enriched with a successful
The three R1-4 threads have been replied to with details. |
…us-polling-v2 # Conflicts: # packages/cli/src/serve/server/telemetry.test.ts
|
Heads-up on the new head: 中文说明新 head |
R3-3: cap promptId, stopReason, and originatorClientId at 256 chars in isTurnResultRecordPayload, closing the unbounded echo of corrupted-transcript values through GET /session/:id/turns/:promptId; recordTurnResult now validates payloads against the same contract before appending, so type-correct but invalid shapes (error state without error, error on non-error states) can no longer produce records invisible to the restart scan. Also lands the four round-5 test assertions: merged-payload error-leak pin, multi-model-call settle count, successor attribution in the superseded-throws test, and the early session-mismatch guard pin.
Final round: remaining six findings fixed — zero unresolved threadsThe six threads left open after the round-5 closeout were all resolved by fixing them directly in
Verification: chatRecordingService 100/100, Session 644/644, bridge 698/698, full typecheck clean, lint/format clean. Current state: all review threads resolved (0 open), 中文说明R5 收口后剩余的 6 个线程已全部直接修复于 |
Review: pollable daemon turn statusI read the non-test diff hunk by hunk against 1. (high) The new outer
|
yiliang114
left a comment
There was a problem hiding this comment.
Second-pass verification of the open findings against head 82d689b.
Finding 1 (high) — confirmed at code level. The outer catch in prompt() (~L3549) converts any thrown error to {stopReason:'cancelled'} whenever the abort controller carries USER_CANCEL / NEW_PROMPT / SESSION_DISPOSE. The inner send-error handler (~L4717) deliberately excludes NEW_PROMPT from controlled cancellation, with the explicit comment "Other AbortErrors still surface so infrastructure failures are not hidden as cancellations." The scenario is real: a prompt aborted by a newer prompt that then fails with a genuine API error now settles as cancelled, and via mergeTerminalWithPersisted that non-error terminal supersedes the real failure on the poll surface. Agree it should be gated on the same reasons the inner handler accepts.
Finding 2 (medium) — substantively valid, one nuance. getSessionTurnStatus does consult live status before the child read (liveBeforeRead, ~L10243), but the terminal overlay (entry.terminalTurnStatuses) is only consulted after requestSessionStatus resolves or fails. An already-settled promptId therefore still pays the full child transcript scan, and an unknown promptId pays the worst case with no early exit.
Routes/auth check (my own pass). Both new GET routes resolve through the shared requireSessionRuntime gate and the bridge calls resolveTrustedClientId before any read; no authorization gap found. Route ordering (/turns/current registered before /turns/:promptId) is correct.
Holding approval until finding 1 is addressed.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: reverse audit — did not converge within the reverse-audit round cap of 5.
Deferred under the convergence posture (round 6, not a blocker) — recorded, not requested in this round:
packages/cli/src/acp-integration/session/Session.test.ts:4901 — [probe] error/cancelled settle tests lack a toHaveBeenCalledTimes(1) pin against double-settle regressionsdocs/developers/qwen-serve-protocol.md:2473 — [review] protocol doc overstates deadline supersede — an error settle keeps the deadline errorpackages/core/src/services/chatRecordingService.ts:623 — [probe] UTF-16 truncation can split a surrogate pair; strict JSON decoders reject the lone surrogate
中文说明
仅完成部分审查,审查缺口已披露。
未审查:reverse audit — did not converge within the reverse-audit round cap of 5。
收敛姿态下延后(第 6 轮,非阻断)——已记录,本轮不要求修改:共 3 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.21.13)
- Session: settle a successor-aborted turn as cancelled only when the thrown error is the abort itself; genuine failures after a NEW_PROMPT abort surface as error, matching the send-loop contract - bridge: serve repeat polls of a settled promptId from the enriched overlay instead of re-scanning the child transcript, and give the turn-status read the transcript timeout instead of the 10s init default - bridge: forward the channel display text unchanged; Session treats an empty display text as absent for the turn record ([image] fallback) - Session: cap streamed-response accumulation for turns without a channel delivery at the turn-result bound - docs: document the bounded non-monotonicity of poll terminals
Round-6 review fixes — head
|
|
Merged Conflict resolution notes:
Re-verified at the merge commit: 中文摘要已合并 |
|
@qqqys @yiliang114 — requesting a re-verification at the new head Why the head moved:
Re-verified at Suggested focus: the catalog union counts above, and that the two feature sets coexist cleanly in 中文@qqqys @yiliang114 请在新 head |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: reverse audit — stopped at the 5-round cap without converging (round 5 still reporting).
Deferred under the convergence posture (round 7, not a blocker) — recorded, not requested in this round:
packages/acp-bridge/src/bridge.ts:10569 — [review] exact-promptId path never checks persisted.promptId === promptId (defense-in-depth across the ACP boundary)packages/cli/src/acp-integration/session/Session.ts:1391 — [review] single oversized part defeats the capChars memory bound in appendChannelDeliveryResponseTextpackages/acp-bridge/src/bridge.ts:10570 — [probe] write-back cache never warms for the current route; every current poll pays a full child scan; design doc overpromises
中文说明
仅完成部分审查,审查缺口已披露。
未审查:reverse audit — stopped at the 5-round cap without converging (round 5 still reporting)。
收敛姿态下延后(第 7 轮,非阻断)——已记录,本轮不要求修改:共 3 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.21.13)
…us-polling-v2 # Conflicts: # packages/acp-bridge/src/bridge.ts # packages/cli/src/serve/acp-session-bridge.ts
… trusted prompt projection A successful rewind that completes while a getSessionTurnStatus child transcript scan is in flight could let the pre-rewind record be cached into the freshly cleared overlay and served forever. Track a per-session rewind generation captured before the scan and discard the scanned outcome when it moved. enrichTerminalTurnStatus and the deadline-supersede merge returned the child-recorded promptText ahead of the bridge's trusted display projection, leaking hidden channel context on the poll surface. Make promptText/promptTextTruncated backfill-only and keep the terminal's projection in the supersede path. Make the pinning test adversarial and correct a false comment about the child's ''-as-absent fallback.
Round-7 closeout — both Criticals fixed, head
|
Real-model E2E at head
|
|
@qqqys @yiliang114 — requesting a re-review at the new head Since the last request (
中文说明请在新 head |
yiliang114
left a comment
There was a problem hiding this comment.
Approving at head f7659d6 (merge of main; PR files unchanged by the merge).
- Round-7 Criticals verified at f7f155d: the rewind-generation counter discards a child-transcript scan outcome when a successful rewind completes mid-scan (neither served nor cached), and
promptText/promptTextTruncatedare now backfill-only with the deadline-supersede path keeping the bridge's trusted projection — hidden channel context can no longer surface on the poll routes. Both have adversarial regression tests. - Prior review rounds' findings all carry through at this head; all 70 review threads are resolved.
- Test suites were green at the pre-merge head f7f155d; CI on the merged head is still running and merge gating will hold until it lands.
Both R7 Criticals fixed at f7f155d per the bot's own suggestions; threads resolved, tests green, two write-access approvals at new head.
|
Released in v0.21.14. |
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
turn_errorevent was already delivered at deadline time), so polls never emit an error state enriched with a successfulresultText. This PR intentionally does not add durable terminal reconciliation or a permanent task-result ledger.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。风险与范围
turn_error事件已在 deadline 时发出),因此轮询永远不会输出带成功resultText的 error 状态。本 PR 有意不引入 durable terminal reconciliation 或永久任务结果账本。关联 Issue
Related to #8680
Supersedes #8682