feat(ql3): establish 3.0 incubation baseline

This commit is contained in:
whyour
2026-08-12 00:25:26 +08:00
parent 4bf92dcfeb
commit c699c32461
2817 changed files with 779642 additions and 653 deletions
@@ -0,0 +1,165 @@
# ADR-0168Durable ModelInvocation 与唯一 StepRun Fence
- 状态:Accepted
- 日期:2026-07-26
- 关联:RFC D-12、D-13、D-156、D-157ADR-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_out` completion
- 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_ai` schema,避免被核心 `ql3`
exact 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` 必须
使用新的 invocationIdStepRun 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 链。
## 被否决方案
1. 把 adapter 放入双方言 storage package 并反向 optional peer AI:会污染默认 packlist
且真实 Edge artifact 已证明即使 AI 不安装也会增加文件。
2. 在 AI 包引入 `pg`/Drizzle:会扩大可选 Edge AI 的依赖和供应链闭包。
3. 把 durable contract 放入 runtime-core root:禁用 AI 的基础 artifact 也会增长。
4. 把 AI 表加入双方言 main migration:禁用功能仍产生 schema、备份和写放大。
5. start audit 使用 `Promise.race`:调用返回后事务仍可能提交 running 状态。
6. 对 unknown outcome 自动 retry:可能重复计费且输出非确定。
7. 保存 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` 与 Standalone `WAL/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 返回 existingrecovery 收敛为
`lost/outcome_unknown`
- SQLite 与 PostgreSQL 都覆盖 User `retry/fail/cancel` 原子 Resolution`retry`
后同一 StepRun 可用新 invocationId 进入第二次 runningattemptCount 从 1 增至 2
原 unknown completion 保持不变;不同 decision replay 被拒绝;
- dependency audit `findings=[]`AI 只有 runtime-core workspace dependency
- Edge 基线仍为 3,902,728 bytes/478 files/40 modulesedge-ai 为
4,212,508 bytes/508 files/41 modulesstandalone-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`
## 后续门禁
1. credential binding audit 的双方言持久化,以及 binding digest 与 invocation
查询的关联;Project-bound SecretRef 和可清零 material lease 已由 ADR-0169
完成;
2. ADR-0170 ledger 之上的原子 Project quota、计价 catalog/revision、不可变 rollup
与 retention coverage receipt
3. AI invocation 数据行级 promotion/partition fault,而不只 schema promotion
4. 正式产品 importer、配置 ceremony、read-only usage/Copilot/MCP authority
5. 在这些门禁完成前保持 HTTP/MCP/UI 产品 route 和费用配额关闭。