任务管理:共享账本、状态机与跨会话持久化
在处理复杂、多步骤的任务时,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工具会自动过滤掉那些仍被阻塞的任务,模型在当前轮次只能看到“可执行”的原子步骤。
相关主题
- 如果你想深入了解多个智能体如何认领这些任务,请看 Agent Teams:协作协议与 Swarm 编排。
- 如果你关心任务在上下文压缩时是如何被保护的,请看 上下文窗口管理:装配、补载与压缩。
- 如果你想了解 UI 是如何渲染这些复杂状态的,请看 全屏渲染:Ink 布局与状态同步。
源码锚点
claude-code-opensource/src/utils/tasks.ts: 核心账本实现,包含路径解析、文件锁和 JSON 任务模型。
📄 src/utils/tasks.ts — 核心账本实现,包含路径解析、文件锁和 JSON 任务模型。
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 — 任务创建工具入口。
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 — 状态变更、认领逻辑与依赖追加。
type TaskStatus,
TaskStatusSchema,
updateTask,
} from '../../utils/tasks.js'
import {claude-code-opensource/src/tools/TaskListTool/TaskListTool.ts: 实现基于依赖图的任务过滤逻辑。
📄 src/tools/TaskListTool/TaskListTool.ts — 实现基于依赖图的任务过滤逻辑。
const inputSchema = lazySchema(() => z.strictObject({}))claude-code-opensource/src/hooks/useTasksV2.ts: 任务 UI 的数据流监听与 5 秒自动回收逻辑。
📄 src/hooks/useTasksV2.ts — 任务 UI 的数据流监听与 5 秒自动回收逻辑。
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 — 负责在终端界面中对任务进行排序和裁剪展示。
type Props = {
tasks: Task[];
isStandalone?: boolean;
};