The thread director
The thread director gives a conversation the floor: one bot replies, or everybody stays quiet. It should notice a useful specialist, honor an explicit request, let humans finish a thought, and stop when the conversation needs nothing more.
It runs inside the backstage. A wake says that a thread may have new work; the director catches up with that thread’s messages under one lease.
message → wake → claim the thread lease ↓ read what’s new ↓ wait for a human burst, or choose ↓ one bot replies, or silence ↓ record progress → read again → idleWho gets the floor
Eligibility is a rule; usefulness is a judgment. The director applies the rules first and asks a model only when the remaining choice needs judgment. Being eligible does not by itself mean a bot should speak.
Only current, active participant bots can be chosen. The director refreshes the roster and configuration before each decision, so removing a bot or changing its trigger mode affects the next choice. Archiving a thread also stops selection after the current turn; archived history stays readable. A reply already in progress is not cancelled.
A bot’s own turn lands as an assistant entry; a chat entry from a bot can only arrive from outside the room (a post_to_thread from another thread, a workflow’s bot), so it restarts the bot-turn count exactly as a human message does. An empty bot chat (an uploaded attachment) does not.
Historical authors keep their human or bot identity after leaving the thread or house. Known System authors retain their identity, so workflow instructions can trigger the assigned always bot. System messages do not open human mentions or reset the bot-turn count. An unresolved author stays unknown; it never becomes a human by default, resets the bot-turn count, or creates a mention obligation.
A bot’s trigger mode says when it is eligible: mention needs an open human @handle, ambient needs something new from a human it could usefully answer, always owes a reply to any message it did not write. An always pick is free, subject to its turn limit; when several owe one, the bot that spoke least recently goes first.
A plain name is a topic hint, not a summons. Bots mentioning other bots create no obligation. An unanswered human mention stays open until that bot speaks or fails permanently; several mentions are served in order across successive decisions.
The oldest open mention is normally a free pick. If an older unanswered human message precedes it, the model considers that older need first. An always bot that owes a reply still takes a free turn before the model. The exact precedence belongs to the selection rules.
The judgment call is about what the human still needs. A useful answer should end the exchange; agreement, a paraphrase, or a goodbye does not earn another turn. Human-to-human conversation should be allowed to pass without a bot coda. A lone ambient bot still needs this judgment: one available speaker does not mean an answer is wanted.
The model sees a bounded conversation window, with older context separated from entries new since the last decision. Candidate cards carry each bot’s persona, trigger mode, and reason for eligibility. It returns one candidate or nobody. Unreadable entries appear as [unreadable entry], count toward the context budget, and cannot justify an invented answer. The decision prompt owns the wording of that judgment.
Pace and turn-taking
Threads with one active participant bot start deciding immediately by default. Multi-bot threads keep a three-second pause so human bursts such as “Plan a rainy afternoon in Oslo”, “With a six-year-old”, and “Near the station by five” can become one request. Each recent human message postpones the decision until typing pauses. An explicit dispatch.debounceMs inherited by or set on the thread wins over the one-bot default; zero disables the wait. A null leaf explicitly resets to the configured app default. Adding or removing bots recalculates the default on the next pass. The trace reports the effective pause; generic config reads show the scope-merged setting before participant-based defaults.
Debounce helps before a decision starts. It does not revise a bot’s answer already in progress. New messages arriving during a decision or reply remain unread and receive a later pass. Interrupting or updating an in-flight answer is not implemented.
Each bot has a turn limit, dispatch.botTurnLimit, default three: it rests after that many consecutive bot turns since the last human message. A contiguous run of entries by one bot is one turn, so a tool post or GIF is not an extra turn. A new human message resets the run, an open human mention overrides resting, and when every candidate is resting the thread goes quiet. Zero disables the limit; bots then continue until the house spend cap stops Arbe-funded calls, and your own provider key is uncapped. The limit bounds bot-to-bot talk; it does not replace the model’s job of stopping an ambient exchange once the human is answered.
Two more brakes. arbe thread pause <id> makes the thread mention-only and arbe thread resume <id> drops that override (it does not restore an older one); these are config shortcuts, the director never reads chat text as a command, and a reply in progress is not cancelled. An unlimited room also settles after dispatch.settledBotTurnThreshold consecutive bot replies (default eight) of at most eight words with no question and no tool call; the trace records conversation settled …. An open mention or a new human message wakes it; zero disables the rule.
Catch-up and recovery
A wake carries no message to process. It asks the director to read past the thread’s decided offset—the point through which previous decisions have been settled. One decision covers only the entries already present when its window was read. Advancing that offset must never swallow a newer message.
One lease allows one active director to drive a thread. The holder renews it during work and checks its authority before writes, including writes inside the bot turn. A competing wake yields; the holder keeps reading until caught up. Lease loss stops the old holder from continuing to write.
Picks and silence decisions over new conversation are persisted, whether chosen by rules or a model. A bot attempt then records an outcome tied to its decision: a reply or a failure. Windows containing only bookkeeping advance without another decision, so the director does not keep reacting to its own records.
After a restart, a persisted decision without a settled outcome resumes its chosen bot instead of paying to choose again. If the outcome already landed, recovery advances progress. Transient failures retry the same decision; three failures, or one permanent failure such as an invalid model, end that attempt so the thread can move on. See settlement.
This is crash recovery, not exactly-once model execution. A crash before a paid result or reply is durably acknowledged can repeat paid work or visible text. The durable decision and outcome narrow that uncertainty; they do not eliminate it.
A five-minute scan wakes stranded threads whose recorded head is ahead of their decided progress and which have no queued wake. A [thread-director.scan] stranded thread log means delivery or catch-up failed somewhere. Investigate it; the scan is the recovery path.
The backstage wake loop owns the lease, reads, decisions, turns, and progress writes. The conversation supplies replay evidence; the thread’s state row tracks the lease and catch-up position.
Inspect and prove it
Use an explicit house with every thread command:
arbe thread trace <thread> --house <house>arbe thread director <thread> --house <house>arbe thread entries list <thread> --house <house>trace shows persisted decisions, outcomes, and reply timings. director shows whether the thread is caught up and whether a lease is held. entries list shows what people and bots actually said. A missing reply is not proof of intentional silence: inspect the director state and failures before judging the conversation. Intentional silence has a saved nobody decision with its reason.
Separate “who spoke?” from “was it natural?” Explicit mention order, no repeated ambient answer, turn limits, and eventual catch-up are mechanical checks. Relevance, timing, responding to the whole thought, and leaving humans room need the conversation itself. Reply latency must distinguish time spent waiting for the director, deciding, handing off, and generating; a correct choice can still arrive too late.
The live scenarios describe these expectations as conversations. Follow run-scenarios, use a fresh test house, and keep thread IDs with the result. A scenario’s input must actually exercise its claim: labels for two humans are insufficient if both posts have the same author, and a slow test driver cannot prove burst handling.
For a stuck thread, start with debugging. arbe thread wake <thread> --house <house> requests catch-up again; it does not explain why the previous wake failed.