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 inbox/outbox model

Every agent session has two SQLite files:
  • inbound.db — anything the agent needs to see. User messages, webhooks, scheduled tasks when they come due, agent-to-agent requests. The host writes; the container reads.
  • outbound.db — anything the agent wants to do. Replies, tool calls, schedule requests, sub-agent spawns. The container writes; the host reads.
One writer per file means no SQLite cross-mount contention, no stdin piping, no IPC coordination layer. Every inbound source uses the same messages_in table; every outbound action uses the same messages_out table. Scheduling, channels, and agent-to-agent routing are the same pattern with different metadata.

Message flow

1

Message arrives

A channel adapter receives a message and calls the onInbound callback.
2

Thread policy

Non-threaded adapters (Telegram, WhatsApp, iMessage) collapse threadId to null.
3

Messaging group lookup

The router finds or auto-creates a messaging group based on the channel type and platform ID.
4

Sender resolution

The permissions module extracts a namespaced user ID (e.g., tg:123456) and upserts the user record.
5

Fan-out to agents

Each wired agent is evaluated independently against engage mode, sender scope, and access gates.
6

Session write

For engaging agents, the message is written to inbound.db and the container is woken.
7

Response delivery

The delivery system polls outbound.db and routes responses back through channel adapters.

Engage modes

Engage modes control when an agent responds to messages. Each wiring has its own engage mode:

Pattern mode

The agent responds whenever a message matches the engage_pattern regex:
  • '.' — matches all messages (always respond)
  • '^@Andy\\b' — matches messages starting with @Andy
  • Custom regex — any valid regex pattern

Mention mode

The agent only responds when explicitly mentioned at the platform level (e.g., @bot in Discord, direct reply in WhatsApp).

Mention-sticky mode

The agent responds to a platform mention OR if there’s an existing active session for this agent, messaging group, and thread combination. Once activated by a mention, the agent continues responding in that thread.
Messages that don’t trigger an agent can still be stored if the wiring has ignored_message_policy='accumulate'. These messages provide context when the agent is eventually triggered.

Sender scope

Per-wiring sender_scope controls who can trigger the agent:
ScopeBehavior
allAny user can trigger the agent
knownOnly owner, admin, or group members can trigger
sender_scope='known' provides an additional access layer on top of unknown_sender_policy. Even if a messaging group is public, a wiring with sender_scope='known' restricts its agent to authorized users.

Message formatting

Messages are formatted for the agent with sender information, timestamps, and metadata. The agent-runner inside the container handles formatting for the configured provider.

Channel-aware formatting

This feature requires the /channel-formatting skill. Apply it with:
git fetch upstream skill/channel-formatting
git merge upstream/skill/channel-formatting
When the channel-formatting skill is applied, outbound messages are automatically converted from Claude’s Markdown output to each channel’s native text syntax.
ChannelTransformation
WhatsApp**bold***bold*, *italic*_italic_, headings → bold
TelegramSame as WhatsApp, but links preserved (Markdown v1)
SlackSame as WhatsApp, but links become <url|text>
DiscordPassthrough (Discord renders Markdown)
Code blocks are always protected — their content is never transformed.

Concurrency

Container concurrency is managed globally:
  • Maximum concurrent containers: 5 by default (MAX_CONCURRENT_CONTAINERS)
  • Wake deduplication: concurrent wake calls for the same session share a single in-flight promise
  • Sessions with running containers are polled every 1 second for outbound messages
  • All active sessions are swept every 60 seconds

Channel routing

Every channel implements the same adapter interface. Chat SDK-backed channels use Vercel’s Chat SDK; native channels keep platform-specific clients behind the same callbacks (onInbound, onInboundEvent, onMetadata, onAction). Routing, fan-out, and delivery do not need platform-specific branches. Optional adapters live on the channels branch or in the current setup flows (Discord, Slack, Telegram, Signal, Teams, Google Chat, WhatsApp, Matrix, iMessage, GitHub, Linear, and more). Install one with /add-<name> or select it during bash nanoclaw.sh. See the integrations overview for the full list.
Channels are installed as skills, not configured through env vars or files. Use /add-telegram, /add-discord, etc. to copy an adapter module into your fork.

Delivery system

The delivery system uses a two-poll architecture:
  • Active poll (1s) — polls outbound.db for all running-container sessions
  • Sweep poll (60s) — polls all active sessions (catches messages from exited containers)
Delivery pipeline per message:
  1. Read due outbound messages from outbound.db
  2. Filter already-delivered via inbound.db’s delivered table
  3. Route by kind: system → delivery action handlers, agent → agent-to-agent module, normal → channel adapter
  4. Permission check for cross-channel delivery
  5. Retry up to 3 times on failure
Typing indicators are paused after each real user-facing delivery to avoid visual flicker.

Channel approval

When a message arrives on an unwired channel (no agent wirings exist):
  1. The router’s channel-request gate sends an approval card to the owner
  2. Approve — creates a wiring with defaults (mention-sticky for groups, pattern='.' for DMs)
  3. Deny — future mentions on this channel are silently dropped
Last modified on April 28, 2026