Deep Dive | utils/sessionStorage.ts 5105 行会话存储拆解¶
重要性:⭐⭐⭐⭐(会话数据持久化 —— Claude Code "能恢复" 的关键) 真实位置:
src/utils/sessionStorage.ts(5105 行,项目第四大文件) 核心角色: - 写到~/.claude/sessions/- 加密(敏感数据) - 压缩(节省磁盘) - 索引(快速查找) - 恢复(crash 后) - 跨设备同步(推测)关联:phase-03-state.md、topics/big-files-untold-stories.md、walkthrough/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)¶
跳过某些首条 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)¶
所有项目 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 "原子写入"防崩溃¶
关键:rename 是原子操作(同一文件系统内)。
含义:写一半崩溃 → 旧文件还在 → 数据不丢。
7.2 "Ephemeral 过滤"节省磁盘¶
剔除 progress / 临时状态 —— 节省 50%+ 磁盘。
7.3 "gzip 压缩"省钱¶
实测: - 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. 阅读清单¶
- ✅ 完整通读
src/utils/sessionStorage.ts(5105 行) - ✅ 读 phase-03-state.md 配合
- ✅ 读 topics/deep-dive-app-state-store.md 配合
- 📌 读
src/state/onChangeAppState.ts副作用编排 - 📌 读
src/memdir/推测的 memory 持久化 - 📌 读
src/state/teammateViewHelpers.ts多 agent 持久化
12. 练习任务¶
- 数
write*/read*/list*/delete*函数各几个 —— 推测 20+/20+/10+/5+ - 列出
MAX_*常量 —— MAX_TRANSCRIPT_READ_BYTES = 50MB 是怎么定的? - 设计你自己的 sessionStorage —— 写 100 行的简化版
- 思考:JSONL 格式 + gzip 压缩 + 加密 = 完美的"文件级数据库"吗?还有哪些 trade-off?