# Architecture

Use this page to trace a communication fact from an independently opened client through
core, storage, and an optional delivery adapter. ACC is the local control plane; the AI
clients remain independent execution planes. Core records the truth before an adapter is
asked to make it arrive sooner.

```mermaid
flowchart LR
  S["sender session"] -->|record| C["core"]
  C --> D[("filesystem store<br/>outside repositories")]
  C -->|message id| R["delivery router"]
  R --> A["certified recipient adapter"]
  A --> T["already-running recipient session"]
  T -->|reply or acknowledge| C
```

Message delivery never starts, resumes, interrupts, supervises, or terminates the target
client. A transport that requires owning that lifecycle is outside the product boundary.
Separately, explicit update maintenance can restart a verified client service after the
user confirms the disconnection; it does not launch or assign agent tasks.

## Packages and dependency direction

| Package | Owns |
|---|---|
| `protocol` | ids, closed schemas, semantic validation, receipt transitions, error and exit codes |
| `core` | sessions, intent, claims, conversations, inbox, receipts, attention, ephemeral delivery bindings |
| `storage-filesystem` | atomic transactions, event journal, materialised views, recovery, writer lock |
| `adapter-sdk` | capability validation, exact-version certification, context projection, hook and install helpers |
| `adapter-*` | vendor-specific hooks, responses, installation, evidence, and optional transport |
| `delivery-router` | recipient policy, one-generation selection, safe offer, then receipt commit |
| `hook-runner` | bounded adapter hook entry point |
| `mcp-server` | polling participation for clients without a native adapter |
| `cli` | universal local command boundary |

Core cannot branch on a vendor name or import an adapter, Git, or
`node:child_process`. Package-boundary tests enforce this rather than relying on
convention.

## Record, then offer

The order is part of the public guarantee:

1. validate and commit the message plus one `queued` receipt per resolved recipient;
2. resolve the recipient's current delivery bindings;
3. check recipient policy and the current binding's reachability and offered native mode;
4. ask one adapter to cross its transport boundary;
5. only after acceptance, commit `offered` and an immutable success event.

An adapter whose contract declares `offerKind: "wake"` puts only a notice in front of the
client, never the body. The router records no offer for an accepted wake and returns the
outcome `woken`. The receipt stays `queued` until the recipient's next-turn hook output
carried the body. That hook then commits `offered` via `next-turn`.

A failed offer records the selected recipient session id and generation with a safe
diagnostic code, then leaves the receipt queued. Core validates that target provenance and
derives the event actor from it. A crash after bytes cross the boundary but before the
receipt commit may duplicate a later offer; ACC prefers that truthful underclaim to
claiming delivery before it happened.

## Delivery router is not a session manager

Messages address participants. The router may choose a live path only when exactly one
unexpired session generation is eligible. It does not open a session to make one eligible.
Zero candidates, several candidates, an unknown version, recipient policy `off`, or an
adapter refusal all produce durable fallback.

The binding is ephemeral and stores:

```text
sessionId · generation · adapterId · clientVersion · availableModes
livePolicy · opaqueEndpointRef · leaseUntil
```

Core validates identity and expiry but never interprets the opaque endpoint. That remains
inside the owning adapter. `acc status` reports available modes, policy, reachability, and
lease without exposing the endpoint. The hook runner also replaces one optional native
attempt diagnostic beside its session-owner file: closed policy/process/handshake
metadata with a timestamp, never vendor output. Doctor joins it only to the current open
generation and reports it separately from transport and receipt facts. It is not journalled
or projected into agent context; session restart replaces it and SessionEnd attempts bounded
cleanup. Doctor ignores closed or superseded generations even if cleanup could not finish. Diagnostic disk I/O runs in an
unreferenced worker, so a stalled write cannot hold the hook process open.

