From b7091dbd1e95e0c3e18e5da0c2ebaf5ca77b73ea Mon Sep 17 00:00:00 2001 From: AirCoding Date: Mon, 1 Jun 2026 15:23:05 +0800 Subject: [PATCH] =?UTF-8?q?Detailed=20design:=20add=20=C2=A718.6=20domain?= =?UTF-8?q?=20write-ownership=20invariants?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../architecture/system-detailed-design.md | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/AirPlan/docs/architecture/system-detailed-design.md b/AirPlan/docs/architecture/system-detailed-design.md index 85b094e..d6dfac0 100644 --- a/AirPlan/docs/architecture/system-detailed-design.md +++ b/AirPlan/docs/architecture/system-detailed-design.md @@ -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