Skip to content

fix(goal): cite this turn's delivered output instead of refusing over it - #9880

Merged
wenshao merged 4 commits into
QwenLM:mainfrom
qqqys:goal/g1-completion-gate
Aug 25, 2026
Merged

fix(goal): cite this turn's delivered output instead of refusing over it#9880
wenshao merged 4 commits into
QwenLM:mainfrom
qqqys:goal/g1-completion-gate

Conversation

@qqqys

@qqqys qqqys commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

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's evidenceRefs and reports what it added as autoCitedCurrentDeliveredOutput. The verifier still receives the current turn's delivered output, which is what the refusal was protecting. Only complete is 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 calling update_goal becomes 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 pause at 07:46 — 34 minutes, 67 model calls, 8,638,576 tokens, of which 32 were get_goal and 23 were update_goal. The refusals carry the signature of the defect directly:

07:20  uncitedCurrentDeliveredOutput: [05131cb7]
07:27  uncitedCurrentDeliveredOutput: [05131cb7, a36c35d5]
07:33  uncitedCurrentDeliveredOutput: [a36c35d5, 14928ca1]
07:36  uncitedCurrentDeliveredOutput: [a36c35d5, 14928ca1, ffb150bd]

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: a blocked proposal 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 recordTerminalProposal receives 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 by validateGoalEvidenceReferences, so the fold has to be a union rather than an append. A third asserts a blocked proposal 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_LIMIT is CATALOG_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 run passes 477 tests across 18 files covering packages/core/src/goals, the CLI goal command and the daemon goals route. npx tsc --noEmit in packages/core is clean, and prettier and eslint are 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

OS Status
🍏 macOS N/A
🪟 Windows ⚠️
🐧 Linux

Environment (optional)

Linux, Node.js 22, unit tests only.

Risk & Scope

  • Main risk or tradeoff: the proposal the verifier judges now contains references the model did not write. That is deliberate and it does not weaken the verdict — delivered_output proves 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.
  • Not validated / out of scope: this does not add a retry bound. A Goal that keeps proposing completion for some other reason will still retry without limit, because the runtime has no turn, token or wall-clock ceiling; that belongs with the Goal budget work and putting it here would mix an unrelated contract change into this diff. Windows and macOS were not exercised locally and remain covered by CI.
  • Breaking changes / migration notes: none. uncitedCurrentDeliveredOutput no longer appears in tool results because that refusal no longer happens; the new autoCitedCurrentDeliveredOutput field 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_outputgoal-evidence.tsproofKindOf),并被打上产生它的那一轮的标记;因此在「读取目录」与「调用 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。拒绝信息直接携带着这个缺陷的特征:

07:20  uncitedCurrentDeliveredOutput: [05131cb7]
07:27  uncitedCurrentDeliveredOutput: [05131cb7, a36c35d5]
07:33  uncitedCurrentDeliveredOutput: [a36c35d5, 14928ca1]
07:36  uncitedCurrentDeliveredOutput: [a36c35d5, 14928ca1, ffb150bd]

这道闸门守护的东西是真实的——不应在「作为交付物的产出」尚未被引用时就把 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/corenpx tsc --noEmit 干净,两个改动文件的 prettiereslint 均干净。

证据(修复前后)

修复前:{"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..."}

测试平台

操作系统 状态
🍏 macOS N/A
🪟 Windows ⚠️
🐧 Linux

环境(可选)

Linux、Node.js 22,仅单元测试。

风险与范围

  • 主要风险或取舍:verifier 判定的提案中,现在含有并非模型写下的引用。这是有意为之,且不会削弱裁决——delivered_output 只能证明「内容被交付过」,因此加入它无法为「用户说过什么」或「外部系统发生了什么」这类主张制造支持,verifier 仍必须为提案理由中的每一条主张找到支持。被加入的内容会在工具返回值中回报,因此是可见的而非静默的。
  • 未验证/不在范围:本 PR 不加入重试边界。因其他原因反复提出完成的 Goal 仍会无限重试,因为运行时没有任何轮次、token 或时钟上限;那属于 Goal 预算那部分工作,把它放进来会把一项无关的契约变更混入本 diff。Windows 与 macOS 未在本地验证,仍由 CI 覆盖。
  • 破坏性变更/迁移说明:无。uncitedCurrentDeliveredOutput 不再出现在工具返回值中,因为那条拒绝不再发生;新增的 autoCitedCurrentDeliveredOutput 字段是纯新增,且仅在确实并入了内容时出现。供 core triage gate 参考的规模:单个文件、新增 35 行、删除 28 行生产代码,无跨包改动。

关联 Issue

