0071 — A Smith/Trinity thread has one driver, shown by a sticker on its top message, and each seat waits for the other's final answer

The problem

The strategy review of 2026-10-19 grouped 554 distinct Smith/Trinity findings by root cause. The largest, 184 of them (33%), was no single owner per thread and no single outbound layer. Its ownership half swung between two failures:

And the two seats talk past each other. A reply is posted as a stub ("looking into it…") and edited into the answer minutes later. slack.update_message sent no metadata, and the socket drops edits, so the other seat saw only the stub and replied to it.

Gera's rulings, verbatim (2026-10-19)

  1. Owner sticker, picked by area. Every thread or alert gets a driver: Gera's area, Smith drives; Fede's area, Trinity drives; the other agent helps. "This is something that would happen in more than just alerts. So, we don't want to just tie the logic to alerts."
  2. The sticker is an emoji on the thread's top message (:driver-smith: / :driver-trinity:). "Whoever's driving it would just react to the top thread and it's clear for a new agent down the line of who owns it."
  3. Tie-break: Smith owns #alerts as triager. "We could just have Agent Smith own alerts channel. And if Agent Smith thinks Trinity owns it, he can CC her and tag the top thread's emoji to Trinity. And then Trinity will be like, okay, I'm the driver."
  4. A driver card: subject · driver · whose work (GERA/FEDE) · status (investigating / planning / driving / …). "Okay, I'm the driver. This is regarding GERA's work."
  5. Cross-review before merge, in a critique headspace. "For alerts, I think it's good to have both machines aligned." The reviewer asks "is this a quick fix… a bandage solution or is this a real genuine fix?" so "they can kind of help each other stay accountable."
  6. Turn-taking openly in the thread, each waiting for the other's FINAL answer.
  7. Acks: 👀 if the agent will act, 👌 if no input is needed. 👌 is kept because 👍 approves held cards.
  8. Ship once in agent-smith. Fede is told in #engineering-bridge that his 10-12 rule is replaced.

The decision

  1. Turn state rides on the message. Every reply-workflow send stamps turn_state in Slack message metadata: the work-ack says working; every other send says final. The names are A2A's task states, with completed spelled final. Slack keeps a message's metadata on a chat.update that omits it (verified live 2026-10-19 in #apartment-yale-test), so the ~30s progress edits leave a stub working until its final edit flips it. Default final: a send nobody thought about can be acted on, never pin the other seat. A seat reads the state in the thread fetch it already makes (conversations.replies with include_all_metadata). An unmarked message, from a seat on older code, is never treated as pending.

  2. A seat never answers over the other seat's stub. Before it dispatches a turn into a thread whose sibling message is working, it 👀s the human's message at once, then waits for that message to turn final and answers with the re-read thread. The wait is bounded at 180s from the stub's own ts. Past that, it answers anyway, and the brain is told the sibling's answer is still pending, so it says so instead of answering for it. One stuck run never silences the other seat.

  3. The driver is a sticker on the thread's top message. The emoji names are configurable, with built-in fallbacks that exist today, because :driver-smith: and :driver-trinity: are not uploaded yet (Gera uploads them as admin). Each seat reads emoji.list at runtime (the bot has emoji:read, verified 2026-10-19) and uses the custom names once they exist. A seat stamps only its own sticker. The one exception is Smith handing an alert to Trinity (ruling 3).

  4. Who drives, read in this order:

    1. A sticker placed by a human wins. Anyone can re-sticker by hand to override. Reactions carry no timestamp, so "latest wins" cannot be computed, but a human uid in a reaction's users can.
    2. Otherwise, a sticker placed by a seat.
    3. Otherwise, the area rule.

    The area rule (lane ruling, fable-decide fec94f788, refined by f72914c32): a thread a human started is its author's work first, so Fede's root → Trinity and Gera's root → Smith. Without that, every thread Fede starts with Trinity in a shared channel would get Smith as driver, and Smith would answer Fede's un-addressed follow-ups in what is Fede and Trinity's conversation. Anyone else's root, and every bot or alert root, goes by the content: Smith by default; Trinity where a small reviewed map in agent-smith names a Fede lane with evidence: Clara's voice runtime (agents/clara), the AppFolio sync lambda, PMS delivery and guest-card, email-stress and voice-harness. The rule is a pure function of the root message's text and alert payload, so both seats compute the same driver with no lock. Every hand re-sticker is a prompt to amend the map. Rejected: deriving ownership from git authorship (two clones and a rolling window disagree, and src/lib/domain is co-owned), and mapping all of propflowai to Trinity (against Gera, 10-14: "anything on Propflow's side is OUR plate").

  5. The driver card goes on driven work, not on every answer (lane ruling, fable-decide f8c994687). The sticker goes on every thread a seat takes a turn in. The card goes on when work is driven: an alert thread, an investigation, or a fix/PR. It is one message, a thread reply and never top-level, edited in place as the status moves. Its status lives in its own metadata, found by reading the thread, with no new store. A card on every Q&A thread would be the per-event posting ADR-0056 agent-smith-channel removed. The cost if wrong is bounded, because the sticker already names the driver. ⚠️ Gera's ruling 4 can be read as every thread; only he can confirm that reading.

  6. #alerts hand-off. (The channel-gate parts are superseded by the 2026-10-22 amendment at the end of this decision.) Smith triages #alerts. When the bridge accepts a hand-off to Fede's lane, Smith removes its own sticker from the alert, stamps Trinity's, and posts one line in the thread: "Handed to @Trinity (lane, Fede's area): the same item as the bridge hand-off (link)". The bridge HANDOFF carries the alert's permalink.

    Amended as built (2026-10-20, fable-decide f68fb5a06). The CC was to carry the sibling marker so that "Trinity takes it as driver". In #alerts that is not true yet:

    • Smith answers every person there before any driver router runs (alerts_responds_to_all).
    • Trinity's socket is enforce-gated, #alerts is not one of her channels, and she drops any message that does not @-mention her.

    A marked CC therefore woke a second Trinity turn beside the bridge's turn, with nothing tying the two together. So the CC is unmarked: her one turn on the item is the bridge's, and the sticker is the visible owner mark.

    • Named follow-up, not built (built: see the 2026-10-22 amendment below): making the sticker route follow-ups in #alerts. That is a channel-gate change on both seats, and it reopens what the plist tests pin shut on Fede's seat, so it needs Fede.

    Amended 2026-10-22: seats are equal, and the channel gate is retired (agent-smith #1352, decider receipt f383599b6). Gera, verbatim: "being an owner honestly doesn't really mean anything … treat them as like equals and then their home base where they're more comfortable posting could probably be their specific channel." Also: "if we respond to an agent … they should always be nudged to respond. Always, despite channel." What is retired is per-seat channel ownership. The one driver per thread, shown by its sticker, stands. This changes decision 6 in four ways:

    • Trinity no longer drops messages in channels that are not on her list, because AGENT_OWNED_CHANNELS and AGENT_CHANNEL_GATE are gone.
    • "Smith triages #alerts" is now one shared rule, agent-smith's thread_driver.triages_alerts. It is the same on both seats and scopes the alert decider and #alerts' answer-everything to Smith. That rule is what keeps the two-deciders incident closed.
    • The named follow-up above is built. An #alerts thread reply goes through the driver router like any thread. A thread handed to Trinity is hers, and Smith waits for her in the 12s window.
    • A human reply in a thread whose only seat post is a mirror card goes to the seat that posted the mirror, unless a sticker names the other seat (agent-smith #1351).
  7. Cross-review before merge. Before merging a fix, the driver CCs the other seat for a critique, not a syntax review. The questions: is this a bandage or a root fix; would it recur; does it fix the class or only this instance. That is the fix-history question ADR-0070 asks of one seat, now asked by both. Amended 2026-10-31 by ADR-0078: the critique is no longer advisory. The reviewer also labels the risk (low, med or high). ROOT with low or med merges with no person; anything else, or no label in time, pages both founders.

  8. Acks are unchanged. 👀 to act and 👌 for no input, as agent-smith's ADR-0004 amendment of 2026-10-19 already records.

  9. The hop cap is not raised. A CC is hop 1 and the critique is hop 2, both under agent-smith's AGENT_SIBLING_MAX_HOPS of 3. Ordinary helper turns are not hops; only a stamped @-mention of the sibling is.

  10. Amended 2026-11-03: a person's routing @-mention moves the driver (Gera, after #engineering-bridge thread 1793637694.850649). In that thread Smith posted a hand-off to Fede's side, and Gera replied with a bare @Trinity. Smith stayed out (👌) and Trinity did the work, but the root kept :agent-smith:. The socket logged driver=smith source=solo. Gera, verbatim: "we can kind of change the driver if something like makes sense. In this case, it makes sense since I like brought attention to Trinity. But just bringing attention doesn't make it enough. It has to be like, oh, okay, this seems like a question for Trinity."

    • When it moves. A person's message @-mentions the seat that does not drive, does not @-mention the driver, and hands that seat the work or the question. Examples: "@Trinity can you take this"; a question in her lane put to her; a bare @Trinity under a hand-off to Fede's side. These do not move the driver: "@Trinity FYI", "cc @Trinity", a mention inside a sentence about something else, and a mention while asking the driver to do something. An @ of both seats or of @Agent Force changes nothing, because the dispatcher picks there. A later routing @ back to the first seat switches it back.
    • Who decides. The seat that drives now. Slack lets a seat remove only its own reaction, so only the driver can take its sticker off. One seat asking also means one verdict, where two seats asking a model could disagree. It asks one MODEL_LOW question (intent_classify.classify_driver_routing), and the thread's top message is part of the context. The answer fails closed: only a clean "route" moves the driver. An error is logged as unavailable, never as "stay". The mentioned seat answers the message either way, as before.
    • Where it is recorded: the sticker. The driver removes its own sticker and stamps the other seat's, through the same door as the decision 6 hand-over (thread_driver.hand_over). decide already reads the sticker on both seats and in the Agent Force dispatcher, so both compute the same driver with no new store. A person can see the sticker and undo it by hand. A seat on older code reads a lone seat sticker the same way. Rejected: a marker post in the thread, or a field in the seat-heartbeat table. Either would be a second driver source that both seats must learn to read, and it would disagree with the visible sticker. A sticker a person placed by hand on the top message (rule 4.1) still outranks this: the seat moves nothing, because it cannot remove a person's reaction and a hand sticker is the stronger signal. A person who wants a routing @-mention to take effect re-stickers by hand or removes their own sticker.
    • When it takes effect. From the new driver's first post in the thread. Until then the follow-up rule "a seat alone in its thread drives it" still applies, and her answer to the mention is that first post.
    • The stamp race. The move waits for any stamp in flight from the same seat on the same thread, and holds the same claim while it writes. A late stamp would otherwise put the old seat's sticker back beside the new one, and two seat stickers tie to Smith.
    • Owed items follow the driver. A hand-off this seat received in that thread is handed on to the new driver, the way a bridge re-hand-off does. Nudges and the page then go to the seat that now holds the work.
    • Not a hop. A person's @-mention is not a seat-to-seat message, so it does not count toward AGENT_SIBLING_MAX_HOPS (decision 9). Neither does the move: it is a reaction change and a ledger write, and posts nothing.
    • Seat protocol 5 (seat_heartbeat.PROTOCOL_CHANGES). Trinity applies the rule when Fede's machine pulls. Until then, a move back from her runs only when she has the code. Smith's moves to her work from the first deploy, because she already reads her own sticker.

How this composes with the 12-second window

agent-smith #1270 (in flight when this was written) makes the non-owner of a thread wait 12s to see whether the owner takes a human's message before judging it. The turn-state wait in decision 2 is a different gate on a different question: whether the other seat's reply is finished. The 12s window decides whether this seat answers. The final-state wait decides when. Do not merge them into one wait.

What this amends and what it leaves alone

Rejected

Build

Four vertical parts in agent-smith. Each is one PR off origin/main and works on its own:

  1. Final-state metadata plus wait-for-final.
  2. The router, sticker and driver card, replacing first_seat_poster. Lands after #1270, on top of it.
  3. The #alerts hand-off.
  4. The cross-review request and the critique prompt.

Consequences