Claude Agent SDK 的会话怎么存、怎么续:session 与 session storage 分别管什么

2026-08-18

写 Agent 应用的人迟早会撞上同一个问题:第一次 query() 让 agent 读完了一堆文件、做完了分析,第二次想让它「接着刚才那个结论改代码」,结果它什么都不记得,又从头读了一遍。

Claude Agent SDK 对这件事的答案分成两层,很多人把它们混成一件事,然后在跨机器恢复的时候翻车:

  • session:SDK 在 agent 干活时累积的对话记录。官方文档《Work with sessions》写明它包含你的 prompt、agent 发起的每一次工具调用、每一个工具结果和每一次回复,SDK 会自动写到磁盘上。
  • session storage:一个叫 SessionStore 的适配器接口,让你把上面那份记录镜像到自己的后端(文档举的例子是 S3、Redis 或数据库),好让另一台主机能接着跑。

这两层的关系是镜像,不是替换——这一点后面会咬到你,先记住。

还有一句在文档里被放进 Note 里、但值得单独拎出来的话:session 持久化的是「对话」,不是「文件系统」。想给 agent 改过的文件做快照和回滚,文档指向的是另一套东西 file checkpointing,不是 session。

一、先确认前置条件

在动手之前有几件事得先对上,不然后面每一步都白做。

语言侧的能力不对等。 文档里明确写了 TypeScript 有、Python 没有的选项:persistSession: falseTypeScript only,Python SDK 没有等价选项,想抑制 transcript 写盘要在 env 选项里设 CLAUDE_CODE_SKIP_PROMPT_HISTORY。另一处是 startup(),文档写明它 has no Python equivalent。

版本会影响行为,而且文档写出了分界线。 会话查找的范围在 v2.1.223 前后不一样:在那之前,查找被限定在当前项目目录及其 git worktree 内;文档还补了一句,打包了旧 CLI 的 SDK 版本仍然是旧行为——也就是说光看 SDK 版本号不够。另外有两条和跨机恢复直接相关:Agent SDK v0.3.232 之前,TypeScript SDK 不会剥掉 additionalMarketplaces 这个别名键;v0.3.222 之前,它只拷贝 credentials 和 .claude.json

一个已经没了的接口。 那个实验性(experimental)的 V2 session API,提供 createSession()send / stream 模式的那个,文档写明已在 TypeScript Agent SDK 0.3.142 中移除。网上如果还能搜到基于它的示例,那是过期内容,官方要求改用 query() 加本文讲的这些 session 选项。

