0082 — Each seat publishes a heartbeat to a shared row and checks its sibling's; a stale or behind sibling is reminded once and never handed work
- Status: Accepted 2026-11-01. Written while the slices are in flight, as ADR-0004 requires.
- Date: 2026-11-01
- Deciders: Gera, 2026-11-01, on Trinity: "make sure she checks in somehow. And if she doesn't, Agent Smith has to like double ping her and say, hey, make sure you're on the latest version … So they kinda hold each other accountable." The transport, the thresholds and the escalation path below were picked by the lane that built it.
- Systems: seat-heartbeat (agent-harness
seat_heartbeat, and the sibling check that reads it). It also changes fleet-deploy's premise and the team-protocol handoff and cross-review paths. - Relation: closes the transport ADR-0011 left open ("a general cross-host transport is a separate decision"), keeping its rule that an unread seat is UNKNOWN and never current. It respects ADR-0003: a seat emits a row, and the other seat reads it. Nothing calls into the other seat.
1. The problem
Smith runs on the mini and Trinity runs the same agent-harness code on Fede's machine. On 2026-11-01, three cross-seat protocol changes landed in one day:
- the reviewer's
Risk:label (agent-harness #1769–#1779); - owed asks (#1791–#1793);
- restart safety (#1797–#1799).
Trinity follows each one only after Fede pulls main and restarts her. The mini had no record of her running commit. Each worker writes its boot commit (deploy_state.record_boot) only into its own state dir. So fleet_deploy reads her as UNKNOWN, and the only way to learn her version was for her to say it in Slack.
2. The decision
- Every seat publishes a heartbeat to one shared row. The row lives in the DynamoDB table the Construct's AutomationRuns already use (
propflow-prod, viaautomation_run._table_name), atPK = SEAT_HEARTBEAT#<registry slug>,SK = #LATEST. It is overwritten in place, at boot and every 5 minutes, from a daemon thread in the worker.- Contents: seat, the boot commit the worker declared, boot time, last-turn time, host, and a protocol number.
- Why this table: both machines already write it. Fede's machine records
granola-transcripts-pollruns there every ~5 minutes (hostFedericos-Mac-mini.local). So this adds no credential, no daemon and no Slack traffic. - Why not the alternatives:
- The relay is reachable only from the mini.
- Slack messages are a store nobody can query cheaply.
- A Temporal query needs the other seat's worker alive to answer, so a dead seat would read as an error rather than as "silent since T".
- A protocol number, bumped by any PR that changes how the seats talk.
seat_heartbeat.PROTOCOL_CHANGESmaps each version to one line saying what it adds. The version is its newest key, so the two cannot drift. A seat on a lower number is behind even when it is alive. Version 3, the first published, covers the three changes above plus the heartbeat itself (agent-harness #1810). Amended 2026-11-02: the sibling check and its reminders are version 4 (agent-harness #1813), so a seat on #1810 alone reads behind and is reminded to pull them. - Each seat checks its sibling before handing it work or asking it for a cross-review, and every ~15 minutes. A sibling is ok only when its row was written in the last 15 minutes AND its protocol is at least the checker's. Missing, stale, behind and unreadable are all not ok (ADR-0011: absence never reads as health).
- One reminder per episode, then people, then stop. An episode opens when the sibling stops being ok, and closes when it is ok again.
- At once: one line on the engineering bridge, tagging the sibling's bot. It names the sibling's commit and protocol, the checker's commit and protocol, and what the newer protocol adds, and asks it to pull and restart.
- About 30 minutes later, if still open: one message in the checker's own home channel, tagging the person who runs the sibling's machine.
- About 3 hours later, if still open: one message tagging the checker's own person.
- Then nothing more until the episode closes. For Smith checking Trinity that ladder is Trinity → Fede → Gera. For Trinity checking Smith it is Smith → Gera → Fede.
- Why the people go to the home channel, not the bridge: the bridge never pages a person (agent-harness #937 rewrites every human mention there to a plain name), so a mention of Fede there would silently not reach him. Each seat can always post to its own home channel, and fleet health belongs in
#agent-smith(ADR-0056 agent-smith-channel, fleet-down).
- It is symmetric. Trinity runs the same code, so she checks Smith the same way, under her registry slug (
trinity, not her boot slugclara). - No work is handed to a seat that isn't ok. A cross-review request or a handoff to a sibling that is not ok is not sent. The seat keeps the work and says so in the original thread. A PR that would have waited on the sibling's risk label waits on a person instead, which is where the merge gate already sends a PR with no reviewer label.
3. What this does not do
- It does not restart or update anything. Updating Trinity is still Fede's action on his machine. The reminder only makes the gap visible within 15 minutes instead of whenever someone notices.
- It is not a liveness check of the machine. A fresh row proves the seat's worker wrote it. A seat whose worker is down while its Slack socket still answers reads as stale, which is the true statement about whether it can do work.
fleet_deploystill reads boot files. Pointing it at this row, so Trinity stops reading UNKNOWN there too, is a follow-up.
4. Signals
The SYSTEMS.md seat-heartbeat entry matches SEAT-HEARTBEAT published in the mini's worker log. The line is written on every successful publish, so it goes stale only when publishing stops. smith doctor shows both seats' rows ("Seat heartbeats"), so either machine can see the other's commit and protocol.