关联 #9877——本 PR 取消自我否定的引用闸门(该 issue 范围的前一半)。重试上界的后一半由 #9891 以覆盖全部自主续跑的 token 预算落地;两者都合入后再手动关闭 #9877,避免任一合并提前关闭它。

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>
@github-actions github-actions Bot added the review/self-reported The linked issue was opened by the PR author (self-reported) label Aug 24, 2026
@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

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

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

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 uncitedCurrentDeliveredOutput list grew by exactly one UUID per retry — the signature of a gate that cannot converge. I checked the mechanism against the code: the refusal at goal-tools.ts demands citing current-turn delivered output, but assistant text emitted to comply is itself new delivered_output in the same turn (proofKindOf('assistant_output') in goal-evidence.ts). The reproduction and the mechanism both check out.

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 (packages/core/src/goals/goal-tools.ts): 63 production lines (35+ / 28−); tests: 155 lines (145+ / 10− in goal-tools.test.ts); no generated/schema lines. Well under every escalation threshold.

Approach: the scope is exactly right — one production file, the refusal replaced by a fold, complete only, blocked untouched, and an additive result field so the fold is visible rather than silent. One forward-looking observation: the update_goal tool description (not part of this diff) still walks the model through the get_goal round trip that this fix makes optional. Explicit citation still converges, so it's fine as-is — worth a look once this lands.

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 反复提出完成,直到人工暂停;拒绝信息里的 uncitedCurrentDeliveredOutput 列表每次重试恰好多一个 UUID——正是无法收敛的闸门的特征。我对照代码核实了机制:goal-tools.ts 中的拒绝要求引用本轮已交付的产出,但为服从而发出的助手文本本身就是同一轮里新的 delivered_output(见 goal-evidence.tsproofKindOf('assistant_output'))。复现与机制均核实无误。

方向:对齐。这道拒绝守护的东西是真实的——verifier 必须看到交付了什么——但它要求引用的产出在引用列表组装的那一刻尚不存在。把工具已经算好的集合并入提案,保住了保证,也消除了这场比赛;这正是 issue 中的方向 2,也是它的最小形态。重试边界被刻意排除在外,属于 Goal 预算那部分工作——我认同这个拆分。

规模:触及核心路径(packages/core/src/goals/goal-tools.ts):63 行生产代码(35+ / 28−);测试:155 行(goal-tools.test.ts 145+ / 10−);无生成/schema 行数。远低于所有升级阈值。

方案:范围恰到好处——单个生产文件,用并入替换拒绝,只影响 completeblocked 不受影响,结果字段纯新增,使并入可见而非静默。一个供后续参考的观察:update_goal 的工具描述(不在本 diff 内)仍在引导模型走那个因本修复而变为可选的 get_goal 往返。显式引用依然收敛,现状没问题——合并后值得看一眼。

风险:未命中高风险路径(已对照与 revert 相关的路径清单检查);无升级风险信号。

进入代码审查 🔍

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Code review

I 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:

  • The fold reuses the refusal's filter verbatim (delivered_output + same turn + not already cited), so the verifier still receives this turn's delivered output — the guarantee the refusal protected is preserved, just without the round trip that manufactured new uncited entries.
  • The union is disjoint by construction (the filter excludes already-cited UUIDs), so no duplicate can reach validateGoalEvidenceReferences, which rejects duplicates outright. Both sets are subsets of the presented catalog, and that catalog is capped at CATALOG_ENTRY_LIMIT = GOAL_EVIDENCE_REFERENCE_LIMIT (verified in goal-evidence.ts), so the 100-ref limit cannot overflow — the PR's "no cap handling needed" claim holds.
  • Scope is held: blocked proposals fold nothing, the no-catalog path is untouched, and a repo-wide grep finds no other consumers of the removed uncitedCurrentDeliveredOutput result beyond this file and its test.
  • Tests pin the change: the old refusal test now asserts the union ['tool-result-1', 'letter-x'] reaches recordTerminalProposal, a de-dup test covers the already-cited case, and a blocked test guards the scope.

One non-blocking observation, outside the diff: the update_goal tool description still instructs the get_goal round trip this fix makes optional. It still converges, so nothing to change here — a candidate for a small follow-up.

Testing evidence

Unattended 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 35943f5 (auto-updated by the triage finalize job after CI completed):

Check Conclusion
Classify PR ✅ success
Dependency CVE audit ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
Secret scan (TruffleHog) ✅ success
Test (ubuntu-latest, Node 22.x) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success

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

Sandboxed verification would settle the remaining gap: @qwen-code /verify — the claim that a completion proposal now records on the first attempt, and that the refusal → continuation → retry loop cannot recur, is end-to-end Goal-runtime behavior; the unit suite pins the tool contract, but only a live Goal session demonstrates the loop is gone.

中文说明

代码审查

我在读 diff 之前先形成了自己的方案——把拒绝逻辑已经算好的集合并入提案,而不是要求它被复述回来——落地的正是这个方案。无阻塞问题:

  • 并入原样复用了拒绝的过滤条件(delivered_output + 同一轮 + 未被引用),因此 verifier 仍会收到本轮交付的产出——拒绝所要保护的保证得以保留,只是不再需要那次会制造新未引用条目的往返。
  • 并集按构造互斥(过滤条件排除了已引用的 UUID),不会有重复项到达 validateGoalEvidenceReferences(它对重复引用直接拒绝)。两个集合都是所呈现目录的子集,而该目录的上限就是 CATALOG_ENTRY_LIMIT = GOAL_EVIDENCE_REFERENCE_LIMIT(已在 goal-evidence.ts 中核实),因此 100 条引用上限不会溢出——PR 中「无需上限处理」的说法成立。
  • 范围受控:blocked 提案不并入任何内容,无目录路径不受影响,仓库范围内除本文件及其测试外没有其他读取被移除的 uncitedCurrentDeliveredOutput 结果的地方。
  • 测试钉住了改动:原拒绝测试现断言并集 ['tool-result-1', 'letter-x'] 到达 recordTerminalProposal;去重测试覆盖已引用的情形;blocked 测试守住范围边界。

一个 diff 之外的非阻塞观察:update_goal 的工具描述仍在指示那个因本修复而变为可选的 get_goal 往返。它依然收敛,此处无需改动——可作为后续的小改进。

测试证据

无人值守 CI 运行——本环境未构建或执行 PR 代码;以上证据为通过 API 读取的、该 PR 自身在被审提交上的 CI 结果。评审时主单元测试套件仍在运行,表格将在 CI 结束后原地更新。目前已完成的检查无失败。作者自报的本地数据(477 个测试通过、两次变异检验)是其声明,未在此独立复跑。

沙箱验证可以补齐剩余缺口:@qwen-code /verify ——「完成提案首次尝试即被记录、拒绝→续跑→重试循环不再发生」是端到端 Goal 运行时行为;单元测试钉住的是工具契约,只有真实 Goal 会话能证明循环已消失。

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

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 update_goal tool description still walks the model through the get_goal round trip that is now optional. It still converges, so it does not block; worth a follow-up once this lands.

Verdict: approve. The main unit suite is still in flight, so approval is deferred until CI lands green on 35943f5f4c7030d9fc2bbc0036f37ddce40c80ab; if anything lands red, the table above gets flagged instead.

中文说明

置信度:4/5 —— 针对复现充分的 P1 的干净、最小化修复;保留的一分是因为评审时主单元测试仍在运行,且端到端循环行为目前依赖 PR 自己的测试。

退一步看:这正是闸门期待的 PR。问题真实且被量化——一个 Goal 花了 34 分钟、860 万 token 反复提出完成,直到人工暂停。根因被精确理解:拒绝要求引用的产出,恰恰由服从拒绝这一行为本身制造。而修法是保住保证的最小改动——把这份本该交给 verifier 的列表直接交给提案,而不是要求它被复述回来。我在读 diff 之前独立构想的修法与此相同。没有顺手改动,没有范围蔓延;重试边界刻意留给 Goal 预算工作,混进来确实会把一次 bug 修复变成契约变更。如果六个月后由我来维护,注释里「为什么拒绝无法收敛」的解释正是我想要的上下文。

我唯一的保留意见与 diff 无关:update_goal 的工具描述仍在引导模型走那个如今已是可选的 get_goal 往返。它依然收敛,因此不构成阻塞;合并后值得跟进。

结论:批准。主单元测试仍在运行,因此批准推迟到 CI 在 35943f5f4c7030d9fc2bbc0036f37ddce40c80ab 上全绿之后;若有检查变红,则会转而在上方表格中标记。

Qwen Code · qwen3.8-max

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

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

中文说明

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

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

Comment thread packages/core/src/goals/goal-tools.ts
Comment thread packages/core/src/goals/goal-tools.test.ts Outdated
Comment thread packages/core/src/goals/goal-tools.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.

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 payload
  • packages/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 branch
  • packages/core/src/goals/goal-tools.test.ts:642 — [probe] No fixture cites a whitespace-padded ref, leaving the trimmed cited-set untested
  • packages/core/src/goals/goal-tools.test.ts:589 — [probe] result.terminateTurn is asserted only in auto-cite-empty branches
  • packages/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)

Comment thread packages/core/src/goals/goal-tools.test.ts
Comment thread packages/core/src/goals/goal-tools.test.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.

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)

Comment thread packages/core/src/goals/goal-tools.test.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: Test (ubuntu-latest, Node 22.x). Reviewed.

中文说明

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

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

@github-actions github-actions Bot removed the review/self-reported The linked issue was opened by the PR author (self-reported) label Aug 25, 2026
@qqqys

qqqys commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /review

@github-actions

Copy link
Copy Markdown
Contributor

Qwen Code review request accepted. Review is queued in 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.

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)

@qqqys

qqqys commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /takeover

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

Copy link
Copy Markdown
Collaborator

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

中文说明

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

@qwen-code-dev-bot

qwen-code-dev-bot commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

AutoFix round 1 finishedview run. See this round's report below.

中文说明

