Skip to content

refactor(core): hold Goal sends to the caller's recursion budget, and say why the other carve-outs stay - #10259

Open
qqqys wants to merge 4 commits into
QwenLM:mainfrom
qqqys:goal/d3-goal-turn-ceilings-v2
Open

refactor(core): hold Goal sends to the caller's recursion budget, and say why the other carve-outs stay#10259
qqqys wants to merge 4 commits into
QwenLM:mainfrom
qqqys:goal/d3-goal-turn-ceilings-v2

Conversation

@qqqys

@qqqys qqqys commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Audits the places where client.ts carves runtime-scheduled Goal turns out of the session ceilings, now that a Goal has its own autonomous spend bound (#9891), and acts on each by its own merits. One exemption goes: a Goal-type send no longer ignores the caller's recursion budget (turns) — boundedTurns is Math.min(turns, MAX_TURNS) for every message type. Three stay, with their comments rewritten to say the real reason they stay rather than the reason that stopped being true when the token budget landed.

Site Ceiling it bypasses Why it was added (#7895) Verdict Why
boundedTurns = Goal ? MAX_TURNS : min(turns, MAX_TURNS) The per-send recursion budget (MAX_TURNS = 100 nested steer / next-speaker / hook continuations) Goal turns should run "beyond the former fixed limit" regardless of what the caller passed Removed Every runtime continuation is a fresh top-level send that starts from MAX_TURNS on its own; no production caller passes turns for a Goal send (TUI and headless both use the default; ACP does not go through client.sendMessageStream at all). The carve-out only ever mattered for a caller handing over an exhausted budget, and for that caller a Goal should be refused like everything else — which it now is, and the interrupted-exit path pauses the Goal instead of leaving it running with a permit nobody will finish.
if (messageType !== Retry && !isGoalRuntimeTurn)sessionTurnCount++ and the max_session_turns check maxSessionTurns (user-set, default unlimited) "runtime-scheduled Goal turns … without session budgets" Retained, comment rewritten sessionTurnCount increments on every model call the user's prompts drive, tool continuations included. Counting Goal turns would let a user cap of N kill a healthy Goal after roughly N/4 continuations, with no resume path in headless (exit 53). The Goal's spend is bounded by its own token budget, re-armed only by an explicit resume or edit; and the headless host already excludes runtime Goal turns from the same cap (enforceSessionTurnLimit(isRuntimeGoalTurn)), so counting them here would split the two ceilings.
takeSteerInput: !isGoalRuntimeTurn && sessionTurnCount >= maxSessionTurns Same ceiling, applied to steer admission Consistency with the row above Retained, comment added A turn that does not count toward the cap must not be refused steer input on that count. Stands or falls with the row above.
Legacy hook Goal: hookTurnBudget = activeGoal ? boundedTurns : boundedTurns - 1 The recursion budget again, for the pre-runtime stop-hook Goal chain "bounded by stopHookBlockingCap / MAX_GOAL_ITERATIONS" Retained, comment extended This chain recurses inside one sendMessageStream call, so the runtime's token budget — which meters continuations the Goal runtime schedules — never sees it. Its bounds are MAX_GOAL_ITERATIONS (50) and stopHookBlockingCap (8, pauses the Goal); a 50-iteration chain with steer and next-speaker continues would exhaust MAX_TURNS before its own cap, so decrementing here would trade one runaway guard for a new way to kill a legitimate run.

Two further goal-runtime carve-outs in this file are not ceilings and are untouched: the hook-re-entry microcompaction checkpoint and the managed auto-memory background tasks are skipped for runtime turns. Neither has anything to do with budget; they are noted here so the next reader does not mistake them for the four above.

Why it's needed

The series plan recorded these as "the client.ts goal-turn ceiling exemptions to remove once a budget exists" (#9891 is that budget). Read one by one they are not one thing: one is a recursion budget the Goal had no reason to ignore, two are a user-set session cap whose semantics (every model call counts) would make Goals unusable under it, and one guards a legacy path the token budget cannot reach. Removing all four on the strength of "there is a budget now" would have regressed the two user-visible ones. The value of this PR is as much the recorded reasoning as the one-line removal: every remaining exemption now carries the reason it actually stays.

Reviewer Test Plan

How to verify

  • cd packages/core && npx vitest run src/core/client.test.ts src/core/client-goal.test.ts src/goals/ — 18 files, 824 tests. New case: holds a runtime Goal turn to the caller recursion budget like any other send (a Goal send with turns = 0 does not run the model, pauses the Goal through the interrupted-exit path, and finishes the permit). The existing 75-turn runtime test no longer passes 0 as the budget — it was only ever passing because of the carve-out this PR removes; with the default budget it still runs all 75 turns and still leaves sessionTurnCount at 0, which pins the retained session-turn exclusion.
  • Mutation probe: re-adding messageType === SendMessageType.Goal ? MAX_TURNS : … fails exactly the new test (22 others green); restored, 23/23.
  • npx tsc --noEmit in packages/core: 32 errors with and without the diff (pre-existing dependency skew outside this change, verified by exporting the diff, checking out the pristine files, and re-applying — no git stash). prettier + eslint clean on both files.

Evidence (Before & After)

N/A (no user-visible change for any production caller; the only behavioral change is for a caller passing an exhausted turns budget to a Goal send, which none does).

Tested on

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

Environment (optional)

N/A (unit tests only).

Risk & Scope

  • Main risk or tradeoff: retaining the maxSessionTurns exclusion means a user who sets a session-turn cap still sees Goals run past it. That is deliberate and now documented at the site: the Goal is bounded by its own token budget, and the headless host applies the same exclusion, so the alternative is two ceilings that disagree. If the project later wants Goals under the session cap, the right shape is a per-spend-window count reset on resume, not this exclusion's removal.
  • Not validated / out of scope: the headless host's own enforceSessionTurnLimit exclusion (mirrors the retained one; untouched). The attribution-snapshot and auto-memory carve-outs for runtime turns (not ceilings; noted above). ACP, which drives GeminiChat directly and never passes through these checks.
  • Breaking changes / migration notes: none.

Linked Issues

中文说明

这个 PR 做了什么

审计 client.ts 中把运行时调度的 Goal 轮次排除在会话上限之外的各处豁免——现在 Goal 已经有了自己的自主消费边界(#9891)——并逐一按各自的理由处理。撤掉一处:Goal 类型的发送不再忽略调用方给的递归预算(turns),boundedTurns 对所有消息类型统一为 Math.min(turns, MAX_TURNS)。保留三处,并重写注释,写明它们真正保留的理由,而不是那个在 token 预算落地后已不再成立的理由。

位置 绕过的上限 当初为何加入(#7895) 结论 理由
boundedTurns = Goal ? MAX_TURNS : min(turns, MAX_TURNS) 单次发送的递归预算(MAX_TURNS = 100 层嵌套的 steer / next-speaker / hook 续跑) Goal 轮次应「超越旧的固定上限」运行,不管调用方传了什么 撤掉 每次运行时续跑都是一次全新的顶层发送,自己从 MAX_TURNS 起步;生产中没有任何调用方给 Goal 发送传 turns(TUI 和 headless 都用默认值;ACP 根本不经过 client.sendMessageStream)。这个豁免只对「调用方递交一个已耗尽预算」的情形有意义,而这种情形下 Goal 应该和其他类型一样被拒绝——现在正是如此,并且中断退出路径会暂停 Goal,而不是让它带着一个无人完成的 permit 继续「运行」。
if (messageType !== Retry && !isGoalRuntimeTurn)——sessionTurnCount++max_session_turns 检查 maxSessionTurns(用户设定,默认不限) 「运行时调度的 Goal 轮次……不受会话预算约束」 保留,重写注释 sessionTurnCount 对用户提示驱动的每一次模型调用递增,含工具续跑。若把 Goal 轮次计入,用户设的上限 N 会让一个健康的 Goal 在大约 N/4 次续跑后中途死亡,而 headless 中没有恢复路径(退出码 53)。Goal 的消费由它自己的 token 预算约束,且只能由显式的 resume 或 edit 重新武装;headless host 已经把运行时 Goal 轮次排除在同一上限之外(enforceSessionTurnLimit(isRuntimeGoalTurn)),在这里计入会让两处上限分裂。
takeSteerInput:!isGoalRuntimeTurn && sessionTurnCount >= maxSessionTurns 同一上限,作用于 steer 准入 与上一行保持一致 保留,补注释 不计入上限的轮次也不能因该计数被拒绝 steer 输入。与上一行同进退。
旧版 hook Goal:hookTurnBudget = activeGoal ? boundedTurns : boundedTurns - 1 同样是递归预算,针对 runtime 之前的 stop-hook Goal 链 「受 stopHookBlockingCap / MAX_GOAL_ITERATIONS 约束」 保留,扩展注释 这条链在单次 sendMessageStream 调用内递归,而 runtime 的 token 预算只计量 Goal runtime 调度的续跑,永远看不到它。它的边界是 MAX_GOAL_ITERATIONS(50)和 stopHookBlockingCap(8,会暂停 Goal);一条 50 次迭代、带 steer 与 next-speaker 续跑的链会在触及自己的上限之前先耗尽 MAX_TURNS,所以在这里递减等于用一种新的杀死合法运行的方式换掉一个失控防护。

本文件中还有两处针对 goal-runtime 的豁免不是上限,未改动:hook 重入时的 microcompaction 检查点,以及托管自动记忆的后台任务,对运行时轮次都会跳过。二者与预算无关;在此注明,以免下一位读者把它们误认为上述四处之一。

为什么需要

系列计划把这些记录为「预算落地后要撤掉的 client.ts goal 轮次上限豁免」(#9891 就是那个预算)。逐一读过之后它们并不是同一件事:一处是 Goal 没有理由忽略的递归预算,两处是一个用户设定的会话上限、其语义(每次模型调用都计数)会让 Goal 在其下无法使用,还有一处守护着 token 预算触及不到的旧路径。仅凭「现在有预算了」就全部撤掉,会让其中两处用户可见的行为倒退。本 PR 的价值既在于那一行删除,更在于记录下来的推理:每一处保留的豁免现在都写明了它真正保留的原因。

评审验证计划

如何验证

  • cd packages/core && npx vitest run src/core/client.test.ts src/core/client-goal.test.ts src/goals/——18 个文件,824 个测试。新增用例:holds a runtime Goal turn to the caller recursion budget like any other send(turns = 0 的 Goal 发送不会调用模型,通过中断退出路径暂停 Goal,并完成 permit)。原有的 75 轮运行时测试不再传 0 作为预算——它此前之所以能过,只是因为本 PR 撤掉的这个豁免;改用默认预算后它仍跑完全部 75 轮,sessionTurnCount 仍为 0,这固定住了保留下来的会话轮次排除。
  • 变异检验:重新加回 messageType === SendMessageType.Goal ? MAX_TURNS : …,恰好挂新测试(其余 22 绿);恢复后 23/23。
  • packages/corenpx tsc --noEmit:有无 diff 均为 32 个错误(本变更之外的已有依赖偏差,通过导出 diff、检出原始文件、再重新应用来验证——未使用 git stash)。两个文件 prettier + eslint 干净。

证据(前后对比)

N/A(对任何生产调用方都没有用户可见的变化;唯一的行为变化针对给 Goal 发送传入已耗尽 turns 预算的调用方,而现实中没有这样的调用方)。

已测试平台

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

环境(可选)

N/A(仅单元测试)。

风险与范围

  • 主要风险或权衡:保留 maxSessionTurns 排除意味着设置了会话轮次上限的用户仍会看到 Goal 超出该上限运行。这是刻意的并已在现场注明:Goal 由自己的 token 预算约束,headless host 也采用同样的排除,否则就是两个互相矛盾的上限。如果项目日后希望 Goal 受会话上限约束,正确的形态是按消费窗口计数并在 resume 时重置,而不是撤掉这个排除。
  • 未验证/范围外:headless host 自己的 enforceSessionTurnLimit 排除(与保留的那处一致;未改动)。运行时轮次的 attribution snapshot 与自动记忆豁免(不是上限;上文已注明)。ACP 直接驱动 GeminiChat,从不经过这些检查。
  • 破坏性变更/迁移说明:无。

关联 Issue

qqqys and others added 3 commits August 26, 2026 17:34
… say why the other carve-outs stay

Once a Goal had its own autonomous spend bound (QwenLM#9891), the places
where client.ts carved runtime Goal turns out of the session ceilings
were due for a second look. Read one at a time they are not one thing.

Removed: a Goal-type send ignored the caller's `turns` budget and
always started from MAX_TURNS. Every runtime continuation is a fresh
top-level send that starts from MAX_TURNS on its own, and no production
caller passes `turns` for a Goal send, so the carve-out only ever
mattered for a caller handing over an exhausted budget -- and for that
caller a Goal should be refused like every other message type. It now
is, and the interrupted-exit path pauses the Goal instead of leaving it
running with a permit nobody will finish.

Retained, with the reason written at the site:
- sessionTurnCount / maxSessionTurns. The counter increments on every
  model call the user's prompts drive, tool continuations included, so
  counting Goal turns would let a user cap of N kill a healthy Goal
  after ~N/4 continuations with no resume path in headless. The Goal
  is bounded by its own token budget, and the headless host excludes
  runtime Goal turns from the same cap, so counting here would split
  the two ceilings.
- The steer-input check on the same count, for the same reason.
- The legacy stop-hook Goal chain's undecremented recursion budget.
  That chain recurses inside one sendMessageStream call, so the
  runtime's token budget never meters it; it is bounded by
  MAX_GOAL_ITERATIONS and stopHookBlockingCap, and a 50-iteration chain
  with steer and next-speaker continues would exhaust MAX_TURNS before
  its own cap.

The 75-turn runtime test no longer passes 0 as the budget; it was only
passing because of the removed carve-out. Mutation probe: re-adding the
carve-out fails exactly the new test (22 others green).
@qqqys

qqqys commented Aug 27, 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 commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

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

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR — a tidy follow-up to the Goal token-budget work in PR 9891.

Template looks good ✓

Problem: Not a user-facing bug — this is recorded-debt cleanup. PR 7895 carved runtime Goal turns out of several session ceilings, and the series plan said those exemptions would be re-audited once a Goal had its own spend bound (PR 9891, merged 2026-08-26). The exemptions are real: I verified all four sites exist in client.ts exactly as the PR table describes, so there is something concrete to act on. No before/after reproduction is expected for a refactor of this shape; the observable is in the code.

Direction: Aligned. The audit keeps what still earns its place and removes only the recursion-budget exemption. The "no behavioral change for production callers" claim checks out: the TUI (useGeminiStream.ts) and headless (nonInteractiveCli.ts) both invoke sendMessageStream without an explicit turns argument on Goal sends, and ACP drives GeminiChat directly and never passes through these checks.

Size: 31 production lines (client.ts, +25/−6) and 122 test lines (client-goal.test.ts, +120/−2). No generated/schema files. Far below any escalation threshold.

Approach: Scope feels exactly right — one behavioral line, three rewritten comments stating why the other carve-outs stay, and a pinning test per site. Nothing I would cut.

Risk: No elevated risk signals — no high-risk paths matched the revert-history patterns. One process note up front: this is a fork refactor, so the approval guardrail applies — the review will run fully, but the final decision defers to a maintainer instead of auto-approving.

Moving on to code review. 🔍

中文说明

感谢贡献——这是继 PR 9891(Goal token 预算)之后的干净续作。

模板完整 ✓

问题:不是用户可见的 bug,而是记录在案的债务清理。PR 7895 把运行时 Goal 轮次从多个会话上限中豁免出来,系列计划说好等 Goal 有了自己的消费边界(PR 9891,已于 2026-08-26 合并)后重新审计这些豁免。豁免确实存在:client.ts 中四处现场均已核实,与 PR 表格描述一致,所以有具体的东西可改。这类重构不要求 before/after 复现;问题本身就在代码里。

方向:对齐。审计保留了仍有存在理由的豁免,只撤掉递归预算这一处。「对生产调用方无行为变化」的说法成立:TUI(useGeminiStream.ts)和 headless(nonInteractiveCli.ts)给 Goal 发送调用 sendMessageStream 时都不传显式 turns 参数,ACP 直接驱动 GeminiChat,根本不经过这些检查。

规模:31 行生产代码(client.ts,+25/−6)加 122 行测试(client-goal.test.ts,+120/−2)。无生成/schema 文件。远低于任何升级阈值。

方案:范围恰到好处——一行行为改动、三处重写注释(说明其他豁免为何保留)、每个位置一个固定测试。没有可砍的。

风险:无升级风险信号——未命中 revert 历史高风险路径。一点流程预告:这是 fork 的 refactor,适用审批护栏——审查会完整进行,但最终决定交给 maintainer,而不是自动批准。

进入代码审查 🔍

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Code review

Before reading the diff, my independent take on "audit the Goal ceiling exemptions now that a budget exists" was: remove only the recursion-budget exemption (each runtime continuation is a fresh top-level send, so a Goal never needs to outlive one turn's allowance), keep the session-cap exclusion (counting Goal turns would let a user-set cap kill a healthy Goal mid-run), keep steer admission consistent with it, and keep the legacy hook chain's undecremented budget because the token budget cannot see recursion inside a single send. The PR lands exactly there — and goes one step further by pinning each kept carve-out with a test.

Claims I verified against the base tree at the reviewed commit:

  • The removal is safe for every production caller. The TUI (useGeminiStream.ts) and headless (nonInteractiveCli.ts) both call sendMessageStream without a turns argument on Goal sends, so they get the default budget of 100 either way. ACP calls GeminiChat.sendMessageStream directly and never passes through GeminiClient's ceilings. The only senders of the Goal type are the runtime goal queues in those two hosts.
  • The newly reachable refusal path does not leak the permit. A Goal send handed an exhausted budget now hits the !boundedTurns early return, which sits inside the main try — the finally calls releaseGoalPermitOnInterruptedExit, which pauses the active Goal and finishes the permit. No new code was needed for that, and the new test asserts exactly this outcome (no model call, pause dispatched, finishTurn called, no MaxSessionTurns event).
  • The three retained carve-outs each check out. sessionTurnCount counts every model call the user's prompts drive, tool continuations included, so counting Goal turns would burn a user cap of N in roughly N/4 continuations with no resume path in headless; the headless host already excludes runtime Goal turns from the same cap (enforceSessionTurnLimit(goalTurn?.origin === 'runtime')), so counting them in client.ts would split the two ceilings. The steer-admission exclusion is the same ceiling's other half. The legacy hook chain recurses inside one sendMessageStream call — the runtime's token budget meters only continuations the Goal runtime schedules, so it never sees that chain, and decrementing the budget there would let a long chain with steer/next-speaker continues starve before its own MAX_GOAL_ITERATIONS / stopHookBlockingCap bounds.

Tests: the three new cases pin the removed exemption, steer admission at a hit cap (mock cap is 1, count is forced to 1, and getSteerInput must still be called), and the legacy chain's undecremented budget (budget 2, two blocked stops, three model turns — a decremented budget would refuse the third). The existing 75-turn runtime test drops its turns = 0 trick — which only ever passed because of the removed carve-out — and still pins sessionTurnCount === 0. Imports (setActiveGoal, __resetActiveGoalStoreForTests) resolve against the merged goal-runtime code.

No critical issues, no blockers.

Testing

Evidence in this section is the PR's own CI, read through the GitHub API — this run does not execute PR code.

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

The unit suite was still running at the time of this pass; the table updates in place once CI settles. Real-scenario (tmux) coverage: N/A — the changed branch is unreachable for every production caller (none passes an explicit budget on a Goal send), so there is no user-visible surface to drive.

中文说明

代码审查

读 diff 之前,我对「预算落地后审计 Goal 上限豁免」的独立方案是:只撤递归预算豁免(每次运行时续跑都是全新的顶层发送,Goal 没有理由超越单轮的递归额度)、保留会话上限排除(计入 Goal 轮次会让用户设定的上限在约 N/4 次续跑后杀死健康 Goal)、steer 准入与之保持一致、旧版 hook 链不递减预算(token 预算看不到单次发送内部的递归)。PR 的落点与此完全一致——并更进一步,为每处保留的豁免补了固定测试。

基于评审提交点的基线代码核实过的说法:

  • **撤掉豁免对所有生产调用方都安全。**TUI(useGeminiStream.ts)与 headless(nonInteractiveCli.ts)给 Goal 发送调用 sendMessageStream 时都不传 turns 参数,两种情况下拿到的都是默认预算 100。ACP 直接调用 GeminiChat.sendMessageStream,根本不经过 GeminiClient 的这些上限。Goal 类型的发送方只有这两个宿主的运行时 goal 队列。
  • **新近可达的拒绝路径不会泄漏 permit。**预算耗尽的 Goal 发送现在会命中 !boundedTurns 提前返回,该路径位于主 try 之内——finally 会执行 releaseGoalPermitOnInterruptedExit,暂停活跃 Goal 并完成 permit。这部分不需要新代码,新测试恰好断言了这个结果(模型未被调用、派发 pause、调用 finishTurn、不产生 MaxSessionTurns 事件)。
  • 三处保留的豁免逐一成立。sessionTurnCount 对用户提示驱动的每次模型调用(含工具续跑)计数,计入 Goal 轮次会让用户上限 N 在约 N/4 次续跑内耗尽,且 headless 中没有恢复路径;headless host 已把运行时 Goal 轮次排除在同一上限之外(enforceSessionTurnLimit(goalTurn?.origin === 'runtime')),在 client.ts 里计入会让两处上限分裂。steer 准入豁免是同一上限的另一半。旧版 hook 链在单次 sendMessageStream 调用内递归——运行时 token 预算只计量 Goal 运行时调度的续跑,看不到这条链;在这里递减预算会让一条带 steer/next-speaker 续跑的长链在自己的 MAX_GOAL_ITERATIONS / stopHookBlockingCap 边界之前先耗尽。

测试:三个新用例分别固定了被撤的豁免、上限触顶时的 steer 准入(mock 上限为 1、计数强制为 1,getSteerInput 仍须被调用)、旧版链的不递减预算(预算 2、两次阻断停止、三次模型调用——递减预算会拒绝第三次)。原有 75 轮运行时测试去掉了 turns = 0 技巧——它此前能过只是靠被撤的豁免——仍固定 sessionTurnCount === 0。导入(setActiveGoal__resetActiveGoalStoreForTests)都能对上已合并的 goal-runtime 代码。

无严重问题,无阻塞项。

测试

本节的证据是 PR 自己的 CI(通过 GitHub API 读取)——本次运行不执行 PR 代码。评审时单元套件仍在运行,CI 落定后表格会就地更新。真实场景(tmux)覆盖:N/A——被改动的分支对所有生产调用方都不可达(没有任何调用方给 Goal 发送传显式预算),不存在可驱动的用户可见面。

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 3/5 — clean review across every stage, but the fork-refactor guardrail needs a maintainer's sign-off; the score reflects the policy cap, not doubt about the change.

Stepping back: this is the kind of refactor where the write-up is most of the value, and the write-up survives contact with the code. Every load-bearing claim checked out against the base tree — the four carve-out sites exist as described, no production caller passes an explicit budget on a Goal send, the permit-release in the finally makes the newly reachable refusal path safe without new code, and the headless host's mirrored session-cap exclusion means the retained exclusion keeps two ceilings from disagreeing. My independent proposal before reading the diff matched the PR's verdict site-for-site; the PR goes beyond it by adding a pinning test for each retained carve-out, which is strictly an improvement. The diff is minimal — one behavioral line, comments that now state the real reasons instead of stale ones, and tests. Six months from now, the next reader of client.ts will know exactly why each exemption stays. I'd thank the author for that.

Why this run does not approve: the PR is cross-repository with a refactor title, which the gate never auto-approves regardless of review outcome — fork refactors always get a human maintainer's eye. Context for the maintainer: the author is a code owner for packages/core and wrote the budget work (PR 9891) that this PR completes, so this may deserve to be treated as effectively maintainer-driven — but that is a human call, not one this gate makes. CI was also still settling at review time (one unit-suite run in flight); the Stage 2 table updates in place when it lands.

⏸️ Deferring to @wenshao — review found no blockers; the fork-refactor approval guardrail is the only thing standing between this PR and a merge. Needs a maintainer's sign-off (or another core maintainer's) on b0a3020b428ed15747e880ff443fa55d3370a547 once CI lands green.

中文说明

置信度:3/5——各阶段审查都很干净,但 fork 重构护栏需要 maintainer 签核;这个分数反映的是政策上限,而不是对改动本身的疑虑。

退后一步看:这类重构的价值大半在说明文字里,而这份说明文字经得起与代码对照。所有承重说法都在基线代码上核实过——四处豁免现场与描述一致、没有任何生产调用方给 Goal 发送传显式预算、finally 中的 permit 释放让新近可达的拒绝路径无需新代码即安全、headless host 对同一上限的镜像排除意味着保留该排除可避免两处上限互相矛盾。读 diff 之前我的独立方案与 PR 的逐点结论一致;PR 还更进一步,为每处保留的豁免补了固定测试,这是严格的加分项。diff 极小——一行行为改动、把陈旧理由换成真实理由的注释、以及测试。半年后下一位读 client.ts 的人会清楚每处豁免为何保留。这样的改动值得感谢作者。

本次不批准的原因:该 PR 来自 fork 且标题为 refactor,门禁对此类 PR 无论审查结果如何都不自动批准——fork 重构必须有人类 maintainer 过目。给 maintainer 的背景:作者是 packages/core 的 code owner,也是本 PR 收尾的预算工作(PR 9891)的作者,因此可以考虑按「实质上由维护者驱动」对待——但这是人类的判断,不是本门禁能做的。评审时 CI 也尚未落定(单元套件有一个 run 在跑);CI 落定后 Stage 2 的表格会就地更新。

⏸️ 转交 @wenshao——审查未发现阻塞项;fork 重构审批护栏是这个 PR 与合并之间唯一的障碍。待 CI 变绿后,需要 maintainer(或其他核心维护者)在 b0a3020b428ed15747e880ff443fa55d3370a547 上签核。

Qwen Code · qwen3.8-max

Reviewed at b0a3020b428ed15747e880ff443fa55d3370a547 · 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.

Reviewed — no blockers. Suggestions are inline.

中文说明

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

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

Comment thread packages/core/src/core/client.ts Outdated

@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 still running. Partially reviewed — gaps disclosed.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

中文说明

⚠️ 已从批准降级为评论:CI still running。 仅完成部分审查,审查缺口已披露。

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

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

…oal chain

The comment said stopHookBlockingCap and MAX_GOAL_ITERATIONS were the
only bounds on the legacy stop-hook chain. A user-set maxSessionTurns is
a third: each hook hop is a plain send with no Goal permit, so it counts
as a session turn, and the cap truncates the chain before either named
bound can fire. Measured by the reviewer on this PR's own fixture: with
maxSessionTurns=2 the chain dies at hop 2; with the cap off it runs all 7.
@qqqys

qqqys commented Aug 27, 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 27, 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 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.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

中文说明

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

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

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

@qwen-code-dev-bot

qwen-code-dev-bot commented Aug 27, 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 review round — no action needed

No code changes were made this round: the only actionable finding was already fixed at the current HEAD, and every other feedback item is informational.

Feedback triage

Item Source Disposition
rc:3870137543[Suggestion] R1-1: comment overclaimed that stopHookBlockingCap / MAX_GOAL_ITERATIONS "are the only bounds on this path", omitting that a user-set maxSessionTurns also cuts the legacy hook Goal chain automated reviewer Already resolved in code — fixed by maintainer commit cbb04770a4 (current HEAD); re-verified this round, see below
rc:3870252882 — "Fixed in cbb04770a4 — took your wording…" @qqqys (maintainer reply in the R1-1 thread) Not a finding — confirmation of the fix; no response needed
rv:5038882639 — round-1 review, "no blockers, suggestions inline" automated reviewer Informational; its single suggestion is rc:3870137543
rv:5038898247 — approval downgraded to comment because CI was still running automated reviewer Informational reviewer-coverage disclosure; nothing to fix
rv:5039013824 — empty review body @qqqys Review wrapper for the inline reply above; nothing to fix
rv:5039528733 — round-2 review at the fixed HEAD, zero findings automated reviewer Informational; confirms the fixed state re-reviewed cleanly

No failed checks, no still-red checks, no base conflicts (--conflict false).

Re-verification of R1-1 at HEAD (cbb04770a4)

  1. Comment wording matches the accepted suggestion. packages/core/src/core/client.ts (around line 4171) now reads: "…the chain itself is bounded by stopHookBlockingCap / MAX_GOAL_ITERATIONS. Those are the only Goal-specific bounds on this path (a user-set maxSessionTurns still cuts the chain: each hook hop is a plain send with no Goal permit, so it counts as a session turn): the legacy hook Goal recurses inside one sendMessageStream call…". The inaccurate absolute claim is gone, and the third bound plus its reason are named, as the suggestion asked.
  2. The comment's factual claim holds in code. Each legacy-chain continuation is a recursive sendMessageStream send with type: SendMessageType.Hook and no goalPermit/goalOrigin (client.ts ≈ 4229–4251), so isGoalRuntimeTurn is false (≈ 3109), every hop increments sessionTurnCount (≈ 3269/3279), and the maxSessionTurns refusal (≈ 3301–3315) can truncate the chain — consistent with the reviewer's probe numbers (maxSessionTurns=2 → chain dies at hop 2 with a MaxSessionTurns event; cap off → all 7 hops run) run against this PR's own legacy-chain fixture in client-goal.test.ts.

Verification

No source changes were made this round, so no build/typecheck/lint/test commands were run — there is no new diff to verify. The current HEAD cbb04770a4 is already covered by the round-2 automated review and CI; the working tree is clean.

resolved-comments.txt lists rc:3870137543 so the fixed thread can be resolved; no inline finding was declined, deferred, or escalated, so no comment-replies.json is written.

中文说明

Autofix 审查轮次 — 无需处理

本轮未做任何代码改动:唯一可执行的发现已在当前 HEAD 上修复,其余反馈条目均为信息性内容。

反馈分类

条目 来源 处置
rc:3870137543[Suggestion] R1-1:注释过度声称 stopHookBlockingCap / MAX_GOAL_ITERATIONS「是这条路径上唯一的边界」,遗漏了用户设置的 maxSessionTurns 同样会截断旧版 hook Goal 链 自动审查器 已在代码中解决 — 由维护者在提交 cbb04770a4(当前 HEAD)中修复;本轮已重新验证,见下文
rc:3870252882 — 「已在 cbb04770a4 中修复 — 采用了你的措辞……」 @qqqys(维护者在 R1-1 线程中的回复) 不是发现 — 是对修复的确认;无需回应
rv:5038882639 — 第一轮审查,「无阻断问题,建议见行内」 自动审查器 信息性内容;其唯一建议即 rc:3870137543
rv:5038898247 — 因 CI 仍在运行而从批准降级为评论 自动审查器 审查器自身覆盖缺口的披露;无可修复项
rv:5039013824 — 空审查正文 @qqqys 上述行内回复的审查外壳;无可处理项
rv:5039528733 — 在修复后的 HEAD 上进行第二轮审查,零发现 自动审查器 信息性内容;确认修复后的状态重新审查通过

无失败检查,无持续变红的检查,无与基础分支的冲突(--conflict false)。

在 HEAD(cbb04770a4)上对 R1-1 的重新验证

  1. 注释措辞与被接受的建议一致。 packages/core/src/core/client.ts(约第 4171 行)现在写道:「……链本身由 stopHookBlockingCap / MAX_GOAL_ITERATIONS 约束。这些是这条路径上唯一的 Goal 专属边界(用户设置的 maxSessionTurns 仍会截断这条链:每一跳 hook 都是不带 Goal 许可的普通发送,因此会计入一次会话轮次):旧版 hook Goal 在一次 sendMessageStream 调用内递归……」。不准确的绝对化表述已被移除,并按建议要求点名了第三个边界及其原因。
  2. 注释的事实性陈述在代码中成立。 旧版链上的每次续发都是一次 type: SendMessageType.Hook、不带 goalPermit/goalOrigin 的递归 sendMessageStream 发送(client.ts 约 4229–4251 行),因此 isGoalRuntimeTurn 为 false(约 3109 行),每一跳都会递增 sessionTurnCount(约 3269/3279 行),maxSessionTurns 拒绝逻辑(约 3301–3315 行)可以截断这条链 —— 与审查器在本 PR 自带的 client-goal.test.ts 旧版链测试夹具上运行探针得到的数据一致(maxSessionTurns=2 时链在第 2 跳带 MaxSessionTurns 事件终止;关闭上限时跑满全部 7 跳)。

验证

本轮未改动任何源码,因此未运行 build / typecheck / lint / 测试命令 —— 没有新的差异需要验证。当前 HEAD cbb04770a4 已被第二轮自动审查和 CI 覆盖;工作树干净。

resolved-comments.txt 中列出了 rc:3870137543,以便解析该已修复的线程;没有任何行内发现被拒绝、推迟或升级,因此不写 comment-replies.json

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


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

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.

3 participants