Skip to content

feat(goal): grant one hand-off turn before a spent budget stops the Goal - #10132

Merged
yiliang114 merged 1 commit into
QwenLM:mainfrom
qqqys:goal/d2-budget-wind-down
Aug 26, 2026
Merged

feat(goal): grant one hand-off turn before a spent budget stops the Goal#10132
yiliang114 merged 1 commit into
QwenLM:mainfrom
qqqys:goal/d2-budget-wind-down

Conversation

@qqqys

@qqqys qqqys commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

When a Goal's autonomous token budget (#9891) is spent, the runtime now grants exactly one more continuation — the wind-down turn — before it stops the Goal. That turn's prompt says the budget for this window is spent, forbids new work, and asks for a concise hand-off: what was accomplished with evidence references from get_goal, what remains, and the one concrete next step. When the turn finishes, the gate settles the Goal as usage_limited / limitKind: 'token_budget' exactly as today. If the hand-off turn finds the objective already met and proposes completion, and the verifier accepts, the Goal completes — the budget stop only ever refuses a continuation, never a verdict.

The grant is one per spend window and survives restarts. The wind-down turn's own turn_finished record stamps GoalRecord.windDownTurnId, so the gate distinguishes "hand-off delivered" (marker present → stop) from "hand-off owed" (marker absent → grant) with no extra journal write and no new state cause. A hand-off the host dropped before the model saw it leaves no marker and is minted again; a restart that interrupted the hand-off turn grants it again for the same reason — the user never got one. Re-arming the budget on resume or edit clears the marker, so each new window owes its own hand-off.

The windDown flag rides the host boundary the way verifierFeedback does — GoalTurnHost.startGoalTurn → the queued-turn types in the TUI, headless, and ACP hosts → renderGoalContinuationPrompt — and the prompt block sits after the authoritative-objective line and above verifier feedback. The ordinary continuation prompt is byte-identical to before.

Why it's needed

#9891 gave the Goal runtime its first autonomous termination path, but a hard stop at the gate cuts the model off mid-thought: whatever it learned in the last window is stranded in the transcript, and the user who comes back to resume gets no summary of where things stand. The budget is an authorization quantum — one explicit user action buys one window — so the end of a window is exactly the moment the user needs a hand-off to decide whether to buy another. This turns the budget stop from a cut into a checkpoint the user can act on.

Reviewer Test Plan