AutoFix 第 1 轮已完成 —— 查看运行。本轮报告见下方。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Autofix round: no code changes needed

This takeover round triaged everything newer than the last evaluation. No new code defect was found; the branch stays at 1fab00fea1 with no commit this round.

CI downgrade (review round 4) diagnosed as transient — already cleared

The round-4 review downgraded Approve → Comment citing CI failing: Test (ubuntu-latest, Node 22.x) on commit 1fab00fea1. Since then, without any code change, the same check re-ran green on the same commit (workflow run 32749628485, 2026-08-25 02:18–02:41 UTC), and a fresh automated review APPROVED 1fab00fea1 (2026-08-25 07:38 UTC). A failure that clears on re-run at the identical SHA with no code change is transient (infrastructure or flake), not a defect in this branch. This flow has no GitHub credentials to pull the failed run's logs; the green re-run at the same SHA is the load-bearing evidence, and local reproduction (below) found nothing to fix.

Local reproduction at 1fab00fea1:

  • cd packages/core && npx vitest run src/goals/goal-tools.test.ts — 37/37 passed, deterministic across three consecutive runs (the touched tests use no timers and no network).
  • The full packages/core suite in this sandbox fails only in files this PR does not touch (failures in config.test.ts, editor.test.ts, agent-headless.test.ts, worktree/session tests, etc.), all traceable to the sandboxed runner environment — e.g. EACCES: permission denied, mkdir '/home/github-runner/.qwen' and sandbox-mode detection — and several of those files pass when run standalone. This PR's diff touches only packages/core/src/goals/goal-tools.ts and its test file, so there is no causal path from the branch to those failures.
  • npm run typecheck — passed.

Finding dispositions

All 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:

