Skip to content

feat(goal): carry budget figures and progress guidance in the continuation prompt - #11257

Merged
wenshao merged 13 commits into
QwenLM:mainfrom
qqqys:feat/goal-continuation-budget
Sep 8, 2026
Merged

feat(goal): carry budget figures and progress guidance in the continuation prompt#11257
wenshao merged 13 commits into
QwenLM:mainfrom
qqqys:feat/goal-continuation-budget

Conversation

@qqqys

@qqqys qqqys commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Every runtime-scheduled Goal turn now reports its token usage, limit, remainder, and completed-turn count when those figures are available. Non-wind-down turns carry four standing progress checks, except when supplied usage identifies turnCount === 0; that turn has no previous turn to judge, so it carries three.

The four lines ask it to treat the workspace and this turn's tool results as authoritative rather than what earlier turns reported, to work toward the end state the objective asks for rather than a narrower one it can reach sooner, to judge whether its previous turn actually changed anything before spending this one, and to check every explicit requirement against citable evidence before proposing that the Goal is done. The judgement line is held back on the Goal's very first turn, which create schedules before any turn has finished: there is no previous turn to judge there, and asking for the judgement invites the model to describe one.

Placement is deliberate. The figures sit after the standing objective guard and before the objective-updated notice. They stay outside the data block because that block holds explicitly untrusted task data, while the figures are trusted runtime context. The remainder is clamped at zero because the wind-down turn may begin after the window is overspent.

The progress lines are skipped on that wind-down hand-off, which is told not to start new work: a line asking for "a different concrete action now" would contradict it. The budget line is kept there, because a hand-off reports the numbers it stopped at.

usage remains optional on the public host contract. When it is absent, the Token budget: line is omitted and all four non-wind-down progress checks render because no turn count is available. The renderer and host queues share a GoalContinuationUsage projection from GoalRecord, and the host tests cover each queue copy.

Why it's needed

The model could not see how much of its spend window was left. A Goal stops when tokensUsed reaches tokenBudget, 30,000,000 by default, and gets one wind-down turn to hand off. Until that turn arrived there was no signal at all, so the model could not tell turn 3 of a long run from the turn before the budget stops it, and so could not choose between opening a broad investigation and finishing what it had in hand. get_goal can provide the same figures, but asking for them costs an extra tool call and does not make the budget visible at the start of every scheduled turn.

It was also never asked whether its last turn accomplished anything. The verifier only ever sees a terminal proposal, so a turn that proposes nothing is judged by nobody, and a turn spent restating status is exactly the turn that proposes nothing.

Codex's continuation template runs to 56 lines and covers both, with the same budget figures plus work-from-evidence, no-progress, fidelity and completion-audit sections. This is the equivalent for the parts that apply here. The blocked-audit section is deliberately not copied: this runtime already enforces that as a three-turn fingerprint check in code rather than as prompt text.

Design note in docs/design/2026-09-07-goal-continuation-budget.md.

Reviewer Test Plan

How to verify

cd packages/core && npx vitest run src/goals/goal-continuation-prompt.test.ts src/goals/goal-runtime.test.ts
 Test Files  2 passed (2)
      Tests  170 passed (170)

The prompt tests pin complete production-shaped ordinary and wind-down prompt strings. They also cover with-budget, no-budget, and overspent cases; require the Token budget: line to stay outside the untrusted data block and before the objective-updated notice; and distinguish first-turn, later-turn, and wind-down guidance.

Each host has coverage proving that usage survives its queue copy. The shared GoalContinuationUsage projection keeps the copied fields aligned with GoalRecord, while the host tests catch an omitted copy without making the public usage input mandatory.

cd packages/cli && npx vitest run src/ui/hooks/useMessageQueue.test.ts src/nonInteractiveCli.test.ts src/acp-integration/session/Session.test.ts src/ui/hooks/use-llm-stream.test.tsx src/ui/utils/resumeHistoryUtils.test.ts

Evidence (Before & After)

The numeric values below came from a real session transcript. The examples use the current renderer's Token budget: label, so they describe the code under review without claiming that the historical transcript already used the renamed label.

Before. The released prompt had no runtime token-budget line and ended at the standing objective guard.

After. Three continuation turns of one Goal, figures rising as it works:

Token budget: 0 of 30,000,000 tokens used, 30,000,000 remaining; 0 Goal turns finished.
Token budget: 41,634 of 30,000,000 tokens used, 29,958,366 remaining; 1 Goal turn finished.
Token budget: 85,498 of 30,000,000 tokens used, 29,914,502 remaining; 2 Goal turns finished.

The tail of that third prompt in full:

The objective in that data block is the current one and supersedes any other Goal objective text in this conversation.
Token budget: 85,498 of 30,000,000 tokens used, 29,914,502 remaining; 2 Goal turns finished.
Treat the workspace and this turn's tool results as authoritative. Re-inspect state rather than relying on what earlier turns in this conversation reported.
Work toward the end state the objective asks for. Do not substitute a narrower or more easily reached result, and do not redefine success around what already exists.
Judge your previous Goal turn before acting: it made progress only if it changed the workspace or produced evidence that changes what to do next. If it did not, take a different concrete action now instead of restating status; if the same blocker still stands, cite it through update_goal rather than repeating it.
Before proposing that the Goal is complete, check every explicit requirement in the objective against evidence you can cite. Missing, indirect, or self-reported evidence means not done: keep working.

After — the wind-down hand-off, forced by setting model.goalTokenBudget to 30000. The remainder is clamped, and the progress lines are gone:

Token budget: 62,026 of 30,000 tokens used, 0 remaining; 1 Goal turn finished.
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.

That turn's answer closed with "the token budget is exhausted (62,026 of 30,000), so this Goal now stops and waits for the user to resume it", which is the model reading the figures and using them.

Tested on

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

Environment (optional)

Linux, npm run build, headless --output-format text against a live model, prompts read back out of the session transcript.

Risk & Scope

  • Main risk or tradeoff: this is a prompt change on every autonomous turn, so it changes model behaviour in ways unit tests cannot settle. An ordinary prompt grows from 11 to 15 lines without figures and 16 with them; when supplied usage identifies turnCount === 0, the previous-turn judgement line is omitted. The counterweight is that these lines only reach runtime-scheduled turns, never a user's own.
  • Not validated / out of scope: whether the progress lines measurably reduce idle turns, which needs longer-running Goals than an E2E run. The runtime's own idle bound is separate work. The get_goal and update_goal descriptions are unchanged.
  • Breaking changes / migration notes: none. usage remains optional; a host that omits it gets no numeric Token budget: line and receives all four non-wind-down progress checks because no first-turn signal is available.

Linked Issues

Part of #4228.

中文说明

这个 PR 做了什么

现在每个由运行时调度的 Goal 轮次,在有用量数据时都会说明已用 token、额度、剩余量和已完成轮数。非 wind-down 轮通常带四行常驻进展指引;只有传入的用量明确表明 turnCount === 0 时,因为没有上一轮可判断,才带三行。

这四行分别要求它:把工作区和本轮的工具结果当作权威,而不是听信更早的轮次报告了什么;朝着目标要求的终态去做,而不是换一个更快够得着的更窄结果;在花掉这一轮之前先判断上一轮到底有没有改变什么;以及在提出 Goal 已完成之前,把目标里每一条明确要求都对着可引用的证据核一遍。判断那一行在 Goal 的第一轮上会被跳过——create 会在任何一轮结束之前就调度出这一轮,那里根本没有"上一轮"可判断,硬要它判断只会诱使它编一个出来。

位置是刻意的:数字排在常驻目标守卫之后、"目标已变更"通知之前;它位于数据块之外,因为数据块承载明确标记为不可信的任务数据,而数字属于可信的运行时上下文。剩余量夹到 0,因为 wind-down 轮开始时窗口可能已经超支。

那一轮的交接被告知不要开始新工作,所以四行指引在它上面被跳过:一句"现在换一个具体动作"会和它直接打架。预算行则保留,因为交接本来就要报出它停在哪个数上。

usage 在公开 host 契约上仍然可选;缺失时不渲染 Token budget: 数字行,并因为没有轮次信息而在非 wind-down 轮渲染全部四行进展指引。renderer 与各 host 队列共享由 GoalRecord 投影出的 GoalContinuationUsage 类型,host 测试覆盖每一次队列拷贝。

为什么需要

模型看不到自己的额度窗口还剩多少。Goal 在 tokensUsed 达到 tokenBudget(默认 30,000,000)时停下,并获得一轮 wind-down 用来交接。在那一轮到来之前完全没有信号,所以模型分不清自己是长跑中的第 3 轮,还是预算即将把它停掉的前一轮,也就无从在"展开一次大范围调查"和"把手上的事收尾"之间做选择。get_goal 可以提供这些数字,但那需要额外一次工具调用,也不能让每个调度轮次一开始就看到额度。

它也从来没有被要求判断上一轮到底完成了什么。verifier 只会看到终态提案,所以一个什么都不提案的轮次没有任何人来判定;而花在复述状态上的轮次,恰恰就是什么都不提案的那种轮次。

Codex 的续跑模板有 56 行,把这两件事都覆盖了:同样的预算数字,加上 work-from-evidence、no-progress、fidelity 和 completion-audit 几段。本 PR 做的是其中适用部分的等价物。blocked-audit 那段刻意没有照搬:本运行时已经把它作为三轮指纹检查在代码里执行了,而不是写成提示词。

设计说明在 docs/design/2026-09-07-goal-continuation-budget.md

评审验证方式

如何验证

cd packages/core && npx vitest run src/goals/goal-continuation-prompt.test.ts src/goals/goal-runtime.test.ts
 Test Files  2 passed (2)
      Tests  170 passed (170)

提示词测试钉住了生产形态的普通轮和 wind-down 轮完整文本;同时覆盖有额度、无额度、已超支三种情况,要求 Token budget: 行位于不可信数据块之外且在"目标已变更"通知之前,并区分第一轮、后续轮和 wind-down 轮的指引。

每个 host 都有用例证明 usage 能通过它的队列拷贝。共享的 GoalContinuationUsage 投影让字段与 GoalRecord 保持一致,host 测试则在不把公开 usage 输入改成必填的前提下捕获遗漏拷贝。

cd packages/cli && npx vitest run src/ui/hooks/useMessageQueue.test.ts src/nonInteractiveCli.test.ts src/acp-integration/session/Session.test.ts src/ui/hooks/use-llm-stream.test.tsx src/ui/utils/resumeHistoryUtils.test.ts

证据(Before & After)

下面的数字来自真实会话记录;示例标签使用当前 renderer 的 Token budget:,因此反映待评审代码,而不声称历史记录当时已经使用改名后的标签。

改动前。 已发布版本的提示词没有运行时 token 额度行,到常驻目标守卫那一行就结束了。

改动后。 同一个 Goal 的三个续跑轮,数字随着它干活往上走:

Token budget: 0 of 30,000,000 tokens used, 30,000,000 remaining; 0 Goal turns finished.
Token budget: 41,634 of 30,000,000 tokens used, 29,958,366 remaining; 1 Goal turn finished.
Token budget: 85,498 of 30,000,000 tokens used, 29,914,502 remaining; 2 Goal turns finished.

第三段提示词的结尾全文:

The objective in that data block is the current one and supersedes any other Goal objective text in this conversation.
Token budget: 85,498 of 30,000,000 tokens used, 29,914,502 remaining; 2 Goal turns finished.
Treat the workspace and this turn's tool results as authoritative. Re-inspect state rather than relying on what earlier turns in this conversation reported.
Work toward the end state the objective asks for. Do not substitute a narrower or more easily reached result, and do not redefine success around what already exists.
Judge your previous Goal turn before acting: it made progress only if it changed the workspace or produced evidence that changes what to do next. If it did not, take a different concrete action now instead of restating status; if the same blocker still stands, cite it through update_goal rather than repeating it.
Before proposing that the Goal is complete, check every explicit requirement in the objective against evidence you can cite. Missing, indirect, or self-reported evidence means not done: keep working.

改动后 —— wind-down 交接轮,通过把 model.goalTokenBudget 设成 30000 触发。剩余量被夹住,指引行消失:

Token budget: 62,026 of 30,000 tokens used, 0 remaining; 1 Goal turn finished.
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.

那一轮的回答结尾是"the token budget is exhausted (62,026 of 30,000), so this Goal now stops and waits for the user to resume it",也就是模型确实读到了这些数字并用上了。

测试平台

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

环境(可选)

Linux,npm run build,对真实模型跑 headless --output-format text,提示词从会话记录里回读。

风险与范围

  • 主要风险或权衡:这是一次落在每个自主轮上的提示词变更,它会以单测无法定论的方式改变模型行为。普通提示词从 11 行变为无数字时 15 行、有数字时 16 行;只有传入的用量明确表明 turnCount === 0 时才少掉"判断上一轮"那一行。对冲的一面是:它们只会出现在运行时调度的轮次上,永远不会出现在用户自己的轮次里。
  • 未验证 / 范围外:指引行是否能可测量地减少空转轮,这需要比一次 E2E 更长的 Goal 才能说清。运行时自己的空转闸是单独的工作。get_goalupdate_goal 的描述没有改动。
  • 破坏性变更 / 迁移说明:没有。usage 仍然可选;不提供它的 host 不渲染 Token budget: 数字行,并因为没有首轮信号而在非 wind-down 轮收到全部四行进展指引。

关联 Issue

属于 #4228 的一部分。

…ation prompt

The continuation prompt told the model what the objective was and how to
deliver it, and nothing about two things it had no other cheap way to know.

It could not see how much of the spend window was left. A Goal stops when
`tokensUsed` reaches `tokenBudget`, 30,000,000 by default, and gets one
wind-down turn to hand off; until that turn arrives there was no signal at
all, so the model could not tell turn 3 of a long run from the turn before the
budget stops it, and could not choose between opening a broad investigation
and finishing what it had. `get_goal` does not carry the figures either, so
there was not even an expensive way to ask.

It was also never asked whether its last turn accomplished anything. The
verifier only ever sees a terminal proposal, so a turn that proposes nothing is
judged by nobody -- and a turn spent restating status is exactly the turn that
proposes nothing.

Each runtime-scheduled turn now opens with what has been spent, out of what,
what remains, and how many turns are behind it, followed by four standing
lines: treat the workspace rather than the conversation as authoritative; work
toward the end state the objective asks for rather than a more easily reached
one; judge whether the previous turn actually changed anything before spending
this one; and check every requirement against citable evidence before
proposing completion.

The figures sit after the standing objective guard and before the
objective-updated notice, which is about what changed since the last turn and
so reads last. They stay out of the data block on purpose: that block is
compared by content to decide whether the objective changed, and a number that
moves every turn would make every turn look like an edit. The remainder is
clamped at zero, since the hand-off turn runs with the window already
overspent.

The progress lines are skipped on that hand-off turn, which is told not to
start new work -- a line asking for "a different concrete action now" would
contradict it. The budget line stays, because a hand-off reports the numbers it
stopped at.

`usage` is optional on the host contract, so a host with no figures renders
exactly the prompt it did before. The three hosts copy it through their queue
entries alongside the fields they already copy.
@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finishedview run. See the stage comments in this thread for the result.

Qwen Triage 已完成 —— 查看运行。结果见本线程中的各阶段评论。

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Thanks — re-run at the current head. Only two Merge upstream/main commits landed since the last full review (0bac4bbf), so the substantive diff is unchanged; what did change is CI, which is now green end to end. That matters because the previous pass withheld approval over two red lanes it classified as main's, and this re-run is the one that clears them.

Template looks good ✓ — every required heading is there, including a real test plan and before/after evidence.

Problem: a capability gap, not theoretical hardening. A Goal stops when tokensUsed reaches tokenBudget and gets one wind-down turn, but until that turn the model had no signal at all about where it stood in the window — and nothing ever asked it whether its previous turn changed anything, because the verifier only sees terminal proposals. Both halves are structural facts about the runtime, and I confirmed them in the code rather than taking the description's word: flushContinuation held no figures, and the only no-progress judgement on main is the mechanical noProgressTurns counter, which detects idling after the fact instead of preventing it. The transcript figures in the description are the author's own local run against a live model — I can't re-run that here (no credentials in this environment), so I'm treating them as the author's evidence, not mine.

Direction: aligned. This sharpens an existing first-party feature rather than adding surface, and the two halves pull in the same direction: the figures give the model something to reason with, the progress lines give it a reason. CHANGELOG signal is supportive but not a direct match — claude-code's /goal surfaces live elapsed/turns/tokens to the user as an overlay, not to the model in a continuation prompt, and its Stop hook feeds back a refusal reason rather than standing guidance. The design note's own read of that ("Claude Code has no equivalent") is accurate, and it cites Codex's continuation template as the closer precedent.

