# 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)
                          ▼
                       DISPATCHER  cheap routing: mention + always; loop guard, fanout cap
                          │ eligible
                          ▼
                       BOT GATE    ambient only: gating prompt, topic match
                          │ yes
                          ▼
                       LLM TURN    via buildMessages(selfId, entries)
                          │
                          ▼
                       appends pi.assistant (authored by the bot) to the same stream
```

Three layers, each with one job. The thread stream is an append-only multi-party log, no alternation constraints — there is no separate per-bot stream, just one shared stream where each agent's entries carry its `authorId`. The dispatcher is a pure `(event, roster, policy) → wake list`; cross-bot rules (loop guard, fanout cap, fairness, dedup) need a global view, so it stays central. The per-bot LLM view is a disposable projection — system prompt is `<persona> + <thread context> + <agent roster>`. Cost-split: cheap dispatcher first (mention + always routing, loop guard `M events per T seconds without human input`), then the optional bot-gate (gating prompt, topic match) for ambient.

Projection rules in `buildMessages(selfId, entries) → pi.Message[]` (`packages/core/dispatch/dispatch.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: dispatcher + projection in `packages/core/dispatch/` (`dispatch.ts` holds `buildMessages` and the turn loop, `selection.ts` the routing).<br>
See [threads](./threads.md), [system/dispatch](./dispatch.md), [system/agents](./agents.md).

_Deferred: per-(bot, thread) trigger policy when one bot inhabits many threads; per-turn checkpoints for cheap replay._
