Skip to main content

Gateway

The Gateway is the server component that sits between incoming channel messages and the agent that handles them. It resolves threads, selects agents, manages conversation state, and routes replies back to the originating platform.

What the Gateway does

When a plugin calls gateway.routeMessage(), the server runs this sequence:

GatewayMessage arrives

├─ 1. Thread resolution
│ Build a lookup key from (sourceId + strategy-specific field)
│ Look up threadId in the persisted thread map
│ Create a new thread if none exists

├─ 2. Agent resolution
│ Use the agentId from the message, or the channel's default agent

├─ 3. Process check
│ Is an agent already RUNNING on this thread?
│ ├─ Yes → queue the message as a PENDING step
│ └─ No → spawn a new agent process and send the message

└─ 4. Reply registration
Register a ResponseListener (30 min TTL) with a callback
Callback: send agent reply back to the plugin over WebSocket

Thread maps are persisted to .codebolt/gateway-thread-map.json so conversation history survives server restarts.

Thread strategies

The threadStrategy field in GatewayMessage controls how the Gateway maps platform conversations to Codebolt threads.

StrategyLookup keyUse when
per-usersourceId + userIdOne persistent conversation per user (DMs, personal bots)
per-conversationsourceId + externalThreadIdOne thread per group chat or channel thread
per-messagesourceId + timestampStateless — each message starts fresh
singlesourceIdAll messages on one shared thread (rare)
existingUse threadId directlyManual thread assignment

Example — choosing a strategy in a Discord plugin:

plugin.gateway.routeMessage({
source: 'channel',
sourceId: plugin.pluginId,
threadStrategy: 'per-conversation', // one thread per Discord channel
externalThreadId: message.channelId, // Discord channel ID as thread key
userId: message.author.id,
text: message.content,
replyTo: {
channelId: registeredChannelId,
externalThreadId: message.channelId,
userId: message.author.id,
},
});

GatewayMessage format

interface GatewayMessage {
source: 'channel' | 'hook' | 'webhook' | 'calendar';
sourceId: string; // plugin ID or webhook ID
threadStrategy: ThreadStrategy;
threadId?: string; // set only when strategy is 'existing'
agentId?: string; // use channel default if omitted
text: string;
userId?: string; // required for per-user strategy
externalThreadId?: string; // required for per-conversation strategy
metadata?: Record<string, any>;
replyTo: {
channelId: string;
externalThreadId?: string;
userId?: string;
pluginId?: string; // filled in by server from plugin connection
};
timestamp?: string;
}

RoutingResult

Every routeMessage() call returns a RoutingResult:

interface RoutingResult {
success: boolean;
action: 'routed-to-running' | 'spawned-new' | 'queued' | 'failed';
threadId: string;
agentId?: string;
instanceId?: string;
error?: string;
timestamp: string;
}
  • spawned-new — a new agent process was started for this thread
  • routed-to-running — the message was delivered to an already-running agent
  • queued — agent is busy (stopping), message queued as PENDING step

Channel registration

Plugins register their channels with the Gateway so they appear in the Routing Rules UI:

plugin.gateway.registerChannel({
channelId: 'my-discord-server',
name: 'Discord — My Server',
platform: 'discord',
agentId: config.agentId,
threadStrategy: config.threadStrategy,
});

Registration persists to .codebolt/channels.json. On disconnect, call plugin.gateway.unregisterChannel(channelId) to mark it as disconnected.

Response routing

The Response Router runs inside the server and intercepts all sendMessage() calls from agents on gateway-managed threads. When it finds a registered listener for the thread, it calls the listener's callback, which sends the reply back to the plugin over WebSocket.

Listeners expire after 30 minutes of inactivity. The TTL resets each time a message arrives on the thread.

Proactive messages

An agent can send a message to a channel without waiting for user input:

// In an agent handler
await codebolt.channel.send({
channelId: 'registered-channel-uuid',
targetId: 'discord:channel-123',
text: 'Build finished successfully.',
});

The server looks up which plugin owns the channel and delivers the message via gateway.messageToChannel.

See also