Skip to content
源码分析手册

会话恢复与选择器:从本地索引到运行时重载

Claude Code 提供了灵活的会话管理机制,允许用户在不同终端、甚至不同时间点,无缝接回之前的对话。这一机制的核心在于对本地 Transcript(对话转录本)的持久化与渐进式恢复协议。

先搞清楚这是什么

会话管理本质上是 运行时状态的序列化与渐进反序列化协议

这不是简单地“重新打开一个文本文件”,而是要重建整个应用的状态机(State Machine)。当执行 --resume--fork-session 或在启动时进入交互式选择界面时,系统需要决定:是否保留旧的 Session ID、是否继承之前的成本统计、以及是否需要将工作目录(CWD)切回上次退出时的位置。/resume 调出的 session picker 也不是一个简单的文件列表,而是一个针对恢复动作优化的本地索引层。

从源码看实现

会话恢复逻辑由 claude-code-opensource/src/utils/sessionRestore.tsclaude-code-opensource/src/utils/sessionStorage.ts 共同驱动,其执行流程如下:

  1. 渐进式加载(Progressive Loading): 为了保证极速的首屏感官,session picker 采用了“由浅入深”的加载策略:

    • Lite Metadata 扫描:系统首先仅调用 stat 扫描 ~/.claude/sessions/ 目录下的文件路径、修改时间与大小。
    • 头尾窗口探测:利用 readLiteMetadata 只读取 Transcript 的头部和尾部小窗口,提取出 firstPrompt(标题)、gitBranchcustomTitlesummary 等元数据,而不是全量反序列化整个消息链。
    • Full Log 延迟补全:只有当用户真正选中某一条进行恢复时,系统才会执行 loadFullLog
  2. 分叉判定(Fork vs Resume)

    • Resume/Continue:系统继承原始的 Session ID。在云端计费和本地日志中,这被视为同一个连续的事务。
    • Fork:系统生成全新的 Session ID,但会从旧 Transcript 中克隆所有消息。这种方式利用了“缓存安全参数(Cache Safe Params)”,确保新会话能够复用旧会话的 Prompt Cache。
  3. 状态复水(Rehydration)

    • 工作树锁定:如果原始会话是在特定 Git Worktree 中运行的,系统会尝试自动 chdir 回去,确保路径引用的物理一致性。
    • 成本审计恢复:通过 restoreCostStateForSession 恢复该会话累积的美元消耗和 Token 计数。
    • 环境适配:如果旧记录的模型设置与当前 CLI 参数不同,系统会通过 setMainLoopModelOverride 实施强行覆盖。
  4. 跨项目接线(Cross-Project Logic): 如果恢复的是完全不同的项目目录,claude-code-opensource/src/utils/crossProjectResume.ts 会拦截直接切换,而是生成一条 cd <path> && claude --resume <id> 命令并复制到剪贴板,引导用户在正确环境下接回会话。

别踩这些坑

  • 路径物理依赖:虽然系统尝试恢复工作树,但如果物理目录已被删除,恢复将退回到当前的 CWD,这可能导致模型对相对路径的理解出现严重偏差。
  • 附件遗失风险:Transcript 主要存储文本。如果原始会话包含大量的临时大型附件(如已删除的临时测试报告),恢复后的会话可能无法重新加载这些附件内容。
  • 过滤规则session picker 会主动过滤掉 isSidechain(侧链任务)和 teamName(团队协作)类型的会话,它仅面向主会话恢复。
  • Session ID 凭证:Session ID 是云端审计的唯一凭证。使用 --fork-session 可以在保留上下文的同时,开启一个新的独立审计隔离。

继续探索

源码锚点

  • claude-code-opensource/src/utils/sessionRestore.tsrestoreSessionStateFromLog 核心状态重装函数。
📄 src/utils/sessionRestore.ts — `restoreSessionStateFromLog` 核心状态重装函数。L99-128 of 552
typescript
export function restoreSessionStateFromLog(
  result: ResumeResult,
  setAppState: (f: (prev: AppState) => AppState) => void,
): void {
  // Restore file history state
  if (result.fileHistorySnapshots && result.fileHistorySnapshots.length > 0) {
    fileHistoryRestoreStateFromLog(result.fileHistorySnapshots, newState => {
      setAppState(prev => ({ ...prev, fileHistory: newState }))
    })
  }

  // Restore attribution state (ant-only feature)
  if (
    feature('COMMIT_ATTRIBUTION') &&
    result.attributionSnapshots &&
    result.attributionSnapshots.length > 0
  ) {
    attributionRestoreStateFromLog(result.attributionSnapshots, newState => {
      setAppState(prev => ({ ...prev, attribution: newState }))
    })
  }

  // Restore context-collapse commit log + staged snapshot. Must run before
  // the first query() so projectView() can rebuild the collapsed view from
  // the resumed Message[]. Called unconditionally (even with
  // undefined/empty entries) because restoreFromEntries resets the store
  // first — without that, an in-session /resume into a session with no
  // commits would leave the prior session's stale commit log intact.
  if (feature('CONTEXT_COLLAPSE')) {
    /* eslint-disable @typescript-eslint/no-require-imports */
  • claude-code-opensource/src/utils/sessionStorage.ts — 渐进式日志加载、Lite 元数据提取与同仓 Worktree 聚合。
📄 src/utils/sessionStorage.ts — 渐进式日志加载、Lite 元数据提取与同仓 Worktree 聚合。L452-461 of 5106
typescript
        // window. readLiteMetadata only reads the tail to extract these
        // fields — if enough messages are appended after a /rename, the
        // custom-title entry gets pushed outside the window and --resume
        // shows the auto-generated firstPrompt instead.
        await project?.flush()
        try {
          project?.reAppendSessionMetadata()
        } catch {
          // Best-effort — don't let metadata re-append crash the cleanup
        }
  • claude-code-opensource/src/components/LogSelector.tsxsession picker UI 的分组、预览与搜索实现。
📄 src/components/LogSelector.tsx — `session picker` UI 的分组、预览与搜索实现。L33-44 of 1575
tsx
type AgenticSearchState = {
  status: 'idle';
} | {
  status: 'searching';
} | {
  status: 'results';
  results: LogOption[];
  query: string;
} | {
  status: 'error';
  message: string;
};
  • claude-code-opensource/src/screens/ResumeConversation.tsx — 启动时交互式恢复界面的顶层容器。
📄 src/screens/ResumeConversation.tsx — 启动时交互式恢复界面的顶层容器。L36-46 of 399
tsx
function parsePrIdentifier(value: string): number | null {
  const directNumber = parseInt(value, 10);
  if (!isNaN(directNumber) && directNumber > 0) {
    return directNumber;
  }
  const urlMatch = value.match(/github\.com\/[^/]+\/[^/]+\/pull\/(\d+)/);
  if (urlMatch?.[1]) {
    return parseInt(urlMatch[1], 10);
  }
  return null;
}

基于 Claude Code v2.1.88 开源快照的深度分析