六、深入 Claude Code CLI 源码:会话管理与持久化

你有没有想过,Claude Code 如何在关闭后还能恢复之前的对话?今天我们来深入分析会话存储系统,这是 CLI 的记忆中枢。

JSONL 日志格式

会话数据保存在 JSONL (JSON Lines) 文件中,每行一条记录:

bash 复制代码
~/.claude/projects/<project-id>/session-<uuid>.jsonl

为什么用 JSONL 而不是普通 JSON?

typescript 复制代码
// JSONL 允许逐行追加,不需要读写整个文件
await appendFile(sessionPath, jsonLine + '\n')

普通 JSON 需要读取整个数组、追加、再写入------文件越大越慢。JSONL 只追加一行,性能稳定。

消息类型与转录

Entry 类型定义

typescript 复制代码
type Entry =
  | UserMessage           // 用户输入
  | AssistantMessage      // AI 响应
  | AttachmentMessage     // 文件附件
  | SystemMessage         // 系统消息
  | ToolUseSummaryMessage // 工具执行摘要
  | ProgressMessage       // 进度状态(UI 临时)
  | TranscriptMessage     // 转录记录

Transcript Message:转录的核心

typescript 复制代码
export function isTranscriptMessage(entry: Entry): entry is TranscriptMessage {
  return (
    entry.type === 'user' ||
    entry.type === 'assistant' ||
    entry.type === 'attachment' ||
    entry.type === 'system'
  )
}

注意:ProgressMessage 不是转录消息!它是临时 UI 状态,不持久化到 JSONL。这避免了链式关系断裂(曾导致恢复时消息丢失)。

parentUuid 链式关系

每条消息通过 parentUuid 连接到上一条,形成消息链:

typescript 复制代码
type Message = {
  uuid: UUID
  parentUuid: UUID | null  // 第一条消息 parentUuid 为 null
  type: string
  // ...
}

链式插入

typescript 复制代码
function insertMessageChain(messages, parentUuid) {
  let currentParent = parentUuid
  for (const msg of messages) {
    msg.uuid = randomUUID()
    msg.parentUuid = currentParent
    currentParent = msg.uuid
  }
}

这种设计支持:

  • 消息分支(重试时创建分支)
  • 历史回溯(顺着 parentUuid 回溯)
  • 压缩恢复(压缩后恢复链式关系)

会话恢复流程

当你用 /resume 或启动时自动恢复:

typescript 复制代码
async function loadSession(sessionId: SessionId) {
  // 1. 定位会话文件
  const sessionPath = getSessionPath(sessionId)

  // 2. 解析 JSONL
  const entries = parseJSONL(await readFile(sessionPath))

  // 3. 过滤转录消息
  const transcriptMessages = entries.filter(isTranscriptMessage)

  // 4. 排序(按时间/uuid)
  const sorted = sortLogs(transcriptMessages)

  // 5. 加载到 REPL
  setMessages(sorted)
}

Tombstone 处理

压缩后,被移除的消息用 Tombstone 标记:

typescript 复制代码
type TombstoneMessage = {
  type: 'tombstone'
  uuid: UUID
  parentUuid: UUID
  replacesUuids: UUID[]  // 被替换的消息列表
}

Tombstone 表示"这条消息已被压缩替代",恢复时跳过原始消息,使用压缩后的消息。

压缩边界

压缩时会插入边界标记:

typescript 复制代码
type SystemCompactBoundaryMessage = {
  type: 'system'
  subtype: 'compact_boundary'
  uuid: UUID
  parentUuid: UUID
  compactedUuids: UUID[]  // 被压缩的消息
  summary: string         // 压缩摘要
}

恢复时遇到 compact_boundary,就知道之前的消息已被压缩,可以跳过处理。

并发会话

Claude Code 支持多个项目独立会话:

typescript 复制代码
// 项目目录 → 项目 ID
const projectId = sanitizePath(getOriginalCwd())

// 会话存储路径
const sessionDir = join(
  getClaudeConfigHomeDir(),
  'projects',
  projectId
)

不同项目目录的会话分开存储,不会互相干扰。

会话命名

typescript 复制代码
async function updateSessionName(sessionId: SessionId, name: string) {
  // 名称写入 .session-name 文件
  await writeFile(join(sessionDir, '.session-name'), name)
}

用户可以为会话命名,方便识别。

