跳转至

Deep Dive | utils/sessionStorage.ts 5105 行会话存储拆解

重要性:⭐⭐⭐⭐(会话数据持久化 —— Claude Code "能恢复" 的关键) 真实位置src/utils/sessionStorage.ts5105 行,项目第四大文件) 核心角色: - 写到 ~/.claude/sessions/ - 加密(敏感数据) - 压缩(节省磁盘) - 索引(快速查找) - 恢复(crash 后) - 跨设备同步(推测)

关联phase-03-state.mdtopics/big-files-untold-stories.mdwalkthrough/handwrite-store.md


1. 文件结构总览

sessionStorage.ts (5105 行)
├── 行 1-98   :imports + 工具类型
├── 行 99     :VERSION 常量
├── A. 消息类型守卫(行 101-196)
│   ├── Transcript 类型定义 (行 101-122)
│   ├── MAX_TOMBSTONE_REWRITE_BYTES 常量 (行 123)
│   ├── SKIP_FIRST_PROMPT_PATTERN (行 125-138)
│   ├── isTranscriptMessage (行 139-153)
│   ├── isChainParticipant (行 154-157)
│   ├── LegacyProgressEntry (行 158-167)
│   ├── isLegacyProgressEntry (行 169-184)
│   ├── EPHEMERAL_PROGRESS_TYPES (行 186-192)
│   ├── isEphemeralToolProgress (行 194-196)
├── B. 路径解析(行 198-261)
│   ├── getProjectsDir (行 198-200)
│   ├── getTranscriptPath (行 202-205)
│   ├── getTranscriptPathForSession (行 207-227)
│   ├── MAX_TRANSCRIPT_READ_BYTES (行 229-232)
│   ├── agentTranscriptSubdirs (行 234)
│   ├── setAgentTranscriptSubdir (行 236-241)
│   ├── clearAgentTranscriptSubdir (行 243-245)
│   ├── getAgentTranscriptPath (行 247-258)
├── C. Agent 元数据(行 260-399)
│   ├── getAgentMetadataPath (行 260-262)
│   ├── AgentMetadata 类型 (行 264-281)
│   ├── writeAgentMetadata (行 283-290)
│   ├── readAgentMetadata (行 292-303)
│   ├── RemoteAgentMetadata 类型 (行 305-318)
│   ├── getRemoteAgentsDir (行 320-325)
│   ├── getRemoteAgentMetadataPath (行 327-335)
│   ├── writeRemoteAgentMetadata (行 337-344)
│   ├── readRemoteAgentMetadata (行 346-357)
│   ├── deleteRemoteAgentMetadata (行 359-371)
│   ├── listRemoteAgentMetadata (行 373-399)
├── D. Session ID 操作(行 401-?)
│   ├── sessionIdExists (行 401-?)
├── E. 推测:saveSession / loadSession(推测大段)
├── F. 推测:加密 / 压缩
├── G. 推测:索引 / 列表
└── H. 推测:迁移 / 兼容旧版本

2. A 段:消息类型守卫(行 101-196)

2.1 Transcript 类型(行 101-122)

type Transcript = (
  | UserMessage
  | AssistantMessage
  | ProgressMessage
  | SystemMessage
  // ... 20+ message types
)

Session 持久化的"全部消息类型"联合

2.2 SKIP_FIRST_PROMPT_PATTERN(行 125-138)

const SKIP_FIRST_PROMPT_PATTERN = /^.../  // 推测

跳过某些首条 prompt 的模式 —— 比如 /login 这种命令不记录到 transcript

意义避免敏感信息(如密码)被持久化

2.3 isTranscriptMessage(行 139-153)

export function isTranscriptMessage(entry: Entry): entry is TranscriptMessage {
  return 'role' in entry && ['user', 'assistant', 'system'].includes(entry.role)
}

类型守卫 —— 过滤出"真消息"(排除 progress、tool_use 等)。

2.4 isChainParticipant(行 154-157)

export function isChainParticipant(m: Pick<Message, 'type'>): boolean {
  return m.type === 'user' || m.type === 'assistant'
}

"对话链"参与者 —— 只有 user + assistant 是,其他(system/progress)不算。

为什么: - 上下文压缩时,只压缩 user/assistant(保留"对话流") - progress / system 是"附加信息",可丢

2.5 isEphemeralToolProgress(行 194-196)

const EPHEMERAL_PROGRESS_TYPES = new Set(['bash_progress', 'tool_use_progress'])