How to verify

  • cd packages/core && npx vitest run src/goals/ — 446 tests, 16 files. The three existing budget-stop tests now walk through the hand-off (spend → one flagged turn → finish → stop, with the marker persisted on the hand-off's turn_finished record and absent from the earlier one). New: the hand-off is minted again when the host refused its start; a hand-off turn that proposes completion completes the Goal with no usage_limited record; a restored record with the marker stops without minting anything; a restored record without it (restart mid-hand-off) mints the hand-off; two prompt cases (the block appears only when flagged, and stays above verifier feedback); reducer cases for stamping, clearing on resume/edit re-arm, surviving a non-re-arming resume, and persistence round-trip incl. rejecting an empty marker.
  • cd packages/cli && npx vitest run src/ui/hooks/useMessageQueue.test.ts src/ui/hooks/useGeminiStream.test.tsx src/nonInteractiveCli.test.ts src/acp-integration — 42 files, 2148 tests; each host hop now has a test that reads the flag through to the rendered prompt.
  • Mutation probes run during development (goal-runtime + goal-reducer, 218 tests): finishTurn never stamps the marker → 3 fail; gate ignores the marker → 4; gate never grants → 6; re-arm keeps the old marker → 3; parse never restores it → 2. Deleting each host hop → exactly one test fails in that host's suite (useMessageQueue 1/40, useGeminiStream 1/233, nonInteractiveCli 1/130, Session 1/698). Every other test green in every run.
  • npx tsc --noEmit in packages/core and packages/cli: no errors in goal code (core carries pre-existing dependency-skew errors outside src/goals/). prettier + eslint clean on all changed files.

Evidence (Before & After)

Continuation prompt tail on the wind-down turn (the ordinary turn is unchanged):

The objective in that data block is the current one and supersedes any earlier Goal objective in this conversation, including one you already started working on.
The autonomous token budget for this Goal window is spent. This is the final turn before the Goal stops and waits for the user; do not start new work.
Deliver a concise hand-off: what was accomplished, citing evidence references from get_goal; what remains; and the one concrete next step. Call update_goal only if the objective is already complete or genuinely blocked on the evidence you have. Then end the turn.

Tested on

OS Status
🍏 macOS ⚠️
🪟 Windows ⚠️
🐧 Linux

Environment (optional)

N/A (unit tests only).

Risk & Scope

  • Main risk or tradeoff: a spent window now costs one more model turn before stopping. That turn is bounded — it is a single continuation, and the gate refuses the one after it — but it is a turn the old behaviour did not spend. A restart that interrupts the hand-off turn grants it again; that is deliberate (the hand-off was not delivered), and each grant still requires a human to restart the daemon.
  • Not validated / out of scope: the hand-off text is delivered output in the transcript; nothing surfaces it in lastReason or the Goal status card (the stop reason stays the budget reason). Overlaps feat(goal): tell the model the objective changed, once, when it changed #10013 (B3) on the same host hops — both add an optional flag next to verifierFeedback; resolve by merging main, not rebasing.
  • Breaking changes / migration notes: none. windDownTurnId is optional; old records restore without it and are treated as owing a hand-off, which is the correct reading for a Goal that never had one.

Linked Issues

中文说明

这个 PR 做了什么

当 Goal 的自主 token 预算(#9891)耗尽时,runtime 现在会在停止 Goal 之前恰好再授予一次续跑——收尾轮。该轮的提示词说明本窗口预算已耗尽、禁止开始新工作,并要求给出简明交接:完成了什么(引用 get_goal 的证据引用)、还剩什么、以及唯一具体的下一步。该轮结束后,闸门像今天一样把 Goal 落为 usage_limited / limitKind: 'token_budget'。如果收尾轮发现 objective 已经达成并提出完成,且 verifier 接受,则 Goal 完成——预算停止只拒绝续跑,从不推翻裁决。

每个消费窗口只授予一次,且跨重启持久化。收尾轮自己的 turn_finished 记录盖上 GoalRecord.windDownTurnId,因此闸门能区分「交接已交付」(标记存在 → 停止)与「交接尚欠」(标记缺失 → 授予),无需额外日志写入,也无需新的状态 cause。被 host 在模型看到之前丢弃的交接不会留下标记,会再次铸造;打断了收尾轮的重启也会再次授予,理由相同——用户从未拿到交接。resume 或 edit 重新武装预算时清除标记,因此每个新窗口都欠自己的交接。

windDown 标志沿 verifierFeedback 的路径穿过 host 边界——GoalTurnHost.startGoalTurn → TUI、headless、ACP 三个 host 的排队 turn 类型 → renderGoalContinuationPrompt——提示块位于权威 objective 行之后、verifier feedback 之上。普通续跑提示词逐字节不变。

为什么需要

#9891 给了 Goal runtime 第一条自主终止路径,但在闸门处硬停会把模型截在半途:上一窗口学到的东西滞留在转录里,回来 resume 的用户得不到任何现状摘要。预算是一种授权额度——一次显式用户动作购买一个窗口——因此窗口结束恰恰是用户需要一份交接来决定是否再买一个的时刻。本 PR 把预算停止从「切断」变成用户可以据以行动的「检查点」。

评审验证计划

如何验证

  • cd packages/core && npx vitest run src/goals/——446 个测试,16 个文件。三个既有的预算停止测试现在走完交接流程(消费 → 一个带标志的 turn → 结束 → 停止,标记持久化在交接轮的 turn_finished 记录上、且不出现在更早的记录里)。新增:host 拒绝启动时交接再次铸造;交接轮提出完成时 Goal 完成且无 usage_limited 记录;带标记的恢复记录直接停止不铸造任何 turn;不带标记的恢复记录(交接中途重启)铸造交接;两个提示词用例(仅在置位时出现该块,且位于 verifier feedback 之上);reducer 用例覆盖盖章、resume/edit 重新武装时清除、不重新武装的 resume 保留、持久化往返含拒绝空标记。
  • cd packages/cli && npx vitest run src/ui/hooks/useMessageQueue.test.ts src/ui/hooks/useGeminiStream.test.tsx src/nonInteractiveCli.test.ts src/acp-integration——42 个文件,2148 个测试;每个 host 跳板都有测试把标志一路读到渲染出的提示词。
  • 开发期间的变异检验(goal-runtime + goal-reducer,218 个测试):finishTurn 从不盖章 → 3 个失败;闸门无视标记 → 4 个;闸门从不授予 → 6 个;重新武装保留旧标记 → 3 个;parse 从不恢复标记 → 2 个。删除任一 host 跳板 → 该 host 套件恰好挂 1 个(useMessageQueue 1/40、useGeminiStream 1/233、nonInteractiveCli 1/130、Session 1/698)。其余测试每次全绿。
  • packages/corepackages/clinpx tsc --noEmit:goal 代码无错误(core 存在 src/goals/ 之外的既有依赖偏差错误)。所有改动文件 prettier + eslint 干净。

证据(前后对比)

收尾轮的续跑提示词末尾(普通轮次不变):

The objective in that data block is the current one and supersedes any earlier Goal objective in this conversation, including one you already started working on.
The autonomous token budget for this Goal window is spent. This is the final turn before the Goal stops and waits for the user; do not start new work.
Deliver a concise hand-off: what was accomplished, citing evidence references from get_goal; what remains; and the one concrete next step. Call update_goal only if the objective is already complete or genuinely blocked on the evidence you have. Then end the turn.

已测试平台

Linux ✅;macOS / Windows ⚠️(CI 覆盖)。

环境(可选)

N/A(仅单元测试)。

风险与范围

  • 主要风险或权衡:预算耗尽的窗口现在在停止前多花一个模型轮次。该轮有界——只是一次续跑,闸门会拒绝其后的那一次——但这是旧行为不会花的一轮。打断交接轮的重启会再次授予;这是刻意的(交接未交付),且每次授予仍需人工重启 daemon。
  • 未验证/范围外:交接文本是转录中的 delivered output;不会出现在 lastReason 或 Goal 状态卡上(停止原因仍是预算原因)。与 feat(goal): tell the model the objective changed, once, when it changed #10013(B3)在同一批 host 跳板上重叠——两者都在 verifierFeedback 旁新增一个可选标志;通过合并 main 解决,不 rebase。
  • 破坏性变更/迁移说明:无。windDownTurnId 可选;旧记录恢复时没有它,被视为尚欠交接——对从未有过交接的 Goal 而言这是正确的解读。

关联 Issue

QwenLM#9891 stops a Goal the moment its autonomous token budget is spent: the
continuation gate refuses the next turn and settles usage_limited. That
bounds runaway spend, but it cuts the model off mid-thought -- whatever
it had learned in the last window is stranded in the transcript, and
the user who resumes gets no hand-off.

A spent window now buys exactly one more continuation, flagged
`windDown`, whose prompt says the budget is spent, forbids new work,
and asks for a concise hand-off: what was accomplished with evidence
refs, what remains, the one concrete next step. When that turn
finishes, the gate stops the Goal as before. A hand-off turn that finds
the objective already met and proposes completion still completes the
Goal: the stop only ever refuses a continuation, never a verdict.

Exactly one per window, and persisted: the wind-down turn's own
turn_finished record stamps `GoalRecord.windDownTurnId`, so the gate
can tell "hand-off delivered" from "hand-off owed" across a restart
with no extra journal write and no new state cause. A hand-off the
host dropped undelivered leaves no marker and is minted again; a
restart that interrupted the hand-off turn grants it again for the
same reason -- the user never got one. Re-arming the budget on resume
or edit clears the marker, so each window owes its own.

The flag rides the host boundary like verifierFeedback, through all
three hosts, and the prompt block sits after the authoritative
objective line and above verifier feedback; the ordinary prompt is
byte-identical to before.

Mutation probes (goal-runtime + goal-reducer, 218 tests): finishTurn
never stamps the marker -> 3 fail; gate ignores the marker -> 4; gate
never grants -> 6; re-arm keeps the old marker -> 3; parse never
restores it -> 2. Each host hop deleted -> exactly one test fails in
that host's suite (useMessageQueue, useGeminiStream, nonInteractiveCli,
Session).
@qqqys

qqqys commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /takeover

@qwen-code-dev-bot qwen-code-dev-bot added the autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+) label Aug 26, 2026
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. This is a fork PR, so the first round comes from the next scheduled scan (usually within minutes). Remove the autofix/takeover label (or comment @qwen-code /takeover stop) to release.

中文说明

🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。本 PR 来自 fork,首轮处理将由下一次定时扫描执行(通常几分钟内)。移除 autofix/takeover 标签(或评论 @qwen-code /takeover stop)即可释放。

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finished — CI landed green on 4031cb4 and the deferred approval was posted. finalize run

Qwen Triage 已完成 —— 4031cb4 的 CI 全绿,延迟审批已提交。查看 finalize 运行

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

  • Template: looks good ✓ — all sections present, bilingual body complete.
  • Problem: this is not a field-reported bug but a direct consequence of the behavior feat(goal): stop autonomous continuation at a token budget the user re-arms #9891 shipped: the budget stop refuses the next continuation, so whatever the model learned in the final window stays stranded in the transcript and the returning user gets no hand-off. The dependency is linked and already merged, so the gap is observable in shipped design rather than hypothetical. Proceeding.
  • Direction: aligned. This extends the token-budget stop merged today (feat(goal): stop autonomous continuation at a token budget the user re-arms #9891) with a bounded checkpoint, consistent with the broader goal-lifecycle direction — the reference CHANGELOG has no exact match for a wind-down turn, but several recent /goal entries (check-ins while background work runs, goal restore on resume, graceful clears on unrecoverable errors) point the same way. Not auth/sandbox/model-selection/telemetry surface; no direction escalation needed.
  • Size: core paths are touched (packages/core/src/goals/** + cross-package host hops). Breakdown: 88 production lines vs 373 test lines (additions + deletions; no generated/schema files) — well under the 500/1000 thresholds, and the test-to-production ratio is the right way around.
  • Approach: the scope feels right. One persisted marker (windDownTurnId) stamped on the hand-off turn's own turn_finished journal record — no extra journal write, no new state cause; one prompt block; one windDown flag riding the existing verifierFeedback host path through the TUI, headless, and ACP hosts. The obvious cheaper alternative (synthesize a summary at the gate instead of granting a turn) can't produce a model-authored hand-off, which is the point. One genuine question for later review: the re-grant semantics after restarts and host-dropped turns — the description argues them carefully, and the tests will need to pin them.
  • Risk: Stage 1e matched packages/cli/src/acp-integration/ (Session.ts hop — only 2 lines of flag plumbing, but the path is in the revert-correlated set). Full review depth applies, and CI evidence is required before any approval.

Moving on to code review. 🔍

中文说明

感谢贡献!

  • **模板:**完整 ✓ —— 各节齐全,中英双语完整。
  • **问题:**这不是用户上报的 bug,而是 feat(goal): stop autonomous continuation at a token budget the user re-arms #9891 已上线行为的直接后果:预算闸门拒绝下一次续跑,模型在最后一个窗口学到的东西滞留在转录里,回来的用户拿不到任何交接。依赖的 PR 已关联且已合并,因此该缺口是已上线设计中可观察到的,而非假设。继续评审。
  • **方向:**对齐。本 PR 在当天刚合并的 token 预算停止(feat(goal): stop autonomous continuation at a token budget the user re-arms #9891)之上增加一个有界检查点,与 goal 生命周期的整体方向一致——参考 CHANGELOG 中没有与"收尾轮"完全对应的条目,但近期多条 /goal 相关变更(后台任务运行期间自动 check-in、resume 时恢复 goal、不可恢复错误时的优雅清除)方向相同。不涉及 auth/沙箱/模型选择/遥测面,无需方向升级。
  • **规模:**触及核心路径(packages/core/src/goals/** + 跨包 host 跳板)。拆分:生产代码 88 行测试 373 行(增 + 删;无生成/schema 文件)——远低于 500/1000 阈值,测试与生产代码的比例也是正确的方向。
  • **方案:**范围合理。一个持久化标记(windDownTurnId),盖在交接轮自己的 turn_finished 日志记录上——无额外日志写入、无新的状态 cause;一个提示块;一个沿现有 verifierFeedback host 路径穿过 TUI、headless、ACP 三个 host 的 windDown 标志。显而易见的廉价替代方案(在闸门处合成摘要而不是授予一轮)产生不了模型亲自写的交接,而那正是重点。一个留待代码评审的真问题:重启与 host 丢弃轮次之后的重新授予语义——描述里论证得很仔细,测试需要把它们钉死。
  • **风险:**Stage 1e 命中 packages/cli/src/acp-integration/(Session.ts 跳板——仅 2 行标志透传,但该路径在与 revert 相关的集合中)。适用完整审查深度,且任何批准前必须有 CI 证据。

进入代码审查。🔍

Qwen Code · qwen3.8-max

Reviewed at 4031cb4db082e748f44b077e0b32582c7bdccad3 · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Code review

I traced this end-to-end against the merged #9891 machinery, and the design holds up. My independent read of the problem would have landed on the same shape: persist the "hand-off delivered" fact on the wind-down turn's own turn_finished journal record instead of adding a new journal write or state cause, and ride the existing verifierFeedback plumbing for the flag. That's exactly what the PR does.

What I verified by reading the runtime, not just the diff:

  • No re-grant loop. finishTurn stamps the marker into the goal record, appends the journal, updates the live snapshot, and only then does the dispatch tail re-enter queueContinuation — so the gate sees its own stamp and settles usage_limited instead of minting another turn. All ten queueContinuation call sites funnel through that one central gate, so there's no path that bypasses the check.
  • Re-mint semantics are real, not just asserted. A host-refused start flows through handleStartFailure back into queueContinuation; bindHost and restore activation re-enter the same gate. Marker absent → the hand-off is owed → it is minted; marker present → stop. Both restart cases are pinned by tests.
  • The budget stop never overrules a verdict. If the wind-down turn proposes completion and the verifier accepts, the runtime takes the verifying path and the Goal completes with no usage_limited record — tested explicitly.
  • All three hosts are covered and there is no fourth. The TUI (AppContainer → useMessageQueue → useGeminiStream), headless (nonInteractiveCli), and ACP (Session) hosts are the only GoalTurnHost implementations, and each passes the whole queued turn object to buildGoalContinuationParts, so the flag flows structurally with no dropped hop. The ordinary continuation prompt is byte-identical — the two wind-down lines are pushed only when the flag is set.
  • Persistence is defensive: the parser accepts only a non-empty string marker and round-trips it; re-arming clears it; a resume that re-arms nothing keeps it (same window, hand-off already delivered).

No critical issues, no convention violations — the comments explain genuinely non-obvious ordering constraints, and the 4:1 test-to-production ratio is doing real work. One observation, non-blocking: the mutation-resistance numbers in the PR body are the author's own report and can't be re-run here, and the paused + marker-without-spend combination the reducer tests pin is reachable only through narrow interleavings — the pinned direction (keep the marker, never double-grant) is the safe one, so this is fine as-is.

How the flow works

sequenceDiagram
    participant P1 as Model turn
    participant P2 as Budget gate (queueContinuation)
    participant P3 as Host (TUI, headless, ACP)
    participant P4 as Reducer and journal
    P1->>P2: turn ends, budget spent
    alt no wind-down marker on the record
        P2->>P3: grant one continuation with windDown flag
        P3->>P1: hand-off prompt, no new work
        P1->>P4: turn_finished stamps windDownTurnId
        P4->>P2: gate re-checks, marker present
        P2->>P4: stopForSpentBudget settles usage_limited
    else marker already present
        P2->>P4: stopForSpentBudget settles usage_limited
    end
Loading
Files changed (15 of 15 shown)
File What changed
packages/core/src/goals/goal-runtime.ts In-memory wind-down permit id; gate grants exactly one flagged continuation before stopping; finishTurn stamps the record
packages/core/src/goals/goal-runtime.test.ts Three budget-stop tests walk through the hand-off; new cases for host-drop re-mint, completion precedence, restart with and without marker
packages/core/src/goals/goal-reducer.ts Transition stamps the marker; re-arm clears it; delete-on-undefined and parse validation for a non-empty string
packages/core/src/goals/goal-reducer.test.ts Stamping, re-arm clearing, non-re-arm survival, persistence round-trip and rejection of bad markers
packages/core/src/goals/goal-protocol.ts Optional documented windDownTurnId field on GoalRecord
packages/core/src/goals/goal-continuation-prompt.ts Two wind-down lines appended after the supersedes line, only when flagged
packages/core/src/goals/goal-continuation-prompt.test.ts Block appears only when flagged, stays above verifier feedback, ordinary prompt unchanged
packages/cli/src/ui/hooks/useMessageQueue.ts QueuedGoalTurn gains windDown, set only when flagged
packages/cli/src/ui/hooks/useMessageQueue.test.ts Queue preserves the flag through startGoalTurn
packages/cli/src/ui/hooks/useGeminiStream.ts Passes the queued flag into renderGoalContinuationPrompt
packages/cli/src/ui/hooks/useGeminiStream.test.tsx TUI renders the exact full prompt with wind-down lines above verifier feedback
packages/cli/src/nonInteractiveCli.ts HeadlessGoalTurn gains windDown, set only when flagged
packages/cli/src/nonInteractiveCli.test.ts Headless end-to-end: flag reaches the prompt sent to the model
packages/cli/src/acp-integration/session/Session.ts AcpGoalTurn gains windDown, set only when flagged
packages/cli/src/acp-integration/session/Session.test.ts ACP prompt carries the wind-down block in the expected position

Test evidence — the PR's own CI

CI on the reviewed commit at review time: the main Linux unit suite was still running (it takes ~30 minutes, no polling per process), preflight/secret/CVE checks passed, and the macOS/Windows/integration/tmux jobs were skipped on this fork PR. The finalize workflow updates the table below once CI settles.

Final CI results for 4031cb4 (auto-updated by the triage finalize job after CI completed):

Check Conclusion
Classify PR ✅ success
Dependency CVE audit ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
macos-latest / Java 21 ✅ success
Real daemon E2E / Java 11 ✅ success
route ✅ success
Secret scan (TruffleHog) ✅ success
Test (ubuntu-latest, Node 22.x) ✅ success
ubuntu-latest / Java 11 ✅ success
ubuntu-latest / Java 17 ✅ success
ubuntu-latest / Java 21 ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
windows-latest / Java 21 ✅ success

One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。

Not verified here: live behavior on any platform — CI evidence above is the only test signal this pass carries, and the author's mutation-probe numbers are their own report, not independently re-run (this process never executes PR code). The change itself is pure state-machine and prompt-string logic with no platform APIs, which limits what the skipped macOS/Windows jobs could have caught. Sandboxed verification would settle the rest: @qwen-code /verify (a sponsored run — the author has no write access) — that a spent budget mints exactly one wind-down continuation end-to-end and the hand-off block reaches the rendered prompt, proving the new tests are load-bearing against the base build. A sponsored run carries a pre-execution risk screen and a full workspace wipe; the maintainer should read its report with the same skepticism as the fork's own CI logs.

Real-scenario tmux testing: N/A on this unattended CI run (no local product driving; a maintainer can trigger the isolated @qwen-code /tmux job if the TUI surface needs live evidence).

中文说明

代码审查

对照已合并的 #9891 机制做了端到端走查,设计成立。我独立分析该问题得到的方案与之一致:把"交接已交付"这一事实持久化在收尾轮自己的 turn_finished 日志记录上,而不是新增日志写入或状态 cause;标志沿现有 verifierFeedback 通道透传。PR 正是这么做的。

通过阅读运行时代码(而非只看 diff)确认的关键点:

  • 不会重复授予。 finishTurn 先把标记盖入 goal 记录、追加日志、更新活动快照,分发尾部才重新进入 queueContinuation——闸门看到自己刚盖的标记,落为 usage_limited 而不是再铸一轮。全部十个 queueContinuation 调用点都汇入同一个中心闸门,没有绕过检查的路径。
  • 重铸语义是真实的。 host 拒绝启动经由 handleStartFailure 回到 queueContinuationbindHost 与恢复激活也重新进入同一闸门。标记缺失 → 交接尚欠 → 铸造;标记存在 → 停止。两种重启场景都有测试钉死。
  • 预算停止不推翻裁决。 收尾轮提出完成且 verifier 接受时,运行时走 verifying 路径,Goal 完成且不产生 usage_limited 记录——有专门测试。
  • 三个 host 全覆盖且没有第四个。 TUI(AppContainer → useMessageQueue → useGeminiStream)、headless(nonInteractiveCli)、ACP(Session)是 GoalTurnHost 仅有的三个实现,且都把完整排队对象传给 buildGoalContinuationParts,标志结构化流动,无漏板。普通续跑提示词逐字节不变——两行收尾文案仅在标志置位时追加。
  • 持久化有防御性:解析器只接受非空字符串标记并可往返;重新武装时清除;不重新武装的 resume 保留(同一窗口,交接已交付)。

无关键问题、无规范违规——注释解释的是真正不自明的顺序约束,4:1 的测试/生产比在做实事。一条非阻塞观察:PR 正文里的变异检验数字是作者自述,此处无法复跑;reducer 测试钉死的 paused + 有标记但预算未耗尽的组合只能通过很窄的交错到达——钉死的方向(保留标记、绝不重复授予)是安全方向,现状即可。

流程示意

时序图见英文正文(预算闸门 → 授予一次带 windDown 标志的续跑 → 交接轮结束盖章 → 闸门复查标记存在 → 落为 usage_limited;标记已存在时直接停止)。

测试证据

审查时点上的 CI:Linux 主单元测试套件仍在运行(约 30 分钟,按流程不轮询),预检/密钥扫描/依赖 CVE 检查通过,macOS/Windows/集成/tmux 作业在本 fork PR 上被跳过。CI 落定后 finalize 工作流会更新上方表格。此处未验证任何平台的实际运行行为——本 pass 只携带上述 CI 信号;作者的变异检验数字为其自述。改动本身是纯状态机与提示词字符串逻辑、不涉及平台 API,被跳过的平台作业能捕获的风险有限。沙箱验证可以补足剩余部分:@qwen-code /verify赞助运行——作者无写权限)可端到端证明预算耗尽时恰好铸造一次收尾续跑、交接块进入渲染出的提示词,即新测试相对 base 构建是承重墙。赞助运行带执行前风险筛查与完整工作区清理;维护者应以审视 fork CI 日志的同怀疑态度阅读其报告。真实场景 tmux 测试:本次无人值守 CI 运行为 N/A(维护者可触发隔离的 @qwen-code /tmux 作业获取 TUI 实机证据)。

Qwen Code · qwen3.8-max

Reviewed at 4031cb4db082e748f44b077e0b32582c7bdccad3 · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — clean, minimal, thoroughly pinned; the last point is withheld only for CI that had not settled and platform jobs this fork PR skips.

Stepping back: this is what a good increment on a living subsystem looks like. The problem is real — the budget stop #9891 shipped today cuts the model off with no hand-off, and anyone resuming has to reconstruct where things stand from a raw transcript. The fix is the smallest thing that works: one granted turn, one persisted marker, one prompt block, and a flag that rides plumbing already built for verifierFeedback. I independently proposed the same shape before reading the diff, and found no simpler path the PR missed — the obvious cheaper alternative (have the gate synthesize a summary itself) cannot produce a model-authored hand-off, which is the whole point.

The edge cases are where this kind of change usually rots, and they are the best part of the PR: restart mid-hand-off re-grants because the user never got one; a host-dropped start re-mints for the same reason; re-arming buys a fresh window with a fresh hand-off; and a completion verdict on the wind-down turn outranks the budget stop. Each is argued in the description and each is pinned by a test — the description and the code agree, which is rarer than it should be. 88 production lines carry 373 lines of tests, there are no drive-by edits, and every one of the ten gate entry points funnels through the single checked path.

Reservations, all non-blocking: the mutation-resistance numbers are the author's own report; the macOS/Windows/integration jobs skipped on this fork PR leave platform coverage to the Linux suite (limited risk for pure state-machine and prompt-string logic); and the paused-with-marker combination the reducer pins is reachable only through narrow interleavings, though the pinned direction is the safe one. None of these change what the code does today.

Verdict: approve. CI on the reviewed commit is still running, so approval is deferred until it lands green on 4031cb4db082e748f44b077e0b32582c7bdccad3 — the finalize workflow will pin it to that commit once every check completes, and withhold it if anything lands red or the head moves.

中文说明

置信度:4/5 —— 干净、最小化、钉得扎实;扣掉的一分只因为审查时 CI 尚未落定,且本 fork PR 的平台作业被跳过。

退一步看:这是一个活跃子系统上好的增量。问题是真实的——今天刚合并的 #9891 的预算停止会把模型直接切断、没有任何交接,回来 resume 的人只能从原始转录里重建现状。修复是可行的最小方案:授予一轮、一个持久化标记、一个提示块,标志沿已为 verifierFeedback 建好的通道透传。我在读 diff 之前独立得出的方案与之相同,也没有找到被 PR 错过的更简路径——显而易见的廉价替代(由闸门自己合成摘要)产生不了模型亲自写的交接,而那恰恰是重点。

这类改动通常在边界情况上腐烂,而边界情况恰是本 PR 最出色的部分:交接中途重启会重新授予(因为用户从未拿到);host 丢弃的启动同理重铸;重新武装购买新窗口、附带新的交接;收尾轮上的完成裁决优先于预算停止。每一条都在描述里论证过、又各被一个测试钉死——描述与代码一致,这比应有的更少见。88 行生产代码承载 373 行测试,没有顺手改动,十个闸门入口全部汇入唯一受检路径。

保留意见(均不阻塞):变异检验数字为作者自述;被跳过的平台作业使平台覆盖只剩 Linux 套件(对纯状态机与提示词字符串逻辑而言风险有限);reducer 钉死的 paused + 有标记组合只能经很窄的交错到达,但钉死的方向是安全方向。这些都不改变代码今天的实际行为。

结论:批准。审查提交上的 CI 仍在运行,因此批准推迟到其在该提交上落绿——finalize 工作流会在全部检查完成后把批准钉到该提交;若有检查落红或 HEAD 移动,则不会批准。

Qwen Code · qwen3.8-max

Reviewed at 4031cb4db082e748f44b077e0b32582c7bdccad3 · re-run with @qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship — CI landed green after the review. ✅

@yiliang114
yiliang114 added this pull request to the merge queue Aug 26, 2026
Merged via the queue into QwenLM:main with commit 5a88324 Aug 26, 2026
117 checks passed
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.22.2.

qqqys added a commit to qqqys/qwen-code that referenced this pull request Aug 28, 2026
…red (QwenLM#10260)

* fix(goal): stamp the wind-down hand-off only when its turn was delivered

QwenLM#10132 marked the record's `windDownTurnId` whenever the turn holding
the wind-down permit finished -- reading "the permit was used" as "the
user got the hand-off". QwenLM#10013 established why that inference is wrong
for the objective-updated notice: a system message or a direct user
query can claim a queued continuation's permit and send its own text
under it, so the turn finishes with the prompt never reaching the
model. Hosts therefore mark delivery at the real send site, and only a
delivered turn commits what it carried.

The hand-off now follows the same rule. `finishTurn` stamps the marker
only when the wind-down turn was marked delivered; an undelivered one
leaves the record clean, so the next `queueContinuation` grants the
hand-off again instead of settling `usage_limited` on a hand-off the
user never received (which a resume would not have repaired either,
since the marker is cleared only by a re-arm). The in-memory permit
marker is released either way; it belongs to the permit, not the
outcome.

The wind-down tests that finish the hand-off turn now mark it delivered
first, so they keep meaning "the model saw the hand-off". Two new cases
pin the split: finished-but-undelivered leaves no marker and re-mints
the hand-off; finished-and-delivered stamps it and stops.

Mutation probe: making the stamp unconditional again fails exactly the
undelivered case (145 others green).

* docs(goal): align wind-down comments with the delivered-stamp rule (QwenLM#10260)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants