Detailed design: close five R-series regressions from Opus 4.7 audit

Independent regression audit (Opus 4.7) verified P1×4 + P2×9 fixes were
closed but found that three of the P2 fixes had introduced new baseline
violations and two had minor errors. This commit closes all five.

R-series fixes (in system-detailed-design.md):

- R1-01 §10.2: L2 Safety source no longer names a fictional
  PermissionEngine.current_profile() method. The L2 row now describes
  the active permission profile sources (~/.air/permissions.yaml +
  project permission config) without inventing a contract method,
  honoring DD §0 "no new public contracts."

- R1-02 §5.4 Table A: agent.started projection no longer claims a
  two-step "starting → running" update within a single event commit
  (which would violate event atomicity). The row now matches
  event-registry §3: a single status (starting or running) at emission
  time, with the follow-up transition handled by WorkerManager per the
  state machine in §20.4.

- R1-03 §18.4 + §5.4 Table B: removed the non-baseline phase: intent |
  committed payload-field extension from memory.promoted and
  debug.record.created. Outbox semantics now follow the baseline model:
  owning store performs the external write first, then ingests a single
  durable completion event whose payload matches event-registry §3
  exactly. Any future intent/commit split must go through an ADR plus
  payload version bump.

- R2-01 §7.5: corrected workspace responsibility cross-reference from
  "overview §10.5" (Direct mode) to "overview §10.3" (Scheduler state
  machine, which actually covers workspace assignment and merge).

- R2-02 §3: resolved self-contradiction in the contracts file-set
  decision. The 16 files are now stated as mandatory with default
  inlining of all overview §4 symbol groups for V1.0.0 Alpha; any
  future split is explicitly out of scope until an ADR is added.

Also adds opus4.7详细设计与UML审查.md documenting the regression
findings, baseline-evidence trails, and recommended fixes that drove
these changes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
AirCoding
2026-06-01 10:06:44 +08:00
parent 543743bbc1
commit af99aed1f4
2 changed files with 452 additions and 57 deletions

View File

@@ -0,0 +1,369 @@
# Opus 4.7 系统详细设计与 UML 类图回归审查
Date: 2026-06-01
Reviewer: Claude Opus 4.7 (independent regression audit)
Scope: 修复后回归审查 — 验证 P1×4 + P2×9 修复是否真正闭合,且未在修复过程中引入新偏差
Inputs:
- 详细设计:`system-detailed-design.md`2264 行23 章节commit `543743b`
- 受冻结基线22 份基线 + 已冻结概要设计
- 此前审查DeepSeek / MIMO 2.5 Pro / GPT-5.5 Pro / Opus 4.8 四轮审查 + 汇总
---
## 1. 审查方法
本次为**回归审查**,不重复覆盖性扫描,而是:
1. **P1×4 修复验证**:检查每项修复是否真正落地、文字是否清晰、是否与基线一致
2. **P2×9 修复验证**:同上,并特别检查"修复过程中是否引入新的偏离基线条款"
3. **跨文档一致性**DD 修复后是否仍与 contracts、event-registry、runtime-semantics、scope-escalation 等基线一致
4. **新发现**:审查过程中浮出的、之前轮次未发现的新问题
---
## 2. 总览结论
| 维度 | 状态 |
|---|---|
| P1×4 修复闭合性 | ✅ 4/4 已闭合 |
| P2×9 修复闭合性 | ⚠ 9/9 文字已落地,但 3 项引入新基线偏离 |
| 新增 P0 | 0 |
| 新增 P1修复引入的回归 | **3** |
| 新增 P2 | 2 |
| 覆盖性 | ✅ 维持 100% |
| 架构合理性 | ✅ 维持,无循环依赖 |
| 综合判定 | **CONDITIONAL PASS次轮** — 需收束 3 项 P1 回归 |
---
## 3. P1×4 修复验证
### 3.1 P1-01: Worker exit code 语义冲突 — **✅ 已闭合**
| 检查项 | 结果 |
|---|---|
| DD §8.1 与 `baselineV1.md §8` 对齐 | ✓ 完全一致6 个 code 含义逐字对应)|
| overview §11 与 baselineV1 对齐 | ✓commit `8d9c420`/`453df09` 中验证)|
| 任务结果与 exit code 解耦说明 | ✓ DD §8.1 末尾"Design decision"明确:任务 status 走 `WorkerResult.status`exit code 仅表达进程协议层语义 |
| 表格格式与 baselineV1 一致 | ✓ |
**结论**:完全闭合。无残留歧义。
### 3.2 P1-02: PromptLayerLevel 枚举与 L0-L9 不对齐 — **✅ 已闭合**
| 检查项 | 结果 |
|---|---|
| `interface-contracts-v1.md §16` 增加 `"safety"` 枚举值 | ✓ |
| 11 个枚举值(含 `system_debug`)映射到 L0-L9 + 系统调试 | ✓ |
| DD §10.2 表 1枚举 → L# → loader正确显示 11 行 | ✓ |
| `system_debug` 注释"applied within L9 when present" | ✓ 与 prompt-layering-v1 §2 注脚对齐 |
**结论**完全闭合。L0-L9 全部有显式枚举对应。
### 3.3 P1-03: EventStore.project() 错误处理未定义 — **✅ 已闭合**
| 检查项 | 结果 |
|---|---|
| DD §5.3 包含"Error handling for `project()`"块 | ✓ |
| 异常 → 事务回滚 → `EventBus.publish()` 不触发 | ✓ |
| 返回 `AirError{kind: "system_error"}` | ✓ |
| FK-off 不一致触发 `SessionStore.referential_check()` | ✓ |
**结论**:完全闭合,并与 §4.3、§18.3 形成闭环。
### 3.4 P1-04: PromptLayerLoader 接口不完整 — **✅ 已闭合**
| 检查项 | 结果 |
|---|---|
| DD §10.2 第二张表L2/L4/L6/L7/L8/L9 → ContextAssembler 内部装配)| ✓ |
| 显式说明 PromptLayerLoader 只覆盖 L0/L1/L3/L5 | ✓ |
| Runtime rolesmain/architecture/scheduler不走 `load_role()` 的设计决策 | ✓ |
| 与 §2"Worker AgentType vs. runtime roles"块互为印证 | ✓ |
**⚠ 次级发现**L2 Safety 来源写为 `PermissionEngine.current_profile()`,但 `interface-contracts-v1.md §13``PermissionEngine` 接口**只声明 `evaluate()``record()` 两个方法**。`current_profile()` 是基线之外的方法引用 → 见 §5.3 新发现 R1-01。
---
## 4. P2×9 修复验证
| ID | 文字落地 | 内容质量 | 新偏离基线 |
|---|---|---|---|
| P2-01 Architecture gate 序列图 | ✓ §19.4 | ✓ 完整 | 无 |
| P2-02 CLI catalog 命令类归属 | ✓ §17 | ✓ 11 个 Command 类 | 无 |
| P2-03 agent.started 投影措辞 | ✓ §5.4 表 A | ⚠ | **是** → R1-02 |
| P2-04 WorkspaceManager 职责矩阵 | ✓ §7.5 | ✓ | ⚠ 引用错误 → R2-01 |
| P2-05 §5.4 拆分两张表 | ✓ §5.4 | ✓ Table A/B 分离清晰 | 无 |
| P2-06 memory.promoted 两相 | ✓ §18.4 | ⚠ | **是** → R1-03 |
| P2-07 docs task 闭合 | ✓ §8.3 | ✓ | 无 |
| P2-08 AgentType vs runtime roles | ✓ §2 | ✓ | 无 |
| P2-09 contracts 文件集冻结 | ✓ §3 | ✓ | ⚠ → R2-02 |
整体:**9/9 文字落地,但 3 项P2-03 / P2-04 / P2-06 / P2-09在修复过程中引入新偏离**。
---
## 5. 新发现(回归引入)
### 5.1 [R1-01] P1-04 副作用:`PermissionEngine.current_profile()` 不在基线 — **P1**
**位置**DD §10.2 "Design decision — Layer loading responsibility" 表L2 Safety 来源
**问题**
```text
L2 Safety → PermissionEngine.current_profile() + ~/.air/permissions.yaml + project permissions
```
`interface-contracts-v1.md §13``PermissionEngine` 接口仅声明:
```ts
export interface PermissionEngine {
evaluate(context: PermissionRequestContext): Promise<PermissionDecision>
record(decision: PermissionDecision, context: PermissionRequestContext): Promise<PermissionRecordResult>
}
```
`current_profile()` 是详细设计层引入的方法名,没有对应的契约方法。这违反了 DD §0 "does not introduce new public contracts" 的约束。
**根因**P1-04 修复时为给 L2 Safety 找一个明确来源,引用了一个并不存在的方法名。
**建议处理**
- 选项 A把来源改为"`PermissionEngine` 内部状态(实现细节)+ `~/.air/permissions.yaml` 文件 + 项目权限配置",避免点名一个不存在的方法
- 选项 B`PermissionEngine` 契约中加 `current_profile()` 方法 → 需要 ADR 并修改 contracts §13违反"基线冻结"约束)
- **推荐**:选项 A
**预计工作量**5 分钟(文字调整)
---
### 5.2 [R1-02] P2-03 副作用:`agent.started` 引入"同事件内分阶段更新"概念,超出基线 — **P1**
**位置**DD §5.4 Table A 中 `agent.started`
**当前文字**
```text
insert `agents` row with `status='starting'` upon spawn intent, then update to
`status='running'` on the same event's commit when WorkerProcess handshake has
succeeded (`workers` ack); domain row carries final state at commit time
```
**问题**:这段话描述了"同一个事件提交过程中先 starting 再 running"的语义,事件投影在一次事务内对同一行做 insert+update。
`event-registry-v1.md §3``agent.started` 的定义是:
```text
Domain update: insert `agents` row with `status = running` or `starting`.
```
即基线明确"插入时 row 的 `status` 等于 `running``starting` 其一"**不是两步**。状态从 `starting``running` 的过渡由独立事件 / 后续状态机决定(参见 DD §20.4 中 `starting ──ready──▶ running`,但 baseline 并未对应 "ready" 持久事件)。
把握手成功"在同一事件提交时"反映到 row 上,事实上无法实现 — 因为 `agent.started` 事件已经在 `WorkerManager.spawn` 完成 handshake **之前或之后**就已经被持久化。
**根因**P2-03 旧文字"insert agents (starting/running)"措辞被认为"含糊",修复时反而引入了一个**违反事件原子性**的描述。
**建议处理**:恢复为符合基线的清晰版本:
```text
| `agent.started` | insert `agents` row with `status = 'starting'` (initial spawn)
or `status = 'running'` (if handshake completed before the event is emitted);
final transition `starting → running` is recorded via the implicit state
machine in §20.4 without a dedicated durable event |
```
**预计工作量**5 分钟
---
### 5.3 [R1-03] P2-06 副作用:`memory.promoted` 引入 `phase` 字段,扩展基线 payload — **P1**
**位置**DD §18.4 "memory.promoted two-phase semantics" 表
**当前文字**
```text
| Phase | Payload marker |
| Intent | phase: "intent", no artifact_ref/memory_id yet |
| Completion | phase: "committed", includes artifact_ref / memory_id |
```
**问题**`event-registry-v1.md §3``MemoryPromotedPayload` 定义为:
```ts
interface MemoryPromotedPayload {
candidate_id: string
target_ref: string
promoted_by: "user" | "curator" | "system"
summary: string
}
```
**没有 `phase` 字段**。P2-06 修复引入的"intent vs committed"两相是 DD 层对 outbox 模型的额外扩展,事实上在基线中是通过**单一 `memory.promoted` 事件 + 上下游推断**完成的runtime-semantics §6.4 直接说"session event records completed promotion and target ref",即单事件即为完成态)。
此外DD §18.4 也对 `debug.record.created` 同样应用了 `phase` 字段("the same two-phase pattern (intent → committed) with payload field `phase`"),同样不在 `event-registry-v1.md``DebugRecordCreatedPayload`中。
**根因**GPT-5.5 Pro 的原 P2-06 关注点是"outbox intent/completion 阶段不清"。修复时为了在 DD 层明确区分,新增了 `phase` 字段。但**契约层从未声明这个字段**,且 baseline `runtime-semantics §6.4` 实际上认为 `memory.promoted` 是单事件、完成态语义。
**建议处理**:去掉 `phase` 字段引入,改用"两个事件"或"前置 candidate + 单完成事件"叙述:
```text
**`memory.promoted` outbox flow**(不引入新 payload 字段):
1. `memory.candidate.created` 已持久化(已存在事件)
2. 决定 promote 后,业务侧执行外部 store 写入
3. 写入成功后发出 `memory.promoted` —— 这就是基线说的"completed promotion"
4. 写入失败:发出 `memory.archived` 或回退到一个新的 candidate运行时不发未完成的 `memory.promoted`
如果未来需要严格的 outbox intent应通过 ADR 引入新事件类型,而非给现有事件增加 payload 字段。
```
`debug.record.created`:基线 runtime-semantics §6.3 说"intent/completion event",但实际 payload 是单一 schema建议同样不引入 `phase`,而是依赖 `debug.record.created` + `debug.record.failed`(如果未来加入)形成 outbox。
**预计工作量**15 分钟
---
### 5.4 [R2-01] P2-04 副作用DD §7.5 引用 "overview §10.5" 不正确 — **P2**
**位置**DD §7.5 "Responsibility split — Scheduler vs. WorkspaceManager"
**当前文字**`**Responsibility split — Scheduler vs. WorkspaceManager** (overview §10.5, scheduler-state-machine §4/§MERGING)`
**问题**`system-overview-design.md §10.5` 是 "Direct mode",不包含 workspace 职责描述。workspaces 描述实际位于 overview §10.3 (Scheduler state machine, 含 "workspace assignment", "workspace merge and conflict handling")。
**根因**P2-04 修复时引用错章节号。
**建议处理**:把 `(overview §10.5, scheduler-state-machine §4/§MERGING)` 改为 `(overview §10.3, scheduler-state-machine §4/§MERGING)`
**预计工作量**1 分钟
---
### 5.5 [R2-02] P2-09 副作用file-set 决策中存在 self-contradicting 描述 — **P2**
**位置**DD §3 "Frozen file-set decision (P2-09)" 块
**当前文字**
```text
For V1.0.0 Alpha, the canonical contracts package file set is exactly the 16 files listed above
```
**问题**
1. 上方 ASCII 列表中实际上是 **15 个文件**ids/error/event/runtime/ipc/task/worker-result/tool/permission/artifact/evidence/project/provider/ui/capability/platform = 16让我数一遍ids, error, event, runtime, ipc, task, worker-result, tool, permission, artifact, evidence, project, provider, ui, capability, platform = 16数对了。
2. 但紧接着的 file-mapping 表说"context.ts symbols → runtime.ts + dedicated context.ts only if needed (otherwise inlined in runtime.ts)"以及"dedicated doctor.ts only if file size warrants split"等条目,**实际允许额外文件**。这与"exactly the 16 files listed above"自相矛盾。
3. "no new `.ts` files are added in the contracts package without an ADR" 是好的硬约束,但表中的"only if needed"放宽了它。
**根因**P2-09 修复想同时表达"主体冻结" + "极少数情况可拆分",但语言模糊。
**建议处理**:明确两层:
- 冻结16 个**必有**文件
- 可选:`context.ts` / `doctor.ts` / `knowledge.ts` / `diagnostics.ts` 在出现明确大小/分层需求时**可以追加**,追加同时需要更新 code-view §3 + ADR
或更严格:彻底冻结到 16 个,不预留追加口子。
**预计工作量**5 分钟
---
## 6. 重新验证未受影响的领域
| 维度 | 状态 |
|---|---|
| 22 份基线契约覆盖 | ✅ 100% |
| 55 个 durable + 7 个 ephemeral 事件覆盖 | ✅ 100% |
| 19 张 DB 表的 Repository 覆盖 | ✅ 100% |
| 6 个状态机覆盖 | ✅ 100%main agent, scheduler, task, agent, workspace, capability|
| 10 条 forbidden edge | ✅ §2 显式继承,禁止路径无破口 |
| 8 个 UML 类图 | ✅ 全部存在22.1-22.8|
| 5 个序列图(含 P2-01 新增 gate| ✅ 5/5 |
| 5 项 traceability 矩阵 | ✅ 5/5 |
| 依赖方向 | ✅ 无循环 |
| 事务语义 | ✅ project + publish 时序正确 |
---
## 7. UML 类图回归审查
**8 个 Mermaid 类图**全部存在且语法可解析,无新偏差。需要补充指出:
- **§22.1 EventSource.kind**:枚举 `"main"|"architecture_designer"|"scheduler"|"agent"|"tool"|"system"` 现与 §2 "runtime role" 表协同P2-08 修复正确指向 `event.ts`)。✓
- **§22.3 Scheduler vs WorkspaceManager 关系**:图中 `Scheduler --> WorkspaceManager` 已暗示策略/机制层级,但**未引入 P2-04 矩阵中的"Scheduler 拥有 conflict resolution policy"语义**。建议是否在图旁加注 "Scheduler owns policy; WorkspaceManager owns mechanism"。
- **§22.5 WorkerRole 继承**5 个 Role 类全部出现P2-07 引入的 `docs` TaskType 通过复用 `ExecutorRole`,类图无需新增节点 — ✓ 与文字一致。
- **§22.7 ContextAssembler vs PromptLayerLoader**:图中 `ContextAssembler --> PromptLayerLoader` 是单向依赖,符合 P1-04 / P2-08 决策。建议补充 `ContextAssembler --> PermissionEngine`(用于 L2 Safety 加载)— 但若采纳 R1-01 选项 A此依赖也应消失。
---
## 8. 门禁判定
| 条件 | 状态 |
|---|---|
| P0 = 0 | **PASS** |
| 契约覆盖 100% | **PASS** |
| 事件覆盖 100% | **PASS** |
| DB Schema 覆盖 100% | **PASS** |
| 状态机覆盖 100% | **PASS** |
| 禁止路径全执行 | **PASS** |
| 架构无循环依赖 | **PASS** |
| 职责分离无泄漏 | **PASS** |
| 此前 4 处 P1 已闭合 | **PASS** |
| 此前 9 处 P2 文字已落地 | **PASS** |
| 修复未引入新偏离基线 | **FAIL**3 处 P1 回归 + 2 处 P2 回归) |
**门禁结果CONDITIONAL PASS次轮**
---
## 9. 修复优先级建议
| 优先级 | Finding | 建议处理 | 工作量 |
|---|---|---|---|
| **P1** | R1-01 `PermissionEngine.current_profile()` 不在契约 | 改写 L2 Safety 来源描述 | 5 min |
| **P1** | R1-02 `agent.started` 同事件内 starting→running 违反事件原子性 | 恢复为基线允许的"starting OR running" | 5 min |
| **P1** | R1-03 `memory.promoted` 引入未冻结 `phase` payload 字段 | 移除 phase 字段,回归基线 outbox 单事件叙述 | 15 min |
| **P2** | R2-01 §7.5 "overview §10.5" 引用错误 | 改为 §10.3 | 1 min |
| **P2** | R2-02 §3 file-set 表内 self-contradicting | 明确"16 必有 + 可选追加(需 ADR"或彻底冻结 | 5 min |
**总预计工作量30 分钟**
---
## 10. 多模型审查累积视角
| 轮次 | 模型 | 主要贡献 | 累积 P0 | 累积 P1 | 累积 P2 |
|---|---|---|---|---|---|
| R1 | DeepSeek | 全量覆盖性 | 0 | 0 | 3 |
| R2 | MIMO 2.5 Pro | 内在一致性 | 0 | 2 | 5 |
| R3 | GPT-5.5 Pro | 基线冲突检测 | 0 | 2 (新) | 4 (新) |
| R4 | Opus 4.8 | 验证 + 根因 + 建议 | 0 | 2 (确认) | 5 (确认) |
| R5 | Opus 4.7(本次回归) | 修复闭合性 + 回归引入检测 | 0 | **3 (新回归)** | **2 (新回归)** |
**累积说明**
- R1R4 发现的 P1×4 + P2×9 已全部文字闭合commit `453df09` + `543743b`
- R5 发现修复过程引入新偏离基线的 3 P1 + 2 P2需要再迭代一次
---
## 11. 最终结论
### 11.1 修复有效性
- **P1×4 修复**4/4 全部文字闭合,质量良好。其中 P1-04 副作用引入 R1-01。
- **P2×9 修复**9/9 全部文字闭合,但 P2-03 / P2-06 / P2-09 引入新偏差P2-04 含小引用错误。
### 11.2 详细设计当前状态
- **从覆盖性 / 架构合理性 / 状态机一致性看**:详细设计仍然 PASS
- **从契约/事件 payload 不变性看**:当前版本存在 **3 处契约/payload 越界引用**R1-01 ~ R1-03
### 11.3 建议
**短迭代修复30 分钟以内)** 收束 3 P1 + 2 P2然后可以正式冻结详细设计、进入实现阶段。
不建议在不解决 R1-01 ~ R1-03 的情况下开始实现:
- R1-01 会让 ContextAssembler 实现时找不到 `current_profile()` 方法
- R1-02 会让 EventStore 实现者误以为 `agent.started` 投影需要做两次 status 更新
- R1-03 会让消息处理代码尝试读不存在的 `event.payload.phase` 字段
### 11.4 最终判定
**CONDITIONAL PASS (R5)** — 详细设计相比 R4 后版本仍然满足覆盖性与结构正确性,但修复过程引入了 3 处契约 / payload 越界条款,需要在冻结/实现前 30 分钟内修正。
---
End of regression audit.

View File

@@ -156,24 +156,28 @@ platform.ts → cross-platform tier enums referenced by Doctor (from cross-p
above; the binding requirement is that the exported symbol set equals the contract set. This design
keeps the code-view §3 list as canonical and treats overview §4 as the superset note.
**Frozen file-set decision (P2-09)**: For V1.0.0 Alpha, the canonical contracts package file set is
exactly the 16 files listed above (frozen by code-view §3). Overview §4 symbol groups that do not
have a matching dedicated file are merged into the existing files per the mapping below; no new
`.ts` files are added in the contracts package without an ADR.
**Frozen file-set decision (P2-09)**: For V1.0.0 Alpha, the canonical contracts package has exactly
**16 mandatory files** (frozen by code-view §3); see the list above. Overview §4 symbol groups that
do not have a matching dedicated file are merged into one of these 16 files per the mapping below.
The default for V1.0.0 Alpha is: **all overview §4 symbol groups are inlined into the 16
mandatory files** (i.e. the "default home" column).
| Overview §4 symbol group | Canonical file (code-view §3) |
| Overview §4 symbol group | Default home (mandatory, no ADR needed) |
|---|---|
| `storage.ts` symbols (TransactionManager, Repository facades) | merged into `task.ts` (TransactionManager) and per-domain files |
| `scheduler.ts` symbols (SchedulerWavePlan, SchedulerRunResult) | `task.ts` |
| `workers.ts` symbols (WorkerRole, WorkerRuntime, IPC payloads) | `ipc.ts` + `worker-result.ts` |
| `context.ts` symbols (PromptLayer, PromptLayerLoader, ContextAssembler) | `runtime.ts` (`ContextPack`) + dedicated `context.ts` only if needed (otherwise inlined in `runtime.ts`) |
| `context.ts` symbols (PromptLayer, PromptLayerLoader, ContextAssembler) | `runtime.ts` (`ContextPack` and prompt-layer types are co-located) |
| `projection.ts` symbols | `ui.ts` |
| `doctor.ts` symbols (DoctorService, DoctorRunInput/Output) | `platform.ts` (cross-platform tier types) + dedicated `doctor.ts` only if file size warrants split |
| `knowledge.ts` symbols (DebugKnowledgeStore, LearnedMemoryStore) | inlined into `artifact.ts` / dedicated file only if needed |
| `diagnostics.ts` symbols (Diagnostic, semantic_signature types) | inlined into `tool.ts` / dedicated file only if needed |
| `doctor.ts` symbols (DoctorService, DoctorRunInput/Output) | `platform.ts` (cross-platform tier + doctor types co-located) |
| `knowledge.ts` symbols (DebugKnowledgeStore, LearnedMemoryStore) | `artifact.ts` |
| `diagnostics.ts` symbols (Diagnostic, semantic_signature types) | `tool.ts` |
The barrel `index.ts` exports the full union. Any future split into additional files requires an ADR
under `docs/architecture/adr/` and a synchronized update to code-view §3.
The barrel `index.ts` exports the full union. **No new `.ts` files** are added to the contracts
package for V1.0.0 Alpha. Any future split (for example extracting a dedicated `context.ts`,
`doctor.ts`, `knowledge.ts`, or `diagnostics.ts`) is **out of scope for V1.0.0 Alpha** and requires
an ADR under `docs/architecture/adr/` plus a synchronized update to code-view §3 before it may be
introduced in a later version.
## 4. Storage and Repositories
@@ -376,7 +380,7 @@ post-commit outbox/compensation work explicit, the map is split into two tables.
| `assistant.message.started` | upsert `message_drafts` (status=streaming) |
| `assistant.message.created` | insert `messages` + delete matching `message_drafts` |
| `assistant.message.failed` | `message_drafts.status=error` or failure artifact ref |
| `agent.started` | insert `agents` row with `status='starting'` upon spawn intent, then update to `status='running'` on the same event's commit when WorkerProcess handshake has succeeded (`workers` ack); domain row carries final state at commit time |
| `agent.started` | insert `agents` row with `status = 'starting'` when emitted at spawn (handshake not yet acknowledged) or `status = 'running'` when emitted after the WorkerProcess handshake has completed; the event carries exactly one terminal-of-emission status. The follow-up `starting → running` transition (without a dedicated durable event) is recorded by `WorkerManager` updating the row directly per the state machine in §20.4 |
| `agent.completed/failed/lost/cancelled` | update `agents.status` |
| `task.created` | insert `tasks` (+ optional `task_dependencies`) |
| `task.started` | `tasks.status=running`, set started/agent/workspace; insert `task_attempts` |
@@ -414,28 +418,29 @@ post-commit outbox/compensation work explicit, the map is split into two tables.
| `memory.candidate.created` | append |
| `memory.archived` | append (mark memory inactive in session-side mirror if any) |
**Table B — Projection + post-commit outbox/compensation (cross-DB or external write):**
**Table B — Projection paired with prior owner-side external write (cross-DB):**
These events project a session-side intent row inside the same transaction, then drive a
follow-up external write via the outbox model (§18.4, runtime-semantics §6.3-§6.4). The
external write is performed by the *owning service* after EventStore.commit, never inside
`project(event, tx)`.
For these events the **external write happens first** in the owning store; the durable
session event is then ingested by the owning service to record the completed cross-store
transition. `project(event, tx)` only writes session-DB rows; the cross-DB pair is
eventually consistent (§18.4, runtime-semantics §6.3-§6.4).
| Event type | Projection step (in transaction) | Outbox step (post-commit) | Owner |
| Event type | Projection step (in session-DB transaction) | External write (already done by owner before event is ingested) | Owner |
|---|---|---|---|
| `memory.promoted` | append durable event recording promotion intent | write `rules/`, `skills/`, or `learned-memory.db` row | `LearnedMemoryStore` / `RuleStore` |
| `memory.archived` (when external mirror exists) | append durable event | mark external memory inactive | `LearnedMemoryStore` |
| `debug.record.created` | append durable session event | insert/update row in `debug-records.db` | `DebugKnowledgeStore` |
| `memory.promoted` | append durable event (event log row); domain side has no dedicated table — `MemoryRepository` is project-DB only | write `rules/`, `skills/`, or `learned-memory.db` row | `LearnedMemoryStore` / rules subsystem |
| `memory.archived` | append durable event | mark memory inactive in external store | `LearnedMemoryStore` |
| `debug.record.created` | append durable event | insert/update row in `debug-records.db` | `DebugKnowledgeStore` |
Rules:
- `project(event, tx)` itself never opens external DBs or files; it only writes
`events_session.db` rows (events, drafts, domain projections).
- After successful commit, `EventBus.publish(event)` fires; outbox-aware subscribers
(`LearnedMemoryStore`, `DebugKnowledgeStore`, ...) then perform the external write and
emit a completion event (`memory.promoted` completion artifact, `debug.record.created`
status update) per §18.4.
- On restart, recovery scans pending outbox intents in the session DB and retries
external writes (runtime-semantics §6.4).
- `project(event, tx)` never opens external DBs or files; it only writes
`events_session.db` rows (events, drafts, session domain projections).
- The owning service performs its external write **before** ingesting the durable event.
This makes the session-DB event the "we observed the external write succeeded" record.
- If the external write fails, the owning service does **not** ingest the success event.
See §18.4 for failure modes (`task.failed` carrying `AirError`, candidate re-queue).
- On restart, recovery cross-checks for upstream session rows (e.g. `memory.candidate.created`)
without a matching downstream event (`memory.promoted`) and re-queues work
(runtime-semantics §6.4).
### 5.5 EventBus
@@ -598,7 +603,7 @@ Strategies (db-schema §16, scheduler-state-machine §MERGING): `main` (no merge
GC retention follows overview §15 (active until merge/cancel; merged 7d; abandoned 3d; cleaned keeps
DB row).
**Responsibility split — Scheduler vs. WorkspaceManager** (overview §10.5, scheduler-state-machine §4/§MERGING):
**Responsibility split — Scheduler vs. WorkspaceManager** (overview §10.3, scheduler-state-machine §4/§MERGING):
| Responsibility | Owner |
|---|---|
@@ -922,7 +927,7 @@ configuration or resource loading (L0, L1, L3, L5). The remaining layers are ass
| Layer | Source | Assembled by |
|---|---|---|
| L2 Safety | `PermissionEngine.current_profile()` + `~/.air/permissions.yaml` + project permissions | `ContextAssembler` |
| L2 Safety | Active permission profile (`~/.air/permissions.yaml` + project permission config; same source the `PermissionEngine` implementation reads internally — no new contract method) | `ContextAssembler` |
| L4 Architecture | `TaskSpec.context_refs.arc_ref` → load from plan/ADR/C4 docs | `ContextAssembler` |
| L6 Evidence | `TaskSpec.context_refs.artifacts` + `EvidenceStore.list_for_task()` | `ContextAssembler` |
| L7 Conversation | `SessionStore.messages.list_by_session()` (recent N messages) | `ContextAssembler` |
@@ -1322,40 +1327,61 @@ developer log and either re-parented or archived (runtime-semantics §5).
### 18.4 Outbox / compensation for cross-DB writes
Writes to project-level DBs (`debug-records.db`, `learned-memory.db`) or external files follow the
outbox model (runtime-semantics §6.3-§6.4, overview §8.3):
outbox model (runtime-semantics §6.3-§6.4, overview §8.3). The general pattern is:
```text
1. Insert durable session event recording intent/request (e.g. memory.promoted intent payload,
debug.record.created intent payload). Domain row in session DB marks status="pending_external".
2. Commit the session-side transaction (events_session.db).
3. After commit, EventBus delivers the event to the owning service (LearnedMemoryStore /
DebugKnowledgeStore). The service performs the external DB/file operation.
4. On success, the owning service emits a durable completion event (memory.promoted completion
payload with artifact_ref, debug.record.created status="committed") that updates the
session-side row from pending_external to committed.
5. On external failure, the service emits a durable failure event; recovery retries from step 3
based on the pending intent row.
6. On restart, recovery scans pending external intents and resumes step 3.
1. Owning service (LearnedMemoryStore, DebugKnowledgeStore, ...) performs the external
DB/file write through its own transaction. Each owning store is single-writer.
2. On success: the owning service ingests a single durable session event whose payload is
already the completion form fixed by event-registry §3 (e.g. memory.promoted with
target_ref populated, debug.record.created with debug_record_id populated). The
EventStore commit makes the cross-DB pair eventually consistent.
3. On external failure: the owning service does NOT emit the success event. It either
emits a task.failed carrying the AirError (so Scheduler/Doctor can decide), or queues
a fresh upstream candidate (e.g. memory.candidate.created) for retry.
4. On restart: recovery cross-checks session-side "upstream" rows (memory.candidate.created
without a matching memory.promoted, or pending debug-task tasks) against external store
state. Stale candidates are re-queued; orphan external rows are surfaced through Doctor.
```
**`memory.promoted` two-phase semantics** (event-registry §3, runtime-semantics §6.3):
No event payload is extended with intent/commit phase markers; the existing payload
schemas in `event-registry-v1.md §3` are honored as-is. Any future change to add an
intent-phase event would require an ADR plus a new event type or payload version bump
(event-registry §2 rule 7).
The single `memory.promoted` event type carries two semantically distinct phases distinguished by
its payload:
**`memory.promoted` outbox semantics** (event-registry §3 `MemoryPromotedPayload`, runtime-semantics §6.4):
| Phase | Payload marker | Meaning | When emitted |
|---|---|---|---|
| Intent | `phase: "intent"`, no `artifact_ref`/`memory_id` yet | The system has decided to promote a candidate; external write is pending | Step 1 above, inside session-side transaction |
| Completion | `phase: "committed"`, includes `artifact_ref` / `memory_id`, `target_store` | External store now holds the promoted memory | Step 4 above, after external write succeeds |
Per `event-registry-v1.md §3`, `memory.promoted` payload is fixed to:
`{ candidate_id, target_ref, promoted_by, summary }` — there is no `phase` field
and no separate intent/completion variants. Per `runtime-semantics-v1.md §6.4`,
`memory.promoted` records the **completed** promotion (target_ref already populated).
The intent event makes the promotion durable even if the runtime crashes before the external
write. The completion event closes the outbox loop and exposes the resulting memory ID/artifact
to downstream readers. If the external write fails permanently, a `memory.candidate.created`
follow-up may re-queue the promotion, or a Doctor task surfaces it for user attention; the
intent event itself remains in the log for audit.
The outbox sequence is therefore:
`debug.record.created` follows the same two-phase pattern (intent → committed) with payload
field `phase`.
| Step | Event / action | DB |
|---|---|---|
| 1 | `memory.candidate.created` (durable, already in registry) | session DB |
| 2 | Owning service (`ExperienceMiner` / curator) approves the candidate and writes the external store (`learned-memory.db` row, `.air/shared/rules` file, or skill file) | external DB / file |
| 3 | On success, emit `memory.promoted` with `target_ref` pointing at the just-written external row/file | session DB |
| 4 | On write failure: do **not** emit `memory.promoted`. Either re-queue via a fresh `memory.candidate.created`, or emit a `task.failed` carrying the `AirError` so Scheduler / Doctor can surface it to the user |
| 5 | On restart: recovery uses session DB candidates without matching promotion events to detect work in flight (runtime-semantics §6.4) |
`memory.archived` likewise records a completed archival (no `phase` field; payload
is fixed by event-registry §3 `MemoryArchivedPayload`).
**`debug.record.created` outbox semantics** (event-registry §3 `DebugRecordCreatedPayload`, runtime-semantics §6.3):
The payload is a single fixed schema with `debug_record_id` already assigned by
the owning `DebugKnowledgeStore`. Per `runtime-semantics §6.3` the flow is:
1. Owning `DebugKnowledgeStore` performs the `debug-records.db` insert/update.
2. On success, emit `debug.record.created` referencing the new `debug_record_id`.
3. On `debug-records.db` write failure: emit a `task.failed` (or future
`debug.record.failed`, gated by ADR) carrying the `AirError`; do **not** emit
`debug.record.created`.
No `phase` payload field is introduced in either event. Any future intent/commit
split must go through an ADR + payload version bump (event-registry §2 rule 7).
### 18.5 Security invariants