Chat Pipeline
The chat pipeline turns a single Discord messageCreate event into zero, one, or
many persona replies. It is the spine of TomoriBot — most other AI subsystems
(context build, tool loop, provider streaming, memory capture) are reached from
inside this pipeline.
Entry point: src/events/messageCreate/tomoriChat.ts:tomoriChat()
Triggered by: every Discord messageCreate event (including bot/webhook
messages — filtering happens inside evaluateChatAdmission), plus internal
re-invocations from retries, queue replays, stop-response generation, boomerang
follow-ups, and command-driven manual triggers.
Read order
Section titled “Read order”This folder is numbered. Read the stage files top to bottom; the per-turn loop
lives in 06-per-turn/.
Stage flow
Section titled “Stage flow”tomoriChat(TomoriChatInput) │ ▼[01] normalizeChatInvocation → ChatIncoming │ ▼[02] evaluateChatAdmission → ChatAdmission │ (run | ignore | queued | blocked | error) │ disposition === "run"? │ no ─────────────────────────────→ [03] handleChatDisposition → end │ yes ▼[04] runWithChannelLock { ← concurrency wrapper (not a transform) │ ▼[05] planChatTurns → ChatTurnPlan { turns: ChatTurn[] } │ │ turns.length === 0? ────────────────→ release lock, replay queue, end │ else: for each turn: │ │ │ ▼ │ [06-per-turn] │ ├─ [01] buildChatTurnContext → ChatTurnContext │ ├─ [02] createChatResponseSink → ChatResponseSink │ ├─ [03] runGenerationTurn → GenerationTurnResult │ └─ [04] runPostTurnEffects │} ← lock released, queued messages replayedStage index
Section titled “Stage index”| # | Stage | File | Mission |
|---|---|---|---|
| 01 | normalizeChatInvocation | 01-normalize-invocation.md | Defensive input normalization. |
| 02 | evaluateChatAdmission | 02-evaluate-admission.md | Decide if/how this message turns into a generation. |
| 03 | handleChatDisposition | 03-handle-disposition.md | Terminal handler for non-run dispositions. |
| 04 | runWithChannelLock | 04-channel-lock.md | Per-channel mutex + typing keepalive + queue replay. |
| 05 | planChatTurns | 05-plan-turns.md | Persona selection + per-turn state assembly. |
| 06 | per-turn loop | 06-per-turn/ | Iterated once per responding persona. |
Cross-references
Section titled “Cross-references”- Per-turn stage 01 (build context) delegates to the context-build pipeline.
- Per-turn stage 03 (generation) delegates to the tool-loop pipeline and the provider pipeline.
- Per-turn stage 04 (post-turn effects) writes to
memory and may schedule cross-channel
work via the boomerang mechanism in
crossChannelMessageTool.
Concurrency model
Section titled “Concurrency model”- One channel ⇄ one active turn-sequence at a time. Enforced by
runWithChannelLock. - Messages arriving while a channel is locked are either enqueued for replay
after lock release, converted to a follow-up interrupt (if eligible),
converted to a natural-stop signal (if matching stop phrasing), or
dropped — full decision tree in
04-channel-lock.md. - Three recursive re-entries into
tomoriChat()are by design: empty-response retry (withskipLock=true), stop-response generation (after lock release, viahandleStopResponse), and boomerang follow-up (withsuppressNextSelfReply).
Quality notes
Section titled “Quality notes”tomoriChat.tsis the coordinator only — all stage implementations live undersrc/utils/chat/. The event-loader scanssrc/events/messageCreate/*.tsshallowly, so helper modules colocated with chat logic must not sit in that folder (they would be auto-registered as additional handlers).