feat: gateway channel system — MCP server + router infrastructure - #72
Conversation
Build the autonomOS gateway infrastructure for structured messaging between the dashboard, CC sessions, and platform adapters (Discord, Telegram, Slack). Key components: - server:autonomos MCP channel server — standalone subprocess spawned by CC via --mcp-config, connects back to the gateway over WebSocket, exposes reply/send_to_agent/list_agents tools - Gateway router — routes messages between platform adapters and CC sessions, fans out to dashboard for observability - Platform adapter stubs — Discord, Telegram, Slack (interface only, implementations deferred) - /ws/gateway WebSocket endpoint for channel server ↔ server comms - Session spawn integration — injects --dangerously-load-development-channels and --mcp-config with the channel server config - Precompiled dist.mjs via esbuild so CC can spawn with plain `node` - Settings panel simplified to autonomOS Gateway toggle only (removed Discord/Telegram plugin toggles — gateway handles platforms directly) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| @@ -0,0 +1,248 @@ | |||
| #!/usr/bin/env node | |||
There was a problem hiding this comment.
🟡 Warning
Problem: dist.mjs is a compiled artifact committed to source. It is built from index.ts but only regenerated by make prod — not make dev. Any change to index.ts that lands without a matching rebuild will silently deploy stale code in production.
Why it matters: sessions.ts hardcodes the path to this file and spawns it with node. If it drifts from index.ts, Claude sessions will run the old channel server with no error — behaviour mismatch will be subtle and hard to debug.
Suggested fix:
// Add dist.mjs to .gitignore:
// packages/server/src/channel-server/dist.mjs
// And add the build step to make dev (watch mode):
// dev:
// @bunx esbuild packages/server/src/channel-server/index.ts --bundle ...
// @bunx esbuild packages/server/src/channel-server/index.ts --bundle --watch &Or at minimum add a make build-channel-server and call it from both dev and prod.
| }; | ||
| } | ||
| const platform = lastInboundFrom.platform; | ||
| const msg: GatewayWsMessage = { |
There was a problem hiding this comment.
🟡 Warning
Problem: lastInboundFrom.platform is global mutable state shared across all inbound messages. The reply tool derives the target platform from whichever message arrived last — not from the message Claude is actually responding to.
Why it matters: If two messages arrive from different platforms before Claude calls reply (e.g. Discord message then Telegram message), the platform used for routing will be telegram even if Claude is replying to the Discord message. The reply goes to the wrong platform. Since chat_id is already platform-specific, Claude knows where to reply — it just needs the platform encoded alongside it.
Suggested fix:
// Option A: encode platform in chat_id (e.g. "discord:guild:channel")
// then parse it back in the reply tool:
const [platform, ...rest] = chat_id.split(":");
const routedChatId = rest.join(":");
// Option B: add platform as an explicit tool parameter
{
name: "platform",
type: "string",
enum: ["discord", "telegram", "slack"],
description: "Platform the chat_id belongs to",
}Either approach removes the ambient state dependency and makes reply routing explicit and safe.
nox-0x
left a comment
There was a problem hiding this comment.
Solid architecture — the MCP channel server + gateway router pattern is clean and the type-sharing via @autonomos/core is the right call. Two warnings left inline: (1) dist.mjs should be gitignored and built by both dev and prod targets to prevent source/binary drift; (2) lastInboundFrom.platform is global mutable state that can misroute replies if two platform messages arrive before Claude responds — consider encoding platform into the chat_id or adding it as an explicit tool param. Neither blocks this PR; the stub adapter scaffolding and WebSocket reconnect logic are correct.
Summary
server:autonomosMCP channel server that CC spawns as a subprocess — connects back to the gateway over WebSocketdist.mjsvia esbuild so CC can spawn with plainnode(no tsx dependency)--dangerously-load-development-channelsand--mcp-configautomaticallyArchitecture
New Files
packages/core/src/types/gateway.tspackages/server/src/channel-server/index.tspackages/server/src/channel-server/dist.mjsnodespawningpackages/server/src/gateway/router.tspackages/server/src/gateway/index.tspackages/server/src/gateway/adapters/stub.tspackages/server/src/gateway/adapters/{discord,telegram,slack}.tspackages/server/src/routes/gateway.ts/ws/gatewayWebSocket endpointTest plan
--dangerously-load-development-channels server:autonomos --mcp-config {...}node dist.mjs)/mcpshowsautonomosas connected in a CC session<channel>tag → Claude replies🤖 Generated with Claude Code