Size: core paths are touched (packages/core/src/goals/**) and the change spans two packages, so the two-tier gate applies. 259 production logic lines, 376 test lines, 118 doc lines (753 total). Well under the 500-line escalation threshold and under the 1000-line advisory. The author has write access, so this is maintainer-authored and exempt from the gate either way — no escalation.

Approach: the scope feels right, and it's smaller than it looks. All the new behaviour lands in the one renderer every host already goes through, usage stays optional on the public contract, and nothing speculative was added. The refactor(goal): centralize continuation payload commit is the part I'd single out: it deleted four hand-maintained copies of the same conditional-spread block (that's most of the 108 deletions) and made the host turn types extend a shared GoalContinuationTurn. Before that, a host silently dropping an optional hint field typechecked fine and cost one host its budget line — which is exactly the finding shape this PR had already been through three rounds of. Now it's a type error. I'd have kept the four copies and leaned on tests; this is better than what I'd have written.

Risk: Stage 1e matched one high-risk path — packages/cli/src/acp-integration/session/Session.ts (acp-integration). Not a block, but it sets review depth: no Stage 2 enrichment skipped, CI evidence required before approving, and a named sandboxed lane in Stage 2. Worth knowing that the ACP change here is the smallest of the four hosts (6 added / 15 deleted) and is purely the queue-entry spread, so the path is flagged by association rather than by depth of change.

One thing I checked specifically because it's the claim the whole risk section rests on: these lines reach only runtime-scheduled turns, never a user's own. That holds. use-llm-stream renders them only under submitType === SendMessageType.Goal, a user's own text goes through prepareQueryForLlm on a different submit type, and in the ACP session buildGoalContinuationParts is reached only from #drainGoalQueueExclusive, which shifts the runtime queue — the user-origin turn built at Session.ts:4799 carries a continuationContext but is never queued, so it never renders.

Moving on to code review. 🔍

中文说明

感谢贡献 —— 本次是在当前 head 上的重跑。自上次完整评审(0bac4bbf)以来只落了两个 Merge upstream/main 提交,所以实质性 diff 没有变化;变化的是 CI,现在全链路都是绿的。这一点很关键:上一轮因为两条被判定为 main 造成的红通道而没有批准,而这次重跑正是清掉它们的那一次。

模板完整 ✓ —— 所有必需小节都在,包括真实的评审验证方式和 before/after 证据。

问题: 是能力缺口,不是理论性加固。Goal 在 tokensUsed 达到 tokenBudget 时停下并获得一轮 wind-down,但在那一轮到来之前模型完全不知道自己在窗口里的位置;而且也从来没有人问过它上一轮到底改变了什么,因为 verifier 只看终态提案。这两半都是运行时的结构性事实,我是自己在代码里确认的、不是采信描述:flushContinuation 里没有这些数字,而 main 上唯一的空转判断是机械的 noProgressTurns 计数器——它是事后检测空转,而不是预防空转。描述里的会话记录数字来自作者本地对真实模型的运行,我在这里无法复跑(环境无凭据),所以我把它当作作者的证据、不是我的。

方向: 对齐。这是打磨一个已有的一方特性,而不是增加表面积,而且两半是同一个方向:数字给模型可推理的材料,进展指引给它推理的理由。CHANGELOG 信号是支持性的、但不是直接对应——claude-code 的 /goal 是把实时的 elapsed/turns/tokens 以浮层展示给用户,而不是在续跑提示词里给模型;它的 Stop hook 反馈的是拒绝理由,而不是常驻指引。设计文档对此的判断("Claude Code 没有等价物")是准确的,它引用 Codex 的续跑模板作为更接近的先例。

规模: 触及了核心路径(packages/core/src/goals/**),且跨两个 package,所以两级门适用。生产逻辑 259 行、测试 376 行、文档 118 行(合计 753)。远低于 500 行的升级阈值,也低于 1000 行的大 PR 建议线。作者有 write 权限,因此属于维护者自己提交的 PR,本来就豁免于该门 —— 无需升级。

方案: 范围合理,而且比看起来更小。所有新行为都落在每个 host 本来就会经过的那一个渲染器里,usage 在公开契约上保持可选,没有加任何投机性的东西。refactor(goal): centralize continuation payload 那个提交是我要单独点出的:它删掉了四份手工维护的、同样的条件展开代码块(108 行删除大部分来自这里),并让各 host 的轮次类型继承共享的 GoalContinuationTurn。在那之前,某个 host 静默丢掉一个可选提示字段是能通过类型检查的,代价是这一个 host 少了预算行 —— 而这正是本 PR 已经经历过三轮的那种发现形态。现在它是一个类型错误。我本来会保留那四份拷贝、依赖测试;这个做法比我写的更好。

风险: Stage 1e 命中一条高风险路径 —— packages/cli/src/acp-integration/session/Session.tsacp-integration)。这不是阻塞项,但它决定了评审深度:Stage 2 的补充内容一项都不跳过、批准前必须有 CI 证据、并在 Stage 2 点名一条沙箱通道。需要知道的是,这里的 ACP 改动是四个 host 中最小的(新增 6 / 删除 15),而且纯粹是队列条目的展开,所以这条路径是被关联性标出的,不是因为改动深度。

有一件事我专门查了,因为整个风险小节都建立在它上面:这些行只会到达运行时调度的轮次,永远不会到达用户自己的轮次。这一点成立。use-llm-stream 只在 submitType === SendMessageType.Goal 下渲染它们,用户自己的文本走的是另一种 submit type 上的 prepareQueryForLlm;而在 ACP session 里,buildGoalContinuationParts 只能从 #drainGoalQueueExclusive 到达,它 shift 的是运行时队列 —— Session.ts:4799 构造的用户来源轮次虽然带着 continuationContext,但从不入队,因此从不渲染。

进入代码审查 🔍

Qwen Code · qwen3.8-max-2026-09-02

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

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Code review

I wrote my own proposal down before opening the diff: put the figures in the single renderer every host already goes through, read them off the record at scheduling time, keep the field optional so an embedder with no figures degrades to today's prompt, clamp the remainder, hold the progress lines back on the hand-off, and pin the whole thing with complete-string tests. That is what this PR does. I did not find a cheaper route afterwards.

The one place it beats my proposal is the centralization I would not have written. I'd have kept the four per-host copies and leaned on the four tests that pin them — which is precisely the shape that produced three earlier rounds of "a host silently drops the optional field". Making the host turn types extend a shared GoalContinuationTurn and replacing each copy block with const { permit, ...continuation } = input turns that mistake into a type error, so the next person who adds a hint field cannot repeat it. It also accounts for most of the 108 deletions, so the PR is net smaller in the places it touches most.

No critical blockers and no AGENTS.md violations. What follows is what I went and checked because each one could have been a real bug and none is.

  • turnCount semantics carry the wording. "N Goal turns finished" is only honest if the counter means finished turns. It does — reduceGoalTurnFinished does turnCount: current.turnCount + 1 — and that makes turnCount === 0 exactly the first scheduled continuation, which is the one case where the judge-your-previous-turn line is withheld. The runtime tests pin both ends (0 on the first schedule, 1 after a turn bills), and it matches the transcript in the description going 0 → 1 → 2.
  • The figures cannot go stale between scheduling and rendering. This was my main suspicion, since the hosts queue the turn and drain it later. flushContinuation returns early unless !currentPermit and activity === 'idle', then takes the permit before calling startGoalTurn; tokensUsed and turnCount only move when a Goal turn finishes, and the permit is held across that. So the numbers read at scheduling are still the numbers when the prompt renders. The comment at the read site says as much, and it's right.
  • The trust boundary survives. The budget line is pushed after AUTHORITATIVE_OBJECTIVE_LINE, outside <goal_runtime_data>, and an ordering test pins it (budget > dataClose, notice > budget). renderBudgetLine interpolates numbers only, through toLocaleString, so it yields digits and commas — there is no path for hostile text to ride in on a field the runtime owns. Pinning 'en-US' explicitly is the right call too: the rendered prompt is then identical on any host locale, which the exact-string tests depend on. It also matches goal-protocol.ts:55, which already spells token budgets this way in this same subsystem.
  • The host census is complete. Three production GoalTurnHost implementations exist — nonInteractiveCli.ts:652, Session.ts:2298, AppContainer.tsx:2626. The first two are updated directly; the third forwards input unchanged into enqueueGoalTurn, so it inherits the fix from useMessageQueue.ts without needing one. Both ACP render paths are covered: #drainGoalQueueExclusive calls buildGoalContinuationParts(turn) on an AcpGoalTurn that now extends GoalContinuationTurn, so usage flows without a new copy.
  • usage is not a dead switch. Optional on the contract, read by the renderer, and set unconditionally by the only production caller. The optional-ness buys something real — an embedder with no runtime figures gets today's prompt plus the progress lines — and the no-usage case is tested rather than assumed.
  • The new guidance agrees with the enforcement that already landed. Main now counts idle continuations mechanically (noProgressTurns, pausing at GOAL_NO_PROGRESS_TURN_LIMIT), and the definitions line up: the runtime scores progress as an evidence-bearing tool result or a terminal proposal, the prompt asks for "changed the workspace or produced evidence that changes what to do next". Both exempt the wind-down turn. So the prompt is prevention and the counter is detection of the same event, not two competing notions of progress — and the counter is the metric that will eventually say whether the prevention works.

Two small things, neither a gate. renderGoalContinuationTurn(queuedGoal) spreads kind and turnKey through ...hints into renderGoalContinuationPrompt; harmless (TypeScript doesn't excess-property-check a spread, the renderer reads only fields it knows, and neither name collides with the four hints), just looser than it needs to be. And the four host tests justify themselves with "a dropped copy typechecks" — true when they were written, no longer true now that the spread makes dropping one a type error. The tests are still worth exactly what they cost, because they assert the rendered line on the model-call mock rather than on an intermediate object; only the comments overstate the risk.

sequenceDiagram
    participant P1 as goal-runtime flushContinuation
    participant P2 as host startGoalTurn
    participant P3 as host queue entry
    participant P4 as render site
    participant P5 as model call
    P1->>P1: read tokensUsed, tokenBudget, turnCount off the record
    P1->>P1: take the permit, then broadcast
    P1->>P2: permit, continuationContext, usage, hints
    P2->>P3: permit plus rest-spread continuation
    P3->>P4: claimed turn
    P4->>P4: renderGoalContinuationTurn
    P4->>P5: budget line plus progress lines, outside the data block
Loading
Files changed (all 15 shown)
File What changed
docs/design/2026-09-07-goal-continuation-budget.md New design note: the two gaps, the placement and trust-boundary reasoning, the two exceptions, scope and verification
docs/users/features/goals.md One user-facing paragraph on what each self-scheduled turn now reports and instructs
packages/core/src/goals/goal-continuation-prompt.ts The substance: shared usage projection, the four hint lines, budget rendering, placement, and the new turn-level render entry point
packages/core/src/goals/goal-continuation-prompt.test.ts Complete-string pins for ordinary and wind-down shapes, plus budget, no-budget, overspent, ordering, first-turn and no-usage cases
packages/core/src/goals/goal-runtime.ts Host contract narrowed to the shared turn type, and figures read off the record at scheduling time
packages/core/src/goals/goal-runtime.test.ts Three cases: figures at schedule time and after a turn bills, no ceiling, and the wind-down hand-off
packages/core/src/goals/index.ts Exports the new render function and the two new types
packages/cli/src/ui/hooks/useMessageQueue.ts Queued turn type extends the shared turn type, copy block becomes a rest-spread
packages/cli/src/ui/hooks/useMessageQueue.test.ts Pins that figures survive the queue hop
packages/cli/src/ui/hooks/use-llm-stream.ts Render site switches to the turn-level function, so it can no longer omit a field
packages/cli/src/ui/hooks/use-llm-stream.test.tsx Pins the rendered budget line on the stream call
packages/cli/src/acp-integration/session/Session.ts Same shape as the queue hook: type inheritance plus rest-spread
packages/cli/src/acp-integration/session/Session.test.ts Pins the rendered line inside the message actually sent to the model
packages/cli/src/nonInteractiveCli.ts Same shape as the other two hosts
packages/cli/src/nonInteractiveCli.test.ts Pins the rendered line on the headless stream call

Testing

This is an unattended CI run, so I did not build or execute anything from this PR — the evidence below is the PR's own CI, read through the API for the reviewed commit. All 68 check-runs on c518b774 are completed, with zero failures: every substantive lane is success and the rest are skipped by matrix or by the bot-orchestration jobs' own conditions. Nothing is pending.

The two lanes that were red at the previously reviewed head are the ones to look at, and both are green here:

  • Test (ubuntu-latest, Node 22.x)success. At 0bac4bbf this went red on a single unhandled rejection in src/extension/extension-store.ts, a file this diff does not touch.
  • web-shell E2E Smoke (ubuntu-latest, Node 22.x)success. At 0bac4bbf this was a deterministic scroll-anchoring drift in packages/web-shell, which this diff does not touch at all.

Both were classified last round as inherited from main rather than caused by this PR, and the re-merge of a fixed main is exactly what would clear them. That is what happened, so the classification is now confirmed by outcome rather than resting on my reasoning about the diff. Test (macos-latest) and Test (windows-latest) are skipped by the repo's matrix, consistent with the description's Tested-on table marking only Linux ✅.

Check Conclusion
Test (ubuntu-latest, Node 22.x) success
Lint & Static (ubuntu-latest, Node 22.x) success
Integration Tests (no-AK, No Sandbox) success
Integration Tests (CLI, No Sandbox) skipped
web-shell E2E Smoke (ubuntu-latest, Node 22.x) success
Desktop Shell (ubuntu-22.04) success
Desktop Shell (windows-2022) success
TUI parity snapshots (ink vs opentui) success
OpenTUI no-flicker gate success
Real daemon E2E / Java 11 success
SDK Java (ubuntu 11 / 17 / 21, macOS 21, windows 21) success
Test (macos-latest, Node 22.x) skipped
Test (windows-latest, Node 22.x) skipped
Classify PR · assign · label · route · authorize · precheck-pr success
review-pr · delay-automatic-review · Remind on force-push success

Not verified: whether a live model reads these figures and idles less. The transcript output in the description is the author's own local run against a real model; I cannot reproduce it here, and no pre-merge lane in this repo can — the sandbox is credential-free by design, so it never reaches a model. The description carries this honestly under Not validated / out of scope, and I'd keep it there rather than let anyone read the green suite as settling it. A green suite proves the prompt text is assembled and sent, not that the model acts on it.

Sandboxed verification would settle the other half, the one that is actually checkable: @qwen-code /verify — that these pins are load-bearing rather than decorative. By inspection they should be, since the renderer tests compare complete prompt strings with toBe and each host test asserts the exact rendered budget line on its own model-call mock, so deleting a host's spread or the render-site read should fail that host alone and nothing else. But that is my reading of the tests, not an observed A/B: /verify would run the suite with and without the diff and show the count. The author has write access, so /tmux is available too — though it would prove nothing here, since the continuation prompt is never rendered into the TUI.

中文说明

代码审查

我在打开 diff 之前先写下了自己的方案:把数字放进每个 host 本来就会经过的那一个渲染器;在调度时从记录里读出来;字段保持可选,让没有数字的嵌入方退化到今天的提示词;剩余量夹住;进展指引在交接轮上跳过;并用完整字符串测试钉住。这个 PR 做的正是这件事。之后我没有找到更省的路。

它超出我方案的唯一一处,正是我不会写的那个集中化。我本来会保留四份 per-host 拷贝、依赖钉住它们的四个用例 —— 而那恰恰是产生了前三轮"某个 host 静默丢掉可选字段"的形态。让各 host 的轮次类型继承共享的 GoalContinuationTurn、并把每个拷贝块换成 const { permit, ...continuation } = input,把这个错误变成类型错误,所以下一个要加提示字段的人无法重犯。它也解释了 108 行删除的大部分,所以这个 PR 在它改动最多的地方反而净变小了。

没有关键阻塞项,也没有违反 AGENTS.md 的地方。 下面是我专门去查的东西,因为每一项本来都可能是真 bug,而结果都不是。

  • turnCount 的语义撑得起这个措辞。 "N Goal turns finished" 只有在计数器确实表示已完成轮次时才诚实。它确实是 —— reduceGoalTurnFinished 里是 turnCount: current.turnCount + 1 —— 而这使 turnCount === 0 恰好就是第一个被调度的续跑轮,也就是唯一那个 withheld 掉"判断上一轮"的情况。运行时测试钉住了两端(首次调度时为 0,一轮计费后为 1),也和描述里 0 → 1 → 2 的会话记录一致。
  • 数字不会在调度与渲染之间变旧。 这是我主要的怀疑点,因为 host 会先入队、稍后才 drain。flushContinuation!currentPermitactivity === 'idle' 之外直接返回,然后在调用 startGoalTurn 之前就取得 permit;而 tokensUsedturnCount 只在 Goal 轮次结束时才变化,那段时间 permit 是被持有的。所以调度时读到的数字,在提示词渲染时仍然是那些数字。读取点上的注释也是这么说的,而且它是对的。
  • 信任边界保住了。 预算行是在 AUTHORITATIVE_OBJECTIVE_LINE 之后 push 的,位于 <goal_runtime_data> 之外,并且有一个顺序测试钉住它(budget > dataClosenotice > budget)。renderBudgetLine 只通过 toLocaleString 插入数字,所以产出的是数字和逗号 —— 没有任何路径能让敌意文本搭在一个由运行时拥有的字段上进来。显式钉住 'en-US' 也是对的:这样渲染出的提示词在任何 host locale 上都相同,而那些精确字符串测试正依赖这一点。它也和 goal-protocol.ts:55 一致,后者在同一个子系统里本来就是这样拼写 token 额度的。
  • host 清点是完整的。 生产代码里有三个 GoalTurnHost 实现 —— nonInteractiveCli.ts:652Session.ts:2298AppContainer.tsx:2626。前两个直接改了;第三个把 input 原样转发给 enqueueGoalTurn,所以它从 useMessageQueue.ts 继承到了修复、自己不需要改。ACP 的两条渲染路径都覆盖了:#drainGoalQueueExclusive 对一个现在继承 GoalContinuationTurnAcpGoalTurn 调用 buildGoalContinuationParts(turn),所以 usage 无需新的拷贝就会流过去。
  • usage 不是一个死开关。 契约上可选、被渲染器读取、并且被唯一的生产调用方无条件设置。可选性买到了真实的东西 —— 没有运行时数字的嵌入方会得到今天的提示词加上进展指引 —— 而且无 usage 的情况是被测试过的,不是被假定的。
  • 新指引与已经落地的强制机制是一致的。 main 上现在会机械地统计空转续跑(noProgressTurns,在 GOAL_NO_PROGRESS_TURN_LIMIT 处暂停),而两边的定义对得上:运行时把进展判定为"有承载证据的工具结果或一个终态提案",提示词要求的是"改变了工作区、或产生了会改变下一步的证据"。两边都豁免 wind-down 轮。所以提示词是预防、计数器是对同一事件的检测,而不是两种互相竞争的进展定义 —— 而计数器正是最终能说明这个预防有没有用的指标。

两件小事,都不是门。renderGoalContinuationTurn(queuedGoal) 会把 kindturnKey 经由 ...hints 展开进 renderGoalContinuationPrompt;无害(TypeScript 不对展开做多余属性检查,渲染器只读它认识的字段,两个名字也不与四个提示字段相撞),只是比必要的松一点。另外四个 host 测试用"丢掉一份拷贝也能通过类型检查"来为自己辩护 —— 在它们写下时是真的,而现在展开已使丢掉一份变成类型错误,就不再是真的了。这些测试仍然完全值回它们的成本,因为它们断言的是模型调用 mock 上渲染出的那一行、而不是某个中间对象;只有注释高估了风险。

测试

这是一次无人值守的 CI 运行,所以我没有构建或执行本 PR 的任何东西 —— 下面的证据是 PR 自己的 CI,通过 API 针对被评审的 commit 读取。c518b774 上全部 68 个 check-run 都是 completed零失败:每一条实质通道都是 success,其余是矩阵或机器人编排任务自身条件导致的 skipped。没有 pending。

要看的是在上一个被评审 head 上变红的那两条通道,它们在这里都是绿的:

  • Test (ubuntu-latest, Node 22.x) —— success。在 0bac4bbf 上它因为 src/extension/extension-store.ts 里一个未处理的 rejection 变红,而那个文件本 diff 完全没碰。
  • web-shell E2E Smoke (ubuntu-latest, Node 22.x) —— success。在 0bac4bbf 上是 packages/web-shell 里一个确定性的滚动锚定漂移,而那个包本 diff 根本没碰。

上一轮把两者都判定为继承自 main、而非本 PR 造成,而重新合入一份修好的 main 正是会清掉它们的东西。事实就是这样发生的,所以这个判定现在是被结果确认的,而不是只依靠我对 diff 的推理。Test (macos-latest)Test (windows-latest) 按仓库矩阵是 skipped,与描述里 Tested-on 表只把 Linux 标 ✅ 一致。

未验证:真实模型是否会读这些数字并因此少空转。 描述里的会话记录输出来自作者本地对真实模型的运行;我无法在这里复现,而本仓库没有任何合并前通道能做到 —— 沙箱按设计无凭据,因此它根本到不了模型。描述在 Not validated / out of scope 里诚实地承载了这一点,我也希望它留在那里,而不是让任何人把绿灯套件读成已经把这件事定论了。绿灯套件证明的是提示词文本被组装并发送,不是模型照它行动。

另一半 —— 真正可查的那一半 —— 可以由沙箱验证来定论:@qwen-code /verify,用来证明这些钉子是承重的、而不是装饰性的。从检查看它们应该是:渲染器测试用 toBe 比较完整提示词字符串,每个 host 测试在自己的模型调用 mock 上断言精确渲染出的预算行,所以删掉某个 host 的展开、或删掉渲染点的读取,应当只让那一个 host 失败、不影响其他。但这是我对测试的解读,不是一次观测到的 A/B:/verify 会在带 diff 与不带 diff 两种情况下跑套件并给出计数。作者有 write 权限,所以 /tmux 也可用 —— 不过它在这里证明不了什么,因为续跑提示词从来不会渲染进 TUI。

Qwen Code · qwen3.8-max-2026-09-02

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

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — clean across every stage and I'd merge it; not 5/5 because two of the PR's own accounts of itself lag the code it ships, and because the effect half of the claim is unmeasured and no lane here can measure it.

Stepping back. My independent proposal and this PR's design are the same design, which is the strongest thing I can say about the budget half — I wrote mine down before opening the diff and did not find a cheaper route afterwards. Where it beats mine is the centralization nobody asked for. I would have kept the four per-host field copies and leaned on the four tests that pin them, which is exactly the shape that produced three earlier rounds of "a host silently drops the optional field". Making the shared turn type carry the hints turns that mistake into a type error instead of a missing prompt line on one host, and it does that while deleting more code than it adds in the hosts it touches. In six months I would thank whoever wrote that commit rather than curse them, which is the question I actually care about here.

The pushover check, since this PR has been through roughly five bot review rounds and I'm now recommending it. I didn't inherit the previous pass's conclusions — it reviewed a different commit, and its approval was withheld for a reason that no longer exists. What it withheld on was two red CI lanes it classified as main's, on the reasoning that neither touched a file in this diff. At this head both are green, and the re-merge of a fixed main is the specific thing that would clear them. So the classification is now confirmed by outcome rather than resting on an argument about the diff, which is a strictly better evidentiary position than the one that blocked last round. The reason to defer is gone, and continuing to defer after the specific reason disappears is noise, not caution.

What I verified myself at this commit rather than taking on trust: that turnCount really counts finished turns, so the wording and the first-turn exception are both honest; that the figures cannot go stale between scheduling and rendering, because the permit is taken before the host is called and those fields only move when a turn finishes; that the budget line stays outside the untrusted data block and can only ever interpolate digits and commas; that the host census is complete at three production implementations, with the third covered by forwarding rather than by an edit; and that the claim the whole risk section rests on — these lines never reach a user's own turn — holds at all three render sites. The last one I checked specifically because it's the kind of claim that reads as obviously true and often isn't.

What's left is genuinely non-blocking, and I want to be precise about which kind. The design note still describes the hosts as copying usage "alongside the fields they already copy" and its scope section still says "one field copied through each host's queue entry" — that predates the centralization commit, and the note never mentions the shared turn type or the turn-level render function that are now the actual mechanism. Separately, both the note and the description call the runtime's idle bound "separate work"; it has landed on main and is in this branch, which is worth one line partly because that counter is the metric that will answer the open question objectively. Neither changes what the code does, and neither would surprise a future reader into a bug — they're documentation debt on a PR whose deliverable is wording.

Per the repo's own guidance on not letting review rounds balloon a PR, this one is at the point where only critical fixes should land and suggestions get deferred. There are no critical fixes. So I'm recording the two documentation items here as deferred to a follow-up commit rather than asking for another push and another round — noted, not dropped.

The unmeasured half is different in kind and I'm not going to pretend otherwise: nobody knows yet whether a live model reads these figures and idles less. That isn't a gap in this PR's evidence, it's a gap in what any pre-merge lane can produce here — the sandbox is credential-free by design, so it never reaches a model, and the prompt is never rendered into the TUI, so the terminal lane has nothing to show either. The description says so itself under Not validated, which is the honest way to carry it. Stage 2 names /verify for the half that is checkable — whether these test pins are load-bearing — and I'd read that as worth running if anyone wants the A/B, but I'm not gating on it: the pins compare complete prompt strings and assert the rendered line on each host's own model-call mock, so they are load-bearing by construction.

On volume: this author has a stack of open Goal-shaped PRs and I checked whether that wore me down. It didn't change what I read — the consumer census, the render-site gating, the stale-figure window and the counter semantics are all things I went and looked at in this branch rather than accepting from the description, and the centralization commit is the one I spent most time on. It does still argue for the sequencing the author proposed: the runtime bound landed first, this follows on top of it, and the two agree about what "progress" means.

One note on approval state, because it's the thing that has bitten this PR's thread before: @wenshao approved at this exact commit, and that is a separate vote which I have not counted as mine. main wants two. Mine is below.

Verdict: approve. Approval is pinned to c518b774773829dde04c6756f389f5ae95df5884 through the reviews API rather than gh pr review --approve, so it records against the commit I actually read. CI on that commit is fully settled with zero pending pull_request runs, so there is no deferred-approval marker this round — the one the previous pass left was pinned to 0bac4bbf and is removed by this update. If the branch moves, this pass does not carry over; re-run me.

中文说明

信心度:4/5 —— 每个阶段都干净,我会合并它;不给 5/5 是因为 PR 对自身行为的两处陈述落后于它交付的代码,而且这个主张的"效果"那一半没有被测量,这里也没有任何通道能测它。

退一步看。我独立想到的方案和这个 PR 的设计是同一个设计,这是我对预算那一半能给出的最高评价 —— 我的方案是在打开 diff 之前写下的,之后也没找到更省的路。它超出我方案的地方是那个没人要求的集中化。我本来会保留四份 per-host 字段拷贝、依赖钉住它们的四个用例,而那正是产生了前三轮"某个 host 静默丢掉可选字段"的形态。让共享的轮次类型承载这些提示字段,把这个错误从"某一个 host 上少了一行提示词"变成了类型错误;而且它在所触及的 host 里删掉的代码比加上的还多。半年后我会感谢写下那个提交的人、而不是骂他 —— 这才是我真正在意的那个问题。

关于"我是不是太好说话"这项检查,因为这个 PR 已经过了大约五轮机器人评审,而我现在推荐它。我没有继承上一轮的结论 —— 它评审的是另一个 commit,而且它 withheld 批准的理由现在已经不存在了。它当时挡在两条红 CI 通道上,依据是两者都没碰本 diff 里的任何文件。在这个 head 上两者都是绿的,而重新合入一份修好的 main 正是会清掉它们的那件具体事情。所以这个判定现在是被结果确认的,而不是靠一个关于 diff 的论证撑着 —— 这是一个严格优于上一轮被挡时那种位置的证据状态。defer 的理由已经消失,而在具体理由消失之后继续 defer 是噪音,不是谨慎。

在这个 commit 上我自己核实、而不是采信的东西:turnCount 确实统计已完成轮次,所以措辞和首轮例外都是诚实的;数字不会在调度与渲染之间变旧,因为 permit 是在调用 host 之前取得的,而那些字段只在轮次结束时才变化;预算行留在不可信数据块之外,并且只可能插入数字和逗号;host 清点是完整的,生产实现共三个,其中第三个是靠转发而不是靠改动被覆盖的;以及整个风险小节所依赖的那个主张 —— 这些行永远不会到达用户自己的轮次 —— 在全部三个渲染点上都成立。最后这一项我是专门去查的,因为它属于那种读起来显然为真、却常常不是的断言。

剩下的确实都不阻塞,而我想说清是哪一种不阻塞。设计文档仍然把各 host 描述成"沿着它们已经拷贝的字段一起拷贝 usage",其 scope 小节也仍然写着"每个 host 的队列条目里拷贝一个字段" —— 那早于集中化提交,而且这份文档从未提到现在真正作为机制的共享轮次类型或轮次级渲染函数。另外,文档和描述都把运行时的空转闸称为"单独的工作";它已经落在 main 上、也在这个分支里,这一点值得写一行,部分原因是那个计数器正是能客观回答那个悬而未决问题的指标。两者都不改变代码的行为,也不会让未来的读者因此踩到 bug —— 它们是一个以措辞为交付物的 PR 上的文档欠账。

按仓库自己关于不要让评审轮次把 PR 撑大的指引,这个 PR 已经到了只应落关键修复、建议项应当延后的阶段。没有关键修复。所以我把这两条文档项在此记录为延后到一次后续提交,而不是再要求一次推送、再多一轮 —— 记录在案,不是悄悄丢掉。

未测量的那一半在性质上不同,我也不打算含糊:还没有人知道真实模型是否会读这些数字并因此少空转。这不是本 PR 证据上的缺口,而是这里任何合并前通道都无法产出的东西 —— 沙箱按设计无凭据,所以它根本到不了模型;而提示词从不渲染进 TUI,所以终端通道也没有任何东西可展示。描述自己在 Not validated 里写明了这一点,这是承载它的诚实方式。Stage 2 为可查的那一半点名了 /verify —— 即这些测试钉子是否承重 —— 如果有人想要那次 A/B,我认为值得跑,但我不以它为门:这些钉子比较的是完整提示词字符串,并在每个 host 自己的模型调用 mock 上断言渲染出的那一行,所以它们在构造上就是承重的。

关于数量:这位作者手上有一叠 Goal 形态的开放 PR,我检查了这是否让我疲掉。它没有改变我读的内容 —— 消费者清点、渲染点门槛、数字变旧的窗口与计数器语义,都是我到这个分支里去看的,而不是从描述里接受的;而集中化那个提交是我花时间最多的地方。它确实仍然支持作者提出的排序:运行时上限先落地,这个叠在它上面,而且两者对"进展"的含义是一致的。

关于批准状态有一点要说明,因为这是本 PR 讨论串上以前吃过亏的地方:@wenshao 在这个确切的 commit 上批准了,那是另一张票,我没有把它算作我的。main 需要两张。我的那一张在下面。

结论:批准。 批准通过 reviews API 钉在 c518b774773829dde04c6756f389f5ae95df5884 上,而不是用 gh pr review --approve,所以它记录在我真正读过的那个 commit 上。该 commit 的 CI 已完全落定、没有 pending 的 pull_request 运行,所以这一轮没有延后批准标记 —— 上一轮留下的那个钉在 0bac4bbf 上,已被本次更新移除。如果分支移动,本轮判断不再适用,请重新触发我。

Qwen Code · qwen3.8-max-2026-09-02

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

…dgement on turn one

The three host copies were the only link in the chain with nothing behind
them: `usage` is optional on both sides of every hop, so deleting a copy
typechecks and costs the prompt its budget line on that host alone. One
case per host now fails when its copy is removed.

The judge-your-previous-turn line is held back on the Goal's first turn.
`create` schedules a continuation before any turn has finished, so that
line asks the model to judge a turn that does not exist. A host that
reports no figures still gets the line: not knowing the turn number is
not evidence of a first turn.
@qqqys

qqqys commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

Both open points taken, in 109c1e6.

The host pass-throughs now have assertions. You are right that this was the one link with nothing behind it, and that an unchanged suite is the tell. One case per host: useMessageQueue.test.ts claims a queued turn and reads the figures back, use-llm-stream.test.tsx asserts the synthetic prompt carries the budget line, and nonInteractiveCli.test.ts and Session.test.ts each drive a real startGoalTurn and assert the same line reaches the model. I removed each copy in turn and confirmed the matching case fails; with all four copies in place they pass.

The turnCount: 0 turn no longer asks for the judgement. Your call to make, and the wording really does not apply there — worse, asking a model to judge a turn that does not exist is an invitation to describe one. Judge your previous Goal turn is now held back when the figures say turnCount === 0; the other three progress lines and the budget line stand. A host that supplies no figures still gets all four, since not knowing the turn number is not evidence of a first turn. Two cases pin both sides, and the design note says why.

On splitting the budget line from the progress lines: I left them together. They ship as one prompt, the E2E evidence is one transcript, and either half is a one-line revert in the same file. If either misbehaves the revert is cheap, and splitting now would mean two prompt-text PRs racing on the same five whole-prompt expectations.

Merge order, unchanged: this needs to land after #11239 and #11248. It shares goal-runtime.test.ts with the first (which also renames tokenLedger to ledger, so those two fixtures need updating on the merge) and a goals.md anchor with the second.

中文说明

两个开放问题都采纳了,在 109c1e6

host 侧的透传现在有断言了。 你说得对,那是整条链上唯一没有东西兜着的一环,而"测试套原样通过"本身就是破绽。每个 host 一个用例:useMessageQueue.test.ts 认领队列里的轮次并把数字读回来,use-llm-stream.test.tsx 断言合成提示词里带着预算行,nonInteractiveCli.test.tsSession.test.ts 各自真正驱动一次 startGoalTurn 并断言同一行到达了模型。我把四处拷贝逐个删掉验证过,对应的用例都会失败;四处都在时全部通过。

turnCount: 0 那一轮不再要求做判断了。 这确实该由我定,而那句措辞在那一轮上根本不适用——更糟的是,让模型去判断一个不存在的轮次,等于邀请它编一个出来。现在当数字表明 turnCount === 0 时,Judge your previous Goal turn 会被跳过;另外三行指引和预算行保留。不提供数字的 host 仍然拿到四行,因为"不知道这是第几轮"并不等于"这是第一轮"。两个用例把两边都钉住了,设计文档里写了原因。

关于把预算行和指引行拆开:我没有拆。它们作为同一段提示词上线,E2E 证据也是同一份记录,而任何一半都是同一个文件里的一行回滚。真出问题回滚很便宜;现在拆开反而会让两个改提示词的 PR 在同样那五处整段断言上互相排队。

合并顺序不变:本 PR 要排在 #11239#11248 之后。它与前者共用 goal-runtime.test.ts(前者还把 tokenLedger 改名为 ledger,所以合并时那两处夹具要跟着改),与后者共用 goals.md 的同一个锚点。

@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.

Reviewed. Suggestions are inline.

Not explored to full depth (tool budget reached): "agent 6c": I did not execute the new tests ( packages/core and packages/cli vitest runs) — my conclusions about them are from reading the assertions against the impleme….

Test Plan (not a blocker): Tests 168 passed — this review observed 28913, 23668, 1959, 298, 1818, 513, 6408 passed.

中文说明

已审查。 建议见行内评论。

未探索到全部深度(达到工具调用预算):"agent 6c"I did not execute the new tests ( packages/core and packages/cli vitest runs) — my conclusions about them are from reading the assertions against the impleme…

Test Plan(非阻断):Tests 168 passed — this review observed 28913, 23668, 1959, 298, 1818, 513, 6408 passed

— qwen3.8-max via Qwen Code /review (v0.23.0)

Comment thread docs/users/features/goals.md Outdated
Comment thread docs/users/features/goals.md Outdated
Comment thread packages/core/src/goals/goal-continuation-prompt.test.ts Outdated
Comment thread packages/core/src/goals/goal-continuation-prompt.ts
Comment thread packages/core/src/goals/goal-runtime.ts Outdated
Comment thread packages/core/src/goals/goal-continuation-prompt.ts Outdated
@wenshao

wenshao commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /resolve

Both main (QwenLM#11248) and this branch inserted a new paragraph at the same point in the Goals introduction. The two describe different surfaces and are both kept: main's covers the footer pill and status cards rendering spend against the budget window, this branch's covers the figures and standing guidance the model receives at the start of each autonomous turn. Main's paragraph goes first because it defines "the window it is allowed" and names model.goalTokenBudget, which the second paragraph then relies on.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

Qwen Code resolved the merge conflicts and pushed the branch update.

Merge: PR #11257main

Merge commit 1709ae7808. One file conflicted, one block.

Root cause. Main's 50b33052cd (PR #11248, the Goal spend display) added a paragraph to docs/users/features/goals.md exactly where this PR added one — after the /trust sentence. Two insertions on one line, no shared anchor. Main's other hunks there (#11284) merged cleanly. No code file conflicted.

Textual, not semantic. Both paragraphs kept, main's first; they cover different surfaces — main's the footer pill and status cards, ours the figures and guidance the model gets at each autonomous turn's start. nonInteractiveCli.ts, the only code file both touched, auto-merged: main edited formatGoalState, ours the headless host turn record. Both present in the result.

Load-bearing.

  • Order: main's paragraph defines "the window it is allowed" and names model.goalTokenBudget; ours reuses that wording, so it must come second.
  • Keep each paragraph one long line: .prettierrc.json sets no proseWrap, so Prettier preserves rather than rewraps.
  • usage is optional at every hop, so a dropped copy typechecks and silently costs that host its Budget: line. All four forwarding sites survive; AppContainer.tsx needs none as it spreads enqueueGoalTurn(input) — an edit that enumerates fields instead must re-add it. Main added no fourth host or render site.
  • Two independent gates: the budget line prints whenever usage exists (including turnCount === 0); NO_PROGRESS_LINE is suppressed only at turnCount === 0. Do not collapse them.

Not verified — no build, typecheck, lint, or tests run.

  1. Doc tension left as-is on purpose: main says a Goal that has not billed a turn "shows no figures at all", ours says each autonomous turn begins with them. Not a contradiction — the display gates on turnCount > 0 / tokensUsed > 0, the prompt only on usage — but a reader may read it as one, and fixing it means editing main's paragraph.
  2. feat(goal): show what a Goal has spent against the window it is allowed #11248 also added tests to nonInteractiveCli.test.ts, the file this PR extends. Auto-merged, no shared fixtures, and this PR does not change TEXT output — but I could not run them.
中文说明

合并提交 1709ae7808。仅一个文件、一个冲突块。

根因:main 的 50b33052cd(PR #11248)在 docs/users/features/goals.md 与本 PR 同一处新增一段(均在 /trust 那句之后),同行两处插入、无共同锚点;main 其余改动(#11284)已干净合并。无代码文件冲突。

文本冲突,非语义冲突:两段都保留、main 在前——main 讲底部状态条与卡片显示,本 PR 讲模型在每个自动回合开头收到的数字与指引。双方都改过的 nonInteractiveCli.ts 自动合并(main 改 formatGoalState,本 PR 改无头 host 回合记录),两边都在。

关键点:main 那段先定义「the window it is allowed」并点出 model.goalTokenBudget,本 PR 沿用,故须在后;每段保持单行长文本(未设 proseWrap,Prettier 不重排);usage 每跳皆可选,漏拷贝仍过类型检查、只让该 host 丢掉 Budget: 行——四处透传点均完好,AppContainer.tsx 整体透传无需改动,改为逐字段枚举须补回;main 未新增第四个 host 或渲染点;预算行与 NO_PROGRESS_LINE 条件独立(后者仅在 turnCount === 0 时抑制),勿合并。

未能验证(未跑构建、类型检查、lint、测试):

  1. 刻意未改的观感矛盾:main 称未计费回合的 Goal「完全不显示数字」,本 PR 称每个自动回合都以数字开头。实际不冲突(显示turnCount > 0/tokensUsed > 0提示词只看 usage),但调和需改 main 段落。
  2. feat(goal): show what a Goal has spent against the window it is allowed #11248 也往 nonInteractiveCli.test.ts 加用例,与本 PR 同文件;已自动合并、不共用 fixture,本 PR 不改 TEXT 输出,但无法运行验证。

@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.

Reviewed — no blockers. Suggestions are inline.

2 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • R1-1 docs paragraph over-promises the standing guidance lines — already reported (docs/users/features/goals.md:21, comment 3949324960)
  • R1-2 design note deleted, description pointer survives — already reported (docs/users/features/goals.md:21, comment 3949324984)

Test Plan (not a blocker): Tests 168 passed — this review observed 29027, 23695, 1971, 298, 1819, 513, 6533 passed.

Deferred under the convergence posture (round 2, not a blocker) — recorded, not requested in this round:

  • packages/core/src/goals/goal-continuation-prompt.ts:187 — [review] D2-1 first-turn gate reads the persisted record turnCount, not whether a previous continuation is in the model's context
  • packages/core/src/goals/goal-continuation-prompt.ts:120 — [review] D2-2 EVIDENCE_LINE calls the untrusted workspace and tool channels authoritative without the house qualifier
  • packages/core/src/goals/goal-continuation-prompt.ts:90 — [review] D2-3 the description's premise that get_goal cannot supply the figures is contradicted by the permitted snapshot
中文说明

已审查——无阻断问题。 建议见行内评论。

本轮确认的 2 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

Test Plan(非阻断):Tests 168 passed — this review observed 29027, 23695, 1971, 298, 1819, 513, 6533 passed

收敛姿态下延后(第 2 轮,非阻断)——已记录,本轮不要求修改:共 3 条(原文未翻译,列表见上方英文部分)。

— qwen3.8-max via Qwen Code /review (v0.23.0)

Comment thread packages/core/src/goals/goal-continuation-prompt.test.ts
Comment thread packages/core/src/goals/goal-continuation-prompt.ts Outdated
Comment thread packages/core/src/goals/goal-continuation-prompt.ts Outdated
Comment thread packages/core/src/goals/goal-continuation-prompt.ts
Comment thread packages/core/src/goals/goal-runtime.ts Outdated
@wenshao

wenshao commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ❌ not passed — findings reported (agent verdict) - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 53 passed · 6 failed · 59 total

Flakiness gate: ⚠️ timeout — only 3 of 5 rounds fit the 15-minute budget; the completed rounds agreed

中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:53 通过 · 6 失败 · 59 总计

抖动门:⚠️ timeout — only 3 of 5 rounds fit the 15-minute budget; the completed rounds agreed

Verification report

PR 11257 — deep verification

Verdict: findings — 53 pass / 6 fail / 59 total scripted assertions.
Verified head 1709ae7808c16f3f550fd718820a1952a9142084 (HEAD^2), base
f1ed3bc31a2b618072dbd3c8d9ee3e04f48416d1 (HEAD^1), merge 8f65f9a3.

The code is correct. Every assertion about the shipped behaviour of this
change passed: the budget line, its arithmetic, the four progress lines, the
first-turn gate, the wind-down suppression, the zero clamp, the placement, and
all four per-host copies measured right, and 15/15 mutants were killed
(9 guards + 6 same-file positive controls), zero survivors
— every guard the
PR adds is pinned by a test that goes red when the guard is removed.

All 6 failures are claims made in the PR description or in a committed code
comment, not defects in the change.
They are reported as findings because one
of them is a false mechanism written into a source comment that the next reader
will trust, and because the migration-notes and risk-sizing claims are what a
reviewer weighs. Details in Findings; the falsified claims with their
evidence are in 03-six-description-claims-falsified.png.

中文摘要

结论:findings — 59 条脚本化断言中 53 通过 / 6 失败。已验证 head
1709ae78,base f1ed3bc3

代码本身是正确的。 所有关于这次改动实际行为的断言都通过了:预算行及其算术、
四行进展指引、首轮的豁免、wind-down 轮的抑制、剩余量夹到 0、位置、以及四个 host
各自的拷贝,全部测量正确;变异矩阵 15/15 全部被杀死(9 个防护点 + 6 个同文件
阳性对照),零幸存——PR 新增的每个防护点都有一个会在它被删掉时变红的测试钉住。

A/B 结论(见 Central claim + A/B 表与 01-ab-head-vs-base-prompt-cells.png):
真实 createGoalRuntime 驱动一个 3 轮 Goal,head 侧 4/4 个续跑提示词都带预算行,
数字与运行时自己的账本逐字相符(0 → 41,634 → 85,498 → 135,498);base 侧 0/4。
四行指引在 head 侧首轮为 3/4(判断上一轮那行按设计被扣下)、其后每轮 4/4;base
侧全为 0/4。wind-down 轮 head 侧保留预算行、指引行为 0,超支时剩余量夹到 0。

6 条失败全部是 PR 描述或已提交代码注释里的说法被测量推翻,不是改动的缺陷:

  1. 代码注释与 PR 正文称"数字放进数据块会让每一轮看起来都像一次编辑"。把预算行移进
    <goal_runtime_data> 的变异体实测 objectiveUpdated 仍是
    [false,false,false,false]——目标变更判定比较的是目标字符串
    announcedObjective !== continuationContext),从不比较数据块。
  2. "不提供数字的 host 渲染出来的就是原来那段提示词"不成立:实测多出 4 行指引
    (11 行 → 15 行,1112 → 1950 字符,+75.4%)。PR 自己钉住全文的期望值
    toHaveLength(15))就是这个真实行为。
  3. 风险段落"原本 17 行的提示词加了 5 行":穷举 16 种开关组合后,base 侧是
    11–15 行,从不出现 17 行;17 行是 head 侧的渲染结果。典型一轮实测
    1112 → 2036 字符,即 +83%,而非 +29%。
  4. 正文指向的设计文档 docs/design/2026-09-07-goal-continuation-budget.md
    在已验证 head 上不存在,diff 里也没有。
  5. 评审验证方式里写的 168 passed,实测为 170 passed
  6. (观察,未计入断言)tokensUsed 为小数时会渲染成 41,634.5

未覆盖范围:逐 commit 归因(浅克隆 depth 2,本地 HEAD^1..HEAD^2 只有 1 个
commit,快照列了 5 个);真实模型 E2E(沙箱无凭据,本 PR 的 transcript 证据未被
独立复现——本轮复现的是提示词字节本身,不是活会话);base 侧 vitest 被 workspace
构建守卫挡住,base 测试数由跨 commit 静态比对得出;指引行是否能减少空转轮(PR 自己
已列为范围外);macOS/Windows;仓库全量测试与集成测试。

Scope

Central claim. Every runtime-scheduled Goal continuation prompt now opens
with a budget line (spend / window / remainder / turns finished) and carries
four standing progress lines, at every host.

Secondary claims. (S1) usage survives every hop from the runtime to the
text the model receives, at all three hosts. (S2) Figures that move every turn
do not disturb objective-change detection.

Out of scope by choice, and listed under Not covered: live-model E2E,
per-commit attribution, the repo-wide suite, and whether the progress lines
measurably reduce idle turns.

Central claim + A/B

Both arms drive the real createGoalRuntime with an in-memory journal (the
persistence seam the runtime itself declares) and a host whose startGoalTurn
calls the real buildGoalContinuationParts — exactly what all three
production hosts do — then records parts[0].text. The observation point is the
rendered prompt the model would receive, not the usage object at the host
boundary. Witness: 01-ab-head-vs-base-prompt-cells.png.

Scenario threeTurns, grant 30,000,000, turns billed 41,634 / 43,864 / 50,000:

continuation HEAD budget line (verbatim) HEAD progress BASE budget line BASE progress
#0 (scheduled by create) Budget: 0 of 30,000,000 tokens used, 30,000,000 remaining; 0 Goal turns finished. 3/4 (none) 0/4
#1 Budget: 41,634 of 30,000,000 tokens used, 29,958,366 remaining; 1 Goal turn finished. 4/4 (none) 0/4
#2 Budget: 85,498 of 30,000,000 tokens used, 29,914,502 remaining; 2 Goal turns finished. 4/4 (none) 0/4
#3 Budget: 135,498 of 30,000,000 tokens used, 29,864,502 remaining; 3 Goal turns finished. 4/4 (none) 0/4

4/4 flip from absent to present, and each line's used / remaining / turn
figures equal the runtime's own ledger exactly (assertion A3). All three
Budget: lines quoted in the PR body's transcript evidence — including the
0 of 30,000,000 first turn — plus the wind-down line, are reproduced
byte-identically by this independent harness, which corroborates that the
author's transcript was read off this code path. Assertion A5 is the
destination check: the usage the runtime handed the host appears verbatim in
the rendered text, on every continuation.

Other cells (all in the capture):

cell HEAD BASE
windDown (grant 30,000, turn 0 bills 62,026) Budget: 62,026 of 30,000 tokens used, 0 remaining; 1 Goal turn finished. + hand-off lines, 0/4 progress no figures, hand-off lines only
noBudget (grant Infinity) Budget: 0 tokens used, with no budget on this Goal; 0 Goal turns finished. (none)
hostWithoutUsage (host supplies no figures) no budget line, 4/4 progress, 15 lines no budget line, 0/4 progress, 11 lines
overspentClamp (99,999 of 30,000) 0 remaining, no - anywhere n/a
objectiveUpdated across 4 continuations with moving figures [false,false,false,false] [false,false,false,false]
figures inside <goal_runtime_data> false on every prompt false
budget line after the authoritative-objective guard, before the objective-updated notice orderOk=true on all 8 prompts that carry one n/a

Control validity (assertions C1C4): the two arms load different module
bytes (prompt 1c035e12… vs c794bb38…; runtime c1292aba… vs de7959a2…),
each arm's realpath is inside its own tree, and the base arm renders
identically whether or not usage is passed in — so base is a true control that
ignores the field, not a partial build. The PR touches no package.json or
lockfile (git diff --name-only HEAD^1..HEAD matched none), so sharing the
already-installed root node_modules is a clean control.

Corrections

These correct the description and a committed comment, not the code. Nothing
here asks for a behaviour change.

1. The data-block rationale names a mechanism that does not exist. Both the
PR body and the committed comment above renderBudgetLine say the figures are
kept out of <goal_runtime_data> because "that block is untrusted task data
compared by content to decide whether the objective changed, and a number that
moves every turn would make every turn look like an edit."

The objective-change check is announcedObjective !== continuationContext
(goal-runtime.ts:562-564), where continuationContext = snapshot.goal.objective.
It compares the objective string. The data block is serialised in exactly
one place and never read back or compared anywhere in the repo: among production
sources grep -rln "goal_runtime_data" matches only
goal-continuation-prompt.ts itself (the four other matches are test files).

Proven by mutant rather than by reading: with the budget line moved inside
the data block, the same 4-turn drive gives
figuresInsideDataBlock=[true,true,true,true] and
objectiveUpdated=[false,false,false,false] — unchanged. No turn looks like an
edit. Evidence retained as mutant-datablock.json / mutant-datablock.log;
recreate with:

cd /__w/qwen-code/qwen-code
git worktree add tmp/mutant-tree HEAD
ln -sfn "$PWD/node_modules" tmp/mutant-tree/node_modules
ln -sfn "$PWD/packages/core/node_modules" tmp/mutant-tree/packages/core/node_modules
# in tmp/mutant-tree/packages/core/src/goals/goal-continuation-prompt.ts:
#   add `...(input.usage ? { budget: renderBudgetLine(input.usage) } : {})` to
#   serializeGoalData's JSON, and delete the `if (input.usage) { lines.push(...) }` block
npx tsx tmp/pr11257-verify-20260907-233622/ab-harness.ts \
  --tree "$PWD/tmp/mutant-tree" --out /tmp/mutant.json
git worktree remove --force tmp/mutant-tree

The placement decision itself is still defensible on a different ground — the
block is framed to the model as untrusted task data, while the figures are
trusted runtime facts, and mixing them would weaken that framing. That is worth
saying in the comment instead, because the reason currently committed is not
what the code does, and the next reader will reason from it.

2. "A host that omits usage renders the previous prompt exactly" is not
true.
Stated three times: the body ("usage is optional on the host
contract, so a host that supplies no figures renders exactly the prompt it
rendered before"
), Breaking changes / migration notes ("a host that omits it
renders the previous prompt exactly"
), and the 中文 section ("不提供它的 host
渲染出来的就是原来那段提示词"
).

Measured (assertions B1, B2, B2b, B3): a no-figures render at head is
15 lines / 1,950 chars against base's 11 lines / 1,112 chars — the four
progress lines are still added (+838 chars, +75.4%). Nothing base rendered
was lost; the divergence is exactly those four lines and nothing else.

The PR's own tests already encode the true behaviour, which is why this went
unnoticed: the pinned full-text expectation for
renderGoalContinuationPrompt({goalId, revision, objective}) — no usage — now
ends with all four progress lines, and the escaping test's line count was
changed from toHaveLength(11) to toHaveLength(15). The one new test aimed at
this case, carries no budget line for a host that supplies no figures, asserts
only not.toContain('Budget: '), so it pins the budget line's absence and not
the prompt's identity.

Bounded — no production impact: the runtime populates usage
unconditionally (goal-runtime.ts:614), so all three shipped hosts always have
figures and never take this path. The claim is about a branch only an external
host (or a test) can reach. It does hold on the wind-down cell, where both arms
render byte-identically (B4, sha d485c292… on both), because progress lines
are suppressed there in either case.

3. The risk sizing understates the change by roughly 3×. Risk & Scope
says "Five lines are added to a prompt that was seventeen, which is a real
share of it."
An exhaustive census of all 16 flag combinations
(usage × objectiveUpdated × windDown × verifierFeedback) on both arms
gives:

arm lines chars a 17-line rendering exists?
base 11..15 1090..1762 no
head 13..18 1505..2272 yes

17 lines is a head rendering (usage + objectiveUpdated), not the prompt
the lines were added to. Against the real base of 11 lines, a typical
continuation grew 1,112 → 2,036 chars (+83%) and the first turn of a Goal
grew 1,112 → 1,717 (+54%); only the wind-down turn is cheap, 1,527 → 1,600
(+4.8%, the budget line alone). The stated risk is real but larger than
described, which matters because that paragraph is the counterweight a reviewer
is asked to accept.

Findings

Ordered by severity. None is a defect in the shipped behaviour; 1 and 2 are the
corrections above restated as actionable items, because both are text a future
reader will rely on.

1. Suggestion — a committed source comment states a false mechanism.
packages/core/src/goals/goal-continuation-prompt.ts, the docblock above
renderBudgetLine. Reproduce with the data-block mutant (command in
Corrections §1): figures inside the block, objectiveUpdated still all false.
Suggested change is to the comment only — replace the "compared by content …
would make every turn look like an edit" clause with the trust-boundary reason
that does hold. No code change, so no fixture would go red either way: the suite
is green with and without the reworded comment, which is exactly why the
inaccuracy is invisible to CI.

2. Suggestion — the backwards-compatibility guarantee in the description does
not hold as written.
Reproduce (the base worktree is set up as in
Corrections §1, at HEAD^1 instead of HEAD):

cd /__w/qwen-code/qwen-code
npx tsx tmp/pr11257-verify-20260907-233622/ab-harness.ts --tree "$PWD" --out /tmp/head.json
git worktree add tmp/base-tree HEAD^1
ln -sfn "$PWD/node_modules" tmp/base-tree/node_modules
ln -sfn "$PWD/packages/core/node_modules" tmp/base-tree/packages/core/node_modules
npx tsx tmp/pr11257-verify-20260907-233622/ab-harness.ts --tree "$PWD/tmp/base-tree" --out /tmp/base.json
git worktree remove --force tmp/base-tree
# compare renderCells.noUsageAtAll: head 15 lines, base 11 lines

Fix is to the wording: a host that omits usage renders the previous prompt
plus the four progress lines; only the budget line is conditional. Bounded
as in Corrections §2 — unreachable from every shipped host.

3. Nice to have — the design note the body points at is not in the PR.
"Design note in docs/design/2026-09-07-goal-continuation-budget.md." That
path does not exist at the verified head, and the diff adds only
docs/users/features/goals.md (+2 lines). AGENTS.md puts design docs in
docs/design/ under version control, and the repo has sibling notes
(2026-09-02-goal-pause-reasons.md, 2026-08-25-goal-draft-skill.md). Either
the note was not staged or the reference is stale.

4. Nice to have — the Reviewer Test Plan's expected count is stale. The plan
predicts Tests 168 passed (168); the command yields 170 passed (170) at
the verified head (gate-core-tests.log). git diff HEAD^2..HEAD is empty, so
the merge introduced nothing — the stated number predates the PR's own final
commits. A reviewer following the plan sees a different number and has to work
out whether that is expected.

5. Nice to have — fractional spend renders with decimals. The reducer
validates tokensUsed with isNonNegativeNumber (finite, ≥ 0), not
isNonNegativeInteger, so a fractional ledger value reaches
toLocaleString('en-US') and renders
Budget: 41,634.5 of 30,000,000 tokens used, 29,958,365.5 remaining; 2 Goal turns finished.
(renderCells.fractionalSpend). Bounded: takeGoalTurnTokens sums integer
totalTokenCount values from API usage metadata, so this is not reachable in
practice — an observation from the harness, not counted in assertions.json.

Checked and clear

Named so the absence of a finding is not read as an absence of checking:

  • No missed host. Exactly three bindGoalTurnHost registrations exist
    (nonInteractiveCli.ts:686, AppContainer.tsx:2635, Session.ts:2335), and
    all three carry usageAppContainer's forwards the whole input object to
    enqueueGoalTurn, so it needs no field copy.
  • No missed hop. A fourth origin: 'runtime' construction exists at
    use-llm-stream.ts:5075 that the PR does not touch. It builds a
    GoalTurnBinding{permit, turnKey, controller, origin} — which carries no
    continuationContext and never reaches renderGoalContinuationPrompt. Not a
    dropped copy.
  • The budget line never reaches a user turn. Both user-origin construction
    sites (nonInteractiveCli.ts:1495, Session.ts:4797) omit usage and go out
    through the user's own parts, not buildGoalContinuationParts — matching the
    body's claim that these lines "only ever reach runtime-scheduled turns".
  • Data-block escaping is unaffected. escapeJsonTagCharacters still wraps
    the serialisation, and the tag/quote/newline escaping tests pass unmutated.
  • The author's coverage claim is true. "Each of the four fails when its
    copy is removed; I checked by removing them"
    — verified: all four host-copy
    mutants were killed, each by exactly one test.
  • All three host tests assert at the destination, not at the queue boundary:
    they read the text handed to sendMessageStream. The one boundary-level
    assertion is useMessageQueue.test.ts, whose render hop is covered separately
    by use-llm-stream.test.tsx.

Mutation matrix

15/15 killed, 0 survivors, 6/6 positive controls red. Witness:
02-mutation-matrix-15-of-15-killed.png. Each mutated file gets a control that
removes the pre-existing windDown copy sitting next to the new usage
copy, which proves the chosen vitest command collects tests that exercise that
file — without it, "the usage mutant survived" and "my command never ran your
suite" are the same observation.

mutant file suite result
drop usage, handed to the host goal-runtime.ts goal-runtime.test.ts KILLED 3 red / 148 green
control: corrupt turnCount goal-runtime.ts goal-runtime.test.ts KILLED 3 red
drop the budget line goal-continuation-prompt.ts goal-continuation-prompt.test.ts KILLED 5 red / 14 green
drop the Math.max(0, …) clamp goal-continuation-prompt.ts goal-continuation-prompt.test.ts KILLED 1 red / 18 green
drop the first-turn gate (turnCount > 0) goal-continuation-prompt.ts goal-continuation-prompt.test.ts KILLED 1 red
render progress lines on wind-down too goal-continuation-prompt.ts goal-continuation-prompt.test.ts KILLED 1 red
control: reword a pinned wind-down line goal-continuation-prompt.ts goal-continuation-prompt.test.ts KILLED 1 red
drop the usage copy nonInteractiveCli.ts nonInteractiveCli.test.ts KILLED 1 red / 146 green
control: drop the windDown copy nonInteractiveCli.ts nonInteractiveCli.test.ts KILLED 1 red
drop the usage copy useMessageQueue.ts useMessageQueue.test.ts KILLED 1 red / 52 green
control: drop the windDown copy useMessageQueue.ts useMessageQueue.test.ts KILLED 1 red
drop the usage: queuedGoal.usage read use-llm-stream.ts use-llm-stream.test.tsx KILLED 1 red / 266 green
control: drop the windDown read use-llm-stream.ts use-llm-stream.test.tsx KILLED 1 red
drop the usage copy Session.ts Session.test.ts KILLED 1 red / 851 green
control: drop the windDown copy Session.ts Session.test.ts KILLED 1 red

No survivor needs adjudicating, so no coverage-gap / dead-code /
redundant-defence classification is owed. One structural note: the runtime and
prompt halves are each pinned separately, but no single test drives the whole
chain
runtime → host → rendered prompt — the PR's host tests inject usage at
the host seam, and the runtime tests assert the usage object rather than the
text. Assertion A5 closes that gap for this round; a fixture that drives the
real runtime and asserts on the rendered text would pin it permanently.

Not covered

  • Per-commit attribution. The checkout is depth 2, so only the merge, the
    base tip and the PR head exist. git rev-list HEAD^1..HEAD^2 returns 1
    while $QWEN_VERIFY_CONTEXT lists 5 commits — at a shallow boundary that
    command returns a plausible small number instead of erroring, so the gap is
    easy to miss. The aggregate HEAD^1..HEAD diff was verified instead.
    git diff HEAD^2..HEAD is empty, so the merged tree is byte-identical to the
    PR head and nothing from main could have shifted these measurements.
  • Live-model E2E. No credentials in this sandbox. The PR's transcript
    evidence (rising Budget: lines, the wind-down hand-off, the model reading
    the figures back) was not independently reproduced. What was reproduced is
    the rendered prompt bytes through the real runtime — the shape, not a
    live session, and not the model's reaction to the figures.
  • Whether the progress lines reduce idle turns. The PR scopes this out
    itself; it needs longer-running Goals than one round.
  • Base-arm vitest. Blocked by the workspace globalSetup build guard (a
    scratch worktree has no packages/core/dist). Base test counts were derived
    statically across the three commits instead: 12 + 136 it blocks at base
    versus 19 + 139 at head, +10 tests. The A/B is unaffected — it imports source
    directly and asserts per-arm module hashes and realpaths.
  • The usage === undefined branch is unreachable from every shipped host,
    because the runtime always populates usage. The no-figures rendering was
    therefore verified at the renderer and through a harness host that strips the
    field — not through any production host.
  • Gates not run: the repo-wide test suite, integration tests
    (test:integration:*), and the sandbox build. Targeted gates only:
    npm run typecheck (repo-wide, exit 0), prettier and eslint on all 13 changed
    files (clean, and proven live — a planted violation was reported by both with
    nonzero exit, then removed; gate-lint-probe.log), the two core suites
    (170 passed), and the five cli suites from the test plan
    (1377 passed / 1 skipped, 164 s).
  • macOS and Windows, which the PR marks ⚠️. Formatting is pinned to
    toLocaleString('en-US'), so it does not follow the host locale.

Methodology

CI verify job: node:22-bookworm container, Node v22.23.2, working tree at
refs/pull/11257/merge (depth 2), npm ci and npm run build already
complete; no GitHub token, and no network calls were made — the metadata
snapshot at $QWEN_VERIFY_CONTEXT plus the local tree were the whole world.
Controls were scratch worktrees under tmp/ (tmp/base-tree at HEAD^1,
tmp/mutant-tree at HEAD) with node_modules symlinked to the installed
root, which is a clean control because the PR changes no dependency manifest;
each arm's module hash and realpath were asserted so no arm could silently
load the other's code. Both were removed with git worktree remove --force
once their cells were captured, so the recreate steps are spelled out above.

Harnesses ran under tsx against TypeScript source,
importing the tree's own createGoalRuntime, buildGoalContinuationParts and
renderGoalContinuationPrompt by absolute path — nothing in the unit under test
was stubbed, and the only seams used are the two the runtime itself declares
(the journal, and the host callback the PR is changing). Mutations were applied
in place by mutation-matrix.py, which asserts the tree is clean before it
starts, restores with git checkout -- after every mutant, and asserts
cleanliness again after each restore and at the end; the tree is clean now.
Raw logs live beside the harnesses: head.log, base.log,
mutant-datablock.log, census-base.log, census-head.log, cli-head.log,
gate-*.log, matrix-cli.log, assertions.log, with per-arm machine-readable
output in head.json, base.json, mutant-datablock.json,
mutation-matrix{,-core,-cli}.json and assertions.json. Rerun the A/B with
npx tsx ab-harness.ts --tree <tree> --out <json>, the summary with
python3 ab-driver.py.

Three assertions were wrong in draft and were fixed rather than reported, so
the final counts come only from checks that measure what they claim: cross-run
prompt comparisons first hashed the data-block line, which carries a random
goalId the runtime mints per create, so two identical code paths compared
unequal (fixed by normalising that one line, which turned control-validity C4
green and made B2/B2b measure the progress lines instead of a UUID); and the
gate assertions first regexed vitest's summary raw, where ANSI styling sits
between Tests and the number, so three gates read as unmeasured (fixed by
stripping escapes, which turned G4/G5 green and let G7 report the real
170-versus-168 mismatch). The base arm also needed node_modules wired before
it would import at all — ajv/dist/2020.js resolves from
packages/core/node_modules, which a fresh worktree does not have.

Flakiness gate log

rounds=5 files=6 skipped=0
file packages/cli/src/acp-integration/session/Session.test.ts: (cd packages/cli) npx --no-install vitest run ./src/acp-integration/session/Session.test.ts
file packages/cli/src/nonInteractiveCli.test.ts: (cd packages/cli) npx --no-install vitest run ./src/nonInteractiveCli.test.ts
file packages/cli/src/ui/hooks/use-llm-stream.test.tsx: (cd packages/cli) npx --no-install vitest run ./src/ui/hooks/use-llm-stream.test.tsx
file packages/cli/src/ui/hooks/useMessageQueue.test.ts: (cd packages/cli) npx --no-install vitest run ./src/ui/hooks/useMessageQueue.test.ts
file packages/core/src/goals/goal-continuation-prompt.test.ts: (cd packages/core) npx --no-install vitest run ./src/goals/goal-continuation-prompt.test.ts
file packages/core/src/goals/goal-runtime.test.ts: (cd packages/core) npx --no-install vitest run ./src/goals/goal-runtime.test.ts


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  packages/cli/src/acp-integration/session/Session.test.ts: PPPP
  packages/cli/src/nonInteractiveCli.test.ts: PPP
  packages/cli/src/ui/hooks/use-llm-stream.test.tsx: PPP
  packages/cli/src/ui/hooks/useMessageQueue.test.ts: PPP
  packages/core/src/goals/goal-continuation-prompt.test.ts: PPP
  packages/core/src/goals/goal-runtime.test.ts: PPP

verdict: timeout
summary: only 3 of 5 rounds fit the 15-minute budget; the completed rounds agreed

--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/cli/src/acp-integration/session/Session.test.ts: P (exit 0)
round 1 · packages/cli/src/nonInteractiveCli.test.ts: P (exit 0)
round 1 · packages/cli/src/ui/hooks/use-llm-stream.test.tsx: P (exit 0)
round 1 · packages/cli/src/ui/hooks/useMessageQueue.test.ts: P (exit 0)
round 1 · packages/core/src/goals/goal-continuation-prompt.test.ts: P (exit 0)
round 1 · packages/core/src/goals/goal-runtime.test.ts: P (exit 0)
round 2 · packages/cli/src/acp-integration/session/Session.test.ts: P (exit 0)
round 2 · packages/cli/src/nonInteractiveCli.test.ts: P (exit 0)
round 2 · packages/cli/src/ui/hooks/use-llm-stream.test.tsx: P (exit 0)
round 2 · packages/cli/src/ui/hooks/useMessageQueue.test.ts: P (exit 0)
round 2 · packages/core/src/goals/goal-continuation-prompt.test.ts: P (exit 0)
round 2 · packages/core/src/goals/goal-runtime.test.ts: P (exit 0)
round 3 · packages/cli/src/acp-integration/session/Session.test.ts: P (exit 0)
round 3 · packages/cli/src/nonInteractiveCli.test.ts: P (exit 0)
round 3 · packages/cli/src/ui/hooks/use-llm-stream.test.tsx: P (exit 0)
round 3 · packages/cli/src/ui/hooks/useMessageQueue.test.ts: P (exit 0)
round 3 · packages/core/src/goals/goal-continuation-prompt.test.ts: P (exit 0)
round 3 · packages/core/src/goals/goal-runtime.test.ts: P (exit 0)
round 4 · packages/cli/src/acp-integration/session/Session.test.ts: P (exit 0)

Evidence images

01-ab-head-vs-base-prompt-cells

02-mutation-matrix-15-of-15-killed

03-six-description-claims-falsified

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Triage re-run completed without a new review.

⚠️ The bot's only review on 1709ae7808c16f3f550fd718820a1952a9142084 is a COMMENTED one, which carries no vote — so it has no verdict of its own on this commit, and main needs two approving reviews: an approval left by another account is a separate vote and does not count as the bot's own. Two different things look like this, and the stage-3 comment above says which: the triage skill deferring on purpose at 3/5 — a fork refactor hitting the approval guardrail, or a core change escalated for maintainer awareness, both normal outcomes — or an earlier approval that a push dismissed, leaving only the comment behind, which needs a fresh review.

⚠️ 机器人在 1709ae7808c16f3f550fd718820a1952a9142084 上唯一的评审是 COMMENTED不带票 —— 因此它在该 commit 上没有自己的裁决,而 main 需要两个批准(其他账号的批准是另一张票)。有两种情况长这样,上方的 stage-3 评论会说明是哪一种:triage skill 在 3/5 时有意 defer(fork refactor 命中审批护栏,或核心改动被升级交由维护者把关,两者都是正常结果);或者更早的批准被一次推送作废、只剩下这条评论,此时需要重新评审。

The stage comments above were updated with the latest result. View workflow run.

上方各阶段评论已更新为最新结果。查看工作流运行

@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.

Reviewed — no blockers. Suggestions are inline.

1 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • D2-1 first-turn gate reads the persisted record turnCount, not whether a previous continuation is in the model's context — already reported (packages/core/src/goals/goal-continuation-prompt.ts:184, round 2 review body deferral list, review …

Test Plan (not a blocker): Tests 170 passed — this review observed 29030, 23693, 1971, 298, 1819, 513, 6629 passed.

中文说明

已审查——无阻断问题。 建议见行内评论。

本轮确认的 1 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

Test Plan(非阻断):Tests 170 passed — this review observed 29030, 23693, 1971, 298, 1819, 513, 6629 passed

— qwen3.8-max via Qwen Code /review (v0.23.0)

Comment thread packages/core/src/goals/goal-runtime.ts Outdated
@wenshao

wenshao commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Local verification at 7bf27d5c — round 4, delta only

Verdict: merge-ready. I rebuilt the change locally in two real worktrees and drove it end to end against a scripted model. Every claim the PR makes about its own behaviour holds at this head, all six findings the sandboxed round left open on 1709ae78 are closed except the one that was never a defect, and the mutation matrix is clean.

This is the fourth pass on this PR (triage stages 1–3, the sandboxed verification at 1709ae78, and a round-3 /review at this head), so I only report what those did not cover:

  • the head moved since the sandbox round — 8e399957 renamed the label, reworded the comment, added the design note and corrected the description, and none of that was verified;
  • a live end-to-end run, which the sandbox round explicitly listed under Not covered ("no credentials in this sandbox … the PR's transcript evidence was not independently reproduced"). I reproduced it, on all three production hosts;
  • the /goal resume path, an injection probe against the new trusted line, and a census proving user turns never carry it.

Setup

Two worktrees at 7bf27d5c (head) and 663e55bd (the main tip it merges), each fully built (node scripts/build.js --cli-only && npm run bundle) — Linux, Node v22.22.2. Model turns are served by the repo's own integration-tests/fake-openai-server.ts billing a fixed 41,634 total_tokens per turn, so the ledger is deterministic; no credentials and no live provider are involved. Control validity: the head bundle contains the string Token budget:, the base bundle contains it zero times, and the two source arms load different module bytes (prompt 98f0a364 vs c794bb38, runtime e21c6e93 vs de7959a2). Base renders 11 lines / 1,184 chars whether or not usage is handed to it, so it is a control that genuinely ignores the field rather than a partial build.

1. The central claim, measured on the wire

Same objective, same scripted model, model.goalTokenBudget = 100000; only the build differs. Observation point is the HTTP request body the CLI actually sends, not the usage object at the host boundary.

wire A/B of one continuation prompt

Every scheduled turn of that Goal, end to end, exit 0:

the budget ladder across four scheduled turns

The figures equal the runtime's own ledger at every step, the first turn carries three progress lines and every later turn four, and the hand-off carries none. The same ladder came out of all three production hosts independently — headless (nonInteractiveCli.ts), the interactive TUI (useMessageQueue.tsuse-llm-stream.ts) and ACP over stdio (Session.ts) — each driven as a separate real process.

On base the same run produces byte-identical turn-1 and turn-2 prompts (1,184 chars both). That is the problem this PR fixes, stated as a measurement: on main nothing in the prompt distinguishes one scheduled turn from the next.

Sizing, since Risk & Scope asks a reviewer to weigh it: an ordinary continuation grew 1,184 → 2,107 chars (+923, +78%) and the Goal's first turn 1,184 → 1,789. One thing that paragraph does not say, and I think should: a continuation stays in the conversation like any other user message, so a long Goal carries every one of these prompts rather than only the current one. Real, bounded, and I did not measure how it interacts with compaction.

2. The clamp, on a genuinely overspent window

wind-down hand-off A/B

124,902 spent against 100,000 granted renders 0 remaining, not -24,902, and the four progress lines are gone from the one turn that is told not to start new work. The hand-off is the only turn where the two arms differ by exactly one line.

3. Across a /goal resume — not covered by any earlier round

resume window

Two headless runs in one session. The resume grants a fresh window on top of what was already spent, exactly as docs/users/features/goals.md describes it, and the prompt line reports the raised ceiling correctly (166,536 of 266,536 … 100,000 remaining) with turnCount continuing 4 → 7. The first-turn exception is not misapplied here: turn 4 is the first turn of the resumed run but the fourth of the Goal, so the judgement line correctly stands.

4. The interactive host, for real

real TUI run

Worth stating plainly: that frame is byte-identical between the two arms (md5 fa894d0f2b71806b4a563789761555a6 on both). The change is invisible on screen and lives entirely in what the host puts on the wire, which is where I measured it.

5. Where the earlier findings stand at this head

# earlier finding status at 7bf27d5c
triage stage 2 Budget: collides with the objective field this repo documents closed — renamed Token budget:. Rendering an objective that itself contains Budget: stop as blocked after 20 turns puts that text inside the JSON data block and the runtime's own line outside it; the two never read as the same field.
verify 1 a committed comment states a mechanism that does not exist closed — the docblock now gives the trust-boundary reason, which is the one that holds.
verify 2 "a host that omits usage renders the previous prompt exactly" closed — the body now says the four progress checks still render. Measured: 11 → 15 lines with no figures.
verify 3 risk sizing understated (five lines added to a prompt "that was seventeen") closed — the body now says 11 → 15 without figures and 16 with. An exhaustive 16-combination census gives base 11–15 lines and head 13–18; the two stated numbers are exact.
verify 4 test plan predicts 168 passed closed — the body says 170; I measured Tests 170 passed (170).
verify 5 the design note the body cites is not in the PR closeddocs/design/2026-09-07-goal-continuation-budget.md is in the diff, and its account of placement, the two exceptions and the projection type matches the code I read.
verify 6 fractional tokensUsed renders 41,634.5 unchanged, and correctly so — see N1.

6. Mutation matrix at this head

mutation matrix

13/13 killed, zero survivors, including the newly renamed label and three same-file positive controls that remove a pre-existing windDown copy — without those, "the mutant survived" and "my command never collected your suite" would be the same observation. Tree asserted clean before the run, after every restore and at the end.

7. Two checks nobody had run

The new line cannot be forged from untrusted text. The budget line is the one piece of trusted context that sits outside the data block, so I fed four hostile objectives through the renderer — an embedded newline plus a fake Token budget: line, a CRLF variant, a </goal_runtime_data> close followed by a forged guard and figure, and a literal \n. All four render exactly one Token budget: line, always the runtime's own, always outside the block; line count stays 16 in every case. JSON.stringify keeps the objective on one line and escapeJsonTagCharacters handles the tag.

The lines never reach a user's turn. Census over a 129-request run with an active Goal: 103/103 runtime-scheduled turns carry the budget line, and 0/26 of everything else does — including get_goal tool results and an ordinary user message sent on --continue while the Goal was active.

8. Gates

gate result
packages/core — the two suites from the test plan Tests 170 passed (170)
packages/cli — the five suites from the test plan 1377 passed | 1 skipped (1378), 102 s
npm run typecheck (repo-wide) exit 0
prettier --check on all 15 changed files clean
eslint --max-warnings 0 on the 13 changed TS files clean
CI on this head 23 pass, 0 fail, 26 skipping

Not covered

Live provider traffic (the model was scripted, so this reproduces the prompt bytes and the runtime's arithmetic, not a real model's reaction to the figures); macOS and Windows, which the PR marks ⚠️ — formatting is pinned to toLocaleString('en-US') so it does not follow the host locale; the repo-wide suite and the integration suites; and whether the progress lines measurably reduce idle turns, which the PR scopes out itself.

Non-blocking notes

N1 — fractional spend still renders decimals, and I would leave it. isNonNegativeNumber admits a fractional tokensUsed, which reaches toLocaleString as Token budget: 41,634.5 of …. takeGoalTurnTokens sums integer totalTokenCount values, so nothing shipped can produce it. Guarding it would add a branch no test can reach honestly.

N2 — nothing pins the whole chain permanently. The runtime tests assert the usage object, the prompt tests assert the text, the host tests inject at the host seam; no fixture drives runtime → host → rendered text. My E2E closes that for this round only. A single integration fixture that drives the real runtime and asserts on the rendered string would keep it closed.

N3 — the usage === undefined branch is unreachable from every shipped host, since flushContinuation populates usage unconditionally. The description and design note are right about what happens there, but a reader may take it for a live configuration; one clause in the design note saying only an external embedder can reach it would settle that.

N4 — the projection is one rename away from silence. GoalContinuationUsage = Pick<GoalRecord, …> is the right call and fixes the drift the earlier rounds worried about, but a field renamed on GoalRecord still costs the line rather than failing the build. Already raised in round 3; I mention it only because I looked and agree it is a suggestion, not a blocker.

中文版 — 在 7bf27d5c 上的本地验证(第 4 轮,仅增量)

结论:可以合入

我在本地用两个真实 worktree 重新构建了这次改动,并对着脚本化模型跑了完整的端到端。PR 对自身行为的每一条说法在这个 head 上都成立;沙箱轮在 1709ae78 上留下的 6 条发现,除了那条本来就不是缺陷的以外全部关闭;变异矩阵干净。

这是本 PR 的第 4 轮(triage stage 1–31709ae78 上的沙箱验证、以及本 head 上的第 3 轮 /review),所以我只报告它们没有覆盖的部分:

  • 沙箱轮之后 head 变了 —— 8e399957 改了标签、重写了注释、补了设计文档、修正了描述,这些都没被验证过;
  • 真实端到端运行,沙箱轮在 Not covered 里明确写着没做("沙箱里没有凭据……PR 的 transcript 证据没有被独立复现")。我复现了,而且是在三个生产 host 上都复现;
  • /goal resume 路径、针对新增可信行的注入探针,以及"用户自己的轮次绝不携带这些行"的普查。

环境

两个 worktree:head 7bf27d5c、base 663e55bd(它合入的 main tip),各自完整构建(node scripts/build.js --cli-only && npm run bundle)—— Linux、Node v22.22.2。模型轮由仓库自带的 integration-tests/fake-openai-server.ts 提供,每轮固定计 41,634 total_tokens,账本因此是确定的;全程无凭据、不接真实 provider。对照有效性:head bundle 里有字符串 Token budget:,base bundle 里出现 0 次;两个源码臂加载的模块字节也不同(prompt 98f0a364 vs c794bb38,runtime e21c6e93 vs de7959a2)。base 无论是否收到 usage 都渲染 11 行 / 1,184 字符,所以它是真正忽略该字段的对照,而不是一个构建不全的臂。

1. 核心主张,在线上测得

同一个目标、同一个脚本模型、model.goalTokenBudget = 100000,只有构建不同。观测点是 CLI 真正发出的 HTTP 请求体,而不是 host 边界上的 usage 对象。

wire A/B

那个 Goal 的每一个调度轮,端到端,退出码 0:

budget ladder

数字与运行时自己的账本每一步都相符;第一轮 3 行指引、之后每轮 4 行、交接轮 0 行。同一条阶梯在三个生产 host 上各自独立地跑出来了 —— headless(nonInteractiveCli.ts)、交互式 TUI(useMessageQueue.tsuse-llm-stream.ts)、以及 stdio 上的 ACP(Session.ts),每个都是单独的真实进程。

base 侧同样的运行里,第 1 轮和第 2 轮的提示词逐字节相同(都是 1,184 字符)。这就是本 PR 要解决的问题的度量化表述:在 main 上,提示词里没有任何东西能区分相邻两个调度轮。

体量方面(Risk & Scope 要求评审者权衡这一点):普通续跑轮从 1,184 涨到 2,107 字符(+923,+78%),Goal 首轮 1,184 → 1,789。那一段没说、但我认为应该说的一点:续跑提示词会像其他用户消息一样留在对话里,所以一个长跑的 Goal 携带的是它们全部,而不只是当前这一条。真实、有界,且我没有测量它与压缩(compaction)之间的相互作用。

2. 真正超支时的夹取

wind-down A/B

100,000 的额度花掉 124,902,渲染出来是 0 remaining 而不是 -24,902;四行指引从这个被告知"不要开始新工作"的轮次上消失了。交接轮是两臂只差一行的唯一一轮。

3. 跨 /goal resume —— 此前任何一轮都没覆盖

resume window

同一会话里的两次 headless 运行。resume 在已花掉的量之上又授予一个新窗口,与 docs/users/features/goals.md 的描述完全一致;提示词行正确报出抬高后的上限(166,536 of 266,536 … 100,000 remaining),turnCount 从 4 连到 7。首轮豁免在这里没有被误用:第 4 轮是 resume 后那次运行的第一轮,却是这个 Goal 的第四轮,所以"判断上一轮"那行理应保留 —— 它确实保留了。

4. 交互式 host 的真实运行

real TUI

有必要挑明:这一帧在两臂之间逐字节相同(两边 md5 都是 fa894d0f2b71806b4a563789761555a6)。这次改动在屏幕上是不可见的,它完全活在 host 送上线的内容里 —— 而那正是我测量的地方。

5. 此前的发现在本 head 上的状态

# 此前的发现 7bf27d5c 上的状态
triage stage 2 Budget: 与仓库自己文档化的目标字段撞名 已关闭 —— 改名为 Token budget:。渲染一个本身含有 Budget: stop as blocked after 20 turns 的目标时,那段文本落在 JSON 数据块里,而运行时自己那行在块外;两者不会被读成同一个字段。
verify 1 已提交的注释写了一个并不存在的机制 已关闭 —— 注释改成了成立的那个理由(信任边界)。
verify 2 "不提供 usage 的 host 渲染出来就是原来那段提示词" 已关闭 —— 正文现在说四行进展指引仍会渲染。实测:无数字时 11 → 15 行。
verify 3 风险体量被低估("原本 17 行的提示词加了 5 行") 已关闭 —— 正文现在写的是无数字 11 → 15 行、有数字 16 行。穷举 16 种开关组合:base 11–15 行、head 13–18 行,正文这两个数字精确无误。
verify 4 验证方式里写 168 passed 已关闭 —— 正文写 170,我实测 Tests 170 passed (170)
verify 5 正文指向的设计文档不在 PR 里 已关闭 —— docs/design/2026-09-07-goal-continuation-budget.md 已在 diff 中,其对位置、两处豁免和投影类型的叙述与我读到的代码一致。
verify 6 tokensUsed 为小数时渲染成 41,634.5 未改动,而且我认为不该改 —— 见 N1。

6. 本 head 上的变异矩阵

mutation matrix

13/13 全部被杀死,零幸存,其中包括新改名的标签,以及 3 个同文件阳性对照(删掉一个已有的 windDown 拷贝)—— 没有它们的话,"变异体幸存了"和"我的命令根本没收集到你的用例"是同一个观测。变异开始前、每次还原后、以及结束时都断言了工作树干净。

7. 两项此前无人做过的检查

新增的这一行无法被不可信文本伪造。 预算行是唯一一个位于数据块之外的可信上下文,所以我把四种敌意目标喂给渲染器 —— 内嵌换行加一条假的 Token budget: 行、CRLF 变体、</goal_runtime_data> 闭合后跟伪造的守卫行和数字、以及字面量 \n。四种情况都只渲染出一条 Token budget: 行,始终是运行时自己的那条,始终在块外;行数在每种情况下都保持 16。JSON.stringify 把目标压在一行里,escapeJsonTagCharacters 处理标签。

这些行绝不会落到用户自己的轮次上。 在一次带活跃 Goal 的 129 次请求的运行上做普查:103/103 个运行时调度轮携带预算行,其余 0/26 全都不带 —— 包括 get_goal 的工具结果,以及 Goal 活跃期间通过 --continue 发出的一条普通用户消息。

8. 门禁

门禁 结果
packages/core —— 验证方式里的两个套件 Tests 170 passed (170)
packages/cli —— 验证方式里的五个套件 1377 passed | 1 skipped (1378),102 秒
npm run typecheck(全仓) exit 0
prettier --check 全部 15 个改动文件 干净
eslint --max-warnings 0 13 个改动 TS 文件 干净
本 head 上的 CI 23 通过、0 失败、26 skipping

未覆盖

真实 provider 流量(模型是脚本化的,所以复现的是提示词字节和运行时的算术,不是真实模型对这些数字的反应);macOS 与 Windows —— PR 自己标了 ⚠️,格式固定用 toLocaleString('en-US'),不跟随宿主 locale;全仓测试与集成测试;以及指引行是否能可测量地减少空转轮,这一点 PR 自己已列为范围外。

非阻塞事项

N1 —— 小数额度仍会渲染出小数,我倾向于保持现状。 isNonNegativeNumber 允许小数 tokensUsed,它会带着小数进入 toLocaleString,渲染成 Token budget: 41,634.5 of …。但 takeGoalTurnTokens 累加的是整数 totalTokenCount,已发布的代码路径产生不出这种值。为它加守卫等于加一个没有测试能诚实覆盖到的分支。

N2 —— 没有任何东西长期钉住整条链路。 运行时测试断言 usage 对象、提示词测试断言文本、host 测试在 host 缝隙处注入;没有任何 fixture 驱动 运行时 → host → 渲染文本。我这一轮的 E2E 只在本轮把这个缺口补上了。一条驱动真实运行时并断言渲染字符串的集成 fixture 可以把它长期补住。

N3 —— usage === undefined 分支在所有已发布的 host 上都不可达,因为 flushContinuation 无条件填充 usage。描述和设计文档对那里会发生什么说得没错,但读者可能会把它当成一种现实存在的配置;在设计文档里加一句"只有外部嵌入方能走到这条分支"就能说清。

N4 —— 这个投影离一次改名只有一步之遥。 GoalContinuationUsage = Pick<GoalRecord, …> 是对的选择,也修掉了此前几轮担心的漂移;但如果 GoalRecord 上的字段被改名,代价仍然是少一行,而不是构建失败。第 3 轮已经提过;我写在这里只是因为我看过之后同意它是建议而非阻塞。


🤖 Generated with Claude Code — Claude Opus 5 (1M context)

…948/upstream-main-latest' into HEAD

# Conflicts:
#	packages/core/src/goals/goal-runtime.test.ts
wenshao
wenshao previously approved these changes Sep 8, 2026
@wenshao

wenshao commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@wenshao
wenshao enabled auto-merge September 8, 2026 07:06
@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ❌ not passed — findings reported (agent verdict) - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 50 passed · 1 failed · 51 total

Flakiness gate: ⚠️ timeout — only 3 of 5 rounds fit the 15-minute budget; the completed rounds agreed

中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:50 通过 · 1 失败 · 51 总计

抖动门:⚠️ timeout — only 3 of 5 rounds fit the 15-minute budget; the completed rounds agreed

Verification report

PR 11257 — deep verification (round 2)

Verdict: findings — 50 pass / 1 fail / 51 total scripted assertions.
Verified head 0bac4bbfec69fd2a65cda38256df09c15720f287 (HEAD^2), base
bbc8ea64882054a77a3fda73140339945de3dafa (HEAD^1), merge 0c752f4c.

The code is correct, and the previous round's three substantive corrections
were all adopted.
Every assertion about the shipped behaviour of this change
passed: the budget line and its arithmetic, the four progress lines, the
first-turn gate, the wind-down suppression, the zero clamp, the placement, the
unbounded-Goal wording, and all four host hops. The mutation matrix killed
9/9 guards with the suite that owns the file plus 6/6 same-file positive
controls
, and both survivors were adjudicated to non-defects (one was my own
too-narrow suite choice, the other is behaviourally inert).

The single failing assertion is a stale number in the PR body, not a defect:
the Reviewer Test Plan predicts Tests 170 passed (170) and the command yields
187 passed (187) at this head. It is reported as a finding because it is
the one step a reviewer is asked to run and the number they will see differs.
Four further Nice-to-haves and one uncounted observation are in Findings.

中文摘要

结论:findings — 51 条脚本化断言中 50 通过 / 1 失败。已验证 head
0bac4bbf,base bbc8ea64(本轮 base 与上一轮的 f1ed3bc3 已不同,所有沿用
的测量都在新 head 上重跑,未做报告对比)。

代码本身正确,上一轮的三条实质性更正全部被采纳。 关于这次改动实际行为的
断言全部通过:预算行及其算术、四行进展指引、首轮豁免、wind-down 抑制、剩余量
夹到 0、位置、无额度 Goal 的措辞、以及四个 host 的每一跳。变异矩阵
9/9 个防护点被其所属文件的测试套件杀死,另有 6/6 个同文件阳性对照变红
两个幸存者都已裁定为非缺陷(一个是我自己选的套件范围太窄,另一个在行为上完全
无影响)。

A/B 结论(见 Central claim + A/B 表与
01-ab-head-vs-base-prompt-cells.png):用真实 createGoalRuntime 驱动一个
3 轮 Goal,head 侧 4/4 个续跑提示词都带预算行,数字与运行时自己的账本逐项相符;
base 侧 0/4。首轮 3/4 行指引("判断上一轮"那行按设计扣下),其后每轮 4/4;base
全为 0/4。PR 正文 Evidence 里引用的三行预算数字、那段六行提示词结尾、以及
wind-down 交接轮的三行结尾,都由这个独立 harness 逐字复现(A8/A10/A11)。

唯一失败的断言是 PR 正文里的一个过期数字,不是缺陷:评审验证方式写的是
Tests 170 passed (170),实测 187 passed (187)。之所以仍作为发现上报,
是因为这正是评审被要求亲自执行的那一步,而他看到的数字会不一样。

上一轮 5 条发现的状态(详见 Previous-finding status 表):#1 代码注释里的
错误机制 → 已修#2 "不提供 usage 的 host 渲染出的就是原来那段提示词" →
已修#3 风险段落把 base 说成 17 行 → 已修(新正文的 11→15/16 与实测
逐项吻合);#4 测试数过期 → 仍存在(数值从 168/170 变成 170/187)
#5 小数 token 渲染 → 仍存在(观察项,未计入断言)。上一轮那条结构性备注
(没有任何单个测试贯通 运行时 → host → 渲染文本)也仍然存在

未覆盖范围:逐 commit 归因(浅克隆 depth 2,本地 HEAD^1..HEAD^2 只有 1 个
commit,快照列了 11 个);真实模型 E2E(沙箱无凭据——本轮复现的是提示词字节
本身,不是活会话,也不是模型对这些数字的反应);指引行是否能减少空转轮(PR 自己
已列为范围外);base 侧 vitest(本轮未跑,属主动取舍);上一轮的数据块变异体
本轮未重跑(该注释已不再声称那个机制,行为面由 S2a/S2b 覆盖);仓库全量测试与
集成测试;macOS/Windows。

Previous-finding status

The previous round verified head 1709ae78 against base f1ed3bc3. Both
sides moved
: the head gained 6 commits (including 8e399957 "fix(goal):
address continuation prompt review"
and 4fa4be63 "refactor(goal):
centralize continuation payload"
), and the base tip advanced to bbc8ea64.
Neither 1709ae78 nor f1ed3bc3 is reachable in this depth-2 checkout, so
every row below was re-measured from scratch at the new head — nothing was
carried forward by diffing the old report, and no input-closure shortcut
applied (the base tip itself changed). Witness for rows 1–3 and for the
new-delta rows: 03-description-claims-re-measured-and-new-delta.png.

# Previous finding Sev Status at 0bac4bbf Evidence
1 Committed comment above renderBudgetLine stated a false mechanism ("that block is compared by content … a number that moves every turn would make every turn look like an edit") Suggestion fixed The clause is gone. The comment now reads "Kept out of the data block on purpose: these are trusted runtime figures, while that block is explicitly framed as untrusted task data" — the trust-boundary ground that does hold. D8; gates.commentDetail
2 "A host that omits usage renders the previous prompt exactly" — false, the four progress lines are still added Suggestion fixed Body, migration notes and 中文 all now say the line is omitted and "all four non-wind-down progress checks render". Measured: head 15 lines / 4 progress vs base 11 lines / 0. F3, D2
3 Risk sizing "five lines added to a prompt that was seventeen" — base was 11, and no 17-line base rendering exists Correction fixed Body now says "grows from 11 to 15 lines without figures and 16 with them". Census of all 16 flag combos: base 11..15 lines (no 17), head 13..18 (17 exists). D1, D3
4 Reviewer Test Plan's expected count stale (body said 168, measured 170) Nice to have stands — value moved Body now says 170 passed (170); measured 187 passed (187). git diff HEAD^2..HEAD is empty, so 187 is the head's own count. This is the round's only failing assertion. D6
5 Fractional tokensUsed renders with decimals Nice to have stands Token budget: 41,634.5 of 30,000,000 tokens used, 29,958,365.5 remaining; 2 Goal turns finished. tokensUsed is still validated by isNonNegativeNumber while its neighbours revision/turnCount use isNonNegativeInteger. Observation, not counted in assertions.json
Structural note: no single test drives the whole chain runtime → host → rendered prompt note stands The three host tests still inject usage at the host seam (nonInteractiveCli.test.ts wraps the bound host and adds the field), and the runtime tests assert the usage object rather than the text. Assertions A3/A5 close it for this round only

The previous round's Checked and clear list was also re-verified rather than
trusted: still exactly three bindGoalTurnHost registrations
(Session.ts:2323, AppContainer.tsx:2635, nonInteractiveCli.ts:674); the
fourth origin: 'runtime' construction (use-llm-stream.ts:5063) still builds
a GoalTurnBinding with no continuationContext, so it is not a dropped copy
(R6); escapeJsonTagCharacters still wraps the serialisation and is untouched
by the diff; and both user-origin entries (nonInteractiveCli.ts:1483,
Session.ts:4785) are explicit literals with no usage key (R3).

One delta worth naming: the base prompt module is byte-identical to the
previous round's base
(c794bb38… at both f1ed3bc3 and bbc8ea64), while
the head module changed (1c035e12…23e48ff7…). The base runtime module
did move (de7959a2…5f4db331…) — main grew goal-runtime.test.ts between
the two base tips (146 it blocks at bbc8ea64, against the 136 the previous
report recorded at f1ed3bc3), which is where finding 4's 170 → 187 drift comes
from: the body's figure predates the head's own merge of main.

Scope

Central claim. Every runtime-scheduled Goal continuation prompt now opens
with a Token budget: line (spend / window / remainder / turns finished) and
carries four standing progress lines — three on the Goal's first turn, none on
the wind-down hand-off — at every host.

Secondary claims. (S1) usage survives every hop from the runtime to the
text the model receives, at all three hosts. (S2) Figures that move every turn
do not disturb objective-change detection.

New this round (the delta). 4fa4be63 replaced each host's field-by-field
queue copy with a rest-spread and added a shared GoalContinuationUsage
projection plus a new renderGoalContinuationTurn / GoalContinuationTurn
surface; the label was renamed Budget:Token budget:. That changes the
shape of every guard the previous matrix pinned, so the matrix was rebuilt
rather than re-read.

Out of scope by choice, and listed under Not covered: live-model E2E,
per-commit attribution, base-arm vitest, the repo-wide suite.

Central claim + A/B

Both arms drive the real createGoalRuntime with an in-memory journal (the
persistence seam the runtime itself declares) and a host whose startGoalTurn
calls the same tree's real buildGoalContinuationParts — exactly what all
three production hosts do — then records parts[0].text. The loop reproduces
the production host sequence (permitForTurn(turnKey) ?? beginTurn(turnKey),
then markTurnDelivered, then finishTurn), and additionally re-renders
through an emulated Session.ts queue copy carrying the host-only fields, to
test the new rest-spread. The observation point is the rendered prompt the
model would receive, not the usage object at the host boundary.
Witness: 01-ab-head-vs-base-prompt-cells.png.

Scenario threeTurns, default 30,000,000 grant, turns billed 41,634 / 43,864 / 50,000:

continuation HEAD budget line (verbatim) HEAD progress BASE
#0 (scheduled by create) Token budget: 0 of 30,000,000 tokens used, 30,000,000 remaining; 0 Goal turns finished. 3/4 11 lines, no figures, 0/4
#1 Token budget: 41,634 of 30,000,000 tokens used, 29,958,366 remaining; 1 Goal turn finished. 4/4 11 lines, no figures, 0/4
#2 Token budget: 85,498 of 30,000,000 tokens used, 29,914,502 remaining; 2 Goal turns finished. 4/4 11 lines, no figures, 0/4
#3 Token budget: 135,498 of 30,000,000 tokens used, 29,864,502 remaining; 3 Goal turns finished. 4/4 11 lines, no figures, 0/4

4/4 flip from absent to present (A1 vs control A2), and each line equals
the figures recomputed independently from the runtime's own ledger at schedule
time (A3), which in turn equals the usage object handed to the host (A4).
A5 is the destination check: the figures appear in the rendered text, not
only in the boundary object. A9 bounds the change: the final ledger is
identical on both arms (tokensUsed 135498 / tokenBudget 30000000 / turnCount 3), so the PR changes what the model is told, not what the runtime accounts.

The PR body's transcript evidence reproduces byte-for-byte. All three
quoted budget lines (A8), the full six-line tail of the third prompt (A10),
and the three-line wind-down tail (A11) are verbatim. That corroborates that
the author's transcript was read off this code path — the shape, not a live
session (see Not covered).

Other cells (all in the capture):

cell HEAD BASE
windDown (grant 30,000, turn 0 bills 62,026) Token budget: 62,026 of 30,000 tokens used, 0 remaining; 1 Goal turn finished. + hand-off lines, 0/4 progress, 14 lines no figures, hand-off lines only, 13 lines — so the hand-off grows by exactly the budget line (W3)
noBudget (grant Infinity) Token budget: 0 tokens used, with no budget on this Goal; 0 Goal turns finished. (none)
overspentClamp (99,999 of 30,000) 0 remaining, no negative figure anywhere in the line n/a
firstTurnUsageZero (turnCount: 0) 3/4, flags [T,T,F,T] — only the judgement line held back n/a
turnCountOne 4/4 and singular 1 Goal turn finished n/a
no usage at all no budget line, 4/4 progress, 15 lines no budget line, 0/4, 11 lines
placement, on the continuation that actually carries the notice after the authoritative-objective guard, after </goal_runtime_data>, before the objective-updated notice (D4) n/a
objectiveUpdated across 4 continuations with moving figures [false,false,false,false] (S2a) [false,false,false,false]
positive control, run on BOTH arms: a real objective edit after a delivered turn fires — objectiveUpdated=true, 17 lines (S2b) fires — 12 lines

S2b is what makes S2a mean something: the one-shot notice is observable at
this seam, so "the moving figures never fired it" is a measurement and not a
dead probe. Building that control also surfaced a runtime behaviour worth
recording — an edit dispatched before the queued turn is delivered
invalidates and re-schedules the continuation under a new turnId and
revision, and the notice correctly stays owed to the replacement.

Control validity (C1C5, in the capture): the two arms load different
prompt bytes (23e48ff7… vs c794bb38…) and different runtime bytes
(49855ce2… vs 5f4db331…); the two files they both import
(goal-protocol.ts, formatters.ts) are byte-identical across arms, so the
A/B differs by nothing but the change under test. C4 is the workspace-symlink
guard: readlink -f node_modules/@qwen-code/qwen-code-core from the base
worktree resolves into the head tree, so the harness imports each arm's
source by absolute path and asserts the code under test resolves nothing across
the boundary — its only non-relative imports are node:crypto and
@google/genai, identical on both arms. C5 proves base is a true control
that ignores usage rather than a partial build: all 8 census flag-combos
render identically with and without it. The PR touches no package.json or
lockfile, so sharing the installed root node_modules is clean.

Corrections

None new. The three corrections the previous round raised against the
description and a committed comment were all adopted, and the re-measured
wording now matches the code (rows 1–3 of the status table). Two body claims
that were not previously checked were verified this round and are accurate:
the label rename is disclosed honestly ("The examples use the current
renderer's Token budget: label … without claiming that the historical
transcript already used the renamed label"
), and usage?: really is optional
on the public renderer contract (R7).

Findings

Ordered by severity. None is a defect in the shipped behaviour. 2–5 are
completeness reporting about tests and docs, not merge conditions.

1. Nice to have — the Reviewer Test Plan's expected count is stale (the
round's only failing assertion).
The plan predicts:

 Test Files  2 passed (2)
      Tests  170 passed (170)

The command yields Test Files 2 passed (2), Tests 187 passed (187).
Reproduce:

cd /__w/qwen-code/qwen-code/packages/core && \
  npx vitest run src/goals/goal-continuation-prompt.test.ts src/goals/goal-runtime.test.ts

git diff HEAD^2..HEAD is empty, so the merge introduced nothing — 187 is what
the verified head itself produces, and the body's figure predates the head's own
merge of main (which had grown goal-runtime.test.ts; see the status table).
This is the same finding as the previous round's #4 with the value moved, i.e.
it was edited once and went stale again for a different reason. Bounded: it
costs a reviewer a moment of doubt, nothing else.

2. Nice to have — the core package has no test that drives
renderGoalContinuationTurn with figures.
The new turn-shaped entry point is
the one every production host now calls, and in its own package it is exercised
only by goal-continuation-prompt.test.ts's buildGoalContinuationParts
block — whose two cases both pass no usage. Proven by mutant M5
(destructure usage out of the ...hints sweep): the core suite stayed green
at 19/19, while all three cli suites went red, each on exactly the test the
PR added for that host:

cd /__w/qwen-code/qwen-code
python3 tmp/pr11257-verify-20260908-074556/adjudicate-survivors.py /tmp/survivors.json

So the guard is pinned (MX2) — but only from packages/cli. The fixture
that would pin it in core is one line added to either existing case in the
buildGoalContinuationParts describe: pass usage: { tokensUsed: 1234, tokenBudget: 30000000, turnCount: 4 } and expect the Token budget: line in
part.text. Worth having because the whole point of 4fa4be63 was to make the
renderer the single place a field can be lost.

3. Nice to have — toEqual does not pin the tokenBudget omission. In
goal-runtime.ts the usage projection deliberately omits the key when a Goal is
unbounded:

...(snapshot.goal.tokenBudget === undefined ? {} : { tokenBudget: snapshot.goal.tokenBudget }),

Mutant M7 replaces that with an unconditional tokenBudget: snapshot.goal.tokenBudget and survived goal-runtime.test.ts at 168 green,
even though it('omits the ceiling for a Goal that has none') looks like it
pins exactly this. The mechanism is proven, not assumed (MX5): that test
asserts with toEqual, and a scratch vitest probe run under the project's own
vitest shows expect({tokensUsed: 0, tokenBudget: undefined, turnCount: 0}).toEqual({tokensUsed: 0, turnCount: 0}) passes while toStrictEqual on
the same pair throws (2/2, gate-toequal-probe.log; the scratch file was
removed and packages/ verified clean). exactOptionalPropertyTypes is not
set in any tsconfig, so the type system does not catch it either.

Adjudicated as a coverage gap, not a defect — and bounded by measurement
(MX4): driving the full A/B harness with M7 applied gives 0 rendered-prompt
differences across all 12 cells
; the only effect is that the usage object
gains an explicit tokenBudget: undefined key on the 2 unbounded-Goal cells.
renderBudgetLine branches on === undefined, so the prompt cannot tell. The
one-word fix is toStrictEqual in that one test; the clause is worth keeping
either way, since an explicit-undefined key is exactly what a future
'tokenBudget' in usage reader would trip on.

4. Nice to have — the design doc describes the host copies the refactor
removed.
docs/design/2026-09-07-goal-continuation-budget.md (added by this
PR, 117 lines, otherwise accurate and a real improvement over the previous
round's missing-file finding) still says "The three hosts copy it into their
queue entries alongside the fields they already copy"
and, under Scope, "one
field copied through each host's queue entry"
. After 4fa4be63 there is no
per-field copy: each host does const { permit, ...continuation } = input and
spreads. The doc also predates the refactor's two new exports — it names
GoalContinuationUsage but never GoalContinuationTurn or
renderGoalContinuationTurn, which are now the surface every host renders
through. Its Verification section is still correct (each host case does fail
when its copy is removed — M8–M11 all killed).

5. Nice to have — renderGoalContinuationTurn's ...hints sweep forwards
host internals into the core renderer.
The new function is:

const { permit, continuationContext, ...hints } = turn;
return renderGoalContinuationPrompt({ goalId: permit.goalId, revision: permit.revision, objective: continuationContext, ...hints });

Production hosts pass their whole queue entry, so hints also collects
turnKey, controller (a live AbortController), origin, modelStarted
and kind. Measured on 12/12 driven continuations (R2): those four keys
do reach the prompt-input object. Bounded, and explicitly not a defect —
R1 shows the rendered text is byte-identical with and without them on every
cell, because renderGoalContinuationPrompt only reads known keys, and no
host field collides with a hint name today. The reason to note it is that the
sweep is what makes the refactor safe (a new hint can never be dropped again),
and the same property means a future host field named usage, windDown,
objectiveUpdated or verifierFeedback would silently override the runtime's
value with no test necessarily noticing. Destructuring the four known hints
explicitly would keep the safety and drop the leak; the type signature cannot
enforce it either way, since passing a variable skips excess-property checks.

6. Observation, not counted in assertions.json — fractional spend renders
with decimals.
renderCells.fractionalSpend gives
Token budget: 41,634.5 of 30,000,000 tokens used, 29,958,365.5 remaining; 2 Goal turns finished. Unchanged from the previous round and still bounded:
goal-reducer.ts validates tokensUsed with isNonNegativeNumber while the
adjacent revision and turnCount use isNonNegativeInteger, but
takeGoalTurnTokens (chatRecordingService.ts:2083) returns accumulated
totalTokenCount values, which are integers, so a fractional ledger is not
reachable in practice. Recorded because the asymmetry is one character of
diff (isNonNegativeInteger) if a maintainer wants it closed.

Mutation matrix

9/9 guards killed by the suite that owns the file, 6/6 same-file positive
controls red, 2 survivors both adjudicated. Witness:
02-mutation-matrix-9-of-9-killed-plus-survivor-adjudication.png. Every
mutated file gets a control corrupting pre-existing code in the same object
literal or block, which proves the chosen vitest command collects tests that
exercise that file — without it, "the mutant survived" and "my command never
ran your suite" are the same observation.

mutant file suite result
M1 drop the budget line goal-continuation-prompt.ts prompt KILLED 6 red
M2 drop the Math.max(0, …) clamp goal-continuation-prompt.ts prompt KILLED 2 red — incl. never reports a negative remainder
M3 drop the first-turn gate (turnCount > 0) goal-continuation-prompt.ts prompt KILLED 1 red — asks for no judgement of a previous turn on the first one
M4 render progress lines on wind-down too goal-continuation-prompt.ts prompt KILLED 1 red
M5 new: renderGoalContinuationTurn drops usage from its sweep goal-continuation-prompt.ts prompt, then all 5 suites SURVIVED prompt (19 green) → KILLED by 3 cli suites; adjudicated: pinned, my suite choice was too narrow (finding 2)
control reword a pinned wind-down line goal-continuation-prompt.ts prompt KILLED 1 red
M6 drop usage, from the startGoalTurn payload goal-runtime.ts runtime KILLED 3 red
M7 new: set tokenBudget unconditionally goal-runtime.ts runtime SURVIVED 168 green; adjudicated: inert at the prompt + toEqual (finding 3)
control corrupt turnCount goal-runtime.ts runtime KILLED 3 red
M8 new: replace the rest-spread copy with an explicit copy omitting usage Session.ts Session KILLED 1 red
control corrupt the pre-existing turnKey Session.ts Session KILLED 27 red
M9 new: same, explicit copy omitting usage nonInteractiveCli.ts headless KILLED 1 red
control corrupt the pre-existing turnKey nonInteractiveCli.ts headless KILLED 11 red
M10 new: same, explicit copy omitting usage useMessageQueue.ts queue KILLED 1 red
control corrupt the pre-existing turnKey useMessageQueue.ts queue KILLED 5 red
M11 new: render with usage stripped from the queued turn use-llm-stream.ts stream KILLED 1 red — renders the queued spend figures into the synthetic Goal turn
control flip the pre-existing shouldProceed use-llm-stream.ts stream KILLED 6 red

M8–M11 are the refactor's own hazard class, and the author's claim in the
design doc — "each case fails when its copy is removed" — holds at this head:
all four were killed by exactly one test, and in each case by the test the
PR added for that host, which is the attribution that matters (a mutant killed
by an unrelated red test proves nothing about the guard):

mutant the one test that went red
M8 Session.ts Session > prompt > auto-compress > carries the spend figures into the continuation prompt
M9 nonInteractiveCli.ts runNonInteractive > carries the spend figures into a scheduled Goal continuation
M10 useMessageQueue.ts useMessageQueue > carries the runtime spend figures onto the queued Goal turn
M11 use-llm-stream.ts useLlmStream > renders the queued spend figures into the synthetic Goal turn

Note what the refactor changed about the matrix itself:
the previous round's per-host mutant was "delete the usage, line", which no
longer exists as a shape. Reverting a host to the old field-by-field copy and
omitting usage (M8–M10) is the surviving form of the same hazard, and the
rest-spread is what makes it not happen by accident.

No combination row was owed: no two hunks in this PR defend the same hazard
from different directions — the runtime produces usage once, the renderer
consumes it once, and each host is a separate hop, so every guard is observable
alone (which the 9 single-hunk kills confirm).

Not covered

  • Per-commit attribution. Depth-2 checkout: only the merge, the base tip
    and the PR head exist. git rev-list --count HEAD^1..HEAD^2 returns 1
    while $QWEN_VERIFY_CONTEXT lists 11 commits — at a shallow boundary
    that command returns a plausible small number instead of erroring. The
    aggregate HEAD^1..HEAD diff was verified instead. In particular the two
    commits that make up this round's delta (8e399957 review fixes,
    4fa4be63 centralization) could not be exercised separately; the
    refactor's effect was measured as part of the aggregate.
  • Live-model E2E. No credentials in this sandbox. The PR's transcript
    evidence (rising budget lines, the wind-down hand-off, and the model reading
    the figures back as "the token budget is exhausted (62,026 of 30,000)") was
    not independently reproduced. What was reproduced is the rendered prompt
    bytes through the real runtime — the shape, not a live session, and not
    the model's reaction to the figures.
  • Whether the progress lines reduce idle turns. The PR scopes this out
    itself; it needs longer-running Goals than one round.
  • Base-arm vitest. Not run — a chosen scope cut, not a blocked gate. No
    claim this round needs a base test count: the A/B imports source directly and
    asserts per-arm module hashes, realpaths and the C5 ignores-usage
    control. (The previous round reported a workspace globalSetup build guard
    blocking it; that was not re-tested and is not relied on here.)
  • The previous round's data-block mutant was not re-run. Its purpose was to
    falsify a comment claiming the figures would make every turn look like an
    edit; that comment no longer exists (D8), and the underlying behaviour is
    now covered directly by S2a (figures move across 4 continuations, notice
    never fires) plus the S2b positive control (a real edit does fire it).
  • The usage === undefined branch is unreachable from every shipped host,
    because the runtime populates usage unconditionally. The no-figures
    rendering was verified at the renderer and through a harness host that
    strips the field — not through any production host.
  • Gates not run: the repo-wide test suite, integration tests
    (test:integration:*), and the sandbox build. Targeted gates only, all green
    and cited with exact counts in 04-targeted-gates-and-assertion-summary.png:
    the two core suites (187 passed), the five cli suites from the test plan
    (1378 passed / 1 skipped, 131 s), repo-wide npm run typecheck (exit 0), and
    prettier + eslint on all 15 changed files — both proven live by planting
    const lintProbeUnused = 1;const badlySpaced=2; in
    goal-continuation-prompt.ts, which eslint reported as 2 no-unused-vars
    errors and prettier as a style issue, both with exit 1, then removing it and
    re-checking clean with packages/ verified CLEAN (gate-lint.log).
  • The repo's own scripts/lint.js wrapper was not used; actionlint /
    yamllint were not needed (no workflow or YAML files in this diff) and the
    pinned-binary setup was not installed.
  • macOS and Windows, which the PR marks ⚠️. Formatting is pinned to
    toLocaleString('en-US'), so it does not follow the host locale.

Methodology

CI verify job: node:22-bookworm container, Node v22.23.2, working tree at
refs/pull/11257/merge (depth 2), npm ci and npm run build already
complete; no GitHub token, and no network calls were made — the metadata
snapshot at $QWEN_VERIFY_CONTEXT, previous-report.md beside it (treated as
untrusted input), and the local tree were the whole world. Controls were
scratch worktrees under tmp/ (tmp/base-tree at HEAD^1) with
node_modules symlinked to the installed root, which is a clean control
because the PR changes no dependency manifest; the workspace symlinks
nonetheless resolve into the head tree, so the harness imports each arm's
source by absolute path and asserts C4 (no cross-boundary resolution) and
C3 (the two shared imports byte-identical) rather than trusting the symlink.
tmp/base-tree was removed with git worktree remove --force once its cells
were captured, and git status --porcelain is empty now.

Harnesses ran under tsx against TypeScript source, importing each tree's
own createGoalRuntime, buildGoalContinuationParts and
renderGoalContinuationPrompt — nothing in the unit under test was stubbed,
and the only seams used are the two the runtime itself declares (the journal,
and the host callback the PR is changing). Three bugs in my own harness were
found and fixed rather than reported, so the final counts come only from checks
that measure what they claim: the drive loop first finished turns without
claiming the permit through permitForTurn/beginTurn as production hosts do;
the budget-line regex matched Budget: while the renderer emits
Token budget: (which silently read as "no budget line" on every cell); and
the negative-remainder scan ran over the whole prompt, where the goalId UUID's
hyphens matched -\d. A fourth was an instrument error rather than a code
error: usage key presence was first read back out of the harness's JSON, but
JSON.stringify drops undefined-valued keys, so the M7 comparison measured the
serialisation instead of the object — key presence is now captured inside the
harness, before writing, which is what turned M7 from "no difference" into the
precise "0 prompt diffs, 2 object-shape diffs".

Mutations were applied in place by mutation-matrix.py and
adjudicate-survivors.py, which assert packages/ is clean before starting,
restore with git checkout -- after every mutant, and assert cleanliness again
after each restore and at the end (MX7); both anchors were verified to occur
exactly once before any run. Raw logs live beside the harnesses:
head.log, base.log, matrix.log, survivors.log, m7-inertness.log,
gate-core.log, gate-cli.log, gate-typecheck.log, gate-lint.log,
gate-toequal-probe.log, gates-static.log, assertions.log, with
machine-readable output in head.json, base.json, m7-mutant.json,
mutation-matrix.json, survivors.json, m7-inertness.json, gates.json,
assertions.json and assertions-detail.json. gates.json is built by
make-gates.mjs so that every static value in it (who carries usage, where
the renderer is called from, what the committed comment says) comes from a
command that ran. Rerun the A/B with
npx tsx ab-harness.ts --tree <tree> --out <json> on each arm, then
node ab-driver.mjs <dir>; capture any section with
node ab-driver.mjs <dir> | node scripts/verify-capture.mjs --out <png>.

Flakiness gate log

rounds=5 files=6 skipped=0
file packages/cli/src/acp-integration/session/Session.test.ts: (cd packages/cli) npx --no-install vitest run ./src/acp-integration/session/Session.test.ts
file packages/cli/src/nonInteractiveCli.test.ts: (cd packages/cli) npx --no-install vitest run ./src/nonInteractiveCli.test.ts
file packages/cli/src/ui/hooks/use-llm-stream.test.tsx: (cd packages/cli) npx --no-install vitest run ./src/ui/hooks/use-llm-stream.test.tsx
file packages/cli/src/ui/hooks/useMessageQueue.test.ts: (cd packages/cli) npx --no-install vitest run ./src/ui/hooks/useMessageQueue.test.ts
file packages/core/src/goals/goal-continuation-prompt.test.ts: (cd packages/core) npx --no-install vitest run ./src/goals/goal-continuation-prompt.test.ts
file packages/core/src/goals/goal-runtime.test.ts: (cd packages/core) npx --no-install vitest run ./src/goals/goal-runtime.test.ts


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  packages/cli/src/acp-integration/session/Session.test.ts: PPPP
  packages/cli/src/nonInteractiveCli.test.ts: PPP
  packages/cli/src/ui/hooks/use-llm-stream.test.tsx: PPP
  packages/cli/src/ui/hooks/useMessageQueue.test.ts: PPP
  packages/core/src/goals/goal-continuation-prompt.test.ts: PPP
  packages/core/src/goals/goal-runtime.test.ts: PPP

verdict: timeout
summary: only 3 of 5 rounds fit the 15-minute budget; the completed rounds agreed

--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/cli/src/acp-integration/session/Session.test.ts: P (exit 0)
round 1 · packages/cli/src/nonInteractiveCli.test.ts: P (exit 0)
round 1 · packages/cli/src/ui/hooks/use-llm-stream.test.tsx: P (exit 0)
round 1 · packages/cli/src/ui/hooks/useMessageQueue.test.ts: P (exit 0)
round 1 · packages/core/src/goals/goal-continuation-prompt.test.ts: P (exit 0)
round 1 · packages/core/src/goals/goal-runtime.test.ts: P (exit 0)
round 2 · packages/cli/src/acp-integration/session/Session.test.ts: P (exit 0)
round 2 · packages/cli/src/nonInteractiveCli.test.ts: P (exit 0)
round 2 · packages/cli/src/ui/hooks/use-llm-stream.test.tsx: P (exit 0)
round 2 · packages/cli/src/ui/hooks/useMessageQueue.test.ts: P (exit 0)
round 2 · packages/core/src/goals/goal-continuation-prompt.test.ts: P (exit 0)
round 2 · packages/core/src/goals/goal-runtime.test.ts: P (exit 0)
round 3 · packages/cli/src/acp-integration/session/Session.test.ts: P (exit 0)
round 3 · packages/cli/src/nonInteractiveCli.test.ts: P (exit 0)
round 3 · packages/cli/src/ui/hooks/use-llm-stream.test.tsx: P (exit 0)
round 3 · packages/cli/src/ui/hooks/useMessageQueue.test.ts: P (exit 0)
round 3 · packages/core/src/goals/goal-continuation-prompt.test.ts: P (exit 0)
round 3 · packages/core/src/goals/goal-runtime.test.ts: P (exit 0)
round 4 · packages/cli/src/acp-integration/session/Session.test.ts: P (exit 0)

Evidence images

01-ab-head-vs-base-prompt-cells

02-mutation-matrix-9-of-9-killed-plus-survivor-adjudication

03-description-claims-re-measured-and-new-delta

04-targeted-gates-and-assertion-summary

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Triage re-run completed without a new review.

⚠️ The bot has neither a verdict nor a deferral on 0bac4bbfec69fd2a65cda38256df09c15720f287 — no APPROVED, CHANGES_REQUESTED, or COMMENTED review of its own. A DISMISSED one does not count: dismiss_stale_reviews voids the bot's approval on every push, which is exactly when a fresh one is needed. If this re-run was meant to review or approve, it did not, and an approval left by another account is a separate vote that does not count as the bot's own.

⚠️ 机器人在 0bac4bbfec69fd2a65cda38256df09c15720f287既没有裁决也没有 defer —— 没有属于它自己的 APPROVEDCHANGES_REQUESTEDCOMMENTED 评审。DISMISSED 不算:dismiss_stale_reviews 会在每次推送时作废机器人的批准,而那恰恰是需要一次新批准的时刻。如果这次重跑本应评审或批准,那么它没有做到;而其他账号留下的批准是另一张票,不能算作机器人自己的。

The stage comments above were updated with the latest result. View workflow run.

上方各阶段评论已更新为最新结果。查看工作流运行

# Conflicts:
#	docs/users/features/goals.md
#	packages/core/src/goals/goal-runtime.ts

@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.

⚠️ Downgraded from Approve to Comment: CI failing: Lint & Static (ubuntu-latest, Node 22.x), Test (ubuntu-latest, Node 22.x). Reviewed.

Test Plan (not a blocker): Tests 170 passed — this review observed 29126, 23788, 1972, 298, 1819, 515, 6687 passed.

中文说明

⚠️ 已从批准降级为评论:CI failing: Lint & Static (ubuntu-latest, Node 22.x), Test (ubuntu-latest, Node 22.x)。 已审查。

Test Plan(非阻断):Tests 170 passed — this review observed 29126, 23788, 1972, 298, 1819, 515, 6687 passed

— qwen3.8-max via Qwen Code /review (v0.23.0)

@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.

Partially reviewed — gaps disclosed.

1 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • R5-9 first-turn gate keys on the record's turnCount rather than the previous turn's authorship — already reported as D2-1 (round 2 review body deferral list, review 5135260622; re-noted round 3, review 5137467791)

Not reviewed: test-efficacy probe — Agent 7's mutation and hunk-revert probe could not run: all 6 probes returned inconclusive because the probe runner died on the vitest globalSetup build guard, so harnessValidated is null and no survivor is either validated or refuted. The build (19/19 workspaces), every in-scope test suite, base-tree and test-delta all did run, and the round-2 /verify-pr mutation matrix measured 9/9 guards killed on goal sources that are byte-identical to this head.

Test Plan (not a blocker): Tests 170 passed — this review observed 29259, 23835, 1973, 298, 1819, 515, 6687 passed.

中文说明

仅完成部分审查,审查缺口已披露。

本轮确认的 1 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未审查(原文为英文):test-efficacy probe — Agent 7's mutation and hunk-revert probe could not run: all 6 probes returned inconclusive because the probe runner died on the vitest globalSetup build guard, so harnessValidated is null and no survivor is either validated or refuted. The build (19/19 workspaces), every in-scope test suite, base-tree and test-delta all did run, and the round-2 /verify-pr mutation matrix measured 9/9 guards killed on goal sources that are byte-identical to this head.

Test Plan(非阻断):Tests 170 passed — this review observed 29259, 23835, 1973, 298, 1819, 515, 6687 passed

— qwen3.8-max via Qwen Code /review (v0.23.0)

wenshao pushed a commit to wenshao/qwen-code that referenced this pull request Sep 8, 2026
@wenshao

wenshao commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Local verification at c518b774 — round 5, delta only

Verdict: merge-ready. I rebuilt this head and its merge-base in two real worktrees and drove the change end to end against a scripted model on all three production hosts. Everything the PR claims about its own behaviour holds at the merged head, the mutation matrix is clean, and the one commit added since my last pass — 4fa4be63 "refactor(goal): centralize continuation payload" — preserves the behaviour it refactors on every host.

This is the fifth pass (triage 1–3, the sandboxed round at 1709ae78, my round 4 at 7bf27d5c, and the bot's round-2 sandbox verification at 0bac4bbf), so I only report what those could not cover:

  • the refactor itself. 4fa4be63 replaced the hosts' named field copies with const { permit, ...continuation } = input, and the renderer's named forwarding with a ...hints spread. That is exactly the code every earlier round proved correct in its previous shape, and it landed after round 4;
  • the merged head. The bot verified 0bac4bbf; 28 commits of main have landed since, and three of this PR's own files took content from them (goal-runtime.ts 46 changed lines, nonInteractiveCli.ts 77, Session.ts 18). Nobody has driven that merged state;
  • the interaction with feat(goal): pause a Goal after three autonomous turns that make no progress #11239, which merged into main after round 4 and pauses a Goal after three autonomous turns that record nothing. This PR's new "judge your previous turn" line and that bound now ship together for the first time;
  • a correction to my own round-4 note N4, which was wrong.

Setup

Two worktrees at c518b774 (head) and a374d1b7 (the merge-base with main), each npm install && node scripts/build.js --cli-only && npm run bundle — Linux, Node v22.22.2. Model turns are served by a local OpenAI-compatible mock that bills a fixed 20,000 total_tokens per call and records every request body, so the ledger is deterministic; no credentials and no live provider are involved. The mock calls run_shell_command on each Goal turn, so every turn records real evidence and the run is not cut short by #11239's bound.

Control validity: the head's built CLI contains the string Token budget: exactly once, the base's contains it zero times, and an exhaustive census over 32 flag combinations gives base 11–15 lines against head 13–18. Base renders 11 lines whether or not usage is handed to it, so it is a control that genuinely ignores the field rather than a partial build.

1. The central claim, measured on the wire

One Goal, one objective, model.goalTokenBudget = 100000; only the build differs. Observation point is the HTTP request body the CLI actually sends.

A/B of one Goal's continuation prompts

On base, turns 0, 1 and 2 are byte-identical (md5 177c7efe three times). That is the problem this PR fixes, stated as a measurement: on main nothing in the prompt distinguishes one scheduled turn from the next.

The objective I used ends with Budget: stop as blocked after 20 turns. — the collision case triage stage 2 raised. It renders inside the JSON data block while the runtime's own Token budget: line sits outside it; the two never read as the same field.

2. The refactor, on all three production hosts

the same ladder from three hosts

Three separate real processes — headless (nonInteractiveCli.ts), the interactive TUI (useMessageQueue.tsuse-llm-stream.ts, driven in tmux) and ACP over stdio (Session.ts, driven by a raw ndjson client) — produce the same line counts, the same char counts and the same figures. All four changed host files are covered. The spread carries usage through every queue copy.

3. Oracle: the runtime's own ledger

ledger vs rendered figures

Read out of the session record's goal_state snapshots, not out of the thing under test. Exact at every step, including the clamp.

4. The wind-down hand-off

wind-down, base vs head

120,000 spent against a 100,000 window renders 0 remaining, not -20,000, and the four progress lines are gone from the one turn told not to start new work. The hand-off is the only turn where the two arms differ by exactly one line.

5. Interaction with #11239 — not covered by any earlier round

the no-progress pause on both arms

Same run with a model that answers with prose and calls no tool. Both arms pause on the same turn, at the same spend, for the same reason (status=paused, noProgressTurns=3). The PR does not move that bound. On head the model was asked to judge its previous turn, with rising figures, on turns 1 and 2 before the runtime pulled the plug; on base it received the same 11-line prompt three times. The two features compose the way the design note says they should.

6. Mutation matrix at this head

21/21 killed

21/21 killed, zero survivors. Four of them target the refactor specifically — dropping ...hints from the renderer, and dropping usage from each of the three host queue copies — so the seams the refactor introduced are covered by tests, not only by my run. Three same-file positive controls mutate pre-existing behaviour: without those, "the mutant survived" and "my command never collected your suite" would be the same observation. Tree asserted clean before the run, after every restore, and at the end.

7. Two checks the new plumbing has to keep true

injection probe and census

The budget line is the one trusted line outside the untrusted data block, so I fed five hostile objectives through the renderer — an embedded newline plus a forged Token budget: line, a CRLF variant, a </goal_runtime_data> close followed by a forged guard and figure, a literal \n, and a tag-shaped open. All five render exactly one budget line, always the runtime's own, always outside the block, at a constant 16 lines.

And the lines never reach a user's own turn: over 81 requests from five real runs, 68/68 runtime-scheduled turns carry the line and 0/13 of everything else does — including all nine side model calls these runs made (output-language side queries and a memory-extraction subagent) and an ordinary user message typed into the TUI while the Goal was active.

real TUI

That frame is a separate TUI run on a 400,000 budget, left open so the user message could be typed mid-Goal. The on-screen figure and the prompt figures come off the same ledger — the pill reads 440.0k/400.0k after the window is spent — and the user's own turn, visible above the composer, carries none of the new lines.

8. Gates

gate result
packages/core — the two suites from the test plan Tests 190 passed (190)
packages/cli — the five suites from the test plan 1393 passed | 1 skipped (1394), 134 s
npm run typecheck (repo-wide) exit 0
prettier --check on all 15 changed files clean
eslint --max-warnings 0 on the 13 changed TS files clean
CI on this head 23 pass, 0 fail, 26 skipping

Not covered

Live provider traffic (the model is scripted, so this reproduces the prompt bytes and the runtime's arithmetic, not a real model's reaction to the figures); the verifier path, since the scripted model never proposes a terminal state — every run ends at a limit, not at a verdict; macOS and Windows, which the PR marks ⚠️ — formatting is pinned to toLocaleString('en-US'), so it does not follow the host locale; the repo-wide suite and the integration suites; and whether the progress lines measurably reduce idle turns, which the PR scopes out itself.

Notes — none blocking

N1 — the ...hints spread is ordered so a caller's stray key outranks the permit.

the spread-ordering probe

renderGoalContinuationTurn puts ...hints after goalId, revision and objective, so a turn object carrying any of those three keys silently replaces the permit-derived values in the rendered data block. The old named-copy form could not do this. Nothing shipped reaches it — all four hosts carry only turnKey, origin, controller, modelStarted, kind, and I measured the rendered prompt byte-identical with those present — and TypeScript catches the literal form but not the variable form, which is the shape every host uses. Moving ...hints to the front of that object literal closes it in one line. Suggestion, not a blocker.

N2 — the design note now describes the pre-refactor copy shape. docs/design/2026-09-07-goal-continuation-budget.md says "The three hosts copy it into their queue entries alongside the fields they already copy" and scopes the host change as "one field copied through each host's queue entry". After 4fa4be63 no field is copied by name: the hosts spread the whole payload, and the renderer gained renderGoalContinuationTurn and the GoalContinuationTurn type, neither of which the note mentions. The note is in this PR's own diff, so it is worth a paragraph.

N3 — the Reviewer Test Plan's expected count is stale again. It predicts Tests 170 passed (170); this head yields 190 passed (190). That was 170 at 7bf27d5c and 187 at 0bac4bbf — the number moves with main, not with this PR, because goal-runtime.test.ts keeps gaining cases from other Goal work. Not a defect, but it is the one step a reviewer is asked to run.

Correction to my round-4 note N4. I wrote that GoalContinuationUsage = Pick<GoalRecord, …> leaves the projection "one rename away from silence" — that a field renamed on GoalRecord would cost the line rather than fail the build. That is wrong. I renamed tokensUsed to tokensSpent on GoalRecord and the build fails immediately at the projection itself: goal-continuation-prompt.ts(13,3): error TS2344: Type '"tokensUsed" | "tokenBudget" | "turnCount"' does not satisfy the constraint 'keyof GoalRecord', plus 82 more. The Pick is a load-bearing guard, not a silent one. Withdrawn.

Round 4's other notes stand at this head: fractional tokensUsed still renders decimals and still cannot be produced by anything shipped; no single fixture drives runtime → host → rendered text, which my E2E closes only for this round; and the usage === undefined branch remains unreachable from every shipped host, since flushContinuation populates usage unconditionally.

中文版 — 在 c518b774 上的本地验证(第 5 轮,仅增量)

结论:可以合入

我在两个真实 worktree 里重新构建了这个 head 和它的 merge-base,并在三个生产 host 上对着脚本化模型跑了完整的端到端。PR 对自身行为的每一条说法在合并后的 head 上都成立;变异矩阵干净;上一轮之后新增的那个 commit —— 4fa4be63 "refactor(goal): centralize continuation payload" —— 在每个 host 上都保持了它所重构的行为。

这是本 PR 的第 5 轮(triage 1–31709ae78 上的沙箱轮、我在 7bf27d5c 上的第 4 轮、以及机器人在 0bac4bbf 上的第 2 轮沙箱验证),所以我只报告它们无法覆盖的部分:

  • 重构本身。 4fa4be63 把四个 host 里逐字段的拷贝换成了 const { permit, ...continuation } = input,把 renderer 里逐字段的转发换成了 ...hints 展开。那正是此前每一轮在旧形态下证明正确的那段代码,而它是在第 4 轮之后才落下的;
  • 合并后的 head。 机器人验证的是 0bac4bbf;此后 main 上又落了 28 个 commit,本 PR 自己的三个文件也从中拿到了内容(goal-runtime.ts 46 行改动、nonInteractiveCli.ts 77 行、Session.ts 18 行)。没有人驱动过这个合并后的状态;
  • feat(goal): pause a Goal after three autonomous turns that make no progress #11239 的相互作用。它在第 4 轮之后并入 main,会在三个"什么都没记录下来"的自主轮之后把 Goal 暂停。本 PR 新增的"判断上一轮"那一行与这个闸门是第一次一起出货;
  • 以及一条对我自己第 4 轮 N4 备注的更正 —— 那条是错的。

环境

两个 worktree:head c518b774、base a374d1b7(与 main 的 merge-base),各自 npm install && node scripts/build.js --cli-only && npm run bundle —— Linux、Node v22.22.2。模型轮由本地 OpenAI 兼容 mock 提供,每次调用固定计 20,000 total_tokens,并记录每一个请求体,因此账本是确定的;全程无凭据、不接真实 provider。mock 在每个 Goal 轮都调用 run_shell_command,所以每一轮都记录了真实证据,运行不会被 #11239 的闸门提前掐断。

对照有效性:head 构建出的 CLI 里 Token budget: 恰好出现 1 次,base 里 0 次;对 32 种开关组合的穷举普查给出 base 11–15 行、head 13–18 行。base 无论是否收到 usage 都渲染 11 行,所以它是真正忽略该字段的对照,而不是一个构建不全的臂。

1. 核心主张,在线上测得

同一个 Goal、同一个目标、model.goalTokenBudget = 100000,只有构建不同。观测点是 CLI 真正发出的 HTTP 请求体。

A/B

base 侧第 0、1、2 轮逐字节相同(md5 177c7efe 出现三次)。这就是本 PR 要解决的问题的度量化表述:在 main 上,提示词里没有任何东西能区分相邻两个调度轮。

我用的目标以 Budget: stop as blocked after 20 turns. 结尾 —— 正是 triage stage 2 提出的撞名场景。它渲染在 JSON 数据块里面,而运行时自己的 Token budget: 行在块外面;两者不会被读成同一个字段。

2. 重构,在三个生产 host 上

三个 host

三个独立的真实进程 —— headless(nonInteractiveCli.ts)、交互式 TUI(useMessageQueue.tsuse-llm-stream.ts,在 tmux 里驱动)、以及 stdio 上的 ACP(Session.ts,由一个裸 ndjson 客户端驱动)—— 给出相同的行数、字符数和数字。四个改动的 host 文件全部覆盖。展开语法把 usage 带过了每一次队列拷贝。

3. 预言机:运行时自己的账本

账本

从会话记录的 goal_state 快照里读出来,而不是从被测对象里读。每一步都精确相符,包括夹取。

4. wind-down 交接轮

wind-down

100,000 的额度花掉 120,000,渲染出来是 0 remaining 而不是 -20,000;四行指引从这个被告知"不要开始新工作"的轮次上消失了。交接轮是两臂只差一行的唯一一轮。

5. 与 #11239 的相互作用 —— 此前任何一轮都没覆盖

no-progress

同样的运行,但模型只输出散文、不调用任何工具。两臂在同一轮、同样的花费上、以同样的理由暂停(status=pausednoProgressTurns=3)。本 PR 没有移动那个闸门。在 head 侧,模型在第 1、2 轮被要求判断上一轮,并且看到了递增的数字,然后运行时才把它停下;在 base 侧,它三次收到同一段 11 行提示词。两个特性的组合方式与设计文档所说的一致。

6. 本 head 上的变异矩阵

21/21

21/21 全部被杀死,零幸存。其中 4 个专门针对本次重构 —— 从 renderer 里删掉 ...hints,以及从三个 host 的队列拷贝里各删掉 usage —— 所以重构引入的这些接缝是被测试覆盖的,而不只是被我这一次运行覆盖。3 个同文件阳性对照变异的是已有行为:没有它们的话,"变异体幸存了"和"我的命令根本没收集到你的用例"是同一个观测。变异开始前、每次还原后、以及结束时都断言了工作树干净。

7. 新管线必须保持为真的两件事

注入与普查

预算行是唯一一个位于不可信数据块之外的可信行,所以我把五种敌意目标喂给渲染器 —— 内嵌换行加一条伪造的 Token budget: 行、CRLF 变体、</goal_runtime_data> 闭合后跟伪造的守卫行和数字、字面量 \n、以及一个标签形状的开头。五种情况都只渲染出一条预算行,始终是运行时自己的那条,始终在块外,行数恒为 16。

而这些行绝不会落到用户自己的轮次上:在来自五次真实运行的 81 个请求里,68/68 个运行时调度轮携带这一行,其余 0/13 全都不带 —— 包括这些运行发出的全部 9 次旁路模型调用(输出语言 side query 和记忆抽取 subagent),以及在 Goal 活跃期间敲进 TUI 的一条普通用户消息。

真实 TUI

这一帧是另一次 TUI 运行(额度 400,000),特意留着好在 Goal 活跃期间敲进用户消息。屏幕上的数字和提示词里的数字来自同一本账 —— 额度花完后胶囊读作 440.0k/400.0k —— 而输入框上方那条用户自己的轮次,一行新内容都没带。

8. 门禁

门禁 结果
packages/core —— 验证方式里的两个套件 Tests 190 passed (190)
packages/cli —— 验证方式里的五个套件 1393 passed | 1 skipped (1394),134 秒
npm run typecheck(全仓) exit 0
prettier --check 全部 15 个改动文件 干净
eslint --max-warnings 0 13 个改动 TS 文件 干净
本 head 上的 CI 23 通过、0 失败、26 skipping

未覆盖

真实 provider 流量(模型是脚本化的,所以复现的是提示词字节和运行时的算术,不是真实模型对这些数字的反应);verifier 路径 —— 脚本模型从不提出终态提案,每次运行都停在某个限制上而不是停在一个判定上;macOS 与 Windows —— PR 自己标了 ⚠️,格式固定用 toLocaleString('en-US'),不跟随宿主 locale;全仓测试与集成测试;以及指引行是否能可测量地减少空转轮,这一点 PR 自己已列为范围外。

备注 —— 均不阻塞

N1 —— ...hints 的展开顺序让调用方的杂散键盖过 permit。

展开顺序

renderGoalContinuationTurn...hints 放在 goalIdrevisionobjective 之后,所以只要 turn 对象上带了这三个键中的任何一个,它就会静默替换掉由 permit 推出的值。旧的逐字段拷贝形态做不到这一点。已发布的代码路径都碰不到它 —— 四个 host 只带 turnKeyorigincontrollermodelStartedkind,我实测在它们存在时渲染结果逐字节不变 —— 而 TypeScript 只能拦住字面量形式,拦不住变量形式,偏偏后者才是每个 host 的实际写法。把 ...hints 移到那个对象字面量的最前面,一行就能关掉。建议,非阻塞。

N2 —— 设计文档描述的还是重构前的拷贝形态。 docs/design/2026-09-07-goal-continuation-budget.md 写的是 "The three hosts copy it into their queue entries alongside the fields they already copy",并把 host 侧改动的范围写成 "one field copied through each host's queue entry"4fa4be63 之后没有任何字段是按名字拷贝的:host 展开整个载荷,renderer 新增了 renderGoalContinuationTurnGoalContinuationTurn 类型,而文档两者都没提。这份文档就在本 PR 的 diff 里,值得补一段。

N3 —— 评审验证方式里的预期数字又过期了。 它写的是 Tests 170 passed (170),这个 head 实测 190 passed (190)。在 7bf27d5c 上是 170,在 0bac4bbf 上是 187 —— 这个数跟着 main 走而不是跟着本 PR 走,因为 goal-runtime.test.ts 一直在从其他 Goal 工作里获得新用例。不是缺陷,但这恰恰是评审被要求亲自执行的那一步。

对我第 4 轮 N4 备注的更正。 我当时写 GoalContinuationUsage = Pick<GoalRecord, …> "离一次改名只有一步之遥",说 GoalRecord 上的字段被改名只会少一行、不会让构建失败。这是错的。我把 GoalRecord 上的 tokensUsed 改名成 tokensSpent,构建立刻在投影本身处失败:goal-continuation-prompt.ts(13,3): error TS2344: Type '"tokensUsed" | "tokenBudget" | "turnCount"' does not satisfy the constraint 'keyof GoalRecord',另加 82 个错误。这个 Pick 是一个承重的守卫,不是一个静默的守卫。该备注撤回。

第 4 轮的其余备注在本 head 上仍然成立:小数 tokensUsed 仍会渲染出小数,而已发布的代码路径仍然产生不出这种值;没有任何单个 fixture 贯通 运行时 → host → 渲染文本,我这一轮的 E2E 只在本轮把这个缺口补上;usage === undefined 分支在所有已发布的 host 上仍然不可达,因为 flushContinuation 无条件填充 usage


🤖 Generated with Claude Code — Claude Opus 5 (1M context)

@wenshao

wenshao commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ❌ not passed — findings reported (agent verdict) - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 63 passed · 1 failed · 64 total

Flakiness gate: ⚠️ timeout — only 4 of 5 rounds fit the 15-minute budget; the completed rounds agreed

中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:63 通过 · 1 失败 · 64 总计

抖动门:⚠️ timeout — only 4 of 5 rounds fit the 15-minute budget; the completed rounds agreed

Verification report

Sandboxed verification: ❌ not passed — findings reported (agent verdict)

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 63 passed · 1 failed · 64 total

Verified head c518b774773829dde04c6756f389f5ae95df5884 (HEAD^2), base a374d1b7f97b6e8a2801451232ecf08ce2f1e594 (HEAD^1), merge 644e195d.

中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:63 通过 · 1 失败 · 64 总计。已验证 head c518b774,base a374d1b7(两侧都与上一轮不同,所有沿用测量都在新 head 上重跑)。

结论:代码本身正确,行为面零缺陷。 关于这次改动实际行为的断言全部通过:预算行及其算术、四行进展指引、首轮豁免、wind-down 抑制与"只多一行"、剩余量夹到 0、位置、无额度措辞、四个 host 的每一跳、以及"用户自己的轮次永远看不到这些行"。A/B 中 head 4/4 个续跑提示词带预算行、base 0/4;positive control(真实 objective 编辑)在两侧都触发。变异矩阵 9/11 个防护点被其所属套件杀死、6/6 个同文件阳性对照变红,两个幸存者都已裁定(其中一个现在有了实测的一词修复)。

唯一失败的断言是 PR 正文里的过期测试数:评审验证方式写 Tests 170 passed (170),实测 190 passed (190)。归因已测:PR 自己往这两个文件加了恰好 10 个测试块,所以 base tip 是 180;上一轮测得 187(base 177),即 main 在两个 base tip 之间又给 goal-runtime.test.ts 加了 3 个测试。

上一轮 6 条发现 + 1 条结构性备注#1 测试数过期 → 仍存在(170/187 → 170/190)#2 core 没有带数字驱动 renderGoalContinuationTurn 的测试 → 仍存在#3 toEqual 钉不住 tokenBudget 省略 → 仍存在,但本轮实测了一词修复toStrictEqual 单独跑全绿、与 M7 同跑恰好红那一条);#4 设计文档仍描述已删除的逐字段拷贝 → 仍存在#5 ...hints 扫掠转发 host 内部字段 → 仍存在(潜伏、无碰撞)#6 小数 token 带小数点渲染 → 仍存在;结构性备注 → 仍存在。上一轮已修的三条(注释机制、"不提供 usage 即原提示词"、17 行 sizing)本轮复核仍为已修

未覆盖:逐 commit 归因(浅克隆,快照 13 个 commit 本地只可达 1 个);真实模型 E2E(无凭据——复现的是提示词字节,不是活会话);指引行是否减少空转轮(PR 自列范围外);base 侧 vitest(被工作区构建闸挡住,已证明是环境而非回归);仓库全量测试与集成测试;macOS/Windows。

Verification report

PR 11257 — deep verification (round 3)

Verdict: findings — 63 pass / 1 fail / 64 total scripted assertions.
Verified head c518b774773829dde04c6756f389f5ae95df5884 (HEAD^2), base
a374d1b7f97b6e8a2801451232ecf08ce2f1e594 (HEAD^1), merge 644e195d.

The code is correct and the shipped behaviour has zero defects. Every
assertion about what this change does passed: the budget line and its
arithmetic, the four progress lines, the first-turn gate, the wind-down
suppression and its exactly-one-line growth, the zero clamp, the placement,
the unbounded-Goal wording, all four host hops, and — newly verified this
round — the body's claim that these lines never reach a user's own turn.
The mutation matrix killed 9/11 guards with the suite that owns the file
plus 6/6 same-file positive controls
, and both survivors were adjudicated,
one of them now with a measured one-word fix.

The single failing assertion is a stale number in the PR body, not a
defect: the Reviewer Test Plan predicts Tests 170 passed (170) and the
command yields 190 passed (190) at this head. It is reported as a
finding because it is the one step a reviewer is asked to run and the number
they will see differs. Five further Nice-to-havers/observations, all carried
forward and re-measured, are in Findings.

Previous-finding status

The previous round verified head 0bac4bbf against base bbc8ea64. Both
sides moved again
: the head gained two commits, both Merge remote-tracking branch 'upstream/main' (0002b4af, c518b774), and the base tip advanced to
a374d1b7. Neither round-2 OID is reachable in this depth-2 checkout, so
every row below was re-measured from scratch at the new head — nothing was
carried forward by diffing the old report. The delta is main-sync only, and
the hash census pins exactly what moved: the prompt module is
byte-identical to round 2 on both arms (23e48ff7 head / c794bb38
base), while goal-runtime.ts moved on both arms (head 49855ce259cfd4ac,
base 5f4db33197635d53) because main advanced — the PR's contribution to
it is still exactly the four hunks round 2 pinned. Witness:
01-ab-head-vs-base-prompt-cells.png, 03-gates-and-description-claims.png.

# Previous finding Sev Status at c518b774 Evidence
1 Reviewer Test Plan's expected count stale (body 170, measured 187) Nice to have stands — value moved again Body still says 170; measured 190. The PR adds exactly 10 test blocks to those two files (D6), so the base tip measures 180; round 2's 187 implied base 177, i.e. main grew goal-runtime.test.ts by 3 between the two base tips. git diff HEAD^2..HEAD is empty, so 190 is the head's own count (D7). This is the round's only failing assertion (D5)
2 Core has no test that drives renderGoalContinuationTurn with figures Nice to have stands M5 survived the core prompt suite at 19 green and — a nuance new this round — also the queue suite at 53 green, because that suite pins the copy, not the render; killed by the three cli suites that render, each on exactly the test the PR added for that host (MX3, MX4)
3 toEqual does not pin the tokenBudget omission Nice to have stands — now with a measured fix M7 survived goal-runtime.test.ts at 171 green (was 168; the 3 new tests are main's and pin nothing here). Reverse mutation: toStrictEqual alone stays green (RF1a, no collateral); toStrictEqual + M7 goes red on exactly goal runtime > continuation usage figures > omits the ceiling for a Goal that has none (RF1b). The one-word fix is proven, not eyeballed
4 Design doc describes the per-field host copies the refactor removed Nice to have stands D8: the doc still says "copy it into their queue entries alongside the fields they already copy" (line 74) and "one field copied through each host's queue entry" (line 90), and still never names GoalContinuationTurn or renderGoalContinuationTurn. D9: the hosts use a rest-spread at Session.ts:2305, nonInteractiveCli.ts:658, useMessageQueue.ts:194
5 renderGoalContinuationTurn's ...hints sweep forwards host internals Nice to have stands — sharpened R5: rendered text byte-identical with and without the four host internals (16 lines both), and no collision today (collisionsToday: []). The leak itself is now proven behaviourally rather than by key-listing: injecting windDown, objectiveUpdated, verifierFeedback or usage as extra properties on the turn object each changes the rendered output, which only happens if the sweep forwards arbitrary own keys
6 Fractional tokensUsed renders with decimals observation stands F2: Token budget: 41,634.5 of 30,000,000 tokens used, 29,958,365.5 remaining; 2 Goal turns finished. Still not counted in the fail column
Structural note: no single test drives runtime → host → rendered text note stands The four host tests still inject usage at the host seam (Session.test.ts passes it into boundGoalHost.startGoalTurn, nonInteractiveCli.test.ts wraps the bound host and adds it, useMessageQueue.test.ts asserts the queue entry, use-llm-stream.test.tsx constructs the queued turn). A3/A4/A5 close the chain for this round only

Previously fixed rows, re-verified rather than trusted: the committed
comment above renderBudgetLine no longer claims content comparison and does
cite the trust boundary (F1); a host that omits usage does not render
the old prompt — head 15 lines / 4-of-4 progress vs base 11 / 0-of-4 (A15);
and the body's risk sizing matches the census exactly — base 11 lines without
figures, head 15 without and 16 with (D3).

Scope

Central claim. Every runtime-scheduled Goal continuation prompt now opens
with a Token budget: line (spend / window / remainder / turns finished) and
carries four standing progress lines — three on the Goal's first turn, none on
the wind-down hand-off — at every host.

Secondary claims. (S1) usage survives every hop from the runtime to the
text the model receives, at all three hosts. (S2) Figures that move every turn
do not disturb objective-change detection.

New this round (the delta). The delta is two main-sync merges, so there is
no new PR-authored code to probe; the probes new to this round are scoped to
what main's move could have broken: a re-census of every host registration and
queue-entry construction at the shifted line numbers, a verification of the
body's previously-untested claim that user-driven turns never render this
prompt, a per-file hash census of the whole import closure on both arms, and
the base-tip test-count attribution. Out of scope by choice, and listed under
Not covered: live-model E2E, per-commit attribution, base-arm vitest, the
repo-wide suite.

Central claim + A/B

Both arms drive the real createGoalRuntime with an in-memory journal (the
persistence seam the runtime itself declares) and a host whose startGoalTurn
emulates the production queue copy verbatim (the rest-spread, plus the
host-only fields) and then renders through the same tree's real
buildGoalContinuationParts and renderGoalContinuationTurn — exactly what
all three production hosts do — recording parts[0].text. The drive loop
reproduces the production host sequence (permitForTurn(turnKey) ?? beginTurn(turnKey), then markTurnDelivered, then finishTurn). The
observation point is the rendered prompt the model would receive, not the
usage object at the host boundary. Witness:
01-ab-head-vs-base-prompt-cells.png.

Scenario threeTurns, default 30,000,000 grant, turns billed 41,634 / 43,864 /
50,000:

continuation HEAD budget line (verbatim) HEAD progress BASE
#0 (scheduled by create) Token budget: 0 of 30,000,000 tokens used, 30,000,000 remaining; 0 Goal turns finished. 3/4 11 lines, no figures, 0/4
#1 Token budget: 41,634 of 30,000,000 tokens used, 29,958,366 remaining; 1 Goal turn finished. 4/4 11 lines, no figures, 0/4
#2 Token budget: 85,498 of 30,000,000 tokens used, 29,914,502 remaining; 2 Goal turns finished. 4/4 11 lines, no figures, 0/4
#3 Token budget: 135,498 of 30,000,000 tokens used, 29,864,502 remaining; 3 Goal turns finished. 4/4 11 lines, no figures, 0/4

4/4 flip from absent to present (A1 vs control A2), each line equals
the figures recomputed independently from the runtime's own ledger at schedule
time (A3), which in turn equals the usage object handed to the host (A4),
and both render entry points agree on every driven continuation (A5 — the
destination check). A9 bounds the change: the final ledger is identical on
both arms (tokensUsed 135498 / tokenBudget 30000000 / turnCount 3), so the PR
changes what the model is told, not what the runtime accounts.

The PR body's transcript evidence reproduces byte-for-byte: all three
quoted budget lines (A8), the full six-line tail of the third prompt (A10),
and the three-line wind-down tail (A11). That corroborates that the author's
transcript was read off this code path — the shape, not a live session (see
Not covered).

Other cells (all in the capture):

cell HEAD BASE
windDown (grant 30,000, turn 0 bills 62,026) Token budget: 62,026 of 30,000 tokens used, 0 remaining; 1 Goal turn finished. + hand-off lines, 0/4 progress, 14 lines no figures, hand-off lines only, 13 lines — the hand-off grows by exactly the budget line (A16); across the census all 12 usage-carrying wind-down combos grow by exactly +1 line and all 4 no-usage ones are unchanged
noBudget (grant Infinity) Token budget: 0 tokens used, with no budget on this Goal; 0 Goal turns finished. (none)
overspentClamp (99,999 of 30,000) 0 remaining, no negative figure anywhere in the line n/a
no usage at all no budget line, 4/4 progress, 15 lines no budget line, 0/4, 11 lines
placement, on the continuation that carries the notice after the authoritative-objective guard (row 10) and </goal_runtime_data> (row 9), before the notice (row 16) budget line absent
noticeWatch: 4 continuations with moving figures [false,false,false,false] (S2a) [false,false,false,false] (S2c)
positive control, run on BOTH arms: a real objective edit after a delivered turn fires — objectiveUpdated=true, 17 lines (S2b) fires — 12 lines

S2b is what makes S2a mean something: the one-shot notice is observable at
this seam on both arms, so "the moving figures never fired it" is a
measurement, not a dead probe. The edit invalidates and re-schedules the
in-flight continuation under a new turnId/revision, and the notice correctly
stays owed to the replacement — the same runtime behaviour round 2 recorded.

Control validity (C1C6, in the capture): the two arms load different
prompt bytes (23e48ff7 vs c794bb38) and different runtime bytes (59cfd4ac
vs 97635d53); a per-file sha256 census of the whole transitive import
closure shows the arms differ by exactly one file — goal-runtime.ts — with
goal-continuation-prompt.ts present only in head's runtime closure (the PR's
new type-only import) and nothing present only in base (C3); no closure file
on either arm resolves outside its own tree (C4), which matters because
readlink -f tmp/base-tree/node_modules/@qwen-code/qwen-code-core resolves
into the head tree, so the harness imports each arm's source by absolute
path; base renders all 8 flag shapes identically with and without usage
(C5) while head does not (C5b), so base is a true control that ignores
usage rather than a partial build; and the PR touches no dependency manifest
(C6), so sharing the installed root node_modules is clean.

Newly verified body claim. "User-driven turns never render this prompt,
so they never carry figures"
and the Risk section's "these lines only reach
runtime-scheduled turns, never a user's own"
are both true, proven by
census rather than by reading: each render-feeding queue has exactly one
writer and it is the runtime host (R7queuedGoalTurns.push at
nonInteractiveCli.ts:659, this.goalQueue.push at Session.ts:2306, the
goalQueueRef write at useMessageQueue.ts:201), and the two user-origin
literals
(nonInteractiveCli.ts:1488, Session.ts:4789) are locals never pushed into
those queues (R8). The interactive host (AppContainer.tsx:2635) forwards
input straight to enqueueGoalTurn, so it is covered by the queue mutant.

Corrections

None new. The three corrections raised in earlier rounds against the
description and a committed comment were all adopted and re-verified above.
Two body claims that had not been checked before are verified accurate this
round: the user-turn claim (above), and usage?: really being optional on the
public renderer contract and a Pick&lt;GoalRecord, 'tokensUsed' | 'tokenBudget' | 'turnCount'> (R9).

Findings

Ordered by severity. None is a defect in the shipped behaviour. 2–6 are
completeness reporting about tests and docs, not merge conditions.

1. Nice to have — the Reviewer Test Plan's expected count is stale (the
round's only failing assertion).
The plan predicts:

 Test Files  2 passed (2)
      Tests  170 passed (170)

The command yields Test Files 2 passed (2), Tests 190 passed (190).
Reproduce:

cd /__w/qwen-code/qwen-code/packages/core && \
  npx vitest run src/goals/goal-continuation-prompt.test.ts src/goals/goal-runtime.test.ts

git diff HEAD^2..HEAD is empty, so the merge introduced nothing — 190 is what
the verified head itself produces. Attributed rather than merely observed: the
PR adds exactly 10 test blocks to those two files (7 + 3, counted from the
diff), so the base tip measures 180; round 2 measured 187 with the same +10
contribution, i.e. base 177, so main grew goal-runtime.test.ts by 3 between
the two base tips. The body's figure therefore predates both main's growth and
the PR's own tests. Bounded: it costs a reviewer a moment of doubt, nothing
else.

2. Nice to have — the core package has no test that drives
renderGoalContinuationTurn with figures.
The new turn-shaped entry point is
the one every production host now calls, and in its own package it is
exercised only by goal-continuation-prompt.test.ts's buildGoalContinuationParts
block, whose cases pass no usage. Proven by mutant M5 (destructure
usage out of the ...hints sweep): the core prompt suite stayed green at
19/19, and — a nuance the previous round's three-suite adjudication did not
separate — the queue suite stayed green at 53/53 too, because that
suite pins the copy while M5 breaks the render; the three suites that render
(headless, session, stream) each went red on exactly the test the PR added for
that host (MX3, MX4). So the guard is pinned — but only from
packages/cli. The fixture that would pin it in core is one line added to an
existing case in the buildGoalContinuationParts describe: pass
usage: { tokensUsed: 1234, tokenBudget: 30000000, turnCount: 4 } and expect
the Token budget: line in part.text.

3. Nice to have — toEqual does not pin the tokenBudget omission, and the
one-word fix is now measured.
In goal-runtime.ts the usage projection
deliberately omits the key when a Goal is unbounded. Mutant M7 replaces that
with an unconditional tokenBudget: snapshot.goal.tokenBudget and survived
goal-runtime.test.ts at 171 green
, even though
it('omits the ceiling for a Goal that has none') looks like it pins exactly
this. The mechanism is the same as round 2's (toEqual treats an explicit
undefined key as absent; exactOptionalPropertyTypes is not set), and the
inertness bound carries over: renderBudgetLine branches on === undefined,
so the prompt cannot tell. What is new is the measured fix: applying
toStrictEqual in that one test alone leaves the suite green at 171 (RF1a,
zero collateral), and applying it together with M7 turns exactly one test red —
goal runtime > continuation usage figures > omits the ceiling for a Goal that has none (RF1b). So the clause is load-bearing, the suite has a one-word
gap, and the fix ships with its own proof. Adjudicated as a coverage gap, not a
defect.

4. Nice to have — the design doc describes the host copies the refactor
removed.
docs/design/2026-09-07-goal-continuation-budget.md (added by this
PR, otherwise accurate) still says "The three hosts copy it into their queue
entries alongside the fields they already copy"
(line 74) and, under Scope,
"one field copied through each host's queue entry" (line 90). After the
centralization commit there is no per-field copy: each host does
const { permit, ...continuation } = input and spreads (D9). The doc also
still never names GoalContinuationTurn or renderGoalContinuationTurn, which
are now the surface every host renders through. Its Verification section
remains correct — each host case does fail when its copy is removed (M8–M11
all killed).

5. Nice to have — renderGoalContinuationTurn's ...hints sweep forwards
host internals into the core renderer.
Production hosts pass their whole
queue entry, so hints also collects turnKey, controller (a live
AbortController), origin, modelStarted and kind. Bounded, and
explicitly not a defect: the rendered text is byte-identical with and
without them on every cell (R5), because renderGoalContinuationPrompt only
reads known keys, and no host field collides with a hint name today
(collisionsToday: []). The leak is now proven behaviourally: injecting
windDown: true, objectiveUpdated: true, verifierFeedback: 'injected' or a
different usage as extra properties on the turn object each changes the
rendered output, which can only happen if the sweep forwards arbitrary own
keys. The reason to note it is that the sweep is what makes the refactor safe
(a new hint can never be dropped again), and the same property means a future
host field named like a hint would silently override the runtime's value.
Destructuring the known hints explicitly would keep the safety and drop the
leak.

6. Observation, not counted in assertions.json — fractional spend renders
with decimals.
Token budget: 41,634.5 of 30,000,000 tokens used, 29,958,365.5 remaining; 2 Goal turns finished. Unchanged from both previous
rounds and still bounded: tokensUsed is validated by isNonNegativeNumber
while the adjacent revision and turnCount use isNonNegativeInteger, but
the ledger feeds accumulated integer token counts, so a fractional value is not
reachable in practice. One character of diff (isNonNegativeInteger) closes it
if a maintainer wants it.

Mutation matrix

9/11 guards killed by the suite that owns the file, 6/6 same-file positive
controls red, both survivors adjudicated, and a reverse mutation that prices
the fix for one of them. Witness:
02-mutation-matrix-9-of-11-killed-6-of-6-controls.png. Every mutated file
gets a control corrupting pre-existing code in the same object literal or
block, which proves the chosen vitest command collects tests that exercise
that file — without it, "the mutant survived" and "my command never ran your
suite" are the same observation.

mutant file suite result
M1 drop the budget line goal-continuation-prompt.ts prompt KILLED 6 red
M2 drop the Math.max(0, …) clamp goal-continuation-prompt.ts prompt KILLED 2 red — incl. never reports a negative remainder
M3 drop the first-turn gate (turnCount > 0) goal-continuation-prompt.ts prompt KILLED 1 red — asks for no judgement of a previous turn on the first one
M4 render progress lines on wind-down too goal-continuation-prompt.ts prompt KILLED 1 red
M5 renderGoalContinuationTurn drops usage from its sweep goal-continuation-prompt.ts prompt, then 4 cli suites SURVIVED prompt (19 green) and queue (53 green) → KILLED by headless/session/stream; adjudicated: pinned, my suite choice was too narrow (finding 2)
control reword a pinned wind-down line goal-continuation-prompt.ts prompt KILLED 1 red
M6 drop usage, from the startGoalTurn payload goal-runtime.ts runtime KILLED 3 red
M7 set tokenBudget unconditionally goal-runtime.ts runtime SURVIVED 171 green; adjudicated: inert at the prompt + toEqual (finding 3), fix priced by RF1a/RF1b
control corrupt turnCount goal-runtime.ts runtime KILLED 3 red
M8 replace the rest-spread copy with an explicit copy omitting usage Session.ts session KILLED 1 red
control corrupt the pre-existing turnKey Session.ts session KILLED 27 red
M9 same, explicit copy omitting usage nonInteractiveCli.ts headless KILLED 1 red
control corrupt the pre-existing turnKey nonInteractiveCli.ts headless KILLED 11 red
M10 same, explicit copy omitting usage useMessageQueue.ts queue KILLED 1 red
control corrupt the pre-existing turnKey useMessageQueue.ts queue KILLED 6 red
M11 render with usage stripped from the queued turn use-llm-stream.ts stream KILLED 1 red
control flip the pre-existing shouldProceed use-llm-stream.ts stream KILLED 6 red
reverse toStrictEqual alone goal-runtime.test.ts runtime GREEN 171 — the fix has no collateral
reverse toStrictEqual + M7 goal-runtime.test.ts + goal-runtime.ts runtime KILLED 1 red — exactly the omission test

M8–M11 are the refactor's own hazard class, and the author's design-doc claim —
"each case fails when its copy is removed" — holds at this head: all four
were killed by exactly one test, and in each case by the test the PR added
for that host, which is the attribution that matters:

mutant the one test that went red
M8 Session.ts Session > prompt > auto-compress > carries the spend figures into the continuation prompt
M9 nonInteractiveCli.ts runNonInteractive > carries the spend figures into a scheduled Goal continuation
M10 useMessageQueue.ts useMessageQueue > carries the runtime spend figures onto the queued Goal turn
M11 use-llm-stream.ts useLlmStream > renders the queued spend figures into the synthetic Goal turn

The two reverts that kill the central claim fail the intended assertion,
not an import or a fixture: M1's red is an Object.is equality whose diff
names the removed line (- Token budget: 1,500 of 1,000 tokens used, 0 remaining; 2 Goal turns finished.), and M6's red is expected undefined to deeply equal { tokensUsed: +0, tokenBudget: 30000, turnCount: 0 } (MX8,
MX9).

No combination row was owed: no two hunks in this PR defend the same hazard
from different directions — the runtime produces usage once, the renderer
consumes it once, and each host is a separate hop, so every guard is
observable alone (which the 9 single-hunk kills confirm).

Not covered

  • Per-commit attribution. Depth-2 checkout: only the merge, the base tip
    and the PR head exist. git rev-list HEAD^1..HEAD^2 returns 1 while
    $QWEN_VERIFY_CONTEXT lists 13 commits — at a shallow boundary that
    command returns a plausible small number instead of erroring. The aggregate
    HEAD^1..HEAD diff was verified instead; the two main-sync commits that make
    up this round's delta could not be exercised separately.
  • Live-model E2E. No credentials in this sandbox. The PR's transcript
    evidence (rising budget lines, the wind-down hand-off, and the model reading
    the figures back) was not independently reproduced as a live session.
    What was reproduced is the rendered prompt bytes through the real
    runtime — the shape, not a live session, and not the model's reaction to
    the figures.
  • Whether the progress lines reduce idle turns. The PR scopes this out
    itself; it needs longer-running Goals than one round.
  • Base-arm vitest. Blocked by the workspace globalSetup build guard, and
    proven environmental rather than a regression: the guard fires because my
    scratch worktree has no packages/core/dist, while the head tree does
    (G6, gate-base-arm-blocked.log). No claim in this round needs a base
    test count — the A/B imports source directly and asserts per-arm module
    hashes, closure hashes, realpaths and the C5 ignores-usage control. The
    base-tip count of 180 is derived arithmetically from the head count and the
    PR's own +10 test blocks instead.
  • Repetition / flakiness. No dedicated multi-round flakiness gate was run
    this round; the suites were nonetheless exercised repeatedly and agreed —
    the two core suites ran 13 times across the gate, the gates aggregate and the
    matrix (always 19/19 or with exactly the intended reds), and each cli suite
    ran 3+ times with stable counts.
  • Gates not run: the repo-wide test suite, integration tests
    (test:integration:*), and the sandbox build. Targeted gates only, all green
    and cited with exact counts in 03-gates-and-description-claims.png: the two
    core suites (190 passed), the five cli suites from the test plan
    (1393 passed | 1 skipped), repo-wide npm run typecheck (exit 0), and
    prettier + eslint on all 15 changed files — both proven live by planting
    const lintProbeUnused = 1;const badlySpaced=2; in
    goal-continuation-prompt.ts, which eslint reported as 2 no-unused-vars
    errors and prettier as a style issue, both with exit 1, then removing it and
    re-checking clean with packages/ verified CLEAN (gate-lint.log).
  • The repo's own scripts/lint.js wrapper was not used; actionlint /
    yamllint were not needed (no workflow or YAML files in this diff).
  • macOS and Windows, which the PR marks ⚠️. Formatting is pinned to
    toLocaleString('en-US'), so it does not follow the host locale.

Methodology

CI verify job: node:22-bookworm container, Node v22.23.2, working tree at
refs/pull/11257/merge (depth 2), npm ci and npm run build already
complete; no GitHub token, and no network calls were made — the metadata
snapshot at $QWEN_VERIFY_CONTEXT, previous-report.md beside it (treated as
untrusted input), and the local tree were the whole world. The control was a
scratch worktree at HEAD^1 under tmp/base-tree with the root and per-package
node_modules symlinked in (a clean control because the PR changes no
dependency manifest); the per-package links were needed because root ajv is
v6 while packages/core needs v8's dist/2020.js. The workspace symlinks
nonetheless resolve into the head tree, so the harness imports each arm's
source by absolute path and asserts C4 (no cross-boundary resolution) and
C3 (per-file closure hashes differ by exactly the changed file) rather than
trusting the symlink. tmp/base-tree was removed with
git worktree remove --force once its cells were captured, and
git status --porcelain is empty now.

Harnesses ran under tsx against TypeScript source, importing each tree's
own createGoalRuntime, buildGoalContinuationParts and
renderGoalContinuationTurn — nothing in the unit under test was stubbed, and
the only seams used are the two the runtime itself declares (the journal, and
the host callback the PR is changing). Two harness bugs were found and fixed
rather than reported, so the final counts come only from checks that measure
what they claim: the objective-edit positive control first dispatched the edit
and then finished the invalidated turn (which the runtime correctly refuses
with Goal turn permit is no longer valid), so the control now edits after
delivery and observes the re-scheduled replacement; and the host census first
anchored on origin: 'runtime', which matched three interface type unions and
missed useMessageQueue's entry entirely, so it now anchors on the
turnKey: \goal-runtime:${…}`construction every real queue entry sets. Mutations were applied in place bymutation-matrix.py, which asserts packages/is clean before starting, restores withgit checkout -- after every case, and asserts cleanliness again after each restore and at the end (MX10); every anchor was verified to occur exactly once before any run. Raw logs live beside the harnesses: head.log, base.log, ab-driver.log, matrix.log, mutant-*.log, gate-core.log, gate-cli.log, gate-typecheck.log, gate-lint.log, gate-base-arm-blocked.log, gates.log, combine.log, with machine-readable output in head.json, base.json, mutation-matrix.json, gates.json, ab-assertions.json, gate-assertions.json, matrix-assertions.json, assertions.jsonandassertions-detail.json. Rerun the A/B with npx tsx ab-harness.ts --tree <tree> --out <json>on each arm, thennode ab-driver.mjs <dir>; capture any section with node ab-driver.mjs <dir> | node scripts/verify-capture.mjs --out <png>`.

Evidence images

01-ab-head-vs-base-prompt-cells

02-mutation-matrix-9-of-11-killed-6-of-6-controls

03-gates-and-description-claims

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

Flakiness gate log

rounds=5 files=6 skipped=0
file packages/cli/src/acp-integration/session/Session.test.ts: (cd packages/cli) npx --no-install vitest run ./src/acp-integration/session/Session.test.ts
file packages/cli/src/nonInteractiveCli.test.ts: (cd packages/cli) npx --no-install vitest run ./src/nonInteractiveCli.test.ts
file packages/cli/src/ui/hooks/use-llm-stream.test.tsx: (cd packages/cli) npx --no-install vitest run ./src/ui/hooks/use-llm-stream.test.tsx
file packages/cli/src/ui/hooks/useMessageQueue.test.ts: (cd packages/cli) npx --no-install vitest run ./src/ui/hooks/useMessageQueue.test.ts
file packages/core/src/goals/goal-continuation-prompt.test.ts: (cd packages/core) npx --no-install vitest run ./src/goals/goal-continuation-prompt.test.ts
file packages/core/src/goals/goal-runtime.test.ts: (cd packages/core) npx --no-install vitest run ./src/goals/goal-runtime.test.ts


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  packages/cli/src/acp-integration/session/Session.test.ts: PPPPP
  packages/cli/src/nonInteractiveCli.test.ts: PPPP
  packages/cli/src/ui/hooks/use-llm-stream.test.tsx: PPPP
  packages/cli/src/ui/hooks/useMessageQueue.test.ts: PPPP
  packages/core/src/goals/goal-continuation-prompt.test.ts: PPPP
  packages/core/src/goals/goal-runtime.test.ts: PPPP

verdict: timeout
summary: only 4 of 5 rounds fit the 15-minute budget; the completed rounds agreed

--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/cli/src/acp-integration/session/Session.test.ts: P (exit 0)
round 1 · packages/cli/src/nonInteractiveCli.test.ts: P (exit 0)
round 1 · packages/cli/src/ui/hooks/use-llm-stream.test.tsx: P (exit 0)
round 1 · packages/cli/src/ui/hooks/useMessageQueue.test.ts: P (exit 0)
round 1 · packages/core/src/goals/goal-continuation-prompt.test.ts: P (exit 0)
round 1 · packages/core/src/goals/goal-runtime.test.ts: P (exit 0)
round 2 · packages/cli/src/acp-integration/session/Session.test.ts: P (exit 0)
round 2 · packages/cli/src/nonInteractiveCli.test.ts: P (exit 0)
round 2 · packages/cli/src/ui/hooks/use-llm-stream.test.tsx: P (exit 0)
round 2 · packages/cli/src/ui/hooks/useMessageQueue.test.ts: P (exit 0)
round 2 · packages/core/src/goals/goal-continuation-prompt.test.ts: P (exit 0)
round 2 · packages/core/src/goals/goal-runtime.test.ts: P (exit 0)
round 3 · packages/cli/src/acp-integration/session/Session.test.ts: P (exit 0)
round 3 · packages/cli/src/nonInteractiveCli.test.ts: P (exit 0)
round 3 · packages/cli/src/ui/hooks/use-llm-stream.test.tsx: P (exit 0)
round 3 · packages/cli/src/ui/hooks/useMessageQueue.test.ts: P (exit 0)
round 3 · packages/core/src/goals/goal-continuation-prompt.test.ts: P (exit 0)
round 3 · packages/core/src/goals/goal-runtime.test.ts: P (exit 0)
round 4 · packages/cli/src/acp-integration/session/Session.test.ts: P (exit 0)
round 4 · packages/cli/src/nonInteractiveCli.test.ts: P (exit 0)
round 4 · packages/cli/src/ui/hooks/use-llm-stream.test.tsx: P (exit 0)
round 4 · packages/cli/src/ui/hooks/useMessageQueue.test.ts: P (exit 0)
round 4 · packages/core/src/goals/goal-continuation-prompt.test.ts: P (exit 0)
round 4 · packages/core/src/goals/goal-runtime.test.ts: P (exit 0)
round 5 · packages/cli/src/acp-integration/session/Session.test.ts: P (exit 0)

Evidence images

01-ab-head-vs-base-prompt-cells

02-mutation-matrix-9-of-11-killed-6-of-6-controls

03-gates-and-description-claims

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@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. ✅

@wenshao
wenshao added this pull request to the merge queue Sep 8, 2026
Merged via the queue into QwenLM:main with commit 1f89008 Sep 8, 2026
87 checks passed
qqqys added a commit to qqqys/qwen-code that referenced this pull request Sep 9, 2026
…-claim-budget-retry

Conflict in docs/users/features/goals.md: upstream added the continuation-prompt
paragraph (QwenLM#11257) immediately before the checkpoint paragraph this branch
rewrote. Keep both -- the branch's wording also corrects the stall accounting
upstream still describes as unconditional, which QwenLM#11304 made conditional on an
overflowing evidence window.
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.23.2.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants