Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
09b3d07
feat(cli): adopt Goal v3 in ACP sessions
qqqys Aug 8, 2026
b1689bb
test(cli): give the ACP Session fakes a Goal runtime
qqqys Aug 8, 2026
e6a3735
fix(cli): settle Goal turns that fail, and answer goal RPCs without p…
qqqys Aug 8, 2026
b0f78ad
fix(cli): stop Goal turns leaking permits, and show a paused goal as …
qqqys Aug 8, 2026
69a27b3
fix(cli): unblock deadlocked Goal turns and stop a capped goal loop
qqqys Aug 9, 2026
086406f
fix(cli): keep Goal degradation working headless and re-subscribe aft…
qqqys Aug 9, 2026
5de2229
Merge upstream/main into agent/goal-v3-acp
qqqys Aug 9, 2026
feb90d7
merge: resolve acpAgent conflict with main's session-restore profiler
qqqys Aug 9, 2026
6d25cbb
fix(cli): repair Goal recovery on ACP resume
qqqys Aug 9, 2026
c5c90ff
fix(acp-bridge): replay paused goal cards instead of dropping them
qqqys Aug 9, 2026
8dcbe4b
fix(core): report a lost session writer as a Goal persistence failure
qqqys Aug 10, 2026
985d0b1
fix(cli): deliver the recovered Goal card on the bulk load and resume…
qqqys Aug 10, 2026
d178b96
fix(cli): stop Goal permits leaking on turns that never reached the m…
qqqys Aug 10, 2026
ea64631
Merge branch 'main' into agent/goal-v3-acp
qwen-code-dev-bot Aug 10, 2026
e69b5d2
Merge branch 'main' into agent/goal-v3-acp
qwen-code-dev-bot Aug 10, 2026
8218067
fix(goals): surface failed clear persistence
qqqys Aug 10, 2026
56d9d4f
fix(goal): surface persistence recovery failures
qqqys Aug 11, 2026
347f30c
fix(goal): clean up failed permit claims
qqqys Aug 11, 2026
d1bb047
test(cli): align degraded Goal CI setup
qqqys Aug 11, 2026
3c8c848
fix(cli): stop Goal continuations after cancel
qqqys Aug 11, 2026
8c5dc82
test(webui): cover paused Goal status normalization
qqqys Aug 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 7 additions & 9 deletions packages/acp-bridge/src/bridgeTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import type {
ApprovalMode,
GoalSnapshotV2,
SessionGroupPresetColor,
} from '@qwen-code/qwen-code-core';
import type {
Expand Down Expand Up @@ -601,16 +602,14 @@ export interface BridgeSessionSummary {
}

/**
* A session's live `/goal` state, as reported by the `qwen --acp` child.
*
* Only the active goal crosses the bridge. The child also caches the most
* recent goal that ended on its own, but nothing on this side reads it, so it
* is not part of the wire shape — add it back alongside the first consumer.
* A session's live canonical Goal state, as reported by the `qwen --acp`
* child. `active` remains as a compatibility projection for existing hosts.
*/
export interface BridgeSessionGoal {
snapshot: GoalSnapshotV2;
active: {
Comment on lines 608 to 610

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] R2-11: The new required snapshot field on BridgeSessionGoal is produced by every sessionGoalGet path but read by no consumer — per the AGENTS.md rule, every added field's read sites were grepped, including outside the diff. The only reader (GET /goals) projects active only, so paused/blocked goals — a state this PR newly makes reachable in ACP — vanish from the Goals page. — Failure scenario: the sole consumer of BridgeSessionGoal is packages/cli/src/serve/routes/goals.ts, which reads only goal.active.* and does if (!goal.active) continue;; webui/web-shell consume the mapped GoalView, and the SDK/webui clear path reads only {cleared} — the field crosses the wire and is dropped. Concretely: /goal pause is now valid in ACP mode, and sessionGoalGet projects active only when snapshot.goal?.status === 'active' — so a paused goal returns active: null and is silently filtered out of GET /goals. The user pauses a goal from chat and it disappears from the Web Shell Goals page, with no way to see or resume it from there.

Suggested fix: either consume the new field (have GET /goals emit paused/blocked goals from snapshot, presumably why it was added to the bridge shape) or drop snapshot from BridgeSessionGoal until a reader exists — and decide explicitly whether paused goals belong on the Goals page.

中文说明

BridgeSessionGoal 新增的必需 snapshot 字段被每个 sessionGoalGet 路径产出,却没有任何 consumer 读取——按 AGENTS.md 规则已 grep 其所有读取点(含 diff 之外)。唯一读取方(GET /goals)只投影 active,因此 paused/blocked goal——本 PR 新近让 ACP 可达的状态——会从 Goals 页面消失。失败场景:BridgeSessionGoal 的唯一 consumer 是 packages/cli/src/serve/routes/goals.ts,它只读 goal.active.* 并执行 if (!goal.active) continue;;webui/web-shell 消费映射后的 GoalView,SDK/webui 的 clear 路径只读 {cleared}——字段过线即被丢弃。具体地:/goal pause 现在在 ACP 模式有效,而 sessionGoalGet 只在 snapshot.goal?.status === 'active' 时投影 active——于是 paused goal 返回 active: null 并被 GET /goals 静默过滤。用户从聊天暂停一个 Goal,它就从 Web Shell Goals 页面消失,且没有任何途径查看或恢复。修复建议:要么消费新字段(让 GET /goalssnapshot 发出 paused/blocked goal——这大概正是把它加入 bridge 形状的原因),要么在 reader 存在之前从 BridgeSessionGoal 移除 snapshot——并明确决定 paused goal 是否属于 Goals 页面。

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

condition: string;
/** Judge turns completed so far; 0 before the first stop-hook evaluation. */
/** Canonical Goal turns completed so far. */
iterations: number;
setAt: number;
/** The judge's verdict on the most recent turn, when it has run. */
Expand Down Expand Up @@ -1450,9 +1449,8 @@ export interface AcpSessionBridge {
): Promise<{ cleared: boolean; condition?: string }>;

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] R1-7: The clear-goal route now returns snapshot on the wire, but this typed contract — and the bridge impl's requestSessionStatus generic plus SDK DaemonClient.sessionGoalClear — was left without it, unlike BridgeSessionGoal, which this PR updated to carry snapshot. — Concrete cost: a typed consumer that wants to update its goal card from the post-clear snapshot (instead of a second sessionGoalGet round-trip) needs an as cast; the get and clear contracts now disagree about the same canonical field, and this PR's own test (snapshot: after in the clear response) codifies a wire shape no typed layer declares.

Suggested change
): Promise<{ cleared: boolean; condition?: string }>;
): Promise<{ cleared: boolean; condition?: string; snapshot: GoalSnapshotV2 }>;

