Skip to content
源码分析手册

任务管理:共享账本、状态机与跨会话持久化

在处理复杂、多步骤的任务时,Claude Code 展现出的惊人“记性”并非完全依赖模型的原生记忆,而是源于一套严谨的任务管理系统。这套系统不是屏幕角落的进度条,而是落盘为实、可跨会话共享的任务账本。

它解决了什么问题

任务管理(Task List)本质上是 taskListId 分桶的本地异步任务账本(Disk-backed Shared Ledger)

它与普通 Todo 清单最大的区别在于其“物理存在”:任务并不是简单地写在聊天历史里,而是作为独立的 JSON 文件保存在 ~/.claude/tasks/<taskListId>/ 目录下。

这意味着,即使对话历史因为上下文压缩(Compaction)而被裁剪,或者模型因为某种原因发生了重启,只要这个本地账本还在,Claude 就能通过重新读取任务目录,迅速找回之前的执行进度和待办细节。

实现细节

这套账本的运作逻辑依托于文件锁(File Lock)和一套标准的原子工具。

1. 任务账本的定位与共享

任务目录的路径解析逻辑位于 claude-code-opensource/src/utils/tasks.ts。系统通过 getTaskListId() 确定当前账本的归属:

  • 单兵模式:默认为当前的 sessionId
  • 团队模式:所有成员(Teammates)会强制绑定到同一个 teamName 对应的账本 ID。 这套逻辑使得多个 Agent 进程可以同时对同一个任务池进行“认领(Claim)”和“更新”,实现了真正的多工协作。

2. 并发安全的读写机制

既然是共享账本,就必须解决并发冲突。在 createTask()updateTask() 的实现中,系统引入了 .lock 文件机制。任何对任务状态(Pending / In-progress / Completed)或依赖关系(Blocks / BlockedBy)的修改,都必须先获取文件锁,确保账本的一致性。

3. 工具协议驱动的自动化

Claude 并不是“自觉”地在列清单,而是受到了 TaskCreateTool/prompt.ts 中硬性指令的驱动。该指令明确要求模型在面对复杂请求、Plan 模式或多项用户要求时,必须主动创建任务,并在操作前将状态更新为 in_progress

4. 生命周期与自动回收

任务账本并非永久存在的 Backlog。claude-code-opensource/src/hooks/useTasksV2.ts 监控着任务目录:

  • 实时同步:UI 监听文件变化,确保终端显示与磁盘账本同步。
  • 自动收口:当所有任务均标记为 completed 且持续 5 秒后,系统会自动调用 resetTaskList() 清理掉这些 JSON 文件并隐藏 UI,保持环境整洁。

踩坑指南

  • 非聊天 Todo:任务账本是独立于 Transcript 之外的。你在聊天里随口说“记下一件事”,模型必须调用 TaskCreate 工具,这件事才会出现在 Task List 中。
  • TaskListId 是关键:如果你想让两个不同的窗口或 Agent 看到同一份任务,必须手动将它们的 taskListId 设为一致(例如在 Team 模式下)。
  • 与 /tasks 命令的区别:这是一个极易混淆的点。/tasks 展示的是后台执行实体(如正在跑的 Bash 进程);而 Task List 管理的是业务逻辑拆解(如“修复 Bug”、“编写测试”)。
  • 依赖图结构:任务之间的依赖(BlockedBy)是真实的逻辑约束。TaskList 工具会自动过滤掉那些仍被阻塞的任务,模型在当前轮次只能看到“可执行”的原子步骤。

相关主题

源码锚点

  • claude-code-opensource/src/utils/tasks.ts: 核心账本实现,包含路径解析、文件锁和 JSON 任务模型。
📄 src/utils/tasks.ts — 核心账本实现,包含路径解析、文件锁和 JSON 任务模型。L18-37 of 863
typescript
const tasksUpdated = createSignal()

/**
 * Team name set by the leader when creating a team.
 * Used by getTaskListId() so the leader's tasks are stored under the team name
 * (matching where tmux/iTerm2 teammates look), not under the session ID.
 */
let leaderTeamName: string | undefined

/**
 * Sets the leader's team name for task list resolution.
 * Called by TeamCreateTool when a team is created.
 */
export function setLeaderTeamName(teamName: string): void {
  if (leaderTeamName === teamName) return
  leaderTeamName = teamName
  // Changing the task list ID is a "tasks updated" event for subscribers —
  // they're now looking at a different directory.
  notifyTasksUpdated()
}
  • claude-code-opensource/src/tools/TaskCreateTool/TaskCreateTool.ts: 任务创建工具入口。
📄 src/tools/TaskCreateTool/TaskCreateTool.ts — 任务创建工具入口。L18-32 of 139
typescript
const inputSchema = lazySchema(() =>
  z.strictObject({
    subject: z.string().describe('A brief title for the task'),
    description: z.string().describe('What needs to be done'),
    activeForm: z
      .string()
      .optional()
      .describe(
        'Present continuous form shown in spinner when in_progress (e.g., "Running tests")',
      ),
    metadata: z
      .record(z.string(), z.unknown())
      .optional()
      .describe('Arbitrary metadata to attach to the task'),
  }),
  • claude-code-opensource/src/tools/TaskUpdateTool/TaskUpdateTool.ts: 状态变更、认领逻辑与依赖追加。
📄 src/tools/TaskUpdateTool/TaskUpdateTool.ts — 状态变更、认领逻辑与依赖追加。L18-22 of 407
typescript
  type TaskStatus,
  TaskStatusSchema,
  updateTask,
} from '../../utils/tasks.js'
import {
  • claude-code-opensource/src/tools/TaskListTool/TaskListTool.ts: 实现基于依赖图的任务过滤逻辑。
📄 src/tools/TaskListTool/TaskListTool.ts — 实现基于依赖图的任务过滤逻辑。L13-13 of 117
typescript
const inputSchema = lazySchema(() => z.strictObject({}))
  • claude-code-opensource/src/hooks/useTasksV2.ts: 任务 UI 的数据流监听与 5 秒自动回收逻辑。
📄 src/hooks/useTasksV2.ts — 任务 UI 的数据流监听与 5 秒自动回收逻辑。L16-45 of 251
typescript
const HIDE_DELAY_MS = 5000
const DEBOUNCE_MS = 50
const FALLBACK_POLL_MS = 5000 // Fallback in case fs.watch misses events

/**
 * Singleton store for the TodoV2 task list. Owns the file watcher, timers,
 * and cached task list. Multiple hook instances (REPL, Spinner,
 * PromptInputFooterLeftSide) subscribe to one shared store instead of each
 * setting up their own fs.watch on the same directory. The Spinner mounts/
 * unmounts every turn — per-hook watchers caused constant watch/unwatch churn.
 *
 * Implements the useSyncExternalStore contract: subscribe/getSnapshot.
 */
class TasksV2Store {
  /** Stable array reference; replaced only on fetch. undefined until started. */
  #tasks: Task[] | undefined = undefined
  /**
   * Set when the hide timer has elapsed (all tasks completed for >5s), or
   * when the task list is empty. Starts false so the first fetch runs the
   * "all completed → schedule 5s hide" path (matches original behavior:
   * resuming a session with completed tasks shows them briefly).
   */
  #hidden = false
  #watcher: FSWatcher | null = null
  #watchedDir: string | null = null
  #hideTimer: ReturnType<typeof setTimeout> | null = null
  #debounceTimer: ReturnType<typeof setTimeout> | null = null
  #pollTimer: ReturnType<typeof setTimeout> | null = null
  #unsubscribeTasksUpdated: (() => void) | null = null
  #changed = createSignal()
  • claude-code-opensource/src/components/TaskListV2.tsx: 负责在终端界面中对任务进行排序和裁剪展示。
📄 src/components/TaskListV2.tsx — 负责在终端界面中对任务进行排序和裁剪展示。L17-20 of 378
tsx
type Props = {
  tasks: Task[];
  isStandalone?: boolean;
};

基于 Claude Code v2.1.88 开源快照的深度分析