性能优化

读取尾部优化

大文件读取尾部用特殊方法:

typescript 复制代码
// 不读整个文件,只读尾部 N 行
const tail = readFileTailSync(sessionPath, 1000)

原理:用 fstatSync 获取文件大小,然后从末尾向前读取固定字节数。

首次提示提取

恢复时只需要提取第一个用户提示:

typescript 复制代码
const SKIP_FIRST_PROMPT_PATTERN =
  /^(?:\s*<[a-z][\w-]*[\s>]|\[Request interrupted by user[^\]]*\])/

function extractFirstPrompt(entries) {
  // 跳过 IDE context、hook output 等非核心消息
  for (const entry of entries) {
    if (isUserMessage(entry) && !SKIP_FIRST_PROMPT_PATTERN.test(entry.content)) {
      return entry.content
    }
  }
}

这避免了加载大量历史消息。

Tombstone 重写限制

typescript 复制代码
const MAX_TOMBSTONE_REWRITE_BYTES = 50 * 1024 * 1024  // 50MB

Tombstone 重写整个文件,超过 50MB 时跳过,防止内存溢出。

消息压缩策略

会话文件会越来越大。压缩策略:

typescript 复制代码
type ContextCollapseSnapshotEntry = {
  type: 'context_collapse_snapshot'
  uuid: UUID
  parentUuid: UUID
  tokenCount: number
  summary: string
  preservedMessages: UUID[]
}

压缩时:

  1. 识别低价值消息(大量 Read/Grep 工具调用)
  2. 生成摘要替代
  3. 插入 context_collapse_snapshot 标记
  4. 原始消息被 Tombstone 替换

恢复时,压缩摘要作为上下文,避免加载所有原始消息。

会话恢复的边界情况

链断裂修复

typescript 复制代码
// 修复断裂的 parentUuid 链
function repairChain(messages) {
  for (let i = 1; i < messages.length; i++) {
    if (!messages[i].parentUuid) {
      messages[i].parentUuid = messages[i - 1].uuid
    }
  }
}

曾出现过 Progress Message 被误加入链导致断裂,现在严格过滤。

Worktree 会话

typescript 复制代码
type PersistedWorktreeSession = {
  type: 'persisted_worktree_session'
  worktreePath: string
  sessionId: SessionId
}

Git worktree 有独立的会话,恢复时能正确切换目录。

总结

Claude Code 的会话管理系统展示了几个有趣的设计:

  1. JSONL 格式:追加式写入,性能稳定
  2. 链式关系:parentUuid 构建消息树
  3. 类型过滤:Progress Message 不持久化,避免链断裂
  4. 压缩边界:compact_boundary 标记压缩边界
  5. 并发会话:不同项目独立存储
  6. 性能优化:尾部读取、首次提示提取、重写限制

下一篇我会分享命令系统,看看斜杠命令是如何设计和实现的。

相关推荐
云安全助手12 小时前
Anthropic年度报告解读:AI重塑网络攻击形态,传统防御体系亟待升级
人工智能·安全·网络安全·ai大模型
谁似人间西林客12 小时前
汽车智能制造解决方案:如何通过智能仓储物流降本提效?
人工智能·汽车·制造
jiushiapwojdap12 小时前
Antigravity Awesome Skills:1527+ AI 编程助手的可安装技能库
人工智能·其他
顾北顾13 小时前
多头注意力机制
人工智能·深度学习·算法
hujinyuan2016013 小时前
2025年12月中国电子学会青少年机器人技术等级考试试卷(二级) 真题+答案
人工智能·算法·机器人
码农小白AI13 小时前
采购合同与来料证书对标校验,IACheck联动AI报告审核通审Agent版自动识别指标不符单据
人工智能
元岳数字人小元14 小时前
AI 数字人开发公司浅谈 虚拟数字人打造景区新服务
人工智能·人机交互·交互
哦哦~92114 小时前
AI赋能生物医学:从临床数据到药物分子性质预测实战培
人工智能·生物医学·药物分子
GIS数据转换器14 小时前
城市排水生命线安全运行监测平台深度解析
java·运维·人工智能·python·安全·数据挖掘·无人机
虫无涯14 小时前
本地离线大模型实战:Ollama + Llama 3.1 8B 全流程部署(适配VSCode Continue代码助手)
人工智能