Skip to content

feat(goal): let a Goal stop early when its objective is infeasible - #10125

Merged
yiliang114 merged 1 commit into
QwenLM:mainfrom
qqqys:goal/d2b-infeasible-blocker
Aug 26, 2026
Merged

feat(goal): let a Goal stop early when its objective is infeasible#10125
yiliang114 merged 1 commit into
QwenLM:mainfrom
qqqys:goal/d2b-infeasible-blocker

Conversation

@qqqys

@qqqys qqqys commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Adds blockerKind: 'infeasible' to Goal terminal proposals, so a Goal whose objective cannot be satisfied as written can stop on the turn that proves it instead of burning until the token budget does. It is not a new status: the Goal settles as blocked, which every surface already renders and which resumes into /goal edit — the only fix for an objective that cannot hold.

Three rules keep it from becoming an "I think this can't be done" exit. It bypasses the three-turn repetition rule that ordinary technical blockers need — waiting three turns to report an impossibility is the runaway this kind exists to end, and the evidence bar is what earns the early exit. The cited evidence must include an external_fact (a tool result): user input can authorise a stop, which is what authority is for, but cannot make an objective impossible, and assistant prose saying so is exactly what must not count; like the other immediate blockers it must also cite every newer record so a contradicting fact cannot be omitted. And the verifier policy accepts it only for self-contradiction, a target that verifiably does not exist, or an action outside the tools, and rejects difficulty, uncertainty, obtainable information, or a preference to ask.

An accepted infeasible stop appends a fixed next step to lastReason ("edit or replace the Goal with an objective the evidence allows before resuming it"), so the stopped Goal tells the user what to do, not only what went wrong. The update_goal tool description and schema mirror the policy so the model learns when the kind applies and what it must cite.

Why it's needed

Blocked proposals stop immediately only for user authority or an external change; everything else is treated as a repeated technical blocker and must recur with identical reason text on three consecutive turns before the verifier sees it. There was no sanctioned way for the model to say "this cannot be done as stated" that the verifier would honour early, so an impossible objective ran until #9891's budget or a human stopped it. The session that motivated this series spent 34 minutes and 8.6M tokens on an objective ("验证下版本") too under-specified to ever complete. CC's stop evaluator can answer {ok: false, impossible: true} and end the loop; this is the counterpart, held to a stricter evidence bar because our verifier judges from cited transcript records, not from the model's own assessment.

Reviewer Test Plan

How to verify

  • cd packages/core && npx vitest run src/goals/ — 437 tests, 16 files. New: an evidence-validation case (assistant-only or user-only citations are refused with infeasible_blocker_external_fact_required; a tool result is accepted with proofKind: 'external_fact'; leaving newer evidence uncited is refused like any immediate blocker), a runtime case (an infeasible proposal is readyForVerification on its first turn, the verifier receives the infeasible policy sentence, and the accepted Goal is blocked with the next step appended to lastReason), and tool-schema/description assertions.
  • Mutation probes, each run on the three affected suites (194 tests) and each killing exactly one test: route infeasible through the repetition audit; remove the policy sentence; remove the external_fact requirement; drop the lastReason suffix; remove 'infeasible' from the schema enum.
  • npx tsc --noEmit in packages/core: 0 errors in src/goals/, total identical to the merge base. prettier + eslint clean on the seven changed files. No SDK, webui, web-shell or CLI code enumerates blockerKind, so nothing crosses the wire.

Evidence (Before & After)

N/A (runtime policy; the stop renders through the existing blocked-Goal surfaces). lastReason of an accepted infeasible stop, for reference: <verifier reason> The objective cannot be satisfied as written; edit or replace the Goal with an objective the evidence allows before resuming it.

Tested on

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

Environment (optional)

N/A (unit tests only).

Risk & Scope

  • Main risk or tradeoff: a model could over-use infeasible to escape hard work. The three rules above are the mitigation — external-fact evidence, full newer-evidence citation, and a verifier policy that names the disqualifiers — and the outcome is blocked, which the user can resume or edit, never a silent completion.
  • Not validated / out of scope: no change to the blocked-audit fingerprint machinery (infeasible never enters it); no new status, no wire changes; no attempt to detect infeasibility at /goal set time — that stays the model's call, judged by the verifier.
  • Breaking changes / migration notes: none. blockerKind is optional and the new member is additive.

