跳到主要内容

会话 — 列举/改写、resume 落地、外部存储镜像

本章讲会话怎么被持久化、怎么恢复、怎么搬到外部存储。这是 SDK 里最独立的一块,不影响主对话流程,按需读。

1. 先建立心智模型

会话 = 一个本地 JSONL 转录文件。 子进程(claude CLI)把每轮对话逐行写进 ~/.claude/projects/<project_key>/<session_id>.jsonl。SDK 的会话功能都是围绕这个文件:

三件事,别混:
① 读/改本地转录 list_sessions / fork_session … ← 纯操作本地 JSONL
② resume 落地 从外部存储写回临时 JSONL,让子进程 --resume
③ 镜像到外部 每写一行,顺手 append 到你的 SessionStore

2. 会话列举与改写(操作本地文件)

2.1 能干什么

__init__.py 导出的一组函数,全部直接读写本地转录文件:

函数干什么出处
list_sessions列当前项目的历史会话(id + 元信息)_internal/sessions.py:680
get_session_info / get_session_messages取某会话的元信息 / 消息sessions.py:739 / :1054
list_subagents / get_subagent_messages列子 agent 转录sessions.py:1281 / :1323
rename_session / tag_session改名 / 打标签session_mutations.py:53 / :112
delete_session删会话session_mutations.py:182
fork_session从某会话分叉出新会话session_mutations.py:240

2.2 一个安全细节:UUID 校验

session_id 会被当路径组件用,所以处处校验它是合法 UUID(_validate_uuid,sessions.py:69),防目录穿越。resume 落地路径也走同样校验(session_resume.py:151)。

3. resume 落地:从存储写回临时目录

3.1 它要解决的小问题

"我把会话存在 S3,本地没有那个 JSONL 文件,但子进程只会从本地文件 --resume。怎么办?"

3.2 思路

在子进程启动,SDK 先从 SessionStore 把会话 load() 出来,写进一个临时目录,再让子进程用 CLAUDE_CONFIG_DIR 指向那里去 resume。用完删临时目录。

materialize_resume_session(options)
├─ 没设 store 或没 resume/continue → 返回 None(走普通路径)
├─ 解析要恢复的 session_id(显式 resume 优先,否则取最近的)
├─ mkdtemp → 写 <session_id>.jsonl
├─ 复制鉴权文件(.credentials.json 等)到临时目录
├─ 若 store 支持 list_subkeys → 一并落地子 agent 转录
└─ 返回 MaterializedResume(config_dir, resume_session_id, cleanup)

依据:materialize_resume_session(session_resume.py:123)。之后 apply_materialized_options(session_resume.py:70)把 CLAUDE_CONFIG_DIR 塞进 env、把 resume 设成落地的 id、清掉 continue_conversation

3.3 清理的严谨

临时目录里有 .credentials.json 副本,必须在每条退出路径删掉。InternalClient.process_querytry/finally + 显式 inner.aclose() 保证:先终止子进程(它在读写临时目录),删临时目录(_internal/client.py:72-89,注释解释了 PEP 533 未落地导致 async for 不会自动关迭代器的坑)。

4. 外部存储镜像:SessionStore

4.1 协议长什么样

SessionStore(types.py:1425)是个 Protocol,只有 appendload 必须实现,其余(list_sessionslist_session_summariesdeletelist_subkeys)可选——调用点运行时探测存在性,不用 isinstance,所以鸭子类型的适配器不必继承(types.py:1439-1444)。

方法必需?干什么
append(key, entries)镜像一批转录条目(本地写成功才调)
load(key)resume 时读回整个会话
list_sessions / list_session_summaries列会话
delete删(不实现则删是 no-op,适合 WORM 存储)
list_subkeys枚举子 agent 转录

参考实现是 InMemorySessionStore(session_store.py:35,仅供测试/开发)。examples/session_stores/ 下有 S3、Redis、Postgres 的真实适配器。

4.2 写路径:transcript_mirror 帧 + 批处理

镜像不是同步阻塞的。子进程往 stdout 混发 transcript_mirror 帧,Query 读循环把它们剥离(不产出给你),交给 TranscriptMirrorBatcher(transcript_mirror_batcher.py:46):

transcript_mirror 帧 → batcher.enqueue()(即发即走)
flush 时机二选一:
① result 消息到达 → 显式 flush(保证消费者看到 result 时存储已最新)
② 挂起量超阈值(500 条 / 1 MiB)→ 后台 eager flush
失败重试 3 次 + 短退避,仍失败才丢弃并报 MirrorErrorMessage

依据:读循环剥离(query.py:287-294)、result 前 flush(query.py:300-302)、阈值与重试常量(transcript_mirror_batcher.py:28-35)。这样把适配器延迟挡在模型流式输出的热路径之外。

4.3 两种 flush 模式

session_store_flush(types.py:1968):"batched"(默认,每轮或超阈值刷一次)vs "eager"(每帧后台刷,近实时)。build_mirror_batcher(session_resume.py:90)用把阈值清零的方式实现 eager。

4.4 幂等与失败语义

append 的 docstring(types.py:1447-1466)约定:多数条目带稳定 uuid,适配器应当拿它当幂等键(upsert/去重);没 uuid 的(标题、标签)直接追加。失败批次重试 3 次,超时重试(在途调用可能仍会落库)。整套是"至多一次"投递,本地转录始终是权威副本。

4.5 SessionKey 的形状

SessionKey(types.py:1331):project_key + session_id + 可选 subpath(子 agent 用)。file_path_to_session_key(session_store.py:149)负责把子进程写的文件路径反解成 key,主转录和子 agent 转录各有路径模式。

5. 代码地图

主题文件路径符号名
会话列举src/claude_agent_sdk/_internal/sessions.pylist_sessions get_session_messages
会话改写src/claude_agent_sdk/_internal/session_mutations.pyfork_session rename_session delete_session
resume 落地src/claude_agent_sdk/_internal/session_resume.pymaterialize_resume_session apply_materialized_options
存储协议src/claude_agent_sdk/types.pySessionStore SessionKey
参考实现src/claude_agent_sdk/_internal/session_store.pyInMemorySessionStore file_path_to_session_key
镜像批处理src/claude_agent_sdk/_internal/transcript_mirror_batcher.pyTranscriptMirrorBatcher
摘要折叠src/claude_agent_sdk/_internal/session_summary.pyfold_session_summary