diff --git a/.env.example b/.env.example index b32ad6a..8a189e0 100644 --- a/.env.example +++ b/.env.example @@ -30,6 +30,10 @@ MATRIX_CONTROL_ROOM_ID= # Required: without it the bot can't tell its own messages apart from real ones and would # reply to itself in a loop, so it refuses to start. MATRIX_BOT_USER_ID= +# Comma-separated Matrix IDs of OTHER agents sharing the control room (currently just +# @hermes:...) — without this, claude-bot treats every message another bot posts as +# fresh chat input and replies to it, which that bot may then react to in turn. +OTHER_AGENT_USER_IDS=@hermes:matrix.apps.williamturner.eu # Comma-separated "owner/repo" list the chat router is allowed to open code-change PRs # against. A plain chat message mentioning a repo NOT in this list is treated as chat, # never as a code task — the router only matches confidently against known repos. diff --git a/agent/src/matrixBot.js b/agent/src/matrixBot.js index b60243b..46c8771 100644 --- a/agent/src/matrixBot.js +++ b/agent/src/matrixBot.js @@ -11,6 +11,15 @@ const GITEA_URL = process.env.GITEA_URL; // to the endpoint itself. This is also the only reliable way to filter the bot's own // messages now that there's no command prefix to naturally exclude them by. const BOT_USER_ID = process.env.MATRIX_BOT_USER_ID; +// Other agents sharing this room (currently just Hermes) — their own messages must be +// ignored the same way claude-bot ignores its own, or claude-bot's classifier treats +// every message another bot posts as fresh chat input and replies to it, which that +// bot may then react to in turn. Found the hard way: a single @hermes mention cascaded +// into claude-bot replying to Hermes's own thread messages ("Hermes says: ..."). +const OTHER_AGENT_USER_IDS = (process.env.OTHER_AGENT_USER_IDS || "") + .split(",") + .map((id) => id.trim()) + .filter(Boolean); const KNOWN_REPOS = (process.env.KNOWN_REPOS || "") .split(",") .map((r) => r.trim()) @@ -47,6 +56,7 @@ export async function startMatrixBot() { client.on("room.message", async (roomId, event) => { if (roomId !== CONTROL_ROOM_ID) return; if (event.sender === BOT_USER_ID) return; + if (OTHER_AGENT_USER_IDS.includes(event.sender)) return; const body = event.content?.body; if (!body) return; // Messages explicitly addressed to another agent in this room (currently just @@ -67,6 +77,7 @@ export async function startMatrixBot() { } const reply = await chatReply(body); + console.log("chat reply sent, length:", reply.length); await client.sendText(roomId, truncate(reply)); } catch (err) { console.error("message handling failed", err); diff --git a/docker-compose.yml b/docker-compose.yml index 525be44..a59d913 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -77,7 +77,15 @@ services: # rooms (DMs to it would respond unprompted, per Hermes's own default behavior). MATRIX_ALLOWED_USERS: ${MATRIX_HUMAN_USER_ID} MATRIX_REQUIRE_MENTION: "true" - OPENROUTER_API_KEY: ${OPENROUTER_API_KEY} + # Routed through the local litellm gateway, not OpenRouter directly — same pattern + # as claude-agent's chat path, one place to hold the OpenRouter credential and swap + # models. Hermes's "main"/custom-endpoint provider is any OpenAI-compatible API + # reachable via OPENAI_BASE_URL + OPENAI_API_KEY. Note: this does NOT give Hermes + # access to the Claude Pro/Max subscription — that's blocked by Anthropic itself for + # any caller other than the real Claude Code CLI, proven earlier in this session + # (reproduced with plain curl straight to api.anthropic.com, LiteLLM or not). + OPENAI_BASE_URL: http://litellm:4000/v1 + OPENAI_API_KEY: ${LITELLM_MASTER_KEY} # Left disabled: Hermes itself warns that a network-reachable API server combined # with the default unsandboxed ('local') terminal backend gives any caller full # terminal/file access within the container. Matrix is the actual interface in use; @@ -113,10 +121,10 @@ services: MATRIX_BOT_TOKEN: ${MATRIX_BOT_TOKEN} MATRIX_CONTROL_ROOM_ID: ${MATRIX_CONTROL_ROOM_ID} MATRIX_BOT_USER_ID: ${MATRIX_BOT_USER_ID} + OTHER_AGENT_USER_IDS: ${OTHER_AGENT_USER_IDS} KNOWN_REPOS: ${KNOWN_REPOS} - # All model calls now go through the local litellm service, not OpenRouter directly — - # one gateway for OpenRouter's models (incl. its auto-router) and, for the - # claude-subscription route, Anthropic itself via the forwarded OAuth token above. + # Chat replies go through the local litellm service (OpenRouter's models, incl. its + # auto-router), not OpenRouter directly. LITELLM_BASE_URL: http://litellm:4000 LITELLM_MASTER_KEY: ${LITELLM_MASTER_KEY} volumes: