All articles

GroupThink

Who Acts

20 min read

Every device on your account can send a message, and it does not matter which one does — the result is identical either way. Autonomous agent work is the opposite: run the note-taking pass on your phone, your iPad and your Mac and you get three notes; join one group conversation from all three and your agent turns up as three participants. So exactly one device has to act, which is a leader election — and there is no election service, no consensus protocol, and no server this system is willing to let watch which of your devices are awake.

Prerequisites: GroupThink for the sealed same-account channel these devices coordinate over, and Agent for what autonomous work actually is.


1. Two kinds of work

Start with the easy case; the hard one is defined by contrast.

You write a message on your iPad. It goes out to the recipient, and a sealed copy — a self-echo — goes to your other devices, which file it into their own stores. The reply comes back to all three, because a 1:1 message is addressed per device, and a device that was asleep backfills from a sibling. Now ask which of them "handled" that reply. The question has no content: each decrypted it, each filed it, each now holds the same row, and filing is keyed by envelope id, so a second attempt is a no-op.

DeviceRoleService's own header states the rule this produces: all of an account's devices are equal siblings for non-agentic actions — post or chat from any device, they fan out. Equality is not a courtesy. It is what makes the account survivable: your phone can be flat, your iPad in a drawer and your Mac shut, and any one of them coming up is sufficient.

Now the other kind. When a conversation accrues new material, the agent reads it and distills it into Notebook entries — a summary of the thread, a line on the person's page, a fact it learned about you. This is not a transform of a message that arrived. It is work the agent decided to do, and doing it means asking a language model a question.

Run that on three devices and you do not get one note three times. You get three notes.


2. Where idempotence stops

Idempotent means doing something twice leaves the same state as doing it once. Filing a message is idempotent, and why is exactly what the note pass lacks: a message carries an identifier every device computes the same way, so the second filing finds the first.

The note pass looks idempotent too, from inside one device. NotetakingService early-exits on a watermark when nothing is new, and NotebookStore.upsertClear will not overwrite a note you have edited; its header calls the pass "incremental + idempotent (re-running doesn't duplicate)." True — and scoped to one device.

Across devices it fails twice over. First, the model is not a function: three devices ask the same question about the same thread and get three different sentences, all reasonable, none equal. Second — the part that surprises people — each note is a new row, and new rows are precisely what the sync layer exists to propagate. Your phone's note reaches your iPad, your iPad's reaches your Mac, and every device ends up holding all three. The convergence machinery in Staying in Sync works perfectly, and its correct behaviour is what spreads the damage, because insert-if-absent cannot know that three distinct rows mean the same thing. There is nothing for it to deduplicate by.

The huddle case is worse because it is visible to other people. A huddle is a live conversation between the agents of a small mutually-trusting group, and joining one means becoming a member of a group with a cryptographic roster. Three devices joining means three of you in the room, each with a partial view of your context, each hearing the other two and treating them as independent participants. Your friends' agents watch your agent argue with itself. The huddle path in SocialServices says it in one line: only the driver joins, or the user's agent would appear N times in one cohort.

So the boundary is not "reads versus writes" or "cheap versus expensive." It is this: an operation is safe to run everywhere when its result is determined by data every device already has, and unsafe the moment the result is invented. An invented result has no prior identity, so nothing downstream can collapse two of them into one.

TRANSFORM OF RECEIVED DATA INVENTED RESULT A REPLY ARRIVES PHONE IPAD MAC ONE ROW SAME ID → SECOND FILING IS A NO-OP THE SAME REPLY ARRIVES PHONE IPAD MAC THREE ANSWERS — ALL REASONABLE, NONE EQUAL NO SHARED ID — NOTHING TO DEDUP BY SAFE EVERYWHERE WHEN DERIVED — INVENTED RESULTS HAVE NO PRIOR IDENTITY

3. The line the code draws

That distinction is not left to judgement at each call site. AgentRuntime is the single seam every agent behaviour passes through, and its header writes the litmus down:

Does it call the model / make a non-deterministic choice the user's single agent should own? → run it here. Is it an idempotent transform of received data? → leave it per-device.

AGENT WORK AGENTRUNTIME CALLS THE MODEL / DECIDES? YES E.G. CHOOSING A THREAD'S NAME DRIVER — HOLDS THE SEAT PHONE ONLY WHILE ISDRIVING ELSEWHERE RUN( ) → NIL IPAD MAC NO E.G. APPLYING A NAME A SIBLING DECIDED EVERY DEVICE, ALWAYS NON-DETERMINISM IS GATED TO THE DRIVER — TRANSFORMS RUN EVERYWHERE

On the gated side, AgentActivity is the catalogue: notetaking, saga, threadNaming, brief, huddle, followUpAsk, reachOutNudge, eveningCheckin. Eight things, each of which either asks a model a question or represents you outwardly to somebody else.

The ungated side is longer, and reading it corrects the impression "one device acts" leaves. Message decrypt, route and acknowledge; memory and Notebook writes that merely capture what happened; sync_delta traffic itself; fan-out and dedup; creating a plan proposal (dedup'd by id); applying a title a sibling already decided. All of that runs everywhere, always — as does anything you do with your hands, since the seat has no opinion about the device you are holding.

Note the pairing inside threadNaming. Choosing a thread's name is an LLM call, so it is gated; applying a name that arrived from a sibling is a transform of received data, so it is not. Same feature, opposite sides of the line, and the line is drawn by where the non-determinism sits.

The gate itself is one boolean on DeviceRoleService:

var isDriving: Bool { holdsSeat && !yielding }

AgentRuntime.run returns nil when this device is not driving, and callers treat nil as "a sibling has this." One asymmetry worth flagging: the AgentActivity you pass to run is not consulted in the decision. It documents intent at the call site and anchors handoff for the resumable activities; the answer is the same account-wide boolean for all eight.

One gated thing is not cognition at all: publishOwnKeyTransparencyIdentity advances a shared version counter two devices would race, so it rides the same check. Same shape, different problem.


4. An election with nothing to elect with

The requirement is now stated: exactly one device may run autonomous work, and which one changes as devices come and go. That is leader election, one of the oldest problems in distributed systems, with a large literature of good answers — almost all of which assume something this system deliberately does not have.

There is no election service: nothing in the deployment runs one. There is no consensus protocol either — no Raft, no Paxos, no quorum, and a quorum is an awkward fit anyway, since the typical account has two devices, a majority of two is two, and requiring both to be reachable would stop your agent the moment your iPad's battery dies.

There is a server that knows your devices exist. GET /v1/sync/siblings on identity-svc returns your account's device pubkeys, and it must: membership has to be server-authoritative or a removed device could talk itself back in (Joining and Leaving). But look at the query:

SELECT pubkey, mldsa_envelope_pubkey FROM identity_records
 WHERE user_id = $1 AND revoked_at IS NULL ORDER BY created_at DESC

A set and a registration order. Not one column about liveness. Giving it one is what the design refuses, and the refusal is not squeamishness: a presence table here is a minute-by-minute record of when you pick up which device, held by a server that already knows which account you are — a behavioural profile, produced by a feature whose entire purpose is internal bookkeeping.

The relay could not help even if you wanted it to. The heartbeats that carry seat state are ordinary sealed envelopes on a sibling subject — an opaque, rotating value derived from your account key and the receiving device's pubkey, computed exactly the way your conversations' pair subjects are. The relay sees a subject and a blob: it cannot separate your devices' traffic from a friend's, cannot count your devices, and has no way to answer "who is online" if asked. The payload is two bytes.


5. Three answers that do not survive contact

A lock row. The industrial answer, and a good one: put a row in a database, have each device try UPDATE seat SET holder = me WHERE holder IS NULL OR lease_expires < now(), and let the winner drive. It fails here twice. The first reason is §4's: a lease has to expire, so the server must be told continually that you are alive — the lock service is a presence service wearing a different hat. The second would bite even if you accepted the first. A lease length is a guess about how long a device may be unreachable while still being fine: set it short and your Mac loses the seat mid-huddle over a four-second Wi-Fi handoff, set it long and the seat sits dead for a minute after your phone's battery dies. Phones make that guess far harder than servers do (§7).

Whoever is awake first. Skip the coordination: each device checks whether anyone else seems to be driving, and drives if not. This is a race, and the interesting part is that it is not a rare one. Autonomous work is usually triggered by something arriving, and §1 established that an inbound envelope fans out to every device at once — so all your devices wake on the same event and run the same check against the same stale information. Simultaneous wake is not the corner case here; it is the normal case, and a protocol whose correctness depends on two devices not deciding at the same instant is being asked for the one thing this workload guarantees it will not get.

A timestamp tiebreak. Add an ordering: newest claim wins, or longest-awake wins. That requires two devices to agree what time it is, and they do not. A phone's clock is set by the OS, drifts between syncs, jumps at timezone and daylight-saving boundaries, and can be moved by the user. Clocks You Cannot Trust covers why cross-device wall-clock comparison is unsafe in general; the harm here is that it fails silently. A mis-resolved tiebreak does not throw. It elects the wrong device, and everything downstream behaves as though the election succeeded.

That left a visible mark on the code. Every time comparison in DeviceRoleService is between a timestamp it recorded itself (lastHeard[pubkey] = .now) and its own current time. Both endpoints of every subtraction are the same device's clock, so there is no skew for skew to corrupt.


6. The two failures, and why they pull apart

Two things can go wrong. They are opposites, and naming them is most of the work.

Orphaned control: nobody drives. No device believes it holds the seat, so no autonomous work runs anywhere. The symptom is silence — notes stop appearing, nudges stop arriving, a huddle invitation goes unanswered — and nothing errors, because from each device's point of view it is correctly deferring to a sibling. This is the failure with no alarm attached, which makes it the more dangerous of the two despite being the less dramatic.

Sustained overlap: two devices drive. Everything happens twice — duplicate notes, duplicate nudges, and, in the case that leaves your machines and reaches other people, two of your agents in one huddle.

Now the part that makes this a design problem rather than a matter of care. Consider a handoff of the seat from device B to device A. There is a moment B stops holding and a moment A starts, separated by a message crossing a network, so one happens first. If B releases first, there is an interval with no holder; if A then crashes, or never receives the message, that interval never ends and you have orphaned control. If B holds until it knows A has taken over, there is an interval with two holders, and that is overlap. There is no third ordering. You do not get to choose whether there is a window; only which window it is.

IF B RELEASES FIRST THE SEAT TIME → B A NO HOLDER → ORPHANED CONTROL ENDS ONLY BY TIMEOUT THERE IS NO THIRD ORDERING IF B HOLDS UNTIL IT KNOWS A HAS IT B A TWO HOLDERS → OVERLAP ENDS ON A POSITIVE SIGNAL YOU CHOOSE WHICH WINDOW EXISTS — NEVER WHETHER ONE DOES

That is why tuning does not rescue you. The one parameter available — how long to wait before assuming a device is gone — slides you along a single curve. Shorten it and you steal the seat from devices that were merely slow, producing overlap; lengthen it and the seat stays empty longer after a device dies, producing orphaned control. Aggressively avoiding one failure is a description of how to cause the other.

Given that, the design picks the overlap window — the one that ends on a positive signal rather than a timeout — and removes its teeth, by separating holding the token from acting on it, so a device can be the nominal holder while having already stopped doing anything. The Driver's Seat is that mechanism in full, and this article deliberately stops here.


7. "Online" is not a stable predicate

Both failure modes reduce to one question — is that other device up? — and phones are unusually bad at answering it. On a server fleet, "up" is close to a stable property: machines are plugged in, on a known network, and a few missed heartbeats really is evidence of trouble. None of that transfers.

  • iOS suspends an app shortly after you background it, and its sockets go with it. SocialServices tears the relay connections down explicitly, because a suspended process may freeze before its close frames land, leaving the relay counting a subscriber that is not there. On the wire, a suspended app and a destroyed one look identical.
  • Background execution happens at the OS's discretion. AgentBackgroundRunner schedules a BGProcessingTask for the agent's passes and its header is blunt that iOS "may defer or skip it" — foreground triggers remain the reliable path.
  • Radios idle to save power. Tunnels, lifts, aeroplane mode, a flat battery, a walk between cells.
  • The fleet is heterogeneous in exactly this dimension. A Mac acting as a full peer is usually awake and plugged in; a phone is neither. The device most reachable is not the one you use most.

So any answer to "is it online" has a half-life, and the code is careful about what its answer means. presenceWindow is 8 seconds, and its comment spends most of its length on what it is not: not a passive "we overheard something recently" decay, but "answered our most recent handshake." There is no periodic poll. The check is event-driven — the first call inside a handshakeDebounce window (5 seconds) pings every sibling asking for an immediate reply, waits one round trip (settleDelay, 1.5 seconds in the app, 0 in tests), then decides on whoever answered. An idle account generates no heartbeat traffic at all.

Notice the inversion. §5 said simultaneous wake is what makes the naive race unavoidable; it is also what makes an event-driven check cheap and well-timed, because every sibling handshakes on the same event and they converge precisely when there is work to converge about. The property that broke the easy answer is load-bearing for the real one.

The cost is that 8 seconds is aggressive: a device genuinely online but slow to answer one handshake reads as offline, and something else takes the seat. The code's defence is that seat assignment is per-event and self-correcting — a claim that only holds if the handoff is safe, which is the burden The Driver's Seat carries.


8. The backstops underneath

If the seat were a hard mutual-exclusion lock, no activity would need to defend itself against running twice. Every gated activity does anyway.

Note-taking early-exits on a watermark and writes through a path that will not clobber an edited note. The reach-out nudge carries a per-contact cooldown derived from the row itself, described in its comment as what makes "re-runs and driver handoffs idempotent, so firing on every foreground is safe." The evening check-in stamps the local calendar day before the slow generation runs, and names the case it is defending against: a handoff race. A live huddle refuses a second concurrent run on the same device outright.

Put plainly: the seat narrows duplicate autonomous work to a window, and each activity is expected to survive that window on its own. It is a filter, not a lock.

The evening check-in also leaks the cost model, in the ordering of its gate stack. isDriving is evaluated last, after seven purely local reads — feature on, home captured, visit ongoing, close enough to home, past the evening hour, not already fired today, not mid-conversation — and the comment says why: callers pre-flight with isDriving: true so they "only pay the driver's-seat relay handshake when everything else already passes." Read top to bottom, that gate stack is a list of predicates sorted by price.


9. The residual

Five things this framing does not give you, stated before you find them.

The gate fails open. Seat reconciliation begins by locating this device in the ranked roster, and if it cannot find itself it takes the seat unconditionally — the comment reads act solo so the agent never stalls. But "cannot find itself" also covers the case where the roster fetch failed, because siblings() yields an empty list on error. So an identity-svc outage, or a network blip all your devices hit at once, produces §6's sustained overlap on every device at once. That is a deliberate trade — an agent that keeps working over one that is provably singular — and the §8 backstops are what make it survivable. A chosen cost, and the largest hole in this article's property.

It is eventual, not instantaneous. Convergence is bounded by the presence window plus the handshake round, so there is a real interval after a device wakes in which the wrong device believes it drives.

The token is not the invariant. During a handoff two devices hold the token and only one acts. Exclusion is carried by isDriving, a conjunction of two flags, not by uniqueness of the token, so code that reads holdsSeat and thinks it asked "am I the agent" asked a different question.

Rank has nothing to do with fitness. The ordering derives from what identity-svc returns — registration order, newest first, reversed — which is uncorrelated with battery, thermal headroom, network quality, which model a device can run, or which device is in your hand. There is a user-facing override, covered in The Driver's Seat, but the default is an accident of history.

The seat does not partition work. One token, eight activities, and the activity is not an input to the decision. The device that drives drives everything: a Mac deep in a huddle is also expected to run the note-taking pass. Splitting the seat by activity is not something this design can express.

What remains is the mechanism: how the ordering is fixed without negotiation, and how the handoff makes the overlap window harmless. That is The Driver's Seat.


References & further reading