Skip to content

feat(cli): adopt Goal v3 in interactive TUI - #8005

Merged
wenshao merged 24 commits into
QwenLM:mainfrom
qqqys:agent/goal-v3-tui
Aug 1, 2026
Merged

feat(cli): adopt Goal v3 in interactive TUI#8005
wenshao merged 24 commits into
QwenLM:mainfrom
qqqys:agent/goal-v3-tui

Conversation

@qqqys

@qqqys qqqys commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

This PR connects the interactive TUI to the Goal v3 runtime. It adds the canonical /goal lifecycle commands, persistent lifecycle cards and footer status, Goal-aware resume and branch recovery, and a two-lane input queue that keeps ordinary messages queued while Goal continuations are running. Explicit Goal control commands can still preempt the loop, and ordinary queued messages are released only after the Goal reaches a terminal state or is cleared.

It also closes two correctness gaps found during real terminal testing: synthetic continuation prompts can no longer count objective text as user evidence, and completion claims that depend on user input must cite real user_input evidence.

Why it's needed

The Goal v3 runtime was available below the UI, but the interactive TUI still used the legacy command and queue behavior. That mismatch caused premature completion, ordinary queued messages leaking into an active Goal, inconsistent pause/resume behavior, and incomplete recovery after restart or branching. This PR makes the TUI a faithful host for the existing runtime without pulling the non-interactive, ACP, SDK, Web, WebShell, or Desktop surfaces into the same review.

Reviewer Test Plan

How to verify

  1. Start a fresh interactive session and set a Goal as the first interaction. Confirm the active lifecycle card and footer appear without requiring a prior chat message.
  2. Send ordinary input while the Goal is running with queue mode enabled. Confirm it remains visibly queued across Goal turns and is not delivered to the model until the Goal completes or is cleared.
  3. Use the insert action on a stop-condition message. Confirm the real user input reaches the active turn and the Goal completes only after independent verification.
  4. Pause, edit, and resume the Goal. Confirm the updated objective and revision are retained and the loop resumes.
  5. Cancel an active turn with Escape. Confirm the Goal becomes paused rather than being silently cleared.
  6. Restart with continue mode and create a branch from a paused Goal session. Confirm the objective, lifecycle state, history, and footer recover in both flows.
  7. Clear the Goal. Confirm no confirmation dialog is shown and any queued ordinary message is released.

Evidence (Before & After)

Before: the interactive TUI used legacy Goal commands and a single effective delivery lane, allowing unsupported completion and ordinary queued input to enter the active Goal loop.

After: the tmux E2E report posted as a separate PR comment covers first-message creation, normal continuation, real stop-input completion, unsupported-completion rejection, queued-message hold and release, pause/edit/resume, Escape cancellation, process recovery, branch recovery, and clear-without-confirmation. Screenshots will be attached to that report.

Tested on

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

Environment (optional)

macOS arm64, Node.js 24, no sandbox, interactive tmux sessions against the local development bundle.

Risk & Scope

  • Main risk or tradeoff: Goal admission and message delivery now share a stricter lifecycle boundary, so regressions would primarily appear as a stuck continuation or a message released at the wrong time. Focused queue, stream, host, recovery, and runtime tests cover those boundaries.
  • Not validated / out of scope: Windows and Linux terminal rendering; non-interactive CLI, ACP, SDK, Web UI, WebShell, and Desktop adoption remain separate follow-up PRs.
  • Breaking changes / migration notes: None. Legacy behavior remains temporarily available to non-interactive and ACP surfaces until their dedicated follow-up PRs.

Linked Issues

Follow-up to #7895.

中文说明

本 PR 做了什么

本 PR 将交互式 TUI 接入 Goal v3 运行时,补齐规范化的 /goal 生命周期命令、持久化生命周期卡片与底部状态、Goal 感知的恢复和分支继承,以及双通道输入队列。Goal 自动续轮期间,普通消息会持续排队;显式 Goal 控制命令仍可抢占循环;普通排队消息只会在 Goal 进入终态或被清除后释放。

真实终端测试还发现并修复了两个正确性问题:合成续轮提示不再把目标文本误当作用户证据;依赖用户输入的完成声明必须引用真实的 user_input 证据。

为什么需要

Goal v3 运行时已经存在于底层,但交互式 TUI 仍使用旧命令与旧队列行为。这个错位会导致目标过早完成、普通排队消息泄漏进活动 Goal、暂停/恢复行为不一致,以及重启或分支后的恢复不完整。本 PR 让 TUI 成为现有运行时的可靠宿主,同时不把非交互 CLI、ACP、SDK、Web、WebShell 或 Desktop 一并塞进同一次评审。

Reviewer Test Plan

如何验证

  1. 新建交互会话,并把设置 Goal 作为第一次交互。确认无需先聊天即可出现活动生命周期卡片和底部状态。
  2. Goal 运行期间开启排队模式并发送普通消息。确认消息跨多个 Goal 轮次持续可见地排队,在 Goal 完成或被清除前不会交付给模型。
  3. 对停止条件消息使用插入操作。确认真实用户输入进入活动轮次,并且 Goal 只在独立验证后完成。
  4. 暂停、编辑并恢复 Goal。确认更新后的目标和修订号被保留,循环能够继续。
  5. 使用 Escape 取消活动轮次。确认 Goal 变为已暂停,而不是被静默清除。
  6. 使用 continue 模式重启,并从暂停的 Goal 会话创建分支。确认两种流程都能恢复目标、生命周期状态、历史记录和底部状态。
  7. 清除 Goal。确认不弹确认框,并释放普通排队消息。

证据(Before & After)

Before:交互式 TUI 使用旧 Goal 命令和单一的有效交付通道,可能接受缺少依据的完成提案,也可能让普通排队输入进入活动 Goal 循环。

After:单独发布的 tmux E2E 报告覆盖首次消息创建、正常续轮、真实停止输入完成、无依据完成拒绝、排队消息保持与释放、暂停/编辑/恢复、Escape 取消、进程恢复、分支恢复和无确认清除。截图会附在该报告中。

测试平台

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

环境(可选)

macOS arm64、Node.js 24、无沙箱,使用本地开发 bundle 在 tmux 交互会话中测试。

风险与范围

  • 主要风险或权衡:Goal 准入和消息交付现在共享更严格的生命周期边界,因此回归主要会表现为续轮卡住或消息在错误时机释放。聚焦的队列、流、宿主、恢复与运行时测试覆盖了这些边界。
  • 未验证 / 范围外:Windows 和 Linux 终端渲染;非交互 CLI、ACP、SDK、Web UI、WebShell 和 Desktop 的接入留给后续独立 PR。
  • 破坏性变更 / 迁移说明:无。在对应的独立后续 PR 完成前,非交互和 ACP 表面暂时保留旧行为。

关联

承接 #7895

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

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

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

@qqqys

qqqys commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator Author

Goal v3 TUI PR7 E2E

Date: 2026-07-29

Binary: locally built node dist/cli.js

Runtime isolation: timestamped QWEN_RUNTIME_DIR under /tmp; existing provider
configuration is read from the user's Qwen configuration.

Scenarios

Scenario Result Evidence
Set /goal as the first interaction in a fresh session Pass Goal card and footer appeared immediately; no ordinary chat turn was required.
Automatic continuation and unsupported completion proposal Pass after fix The model proposed completion without a real stop input. The verifier rejected because every cited record was delivered_output; the Goal continued.
Real stop input and verified completion Pass GOAL_STOP was recorded as real_user/user_input; only then did the terminal Goal complete card appear.
/goal terminal status query Pass The completed objective, turn count, duration, and verifier reason were rendered again without a model turn.
/goal pause, /goal edit, /goal resume Pass Pause preempted the active turn, edit advanced the revision while preserving pause, and resume restarted canonical Goal work.
Ordinary Ctrl+Q queue during active Goal Pass after fix HELD_UNTIL_GOAL_END stayed visible in the queue across four Goal turns and was absent from the transcript.
Queue release after /goal clear Pass The held message was recorded as real_user only after the Goal was cleared.
Escape cancellation Pass Escape produced Request cancelled and atomically moved the Goal to paused.
Process restart with --continue Pass The paused objective, duration, history card, and footer state recovered in a new process.
/branch from a paused Goal Pass The branch loaded with the same objective and paused footer state.
Clear without confirmation Pass /goal clear rendered Goal cleared immediately with no dialog.

Defects found and repaired

Unsupported stop-token completion

The synthetic continuation prompt copied the objective verbatim. A model
eventually treated a stop token mentioned in that objective as new user input,
then proposed completion using only assistant-output evidence. The independent
verifier accepted the unsupported claim.

The synthetic turn now states explicitly that it contains no new user input and
does not repeat the objective. The verifier now requires cited user_input
evidence for every claim that a user sent, typed, confirmed, chose, approved, or
provided something.

Queued ordinary input entered the Goal

At an idle boundary between autonomous Goal turns, active-mode dequeue selected
ordinary queued input before the hidden Goal continuation. Ctrl+Q therefore
became real_user input on the next Goal turn.

Active-mode dequeue now allows Goal controls first, then a hidden Goal
continuation, and otherwise returns no submission. Ordinary queued input is
released only after the Goal reaches a terminal or cleared state.

Evidence

Each meaningful transition was captured with tmux capture-pane into these
timestamped report directories:

  • tmp/goal-v3-tui-tmux-20260729-125608: original false-positive completion
  • tmp/goal-v3-pr7-fixed2-tmux-20260729-130935: verifier rejection, real-user completion, pause/edit/resume
  • tmp/goal-v3-pr7-queue-fixed-tmux-20260729-131932: queued message held and released after clear
  • tmp/goal-v3-pr7-recovery-a-tmux-20260729-132219: paused state before restart
  • tmp/goal-v3-pr7-recovery-b-tmux-20260729-132450: paused state after --continue
  • tmp/goal-v3-pr7-esc-branch-tmux-20260729-132742: Escape cancellation and branch recovery

PNG screenshots will be captured from an attached terminal for the queue-held,
completion, pause/edit/resume, restart recovery, and branch states once the Mac
desktop is unlocked.

Automated verification

  • CLI focused suite: 13 files, 628 tests passed
  • Core focused suite: 5 files, 446 tests passed
  • Workspace typecheck: passed
  • Workspace lint: passed
  • Workspace build and bundle: passed

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Re-triage at the current head (b292a2af), re-triggered by @wenshao. The head is unchanged since the last pass and I re-verified every load-bearing fact below rather than carrying the prior conclusions forward.

Template looks good ✓

Problem: still valid and unchanged — this is a feature adoption, not a bug fix. The Goal v3 runtime landed in core via #7895, but the interactive TUI still ran the legacy commands and a single delivery lane. The PR describes concrete mismatches observed during real terminal testing: premature completion, ordinary queued messages leaking into an active Goal, inconsistent pause/resume, and incomplete recovery after restart or branching. Observed gaps with a clear before/after, not theoretical hardening.

Direction: aligned. The runtime is already in main; the TUI has to adopt it to avoid the exact mismatch described. /goal is also a shipped competitive feature — Claude Code's CHANGELOG lists "Added /goal command: set a completion condition and Claude keeps working across turns until it's met," with several follow-up fixes since — so this is squarely within qwen-code's core mission.

Size: 2702 production lines (additions + deletions, excluding tests) across 28 production files, plus 3883 test lines across 19 test files (47 files total). Core paths (packages/core/src/**) carry only 50 production linesterminateTurn promoted to the base ToolResult, a 50-turn continuation budget, a resume turnCount reset, and the verifier-prompt hardening. The bulk is in packages/cli/src/ui/**. Because production lines exceed 500 this is flagged for maintainer awareness, and because they exceed 1000 it's a large PR — worth a human confirming the scope feels right for one merge. The queue/stream/command/UI changes are interdependent, so I don't see a clean split.

Approach: the scope feels right for the stated goal — lifecycle commands, persistent cards/footer, a two-lane input queue, and Goal-aware resume/branch recovery are all needed to make the TUI a faithful host, and scoping out non-interactive/ACP/SDK/Web/WebShell/Desktop is the correct call. The two correctness fixes (synthetic continuation prompts no longer counting objective text as evidence, and completion claims requiring real user_input evidence) are tightly coupled to the adoption and belong here.

Risk: no elevated risk signals — no high-risk revert-prone paths matched.

Moving on to code review. 🔍

中文说明

在当前 head(b292a2af)重新 triage,由 @wenshao 再次触发。head 自上次审查以来未变,我重新验证了下述每一个关键事实,而非沿用先前结论。

模板完整 ✓

问题:仍然成立且不变——这是功能接入,不是 bug 修复。Goal v3 运行时已通过 #7895 合入 core,但交互式 TUI 仍使用旧命令和单一交付通道。PR 描述了真实终端测试中观察到的具体错位:过早完成、普通排队消息泄漏进活动 Goal、暂停/恢复不一致、重启或分支后恢复不完整。是有明确 before/after 的已观测差距,不是理论性加固。

方向:对齐。运行时已在 main 中,TUI 必须接入以避免所描述的错位。/goal 也是已发布的竞争功能——Claude Code 的 CHANGELOG 列有"Added /goal command: set a completion condition and Claude keeps working across turns until it's met",此后还有若干修复——属于 qwen-code 核心使命。

规模:2702 行生产代码(加减,不含测试),分布在 28 个生产文件,另有 3883 行测试代码,19 个测试文件(共 47 个文件)。核心路径(packages/core/src/**)仅 50 行生产代码——terminateTurn 提升到基础 ToolResult、50 轮续轮预算、resume 的 turnCount 重置、验证器提示加固。主体在 packages/cli/src/ui/**。生产行数超过 500,提请维护者关注;超过 1000,属于大 PR——值得人工确认范围对单次合并是否合适。队列/流/命令/UI 改动相互依赖,看不到清晰的拆分方式。

方案:范围与目标匹配——生命周期命令、持久化卡片/底部状态、双通道输入队列、Goal 感知的恢复/分支继承都是让 TUI 成为可靠宿主所必需的,排除非交互/ACP/SDK/Web/WebShell/Desktop 是正确的。两个正确性修复与接入紧密耦合,应包含在此处。

风险:无升级风险信号——未匹配高风险易回滚路径。

进入代码审查 🔍

Qwen Code · qwen3.8-max-preview

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

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal vs. actual approach: I would have structured this the same way — a separate Goal-turn queue in useMessageQueue, Goal-turn bindings in useGeminiStream, runtime-driven lifecycle cards, and waitForGoalRuntime replacing restoreGoalFromHistory. The PR matches and exceeds that: the GoalTurnBinding abstraction (permit + turnKey + controller + origin) with a bind/release/fail-closed lifecycle is well designed, and the sharedGoalPermit check that rejects mixed-Goal tool batches is a correctness safeguard I wouldn't have added upfront.

I re-read the core touches in detail at this head — no critical blockers. They are minimal (50 production lines) and sound:

  • terminateTurn is promoted from GoalToolResult to the base ToolResult (tools.ts) with a doc comment noting it's only honored when the batch carries a Goal context, propagated generically by coreToolScheduler.ts, and typed on ToolCallResponseInfo (turn.ts). goal-tools.ts collapses to a plain ToolResult alias. Right architectural call — no Goal-specific interface leaking into the scheduler.
  • goal-runtime.ts adds a MAX_GOAL_CONTINUATION_TURNS = 50 budget: on overflow it transitions to usage_limited, records the journal state, and broadcasts — re-checking status === 'active' inside the enqueued closure to avoid a race. A sensible bound on autonomous continuation.
  • goal-reducer.ts resets turnCount: 0 on an explicit resume (with a comment explaining that keeping the exhausted count would report active and immediately re-transition to usage_limited), and clears lastReason on edit. Both are genuine correctness fixes.
  • goal-verifier.ts hardens the prompt: every objective condition and factual claim must be supported by cited evidence, user-action claims require user_input proofKind, and the objective/proposal reason are claims, not evidence. This closes the synthetic-continuation evidence gap described in the PR.

Observations worth noting (none blocking):

  • The goalQueueRef parameter on useGeminiStream carries a wide inline type. It follows the existing midturnDrainRef/midturnRestoreRef pattern, but if this surface grows further, extracting a named interface would help readability.
  • The change is concentrated in a few files (useGeminiStream.ts +964, goalCommand.ts +431, AppContainer.tsx +392, useMessageQueue.ts +325) and carries very heavy test coverage relative to production code (useGeminiStream.test.tsx +1085, goalCommand.test.ts +718, useMessageQueue.test.ts +672, AppContainer.test.tsx +495) — a healthy ratio for a lifecycle this intricate.
sequenceDiagram
    participant P1 as Goal Runtime
    participant P2 as Message Queue
    participant P3 as Drain Effect
    participant P4 as submitQuery
    participant P5 as Stream Handler
    participant P6 as Tool Scheduler
    P1->>P2: enqueueGoalTurn (permit, context)
    P3->>P2: popNextSubmission (priority)
    P2-->>P3: QueuedGoalTurn
    P3->>P4: submitQuery (Goal, metadata)
    P4->>P4: bindGoalTurn (permit, turnKey)
    P4->>P5: processGeminiStreamEvents
    P5->>P6: scheduleToolCalls (goalBinding)
    P6-->>P5: tool results + terminateTurn
    alt terminateTurn
        P5->>P1: finishTurn (permit)
        P1-->>P5: snapshot (complete or active)
    else error or cancel
        P5->>P1: failClosedGoalTurn (pause + release)
    end
Loading
Files changed (30 of 47 shown)
File What changed
packages/cli/src/ui/hooks/useGeminiStream.ts Core integration: Goal turn bindings, admission, fail-closed lifecycle, tool-result Goal context validation, notification deferral
packages/cli/src/ui/commands/goalCommand.ts Replaces legacy hook-based command with Goal v3 runtime dispatch; legacy path preserved for non-interactive
packages/cli/src/ui/AppContainer.tsx Wires goalQueueRef, extracts useQueuedSubmissionDrain, GoalTurnHost binding, releaseQueuedGoalReservations
packages/cli/src/ui/hooks/useMessageQueue.ts Two-lane queue: separate Goal turn queue with dedup, popNextSubmission with goalControlMode, Goal-aware drain
packages/cli/src/ui/components/GoalPill.tsx Subscribes to Goal runtime instead of polling legacy store; shows paused/blocked/usage-limited states
packages/cli/src/ui/components/messages/GoalStatusMessage.tsx GoalStateCard renders GoalSnapshotV2 lifecycle states; legacy card retained
packages/cli/src/ui/utils/goal-runtime.ts New: shouldDisplayGoalStateCause exhaustive filter, waitForGoalRuntime with graceful unavailable handling
packages/cli/src/ui/hooks/slashCommandProcessor.ts Handles goal_control action return: renders goal_state card or info message
packages/cli/src/ui/hooks/useBranchCommand.ts Replaces restoreGoalFromHistory with waitForGoalRuntime
packages/cli/src/ui/hooks/useResumeCommand.ts Replaces restoreGoalFromHistory with waitForGoalRuntime
packages/cli/src/ui/types.ts Adds HistoryItemGoalState, GOAL_STATE MessageType, paused GoalStatusKind
packages/cli/src/ui/commands/types.ts Adds GoalControlActionReturn, GoalCommandOperation types
packages/cli/src/ui/components/Footer.tsx Passes snapshot to GoalPill, uses isLiveGoalSnapshot
packages/cli/src/ui/components/HistoryItemDisplay.tsx Renders goal_state items via GoalStatusMessage
packages/cli/src/ui/utils/resumeHistoryUtils.ts Converts goal_state and goal_runtime records from JSONL
packages/cli/src/ui/utils/historyUtils.ts Marks goal_state as non-synthetic
packages/cli/src/nonInteractiveCliCommands.ts Returns unsupported for goal_control in non-interactive mode
packages/core/src/tools/tools.ts Adds terminateTurn to base ToolResult interface
packages/core/src/core/turn.ts Adds terminateTurn to ToolCallResponseInfo
packages/core/src/core/coreToolScheduler.ts Propagates terminateTurn from tool result
packages/core/src/goals/goal-runtime.ts Adds MAX_GOAL_CONTINUATION_TURNS budget and usage_limited transition
packages/core/src/goals/goal-reducer.ts Resets turnCount on resume, clears lastReason on edit
packages/core/src/goals/goal-tools.ts Simplifies GoalToolResult to plain ToolResult alias
packages/core/src/goals/goal-verifier.ts Verifier prompt: objective text is a claim not evidence; user_input proofKind required for user-action claims
packages/cli/src/ui/hooks/useGeminiStream.test.tsx Tests for Goal turn binding, admission, fail-closed, tool context validation
packages/cli/src/ui/hooks/useMessageQueue.test.ts Tests for two-lane queue, goalControlMode, Goal-aware drain
packages/cli/src/ui/AppContainer.test.tsx Tests for useQueuedSubmissionDrain, admission failure tracking
packages/cli/src/ui/commands/goalCommand.test.ts Tests for parseGoalCommand, legacy and v3 dispatch paths
packages/cli/src/ui/components/GoalPill.test.tsx Tests for runtime subscription, lifecycle states
packages/core/src/goals/goal-runtime.test.ts Tests for the continuation budget and usage_limited transition
…and 17 more files

CI Test Evidence

At the reviewed commit b292a2af the load-bearing unit suite is green: Test (ubuntu-latest, Node 22.x) passed. Precheck and PR classification passed. Windows/macOS unit tests and the CLI integration tests were skipped (fork PRs need maintainer authorization to run those lanes), and the web-shell E2E Smoke job was cancelled during setup — its log ends in orphan-process cleanup while running playwright install --with-deps chromium, i.e. a concurrency/setup cancellation, not a test failure caused by this PR. No PR-CI workflow runs are still pending on this head. review-pr (the bot review orchestration) is bot infrastructure, not part of the PR's own CI.

Check Conclusion
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ⚪ cancelled (setup)
Test (ubuntu-latest, Node 22.x) ✅ success
precheck-pr / precheck ✅ success
Classify PR ✅ success

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

Sandboxed verification would settle the behavioural claim: @qwen-code /tmux — the PR's central claim is that the interactive TUI correctly hosts the Goal v3 lifecycle (first-message creation, queued-message hold/release, pause/edit/resume, Escape cancellation, process and branch recovery). None of that is observable from the diff or the unit suite, and the author tested on macOS only. @wenshao has posted several local real-run E2E reports (most recent at the current head), which is strong signal — a /tmux run on the Linux CI environment would independently confirm the TUI surface where the merge target runs. (This is a fork PR, so /tmux needs a maintainer trigger; /verify is available as a sponsored run with a pre-execution risk screen and workspace wipe — read its report with the same skepticism as the fork's own CI logs.)

中文说明

代码审查

独立方案与实际方案对比: 我的独立方案与 PR 结构一致——在 useMessageQueue 中分离 Goal 轮次队列、在 useGeminiStream 中管理 Goal 轮次绑定、运行时驱动的生命周期卡片、用 waitForGoalRuntime 替代 restoreGoalFromHistory。PR 的方案更进一步:GoalTurnBinding 抽象(permit + turnKey + controller + origin)配合 bind/release/fail-closed 生命周期设计良好,sharedGoalPermit 拒绝混合 Goal 工具批次是我不会预先想到的正确性保障。

在当前 head 详细复查了 core 改动——无关键阻塞。 改动很小(50 行生产代码)且合理:

  • terminateTurnGoalToolResult 提升到基础 ToolResulttools.ts),带有文档注释说明仅在批次携带 Goal 上下文时生效,由 coreToolScheduler.ts 通用传播,并在 ToolCallResponseInfoturn.ts)中定型。goal-tools.ts 收敛为纯 ToolResult 别名。架构正确——没有 Goal 专属接口泄漏进调度器。
  • goal-runtime.ts 新增 MAX_GOAL_CONTINUATION_TURNS = 50 预算:超限时转入 usage_limited、记录 journal 状态并广播——在入队闭包内重新检查 status === 'active' 以避免竞态。是对自主续轮的合理约束。
  • goal-reducer.ts 在显式 resume 时重置 turnCount: 0(注释说明保留耗尽计数会立即重新转入 usage_limited),并在 edit 时清除 lastReason。都是真正的正确性修复。
  • goal-verifier.ts 加固提示:每个目标条件和事实声明必须有引用证据支持,用户操作声明需要 user_input proofKind,目标和提案理由是声明而非证据。关闭了 PR 所述的合成续轮证据缺口。

值得注意的观察(均非阻塞):goalQueueRef 内联类型较宽,若继续增长建议提取命名接口;改动集中在少数文件,且测试覆盖相对生产代码非常充分——对如此复杂的生命周期而言比例健康。

CI 测试证据

在审查提交 b292a2af,关键单元测试套件为绿色Test (ubuntu-latest, Node 22.x) 通过。预检和 PR 分类通过。Windows/macOS 单元测试和 CLI 集成测试被跳过(fork PR 需维护者授权),web-shell E2E Smokesetup 期间被取消——日志在执行 playwright install 时以孤儿进程清理结束,属于并发/setup 取消,并非本 PR 导致的测试失败。当前 head 上没有仍在进行的 PR CI 工作流运行。review-pr(机器人审查编排)是机器人基础设施,不属于 PR 自身 CI。

沙箱验证可确认行为声明:@qwen-code /tmux——PR 的核心声明是交互式 TUI 正确承载 Goal v3 生命周期,这些无法仅从 diff 或单元测试观察到,且作者仅在 macOS 测试。@wenshao 已发布多份本地真实运行 E2E 报告(最新在当前 head),是有力的信号——在 Linux CI 环境运行 /tmux 可独立确认合并目标平台上的 TUI 表面。(这是 fork PR,/tmux 需维护者触发;/verify 可作为带执行前风险筛查和工作区清理的赞助运行——请以与 fork 自身 CI 日志相同的审慎态度阅读其报告。)

Qwen Code · qwen3.8-max-preview

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

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Confidence: 3/5 — clean review with no blocking issues at the current head, but the 2702-production-line, cross-package scope triggers the Stage 0 maintainer-awareness escalation, which caps the score and keeps the bot from auto-approving.

This is a well-executed feature PR. The Goal v3 runtime is already in main (#7895), and this makes the interactive TUI a faithful host for it — the right next step, scoped correctly to one surface. I re-read the core touches in detail and they are sound: the terminateTurn promotion to the base ToolResult, the 50-turn continuation budget, the resume turnCount reset, and the verifier-prompt hardening are all minimal, well-commented where the why is non-obvious, and follow existing patterns. Test coverage is thorough (3883 test lines against 2702 production). The unit suite is green at this head and no PR-CI runs are pending. I found no correctness bugs, security issues, or AGENTS.md violations.

The 3/5 is pure policy, not doubt. Since the last pass, @wenshao has approved this exact head — that is the maintainer sign-off the scale escalation asks for. My own approval is a separate vote (main requires two), and the two-tier gate keeps the bot from auto-approving a cross-package PR of this production-line scale, so I am withholding it and leaving the merge decision to the maintainers. Two things a human should still weigh:

  1. Scale. 2702 production lines across 28 production files, touching both packages/cli and packages/core. The interactive-TUI-only scoping is the right call and I don't see a meaningful way to split it — the queue, stream, command, and UI changes are interdependent — but a maintainer should confirm the scope feels right for a single merge.
  2. Behavioural verification. The unit suite covers the boundaries well, but the TUI lifecycle (first-message creation, queued-message hold/release, pause/edit/resume, Escape cancellation, process and branch recovery) is only verifiable end-to-end. @wenshao has posted several local real-run E2E reports at the current head, which is strong signal; a @qwen-code /tmux run on Linux would independently confirm the surface where the merge target runs. Windows/macOS unit tests and CLI integration tests were skipped (fork authorization), so those lanes haven't exercised this code.

⏸️ Deferring to @wenshao (cc @yiliang114) — the review is clean and @wenshao has already approved, but the production-line scale and cross-package core touch keep this behind the two-tier gate's maintainer-awareness cap, so the bot is not adding its own approval. For the record: my standing changes-requested review on this PR reflects that policy cap, not any blocking code issue — I found none. The merge call now rests with the maintainers.

中文说明

置信度:3/5——在当前 head 审查干净、无阻塞问题,但 2702 行生产代码、跨包的范围触发了 Stage 0 维护者关注升级,限制了评分,并使机器人不自动批准。

这是一个执行良好的功能 PR。Goal v3 运行时已在 main 中(#7895),本 PR 让交互式 TUI 成为其可靠宿主——正确的下一步,范围正确地限定在一个表面。我详细复查了 core 改动,均合理:terminateTurn 提升到基础 ToolResult、50 轮续轮预算、resume 的 turnCount 重置、验证器提示加固都很精简,在 why 不显然处有良好注释,并遵循现有模式。测试覆盖充分(3883 行测试对 2702 行生产)。单元测试套件在当前 head 为绿色,且无仍在进行的 PR CI 运行。未发现正确性 bug、安全问题或 AGENTS.md 违规。

3/5 是纯策略性的,不是疑虑。自上次审查以来,@wenshao 已批准了这个完全相同的 head——这正是规模升级所要求的维护者签字。我自己的批准是独立的一票(main 需要两票),而两级门控使机器人不自动批准这种生产代码规模的跨包 PR,因此我保留不投,并把合并决定留给维护者。人工仍需权衡两点:

  1. 规模。 28 个生产文件 2702 行生产代码,同时触及 packages/clipackages/core。仅限交互式 TUI 的范围划分是正确的,看不到有意义的拆分方式——队列、流、命令和 UI 改动相互依赖——但需要维护者确认范围对单次合并是否合适。
  2. 行为验证。 单元测试覆盖了边界,但 TUI 生命周期(首次消息创建、排队消息保持/释放、暂停/编辑/恢复、Escape 取消、进程和分支恢复)只有端到端才能验证。@wenshao 已在当前 head 发布多份本地真实运行 E2E 报告,是有力的信号;在 Linux 上运行 @qwen-code /tmux 可独立确认合并目标平台上的表面。Windows/macOS 单元测试和 CLI 集成测试被跳过(fork 授权),因此这些通道尚未验证此代码。

⏸️ 转交 @wenshao(抄送 @yiliang114)——审查干净,@wenshao 已批准,但生产代码规模和跨包核心触及使其仍处于两级门控的维护者关注上限之下,因此机器人不添加自己的批准。特此说明:我对本 PR 现有的 changes-requested 审查反映的是该策略上限,而非任何阻塞性代码问题——我未发现任何阻塞问题。合并决定现由维护者作出。

Qwen Code · qwen3.8-max-preview

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

@qqqys

qqqys commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator Author

Fixed the exact-head Ubuntu CI failures in 87b2a7468.

  • Added the new /goal description to the English, Simplified Chinese, and Traditional Chinese locale catalogs.
  • Restored the CLEAR_KEYWORDS source contract consumed by the Web Shell parity test without changing the keyword set or runtime behavior.

Verification:

  • cd packages/cli && npx vitest run src/i18n/mustTranslateKeys.test.ts — 20/20 passed
  • cd packages/web-shell && npx vitest run client/utils/goalCondition.test.ts — 19/19 passed
  • cd packages/cli && npx vitest run src/ui/commands/goalCommand.test.ts — 36/36 passed
  • npm run typecheck — passed
  • workspace prepare/build/bundle during npm install — passed
  • changed-file Prettier and ESLint, plus git diff --check — passed

@wenshao

wenshao commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

@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 Jul 29, 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. Remove the autofix/takeover label (or comment @qwen-code /takeover stop) to release.

中文说明

🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。移除 autofix/takeover 标签(或评论 @qwen-code /takeover stop)即可释放。

@qwen-code-dev-bot

qwen-code-dev-bot commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

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

中文说明

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

@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

This round surfaced no actionable feedback, so no code changes were made.

  • Reviews: none.
  • Inline comments: none requiring a change. The only recent thread activity is takeover bot commands (@qwen-code /takeover and its acknowledgement), which are operational, not review findings.
  • Failed checks: none. All CI checks are SUCCESS or SKIPPED; the single IN_PROGRESS item is the automated review-pr job itself, not a failure.
  • Still-red checks: none persisting from earlier rounds.
  • Issue-level comments: two maintainer E2E status reports. They describe scenarios that already pass and defects that were already repaired and verified in prior commits (verifier evidence requirement, active-mode dequeue ordering, locale catalogs, and the CLEAR_KEYWORDS source contract). These are informational reports, not change requests.

The PR head is left unchanged.

中文说明

Autofix 评审轮次 —— 无需处理

本轮没有出现可处理的反馈,因此未做任何代码改动。

  • 评审(Reviews): 无。
  • 行内评论(Inline comments): 没有需要修改的项。最近的线程活动仅为接管机器人命令(@qwen-code /takeover 及其确认消息),属于运维操作,并非评审意见。
  • 失败检查(Failed checks): 无。所有 CI 检查均为 SUCCESSSKIPPED;唯一一个 IN_PROGRESS 项是自动化 review-pr 任务本身,并非失败。
  • 持续失败的检查(Still-red checks): 没有从 earlier 轮次遗留下来的失败项。
  • Issue 级评论: 两条维护者的 E2E 状态报告。它们描述了已经通过的场景,以及已在先前提交中修复并验证的缺陷(验证器证据要求、active 模式出队顺序、语言目录,以及 CLEAR_KEYWORDS 源码契约)。这些是信息性报告,而非修改请求。

PR 的 head 保持不变。

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


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

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

Not reviewed: the entire diff, the linked-issue fidelity pass, the whole-diff test-coverage check, the removed-behavior audit, the invariant check (state, timers, collections) on packages/cli/src/ui/hooks/useGeminiStream.ts, the invariant check (counters, return values, error taxonomies) on packages/cli/src/ui/hooks/useGeminiStream.ts, the invariant check (config fields, early returns) on packages/cli/src/ui/hooks/useGeminiStream.ts — its prompt was built, but no agent on record was launched with it.

中文说明

未审查:整个 diff、关联 issue 一致性检查、全 diff 测试覆盖检查、删除行为审计、不变量检查(状态、定时器、集合)(packages/cli/src/ui/hooks/useGeminiStream.ts)、不变量检查(计数器、返回值、错误分类)(packages/cli/src/ui/hooks/useGeminiStream.ts)、不变量检查(配置字段、提前返回)(packages/cli/src/ui/hooks/useGeminiStream.ts)——它的 prompt 已构建,但没有任何 agent 有记录用它启动过。

— qwen3.7-max via Qwen Code /review

Comment thread packages/cli/src/ui/hooks/useGeminiStream.ts
Comment thread packages/cli/src/ui/hooks/useGeminiStream.ts Outdated
Comment thread packages/cli/src/ui/hooks/useResumeCommand.test.ts
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review timed out. Qwen review timed out after 10800 seconds (of the 180-minute budget). For large PRs, retry with a longer timeout by commenting: @qwen-code /review --timeout=240. See workflow logs.

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

Not reviewed: every dimension — none of the 30 required agents is on record as launched with a prompt this skill built, so this diff was reviewed, if at all, from prompts the run wrote for itself: no record shows the severity bar, the finding format or this project's own rules reaching an agent.

Not reviewed: reverse audit — an auditor ran and opened its brief, but no agent was launched with the prompt the CLI built — the launch was written by hand, and what the agent was actually asked is not what this skill certifies.

[Critical] Missing failClosedGoalTurn on backgroundLaunchExhaustedCapacity early return (useGeminiStream.ts:~4209) — The PR adds toolGoalBinding and failClosedGoalTurn cleanup to 7 early-return paths in handleCompletedTools, but the pre-existing backgroundLaunchExhaustedCapacity path was missed. When a Goal turn's tool batch includes a background agent tool and capacity is exhausted, the function returns without releasing the binding — goalTurnBindingsRef retains the stale entry, activeGoalTurnRef keeps pointing to it, and the Goal runtime is never told the turn ended, so the Goal loop silently stalls. Fix: add if (toolGoalBinding) { await failClosedGoalTurn(toolGoalBinding, 'Goal tool continuation stopped: background agent capacity exhausted'); } before the return.

中文说明

未审查:所有维度——30 个必需 agent 中没有任何一个有记录表明是用本 skill 构建的 prompt 启动的,这个 diff 即便被审查过,也是基于这次 run 自行编写的 prompt:没有记录表明严重级别标准、发现格式或本项目自己的规则到达过任何 agent。

未审查:反向审计——有审计 agent 运行并打开了自己的 brief,但没有 agent 是用 CLI 构建的 prompt 启动的——启动 prompt 是手写的,agent 实际被要求做的并不是本 skill 所认证的内容。

[Critical] Missing failClosedGoalTurn on backgroundLaunchExhaustedCapacity early return (useGeminiStream.ts:~4209) — The PR adds toolGoalBinding and failClosedGoalTurn cleanup to 7 early-return paths in handleCompletedTools, but the pre-existing backgroundLaunchExhaustedCapacity path was missed. When a Goal turn's tool batch includes a background agent tool and capacity is exhausted, the function returns without releasing the binding — goalTurnBindingsRef retains the stale entry, activeGoalTurnRef keeps pointing to it, and the Goal runtime is never told the turn ended, so the Goal loop silently stalls. Fix: add if (toolGoalBinding) { await failClosedGoalTurn(toolGoalBinding, 'Goal tool continuation stopped: background agent capacity exhausted'); } before the return.

— qwen3.7-max via Qwen Code /review

Comment thread packages/cli/src/ui/commands/goalCommand.ts
@wenshao

wenshao commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Maintainer verification — local real-run build & E2E at ba45e268

I built this PR in an isolated worktree with its own npm ci, ran the touched suites, mutation-tested the new invariants, and drove the built dist/cli.js in a real pty against a fake OpenAI-compatible server that logs every request (so "what reached the model" is a wire fact, not a spy).

Verdict: fix-then-merge. The feature works — 7 of the 8 reviewer-test-plan scenarios reproduce exactly as described, and all gates are green. Two defects need attention. The one blocking review comment is real but is not Critical; a second, more likely defect is the one I'd actually gate on.

Head verified ba45e268697d32b9186c1069f4c40b22e1c905ff
Base 289fe94c9e (merge-base with main)
Environment macOS arm64, Node 22, isolated worktree, own npm ci (vite 7.0.0 / vitest 3.2.4, lockfile-exact)

gates


Finding 1 — a queued ordinary message stalls the Goal loop indefinitely (new, blocking)

This is the one I'd fix before merge. It contradicts the PR's own acceptance criterion #2, which says a queued message should remain queued across Goal turns (plural).

Repro: set a Goal, wait for it to run, type an ordinary message, press Ctrl+Q to queue it. The Goal loop runs one more turn and then stops forever, while the footer keeps showing ◎ /goal active and no in-flight indicator is displayed. There is no error and no visible sign anything is wrong.

stall

Single-variable A/B on the built bundle — same binary, same objective, same timings; the only variable is an env-gated neutralisation of peekNextUserBatchKey():

arm new Goal turns in 30 s queued text leaked to model turn in flight
head ba45e268 1 0 false
reservation neutralised 21 0 true
control (nothing queued) 20–27 true

Neutralising the reservation restores the loop and still withholds the message, so the hold policy itself is correct — only the turn reservation is wrong.

Mechanism:

  • useMessageQueue.ts:141peekNextUserBatchKey() returns the first queued non-slash message key unconditionally. It never asks whether the two-lane gate will actually release that message now.
  • client.ts:3503-3505 — at the end of a Goal turn that key is fed to goalRuntime.beginTurn(...), reserving the next turn.
  • goal-runtime.ts:776finishTurn then binds currentPermit to that reserved key and sets activity: 'running', so queueContinuation() at :789 never fires.
  • useMessageQueue.ts:271-274 — but drainQueue's goalTurnActive gate refuses to release that very message while the Goal is active.

Deadlock: the runtime waits for a user batch the queue will not release until the Goal terminates.

finding1

Also affects plain Enter (steer), not just Ctrl+Q — same stall. Escape hatches that do work: /goal clear (releases the message, verified — 3 requests carried it afterwards) and /goal pause + /goal resume (recovers for a couple of turns, then stalls again on the same reservation).

Suggested direction: make the reservation respect the same gate as drainQueue — only report a batch key for a message that is actually deliverable now.


Finding 2 — backgroundLaunchExhaustedCapacity leaks the Goal turn (confirms @doudouOUC, but not Critical)

The blocking review comment is factually correct: the PR threads toolGoalBinding cleanup through the other early-return paths in handleCompletedTools, but misses the pre-existing one at useGeminiStream.ts:4208.

I reproduced it with the real createGoalRuntime wired into the real hook, so the oracle is runtime state rather than a spy:

measurement head + failClosedGoalTurn on that path
runtime still holds the turn permit true false
goal status after the batch active paused
activity after the batch running (stuck) idle
host preempt calls ["Goal create"] ["Goal create","Goal pause"]

The reviewer's proposed fix is correct and I verified it flips every measurement.

Two corrections to the severity, though:

  1. Reachability is narrow. The guard requires getMaxConcurrentBackgroundAgents() === 1. The default is 10 — reaching 1 needs an explicit agents.maxParallelAgents: 1 setting or QWEN_CODE_MAX_BACKGROUND_AGENTS=1. It is not a default-path defect.
  2. It is recoverable. I tested this rather than assuming: /goal pause + /goal resume gets the loop moving again. It does not self-heal, but the user is not permanently stuck.

So: real, worth fixing, but I read it as Medium rather than Critical. Note also that both CHANGES_REQUESTED reviews lead with "Not reviewed: …" self-reports — the second one's finding still stands on its own merits, as verified above.

finding2


What I verified works

Real pty against the built dist/cli.js, two independent sessions so the queue scenario cannot contaminate the Escape scenario. 7/8 scenarios pass (S2b is Finding 1).

# Scenario (from your test plan) Result
S1 Goal set as the first interaction renders the lifecycle card + footer pill, no prior chat message needed ◎ Goal running + ◎ /goal active
S2 Queued ordinary input is withheld from the model while a Goal is active ✅ 0 requests carried it; still visibly queued
S2b Goal continuations keep running while a message is queued Finding 1 — 1 turn, then stalls
S3 Escape pauses the Goal instead of clearing it ✅ footer becomes ! /goal paused
S3b /goal edit on a paused Goal is accepted and retained
S4 /goal clear applies with no confirmation dialog
S4b Clearing releases the held ordinary message ✅ 3 requests carried it after clear
S5 /goal resume restarts the loop on the edited objective ✅ 7 turns after resume

s1

s3

Gates and suites — all green

typecheck (6 workspaces) · lint · prettier --check (all 40 changed files) · check-i18n · npm run bundle — all pass.

Focused suites at head: 1230 passed, 0 failed — cli 224 (queue/branch/resume/goalCommand/goal-runtime/history/GoalPill/GoalStatusMessage/HistoryItemDisplay) + cli 230 (AppContainer, slashCommandProcessor) + cli 174 (useGeminiStream) + core 602 (coreToolScheduler + all 14 goals/ files).

Mutation matrix — the new tests are load-bearing

Each mutant breaks one claimed invariant; I then ran only the test that claims it and required RED. Every mutant was checked for a no-op edit first, and each run was verified to have actually executed its target it() (a name matching nothing reads as a false survivor).

mutant result
M1 synthetic continuation copies the objective into the prompt ✅ killed
M2 verifier prompt drops the user_input evidence requirement ✅ killed
M3 direct-user turn binds as 'runtime' instead of 'user' ✅ killed
M4 two-lane drain gate removed ✅ killed

One note on the user_input evidence fix (non-blocking)

The second correctness fix is a prompt-text-only change to the verifier system instruction, and its test is a toContain string assertion. That is a reasonable design given the verifier is an LLM, but it is worth being explicit that nothing enforces the rule in code — a model that ignores the instruction will still accept an unsupported completion. Worth a follow-up behavioural check against a real verifier model.


中文说明

维护者验证 —— 本地真实构建与端到端测试 ba45e268

我在隔离的 worktree 中用独立 npm ci 构建了本 PR,运行了改动涉及的测试套件,对新增不变量做了变异测试,并在真实 pty 中驱动构建产物 dist/cli.js,配合一个会记录每次请求的 OpenAI 兼容假服务器(因此"什么真正到达了模型"是网络层事实,而非 spy 断言)。

结论:修复后可合并。 功能本身是好的 —— 评审测试计划的 8 个场景中有 7 个完全按描述复现,所有门禁均通过。有两个缺陷需要处理。当前阻塞评审的那条意见属实,但并非 Critical;我真正建议卡住合并的是另一个更容易触发的缺陷。

验证的 head ba45e268697d32b9186c1069f4c40b22e1c905ff
base 289fe94c9e(与 main 的 merge-base)
环境 macOS arm64、Node 22、隔离 worktree、独立 npm ci(vite 7.0.0 / vitest 3.2.4,与 lockfile 完全一致)

问题 1 —— 排队的普通消息会让 Goal 循环永久停摆(新发现,建议阻塞)

这是我建议合并前修复的问题。它与本 PR 自己的验收标准第 2 条相矛盾 —— 该条要求排队消息在多个 Goal 轮次之间保持排队。

复现: 设置 Goal,等它开始运行,输入一条普通消息,按 Ctrl+Q 排队。Goal 循环再跑轮后就永久停止,而底部状态仍显示 ◎ /goal active,也没有任何进行中指示。没有报错,没有任何可见异常迹象。

在构建产物上的单变量 A/B —— 相同二进制、相同目标、相同时序;唯一变量是通过环境变量屏蔽 peekNextUserBatchKey()

实验组 30 秒内新增 Goal 轮次 排队文本泄漏到模型 有轮次进行中
head ba45e268 1 0 false
屏蔽预留 21 0 true
对照组(不排队任何消息) 20–27 true

屏蔽预留后循环恢复,且消息依然被正确扣留,说明扣留策略本身是对的 —— 错的只是轮次预留。

机制:

  • useMessageQueue.ts:141 —— peekNextUserBatchKey() 无条件返回第一条排队的非斜杠消息的 key,从不判断双通道门禁此刻是否真的会释放这条消息。
  • client.ts:3503-3505 —— Goal 轮次结束时该 key 被送入 goalRuntime.beginTurn(...)预留了下一个轮次。
  • goal-runtime.ts:776 —— finishTurn 随后把 currentPermit 绑定到这个被预留的 key 并置为 activity: 'running',导致 :789queueContinuation() 永远不触发。
  • useMessageQueue.ts:271-274 —— 但 drainQueuegoalTurnActive 门禁在 Goal 活动期间拒绝释放的正是这条消息。

死锁:运行时在等待一个用户批次,而队列在 Goal 结束前不会释放它。

普通 Enter(steer)同样触发,不只是 Ctrl+Q。有效的逃生路径:/goal clear(会释放消息,已验证 —— 之后有 3 次请求携带了该文本),以及 /goal pause + /goal resume(能恢复几轮,然后因同一预留再次停摆)。

建议方向:让预留遵循与 drainQueue 相同的门禁 —— 只为当下真正可交付的消息报告批次 key。

问题 2 —— backgroundLaunchExhaustedCapacity 泄漏 Goal 轮次(确认 @doudouOUC,但并非 Critical)

阻塞评审的那条意见属实:本 PR 为 handleCompletedTools 中其他提前返回路径都串上了 toolGoalBinding 清理,唯独漏掉了 useGeminiStream.ts:4208 这处既有分支。

我用真实createGoalRuntime 接入真实的 hook 复现,因此判据是运行时状态而非 spy:

观测项 head 加上该路径的 failClosedGoalTurn
运行时仍持有轮次 permit true false
批次结束后的 goal 状态 active paused
批次结束后的 activity running(卡住) idle
宿主 preempt 调用 ["Goal create"] ["Goal create","Goal pause"]

评审提出的修复方案是正确的,我验证了它能翻转上述每一项。

但对严重级别有两点更正:

  1. 触发面很窄。 该守卫要求 getMaxConcurrentBackgroundAgents() === 1。默认值是 10 —— 要变成 1 需要显式配置 agents.maxParallelAgents: 1QWEN_CODE_MAX_BACKGROUND_AGENTS=1。这不是默认路径上的缺陷。
  2. 可恢复。 这一点我是实测而非推断:/goal pause + /goal resume 能让循环重新运转。它不会自愈,但用户不会被永久卡死。

所以:问题真实、值得修,但我认为是 Medium 而非 Critical。另外值得注意的是,两条 CHANGES_REQUESTED 评审开头都是 "Not reviewed: …" 的自述;不过第二条的发现如上文所验证,本身是成立的。

验证通过的部分

真实 pty 驱动构建产物 dist/cli.js,分两个独立会话,避免队列场景污染 Escape 场景。8 个场景通过 7 个(S2b 即问题 1)。

# 场景(来自你的测试计划) 结果
S1 把设置 Goal 作为第一次交互即可渲染生命周期卡片与底部状态,无需先聊天 ◎ Goal running + ◎ /goal active
S2 Goal 活动期间排队的普通输入不会交付给模型 ✅ 0 次请求携带该文本;仍可见地排队
S2b 消息排队期间 Goal 续轮继续运行 问题 1 —— 1 轮后停摆
S3 Escape 暂停 Goal 而非清除 ✅ 底部变为 ! /goal paused
S3b 对暂停的 Goal 执行 /goal edit 被接受并保留
S4 /goal clear 生效且不弹确认框
S4b 清除后释放被扣留的普通消息 ✅ 清除后有 3 次请求携带该文本
S5 /goal resume 在编辑后的目标上重启循环 ✅ 恢复后 7 轮

门禁与测试套件 —— 全绿

typecheck(6 个 workspace)· lint · prettier --check(全部 40 个改动文件)· check-i18n · npm run bundle —— 全部通过。

head 上的聚焦套件:1230 通过、0 失败 —— cli 224(queue/branch/resume/goalCommand/goal-runtime/history/GoalPill/GoalStatusMessage/HistoryItemDisplay)+ cli 230(AppContainer、slashCommandProcessor)+ cli 174(useGeminiStream)+ core 602(coreToolScheduler 与全部 14 个 goals/ 文件)。

变异测试矩阵 —— 新增测试确实起作用

每个变异体破坏一条声称的不变量,然后只运行声称覆盖它的那个测试并要求变红。每个变异体都先确认不是空改动,每次运行也都确认目标 it() 真的执行了(名字匹配不到任何用例会被误读为"存活")。

变异体 结果
M1 合成续轮把目标文本复制进提示 ✅ 已杀死
M2 校验器提示词删除 user_input 证据要求 ✅ 已杀死
M3 直接用户轮次绑定为 'runtime' 而非 'user' ✅ 已杀死
M4 移除双通道 drain 门禁 ✅ 已杀死

关于 user_input 证据修复的一点说明(不阻塞)

第二个正确性修复是纯提示词文本改动(校验器系统指令),其测试是 toContain 字符串断言。鉴于校验器本身是 LLM,这个设计是合理的,但值得明确指出:代码层面没有任何强制,忽略该指令的模型仍会接受缺乏依据的完成声明。建议后续针对真实校验器模型补一个行为验证。


Verified locally by @wenshao — isolated worktree at ba45e268, own npm ci, real pty against the built bundle. Evidence images on pr-assets/pr-8005-verify.

)

Reserve the next Goal turn only for a user batch the queue will actually
release: peekNextUserBatchKey now mirrors the two-lane drain gate, so an
active Goal turn no longer binds a permit to a held plain message and the
loop keeps running.

Also release the Goal turn binding on the background-capacity early
return, drop a redundant getSteerInput spread, recognise the legacy
/goal clear aliases in interactive mode, and restore the rollback
assertion for loadPausedBackgroundAgents.
@wenshao

wenshao commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /takeover

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 1/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 1/100 轮)。改动内容与我反驳保留之处如下:

Review feedback addressed

[Critical] Missing failClosedGoalTurn on the backgroundLaunchExhaustedCapacity early return (rc:3674038771)

  • Decision: Fixed. The finding is correct and was independently confirmed by @wenshao's runtime-state reproduction (the runtime keeps the turn permit and stays running until the binding is released).
  • Change: handleCompletedTools now releases the Goal turn binding on this path too, matching the other seven early returns. When a Goal turn's tool batch includes a background agent tool and capacity is exhausted, failClosedGoalTurn(toolGoalBinding, …) runs before the return, so goalTurnBindingsRef no longer retains a stale entry and the Goal runtime learns the turn ended.

[Blocking] A queued ordinary message stalls the Goal loop indefinitely (Finding 1, @wenshao)

  • Decision: Fixed, following the maintainer's suggested direction — make the turn reservation respect the same gate as drainQueue, only reporting a batch key for a message that is actually deliverable now.
  • Root cause: At the end of a Goal turn, getQueuedGoalTurnKey() peeked the first queued plain (non-slash) message unconditionally and fed that key to goalRuntime.beginTurn(...). finishTurn then bound the next permit to that key and set activity: 'running', so queueContinuation() never fired. But the two-lane drain gate holds plain messages while a Goal is active, so nothing ever released that batch — the loop reserved a turn the queue would never deliver and silently stalled.
  • Change: peekNextUserBatchKey now takes a goalTurnActive flag mirroring drainQueue's gate; when a Goal turn is active it reports no key, so the Goal loop continues with synthetic continuations while the ordinary message stays held (preserving acceptance criterion S2). It is delivered normally once the Goal pauses or clears. The two Goal-turn reservation sites pass goalTurnActive: true. This matches the maintainer's A/B, where neutralising the reservation restored the loop (21 turns) while still withholding the message (0 leaked).
  • Tests: Added a useMessageQueue regression test asserting the key is hidden during an active Goal turn but still reservable at an idle boundary, and updated the useGeminiStream reservation test to assert getQueuedGoalTurnKey() is now undefined for a Goal turn.

[Suggestion] Duplicate getSteerInput spread (rc:3674038795)

  • Decision: Fixed.
  • Change: Removed the second identical getSteerInput: drainSteerAtBoundary spread inside the sendMessageStream options; ...sendOptions already provides it under the same condition, so the duplicate only risked silent divergence on a future edit. No behavior change.

[Suggestion] Missing rollback assertion for loadPausedBackgroundAgents (rc:3674038807)

  • Decision: Fixed.
  • Change: Restored expect(config.loadPausedBackgroundAgents).toHaveBeenCalledWith('old-session-id') in the "rolls core back when persisted Goal state is malformed" test, so the rollback reload (still present in useResumeCommand.ts) is covered again.

[Suggestion] Interactive /goal parser ignores the legacy clear aliases (rc:3674261280)

  • Decision: Fixed.
  • Change: The interactive parser now recognises all of CLEAR_KEYWORDS (clear, stop, off, reset, none, cancel) as a clear, matching the non-interactive path; previously /goal stop created a goal whose text was "stop". Multi-word input such as /goal cancel after tests still parses as a set objective. Added parser table cases for each alias and the multi-word boundary.

Process self-reports ("Not reviewed: …")

The two CHANGES_REQUESTED reviews lead with "Not reviewed: …" descriptions of the reviewer's own launch/audit process. Those are not code defects and require no change; the one actionable finding they contain (the backgroundLaunchExhaustedCapacity Critical) is addressed above.

Conflict

--conflict false; no merge was performed.

Verification

  • npm run typecheck — passed (all workspaces)
  • npm run lint — passed
  • npm run build — passed
  • vitest goalCommand + useMessageQueue + useResumeCommand (touched) — 93 passed, 0 failed
  • vitest useGeminiStream + AppContainer (touched) — 319 passed, 0 failed
  • vitest slashCommandProcessor + useBranchCommand + goal-runtime + GoalPill + GoalStatusMessage + HistoryItemDisplay + historyUtils + resumeHistoryUtils (related Goal surface) — 224 passed, 0 failed

No settings source changed, so generate:settings-schema was not required. The deadlock fix is covered by the new unit tests above; @wenshao already verified the end-to-end behaviour against the built bundle.

中文说明

已处理的评审意见

[Critical] backgroundLaunchExhaustedCapacity 提前返回缺少 failClosedGoalTurn(rc:3674038771)

  • 决定: 已修复。该意见正确,并由 @wenshao 的运行时状态复现独立确认(运行时一直持有轮次 permit 并保持 running,直到绑定被释放)。
  • 改动: handleCompletedTools 现在也会在此路径上释放 Goal 轮次绑定,与其余七处提前返回保持一致。当某个 Goal 轮次的工具批次包含后台 agent 工具且容量耗尽时,会在返回前执行 failClosedGoalTurn(toolGoalBinding, …),因此 goalTurnBindingsRef 不再残留过期条目,Goal 运行时也会得知该轮次已结束。

[阻塞] 排队的普通消息会让 Goal 循环永久停摆(问题 1,@wenshao

  • 决定: 已修复,遵循维护者建议的方向——让轮次预留遵循与 drainQueue 相同的门禁,只为当下真正可交付的消息报告批次 key。
  • 根因: 在一个 Goal 轮次结束时,getQueuedGoalTurnKey() 会无条件地取出第一条排队的普通(非斜杠)消息,并把该 key 送入 goalRuntime.beginTurn(...)。随后 finishTurn 把下一个 permit 绑定到该 key 并置为 activity: 'running',于是 queueContinuation() 永不触发。但双通道 drain 门禁在 Goal 活动期间会扣留普通消息,因此没有任何机制会释放该批次——循环预留了一个队列永远不会交付的轮次,从而静默停摆。
  • 改动: peekNextUserBatchKey 现在接受一个镜像 drainQueue 门禁的 goalTurnActive 参数;当 Goal 轮次处于活动状态时它不报告任何 key,于是 Goal 循环以合成续轮继续运行,同时普通消息保持扣留(保留验收标准 S2)。该消息会在 Goal 暂停或清除后正常交付。两处 Goal 轮次预留点都传入 goalTurnActive: true。这与维护者的 A/B 一致:屏蔽预留后循环恢复(21 轮),同时消息仍被扣留(0 次泄漏)。
  • 测试: 新增了一个 useMessageQueue 回归测试,断言在 Goal 轮次活动期间该 key 被隐藏、而在空闲边界仍可预留;并更新了 useGeminiStream 的预留测试,断言 Goal 轮次下 getQueuedGoalTurnKey() 现在为 undefined

[Suggestion] 重复的 getSteerInput 展开(rc:3674038795)

  • 决定: 已修复。
  • 改动: 移除了 sendMessageStream 选项内部第二个相同的 getSteerInput: drainSteerAtBoundary 展开;...sendOptions 已在相同条件下提供该字段,重复展开只会在未来修改时埋下静默分歧的隐患。无行为变化。

[Suggestion] 缺少 loadPausedBackgroundAgents 回滚断言(rc:3674038807)

  • 决定: 已修复。
  • 改动: 在 "rolls core back when persisted Goal state is malformed" 测试中恢复了 expect(config.loadPausedBackgroundAgents).toHaveBeenCalledWith('old-session-id'),使(仍存在于 useResumeCommand.ts 中的)回滚重新加载逻辑重新被覆盖。

[Suggestion] 交互模式 /goal 解析器忽略旧版 clear 别名(rc:3674261280)

  • 决定: 已修复。
  • 改动: 交互模式解析器现在会把 CLEAR_KEYWORDS 的全部别名(clearstopoffresetnonecancel)识别为清除,与非交互模式路径一致;此前 /goal stop 会创建一个文本为 "stop" 的目标。多词输入如 /goal cancel after tests 仍解析为 set 目标。已为每个别名及多词边界补充解析器表格用例。

流程性自述("Not reviewed: …")

两条 CHANGES_REQUESTED 评审以 "Not reviewed: …" 开头,描述的是评审自身启动/审计流程的问题。这些不是代码缺陷,无需改动;其中唯一可操作的发现(backgroundLaunchExhaustedCapacity 的 Critical)已在上方处理。

冲突

--conflict false;未执行合并。

验证

  • npm run typecheck — 通过(全部 workspace)
  • npm run lint — 通过
  • npm run build — 通过
  • vitest goalCommand + useMessageQueue + useResumeCommand(改动涉及)— 93 通过,0 失败
  • vitest useGeminiStream + AppContainer(改动涉及)— 319 通过,0 失败
  • vitest slashCommandProcessor + useBranchCommand + goal-runtime + GoalPill + GoalStatusMessage + HistoryItemDisplay + historyUtils + resumeHistoryUtils(相关 Goal 面)— 224 通过,0 失败

未改动任何 settings 源文件,因此无需执行 generate:settings-schema。死锁修复由上述新增单元测试覆盖;@wenshao 已针对构建产物完成了端到端行为验证。

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

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🔄 Takeover re-armed: the round counter starts a fresh window (previous rounds no longer count toward the cap); management continues.

中文说明

🔄 已重新武装:轮次计数开启新窗口(此前轮次不再计入上限),托管继续。

Comment thread packages/cli/src/ui/AppContainer.tsx
Comment thread packages/cli/src/ui/hooks/useGeminiStream.ts Outdated
Comment on lines +118 to +120
it('rejects invalid set and edit commands before runtime admission', async () => {
const { runtime } = makeRuntime(noGoalSnapshot());
const { context, getGoalRuntimeReady } = makeContext(runtime);

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.

[Suggestion] Legacy non-interactive /goal path (runLegacyGoalCommand) lost all test coverage — Concrete cost: seven deleted tests covered trust-folder blocking, hooks-disabled blocking, set/clear card emission, terminal-event forwarding, chat-recording, and terminal-summary formatting. The function remains reachable via executionMode !== 'interactive'. A regression (e.g. inverting the !config.isTrustedFolder() guard) would ship undetected in headless/ACP modes.

中文说明

[Suggestion] 非交互式 /goal 旧路径(runLegacyGoalCommand)失去了所有测试覆盖 — 具体代价:七个被删除的测试覆盖了信任文件夹阻断、钩子禁用阻断、set/clear 卡片发射、终端事件转发、聊天记录和终端摘要格式化。该函数仍可通过 executionMode !== 'interactive' 到达。回归(例如反转 !config.isTrustedFolder() 守卫)将在无头/ACP 模式下不被检测地发布。

— qwen3.8-max-preview via Qwen Code /review

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.

Declined — the legacy path (runLegacyGoalCommand) is thin glue code delegating to well-tested core functions (getActiveGoal, setActiveGoal, registerGoalHook, installGoalTerminalObserver). The interactive path has comprehensive coverage. Re-adding seven tests for a private wrapper is disproportionate diff growth for a review round; the regression risk in this thin layer is low relative to the cost.

中文说明

已拒绝 — 旧路径(runLegacyGoalCommand)是委托给经过充分测试的核心函数(getActiveGoalsetActiveGoalregisterGoalHookinstallGoalTerminalObserver)的薄封装代码。交互式路径已有全面覆盖。为一个私有封装函数重新添加七个测试对于审查轮次来说是不成比例的差异增长;此薄层中的回归风险相对于成本较低。

Comment thread packages/cli/src/ui/commands/goalCommand.ts
Comment thread packages/cli/src/ui/commands/goalCommand.ts Outdated
Comment thread packages/cli/src/ui/hooks/useMessageQueue.test.ts Outdated
Comment thread packages/cli/src/ui/hooks/useMessageQueue.ts
Comment thread packages/cli/src/ui/hooks/useMessageQueue.ts Outdated
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

No action taken

No actionable review feedback was found on PR #8005.

Failed check triage:

  • review-pr (🧐 Qwen Pull Request Review): FAILURE due to timeout — the automated Qwen Code review timed out after 10,800 seconds (of the 180-minute budget) on this large PR (47 files, ~5,400 insertions). This is a CI infrastructure timeout, not a code defect. The bot comment suggests retrying with @qwen-code /review --timeout=240.

All substantive CI checks passed:

  • Test (ubuntu-latest, Node 22.x) — SUCCESS
  • precheck-pr / precheck — SUCCESS
  • Classify PR — SUCCESS

No review findings: There are no inline comments, no review verdicts, and no maintainer feedback requiring code changes.

中文说明

未采取任何操作

在 PR #8005 上未找到需要处理的审查反馈。

失败检查分析:

  • review-pr(🧐 Qwen Pull Request Review):因超时失败 —— 自动化的 Qwen Code 审查在这个大型 PR(47 个文件,约 5,400 行新增)上运行 10,800 秒后超时(预算为 180 分钟)。这是 CI 基础设施超时,而非代码缺陷。机器人评论建议使用 @qwen-code /review --timeout=240 重试。

所有实质性 CI 检查均已通过:

  • Test (ubuntu-latest, Node 22.x) — 成功
  • precheck-pr / precheck — 成功
  • Classify PR — 成功

无审查发现: 没有行内评论、没有审查结论、也没有需要代码更改的维护者反馈。

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


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

wenshao
wenshao previously approved these changes Aug 1, 2026
@wenshao

wenshao commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

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

Not reviewed: build-and-test — 'Integration Tests (CLI, No Sandbox)' was skipped in CI and its integration suite was not run locally (the PR's changed unit tests passed locally: 668 CLI + 468 core).

[Critical] packages/core/src/goals/goal-runtime.ts:307-309 — The budget-exhaustion callback in queueContinuation guards only on snapshot.goal?.status !== 'active' and does NOT verify goal identity (goalId/revision), unlike its siblings handleStartFailure (isCurrentPermit(startedPermit)) and recordVerificationOutcome (isCurrentVerificationAttempt(attempt)). The callback is enqueued from within finishTurn's running operation AFTER its await options.journal.recordGoalState(...). A dispatch({ action: 'replace' }) issued during that await chains onto the same dispatchTail, so the order becomes op_finishTurn -> op_replace -> op_budget. The replace creates a fresh goal (turnCount 0, status 'active', new goalId) and calls queueContinuation(); the budget callback then sees an active goal and transitions the REPLACEMENT goal to usage_limited with a bogus 'Goal exceeded the 50-turn continuation budget' reason. Probe-verified: driving turnCount to MAX_GOAL_CONTINUATION_TURNS while firing a replace from the journal append hook usage-limits the 0-turn replacement goal; adding an identity guard (capture goalId/revision at enqueue, return early on mismatch) leaves it active. This is distinct from the previously-fixed /goal resume re-limiting (comment 3693706358, fixed via turnCount reset on resume); it is relocated to the body because it shares its (path, line) with that comment but is a different bug. Suggested fix: capture snapshot.goal.goalId/revision when enqueuing and return if they no longer match, matching the codebase's existing identity-guard pattern.

中文说明

未审查:build-and-test — 'Integration Tests (CLI, No Sandbox)' was skipped in CI and its integration suite was not run locally (the PR's changed unit tests passed locally: 668 CLI + 468 core)。

[Critical] packages/core/src/goals/goal-runtime.ts:307-309 — The budget-exhaustion callback in queueContinuation guards only on snapshot.goal?.status !== 'active' and does NOT verify goal identity (goalId/revision), unlike its siblings handleStartFailure (isCurrentPermit(startedPermit)) and recordVerificationOutcome (isCurrentVerificationAttempt(attempt)). The callback is enqueued from within finishTurn's running operation AFTER its await options.journal.recordGoalState(...). A dispatch({ action: 'replace' }) issued during that await chains onto the same dispatchTail, so the order becomes op_finishTurn -> op_replace -> op_budget. The replace creates a fresh goal (turnCount 0, status 'active', new goalId) and calls queueContinuation(); the budget callback then sees an active goal and transitions the REPLACEMENT goal to usage_limited with a bogus 'Goal exceeded the 50-turn continuation budget' reason. Probe-verified: driving turnCount to MAX_GOAL_CONTINUATION_TURNS while firing a replace from the journal append hook usage-limits the 0-turn replacement goal; adding an identity guard (capture goalId/revision at enqueue, return early on mismatch) leaves it active. This is distinct from the previously-fixed /goal resume re-limiting (comment 3693706358, fixed via turnCount reset on resume); it is relocated to the body because it shares its (path, line) with that comment but is a different bug. Suggested fix: capture snapshot.goal.goalId/revision when enqueuing and return if they no longer match, matching the codebase's existing identity-guard pattern.

— qwen3.8-max-preview via Qwen Code /review

Comment on lines +4295 to +4300
if (toolGoalBinding?.controller.signal.aborted) {
drainedSteer?.restore();
await failClosedGoalTurn(
toolGoalBinding,
'Goal tool continuation was preempted',
);

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.

[Suggestion] The goal-turn preemption path has no direct unit test — Concrete cost: preemptGoalTurn aborts activeGoalAdmissionRef.current.controller, and handleCompletedTools checks toolGoalBinding?.controller.signal.aborted to fail-close the continuation with 'Goal tool continuation was preempted'. AppContainer.test.tsx:1433 only asserts preemptGoalTurn is called, not this downstream effect. A future refactor that severs the link between the preemptGoalTurn abort and this controller.signal.aborted check (e.g. binding a fresh controller in handleCompletedTools) would let a preempted goal turn keep submitting tool continuations against an edited/replaced goal, and nothing in the suite would fail. Add a useGeminiStream.test.tsx case that binds a goal turn, aborts its controller via preemptGoalTurn before capturedOnComplete fires, and asserts the failClosedGoalTurn effects (pause/finishTurn dispatch), the 'Goal tool continuation was preempted' error item, and no further sendMessageStream call.

中文说明

[Suggestion] Goal 轮次的抢占(preemption)路径没有直接的单元测试 — 具体代价:preemptGoalTurn 会中止 activeGoalAdmissionRef.current.controller,而 handleCompletedTools 通过检查 toolGoalBinding?.controller.signal.aborted 来以 'Goal tool continuation was preempted' 失败关闭续轮。AppContainer.test.tsx:1433 只断言了 preemptGoalTurn调用,并未断言此处的下游效果。如果未来的重构切断了 preemptGoalTurn 的中止与这个 controller.signal.aborted 检查之间的联系(例如在 handleCompletedTools 中绑定新的 controller),被抢占的 Goal 轮次就会继续针对已编辑/已替换的 Goal 提交工具续轮,而套件中没有任何用例会失败。建议新增一个 useGeminiStream.test.tsx 用例:绑定一个 Goal 轮次,在 capturedOnComplete 触发前通过 preemptGoalTurn 中止其 controller,并断言 failClosedGoalTurn 的效果(pause/finishTurn dispatch)、'Goal tool continuation was preempted' 错误项,以及不再有 sendMessageStream 调用。

— qwen3.8-max-preview via Qwen Code /review

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.

Declined — writing this test correctly requires interleaving preemptGoalTurn during the async window between bindGoalTurn (which sets activeGoalAdmissionRef) and the controller.signal.aborted check inside handleCompletedTools. The binding is created fresh via bindGoalTurn, so the abort must land between that synchronous call and the subsequent async finalization — a timing-dependent setup that would make the test fragile. The preemptGoalTurn call site is already covered by AppContainer.test.tsx:1433, and the single-line guard is structurally coupled to the binding mechanism. Deferring to a follow-up that can refactor the binding lifecycle to make this path directly testable.

中文说明

已拒绝 — 正确编写此测试需要在 bindGoalTurn(设置 activeGoalAdmissionRef)和 handleCompletedTools 内部的 controller.signal.aborted 检查之间的异步窗口中插入 preemptGoalTurn。绑定通过 bindGoalTurn 新建,因此中止必须落在该同步调用和后续异步终结之间——这是一个依赖时序的设置,会使测试变得脆弱。preemptGoalTurn 的调用点已由 AppContainer.test.tsx:1433 覆盖,且该单行守卫与绑定机制结构耦合。推迟到可以重构绑定生命周期以使此路径可直接测试的后续 PR。

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 12 passed · 0 failed · 12 total

中文 — 判定:✅ 通过 · 可合入(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:12 通过 · 0 失败 · 12 总计

Verification report

<!-- qwen-triage:verify -->
<!-- qwen-triage:verify-substantive -->

Sandboxed verification: ✅ passed — merge-ready (agent verdict)

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 12 passed · 0 failed · 12 total

中文 — 判定:✅ 通过 · 可合入(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:12 通过 · 0 失败 · 12 总计

Verification report

PR #8005 — Deep Verification Report (follow-up round 2)

Verdict: merge-ready — executed scripted assertions: 12 pass / 0 fail / 12 total (6 A/B proofs × {head cell, control cell}; every control cell produced its expected outcome, so fail counts only unexpected outcomes and is zero). Verified head OID: b292a2af915daf8262b36b7516f0d5235f4d5951 (HEAD^2; merge commit f081795d7, base tip HEAD^1 = 2bbd82f14). No blocking finding reproduced. Affected-surface gate green (19 files / 1136 tests: cli 13 / 668 + core 6 / 468); typecheck of the two touched workspaces clean (0 error TS).

中文 — 判定:✅ 通过 · 可合入(agent 判定)

这是第二轮跟进。上一轮(round 1)在 head b292a2af9 上判定 merge-ready、无阻断性发现;本轮的 head 完全相同——HEAD^{tree} = 77110a58… 与上一轮验证过的 head 树哈希逐字节一致,自上一轮以来零新提交。因此本轮没有「新提交带来的增量」可测,唯一的新增价值是把上一轮明确列为「门控覆盖但未独立 A/B」的一处表面——消费端对 response.terminateTurn 的兑现——升级为一条完整的两单元 A/B 证明(新 claim #6)。其余五项承接 claims 全部在新 head 上重新测量(非从旧报告 diff),均 stands

  • 结论merge-ready。脚本化断言 12 通过 / 0 失败 / 12 总计(6 个 A/B × {head 单元, 对照单元},对照单元均出现预期结果,故 fail 为 0);未发现阻断性问题。
  • 承接结论状态:见下方「Previous-finding status table」。五项承接 claims 全部 stands;上一轮 Not covered 中的「消费端 terminateTurn 兑现」本轮已覆盖(新 claim OpenAI API Error: 401 Incorecct API Key provided #6,A/B 证明)。
  • 新增 A/B 结论(本轮唯一增量)OpenAI API Error: 401 Incorecct API Key provided #6 消费端 terminateTurn 兑现useGeminiStream.tstoolCall.response.terminateTurn === trueterminatesGoalTurnruntime.finishTurn(permit) + goal_state 卡片 + 不再发起模型调用):head 绿;把消费端判定取反(== base,base 整段缺失该块,grep -c = 0)后整段终止逻辑被跳过,client.addHistory/finishTurn/goal_state 卡片均不触发,测试变红 expected "spy" to be called with arguments: [{ role: 'user', … }]01-terminate-consumer-ab.png)。这把上一轮「producer 侧传播已证、consumer 侧仅门控覆盖」的链条补成端到端 A/B。
  • 门禁:受影响面 19 文件 / 1136 测试全绿;cli + core typecheck 0 error TS。门禁经变异证明为「活」(六个 A/B 的对照单元各自把对应 oracle 测红)。
  • 未覆盖范围:交互式 tmux E2E 全流程、校验器模型最终决策(非确定性)、非交互/ACP/SDK/Web 表面、逐提交归因(depth-2 浅克隆)、merge 新鲜度(未 trial-merge)。详见 Not covered

数字以下方表格为准,不在散文里重复。

Previous-finding status table

The previous round (round 1, head b292a2af9) reported merge-ready with no blocking findings, so there are no prior findings to re-litigate — only prior verified claims to re-measure. This round's head is byte-identical to round 1's: git rev-parse HEAD^{tree} = 77110a580a4013ec21dc568fc780eca14e7468d7 equals HEAD^2^{tree}, and HEAD^2 = b292a2af9 is exactly the OID round 1 verified — so zero commits landed between rounds and the entire input closure (source, lockfile, config, fixtures) is unchanged. Per the carry-forward rule I nevertheless re-ran every measurement rather than quoting the old numbers, so each row below is a fresh witness at the same tree, not a diff of the old report.

# Previous-round claim Severity Status at this head
1 Two-lane drain gate drainQueue(_, goalTurnActive) holds ordinary input during an active Goal turn (load-bearing) stands — re-measured. Head 40/40; faithful-base predicate flips the same 2 gate tests red (keeps Goal creation queued until an ordinary turn reaches idle, drains only Goal controls while a Goal turn is running).
2 terminateTurn propagated tool-result → host ToolCallResponseInfo via CoreToolScheduler (producer side) (load-bearing) stands — re-measured. Head green; removing the copy line (== base) flips the scheduler test red (expected undefined to be true).
3 Verifier system prompt requires user_input evidence and reaches request.systemInstruction (load-bearing) stands — re-measured. Head green; base prompt constant grep -c user_input = 0 vs head = 1.
4 Resume continuation-budget reset (reduceGoalControl zeroes turnCount on resume) (load-bearing) stands — re-measured. Head green; removing the turnCount: 0 line (== base) flips the reducer test red ("turnCount": 0"turnCount": 50) (02-resume-budget-ab.png).
5 State-aware queue gating / stranding fix (useQueuedSubmissionDrain: only status === 'active' holds input) (load-bearing) stands — re-measured. Head green; if (status === 'active')if (status) (pre-fix) flips the two target tests red (expected "spy" to be called with arguments: ['normal']) (03-state-gating-ab.png).
(round-1 Not covered) consumer-side honouring of response.terminateTurn in the stream loop was: gate-covered only now COVERED — see new claim #6 below. This was the single highest-value gap round 1 named; it is closed this round.

No prior finding regressed (there were none). No prior verified claim flipped to red.

Scope

47-file / +5416 −1169 feature PR wiring the interactive TUI to the Goal v3 runtime. Because the head is unchanged from round 1, scope this round = re-prove the five carried-forward load-bearing claims with fresh witnesses, plus one deepening probe that converts round 1's gate-only consumer-side coverage into a two-cell A/B:

  1. (carried-forward) Two-lane drain gatedrainQueue(includeDeferred, goalTurnActive) in useMessageQueue.ts.
  2. (carried-forward) terminateTurn producer propagation — the copy line in coreToolScheduler.ts:4808.
  3. (carried-forward) Verifier user_input rule — the prompt constant in goal-verifier.ts:37.
  4. (carried-forward) Resume continuation-budget resetreduceGoalControl resume branch in goal-reducer.ts:128-134.
  5. (carried-forward) State-aware queue gatinguseQueuedSubmissionDrain in AppContainer.tsx:421-434.
  6. (NEW deepening) Consumer-side terminateTurn honouringuseGeminiStream.ts:4071-4110 reads toolCall.response.terminateTurn === trueterminatesGoalTurn → on a matching Goal permit calls runtime.finishTurn(permit), records the tool-result history, flushes, emits a terminal goal_state card, and does not issue another model call. Base lacked this block entirely (git show HEAD^1:…/useGeminiStream.ts | grep -c terminatesGoalTurn = 0), so the negated predicate reproduces base.

A/B proofs

Each A/B runs the PR's own test as the head-side oracle, then a single-hunk revert reproducing base / pre-fix behaviour as the control. A red control cell is the expected outcome (it proves the head behaviour is load-bearing and the test non-vacuous) and is encoded as a pass. Rerunnable: harness/ab.sh <claim> (per-claim) and harness/cells.sh <claim> (side-by-side, what the captures ran).

# claim head cell control cell (== base / pre-fix) capture
1 drain gate 40/40 pass expected red, 2 tests (38 passed | 2 failed) (round-1 witness reused in shape; re-run logged logs/drain-gate-*.log)
2 terminate producer 1 pass expected red: expected undefined to be true logs/terminate-prop-*.log
3 verifier user_input 1 pass expected absence: base grep -c user_input = 0, head = 1 logs/verifier-head.log
4 resume budget 1 pass expected red: "turnCount": 0"turnCount": 50 02-resume-budget-ab.png
5 state gating 5 pass (Goal group) expected red, 2 tests: expected "spy" … ['normal'] 03-state-gating-ab.png
6 terminate consumer (NEW) 1 pass expected red: expected "spy" to be called with arguments: [{ role: 'user', … }] (whole termination block skipped) 01-terminate-consumer-ab.png

Every control cell produced its expected outcome, so assertions.json counts the 12 cells as pass with fail: 0. The mutation reds are the expected outcome of the vacuity/load-bearing probe, not unexpected failures.

#6 Consumer-side terminateTurn honouring (NEW — closes round-1 gap)

Oracle: useGeminiStream.test.tsx › "finishes a Goal turn without another model call after update_goal" — feeds a completed update_goal tool call whose response.terminateTurn === true under an active Goal permit, and asserts the stream (a) calls client.addHistory({ role: 'user', parts }), (b) flushes recording, (c) calls runtime.finishTurn(permit), (d) emits a goal_state card with cause: 'complete', and (e) does not call mockSendMessageStream again.

cell build result
HEAD PR head PASS — all five assertions hold (01-terminate-consumer-ab.png, head cell)
CONTROL (== base) head with toolCall.response.terminateTurn === true… === false (reproduces base, which had no consumer block at all) expected redterminatesGoalTurn is false, so the entire if (terminatesGoalTurn && toolGoalBinding) { … } block is skipped: addHistory/finishTurn/the goal_state card never fire, and the test fails at expect(client.addHistory).toHaveBeenCalledWith({ role: 'user', … }) (01-terminate-consumer-ab.png, control cell; logs/terminate-consumer-base.log)

This is the load-bearing consumption half of the terminateTurn chain that round 1 left at "gate-covered only": the producer copy (#2) puts the flag on the wire-internal response, and #6 proves the stream loop actually reads it and terminates the Goal turn. With #2 + #6 both A/B-proven, the producer→consumer path is verified end to end. Restored clean (git status --porcelain packages/ empty).

#4 Resume continuation-budget reset (carried-forward, re-measured)

Oracle: goal-reducer.test.ts › "resets the continuation turn budget when resuming an exhausted goal".

cell build result
HEAD PR head turnCount: 0, status: 'active'PASS
CONTROL (== base) head with the turnCount: 0, line removed from the resume branch expected red: - "turnCount": 0, + "turnCount": 50, (02-resume-budget-ab.png)

#5 State-aware queue gating / stranding fix (carried-forward, re-measured)

Oracle: AppContainer.test.tsx › "holds ordinary input while the Goal is active and drains it once paused" + "treats paused, blocked and usage_limited Goals as drain-eligible".

cell build result
HEAD PR head both PASS (popNextSubmission called with 'priority' while active, 'normal' otherwise)
CONTROL (pre-fix) head with if (status === 'active')if (status) expected red, both: expected "spy" to be called with arguments: [ 'normal' ] (03-state-gating-ab.png)

#1 Two-lane drain gate (carried-forward, re-measured)

Oracle: useMessageQueue.test.ts (40 tests). Faithful base mutation: head predicate (goalTurnActive ? GOAL_COMMAND_RE.test(text) : !isSlashCommand(text)) → verbatim base predicate (!isSlashCommand(text) || (!includeDeferred && GOAL_COMMAND_RE.test(text))).

cell build result
HEAD PR head 40/40 PASS
CONTROL (faithful base) head with the base predicate expected red, 2 tests (38 passed | 2 failed)

#2 terminateTurn producer propagation (carried-forward, re-measured)

Oracle: coreToolScheduler.test.ts › "propagates a tool turn-termination boundary to the host".

cell build result
HEAD PR head PASS
CONTROL (== base) head with the copy line ...(toolResult.terminateTurn ? { terminateTurn: true } : {}) removed expected red: expected undefined to be true

#3 Verifier user_input rule on the wire (carried-forward, re-measured)

Oracle: goal-verifier.test.ts › "uses a tool-free deterministic side query with bounded fields" (asserts request.systemInstruction carries the user_input rule).

cell build result
HEAD PR head PASS
CONTROL (== base) git show HEAD^1:…/goal-verifier.ts expected absence: grep -c user_input = 0 (base) vs 1 (head)

The model's resulting accept/reject decision is non-deterministic and is not asserted; the load-bearing deterministic part — the rule reaching the wire request — is.

Corrections

None to the PR code, and none to round 1's report this time: round 1's vacuity-count correction (3 → 2 for the drain gate) was already correct, and my faithful-base mutation this round reproduces exactly 2 reds, confirming it. The one substantive change versus round 1 is not a correction but an addition: the consumer-side terminateTurn item moves from Not covered to a proven A/B (#6).

Findings

None blocking. No defect reproduced against the PR head. All red cells above are expected control/mutation outcomes that prove the head behaviour, not findings against the PR.

Not covered

  • Interactive tmux E2E full flow (first-message Goal creation, pause/edit/resume, Escape-cancel, process/branch recovery, clear-without-confirmation, verifier-rejection cards): not reproduced in this sandbox. The PR body references a separately posted tmux report; per the verify contract PR text is untrusted and that report was not available to me, so I neither relied on it nor re-ran it.
  • Verifier model decision: non-deterministic; only the prompt-on-the-wire was asserted (如何自定义密钥文件 .env可能与其他文件冲突 #3).
  • Stream-glue fixes still gate-covered only (their tests are green but I did not independently A/B them): the orphan-prompt strip on cancel, the /goal set clear-keyword bypass, goalTerminalErrorRef, and the "clear stale lastReason on edit" fix. (Note: the consumer-side terminateTurn honouring that round 1 listed here is no longer in this list — it is now OpenAI API Error: 401 Incorecct API Key provided #6.)
  • Out-of-scope surfaces (non-interactive CLI, ACP, SDK, Web, WebShell, Desktop): unchanged-by-design per the PR; not exercised.
  • Per-commit attribution: the checkout is depth-2 (shallow, git rev-parse --is-shallow-repository = true); the 23 commits in the metadata are not individually reachable, so I verified the aggregate HEAD^1..HEAD diff only.
  • Merge freshness: I did not trial-merge into current main; the A/B is valid against the merge-ref base actually built (HEAD^1 = 2bbd82f14). Because the head is unchanged from round 1, this carries the same caveat round 1 reported.

Targeted gates

gate scope result
cli affected tests all 13 changed cli test files (AppContainer, goalCommand, GoalPill, HistoryItemDisplay, GoalStatusMessage, slashCommandProcessor, useBranchCommand, useGeminiStream, useMessageQueue, useResumeCommand, goal-runtime, historyUtils, resumeHistoryUtils) 13 files / 668 passed / 0 failed (logs/gate-cli.log)
core affected tests client-goal, coreToolScheduler, goal-reducer, goal-runtime.integration, goal-runtime, goal-verifier 6 files / 468 passed / 0 failed (logs/gate-core.log)
affected surface total 19 files 1136 passed / 0 failed
typecheck npm run typecheck -w packages/cli -w packages/core exit 0, 0 error TS (logs/typecheck.log)

Gate liveness (proven, not assumed): the cli gate is live because the #1 mutation turned 2 useMessageQueue tests red and the #5 mutation turned 2 AppContainer tests red and the #6 mutation turned the useGeminiStream consumer test red; the core gate is live because the #4 mutation turned the reducer test red, the #2 mutation turned the scheduler test red, and #3's base grep-absence distinguishes the prompt. A green-by-no-match artefact is ruled out. (Repo-wide npm run typecheck --workspaces exits 1 only because several unrelated workspaces — mobile-mcp, vscode-ide-companion, channels/*, web-templates — define no typecheck script; the two workspaces this PR touches both typecheck clean.)

Methodology

Environment: CI verify container, refs/pull/8005/merge at depth 2 (HEAD merge f081795d7, HEAD^1 base tip 2bbd82f14, HEAD^2 PR head b292a2af9), npm ci + npm run build pre-run at HEAD; this is follow-up round 2 (previous-report.md present, prior head also b292a2af9 — i.e. identical tree, proven by HEAD^{tree} == HEAD^2^{tree} == 77110a58…). Each A/B drove the real source module under vitest (jsdom for the React hooks via renderHook/act; node for core) — the reducer, the scheduler copy path, the verifier prompt construction, the queue gate, the status→mode mapping, and the stream-loop consumer are real code, never stubbed (the verifier test stubs only the model client at the transport boundary; the stream test stubs the scheduler callback and the Goal runtime at their boundaries but exercises the real terminatesGoalTurn branch). Base/pre-fix controls are single-hunk reverts of the head source, each confirmed confound-free (the queue hook's runtime imports are unchanged; package.json/lockfile untouched; the base reducer/scheduler/verifier/consumer deltas isolated to the reverted line/block; the drain-gate control uses the verbatim base predicate including !includeDeferred &&, per round 1's faithfulness correction). Every control cell produced its expected outcome (red for a behavioural revert, grep-absence for the base prompt), so assertions.json counts the 12 A/B cells as pass with fail: 0. Evidence PNGs were produced by scripts/verify-capture.mjs (the captured side-by-side cell runs' ANSI rendered through @xterm/headless + sharp); a non-zero exit from a captured control run is the intended red witness. Raw per-cell logs are in logs/; the rerunnable A/B scripts are harness/ab.sh and harness/cells.sh. All mutations were restored (git checkout -- packages/) and git status --porcelain packages/ confirmed empty after every claim and after the gates/typecheck.

Evidence images

01-terminate-consumer-ab

02-resume-budget-ab

03-state-gating-ab

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

Evidence images

01-terminate-consumer-ab

02-resume-budget-ab

03-state-gating-ab

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@wenshao

wenshao commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

Local verification report — real model, real interactive TUI

I built this PR in an isolated worktree and drove the built interactive CLI through the Reviewer Test Plan in real tmux sessions against a real provider. Six of the seven listed behaviors reproduce as described. One blocking defect surfaced that is only observable on the real stack: tool results never enter the Goal evidence catalog, so any objective whose completion depends on external state can never be verified and the Goal loop runs until it is cleared or hits the usage limit.

Environment: PR head b292a2af9, macOS arm64 (Darwin 25.6.0), Node 24.18.1, isolated QWEN_HOME, qwen3.8-max-preview over the OpenAI-compatible provider, approvalMode: yolo, tmux 150×45, workspaces are throwaway git repos. Build: npm run build && npm run bundle; the binary under test is the unmodified PR bundle (instrumentation described below was added and reverted separately, then the bundle was rebuilt clean).

Focused unit tests on the changed surface: packages/core goals + client-goal + coreToolScheduler → 632 passed (15 files); packages/cli goalCommand, useMessageQueue, useGeminiStream, goal-runtime, GoalPill, GoalStatusMessage, useResumeCommand, useBranchCommand, historyUtils, resumeHistoryUtils, AppContainer, slashCommandProcessor, HistoryItemDisplay → 668 passed (13 files).

Reviewer Test Plan results

# Behavior Result
1 Goal set as the first interaction shows the lifecycle card and footer without a prior chat message ✅ reproduced
2 Ordinary input queued while the Goal runs stays queued across Goal turns; released at terminal state ✅ reproduced (held across 7+ Goal turns, released on terminal)
3 Insert action on a stop-condition message delivers real user input into the active turn ⚠️ could not reproduce — see Suggestion 3
4 Pause, edit, resume retains the updated objective and revision, loop resumes ✅ reproduced (revision 2 after edit, loop resumed)
5 Escape on an active turn pauses the Goal instead of clearing it ✅ reproduced
6 Continue-mode restart and branch-from-paused recover objective, lifecycle, history, footer ✅ reproduced (minor replay ordering nit, Suggestion 2)
7 Clear shows no confirmation dialog and releases the queued ordinary message ✅ reproduced

Happy path end to end — a conversational objective is proposed, independently verified and completed in one turn (36.9s):

happy path

Lifecycle controls in one frame — Escape → paused, /goal edit while paused, /goal resume back to active at revision 2:

lifecycle

Two-lane queue — an ordinary message stays visibly queued while the Goal drives turn after turn:

queue hold

…and is delivered only once the Goal reaches a terminal state:

queue release

Recovery — relaunch with continue mode restores the paused Goal, and a branch created from that session inherits objective, lifecycle state and footer:

recovery

Blocking: tool results never reach the Goal evidence catalog

Objective under test: create a file named goal-done.txt in the current directory whose only content is the word DONE. The model wrote the correct file 38 seconds in and verified it with read_file and shell commands. The Goal still never completed: it burned 16 Goal turns, 59 API calls and ~3.49M input tokens over 12m29s and ended in usage_limited.

evidence gap

The recorded session shows why. Every assistant record in a Goal turn carries goalContext (48/48) while no tool-result record does (0/45). Because the catalog only admits a record whose goalContext matches the active Goal and revision, the catalog handed back by get_goal contains nothing but delivered_output entries, and the independent verifier correctly refuses to accept assistant prose as proof of filesystem state. Eight distinct rejections all say the same thing, e.g. "All cited evidence has proofKind delivered_output, which only proves assistant output was delivered and cannot prove files, tool results, or external state." The blocked escape hatch is closed for the same reason: an immediate blocker requires cited user input or external tool evidence, and neither can exist.

Root cause, confirmed by temporary instrumentation on the real binary (added, traced, then reverted): the interactive TUI records tool results itself, at the two-argument call site in useGeminiStream.ts (orderedResponses.forEach(... recordToolResult(parts, result)), around line 3963), which never passes goalContext. The Goal-aware recorder in CoreToolScheduler.recordToolResults — which does stamp goalContext and even tags get_goal/update_goal results as goal_runtime — is inert in the TUI because useReactToolScheduler constructs the scheduler without a chatRecordingService. The trace shows the request carrying a valid permit (record write_file rec=NO goalContext={goalId…,revision:1,turnId…}) at exactly the moment the CLI writes the record with optGoal=null. I only tested the interactive TUI; the ACP and non-interactive recording call sites also pass no goalContext, so they would likely need the same treatment when their adoption PRs land.

The recording block itself predates this PR (b7c78721), so this is not a regression in the diff — but this PR is what makes the interactive TUI depend on evidence cataloguing, so the gap becomes user-visible here for the first time. Scope check: a purely conversational objective completes normally (screenshot above), so the runtime wiring is otherwise correct; the failure is specific to objectives that need external_fact evidence, which is most real coding work.

Suggested direction: either give useReactToolScheduler the chatRecordingService and drop the duplicate CLI-side recording, or pass { goalContext: request.goalContext } at the CLI call site. A regression test asserting that a tool result recorded during a Goal turn is catalogued as external_fact would keep this from silently regressing.

Suggestions (non-blocking)

  1. The footer hint is not Goal-aware. While a Goal is active the footer still reads Enter to steer · Ctrl+Q to queue, but with goalControlMode === 'priority' an ordinary message is held regardless of which key is used — pressing Enter does not steer. Worth wording the hint for the Goal case so the queue behavior is not surprising.
  2. Replay ordering is inverted. On continue-mode restart and in a branch, each Goal lifecycle card renders immediately before the /goal … command that produced it, whereas live it renders after. Cosmetic, visible in the recovery screenshot.
  3. No route for real user chat input into an active Goal turn. I could not find the insert affordance the test plan refers to. With a Goal active, typed messages queue and are held; the model's own AskUserQuestion answer arrives as a tool result, which is not catalogued (see above), so a Goal whose objective depends on user input cannot be satisfied by either path. If the affordance exists, could you point at the key binding so I can retest that item?

Recommendation

Everything the PR claims about the TUI lifecycle, the two-lane queue, cancellation, clear and recovery holds up on the real stack. I would hold the merge until the evidence-catalog gap is fixed or explicitly scoped out in writing, because in its current state an interactive Goal over any file- or command-based objective cannot terminate on its own.

中文版

本地验证报告 —— 真实模型、真实交互式 TUI

我在隔离 worktree 里构建了本 PR,并在真实 tmux 会话中用真实模型跑完了 Reviewer Test Plan。七项行为里有六项与描述一致。另外发现一个只有在真实环境才能暴露的阻塞缺陷:工具结果永远不会进入 Goal 证据目录,因此任何依赖外部状态的目标都无法通过验证,Goal 循环会一直跑到被清除或触发用量限制。

环境:PR head b292a2af9,macOS arm64(Darwin 25.6.0),Node 24.18.1,隔离的 QWEN_HOME,OpenAI 兼容协议下的 qwen3.8-max-previewapprovalMode: yolo,tmux 150×45,工作区是一次性 git 仓库。构建 npm run build && npm run bundle;被测二进制是未修改的 PR bundle(下文提到的插桩是单独加的,追踪完即回滚并重新干净构建)。

改动面聚焦单测packages/core goals + client-goal + coreToolScheduler → 632 通过(15 个文件);packages/cli goalCommand、useMessageQueue、useGeminiStream、goal-runtime、GoalPill、GoalStatusMessage、useResumeCommand、useBranchCommand、historyUtils、resumeHistoryUtils、AppContainer、slashCommandProcessor、HistoryItemDisplay → 668 通过(13 个文件)。

Reviewer Test Plan 结果

# 行为 结果
1 首次交互即设置 Goal,无需先聊天就出现生命周期卡片和底部状态 ✅ 复现
2 Goal 运行期间排队的普通消息跨轮保持排队,终态后才释放 ✅ 复现(跨 7+ 个 Goal 轮次保持,终态后释放)
3 对停止条件消息使用插入操作,让真实用户输入进入活动轮次 ⚠️ 未能复现 —— 见建议 3
4 暂停、编辑、恢复后保留更新的目标与修订号,循环继续 ✅ 复现(编辑后 revision 2,循环恢复)
5 Escape 取消活动轮次后 Goal 变为暂停而非被清除 ✅ 复现
6 continue 模式重启与从暂停会话分支都能恢复目标、生命周期、历史与底部状态 ✅ 复现(有一个渲染顺序小问题,见建议 2)
7 清除不弹确认框,并释放排队的普通消息 ✅ 复现

截图依次为:完整成功路径(对话型目标 36.9s 内经独立验证完成)、生命周期控制(Escape → 暂停 → 编辑 → 恢复到 revision 2)、排队消息在 Goal 运行期间被保持、终态后才被交付、以及 continue 重启与 /branch 的恢复效果,见英文部分的图片。

阻塞问题:工具结果从未进入 Goal 证据目录

测试目标:在当前目录创建 goal-done.txt,内容只有 DONE。模型在第 38 秒就正确写出文件,并用 read_file 和 shell 命令做了校验。但 Goal 始终没有完成:一共烧掉 16 个 Goal 轮次、59 次 API 调用、约 349 万输入 token,历时 12 分 29 秒,最后以 usage_limited 收场。

会话记录说明了原因:Goal 轮次里每一条 assistant 记录都带 goalContext(48/48),而工具结果记录一条都没有(0/45)。证据目录只收录 goalContext 与当前 Goal、revision 匹配的记录,于是 get_goal 返回的目录里只有 delivered_output,独立校验器正确地拒绝把助手的自述当作文件系统状态的证据。八次不同的拒绝理由说的是同一件事,例如:"All cited evidence has proofKind delivered_output, which only proves assistant output was delivered and cannot prove files, tool results, or external state." blocked 这条退路同样被堵死:immediate blocker 要求引用用户输入或外部工具证据,而这两者都无法存在。

根因(在真实二进制上临时插桩确认后已回滚):交互式 TUI 自己在 useGeminiStream.ts 里用两参数形式记录工具结果(约 3963 行的 orderedResponses.forEach(... recordToolResult(parts, result))),从不传 goalContext。真正会打 goalContext、还会把 get_goal/update_goal 结果标记为 goal_runtimeCoreToolScheduler.recordToolResults 在 TUI 里是空转的,因为 useReactToolScheduler 构造 scheduler 时没有传入 chatRecordingService。插桩输出显示,在 CLI 以 optGoal=null 写记录的同一时刻,请求上其实带着有效 permit(record write_file rec=NO goalContext={goalId…,revision:1,turnId…})。我只测了交互式 TUI;ACP 与非交互路径的记录调用同样没有传 goalContext,等它们各自的接入 PR 落地时大概率需要同样处理。

这段记录代码本身早于本 PR(b7c78721),所以不是本次 diff 的回归;但正是本 PR 让交互式 TUI 开始依赖证据目录,问题因此第一次对用户可见。范围确认:纯对话型目标可以正常完成(见截图),说明运行时接线本身没问题,失效只发生在需要 external_fact 证据的目标上——而这恰恰是大多数真实编码任务。

建议方向:要么把 chatRecordingService 传给 useReactToolScheduler 并去掉 CLI 侧重复记录,要么在 CLI 调用处补上 { goalContext: request.goalContext }。再加一个回归测试断言「Goal 轮次内记录的工具结果会被编入目录且 proofKind 为 external_fact」,可以避免以后再次悄悄退化。

非阻塞建议

  1. 底部提示没有 Goal 感知:Goal 活跃时底部仍显示 Enter to steer · Ctrl+Q to queue,但在 goalControlMode === 'priority' 下,不管按哪个键普通消息都会被保持,Enter 并不会 steer。建议为 Goal 场景单独措辞。
  2. 回放顺序颠倒:continue 重启和分支会话里,每张 Goal 生命周期卡片都渲染在产生它的 /goal … 命令之前,与实时顺序相反。属于外观问题,可在恢复截图中看到。
  3. 活动 Goal 轮次没有接收真实用户聊天输入的通道:我没有找到测试计划里说的插入操作。Goal 活跃时输入会排队并被保持;模型自己发起的 AskUserQuestion 回答落地为工具结果,而工具结果不会进目录(见上),所以依赖用户输入的目标两条路都走不通。如果该操作确实存在,能否指一下按键,我再补测这一项。

结论

PR 声称的 TUI 生命周期、双通道队列、取消、清除与恢复行为在真实环境下都成立。但建议在证据目录这个缺口修复、或明确写清不在本 PR 范围之前先不要合并,因为按当前状态,交互式 Goal 对任何涉及文件或命令的目标都无法自行终止。

…to tool-result recording (QwenLM#8005)

The budget-exhaustion callback in queueContinuation only checked
goal status, not identity. A replace dispatched during the journal
append could create a fresh goal that the stale callback then
incorrectly usage-limited. Capture goalId/revision at enqueue time
and return early on mismatch, matching the existing identity-guard
pattern used by handleStartFailure and recordVerificationOutcome.

The TUI recorded tool results without goalContext, so the evidence
catalog never admitted tool-result records and any Goal whose
objective depended on external state could not be verified. Pass
request.goalContext at both recordToolResult call sites in
useGeminiStream, tagging get_goal/update_goal results as
goal_runtime to match the CoreToolScheduler pattern.
tanzhenxin
tanzhenxin previously approved these changes Aug 1, 2026

@tanzhenxin tanzhenxin 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!

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 1/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 1/100 轮)。改动内容与我反驳保留之处如下:

Review feedback addressed

[Critical] Budget-exhaustion callback lacks identity guard — FIXED

The budget-exhaustion callback in queueContinuation (goal-runtime.ts) guarded only on snapshot.goal?.status !== 'active' without verifying goal identity. A dispatch({ action: 'replace' }) issued during the journal append chains onto the same dispatchTail, so the order becomes op_finishTurn → op_replace → op_budget. The replace creates a fresh goal (turnCount 0, new goalId) and the stale budget callback then transitions the replacement goal to usage_limited.

Fix: Capture goalId and revision at enqueue time and return early on mismatch, matching the existing identity-guard pattern used by handleStartFailure (isCurrentPermit) and recordVerificationOutcome (isCurrentVerificationAttempt).

Test: Added does not usage-limit a replacement goal created during budget-exhaustion persistence — drives turnCount to the budget cap, blocks during the budget callback's journal append, dispatches a replace, and asserts the replacement goal stays active with no usage_limited record.

[Blocking] Tool results never reach the Goal evidence catalog (@wenshao) — FIXED

The interactive TUI recorded tool results via recordToolResult(parts, result) without passing goalContext, so the evidence catalog never admitted tool-result records. Any Goal whose objective depended on external state (files, commands) could never be verified and ran until the usage limit.

Fix: Pass request.goalContext at both recordToolResult call sites in useGeminiStream.ts (the main tool-response path and the duplicate-response path), tagging get_goal/update_goal results with provenance: 'goal_runtime' to match the CoreToolScheduler.recordToolResults pattern.

Test: Added records tool results with goalContext during a Goal turn — completes a shell tool call with goalContext on the request and asserts recordToolResult receives the permit as the third argument.

[Suggestion] rc:3695190171 — Preemption path test — DECLINED

The suggestion asks for a useGeminiStream.test.tsx case that aborts a goal-turn controller via preemptGoalTurn before capturedOnComplete fires. Writing this test correctly requires interleaving preemptGoalTurn during the async window between bindGoalTurn (which sets activeGoalAdmissionRef) and the controller.signal.aborted check inside handleCompletedTools. The binding is created fresh inside handleCompletedTools via bindGoalTurn, so the abort must land between that synchronous call and the subsequent async finalization — a timing-dependent setup that would make the test fragile. The preemptGoalTurn call site is already covered by AppContainer.test.tsx:1433, and the single-line guard is structurally coupled to the binding mechanism (severing it would break the failClosedGoalTurn path that other tests exercise). Deferring to a follow-up that can refactor the binding lifecycle to make this path directly testable.

@wenshao non-blocking suggestions — NOTED

  1. Footer hint not Goal-aware: Valid UX improvement. The footer reads "Enter to steer · Ctrl+Q to queue" but during a Goal turn both keys queue. Best addressed in a focused follow-up PR.
  2. Replay ordering inverted: Cosmetic issue in continue-mode restart and branch replay. Requires investigation of the history rendering order. Best addressed separately.
  3. No route for real user chat input into an active Goal turn: The insert affordance is the message queue's steer mechanism — when a Goal turn reaches a stop condition, queued messages are delivered. The AskUserQuestion answer path depends on the evidence-catalog fix above. If the insert key binding is not discoverable, that is a documentation/UX gap for a follow-up.

Verification

  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • vitest packages/core goals + coreToolScheduler + client-goal (15 files) — 633 passed
  • vitest packages/cli goal-related surface (13 files) — 669 passed
中文说明

已处理的评审反馈

[Critical] 预算耗尽回调缺少身份守卫 — 已修复

queueContinuation(goal-runtime.ts)中的预算耗尽回调仅检查 snapshot.goal?.status !== 'active',未验证 goal 身份。在 journal 写入期间发出的 dispatch({ action: 'replace' }) 会链入同一个 dispatchTail,顺序变为 op_finishTurn → op_replace → op_budget。replace 创建一个新 goal(turnCount 0、新 goalId),而过期的预算回调随后会将替代 goal 错误地转为 usage_limited

修复: 在入队时捕获 goalIdrevision,在回调中检查不匹配则提前返回,与 handleStartFailureisCurrentPermit)和 recordVerificationOutcomeisCurrentVerificationAttempt)使用的现有身份守卫模式一致。

测试: 新增 does not usage-limit a replacement goal created during budget-exhaustion persistence — 将 turnCount 驱动到预算上限,在预算回调的 journal 写入期间阻塞,发出 replace,并断言替代 goal 保持 active 且无 usage_limited 记录。

[Blocking] 工具结果从未进入 Goal 证据目录(@wenshao)— 已修复

交互式 TUI 通过 recordToolResult(parts, result) 记录工具结果时未传递 goalContext,导致证据目录从未收录工具结果记录。任何依赖外部状态(文件、命令)的 Goal 都无法通过验证,只能运行到用量限制。

修复:useGeminiStream.ts 的两个 recordToolResult 调用点(主工具响应路径和重复响应路径)传递 request.goalContext,并将 get_goal/update_goal 结果标记为 provenance: 'goal_runtime',与 CoreToolScheduler.recordToolResults 的模式一致。

测试: 新增 records tool results with goalContext during a Goal turn — 完成一个带有 goalContext 的 shell 工具调用,并断言 recordToolResult 收到 permit 作为第三个参数。

[Suggestion] rc:3695190171 — 抢占路径测试 — 已拒绝

该建议要求新增一个 useGeminiStream.test.tsx 用例,在 capturedOnComplete 触发前通过 preemptGoalTurn 中止 goal 轮次的 controller。正确编写此测试需要在 bindGoalTurn(设置 activeGoalAdmissionRef)和 handleCompletedTools 内部的 controller.signal.aborted 检查之间的异步窗口中插入 preemptGoalTurn。绑定在 handleCompletedTools 内部通过 bindGoalTurn 新建,因此中止必须落在该同步调用和后续异步终结之间——这是一个依赖时序的设置,会使测试变得脆弱。preemptGoalTurn 的调用点已由 AppContainer.test.tsx:1433 覆盖,且该单行守卫与绑定机制结构耦合(切断它会破坏其他测试所验证的 failClosedGoalTurn 路径)。推迟到可以重构绑定生命周期以使此路径可直接测试的后续 PR。

@wenshao 非阻塞建议 — 已记录

  1. 底部提示没有 Goal 感知: 有效的 UX 改进。Goal 活跃时底部仍显示 "Enter to steer · Ctrl+Q to queue",但两个键都会排队。最好在专门的后续 PR 中处理。
  2. 回放顺序颠倒: continue 模式重启和分支回放中的外观问题。需要调查历史渲染顺序。最好单独处理。
  3. 活动 Goal 轮次没有接收真实用户聊天输入的通道: 插入操作是消息队列的 steer 机制——当 Goal 轮次到达停止条件时,排队的消息会被交付。AskUserQuestion 的回答路径依赖于上述证据目录修复。如果插入快捷键不易发现,那是后续 PR 的文档/UX 缺口。

验证

  • npm run build — 通过
  • npm run typecheck — 通过
  • npm run lint — 通过
  • vitest packages/core goals + coreToolScheduler + client-goal(15 个文件)— 633 通过
  • vitest packages/cli goal 相关表面(13 个文件)— 669 通过

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

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


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

@qwen-code-dev-bot
qwen-code-dev-bot dismissed stale reviews from tanzhenxin and wenshao via e1d6c77 August 1, 2026 09:50
OrbitZore pushed a commit to OrbitZore/qwen-code that referenced this pull request Aug 1, 2026
…oom (QwenLM#8257)

* fix(autofix): budget the whole round, not just the agent step

The primary attempt took run-agent.mjs's 50-minute default while its step
capped at 80, so a third of that step was unreachable and every "ran out
of time" round ended for a reason this file never named. Raising it
surfaced the larger problem: the budget was never checked as a whole.

Measured on one scan (run 30646547838): setup 5-7m in earlier steps, so
it never competes with the agent step; Triage and address 50m03s on QwenLM#8005
round 9 (its own timer) and 12m45s on QwenLM#8211; the Verification gate 22m48s
on QwenLM#8211 — the largest consumer in the job, and unbounded; push, report
and finalize 3-4s. The old arithmetic ("80 + 20 leaves ~50 for setup, two
verification passes, and reporting") predates that measurement: the real
worst case was 7 + 80 + 23 + 20 + 23 = 153 against a 150-minute job, and
a JOB timeout cancels the always() reporters, which is the silent round
the design exists to prevent.

Sized for the PRs that actually exhaust the budget (47 and 35 changed
files): agent 120m under a 130m step cap, each verification gate bounded
at 60m — 2.6x the measured pass, and a graceful degrade because both
gates already carry continue-on-error, so a bound turns a job kill into
the ordinary verification-failure path that reports. Worst case
7 + 130 + 60 + 20 + 60 + 3 = 280 against a 300-minute job, itself under
the 360-minute ceiling ubuntu-latest imposes regardless.

Raising the budget does not make a doomed round cheaper — each exhausted
budget still pushes nothing, which is why TIMEOUT_WINDOW_CAP stops a PR
after three in one window. It buys rounds that were close to finishing.

The test now asserts the SUM against the job cap and the count of bounded
steps, because asserting the numbers individually is exactly what let
153-against-150 pass. Mutations checked: budget over its step cap, either
verification bound removed, job cap back to 150, job cap over the runner
ceiling, and continue-on-error dropped are each caught.

* fix(autofix): bound every long step and cap the timeout override (QwenLM#8257)

* fix(autofix): enforce the timeout ceiling and trim review feedback (QwenLM#8257)

* fix(autofix): force base-10 clamp and align stale bound with job cap (QwenLM#8257)

* fix(autofix): close the clamp int64 escape and pin it with a bash replay (QwenLM#8257)

* fix(autofix): give the timeout clamp a floor, not only a ceiling

The review's Finding 1: the guard clamped only the ceiling, and the
uncovered side is the likelier typo. Every comment in this file, the PR
body and the operator message speak in MINUTES; this one variable wants
MILLISECONDS. A maintainer told to "raise the agent time budget" who sets
QWEN_AUTOFIX_TIMEOUT_MS=120 armed a 120 ms timer — every round SIGTERMs
instantly, writes agent-timeout, and reports "ran out of time (timeout
(120ms))" until TIMEOUT_WINDOW_CAP trips and AutoFix stops on the PR,
advising the human to raise the budget they just raised. No warning
anywhere in that loop, which is the exact misreport the clamp exists to
prevent, reached from the other direction.

A 60000 ms floor rejects every minutes-shaped value, and it also closes
the `0`/`000` hole the review noted alongside it — those passed the bare
regex while the message asserted the value had to be positive. The
message now names the units, because a units confusion is the whole
failure mode.

Replayed the review's own table against the extracted block, stdout and
stderr separated: 7200000, 3600000 and the floor itself pass untouched;
120, 60, 0, 000 and 59999 all clamp with a warning, alongside the
over-cap, malformed, octal and int64 cases the previous round closed.
The test pins both boundaries from each side (59999 clamps, 60001 does
not) and asserts the warning names MILLISECONDS.

---------

Co-authored-by: verify <verify@local>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen Autofix <qwen-autofix@users.noreply.github.com>

@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. Not reviewed: reverse audit — an auditor ran and opened its brief, but no agent was launched with the prompt the CLI built — the launch was written by hand, and what the agent was actually asked is not what this skill certifies.

中文说明

已审查。 未审查:反向审计——有审计 agent 运行并打开了自己的 brief,但没有 agent 是用 CLI 构建的 prompt 启动的——启动 prompt 是手写的,agent 实际被要求做的并不是本 skill 所认证的内容。

— qwen3.8-max-preview via Qwen Code /review

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

The only feedback newer than the last evaluation is a single COMMENTED review from the automated reviewer:

Reviewed. Not reviewed: reverse audit — an auditor ran and opened its brief, but no agent was launched with the prompt the CLI built — the launch was written by hand, and what the agent was actually asked is not what this skill certifies.

This is an informational comment about the /review skill's own reverse-audit certification process (how the auditor agent was launched), not a finding about the code in this PR. It names no defect, requests no change, and references no file, function, or behavior in the diff.

There is nothing to address in code this round:

  • No CHANGES_REQUESTED finding naming a real defect (the review state is COMMENTED).
  • No inline comments to resolve or reply to.
  • No failed checks and no still-red checks.
  • No **[Suggestion]** finding tied to the PR's changes.

The earlier CHANGES_REQUESTED reviews predate the last evaluation and were already handled in prior rounds; the DISMISSED entries cleared the remaining reviews. The working tree is clean and no commit is warranted.

中文说明

Autofix 审查轮次——未采取任何操作

自上次评估以来唯一的新反馈是来自自动审查器的一条 COMMENTED(评论性质)review:

已审查。未审查:反向审计——有审计 agent 运行并打开了自己的 brief,但没有 agent 是用 CLI 构建的 prompt 启动的——启动 prompt 是手写的,agent 实际被要求做的并不是本 skill 所认证的内容。

这是一条关于 /review skill 自身反向审计认证流程(审计 agent 是如何被启动的)的信息性评论,而不是针对本 PR 代码的发现。它没有指出任何缺陷,没有请求任何更改,也没有引用 diff 中的任何文件、函数或行为。

本轮在代码层面没有任何需要处理的内容:

  • 没有指出真实缺陷的 CHANGES_REQUESTED 发现(该 review 的状态为 COMMENTED)。
  • 没有需要解决或回复的行内评论。
  • 没有失败的检查,也没有持续飘红的检查。
  • 没有与本 PR 改动相关联的 **[Suggestion]** 发现。

较早的 CHANGES_REQUESTED review 都早于上次评估,已在之前的轮次中处理完毕;DISMISSED 条目已清除了其余的 review。工作树是干净的,无需提交。

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


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

@wenshao
wenshao dismissed stale reviews from qwen-code-ci-bot and doudouOUC August 1, 2026 11:17

fixed

@wenshao
wenshao enabled auto-merge August 1, 2026 11:17
@ytahdn

ytahdn commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

Review 总结

变更概述

在交互式 TUI 中采用 Goal v3,47 文件,+5629/-1174。涵盖 Goal 命令、GoalPill 组件、GoalStatusMessage、goal-runtime、goal-reducer、goal-verifier、useGeminiStream 集成、useMessageQueue 集成、resume/history 适配、i18n 等。

代码质量评估

  • 变更范围大但结构清晰:core 层(goal-reducer/runtime/verifier/tools)+ CLI UI 层(命令/组件/hooks)+ i18n 分层明确
  • 测试覆盖充分:goalCommand、GoalPill、GoalStatusMessage、goal-runtime、goal-reducer、goal-verifier、useGeminiStream、useMessageQueue、resumeHistory 均有对应测试
  • 与现有 TUI 架构(Ink 组件、slash command、message queue)集成方式一致

结论

无 Critical 缺陷,可以合并。👍

@ytahdn ytahdn 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. Goal v3 TUI 集成结构清晰,core/UI 分层明确,测试覆盖充分。

— qwen3.7-plus via Qwen Code /review

@wenshao
wenshao added this pull request to the merge queue Aug 1, 2026
Merged via the queue into QwenLM:main with commit 4aa7d1e Aug 1, 2026
60 checks passed

@tanzhenxin tanzhenxin 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!

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.21.3.

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.

8 participants