Finding Disposition
R1-1 (Fixes #9877 would close the retry-bound scope) Addressed by the workflow, not in code: per the thread reply (2026-08-25 02:22 UTC) the PR body no longer carries a closing keyword for #9877, and follow-up issue #9891 tracks the retry/token-budget bound on autonomous continuations. Left open because its resolution is a PR-body change plus a follow-up issue, not code in this diff.
R1-2 (test executed twice, second result inspected) Resolved in code by b3b8cad7 — the de-dup test executes once and asserts recordTerminalProposal was called exactly once; re-verified in the current diff and green runs.
R1-3 (fold predicates unpinned) Resolved in code by b3b8cad7 + 44845cff — a same-turn external_fact decoy pins the proofKind predicate and a prior-turn delivered_output decoy pins the turnId predicate.
R2-1 (equality vs lineage-membership filter) Resolved in code by 44845cff'prior-turn' is inside lineageTurnIds, so a membership filter would fold the decoy and fail the ['letter-x', 'letter-y'] assertion.
R2-2 (union vs all-or-nothing skip-fold) Resolved in code by 44845cff — the uncited sibling letter-y must fold into the recorded evidenceRefs.
R3-1 (empty-fold key omission unpinned) Resolved in code by 1fab00fea1 — the blocked-path test captures the result and asserts autoCitedCurrentDeliveredOutput is absent.

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:

  1. "Blocked-path test never inspects the result payload" — superseded: 1fab00fea1 made that test capture the result and assert the empty-fold omission.
  2. "Every toEqual pin runs the empty branch" — the contract at risk (key present ⇔ something was folded) is pinned on both arms: omission by the blocked-path test, presence by the two completion tests. Declined — no discriminating gap worth another round post-approval.
  3. "No whitespace-padded ref fixture" — real but minor gap; build-time validation already pins trim-aware uniqueness (['same-reference', ' same-reference '] throws). Declined post-approval for convergence: the reviewer recorded this probe as not requested and then approved.
  4. "terminateTurn asserted only in auto-cite-empty branches" — terminateTurn is pinned on the recorded-complete path (true), the audit-blocked path (undefined), and the re-proposal path (true); the return that sets it does not branch on the fold result, so no realistic mutation discriminates on fold non-emptiness. Declined.
  5. "Blocked test pins only evidenceRefs" — real but minor gap: no test asserts blockerKind on the recorded proposal. Declined post-approval for convergence, per the round-3 observation that remaining Suggestion threads are better moved to a follow-up than iterated here.

No finding was deferred to the follow-up queue and none was escalated this round.

Verification

Commands actually run this round (no commit was made):

  • cd packages/core && npx vitest run src/goals/goal-tools.test.ts — 37/37 passed (three consecutive runs)
  • cd packages/core && npx vitest run (full package suite) — only sandbox-environment failures in files untouched by this PR (see diagnosis above); goals files green
  • npm run typecheck — passed
  • CI Test (ubuntu-latest, Node 22.x) at 1fab00fea1 — SUCCESS on re-run (run 32749628485); automated review APPROVED at the same commit
中文说明

Autofix 轮次:无需代码改动

本轮接管(takeover)对上次评估之后的全部反馈做了分诊。未发现新的代码缺陷;本轮不产生提交,分支保持在 1fab00fea1

CI 降级(评审第 4 轮)判定为瞬时问题——现已恢复

第 4 轮评审以「CI failing: Test (ubuntu-latest, Node 22.x)」为由,将批准(Approve)降级为评论(Comment),当时针对的是提交 1fab00fea1。此后在没有任何代码改动的情况下,同一检查在同一提交上重跑转绿(workflow 运行 32749628485,2026-08-25 02:18–02:41 UTC),随后一次全新的自动评审对 1fab00fea1 给出了批准(APPROVED)(2026-08-25 07:38 UTC)。在同一 SHA 上、没有任何代码变化时重跑即恢复的失败,属于瞬时问题(基础设施或偶发失败),不是本分支的缺陷。本流程没有 GitHub 凭据,无法拉取那次失败运行的日志;同一 SHA 重跑转绿是关键证据,本地复现(见下)也没有发现任何需要修复的问题。

1fab00fea1 上的本地复现:

  • cd packages/core && npx vitest run src/goals/goal-tools.test.ts — 37/37 通过,连续运行三次结果一致(本 PR 触及的测试不使用定时器、不使用网络)。
  • 本沙箱中运行 packages/core 全量测试套件时,失败只出现在本 PR 未触及的文件中(config.test.tseditor.test.tsagent-headless.test.ts、worktree/session 等测试),全部可归因于沙箱运行环境——例如 EACCES: permission denied, mkdir '/home/github-runner/.qwen' 以及沙箱模式检测——其中多个文件单独运行时通过。本 PR 的 diff 只触及 packages/core/src/goals/goal-tools.ts 及其测试文件,分支与这些失败之间不存在因果路径。
  • npm run typecheck — 通过。

各项发现的处理

评审第 1–3 轮提出的全部 6 条行内发现,均已在分支上修复,并由更早的轮次在各自线程中作答;本轮对每一条都在当前代码上重新核验:

发现 处理
R1-1(Fixes #9877 会连带关闭「重试边界」范围) 已由工作流处理,非代码改动:按该线程回复(2026-08-25 02:22 UTC),PR 正文已不再携带针对 #9877 的关闭关键字,后续 issue #9891 负责跟踪对自动续跑的「重试/token 预算」边界。线程保持未解决,因为其处理方式是 PR 正文修改加后续 issue,而非本 diff 中的代码。
R1-2(测试执行两次、只检查第二次结果) 已在代码中解决(b3b8cad7)——去重测试只执行一次,并断言 recordTerminalProposal 恰好被调用一次;已在当前 diff 与绿色运行中复核。
R1-3(折叠谓词无测试钉住) 已在代码中解决(b3b8cad7 + 44845cff)——同轮 external_fact 诱饵钉住 proofKind 谓词,更早轮次的 delivered_output 诱饵钉住 turnId 谓词。
R2-1(等值过滤 vs lineage 成员过滤) 已在代码中解决(44845cff)——'prior-turn' 已列入 lineageTurnIds,若改为成员过滤会折叠诱饵并使 ['letter-x', 'letter-y'] 断言失败。
R2-2(逐条并集 vs 整体跳过折叠) 已在代码中解决(44845cff)——未引用的兄弟条目 letter-y 必须被折叠进记录的 evidenceRefs
R3-1(空折叠时省略键无断言钉住) 已在代码中解决(1fab00fea1)——blocked 路径测试捕获返回结果并断言 autoCitedCurrentDeliveredOutput 不存在。

第 2 轮延后的探针(「已记录,本轮不要求」)

第 2 轮评审记录了 5 条探针观察并明确未作要求;第 4 轮未发布任何发现,且评审者此后已批准分支。逐条在当前代码上复核:

  1. 「blocked 路径测试未检查返回载荷」——已被覆盖:1fab00fea1 已让该测试捕获结果并断言空折叠时键被省略。
  2. 「所有 toEqual 钉住都运行空分支」——所涉契约(键存在 ⇔ 有内容被折叠)两侧均被钉住:省略由 blocked 路径测试钉住,存在由两个完成测试钉住。拒绝——批准后不再有值得再开一轮的区分性缺口。
  3. 「没有带空白填充的引用夹具」——真实但轻微的缺口;构建期校验已钉住「trim 后判重」(['same-reference', ' same-reference '] 会抛错)。以收敛为由在批准后拒绝:评审者记录该探针时明确不要求,随后又批准了分支。
  4. terminateTurn 只在 auto-cite 为空分支被断言」——terminateTurn 已在记录完成路径(true)、审计 blocked 路径(undefined)、重复提案路径(true)上被钉住;设置该值的返回语句不按折叠结果分支,因此没有现实的变异能以折叠非空来区分。拒绝。
  5. 「blocked 测试只钉住 evidenceRefs」——真实但轻微的缺口:没有测试断言记录提案上的 blockerKind。以收敛为由在批准后拒绝,依据第 3 轮观察:剩余 Suggestion 线程更适合转入后续跟踪,而不是在这里继续迭代。

本轮没有发现被延后到后续队列,也没有升级给维护者的事项。

验证

本轮实际运行的命令(未产生提交):

  • cd packages/core && npx vitest run src/goals/goal-tools.test.ts — 37/37 通过(连续三次运行)
  • cd packages/core && npx vitest run(全量包套件)— 仅在本 PR 未触及的文件中出现沙箱环境导致的失败(见上方诊断);goals 相关文件全绿
  • npm run typecheck — 通过
  • CI Test (ubuntu-latest, Node 22.x)1fab00fea1)— 重跑成功(运行 32749628485);自动评审在同一提交上批准

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@wenshao

wenshao commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Local end-to-end verification — recommend merge

I built a real verification environment for this PR rather than reading the diff: the actual interactive CLI, the real GoalRuntime, real Goal turns, real evidence catalog and a real verifier round trip, driven by a Goal-protocol-aware OpenAI-compatible mock model so the same model behaviour can be replayed byte-for-byte against two builds.

A/B method. One-file revert: packages/core/src/goals/goal-tools.ts reverted to its merge-base version (e0d933b23ef5) with everything else at PR head, bundled to dist-base; PR head 1fab00fea1 bundled to dist-pr. Both bundles verified to contain the expected branch (uncitedCurrentDeliveredOutput ×3 in base, autoCitedCurrentDeliveredOutput ×4 in PR). Linux, Node 22.22.2.

The modelled agent is deliberately well-intentioned and does exactly what the tool description asks: it emits the deliverable, calls get_goal, then proposes completion citing every uuid get_goal just returned. The only variable is whether it also narrates.


1. The defect reproduces, and it does not stop

dist-base, objective "Reply with exactly the single letter X and nothing else.", model narrates:

base (e0d933b2) PR (1fab00fe)
Goal turns 218 1
Model calls 655 2
Completion proposals 218 1
Proposals recorded 0 1
Verifier reached never once, accept
Outcome still running at 61 s, ended only because I pressed Esc Goal complete · 1 turn · 3.0 s

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

before

after

2. The verifier really does still receive the current turn's delivered output

This 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 01542806. 29b93bcf — the narration it emitted in the same step as the update_goal call, i.e. exactly the output the old refusal complained about — was folded in and reached the verifier with its full content. The claim in "Risk & Scope" holds on the wire.

3. Scope did not widen

  • blocked in a turn that also delivered uncited output (PR build): recorded with evidenceRefs exactly as supplied, and the result payload carries no autoCitedCurrentDeliveredOutput. Model sent ["bfe77efb-…"], runtime recorded ["bfe77efb-…"].
  • Compliant model (emits no text in the update_goal step): base completes in 1 turn / 2.4 s, PR completes in 1 turn / 2.4 s with the identical single-ref proposal. The PR is a no-op on the path that already worked.

4. The tests are non-vacuous, and the "cannot overflow" claim holds at the boundary

Seven 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:

mutation tests failed which
M1 drop the fold from the proposal 2 / 37 cites this turn's delivered output…, does not duplicate…
M2 de-dup predicate always true 2 / 37 same two
M3 drop entry.turnId === permit.turnId 1 / 37 does not duplicate output the completion already cited
M4 drop proofKind === 'delivered_output' 1 / 37 does not duplicate output the completion already cited
M5 fold for blocked too 1 / 37 leaves a blocked proposal to cite whatever it chose
M6 never report the field 2 / 37 cites this turn's…, does not duplicate…
M7 always report the field 4 / 37 incl. rejects a second proposal in the same exact turn

Both narrowing predicates and both reporting branches are pinned — the round-1 R1-3 concern is closed.

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 delivered_output. The recorded proposal carries exactly GOAL_EVIDENCE_REFERENCE_LIMIT = 100 unique refs. The union cannot overflow, so the absence of cap handling in the diff is correct.

5. Findings — none blocking

F1 · 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 update_goal step completes on the base build in one turn (verified above): a tool-call-only assistant record has an empty preview, so finish() in goal-evidence.ts never creates a catalog entry for it, and the get_goalupdate_goal round trip alone manufactures nothing. The gate is unsatisfiable for any model that narrates — which is every real one, and the reported session proves it. Worth rewording, because the current phrasing invites "then just fix the prompt" as a counter-proposal, and the honest answer is that the prompt fix has already been tried (goal-continuation-prompt.ts on main now carries two lines aimed at exactly this) and models still narrate.

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 · autoCitedCurrentDeliveredOutput is reported when nothing was recorded. recordTerminalProposal returns recorded: false when a proposal already exists for the turn (goal-runtime.ts:1399), and the payload still spreads the field, so the model reads proposalRecorded: false next to a claim that refs were folded in. Same as the round-5 deferred probe; cosmetic, one-line guard if you want it.

F4 · The guidance is now stale in two places. The update_goal description still walks the model through "call get_goal, wait for its result, and call update_goal in a later model step", and main's newer goal-continuation-prompt.ts adds "call get_goal in the same response before update_goal". Both are now optional. Follow-up, not this PR.

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 get_goal and update_goal evicts the oldest catalogued entry — and if the proposal cited it, the earlier invalidEvidenceRefs branch refuses the proposal, with exactly one evicted uuid per refusal. Same shape: reading the catalog and then proposing invalidates the proposal. Reproduced on the PR build: 990 Goal turns / 75 s / 74 completion proposals / 0 recorded, stopped only by hand.

sibling race

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

check result
packages/core goals suite @ 1fab00fe 396 / 396 pass (16 files)
full packages/core suite 21 424 pass, 2 fail — session-writer-lease.test.ts and skill-curator.test.ts, both chmod-based EACCES fixtures that cannot fail for uid 0; neither imports goals/. Unrelated to this PR.
npx tsc --noEmit -p packages/core clean
prettier --check + eslint, both changed files clean
merge into current main (5d5a2d9c31, 37 commits ahead) clean merge; goals suite on the merged tree 399 / 399

main has moved under this branch in the same area (goal-continuation-prompt.ts is new, goal-reducer.ts changed) and the merge is still clean and green.

Verdict

Recommend 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 blocked path and the already-working compliant path; and every branch of the new predicate is pinned by a test that fails when the branch is mutated. F1–F4 are wording and polish. F5 is a follow-up in the same family, not a defect in this diff.

中文版

本地端到端验证 —— 建议合入

我没有只读 diff,而是为这个 PR 搭建了一套真实验证环境:真实的交互式 CLI、真实的 GoalRuntime、真实的 Goal 轮次、真实的证据目录和真实的 verifier 往返,由一个理解 Goal 协议的 OpenAI 兼容 mock 模型驱动,因此同一份模型行为可以逐字节地在两个构建上重放。

A/B 方法:单文件回退 —— 把 packages/core/src/goals/goal-tools.ts 回退到 merge-base(e0d933b23ef5)的版本、其余全部保持 PR HEAD,打包为 dist-base;PR HEAD 1fab00fea1 打包为 dist-pr。两个 bundle 都已核对含有预期分支(base 中 uncitedCurrentDeliveredOutput 出现 3 次,PR 中 autoCitedCurrentDeliveredOutput 出现 4 次)。环境:Linux,Node 22.22.2。

被建模的 agent 是刻意「守规矩」的,完全按工具描述行事:先给出交付内容,再调用 get_goal,然后引用 get_goal 刚返回的全部 uuid 提出完成。唯一的变量是它是否顺带说了一句话。

1. 缺陷可复现,且不会自行停止

dist-base,目标为「Reply with exactly the single letter X and nothing else.」,模型带叙述:

base (e0d933b2) PR (1fab00fe)
Goal 轮次 218 1
模型调用 655 2
完成提案 218 1
被记录的提案 0 1
到达 verifier 从未 1 次,accept
结果 61 秒时仍在跑,只因我按了 Esc 才结束 Goal complete · 1 turn · 3.0 s

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."}]}

模型只提供了 0154280629b93bcf——它在调用 update_goal 的同一步里发出的那句叙述,也就是旧拒绝所抱怨的那份产出——被并入提案,并带着完整内容抵达 verifier。「风险与范围」一节的说法在真实链路上成立。

3. 范围没有扩大

  • 在同样产生了未引用产出的轮次里提出 blocked(PR 构建):记录下来的 evidenceRefs 与模型提供的完全一致,返回值中没有 autoCitedCurrentDeliveredOutput。模型发送 ["bfe77efb-…"],运行时记录 ["bfe77efb-…"]
  • 守规矩的模型(在 update_goal 那一步不发任何文字):base 1 轮 2.4 秒完成,PR 同样 1 轮 2.4 秒完成,提案的单条引用完全相同。在本来就能走通的路径上,本 PR 是无操作。

4. 测试非空洞,且「不会溢出」的论断在边界成立

对新谓词做了 7 个变异,每个都跑一遍被改动的测试文件。7 个全部被具名测试捕获,且文件内其它测试不受影响:

变异 失败测试数 哪几个
M1 从提案中移除并入 2 / 37 cites this turn's delivered output…does not duplicate…
M2 去重谓词恒真 2 / 37 同上两个
M3 去掉 entry.turnId === permit.turnId 1 / 37 does not duplicate output the completion already cited
M4 去掉 proofKind === 'delivered_output' 1 / 37 does not duplicate output the completion already cited
M5 对 blocked 也并入 1 / 37 leaves a blocked proposal to cite whatever it chose
M6 从不回报该字段 2 / 37 cites this turn's…does not duplicate…
M7 总是回报该字段 4 / 37 rejects a second proposal in the same exact turn

两个收窄谓词与两个回报分支都被钉住了 —— 第 1 轮的 R1-3 关切已闭合。

我另外写了一个一次性边界探针(不建议并入 PR):目录已满 100 条,模型引用其中 99 条,另有 1 条本轮未被引用的 delivered_output。记录下来的提案恰好是 GOAL_EVIDENCE_REFERENCE_LIMIT = 100 条、且互不重复。并集不可能溢出,diff 中不做上限处理是正确的。

5. 发现 —— 均不阻断

F1 · PR 描述把机制说过头了。 「那条拒绝无法被满足」并非代码的实际行为。update_goal 那一步不发任何用户可见文字的模型,在 base 构建上一轮即可完成(上文已验证):只含工具调用的 assistant 记录 preview 为空,goal-evidence.tsfinish() 根本不会为它建立目录条目,因此单纯的 get_goalupdate_goal 往返不会制造任何东西。这道闸门是对任何会叙述的模型不可满足 —— 也就是所有真实模型,被报告的那次 session 已经证明了这一点。建议改写措辞,因为现在的说法容易招来「那把提示词改好不就行了」的反驳,而诚实的答案是:提示词方案已经试过了(main 上的 goal-continuation-prompt.ts 现在正为此加了两行),模型照样会叙述。

F2 · 「重试没有任何边界」对被报告的那种形态成立,但并非普遍成立。 在交互式 CLI 中,跨轮确实没有边界 —— 61 秒 218 个 Goal 轮次,没有任何上限触发。单轮之内则有:当我让模型在同一轮内反复重试时,常开的 per-turn 工具调用上限在 4 秒内约 100 次调用后触发,弹出「A potential loop was detected」对话框并把 Goal 置为 paused。也就是说,轮内重试会被拦下(代价是要问人),跨轮重试不会。这个区别会影响 #9891 的预算该怎么划范围。

F3 · 提案未被记录时仍然回报 autoCitedCurrentDeliveredOutput 当本轮已存在提案时 recordTerminalProposal 返回 recorded: falsegoal-runtime.ts:1399),而返回值仍然展开该字段,于是模型会读到 proposalRecorded: false 与「已并入引用」并列出现。与第 5 轮延后记录的那条探针一致;属外观问题,若要修一行即可。

