合并受阻
变更摘要
本 PR 为 datalog 模块新增保留策略(retention policy),解决 ~/.atomcode/datalog/ 下 JSONL 日志无限制累积、最终写满磁盘的问题(issue #1551)。在 DatalogConfig 中新增 max_age_days(按最后修改时间删除过期 .md/.jsonl)与 max_total_size_mb(全根目录总大小软上限)两个配置项,均默认 None(不启用),老用户升级后行为不变。保留清理在 DatalogHook::new 时通过新的 WriteOp::Retain 一次性入队到专用 writer 线程执行(fire-and-forget,绝不阻塞 turn),按「先按年龄删、再做大小封顶」的顺序遍历 datalog 根目录下所有项目 bucket,仅删除 .md/.jsonl,所有 fs 错误静默忽略。
主要改动
- 新增配置项:
crates/atomcode-config/src/config/mod.rs的DatalogConfig新增max_age_days: Option<u64>与max_total_size_mb: Option<u64>字段,Default实现中两者均为None(默认不清理,避免升级后误删既有文件)。 - 触发时机与执行线程:
DatalogHook::new在任一保留配置启用时解析 datalog 根目录(取配置目录的父级以覆盖所有项目 bucket),并通过新增的DatalogWriter::retain将WriteOp::Retain排在 writer 队列最前,由writer_loop调用apply_retention执行,不阻塞 turn 处理。 - 清理逻辑:新增
apply_retention与collect_datalog_files:先按 mtime 删除超过max_age_days的.md/.jsonl(0表示启动时全部清空),再在总量超出max_total_size_mb时从最旧文件开始逐出直至低于上限;过期文件不计入大小预算,避免二次删除。 - 配置渲染:
render_datalog_section输出带注释的新配置项说明,并在配置了保留值时渲染max_age_days = N/max_total_size_mb = N,方便用户在config.toml中发现与启用。 - 测试覆盖:新增 4 个单元测试(
retention_deletes_files_older_than_max_age_days、retention_size_cap_deletes_oldest_files_first、retention_without_limits_is_a_no_op、retention_leaves_non_log_files_alone),并同步更新datalog.rs、config.rs中既有测试的DatalogConfig构造。


代码审查
审查结论
发现汇总:2 个问题(P1 × 1,P2 × 1),均位于 crates/atomcode-capabilities/src/datalog.rs。
| 优先级 | 数量 | 说明 |
|---|---|---|
| P0 | 0 | — |
| P1 | 1 | 保留策略根目录推导越界,可能删除 datalog 之外的用户 .md/.jsonl 文件 |
| P2 | 1 | Retain 排在 writer 队列最前,慢速扫描可耗尽 Initialize 的 500ms 等待窗口,导致首轮日志静默丢失 |
| P3 | 0 | — |
各文件审查情况:
crates/atomcode-capabilities/src/datalog.rs— 发现问题。核心问题在于DatalogHook::new中ancestors().nth(1)的根目录推导(对~、.等文档支持的dir值会把家目录/项目目录整体纳入删除范围,且collect_datalog_files全递归、仅按扩展名过滤);以及 Retain 排在 writer 队列最前对首次 Initialize 应答的阻塞风险。apply_retention/collect_datalog_files本身的分龄、大小封顶、最旧优先逻辑正确,4 个新测试逻辑自洽,但均直接调用apply_retention、绕过了有缺陷的根目录推导路径。crates/atomcode-coding/src/config.rs— 无问题。仅测试中的DatalogConfig结构体字面量补充了 2 个新字段,与atomcode-config的字段定义一致。crates/atomcode-config/src/config/mod.rs— 无问题。新字段带#[serde(default)](旧配置文件兼容)、Default为None(老用户零行为变化)、render_datalog_section仅在有值时输出并可正确回读,各测试字面量均已同步更新。
总体风险评估: 新功能默认关闭、对存量用户无行为变化,apply_retention 本身的分龄/封顶逻辑正确且容错(失败静默)。但启用保留策略后,删除范围随用户配置的 dir 变化,存在把删除操作扩散到非 datalog 用户数据的实际风险,属于数据丢失级别的问题,建议在合并前修正根目录推导并对删除范围做 bucket 形态校验,同时补充经 DatalogHook::new 的集成测试。
| 类型 | 数量 |
|---|---|
| 🔴 阻塞 | 1 |
| 🟡 建议 | 0 |
⛔ 需要修改


🟠 High Priority
变更行(datalog.rs:110-117):DatalogHook::new 用 resolve_log_dir(...) 得到 <base>/<slug> 后,取 ancestors().nth(1)(即父目录)作为保留策略的遍历根。resolve_log_dir(datalog.rs:126-147)对任何非默认 dir 都直接返回 base.join(project_slug(working_dir)),因此父目录就是用户配置的 dir 本身。
受影响行为/契约:注释声称"遍历 datalog 根目录下所有项目 bucket",但 collect_datalog_files(datalog.rs:545-574)是全递归 + 仅按扩展名 .md/.jsonl 过滤,对父目录下任意深度的任意文件都纳入删除范围,且不校验文件是否位于 bucket 目录内。而 resolve_log_dir 显式支持的配置值 dir = "~"(datalog.rs:131)会让父目录变成整个家目录;dir = "."(相对路径、文档明确支持的写法)会让父目录变成整个项目目录。
失败模式:当用户配置非专用目录(~、.、~/Documents、共享绝对路径等)并启用 max_age_days 或 max_total_size_mb 时,保留策略会递归遍历该目录并删除其中所有超过年龄阈值或按大小逐出的 .md/.jsonl 文件——包括用户笔记、项目文档、其他应用的 jsonl 日志等与 datalog 无关的数据。.md 是用户文档的常见扩展名,这是真实的数据丢失(P1),且 4 个新增测试全部直接调用 apply_retention 传入显式 root,绕过了 new 里这段根目录推导,未覆盖此路径。
建议:不要用 slug 目录的父目录盲推保留根。改为:仅在根目录第一层子目录(bucket)内删除 .md/.jsonl,且不无限递归到任意用户目录;对非默认 dir 额外校验目录形态(只含 slug 桶)后再遍历;补一个经 DatalogHook::new 的集成测试覆盖根目录推导。


感谢审查,1 个 P1 和 1 个 P2 均已修复(commit b65ad003,已推送):
P1:删除范围越界(数据丢失风险)✅ 已修复
修复:collect_datalog_files 重写为非递归扫描,且新增 is_project_bucket_name 校验——只处理根目录第一层、名字匹配 <slug>-<8位hex> 形态的 bucket 目录内的 .md/.jsonl。这与 initialize_files 的实际落盘布局(bucket 目录下一层)严格一致,因此:
dir = "~"/"."时,家目录/项目目录下的用户笔记、其他应用的 jsonl 等不可能被触碰(它们不在合法 bucket 内)- 根目录散落的
.md(如my-notes.md)直接跳过 - 名字像 bucket 但 hash 非 8 位 hex 的目录(如
project-nothex)跳过 - 8-hex 名字的文件(非目录)跳过
P2:Retain 占用 writer 队列头,可能耗尽 Initialize 的 500ms 窗口 ✅ 已修复
修复:retention 不再入 writer 队列,改为独立分离线程(atomcode-datalog-retain)执行。writer 队列保持纯写操作,Initialize 永远不会被扫描阻塞。竞态方面安全:扫描只删除早于 cutoff 的文件,与 hook 即将创建的新文件无交集。
新增集成测试(均经 DatalogHook::new 全路径,不再是绕过根目录推导的直接调用)
retention_via_hook_never_touches_non_bucket_user_files— 覆盖审查指出的全部危险形态:根目录散落.md、8-hex 名文件、嵌套用户目录树、非 hex hash 目录,逐一断言存活;同时验证真 bucket 内的过期文件被删retention_does_not_block_first_turn_log_creation— 开启 retention 时首轮 turn 的 md/jsonl 必须正常落盘(若 P2 回归,Initialize 超时会导致文件缺失,此测试会失败)
验证结果
datalog 模块 13/13 测试通过(11 既有 + 2 新增),既有测试的 seed() 辅助函数已同步改为合法 bucket 名(旧名 project-slug 恰好被新的 bucket 校验正确拒绝——防护按预期工作)。


变更摘要
本 PR 为 datalog 日志引入保留策略(retention policy),解决 issue #1551 中 JSONL 日志 append-only 累积导致磁盘写满的问题。核心是在 DatalogConfig 中新增两个默认关闭的配置项 max_age_days 与 max_total_size_mb,并在 DatalogHook::new 时于专用线程上触发一次性清理:先按文件最后修改时间删除过期文件,再做总量软上限封顶(最旧优先逐出)。保留作用域被严格限制为 datalog 根目录下第一级、名称符合 <slug>-<8 位十六进制> 形态的项目 bucket 内的 .md/.jsonl 文件,非日志文件、松散根文件及嵌套用户目录一律不动;所有文件系统错误静默忽略,确保观测逻辑绝不阻塞 turn。
主要改动
- 新增配置项
max_age_days/max_total_size_mb:在DatalogConfig中增加两个Option<u64>字段(serde(default)且Default均为None),老用户升级后行为不变;render_datalog_section在config.toml渲染时带注释输出这两个新配置项(max_age_days = 0表示启动时清空)。 - 新增保留清理逻辑
apply_retention/collect_datalog_files:apply_retention先按 mtime 删除超过max_age_days的文件,再在总大小超过max_total_size_mb时从最旧文件开始逐出直到回到上限以下,避免过期文件重复计入预算;collect_datalog_files仅收集位于第一级 bucket 目录内的.md/.jsonl文件。 - 作用域安全约束
is_project_bucket_name:bucket 目录名必须匹配<slug>-<8 位十六进制>形态(对应stable_project_hash),松散文件、非 bucket 目录、嵌套目录及形似但哈希不合规的目录均不会被扫描或删除,防止保留策略波及用户自定义dir下的无关数据。 - 执行方式改为独立线程:新增
DatalogWriter::retain,保留扫描跑在名为atomcode-datalog-retain的分离线程上(fire-and-forget),不再占用 writer 队列头部,避免慢扫描挤占首个Initialize的IO_WAIT_TIMEOUT导致首轮日志丢失。 - 补充测试覆盖:新增按天删除过期文件、超限最旧优先逐出、未配置时不动作、非日志文件不受影响 4 个单元测试,以及两个集成测试,验证保留逻辑不会删除非 bucket 用户文件、不会阻塞首轮日志创建。


先谢谢这个 PR,方向(给 datalog 加清理)是对的,实现也遵守了"observability 不改变 turn 行为"这条原则(专用 writer 线程、fire-and-forget、只删 .md/.jsonl、失败静默),质量没问题。
不过在合并前想同步一个背景:#1551 的根因是写放大(on_request 每轮全量写历史,O(n²)),而保留策略删的是已生成的文件,并不减少写入量。我们已从源头修掉了写放大(改成内容寻址记录,单会话 540MB→~9MB,约 58×),单会话写爆盘这个急性故障已经消除。
因此这个 PR 的定位从"必需的防灾手段"变成了"可选的长期卫生"。如果要继续推进,建议先处理两点:
- 清理时机:目前设计在 DatalogHook::new(会话装配时)一次性触发。但把盘写满的恰恰是进行中的长会话——它在会话开始那一刻文件还没长出来,启动清理接不住。建议改成周期性触发(会话进行中也能收),而不只在构造时跑一次。
- max_age_days = 0 = 启动时清空 的语义:与"0 表示不限制"的直觉相反,容易误触全删。建议改成"0/缺省 = 关闭",清空另设显式动作。
综合建议:本 PR 先不合(急性问题已由根因修复解决)


问题
~/.atomcode/datalog/的 JSONL 日志每轮 LLM 请求都写入完整 messages 历史快照(append-only),且无轮转/大小上限/TTL。实测(issue #1551):200 轮会话单文件 540MB,去重后真实内容仅 6.9MB,冗余 77.7 倍;29 天累计 36GB 直接写满系统盘。方案
新增两个
[datalog]配置项(均默认关闭,老用户零行为变化):[datalog] # 删除 mtime 超过 N 天的 .md/.jsonl(0 = 启动时清空) max_age_days = 30 # 全根目录总量软上限(MB),超限时从最旧文件开始删除 max_total_size_mb = 2048设计要点
DatalogHook::new时一次性触发,排在 writer 队列最前;跑在专用 writer 线程上 fire-and-forget,绝不阻塞 turn(遵守模块"observability must never change a turn's behavior"原则).md/.jsonl,其他文件不动render_datalog_section带注释输出新配置项,方便用户在 config.toml 中发现测试
新增 4 个单元测试:
retention_deletes_files_older_than_max_age_days— 按天删除过期文件retention_size_cap_deletes_oldest_files_first— 超限按最旧优先逐出retention_without_limits_is_a_no_op— 未配置时不动作retention_leaves_non_log_files_alone— 非 .md/.jsonl 文件不受影响datalog 模块 11/11 测试通过(含全部既有测试,无回归);
atomcode-config/atomcode-capabilities/atomcode-coding三个受影响 crate 编译通过,零新增警告。Closes #1551