Repository navigation
feat(goal): carry budget figures and progress guidance in the continuation prompt - #11257
Conversation
…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.
…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.
|
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: The 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 中文说明两个开放问题都采纳了,在 109c1e6。 host 侧的透传现在有断言了。 你说得对,那是整条链上唯一没有东西兜着的一环,而"测试套原样通过"本身就是破绽。每个 host 一个用例:
关于把预算行和指引行拆开:我没有拆。它们作为同一段提示词上线,E2E 证据也是同一份记录,而任何一半都是同一个文件里的一行回滚。真出问题回滚很便宜;现在拆开反而会让两个改提示词的 PR 在同样那五处整段断言上互相排队。 合并顺序不变:本 PR 要排在 #11239 和 #11248 之后。它与前者共用 |
…/automation-11257-dedb8502
|
@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 resolved the merge conflicts and pushed the branch update. Merge: PR #11257 ←
|
|
@qwen-code /triage |
…948/upstream-main' into HEAD
Local verification at
|
| # | 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 | closed — docs/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
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 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–3、1709ae78 上的沙箱验证、以及本 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 对象。
那个 Goal 的每一个调度轮,端到端,退出码 0:
数字与运行时自己的账本每一步都相符;第一轮 3 行指引、之后每轮 4 行、交接轮 0 行。同一条阶梯在三个生产 host 上各自独立地跑出来了 —— headless(nonInteractiveCli.ts)、交互式 TUI(useMessageQueue.ts → use-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. 真正超支时的夹取
100,000 的额度花掉 124,902,渲染出来是 0 remaining 而不是 -24,902;四行指引从这个被告知"不要开始新工作"的轮次上消失了。交接轮是两臂只差一行的唯一一轮。
3. 跨 /goal resume —— 此前任何一轮都没覆盖
同一会话里的两次 headless 运行。resume 在已花掉的量之上又授予一个新窗口,与 docs/users/features/goals.md 的描述完全一致;提示词行正确报出抬高后的上限(166,536 of 266,536 … 100,000 remaining),turnCount 从 4 连到 7。首轮豁免在这里没有被误用:第 4 轮是 resume 后那次运行的第一轮,却是这个 Goal 的第四轮,所以"判断上一轮"那行理应保留 —— 它确实保留了。
4. 交互式 host 的真实运行
有必要挑明:这一帧在两臂之间逐字节相同(两边 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 上的变异矩阵
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
|
@qwen-code /triage |
# Conflicts: # docs/users/features/goals.md # packages/core/src/goals/goal-runtime.ts
Local verification at
|
| 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 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.
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–3、1709ae78 上的沙箱轮、我在 7bf27d5c 上的第 4 轮、以及机器人在 0bac4bbf 上的第 2 轮沙箱验证),所以我只报告它们无法覆盖的部分:
- 重构本身。
4fa4be63把四个 host 里逐字段的拷贝换成了const { permit, ...continuation } = input,把 renderer 里逐字段的转发换成了...hints展开。那正是此前每一轮在旧形态下证明正确的那段代码,而它是在第 4 轮之后才落下的; - 合并后的 head。 机器人验证的是
0bac4bbf;此后 main 上又落了 28 个 commit,本 PR 自己的三个文件也从中拿到了内容(goal-runtime.ts46 行改动、nonInteractiveCli.ts77 行、Session.ts18 行)。没有人驱动过这个合并后的状态; - 与 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 请求体。
base 侧第 0、1、2 轮逐字节相同(md5 177c7efe 出现三次)。这就是本 PR 要解决的问题的度量化表述:在 main 上,提示词里没有任何东西能区分相邻两个调度轮。
我用的目标以 Budget: stop as blocked after 20 turns. 结尾 —— 正是 triage stage 2 提出的撞名场景。它渲染在 JSON 数据块里面,而运行时自己的 Token budget: 行在块外面;两者不会被读成同一个字段。
2. 重构,在三个生产 host 上
三个独立的真实进程 —— headless(nonInteractiveCli.ts)、交互式 TUI(useMessageQueue.ts → use-llm-stream.ts,在 tmux 里驱动)、以及 stdio 上的 ACP(Session.ts,由一个裸 ndjson 客户端驱动)—— 给出相同的行数、字符数和数字。四个改动的 host 文件全部覆盖。展开语法把 usage 带过了每一次队列拷贝。
3. 预言机:运行时自己的账本
从会话记录的 goal_state 快照里读出来,而不是从被测对象里读。每一步都精确相符,包括夹取。
4. wind-down 交接轮
100,000 的额度花掉 120,000,渲染出来是 0 remaining 而不是 -20,000;四行指引从这个被告知"不要开始新工作"的轮次上消失了。交接轮是两臂只差一行的唯一一轮。
5. 与 #11239 的相互作用 —— 此前任何一轮都没覆盖
同样的运行,但模型只输出散文、不调用任何工具。两臂在同一轮、同样的花费上、以同样的理由暂停(status=paused、noProgressTurns=3)。本 PR 没有移动那个闸门。在 head 侧,模型在第 1、2 轮被要求判断上一轮,并且看到了递增的数字,然后运行时才把它停下;在 base 侧,它三次收到同一段 11 行提示词。两个特性的组合方式与设计文档所说的一致。
6. 本 head 上的变异矩阵
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 运行(额度 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 放在 goalId、revision、objective 之后,所以只要 turn 对象上带了这三个键中的任何一个,它就会静默替换掉由 permit 推出的值。旧的逐字段拷贝形态做不到这一点。已发布的代码路径都碰不到它 —— 四个 host 只带 turnKey、origin、controller、modelStarted、kind,我实测在它们存在时渲染结果逐字节不变 —— 而 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 新增了 renderGoalContinuationTurn 和 GoalContinuationTurn 类型,而文档两者都没提。这份文档就在本 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)
|
@qwen-code /triage |
…-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.















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
createschedules 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.
usageremains optional on the public host contract. When it is absent, theToken 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 aGoalContinuationUsageprojection fromGoalRecord, 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
tokensUsedreachestokenBudget, 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_goalcan 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
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
GoalContinuationUsageprojection keeps the copied fields aligned withGoalRecord, while the host tests catch an omitted copy without making the publicusageinput mandatory.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:
The tail of that third prompt in full:
After — the wind-down hand-off, forced by setting
model.goalTokenBudgetto 30000. The remainder is clamped, and the progress lines are gone: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
Environment (optional)
Linux,
npm run build, headless--output-format textagainst a live model, prompts read back out of the session transcript.Risk & Scope
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.get_goalandupdate_goaldescriptions are unchanged.usageremains optional; a host that omits it gets no numericToken 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。评审验证方式
如何验证
提示词测试钉住了生产形态的普通轮和 wind-down 轮完整文本;同时覆盖有额度、无额度、已超支三种情况,要求
Token budget:行位于不可信数据块之外且在"目标已变更"通知之前,并区分第一轮、后续轮和 wind-down 轮的指引。每个 host 都有用例证明 usage 能通过它的队列拷贝。共享的
GoalContinuationUsage投影让字段与GoalRecord保持一致,host 测试则在不把公开usage输入改成必填的前提下捕获遗漏拷贝。证据(Before & After)
下面的数字来自真实会话记录;示例标签使用当前 renderer 的
Token budget:,因此反映待评审代码,而不声称历史记录当时已经使用改名后的标签。改动前。 已发布版本的提示词没有运行时 token 额度行,到常驻目标守卫那一行就结束了。
改动后。 同一个 Goal 的三个续跑轮,数字随着它干活往上走:
第三段提示词的结尾全文:
改动后 —— wind-down 交接轮,通过把
model.goalTokenBudget设成 30000 触发。剩余量被夹住,指引行消失:那一轮的回答结尾是"the token budget is exhausted (62,026 of 30,000), so this Goal now stops and waits for the user to resume it",也就是模型确实读到了这些数字并用上了。
测试平台
环境(可选)
Linux,
npm run build,对真实模型跑 headless--output-format text,提示词从会话记录里回读。风险与范围
turnCount === 0时才少掉"判断上一轮"那一行。对冲的一面是:它们只会出现在运行时调度的轮次上,永远不会出现在用户自己的轮次里。get_goal与update_goal的描述没有改动。usage仍然可选;不提供它的 host 不渲染Token budget:数字行,并因为没有首轮信号而在非 wind-down 轮收到全部四行进展指引。关联 Issue
属于 #4228 的一部分。