F4 · 有两处指引已经过时。 update_goal 的描述仍在引导模型「call get_goal, wait for its result, and call update_goal in a later model step」,而 main 上更新的 goal-continuation-prompt.ts 又加了「call get_goal in the same response before update_goal」。两者现在都已非必需。属后续事项,不必在本 PR 处理。

F5 · 同一类缺陷的「兄弟」在本 PR 之后依然存在。 一旦证据目录达到 100 条上限,模型在 get_goalupdate_goal 之间自己发出的叙述会挤掉最旧的那条目录条目;如果提案恰好引用了它,就会被更靠前invalidEvidenceRefs 分支拒绝,每次拒绝恰好列出一条被挤掉的 uuid。形态完全相同:读取目录再提出提案,这个动作本身让提案失效。在 PR 构建上复现:990 个 Goal 轮次 / 75 秒 / 74 次完成提案 / 0 次被记录,只能手动停下。

这不是回归,也不构成拦截本 PR 的理由 —— 它需要目录已满提案恰好引用了处在淘汰边界上的条目;而且在 base 上这个状态反而来得更快,因为正是本 PR 移除的那道闸门让轮次不断空转、把目录灌满。但它与第 3 轮收敛提示所指向的根因是同一个,也是 #9891 应当尽快落地、而非无限期延后的具体理由。

