Priority: Medium
Hermes's gateway is what makes it a 24/7 agent — it pushes scheduled results, cron outputs, and subagent completions to wherever you are. Our Discord integration exists but is chat-only. A proper gateway enables the agent to deliver proactive messages, not just respond to incoming ones.
Depends on: #185 (cron engine needs delivery targets), #180 (MEMORY.md for session continuity across platforms)
Blocks: None
Acceptance Criteria
Coding Prompt
Files to modify: src/integrations/gateway/gateway-engine.ts (new), src/integrations/gateway/platform-adapter.ts (new), src/integrations/gateway/telegram-adapter.ts (new), src/integrations/gateway/slack-adapter.ts (new), src/integrations/gateway/whatsapp-adapter.ts (new), src/integrations/discord/ (enhance existing), src/agent/tool-registry.ts (register deliver tool), src/agent/tool-dispatcher.ts (handle deliver tool), src/server.ts (start gateway on boot), src/config/env.ts (GATEWAY_ENABLED, platform tokens)
Current behavior: The Discord integration in src/integrations/discord/ handles incoming messages and routes them to the agent. There is no outbound delivery mechanism — the agent can only respond in the same channel where the message came from. There is no Telegram, Slack, or WhatsApp integration. The Signal integration in src/integrations/signal/ exists but is receive-only.
Required change:
-
Create src/integrations/gateway/platform-adapter.ts:
interface PlatformAdapter {
platform: string;
send(userId: string, message: string, options?: DeliveryOptions): Promise<DeliveryResult>;
receive(): AsyncIterable<IncomingMessage>;
start(): Promise<void>;
stop(): Promise<void>;
isConnected(): boolean;
}
interface DeliveryOptions {
parseMode?: 'markdown' | 'html' | 'plain';
silent?: boolean;
replyToMessageId?: string;
}
interface DeliveryResult {
success: boolean;
messageId?: string;
platform: string;
timestamp: string;
suppressed: boolean; // true if [SILENT] was detected
}
interface IncomingMessage {
platform: string;
userId: string;
channelId: string;
content: string;
timestamp: string;
metadata?: Record<string, unknown>;
}
-
Create src/integrations/gateway/gateway-engine.ts:
GatewayEngine class that manages all platform adapters
start(): Initialize all configured adapters, begin listening
stop(): Graceful shutdown, wait for in-flight messages
send(platform, userId, message, options?): Route message to correct adapter
broadcast(message, platforms?): Send to all platforms for a user
- Session mapping:
data/gateway/sessions.json maps {userId, platform} → sessionId for cross-platform continuity
[SILENT] detection: if message contains [SILENT], return { suppressed: true } without sending
- Delivery logging: append to
data/gateway/delivery-log.json
-
Create src/integrations/gateway/telegram-adapter.ts:
- Use
node-telegram-bot-api (add as dependency)
- Polling mode (no webhook required for MVP)
- Handle
/start command to register user and link session
- Support markdown formatting
- Reconnect on disconnect with exponential backoff
-
Create src/integrations/gateway/slack-adapter.ts:
- Use
@slack/web-api and @slack/socket-mode (add as dependencies)
- Socket Mode for real-time messages (no public URL needed)
- Support thread replies and channel messages
- Handle app mentions and DMs
-
Enhance existing src/integrations/discord/:
- Add outbound delivery method (currently receive-only)
- Support DMs and channel messages
- Reuse existing Discord.js client
-
Create src/integrations/gateway/whatsapp-adapter.ts:
- Bridge through Signal (reuse existing
src/integrations/signal/)
- WhatsApp messages → Signal → gateway → agent
- Document the Signal-WhatsApp bridge setup in README
-
Register deliver tool:
platform: "telegram" | "discord" | "slack" | "whatsapp"
user_id: string (platform-specific user ID)
message: string
silent: boolean (optional, default false)
Code patterns to follow:
- Follow the existing adapter pattern from
src/integrations/discord/
- Use environment variables for platform tokens (consistent with
src/config/env.ts)
- AsyncIterable for message streams (follow Node.js AsyncGenerator pattern)
- Delivery logging follows the JSON file pattern from
ConversationManager
- Error handling: each adapter catches its own errors and reports via
DeliveryResult, never crashes the gateway
Files to add/update tests in: tests/unit/integrations/gateway/gateway-engine.test.ts, tests/unit/integrations/gateway/platform-adapter.test.ts
Reasoning: The gateway is what makes an agent proactive rather than reactive. Without it, the agent can only respond when you type. With it, scheduled tasks, reflections, and subagent results can reach you wherever you are. Cross-platform session continuity means you can start on Telegram and pick up on Discord without losing context.
Priority: Medium
Hermes's gateway is what makes it a 24/7 agent — it pushes scheduled results, cron outputs, and subagent completions to wherever you are. Our Discord integration exists but is chat-only. A proper gateway enables the agent to deliver proactive messages, not just respond to incoming ones.
Depends on: #185 (cron engine needs delivery targets), #180 (MEMORY.md for session continuity across platforms)
Blocks: None
Acceptance Criteria
src/integrations/gateway/gateway-engine.tscreated withGatewayEngineclasssend(userId: string, message: string, options?: DeliveryOptions)andreceive(): AsyncIterable<IncomingMessage>delivertool registered for sending messages to platforms[SILENT]trick: messages containing[SILENT]are suppressed (no delivery)data/gateway/delivery-log.jsontests/unit/integrations/gateway/Coding Prompt
Files to modify:
src/integrations/gateway/gateway-engine.ts(new),src/integrations/gateway/platform-adapter.ts(new),src/integrations/gateway/telegram-adapter.ts(new),src/integrations/gateway/slack-adapter.ts(new),src/integrations/gateway/whatsapp-adapter.ts(new),src/integrations/discord/(enhance existing),src/agent/tool-registry.ts(register deliver tool),src/agent/tool-dispatcher.ts(handle deliver tool),src/server.ts(start gateway on boot),src/config/env.ts(GATEWAY_ENABLED, platform tokens)Current behavior: The Discord integration in
src/integrations/discord/handles incoming messages and routes them to the agent. There is no outbound delivery mechanism — the agent can only respond in the same channel where the message came from. There is no Telegram, Slack, or WhatsApp integration. The Signal integration insrc/integrations/signal/exists but is receive-only.Required change:
Create
src/integrations/gateway/platform-adapter.ts:Create
src/integrations/gateway/gateway-engine.ts:GatewayEngineclass that manages all platform adaptersstart(): Initialize all configured adapters, begin listeningstop(): Graceful shutdown, wait for in-flight messagessend(platform, userId, message, options?): Route message to correct adapterbroadcast(message, platforms?): Send to all platforms for a userdata/gateway/sessions.jsonmaps{userId, platform}→sessionIdfor cross-platform continuity[SILENT]detection: if message contains[SILENT], return{ suppressed: true }without sendingdata/gateway/delivery-log.jsonCreate
src/integrations/gateway/telegram-adapter.ts:node-telegram-bot-api(add as dependency)/startcommand to register user and link sessionCreate
src/integrations/gateway/slack-adapter.ts:@slack/web-apiand@slack/socket-mode(add as dependencies)Enhance existing
src/integrations/discord/:Create
src/integrations/gateway/whatsapp-adapter.ts:src/integrations/signal/)Register
delivertool:platform: "telegram" | "discord" | "slack" | "whatsapp"user_id: string (platform-specific user ID)message: stringsilent: boolean (optional, default false)Code patterns to follow:
src/integrations/discord/src/config/env.ts)ConversationManagerDeliveryResult, never crashes the gatewayFiles to add/update tests in:
tests/unit/integrations/gateway/gateway-engine.test.ts,tests/unit/integrations/gateway/platform-adapter.test.tsReasoning: The gateway is what makes an agent proactive rather than reactive. Without it, the agent can only respond when you type. With it, scheduled tasks, reflections, and subagent results can reach you wherever you are. Cross-platform session continuity means you can start on Telegram and pick up on Discord without losing context.