(If the snapshot is deliberately not part of the clear contract, strip it from the handler response and the test instead.)

中文说明

clear-goal 路由现在会在线上返回 snapshot 字段,但这个类型契约 —— 以及 bridge 实现的 requestSessionStatus 泛型和 SDK 的 DaemonClient.sessionGoalClear —— 都没有加上它;而本 PR 却更新了 BridgeSessionGoal 使其携带 snapshot。— 具体代价:想利用 clear 后的 snapshot 更新 goal 卡片(避免再发一次 sessionGoalGet)的类型化消费者必须使用 as 强转;get 与 clear 的契约现在对同一个 canonical 字段不一致,且本 PR 自己的测试(clear 响应中的 snapshot: after)把一个没有任何类型层声明的线上形状固化了下来。修复:按 suggestion 拓宽类型;如果 snapshot 有意不属于 clear 契约,则应从 handler 响应和测试中移除它。

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


/**
* Read a live session's goal state. Throws `SessionNotFoundError` when the
* session is not resident — goals live in the child's memory, so a
* non-resident session has no goal to report.
* Read a live session's Goal state. Throws `SessionNotFoundError` when the
* session is not resident because this route addresses the selected runtime.
*/
getSessionGoal(sessionId: string): Promise<BridgeSessionGoal>;
Comment on lines 1449 to 1455

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] R1-7 (still standing at this commit): the child's sessionGoalClear ext method now returns {cleared, condition, snapshot} and the sibling BridgeSessionGoal/getSessionGoal contract was updated to carry snapshot, but the declared clearSessionGoal return type above, the bridge pass-through, and SDK DaemonClient.sessionGoalClear were left without it — so the clear route ships an undeclared wire field. — Failure scenario: the bridge forwards the child response verbatim (requestSessionStatus casts response as unknown as T with no shaping), so every POST /session/:id/goal/clear response carries the full GoalSnapshotV2 (a GoalRecord whose lastReason alone may reach ~16KB per GOAL_PROPOSAL_REASON_MAX_BYTES) that no typed consumer can see: webui clearGoal reads only {cleared}, the SDK type omits it. Dead wire payload, and any consumer wanting the post-clear state must cast around the declared API.

Suggested fix: either declare snapshot: GoalSnapshotV2 in the clearSessionGoal return type here (and the SDK's sessionGoalClear), or drop snapshot from the clear ext-method response if no consumer is meant to read it there.

中文说明

R1-7(在本 commit 仍然存在):child 的 sessionGoalClear ext 方法现在返回 {cleared, condition, snapshot},兄弟契约 BridgeSessionGoal/getSessionGoal 已更新为携带 snapshot,但上方的 clearSessionGoal 返回类型、bridge 透传以及 SDK DaemonClient.sessionGoalClear 都没有加——于是 clear 路由在线上携带了一个未声明的字段。失败场景:bridge 原样转发 child 响应(requestSessionStatusresponse as unknown as T 强转、不做整形),因此每个 POST /session/:id/goal/clear 响应都携带完整 GoalSnapshotV2(其中仅 lastReason 就可达约 16KB,按 GOAL_PROPOSAL_REASON_MAX_BYTES),而没有任何类型化 consumer 能看到:webui clearGoal 只读 {cleared},SDK 类型也没有它。死线上载荷;任何想要清除后状态的 consumer 都必须绕过声明的 API 强转。修复建议:要么在此处的 clearSessionGoal 返回类型(及 SDK 的 sessionGoalClear)声明 snapshot: GoalSnapshotV2,要么如果不打算有 consumer 读取,就从 clear ext 方法响应中移除 snapshot

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


Expand Down
45 changes: 45 additions & 0 deletions packages/acp-bridge/src/transcript-replay.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,16 @@ function goalStateRecord(
});
}

function goalCardRecord(
uuid: string,
...items: ReadonlyArray<Record<string, unknown>>
): TranscriptRecordInput {
return record(uuid, 'system', {
subtype: 'slash_command',
systemPayload: { phase: 'result', outputHistoryItems: items },
});
}

describe('createTranscriptReplayMachine', () => {
it('does not replay internal Goal runtime prompts as user messages', () => {
expect(
Expand Down Expand Up @@ -112,6 +122,41 @@ describe('createTranscriptReplayMachine', () => {
});
});

it('replays a legacy paused goal card instead of leaving the set card newest', () => {
const machine = createTranscriptReplayMachine();
expect(
updates(
machine,
goalCardRecord('goal-set', {
type: 'goal_status',
kind: 'set',
condition: GOAL.objective,
}),
),
).toHaveLength(1);

const projected = updates(
machine,
goalCardRecord('goal-paused', {
type: 'goal_status',
kind: 'paused',
condition: GOAL.objective,
iterations: 4,
lastReason: 'paused by the user',
}),
);

expect(projected).toHaveLength(1);
expect(projected[0]?._meta).toMatchObject({
goalStatus: {
kind: 'paused',
condition: GOAL.objective,
iterations: 4,
lastReason: 'paused by the user',
},
});
});

it('emits legacy goalTerminal metadata for a terminal goal_state', () => {
const projected = updates(
createTranscriptReplayMachine(),
Expand Down
6 changes: 6 additions & 0 deletions packages/acp-bridge/src/transcript-replay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,12 @@ const TRANSCRIPT_GOAL_STATUS_KINDS = new Set([
'cleared',
'failed',
'aborted',
// A paused goal is not running, and dropping the card here is not neutral:
// the replay stream is what feeds the goal renderer, so the older `set` card
// stays newest and every surface keeps claiming autonomous work is under way.
// Kept in step with `GOAL_STATUS_KINDS`, which the daemon-side reader
// (`parseGoalStatusItem`) validates the same on-disk cards against.
'paused',

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] R7-12: The legacy goal-card kind allowlist is maintained as four unlinked manual copies — cli GOAL_STATUS_KINDS (ui/types.ts), acp-bridge TRANSCRIPT_GOAL_STATUS_KINDS (here), web-shell VALID_GOAL_KINDS (GoalStatusMessage.tsx), and webui normalizeGoalStatus (DaemonSessionProvider.tsx) — guarded only by prose comments. — Concrete cost: the drift this PR repairs already happened once — 'paused' existed in the canonical cli list while missing from the other three copies, which is precisely the phantom-running-goal bug being fixed here (dropped paused card → older set card stays newest → every surface claims autonomous work is under way). When the next kind is added, missing one copy silently regresses replay or rendering with no compiler or test signal; no cross-package equality test exists (GoalStatusMessage.test.tsx pins only the cli list). Export a shared legacy-kinds list from core and consume it at all four sites, or add a drift test.

中文说明

[Suggestion] R7-12:legacy Goal 卡片类型允许列表以四份互不关联的手工副本维护——cli GOAL_STATUS_KINDS(ui/types.ts)、acp-bridge TRANSCRIPT_GOAL_STATUS_KINDS(此处)、web-shell VALID_GOAL_KINDS(GoalStatusMessage.tsx)、webui normalizeGoalStatus(DaemonSessionProvider.tsx)——仅靠注释守护。— 具体代价:本 PR 修复的漂移已经发生过一次——'paused' 存在于 cli 规范列表却缺失于其他三份副本,这正是此处要修复的幽灵运行中 Goal 缺陷(暂停卡片被丢弃 → 旧 set 卡片保持最新 → 所有界面声称自动工作在进行)。下次新增类型时,漏改任一副本都会在没有编译器或测试信号的情况下静默回归回放或渲染;目前不存在跨包一致性测试(GoalStatusMessage.test.tsx 只钉住 cli 列表)。应从 core 导出共享的 legacy 类型列表并在四处消费,或添加漂移测试。

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

'checking',
]);

Expand Down
Loading
Loading