6. 检查项

检查 结果
packages/core goals 套件 @ 1fab00fe 396 / 396 通过(16 个文件)
packages/core 全量套件 21 424 通过、2 失败 —— session-writer-lease.test.tsskill-curator.test.ts,两者都用 chmod 构造 EACCES,而 uid 0 下不可能失败;两者都不 import goals/。与本 PR 无关。
npx tsc --noEmit -p packages/core 干净
两个改动文件的 prettier --check + eslint 干净
合入当前 main5d5a2d9c31,领先 37 个提交) 合并干净;合并树上的 goals 套件 399 / 399

main 在同一区域已经前移(新增 goal-continuation-prompt.tsgoal-reducer.ts 有改动),合并仍然干净且全绿。

结论

建议合入。 缺陷真实且可端到端复现;本修复让此前无法终止的那次运行收敛;那条拒绝所保护的保证在真实链路上得以保留;blocked 路径与本来就能走通的守规矩路径上范围都没有扩大;新谓词的每个分支都有测试钉住,变异后都会失败。F1–F4 属措辞与打磨,F5 属同一类问题的后续事项,而非本 diff 的缺陷。

@wenshao
wenshao added this pull request to the merge queue Aug 25, 2026
Merged via the queue into QwenLM:main with commit 814b18d Aug 25, 2026
276 of 282 checks passed
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.22.2.

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants