Context Mode 的会话连续性系统通过一个本地 SQLite 数据库实现,该数据库包含 session_events、session_meta 和 session_resume 三张核心表。它实时捕获 AI 编码助手执行的各类操作(如文件编辑、Git 命令、任务创建等),将其结构化为事件并存储。当会话因上下文窗口耗尽而被压缩时,系统从历史事件中提取关键信息,构建一个轻量的 XML 快照,并在新会话开始时将其注入,从而实现跨压缩的上下文连续性。
Context Mode 会话连续性系统:SQLite 持久化、事件提取与跨压缩恢复的工作原理
本文将从操作角度,逐步拆解 Context Mode 的会话连续性系统是如何工作的。你可以将其理解为一个记录、摘要和恢复会话状态的自动化流水线。
第一部分:SQLite 持久化存储
会话连续性的基础是一个按项目隔离的 SQLite 数据库文件。其 schema 定义在 src/session/db.ts 的 initSchema 方法中。
1.1 三张核心表
-
session_events:这是系统的“日志流”。每一条记录代表一个被捕获的会话事件。关键字段包括:session_id:标识一次独立的 AI 对话。type和category:事件的类型和分类,例如file_edit、git、task等。data:事件的有效负载,例如被编辑的文件路径或 Git 操作的详细信息。priority:事件的重要级别(1-5),用于在事件数量超过上限时进行 FIFO(先进先出)驱逐时的优先级判断。bytes_avoided和bytes_returned:记录此事件为上下文窗口节省和实际返回的字节数,用于多适配器统计聚合。data_hash:基于data字段内容的 SHA256 摘要,用于快速去重。
-
session_meta:存储会话的元数据。每个session_id对应一行,记录了会话开始时间、最后一个事件的时间、事件总数以及被压缩(compact)的次数。 -
session_resume:存储用于恢复的 XML 快照。当会话被压缩时,系统会构建一个快照并写入此表。关键字段是snapshot(XML 内容)、event_count(快照覆盖的事件数)和consumed(标记此快照是否已被新会话消费)。
1.2 存储路径与 Worktree 隔离
数据库文件通过 resolveSessionDbPath 函数确定路径,遵循 <项目目录哈希><worktree后缀>.db 的格式。
- 项目目录哈希:
hashProjectDirCanonical函数会对项目路径进行标准化(在 macOS/Windows 上会转为小写),然后取 SHA256 哈希的前 16 个字符。这确保了不同大小写表示的同一项目指向同一个数据库。 - Worktree 后缀:通过
getWorktreeSuffix函数计算。如果当前目录是 Git Worktree,则会生成一个基于该 worktree 路径的 8 字符哈希作为后缀。这实现了不同 worktree 的会话数据隔离。 - 大小写迁移:代码包含一次性的迁移逻辑。如果发现旧的、基于原始大小写路径的数据库文件存在,会将其重命名为新的、基于规范哈希的文件,以平滑升级。
验证方法:你可以在项目中运行 ctx_doctor 工具或直接查看 ~/.claude/context-mode/sessions/ 目录,观察数据库文件的命名规则。
第二部分:事件提取管道
事件提取的职责由 src/session/extract.ts 中的纯函数完成。它们接收原始的工具调用(HookInput)和用户消息,输出结构化的 SessionEvent 数组。
2.1 支持的事件类型
提取函数覆盖了 AI 编码助手(如 Claude Code)的绝大多数操作,主要类别包括:
- 文件与规则 (
file,rule):捕获Read,Edit,Write,NotebookEdit,apply_patch等工具对文件的读写。特别地,读取CLAUDE.md或.claude/目录下的规则文件时,会同时生成rule和file_read两类事件。 - 工作目录与环境 (
cwd,env):从Bash工具的命令中提取cd操作和source,export,nvm use,pip install等环境配置命令。 - Git 操作 (
git):通过正则匹配Bash命令中的git checkout,git commit,git merge等近 20 种常见 Git 子命令。 - 任务与计划 (
task,plan):捕获TodoWrite,TaskCreate,TaskUpdate以及EnterPlanMode,ExitPlanMode工具调用,记录任务生命周期和计划状态。 - 决策与意图 (
decision,intent):分析用户消息,识别出纠正、角色设定(如“You are a senior engineer”)以及指令意图(如是否是调查性问题)。 - 其他:还包括错误 (
error)、子代理 (subagent)、MCP 工具调用 (mcp)、外部引用 (external-ref) 等。
代码证据:在 extract.ts 中,你可以看到 extractFileAndRule, extractGit, extractTask 等专用函数,每个函数负责一种或一类事件的提取逻辑。
2.2 去重与预算
在 SessionDB.insertEvent 方法中,插入事件前会进行去重检查。它会对比当前事件与最近 5 个事件的 type 和 data_hash。如果相同,则跳过插入,避免重复记录。
同时,每个会话的事件数量上限为 1000(MAX_EVENTS_PER_SESSION)。超出时,系统会驱逐优先级最低且最旧的事件,为新事件腾出空间。
验证方法:tests/session/session-db.test.ts 中的 “Deduplication” 和 “Max Events & FIFO Eviction” 测试用例直接验证了这两项机制。
第三部分:快照构建与跨压缩恢复
这是会话连续性的核心:当上下文窗口被压缩后,如何恢复上下文。
3.1 快照构建 (buildResumeSnapshot)
当检测到上下文压缩(incrementCompactCount 被调用)时,系统会调用 src/session/snapshot.ts 中的 buildResumeSnapshot 函数。
- 事件分组:从
session_events表中读取当前会话的所有事件,按category(如file,task,git)进行分组。 - 生成摘要:为每个非空的事件组生成一个简短的自然语言摘要。例如,对于文件操作组,会列出最近编辑过的文件名;对于任务组,会列出尚未完成的任务。
- 嵌入检索指令:这是关键一步。在每个摘要块下方,嵌入一个
<tool_call>指令,该指令指向ctx_search工具,并包含了精准的查询关键词。例如,针对一个文件编辑事件,查询词可能是"config.ts edit"。这避免了在快照中存放冗长的原始数据。 - 组装 XML:将所有摘要块组装成一个标准的
<session_resume>XML 文档。这个文档就是快照的主体。
代码证据:snapshot.ts 中的 buildFilesSection, buildTaskSection 等函数展示了摘要生成和 toolCall 函数的嵌入逻辑。
3.2 快照的消费与恢复 (claimLatestUnconsumedResume)
恢复过程发生在 SessionStart 钩子中。当一个新的会话(session_id 不同)开始时:
- 原子领取:钩子会调用
SessionDB.claimLatestUnconsumedResume(currentSessionId)。该方法执行一个原子 SQL 操作:查找consumed = 0且不属于当前会话(session_id != ?)的最新快照行,将其consumed字段更新为 1,并返回快照内容。原子性确保了即使有多个进程并发尝试,也只有一个能成功领取。 - 注入上下文:领取到的 XML 快照内容被拼接到新会话的初始上下文中。LLM 读到这个快照,就知道之前发生了什么,并且看到其中嵌入的
ctx_search指令。 - 按需检索:在后续的对话中,当 LLM 需要了解之前某个事件的详细信息(例如,某个错误的具体输出),它可以调用
ctx_search工具,并使用快照中提示的查询词。ctx_search会去session_events表或 FTS5 知识库中进行搜索,返回详细信息。这实现了“目录+按需检索”的模式,最大化节省了上下文窗口。
验证方法:tests/session/continuity.test.ts 中的 “SessionStart Hook — /resume snapshot fallback (#413)” 测试套件完整模拟了快照的写入、领取和消费的生命周期。而 session-db.test.ts 中的 “Atomic claim of latest unconsumed resume” 测试则专门验证了 claimLatestUnconsumedResume 的原子性和自我排除逻辑。
FAQ
Q: 为什么选择 SQLite 而不是其他数据库? A: SQLite 是一个服务器端的嵌入式数据库,无需额外部署,非常适合这种需要按项目隔离、本地持久化的场景。它的事务支持和 WAL 模式能很好地处理来自多个 Hook 进程的并发写入。
Q: 快照恢复的准确性如何保证?
A: 准确性建立在两个基础上:一是事件提取管道捕获了足够丰富且结构化的原始数据;二是快照仅包含摘要和检索指令,不包含冗余信息。当 LLM 需要时,它通过 ctx_search 获取经过验证的原始数据,而非依赖可能已过时的缓存。
Q: bytes_avoided 和 bytes_returned 这两个字段具体代表什么?
A: bytes_returned 是 Context Mode 实际返回给模型上下文窗口的字节数。bytes_avoided 是 Context Mode 通过沙箱执行、索引等手段,避免进入上下文窗口的原始数据字节数。两者之和大致代表了“如果不使用 Context Mode 会消耗的上下文总量”,用于计算节省比率。