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:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user