Skip to content

feat(goal): redesign goal lifecycle across clients - #7494

Closed
qqqys wants to merge 6 commits into
QwenLM:mainfrom
qqqys:feat/goal-protocol-v3
Closed

feat(goal): redesign goal lifecycle across clients#7494
qqqys wants to merge 6 commits into
QwenLM:mainfrom
qqqys:feat/goal-protocol-v3

Conversation

@qqqys

@qqqys qqqys commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

This replaces the legacy hook-driven Goal loop with a durable, versioned lifecycle shared by the TUI, headless CLI, ACP/daemon, WebShell, and Desktop. Goals now have explicit create, replace, edit, pause, resume, clear, complete, and blocked transitions; exact turn ownership; transcript-backed evidence; independent completion verification; and authoritative replay after restart.

The user experience is aligned across clients without adding token or fixed-turn budgets. Ordinary messages remain queued while a Goal owns the model, explicit insertion is admitted at a turn boundary, terminal Goal updates end the current model turn without a trailing reply, and clearing a Goal is immediate. WebShell and Desktop receive compact composer controls and Goal management views while the TUI retains its existing visual language.

Why it's needed

The previous Goal implementation inferred lifecycle state from display cards and Stop hooks. That made completion, resume, queued-message insertion, persistence, and cross-client rendering race-prone: a completed Goal could leave an unsent queued message, insertion could interrupt the Goal unexpectedly, a turn could continue after terminal completion, and UI state could diverge from the durable transcript.

This design gives every client the same authoritative state and concurrency contract. It separates ordinary queued prompts from explicit insertion, preserves exact delivered-output evidence, fails closed when persistence or turn ownership is lost, and removes the historical 50-turn behavior from the Goal lifecycle.

Reviewer Test Plan

How to verify

  1. Start a fresh session with /goal Reply test until I type qqq. Confirm the Goal begins immediately, repeats only test, and remains active without a token or fixed-turn budget.
  2. While a Goal turn is running, submit an ordinary message. Confirm it stays queued. Use Insert and confirm it is admitted at a safe turn boundary without silently clearing or terminating the Goal.
  3. Type qqq. Confirm the user input is durably recorded, completion is independently verified, the Goal becomes complete, and no extra Goal response or model turn appears after the terminal update.
  4. Exercise edit, pause, resume, and clear from TUI commands and the WebShell/Desktop controls. Confirm clear is immediate and a Goal can be created before an ordinary chat message in a new WebShell session.
  5. Restart or reload the session. Confirm the latest authoritative Goal snapshot and active-time display are restored from the transcript.

Evidence (Before & After)

Before: Goal completion could be followed by an extra model turn, queued insertion could be rejected or interrupt the loop, and legacy display-card replay could revive stale state. After: independent CLI E2E session c2bee17f-b724-4904-af84-d756a0d590b8 completed Goal 852ef05e-ea4d-472d-bbe2-a8e68c333a6d in four exact outputs (J, X, R, F), the verifier accepted the completion, and no fifth Goal turn or trailing reply was emitted. Focused verification passed 293 Core tests, 778 CLI tests (1 skipped), 268 WebShell tests, 451 SDK tests, and 33 Desktop tests; the full build and workspace typecheck also passed.

Tested on

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

Environment (optional)

macOS arm64, Node.js 25.9.0 for repository validation, local bundled CLI for the independent Goal E2E, no sandbox.

Risk & Scope

  • Main risk or tradeoff: This is a cross-client lifecycle change that touches transcript persistence, model-turn admission, daemon routing, and queue ordering; correctness depends on every consumer honoring the exact Goal ID and revision.
  • Not validated / out of scope: Windows and Linux interactive UX were not exercised locally. Token budgeting and fixed Goal turn limits are intentionally out of scope.
  • Breaking changes / migration notes: Legacy Goal display records remain readable for projection, but new state is persisted as the authoritative versioned Goal snapshot. Legacy hook-based runtime state is replaced rather than continued.

Linked Issues

N/A

中文说明

这个 PR 做了什么

本 PR 将旧的、由 Hook 驱动的 Goal 循环替换为一个持久化、带版本的生命周期,并在 TUI、无头 CLI、ACP/daemon、WebShell 和 Desktop 之间共享。Goal 现在具有明确的创建、替换、编辑、暂停、恢复、清除、完成和阻塞状态转换,同时具备精确的轮次所有权、基于会话记录的证据、独立完成验证,以及重启后的权威状态回放。

各客户端的用户体验已完成对齐,但没有新增 token 预算或固定轮数预算。Goal 占用模型时,普通消息会继续保持排队;只有显式点击插入后,消息才会在安全的轮次边界进入;终态 Goal 更新会结束当前模型轮次,不再产生额外回复;清除 Goal 会立即执行。WebShell 和 Desktop 增加了紧凑的输入框控制条与 Goal 管理界面,TUI 则保留原有视觉风格。

为什么需要它

旧 Goal 实现通过展示卡片和 Stop Hook 推断生命周期状态。这使完成、恢复、排队消息插入、持久化和跨客户端渲染容易发生竞态:Goal 完成后可能留下未发送的排队消息,插入可能意外中断 Goal,终态完成后仍可能继续一轮,UI 状态也可能与持久化会话记录不一致。

新设计为所有客户端提供同一个权威状态与并发契约。它把普通排队消息与显式插入分离,保留精确的已交付输出证据,在持久化或轮次所有权丢失时关闭失败,并从 Goal 生命周期中移除了历史上的 50 轮行为。

Reviewer 测试计划

如何验证

  1. 在新会话中输入 /goal Reply test until I type qqq。确认 Goal 立即开始,只重复输出 test,并且保持进行中,不展示 token 或固定轮数预算。
  2. Goal 正在执行时发送一条普通消息。确认消息持续保持排队。点击“插入”,确认消息只在安全轮次边界进入,并且不会静默清除或终止 Goal。
  3. 输入 qqq。确认用户输入被持久化记录,完成状态经过独立验证,Goal 进入完成态,并且终态更新后不再出现额外 Goal 回复或模型轮次。
  4. 分别通过 TUI 命令和 WebShell/Desktop 控件验证编辑、暂停、恢复和清除。确认清除立即执行,并且 WebShell 新会话无需先发送普通聊天消息即可创建 Goal。
  5. 重启或重新加载会话。确认最新权威 Goal 快照和活跃时间显示都能从会话记录恢复。

证据(修改前与修改后)

修改前:Goal 完成后可能继续额外模型轮次,排队消息插入可能被拒绝或中断循环,旧展示卡片回放还可能恢复过期状态。修改后:独立 CLI 端到端会话 c2bee17f-b724-4904-af84-d756a0d590b8 中,Goal 852ef05e-ea4d-472d-bbe2-a8e68c333a6d 用四次精确输出(JXRF)完成,验证器接受完成结果,并且没有出现第五轮 Goal 或尾随回复。定向验证通过 293 个 Core 测试、778 个 CLI 测试(1 个跳过)、268 个 WebShell 测试、451 个 SDK 测试和 33 个 Desktop 测试;完整构建与全工作区类型检查也均通过。

已测试平台

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

环境(可选)

macOS arm64;仓库验证使用 Node.js 25.9.0;独立 Goal 端到端验证使用本地打包 CLI;未使用 sandbox。

风险与范围

  • 主要风险或权衡:这是一个跨客户端生命周期改动,涉及会话记录持久化、模型轮次准入、daemon 路由和消息队列顺序;正确性依赖所有消费者遵守精确的 Goal ID 与 revision。
  • 未验证或不在范围内:未在本地验证 Windows 和 Linux 的交互体验。Token 预算和固定 Goal 轮数限制明确不在本次范围内。
  • 破坏性变更或迁移说明:旧 Goal 展示记录仍可用于兼容投影,但新状态会以带版本的权威 Goal 快照持久化。旧的 Hook 运行时状态会被替换,不再延续。

