From efdcc8f6f20784d708b37b4839463d039fce580c Mon Sep 17 00:00:00 2001 From: riotcoder Date: Thu, 5 Nov 2026 17:28:17 +0000 Subject: [PATCH] fix: echo matching knows the hosts' names, however they are transcribed Rehearsal on the merged #51 leg: riotcoder said: "I'm RiotCoder, Anthony's AI assistant and co-host, ..." cha0spunk heard: riotcoder (other agent): Anthony's AI assistant and cohost. room: I'm Riot Coder. <- still riotcoder Our TTS says "RiotCoder" as one word and the transcriber writes "Riot Coder" as two, so neither token is in the bag the floor claim built from "riotcoder". The overlap on that short line fell to 1/3, under the half it needs, and a host introducing itself arrived as the guest - at the top of the show, which is where episode 8 started coming apart. NAME_ALIASES already lists the spellings transcription produces ("riot coder", "chaos punk", "kaos punk"), so echoesFloor now collapses them to one token on both sides, and reads 0 as o. Only the MULTI-WORD aliases: collapsing single generic ones like "security" or "chaos" would merge words the guest is entitled to use himself, and swallowing the guest is the failure this code path already had once. 109 pass, 0 fail; tsc clean. Co-Authored-By: Claude Opus 5 (1M context) --- ops/agent-phone.mjs | 20 ++++++++++++++++++-- test/agent-phone.test.mjs | 15 +++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/ops/agent-phone.mjs b/ops/agent-phone.mjs index 25f3b5c..9f68737 100755 --- a/ops/agent-phone.mjs +++ b/ops/agent-phone.mjs @@ -457,10 +457,26 @@ export function coHostSpeech(pin, agent) { * all of the time. So compare: words that are in what the co-host is saying are * its echo; words that are not are a person, and a person always gets through. */ +/** + * Every multi-word spelling of an agent's name, longest first: the transcriber + * splits our made-up names into two words ("RiotCoder" comes back as "Riot + * Coder", "ChaosPunk" as "chaos punk"), and two tokens never match the one the + * host actually said. Only the multi-word aliases are collapsed - single generic + * ones like "security" or "chaos" would merge words the guest is entitled to use + * himself. + */ +const SPLIT_NAMES = Object.entries(NAME_ALIASES) + .flatMap(([agent, names]) => names.filter((n) => n.includes(' ')).map((n) => [n, agent.replace(/0/g, 'o')])) + .sort((a, b) => b[0].length - a[0].length); +/** One spelling of a name per token, so "Riot Coder" and "RiotCoder" are the same word. */ +function oneWordNames(text) { + let t = String(text).toLowerCase().replace(/0/g, 'o'); + for (const [alias, agent] of SPLIT_NAMES) t = t.split(alias).join(agent); + return t; +} export function echoesFloor(heard, floorText) { const words = (text) => - String(text) - .toLowerCase() + oneWordNames(text) .replace(/[^a-z0-9' ]+/g, ' ') .split(/\s+/) .filter((w) => w.length > 2); diff --git a/test/agent-phone.test.mjs b/test/agent-phone.test.mjs index b43d9f7..3facc87 100644 --- a/test/agent-phone.test.mjs +++ b/test/agent-phone.test.mjs @@ -282,6 +282,21 @@ test('each agent keeps a few recent lines, not the whole call', () => { worker.releaseFloor(pin, 'riotcoder'); }); +test('a host saying its own name is not the guest, however the transcriber spells it', () => { + // The rehearsal: riotcoder said "I'm RiotCoder, Anthony's AI assistant and + // co-host" and the transcriber wrote "I'm Riot Coder." Two tokens never + // matched the one word the host said, so the overlap fell to 1/3 and the host + // introducing itself was logged as `room:` - the guest. + const floor = "I'm RiotCoder, Anthony's AI assistant and co-host,"; + assert.equal(worker.echoesFloor("I'm Riot Coder.", floor), true); + assert.equal(worker.echoesFloor('Hello. I am Chaos Punk, an AI assistant and co host.', "Hello, I'm ChaosPunk, an AI assistant and co-host."), true); + // 0 read as o, both ways. + assert.equal(worker.echoesFloor('chaospunk here', 'cha0spunk here'), true); + // The guest still gets through, and a generic word is not a name. + assert.equal(worker.echoesFloor('Can you hear me?', floor), false); + assert.equal(worker.echoesFloor('We run our own security and nothing else', 'rootkey handles the scanning for us'), false); +}); + test('a model that says PASS is saying nothing, not reading the word out', () => { assert.deepEqual(worker.parseTurn('SAY: PASS'), { kind: 'pass', text: '' }); assert.deepEqual(worker.parseTurn('PASS'), { kind: 'pass', text: '' });