Linked Issues

中文说明

这个 PR 做了什么

为 Goal 终局提案新增 blockerKind: 'infeasible',让一个「按现有表述无法达成」的 Goal 能在证明这一点的那一轮就停下,而不是烧到 token 预算耗尽。它不是新状态:Goal 落为 blocked,所有界面已能渲染,且 resume 的出口就是 /goal edit——对一个不可能成立的 objective,这是唯一的修法。

三条规则防止它变成「我觉得做不到」的逃生口。它绕过普通技术性阻塞所需的三轮重复规则——等三轮再报告不可能,正是这个类型要终结的失控,而证据门槛才是换取提前退出的代价。引用的证据必须包含一条 external_fact(工具结果):用户输入可以授权停止(那是 authority 的用途),但不能让 objective 变得不可能,助手自己说不可能更是绝不能算数;和其他立即阻塞一样,它还必须引用所有更新的记录,以防遗漏相反事实。verifier 策略只在 objective 自相矛盾、指向可验证不存在的目标、或需要工具无法执行的动作时接受,并明确拒绝困难、不确定、可获取的信息、或想要询问这几种情形。

被接受的 infeasible 停止会在 lastReason 末尾附加固定的下一步(「编辑或替换 Goal,给出证据允许的 objective,再恢复」),让停止后的 Goal 告诉用户该做什么,而不只是哪里出了问题。update_goal 的工具描述与 schema 同步了策略,使模型知道该类型何时适用、必须引用什么。

为什么需要

阻塞提案目前只在用户授权或外部变化时立即停止;其余一律视为重复性技术阻塞,必须在连续三轮以完全相同的 reason 文本重现,verifier 才会看到。模型没有任何被认可的方式说出「按现在的表述做不到」并让 verifier 尽早采纳,于是一个不可能的 objective 会一直跑到 #9891 的预算或人工介入。触发本系列的那个 session 在「验证下版本」这种含混到无法完成的 objective 上耗了 34 分钟、860 万 token。CC 的停止评估器可以返回 {ok: false, impossible: true} 结束循环;这是对应物,只是证据门槛更严,因为我们的 verifier 依据引用的转录记录判断,而不是模型的自我评估。

评审验证计划

如何验证

  • cd packages/core && npx vitest run src/goals/——437 个测试,16 个文件。新增:证据校验用例(仅引用助手或用户记录以 infeasible_blocker_external_fact_required 拒绝;工具结果以 proofKind: 'external_fact' 接受;遗漏更新证据与其他立即阻塞一样被拒绝)、runtime 用例(infeasible 提案首轮即 readyForVerification,verifier 收到 infeasible 策略句,接受后的 Goal 为 blockedlastReason 附带下一步)、工具 schema/描述断言。
  • 变异检验,在三个相关套件(194 个测试)上运行,每个恰好挂一个测试:让 infeasible 走重复审计;删掉策略句;删掉 external_fact 要求;去掉 lastReason 后缀;从 schema 枚举移除 'infeasible'
  • packages/corenpx tsc --noEmit:src/goals/ 内 0 错,总数与合并基线相同。七个改动文件 prettier + eslint 干净。SDK、webui、web-shell、CLI 均不枚举 blockerKind,无线上变更。

证据(前后对比)

N/A(运行时策略;停止状态通过现有的 blocked Goal 界面渲染)。被接受的 infeasible 停止的 lastReason 形如:<verifier 理由> The objective cannot be satisfied as written; edit or replace the Goal with an objective the evidence allows before resuming it.

已测试平台

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

环境(可选)

N/A(仅单元测试)。

风险与范围

  • 主要风险或权衡:模型可能滥用 infeasible 逃避困难工作。上述三条规则即是缓解——外部事实证据、完整引用更新证据、点名排除项的 verifier 策略——且结果是 blocked,用户可以 resume 或 edit,绝不会静默完成。
  • 未验证/范围外:不改动 blocked-audit 指纹机制(infeasible 从不进入它);无新状态、无线上变更;不在 /goal set 时尝试检测不可行性——那仍由模型提出、verifier 判断。
  • 破坏性变更/迁移说明:无。blockerKind 为可选字段,新成员是增量。

关联 Issue