export function isEphemeralToolProgress(dataType: unknown): boolean {
  return EPHEMERAL_PROGRESS_TYPES.has(dataType as string)
}

"短暂"工具进度 —— 不持久化

原因: - 工具已结束,progress 没意义 - 节省磁盘


3. B 段:路径解析(行 198-261)

3.1 getProjectsDir(行 198-200)

export function getProjectsDir(): string {
  return path.join(os.homedir(), '.claude', 'projects')
}

所有项目 session 共享的目录 —— ~/.claude/projects/

3.2 getTranscriptPathForSession(行 207-227)

export function getTranscriptPathForSession(sessionId: string): string {
  // 推测:
  // 1. 用 sessionId 哈希得到项目子目录
  // 2. 或直接按 sessionId 命名
  return path.join(getProjectsDir(), derivedProjectDir, `${sessionId}.jsonl`)
}

20+ 行的实现 —— 推测涉及: - Session ID → 项目目录映射 - 跨平台路径处理 - 文件名清理(去掉非法字符)

3.3 agentTranscriptSubdirs + getAgentTranscriptPath(行 234-258)

const agentTranscriptSubdirs = new Map<string, string>()

export function setAgentTranscriptSubdir(agentId: string, subdir: string): void {
  agentTranscriptSubdirs.set(agentId, subdir)
}

export function getAgentTranscriptPath(agentId: AgentId): string {
  const subdir = agentTranscriptSubdirs.get(agentId) ?? 'default'
  return path.join(getProjectsDir(), subdir, `${agentId}.jsonl`)
}

Agent 路径管理 —— 主 session + 多个 sub-agent 各自一个 transcript。

Map<agentId, subdir> —— 推测用于多项目并发时区分 agent。


4. C 段:Agent 元数据(行 260-399)

4.1 AgentMetadata 类型(行 264-281)

export type AgentMetadata = {
  agentId: AgentId
  parentSessionId: SessionId | null  // 父 session
  startTime: number
  // ... 推测 10+ 字段
}

Agent 元数据 —— 标识 agent、关联父 session、记录启动时间。

4.2 writeAgentMetadata / readAgentMetadata(行 283-303)

export async function writeAgentMetadata(
  agentId: AgentId,
  metadata: AgentMetadata,
): Promise<void> {
  // 1. 序列化为 JSON
  // 2. 写到 ~/.claude/projects/<subdir>/<agentId>.meta.json
}

export async function readAgentMetadata(agentId: AgentId): Promise<AgentMetadata | null> {
  // 1. 检查文件存在
  // 2. 读文件
  // 3. JSON.parse
  // 4. 错误时返回 null(不抛)
}

20+ 行的实现 —— 推测涉及: - 原子写(先写临时文件再 rename) - 错误处理(捕获 + 返回 null) - 加密(敏感字段)

4.3 RemoteAgentMetadata 系列(行 305-399)

export type RemoteAgentMetadata = {
  // 推测类似 AgentMetadata
  // 但额外含远程信息(CCR / Bedrock / 远程 server)
  remoteUrl?: string
  remoteSessionId?: string
  // ...
}

export async function listRemoteAgentMetadata(): Promise<RemoteAgentMetadata[]> {
  // 列所有远程 agent
  // 推测:扫 ~/.claude/projects/ 下的 *-remote.meta.json
}

远程 agent 专用 —— 多设备同步时用。


5. 推测的 D-H 段(行 400-5105)

5.1 sessionIdExists(行 401+)

export function sessionIdExists(sessionId: string): boolean {
  return fs.existsSync(getTranscriptPathForSession(sessionId))
}

检查 session 是否存在 —— /resume 时用。

5.2 推测的 saveSession(核心写入)

export async function saveSession(
  sessionId: string,
  state: AppState,
): Promise<void> {
  // 推测的 5 步骤:
  // 1. 序列化 messages(剔除 ephemeral)
  // 2. 压缩(大文件 → gzip)
  // 3. 加密(敏感字段)
  // 4. 写到临时文件
  // 5. 原子 rename
}

核心写入函数 —— session 结束时调。

5.3 推测的 loadSession(核心读取)

export async function loadSession(
  sessionId: string,
): Promise<{ messages: Message[]; state: Partial<AppState> } | null> {
  // 1. 读文件
  // 2. 解密
  // 3. 解压
  // 4. JSON.parse
  // 5. 反序列化消息
  // 6. 返回 messages + 恢复 state
}

