Multi-chat
A thread holds many humans + bots, yet each bot still gets a clean one-on-one LLM call without lying about authorship. One durable stream is what was said and what each bot did, all interleaved and author-stamped; dispatch decides who wakes; buildMessages projects that shared history into pi messages from one bot’s point of view.
Alice, Bob, bot-X ──► THREAD STREAM (arbe-thread-{id}) chat · pi.assistant · pi.tool_result · signal.* │ every entry author-stamped (authorId) ▼ DIRECTOR catches unread entries up to the read-time head │ one lease per thread │ free rule or one cheap model call: one speaker or nobody ▼ LLM TURN via buildMessages(selfId, entries) │ ▼ appends pi.assistant (authored by the bot) to the same streamThree layers, each with one job. The thread stream is an append-only multi-party log with every entry carrying its authorId. The director catches up from decided_offset and derives open mentions and the per-bot turn limit from the stream. A paid verdict or picked bot records a decision; a bot attempt adds its tagged outcome. The per-bot LLM view is a disposable projection whose system prompt is <persona> + <thread context> + <participant list>. A paid director choice uses its own small pinned model; only the chosen bot pays for a full turn. See thread director.
Projection rules in buildMessages(selfId, entries) → pi.Message[] (packages/core/dispatch/build-messages.ts): a bot’s own pi.assistant/pi.tool_result entries pass through (toolCall↔toolResult pairs preserved — providers require it; a missing result is back-filled so the next complete() never sees a dangling call); another agent’s chat becomes a labelled [Alice]: … user turn; another bot’s assistant text becomes a labelled user turn too, so the target bot can tell speakers apart; other authors’ tool results, chunks, compaction, and lifecycle entries are skipped. Pi’s UserMessage is a projection output, never storage.
chat.chunk streams a bot’s final text turn for live UX; tool activity never chunks. Resume a bot by reading the thread and re-running the projection — the stream is the only durable handle, the projection is recomputed, never stored. A coding session is just a thread with one human + one driving bot (agentId set).
Code: projection + turn execution live in packages/core/dispatch/; entry parsing, rules, settlement, and the paid choice live in packages/core/thread-director/. apps/backstage/src/thread-director-wake.ts runs both under the thread lease.
See when bots reply for which bots the director is even choosing between, thread director for the choice, dispatch for the turn, and threads / agents for the nouns.
Deferred: per-(bot, thread) trigger policy when one bot inhabits many threads; per-turn checkpoints for cheap replay.