ADR-CORE-003:本地 AI 任务历史的保留规则
状态:Accepted — 写端已落地,读端待定
日期:2026-09-08
决策范围:浏览器 IndexedDB
taskEvidence表、WorkbenchAgentTaskCoordinator终态写入上位路线:Aira 长期架构计划
一次 AI 任务的过程——跑了几轮、调了哪些原生操作、用掉多少 token 和多少钱、为什么结束——此前
只活在 snapshot.agentTask 的内存投影里,刷新即失。落地的任务还能从版本历史反推(Revision 带
actor.kind),而失败、取消、预算耗尽的任务不产生 Revision,因此不留任何痕迹:最该被回看的
那一类,恰好是留不下的那一类。
存储层早就为它建好了 taskEvidence 表、putTaskEvidence 与 listTaskEvidence,但全仓库零调用。
一旦接上,就必须同时回答两个问题,否则这张表只增不减:
- 一条记录里放什么;
- 记录留多久。
第 1 个问题不是风格问题。运行时 trace 的每一轮都逐字保存该轮计划发出的每一个 Exact 操作响应,
其中 model.read 携带整份 AMIR 文档、STEP 导出携带 base64 字节。原样保存等于把模型——已经由该
任务提交的 Revision 持有——再复制一份进每条记录,且没有上界。
2.1 存投影,不存 trace
Section titled “2.1 存投影,不存 trace”记录里只保留:任务标识与终态、指令原文、provider 与模型、用量与费用、每轮的元数据(轮次、模型、 用量、计划自述、响应字符数)、操作名序列、错误、以及任务前后的 head。
所有大对象留在它们本来就在的地方,用 Revision 寻址:文档在 revisions,证据在 evidence,补丁在
patches,STEP 可从 Revision 重新导出。自由文本设上限(指令 2000 字符,摘要与错误 500 字符),
因为一次粘贴就能让“投影”这件事失效。
2.2 保留最近 500 条,且不超过 90 天
Section titled “2.2 保留最近 500 条,且不超过 90 天”两条规则都要满足,先触发的那条先咬。
- 90 天:这份数据的用途是诊断与对账——那次为什么失败、这个月花了多少。两个问题都不会问到去年 的某次跑动。
- 500 条:只有天数挡不住突发。一天跑数百次任务的人,90 天的规则对他等于不存在。而且条数才是 真正给这张表定界的量。
规则在每次写入时执行:写是这张表唯一的生长方式,因此不需要后台任务,也不需要开机清理。全表扫描 的代价被它自己执行的规则兜住——跑过一次之后表里永远不超过 500 条。
2.3 为什么这样一条粗暴的规则只在这张表上成立
Section titled “2.3 为什么这样一条粗暴的规则只在这张表上成立”没有任何东西指向这些行。 删掉一条只让读的人少看见一次跑动,模型一点不少。同样的做法放在
revisions、patches、evidence 上就是错的——那些是权威数据,删掉就无法重建。
规则可以这样一句话说完:能重建的可以丢,不能重建的必须留。
实测数字(WorkbenchTaskEvidence 投影,3 轮任务、每轮一次 48 KB 文档读):
| 大小 | |
|---|---|
| 原始 trace | 147,507 字节 |
| 投影后 | 2,270 字节(65×) |
| 无 trace 的失败记录 | 401 字节 |
| 单条最坏情况 | 54,728 字节(48 轮跑满,每个文本字段都到上限) |
记录的大小由轮数驱动,所以整表按 500 条计有三档:
| 每条 | 大小 | 整表 |
|---|---|---|
| 典型(2–3 轮) | 2.2 KB | 1.1 MB |
| 48 轮,文本正常长度 | 25.7 KB | 12.2 MB |
| 48 轮,每个文本字段到上限 | 54.7 KB | 26 MB |
上界不只由规则给,也由钱给:轮数要花钱,而预算的绑定约束是 token 上限而非名义的
maxEstimatedCostUsd: 5——1,000,000 × $0.44/M + 384,000 × $1.32/M ≈ **0.95 美元**一次
(DEFAULT_NATIVE_AGENT_BUDGET 的注释同样以 0.95 为界)。所以要让这张表逼近最坏那一档,需要约
473 美元的模型开销。现实中它是一兆出头。
4. 后果与边界
Section titled “4. 后果与边界”- 保留期以设备时钟为准。用户改系统时间会让删除提前或推迟;这个偏差不影响正确性,因为没有东西依赖 这些记录存在。
- 规则作用于整张表,不按 branch 分配额度。当前一个 store 一个 branch,两者等价;将来若一个 store 承载多个 branch,需要重新评估。
- 记录写入失败被吞掉。任务本身早已结算,一条诊断记录不能把一次成功的跑动报成失败。
- 不申请
navigator.storage.persist()。 持久化配额只挡住磁盘压力驱逐一种失效,挡不住清缓存、 换浏览器、换机器、隐私窗口或企业策略。申请它的真实后果是让人更放心地把唯一副本留在浏览器里, 风险变大而不是变小。正确的方向是让“唯一副本”这件事不存在(导出副本),不是给不可靠的地方加一层 可靠的承诺。这条记录同样按“随时可能被整体驱逐”来设计,所以它必须便宜到丢了不心疼。
5. 尚未决定
Section titled “5. 尚未决定”- 读端。 记录已经在写,但界面还没有地方显示它。控制台的 live 段是当前事务的横切面(按 pipeline stage 排序而非时间),ledger 段是 Revision 时间线,两者都以“事务/版本”为骨架,而任务历史不是; 它需要自己的位置,形状单独定。
artifacts与publications两张表已删除,不需要规则。 二者都是建好未接的表:putArtifactCache/getArtifactCache/clearArtifactCache与adoptRevision/listPublicationReceipts全仓库零调用,运行时恒为空。没有任何网格缓冲曾被持久化——渲染包只存在于内存快照里。将来若真要 给渲染缓存定规则,它的形状会和这一条不同:历史是记录,按时间留;缓存是可重建的,应当按容量 留(LRU),因为缓存的价值是命中率,而时间对命中率没有意义——一年前的 Revision 今天打开,缓存 本就该命中。- 真正在无界增长的是每次提交都写的那几张表:
revisions(整份 AMIR 文档)、patches,以及导入 写入的resources(STEP/字体字节)。它们恰好是本 ADR 的规则不适用的那一类——不能重建, 删掉就回不去。给它们定界是模型历史压缩的问题,不是保留期的问题,需要单独决策。
| 关注点 | 位置 |
|---|---|
| 规则常量与说明、投影 | apps/web/src/workbench/WorkbenchTaskEvidence.ts |
| 终态写入(成功与失败两条路) | apps/web/src/workbench/WorkbenchAgentTaskCoordinator.ts |
| 会话写入与清理调用 | apps/web/src/operations/phase1-exact-operation-session-v0.4.ts |
| 清理机制 | apps/web/src/storage/aira-repositories.ts (pruneTasks) |