Small, versioned JSON facts scoped to one local project, shared by that project’s root sessions. The board is level-triggered state — durable truth that peers read at their own pace — and is the deliberate complement of durable agent mailboxes, which carry edge-triggered events that wake sessions. Board writes never wake anyone: if a change needs someone’s attention now, send a mailbox message pointing at the key.
Tracker: tm-uwx.7 (design v1.1 on the bead records the negotiated contract
with code citations). Store: src/coordination_board.rs. HTTP surface:
src/board_routes.rs. MCP tools: src/delegation_mcp.rs
(termal_board_list / termal_board_get / termal_board_set).
Board tables live beside mailbox tables in
~/.termal/coordination.sqlite. Both stores retain one long-lived connection
and share coordination-local FIFO writer admission, isolated from
termal.sqlite session/transcript persistence.
project.id). Callers never name the scope;
it is derived from the calling session’s project. Sessions without a
project, hidden sessions, delegation children (by parent marker OR the
durable delegation index — independent evidence sources), remote proxies,
and sessions in remote projects are all rejected by the backend itself; MCP
tool filtering for delegation children is defense-in-depth on top.revision (starts at 1),
updatedAtGeneration (the scope generation when that key was last written),
plus author, timestamp, and optional stateStamp.generation; single-key
reads expose it as scopeGeneration, distinct from the key’s historical
updatedAtGeneration. Only the current scope value is valid as
knownGeneration or snapshotGeneration.[a-z0-9] then [a-z0-9_-]*,
≤128 bytes total (e.g. activity.rust-suite, freeze.fingerprint).null is a legitimate value, distinct from deletion.Every write carries expectedRevision and an author-scoped idempotencyKey.
| Intent | Input | Against | Result |
|---|---|---|---|
| Create | expectedRevision: 0 + value |
never-existed key | revision 1 |
| Create | expectedRevision: 0 + value |
live head or tombstone | 409 with current head / tombstone revision in detail |
| Update | exact live revision + value |
matching head | revision +1 |
| Restore | exact tombstone revision + value |
tombstoned key | revision +1 (conscious resurrection — the only way back) |
| Delete | exact live revision + delete: true (no value) |
matching head | tombstone, revision +1 |
| Delete | exact tombstone revision | tombstoned key | 404 “already absent” |
| Delete | mismatched revision | tombstoned key | 409 with the tombstone revision in detail |
| Delete | any revision | never-created key | 404 |
expectedRevision: 0 is strictly create-only. It never resurrects a
deleted key — that guards against ABA: a stale pre-creation writer cannot
silently undo a deliberate deletion. Restoration requires reading the
tombstone’s revision from the 409 detail and CAS-ing against it.idempotencyKey (the board key plays no part in the lookup). Replaying the
identical full request — same key, value, expectedRevision, and
stateStamp — returns the original receipt with duplicate: true;
reusing the same idempotencyKey for any different intent → 409.
Receipts survive history compaction inside the bounded 4,096-write replay
window described below.delete: true with the value field absent —
JSON cannot express the difference between “value: null” and “no value”, so
the discriminator is explicit.get and list never return deleted keys as entries; “never existed” and
“deleted” are both 404 on read, but a get 404 for a deleted key may
carry the tombstone head (revision, deleted: true, structurally null
value) in its detail — the same restore token write conflicts expose.get returns both updatedAtGeneration (when that key last
changed) and scopeGeneration (the scope’s current counter). These values
intentionally diverge when another key was written later.list is sorted by key, default page 100 / max 200, and is
generation-bound: page continuations carry snapshotGeneration, and
any mutation between pages yields 409 — restart the listing. A busy scope
larger than one page can therefore livelock a listing; accepted for v1
because expected scale is well under one page (the 512-live-key cap bounds
the returned set).knownGeneration on a first page returns unchanged: true with zero rows
when nothing moved — the O(1) turn-start check.termal.sqlite; only then does a dedicated cleanup worker install an
idempotent deletion fence and cascade the scope in coordination.sqlite.
Unfinished outbox entries are scheduled again on boot. Any cleanup failure
stays durably queued; the already-absent project prevents HTTP callers from
authorizing new work for that scope in the meantime. The fence rejects any
already-authorized stale write that arrives after cleanup. Lifecycle cleanup
uses a short coordination-admission budget, and the cascade runs outside
both boot and the primary persist worker. A busy or large secondary scope
therefore cannot delay the HTTP listener or primary session persistence.
There is deliberately no agent-facing scope wipe: it would bypass per-key
CAS and erase the tombstones and receipts the safety model depends on.termal_board_* MCP tools or the project-scoped HTTP routes.
This keeps its level-triggered coordination role distinct from the visual
Response Board, which stores immutable transcript-card
snapshots for a human workspace.project-<uuid> ids.
Existing persisted ids remain valid, but a restored/reset termal.sqlite
cannot reuse the rewindable legacy counter and accidentally inherit a live
board scope or permanent fence from the independently durable
coordination.sqlite.| Mailbox (doc) | Board (this doc) | |
|---|---|---|
| Trigger model | Edge — messages activate sessions | Level — facts sit still |
| Ordering | Dense per-mailbox sequence, FIFO | Per-key revisions + scope generation |
| Read discipline | Cursor CAS (processedThrough) |
Plain reads; CAS only on write |
| Wakes the peer | Yes (metadata-only notification) | Never |
| Typical use | “Review round 2 is ready for you” | activity.rust-suite, freeze fingerprints, gate status |
Convention: coordinate who does what next through the mailbox; publish
what is currently true on the board. A mailbox message may simply say
“board key gates.union changed — read it when convenient.”
See SQLite session storage for the two-database layout, migration, and boot ordering.