9.7 KiB
ADR-0168:Durable ModelInvocation 与唯一 StepRun Fence
- 状态:Accepted
- 日期:2026-07-26
- 关联:RFC D-12、D-13、D-156、D-157;ADR-0156、ADR-0167、ADR-0170
背景
Model Gateway 已经能执行受预算约束的远程调用,但进程内 audit 不能回答崩溃、
COMMIT 响应丢失、集群 promotion 或用户取消后的 durable outcome。另建一套
Invocation pending/running/succeeded 状态会与既有 Run/StepRun 产生双写;自动重试
结果未知的远程模型调用又可能重复计费,并产生不同输出。
同时,禁用 AI 的路由设备不应因为 workspace 中存在 AI 包就创建 AI 表、加载 adapter 或增加默认 Profile 产物。持久化能力需要复用现有 SQLite/PostgreSQL 部署边界,但 不能再拆一个只有 repository 的 package。
决策
1. ModelInvocation 是 model StepRun 的 receipt,不是第二状态机
每次 invocation 必须绑定一个 StepRun.kind=model:
- admission 原子提交
ready → running、Run version/event、RunEvent、 StepRunMutation 和 ModelInvocationStart; - completion 原子提交 usage/cost/byte receipt 与
running → succeeded|failed|timed_out|lost; succeeded的 StepRun 只保存outputRef=model-invocation:<id>,正文默认不进入 Start、Completion、RunEvent 或 mutation;- deadline 映射为
timed_out; - provider 调用后的 caller abort、consumer stream cancel 或其它结果未知映射为
lost/outcome_unknown,只允许人工裁决,不自动 replay。
Start/Completion/Command 使用独立 v1 schema 和 domain-separated SHA-256 digest, 单 record/command JSON 最大 24 KiB。完整绑定 Project、Run、StepRun、Trace、provider、 model、policy revision、request digest、deadline、输入/输出字节、token 与可选费用。
2. mutation identity 必须由摘要派生
requestId 最长 128 字符,不能用 requestId + ':started' 生成事件 ID。
createModelInvocationMutationIdentity 以 invocationId、phase 和独立 domain 派生完整
SHA-256,并产生均不超过 128 字符的 mutation/event/dedupe identity。Start 与
Completion normalizer 同时复验 identity、StepRun digest/version、Run event sequence
和 command digest;身份或内容复用漂移一律 conflict。
3. durable admission 不得被 Abort race 脱离
Policy 解析和 provider Promise 可以被 deadline 从调用方视角中止;durable admission 不可以。Gateway 必须等待 audit/start sink 自身的受限数据库操作结束:
- admission 未成功时 provider I/O 为零;
- deadline 在 admission 中到达且 start 最终提交时,随后写入
timed_outcompletion; - admission sink 失败时返回
MODEL_AUDIT_UNAVAILABLE; - terminal sink 失败时不把 provider 结果交给上层。
这会让一个超时的 admission 在 repository timeout 内继续占用一个有界并发槽,但避免 后台出现无法解释的 running StepRun。repository 必须有 statement/busy timeout,不能 用 Gateway Promise race 替代数据库取消和事务收敛。
4. migration、repository 与 coordinator 留在可选 AI 能力族
SQLite 和 PostgreSQL feature migration、原子 repository、durable coordinator 与
recovery 都进入既有 @qinglong/ai 的显式 subpath,不新增 workspace package。
双方言实现只依赖 node:sqlite 或 runtime-core 的 structural PostgreSQL
Pool/Client port,不导入 Drizzle、pg、local-sqlite 或 cluster-postgres,也不增加
optional peer。具体 Profile 只在启用 AI 时注入已有数据库 authority。
AI schema 使用独立 feature migration stream:
- AI 未启用时不运行 feature migration、不创建表;
- feature history 使用独立表、stream identity 和 checksum,绝不向双方言 main migration history 插入 feature row;
- PostgreSQL feature tables/history 位于独立
ql3_aischema,避免被核心ql3exact readiness 误判为未知表;SQLite readiness 保留精确 owned-table 校验并允许 同库显式 feature table; - 启用 composition 必须先显式 migrate/readiness,再构造 repository;
- PostgreSQL runtime role 对不可变 invocation 表只取得
SELECT/INSERT,不得为了FOR UPDATE扩大UPDATE/DELETE; - repository subpath 只取得数据库 authority,不取得 provider credential 或外部网络。
Gateway admission sink 返回 created|existing。只有 created 可开始 provider I/O;
existing 包括正常重放和 COMMIT 响应丢失后的耐久回查,必须返回
MODEL_INVOCATION_REPLAY_BLOCKED,禁止再次调用 provider。bounded recovery 只扫描
已过数据库 deadline 且没有 completion 的 start,并收敛为
lost/outcome_unknown,不自动重放模型。
5. unknown outcome 只能以不可变人工 Resolution 推进
lost/outcome_unknown 不再是永久死点,也不能被自动 retry。唯一出口是受信 User
提交一个 qinglong/model-invocation-resolution@v1 receipt,决策严格限定为:
retry:原子执行lost → ready,清除旧结果字段;后续ready → running必须 使用新的 invocationId,StepRun attemptCount 在新 start 时增加;fail:原子执行lost → failed,固定resultCode=model_outcome_rejected;cancel:原子执行lost → cancelled,固定resultCode=model_outcome_cancelled。
Resolution/Command 绑定原 Completion digest、Project/Run/StepRun/Trace、User ID、
StepRun version/digest、mutation/event/dedupe identity 与时间。repository 必须在同一
SQLite/PostgreSQL 事务内复验原 completion 仍是 outcome_unknown、当前 StepRun 仍
精确等于该 completion 的 lost winner,然后提交 StepRun/Run/RunEvent/
StepRunMutation/Resolution。每个 invocation 只能有一个 Resolution;相同命令 exact
replay,不同 User/decision conflict。
一个 StepRun 可以有多个 attempt,因此 Start/Completion 表不能再对
(run_id, step_run_id) 建唯一索引。它们改用包含时间和 invocationId 的普通历史索引,
invocation、mutation、RunEvent 和 digest 继续保持唯一。旧 Start/Completion/
Resolution 永不覆盖,新 attempt 形成新的 invocation 链。
被否决方案
- 把 adapter 放入双方言 storage package 并反向 optional peer AI:会污染默认 packlist, 且真实 Edge artifact 已证明即使 AI 不安装也会增加文件。
- 在 AI 包引入
pg/Drizzle:会扩大可选 Edge AI 的依赖和供应链闭包。 - 把 durable contract 放入 runtime-core root:禁用 AI 的基础 artifact 也会增长。
- 把 AI 表加入双方言 main migration:禁用功能仍产生 schema、备份和写放大。
- start audit 使用
Promise.race:调用返回后事务仍可能提交 running 状态。 - 对 unknown outcome 自动 retry:可能重复计费且输出非确定。
- 保存 Prompt/输出便于调试:默认扩大 Secret、个人数据和备份泄露面。
当前验证
@qinglong/ai默认门:50 pass、1 条 PostgreSQL 条件 skip;真库门另 1 pass;- 最大长度 requestId 的 mutation/event/dedupe identity 均不超过 128 字符;
- admission deadline 期间 provider 调用为零,start sink 不脱离,随后写入明确的 deadline failure;
- success、deadline 与 caller-abort 分别收敛为
succeeded、timed_out、lost/outcome_unknown; - SQLite 与 PostgreSQL feature history 使用独立表;主迁移与 AI 迁移交错重启通过;
- SQLite 原子 admission/completion、provider replay block 和 expired recovery 通过;
- SQLite Edge
DELETE/FULL与 StandaloneWAL/FULL共 14 个真实文件SIGKILL窗口通过:10 个 COMMIT 前窗口零部分事实并重放created,4 个 COMMIT 后窗口重启为 durable winner 并重放existing,全部integrity_check=ok; - PostgreSQL 18 真库以 migration/runtime 分角色验证原子事务、exact replay、rollback、
bounded recovery source,以及 invocation 表仅
SELECT/INSERT; - PostgreSQL ModelInvocation
COMMIT已提交但响应丢失时,provider 调用为 0, coordinator durable recheck 返回 existing,recovery 收敛为lost/outcome_unknown; - SQLite 与 PostgreSQL 都覆盖 User
retry/fail/cancel原子 Resolution;retry后同一 StepRun 可用新 invocationId 进入第二次 running,attemptCount 从 1 增至 2, 原 unknown completion 保持不变;不同 decision replay 被拒绝; - dependency audit
findings=[],AI 只有 runtime-core workspace dependency; - Edge 基线仍为 3,902,728 bytes/478 files/40 modules;edge-ai 为 4,212,508 bytes/508 files/41 modules,standalone-ai 为 4,212,580 bytes/508 files/41 modules;
- ADR-0170 已把非空 usage 从 Completion 原子投影为独立不可变 ledger;失败 outcome 也保留已知 usage,无 usage 不伪造零成本,exact replay 同时复验 Completion 与 ledger;
- PostgreSQL 18.4 arm64 完整 HA 门通过 physical streaming、timeline 1→2、fencing、
双 control replica、
pg_rewind与总passed=true;ql3_ai独立 history 和 Start/Completion/Resolution/UsageLedger 四表在 promotion 前后 9001/9002 checksum、表集与 runtime append-only ACL 完全一致,optionalAiFeatureSchemaSurvivesPromotion=true。
后续门禁
- credential binding audit 的双方言持久化,以及 binding digest 与 invocation 查询的关联;Project-bound SecretRef 和可清零 material lease 已由 ADR-0169 完成;
- ADR-0170 ledger 之上的原子 Project quota、计价 catalog/revision、不可变 rollup 与 retention coverage receipt;
- AI invocation 数据行级 promotion/partition fault,而不只 schema promotion;
- 正式产品 importer、配置 ceremony、read-only usage/Copilot/MCP authority;
- 在这些门禁完成前保持 HTTP/MCP/UI 产品 route 和费用配额关闭。