eleven / messenger Public

agent: bots never reply to other bots (crosstalk loop fix) (#60)

A bot's mention trigger matches its bare name (no @ needed), and bot-authored
text naturally names other bots — Eleven's persona recommends Claude, the
Announcer's release notes mention Eleven — so two bots in one group could
ping-pong indefinitely. Each bot now tracks the roster's agent labels
(refreshed every reconnect) and ignores any message authored by one.
Name-based policy, same trust level as the rest of the payload; sender-signed
messages are the future hardening for this class of check.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell e3c3044f1afc48adf6e4eb8505e0fdcc1d050f78 parent 0138280
2 files changed, +18 −0
  • agent/runtime.mjs +17 −0
  • changelog/2026-07-02-bot-crosstalk.md +1 −0
diff --git a/agent/runtime.mjs b/agent/runtime.mjs
index 421909a..1416f65 100644
--- a/agent/runtime.mjs
+++ b/agent/runtime.mjs
@@ -50,12 +50,27 @@ export function runBot({ name, session, builtinPersona = "", builtinDocs = "" })
const keys = new Map(); // threadId -> { key: CryptoKey, kind: "dm"|"group"|"self" }
const cursors = new Map(); // threadId -> last message id we've processed
+ let agentNames = new Set(); // roster labels of ALL agents — never reply to one
const get = (path) =>
fetch(`${BASE}${path}${path.includes("?") ? "&" : "?"}token=${encodeURIComponent(session.token)}`).then((r) =>
r.json()
);
+ // Bots never talk to bots: a bot's mention trigger matches its bare name, and
+ // bot-authored text naturally names other bots (Eleven recommends Claude, the
+ // Announcer's notes mention Eleven) — without this, two bots in one group
+ // ping-pong forever. Name-based (payloads aren't sender-signed yet), refreshed
+ // on every (re)connect so newly added bots are picked up within a cycle.
+ async function loadAgents() {
+ try {
+ const { roster = [] } = await get("/api/roster");
+ agentNames = new Set(roster.filter((e) => e.is_agent).map((e) => e.label));
+ } catch (e) {
+ tag(`loadAgents: ${e.message}`);
+ }
+ }
+
const maxId = (messages) => messages.reduce((m, x) => Math.max(m, x.id || 0), 0);
// Fetch our threads and cache the (unwrapped) key for any we don't hold yet. A
@@ -178,6 +193,7 @@ export function runBot({ name, session, builtinPersona = "", builtinDocs = "" })
} catch {}
}, 30000);
await loadConfig();
+ await loadAgents();
await loadThreads();
await catchUp();
for (const d of queue.splice(0)) ws.send(d);
@@ -204,6 +220,7 @@ export function runBot({ name, session, builtinPersona = "", builtinDocs = "" })
return;
}
if (p.name === name) return; // our own message, echoed back
+ if (agentNames.has(p.name)) return; // bots never talk to bots (see loadAgents)
if (!wantsReply(entry.kind, p.text)) return;
await reply(threadId, entry);
}
diff --git a/changelog/2026-07-02-bot-crosstalk.md b/changelog/2026-07-02-bot-crosstalk.md
new file mode 100644
index 0000000..22f5c60
--- /dev/null
+++ b/changelog/2026-07-02-bot-crosstalk.md
@@ -0,0 +1 @@
+Helpers no longer reply to each other — no more bot-to-bot chatter in groups.