Documentation Index
Fetch the complete documentation index at: https://qwibitai-nanoclaw-8-mintlify-explicit-destination-addressin.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
The v2 message router handles inbound message evaluation, fan-out to wired agents, and outbound delivery through session databases.
Inbound routing pipeline
The router (src/router.ts) processes inbound messages through these stages:
1. Thread policy
Non-threaded adapters collapse threadId to null:
// Telegram, WhatsApp, iMessage don't support threads
if (!adapter.supportsThreads) {
message.threadId = null;
}
2. Messaging group lookup
Combined query for messaging group and wired agent count. Messaging groups are auto-created only on mentions or DMs — plain chatter is silent.
3. Unwired channel handling
If no agents are wired and it’s a mention, the channel-request gate escalates to the owner for approval.
4. Sender resolution
The permissions module extracts a namespaced user ID and upserts the users row:
// User ID format: channelType:handle
// Examples: phone:+15551234567, tg:123456, discord:789012
5. Fan-out
Each wired agent is evaluated independently. Message IDs are namespaced by agent group ID to prevent collisions.
6. Engage evaluation
Per-agent decision based on the wiring’s engage_mode:
| Mode | Condition |
|---|
pattern | Message matches engage_pattern regex ('.' = always) |
mention | Platform-level mention required |
mention-sticky | Platform mention OR existing active session |
7. Delivery
Engaging agents get a session write and container wake. Non-engaging agents with ignored_message_policy='accumulate' get the message stored with trigger=0.
Module hooks
The router accepts optional pluggable hooks:
| Hook | Purpose |
|---|
setSenderResolver | Runs before agent resolution — extracts user ID |
setAccessGate | Runs after agent resolution — enforces unknown_sender_policy |
setSenderScopeGate | Per-wiring sender scope enforcement |
setChannelRequestGate | Escalation for unwired channels |
All hooks are optional. Without the permissions module, the system is allow-all.
Outbound dispatch (in-container)
Before delivery, the agent runner parses the agent’s final text and dispatches each <message to="name">...</message> block to the named destination.
- Bare text outside
<message> blocks is scratchpad — logged but never sent.
<internal>...</internal> makes scratchpad intent explicit and is stripped before logging.
- Wrapping is required, even with a single configured destination. There is no fallback that sends bare text to the originating channel.
Per-destination thread resolution
For each dispatched block, thread_id and in_reply_to are resolved per destination by querying the most recent inbound message that matches the destination’s channel_type and platform_id:
SELECT thread_id, id FROM messages_in
WHERE channel_type = ? AND platform_id = ?
ORDER BY seq DESC LIMIT 1
This matters in agent-shared sessions, where one session serves multiple messaging groups with distinct thread contexts. If the lookup misses, the runner falls back to the session’s inbound routing context.
Outbound delivery
Delivery polls
| Poll | Interval | Scope |
|---|
| Active | 1 second | Sessions with running containers |
| Sweep | 60 seconds | All active sessions |
Delivery pipeline
For each session with due outbound messages:
- Read from
outbound.db (read-only)
- Filter already-delivered via
inbound.db’s delivered table
- Route by
kind:
system — dispatch to registered delivery action handlers
channel_type='agent' — agent-to-agent module
- Normal — permission check, then channel adapter delivery
- Mark delivered in
inbound.db
- Clean up
outbox/ files (best-effort)
Delivery actions
Modules register handlers via registerDeliveryAction(action, handler):
registerDeliveryAction('schedule_task', handleScheduleTask);
registerDeliveryAction('cancel_task', handleCancelTask);
// etc.
Retry behavior
- 3 attempts per message
- Permanently failed after exhausting retries
- Attempt counter resets on process restart
Types
EngageMode
type EngageMode = 'pattern' | 'mention' | 'mention-sticky';
SenderScope
type SenderScope = 'all' | 'known';
IgnoredMessagePolicy
type IgnoredMessagePolicy = 'drop' | 'accumulate';
SessionMode
type SessionMode = 'shared' | 'per-thread' | 'agent-shared';
MessageInKind
type MessageInKind = 'chat' | 'chat-sdk' | 'task' | 'webhook' | 'system';
MessageInStatus
type MessageInStatus = 'pending' | 'processing' | 'completed' | 'failed';
UnknownSenderPolicy
type UnknownSenderPolicy = 'strict' | 'request_approval' | 'public';