随笔

Claude Code 深度源码解读 09:可恢复会话依赖数据结构,不依赖回忆

拆解 transcript、session JSONL、content replacement、compact boundary、task output、resume 和 telemetry 的协作。

2026-04-18 Claude Code源码解读Agent Harness技术深读

简化版入口:可观测性与持久化

机制图
机制图

我的设问与回应

设问 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 State
  • Query Loop
  • Tool Execution
  • Compaction / Recovery
  • Background Tasks
  • Telemetry / Logs

这部分机制参考前文「可观测性与持久化机制图」。

3. 核心流程图

路线图中的代表链路是:

transcript / session storage / progress / logs / analytics

下图用蓝色表示可恢复消息链,绿色表示任务持久化,橙色表示压缩/大内容替换,紫色表示外部状态,红色表示审计日志。

这部分机制参考前文「可观测性与持久化机制图」。

4. 关键源码入口

职责文件
QueryEngine transcript 写入与 SDK 输出src/QueryEngine.ts
transcript / session storagesrc/utils/sessionStorage.ts
portable transcript load helperssrc/utils/sessionStoragePortable.ts
session restoresrc/utils/sessionRestore.ts
session state / external metadatasrc/utils/sessionState.ts
session activitysrc/utils/sessionActivity.ts
transcript searchsrc/utils/transcriptSearch.ts
query checkpointssrc/utils/queryProfiler.ts
API loggingsrc/services/api/logging.ts
analytics event sinksrc/services/analytics/index.tssrc/services/analytics/sink.ts
OpenTelemetry eventssrc/utils/telemetry/events.tssrc/utils/telemetry/instrumentation.ts
tool result storage / replacementsrc/utils/toolResultStorage.ts
task frameworksrc/utils/task/framework.ts
task output / disk sidecarsrc/utils/task/TaskOutput.tssrc/utils/task/diskOutput.ts
local agent task progresssrc/tasks/LocalAgentTask/LocalAgentTask.tsx
remote agent task restore/pollsrc/tasks/RemoteAgentTask/RemoteAgentTask.tsx
bridge session historysrc/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.tsisTranscriptMessage() 是单一来源: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.tsrestoreWorktreeForResume() 会根据 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.tsnotifySessionStateChanged() 保存 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 状态依赖远端 APIresume 时可能遇到 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 越应该收敛到这个问题本身:哪些能力可以交还给模型,哪些边界必须由工程系统兜住,哪些失败必须被转化为可恢复的协议状态。