Data model
The Message payload is fully relational: every routing field plus the message
body lives in its own typed column. The only JSON TEXT blob is
members.member_card_json. The database is SQLite, accessed synchronously
and bundled into the binary; the schema is managed by a chain of SQL
migrations embedded in the binary — run cafleet setup to migrate to head
(idempotent, data-preserving; see Storage). The exact column-level DDL contract lives
in the repository's SPEC.md.
Minted ids are never reused and real ids are always >= 1.
Tables
fleets
cafleet fleet create writes the fleet row, the root Director (and its
placement), and the director_member_id back-reference in one all-or-nothing
transaction — which is why director_member_id is DB-nullable despite the
post-bootstrap NOT NULL invariant.
members
Active query paths filter status='active'. A member's kind (director /
member) is derived from the fleet's director_member_id back-reference at
read time; no kind marker is stored in member_card_json.
messages
One row per delivery: a unicast row, a broadcast delivery row, or a broadcast
summary row (see Broadcast grouping). from_member_id,
to_member_id, and origin_message_id are deliberately not foreign keys —
historical messages may outlive their sender. status_timestamp is updated on
every state change and drives ORDER BY DESC listing. The rendered envelope is specified in
Message envelope.
member_placements
Links a member to its multiplexer pane; pane ids are stored verbatim as opaque
strings. The root Director keeps its own placement row (it is pane-bound); an
ordinary member is a placed row other than the fleet's root Director
(member_id != fleets.director_member_id). Placement rows have no historical
value.
monitor_runtime
monitor_runtime is the one-row-per-fleet loop pid/heartbeat table: the
single-instance claim (pid, started_at), the liveness heartbeat
(last_tick_at, tick_seconds), last_wake_at — the nullable UTC ISO
timestamp of the last successfully delivered Director wake, kept durable
across loop restarts so an immediate restart honors the remaining wake
cadence — and wake_interval_seconds, the nullable live mirror of the
running loop's Director wake interval: stamped with the startup-resolved
value at every cafleet monitor start (claim and reclaim), re-read by the loop on
every tick, overwritten by PATCH /api/monitor, and preserved across a
loop stop like tick_seconds. It is NULL only in rows that predate the
column and have not been re-claimed since — a running loop's row is always
stamped. The cadence semantics are defined in
Monitoring.
asset_installs
One upserted row per coding agent, recording the CLI version whose skills
and preset (where one exists) install last landed there — the row attests
both. Written by the assets half of cafleet setup; feeds the stale-assets
guard and the cafleet doctor report (see
CLI options).
Foreign key enforcement
SQLite ignores FK declarations unless PRAGMA foreign_keys=ON is issued per
connection; the connection opener applies it on every connection. FKs use
ON DELETE RESTRICT except the member_id PK=FK of the 1:1 child table
(member_placements), which uses CASCADE so a hard-deleted member cannot
leave dangling rows. Normal delete paths are soft-deletes, so neither fires
in practice.
Message Visibility Rules
Read access is by id — the subject row carries its own fleet and recipient, so existence (plus, for ACK, message state) is the enforcement:
Broadcast Grouping
A broadcast produces N+1 rows — one delivery message per active recipient plus
one broadcast_summary message — grouped by origin_message_id:
Because ids are DB-assigned, the summary row is inserted first with a
temporarily NULL origin_message_id, then self-linked before the delivery rows
are inserted. The grouping predicate origin_message_id IS NOT NULL cleanly
partitions the timeline into standalone unicasts vs broadcast groups. The
per-recipient ACK time is read from the completed delivery row's
status_timestamp, which is valid because a delivery message makes exactly one
state transition over its lifetime.