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'. Special members are marked by a
broker-owned cafleet.kind flag inside member_card_json rather than a
column: "monitoring-member" marks the fleet's single monitoring member
(which skips monitor_config enrollment and is located by this marker — see
Monitoring). Callers cannot set cafleet.kind
through any public path.
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 state tables
monitor_config holds one row per enrolled member — exactly the four
schedule columns: interval_seconds, enabled, last_ping_at, and
last_stall_check_at (the nullable UTC ISO timestamp of the last successfully
dispatched stall-check wake, kept durable so the stall-check cadence survives
a monitor-loop restart). Disabling or losing a pane clears
last_stall_check_at; soft deregistration explicitly deletes the config row.
The monitoring member's own stall notes (quiet baselines, ping history) live
in its conversation context, not in the database.
monitor_runtime remains the one-row-per-fleet loop pid/heartbeat table. Which
members are enrolled and 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 two 1:1 child tables
(member_placements, monitor_config), 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 fleet-scoped; per-member identity is enforced only on state transitions:
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.