Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions api/message-routing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@

### 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.

Check warning on line 26 in api/message-routing.mdx

View check run for this annotation

Mintlify / Mintlify Validation (qwibitai-nanoclaw-8) - vale-spellcheck

api/message-routing.mdx#L26

Did you really mean 'DMs'?

### 3. Unwired channel handling

Check warning on line 28 in api/message-routing.mdx

View check run for this annotation

Mintlify / Mintlify Validation (qwibitai-nanoclaw-8) - vale-spellcheck

api/message-routing.mdx#L28

Did you really mean 'Unwired'?

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:

Check warning on line 34 in api/message-routing.mdx

View check run for this annotation

Mintlify / Mintlify Validation (qwibitai-nanoclaw-8) - vale-spellcheck

api/message-routing.mdx#L34

Did you really mean 'namespaced'?

Check warning on line 34 in api/message-routing.mdx

View check run for this annotation

Mintlify / Mintlify Validation (qwibitai-nanoclaw-8) - vale-spellcheck

api/message-routing.mdx#L34

Did you really mean 'upserts'?

```typescript
// User ID format: channelType:handle
Expand All @@ -40,7 +40,7 @@

### 5. Fan-out

Each wired agent is evaluated independently. Message IDs are namespaced by agent group ID to prevent collisions.

Check warning on line 43 in api/message-routing.mdx

View check run for this annotation

Mintlify / Mintlify Validation (qwibitai-nanoclaw-8) - vale-spellcheck

api/message-routing.mdx#L43

Did you really mean 'namespaced'?

### 6. Engage evaluation

Expand All @@ -58,17 +58,37 @@

## Module hooks

The router accepts optional pluggable hooks:

Check warning on line 61 in api/message-routing.mdx

View check run for this annotation

Mintlify / Mintlify Validation (qwibitai-nanoclaw-8) - vale-spellcheck

api/message-routing.mdx#L61

Did you really mean 'pluggable'?

| 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 |

Check warning on line 68 in api/message-routing.mdx

View check run for this annotation

Mintlify / Mintlify Validation (qwibitai-nanoclaw-8) - vale-spellcheck

api/message-routing.mdx#L68

Did you really mean 'unwired'?

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`:

```sql
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
Expand Down
36 changes: 35 additions & 1 deletion features/messaging.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
</Step>

<Step title="Thread policy">
Non-threaded adapters (Telegram, WhatsApp, iMessage) collapse `threadId` to null.

Check warning on line 25 in features/messaging.mdx

View check run for this annotation

Mintlify / Mintlify Validation (qwibitai-nanoclaw-8) - vale-spellcheck

features/messaging.mdx#L25

Did you really mean 'iMessage'?
</Step>

<Step title="Messaging group lookup">
Expand All @@ -30,7 +30,7 @@
</Step>

<Step title="Sender resolution">
The permissions module extracts a namespaced user ID (e.g., `tg:123456`) and upserts the user record.

Check warning on line 33 in features/messaging.mdx

View check run for this annotation

Mintlify / Mintlify Validation (qwibitai-nanoclaw-8) - vale-spellcheck

features/messaging.mdx#L33

Did you really mean 'namespaced'?

Check warning on line 33 in features/messaging.mdx

View check run for this annotation

Mintlify / Mintlify Validation (qwibitai-nanoclaw-8) - vale-spellcheck

features/messaging.mdx#L33

Did you really mean 'upserts'?
</Step>

<Step title="Fan-out to agents">
Expand Down Expand Up @@ -85,7 +85,41 @@

## 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.
Each inbound message is wrapped in an XML element so the agent can see who sent it, when, and from which destination. The `from` attribute carries the destination name resolved from the message's channel and platform ID — the agent uses it to address responses correctly.

| Kind | Format |
|------|--------|
| `chat` | `<message id="..." from="..." sender="..." time="..." reply_to="...">...</message>` |
| `task` | `<task from="..." time="...">Instructions: ...</task>` |
| `webhook` | `<webhook from="..." source="..." event="...">{...payload...}</webhook>` |
| `system` | `<system_response from="..." action="..." status="...">{...result...}</system_response>` |

If routing fields can't be matched to a known destination, `from` falls back to `unknown:<channel_type>:<platform_id>` so nothing is silently dropped.

## Response wrapping

The agent **must wrap every response** in a `<message to="name">...</message>` block — this is required even when only one destination is configured. The destinations and required syntax are injected into the system prompt at wake time:

```
## Sending messages

You can send messages to the following destinations:

- `discord-test`
- `slack-test`

**Every response must be wrapped** in a `<message to="name">...</message>` block.
```

Multiple `<message>` blocks can be included in a single response to fan out to several destinations. Text outside of `<message>` blocks is treated as **scratchpad** — logged but not sent. Use `<internal>...</internal>` to make scratchpad intent explicit.

### Per-destination thread resolution

In `agent-shared` sessions where one agent serves multiple messaging groups, each destination may have a different thread context. When dispatching a `<message to="name">` block, the agent runner resolves `thread_id` and `in_reply_to` from the most recent inbound message matching that destination's channel and platform — never from a single global routing context. This prevents one channel's thread from being stamped onto another.

### Compaction safety

Claude Code's PreCompact hook (`bun /app/src/compact-instructions.ts`) emits custom instructions during context compaction so the summary preserves the routing XML structure (`<message from="...">`, `<task from="...">`, `<webhook from="...">`) and the chronological exchange order. The agent must keep seeing which destination sent each message — otherwise it can't address replies correctly after a compaction. The hook is added automatically to every agent group's `settings.json` on init, and back-filled on existing groups.

## Channel-aware formatting

Expand All @@ -104,7 +138,7 @@
| WhatsApp | `**bold**` → `*bold*`, `*italic*` → `_italic_`, headings → bold |
| Telegram | Same as WhatsApp, but links preserved (Markdown v1) |
| Slack | Same as WhatsApp, but links become `<url\|text>` |
| Discord | Passthrough (Discord renders Markdown) |

Check warning on line 141 in features/messaging.mdx

View check run for this annotation

Mintlify / Mintlify Validation (qwibitai-nanoclaw-8) - vale-spellcheck

features/messaging.mdx#L141

Did you really mean 'Passthrough'?

Code blocks are always protected — their content is never transformed.

Expand All @@ -113,7 +147,7 @@
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

Check warning on line 150 in features/messaging.mdx

View check run for this annotation

Mintlify / Mintlify Validation (qwibitai-nanoclaw-8) - vale-spellcheck

features/messaging.mdx#L150

Did you really mean 'deduplication'?
- Sessions with running containers are polled every 1 second for outbound messages
- All active sessions are swept every 60 seconds

Expand All @@ -121,7 +155,7 @@

Every channel implements the same adapter interface. Chat SDK-backed channels use [Vercel's Chat SDK](https://chat-sdk.dev/docs/adapters); 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](/integrations/overview) for the full list.

Check warning on line 158 in features/messaging.mdx

View check run for this annotation

Mintlify / Mintlify Validation (qwibitai-nanoclaw-8) - vale-spellcheck

features/messaging.mdx#L158

Did you really mean 'iMessage'?

<Warning>
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.
Expand All @@ -148,10 +182,10 @@

## Channel approval

When a message arrives on an unwired channel (no agent wirings exist):

Check warning on line 185 in features/messaging.mdx

View check run for this annotation

Mintlify / Mintlify Validation (qwibitai-nanoclaw-8) - vale-spellcheck

features/messaging.mdx#L185

Did you really mean 'unwired'?

Check warning on line 185 in features/messaging.mdx

View check run for this annotation

Mintlify / Mintlify Validation (qwibitai-nanoclaw-8) - vale-spellcheck

features/messaging.mdx#L185

Did you really mean 'wirings'?

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)

Check warning on line 188 in features/messaging.mdx

View check run for this annotation

Mintlify / Mintlify Validation (qwibitai-nanoclaw-8) - vale-spellcheck

features/messaging.mdx#L188

Did you really mean 'DMs'?
3. **Deny** — future mentions on this channel are silently dropped

## Related documentation
Expand Down