Detailed design: add §18.6 domain write-ownership invariants

Adds a consolidated invariant layer (INV-1..INV-5) so that context-isolated
implementers (e.g. parallel isolated subagents holding only one subsystem
slice) cannot violate global rules they cannot see in their slice.

- INV-1: session-DB state columns written only by event projection, with an
  authoritative table→event map and explicit heartbeat/ui_state exemptions.
  This is the rule the O2 finding violated; centralizing it prevents recurrence.
- INV-2: cross-DB writes use outbox + single writer.
- INV-3: side effects only through ToolRegistry → PermissionEngine.
- INV-4: one-way import/dependency direction.
- INV-5: EventBus is transport, never a source of truth.

Each affected subsystem chapter (§5/§7/§8/§9/§11) now opens with an
"Applicable invariants (§18.6)" pointer so the constraint travels with the
slice. Pure documentation consolidation of already-frozen rules; zero change
to contracts, events, schema, or runtime semantics.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
AirCoding
2026-06-01 15:23:05 +08:00
parent abc244f0da
commit b7091dbd1e

View File

@@ -291,6 +291,8 @@ throws an `AirError` of kind `system_error` (programmer error, never user-facing
## 5. Event Subsystem
*Applicable invariants (§18.6): INV-1 (state columns written only here, in `project()`), INV-2 (project never touches external stores), INV-5 (EventBus is transport only).*
Module: `packages/runtime/src/events/`. Classes: `EventSchemaRegistry`, `EventStore`, `EventBus`,
`EventIngestor`. All implement contracts §7 verbatim.
@@ -513,6 +515,8 @@ captured at open and is immutable for the session (overview §14).
## 7. Scheduler Subsystem
*Applicable invariants (§18.6): INV-1 (Scheduler/WorkspaceManager/AgentMonitor mutate `tasks`/`agents`/`workspaces` status only by emitting events for projection — never direct UPDATE; heartbeat timestamps are the only exemption), INV-5 (rebuild queues from SQLite, not EventBus replay).*
Module: `packages/runtime/src/scheduler/`. Classes: `Scheduler`, `TaskGraph`, `WavePlanner`,
`RetryPlanner`, `WorkspaceManager`, `AgentMonitor` (code-view §4). The Scheduler is an orchestration
service, not a coding agent (scheduler-state-machine §intro). All durable state goes through
@@ -638,6 +642,8 @@ checkpoint and may extend; hard timeout cancels/kills and marks task failed/inte
## 8. Worker and IPC Subsystem
*Applicable invariants (§18.6): INV-1 (WorkerManager never writes `agents.status` directly — `agent.started` projection sets it; `worker.ready` handshake is a live signal, not a status write), INV-3 (workers reach fs/shell/network/SQLite only through parent-mediated tool IPC).*
Module: `packages/runtime/src/workers/`. Classes: `WorkerManager`, `WorkerProcess`,
`WorkerProtocol`, and roles under `roles/`. IPC is NDJSON over stdio (contracts §10, ADR-0005).
@@ -757,6 +763,8 @@ loop additionally:
## 9. Tool, Permission, and Capability Subsystem
*Applicable invariants (§18.6): INV-3 (every side effect goes through `ToolRegistry.call` → `PermissionEngine.evaluate` first), INV-4 (capabilities install dependencies only through Doctor).*
Module: `packages/runtime/src/tools/`, `security/`, `capabilities/`.
### 9.1 ToolRegistry
@@ -967,6 +975,8 @@ backtracking.
## 11. Artifact, Evidence, and Knowledge Subsystem
*Applicable invariants (§18.6): INV-2 (DebugKnowledgeStore / LearnedMemoryStore are single writers of their project DBs and follow the outbox model — external write first, then one completion event), INV-1 (session-side markers like `artifact.created` write session rows only via projection).*
Module: `packages/runtime/src/artifacts/` and `knowledge/`.
### 11.1 ArtifactStore
@@ -1396,6 +1406,67 @@ From contracts §23, security-model-v1, overview §12:
- `.git/` internals are protected from arbitrary write tools.
- `sudo` risk is determined by command intent/target/system sensitivity, not string alone.
### 18.6 Domain write-ownership invariants
This section consolidates the cross-cutting rules that any implementer — including a
context-isolated subagent that only holds one subsystem slice — must not violate. Each
subsystem chapter (§4-§17) lists its applicable invariants by number; this section is the
single source of truth for them.
**INV-1 — Session-DB state columns are written only by event projection.**
Every `*.status` / lifecycle column in the session DB is mutated **only** inside
`EventStore.project(event, tx)` as part of the durable event transaction (§18.2,
runtime-semantics §3). No service (Scheduler, WorkerManager, ToolRegistry, MainAgent, …)
may issue a direct `UPDATE` to these columns outside event projection. The authoritative
table → writing-event map:
| Table.column | Written only by (durable event projection) |
|---|---|
| `sessions.status` | `session.created` (active) / `session.archived` / `session.deleted` |
| `message_drafts.status` | `assistant.message.started` (streaming) / `assistant.message.failed` (error); row deleted by `assistant.message.created` |
| `tasks.status` | `task.created` (pending) / `task.started` (running) / `task.completed` / `task.failed` / `task.blocked` / `task.cancelled` / `task.interrupted` |
| `task_attempts.status` / `failure_*` | `task.started` (insert) / `task.completed` / `task.failed` (update) |
| `agents.status` | `agent.started` (running or starting, projected once) / `agent.completed` / `agent.failed` / `agent.lost` / `agent.cancelled`**no event-less `starting → running` write** (§5.4, §20.4) |
| `tool_runs.status` | `tool.started` (running) / `tool.completed` (ok) / `tool.failed` (error) / `tool.cancelled` |
| `workspaces.status` | `workspace.created` (active) / `workspace.merge.completed` (merged) / `workspace.merge.conflicted` (conflicted) / `workspace.cleaned` (cleaned); `abandoned` set by GC compaction event path (§7.5, runtime-semantics §15) |
| `command_runs` | **no physical status column** — status is *derived* (§4.4), never written |
**INV-1 exemptions (explicitly NOT event-sourced; safe to write directly):**
| Column | Direct writer | Basis |
|---|---|---|
| `agents.last_heartbeat_at`, `tasks.heartbeat_at` | `AgentMonitor` coalesce (throttled) | runtime-semantics §4 — liveness timestamps, recovery does not replay heartbeat events |
| `ui_state.*` | `UiStateRepository.upsert` | db-schema §18 — UI scratch state, not event-driven (§21.3) |
**INV-2 — Cross-DB / external writes use the outbox model with a single writer.**
Writes to `debug-records.db`, `learned-memory.db`, `rules/`, `skills/`, or artifact files
go through the owning store, which is the **single writer** for that store, and follow the
outbox sequence in §18.4 (external write first → then ingest one completion event). No other
component writes those stores. `EventStore.project()` never opens an external DB or file.
**INV-3 — Side effects only through the tool + permission path.**
All filesystem / shell / network / git side effects go through `ToolRegistry.call`, which
always consults `PermissionEngine.evaluate` first (§9.1, §9.3). Workers never touch fs/shell/
network/SQLite except via parent-mediated tool IPC (contracts §10, §23). LLM/provider output
never performs a direct side effect (§18.5).
**INV-4 — Import/dependency direction is one-way.**
The allowed-import graph in §2 (frozen by `c4/module.md` + contracts §23) is never crossed:
`contracts` is imported by all and imports nothing; `workers` import only contracts + the
WorkerRuntime IPC surface; TUI imports only contracts; capabilities install dependencies only
through Doctor.
**INV-5 — EventBus is transport, never a source of truth.**
Recovery and scheduling rebuild from SQLite (events + domain rows), never from replaying
EventBus traffic (§5.5, contracts §7, overview §9.3). A dropped/duplicated EventBus delivery
must never change durable state.
> **For context-isolated execution (e.g. parallel isolated subagents):** inject §18.6 in full
> into every subagent's working context regardless of which subsystem slice it owns. INV-1 in
> particular guards the most common slice-local mistake — writing a status column directly from
> the subsystem that happens to know the new value — which a partial context cannot otherwise
> detect.
## 19. Sequence Designs
### 19.1 User request → task execution → completion