# 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 stream
```

Three 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](thread-director.md).

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.<br>
See [when bots reply](agent-trigger-mode.md) for which bots the director is even choosing between, [thread director](thread-director.md) for the choice, [dispatch](dispatch.md) for the turn, and [threads](threads.md) / [agents](../access/agents.md) for the nouns.

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