Files
claude-code/docs/internals/session-transcript-persistence.md
Slayer 2567e77d37 sub agents docs (#1266)
* docs: 添加 JSONL transcript 会话机制文档

* docs: 重构多 Agent 编排机制文档
2026-06-09 11:50:46 +08:00

38 KiB
Raw Blame History

JSONL Transcript 会话持久化与恢复机制

本文梳理 Claude Code 基于 JSONL transcript 的会话持久化、恢复、错误恢复、上下文压缩、分支、subagent、fork agent 和 remote agent 逻辑。

这不是按文件罗列的源码笔记,而是一份机制手册:先建立心智模型,再看数据结构、生命周期、异常路径和源码入口。

怎么读

如果你想看 建议先读
为什么 resume 能恢复到正确位置 总览读取与链路重建恢复入口
为什么 compact 后历史还在但模型看不到 上下文视图Compact 与投影
为什么 subagent 不污染主会话 存储拓扑Subagent 与 Fork Agent
/branch--fork-session/fork 有什么区别 分支与 Fork 对比
崩溃、超限、取消后如何恢复 错误恢复矩阵

总览

Claude Code 的本地会话核心是 append-only JSONL。每一行是一个 Entry,但恢复时不会按文件顺序重放整个文件,而是:

  1. 把 transcript message 放入 uuid -> message map。
  2. 把 metadata entry 放入各自 map 或数组。
  3. 选择最新 leaf。
  4. 从 leaf 沿 parentUuid 回溯,得到当前有效链。
  5. 应用 compact、snip、preserved segment、content replacement 等投影。
  6. 恢复 sessionId、worktree、mode、agent setting、任务状态等内存状态。

核心不变量:

不变量 含义
JSONL 尽量 append-only compact、branch、sidechain 都优先追加新 entry不直接改旧历史。
uuid/parentUuid 决定世界线 文件顺序只说明写入顺序,真正恢复靠链路回溯。
metadata 不参与主链 title、tag、worktree、content replacement 等通过 sessionId/messageId/agentId 合并。
compact 不删除历史 它追加 boundary模型视图从最后一个 boundary 后开始。
subagent 是 sidechain 子 agent 的完整对话在独立 JSONL父会话只看到 Agent tool 的结果/通知。
remote agent 不是 sidechain remote agent 本地只保存 sidecar 身份,执行状态来自 CCR。

系统分层

flowchart TD
  A[磁盘层<br/>append-only JSONL + sidecar metadata] --> B[链路层<br/>uuid / parentUuid / leaf]
  B --> C[投影层<br/>compact / snip / tool_result budget / context-collapse]
  C --> D[恢复层<br/>deserialize / interrupt detection / metadata restore]
  D --> E[运行层<br/>REPL / QueryEngine / AgentTask / RemoteTask]

存储拓扑

~/.claude/projects/<project-key>/
  <sessionId>.jsonl
  <sessionId>/
    subagents/
      agent-<agentId>.jsonl
      agent-<agentId>.meta.json
      <subdir>/
        agent-<agentId>.jsonl
        agent-<agentId>.meta.json
    remote-agents/
      remote-agent-<taskId>.meta.json
文件 生成函数 用途
<sessionId>.jsonl getTranscriptPath() 主会话 transcript。
subagents/agent-<agentId>.jsonl getAgentTranscriptPath(agentId) 本地 subagent / fork agent sidechain。
subagents/agent-<agentId>.meta.json getAgentMetadataPath(agentId) agentType、worktreePath、description。
remote-agents/remote-agent-<taskId>.meta.json getRemoteAgentMetadataPath(taskId) remote CCR session 身份,用于恢复 polling。

核心源码地图

机制 主要文件
Entry 类型 src/types/logs.ts
路径、写入、读取、链路重建 src/utils/sessionStorage.ts
大文件流式读取 src/utils/sessionStoragePortable.ts
CLI resume 加载和中断检测 src/utils/conversationRecovery.ts
session 切换和状态恢复 src/utils/sessionRestore.ts
SDK/headless query 写 transcript src/QueryEngine.ts
API query loop、compact、错误恢复 src/query.ts
compact 实现 src/services/compact/*
context-collapse stub 与持久化接口 src/services/contextCollapse/*
/branch src/commands/branch/branch.ts
/fork src/commands/fork/fork.tsx
AgentTool 和 subagent packages/builtin-tools/src/tools/AgentTool/*
通用 forked side query src/utils/forkedAgent.ts
remote agent task src/tasks/RemoteAgentTask/RemoteAgentTask.tsx

数据模型

Entry 定义在 src/types/logs.ts,可以分为三大类。

类别 典型 type 是否进入 parentUuid key 恢复用途
transcript message userassistantattachmentsystem uuid 重建对话链、模型上下文、UI scrollback。
session metadata custom-titletagmodeworktree-statepr-linkagent-setting sessionId 恢复标题、标签、模式、worktree、PR、agent 设置。
message metadata file-history-snapshotattribution-snapshotsummary messageIdleafUuid 恢复文件历史、归因、摘要。
replacement metadata content-replacement sessionId + optional agentId 恢复大 tool_result 的替换决策。
context-collapse metadata marble-origami-commitmarble-origami-snapshot sessionId 预留 context-collapse 恢复接口;当前实现为 stub。
queue/task metadata queue-operationtask-summaryspeculation-accept 各自字段 恢复队列、任务摘要、推测接受统计。

TranscriptMessage 字段

真正参与链路的是 TranscriptMessage

字段 含义
uuid 当前消息 ID。
parentUuid 链路父节点,恢复时沿它回溯。
logicalParentUuid compact boundary 等断链场景保留逻辑父节点。
sessionId 所属主 session。
cwd 写入时工作目录。
timestamp 写入时间。
version CLI 版本。
gitBranch 写入时 git 分支。
isSidechain 是否是 subagent sidechain。
agentId sidechain 所属 agent。
teamName/agentName/agentColor swarm / teammate 展示元数据。

JSONL 示例

主会话消息:

{"type":"user","uuid":"u1","parentUuid":null,"sessionId":"s1","isSidechain":false,"cwd":"D:\\vibe\\claude-code","message":{"role":"user","content":"修复测试"}}
{"type":"assistant","uuid":"a1","parentUuid":"u1","sessionId":"s1","isSidechain":false,"message":{"role":"assistant","content":[{"type":"text","text":"我来检查。"}]}}

sidechain 消息:

{"type":"user","uuid":"u2","parentUuid":null,"sessionId":"s1","isSidechain":true,"agentId":"ag1","message":{"role":"user","content":"分析 compact 路径"}}

agent 的 content-replacement

{"type":"content-replacement","sessionId":"s1","agentId":"ag1","replacements":[{"messageUuid":"u2","toolUseId":"toolu_...","blockIndex":0,"kind":"persisted"}]}

compact boundary

{"type":"system","subtype":"compact_boundary","uuid":"b1","parentUuid":"a9","logicalParentUuid":"a9","sessionId":"s1","compactMetadata":{"trigger":"auto","preTokens":182000,"messagesSummarized":94}}

写入生命周期

总流程

sequenceDiagram
  participant User
  participant QE as QueryEngine
  participant SS as sessionStorage.Project
  participant FS as JSONL
  participant API as query()/API

  User->>QE: ask(messages)
  QE->>SS: recordTranscript(user messages)
  SS->>SS: clean + dedup + insertMessageChain
  SS->>SS: appendEntry / enqueueWrite
  SS-->>FS: drain queue append JSONL
  QE->>API: start query loop
  API-->>QE: assistant/user/system compact_boundary
  QE->>SS: recordTranscript(streamed messages)
  QE->>SS: flushSessionStorage before result when needed

关键点:

设计 为什么
用户输入先写 transcript再进 API 进程在 API 前崩溃时resume 仍能看到用户 prompt。
assistant streaming 写入多为 fire-and-forget 不阻塞 token streaming。
result 前按需 flush 避免 SDK/桌面端拿到 result 后立即杀进程导致尾部丢失。
progress 不参与链路 高频 progress tick 不应该制造分叉或膨胀 transcript。

主会话写入

入口:recordTranscript(messages, teamInfo?, startingParentUuidHint?, allMessages?)

流程:

  1. cleanMessagesForLogging() 过滤 UI-only 或不应持久化的消息。
  2. getSessionMessages(sessionId) 读取当前 session 已有 UUID set。
  3. 对未写过的消息调用 insertMessageChain()
  4. insertMessageChain()parentUuid/sessionId/cwd/timestamp/version/gitBranch/isSidechain
  5. appendEntry() 进入 per-file queue。

去重不是简单丢弃所有重复:如果 prefix 中某些消息已写过,写入器会推进 startingParentUuid,确保后续新消息接在正确父节点后。

写队列、materialize 和 flush

Project 内部维护 per-file queue

机制 细节
writeQueues Map<filePath, entry[]>,按文件聚合写入。
drain timer 默认 100msCCR/remote persistence 场景约 10ms。
queue 上限 单队列超过 1000 条会丢弃最老 queued entry 并 resolve防止内存无限增长。
chunk 上限 单次 JSONL append chunk 约 100MB。
flushSessionStorage() 取消 timer等待 active drain 和 tracked writes。

sessionFile 初始为 null。这时 title、tag、mode、worktree 等 metadata 先存在内存或 pendingEntries 中。第一次出现 userassistant 时,materializeSessionFile() 才创建 session 文件,然后:

  1. 写入缓存 metadata。
  2. 回放 pending entries。
  3. 之后所有 entry 正常 append。

这样可以避免“只打开 CLI 没说话”也产生 metadata-only session污染 /resume 列表。

sidechain 写入

subagent 使用 recordSidechainTranscript(messages, agentId, startingParentUuid?)

它底层仍走 insertMessageChain(),但写入字段不同:

isSidechain: true
agentId: agentId

appendEntry() 遇到 isSidechain && agentId 的 transcript message会把它路由到

<project>/<sessionId>/subagents/agent-<agentId>.jsonl

如果 content-replacementagentId,也会路由到该 agent 的 sidechain JSONL而不是主 session JSONL。

一个很重要的例外sidechain 写入不会用主 session UUID set 做去重。fork agent 会复用父会话消息 UUID 来继承上下文;如果按主 session 去重,会把继承上下文从 sidechain 中误删,导致 agent resume 时只剩子 prompt。

读取与链路重建

从 JSONL 到有效链

flowchart TD
  A[loadTranscriptFile(file)] --> B[readTranscriptForLoad<br/>大文件按 chunk 读]
  B --> C[parseJSONL Entry]
  C --> D[messages Map uuid->TranscriptMessage]
  C --> E[metadata maps/arrays]
  D --> F[progress bridge / preserved relink / snip removal]
  F --> G[select leaf]
  G --> H[buildConversationChain]
  H --> I[recoverOrphanedParallelToolResults]
  I --> J[LogOption or agent transcript]

loadTranscriptFile(filePath, opts?) 产出:

输出 用途
messages uuid -> TranscriptMessage
leafUuids 候选 leaf。
title/tag/mode/worktree/PR maps session metadata。
fileHistorySnapshots / attributionSnapshots 文件状态恢复。
contentReplacements 主线程 replacement records。
agentContentReplacements agentId -> replacement records
contextCollapseCommits / contextCollapseSnapshot context-collapse 恢复输入。

leaf 与 parent 链

buildConversationChain(messages, leaf)

  1. 从 leaf 开始。
  2. 读取 parentUuid
  3. 找到父消息并继续回溯。
  4. 检测 parent cycle避免无限循环。
  5. reverse 成正序 transcript。
  6. 补回并行 tool_use 形成的 DAG 分支。

一个简化例子:

u1 <- a1 <- u2 <- a2
                 ^
               leaf

恢复链: a2 -> u2 -> a1 -> u1
正序链: u1, a1, u2, a2

文件顺序不等于有效链。branch、rewind、streaming fallback 都可能让 JSONL 里有死分支;恢复只选择当前 leaf 所在世界线。

metadata 合并规则

metadata 合并方式 说明
custom-titletagmodeworktree-statepr-linkagent-setting sessionId keyed通常 last-wins 恢复最新 session 状态。
file-history-snapshotattribution-snapshot messageId keyed / array 恢复文件历史与归因。
content-replacement append array 多轮 replacement 决策都要保留。
agentContentReplacements agentId keyed + append array agent resume 重建 sidechain replacement state。
marble-origami-commit ordered array 顺序有语义,后一个 commit 可能引用前一个 summary。
marble-origami-snapshot last-wins staged snapshot 只恢复最新状态。

大文件读取优化

transcript 可增长到几百 MB 甚至 GB读取路径有几层防护。

优化 位置 目的
chunk 读取 readTranscriptForLoad() 避免一次性读爆内存。
fd 层跳过大 metadata readTranscriptForLoad() attribution-snapshot 等大 entry 不进入 buffer。
compact 前缀跳过 readTranscriptForLoad() 遇到非 preserved compact boundary 后,只保留 boundary 后内容。
pre-boundary metadata scan scanPreBoundaryMetadata() compact 前被跳过时,仍保留 title/tag/mode/worktree/PR 等展示信息。
byte-level dead branch 裁剪 walkChainBeforeParse() JSON.parse 前只拼 active chain 和 metadata跳过 dead fork/rewind branch。
lite read 限制 MAX_TRANSCRIPT_READ_BYTES 直接读 raw transcript 的调用超过约 50MB 要避开。

walkChainBeforeParse() 只有预计能丢掉至少一半 buffer 时才做 concat避免优化本身变成额外成本。

preserved segment 与 snip

compact boundary 可以带 compactMetadata.preservedSegment。恢复时 applyPreservedSegmentRelinks() 会:

  1. 验证 tailUuid -> headUuid 链是否完整。
  2. 把 preserved segment 的 head 接到 compact anchor 后。
  3. 把 anchor 的其他 children 接到 preserved tail。
  4. 删除最后一个 boundary 前且不属于 preserved segment 的旧消息。
  5. 清零 preserved assistant 的 usage避免恢复后马上又触发 autocompact。

示意:

compact 前: old... -> anchor -> head -> ... -> tail -> next
compact 后: boundary/summary -> head -> ... -> tail -> next

snip 和 compact 不同compact 截断前缀snip 删除中段。JSONL 不能真的删除旧行,所以 applySnipRemovals() 在内存 map 中删除 removedUuids,再把 dangling parentUuid 重连到最近未删除祖先。

旧链路修复

问题 修复
legacy progress 曾进入 parent 链 progressBridge 把指向 progress 的 parent 改回 progress 的真实父节点。
parent cycle buildConversationChain() 检测 cycle记录并返回 partial chain。
并行 tool_use 形成 DAG recoverOrphanedParallelToolResults() 按 assistant message.id 和 tool_result parent 关系补回 sibling。
streaming fallback 孤儿尾巴 tombstone 触发 removeTranscriptMessage(uuid) 删除失败 attempt。

恢复入口

入口矩阵

入口 加载源 是否复用原 sessionId 是否 adopt 原 JSONL 特点
--continue 当前目录最近 session 跳过仍 live 的 bg/daemon 非 interactive session。
--resume <uuid> 指定 session 也支持 custom title / 搜索词 / picker。
--resume <jsonl> 指定 JSONL 文件 Ant 内部/print path 支持。
--fork-session + resume 旧 session messages 保持新 sessionId把旧消息作为新 session 初始内容。
--resume-session-at <message.id> print/headless resume 取决于 resume 取决于 resume 截断到指定 assistant message。
REPL /resume picker / log option 是或 fork 是或否 会跑 SessionEnd/SessionStart hooks切换 UI state。

CLI resume 流程

flowchart TD
  A[main.tsx --continue/--resume] --> B[loadConversationForResume]
  B --> C[load log or transcript]
  C --> D[deserializeMessagesWithInterruptDetection]
  D --> E[processSessionStartHooks]
  E --> F[processResumedConversation]
  F --> G{fork session?}
  G -- no --> H[switchSession + adoptResumedSessionFile]
  G -- yes --> I[keep fresh sessionId + seed content replacement]
  H --> J[restore mode/worktree/agent/context-collapse/cost]
  I --> J
  J --> K[start REPL or print]

核心函数:

函数 责任
loadConversationForResume() 统一加载最近 session、sessionId、LogOption 或 JSONL path补 lite log复制 plan/file history做 consistency check反序列化和中断检测返回 metadata。
processResumedConversation() CLI interactive 启动恢复;切换或 fork session恢复 cost、worktree、mode、agent setting、context-collapse、attribution。
restoreSessionStateFromLog() 恢复 AppState 侧状态file history、attribution、context-collapse、TodoWrite todos。

REPL /resume

REPL 内 resume 比 CLI 启动路径多了“从当前 session 切换到另一个 session”的工作

  1. 清理目标 log messages。
  2. 当前 session 跑 SessionEnd hooks。
  3. 目标 session 跑 SessionStart resume hooks。
  4. 保存当前 session cost恢复目标 session cost。
  5. switchSession(sessionId, dirname(fullPath)) 原子切换 sessionId + project dir。
  6. resetSessionFilePointer() 并恢复 metadata cache。
  7. 非 fork 时退出上一次 worktree恢复目标 worktreeadoptResumedSessionFile()
  8. fork 时不接管原 transcript不退出当前 worktree。
  9. 重建 content replacement state。
  10. 恢复 remote/local task 状态。
  11. 替换 messages、清 tool JSX、清输入框。

中断检测矩阵

deserializeMessagesWithInterruptDetection() 会先清理历史消息:

清理 目的
legacy attachment 迁移 兼容旧 transcript。
非法 permissionMode 删除 防止跨 build 的无效枚举进入运行态。
unresolved tool_use 过滤 避免 API 报 tool_use/tool_result 不配对。
orphaned thinking-only assistant 过滤 避免中断 streaming 留下孤儿 thinking block。
whitespace-only assistant 过滤 避免取消时留下空白 assistant。

然后看最后一个 turn-relevant message

最后有效消息 结果 额外动作
assistant none streaming 持久化里 stop_reason 常为 null不能靠它判断未完成。
普通 user interrupted_prompt 插入 NO_RESPONSE_REQUESTED sentinel 保持 API-valid。
meta user / compact summary user none 不把内部控制消息当用户新请求。
tool_result user 通常 interrupted_turn 例外Brief/SendUserMessage/SendUserFile terminal tool_result 视为完成。
attachment interrupted_turn 追加 meta userContinue from where you left off.
system/progress/API error assistant 跳过 不作为 turn 完成判断依据。

interrupted_turn 会统一转换为 interrupted_prompt,让上层只处理一种“需要续跑”的状态。

错误恢复矩阵

场景 处理策略 transcript 影响
API 前进程崩溃 用户 prompt 已由 QueryEngine.ask() 先写入。 resume 看到普通 user触发 interrupted_prompt
streaming fallback 产生孤儿 assistant yield tombstoneREPL 移除 UI message 并调用 removeTranscriptMessage(uuid) 优先只改 JSONL 尾部 64KB大文件目标不在尾部时跳过慢 rewrite。
prompt-too-long / media-too-large streaming 阶段先 withheld先 context-collapse drain再 reactive compact失败才暴露错误。 compact 成功则写 boundary/summary 并重试;失败才写 API error message。
max_output_tokens 先提高 max output override仍失败则注入内部 recovery prompt 续写;耗尽才暴露错误。 内部 retry prompt 不一定成为普通 transcript取决于是否 yield 到外层。
auto compact 关闭但到 blocking limit 直接 yield prompt-too-long 风格 API error。 保留用户手动 /compact 空间。
abort during streaming/tools 补齐缺失 tool_result必要时 yield user interruption message。 reason === interrupt 时跳过 interruption message因为后续 queued user message 已提供上下文。
stop hook blocking 把 hook blocking error 加入 state 后重试。 有 reactive compact guard避免 hook/error/compact 无限循环。
compact boundary 指向未落盘 tail QueryEngine 写 boundary 前强制补写 preserved tail 前的消息。 避免恢复时 boundary 引用不存在 UUID。
subagent transcript 尾部不完整 resumeAgentBackground() 再次过滤 unresolved tool_use、orphan thinking、空白 assistant。 避免恢复 agent 后 API 请求非法。

上下文视图

同一份消息在系统里有四种视图,不要混在一起:

视图 内容 谁使用
Raw transcript JSONL 中所有 entry包括旧历史、dead branch、metadata、sidechain。 磁盘持久化和审计。
UI scrollback REPL 当前展示的消息,可能保留 compact 前历史和 collapsed UI group。 终端 UI。
Active query view getMessagesAfterCompactBoundary() 后的消息,默认再投影 snip。 query.ts 上下文管理。
API wire view normalizeMessagesForAPI() 后,过滤 system boundary、修复 tool pairing、插入 cache edits。 Anthropic/OpenAI/Gemini 等 API client。

每轮 query 的 active context 顺序:

  1. getMessagesAfterCompactBoundary(messages):取最近 compact boundary 之后的 active slice默认叠加 snip 投影。
  2. 删除旧 toolUseResult 原始 payload只保留 API 需要的 message.content
  3. applyToolResultBudget():过大的 tool_result 替换为 preview/stub并写 content-replacement
  4. snipCompactIfNeeded()HISTORY_SNIP 下删除中段历史。
  5. microcompactMessages()time-based microcompact再 cached microcompact。
  6. contextCollapse.applyCollapsesIfNeeded():当前为 identity stub。
  7. autoCompactIfNeeded():主动 compact优先 session memory compact。
  8. predictive autocompactAPI 前估算本 turn 增长,必要时提前 compact。
  9. API 真实超限后context-collapse drain再 reactive compact。

Compact 与投影

Compact 类型对比

类型 触发 摘要来源 是否调用 compact API 是否保留尾段 失败策略
manual compact /compact compact summary API 或 session memory 取决于路径 取决于 full/partial/SM 显示失败或回退传统 compact。
auto compact token 阈值 先 session memory后 summary API 取决于路径 取决于路径 连续失败 circuit breaker默认 3 次后停止自动 compact。
predictive compact API 前估算增长 同 auto compact 取决于路径 取决于路径 失败则继续原请求或走后续错误恢复。
reactive compact API 真实 413/media error 后 compactConversation() 当前 wrapper 取决于 compact 实现 hasAttemptedReactiveCompact 防循环。
session memory compact manual/auto 前置尝试 session memory 文件 若 post-compact 仍超阈值,放弃并回退传统 compact。
microcompact time/cached 小型压缩 局部清理或 API cache edit 不一定 不适用 通常不改变 JSONL 主历史。
snip HISTORY_SNIP 删除中段 保留前后上下文 通过 snip metadata 投影,不物理删旧行。

Compact 结果形态

传统 compact 会生成:

  1. compact_boundary system message。
  2. compact summary user message。
  3. post-compact attachments例如当前文件、计划模式、技能、MCP/tool schema delta、hook 结果。

简化 before/after

Raw/UI:
  u1, a1, u2, a2, ... u99, a99,
  system:compact_boundary,
  user:compact summary,
  attachment:current files,
  u100

Active query view:
  system:compact_boundary,
  user:compact summary,
  attachment:current files,
  u100

API wire view:
  user:compact summary,
  attachment/content,
  u100

boundary 本身是 system message最后会被 API normalization 过滤;它的价值主要在本地投影、恢复和统计。

Boundary metadata

createCompactBoundaryMessage() 写:

字段 含义
compactMetadata.trigger manualauto
compactMetadata.preTokens compact 前 token 数。
compactMetadata.userContext 用户手动 compact 的额外说明。
compactMetadata.messagesSummarized 被总结消息数量。
logicalParentUuid compact 前最后消息,用于逻辑追踪。

后续路径还会补:

字段 来源 作用
preCompactDiscoveredTools traditional/SM compact 恢复 deferred tool schema 可见性。
preservedSegment.{headUuid,anchorUuid,tailUuid} partial/SM compact 恢复时把保留尾段接到 boundary 后。

Tool result budget 与 content replacement

大 tool_result 不一定直接进入后续上下文。applyToolResultBudget() 会按 API-level user message 聚合预算,必要时把大块内容持久化并替换成较小 preview/stub。

关键点:

说明
replacement decision 会落 JSONL recordContentReplacement()content-replacement
主线程和 agent 分开 agentId 写主 JSONLagentId 写 sidechain JSONL。
resume 会重建 replacement state 避免恢复后同一大结果又变回完整内容,导致 token 暴涨或 prompt cache 失配。
--fork-session 会 seed records fork 新 session 时复制 replacement 决策到新 session。

Session memory compact

sessionMemoryCompact.ts 是传统 summary compact 前的实验路径。流程:

  1. 等待 session memory extraction 完成。
  2. 读取 session memory 文件。
  3. lastSummarizedMessageId 时,从其后保留安全尾段;否则把 resumed session 视为已有 memory summary。
  4. 调整切点,避免断开 tool_use/tool_result 或 thinking blocks。
  5. 创建标准 compact_boundary + summary user message。
  6. 若 post-compact token count 仍超过阈值,放弃并回退传统 compact。

因为产物仍是标准 CompactionResult,下游写 transcript 和恢复逻辑与传统 compact 共用。

Context-collapse 当前状态

本仓库保留了 context-collapse 的持久化接口,但核心实现是 stub

模块 当前行为
contextCollapse/index.ts applyCollapsesIfNeeded() 返回原 messagesrecoverFromOverflow() 返回 committed=0isWithheldPromptTooLong() 恒 false。
contextCollapse/operations.ts projectView() 是 identity。
contextCollapse/persist.ts restoreFromEntries() 是 no-op。

已预留 JSONL entry

Entry 写入接口 内容
marble-origami-commit recordContextCollapseCommit() collapseId、summary UUID/content、archived span 边界。
marble-origami-snapshot recordContextCollapseSnapshot() staged spans、armed、lastSpawnTokens。

loader 会收集这些 entry遇到 compact boundary 时会清空旧 commits/snapshot避免它们引用已被 compact 丢弃的 UUID。

所以当前真实生效的上下文缩减主要是 compact、session memory compact、tool_result budget、microcompact 和 snipcontext-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。

只在主线程 compact 清:

  • context-collapse store。
  • getUserContext cache。
  • memory files cache。

原因subagent 和主线程同进程,共享模块级状态。agent:* compact 如果清主线程 context-collapse 或 memory cache会破坏父会话状态。

它明确不清 resetSentSkillNames(),避免 compact 后重新注入完整 skill listing浪费 token 和 prompt cache。

分支与 Fork 对比

入口 本质 是否新主 session 是否 subagent 持久化位置 父会话看到什么 恢复方式
/branch 复制当前主 transcript 成新 JSONL <newSessionId>.jsonl 直接切到新分支会话 普通 session resume。
--fork-session resume/continue 时把旧消息作为新 session 初始消息 新 session 首次写入时 materialize 启动即在新 session 中继续 新 session resume。
/fork <directive> slash wrapper调用 AgentTool fork subagents/agent-<id>.jsonl + .meta.json fork started + task notification resumeAgentBackground()
AgentTool({ fork: true }) Tool 层 fork 子 agent subagents/agent-<id>.jsonl + .meta.json sync final tool_result 或 async notification resumeAgentBackground()
普通 AgentTool async 后台本地 subagent subagents/agent-<id>.jsonl + .meta.json async_launched + task notification resumeAgentBackground()
remote AgentTool CCR remote session 远端 remote-agents/*.meta.json remote task output/notification restoreRemoteAgentTasks() + CCR。

/branch

/branch 创建新 session 文件,不是在原 JSONL 里追加 branch marker。

流程:

  1. 生成新的 sessionId。
  2. 读取当前 transcript 文件。
  3. 过滤主会话消息,排除 isSidechain 和非 transcript entry。
  4. 复制消息并重写 sessionId
  5. 重新串 parentUuid
  6. 添加 forkedFrom: { sessionId, messageUuid }
  7. 复制原 session 的 content-replacement entry 并改成新 sessionId。
  8. 写入 <newSessionId>.jsonl
  9. 构造 LogOption 并让 REPL resume 到新分支。

--fork-session

--fork-session 只改变 resume 的 ownership

非 fork resume fork-session resume
切到旧 sessionId。 保持启动时 fresh sessionId。
adoptResumedSessionFile() 接管旧 JSONL。 不接管旧 JSONL。
后续继续 append 到旧 transcript。 后续 materialize 成新 transcript。
原 session 继续增长。 原 session 不被写入。

如果旧 session 有 content-replacement,会先把 records seed 到新 session避免大 tool_result 的替换状态丢失。

Subagent 与 Fork Agent

普通 subagent

普通 AgentTool subagent 最终走 runAgent()

sequenceDiagram
  participant Parent as 父会话
  participant Tool as AgentTool
  participant Agent as runAgent
  participant Side as sidechain JSONL
  participant Task as LocalAgentTask

  Parent->>Tool: assistant tool_use Agent
  Tool->>Agent: start sync or async
  Agent->>Side: record initialMessages
  Agent->>Side: record assistant/user/progress/compact_boundary
  alt sync foreground
    Agent-->>Tool: final result
    Tool-->>Parent: Agent tool_result
  else async/background
    Tool-->>Parent: async_launched tool_result
    Agent-->>Task: complete
    Task-->>Parent: <task-notification>
  end

父会话通常只记录:

  • Agent tool_use。
  • Agent tool_result。
  • async launch result。
  • task notification。
  • 必要 progress。

完整子 agent 内部工具调用和消息在 sidechain JSONL 中,不会混进主会话 active context。

Fork agent

fork agent 是 AgentTool 的一种特殊 subagent。它继承父上下文、system prompt、tools、model 和 thinking config目标是让多个子 agent 共享尽可能长的 byte-identical prompt cache prefix。

关键实现:

继承内容 实现
system prompt 优先使用 toolUseContext.renderedSystemPrompt,没有才 fallback 重建。
tools 使用父 toolUseContext.options.toolsuseExactTools: true
model FORK_AGENT.model = "inherit"
thinking/non-interactive 通过 exact tool/options 继承,避免 cache key 分叉。
messages forkContextMessages = toolUseContext.messages

buildForkedMessages() 负责构造 cache-friendly 尾部:

parent history...
assistant: [text/thinking/tool_use A/tool_use B/...]
user:
  tool_result for A = "Fork started — processing in background"
  tool_result for B = "Fork started — processing in background"
  directive = "<this fork's task>"

多个 fork child 的长前缀相同,只有最后 directive 不同。

限制:

限制 原因
需要 FORK_SUBAGENT feature。 功能门控。
coordinator mode 禁用。 coordinator 已有自己的编排模型。
non-interactive session 禁用。 fork subagent 偏交互式后台任务模型。
fork child 禁止递归 fork。 防止无限 fork通过 querySource 和 boilerplate tag 检测。
resume fork agent 不再传 forkContextMessages sidechain 已包含父上下文切片,重复传会造成重复 tool_use id。

runForkedAgent() 不是 AgentTool fork

src/utils/forkedAgent.tsrunForkedAgent() 是内部 cache-safe side query 工具,用于 session memory、prompt suggestion、summary 等。它复用父 system/user/system context、tools、messages可选 skipTranscript,但默认不写 AgentTool metadata也不是用户可继续对话的 AgentTool fork。

Agent 恢复

本地 agent 恢复入口是 resumeAgentBackground()

流程:

flowchart TD
  A[user continues agent] --> B[getAgentTranscript(agentId)]
  B --> C[load sidechain JSONL + build chain]
  C --> D[readAgentMetadata(agentId)]
  D --> E[filter unresolved tool_use/thinking/blank assistant]
  E --> F[reconstruct content replacement state]
  F --> G{metadata.worktreePath exists?}
  G -- yes --> H[runWithCwdOverride(worktreePath)]
  G -- no --> I[parent cwd]
  H --> J[register async LocalAgentTask]
  I --> J
  J --> K[continue query loop]

恢复时:

状态 来源
agent transcript agent-<agentId>.jsonl
agent type agent-<agentId>.meta.json
fork/general agent 选择 metadata agentType
worktree cwd metadata worktreePath,目录不存在则回退父 cwd。
content replacement sidechain records + parent live state gap-fill。
task UI 重新注册 async task。

Remote Agent 恢复

remote CCR agent 不靠本地 sidechain 继续执行。

sequenceDiagram
  participant Tool as AgentTool
  participant R as RemoteAgentTask
  participant Sidecar as remote-agents meta
  participant CCR as CCR session
  participant REPL as REPL resume

  Tool->>CCR: teleportToRemote()
  Tool->>R: registerRemoteAgentTask()
  R->>Sidecar: write remote-agent-<taskId>.meta.json
  REPL->>Sidecar: restoreRemoteAgentTasks()
  REPL->>CCR: fetchSession(sessionId)
  alt running
    REPL->>R: rebuild RemoteAgentTaskState + polling
  else 404/archive
    REPL->>Sidecar: delete sidecar
  end

差异:

本地 subagent remote agent
有完整 sidechain JSONL。 没有本地执行 transcript。
resume 可继续 API 对话。 resume 只恢复 polling。
状态来自 JSONL + .meta.json 状态来自 CCR session + local sidecar。
完成后本地 sidechain 仍可审计。 完成/archived 后 sidecar 会删除。

常见误区

误区 正确理解
JSONL 顺序就是会话顺序 恢复靠 leaf + parentUuid,不是简单顺序 replay。
compact 删除了旧历史 compact 追加 boundary旧历史仍在 raw transcript。
boundary 会发给模型 boundary 是本地 system markerAPI normalization 会过滤。
/branch/fork 都是 fork /branch 是新主 session/fork 是 fork subagent sidechain。
--fork-session 等于 /branch 它不是复制文件命令,而是 resume 时保持 fresh session ownership。
subagent 消息会进入主上下文 父会话只看到 Agent tool result/notification完整内部消息在 sidechain。
remote agent 有本地 sidechain remote 只有 sidecar 身份,执行状态来自 CCR。
context-collapse 已经真实压缩上下文 当前仓库中 context-collapse 核心实现是 stub。

源码入口索引

问题 从这里看
Entry union 有哪些类型 src/types/logs.tsEntry
主 transcript 路径 src/utils/sessionStorage.tsgetTranscriptPath()
subagent transcript 路径 getAgentTranscriptPath(agentId)
remote sidecar 路径 getRemoteAgentsDir() / getRemoteAgentMetadataPath()
主写入 recordTranscript()
sidechain 写入 recordSidechainTranscript()
write queue Project.enqueueWrite() / drainWriteQueue() / flush()
lazy materialize Project.materializeSessionFile()
tombstone 删除 removeTranscriptMessage() / Project.removeMessageByUuid()
读取 transcript loadTranscriptFile()
大文件读取 readTranscriptForLoad() in sessionStoragePortable.ts
dead branch 裁剪 walkChainBeforeParse()
parent 链重建 buildConversationChain()
parallel tool_result 补回 recoverOrphanedParallelToolResults()
preserved segment applyPreservedSegmentRelinks()
snip removal applySnipRemovals()
CLI resume 加载 loadConversationForResume()
resume 状态切换 processResumedConversation()
AppState 恢复 restoreSessionStateFromLog()
中断检测 deserializeMessagesWithInterruptDetection()
active context getMessagesAfterCompactBoundary()
query context pipeline src/query.ts
compact boundary createCompactBoundaryMessage()
auto compact autoCompactIfNeeded() / shouldAutoCompact()
session memory compact src/services/compact/sessionMemoryCompact.ts
reactive compact src/services/compact/reactiveCompact.ts
post compact cleanup runPostCompactCleanup()
context-collapse stub src/services/contextCollapse/*
/branch src/commands/branch/branch.ts
/fork src/commands/fork/fork.tsx
AgentTool fork AgentTool.tsx + forkSubagent.ts
普通 subagent 运行 runAgent.ts
agent resume resumeAgent.ts
remote task restore restoreRemoteAgentTasks()