services/satori-bot/docs/HANDLER.md
This document outlines the complete lifecycle of a message within the Satori Bot, from the initial WebSocket event to the LLM's decision-making process.
The bot operates on a Event-Driven + Autonomous Loop hybrid model:
read_unread_messages (observe) or send_message (act).Location: src/adapter/satori/client.ts → src/core/loop/queue.ts
SatoriClient receives a raw JSON signal and parses it into a SatoriEvent.setupMessageEventHandler (in queue.ts) listens for message-created events.processedIds set (key: channelId-messageId) to prevent double-processing.event is wrapped into a { event, status: 'ready' } object.botContext.eventQueue and persisted to the database via pushToEventQueue.event.message.content, event.user.id, event.channel.id.Location: src/core/loop/scheduler.ts (Function: onMessageArrival)
When the system processing lock is free, it consumes events from the eventQueue:
channelId.ensureChatContext (in src/core/session/context.ts) to load or create the in-memory ChatContext for that channel.event.channel.id is the primary key for all context.selfId. If the sender is the bot itself, the event is removed from the queue and discarded (not counted as unread).botContext.unreadEvents[channelId] and persisted to the database via pushToUnreadEvents.removeFromEventQueue.loopIterationForChannel, waking up the Agent Loop for this specific channel.Location: src/core/loop/scheduler.ts → src/core/planner/llm-client.ts
The LLM is prompted not to "reply to this text," but to "decide the next action based on state."
imagineAnAction):
system-action-gen-v1 (Tool Definitions) and personality-v1 (Persona).chatContext.messages (Recent conversation turns).botContext.unreadEvents.Incoming events block at the end of the prompt.chatContext.actions to show the results of previous attempts (e.g., "Last action: read_messages, Result: Success").{"action": "read_unread_messages", "channelId": "..."}.Location: src/core/dispatcher.ts → src/capabilities/registry.ts
The system looks up the corresponding Handler in globalRegistry based on the JSON Action:
Case: read_unread_messages (src/capabilities/actions/read-messages.ts)
botContext.unreadEvents for the specified channel.[User]: Content).Action Result.unreadEvents for that channel. In the next Tick, the LLM will see this text in its History Actions and generate a reply.Case: send_message (src/capabilities/actions/send-message.ts)
unreadEvents again. If new messages arrived during generation, it might abort the send to prioritize reading.satoriClient.sendMessage.messages array.Location: src/core/loop/scheduler.ts (handleLoopStep)
dispatchAction returns an ActionResult containing a shouldContinue flag.shouldContinue is true (e.g., usually true after reading messages, as a reply is expected), the scheduler waits for LOOP_CONTINUE_DELAY_MS (default 2.5s) and then recursively calls handleLoopStep.MAX_LOOP_ITERATIONS = 5. Reaching this limit will force the loop to break.shouldContinue flag becomes false.