本章总览
tasks/LocalShellTask/(TSX 约 520 行 + guards 41 行 + killShellTasks 77 行)实现 type=local_bash 的后台 shell:spawnShellTask 注册 running 任务、ShellCommand.background _detach 进程、stall watchdog 检测交互式 prompt、完成时 enqueueShellNotification 发送 XML。stopTask.ts(100 行)是 TaskStopTool 与 SDK stop_task 的共享入口,通过 getTaskByType(task.type).kill 委派具体实现,并对 bash 任务抑制 exit 137 噪声 notification。LocalWorkflowTask/ 当前为 stub(isLocalWorkflowTask 恒 false),预留 local_workflow type。本章要求你能从 BashTool background 分支追踪 spawnShellTask,以及从 stopTask 理解 StopTaskError 三分支与 emitTaskTerminatedSdk 补偿。
学完本章你应该能
- 说明 LocalShellTaskState 字段(kind、agentId、isBackgrounded、shellCommand)
- 解释 guards.ts 从 TSX 拆分的 module graph 动机
- 理解 spawnShellTask vs registerForeground vs backgroundTask 三路径
- 掌握 startStallWatchdog 与 looksLikePrompt 的 CC-1175 交互检测
- 阅读 stopTask 对 bash vs agent 的 notified 抑制差异
核心概念(先读懂这些)
local_bash 保留历史 type 名
LocalShellTaskState.type 仍为 local_bash(非 local_shell),兼容 persisted session state。kind 区分 bash 与 monitor:monitor 用 description 作 pill 文案,completion summary 不含 BACKGROUND_BASH_SUMMARY_PREFIX。
TaskOutput 拥有 taskId
spawnShellTask 从 shellCommand.taskOutput.taskId 取 ID,保证 BashTool TaskOutput 组件与 AppState.tasks 键一致。数据经 TaskOutput 自动落盘,无需 stream listener。
stopTask 统一 kill 入口
stopTask 查 AppState.tasks[taskId],校验 running,getTaskByType → kill。LocalShell kill 设 notified=true 抑制后续 exit notification;stopTask 对 bash 额外 emitTaskTerminatedSdk 补偿 SDK。Agent kill 保留 partial result notification。
建议学习步骤
- 阅读 guards.ts LocalShellTaskState 与 isLocalShellTask
- 阅读 spawnShellTask 与 enqueueShellNotification
- 阅读 registerForeground 与 backgroundTask
- 阅读 killShellTasks.ts killTask 与 killShellTasksForAgent
- 阅读 startStallWatchdog 与 looksLikePrompt
- 阅读 stopTask.ts StopTaskError 与 bash 抑制逻辑
- 对照 LocalWorkflowTask stub
常见误区
注意
stall notification 故意无 status 标签,避免 SDK 误判 completed
注意
killTask 立即 notified=true,与 natural exit 的 enqueueShellNotification 竞态
注意
agentId undefined 表示主线程 spawn;runAgent finally 必须 killShellTasksForAgent
注意
LocalWorkflowTask 尚未实现,勿在 runtime 期望 local_workflow 任务存在
目录结构与 module graph 拆分
LocalShellTask 三文件分工:
| 文件 | 职责 | 拆分原因 |
|---|---|---|
| LocalShellTask.tsx | spawn、background、notification、stall watchdog | React/Ink 边界 |
| guards.ts | LocalShellTaskState 类型、isLocalShellTask | stopTask/print.ts 免拉 React |
| killShellTasks.ts | killTask、killShellTasksForAgent | runAgent.ts finally 免拉 Ink |
LocalWorkflowTask.ts 仅 export stub type 与 isLocalWorkflowTask→false,占位 future workflow runner。
stopTask.ts 在 tasks/ 根目录,import guards 的 isLocalShellTask,import tasks.js 的 getTaskByType。
LocalShellTaskState 字段
LocalShellTaskState 扩展 TaskStateBase:
- command — 原始 shell 命令字符串
- result — { code, interrupted } 终端结果
- shellCommand — ShellCommand 运行时句柄,kill 时 null
- completionStatusSentInAttachment — BashTool attachment 去重
- lastReportedTotalLines — SDK/bash progress delta
- isBackgrounded — foreground hint → Ctrl+B background 翻转
- agentId — _spawn 该 bash 的 agent;undefined=主线程
- kind — 'bash' | 'monitor';影响 pill、notification 文案、stall watchdog(monitor 跳过)
type 字段恒为 local_bash,历史兼容 persisted JSON。
源码引用: src/tasks/LocalShellTask/guards.ts · 第 9–41 行(共 42 行)
9| export type BashTaskKind = 'bash' | 'monitor'
10|
11| export type LocalShellTaskState = TaskStateBase & {
12| type: 'local_bash' // Keep as 'local_bash' for backward compatibility with persisted session state
13| command: string
14| result?: {
15| code: number
16| interrupted: boolean
17| }
18| completionStatusSentInAttachment: boolean
19| shellCommand: ShellCommand | null
20| unregisterCleanup?: () => void
21| cleanupTimeoutId?: NodeJS.Timeout
22| // Track what we last reported for computing deltas (total lines from TaskOutput)
23| lastReportedTotalLines: number
24| // Whether the task has been backgrounded (false = foreground running, true = backgrounded)
25| isBackgrounded: boolean
26| // Agent that spawned this task. Used to kill orphaned bash tasks when the
27| // agent exits (see killShellTasksForAgent). Undefined = main thread.
28| agentId?: AgentId
29| // UI display variant. 'monitor' → shows description instead of command,
30| // 'Monitor details' dialog title, distinct status bar pill.
31| kind?: BashTaskKind
32| }
33|
34| export function isLocalShellTask(task: unknown): task is LocalShellTaskState {
35| return (
36| typeof task === 'object' &&
37| task !== null &&
38| 'type' in task &&
39| task.type === 'local_bash'
40| )
41| }
spawnShellTask 与 notification
spawnShellTask 流程:
- taskId = shellCommand.taskOutput.taskId
- registerCleanup → killTask
- createTaskStateBase(taskId, 'local_bash', description, toolUseId)
- isBackgrounded: true,registerTask
- shellCommand.background(taskId)
- startStallWatchdog(非 monitor)
- shellCommand.result.then → update status、enqueueShellNotification、evictTaskOutput
enqueueShellNotification 原子 notified check,abortSpeculation。Monitor kind 用 "Monitor "..." stream ended" 文案,与 bash 的 BACKGROUND_BASH_SUMMARY_PREFIX 区分,避免 UI collapse 混折叠。
BACKGROUND_BASH_SUMMARY_PREFIX = 'Background command ',供 message collapse transform 识别。
源码引用: src/tasks/LocalShellTask/LocalShellTask.tsx · 第 22–42 行(共 652 行)
22| import { registerCleanup } from '../../utils/cleanupRegistry.js'
23| import { tailFile } from '../../utils/fsOperations.js'
24| import { logError } from '../../utils/log.js'
25| import { enqueuePendingNotification } from '../../utils/messageQueueManager.js'
26| import type { ShellCommand } from '../../utils/ShellCommand.js'
27| import {
28| evictTaskOutput,
29| getTaskOutputPath,
30| } from '../../utils/task/diskOutput.js'
31| import { registerTask, updateTaskState } from '../../utils/task/framework.js'
32| import { escapeXml } from '../../utils/xml.js'
33| import {
34| backgroundAgentTask,
35| isLocalAgentTask,
36| } from '../LocalAgentTask/LocalAgentTask.js'
37| import { isMainSessionTask } from '../LocalMainSessionTask.js'
38| import {
39| type BashTaskKind,
40| isLocalShellTask,
41| type LocalShellTaskState,
42| } from './guards.js'
源码引用: src/tasks/LocalShellTask/LocalShellTask.tsx · 第 105–172 行(共 652 行)
105| // overlapping tick's callback sees cancelled=true and bails.
106| cancelled = true
107| clearInterval(timer)
108| const toolUseIdLine = toolUseId
109| ? `\n<${TOOL_USE_ID_TAG}>${toolUseId}</${TOOL_USE_ID_TAG}>`
110| : ''
111| const summary = `${BACKGROUND_BASH_SUMMARY_PREFIX}"${description}" appears to be waiting for interactive input`
112| // No <status> tag — print.ts treats <status> as a terminal
113| // signal and an unknown value falls through to 'completed',
114| // falsely closing the task for SDK consumers. Statusless
115| // notifications are skipped by the SDK emitter (progress ping).
116| const message = `<${TASK_NOTIFICATION_TAG}>
117| <${TASK_ID_TAG}>${taskId}</${TASK_ID_TAG}>${toolUseIdLine}
118| <${OUTPUT_FILE_TAG}>${outputPath}</${OUTPUT_FILE_TAG}>
119| <${SUMMARY_TAG}>${escapeXml(summary)}</${SUMMARY_TAG}>
120| </${TASK_NOTIFICATION_TAG}>
121| Last output:
122| ${content.trimEnd()}
123|
124| The command is likely blocked on an interactive prompt. Kill this task and re-run with piped input (e.g., \`echo y | command\`) or a non-interactive flag if one exists.`
125| enqueuePendingNotification({
126| value: message,
127| mode: 'task-notification',
128| priority: 'next',
129| agentId,
130| })
131| },
132| () => {},
133| )
134| },
135| () => {}, // File may not exist yet
136| )
137| }, STALL_CHECK_INTERVAL_MS)
138| timer.unref()
139|
140| return () => {
141| cancelled = true
142| clearInterval(timer)
143| }
144| }
145|
146| function enqueueShellNotification(
147| taskId: string,
148| description: string,
149| status: 'completed' | 'failed' | 'killed',
150| exitCode: number | undefined,
151| setAppState: SetAppState,
152| toolUseId?: string,
153| kind: BashTaskKind = 'bash',
154| agentId?: AgentId,
155| ): void {
156| // Atomically check and set notified flag to prevent duplicate notifications.
157| // If the task was already marked as notified (e.g., by TaskStopTool), skip
158| // enqueueing to avoid sending redundant messages to the model.
159| let shouldEnqueue = false
160| updateTaskState(taskId, setAppState, task => {
161| if (task.notified) {
162| return task
163| }
164| shouldEnqueue = true
165| return { ...task, notified: true }
166| })
167|
168| if (!shouldEnqueue) {
169| return
170| }
171|
172| // Abort any active speculation — background task state changed, so speculated
源码引用: src/tasks/LocalShellTask/LocalShellTask.tsx · 第 180–252 行(共 652 行)
180| // the stream ended, not "condition met". Distinct from the bash prefix
181| // so Monitor completions don't fold into the "N background commands
182| // completed" collapse.
183| switch (status) {
184| case 'completed':
185| summary = `Monitor "${description}" stream ended`
186| break
187| case 'failed':
188| summary = `Monitor "${description}" script failed${exitCode !== undefined ? ` (exit ${exitCode})` : ''}`
189| break
190| case 'killed':
191| summary = `Monitor "${description}" stopped`
192| break
193| }
194| } else {
195| switch (status) {
196| case 'completed':
197| summary = `${BACKGROUND_BASH_SUMMARY_PREFIX}"${description}" completed${exitCode !== undefined ? ` (exit code ${exitCode})` : ''}`
198| break
199| case 'failed':
200| summary = `${BACKGROUND_BASH_SUMMARY_PREFIX}"${description}" failed${exitCode !== undefined ? ` with exit code ${exitCode}` : ''}`
201| break
202| case 'killed':
203| summary = `${BACKGROUND_BASH_SUMMARY_PREFIX}"${description}" was stopped`
204| break
205| }
206| }
207|
208| const outputPath = getTaskOutputPath(taskId)
209| const toolUseIdLine = toolUseId
210| ? `\n<${TOOL_USE_ID_TAG}>${toolUseId}</${TOOL_USE_ID_TAG}>`
211| : ''
212| const message = `<${TASK_NOTIFICATION_TAG}>
213| <${TASK_ID_TAG}>${taskId}</${TASK_ID_TAG}>${toolUseIdLine}
214| <${OUTPUT_FILE_TAG}>${outputPath}</${OUTPUT_FILE_TAG}>
215| <${STATUS_TAG}>${status}</${STATUS_TAG}>
216| <${SUMMARY_TAG}>${escapeXml(summary)}</${SUMMARY_TAG}>
217| </${TASK_NOTIFICATION_TAG}>`
218|
219| enqueuePendingNotification({
220| value: message,
221| mode: 'task-notification',
222| priority: feature('MONITOR_TOOL') ? 'next' : 'later',
223| agentId,
224| })
225| }
226|
227| export const LocalShellTask: Task = {
228| name: 'LocalShellTask',
229| type: 'local_bash',
230| async kill(taskId, setAppState) {
231| killTask(taskId, setAppState)
232| },
233| }
234|
235| export async function spawnShellTask(
236| input: LocalShellSpawnInput & { shellCommand: ShellCommand },
237| context: TaskContext,
238| ): Promise<TaskHandle> {
239| const { command, description, shellCommand, toolUseId, agentId, kind } = input
240| const { setAppState } = context
241|
242| // TaskOutput owns the data — use its taskId so disk writes are consistent
243| const { taskOutput } = shellCommand
244| const taskId = taskOutput.taskId
245|
246| const unregisterCleanup = registerCleanup(async () => {
247| killTask(taskId, setAppState)
248| })
249|
250| const taskState: LocalShellTaskState = {
251| ...createTaskStateBase(taskId, 'local_bash', description, toolUseId),
252| type: 'local_bash',
Foreground 与 background 转换
registerForeground — BashTool 长时间前台命令:isBackgrounded: false,返回 taskId 供 BackgroundHint 使用。
backgroundTask(内部,LocalShellTask.tsx):
- 校验 isLocalShellTask && !isBackgrounded && shellCommand
- shellCommand.background(taskId)
- setAppState flip isBackgrounded: true
- 安装 result handler + stall watchdog(与 spawn 路径对称)
BashTool Ctrl+B 调用 backgroundTask;超时 auto-background 类似 agent 路径。
LocalShellTask Task 实现:type local_bash,kill → killTask。
源码引用: src/tasks/LocalShellTask/LocalShellTask.tsx · 第 173–179 行(共 652 行)
173| // results may reference stale task output. The prompt suggestion text is
174| // preserved; only the pre-computed response is discarded.
175| abortSpeculation(setAppState)
176|
177| let summary: string
178| if (feature('MONITOR_TOOL') && kind === 'monitor') {
179| // Monitor is streaming-only (post-#22764) — the script exiting means
源码引用: src/tasks/LocalShellTask/LocalShellTask.tsx · 第 259–327 行(共 652 行)
259| isBackgrounded: true,
260| agentId,
261| kind,
262| }
263|
264| registerTask(taskState, setAppState)
265|
266| // Data flows through TaskOutput automatically — no stream listeners needed.
267| // Just transition to backgrounded state so the process keeps running.
268| shellCommand.background(taskId)
269|
270| const cancelStallWatchdog = startStallWatchdog(
271| taskId,
272| description,
273| kind,
274| toolUseId,
275| agentId,
276| )
277|
278| void shellCommand.result.then(async result => {
279| cancelStallWatchdog()
280| await flushAndCleanup(shellCommand)
281| let wasKilled = false
282|
283| updateTaskState<LocalShellTaskState>(taskId, setAppState, task => {
284| if (task.status === 'killed') {
285| wasKilled = true
286| return task
287| }
288|
289| return {
290| ...task,
291| status: result.code === 0 ? 'completed' : 'failed',
292| result: { code: result.code, interrupted: result.interrupted },
293| shellCommand: null,
294| unregisterCleanup: undefined,
295| endTime: Date.now(),
296| }
297| })
298|
299| enqueueShellNotification(
300| taskId,
301| description,
302| wasKilled ? 'killed' : result.code === 0 ? 'completed' : 'failed',
303| result.code,
304| setAppState,
305| toolUseId,
306| kind,
307| agentId,
308| )
309|
310| void evictTaskOutput(taskId)
311| })
312|
313| return {
314| taskId,
315| cleanup: () => {
316| unregisterCleanup()
317| },
318| }
319| }
320|
321| /**
322| * Register a foreground task that could be backgrounded later.
323| * Called when a bash command has been running long enough to show the BackgroundHint.
324| * @returns taskId for the registered task
325| */
326| export function registerForeground(
327| input: LocalShellSpawnInput & { shellCommand: ShellCommand },
Stall watchdog(CC-1175)
startStallWatchdog 每 STALL_CHECK_INTERVAL_MS=5s 检查 output 文件 size:
- size 增长 → 重置 lastGrowth
- 45s 无增长 → tailFile 1024 字节
- looksLikePrompt 匹配末行:(y/n)、Press Enter、Continue? 等
- 非 prompt → 重置 lastGrowth 继续观察(避免慢 git log 误报)
- prompt → 发送无 status 的 task-notification,priority next,附 last output 片段
Monitor kind 直接 no-op watchdog(流式脚本无交互 prompt 语义)。
设计意图:仅在有 actionable prompt 时通知模型,而非一切 stall。
源码引用: src/tasks/LocalShellTask/LocalShellTask.tsx · 第 46–104 行(共 652 行)
46| export const BACKGROUND_BASH_SUMMARY_PREFIX = 'Background command '
47|
48| const STALL_CHECK_INTERVAL_MS = 5_000
49| const STALL_THRESHOLD_MS = 45_000
50| const STALL_TAIL_BYTES = 1024
51|
52| // Last-line patterns that suggest a command is blocked waiting for keyboard
53| // input. Used to gate the stall notification — we stay silent on commands that
54| // are merely slow (git log -S, long builds) and only notify when the tail
55| // looks like an interactive prompt the model can act on. See CC-1175.
56| const PROMPT_PATTERNS = [
57| /\(y\/n\)/i, // (Y/n), (y/N)
58| /\[y\/n\]/i, // [Y/n], [y/N]
59| /\(yes\/no\)/i,
60| /\b(?:Do you|Would you|Shall I|Are you sure|Ready to)\b.*\? *$/i, // directed questions
61| /Press (any key|Enter)/i,
62| /Continue\?/i,
63| /Overwrite\?/i,
64| ]
65|
66| export function looksLikePrompt(tail: string): boolean {
67| const lastLine = tail.trimEnd().split('\n').pop() ?? ''
68| return PROMPT_PATTERNS.some(p => p.test(lastLine))
69| }
70|
71| // Output-side analog of peekForStdinData (utils/process.ts): fire a one-shot
72| // notification if output stops growing and the tail looks like a prompt.
73| function startStallWatchdog(
74| taskId: string,
75| description: string,
76| kind: BashTaskKind | undefined,
77| toolUseId?: string,
78| agentId?: AgentId,
79| ): () => void {
80| if (kind === 'monitor') return () => {}
81| const outputPath = getTaskOutputPath(taskId)
82| let lastSize = 0
83| let lastGrowth = Date.now()
84| let cancelled = false
85|
86| const timer = setInterval(() => {
87| void stat(outputPath).then(
88| s => {
89| if (s.size > lastSize) {
90| lastSize = s.size
91| lastGrowth = Date.now()
92| return
93| }
94| if (Date.now() - lastGrowth < STALL_THRESHOLD_MS) return
95| void tailFile(outputPath, STALL_TAIL_BYTES).then(
96| ({ content }) => {
97| if (cancelled) return
98| if (!looksLikePrompt(content)) {
99| // Not a prompt — keep watching. Reset so the next check is
100| // 45s out instead of re-reading the tail on every tick.
101| lastGrowth = Date.now()
102| return
103| }
104| // Latch before the async-boundary-visible side effects so an
killShellTasks
killTask:
- updateTaskState:running + isLocalShellTask
- shellCommand.kill() + cleanup()
- clear unregisterCleanup、cleanupTimeoutId
- status: killed, notified: true, shellCommand: null
- evictTaskOutput
killShellTasksForAgent(agentId) — runAgent.ts finally 调用:
- 扫描 tasks 中 agentId 匹配且 running 的 local_bash
- 逐个 killTask(防止 fake-logs.sh 僵尸进程)
- dequeueAllMatching(cmd => cmd.agentId === agentId) 清 pending notification
kill 时 notified=true 意味着 natural completion handler 会 skip enqueue。
源码引用: src/tasks/LocalShellTask/killShellTasks.ts · 第 16–76 行(共 77 行)
16| export function killTask(taskId: string, setAppState: SetAppStateFn): void {
17| updateTaskState(taskId, setAppState, task => {
18| if (task.status !== 'running' || !isLocalShellTask(task)) {
19| return task
20| }
21|
22| try {
23| logForDebugging(`LocalShellTask ${taskId} kill requested`)
24| task.shellCommand?.kill()
25| task.shellCommand?.cleanup()
26| } catch (error) {
27| logError(error)
28| }
29|
30| task.unregisterCleanup?.()
31| if (task.cleanupTimeoutId) {
32| clearTimeout(task.cleanupTimeoutId)
33| }
34|
35| return {
36| ...task,
37| status: 'killed',
38| notified: true,
39| shellCommand: null,
40| unregisterCleanup: undefined,
41| cleanupTimeoutId: undefined,
42| endTime: Date.now(),
43| }
44| })
45| void evictTaskOutput(taskId)
46| }
47|
48| /**
49| * Kill all running bash tasks spawned by a given agent.
50| * Called from runAgent.ts finally block so background processes don't outlive
51| * the agent that started them (prevents 10-day fake-logs.sh zombies).
52| */
53| export function killShellTasksForAgent(
54| agentId: AgentId,
55| getAppState: () => AppState,
56| setAppState: SetAppStateFn,
57| ): void {
58| const tasks = getAppState().tasks ?? {}
59| for (const [taskId, task] of Object.entries(tasks)) {
60| if (
61| isLocalShellTask(task) &&
62| task.agentId === agentId &&
63| task.status === 'running'
64| ) {
65| logForDebugging(
66| `killShellTasksForAgent: killing orphaned shell task ${taskId} (agent ${agentId} exiting)`,
67| )
68| killTask(taskId, setAppState)
69| }
70| }
71| // Purge any queued notifications addressed to this agent — its query loop
72| // has exited and won't drain them. killTask fires 'killed' notifications
73| // asynchronously; drop the ones already queued and any that land later sit
74| // harmlessly (no consumer matches a dead agentId).
75| dequeueAllMatching(cmd => cmd.agentId === agentId)
76| }
stopTask.ts 统一停止
StopTaskError codes:not_found | not_running | unsupported_type
stopTask(taskId, context) 步骤:
- appState.tasks[taskId] 存在性
- status === 'running'
- getTaskByType(task.type)?.kill(taskId, setAppState)
- 若 isLocalShellTask:原子设 notified=true,emitTaskTerminatedSdk('stopped') 补偿 SDK(因抑制了 XML)
- 返回 { taskId, taskType, command|description }
注释明确:Bash suppress exit 137 notification;Agent 不 suppress,AbortError catch 发送 partial result。
TaskStopTool 与 SDK control request 共用此函数,保证语义一致。
源码引用: src/tasks/stopTask.ts · 第 10–18 行(共 101 行)
10| export class StopTaskError extends Error {
11| constructor(
12| message: string,
13| public readonly code: 'not_found' | 'not_running' | 'unsupported_type',
14| ) {
15| super(message)
16| this.name = 'StopTaskError'
17| }
18| }
源码引用: src/tasks/stopTask.ts · 第 38–100 行(共 101 行)
38| export async function stopTask(
39| taskId: string,
40| context: StopTaskContext,
41| ): Promise<StopTaskResult> {
42| const { getAppState, setAppState } = context
43| const appState = getAppState()
44| const task = appState.tasks?.[taskId] as TaskStateBase | undefined
45|
46| if (!task) {
47| throw new StopTaskError(`No task found with ID: ${taskId}`, 'not_found')
48| }
49|
50| if (task.status !== 'running') {
51| throw new StopTaskError(
52| `Task ${taskId} is not running (status: ${task.status})`,
53| 'not_running',
54| )
55| }
56|
57| const taskImpl = getTaskByType(task.type)
58| if (!taskImpl) {
59| throw new StopTaskError(
60| `Unsupported task type: ${task.type}`,
61| 'unsupported_type',
62| )
63| }
64|
65| await taskImpl.kill(taskId, setAppState)
66|
67| // Bash: suppress the "exit code 137" notification (noise). Agent tasks: don't
68| // suppress — the AbortError catch sends a notification carrying
69| // extractPartialResult(agentMessages), which is the payload not noise.
70| if (isLocalShellTask(task)) {
71| let suppressed = false
72| setAppState(prev => {
73| const prevTask = prev.tasks[taskId]
74| if (!prevTask || prevTask.notified) {
75| return prev
76| }
77| suppressed = true
78| return {
79| ...prev,
80| tasks: {
81| ...prev.tasks,
82| [taskId]: { ...prevTask, notified: true },
83| },
84| }
85| })
86| // Suppressing the XML notification also suppresses print.ts's parsed
87| // task_notification SDK event — emit it directly so SDK consumers see
88| // the task close.
89| if (suppressed) {
90| emitTaskTerminatedSdk(taskId, 'stopped', {
91| toolUseId: task.toolUseId,
92| summary: task.description,
93| })
94| }
95| }
96|
97| const command = isLocalShellTask(task) ? task.command : task.description
98|
99| return { taskId, taskType: task.type, command }
100| }
LocalWorkflowTask stub
LocalWorkflowTask.ts 当前实现:
export type LocalWorkflowTaskState = Record<string, unknown>
export function isLocalWorkflowTask(_value: unknown): boolean {
return false
}
types.ts 已把 LocalWorkflowTaskState 纳入 TaskState 联合与 BackgroundTaskState,pillLabel 有 'local_workflow' → "N background workflows" 分支,但 runtime 尚未注册真实 workflow 任务。
阅读时将其视为 API 预留:未来 background workflow runner 将填充 type=local_workflow 条目,并 export Task.kill 实现。现阶段调试 workflow pill 不应出现。
源码引用: src/tasks/LocalWorkflowTask/LocalWorkflowTask.ts · 第 1–5 行(共 6 行)
1| export type LocalWorkflowTaskState = Record<string, unknown>
2|
3| export function isLocalWorkflowTask(_value: unknown): boolean {
4| return false
5| }
源码引用: src/tasks/types.ts · 第 17–29 行(共 47 行)
17| | LocalWorkflowTaskState
18| | MonitorMcpTaskState
19| | DreamTaskState
20|
21| // Task types that can appear in the background tasks indicator
22| export type BackgroundTaskState =
23| | LocalShellTaskState
24| | LocalAgentTaskState
25| | RemoteAgentTaskState
26| | InProcessTeammateTaskState
27| | LocalWorkflowTaskState
28| | MonitorMcpTaskState
29| | DreamTaskState
调试清单
| 症状 | 检查点 |
|---|---|
| 后台 bash 无 pill | isBackgrounded;isBackgroundTask 谓词 |
| stop 后仍收 exit 137 | killTask notified;stopTask emitTaskTerminatedSdk |
| agent 退出后 bash 残留 | killShellTasksForAgent 是否在 finally |
| 误报 stall | looksLikePrompt 末行;非 prompt 应继续 watch |
| Monitor 完成被折叠成 bash | kind===monitor 应用独立 summary 前缀 |
Bash notification priority:MONITOR_TOOL feature 下 monitor 用 priority next,普通 bash 用 later。
本章小结与延伸
LocalShellTask = 后台 Bash/Monitor 生命周期 + stall 检测 + XML 通知。stopTask = 跨类型统一停止协议。LocalWorkflowTask 为预留 stub。 继续学习: