fix(goal): cite this turn's delivered output instead of refusing over it - #9880
Conversation
A completion proposal had to cite every `delivered_output` from the current turn or be refused, with guidance to read the catalog and retry. That cannot converge. Assistant output is `delivered_output` stamped with the same turn, so the text emitted while complying becomes another uncited entry, and the required set grows by one on every attempt. In a reported session the loop ran 34 minutes and 8.6M tokens across 67 model calls — 32 `get_goal`, 23 `update_goal` — with the uncited list growing each round, until the user paused the Goal by hand. Nothing bounds the retrying: a refusal ends the turn with the Goal still active, so the runtime queues another continuation. Nothing about that list needs the model's judgment; it is exactly the entries the tool already computes to build the refusal. Fold them into the proposal instead of demanding they be repeated back. The verifier still sees the current turn's delivered output, which is what the gate was protecting, and the model can no longer lose a race against its own narration. The union is safe by construction: both sets are drawn from the same catalog and are disjoint, so it cannot exceed GOAL_EVIDENCE_REFERENCE_LIMIT, which is that catalog's own entry cap. Entries the proposal already cited are not added twice, since a duplicated reference is itself rejected downstream. The scope is unchanged — only a `complete` proposal is affected; a blocker still cites whatever it chose. What was folded in is reported back as `autoCitedCurrentDeliveredOutput` so the proposal reaching the verifier is not a surprise. This does not add a retry bound. That belongs with the Goal budget work, and mixing the two would put an unrelated contract change in this diff. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
✅ Qwen Triage finished — CI landed green on ✅ Qwen Triage 已完成 —— |
|
Thanks for the PR! Template looks good ✓ Problem: observed bug, not theory. The linked issue (#9877, P1) carries a real session export: a Goal spent 34 minutes, 67 model calls and 8.6M tokens proposing completion until a human paused it, and the refusal's Direction: aligned. The refusal guarded something real — the verifier must see what was delivered — but it demanded citing output that does not exist yet at the moment the citation list is assembled. Folding the already-computed set into the proposal keeps the guarantee and removes the race; this is direction 2 from the issue and the minimal version of it. The retry bound is deliberately left out and belongs with the Goal budget work — I agree with that split. Size: core path touched ( Approach: the scope is exactly right — one production file, the refusal replaced by a fold, Risk: no high-risk path matches (checked against the revert-correlated path list); no elevated risk signals. Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ 问题:已观测到的 bug,不是理论问题。关联 issue(#9877,P1)带有真实 session 导出:一个 Goal 花了 34 分钟、67 次模型调用、860 万 token 反复提出完成,直到人工暂停;拒绝信息里的 方向:对齐。这道拒绝守护的东西是真实的——verifier 必须看到交付了什么——但它要求引用的产出在引用列表组装的那一刻尚不存在。把工具已经算好的集合并入提案,保住了保证,也消除了这场比赛;这正是 issue 中的方向 2,也是它的最小形态。重试边界被刻意排除在外,属于 Goal 预算那部分工作——我认同这个拆分。 规模:触及核心路径( 方案:范围恰到好处——单个生产文件,用并入替换拒绝,只影响 风险:未命中高风险路径(已对照与 revert 相关的路径清单检查);无升级风险信号。 进入代码审查 🔍 — Qwen Code · qwen3.8-max Reviewed at |
Code reviewI formed my own proposal before reading the diff — fold the set the refusal already computes into the proposal instead of demanding it be repeated back — and that is exactly what landed. No blockers:
One non-blocking observation, outside the diff: the Testing evidenceUnattended CI run — the PR's code was not built or executed in this environment; the evidence below is the PR's own CI on the reviewed commit, read through the API. The main unit suite was still running at review time; the table updates in place once CI settles. No completed check has failed. The author's local numbers (477 tests passing, two mutation checks) are their claim, not independently re-run here. Final CI results for
One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。 Sandboxed verification would settle the remaining gap: 中文说明代码审查我在读 diff 之前先形成了自己的方案——把拒绝逻辑已经算好的集合并入提案,而不是要求它被复述回来——落地的正是这个方案。无阻塞问题:
一个 diff 之外的非阻塞观察: 测试证据无人值守 CI 运行——本环境未构建或执行 PR 代码;以上证据为通过 API 读取的、该 PR 自身在被审提交上的 CI 结果。评审时主单元测试套件仍在运行,表格将在 CI 结束后原地更新。目前已完成的检查无失败。作者自报的本地数据(477 个测试通过、两次变异检验)是其声明,未在此独立复跑。 沙箱验证可以补齐剩余缺口: — Qwen Code · qwen3.8-max Reviewed at |
|
Confidence: 4/5 — clean, minimal fix for a well-reproduced P1; the point kept back is that the main unit suite was still running at review time and the end-to-end loop behavior rests on the PR's own tests. Stepping back: this is the kind of PR the gate hopes for. The problem is real and quantified — 34 minutes and 8.6M tokens of a Goal proposing completion until a human paused it. The root cause is understood precisely: the refusal demanded citing output created by the very act of complying. And the fix is the smallest thing that keeps the guarantee — hand the verifier-bound list to the proposal instead of demanding it be repeated back. My independent proposal before reading the diff was identical. No drive-by edits, no scope creep; the retry bound is deliberately left to the Goal budget work, and mixing it in here would indeed have turned a bugfix into a contract change. If I were maintaining this in six months, the comment explaining why refusing cannot converge is exactly the context I'd want. My one reservation is not about the diff: the Verdict: approve. The main unit suite is still in flight, so approval is deferred until CI lands green on 中文说明置信度:4/5 —— 针对复现充分的 P1 的干净、最小化修复;保留的一分是因为评审时主单元测试仍在运行,且端到端循环行为目前依赖 PR 自己的测试。 退一步看:这正是闸门期待的 PR。问题真实且被量化——一个 Goal 花了 34 分钟、860 万 token 反复提出完成,直到人工暂停。根因被精确理解:拒绝要求引用的产出,恰恰由服从拒绝这一行为本身制造。而修法是保住保证的最小改动——把这份本该交给 verifier 的列表直接交给提案,而不是要求它被复述回来。我在读 diff 之前独立构想的修法与此相同。没有顺手改动,没有范围蔓延;重试边界刻意留给 Goal 预算工作,混进来确实会把一次 bug 修复变成契约变更。如果六个月后由我来维护,注释里「为什么拒绝无法收敛」的解释正是我想要的上下文。 我唯一的保留意见与 diff 无关: 结论:批准。主单元测试仍在运行,因此批准推迟到 CI 在 — Qwen Code · qwen3.8-max Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship — CI landed green after the review. ✅
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed — no blockers. Suggestions are inline.
中文说明
已审查——无阻断问题。 建议见行内评论。
— qwen3.8-max via Qwen Code /review (v0.22.0)
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed. Suggestions are inline.
Not explored to full depth (tool budget reached): "agent 5": executing packages/core/src/goals/goal-tools.test.ts to confirm the suite is green at this commit — vitest could not start in this scratch worktree ( ERR_MODU….
Not reviewed: reverse audit — stopped before round 9 by the review time budget.
Deferred under the convergence posture (round 2, not a blocker) — recorded, not requested in this round:
packages/core/src/goals/goal-tools.test.ts:709 — [probe] Blocked-path test never inspects the result payloadpackages/core/src/goals/goal-tools.test.ts:589 — [probe] Only non-empty auto-cite test pins the payload with toMatchObject; every toEqual pin runs the empty branchpackages/core/src/goals/goal-tools.test.ts:642 — [probe] No fixture cites a whitespace-padded ref, leaving the trimmed cited-set untestedpackages/core/src/goals/goal-tools.test.ts:589 — [probe] result.terminateTurn is asserted only in auto-cite-empty branchespackages/core/src/goals/goal-tools.test.ts:709 — [probe] Blocked test pins only evidenceRefs; blockerKind/reason/status unasserted on the recorded proposal
中文说明
仅完成部分审查,审查缺口已披露。 建议见行内评论。
未探索到全部深度(达到工具调用预算):"agent 5":executing packages/core/src/goals/goal-tools.test.ts to confirm the suite is green at this commit — vitest could not start in this scratch worktree ( ERR_MODU…。
未审查:反向审计——评审时间预算不足,未能开始第 9 轮。
收敛姿态下延后(第 2 轮,非阻断)——已记录,本轮不要求修改:共 5 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.22.0)
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed — no blockers. Suggestions are inline.
Convergence: round 3 posted 1 inline comment(s), 1 of them reported for the first time; the previous round posted 2 (2 new). Findings keep coming back to the same files: packages/core/src/goals/goal-tools.test.ts (findings in round 2; 1 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. No Critical finding is open on this round, so merging and moving the remaining Suggestion threads to a follow-up issue is available as an ending — a merged pull request cannot diverge further. (Observation only — nothing was withheld from this review because of this observation.)
中文说明
已审查——无阻断问题。 建议见行内评论。
收敛情况:第 3 轮发布了 1 条行内评论,其中 1 条是首次提出;上一轮发布了 2 条(其中 2 条首次提出)。发现反复回到同一批文件:packages/core/src/goals/goal-tools.test.ts(第 2 轮已出过发现,本轮又有 1 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。本轮没有未决的 Critical,因此"合入后把剩余 Suggestion 线程转到后续 issue"是一个可选的结束方式——已合入的 PR 不会继续发散。(仅为观察——本轮评审未因此扣留任何内容。)
— qwen3.8-max via Qwen Code /review (v0.22.0)
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
中文说明
— qwen3.8-max via Qwen Code /review (v0.22.0)
|
@qwen-code /review |
|
Qwen Code review request accepted. Review is queued in workflow run. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No blocking issues. LGTM! ✅
Not explored to full depth (tool budget reached): "agent 4": none — no check was cut short..
Deferred under the convergence posture (round 5, not a blocker) — recorded, not requested in this round:
packages/core/src/goals/goal-tools.ts:296 — [probe] autoCitedCurrentDeliveredOutput reported when the proposal was not recorded
中文说明
无阻断问题。LGTM!✅
未探索到全部深度(达到工具调用预算):"agent 4":none — no check was cut short.。
收敛姿态下延后(第 5 轮,非阻断)——已记录,本轮不要求修改:共 1 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.22.0)
|
@qwen-code /takeover |
|
🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. This is a fork PR, so the first round comes from the next scheduled scan (usually within minutes). Remove the 中文说明🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。本 PR 来自 fork,首轮处理将由下一次定时扫描执行(通常几分钟内)。移除 |
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: Autofix round: no code changes neededThis takeover round triaged everything newer than the last evaluation. No new code defect was found; the branch stays at CI downgrade (review round 4) diagnosed as transient — already clearedThe round-4 review downgraded Approve → Comment citing Local reproduction at
Finding dispositionsAll six inline findings from review rounds 1–3 were already fixed on the branch and answered on their threads by earlier rounds; this round re-verified each against the current code:
Round-2 deferred probes ("recorded, not requested")The round-2 review recorded five probe observations and explicitly did not request them; round 4 posted no findings, and the reviewer has since APPROVED the branch. Each probe re-checked against the current code:
No finding was deferred to the follow-up queue and none was escalated this round. VerificationCommands actually run this round (no commit was made):
中文说明Autofix 轮次:无需代码改动本轮接管(takeover)对上次评估之后的全部反馈做了分诊。未发现新的代码缺陷;本轮不产生提交,分支保持在 CI 降级(评审第 4 轮)判定为瞬时问题——现已恢复第 4 轮评审以「 在
各项发现的处理评审第 1–3 轮提出的全部 6 条行内发现,均已在分支上修复,并由更早的轮次在各自线程中作答;本轮对每一条都在当前代码上重新核验:
第 2 轮延后的探针(「已记录,本轮不要求」)第 2 轮评审记录了 5 条探针观察并明确未作要求;第 4 轮未发布任何发现,且评审者此后已批准分支。逐条在当前代码上复核:
本轮没有发现被延后到后续队列,也没有升级给维护者的事项。 验证本轮实际运行的命令(未产生提交):
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
Local end-to-end verification — recommend mergeI built a real verification environment for this PR rather than reading the diff: the actual interactive CLI, the real A/B method. One-file revert: The modelled agent is deliberately well-intentioned and does exactly what the tool description asks: it emits the deliverable, calls 1. The defect reproduces, and it does not stop
Of the 218 refused proposals on base, 33 were refused by this PR's target gate and 185 by a second gate the first one causes (see finding 5). 2. The verifier really does still receive the current turn's delivered outputThis is the guarantee the refusal was protecting, so I read it off the wire rather than trusting the diff. The verifier request the runtime sent on the PR build: {"goal":{"objective":"Reply with exactly the single letter X and nothing else."},
"currentTurnId":"9bd8cdf3-…",
"proposal":{"status":"complete","evidenceRefs":["01542806-…","29b93bcf-…"]},
"evidence":[
{"uuid":"01542806-…","proofKind":"delivered_output","turnId":"9bd8cdf3-…","content":"X"},
{"uuid":"29b93bcf-…","proofKind":"delivered_output","turnId":"9bd8cdf3-…",
"content":"Read the goal (catalog holds 1 entries); proposing completion now."}]}The model supplied only 3. Scope did not widen
4. The tests are non-vacuous, and the "cannot overflow" claim holds at the boundarySeven mutations of the new predicate, each run against the touched suite. All seven are caught by a named test, and nothing else in the file breaks:
Both narrowing predicates and both reporting branches are pinned — the round-1 Separately I wrote a throwaway boundary probe (not proposed for the PR): a saturated 100-entry catalog with 99 refs cited by the model and one uncited current-turn 5. Findings — none blockingF1 · The PR body overstates the mechanism. "The refusal could not be satisfied" is not what the code does. A model that emits no user-facing text in the F2 · "Nothing bounds the retrying" is true for the reported shape, not universally. In the interactive CLI, across turns there is genuinely no bound — 218 Goal turns in 61 s with no cap firing. Within one turn there is: when I let the model retry inside a single turn, the always-on per-turn tool-call cap fired after ~100 tool calls in 4 s, surfaced the "A potential loop was detected" dialog and paused the Goal. So a Goal that retries in-turn is stopped (by asking a human); one that retries across turns is not. The distinction matters for how #9891 scopes its budget. F3 · F4 · The guidance is now stale in two places. The F5 · A sibling of the same defect survives this PR. Once the evidence catalog is at its 100-entry cap, the model's own narration between This is not a regression and not a reason to hold the PR — it needs a saturated catalog and a proposal that cites an entry sitting on the eviction boundary, and on base this state is reached faster because the gate this PR removes is what keeps the turn spinning and flooding the catalog. But it is the same root cause the round-3 convergence note was pointing at, and it is a concrete argument for #9891 landing rather than being deferred indefinitely. 6. Checks
VerdictRecommend merge. The defect is real and reproduces end to end; the fix converges the exact run that previously did not terminate; the guarantee the refusal protected is preserved on the wire; scope is held on both the 中文版本地端到端验证 —— 建议合入我没有只读 diff,而是为这个 PR 搭建了一套真实验证环境:真实的交互式 CLI、真实的 A/B 方法:单文件回退 —— 把 被建模的 agent 是刻意「守规矩」的,完全按工具描述行事:先给出交付内容,再调用 1. 缺陷可复现,且不会自行停止
base 上这 218 次被拒的提案中,33 次是本 PR 针对的那道闸门,185 次是由它引发的第二道闸门(见发现 5)。 2. verifier 确实仍然收到了本轮的 delivered output这正是那条拒绝所要保护的东西,所以我没有相信 diff,而是直接从线上抓取。PR 构建下运行时发出的 verifier 请求: {"proposal":{"status":"complete","evidenceRefs":["01542806-…","29b93bcf-…"]},
"evidence":[
{"uuid":"01542806-…","proofKind":"delivered_output","content":"X"},
{"uuid":"29b93bcf-…","proofKind":"delivered_output",
"content":"Read the goal (catalog holds 1 entries); proposing completion now."}]}模型只提供了 3. 范围没有扩大
4. 测试非空洞,且「不会溢出」的论断在边界成立对新谓词做了 7 个变异,每个都跑一遍被改动的测试文件。7 个全部被具名测试捕获,且文件内其它测试不受影响:
两个收窄谓词与两个回报分支都被钉住了 —— 第 1 轮的 我另外写了一个一次性边界探针(不建议并入 PR):目录已满 100 条,模型引用其中 99 条,另有 1 条本轮未被引用的 5. 发现 —— 均不阻断F1 · PR 描述把机制说过头了。 「那条拒绝无法被满足」并非代码的实际行为。在 F2 · 「重试没有任何边界」对被报告的那种形态成立,但并非普遍成立。 在交互式 CLI 中,跨轮确实没有边界 —— 61 秒 218 个 Goal 轮次,没有任何上限触发。单轮之内则有:当我让模型在同一轮内反复重试时,常开的 per-turn 工具调用上限在 4 秒内约 100 次调用后触发,弹出「A potential loop was detected」对话框并把 Goal 置为 paused。也就是说,轮内重试会被拦下(代价是要问人),跨轮重试不会。这个区别会影响 #9891 的预算该怎么划范围。 F3 · 提案未被记录时仍然回报 F4 · 有两处指引已经过时。 F5 · 同一类缺陷的「兄弟」在本 PR 之后依然存在。 一旦证据目录达到 100 条上限,模型在 这不是回归,也不构成拦截本 PR 的理由 —— 它需要目录已满且提案恰好引用了处在淘汰边界上的条目;而且在 base 上这个状态反而来得更快,因为正是本 PR 移除的那道闸门让轮次不断空转、把目录灌满。但它与第 3 轮收敛提示所指向的根因是同一个,也是 #9891 应当尽快落地、而非无限期延后的具体理由。 6. 检查项
结论建议合入。 缺陷真实且可端到端复现;本修复让此前无法终止的那次运行收敛;那条拒绝所保护的保证在真实链路上得以保留; |
|
Released in v0.22.2. |



What this PR does
update_goal(status: 'complete')no longer refuses a proposal that leaves the current turn's delivered output uncited. It folds that output into the proposal'sevidenceRefsand reports what it added asautoCitedCurrentDeliveredOutput. The verifier still receives the current turn's delivered output, which is what the refusal was protecting. Onlycompleteis affected; a blocker still cites whatever it chose.Why it's needed
The refusal could not be satisfied. Assistant output is
delivered_output(proofKindOf,goal-evidence.ts) stamped with the turn that produced it, so any text emitted between reading the catalog and callingupdate_goalbecomes a new uncited entry of that same turn — and the refusal's own guidance,Call get_goal after delivering the final output, then retry update_goal, asks for exactly the round trip that produces it. The required set grows by one per attempt. The act of proposing completion manufactures the evidence that invalidates the proposal.Nothing bounds the retrying either. A tool-level refusal never reaches the verifier: the turn simply ends with the Goal still
active, so the runtime queues the next continuation, and the only terminal condition is a human. In a reported session (qwen-code-export-2026-08-24T07-46-35-716Z.jsonl, qwen3.8-max, daemon channel) a short verification Goal set at 07:12 ran until the user typed/goal pauseat 07:46 — 34 minutes, 67 model calls, 8,638,576 tokens, of which 32 wereget_goaland 23 wereupdate_goal. The refusals carry the signature of the defect directly:The gate was guarding something real — a Goal should not be marked complete while the output that is the deliverable sits uncited, because the verifier would then judge completion without seeing what was delivered. But that list requires no judgment from the model: it is precisely the set the tool already computes in order to build the refusal. Handing it to the proposal preserves the guarantee and removes the race.
Reviewer Test Plan
How to verify
Set a Goal that finishes in one turn and let the model deliver its result and propose completion in the same turn. Before this change the proposal is refused and each retry is refused again with one more UUID in the list, indefinitely. After it, the proposal is recorded on the first attempt, the reply reports
autoCitedCurrentDeliveredOutput, and the Goal reaches verification. Then confirm the scope did not widen: ablockedproposal in a turn that also delivered output records with only the refs it supplied.Three tests cover it. The test that pinned the refusal now pins the fold, asserting
recordTerminalProposalreceives the union['tool-result-1', 'letter-x']where the model supplied only the first. A second asserts a proposal that already cited this turn's output is not given a duplicate — a duplicated reference is rejected downstream byvalidateGoalEvidenceReferences, so the fold has to be a union rather than an append. A third asserts ablockedproposal is left alone.Both invariants were mutation-checked rather than merely observed to pass. Removing the fold from the proposal fails the first test and leaves the other 36 green. Making the de-duplication predicate constantly true fails the second and leaves the other 36 green. Both were reverted and the suite re-run clean.
The union cannot overflow:
GOAL_EVIDENCE_REFERENCE_LIMITisCATALOG_ENTRY_LIMIT, and both sets are drawn from that same catalog and are disjoint by construction, so their union is at most the catalog's own size. That is why no cap handling appears in the diff.npx vitest runpasses 477 tests across 18 files coveringpackages/core/src/goals, the CLI goal command and the daemon goals route.npx tsc --noEmitinpackages/coreis clean, andprettierandeslintare clean on both changed files.Evidence (Before & After)
Before:
{"proposalRecorded":false,"readyForVerification":false,"uncitedCurrentDeliveredOutput":[...],"error":"The completion proposal omitted delivered output from the current Goal turn..."}— repeated with a longer list each turn until the run was paused by hand.After:
{"proposalRecorded":true,"readyForVerification":true,"autoCitedCurrentDeliveredOutput":["letter-x"],"nextAction":"End this turn without user-facing text..."}on the first attempt.Tested on
Environment (optional)
Linux, Node.js 22, unit tests only.
Risk & Scope
delivered_outputproves only that content was delivered, so adding it cannot manufacture support for a claim about what the user said or what an external system did, and the verifier still has to find support for every claim in the proposal's reason. What was added is reported back in the tool result so it is visible rather than silent.uncitedCurrentDeliveredOutputno longer appears in tool results because that refusal no longer happens; the newautoCitedCurrentDeliveredOutputfield is additive and only present when something was folded in. Scope for the core triage gate: 35 added and 28 deleted production lines in one file, with no cross-package change.Linked Issues
Part of #9877 — this PR removes the self-invalidating citation gate (the first half of that issue's scope). The retry-bound half lands in #9891 as a token budget on all autonomous continuation; #9877 is closed by hand once both are merged, so neither merge closes it early.
中文说明
本 PR 做了什么
update_goal(status: 'complete')不再因为「本轮 delivered output 未被引用」而拒绝提案。它会把这些产出并入提案的evidenceRefs,并以autoCitedCurrentDeliveredOutput回报所并入的内容。verifier 仍然会收到本轮的 delivered output——这正是那条拒绝所要保护的东西。只有complete受影响;blocked 提案仍然只引用它自己选择的证据。为什么需要
那条拒绝无法被满足。助手输出的 proofKind 是
delivered_output(goal-evidence.ts的proofKindOf),并被打上产生它的那一轮的标记;因此在「读取目录」与「调用update_goal」之间发出的任何文字,都会成为同一轮里一条新的未引用条目——而拒绝自身给出的指引Call get_goal after delivering the final output, then retry update_goal,要求的恰恰是那次会产生它的往返。所需集合每尝试一次就增长一条。提出完成这个动作,制造了让该提案失效的证据。重试也没有任何边界。工具层的拒绝根本走不到 verifier:轮次就此正常结束,而 Goal 仍是
active,于是运行时排下一个续跑轮,唯一的终止条件是人。在一份已报告的 session(qwen-code-export-2026-08-24T07-46-35-716Z.jsonl,qwen3.8-max,daemon 通道)中,07:12 设下的一个简短验证 Goal 一直跑到用户在 07:46 手动输入/goal pause——34 分钟、67 次模型调用、8,638,576 token,其中 32 次get_goal、23 次update_goal。拒绝信息直接携带着这个缺陷的特征:这道闸门守护的东西是真实的——不应在「作为交付物的产出」尚未被引用时就把 Goal 标记为完成,否则 verifier 会在没看到交付了什么的情况下判定完成。但那份列表并不需要模型的判断:它恰恰就是工具为了构造这条拒绝而已经算出来的集合。把它交给提案,既保住了这项保证,又消除了这场比赛。
评审者测试计划
如何验证
设一个一轮内即可完成的 Goal,让模型在同一轮里交付结果并提出完成。改动之前,提案被拒,且每次重试都会以多一个 UUID 的列表再次被拒,无限循环。改动之后,提案在第一次尝试就被记录,返回值报告
autoCitedCurrentDeliveredOutput,Goal 进入验证。然后确认范围没有扩大:在一个同样产生了输出的轮次里提出blocked,记录下来的仍然只有它自己提供的引用。三个测试覆盖它。此前钉住拒绝的那个测试,现在钉住这次并入,断言
recordTerminalProposal收到的是并集['tool-result-1', 'letter-x'],而模型只提供了前者。第二个断言「已经引用过本轮输出的提案不会拿到重复项」——重复引用会被下游的validateGoalEvidenceReferences拒绝,因此这次并入必须是并集而不是追加。第三个断言blocked提案不受影响。两条不变式都做了变异检验而不只是「跑通了」。把并入从提案中移除,第一个测试失败,其余 36 个保持通过。把去重谓词改成恒真,第二个测试失败,其余 36 个保持通过。两次变异都已还原并重跑干净。
并集不可能溢出:
GOAL_EVIDENCE_REFERENCE_LIMIT就是CATALOG_ENTRY_LIMIT,而两个集合都取自同一份目录且按构造互斥,因此并集至多等于该目录自身的规模。这就是 diff 中没有出现任何上限处理的原因。npx vitest run通过 18 个文件共 477 个测试,覆盖packages/core/src/goals、CLI 的 goal 命令与 daemon 的 goals 路由。packages/core的npx tsc --noEmit干净,两个改动文件的prettier与eslint均干净。证据(修复前后)
修复前:
{"proposalRecorded":false,"readyForVerification":false,"uncitedCurrentDeliveredOutput":[...],"error":"The completion proposal omitted delivered output from the current Goal turn..."}——每一轮重复出现,列表更长,直到运行被人手动暂停。修复后:第一次尝试即返回
{"proposalRecorded":true,"readyForVerification":true,"autoCitedCurrentDeliveredOutput":["letter-x"],"nextAction":"End this turn without user-facing text..."}。测试平台
环境(可选)
Linux、Node.js 22,仅单元测试。
风险与范围
delivered_output只能证明「内容被交付过」,因此加入它无法为「用户说过什么」或「外部系统发生了什么」这类主张制造支持,verifier 仍必须为提案理由中的每一条主张找到支持。被加入的内容会在工具返回值中回报,因此是可见的而非静默的。uncitedCurrentDeliveredOutput不再出现在工具返回值中,因为那条拒绝不再发生;新增的autoCitedCurrentDeliveredOutput字段是纯新增,且仅在确实并入了内容时出现。供 core triage gate 参考的规模:单个文件、新增 35 行、删除 28 行生产代码,无跨包改动。关联 Issue
关联 #9877——本 PR 取消自我否定的引用闸门(该 issue 范围的前一半)。重试上界的后一半由 #9891 以覆盖全部自主续跑的 token 预算落地;两者都合入后再手动关闭 #9877,避免任一合并提前关闭它。