关联 Issue

@qqqys

qqqys commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author

E2E verification report

Validated on macOS arm64 with a freshly bundled local CLI and a fresh session.

  • Session: c2bee17f-b724-4904-af84-d756a0d590b8
  • Goal: 852ef05e-ea4d-472d-bbe2-a8e68c333a6d
  • Scenario: reply with the next character of JXRF, then complete after the fourth delivered character
  • Observed delivered outputs: JXRF
  • Observed lifecycle: 4 Goal turns, completion proposal independently accepted, terminal state persisted
  • Regression checks: no fifth Goal turn, no assistant tail after terminal update_goal, and exact delivered-output evidence remained available to the verifier

Automated verification:

  • npm run build — passed
  • npm run typecheck — passed
  • Core Goal/persistence suite — 293 passed
  • CLI Goal/queue/ACP/headless suite — 778 passed, 1 skipped
  • WebShell Goal/queue/session suite — 268 passed
  • SDK daemon suite — 451 passed
  • Desktop Goal suite — 33 passed

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

🖼️ web-shell visual preview

Rendered against a mock daemon (no real backend): the PR base vs this PR head f2a6f9d. Only screenshots that changed are shown (flows below, if any, are head-only) — refreshes on every push.

Screenshots · before / after

extensions-manager-dark before/after

extensions-manager-light before/after

mermaid-diagram-dark before/after

mermaid-diagram-light before/after

model-dialog-dark before/after

model-dialog-light before/after

permission-panel-dark before/after

permission-panel-light before/after

session-transcript-dark before/after

session-transcript-light before/after

sidebar-attention-dark before/after

sidebar-attention-light before/after

slash-menu-dark before/after

slash-menu-light before/after

split-view-dark before/after

split-view-light before/after

split-view-maximized-dark before/after

split-view-maximized-light before/after

theme-dialog-dark before/after

theme-dialog-light before/after

Full-resolution recordings (.webm) are attached to the workflow run.

Qwen Code · web-shell visuals

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

Architecture Review — PR #7494

Verdict: ✅ Approve with deep interest — textbook event-sourced state machine replacing inference-based lifecycle.

Architecture overview

The new Goal system follows a clean layered architecture (27 files in packages/core/src/goals/):

goal-protocol.ts    → State machine definition (transitions, events, states)
goal-reducer.ts     → Pure state reducer (deterministic transitions)
goal-runtime.ts     → Runtime orchestration (turn ownership, admission)
goal-persistence.ts → Transcript-backed persistence (replay after restart)
goal-wire.ts        → Cross-client wire format (TUI/headless/ACP/WebShell/Desktop)
goal-evidence.ts    → Delivered-output evidence tracking
goal-verifier.ts    → Independent completion verification
goal-turn-context.ts → Exact turn ownership
activeGoalStore.ts  → In-memory active goal state
goal-tools.ts       → Model-facing tools
goalHook.ts         → Hook integration
goalJudge.ts        → Completion judgment
goal-cutover.ts     → Legacy → new migration
goal-legacy-projection.ts → Backward compatibility

Key design patterns

1. Explicit state machine > inferred state

Before: lifecycle state was inferred from display cards and Stop hooks — race-prone, divergent across clients.
After: explicit transitions (create, replace, edit, pause, resume, clear, complete, blocked) with exact turn ownership. Every client sees the same authoritative state.

2. Transcript-backed persistence (event sourcing)

Goals are persisted in the transcript, not in ephemeral state. After restart, the authoritative snapshot is restored from the transcript — not from UI state or hook memory. This is the event sourcing pattern: the transcript IS the source of truth.

3. Independent completion verification

goal-verifier.ts verifies completion independently of the model's self-report. The model says "done" → the verifier checks against the Goal's criteria → only then does the state transition to complete. This prevents premature completion.

4. Fail-closed on ownership loss

When persistence or turn ownership is lost, the Goal fails closed (stops) rather than continuing in an ambiguous state. This is consistent with the project's broader fail-closed philosophy (Vision Bridge, Epoch Token).

5. Cross-client consistency via wire format

goal-wire.ts defines the serialization format shared by all five clients. The reducer is pure and client-agnostic; each client renders the same state differently but never diverges.

6. Intentional non-goals

No token budgets, no fixed turn limits. The lifecycle is about state management, not resource limiting. This is explicitly documented in the PR description — good scope discipline.

Observations

  1. Legacy migration path: goal-cutover.ts + goal-legacy-projection.ts handle backward compatibility with legacy Goal display records. Legacy records remain readable but new Goals use the durable lifecycle.

  2. Queue semantics: ordinary messages stay queued while a Goal owns the model; explicit insertion is admitted at a turn boundary. This separates "interrupt the Goal" from "add to the conversation."

  3. Test coverage: 293 Core + 778 CLI + 268 WebShell + 451 SDK + 33 Desktop tests. Integration tests verify the full lifecycle including restart replay.

  4. Scale: +31,697 additions across 218 files is large, but the architecture is clean — each file has a single responsibility and the layer boundaries are well-defined.

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

Built the PR base vs this PR head f2a6f9d, drove a fixed endpoint set against each, and diffed the JSON responses. Only fields that changed are shown.

capabilities

field PR base (before) this PR (after)
features[] "session_goal_control"

Qwen Code · serve A/B

@qqqys

qqqys commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author

TUI + WebShell complete E2E validation

Validated commit: d873892784de630cba0d1cee696a2c0f340b4770

Result

All tested Goal lifecycle and queue interactions passed. No additional source-code fix was required during this pass.

TUI interaction chain

  1. Created a Goal with /goal and observed the first exact-format reply.
  2. Confirmed automatic continuation across turn boundaries without a manual continue command.
  3. Edited the active Goal and confirmed the next turn used revision 2.
  4. Supplied the stop-condition input, observed get_goal and update_goal, and confirmed independent verifier acceptance.
  5. Confirmed the terminal transition stopped the continuation loop with no fifth turn or tail reply.
  6. Re-ran an independent deterministic chain (J → X → R → F) in session c2bee17f-b724-4904-af84-d756a0d590b8; it completed in exactly 4 turns.

TUI — create, automatic continuation, stop condition, verifier completion:

TUI create-run-complete

TUI — edit revision, new output, stop condition, terminal completion:

TUI edit-and-complete

WebShell interaction chain

Fresh session: b32bc2d1-3474-437e-9bfe-a3bdfd850ae1

  1. Entered /goal 回复 WEB_E2E,直到我输入 WEB_DONE on the welcome screen before any normal chat; the session was created and the Goal started successfully.
  2. Sent WEB_QUEUE_ONLY while the Goal was running; it stayed in the queue and was not delivered automatically.
  3. Clicked Insert; the queued message entered the conversation and the Goal continued running instead of being aborted.
  4. Paused at a turn boundary; the UI became Paused / Waiting and stopped scheduling new Goal turns.
  5. Edited the paused objective to WEB_E2E_EDITED, then resumed; the resumed turn used the new objective.
  6. Sent WEB_DONE; it stayed queued until Insert was clicked, then the model proposed completion and the verifier accepted it.
  7. Confirmed terminal state had no extra automatic reply after verification.
  8. Ran /goal clear; the Goal was cleared immediately with no confirmation dialog.

WebShell — Goal created directly from the welcome screen and active strip shown:

WebShell goal active

WebShell — ordinary message remains queued while the Goal continues:

WebShell queue held

WebShell — explicit insertion is acknowledged and does not terminate the Goal:

WebShell insert continues goal

WebShell — pause waits at the turn boundary:

WebShell paused

WebShell — edited Goal resumes with the revised objective:

WebShell resumed edited

WebShell — inserted stop condition is verified and the Goal reaches terminal state:

WebShell completed

WebShell — /goal clear completes without a confirmation dialog:

WebShell cleared

Automated regression evidence

  • Core focused suites: 293 passed.
  • CLI focused suites: 778 passed, 1 skipped.
  • WebShell suite: 268 passed.
  • SDK suite: 451 passed.
  • Desktop Goal suite: 33 passed.
  • npm run build: passed.
  • npm run typecheck: passed.
  • PR checks at report time: Web-shell Visuals and Serve A/B passed; Ubuntu Node 22 test still running; platform-specific jobs classified as skipped by CI.

Timing observation

The client remained in the running state and scheduled the next turn automatically. Visible gaps between replies were model/request latency while the processing indicator remained active, not a stopped Goal loop requiring user interaction.

@qqqys
qqqys marked this pull request as ready for review July 22, 2026 09:34
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR — this is a substantial redesign of the Goal lifecycle.

Template looks good ✓

Problem: The PR describes concrete race conditions in the existing hook-driven Goal implementation — completed Goals leaving unsent queued messages, insertion interrupting the loop, turns continuing after terminal completion, UI state diverging from the transcript. These are plausible failure modes for a hook-inferred lifecycle. That said, there are no linked issues or user reports demonstrating these bugs in practice. For a feat redesign this is less critical than for a fix, but it would strengthen the case to link even one reproduction of the races being solved.

Direction: Aligned. Claude Code ships /goal with a completion-condition model (confirmed in CHANGELOG — "Added /goal command: set a completion condition and Claude keeps working across turns until it's met"). Moving from hook-inferred state to an explicit, versioned lifecycle is a reasonable architectural direction.

