Sessions · 会话怎么存、怎么回退、怎么 fork

进 Part 6。一盘对话怎么落盘、怎么恢复、怎么回退到某一步、怎么 fork 出一个分支。这一章拆 grok 的会话持久化:JSONL 双文件、UUIDv7、两阶段 rewind、跨压缩回放这个硬骨头,以及会话搜索与跨工具接续。

作者 David更新于 第 49 篇(共 60 篇)

Sessions · 会话怎么存、怎么回退、怎么 fork

Part 6 讲运行形态与工程。先看会话持久化——Part 1 的 chat-state 在内存里管对话,这一章看它怎么落盘、恢复、回退、fork。

落盘布局

一个会话存在 ~/.grok/sessions/<编码后的 cwd>/<会话 id>/,主要文件名集中定义在 session/storage/mod.rs 顶部的常量里(UPDATES_FILE、CHAT_HISTORY_FILE、SUMMARY_FILE……)。几个关键文件:

文件装什么
chat_history.jsonl模型面的对话历史(压缩后的视图)
updates.jsonl只追加的 SessionUpdate 流——回放的真相源
summary.json会话元信息(标题、模型、时间、父会话)
rewind_points.jsonl文件快照的回退点
compaction/每段压缩的 markdown 与 INDEX.md
compaction_checkpoints/每次压缩的检查点 JSON

另有 plan.json、plan_mode.json、signals.json、usage.json、tool_state.json 等小的 JSON 边车。

一个文档与实现不符的点值得记:user-guide 的 headless 篇(14-headless-mode.md)在目录表里把 sessions/ 写成「Session transcripts (SQLite)」,实际是 JSONL + JSON 边车文件(sessions 篇 17-sessions.md 倒是写对了)。SQLite 只是派生的 FTS5 全文搜索缓存:如今拆成了独立 crate xai-grok-session-search,库在 <grok home>/sessions/session_search.sqlite,坏了就隔离重建,而且它从不直接读会话目录(由调用方提供 SessionSource,updates.jsonl 的格式只归存储层管)。读源码时别被文档带偏——JSONL 才是权威存储。这也再次说明读真实源码(而非文档)的价值。

会话 id 是 UUIDv7(时间有序、定长 36 字符;客户端也可以用 -s 自带一个 UUID 新建)。写盘走一个持久化 actor(PersistenceMsg),按 OS 做 F_FULLFSYNC/fsync;summary.json 这类小文件用「临时文件 + rename + 同步父目录」原子替换。chat_history.jsonl 的追加不保证崩溃原子,所以读时容忍坏行:跳过它们照常加载,原文件只隔离一次成 chat_history.jsonl.corrupt。

恢复

--resume [id](空 = 最近一次;现在也能按当前目录下的会话标题匹配,UUID 形状的值总走 id 路径)/ -c / --fork-session。解析分「无 I/O 定意图 → 有 I/O 落地」两步:session_startup_intent_from_flags(纯函数,TUI 与 headless 共用,产出 NewAuto/NewWithId/Resume/ForkFrom;--fork-session 配 --worktree 这类非法组合在这一步就被拒)→ materialize_startup_for_cwd(查盘:最近会话、同仓不同 cwd……)。恢复时沙箱 profile 必须匹配:async_main 发现 --sandbox 与会话创建时的 profile 不同,会直接报错退出,不会悄悄改。

rewind:两阶段回退

回退到「第 N 步之前」——保留 0..N-1、撤销 N 起的改动。每个 prompt 都是一个检查点,RewindMode 可选 All(对话 + 文件)、ConversationOnly(只回对话)、FilesOnly(只回文件)。handle_rewind 是两阶段的:

  • force=false:干跑预览——算出哪些文件能干净回退、哪些冲突(被外部改过/删过),不动任何东西;
  • force=true:提交——按 file_snapshots 回放文件的旧内容,截断对话。

最硬的骨头是跨压缩回放:压缩会把 N+1 条用户消息折成三条左右,内存里的对话已经对不上 prompt 序号。所以现在只要会话压缩过(needs_compaction_replay),不管回退点在压缩前还是后,都不再简单截断,而是由 replay_to_prompt 从 updates.jsonl + 压缩检查点重建那个时点的对话。回退还会往 updates.jsonl 追加一个 RewindMarker,回放时据此剪掉死掉的时间线分支。

fork:复制一个分支

fork_session(session/fork.rs)把一个会话复制成新分支:copy_session_data_sync(storage/jsonl/copy.rs,放在阻塞线程池里跑,多个 fork 能真正并行)读 chat_history + updates,截断到目标 prompt,重写每条 update 里的 session_id,复制 tool_state.json、压缩段目录,以及保留下来的记录所引用的 compaction_checkpoints/*.json。新 id 也是 UUIDv7(不嵌源 id,免得 fork-of-fork 越来越长);向后端登记则丢进后台任务,不挡关键路径。这就是 subagent 和 worktree 隔离执行的会话侧基础。

搜索与跨工具接续

写作之后新增的两块:grok sessions search 把本地 FTS5 结果与远端结果合并;/resume 选择器还能列出 Claude Code、Codex、Cursor 的近期会话——xai-grok-foreign-sessions 只读元数据(每个工具最多 50 条、30 天内),选中后交给内置的 resume-claude/resume-codex/resume-cursor 技能接手。

和《从 LLM 到 Coding Agent》对照

对照 session-history:最小实现里存对话就是把数组写个 JSON。grok 的增量是把它做成可恢复、可回退、可 fork、还能跨压缩重建的持久化系统:双文件真相源、UUIDv7、两阶段 rewind、崩溃容错。下一章看一个特别的运行模式:plan mode。


上一篇:PTY 包装与内联媒体 · 下一篇:Plan mode

本页目录