$ cat zh/blog/bounded-local-session-log-scanner

给本地 AI 会话日志扫描器加上边界

一个异常 JSONL 单行可能持续吞内存,一次永远不提交的扫描也可能让后续刷新永久停住。

本地优先的用量报告同样需要生产级故障边界。Claude Code 和 Codex 的会话日志通常是追加写入的 JSONL,但里面也可能出现工具 payload、粘贴图片、未写完的末行、重复事件,以及扫描过程中仍在变化的文件。

Agent Island v1.7.1 处理了两个不同问题:异常单行不再无上限缓存;运行超过十分钟却没有提交结果的扫描,也不再阻塞整个进程后续的刷新。

把内存边界和活性边界分开

单行上限保护内存,扫描年龄上限保护系统继续更新。两者不应被一个笼统的超时替代。

逐个扫描会话文件:
  按平台策略流式读取每一行
  只解析包含 usage 的 assistant 事件
  按稳定身份去重

刷新时:
  扫描仍在进行且未超过卡死阈值 -> 等待
  否则 -> 在 UI 线程外启动新扫描

从日志合同决定单行策略

macOS 的 Claude reader 使用 64 MiB backstop。它是内存保护,不是常规过滤器。Claude 的 usage 可能和较大的工具 payload 位于同一条 assistant 记录里,如果把阈值设得过小,会直接丢掉真实用量。

Windows 的策略更严格:长度超过一百万字符的行在 JSON 解析前就被跳过。这个差异必须明确为平台差异,不能包装成完全相同的解析器。

两端对损坏行和未写完的行都只跳过当前记录。文件指纹变化后,下一轮仍会重新解析。

尽早排除不可能包含用量的结构

通过单行边界后,reader 只接收 type == "assistant"、包含 message.usage、模型不是占位符、有有效时间戳且至少一个 token 字段非零的记录。两个 ID 都存在时,用 messageId:requestId 去重。

按路径、修改时间和大小建立的解析缓存可以跳过未变化文件,但缓存优化不能替代异常单行保护。

让过期的 in-flight gate 失效

“如果正在加载就 return”可以阻止重叠扫描,也会把一次永远不完成的任务变成永久冻结。v1.7.1 的 macOS store 分别记录 Claude 与 Codex 的扫描开始时间。某个 provider 的 gate 超过 600 秒后,下一次刷新可以启动替代扫描,慢的 Claude 扫描不会挡住健康的 Codex 扫描。Windows 对共享扫描锁使用同样的十分钟逃生阈值。

十分钟不是正常耗时目标,而是远高于正常路径的恢复边界。真正收紧阈值前,应先记录扫描耗时分布。

只提交完整结果

允许替代扫描后,还要防止旧任务晚到并覆盖新结果。通用做法是为每轮扫描分配 generation token,只允许最新 generation 提交。当前正式版先解决了活性恢复;generation-aware commit 是允许恢复后重叠扫描时更完整的设计。

需要保留的故障测试

  1. 正常用量事件只计一次。
  2. 重复事件不抬高总量。
  3. 未写完的最后一行不导致崩溃。
  4. 异常行受内存边界约束。
  5. 未变化文件命中解析缓存。
  6. 卡死扫描超过阈值后不再阻止刷新。
  7. 按 provider 分门禁的平台不会互相冻结。
  8. 旧扫描不能覆盖较新的 generation。

限制测量结论

本地 token ledger 只是在用户机器上重建用量事件。它不能证明订阅实际扣费、节省金额或用户数量。Agent Island 把 API value 明确标为估算,并且不会把会话数据上传到 Agent Island 服务。

本文涉及的单行边界与卡死恢复已经包含在 Agent Island v1.7.1,公开源码可以用于核对每个平台的具体行为。