本章总览
tasks/DreamTask/(约 158 行)为 autoDream 记忆整理 fork agent 提供 UI 可见性:registerDreamTask 写入 type=dream 的 running 状态,addDreamTurn 增量更新 turns/filesTouched/phase,completeDreamTask 立即 notified=true(无 model-facing XML)。MonitorMcpTask/ 与 LocalWorkflowTask 类似,当前为 stub(isMonitorMcpTask 恒 false),types 与 pillLabel 已预留 monitor_mcp 分支。pillLabel.ts(87 行)集中定义 footer pill 与 turn-duration 行的文案契约:getPillLabel 按 homogenous task 集合生成 "N shells" / "◇ ultraplan" / "dreaming" 等字符串,pillNeedsCta 判定 ultraplan attention 态是否显示「↓ to view」。本章要求你理解 Dream 任务的 UI-only 定位,以及 pill 文案与 RemoteAgentTask ultraplanPhase 的联动。
学完本章你应该能
- 说明 DreamTaskState 字段(phase、sessionsReviewing、filesTouched、turns)
- 解释 Dream kill 时 rollbackConsolidationLock 与 fork-failure 同路径
- 理解 getPillLabel 各 type 分支与 allSameType 优化
- 掌握 pillNeedsCta 与 ultraplanPhase 状态图
- 对照 MonitorMcpTask stub 与 LocalShellTask kind=monitor 的区别
核心概念(先读懂这些)
Dream 是 UI surfacing,不是新 agent 类型
autoDream.ts 仍 fork 既有 dream agent;DreamTask 仅 registerTask 让 footer pill 显示 "dreaming"。complete/fail 设 notified=true 满足 eviction guard;用户可见完成面是 appendSystemMessage 而非 task-notification XML。
pillLabel 双表面一致
BackgroundTasksIndicator footer pill 与 turn-duration transcript 行共用 getPillLabel,避免「footer 说 2 shells、transcript 说 2 tasks」术语分裂。异构 task 集合 fallback 为 "N background tasks"。
monitor 的两条路径
LocalShellTask kind=monitor 已落地(Bash 脚本 monitor)。MonitorMcpTask type=monitor_mcp 为 MCP 侧预留 stub。pillLabel local_bash 分支会 split shells vs monitors 计数。
建议学习步骤
- 阅读 DreamTaskState 与 registerDreamTask
- 阅读 addDreamTurn phase 翻转逻辑
- 阅读 completeDreamTask / failDreamTask / DreamTask.kill
- 阅读 getPillLabel 全分支
- 阅读 pillNeedsCta ultraplan 条件
- 对照 MonitorMcpTask stub 与 types.ts
常见误区
注意
filesTouched 仅 pattern-match Edit/Write tool_use,不完整反映实际变更
注意
Dream turns 不含 prompt,MAX_TURNS=30 环形缓冲
注意
pillNeedsCta 要求 tasks.length===1 且 remote ultraplan
注意
MonitorMcpTask runtime 不存在,勿与 local_bash kind=monitor 混淆
DreamTask 在 autoDream 链路中的位置
services/autoDream/autoDream.ts
→ fork dream agent (既有 runAgent 路径)
→ registerDreamTask(setAppState, { sessionsReviewing, priorMtime, abortController })
→ onMessage hook → addDreamTurn(taskId, turn, touchedPaths)
→ completeDreamTask / failDreamTask / DreamTask.kill
→ appendSystemMessage 用户可见完成说明
DreamTask 不改变 dream agent 的 prompt 或 tool 池;它仅是 AppState.tasks 注册 + pill 数据源。kill 时 abortController.abort() 并 rollbackConsolidationLock(priorMtime),与 fork-failure catch 共享锁回滚语义,确保下一 session 可重试 consolidation。
DreamTaskState 字段
DreamTaskState 扩展 TaskStateBase:
| 字段 | 含义 |
|---|---|
| phase | 'starting' → 首次 Edit/Write 触达 → 'updating' |
| sessionsReviewing | 本次 dream 扫描的 session 数量 |
| filesTouched | Edit/Write tool_use 观测路径(不完整) |
| turns | DreamTurn[] assistant 文本 + toolUseCount 折叠 |
| abortController | kill 时 abort |
| priorMtime | consolidation lock mtime,kill 回滚用 |
DreamTurn = { text, toolUseCount }。Prompt deliberately 不进入 turns(隐私与体积)。
注释强调:不对 dream prompt 四阶段做 parse,phase 仅二元。
源码引用: src/tasks/DreamTask/DreamTask.ts · 第 14–50 行(共 158 行)
14| // A single assistant turn from the dream agent, tool uses collapsed to a count.
15| export type DreamTurn = {
16| text: string
17| toolUseCount: number
18| }
19|
20| // No phase detection — the dream prompt has a 4-stage structure
21| // (orient/gather/consolidate/prune) but we don't parse it. Just flip from
22| // 'starting' to 'updating' when the first Edit/Write tool_use lands.
23| export type DreamPhase = 'starting' | 'updating'
24|
25| export type DreamTaskState = TaskStateBase & {
26| type: 'dream'
27| phase: DreamPhase
28| sessionsReviewing: number
29| /**
30| * Paths observed in Edit/Write tool_use blocks via onMessage. This is an
31| * INCOMPLETE reflection of what the dream agent actually changed — it misses
32| * any bash-mediated writes and only captures the tool calls we pattern-match.
33| * Treat as "at least these were touched", not "only these were touched".
34| */
35| filesTouched: string[]
36| /** Assistant text responses, tool uses collapsed. Prompt is NOT included. */
37| turns: DreamTurn[]
38| abortController?: AbortController
39| /** Stashed so kill can rewind the lock mtime (same path as fork-failure). */
40| priorMtime: number
41| }
42|
43| export function isDreamTask(task: unknown): task is DreamTaskState {
44| return (
45| typeof task === 'object' &&
46| task !== null &&
47| 'type' in task &&
48| task.type === 'dream'
49| )
50| }
register / addDreamTurn / complete
registerDreamTask:
- generateTaskId('dream')
- createTaskStateBase(id, 'dream', 'dreaming')
- status: running, phase: starting, 空 turns/filesTouched
- registerTask
addDreamTurn updateTaskState:
- 去重 filesTouched
- 空 turn 且无新 touched → no-op skip re-render
- newTouched.length > 0 → phase: 'updating'
- turns slice(-(MAX_TURNS-1)).concat(turn)
completeDreamTask / failDreamTask:terminal + endTime + notified: true + 清 abortController。无 enqueuePendingNotification。
DreamTask.kill:killed + notified + abort + 条件 rollbackConsolidationLock。
源码引用: src/tasks/DreamTask/DreamTask.ts · 第 52–74 行(共 158 行)
52| export function registerDreamTask(
53| setAppState: SetAppState,
54| opts: {
55| sessionsReviewing: number
56| priorMtime: number
57| abortController: AbortController
58| },
59| ): string {
60| const id = generateTaskId('dream')
61| const task: DreamTaskState = {
62| ...createTaskStateBase(id, 'dream', 'dreaming'),
63| type: 'dream',
64| status: 'running',
65| phase: 'starting',
66| sessionsReviewing: opts.sessionsReviewing,
67| filesTouched: [],
68| turns: [],
69| abortController: opts.abortController,
70| priorMtime: opts.priorMtime,
71| }
72| registerTask(task, setAppState)
73| return id
74| }
源码引用: src/tasks/DreamTask/DreamTask.ts · 第 76–104 行(共 158 行)
76| export function addDreamTurn(
77| taskId: string,
78| turn: DreamTurn,
79| touchedPaths: string[],
80| setAppState: SetAppState,
81| ): void {
82| updateTaskState<DreamTaskState>(taskId, setAppState, task => {
83| const seen = new Set(task.filesTouched)
84| const newTouched = touchedPaths.filter(p => !seen.has(p) && seen.add(p))
85| // Skip the update entirely if the turn is empty AND nothing new was
86| // touched. Avoids re-rendering on pure no-ops.
87| if (
88| turn.text === '' &&
89| turn.toolUseCount === 0 &&
90| newTouched.length === 0
91| ) {
92| return task
93| }
94| return {
95| ...task,
96| phase: newTouched.length > 0 ? 'updating' : task.phase,
97| filesTouched:
98| newTouched.length > 0
99| ? [...task.filesTouched, ...newTouched]
100| : task.filesTouched,
101| turns: task.turns.slice(-(MAX_TURNS - 1)).concat(turn),
102| }
103| })
104| }
源码引用: src/tasks/DreamTask/DreamTask.ts · 第 106–157 行(共 158 行)
106| export function completeDreamTask(
107| taskId: string,
108| setAppState: SetAppState,
109| ): void {
110| // notified: true immediately — dream has no model-facing notification path
111| // (it's UI-only), and eviction requires terminal + notified. The inline
112| // appendSystemMessage completion note IS the user surface.
113| updateTaskState<DreamTaskState>(taskId, setAppState, task => ({
114| ...task,
115| status: 'completed',
116| endTime: Date.now(),
117| notified: true,
118| abortController: undefined,
119| }))
120| }
121|
122| export function failDreamTask(taskId: string, setAppState: SetAppState): void {
123| updateTaskState<DreamTaskState>(taskId, setAppState, task => ({
124| ...task,
125| status: 'failed',
126| endTime: Date.now(),
127| notified: true,
128| abortController: undefined,
129| }))
130| }
131|
132| export const DreamTask: Task = {
133| name: 'DreamTask',
134| type: 'dream',
135|
136| async kill(taskId, setAppState) {
137| let priorMtime: number | undefined
138| updateTaskState<DreamTaskState>(taskId, setAppState, task => {
139| if (task.status !== 'running') return task
140| task.abortController?.abort()
141| priorMtime = task.priorMtime
142| return {
143| ...task,
144| status: 'killed',
145| endTime: Date.now(),
146| notified: true,
147| abortController: undefined,
148| }
149| })
150| // Rewind the lock mtime so the next session can retry. Same path as the
151| // fork-failure catch in autoDream.ts. If updateTaskState was a no-op
152| // (already terminal), priorMtime stays undefined and we skip.
153| if (priorMtime !== undefined) {
154| await rollbackConsolidationLock(priorMtime)
155| }
156| },
157| }
getPillLabel 分支详解
getPillLabel(tasks) 先判 allSameType = tasks.every(t => t.type === tasks[0].type):
local_bash(同质):
- 拆分 kind===monitor vs 普通 shell
- "1 shell" / "N shells" + "1 monitor" / "N monitors" 逗号连接
in_process_teammate:按 identity.teamName Set 计数 → "1 team" / "N teams"
local_agent:"1 local agent" / "N local agents"
remote_agent:
- 单任务 ultraplan:ultraplanPhase plan_ready → ◆ filled "ultraplan ready";needs_input → ◇ "needs your input";default ◇ "ultraplan"
- 否则 ◇ "N cloud sessions"
local_workflow:"N background workflow(s)"
monitor_mcp:"N monitor(s)"(stub type 预留)
dream:固定 "dreaming"
异构混合 → "${n} background task(s)"
源码引用: src/tasks/pillLabel.ts · 第 10–67 行(共 83 行)
10| export function getPillLabel(tasks: BackgroundTaskState[]): string {
11| const n = tasks.length
12| const allSameType = tasks.every(t => t.type === tasks[0]!.type)
13|
14| if (allSameType) {
15| switch (tasks[0]!.type) {
16| case 'local_bash': {
17| const monitors = count(
18| tasks,
19| t => t.type === 'local_bash' && t.kind === 'monitor',
20| )
21| const shells = n - monitors
22| const parts: string[] = []
23| if (shells > 0)
24| parts.push(shells === 1 ? '1 shell' : `${shells} shells`)
25| if (monitors > 0)
26| parts.push(monitors === 1 ? '1 monitor' : `${monitors} monitors`)
27| return parts.join(', ')
28| }
29| case 'in_process_teammate': {
30| const teamCount = new Set(
31| tasks.map(t =>
32| t.type === 'in_process_teammate' ? t.identity.teamName : '',
33| ),
34| ).size
35| return teamCount === 1 ? '1 team' : `${teamCount} teams`
36| }
37| case 'local_agent':
38| return n === 1 ? '1 local agent' : `${n} local agents`
39| case 'remote_agent': {
40| const first = tasks[0]!
41| // Per design mockup: ◇ open diamond while running/needs-input,
42| // ◆ filled once ExitPlanMode is awaiting approval.
43| if (n === 1 && first.type === 'remote_agent' && first.isUltraplan) {
44| switch (first.ultraplanPhase) {
45| case 'plan_ready':
46| return `${DIAMOND_FILLED} ultraplan ready`
47| case 'needs_input':
48| return `${DIAMOND_OPEN} ultraplan needs your input`
49| default:
50| return `${DIAMOND_OPEN} ultraplan`
51| }
52| }
53| return n === 1
54| ? `${DIAMOND_OPEN} 1 cloud session`
55| : `${DIAMOND_OPEN} ${n} cloud sessions`
56| }
57| case 'local_workflow':
58| return n === 1 ? '1 background workflow' : `${n} background workflows`
59| case 'monitor_mcp':
60| return n === 1 ? '1 monitor' : `${n} monitors`
61| case 'dream':
62| return 'dreaming'
63| }
64| }
65|
66| return `${n} background ${n === 1 ? 'task' : 'tasks'}`
67| }
pillNeedsCta 与 ultraplan 状态图
pillNeedsCta(tasks) 返回 footer 是否显示 dimmed " · ↓ to view":
条件全部满足:
- tasks.length === 1
- task.type === 'remote_agent'
- task.isUltraplan === true
- task.ultraplanPhase !== undefined
即 needs_input 与 plan_ready 两种 attention 态 surface CTA;plain running 仅 diamond + label。
RemoteAgentTask poller / ExitPlanModeScanner 写入 ultraplanPhase;UI BackgroundTasksIndicator 消费 pillNeedsCta 控制 CTA 渲染。
设计 mockup 注释:◇ open while running/needs-input,◆ filled once ExitPlanMode awaiting approval。
源码引用: src/tasks/pillLabel.ts · 第 69–82 行(共 83 行)
69| /**
70| * True when the pill should show the dimmed " · ↓ to view" call-to-action.
71| * Per the state diagram: only the two attention states (needs_input,
72| * plan_ready) surface the CTA; plain running shows just the diamond + label.
73| */
74| export function pillNeedsCta(tasks: BackgroundTaskState[]): boolean {
75| if (tasks.length !== 1) return false
76| const t = tasks[0]!
77| return (
78| t.type === 'remote_agent' &&
79| t.isUltraplan === true &&
80| t.ultraplanPhase !== undefined
81| )
82| }
源码引用: src/tasks/RemoteAgentTask/RemoteAgentTask.tsx · 第 51–58 行(共 1103 行)
51| import {
52| archiveRemoteSession,
53| pollRemoteSessionEvents,
54| } from '../../utils/teleport.js'
55| import type { TodoList } from '../../utils/todo/types.js'
56| import type { UltraplanPhase } from '../../utils/ultraplan/ccrSession.js'
57|
58| export type RemoteAgentTaskState = TaskStateBase & {
MonitorMcpTask stub
MonitorMcpTask.ts 当前:
export type MonitorMcpTaskState = Record<string, unknown>
export function isMonitorMcpTask(_value: unknown): boolean {
return false
}
与 LocalShellTask kind='monitor' 的区别:
| 维度 | local_bash + kind=monitor | monitor_mcp (stub) |
|---|---|---|
| 实现 | LocalShellTask.tsx 完整 | 无 Task 实现 |
| 触发 | BashTool MONITOR_TOOL | 未来 MCP monitor |
| pill | getPillLabel local_bash 分支内计数 | 独立 monitor_mcp 分支 |
| isBackgroundTask | 可用 | 永不匹配 |
types.ts 已纳入 MonitorMcpTaskState 联合;pillLabel 已写 monitor_mcp case。待 product 实现 MCP monitor runner 后填充。
源码引用: src/tasks/MonitorMcpTask/MonitorMcpTask.ts · 第 1–5 行(共 6 行)
1| export type MonitorMcpTaskState = Record<string, unknown>
2|
3| export function isMonitorMcpTask(_value: unknown): boolean {
4| return false
5| }
源码引用: src/tasks/types.ts · 第 22–29 行(共 47 行)
22| export type BackgroundTaskState =
23| | LocalShellTaskState
24| | LocalAgentTaskState
25| | RemoteAgentTaskState
26| | InProcessTeammateTaskState
27| | LocalWorkflowTaskState
28| | MonitorMcpTaskState
29| | DreamTaskState
isBackgroundTask 与 Dream
isBackgroundTask(types.ts)对 dream 任务:
- dream 注册时 status: running,无 isBackgrounded 字段
- 谓词第三段:'isBackgrounded' in task && task.isBackgrounded === false → false;dream 无此字段 → 通过
- 故 running dream 始终出现在 background pill(文案 "dreaming")
Dream complete 后 status 变 terminal,isBackgroundTask 返回 false,pill 自动消失。
Contrast local_agent foreground:显式 isBackgrounded: false 隐藏 pill 直至 background。
源码引用: src/tasks/types.ts · 第 37–46 行(共 47 行)
37| export function isBackgroundTask(task: TaskState): task is BackgroundTaskState {
38| if (task.status !== 'running' && task.status !== 'pending') {
39| return false
40| }
41| // Foreground tasks (isBackgrounded === false) are not yet "background tasks"
42| if ('isBackgrounded' in task && task.isBackgrounded === false) {
43| return false
44| }
45| return true
46| }
源码引用: src/tasks/DreamTask/DreamTask.ts · 第 43–50 行(共 158 行)
43| export function isDreamTask(task: unknown): task is DreamTaskState {
44| return (
45| typeof task === 'object' &&
46| task !== null &&
47| 'type' in task &&
48| task.type === 'dream'
49| )
50| }
UI 消费方与调试
pillLabel 消费方:
- components/BackgroundTasksIndicator — footer pill 主入口
- turn-duration transcript line — 注释要求 terminology 一致
- useBackgroundTaskNavigation — Shift+Down 对话框标题可能 derive 自同类逻辑
调试 pill 文案错误:打印 getPillLabel(filteredTasks) 输入集合,确认 allSameType 与 type 字段。Ultraplan CTA 不显示:查 ultraplanPhase 是否 undefined(running 态正常无 CTA)。
Dream pill 不消失:查 completeDreamTask 是否调用、status 是否 terminal。Dream kill 后 consolidation 不重试:查 rollbackConsolidationLock 是否执行。
本章小结与延伸
DreamTask = autoDream 的 pill 适配层。pillLabel = footer/transcript 文案 SSOT。MonitorMcpTask = 预留 stub。 继续学习: