JSONL Transcript 会话持久化与恢复机制
本文梳理 Claude Code 基于 JSONL transcript 的会话持久化、恢复、错误恢复、上下文压缩、分支、subagent、fork agent 和 remote agent 逻辑。 这不是按文件罗列的源码笔记,而是一份机制手册:先建立心智模型,再看数据结构、生命周期、异常路径和源码入口。怎么读
总览
Claude Code 的本地会话核心是 append-only JSONL。每一行是一个Entry,但恢复时不会按文件顺序重放整个文件,而是:
- 把 transcript message 放入
uuid -> messagemap。 - 把 metadata entry 放入各自 map 或数组。
- 选择最新 leaf。
- 从 leaf 沿
parentUuid回溯,得到当前有效链。 - 应用 compact、snip、preserved segment、content replacement 等投影。
- 恢复 sessionId、worktree、mode、agent setting、任务状态等内存状态。
系统分层
存储拓扑
核心源码地图
数据模型
Entry 定义在 src/types/logs.ts,可以分为三大类。
TranscriptMessage 字段
真正参与链路的是TranscriptMessage:
JSONL 示例
主会话消息:content-replacement:
写入生命周期
总流程
关键点:主会话写入
入口:recordTranscript(messages, teamInfo?, startingParentUuidHint?, allMessages?)。
流程:
cleanMessagesForLogging()过滤 UI-only 或不应持久化的消息。getSessionMessages(sessionId)读取当前 session 已有 UUID set。- 对未写过的消息调用
insertMessageChain()。 insertMessageChain()补parentUuid/sessionId/cwd/timestamp/version/gitBranch/isSidechain。appendEntry()进入 per-file queue。
startingParentUuid,确保后续新消息接在正确父节点后。
写队列、materialize 和 flush
Project 内部维护 per-file queue:
sessionFile 初始为 null。这时 title、tag、mode、worktree 等 metadata 先存在内存或 pendingEntries 中。第一次出现 user 或 assistant 时,materializeSessionFile() 才创建 session 文件,然后:
- 写入缓存 metadata。
- 回放 pending entries。
- 之后所有 entry 正常 append。
/resume 列表。
sidechain 写入
subagent 使用recordSidechainTranscript(messages, agentId, startingParentUuid?)。
它底层仍走 insertMessageChain(),但写入字段不同:
appendEntry() 遇到 isSidechain && agentId 的 transcript message,会把它路由到:
content-replacement 带 agentId,也会路由到该 agent 的 sidechain JSONL,而不是主 session JSONL。
一个很重要的例外:sidechain 写入不会用主 session UUID set 做去重。fork agent 会复用父会话消息 UUID 来继承上下文;如果按主 session 去重,会把继承上下文从 sidechain 中误删,导致 agent resume 时只剩子 prompt。
读取与链路重建
从 JSONL 到有效链
loadTranscriptFile(filePath, opts?) 产出:
leaf 与 parent 链
buildConversationChain(messages, leaf):
- 从 leaf 开始。
- 读取
parentUuid。 - 找到父消息并继续回溯。
- 检测 parent cycle,避免无限循环。
- reverse 成正序 transcript。
- 补回并行 tool_use 形成的 DAG 分支。
metadata 合并规则
大文件读取优化
transcript 可增长到几百 MB 甚至 GB,读取路径有几层防护。walkChainBeforeParse() 只有预计能丢掉至少一半 buffer 时才做 concat,避免优化本身变成额外成本。
preserved segment 与 snip
compact boundary 可以带compactMetadata.preservedSegment。恢复时 applyPreservedSegmentRelinks() 会:
- 验证
tailUuid -> headUuid链是否完整。 - 把 preserved segment 的 head 接到 compact anchor 后。
- 把 anchor 的其他 children 接到 preserved tail。
- 删除最后一个 boundary 前且不属于 preserved segment 的旧消息。
- 清零 preserved assistant 的 usage,避免恢复后马上又触发 autocompact。
snip 和 compact 不同:compact 截断前缀,snip 删除中段。JSONL 不能真的删除旧行,所以 applySnipRemovals() 在内存 map 中删除 removedUuids,再把 dangling parentUuid 重连到最近未删除祖先。
旧链路修复
恢复入口
入口矩阵
CLI resume 流程
核心函数:REPL /resume
REPL 内 resume 比 CLI 启动路径多了“从当前 session 切换到另一个 session”的工作:
- 清理目标 log messages。
- 当前 session 跑 SessionEnd hooks。
- 目标 session 跑 SessionStart resume hooks。
- 保存当前 session cost,恢复目标 session cost。
switchSession(sessionId, dirname(fullPath))原子切换 sessionId + project dir。resetSessionFilePointer()并恢复 metadata cache。- 非 fork 时退出上一次 worktree,恢复目标 worktree,
adoptResumedSessionFile()。 - fork 时不接管原 transcript,不退出当前 worktree。
- 重建 content replacement state。
- 恢复 remote/local task 状态。
- 替换 messages、清 tool JSX、清输入框。
中断检测矩阵
deserializeMessagesWithInterruptDetection() 会先清理历史消息:
然后看最后一个 turn-relevant message:
interrupted_turn 会统一转换为 interrupted_prompt,让上层只处理一种“需要续跑”的状态。
错误恢复矩阵
上下文视图
同一份消息在系统里有四种视图,不要混在一起:
每轮 query 的 active context 顺序:
getMessagesAfterCompactBoundary(messages):取最近 compact boundary 之后的 active slice,默认叠加 snip 投影。- 删除旧
toolUseResult原始 payload,只保留 API 需要的message.content。 applyToolResultBudget():过大的 tool_result 替换为 preview/stub,并写content-replacement。snipCompactIfNeeded():HISTORY_SNIP下删除中段历史。microcompactMessages():time-based microcompact,再 cached microcompact。contextCollapse.applyCollapsesIfNeeded():当前为 identity stub。autoCompactIfNeeded():主动 compact,优先 session memory compact。- predictive autocompact:API 前估算本 turn 增长,必要时提前 compact。
- API 真实超限后:context-collapse drain,再 reactive compact。
Compact 与投影
Compact 类型对比
Compact 结果形态
传统 compact 会生成:compact_boundarysystem message。- compact summary user message。
- post-compact attachments,例如当前文件、计划模式、技能、MCP/tool schema delta、hook 结果。
Boundary metadata
createCompactBoundaryMessage() 写:
后续路径还会补:
Tool result budget 与 content replacement
大 tool_result 不一定直接进入后续上下文。applyToolResultBudget() 会按 API-level user message 聚合预算,必要时把大块内容持久化并替换成较小 preview/stub。
关键点:
Session memory compact
sessionMemoryCompact.ts 是传统 summary compact 前的实验路径。流程:
- 等待 session memory extraction 完成。
- 读取 session memory 文件。
- 有
lastSummarizedMessageId时,从其后保留安全尾段;否则把 resumed session 视为已有 memory summary。 - 调整切点,避免断开 tool_use/tool_result 或 thinking blocks。
- 创建标准
compact_boundary+ summary user message。 - 若 post-compact token count 仍超过阈值,放弃并回退传统 compact。
CompactionResult,下游写 transcript 和恢复逻辑与传统 compact 共用。
Context-collapse 当前状态
本仓库保留了 context-collapse 的持久化接口,但核心实现是 stub:
已预留 JSONL entry:
loader 会收集这些 entry;遇到 compact boundary 时会清空旧 commits/snapshot,避免它们引用已被 compact 丢弃的 UUID。
所以当前真实生效的上下文缩减主要是 compact、session memory compact、tool_result budget、microcompact 和 snip;context-collapse 只是接口已接好。
Compact 后清理
runPostCompactCleanup(querySource) 总是清:
- microcompact state。
- system prompt sections。
- classifier approvals。
- speculative bash checks。
- beta tracing。
- session messages memo cache。
- compact cleanup callbacks。
COMMIT_ATTRIBUTION下异步 sweep file-content cache。
- context-collapse store。
getUserContextcache。- memory files cache。
agent:* compact 如果清主线程 context-collapse 或 memory cache,会破坏父会话状态。
它明确不清 resetSentSkillNames(),避免 compact 后重新注入完整 skill listing,浪费 token 和 prompt cache。
分支与 Fork 对比
/branch
/branch 创建新 session 文件,不是在原 JSONL 里追加 branch marker。
流程:
- 生成新的 sessionId。
- 读取当前 transcript 文件。
- 过滤主会话消息,排除
isSidechain和非 transcript entry。 - 复制消息并重写
sessionId。 - 重新串
parentUuid。 - 添加
forkedFrom: { sessionId, messageUuid }。 - 复制原 session 的
content-replacemententry 并改成新 sessionId。 - 写入
<newSessionId>.jsonl。 - 构造
LogOption并让 REPL resume 到新分支。
--fork-session
--fork-session 只改变 resume 的 ownership:
如果旧 session 有
content-replacement,会先把 records seed 到新 session,避免大 tool_result 的替换状态丢失。
Subagent 与 Fork Agent
普通 subagent
普通 AgentTool subagent 最终走runAgent():
父会话通常只记录:
- Agent tool_use。
- Agent tool_result。
- async launch result。
- task notification。
- 必要 progress。
Fork agent
fork agent 是 AgentTool 的一种特殊 subagent。它继承父上下文、system prompt、tools、model 和 thinking config,目标是让多个子 agent 共享尽可能长的 byte-identical prompt cache prefix。 关键实现:buildForkedMessages() 负责构造 cache-friendly 尾部:
runForkedAgent() 不是 AgentTool fork
src/utils/forkedAgent.ts 的 runForkedAgent() 是内部 cache-safe side query 工具,用于 session memory、prompt suggestion、summary 等。它复用父 system/user/system context、tools、messages,可选 skipTranscript,但默认不写 AgentTool metadata,也不是用户可继续对话的 AgentTool fork。
Agent 恢复
本地 agent 恢复入口是resumeAgentBackground()。
流程:
恢复时: