How GoClaw decides which agent answers an inbound message: ResolveAgent matches the channel, peer, thread, tenant, roles and account against the routing rules in eight priority tiers, then builds the session key.
internal/routing/routing.go + internal/routing/identity_links.go + internal/routing/routing_test.go
flowchart TD
IN["<b>Inbound Message → RouteInput</b><br/>• Channel, AccountID, PeerKind (direct/group), PeerID<br/>• ParentPeerKind, ParentPeerID (thread inheritance)<br/>• TenantID, PlatformRoles[]"]
RES["<b>ResolveAgent(input) — 8-tier priority matching</b><br/>1. Peer match (exact channel + peer)<br/>2. Parent peer match (thread inherits parent binding)<br/>3. Tenant + Roles (tenant + role intersection)<br/>4. Roles match (channel role/dept without tenant)<br/>5. Tenant match (tenant without role filter)<br/>6. Account match (account ID)<br/>7. Channel match (channel-wide, no peer filter)<br/>8. Default (config.ResolveDefaultAgentID)"]
KEY["<b>buildSessionKey(agentID, input)</b><br/>• ResolveIdentity() → cross-channel session sharing<br/>• Multi-agent: agent:channel:peer<br/>• Single-agent: channel:peer (backward compat)<br/>• Output: AgentID + SessionKey + MatchedBy"]
IN --> RES --> KEY
| # | Feature | OpenClaw | GoClaw | Status |
|---|---|---|---|---|
| R.1 | Multi-agent routing | ✅ | ✅ ResolveAgent() — 8-tier priority |
Done |
| R.2 | Peer binding (channel+peer) | ✅ | ✅ Exact peer match (kind + ID) | Done |
| R.3 | Thread inheritance | — | ✅ ParentPeerID → inherits parent binding | GoClaw only |
| R.4 | Tenant + Role matching | — | ✅ Tenant + platform role intersection (Discord guild and role IDs; App API) | GoClaw only |
| R.5 | Tenant-wide binding | ✅ | ✅ TenantID match (Discord guild, Teams group conversation) | Done |
| R.6 | Account binding | ✅ | ✅ Account ID → specific agent (the bot on Teams, Telegram and Facebook; the sender on Discord) | Done |
| R.7 | Channel-wide binding | ✅ | ✅ All messages on channel → agent | Done |
| R.8 | Default agent fallback | ✅ | ✅ config.ResolveDefaultAgentID() |
Done |
| R.9 | Identity-linked sessions | — | ✅ ResolveIdentity() — cross-channel session sharing |
GoClaw only |
| R.10 | Multi-agent session keys | — | ✅ agent:channel:peer format |
GoClaw only |
| R.11 | Peer kind normalization | — | ✅ "channel" → "group" alias | GoClaw only |
| R.12 | Debug logging | ✅ | ✅ debug.Log("routing", ...) |
Done |
A binding can only match what the channel puts on the message:
| Channel | Account ID | Peer ID | Tenant ID | Platform roles |
|---|---|---|---|---|
| Discord | the sender's user ID | dm:<user ID> in a DM; <channel ID>:<user ID> in a guild channel, for messages, reactions and slash commands alike; the parent is the channel itself, or for a thread the channel it belongs to |
guild ID | the member's role IDs |
| MS Teams | the bot's app ID | conversation ID (;messageid=… removed) |
the conversation ID in a group chat or channel; empty in a 1:1 chat | none |
| Telegram | the bot's ID | chat ID | none | none |
| the page ID | sender's page-scoped ID | none | none | |
| App API | app_api |
the employee's user ID | from the caller's identity | from the caller's identity |
A Discord guild message's peer ID includes the user, so a peer binding with a bare channel ID matches through the parent: every message in that channel and in its threads. Route a whole Discord server with tenant_id. Teams never supplies roles, so a Teams binding with platform_roles never matches.
agents:
default: support-bot
list:
- id: support-bot
model: gemini/gemini-2.5-flash
- id: dev-bot
model: claude/claude-sonnet-4-5-20250929
bindings:
- match:
channel: discord
peer:
kind: group
id: "123456789012345678" # a Discord channel and its threads
agent_id: dev-bot
- match:
channel: discord
tenant_id: "112233445566778899" # a Discord server (guild ID)
platform_roles: ["998877665544332211"] # Discord role ID
agent_id: dev-bot
- match:
channel: msteams
tenant_id: "19:0123456789abcdef0123456789abcdef@thread.v2" # one Teams group chat or channel
agent_id: dev-bot
- match:
channel: discord # the rest of Discord
agent_id: support-bot
Coverage: every feature in the table: routing with 8 priorities, identity links, thread inheritance.