Claude Code's inbox binding runs inside ACC's own hooks, with no ACC process in the
session. Each turn hook reads the session's inbox socket from `CLAUDE_CODE_MESSAGING_SOCKET`
and verifies it against Claude Code's session registry `<config>/sessions/<pid>.json`. The
pid and the socket must match; the session id is checked from the first offer on, because
Claude Code rewrites the entry for a `/resume` only after SessionStart. The hook then writes
a private endpoint record and publishes the binding with a 120-second lease. The router refreshes an expired
lease with the same registry check, so an idle session stays reachable between turns. The
adapter repeats the check before each offer. A registry that disappeared, or that names
another session or socket, refuses the offer, so a wake never reaches a reused pid. This
does not create a session or revive a closed one.

## Certified capability versus current reachability

Ordinary hook capabilities say a captured behaviour passed, and apply forward from the
version that recorded it until a later capture changes it. Native live delivery uses a separate contract: installation checks the
captured minimum, platform, and current feature probe, then each session publishes a binding
only after a generation-bound handshake. The router trusts that admission and verifies the
binding, recipient policy, declared adapter capability, and adapter response; it does not add
a third certification check. A binding says what that current generation
exposes and whether its lease is current, while recipient policy says whether it may spend a
turn.

The Claude Code inbox wake has installed-client evidence on 2.1.282. Codex LocalDaemon
has it on 0.152.1 and 0.153.4. Antigravity CLI has it on 1.2.7 and later through a relay
the agent starts in its own shell. All three use a captured minimum that applies on every
platform, a current probe and an exact session handshake; a prerelease is judged by its
release triple. Claude Code and Codex preserve the ordinary
client launch. Claude Code verifies its session registry, and Codex verifies its registered
thread and workspace. Native delivery owns no vendor daemon lifecycle. Gemini CLI and Kimi
Code have exact-version next-turn evidence only. Grok and generic MCP use inbox polling.
Antigravity CLI additionally attaches a session only where the client reports an open
workspace.

## Storage and workspace identity

The filesystem store is the durable source of truth. Every meaningful mutation updates
materialised records and appends an immutable event in one transaction. Sequence ids sort
lexically, so a cursor is simply the last sequence returned.

Workspace discovery checks an explicit config first, then the Git common directory, then
the plain directory. Multiple worktrees share a workspace id while each session records
its checkout and branch. Runtime state never lands inside those roots; the only project
file ACC writes is an optional `acc.workspace.json` explicitly requested through
`acc config init`.

Native hooks persist the initial workspace directory and id, keyed by adapter and
native session id, under the platform data home's `acc/native-workspaces`. This
record carries routing only, never owner credentials. Subsequent hooks resolve the
saved directory before loading the workspace-local owner binding; their payload's
current cwd still resolves relative file targets. A nested repository cannot change
the room or bypass its claims. A linked worktree of the original repository keeps
repository-relative claim paths. Room publication is serialized before session
opening, and the record survives SessionEnd so native conversation resume keeps the
room. A changed initial workspace identity fails open with a diagnostic instead of
opening a replacement room. If SessionStart was missed, the first user-turn hook
establishes the room. A legacy session without this record establishes it on its
next startup or user-turn hook; its original launch directory cannot be inferred.
Every bound owner header includes a local `--workspace acc://<reference>` selector.
The CLI validates the exact named routing record in ACC's own data home, then resolves
the saved room through the same path as hooks. It does not scan for an owner or infer
credentials. This also keeps a header usable when Git availability changes between
the hook and the CLI command. Ordinary project-config validation remains unchanged.

A lone session can remain ephemeral. Durable state materialises when a second live session
appears or the first claim, message, or handoff is committed. Solo presence therefore
does not require durable workspace history. Native turn hooks still supply the session's
own CLI arguments and workspace directory, so a peer joining later in the same turn
does not require reattachment and a changed shell directory cannot silently select another
workspace. Claude SessionStart also restores this owner header after compaction; it
does not project peer bodies or advance their receipts.
Grok instead receives that own header after a terminal tool result through PreToolUse;
the first public status call makes it available for subsequent owned commands.
Without relevant coordination context, that identity header is the only projected content.

## Inbox, attention, and projection

Public inbox discovery uses core `listInbox`: bounded, read-only message summaries
addressed to the current participant. Exact body reads use `readInbox` and advance only
that participant's receipt to `retrieved`; the embedded core API still supports bulk
retrieval. History sync reads message records without receipt changes, using the same
summary pager or one exact id. Reply validates ownership, creates an `answer` in the
same thread, and acknowledges the original atomically. No participant can advance another
participant's receipt.

Decision lifecycle is derived from immutable message links, without a new record kind
or mutable decision store. Read copies carry bounded status; raw forensic snapshots stay
unchanged. Non-head decisions leave pending inbox, bulk delivery, and attention while
exact reads retain them. Replacement/withdrawal records inherit recipients before commit,
so offline readers receive the change without a forged acknowledgement. See
[the protocol contract](PROTOCOL.md#decision-lifecycle).

Attention is computed from six explicit rules for current obligations:

| Priority | Kind | Observable trigger |
|---|---|---|
| 1 | `reply_required` | this participant owns an unacknowledged receipt whose message requires a reply |
| 2 | `acknowledgement_required` | this participant owns an unacknowledged receipt whose message requires acknowledgement |
| 3 | `recipient_unavailable` | a required recipient of this participant's message has no online session |
| 4 | `claim_conflict` | this session's intent hint overlaps another live claim |
| 5 | `claim_contended` | a peer intent hint overlaps a live claim this session owns |
| 6 | `claim_expired` | a claim this session owned has reached its lease time |

Semantic relevance belongs to the receiving model; correctness never depends on a hidden
classifier.

Adapters project peer bodies in an attributed untrusted frame. If a complete body cannot
fit, the projection keeps the message id and points to `acc inbox --message <id>` instead
of silently truncating it.

On a client that takes next-turn bodies, a live offer that is still `offered` 15 minutes later
is projected once more before a turn,
after the new bodies and marked as a repeat, and never by an end-of-turn continuation; see
[the protocol contract](PROTOCOL.md#one-repeat-of-a-live-offer).

For this participant's unresolved messages already `offered` or `retrieved`, the standard
projector combines reply and acknowledgement reminders into a compact count after new
message bodies. Queued obligations and claim conflicts remain individual attention items.
All pending headers stay discoverable through `acc inbox` pages; owned `acc status` retains
its complete attention list. Full bodies require an exact inbox read; projection
does not acknowledge or delete records. Explicit decision links, rather than the
projector or message age, retire old decision obligations.

## Hooks fail open

Hook execution is bounded. If coordination state cannot be read or a decision cannot be
made in time, the client action continues. ACC may warn or guard where a real hook allows
it, but a coordination tool must not be the reason a session stops working.

Next: [Protocol](PROTOCOL.md) · [Capabilities](CAPABILITIES.md) ·
[Security model](SECURITY_MODEL.md)

## Managed runtime activation

The CLI owns installation generations under `<dataHome>/acc/runtime`, outside workspaces.
All five launch paths select a generation and publish an actual-process lease under one
admission mutex before loading workspace-capable code. Immutable launcher modules and
runtime directories preserve in-progress imports. Leases survive `main()` returning and
are removed only after confirmed process death. Older updaters can omit the store contract
when publishing a newer generation. The reader recovers missing contracts only from the
referenced generation, after checking its package identity and content hash, including file
modes. It preserves explicit declarations and does not rewrite live leases. Missing or
changed generation files and unknown native bindings remain unverified holds.
A session is also pinned to the generation
it started with: its hooks delegate to that generation while the pin declares the same
store contract this one does, and run the active generation otherwise. Apart from
`SessionEnd`, only the client's confirmed death releases a pin, so a session whose client
process cannot be found is not pinned, and a pin that names no client is reaped. A
generation directory is reclaimed once no control pointer, live lease, live pin or staging
hold names it. An update is run by the installed code, so right after an activation the
activated generation's own reclaim runs from its files, and its worker reclaims on every later
pass; each pass is recorded in `reclaim.json`. The updating process still runs from the
generation it replaced, and its lease keeps that one through the pass. Such a pass, like one an
unknown holder postponed, is recorded as unfinished, and the next ACC command an hour or more
later starts the worker that finishes it. Parsed help, version, and update recovery remain
available when workspace admission is unavailable.

Store contract 7 adds exact transaction loads and two rebuildable indexes:
`messageByClientKey` and `receiptsByMessage`. Content-addressed pages contain
portable key tuples and record references, never message bodies. Each page is
bounded by 32 entries and 64 KiB; radix branches have at most 16 children.
Under the writer mutex, the cache is trusted only for the same workspace and the
current idle journal generation. Every message/receipt put or remove advances
that durable authority. Other journalled writes carry valid unchanged roots
forward, while ordinary single-record heartbeats keep their direct publication.
Authoritative writes commit first; cache pages and the manifest are unflushed
derived data. Cache loss or corruption requires verified rebuilding and cannot
turn a recorded message into a failed send.

Contract 6 writers can bypass that authority, so migration is explicit.
`doctor --migrate-store` holds runtime admission before the workspace writer
mutex, refuses incompatible or unknown live holders, recovers decided journals,
builds the complete cache, and durably replaces the identity last. The library
requires explicit opt-in after unmanaged callers are quiesced. A hook never
performs this migration. Record schemas and journal format 2 are unchanged.

An independent worker downloads npm packages with lifecycle scripts disabled, checks exact
stable package identity and the discovered integrity, and health-checks the staged runtime.
Installer detection and planning happen before exclusive admission. The worker then checks
ACC process leases and native binding PIDs and waits only for those whose declared store
contract differs from the incoming generation's or is absent, records `activating`, applies
every integration, and only publishes the new active pointer after all applications succeed. Partial refresh
remains fenced and repairs forward, with the old active pointer retained but normal
workspace admission unavailable until recovery completes. Native bindings cease holding
after observed SessionEnd cleanup or confirmed process death; the vendor daemon may remain
running. A binding that names no client process is judged by its session record instead
(#273): it stops holding when the record is gone, when the record names a process that has
exited, or, with no process recorded, when ACC's presence rule calls the session offline -
closed, or 30 minutes without a heartbeat. A live recorded process, or a record that cannot be
read, keeps it a hold. A client that wakes later runs its next hook through the launcher on
the active generation. A binding that names its client stops holding when that process exits,
or when the process that now has its pid started after the binding was last written, since a
client runs when it writes its binding (#276); a start time that cannot be read decides
nothing. A zombie, a process that has exited but that its parent has not collected, counts as
exited for a binding, an ACC lease and a pin (#280); a process state that cannot be read
decides nothing. Otherwise presence TTL, `acc finish`, and delivery off do not prove native lifecycle
end. A pending release that cannot activate is replaced by a newer one when `acc update` runs
or an automatic check is due (#277). Normal background updates never
restart a vendor daemon. An explicitly approved maintenance job can do so through the
adapter lifecycle port: it waits for idle work, validates exact process identity, stops
the service under the admission fence, refreshes integrations, and restores and verifies
the service even after a refresh failure. Checkpoints and the detached worker live outside
repositories. Approved crash recovery never blindly repeats a stop.

Lock ownership never expires by age. Retained nonempty tombstones prevent delayed observers
from reclaiming a successor. Under an acquired lock, maintenance removes historical
bookkeeping only when no live or unknown contender could still reference it; a stopped
contender postpones cleanup. Admission also removes confirmed-dead process leases. None of
this metadata is agent conversation history or injected context.

The first upgrade from an unmanaged binary still requires a restart. ACC cannot fence old
direct binaries or external applications importing core. Native trust and client-side cache
activation remain vendor responsibilities; refreshed files do not establish observed
capabilities.