核心读取 —— /resume 时调。

5.4 推测的 listSessions

export async function listSessions(
  filter?: SessionFilter,
): Promise<SessionMetadata[]> {
  // 1. 扫 ~/.claude/projects/ 目录
  // 2. 读每个 .meta.json
  // 3. 按时间 / project / model 过滤
  // 4. 排序
  // 5. 返回
}

列所有 session —— /resume 选 session 时用。

5.5 推测的 deleteSession / migrateSession

export async function deleteSession(sessionId: string): Promise<void> {
  // 1. 读 meta
  // 2. 删 transcript 文件
  // 3. 删 meta 文件
  // 4. 删子 agent 文件
}

export async function migrateSession(
  sessionId: string,
  fromVersion: string,
  toVersion: string,
): Promise<void> {
  // 1. 读旧版 session
  // 2. 转换到新版
  // 3. 写新版
}

删除 + 版本迁移 —— 维护用。

5.6 推测的 compressSession / decompressSession

function compressSession(json: string): Buffer {
  return gzipSync(json)
}

function decompressSession(buf: Buffer): string {
  return gunzipSync(buf).toString()
}

压缩 / 解压 —— gzip 节省 70%+ 磁盘。

5.7 推测的 encryptSensitiveFields / decryptSensitiveFields

function encryptSensitiveFields(state: AppState, key: Buffer): AppState {
  // 加密:
  // - API keys
  // - OAuth tokens
  // - 用户邮箱
  return encrypted
}

function decryptSensitiveFields(state: AppState, key: Buffer): AppState {
  // 解密
  return decrypted
}

加密敏感字段 —— 防止 API key 落盘明文。

注意:key 来源是 macOS Keychain(跨平台用 OS 安全 API)。


6. 推测的整体数据流

[Session 进行中]
[每秒 onChange 触发]
[saveSession 异步写入]
  ├→ 1. 序列化 AppState → JSON
  ├→ 2. 过滤掉 ephemeral(progress, ephemeral tool results)
  ├→ 3. 加密敏感字段(API key, OAuth)
  ├→ 4. gzip 压缩
  ├→ 5. 写临时文件(~/.claude/sessions/<id>.jsonl.tmp)
  └→ 6. 原子 rename(确保完整性)
[磁盘]

[用户 /resume <id>]
[loadSession]
  ├→ 1. 检查文件存在
  ├→ 2. 读 + 解压 + 解密
  ├→ 3. JSON.parse
  ├→ 4. 反序列化为 Message[]
  ├→ 5. 恢复 AppState
  └→ 6. 触发 useAppState 订阅
[REPL 渲染历史消息]

7. 关键设计

7.1 "原子写入"防崩溃

// 推测
const tmp = `${path}.tmp`
await fs.writeFile(tmp, data)
await fs.rename(tmp, path)  // 原子操作

关键rename原子操作(同一文件系统内)。
含义:写一半崩溃 → 旧文件还在 → 数据不丢。

7.2 "Ephemeral 过滤"节省磁盘

// 推测
const persistableMessages = messages.filter(m => !isEphemeral(m))

剔除 progress / 临时状态 —— 节省 50%+ 磁盘

7.3 "gzip 压缩"省钱

// 推测
const compressed = gzipSync(JSON.stringify(state))

实测: - 100KB JSON → ~25KB gzip(4x 压缩) - 长会话省 70%+ 磁盘

7.4 "敏感字段加密"安全

// 推测
const key = await getKeychainKey()  // macOS Keychain / Windows DPAPI
const encrypted = encrypt(JSON.stringify(state), key)

意义: - API key 不会明文落盘 - 即便磁盘被偷,攻击者拿不到 key

7.5 "JSONL 格式"流式

推测使用 .jsonl(每行一条 JSON)而不是 .json(一个数组)。
好处: - 追加消息不重写整个文件 - 大会话不爆内存(流式读) - tail -f 可看实时输出

7.6 "跨设备同步"(推测)

RemoteAgentMetadata 暗示有远程 session 概念
推测: - Claude.ai 网页启动的对话 → 同步到本地 CLI - 本地 CLI 启动的对话 → 同步到云端 - 多设备共享会话


8. 实战:写一个简化版 sessionStorage

// 简化版(~50 行)
import { promises as fs } from 'fs'
import { join } from 'path'
import { gzipSync, gunzipSync } from 'zlib'

const SESSIONS_DIR = join(process.env.HOME!, '.claude', 'sessions')

