Documentation Index
Fetch the complete documentation index at: https://docs.nanoclaw.dev/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 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';