权限与存储位置。 你得能读写会话文件所在的目录。文档给的路径是 ~/.claude/projects/<encoded-cwd>/*.jsonl;如果设了 CLAUDE_CONFIG_DIR 环境变量,就改看 $CLAUDE_CONFIG_DIR/projects/。Windows 侧要注意:官方文档写的是 ~/.claude/projects/ 这种波浪号写法,并没有单独说明 Windows 下的展开规则,所以稳妥做法是显式设 CLAUDE_CONFIG_DIR 指到一个你确定有写权限的目录,Linux/macOS 用 export、Windows 用 setx 或系统环境变量面板——这属于操作系统的通用做法,不是该产品文档的内容。

二、会话 ID 从哪儿取

resumefork 都要会话 ID,所以第一步是把它抓出来。文档写明:从 result message 上的 session_id 字段读,Python 是 ResultMessage、TypeScript 是 SDKResultMessage,而且不管成功还是出错,每一个 result 上都有这个字段

两边还有一处不对称:TypeScript 里这个 ID 更早就能拿到,它同时是 init SystemMessage 上的一个直接字段;Python 里它嵌在 SystemMessage.data 里面。

if (message.type === "result") {
  sessionId = message.session_id;
  if (message.subtype === "success") {
    console.log(message.result);
  }
}

文档在示例注释里还交代了一个容易踩的边角:单次 query() 在 yield 出 error result 之后会抛异常(Python 是 raise),所以要在 try 外面接住;如果失败发生在连接或进程层面、根本没产出 result message,那 sessionId 会一直是 undefined

三、continue、resume、fork 分别在解决什么

这三个都是 query() 上的选项字段(Python 在 ClaudeAgentOptions、TypeScript 在 Options),文档把它们的区别讲得很干脆:

做法怎么找到那个会话
continue当前目录下最近的那个会话,你什么都不用记
resume你传一个具体的会话 ID 进去,ID 由你自己保管
fork新建一个会话,起点是原会话历史的一份拷贝,原会话不变

对应到写法:Python 用 ClaudeSDKClient,每次 client.query() 自动续在同一个会话上,ID 由它内部管;TypeScript 没有这样一个持有会话的 client 对象,改成在后续每次 query() 上传 continue: true。跨进程重启的场景也是这两个选项:Python 的 continue_conversation=True 和 TypeScript 的 continue: true

多会话场景(文档举的例子是多用户应用里一人一个会话)就只能用 resume,因为「最近一个」这个定位方式在这里没有意义。

fork 是另一回事,它分叉的是对话历史,不是文件系统。文档专门用 Note 提醒:被 fork 出来的 agent 如果改了文件,那些改动是真实的,同目录下任何会话都看得到。

options=ClaudeAgentOptions(
    resume=session_id,
    fork_session=True,
    max_turns=5,
),

TypeScript 侧对应的是 resumeforkSession: true。以上为按官方文档中的参数语义组合的示例,未经实测,以官方文档与 --help 的实际输出为准。

四、transcript 存在哪、跨机器怎么办

本机路径前面提过了。真正需要抄下来的是目录名的编码规则:把绝对工作目录里的每一个非字母数字字符替换成 -,文档给的例子是 /Users/me/proj 变成 -Users-me-proj。转换后名字超过 200 个字符时,Claude Code 会截断并追加一个 hash,所以列 projects/ 的时候要按前 200 个字符去匹配。

Windows 侧文档没有给示例。按这条规则,盘符后的冒号和反斜杠同样属于非字母数字字符,但官方文档没有给出 Windows 路径的转换示例,别照着猜出一个目录名当结论用。

跨主机恢复,文档给了三条路,并且明说了各自的代价:

  1. 挂一个 session store,让 SDK 把 transcript 镜像到你自己的后端。注意查找键是从工作目录派生的,所以恢复时的 cwd 要和原来那次跑的对得上。
  2. 搬文件:把 ~/.claude/projects/<encoded-cwd>/<session-id>.jsonl 存下来,在新主机的 ~/.claude/projects/ 下任意目录里还原,再调 resume
  3. 干脆别依赖会话恢复:把你真正需要的结果(分析产出、决策、文件 diff)作为应用状态自己存好,喂给一个全新会话的 prompt。文档自述这种做法「往往比到处搬 transcript 文件更稳」。

另外,两个 SDK 都提供了枚举磁盘上会话并读取消息的函数(TypeScript 的 listSessions() / getSessionMessages(),Python 的 list_sessions() / get_session_messages()),以及查询和改单个会话的 getSessionInfo() / renameSession() / tagSession() 一组,用来做会话选择器、清理逻辑或 transcript 查看器。

五、SessionStore 接口:两个必需、四个可选

SessionStore 是一个对象,两个必需方法 appendload,另有四个可选方法listSessionslistSessionSummariesdeletelistSubkeys)。

type SessionKey = {
  projectKey: string;
  sessionId: string;
  subpath?: string;
};

SessionKey 定位一份 transcript:projectKey 是工作目录的一种稳定、文件系统安全的编码;sessionId 是会话 UUID;subpath 只在这条记录属于 subagent transcript 或 sidecar 文件时才有值,文档要求把它当成不透明的键后缀,形如 subagents/agent-<id>subpath 为 undefined 时指的就是主 transcript。

可选方法不实现会有具体后果,这几条值得记:listSessions 没实现时,continue: true 会抛错;delete 没实现时删除是 no-op(文档说这适合 append-only 的后端);listSubkeys 没实现时,恢复只会还原主 transcript,subagent 的那些不会回来。

开发和测试阶段 SDK 自带 InMemorySessionStore

import { query, InMemorySessionStore } from "@anthropic-ai/claude-agent-sdk";

const store = new InMemorySessionStore();

S3、Redis、Postgres 的参考适配器在 TypeScript SDK 仓库的 examples/session-stores/ 下,文档明说它们没有发布到 npm,要自己把 src/ 里的文件拷进项目并安装对应的后端客户端。

六、边界:这几条不写出来就会踩

镜像是双写,不是替换。 Claude Code 子进程总是先写本地磁盘,SDK 再把同一批转发给你的 append()。哪一份能活过这次运行,取决于运行是怎么起来的:全新会话、或者 store 里没有这个会话的 resume,本地 transcript 留下、store 拿到一份拷贝;而从 store 恢复的运行,本地副本会在运行结束时被删掉,store 里那份成了唯一的持久副本

两个选项和 store 互斥,SDK 会在启动时直接抛错:TypeScript 的 persistSession: false(它关掉的正是镜像赖以存在的本地写入),以及 file checkpointing(TypeScript 的 enableFileCheckpointing / Python 的 enable_file_checkpointing,它的文件备份直接写本地盘,不会被镜像到 store)。

镜像写是 best-effort。 append() 被拒绝时,SDK 会用短退避再重试,总共最多三次尝试;超时的调用不重试(因为原调用可能仍会落地)。还是失败就记日志、往迭代器里发一条 { type: "system", subtype: "mirror_error" }、丢掉这批、继续查询。重试可能重复投递已经落地的条目,所以文档要求你在 append()entry.uuid 去重

从 store 恢复时,鉴权可能断在 Python 这一侧。 从 store 恢复时 SDK 会把 transcript 写进一个临时 config 目录、把 CLAUDE_CONFIG_DIR 指过去、结束后删掉。这个临时目录会从你真实的 config 目录里种一部分文件进去,两种语言拷的东西不一样:TypeScript 拷 credentials、.claude.json 和用户 settings.json(并剥掉在临时目录下会出问题的几个键);Python 只拷 credentials 和 .claude.json,所以靠用户 settings.json 里的 apiKeyHelper 鉴权的应用,从 store 恢复时会以 Not logged in 失败——放在 managed 或 project 设置里的 apiKeyHelper 仍然可用。

store 里没有这个会话时,三种回退行为各不相同resume 会把 ID 透传给子进程,等同于没有 store 时的本地恢复;TypeScript 的 continue: true 直接开一个新会话;Python 的 continue_conversation=True 则从最近的本地会话继续。同一个「继续」语义,两边落点不同,这个差异是文档白纸黑字写的。

forkSession 不是字节拷贝:它读源条目、重写每一个 sessionId 字段并重映射消息 UUID,再以新键 append 进去。文档给了理由(属于文档自述):适配器层面的拷贝或 CopyObject 快捷方式会产出一份仍然引用旧会话 ID 的 transcript。

保留策略归你。 SDK 从不主动从你的 store 里删东西,TTL、S3 生命周期、定时清理都是适配器的责任。本地 transcript 由 cleanupPeriodDays 设置独立清扫;而从 store 恢复的那类运行不留本地 transcript,你的 store 的保留策略就是唯一的保留策略

一个容易误判的读取语义getSessionMessages({ sessionStore }) 返回的是 agent 在恢复时会看到的那条消息链,也就是自动压缩之后的结果——早先的轮次已经被摘要替换掉了。想要包含压缩前轮次和元数据条目的完整原始历史,得直接调 store.load(key)

七、怎么验证配对了

文档给的判据都是行为层面的,照着看就行:

  • 续上了没有:resume 之后应该看到「在先前分析基础上继续」的回复,而不是从头开始。
  • fork 成没成forkedId 与原会话 ID 不同,且恢复原会话时仍走原来那条线——说明 fork 没有改动原历史。
  • store 通没通:用 InMemorySessionStore 跑两次 query(),第二次带上同一个 store 实例加 resume,输出里带上了第一次的上下文,就说明是从 store 加载的。
  • 适配器合不合规:两个 SDK 都带一致性测试套件(conformance suite),可选方法没实现时对应用例会自动跳过。TypeScript 需要把 shared/conformance.ts 从示例目录拷进你的测试;Python 的套件随包发布,用 pytest 跑要先 pip install pytest,然后把适配器作为零参构造工厂传给 run_session_store_conformance。文档特别提醒:各条契约复用同一批 session key,所以工厂每次返回的 store 必须是空的——用全新的内存 fake、独立的键前缀或新的测试库都行。
  • 镜像有没有悄悄丢数据:监控 mirror_error。store 挂了不会打断 agent(子进程先写本地),但在从 store 恢复的运行里,丢掉的那批没有任何幸存副本

如果要我挑一条最该贴在墙上的,是这条:「本地那份和 store 那份谁能活下来,取决于这次运行是怎么起来的。」 把这句想清楚,跨机恢复的坑基本就绕开了。


本文依据 Claude Code 官方文档(code.claude.com/docs)于 2026-08-17 的公开内容整理。 该产品闭源,本文只复述官方文档写明的机制,不推断其内部实现我们没有对文中涉及的功能做过实测,因此不涉及界面外观、操作手感与运行速度的任何描述。 该产品迭代频繁,文中涉及的命令、配置项与默认值随版本变动,请以官方文档最新内容为准。 本文不涉及价格、额度与限流的具体数值,相关信息请以官方定价与用量说明页为准。

安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。