Home

Claude Code 会话存储与 rewind 原理

Claude Code 会话存储与 rewind 原理

实测环境:Claude Code v2.1.263 / Windows 11 / 会话 3c056032 所有数据均为本机实际观测,非文档转述。


一、磁盘布局

~/.claude/
├── projects/<项目路径编码>/          # 会话记录(transcript)
│   ├── <session-id>.jsonl            # 一个会话一个文件
│   └── <session-id>/tool-results/    # 超大工具输出溢写到这里
├── file-history/<session-id>/        # checkpoint 的文件快照实体
│   └── <路径哈希>@v<N>
├── sessions/  shell-snapshots/  tasks/  telemetry/  ...

项目路径编码规则:C:\Users\21145C--Users-21145


二、transcript 文件的本质

2.1 格式

.jsonl = JSON Lines,一行一个 JSON 对象。只追加,从不删改

2.2 它不是线性日志,是一棵树

关键字段:

字段含义
uuid本节点唯一 id
parentUuid父节点 id,根节点为 None
leafUuid出现在 last-prompt 记录里,标记当前活跃叶子
sessionId所属会话
isSidechain是否是子 agent 的旁链
timestamp / version / cwd / gitBranch环境快照

「当前对话」的定义:从 leafUuid 沿 parentUuid 一路回溯到根的那条路径。 只有这条路径上的节点会被拼进 prompt 发给模型。文件里其余节点一律不发送。

2.3 记录类型(实测分布)

type条数说明
assistant174模型回复(含 tool_use)
attachment101系统注入的上下文块
user93用户消息 + 工具结果
system33系统事件/警告
last-prompt31checkpoint 标记,携带 leafUuid
mode26权限模式切换
ai-title26自动生成的会话标题
file-history-snapshot21文件快照索引
atis-latch7内部状态
file-history-delta2快照增量
queue-operation / cost-state2 / 2队列、成本累计

attachment 的子类型(这些就是”上下文占用”里看不见的部分):

total_tokens_reminder(78)  task_reminder(8)   deferred_tools_delta(2)
mcp_instructions_delta(2)  skill_listing(2)   environment(2)

三、rewind 的原理:改指针,不删数据

3.1 机制

  1. /rewind(或输入框为空时连按两次 Esc)列出本会话所有 prompt —— 数据来源就是那些 last-prompt 记录
  2. 你选中某条,它对应一个 uuid
  3. 之后新发的消息,parentUuid 写成那个 uuid
  4. 原来的后续链就变成兄弟分支,物理上仍在文件里,只是脱离了活跃路径
  5. 下次请求只把活跃路径喂给模型

3.2 实测证据(本会话,未做过显式 rewind)

文件里 uuid 节点总数 : 411
活跃路径上的节点数   : 375
不在活跃路径上(旁支) : 36
分叉点数量           : 20

节点 c8558227 有 4 个子节点:

2dd28fd1  user    [旁支/已弃]   "看了这些发现我基础并不牢固…"
1ea50ff2  user    [旁支/已弃]   "看了这些发现我基础并不牢固…"
91f50bf7  user    [旁支/已弃]   "看了这些发现我基础并不牢固…"
5a2e41ec  system  [活跃路径]

同一句话三个兄弟分支全部留存 —— 重发/回滚过的内容一条没丢。

3.3 git 类比

Claude Codegit
/rewind → Restore conversationgit checkout <老commit> 后长出新分支,旧 commit 仍在 object 库
/forkgit branch —— 但另开一个 jsonl 文件
活跃路径从 HEAD 回溯的那条链
旁支节点dangling / 未被引用的 commit

结论:rewind 之后,被”丢掉”的上下文仍然完整躺在 transcript 文件里,只是不再发送给模型。


四、checkpoint(代码回滚)是正交的另一套

4.1 存储方式

transcript 里的 file-history-snapshot 只是索引,文件实体在 ~/.claude/file-history/<session-id>/

实测快照记录结构:

{
  "type": "file-history-snapshot",
  "messageId": "...",
  "snapshot": {
    "messageId": "...",
    "timestamp": "...",
    "trackedFileBackups": {
      ".claude\settings.json": {
        "backupFileName": "6a385dba1459677a@v7",
        "version": 7,
        "backupTime": "2026-09-08T07:09:06.103Z",
        "realParentDir": "C:\Users\21145\.claude"
      }
    }
  }
}

备份文件命名:<16位路径哈希>@v<递增版本号>。同一文件每次被改就 +1 个版本。

4.2 /rewind 三个 restore 选项分别动什么

选项改 parentUuid 指针覆写磁盘文件
Restore conversation
Restore code
Restore code and conversation

⚠️ 只想清上下文就选 Restore conversation。带 code 的两个会按快照把被追踪的文件写回旧版本。 本会话被追踪的是 ~/.claude/settings.json(v7)和 Windows Terminal 的 settings.json(v2)—— 误选会把 statusline、fullscreen、字体、配色全部还原。

4.3 两个 summarize 选项

  • Summarize from here —— 把选中点往后压缩成摘要
  • Summarize up to here —— 把选中点之前压缩,保留后面

摘要不改磁盘文件,原始消息也仍留在 transcript 里,只是不再逐条发送。等价于定点 /compact


五、checkpoint 的四个盲区

  1. bash 改的文件不追踪 —— rm / mv / cp / 重定向写文件都不进快照,只有 Edit/Write 类工具的改动才追踪
  2. 子 agent 的改动不还原 —— 后台 subagent、/code-review --fix 的编辑要靠 git 回退(前台 forked skill 除外)
  3. 软链接/硬链接跳过 —— 会提示 Restored the code, but skipped N files
  4. 会话外的改动不追踪 —— 你手动改的、别的会话改的,都不在快照里

六、保留期限

transcript 和快照都受 cleanupPeriodDays 管,默认约 30 天

  • 快照被清理后再 rewind 会报 No files were restored
  • 每个文件的第一份快照会保留(VS Code 扩展拿它当 session diff 基线)
  • 每个会话最多保留 100 个 checkpoint 的快照

想长期留:调大 cleanupPeriodDays,或者把 jsonl 拷走。


七、实用脚本

7.1 列出所有会话及主题

cd ~/.claude/projects/<项目编>
ls -1t *.jsonl | while read f; do
  echo "=== ${f%.jsonl}  $(du -h "$f"|cut -f1)  $(wc -l <"$f")行"
  grep -aco '关键词' "$f"
done

7.2 提取活跃路径 / 找出旁支

import json, collections

p = r'C:\Users\<你>\.claude\projects\<项目编码>\<session-id>.jsonl'
rows = [json.loads(l) for l in open(p, encoding='utf-8') if l.strip()]
byuuid = {r['uuid']: r for r in rows if r.get('uuid')}

# 当前活跃叶子 = 最后一条 last-prompt 的 leafUuid
leaf = [r['leafUuid'] for r in rows if r.get('leafUuid')][-1]

# 回溯出活跃路径
path, cur = [], leaf
while cur and cur in byuuid:
    path.append(cur)
    cur = byuuid[cur].get('parentUuid')
path = set(path)

print('总节点', len(byuuid), '活跃', len(path), '旁支', len(byuuid) - len(path))

# 打印旁支上的用户消息(被 rewind 丢掉的内容)
for u, r in byuuid.items():
    if u in path or r.get('type') != 'user':
        continue
    c = r.get('message', {}).get('content')
    t = c if isinstance(c, str) else ' '.join(
        x.get('text', '') for x in (c or []) if isinstance(x, dict))
    if t.strip():
        print('[旁支]', t.strip()[:100])

7.3 找分叉点

kids = collections.defaultdict(list)
for r in rows:
    if r.get('uuid'):
        kids[r.get('parentUuid')].append(r['uuid'])
for k, v in kids.items():
    if k and len(v) > 1:
        print('分叉点', k[:8], '->', len(v), '个子节点')

八、几个命令的区别速查

命令上下文磁盘文件会话文件
/rewind → Restore conversation回退到选中点不动同一个,加旁支
/rewind → Restore code不动按快照覆写同一个
/rewind → Summarize压缩成摘要不动同一个
/compact全量压缩不动同一个
/clear全部丢弃不动同一个(旧的可从 rewind 菜单顶部恢复)
/fork继承当前不动新建 jsonl,标题带 ⑂
/resume载入选中会话不动切到那个文件

实测:fork 出的 jsonl 不含指向父会话 id 的字段,它有自己的根节点和 sessionId, 仅在 aiTitle 上带 标记(如 MySQL索引和优化基础学习 ⑂)。


九、一句话总结

transcript 是只追加的树,rewind 只是把新消息挂到更早的父节点上 —— 数据从不删除,只是脱离活跃路径。 代码回滚是完全独立的快照机制,存在 file-history/ 下,只有带 code 的选项才会碰它。