A Goal whose objective cannot be satisfied as written -- it contradicts
itself, names a target that verifiably does not exist, or needs an
action no tool can perform -- had no sanctioned way to say so. Blocked
proposals stop immediately only for user authority or an external
change; everything else is treated as a repeated technical blocker and
must recur on three consecutive turns before the verifier sees it. So an
impossible objective burned turns until the token budget (QwenLM#9891) or a
human stopped it. The session that motivated this series ran 34 minutes
on an objective ("验证下版本") too under-specified to ever complete. CC's
stop evaluator can answer `impossible` and end the loop; this is the
counterpart.

`blockerKind: 'infeasible'` joins authority, external and repeated. It
is not a new status: the Goal settles as `blocked`, which every surface
already renders and which resumes into `/goal edit` -- the only fix for
an objective that cannot hold.

Three rules keep it from becoming an "I think this can't be done" exit:

- It bypasses the three-turn repetition rule. Waiting three turns to
  report an impossibility is the runaway this kind exists to end, and
  the evidence bar below is what earns the early exit.
- The cited evidence must include an external_fact. User input can
  authorise a stop (that is `authority`) but cannot make an objective
  impossible, and assistant prose saying so is exactly what must not
  count. Like the other immediate blockers it must also cite every newer
  record, so a contradicting fact cannot be left out.
- The verifier policy accepts it only for self-contradiction, a target
  that verifiably does not exist, or an action outside the tools, and
  rejects difficulty, uncertainty, obtainable information, or a
  preference to ask.

An accepted infeasible stop appends a fixed next step to lastReason, so
the stopped Goal tells the user what to do, not only what went wrong.

Mutation probes (goal-evidence + goal-runtime + goal-tools, 194 tests),
each killing exactly one test: infeasible routed through the repetition
audit; policy sentence removed; external_fact requirement removed; next
step suffix dropped; 'infeasible' removed from the tool schema enum.
@qqqys

qqqys commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /takeover

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

Copy link
Copy Markdown
Collaborator

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

中文说明

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

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

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

Problem: observed, not theoretical. The PR documents a concrete runaway — a session that spent 34 minutes and 8.6M tokens on an objective too under-specified to ever complete — and the current rules genuinely have no exit for that case: only authority/external stop immediately, every other blocker must recur with the exact same reason text across three consecutive turns before the verifier even sees it. The linked predecessors (#9891, #9880) are both merged, and notably the legacy Stop-hook goal system already ends on an impossible judge verdict — the V2 runtime is the one surface missing this counterpart.

Direction: aligned. This completes the stop side of the Goal series (#9880 completion gate, #9891 token-budget backstop), mirroring Claude Code's stop-evaluator impossible answer (no direct CHANGELOG reference found, but the area is clearly relevant). No auth/sandbox/model-selection/telemetry surface is touched, and the new enum member is additive: nothing outside packages/core/src/goals/ enumerates blockerKind — verified by grep across all packages and integration tests.

Size: core paths touched (packages/core/src/goals/): 65 production lines vs 142 test lines, no generated/schema lines. Well below any escalation threshold.

Approach: scope feels right. I worked through the problem independently before reading the diff and landed on essentially this design — a new blockerKind member excluded from the three-turn audit, held to tool-result evidence, with the policy spelled out for the verifier. Every edit is load-bearing for the stated goal; no drive-by changes. The obvious simpler alternative — "just shorten the three-turn rule" — would weaken the evidence bar for ordinary blockers, while infeasible earns its early exit precisely through the stricter bar.

Risk: no elevated risk signals — none of the changed files match the revert-correlated high-risk paths.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

问题:已观测到,不是理论性的。PR 记录了一个具体的失控案例——一个 session 在含混到无法完成的 objective 上耗了 34 分钟、860 万 token——而现有规则确实没有为此提供出口:只有 authority/external 能立即停止,其他阻塞必须以完全相同的 reason 文本连续三轮重现,verifier 才会看到。关联的两个前置 PR(#9891#9880)均已合并;值得注意的是旧版 Stop-hook goal 系统在法官判定 impossible 时就会终止——V2 runtime 正是缺少这个对应物的界面。

方向:对齐。这补全了 Goal 系列的停止侧(#9880 完成闸门、#9891 token 预算兜底),对应 Claude Code 停止评估器的 impossible 回答(CHANGELOG 未找到直接引用,但该领域显然相关)。不触及 auth/沙箱/模型选择/遥测,新枚举成员是增量的:packages/core/src/goals/ 之外没有任何代码枚举 blockerKind——已 grep 验证所有包与集成测试。

规模:触及核心路径(packages/core/src/goals/):65 行生产代码,142 行测试,无生成/schema 行。远低于任何升级阈值。

方案:范围合理。我在读 diff 之前独立推演了同一问题,得到的设计基本一致——新增一个排除在三轮审计之外的 blockerKind 成员、以工具结果作为证据门槛、向验证器明示策略。每处改动都服务于既定目标,无夹带改动。显而易见的更简替代——"直接缩短三轮规则"——会削弱普通阻塞的证据门槛,而 infeasible 正是靠更严的门槛换取提前退出。

风险:无升级风险信号——改动文件均未命中与 revert 相关的高风险路径。

进入代码审查 🔍

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Code review

I sketched my own approach from the title and motivation before reading the diff: a new blockerKind member that skips the three-turn audit, is held to tool-result evidence, and gets an explicit policy sentence for the verifier. The PR matches that almost exactly, and I found no correctness gaps in the routing:

  • The new member is excluded from isRepeatedBlockerProposal, and that function's three call sites are the only routing decisions: recordTerminalProposal (infeasible never enters the blocked-audit fingerprint and is readyForVerification on its first turn), the catalog-truncation gate in evidence validation (fails closed like the other immediate blockers), and the verifier-reject path (checkpoint attempt on reject, same as authority/external).
  • The evidence bar is sound: external_fact can only come from a tool_result record — system records never enter the catalog, and user/assistant provenance maps to user_input/delivered_output — or from a checkpoint claim, which the checkpoint verifier is bound to keep proofKind-faithful. Assistant prose or user input genuinely cannot satisfy the requirement, which is the whole point of this kind.
  • The lastReason next-step suffix is applied only on verifier accept, and every lastReason consumer (legacy projection, limit-kind matching, recovery feedback) treats it as free text, so nothing downstream breaks.
  • The verifier payload carries both blockerKind and the full blockedPolicy sentence, so the independent judge sees the accept/reject criteria, not just the label.

One non-blocking nit: the checkpoint-claim coverage test in goal-evidence.test.ts parameterizes over ['authority', 'external']; 'infeasible' now shares that exact code path but isn't in the list. Worth adding on a future pass — behavior is covered by the shared path, so this doesn't block.

One honest testing note: the new runtime test exercises the real runtime end to end, but the verifier's decision is mocked there — the evidence-validation, routing, and schema assertions run against real code, while the model judgment is faked. See the verification note below.

CI evidence

As of this fetch, the PR's own CI (Qwen Code CI) is still running: the Linux unit-test job is in flight; macOS/Windows/integration jobs show as skipped so far (not yet started for this commit). Security Checks, Qwen Autofix, both Desktop Shell builds, TruffleHog, and the dependency CVE audit are green. The table below is updated in place once CI settles:

Final CI results for db075bb (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
route ✅ 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,失败项排在最前。

The author reports 437 goal tests passing locally plus mutation probes killing each new rule — that is the author's claim, not independently re-run; the CI results above are the evidence that counts here.

Sandboxed verification would settle the remaining gap: @qwen-code /verify — the suite mocks the verifier's decision, so whether a real verifier accepts a genuine infeasible stop (and rejects difficulty or uncertainty dressed up as one) is not pinned by these tests; an A/B run against the base build would show the early stop actually happening end to end.

中文说明

代码审查:我在读 diff 前先独立推演了方案,PR 与之几乎一致,且路由上没找到正确性缺口:新成员被排除在 isRepeatedBlockerProposal 之外,而该函数的三个调用点是仅有的路由决策——recordTerminalProposal(infeasible 从不进入阻塞审计指纹,首轮即 readyForVerification)、证据校验的目录截断闸门(与其他立即阻塞一样失败即关)、验证器拒绝路径(拒绝时生成 checkpoint,与 authority/external 一致)。证据门槛是可靠的:external_fact 只能来自 tool_result 记录(system 记录从不进入目录,用户/助手分别映射为 user_input/delivered_output)或 checkpoint 声明(checkpoint 验证器被约束保持 proofKind 忠实)。助手自述或用户输入确实无法满足该要求——这正是这个类型的意义所在。lastReason 的下一步后缀只在验证器接受时附加,所有 lastReason 消费方都按自由文本处理,下游无破坏。验证器载荷同时携带 blockerKind 和完整 blockedPolicy 句,独立评审能看到接受/拒绝标准,而不只是标签。

一个非阻塞小项:goal-evidence.test.ts 中 checkpoint 声明覆盖测试对 ['authority', 'external'] 参数化;'infeasible' 现在走同一条代码路径但不在列表里,建议后续补上——行为由共享路径覆盖,不构成阻塞。

诚实的测试说明:新的 runtime 测试端到端驱动了真实 runtime,但其中验证器的裁决是 mock 的——证据校验、路由、schema 断言跑在真实代码上,模型判断是假的。见下方验证说明。

CI 证据:截至抓取时,PR 自身 CI(Qwen Code CI)仍在运行:Linux 单元测试进行中;macOS/Windows/集成测试暂显示 skipped(尚未开始)。Security Checks、Qwen Autofix、两个 Desktop Shell 构建、TruffleHog、依赖 CVE 审计均为绿。表格会在 CI 落定后就地更新。作者自述 437 个 goal 测试通过、变异检验杀死每条新规则——这是作者声明,未经独立复跑;上表 CI 结果才是有效证据。

沙箱验证可补齐剩余缺口:@qwen-code /verify——测试套件 mock 了验证器裁决,因此真实验证器是否会接受真正的 infeasible 停止(并拒绝把困难/不确定伪装成不可行)并未被这些测试钉死;对基线构建做一次 A/B 运行即可端到端证明提前停止确实发生。

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — clean, minimal change that closes a real gap; the open items are the still-running CI and one non-blocking test nit, not the code.

This one earns its place. The runaway it fixes is documented — 34 minutes and 8.6M tokens on an objective that could never complete — and the V2 runtime genuinely had no sanctioned exit for it. The design pays for the early stop with a real evidence bar instead of trust: only a tool result can carry the claim, every newer record must be cited alongside it, and the independent verifier gets an explicit accept/reject policy that names the disqualifiers. The outcome is a resumable blocked, never a silent completion, so a mistaken infeasible call costs one verifier round-trip rather than the goal. I walked every blockerKind consumer before being confident: there are exactly four, all inside src/goals/, none elsewhere in the repo, and all four handle the new member consistently. My independent sketch of the fix and the PR's implementation landed on the same design, which is usually the sign there is no materially simpler path being missed.

What keeps this at 4 rather than 5: the unit suite mocks the verifier's judgment, so how a real verifier behaves against the new policy sentence is argued, not demonstrated — that's what the /verify note in my review comment is for — and the checkpoint-claim test list should grow 'infeasible' on a future pass. Neither blocks.

Verdict: approve, but the PR's own CI (Qwen Code CI) is still running on the reviewed commit, so the approval is deferred until it lands green — approval will be pinned to the exact commit reviewed here and will not carry over to any later push.

中文说明

这个 PR 配得上合入。它修复的失控有记录在案——34 分钟、860 万 token 耗在一个永远无法完成的 objective 上——而 V2 runtime 此前确实没有为此提供被认可的退出方式。设计上用真实的证据门槛而非信任来换取提前停止:只有工具结果能承载该论断,所有更新的记录必须一并引用,独立验证器拿到的是点名了排除项的明确接受/拒绝策略。结果是可恢复的 blocked,绝不是静默完成,因此一次误判的 infeasible 只损失一轮验证往返,而不是整个 Goal。在确信之前我走查了每一个 blockerKind 消费方:恰好四个,全部位于 src/goals/ 内,仓库其他地方没有,四者对新成员的处理一致。我独立推演的方案与 PR 实现落在同一设计上——这通常意味着没有遗漏更简的路径。

没给到 5 分的原因:单元测试 mock 了验证器的裁决,真实验证器面对新策略句的行为只是被论证、尚未被演示——这正是审查评论中 /verify 一行的用途——另外 checkpoint 声明测试列表后续应加入 'infeasible'。两者都不构成阻塞。

结论:通过,但 PR 自身 CI(Qwen Code CI)在受审提交上仍在运行,因此批准推迟到 CI 全绿——批准将钉在受审的精确提交上,不会延续到任何后续推送。

Qwen Code · qwen3.8-max

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

@yiliang114
yiliang114 added this pull request to the merge queue Aug 26, 2026
Merged via the queue into QwenLM:main with commit 854356d Aug 26, 2026
111 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