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
- Status: Accepted 2026-10-20. Both conditions hold: part 1 is deployed on Smith's seat (agent-smith #1284
83099d1band #1298d7bdb431, live from the 07:52Z restart; every part live by 10:38Z), and Fede was told in#engineering-bridge(bridge notice). Proposed 2026-10-19, and written at decision time per ADR-0004. Acceptance does not wait on the end-to-end proof below. That proof needs Trinity's pull, and the Consequences section tracks it. Fede was told, but he has not acknowledged; Accepted is not his sign-off. Revisit trigger: this replaces a rule Fede made (#937 item 9). If he objects in#engineering-bridge, whoever sees it first amends this file. Silence is not ratification. - Implemented by agent-smith:
- part 1 (turn state and the wait): #1284
83099d1b, #1298d7bdb431; - part 2 (router, sticker, card): #1299
97a1be69, #13034d24b00f, #1304b6456794; - part 3 (alert hand-off): #1305
93f39a4c; - part 4 (cross-review): #1285
3773f526, #129565024983. - decision 10 (a routing @-mention moves the driver, 2026-11-03): agent-harness #1881.
- part 1 (turn state and the wait): #1284
- Date: 2026-10-19
- Deciders — split on purpose, because attributing a machine's inference to a person is the same defect class as a post going out under his name:
- The protocol is Gera's, spoken 2026-10-19 in the session that ran the Smith strategy review, and quoted verbatim under Gera's rulings.
- The area rule and the driver-card scope are the lane's, ruled through fable-decide: receipt
fb991e472(both passes chose the rules below; the run reported NEEDS HUMAN only because two questions were packed into one slot), then re-asked one question each:fec94f788(area, RESOLVED, high in both orders),f8c994687(card, RESOLVED), andf72914c32(root author as the first area signal, RESOLVED, high in both orders). Neither founder has seen those two narrowings. - What it replaces is Fede's: "first seat to post owns the thread", agent-smith #937 item 9, 2026-10-12.
- Systems: agent-smith, one codebase run as two seats:
smithon the mini (Gera's) andclara, presented as Trinity, on Fede's machine.slack.py(post_message,update_message, the turn marker),activities/slack_send.py,workflows/reply.py, andslack_socket.py(_dispatch_triggerand the thread-participant block that callsfirst_seat_postertoday). Both seats run whatever agent-smithmainthey last pulled.
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:
- Both seats answering. From 09-01 to 10-06 both seats decided the same alerts: 8 of 12 Sentry roots on 09-24, 6 of them with conflicting verdicts. On 10-02, #691 and #715 built the same fix on consecutive days.
- Neither seat answering. The 10-12 fix was "first seat to post owns the thread". An audit of 10-16..19 then found 16 of 66 human messages unacknowledged; one cause was Smith going silent in threads Trinity had posted in first. Gera's "Did we call MCC?" got no answer from either bot.
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)
- 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."
- 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." - 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."
- A driver card: subject · driver · whose work (GERA/FEDE) · status (investigating / planning / driving / …). "Okay, I'm the driver. This is regarding GERA's work."
- 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."
- Turn-taking openly in the thread, each waiting for the other's FINAL answer.
- Acks: 👀 if the agent will act, 👌 if no input is needed. 👌 is kept because 👍 approves held cards.
- Ship once in agent-smith. Fede is told in
#engineering-bridgethat his 10-12 rule is replaced.
The decision
Turn state rides on the message. Every reply-workflow send stamps
turn_statein Slack message metadata: the work-ack saysworking; every other send saysfinal. The names are A2A's task states, withcompletedspelledfinal. Slack keeps a message's metadata on achat.updatethat omits it (verified live 2026-10-19 in#apartment-yale-test), so the ~30s progress edits leave a stubworkinguntil its final edit flips it. Defaultfinal: 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.replieswithinclude_all_metadata). An unmarked message, from a seat on older code, is never treated as pending.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 turnfinaland 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.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 readsemoji.listat runtime (the bot hasemoji: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).Who drives, read in this order:
- 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
userscan. - Otherwise, a sticker placed by a seat.
- Otherwise, the area rule.
The area rule (lane ruling, fable-decide
fec94f788, refined byf72914c32): 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, andsrc/lib/domainis co-owned), and mapping all of propflowai to Trinity (against Gera, 10-14: "anything on Propflow's side is OUR plate").- 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
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.#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_CHANNELSandAGENT_CHANNEL_GATEare 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).
- Smith answers every person there before any driver router runs (
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,medorhigh). ROOT with low or med merges with no person; anything else, or no label in time, pages both founders.Acks are unchanged. 👀 to act and 👌 for no input, as agent-smith's ADR-0004 amendment of 2026-10-19 already records.
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_HOPSof 3. Ordinary helper turns are not hops; only a stamped @-mention of the sibling is.Amended 2026-11-03: a person's routing @-mention moves the driver (Gera, after
#engineering-bridgethread1793637694.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 loggeddriver=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).decidealready 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
Supersedes agent-smith's ADR-0004 rule 1 ("leader by address/topic": an @-mention is an ownership lock, otherwise route by topic lane) as the rule for who drives a thread. An @-mention still addresses one message to one seat.
Reverses agent-smith's ADR-0004 "REJECTED: a pre-speak claim signal" for N=2. The sticker is a claim signal, and a visible one. Its Deferred item "claim signal for un-addressed collisions — build if collisions recur" is now built: the collisions recurred (09-24, 10-06).
Replaces #937 item 9, Fede's first-seat rule. Amended as built:
first_seat_posteris NOT deleted. It stays for the guard branches, which keep both seats on the old rule together until both run the router:- no seat posted before the follow-up;
- a peer on older code (no
protocoltag and no stamped sticker from each seat).
It is deleted once both seats run the router and carry the
protocoltag.Leaves ADR-0051 standing. That record governs which session speaks for a seat in a human's thread. This one governs which seat drives. One voice per thread now holds at both levels.
Consistent with ADR-0056 agent-smith-channel: the card is a thread reply, never a top-level post.
Rejected
- A
work_claimslock on the thread. A deterministic router gives both seats the same answer, so a double stamp is a harmlessalready_reacted.key_for_thread_rootalso mints keys only under an alert marker. - A2A as a transport. It would be an off-Slack back-channel, which agent-smith's ADR-0004 rule 7 forbids. Its state names are borrowed.
agents.sessions.setStatusandchat.stopStreamnow. They need the app declared as an agent and a one-wayagent_viewswitch. That is the UX upgrade to plan before Slack retiresassistant_viewin Feb 2027, not this fix.- Pushing sibling edits over the socket (
message_changed). Whether the nested metadata arrives is unverified. The thread read is verified.
Build
Four vertical parts in agent-smith. Each is one PR off origin/main and works on its own:
- Final-state metadata plus wait-for-final.
- The router, sticker and driver card, replacing
first_seat_poster. Lands after #1270, on top of it. - The #alerts hand-off.
- The cross-review request and the critique prompt.
Consequences
- The two-seat proof needs Trinity's pull. Each seat updates on its own deploy. Until Trinity pulls part 1 on Fede's machine, she stamps nothing, so Smith never waits on her: the old behaviour, safe in either deploy order. The proof that Smith waits on her needs that pull.
- A slow but healthy sibling gets answered over at 180s. The bound runs from the stub's own
ts, which the ~30s progress edits never change, so a sibling still working on a long investigation stops being waited on after three minutes. The waiting seat then answers its own part and says the sibling's answer is still pending. That is the chosen trade: Gera ruled that each seat waits for the other's final answer, and the bound stops one stuck run from silencing the other seat. - The fallback stickers are temporary. They read as ordinary emoji until Gera uploads
:driver-smith:and:driver-trinity:. Nothing is redeployed when he does. - The area map can drift. If it goes stale, the default drifts toward "Smith always leads". The guard is that a hand re-sticker is the signal to amend the map, not a workaround.
- The seats can still disagree in three rare ways, all named in agent-smith's router docstring and ADR-0004:
- a re-sticker between the two seats' reads (reactions carry no time);
- a message before the follow-up deleted between the two reads (an abstain retracting its work-ack);
- an edit to the area map before both seats have deployed it. Deploy map edits to both seats together.
- Deploy forward only. A seat rolled back after tagged turns goes back to the first-poster rule while the other still routes by sticker.
- A hand-over sticker beside another seat sticker ties to Smith. Part 3 removes the handing seat's own sticker for exactly that reason.
- Live proof so far (2026-10-20, #apartment-yale-test):
- Smith's sticker was stamped on a test root, and
thread_driver.decideread it back assmith / sticker-seat. - A driver card was posted by
smith-driver-card, then edited in place frominvestigatingtodriving, with its metadata read back (thread). - Not yet shown: a seat waiting for the other's final answer. Trinity has not pulled part 1, so her posts carry no turn state.
- Smith's sticker was stamped on a test root, and
- Not proven end to end until a test-channel thread shows a sticker, a driver card, and one seat waiting for the other's final answer, with permalinks in the closing report.