export async function saveSession(id: string, messages: Message[]): Promise<void> {
  const data = messages.map(m => JSON.stringify(m)).join('\n')
  const compressed = gzipSync(Buffer.from(data))
  const path = join(SESSIONS_DIR, `${id}.jsonl.gz`)
  const tmp = `${path}.tmp`
  await fs.writeFile(tmp, compressed)
  await fs.rename(tmp, path)
}

export async function loadSession(id: string): Promise<Message[]> {
  const path = join(SESSIONS_DIR, `${id}.jsonl.gz`)
  const compressed = await fs.readFile(path)
  const data = gunzipSync(compressed).toString()
  return data.split('\n').map(line => JSON.parse(line))
}

对比 Claude Code: - 简化版没有加密 - 简化版没有原子写(会丢数据) - 简化版没有 ephemeral 过滤 - Claude Code 多了 100+ 边界处理


9. 关键洞察

9.1 "持久化是用户体验"的核心

没有 sessionStorage: - 用户每次开 Claude Code = 从零开始 - 不能 /resume - 不能跨设备

有了 sessionStorage: - 关掉 Claude Code = 下次能恢复 - 多个 session 切换 - 跨设备同步

这是 Claude Code "能用"的关键

9.2 "5105 行 = 完整数据库"

sessionStorage.ts 当成一个轻量级数据库: - 路径解析(数据库连接) - 序列化 / 反序列化(数据编解码) - 压缩 / 加密(性能 / 安全) - 原子写(事务) - 版本迁移(schema 演进)

实际上就是一个: - 不支持 SQL 的 - 不支持索引的 - 优化过的 - 文件级数据库

9.3 "Onchange 触发"的数据流

AppState 变化
onChange 钩子触发(state/store.ts)
saveSession 异步写入(onChangeAppState.ts → sessionStorage.ts)
磁盘

没有 onChange → 没 saveSession → 没持久化
store.ts 的 onChange 钩子 = 整个持久化系统的"开关"

9.4 "JSONL vs JSON"选择

格式 优点 缺点
JSON 数组 一次读 改一行重写整个
JSONL 追加 + 流式 一次读多行

Claude Code 选 JSONL —— 适合会话"不断追加"的特性。

9.5 "加密 keychain"的安全模型

平台 来源
macOS Keychain
Windows DPAPI
Linux Secret Service API

跨平台统一 API = getKeychainKey()(推测)。


10. 关键文件清单

src/utils/sessionStorage.ts (5105 行)
├── A. 类型守卫 (行 101-196)
│   ├── Transcript, isTranscriptMessage
│   ├── isChainParticipant
│   ├── isEphemeralToolProgress
│   └── ...
├── B. 路径解析 (行 198-261)
│   ├── getProjectsDir
│   ├── getTranscriptPath / getTranscriptPathForSession
│   └── getAgentTranscriptPath
├── C. Agent 元数据 (行 260-399)
│   ├── AgentMetadata / RemoteAgentMetadata
│   ├── writeAgentMetadata / readAgentMetadata
│   └── listRemoteAgentMetadata
├── D. Session 核心 (行 400-?)
│   ├── sessionIdExists
│   └── ...
├── E. saveSession / loadSession (推测 1000+ 行)
├── F. listSessions (推测 500+ 行)
├── G. 加密 / 解密 (推测 500+ 行)
├── H. 压缩 / 解压 (推测 200+ 行)
└── I. 迁移 / 兼容 (推测 500+ 行)

11. 阅读清单

  1. ✅ 完整通读 src/utils/sessionStorage.ts(5105 行)
  2. ✅ 读 phase-03-state.md 配合
  3. ✅ 读 topics/deep-dive-app-state-store.md 配合
  4. 📌 读 src/state/onChangeAppState.ts 副作用编排
  5. 📌 读 src/memdir/ 推测的 memory 持久化
  6. 📌 读 src/state/teammateViewHelpers.ts 多 agent 持久化

12. 练习任务

  1. write* / read* / list* / delete* 函数各几个 —— 推测 20+/20+/10+/5+
  2. 列出 MAX_* 常量 —— MAX_TRANSCRIPT_READ_BYTES = 50MB 是怎么定的?
  3. 设计你自己的 sessionStorage —— 写 100 行的简化版
  4. 思考:JSONL 格式 + gzip 压缩 + 加密 = 完美的"文件级数据库"吗?还有哪些 trade-off?