atlas-checkpoint实现深潜:Blob、锁与时间线的存储架构全解
【免费下载链接】atlasSource control for agents. Use multiple coding agents, track their changes and query them in one place项目地址: https://gitcode.com/GitHub_Trending/atlas115/atlas
Atlas是一款面向 AI 编程代理的源代码管理工具,让你在一个界面里使用多个编码 Agent、追踪它们的每一次改动。它内部的核心记录模块atlas-checkpoint,负责把 Agent 会话、工具调用与 Git 提交持久化为一份可回溯的「Agent 行为时间线」。本文用通俗的方式,带你看懂它的三块存储基石:Blob 溢出存储、WriterLock 写锁,以及 Timeline 时间线读模型。
为什么 Agent 也需要"检查点"?
Agent 会话是短暂的:对话滚动消失后,"这段代码为什么这么写"的答案就跟着丢失了。atlas-checkpoint 的模块说明把问题讲得很直白——六周后没有人能回答"它当时为什么这么改",包括写代码的那个人。🔍
它把整个工作区的历史记录到一个文件里:.atlas/sessions.db(一个 SQLite 数据库),大文件则溢出到旁边的内容寻址目录中。模块文档列出了三条值得记住的设计承诺(见 lib.rs):
- 脱敏发生在落盘之前,而不是上传之前——本地记录本身永远不会泄露密钥;
- 全程不碰网络——离线是常态而非降级模式;
- 提交是被观察的,不是被拦截的——不装 git hooks,只观察引用(refs)移动,所以即使你在终端里提交、或 Atlas 没开时提交,也能事后关联到对应的会话。
Blob 存储:64KB 是那道分水岭
为什么大内容要"溢出"?
这不是理论上的预防措施,而是实测数据逼出来的:在一个真实开发者的语料里,294 个会话共705 MB,最大的单条消息达2.02 MB——已经超过服务端单行上限。行内存储这种数据,今天就必然失败(来源:blobs.rs 顶部注释)。
因此存储策略是:
| 常量 | 值 | 作用 |
|---|---|---|
SPILL_THRESHOLD_BYTES | 64 KB | 超过此大小的消息体溢出为独立文件 |
PREVIEW_BYTES | 2 KB | 溢出后留在行内的预览片段,用于列表渲染 |
绝大多数轮次只有几 KB,留在数据库行内只需一次查询;少数超大内容才付出"一次查询 + 一次文件读取"的代价。
内容寻址:用 SHA-256 当文件名
溢出的文件存放在.atlas/blobs/下,文件名就是内容的 SHA-256 十六进制哈希(见 key_for)。这带来两个好处:
- 天然去重:相同内容写两次只占一个文件;
- 可校验:读取时重新计算哈希,发现"内容对不上文件名"就判定为损坏文件并删除,下次写入同名内容会自动修复。
写入过程也经过精心设计:先写临时文件 →fsync→ 原子重命名。注释里点明了fsync是"承重墙"——断电时如果只重命名不刷盘,内容寻址键下会留下一个永远不会被修复的半截文件(因为put看到文件已存在就会直接短路返回)。
另外,目录按哈希前两位十六进制做了分片(path_for):一位开发者一年会产生几十万个小文件,不分片的话任何文件系统枚举它都很慢。
WriterLock:为什么多窗口应用必须"单写者"
问题:WAL 只让并发"可能",不保证"一致"
Atlas 是多窗口应用,两个窗口可以同时打开同一个工作区。SQLite 的 WAL 模式让读写并发成为可能,但两个独立的写入进程会互相踩踏同步队列(outbox)的状态机——一行记录可能被 A 进程标记为"已发送",而 B 进程还在尝试发送它。
解法:一个"永远不提交"的独占事务
锁的实现出人意料地简单(lock.rs):在一个旁边的小型 sidecar 数据库sessions.lock上,持有一个BEGIN EXCLUSIVE事务直到进程退出。这个未提交的事务本身就是操作系统级的锁:
- 绝不阻塞:抢不到锁的第二个窗口立刻降级为只读,而不是挂起等待——"等待中的窗口"和"卡死的窗口"从外面看完全一样;
- 没有过期时间:注释一针见血——"超时恰恰是产生两个写入者的方式";
- 崩溃即释放:进程崩溃时 OS 关闭句柄、事务回滚,下一个窗口自然接管。
主库sessions.db则按WAL + synchronous=NORMAL配置(store.rs):读(时间线浏览)从不阻塞写(捕获),NORMAL 比 FULL 少一次 fsync,省下的那点持久性可以由"从 Agent 原始转录重建"兜底。
Timeline:把数据表读回"人能看的故事"
时间线模块(timeline.rs)是整个 crate 唯一的读模型——"一个没有查看器的记录器,与一个坏掉的记录器没有区别"。它提供两种形状:
- SessionSummary:每个会话一行,列表视图用,统计全部来自覆盖索引,绝不读取消息正文;
- SessionDetail:单个会话的完整时间线,把提示词、回复、思考、工具调用、检查点混排成一条有序列表。
正文是"条件内联"的
一条消息可能是 40 MB 的粘贴日志。时间线的规则是:正文 ≤ 64 KB(INLINE_LIMIT_BYTES)就完整内联,否则只发 2 KB 预览并打上truncated标记,同时携带body_ref——那是指向 Blob 的钥匙,查看器可以按需取回全文。这正是 Blob 溢出策略在读侧的镜像。
排序键:轮次优先于时间
时间线最精妙的地方在排序函数(order):主排序键是turn_seq,而不是时间戳。因为检查点是在提交被观察到时创建的,可能比写入文件的那轮晚几分钟——只按时间排,提交就会从"产生它的工作"旁边漂走。
排序规则可以概括为:轮次在前 → 同轮内按 提示词 → 思考 → 工具调用 → 回复 →检查点收尾→ 最后才看时间戳。无法归因到任何轮次的孤儿检查点用turn = -1排到最顶部——"属于任何轮次都不属于的提交"恰恰是最需要开发者注意的情况,把它沉底反而是错的。
"Agent 时间"与"墙钟时间"是两回事
会话统计里的active_seconds(Agent 实际工作时间)与wall_seconds(首尾跨度)刻意分开,并配有两个上限保护(IDLE_CAP_SECONDS):消息间隔超过 300 秒算"人走开了",单轮超过 30 分钟视为休眠/崩溃后的事件,不计入工作时间。V8 迁移脚本里还留了一个真实案例:一份六月运行、七月导入的转录,曾用updated_at - started_at报告出1395 小时的工作时长,把一整年历史全塞进了"今天"分组(见 schema.rs)。
检查点如何把提交"认"回会话?
提交与会话的关联规则是不对称的(checkpoint.rs 顶部注释):
- 提交中改动过已存在的文件 → 仅凭文件路径就建立链接(人工审改 Agent 产物再提交是正常流程);
- 文件是新建的 → 提交内容必须与 Agent 写入时的哈希完全一致,否则不算 Agent 的功劳(防止"Agent 建了文件、人删掉重写了"被记到 Agent 头上)。
每次扫描从上次游标走到 HEAD(walk_new_commits);游标丢失时做一次最多 200 个提交的有界重扫,宁可重放也不允许"静默失明"。合并提交本身不产生检查点——那会把合进来的每个改动记第二次账——但会评估合并带入的侧分支提交,保证每份工作在真正产生它的那个提交上被记录恰好一次。
从哪些文件开始读?
如果你想亲手验证本文的每个结论,crate 刻意不依赖 Tauri,测试直接驱动真实临时目录、真实 SQLite 和真实 git 仓库,是极佳的阅读入口:
| 模块 | 路径 | 看点 |
|---|---|---|
| 模块总纲与设计承诺 | lib.rs | 三句话讲清定位 |
| Blob 溢出与内容寻址 | blobs.rs | fsync、去重、分片 |
| 单写者锁 | lock.rs | 99 行读完一个分布式锁 |
| 本地存储与 WAL 配置 | store.rs | 打开、只读降级、事务粒度 |
| 表结构与迁移策略 | schema.rs | 索引即 schema 的一部分 |
| 时间线读模型 | timeline.rs | 排序规则与统计口径 |
| 会话捕获与脱敏 | capture.rs | 唯一的写入入口 |
| 提交观察与关联 | checkpoint.rs | 非对称链接规则 |
| 端到端测试 | tests/ | 捕获、时间线、绑定、健康检查 |
小结
atlas-checkpoint 用三件朴素而扎实的工具解决了 Agent 记录的难题:
- Blob:64 KB 阈值 + SHA-256 内容寻址,让 705 MB 的语料也能稳定落盘;
- 锁:一个永不提交的独占事务,换来无依赖、自动释放、崩溃安全的单写者保证;
- 时间线:轮次优先的排序与条件内联的正文,让"提交紧跟在产生它的工作之后"成为默认视图。
它们共同守护的,是模块文档开篇那句承诺:无论六个月后谁来打开这个工作区,"当时为什么这么写"的答案,都还在。
【免费下载链接】atlasSource control for agents. Use multiple coding agents, track their changes and query them in one place项目地址: https://gitcode.com/GitHub_Trending/atlas115/atlas
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考