Size: This is a very large PR — 10,952 production lines across 137 files (of which 1,590 core production lines in packages/core/src/**), plus 20,729 test lines across 81 files, totaling 218 changed files spanning 6 packages (core, cli, acp-bridge, sdk-typescript, web-shell, desktop). This triggers maintainer awareness for core-path scale (500+ core production lines) and the large-PR advisory (1,000+ total production lines). Consider whether the WebShell/Desktop UI additions and the SDK/ACP protocol changes could land as follow-up PRs once the core lifecycle is reviewed and merged — that would make each piece far easier to review and revert.

Approach: The core idea — replacing hook-inferred lifecycle with explicit create/replace/edit/pause/resume/clear/complete/blocked transitions, turn ownership, and transcript-backed evidence — is sound. The scope, however, is enormous for a single PR: it simultaneously rewrites the core Goal engine, updates the TUI, adds WebShell composer controls and Goal management views, adds Desktop Goal UI, extends the SDK, and modifies the ACP bridge. Have you considered splitting this into (1) core lifecycle + TUI, (2) WebShell/Desktop UI, (3) SDK/ACP protocol? Each would be independently reviewable and revertable.

Flagging for maintainer awareness given the core-path scale and cross-package breadth. Moving on to code review. 🔍

中文说明

感谢贡献!这是一次对 Goal 生命周期的大规模重新设计。

模板完整 ✓

问题: PR 描述了现有 Hook 驱动 Goal 实现中的具体竞态条件——Goal 完成后留下未发送的排队消息、插入意外中断循环、终态完成后继续模型轮次、UI 状态与持久化记录不一致。这些对于 Hook 推断的生命周期来说是合理的故障模式。不过,没有关联的 issue 或用户报告来证明这些 bug 实际发生过。对于 feat 类型的重新设计,这不如 fix 类型那么关键,但关联至少一个竞态复现会增强说服力。

方向: 对齐。Claude Code 已发布 /goal 命令,采用完成条件模型(CHANGELOG 确认——"Added /goal command: set a completion condition and Claude keeps working across turns until it's met")。从 Hook 推断状态转向显式、带版本的生命周期是合理的架构方向。

规模: 这是一个非常大的 PR——10,952 行生产代码,涉及 137 个文件(其中 1,590 行核心生产代码packages/core/src/**),加上 20,729 行测试代码(81 个文件),共 218 个变更文件,横跨 6 个包(corecliacp-bridgesdk-typescriptweb-shelldesktop)。这触发了核心路径规模的维护者关注(500+ 核心生产行)和大 PR 建议(1,000+ 总生产行)。建议考虑将 WebShell/Desktop UI 和 SDK/ACP 协议变更作为后续 PR,在核心生命周期审查合并后再提交——这样每个部分都更容易审查和回滚。

方案: 核心思路——用显式的 create/replace/edit/pause/resume/clear/complete/blocked 状态转换、轮次所有权和基于记录的证据替代 Hook 推断的生命周期——是合理的。但范围对于单个 PR 来说过大:同时重写了核心 Goal 引擎、更新了 TUI、新增了 WebShell 输入框控制和 Goal 管理界面、新增了 Desktop Goal UI、扩展了 SDK、修改了 ACP bridge。是否考虑过拆分为 (1) 核心生命周期 + TUI,(2) WebShell/Desktop UI,(3) SDK/ACP 协议?每部分都可以独立审查和回滚。

因核心路径规模和跨包广度,标记维护者关注。进入代码审查 🔍

Qwen Code · qwen3.8-max-preview

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal vs. PR approach: I would have built the same core — an explicit Goal state machine with versioned snapshots, turn permits, and transcript-backed evidence in packages/core/src/goals/. Where I'd differ is scope: I'd land the core engine + TUI first and defer WebShell/Desktop UI, SDK, and ACP bridge to follow-ups. The PR does everything at once, which makes it harder to review and revert.

Architecture is sound. The new goal-protocol.tsgoal-reducer.tsgoal-runtime.ts stack replaces the old hook-inferred lifecycle (goalHook.ts, goalJudge.ts, activeGoalStore.ts — all deleted) with typed transitions, optimistic-concurrency version checks (GoalConflictError), and fail-closed persistence. The independent verifier (goal-verifier.ts) uses a structured JSON schema and explicitly treats evidence as untrusted data — good prompt-injection defense. The coreToolScheduler.ts changes are mostly mechanical: wrapping tool invocations in runInRequestGoalContext for AsyncLocalStorage propagation.

No critical blockers found. The code follows project conventions (ESM, strict TS, kebab-case, collocated tests). The 165 new Goal tests all pass. Build succeeds.

One observation: sameActiveGoalProjection in client.ts uses JSON.stringify for comparison. This works because the objects are constructed deterministically in the same function, but it's worth noting as a fragility point if the shape ever changes.

Files changed (30 of 218 shown)
File What changed
packages/core/src/goals/goal-protocol.ts New: GoalSnapshotV2, GoalTurnPermit, GoalControlRequest types
packages/core/src/goals/goal-reducer.ts New: pure state transitions with version checks and validation
packages/core/src/goals/goal-runtime.ts New: runtime engine — turn ownership, persistence, verification
packages/core/src/goals/goal-verifier.ts New: independent completion verifier via side query
packages/core/src/goals/goal-evidence.ts New: transcript-backed evidence catalog and validation
packages/core/src/goals/goal-tools.ts New: get_goal and update_goal tool definitions
packages/core/src/goals/goal-persistence.ts New: snapshot recovery from transcript records
packages/core/src/goals/goalHook.ts Deleted: old hook-driven Goal loop
packages/core/src/goals/goalJudge.ts Deleted: old LLM-based Goal judge
packages/core/src/goals/activeGoalStore.ts Deleted: old display-card Goal store
packages/core/src/core/client.ts Goal permit handling in sendMessageStream, GoalState events
packages/core/src/core/coreToolScheduler.ts Wrap tool calls in goalTurnContext for permit propagation
packages/core/src/config/config.ts Goal runtime init/dispose, remove old hook Goal logic
packages/cli/src/nonInteractiveCli.ts Headless Goal turn host and queued turn processing
packages/cli/src/ui/hooks/useGeminiStream.ts TUI Goal state event handling and turn management
packages/cli/src/ui/commands/goalCommand.ts Rewrite /goal slash command for new lifecycle
packages/cli/src/ui/components/GoalPill.tsx Updated Goal status display for V2 snapshots
packages/cli/src/acp-integration/session/Session.ts ACP session Goal integration
packages/web-shell/client/hooks/useGoal.ts New: WebShell Goal management hook
packages/web-shell/client/components/dialogs/GoalDialog.tsx New: WebShell Goal dialog
packages/desktop/.../components/app-shell/input/GoalBar.tsx New: Desktop Goal control bar
packages/sdk-typescript/src/daemon/goal-client.ts New: SDK Goal client
packages/acp-bridge/src/bridge.ts Goal state forwarding across ACP bridge
packages/core/src/services/chatRecordingService.ts Goal state record persistence
packages/core/src/tools/tool-names.ts Added get_goal, update_goal tool names
packages/cli/src/serve/routes/goals.ts Daemon HTTP routes for Goal control
packages/cli/src/ui/hooks/useMessageQueue.ts Queue ordering for Goal-owned turns
packages/core/src/goals/goal-runtime.test.ts 69 tests for the runtime engine
packages/core/src/goals/goal-evidence.test.ts 19 tests for evidence validation
packages/core/src/goals/goal-tools.test.ts 19 tests for Goal tools
…and 188 more files

Real-Scenario Testing

tmux is not available on this CI runner, so I used headless mode with --output-format stream-json to capture the full Goal lifecycle. The output below shows the complete flow.

Before (installed qwen 0.20.0)

$ qwen -p "/goal Reply with just the word test" --output-format stream-json

# Old hook-driven behavior — single turn, no lifecycle events:
{"type":"assistant",...,"content":[{"type":"text","text":"Acknowledged — replying with just the word \"test\" to satisfy the stop condition.\n\ntest"}],...}
{"type":"result","subtype":"success",...,"num_turns":1,"result":"Acknowledged — replying with just the word \"test\" to satisfy the stop condition.\n\ntest",...}

After (this PR via npm run dev)

$ npm run dev -- -p "/goal Reply with just the word test" --output-format stream-json

# 1. Goal created — explicit lifecycle event:
{"type":"stream_event",...,"event":{"type":"goal_state","goal_state":{"v":2,"goal":{"goalId":"e5407bcf-...","revision":1,"objective":"Reply with just the word test","status":"active",...},"activity":"idle"}}}

# 2. Activity transitions to running:
{"type":"stream_event",...,"event":{"type":"goal_state","goal_state":{...,"activity":"running"}}}

# 3. Model calls get_goal to read objective + evidence catalog:
{"type":"assistant",...,"content":[{"type":"tool_use","name":"get_goal","input":{}}],...}

# 4. Model delivers the required content:
{"type":"assistant",...,"content":[{"type":"text","text":"test\n\n"}],...}

# 5. Model calls get_goal again to capture delivered_output evidence UUID:
{"type":"assistant",...,"content":[{"type":"tool_use","name":"get_goal","input":{}}],...}

# 6. Model proposes completion with evidence refs:
{"type":"assistant",...,"content":[{"type":"tool_use","name":"update_goal","input":{"evidenceRefs":["1ef07457-..."],"reason":"Replied with just the word \"test\" as requested by the objective.","status":"complete"}}],...}

# 7. Proposal queued for independent verification:
{"type":"user",...,"content":[{"type":"tool_result",...,"content":"Proposal queued for independent verification at the turn boundary; no terminal lifecycle change was committed."}]}

Unit tests (core Goal module)

$ cd packages/core && npx vitest run src/goals/

 ✓ src/goals/goal-legacy-projection.test.ts (12 tests) 5ms
 ✓ src/goals/goal-evidence.test.ts (19 tests) 10ms
 ✓ src/goals/goal-persistence.test.ts (11 tests) 8ms
 ✓ src/goals/goal-reducer.test.ts (15 tests) 12ms
 ✓ src/goals/goal-cutover.test.ts (3 tests) 5ms
 ✓ src/goals/goal-runtime.integration.test.ts (2 tests) 16ms
 ✓ src/goals/goal-verifier.test.ts (15 tests) 20ms
 ✓ src/goals/goal-runtime.test.ts (69 tests) 621ms
 ✓ src/goals/goal-tools.test.ts (19 tests) 88ms

 Test Files  9 passed (9)
      Tests  165 passed (165)

Build

$ npm run build
✓ built in 11.24s   # desktop
✓ built in 3.08s    # web-shell
中文说明

代码审查

独立方案 vs. PR 方案: 我会构建相同的核心——在 packages/core/src/goals/ 中实现显式的 Goal 状态机,带版本化快照、轮次许可和基于记录的证据。不同之处在于范围:我会先落地核心引擎 + TUI,将 WebShell/Desktop UI、SDK 和 ACP bridge 推迟到后续 PR。这个 PR 一次性做了所有事情,增加了审查和回滚的难度。

架构合理。 新的 goal-protocol.tsgoal-reducer.tsgoal-runtime.ts 栈用类型化转换、乐观并发版本检查(GoalConflictError)和关闭失败的持久化替代了旧的 Hook 推断生命周期。独立验证器使用结构化 JSON schema 并明确将证据视为不可信数据——良好的提示注入防御。coreToolScheduler.ts 的改动主要是机械性的:用 runInRequestGoalContext 包装工具调用以传播 AsyncLocalStorage 上下文。

未发现关键阻塞问题。 代码遵循项目约定。165 个新 Goal 测试全部通过。构建成功。

一个观察: client.ts 中的 sameActiveGoalProjection 使用 JSON.stringify 进行比较。因为对象在同一函数中确定性构造,所以可行,但如果形状变化则脆弱。

真实场景测试

此 CI 环境无 tmux,使用无头模式 --output-format stream-json 捕获完整 Goal 生命周期。

修改前(已安装 qwen 0.20.0): 旧的 Hook 驱动行为——单轮,无生命周期事件。模型直接确认并回复。

修改后(本 PR): 显式生命周期——Goal 创建 → 活动状态转换 → get_goal 读取目标 → 交付内容 → 获取证据 UUID → update_goal 提出完成 → 独立验证排队。

单元测试: 9 个测试文件,165 个测试全部通过。

构建: 成功。

Qwen Code · qwen3.8-max-preview

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 3/5 — clean review and the architecture is genuinely better than what it replaces, but 1,590 core production lines across 6 packages from a fork needs a maintainer's sign-off.

The core redesign is the right call. Replacing hook-inferred Goal state with an explicit, versioned state machine (goal-protocolgoal-reducergoal-runtime) eliminates an entire class of race conditions by construction rather than by convention. The independent verifier with structured schema and untrusted-evidence handling is well thought out. The 165 tests are thorough, the build passes, and the headless E2E shows the full lifecycle working end-to-end: create → turn permit → get_goal → deliver content → evidence capture → update_goal → verification queue.

My reservation is purely about scope, not quality. This PR simultaneously rewrites the Goal engine, updates the TUI, adds WebShell and Desktop Goal UI, extends the SDK, and modifies the ACP bridge — 218 files, ~11K production lines. Each of those client integrations could land as a follow-up once the core lifecycle is reviewed and stable. Splitting would make each piece independently reviewable and revertable, and would let the core engine get battle-tested before the UI layers build on it.

Deferring to the maintainer for the scope call. The code itself is ready; the question is whether it should land as one PR or several.

中文说明

信心:3/5 — 审查干净,架构确实比被替代的方案更好,但来自 fork 的 1,590 行核心生产代码横跨 6 个包,需要维护者签字。

核心重新设计是正确的。用显式、带版本的状态机替代 Hook 推断的 Goal 状态,从构造上而非约定上消除了整类竞态条件。独立验证器使用结构化 schema 和不可信证据处理,设计周到。165 个测试全面,构建通过,无头端到端测试展示了完整生命周期:创建 → 轮次许可 → get_goal → 交付内容 → 证据捕获 → update_goal → 验证排队。

我的保留意见纯粹关于范围,而非质量。这个 PR 同时重写了 Goal 引擎、更新了 TUI、新增了 WebShell 和 Desktop Goal UI、扩展了 SDK、修改了 ACP bridge——218 个文件,约 11K 行生产代码。每个客户端集成都可以作为后续 PR,在核心生命周期审查稳定后再落地。拆分后每部分都可以独立审查和回滚,也让核心引擎在 UI 层构建之前先经过实战检验。

转交维护者做范围决定。代码本身已就绪;问题是应该作为一个 PR 还是多个 PR 落地。

Qwen Code · qwen3.8-max-preview

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

⏸️ Deferring to @wenshao @tanzhenxin @yiliang114 @LaZzyMan — this PR touches 1,590 core production lines across 6 packages (218 files total, ~11K production lines). The code quality is solid and the architecture is a clear improvement, but the cross-package scope from a fork contributor needs a maintainer's call on whether to land as one PR or split into phases (core + TUI first, then WebShell/Desktop/SDK/ACP). Needs a human decision on this one.

qwen-code-ci-bot pushed a commit that referenced this pull request Jul 22, 2026
@qqqys

qqqys commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author

TUI tmux E2E report

This is a fresh end-to-end TUI pass for the Goal v3 interaction model. Every scenario ran in a new tmux session with an isolated /private/tmp/qwen-goal-tui-e2e-* working directory, using the built CLI from this PR. The screenshots below are rendered directly from the corresponding tmux capture-pane output; ANSI transcripts and unrendered captures were retained locally under .qwen/e2e-tests/goal-v3-tmux/.

Environment: macOS arm64, Node.js 25.9.0, Qwen Code 0.20.1, qwen3.8-max-preview, branch feat/goal-protocol-v3, verified fix commit 124371890.

Result summary

Interaction chain Result Evidence
First command in a blank session: /goal ... PASS Goal created without requiring a prior chat message; revision 1 became authoritative.
Automatic continuation and terminal boundary PASS Exact output STEP_1STEP_2STEP_3; completion proposal verified; no fourth model reply and the Goal pill disappeared.
Active Goal edit PASS /goal edit ... updated the objective in place; subsequent turns changed from ACTIVE_OLD to ACTIVE_NEW; terminal read reported revision 2.
Pause → edit while paused → resume PASS after fix Paused state remained authoritative; edit control was admitted ahead of held user messages; resume read revision 2 and emitted NEW, never the old objective.
Normal steering vs queued input PASS Enter sent STEER_NOTE into the next Goal turn. Ctrl+Q visibly held QUEUED_MESSAGE during the active turn and automatically drained it at the next CLI turn boundary.
Clear with queued/active work PASS /goal clear required no confirmation dialog, stopped automatic continuation, removed the Goal pill, and returned to normal input.
Cross-process persistence PASS A paused Goal survived process exit and --resume <session-id> with the original objective, revision 1, turn count, active time, and paused status; resume continued normally and END completed it.
Invalid/no-Goal controls PASS /goal edit without an objective and /goal resume without a Goal returned explicit errors; /goal clear with no Goal was idempotent and silent.
No legacy 50-turn Goal cap PASS (automated) Core reducer/runtime integration tests completed 150 sequential automatic admissions and preserved turnCount: 150; no Goal-specific 50-turn or wall-clock cutoff exists.

Bug found and fixed during this pass

Reproduction: pause an automatically continuing Goal, queue a normal user message, submit /goal edit ..., then resume. Before the fix, the paused state was omitted from the queue-drain stop-state check, so an ordinary queued message could be admitted before the Goal control and the resumed turn could still use the old objective.

Fix: treat paused like blocked and usage_limited while selecting the next queued submission, so /goal controls are drained first while ordinary user work remains held. A focused hook regression test proves the edit command is selected and the held user message is not submitted.

Interaction evidence

1. Finite automatic loop completes without a tail turn

Finite Goal: STEP_1, STEP_2, STEP_3, then terminal state

2. Ctrl+Q holds a message while the current Goal turn is active

Queued message remains visible while Goal is running

3. Queued input drains at the CLI turn boundary; clear needs no confirmation

Queued message drains and Goal clear returns to normal input

4. Pause, edit, resume: revision 2 and the new objective are used

Paused Goal edited and resumed with revision 2

5. Stop-condition input reaches verifier and leaves no active Goal

STOP completes the edited Goal

6. Paused Goal restores across a real process restart

Resumed session restores paused Goal and authoritative objective

7. Enter steers the active Goal; the edited objective remains in force

Steering input enters the active Goal turn

8. Invalid controls produce concise errors

Goal command validation errors

Automated verification

  • packages/cli: AppContainer.test.tsx — 128/128 passed.
  • packages/core: goal-runtime.integration.test.ts + goal-reducer.test.ts — 17/17 passed, including 150 sequential Goal admissions.
  • Repository build — passed.
  • Repository typecheck — passed for all workspaces.
  • Focused ESLint for the changed TUI files — passed with zero warnings.
  • git diff --check — passed.

CLI/WebShell behavior boundary

The CLI intentionally exposes two keyboard paths while a Goal is running: Enter is steer, and Ctrl+Q is queue. A queued CLI message is held for the current turn and automatically drains at the next turn boundary. WebShell instead keeps queued messages visible until the user clicks Insert; its full browser E2E chain and screenshots are in the earlier report: #7494 (comment).

Observed timing: normal automatic turns continued without an artificial safety delay. Independent terminal verification can take model/provider time (roughly 10–20 seconds in these runs), but no fixed 60-minute window, 50-turn Goal cap, or Goal-specific wall-clock timeout was observed.

qwen-code-ci-bot pushed a commit that referenced this pull request Jul 22, 2026
qwen-code-ci-bot pushed a commit that referenced this pull request Jul 22, 2026
qwen-code-ci-bot pushed a commit that referenced this pull request Jul 22, 2026

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

Thanks for the substantial lifecycle redesign and the follow-up hardening pass. The ownership and trust boundaries look solid, and the latest commit fixes the earlier cross-client recovery issues I checked. I am requesting changes for four remaining correctness/scalability blockers; details are inline. Non-blocking test and simplification notes are inline as well. CI note: the current Ubuntu job also exits after the CLI Vitest worker exceeds its heap limit, despite the reported assertions passing, so the branch still needs a green CI run before merge.

this.turnParentUuids.push(this.lastRecordUuid);
const record: ChatRecord = {
...this.createBaseRecord('user'),
subtype: 'goal_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.

[P1] Hide Goal runtime prompts from replay presentation

recordGoalRuntimeMessage deliberately persists the model-facing continuation as a user record, but both DefaultTranscriptReplayMachine.projectUserRecord and buildResumedHistoryItems treat every other user subtype as displayable. After any automatic Goal turn, /resume and paged daemon history (and therefore WebShell/Desktop history) emit the internal Continue working on the active Goal... prompt as if the user typed it, while live delivery emits no corresponding user chunk. Please keep this record for model/evidence reconstruction but explicitly exclude goal_runtime in both presentation projectors, with TUI and ACP replay contract tests.

restore(records: readonly ChatRecord[]): Promise<void> {
return enqueue(async () => {
assertAvailable();
if (restored) return;

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.

[P1] Rebase the Goal runtime when rewinding

This restore is only usable during initialization, but both ACP and TUI rewind only truncate model history and re-root ChatRecordingService; neither updates the live Goal runtime. Rewinding a paused or terminal Goal to before its create record leaves the current process serving the removed Goal, while restarting recovers a different state. Resuming the stale Goal can then point evidenceCursor at the dead branch and end in cursor_not_found/usage_limited. Please make rewind atomically rebase or reset the runtime from the surviving active chain (or persist an explicit reset and update the runtime), and cover both clients.

goalContext: GoalTurnPermit,
): void {
try {
this.turnParentUuids.push(this.lastRecordUuid);

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.

[P1] Do not count automatic Goal prompts as selectable user turns

Pushing this record into turnParentUuids makes an automatic continuation a top-level rewind boundary, while the TUI and ACP truncation mappers count plain role: user API contents and have no subtype metadata. For U0 -> A0 -> U_goal -> A_goal -> U1, selecting the visible U1 can truncate before U_goal and discard A_goal; in both mode the files and conversation can land on different boundaries. Please use one explicit real-user-turn mapping across UI, API history, and recorder instead of counting all user text, and add a Goal-plus-rewind contract test.

for (let index = cursorIndex + 1; index < input.records.length; index++) {
const evidence = eligibleEvidence(input.records[index]!, input);
if (!evidence) continue;
catalog.push(stripContent(evidence));

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.

[P1] Bound the total Goal evidence payload

This catalog grows with every eligible record after the cursor; the per-entry preview cap does not limit entry count, total bytes, or lineage IDs. Since every Goal turn is told to call get_goal and Goals have no fixed turn/token budget, repeated payload and scan work grows quadratically; 1,000 ordinary entries already produce roughly 355 KB of JSON per call. Terminal verification is unbounded too: evidenceRefs has no maxItems, and every cited record is expanded to full content before verifier JSON is built. Please add explicit catalog/byte and verifier reference/evidence budgets, with defined truncation or pagination semantics.

return (
value === undefined ||
(isRecord(value) &&
hasOnlyKeys(value, ['fingerprint', 'count', 'turnIds']) &&

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] Add negative parser cases for blockedAudit

This strict persisted shape protects the three-consecutive-turn blocker gate, but the parser tests currently cover only an audit-free valid payload. Could we add table-driven cases for count 0/4, turnIds/count mismatch, empty fingerprint or turn ID, and extra keys? That would catch accidental loosening of this recovery boundary.

validateBlockerCoverage(input.proposal, citedRecords, analysis);

return {
catalog: analysis.catalog.map((entry) => ({ ...entry })),

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.

[Non-blocking / Ponytail] ValidatedGoalEvidence.catalog and .lineageTurnIds are cloned and returned here, but production only reads citedRecords. Returning only citedRecords removes dead API surface and avoids copying the already-growing catalog during verification.

): Promise<ChatRecord>;
}

export async function recordMigratedGoalState(

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.

[Non-blocking / Ponytail] recordMigratedGoalState and GoalStateRecorder have no production caller; only their dedicated test uses this wrapper, while production calls createMigratedGoalState directly. Deleting the wrapper, interface, and test removes about 39 lines without changing behavior.

}
});

it('removes the legacy Goal runtime modules', () => {

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.

[Non-blocking / Ponytail] These path- and identifier-spelling assertions do not protect a stable behavior: harmless renames fail them, while equivalent legacy behavior under another name passes. Compile/import checks plus the behavioral Goal suites cover the real cutover. Keeping the public-export assertion if desired and deleting the remaining core/CLI cutover checks removes roughly 55 lines.

@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. Suggestions are inline. Not reviewed: chunk 6, chunk 7, chunk 8, chunk 9, chunk 10, chunk 11, chunk 17, chunk 18, chunk 19, chunk 20, chunk 21, chunk 22, chunk 24, chunk 25, chunk 26, chunk 27, chunk 28, chunk 29, chunk 30, chunk 31, chunk 32, chunk 34, chunk 35, chunk 36, chunk 37, chunk 38, chunk 39, chunk 40, chunk 41, chunk 42, chunk 43, chunk 49, chunk 50, chunk 51, chunk 53, chunk 54, chunk 55, chunk 56, chunk 57, chunk 58, chunk 63, chunk 64, chunk 68, chunk 90, chunk 91, chunk 93, chunk 94, chunk 95, chunk 98, chunk 100, chunk 102, chunk 103, chunk 104, chunk 105, chunk 106, chunk 109, chunk 110, chunk 111, chunk 112, chunk 113, chunk 127, chunk 128, chunk 129, chunk 130 — no agent reported covering these; nobody read them. Not reviewed: chunk agents (1-130) — not launched due to PR scale (153 agents required, practical limit reached with whole-diff + invariant agents covering the 6 heavy files and key cross-file traces). Not reviewed: Agent 0: Issue fidelity & root-cause ownership, chunk 1, chunk 2, chunk 3, chunk 4, chunk 5, chunk 6, chunk 7, chunk 8, chunk 9, chunk 10, chunk 11, chunk 12, chunk 13, chunk 14, chunk 15, chunk 16, chunk 17, chunk 18, chunk 19, chunk 20, chunk 21, chunk 22, chunk 23, chunk 24, chunk 25, chunk 26, chunk 27, chunk 28, chunk 29, chunk 30, chunk 31, chunk 32, chunk 33, chunk 34, chunk 35, chunk 36, chunk 37, chunk 38, chunk 39, chunk 40, chunk 41, chunk 42, chunk 43, chunk 44, chunk 45, chunk 46, chunk 47, chunk 48, chunk 49, chunk 50, chunk 51, chunk 52, chunk 53, chunk 54, chunk 55, chunk 56, chunk 57, chunk 58, chunk 59, chunk 60, chunk 61, chunk 62, chunk 63, chunk 64, chunk 65, chunk 66, chunk 67, chunk 68, chunk 69, chunk 70, chunk 71, chunk 72, chunk 73, chunk 74, chunk 75, chunk 76, chunk 77, chunk 78, chunk 79, chunk 80, chunk 81, chunk 82, chunk 83, chunk 84, chunk 85, chunk 86, chunk 87, chunk 88, chunk 89, chunk 90, chunk 91, chunk 92, chunk 93, chunk 94, chunk 95, chunk 96, chunk 97, chunk 98, chunk 99, chunk 100, chunk 101, chunk 102, chunk 103, chunk 104, chunk 105, chunk 106, chunk 107, chunk 108, chunk 109, chunk 110, chunk 111, chunk 112, chunk 113, chunk 114, chunk 115, chunk 116, chunk 117, chunk 118, chunk 119, chunk 120, chunk 121, chunk 122, chunk 123, chunk 124, chunk 125, chunk 126, chunk 127, chunk 128, chunk 129, chunk 130, Test coverage matrix (whole-diff), Agent 1b: Removed-behavior audit, Agent 1c: Cross-file tracer, Invariant agent A: state, timers, collections — packages/cli/src/acp-integration/session/Session.ts, Invariant agent B: counters, return values, error taxonomies — packages/cli/src/acp-integration/session/Session.ts, Invariant agent C: config fields, early returns — packages/cli/src/acp-integration/session/Session.ts, Invariant agent A: state, timers, collections — packages/cli/src/acp-integration/session/history-replayer.ts, Invariant agent B: counters, return values, error taxonomies — packages/cli/src/acp-integration/session/history-replayer.ts, Invariant agent C: config fields, early returns — packages/cli/src/acp-integration/session/history-replayer.ts, Invariant agent A: state, timers, collections — packages/cli/src/ui/hooks/useGeminiStream.ts, Invariant agent B: counters, return values, error taxonomies — packages/cli/src/ui/hooks/useGeminiStream.ts, Invariant agent C: config fields, early returns — packages/cli/src/ui/hooks/useGeminiStream.ts, Invariant agent A: state, timers, collections — packages/core/src/core/client.ts, Invariant agent B: counters, return values, error taxonomies — packages/core/src/core/client.ts, Invariant agent C: config fields, early returns — packages/core/src/core/client.ts, Invariant agent A: state, timers, collections — packages/core/src/core/coreToolScheduler.ts, Invariant agent B: counters, return values, error taxonomies — packages/core/src/core/coreToolScheduler.ts, Invariant agent C: config fields, early returns — packages/core/src/core/coreToolScheduler.ts, Invariant agent A: state, timers, collections — packages/web-shell/client/components/dialogs/GoalsDialog.tsx, Invariant agent B: counters, return values, error taxonomies — packages/web-shell/client/components/dialogs/GoalsDialog.tsx, Invariant agent C: config fields, early returns — packages/web-shell/client/components/dialogs/GoalsDialog.tsx — its prompt was built, but no agent on record was launched with it. Not reviewed: reverse audit — no auditor was launched with a prompt this skill builds — the pass that hunts what the rest of the review missed ran, if at all, without the method its brief carries. Not reviewed: verification — a verifier ran and opened its brief, but no agent was launched with the prompt the CLI built — the launch was written by hand, and the posted findings cannot be counted as verified against it. [Critical] packages/cli/src/acp-integration/acpAgent.ts: The bulk replay path in loadSession does not forward replayPage.goalState to collectHistoryReplayUpdates. The resume path correctly passes it. When an ACP client bulk-loads a session whose history exceeds the page size, the HistoryReplayer starts without pre-existing Goal state, and the TranscriptReplayMachine may project incorrect legacy metadata. Fix: add goalState: replayPage.goalState to the collectHistoryReplayUpdates call in the live-session branch.

— qwen3.7-max via Qwen Code /review

Comment on lines +2497 to +2499
if (isGoalControlPrompt) {
return this.#executePrompt(params, new AbortController());
}

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.

[Critical] The /goal control-prompt path creates a throwaway AbortController that is never stored on any session field (pendingPrompt, goalAbortController, or otherwise), making the resulting #executePrompt call unreachable by cancelPendingPrompt(). — Failure scenario: A user sends /goal then immediately cancels. cancelPendingPrompt() snapshots hadPrompt = false (the /goal path never sets this.pendingPrompt) and hadGoal = false (no automatic goal queue entry exists for a /goal command). Cancel throws NOT_CURRENTLY_GENERATING_CANCEL_MESSAGE, even though #executePrompt is actively running. The /goal handler proceeds to completion — calling #queueGoalStateEmission and awaiting this.goalEmissionTail — after the client was told nothing was generating. The goal state emission lands on the wire after the cancel acknowledgement, producing a surprise state update the client believed it had stopped.

Suggested change
if (isGoalControlPrompt) {
return this.#executePrompt(params, new AbortController());
}
if (isGoalControlPrompt) {
const pendingSend = new AbortController();
this.pendingPrompt = pendingSend;
try {
return await this.#executePrompt(params, pendingSend);
} finally {
if (this.pendingPrompt === pendingSend) this.pendingPrompt = null;
}
}

— qwen3.7-max via Qwen Code /review

Comment on lines +178 to +181
const control = useCallback(
async (item: DaemonGoal, action: 'pause' | 'resume' | 'clear') => {
const goal = item.snapshot.goal;
if (!goal) return;

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 old handleClear required window.confirm() before clearing a goal; the new unified control function dispatches 'clear' with no confirmation, and the clearConfirm i18n key was removed from translations. A user who clicks the trash icon accidentally (e.g., mis-aiming for the adjacent pause/resume button) immediately sends a clear command to the daemon with no recovery path. Pause and resume are reversible; clear is destructive and irreversible within this UI. Consider adding a confirmation step for the 'clear' action only — either re-introduce a window.confirm guard inside control when action === 'clear', or add a confirmation at the button's onClick handler.

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Size review — suggest splitting

This PR is +32,510 / −10,983 across 231 files. The total diff (~43k lines) far exceeds GitHub's 20k-line limit, so the diff cannot be loaded in the GitHub UI or via the API. Reviewing a cross-client lifecycle replacement at this size is not feasible in a single pass.

I understand this is harder to split than a purely additive feature — the PR replaces the legacy hook-driven Goal system rather than extending it, so core and at least the primary consumer (CLI/TUI) must land together to avoid a broken intermediate state. That said, an expand → migrate → contract sequence can keep every PR under the 20k diff limit:

# Phase Scope ~Diff lines Notes
1 Expand core/goals/ new module — 20 new files (protocol, reducer, runtime, persistence, evidence, verifier, tools, wire, legacy-projection) + index.ts export ~5,800 Purely additive. Old goalHook/goalJudge untouched, zero behavior change, independently testable
2 Migrate (core) core integration — wire client.ts, turn.ts, coreToolScheduler, services, config to the new Goal API; old goalHook/goalJudge kept as deprecated compat wrappers ~3,500 Core switches to the new system internally. Old API preserved temporarily so CLI still compiles
3 Migrate (CLI) + Contract CLI full adaptation + old system removal — TUI components/hooks/commands, headless CLI, ACP integration; delete goalHook/goalJudge/goalLoop ~12,000 Production code ~3,400 lines + tests ~7,400 lines + pure deletions ~3,000 lines. Under the 20k limit; actual review burden is much smaller than the line count suggests
4 Consumers daemon/serve + sdk + webui + acp-bridge + web-shell + desktop ~9,300 Remaining client adaptations, mostly components + tests

Key observations from the diff:

  • core/goals/: 20 new files (+5,755) are cleanly separable from the 5 removed old files (−3,042) and the 23 modified existing core files (+2,078/−141).
  • CLI production vs tests: TUI production code is only 1,726+/893−; ACP+headless production is 1,682+/416−. The bulk (~7,400 lines) is test code, which is more mechanical to review.
  • Each PR stays under GitHub's 20k diff limit and is independently buildable/testable.

One more note: this PR modifies 51 files in packages/core (+7,852 / −3,214). Per the repository's contribution guidelines, feat-type changes touching core at this scale should be escalated to a maintainer for awareness.

中文

本 PR 共 +32,510 / −10,983,涉及 231 个文件,diff 总量约 43k 行,远超 GitHub 的 20k 行限制,无法在 GitHub UI 或 API 中加载。

理解这是替换式重设计,比纯新增更难拆。但可以用 expand → migrate → contract 模式拆成 4 个 PR,每个都在 20k 限制内:

  1. Expand — core/goals/ 新模块(~5,800 行):20 个新文件,纯添加,旧系统不动,零行为变更
  2. Migrate (core) — core 接线(~3,500 行):client.ts、turn.ts、tools、services 切到新 Goal API,旧 goalHook/goalJudge 暂留为兼容层
  3. Migrate (CLI) + Contract — CLI 适配 + 删除旧系统(~12,000 行):生产代码仅 ~3,400 行,测试 ~7,400 行,纯删除 ~3,000 行。低于 20k 限制
  4. Consumers — 其余客户端(~9,300 行):daemon/sdk/webui/web-shell/desktop 适配

关键数据:

  • core/goals/ 的 20 个新文件 (+5,755) 与 5 个旧文件删除 (−3,042) 和 23 个现有文件修改 (+2,078/−141) 可以干净分离
  • CLI 的 TUI 生产代码仅 1,726+/893−,ACP+headless 生产代码仅 1,682+/416−,大头 (~7,400 行) 是测试代码

另外:本 PR 修改了 packages/core 的 51 个文件(+7,852 / −3,214),按仓库贡献指南,此规模的 feat 类 core 变更需要 escalate 到 maintainer 知晓。

@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. Unresolved, please confirm: [Critical] packages/core/src/goals/goal-runtime.ts — @yiliang114 P1: Goal runtime not rebased on rewind. Requires tracing rewind → Goal runtime integration across multiple files. [Critical] packages/core/src/goals/goal-evidence.ts — @yiliang114 P1: Goal evidence catalog unbounded. Catalog grows with every eligible record; no entry count, byte, or lineage ID limit. Not reviewed: chunk 1, chunk 2, chunk 3, chunk 4, chunk 5, chunk 6, chunk 7, chunk 8, chunk 9, chunk 10, chunk 11, chunk 12, chunk 21, chunk 22, chunk 23, chunk 24, chunk 25, chunk 26, chunk 27, chunk 28, chunk 29, chunk 30, chunk 31, chunk 32, chunk 33, chunk 34, chunk 35, chunk 36, chunk 37, chunk 38, chunk 39, chunk 40, chunk 41, chunk 42, chunk 43, chunk 44, chunk 45, chunk 46, chunk 47, chunk 48, chunk 49, chunk 50, chunk 51, chunk 52, chunk 53, chunk 54, chunk 55, chunk 56, chunk 57, chunk 58, chunk 59, chunk 60, chunk 61, chunk 62, chunk 63, chunk 64, chunk 65, chunk 66, chunk 67, chunk 76, chunk 77, chunk 78, chunk 79, chunk 80, chunk 81, chunk 82, chunk 83, chunk 84, chunk 85, chunk 86, chunk 87, chunk 88, chunk 89, chunk 90, chunk 91, chunk 92, chunk 93, chunk 94, chunk 95, chunk 96, chunk 97, chunk 98, chunk 99, chunk 100, chunk 101, chunk 102, chunk 103, chunk 104, chunk 105, chunk 106, chunk 107, chunk 108, chunk 109, chunk 110, chunk 111, chunk 112, chunk 113, chunk 114, chunk 115, chunk 116, chunk 117, chunk 118, chunk 119, chunk 120, chunk 121, chunk 122, chunk 123, chunk 124, chunk 126, chunk 127, chunk 128, chunk 129, chunk 130, chunk 131, chunk 132, chunk 133 — no agent reported covering these; nobody read them. Not reviewed: chunk agents (1-133) — PR scale: 156 agents required, focused review on cross-file traces, build/test, and invariant agents for 3 heavy files. Not reviewed: Agent 0: Issue fidelity — not launched (no linked issues, feature PR). Not reviewed: Test coverage matrix — not launched. Not reviewed: verification — no new findings to verify. Not reviewed: Agent 0: Issue fidelity & root-cause ownership, chunk 1, chunk 2, chunk 3, chunk 4, chunk 5, chunk 6, chunk 7, chunk 8, chunk 9, chunk 10, chunk 11, chunk 12, chunk 13, chunk 14, chunk 15, chunk 16, chunk 17, chunk 18, chunk 19, chunk 20, chunk 21, chunk 22, chunk 23, chunk 24, chunk 25, chunk 26, chunk 27, chunk 28, chunk 29, chunk 30, chunk 31, chunk 32, chunk 33, chunk 34, chunk 35, chunk 36, chunk 37, chunk 38, chunk 39, chunk 40, chunk 41, chunk 42, chunk 43, chunk 44, chunk 45, chunk 46, chunk 47, chunk 48, chunk 49, chunk 50, chunk 51, chunk 52, chunk 53, chunk 54, chunk 55, chunk 56, chunk 57, chunk 58, chunk 59, chunk 60, chunk 61, chunk 62, chunk 63, chunk 64, chunk 65, chunk 66, chunk 67, chunk 68, chunk 69, chunk 70, chunk 71, chunk 72, chunk 73, chunk 74, chunk 75, chunk 76, chunk 77, chunk 78, chunk 79, chunk 80, chunk 81, chunk 82, chunk 83, chunk 84, chunk 85, chunk 86, chunk 87, chunk 88, chunk 89, chunk 90, chunk 91, chunk 92, chunk 93, chunk 94, chunk 95, chunk 96, chunk 97, chunk 98, chunk 99, chunk 100, chunk 101, chunk 102, chunk 103, chunk 104, chunk 105, chunk 106, chunk 107, chunk 108, chunk 109, chunk 110, chunk 111, chunk 112, chunk 113, chunk 114, chunk 115, chunk 116, chunk 117, chunk 118, chunk 119, chunk 120, chunk 121, chunk 122, chunk 123, chunk 124, chunk 125, chunk 126, chunk 127, chunk 128, chunk 129, chunk 130, chunk 131, chunk 132, chunk 133, Test coverage matrix (whole-diff), Agent 1b: Removed-behavior audit, Agent 1c: Cross-file tracer, Agent 7: Build & test verification, Invariant agent A: state, timers, collections — packages/cli/src/acp-integration/session/Session.ts, Invariant agent B: counters, return values, error taxonomies — packages/cli/src/acp-integration/session/Session.ts, Invariant agent C: config fields, early returns — packages/cli/src/acp-integration/session/Session.ts, Invariant agent A: state, timers, collections — packages/cli/src/acp-integration/session/history-replayer.ts, Invariant agent B: counters, return values, error taxonomies — packages/cli/src/acp-integration/session/history-replayer.ts, Invariant agent C: config fields, early returns — packages/cli/src/acp-integration/session/history-replayer.ts, Invariant agent A: state, timers, collections — packages/cli/src/ui/hooks/useGeminiStream.ts, Invariant agent B: counters, return values, error taxonomies — packages/cli/src/ui/hooks/useGeminiStream.ts, Invariant agent C: config fields, early returns — packages/cli/src/ui/hooks/useGeminiStream.ts, Invariant agent A: state, timers, collections — packages/core/src/core/client.ts, Invariant agent B: counters, return values, error taxonomies — packages/core/src/core/client.ts, Invariant agent C: config fields, early returns — packages/core/src/core/client.ts, Invariant agent A: state, timers, collections — packages/core/src/core/coreToolScheduler.ts, Invariant agent B: counters, return values, error taxonomies — packages/core/src/core/coreToolScheduler.ts, Invariant agent C: config fields, early returns — packages/core/src/core/coreToolScheduler.ts, Invariant agent A: state, timers, collections — packages/web-shell/client/components/dialogs/GoalsDialog.tsx, Invariant agent B: counters, return values, error taxonomies — packages/web-shell/client/components/dialogs/GoalsDialog.tsx, Invariant agent C: config fields, early returns — packages/web-shell/client/components/dialogs/GoalsDialog.tsx — its prompt was built, but no agent on record was launched with it. Not reviewed: reverse audit — no auditor was launched with a prompt this skill builds — the pass that hunts what the rest of the review missed ran, if at all, without the method its brief carries.

— qwen3.7-max via Qwen Code /review

@qqqys

qqqys commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author

This PR is being split into focused, dependency-ordered changes so each contract and surface can be reviewed independently. Please treat #7494 as the umbrella/reference diff rather than the merge target while the split is in progress.

The first slice is now available as #7517: the versioned Goal state protocol, deterministic reducer, persistence/recovery contract, legacy migration, and compatibility projection.

Planned follow-ups are: evidence and verifier; runtime and tools; recording/replay/rewind; core engine integration; TUI; non-interactive CLI; ACP/serve; SDK/WebUI; WebShell; Desktop; and final legacy cleanup. Each UI slice will carry its own E2E evidence and screenshots.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants