随笔
Claude Code 深度源码解读 09:可恢复会话依赖数据结构,不依赖回忆
拆解 transcript、session JSONL、content replacement、compact boundary、task output、resume 和 telemetry 的协作。
简化版入口:可观测性与持久化。
我的设问与回应
设问 1:可观测性是不是日志?
回应答案: 不是。可观测性在这里首先是可恢复数据结构。transcript、tool_result、content replacement、compact boundary、session JSONL、telemetry 分别承担不同职责。
证据与证明路径: 证据来自第 9 期对 transcript/session state、content replacement、task output、resume、telemetry 的分层拆解。
还值得继续学习或反思: 进一步要学习的是哪些事件进入模型上下文,哪些只服务 UI 或审计。
设问 2:resume 是不是把历史再发一次?
回应答案: 不是。resume 需要重建 query loop 能继续消费的消息链和运行上下文,包括 parentUuid、compact boundary、模式、worktree、agent/task 状态。
证据与证明路径: 证据来自第 9 期对 processResumedConversation、session state、content replacement 和外部状态通知的描述。
还值得继续学习或反思: 如果没有结构化状态,resume 只是把旧聊天记录复制回来。
源码调研主体
本组解读基于冻结源码快照进行教育、防御和架构研究。
本期主题:Harness 如何通过 transcript、session storage、parentUuid chain、content replacement、session restore、task sidecar、progress、telemetry 和 logs 支撑恢复、复盘与调试。
1. 研究问题
本期围绕一个问题展开:
Claude Code Harness 为什么能在进程中断、会话恢复、工具输出过大、异步任务后台运行和 remote/SDK 客户端接入时,仍然保持可恢复、可审计、可调试?
可观测性不是“打印日志”这么简单。源码中至少有六层:
- transcript JSONL:保存可恢复的对话链。
- parentUuid chain:把消息串成可恢复轨迹。
- content replacement:把过大工具结果移出主上下文但保留索引。
- session metadata:保存 agent、mode、worktree、external metadata。
- task sidecar / output file:保存后台任务和 remote task 的状态。
- analytics / OTel / debug logs:保存性能、错误、权限与恢复事件。
这些机制共同解释了为什么 Harness 可以 resume、compact 后继续、查看后台任务、复盘权限和诊断 API/bridge 问题。
2. 对应总览节点
对应第 1 期总览图中的节点:
Transcript / Session StateQuery LoopTool ExecutionCompaction / RecoveryBackground TasksTelemetry / Logs
这部分机制参考前文「可观测性与持久化机制图」。
3. 核心流程图
路线图中的代表链路是:
transcript / session storage / progress / logs / analytics下图用蓝色表示可恢复消息链,绿色表示任务持久化,橙色表示压缩/大内容替换,紫色表示外部状态,红色表示审计日志。
这部分机制参考前文「可观测性与持久化机制图」。
4. 关键源码入口
| 职责 | 文件 |
|---|---|
| QueryEngine transcript 写入与 SDK 输出 | src/QueryEngine.ts |
| transcript / session storage | src/utils/sessionStorage.ts |
| portable transcript load helpers | src/utils/sessionStoragePortable.ts |
| session restore | src/utils/sessionRestore.ts |
| session state / external metadata | src/utils/sessionState.ts |
| session activity | src/utils/sessionActivity.ts |
| transcript search | src/utils/transcriptSearch.ts |
| query checkpoints | src/utils/queryProfiler.ts |
| API logging | src/services/api/logging.ts |
| analytics event sink | src/services/analytics/index.ts、src/services/analytics/sink.ts |
| OpenTelemetry events | src/utils/telemetry/events.ts、src/utils/telemetry/instrumentation.ts |
| tool result storage / replacement | src/utils/toolResultStorage.ts |
| task framework | src/utils/task/framework.ts |
| task output / disk sidecar | src/utils/task/TaskOutput.ts、src/utils/task/diskOutput.ts |
| local agent task progress | src/tasks/LocalAgentTask/LocalAgentTask.tsx |
| remote agent task restore/poll | src/tasks/RemoteAgentTask/RemoteAgentTask.tsx |
| bridge session history | src/assistant/sessionHistory.ts |
4.1 主线与场景特殊处理分类
| 机制/模块 | 分类 | 为什么这样归类 |
|---|---|---|
recordTranscript() | 主线机制 + 可观测/恢复支撑 | 用户消息、assistant、attachment、system 等可恢复消息写入 JSONL |
early transcript write in QueryEngine | 可观测/恢复支撑 | 用户消息接受后、API 前先写入,避免进程中断导致 resume 找不到会话 |
isTranscriptMessage() | 主线恢复支撑 | 定义 transcript 可恢复消息类型;progress 不属于 transcript message |
parentUuid chain | 可观测/恢复支撑 | 通过链路恢复会话轨迹,跳过 progress 防止 chain fork |
| legacy progress bridge | 可观测/恢复支撑 | 兼容旧 transcript 中 progress 参与链的问题 |
| content replacement | 可观测/恢复支撑 | 大工具结果移出主消息,保留替换记录,支持 resume 和 compact 后继续 |
| compact boundary | 可观测/恢复支撑 | 标记压缩边界,恢复时只发送有效历史 |
processResumedConversation() | 主线恢复支撑 | 恢复 session id、metadata、worktree、agent、mode、cost、context collapse |
| task output / sidecar | 场景增强 + 可观测支撑 | background / remote agent 需要额外持久化进度和结果;普通同步任务不依赖 |
notifySessionStateChanged() | 场景增强 + 可观测支撑 | 向 CCR/SDK/bridge 暴露 idle/running/requires_action 和 pending_action |
analytics logEvent() | 可观测/恢复支撑 | 记录权限、API、compact、memory、bridge、agent 等事件 |
| OTel telemetry | 场景增强 + 可观测支撑 | 只有 telemetry/enhanced telemetry 启用时初始化 exporters 和 event logger |
| debug logs / queryProfiler | 可观测/恢复支撑 | 支撑本地调试和性能定位,不进入模型语义主链路 |
5. 机制拆解
5.1 transcript 的核心不是日志,而是可恢复消息链
sessionStorage.ts 中 isTranscriptMessage() 是单一来源:user、assistant、attachment、system 属于 transcript message。注释明确 progress messages 不是 transcript messages,因为它们是 ephemeral UI state,不应持久化进 JSONL 或参与 parentUuid chain;旧 transcript 中 progress 已入链时,load 路径会 bridge chain。
这说明 transcript 不是所有 UI 输出的 dump,而是“能恢复会话语义轨迹”的消息集合。
5.2 用户消息在 API 前先写入,避免中断后无法 resume
QueryEngine.ts 在把 messagesFromUserInput push 到 mutableMessages 后,会在进入 query loop 前调用 recordTranscript(messages)。源码注释说明原因:
- for-await 只有在 API yield assistant/user/compact_boundary 后才会写 transcript。
- 如果用户发送后进程立刻被 Stop/kill,transcript 可能只有 queue-operation entries。
getLastSessionLog会过滤这些 entries,导致--resume报 “No conversation found”。
因此 early write 的目标是:用户消息一旦被接受,即使 API 未响应,也能从该点恢复。
5.3 progress 是 UI 状态,不能污染恢复链
sessionStorage.ts 定义 EPHEMERAL_PROGRESS_TYPES,包括 bash/powershell/mcp progress 以及特定 feature 下的 sleep progress。注释说明这些高频工具 progress tick 用于 UI replace-in-place,工具完成后不再渲染,也不发送给 API。
QueryEngine.ts 对一些 progress-adjacent 输出会 inline recordTranscript,并把已记录状态标记好,避免 deferred progress interleaving 造成 chain fork。第 4 期也已确认 query loop 会为缺失 tool_result 修补轨迹。
核心边界是:可观察不等于可恢复语义。progress 可观察,但不应进入模型轨迹。
5.4 content replacement 解决大工具结果与恢复的一致性
第 4 期已分析 applyToolResultBudget()。第 9 期从持久化角度看,sessionRestore.ts 在 --fork-session 且有 contentReplacements 时,会调用 recordContentReplacement(result.contentReplacements)。注释解释:fork session 会把源 messages 复制到新 JSONL,但 content replacement 是单独 entry 类型;如果不 seed,新 session resume 时会找不到 replacement records,导致 full content 被发送,造成 cache miss 和永久 overage。
这说明 content replacement 不是临时内存优化,而是 session 恢复语义的一部分。
5.5 resume 恢复的不只是 messages
processResumedConversation() 处理多个状态:
- coordinator / normal mode 匹配。
- session id 切换,支持 transcriptPath 指向不同 project dir。
- recording 重命名、session file pointer reset。
- cost state 恢复。
- session metadata 恢复。
- worktree session 恢复。
- context-collapse commit log 和 staged snapshot 恢复。
- agent setting 恢复。
- mode 持久化。
- attribution、file history、agentName、agentColor 等 initial state。
因此 resume 不是简单读取 JSONL 再发给模型,而是恢复会话周边状态。
5.6 worktree resume 有专门处理
sessionRestore.ts 中 restoreWorktreeForResume() 会根据 transcript 记录的最后 worktree enter/exit 状态,尝试 cd 回 session 退出时所在 worktree。如果目录不存在,会清理 cache 并留在当前目录。中途 /resume 切换 session 时,还有 undoWorktreeRestoreBeforeMidSessionResume() 避免从 worktree session 切到非 worktree session 后仍停留在旧工作区。
这对多 agent / worktree isolation 很关键:恢复时工作目录本身也是状态。
5.7 background task 使用 sidecar 和 output file 保持可观察
LocalAgentTaskState 保存 agent progress、messages、pendingMessages、retrieved、retain、diskLoaded 和 evictAfter。TaskOutput 维护 task output file、polling registry 和 active polling。
RemoteAgentTask.tsx 还会写 remote-agent metadata 到 session sidecar,并在 restoreRemoteAgentTasks() 中恢复仍运行的 remote sessions。状态不完全依赖本地内存;恢复后会重新 fetch/poll remote session status。
这解释了为什么后台 agent 可以在 UI 中继续显示、被 kill、被 foreground 或在 resume 后恢复轮询。
5.8 session state 给外部客户端提供权威状态
sessionState.ts 中 notifySessionStateChanged() 保存 currentState,并通知 listener。若 state 是 requires_action 且有 details,会把 pending_action 写到 external_metadata;离开 blocked 状态时用 null 清除。idle 时清掉 task_summary。
在 CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS 开启时,还会 enqueue SDK system event session_state_changed。注释说明这是为了让 CCR、scmuxd、VS Code 等客户端看到 idle/running 信号。
这说明外部客户端不是只能猜最后一条 assistant 消息;session state 是独立的可观测通道。
5.9 analytics、OTel 和 debug logs 分层记录
services/api/logging.ts 记录 API query、success、error、duration、client request id、teleport first message 等。query.ts 在 compact、fallback、orphaned message tombstone、auto compact success 等路径记录事件。permissions、memory、bridge、agent task 也都有 logEvent()。
utils/telemetry/events.ts 提供 logOTelEvent(),用 session 内递增 counter 记录顺序;instrumentation.ts 在 telemetry enabled 时初始化 metrics/log exporters,并在 logout/org switch 前 flush,避免数据泄漏。
queryProfiler.ts 提供 queryCheckpoint(),在 query loop 的 snip、microcompact、autocompact、API streaming 等节点打点,用于定位长任务卡点。
这些观测信号大多不进入模型上下文,但对定位“为什么没恢复、为什么卡住、为什么权限被拒绝”至关重要。
6. 对稳定解决问题能力的贡献
6.1 resume 从“最好能继续”变成工程化能力
用户消息 API 前写入、parentUuid chain、compact boundary、content replacement 和 session metadata 共同保证中断后能重建足够状态。
6.2 大输出不会轻易挤爆上下文
content replacement 和 tool result budget 让大工具输出可被替换、记录和恢复,而不是永久占用模型窗口。
6.3 异步任务不会变成黑盒
task progress、output file、sidecar metadata 和 notification 让 background / remote agent 能被观察、恢复和汇总。
6.4 外部客户端有状态同步点
session_state_changed、external_metadata、pending_action 和 task_summary 让 CCR/SDK/IDE 不必完全依赖文本输出猜测状态。
6.5 审计事件能定位复杂失败
API request id、permission decision、auto classifier、bridge skip、compact/fallback、memory extraction、agent progress 等日志把复杂失败拆成可查节点。
7. 风险、边界与待验证问题
| 风险/边界 | 影响 | 防御性解读 |
|---|---|---|
| transcript 文件可能很大 | 读取/重写有 OOM 或性能风险 | 源码有 tombstone rewrite size 上限、head/tail 读取、content replacement 和 compact |
| progress 不进 transcript | 恢复后不会重放所有 UI tick | 这是有意边界,防止 progress fork parentUuid chain;最终 tool_result 才是语义状态 |
| external metadata 与 transcript 分离 | 客户端可能看到状态与消息不同步 | notifySessionStateChanged() 作为统一 choke point 缓解,但仍依赖监听器接入 |
| remote task 状态依赖远端 API | resume 时可能遇到 401、404、网络失败 | RemoteAgentTask 区分 404 和可恢复 auth/network error,并重新 poll |
| telemetry 可能关闭 | 无法依赖 OTel 做本地恢复 | 恢复主链路依赖 transcript/session storage;telemetry 只用于审计和诊断 |
8. 系列收束
至此,路线图第 1-9 期形成完整闭环:
- 第 1 期给出 Harness 一级机制地图。
- 第 2 期解释用户输入如何进入 Context。
- 第 3 期解释工具与权限控制。
- 第 4 期解释 query loop 与恢复。
- 第 5 期解释 Hooks 纠偏。
- 第 6 期解释 Memory 连续性。
- 第 7 期解释多 Agent / Task 协作。
- 第 8 期复盘安全与信任边界。
- 第 9 期解释可观测、审计与持久化。
整体结论是:Claude Code Harness 的稳定性来自多层工程约束,而不是单个更强模型。模型负责推理和生成行动意图;Harness 负责把输入、上下文、工具、权限、记忆、协作、恢复和审计组织成可持续推进的闭环。
机制主线收束
站在架构师视角,我会把这组源码机制收束为三个问题:为什么需要它,什么时候必须引入它,以及真正要设计的是什么。
- why:可观测性解决的是长任务能否恢复、复盘、审计,而不是让 UI 看起来很忙。
- when:任务会跨工具、跨 compact、跨 session、跨 remote 或 background task 时必须做持久化边界。
- what:先定义 transcript schema,再定义 UI progress;所有副作用结果关联 tool use id、permission decision、duration、source;compact boundary 结构化。
可观测性与持久化这组源码最值得带走的,不是某个函数或某个配置项,而是它如何把模型能力放进一组可验证、可拒绝、可恢复、可审计的工程边界里。读源码时,如果只记住名词,会很快散;如果抓住 why、when、what,就能把这组机制迁移到自己的 agent 架构判断中。
系列最终立论:Harness 最终解决的是可靠性控制
读完这组源码后,我对 agent harness 的判断更明确了:它不是为了永久堆叠越来越复杂的工程技巧。随着大模型继续提升,一部分今天需要外部 Harness 承担的通用工程技巧,确实会被模型能力内化。例如更好的工具选择、更稳定的上下文摘要、更少的格式错误、更强的任务拆分能力,都可能逐渐从外部控制层回到模型本体。
因此,未来的 Harness 很可能会变得更薄、更少、更靠近边界条件,而不是越来越像一个庞大的外部操作系统。但有一件事不会消失:当模型能力越来越接近人,系统仍然要回答“可靠性如何得到保障”。模型可以更聪明,但聪明不等于级联协作不会失控。
传统链路式稳定性很容易落入乘法衰减:
A% * B% * ... * X%只要链路足够长,每个节点再可靠,整体也会被连续相乘拖低。Agent 系统真正要追求的,不应该只是让每个节点各自更可靠,而是把节点失败变成可发现、可拒绝、可恢复、可补救的事件,让多层机制形成互补保障。这个视角下,稳定性更接近:
1 - [(1 - A%) * (1 - B%) * ... * (1 - X%)]这不是数学上把所有机制简单相加,而是一个架构目标:通过权限、Hook、工具结果回灌、compact、transcript、session state、telemetry 等节点,控制每个失败点的外溢范围,避免单点不确定性沿协作链级联放大。
所以,Harness 的核心命题不是“模型之外再造一个复杂系统”,而是控制节点可靠性,进而控制级联协作的稳定性失控。模型越强,Harness 越应该收敛到这个问题本身:哪些能力可以交还给模型,哪些边界必须由工程系统兜住,哪些失败必须被转化为可恢复的协议状态。