Skip to main content

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:
ModeCondition
patternMessage matches engage_pattern regex ('.' = always)
mentionPlatform-level mention required
mention-stickyPlatform 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:
HookPurpose
setSenderResolverRuns before agent resolution — extracts user ID
setAccessGateRuns after agent resolution — enforces unknown_sender_policy
setSenderScopeGatePer-wiring sender scope enforcement
setChannelRequestGateEscalation for unwired channels
All hooks are optional. Without the permissions module, the system is allow-all.

Outbound delivery

Delivery polls

PollIntervalScope
Active1 secondSessions with running containers
Sweep60 secondsAll active sessions

Delivery pipeline

For each session with due outbound messages:
  1. Read from outbound.db (read-only)
  2. Filter already-delivered via inbound.db’s delivered table
  3. Route by kind:
    • system — dispatch to registered delivery action handlers
    • channel_type='agent' — agent-to-agent module
    • Normal — permission check, then channel adapter delivery
  4. Mark delivered in inbound.db
  5. 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';
Last modified on April 22, 2026