跳转到内容

ADR-CORE-003:本地 AI 任务历史的保留规则

状态:Accepted — 写端已落地,读端待定

日期:2026-09-08

决策范围:浏览器 IndexedDB taskEvidence 表、WorkbenchAgentTaskCoordinator 终态写入

上位路线:Aira 长期架构计划

一次 AI 任务的过程——跑了几轮、调了哪些原生操作、用掉多少 token 和多少钱、为什么结束——此前 只活在 snapshot.agentTask 的内存投影里,刷新即失。落地的任务还能从版本历史反推(Revision 带 actor.kind),而失败、取消、预算耗尽的任务不产生 Revision,因此不留任何痕迹:最该被回看的 那一类,恰好是留不下的那一类。

存储层早就为它建好了 taskEvidence 表、putTaskEvidence 与 listTaskEvidence,但全仓库零调用。 一旦接上,就必须同时回答两个问题,否则这张表只增不减:

  1. 一条记录里放什么;
  2. 记录留多久。

第 1 个问题不是风格问题。运行时 trace 的每一轮都逐字保存该轮计划发出的每一个 Exact 操作响应, 其中 model.read 携带整份 AMIR 文档、STEP 导出携带 base64 字节。原样保存等于把模型——已经由该 任务提交的 Revision 持有——再复制一份进每条记录,且没有上界。

记录里只保留:任务标识与终态、指令原文、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 美元的模型开销。现实中它是一兆出头。

  • 保留期以设备时钟为准。用户改系统时间会让删除提前或推迟;这个偏差不影响正确性,因为没有东西依赖 这些记录存在。
  • 规则作用于整张表,不按 branch 分配额度。当前一个 store 一个 branch,两者等价;将来若一个 store 承载多个 branch,需要重新评估。
  • 记录写入失败被吞掉。任务本身早已结算,一条诊断记录不能把一次成功的跑动报成失败。
  • 不申请 navigator.storage.persist()。 持久化配额只挡住磁盘压力驱逐一种失效,挡不住清缓存、 换浏览器、换机器、隐私窗口或企业策略。申请它的真实后果是让人更放心地把唯一副本留在浏览器里, 风险变大而不是变小。正确的方向是让“唯一副本”这件事不存在(导出副本),不是给不可靠的地方加一层 可靠的承诺。这条记录同样按“随时可能被整体驱逐”来设计,所以它必须便宜到丢了不心疼。
  • 读端。 记录已经在写,但界面还没有地方显示它。控制台的 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)