一段 Claude Code 会话做到一半,关掉终端,之后再运行 claude --continue,通常还能接着做。终端里的旧消息会重新出现,能恢复的 agent 配置、Git worktree 等工作状态也会一并带回来。
这看起来像是"把日志重新读一遍",但真正的恢复分成两步:Claude Code 先从本地记录中重建一段可以继续操作的会话;等你输入新消息后,它才整理下一次发给模型的上下文。claude --resume <session-id> 走的是同一条主线,只是选择了指定会话。
下面沿着这条路径,看看会话是怎样被找到、重建并重新投入使用的:

先从 Claude Code 如何找到会话说起。
选择要恢复的会话
Claude Code 提供三个常用入口:
| 入口 | 用途 |
|---|---|
claude --continue |
继续当前项目最近的一次会话 |
claude --resume <id 或名称> |
返回指定会话;不带参数时打开选择器 |
/resume |
在当前 Claude Code 中切换会话 |
会话按项目保存,所以"最近"指当前项目里的最近会话。选择器也可以扩展到同一仓库的其他 worktree,或者机器上的其他项目。
--continue 和 --resume 在启动时恢复会话;/resume 在当前进程中切换会话。它们的入口不同,后面读取日志和恢复消息的步骤基本一致。
从 JSONL 中重建对话
JSONL(JSON Lines)是一种按行保存 JSON 数据的文本格式。每一行都是一条独立的 JSON 记录,因此程序可以在会话运行过程中不断向文件末尾追加新内容。
Claude Code 通常把每段会话保存为一份 JSONL 文件,默认位于 ~/.claude/projects/<项目目录>/<session-id>.jsonl。文件名中的 session-id 用来标识这段会话。
会话中的消息、工具调用和状态变化都会陆续写入这份文件。每行最外层的 type 是 JSONL 记录类型 ,用于告诉 Claude Code 该怎样读取和恢复这条记录。它不是对 AI 消息内容的分类,也不能简单理解成"user 是人说的话,assistant 是 AI 的文字回答"。
对于 user 和 assistant 记录,内部还会有 message.role。两者通常与顶层 type 同名,但表达的是模型交互的方向:user 表示送入模型的一侧,assistant 表示模型返回的一侧。消息里具体装了什么,还要继续看 message.content。

因此,看到顶层 type: "user" 时,还不能判断它是不是用户亲自输入。内部是 text,可能是用户消息;内部是 tool_result,就是工具返回给模型的结果。
读取 JSONL 后,Claude Code 会把可恢复的对话记录整理进内存中的 messageList。一次工具调用通常会形成四条记录:用户提出问题,模型发起 tool_use,工具以 tool_result 返回结果,模型再继续回答。界面上看起来像模型连续完成了"调用工具"和"给出结论",但 JSONL 中的两条 assistant 记录之间还有一条保存工具结果的 user 记录。

这类记录主要依靠下面两个字段连接:
| 字段 | 用途 |
|---|---|
uuid |
标识当前消息 |
parentUuid |
指向逻辑上的上一条消息,不一定是 JSONL 中紧邻的上一行 |
除了 user 和 assistant,JSONL 还会保存会话控制与辅助记录。它们不会作为普通的用户消息或模型回答显示,主要用于恢复控制状态、会话信息、运行方式和工作环境。
type |
记录的内容 | 恢复时的作用 |
|---|---|---|
system |
Claude Code 写入的运行时控制记录 | 根据 subtype 恢复对应的会话控制状态 |
summary |
用于会话列表展示的简短概述 | 帮助识别会话,不作为普通对话内容恢复 |
custom-title |
用户设置的会话标题 | 恢复会话列表中的标题 |
tag |
用户给会话添加的标签 | 用于筛选和查找会话 |
agent-name |
agent 的显示名称 | 恢复会话列表和终端中的名称 |
agent-setting |
当前会话使用的 agent 类型或名称 | 查找并重新应用对应的 agent 配置 |
mode |
会话的运行模式 | 恢复相应的运行方式 |
worktree-state |
worktree 的名称、路径和分支等信息 | worktree 仍存在时,重新进入原来的工作目录 |
file-history-snapshot |
某个消息节点对应的文件备份索引 | 重建文件历史,供 rewind 恢复文件内容 |
pr-link |
会话关联的 PR 信息 | 恢复会话与 PR 的关联关系 |
一条 worktree 状态记录如下:

这些记录通常通过 sessionId 归属到会话,再按 type 交给对应的恢复逻辑处理。system 记录可能参与 parentUuid 消息链;标题、标签和 worktree 等元数据通常不参与消息链。
正常对话通常只有一条路线。分叉通常出现在用户使用 /rewind 回到较早的消息,然后重新输入内容时。
这份会话日志采用追加写入:新消息产生后直接写到文件末尾,不需要每次都重写整个会话。这样写入更简单,即使中途退出,通常也不会影响前面已经保存的记录;消息的 uuid 也可以保持不变。
因此,rewind 只是改变"接下来从哪里继续",并不会删除已经发生的对话。新消息会追加到文件末尾,再通过 parentUuid 指向较早的消息,新旧两条路线也就同时留在了 JSONL 中。
下面是一个按真实字段精简的例子。为了方便阅读,省略了会话 ID、时间戳等无关字段:

前四行是一条完整的对话:用户先要求修复登录问题,随后决定改用 OAuth。接着,用户通过 /rewind 回到 a1 之后,把要求改成"只修密码登录"。因此,第五行虽然写在文件末尾,它的 parentUuid 仍然是 a1。
这 6 行记录实际形成了两条路线:

假设本次要从 a3 继续,Claude Code 会沿着 parentUuid 向前查找:
text
a3 → u3 → a1 → u1
再把顺序反过来,重建结果就是下面这 4 条按正常阅读顺序排列的消息:
text
修复登录失败
→ 我先检查鉴权流程
→ 先不改方案,只修密码登录
→ 问题出在密码校验逻辑
从 a3 回溯并反转后,Claude Code 得到这次需要恢复的对话历史。它会先用这组消息恢复终端中的会话;等用户继续输入,再以此为基础准备下一次模型请求。
这个重建过程不会生成新的 JSONL,也不会重新生成摘要。磁盘上仍然保留 6 条记录,只是 u2 和 a2 属于 OAuth 路线,不在当前消息链中。按这条路线恢复时,它们会被跳过,但仍会留在文件中占用空间。
如果对话之前已经压缩
压缩通常发生在会话运行期间,而不是恢复时。较早的对话被压缩后,Claude Code 会把压缩边界和已经生成的摘要也写入 JSONL。
假设压缩前的对话是:
text
u1 → a1 → u2 → a2
压缩完成后,文件末尾会增加一条压缩边界 B 和一条摘要消息 S。这里的 S 不是前文用于会话列表展示的顶层 type: summary 记录,而是参与当前消息链的压缩结果。下面是按真实字段精简后的例子:

压缩边界中的几个字段分别表示:
| 字段 | 含义 |
|---|---|
subtype: compact_boundary |
这不是普通系统消息,而是一次压缩的分界点 |
parentUuid: null |
新的对话链从这里开始,不再沿父引用读取更早的原始消息 |
logicalParentUuid: a2 |
记录压缩前最后处理到 a2,但重建消息链时不会沿它向前查找 |
compactMetadata |
保存压缩是自动还是手动触发,以及压缩前的大致 token 数 |
isCompactSummary: true |
表示 S 是对早期对话的摘要,不是用户新输入的问题 |
摘要消息 S 的 parentUuid 指向压缩边界 B。后续对话再从 S 继续:
text
原始消息:u1 → a1 → u2 → a2
当前路线:B → S → u3 → a3
旧消息仍可能留在 JSONL 中,但已经不在当前路线里。
恢复时,Claude Code 从 a3 沿 parentUuid 向前查找,只会得到:
text
B → S → u3 → a3
压缩边界是内部标记,不会作为普通聊天内容发给模型。因此,后续真正用于准备模型请求的是:
text
早期对话摘要 S
→ 压缩后的新消息 u3、a3
→ 用户接下来输入的新消息
这个过程不会重新调用模型生成摘要,而是直接复用 JSONL 中已经保存的摘要。
恢复消息和工作状态
重建出对话历史后,还要处理上次没有正常结束的回合。例如:
- 用户的问题已经写入日志,但回答还没开始:保留用户的问题,并补上不显示给用户的内部占位消息,让对话仍然保持一问一答的结构。
- Claude Code 发出了工具调用,但工具还没有返回结果:移除这次未完成的工具调用,避免恢复出一段无法继续的消息。
- 工具已经返回结果,但最终回答还没生成:加入一条不显示给用户的"继续处理"消息,标记这个回合是在中途停止的。
这些处理不会自动重跑工具,而是把消息整理成可以继续对话的结构。
除了消息,Claude Code 还会恢复"工作状态"。这里的工作状态,是指没有直接写在聊天正文里,但会影响接下来在哪个环境、以什么方式继续工作的记录。它不是整个进程的快照,也不会恢复一个已经停止的命令。
这些工作状态的相关记录也保存在当前会话的 JSONL 中。下面的例子只保留了与说明有关的字段:

恢复时,Claude Code 会把消息和状态记录分开处理:消息按照 parentUuid 重建,状态则按各自的类型读取,再写回当前进程。
| 工作状态 | 大概作用 | 之前存在哪里 | 恢复时怎么做 |
|---|---|---|---|
| agent 设置 | 决定使用哪套提示词、工具范围和模型配置 | JSONL 中的 agent-setting,只保存 agent 的名称或类型 |
在当前 settings 中找到对应 agent,并应用它现在的配置 |
| worktree | 让后续修改继续发生在原来的隔离目录和分支中,避免改错代码目录 | JSONL 中的 worktree-state,保存名称、路径和分支等信息 |
检查目录是否存在;存在就切换过去,不存在就留在当前目录 |
| 文件读取记录 | 记录哪些文件已经读过,供 Edit 等工具进行编辑前校验 | 不单独保存,而是包含在 Read、Write、Edit 等工具消息中 | 扫描恢复后的消息,重新建立文件读取缓存 |
| 文件历史 | 保存文件在不同消息节点上的备份,执行 rewind 时可以把代码还原到当时的状态 | JSONL 保存 file-history-snapshot,实际备份保存在配置目录的 file-history 中 |
按当前对话链重建快照,并重新关联对应的备份文件 |
| 会话信息 | 帮助用户识别和查找会话,本身不决定模型如何执行 | JSONL 中的标题、标签和 agent 名称等记录 | 读取最后一次保存的值,恢复到会话列表和终端 |
从实现上看,读取阶段会先得到一个临时的恢复结果,其中包含消息、agent 设置、worktree、文件历史等数据。随后,这些数据会被分别写入消息列表、运行状态、缓存和当前工作目录:

这些数据写回后,Claude Code 得到的不是一份新的状态文件,而是一个已经恢复好运行环境的进程。模型上下文仍要等用户继续输入后再准备。
例如,上次会话使用代码审查 agent,在 fix-login worktree 中读取并修改了 src/auth.ts,随后测试工具执行到一半时终端被关闭。恢复后,Claude Code 会清理未完成的工具调用,重新进入仍然存在的 worktree,并恢复 agent、文件读取记录和文件历史,然后等待用户继续输入。
恢复是有条件的:Claude Code 会重新读取当前 settings,命令行参数可以覆盖会话中保存的设置;如果原 agent 已被删除,或者 worktree 目录已经不存在,就会退回当前可用的配置和目录。
终端里的旧消息,模型都会看到吗
不一定。会话恢复后,终端显示的是当前路线上的对话。下一次真正发给模型的内容,要等你继续输入时才开始整理。
Claude Code 会从最近一次压缩的位置接着读取历史。内容仍然太多时,较早的部分会继续被压缩,剩下的消息再转换成模型请求需要的格式。
这就会出现一种看似矛盾的情况:终端里还能翻到很早的对话,Claude 却没有接住其中某个细节。记录可能没有丢,只是那段内容没有完整进入这次请求,或者已经变成了摘要。当然,也可能是内容进了上下文,但模型没有用到它。
如果恢复后觉得前文被忘了,可以先看终端里的历史。这里已经少了一段,就回头检查会话选择、日志读取和消息链;终端记录还在,再看后续的上下文裁剪和压缩。排查时把这两层分开,就不容易绕错方向。
结语
Claude Code 的会话恢复,更像是把上次的工作重新接起来,而不是让程序回到关闭前的那一刻。JSONL 留下对话和状态记录,parentUuid 找回当前路线,仍然有效的工作状态再回到当前进程。
这套实现也提供了一个很实用的工程思路:日志不一定只是写给人看的几行字符串。JSONL 仍然是文本,但每一行都有固定结构,程序可以一边运行一边追加,也能在之后重新解析。
同一份日志还能保存多种类型的记录。对话消息、工具结果和工作状态各有自己的 type,读取时再交给不同逻辑处理。日志因此不只用于排查问题,也可以成为恢复状态的数据来源。
Claude Code 就是靠这些结构化记录把会话重新接起来。用户继续输入后,新的模型请求才会开始准备;日志保存的内容、终端恢复出的会话和模型最后收到的上下文相互关联,却不一定完全相同。
如果你觉得这篇文章有帮助,欢迎点赞、收藏,也可以关注我。