会话恢复与选择器:从本地索引到运行时重载
Claude Code 提供了灵活的会话管理机制,允许用户在不同终端、甚至不同时间点,无缝接回之前的对话。这一机制的核心在于对本地 Transcript(对话转录本)的持久化与渐进式恢复协议。
先搞清楚这是什么
会话管理本质上是 运行时状态的序列化与渐进反序列化协议。
这不是简单地“重新打开一个文本文件”,而是要重建整个应用的状态机(State Machine)。当执行 --resume、--fork-session 或在启动时进入交互式选择界面时,系统需要决定:是否保留旧的 Session ID、是否继承之前的成本统计、以及是否需要将工作目录(CWD)切回上次退出时的位置。/resume 调出的 session picker 也不是一个简单的文件列表,而是一个针对恢复动作优化的本地索引层。
从源码看实现
会话恢复逻辑由 claude-code-opensource/src/utils/sessionRestore.ts 和 claude-code-opensource/src/utils/sessionStorage.ts 共同驱动,其执行流程如下:
渐进式加载(Progressive Loading): 为了保证极速的首屏感官,
session picker采用了“由浅入深”的加载策略:- Lite Metadata 扫描:系统首先仅调用
stat扫描~/.claude/sessions/目录下的文件路径、修改时间与大小。 - 头尾窗口探测:利用
readLiteMetadata只读取 Transcript 的头部和尾部小窗口,提取出firstPrompt(标题)、gitBranch、customTitle和summary等元数据,而不是全量反序列化整个消息链。 - Full Log 延迟补全:只有当用户真正选中某一条进行恢复时,系统才会执行
loadFullLog。
- Lite Metadata 扫描:系统首先仅调用
分叉判定(Fork vs Resume):
- Resume/Continue:系统继承原始的 Session ID。在云端计费和本地日志中,这被视为同一个连续的事务。
- Fork:系统生成全新的 Session ID,但会从旧 Transcript 中克隆所有消息。这种方式利用了“缓存安全参数(Cache Safe Params)”,确保新会话能够复用旧会话的 Prompt Cache。
状态复水(Rehydration):
- 工作树锁定:如果原始会话是在特定 Git Worktree 中运行的,系统会尝试自动
chdir回去,确保路径引用的物理一致性。 - 成本审计恢复:通过
restoreCostStateForSession恢复该会话累积的美元消耗和 Token 计数。 - 环境适配:如果旧记录的模型设置与当前 CLI 参数不同,系统会通过
setMainLoopModelOverride实施强行覆盖。
- 工作树锁定:如果原始会话是在特定 Git Worktree 中运行的,系统会尝试自动
跨项目接线(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可以在保留上下文的同时,开启一个新的独立审计隔离。
继续探索
- 如果你想了解如何在多个 Git worktree 之间并行切换会话,请看 Git Worktree 协作:隔离的并行开发流。
- 如果你想了解恢复后的状态是如何被注入到新 REPL 循环的,请看 会话启动生命周期:SessionStart 与环境注入。
源码锚点
claude-code-opensource/src/utils/sessionRestore.ts—restoreSessionStateFromLog核心状态重装函数。
📄 src/utils/sessionRestore.ts — `restoreSessionStateFromLog` 核心状态重装函数。
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 聚合。
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.tsx—session pickerUI 的分组、预览与搜索实现。
📄 src/components/LogSelector.tsx — `session picker` UI 的分组、预览与搜索实现。
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 — 启动时交互式恢复界面的顶层容器。
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;
}