Claude Code 源码分析Claude Code 源码分析
首页
源码统计
系统架构
UML 图表
工具系统
CodeGraph
首页
源码统计
系统架构
UML 图表
工具系统
CodeGraph
  • 概览

    • Claude Code 源码分析
    • 源码统计
    • CodeGraph 图谱
  • 架构

    • 系统架构
    • UML 图表索引
    • 查询引擎
    • 核心流程
    • 消息系统
    • 状态管理
  • 功能模块

    • 工具系统
    • 斜杠命令
    • 服务层
    • MCP 协议
    • Skills 技能
    • 子代理系统
  • 分层深度

    • 入口层
    • UI / Ink 层
    • utils 基础设施
    • 桥接 / 远程
    • 上下文压缩
  • 原理与安全

    • 底层原理
    • 技术难点
    • 权限与安全
    • 内部机制
    • 遥测与分析
  • 深度专题

    • Hooks 系统
    • 插件系统
    • 记忆系统
    • API 通信层
    • Ink 终端 UI
    • 认证系统
    • 构建与发布
    • 术语表
  • 调用分析

    • 调用链分析
    • 核心文件索引
  • 模块详解

    • utils

      • 模块: utils
      • messages · 消息工厂与规范化
      • session-storage · JSONL 会话持久化
      • permissions · 工具权限决策
      • shell-hooks · 用户 Shell Hook 系统
    • components

      • 模块: components
      • REPL · 主屏编排
      • messages · 消息行渲染
      • PermissionRequest · 权限弹窗
      • PromptInput · 底部输入
    • services

      • 模块: services
      • api-claude · Anthropic API 流式与重试
      • mcp-client · MCP 连接与工具调用
      • compact · 上下文压缩与自动触发
      • analytics · GrowthBook、Datadog 与 1P 事件
    • tools

      • 模块: tools
      • tool-interface · Tool 契约与注册表
      • bash-tool · Shell 执行与权限
      • streaming-executor · 流式工具并发调度
      • agent-tool · 子 Agent 委派
    • commands

      • 模块: commands
      • command-registry · commands.ts 注册与分派
      • model-command · /model 模型选择
      • mcp-commands · /mcp 服务器管理
      • compact-memory-commands · /compact 与 /memory
    • ink

      • 模块: ink
      • Ink 渲染管线 · Screen 与终端输出
      • 终端事件 · resize、paste、stdin
      • Ink Hooks · 输入、搜索、终端状态
      • Ink 组件 · Box、Text、ScrollBox 原语
    • hooks

      • 模块: hooks
      • useCanUseTool · 权限 UI 接缝
      • 输入与快捷键 Hook
      • 合并态 Hook(MCP + 本地)
      • notifs 通知 Hook
    • bridge

      • 模块: bridge
      • repl-bridge · REPL 桥初始化与传输
      • bridge-messaging · 桥消息路由与入站处理
      • remote-bridge-core · env-less 核心与守护主循环
      • bridge-permissions-ui · 权限、API 与 TUI
    • cli

      • 模块: cli
      • Structured IO · NDJSON SDK 协议
      • CLI Transports · Session Ingress 传输层
      • CLI Handlers · 子命令懒加载实现
      • Update & Upload · 自更新与串行上传原语
    • screens

      • 模块: screens
      • REPL 屏 · Screen 类型与顶层路由
      • ResumeConversation · 会话恢复选择器
      • Doctor · 安装诊断全屏
    • entrypoints

      • 模块: entrypoints
      • cli-entrypoint · Bootstrap 与快路径
      • sdk-types · core / control / runtime 类型体系
      • mcp-entrypoint · MCP stdio 服务器
      • sandbox-types · 沙箱配置单一真相源
    • skills

      • 模块: skills
      • skills-loading · 磁盘加载与 bundled 注册表
      • bundled-skills · 内置 skill 与 initBundledSkills
      • mcp-skills · MCP prompt 转 skill
      • skill-tool-integration · SkillTool 与命令注册
    • types

      • 模块: types
      • message-types · Message 联合与 content blocks
      • tool-permission-types · Tool、Permission、Command 类型
      • api-sdk-types · API 与 Hooks 协议类型
      • misc-types · ids、plugin、generated 与其余类型
    • tasks

      • 模块: tasks
      • local-agent-task · 本地 Agent 与主会话后台化
      • remote-agent-task · 远程 CCR 与 In-Process Teammate
      • shell-workflow-tasks · Bash 后台、Workflow 与 stopTask
      • dream-monitor-tasks · Dream、Monitor MCP 与 pill 文案
    • keybindings

      • 模块: keybindings
      • keybinding-registry · 注册、Provider 与 useKeybinding
      • default-bindings · 默认键位表与平台差异
      • command-bindings · command:* 动态斜杠命令绑定
      • vim-bindings · Vim 模式与 keybindings 边界
    • memdir

      • 模块: memdir
      • memdir-core · 路径、加载与 MEMORY.md
      • memory-extraction · extractMemories 与 SessionMemory
      • memdir-commands · /memory、/remember 与命令集成
    • state

      • 模块: state
      • app-state-core · store、AppState 类型与 Provider
      • app-state-selectors · selectors 与 onChangeAppState
      • teammate-state · 队友视图与 swarm 状态
      • state-boundaries · bootstrap、sessionStorage、FileStateCache
    • query

      • 模块: query
      • query config 与 deps · 配置快照与依赖注入
      • query tokenBudget · +500k 自动续跑
      • query transitions · Continue / Terminal 状态机
      • query stopHooks · Stop 事件与 turn 结束编排
  • 模块详解(扩展)

    • messages · 消息工厂与规范化
    • session-storage · JSONL 会话持久化
    • permissions · 工具权限决策
    • shell-hooks · 用户 Shell Hook 系统
    • REPL · 主屏编排
    • messages · 消息行渲染
    • PermissionRequest · 权限弹窗
    • PromptInput · 底部输入
    • api-claude · Anthropic API 流式与重试
    • mcp-client · MCP 连接与工具调用
    • compact · 上下文压缩与自动触发
    • analytics · GrowthBook、Datadog 与 1P 事件
    • tool-interface · Tool 契约与注册表
    • bash-tool · Shell 执行与权限
    • streaming-executor · 流式工具并发调度
    • agent-tool · 子 Agent 委派
    • command-registry · commands.ts 注册与分派
    • model-command · /model 模型选择
    • mcp-commands · /mcp 服务器管理
    • compact-memory-commands · /compact 与 /memory
    • Ink 渲染管线 · Screen 与终端输出
    • 终端事件 · resize、paste、stdin
    • Ink Hooks · 输入、搜索、终端状态
    • Ink 组件 · Box、Text、ScrollBox 原语
    • useCanUseTool · 权限 UI 接缝
    • 输入与快捷键 Hook
    • 合并态 Hook(MCP + 本地)
    • notifs 通知 Hook
    • repl-bridge · REPL 桥初始化与传输
    • bridge-messaging · 桥消息路由与入站处理
    • remote-bridge-core · env-less 核心与守护主循环
    • bridge-permissions-ui · 权限、API 与 TUI
    • Structured IO · NDJSON SDK 协议
    • CLI Transports · Session Ingress 传输层
    • CLI Handlers · 子命令懒加载实现
    • Update & Upload · 自更新与串行上传原语
    • REPL 屏 · Screen 类型与顶层路由
    • ResumeConversation · 会话恢复选择器
    • Doctor · 安装诊断全屏
    • cli-entrypoint · Bootstrap 与快路径
    • sdk-types · core / control / runtime 类型体系
    • mcp-entrypoint · MCP stdio 服务器
    • sandbox-types · 沙箱配置单一真相源
    • skills-loading · 磁盘加载与 bundled 注册表
    • bundled-skills · 内置 skill 与 initBundledSkills
    • mcp-skills · MCP prompt 转 skill
    • skill-tool-integration · SkillTool 与命令注册
    • message-types · Message 联合与 content blocks
    • tool-permission-types · Tool、Permission、Command 类型
    • api-sdk-types · API 与 Hooks 协议类型
    • misc-types · ids、plugin、generated 与其余类型
    • local-agent-task · 本地 Agent 与主会话后台化
    • remote-agent-task · 远程 CCR 与 In-Process Teammate
    • shell-workflow-tasks · Bash 后台、Workflow 与 stopTask
    • dream-monitor-tasks · Dream、Monitor MCP 与 pill 文案
    • keybinding-registry · 注册、Provider 与 useKeybinding
    • default-bindings · 默认键位表与平台差异
    • command-bindings · command:* 动态斜杠命令绑定
    • vim-bindings · Vim 模式与 keybindings 边界
    • memdir-core · 路径、加载与 MEMORY.md
    • memory-extraction · extractMemories 与 SessionMemory
    • memdir-commands · /memory、/remember 与命令集成
    • app-state-core · store、AppState 类型与 Provider
    • app-state-selectors · selectors 与 onChangeAppState
    • teammate-state · 队友视图与 swarm 状态
    • state-boundaries · bootstrap、sessionStorage、FileStateCache
    • query config 与 deps · 配置快照与依赖注入
    • query tokenBudget · +500k 自动续跑
    • query transitions · Continue / Terminal 状态机
    • query stopHooks · Stop 事件与 turn 结束编排
  • 工具详解

    • tool-interface · Tool 契约与注册表
    • tool-permission-types · Tool、Permission、Command 类型
    • 工具: Bash
    • 工具: PowerShell
    • 工具: Agent
    • 工具: LSP
    • 工具: FileEdit
    • 工具: FileRead
    • 工具: Skill
    • 工具: WebFetch
    • 工具: MCP
    • 工具: SendMessage
    • 工具: FileWrite
    • 工具: Config
    • 工具: Grep
    • 工具: Brief
    • 工具: ExitPlanMode
    • 工具: ToolSearch
    • 工具: NotebookEdit
    • 工具: TaskOutput
    • 工具: WebSearch
    • 工具: ScheduleCron

本章总览

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。

建议学习步骤

  1. 阅读 guards.ts LocalShellTaskState 与 isLocalShellTask
  2. 阅读 spawnShellTask 与 enqueueShellNotification
  3. 阅读 registerForeground 与 backgroundTask
  4. 阅读 killShellTasks.ts killTask 与 killShellTasksForAgent
  5. 阅读 startStallWatchdog 与 looksLikePrompt
  6. 阅读 stopTask.ts StopTaskError 与 bash 抑制逻辑
  7. 对照 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.tsxspawn、background、notification、stall watchdogReact/Ink 边界
guards.tsLocalShellTaskState 类型、isLocalShellTaskstopTask/print.ts 免拉 React
killShellTasks.tskillTask、killShellTasksForAgentrunAgent.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 流程:

  1. taskId = shellCommand.taskOutput.taskId
  2. registerCleanup → killTask
  3. createTaskStateBase(taskId, 'local_bash', description, toolUseId)
  4. isBackgrounded: true,registerTask
  5. shellCommand.background(taskId)
  6. startStallWatchdog(非 monitor)
  7. 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):

  1. 校验 isLocalShellTask && !isBackgrounded && shellCommand
  2. shellCommand.background(taskId)
  3. setAppState flip isBackgrounded: true
  4. 安装 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:

  1. updateTaskState:running + isLocalShellTask
  2. shellCommand.kill() + cleanup()
  3. clear unregisterCleanup、cleanupTimeoutId
  4. status: killed, notified: true, shellCommand: null
  5. 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) 步骤:

  1. appState.tasks[taskId] 存在性
  2. status === 'running'
  3. getTaskByType(task.type)?.kill(taskId, setAppState)
  4. 若 isLocalShellTask:原子设 notified=true,emitTaskTerminatedSdk('stopped') 补偿 SDK(因抑制了 XML)
  5. 返回 { 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 无 pillisBackgrounded;isBackgroundTask 谓词
stop 后仍收 exit 137killTask notified;stopTask emitTaskTerminatedSdk
agent 退出后 bash 残留killShellTasksForAgent 是否在 finally
误报 stalllooksLikePrompt 末行;非 prompt 应继续 watch
Monitor 完成被折叠成 bashkind===monitor 应用独立 summary 前缀

Bash notification priority:MONITOR_TOOL feature 下 monitor 用 priority next,普通 bash 用 later。

本章小结与延伸

LocalShellTask = 后台 Bash/Monitor 生命周期 + stall 检测 + XML 通知。stopTask = 跨类型统一停止协议。LocalWorkflowTask 为预留 stub。 继续学习:

  • dream-monitor-tasks
  • local-agent-task
Prev
remote-agent-task · 远程 CCR 与 In-Process Teammate
Next
dream-monitor-tasks · Dream、Monitor MCP 与 pill 文案