From b1775b01da33704a9770aadd25eae2e9e6f90808 Mon Sep 17 00:00:00 2001 From: riotcoder Date: Thu, 5 Nov 2026 17:41:51 +0000 Subject: [PATCH] fix: a joined leg finds the room status itself GROUPCALL_STATUS_URL was set nowhere, so every Off The Record leg ran blind in two ways. followRoom never started. The room already labels every line by caller ID - {"who":"Anthony","kind":"person","trusted":true} next to {"who":"cha0spunk", "kind":"agent"} - and the leg had none of it, so it fell back to its own speech-to-text and had to guess from audio which voice was its co-host. That guess is what #50, #51 and #52 were repairing; with the room's transcript it is not a guess at all. And roomWatch fell through to its no-status-URL branch, "twenty quiet minutes", instead of "everyone else left" after a minute with no person in the room. So when the guest hung up the hosts stayed: episodes 8 and 9 each left two legs dialled into an empty room, one of them with an hour of its cap still to run, until they were killed by hand. A joined leg can work this out on its own - the room code is on the command line and the worker key is in worker.env - so it now does, and an explicitly passed value still wins. 111 pass, 0 fail; tsc clean. Co-Authored-By: Claude Opus 5 (1M context) --- ops/agent-phone.mjs | 28 ++++++++++++++++++++++++++++ test/agent-phone.test.mjs | 11 +++++++++++ 2 files changed, 39 insertions(+) diff --git a/ops/agent-phone.mjs b/ops/agent-phone.mjs index 2d8191b..e5c29e3 100755 --- a/ops/agent-phone.mjs +++ b/ops/agent-phone.mjs @@ -422,6 +422,10 @@ export function noteSaid(pin, agent, text) { }); } catch {} } +/** Where a joined leg asks the room who is on the line and what was said. */ +export function statusUrlFor(base, pin) { + return `${String(base).replace(/\/+$/, '')}/api/v1/agent-queue/rooms/${pin}`; +} /** How long a co-host's line stays its own: the time its audio can still be coming back. */ const CO_HOST_MEMORY_MS = 30_000; /** @@ -714,6 +718,30 @@ async function main() { process.exit(2); } if (join && typeof a.goal !== 'string') a.goal = 'You were invited to join this group call. Introduce yourself briefly, then listen, and help when someone asks you something.'; + /** + * A joined leg needs the room's own view of itself, and it can work out where + * that is: the room code is on the command line and the worker key is in + * worker.env. + * + * Without GROUPCALL_STATUS_URL a leg is blind in two ways that both showed up + * on Off The Record. followRoom never runs, so instead of the room's transcript + * - which labels every line by caller ID, `{"who":"Anthony","kind":"person"}` + * next to `{"who":"cha0spunk","kind":"agent"}` - the leg has only its own + * speech-to-text and has to guess from audio which voice was its co-host. And + * roomWatch falls back to "twenty quiet minutes", so when the guest hangs up + * the hosts stay on the line: episodes 8 and 9 each left two legs dialled into + * an empty room, one with an hour of its cap still to run. + * + * An explicitly passed value always wins; this only fills in the blank. + */ + if (join) { + const workerEnv = `${HOME}/.config/groupcall/worker.env`; + if (!process.env.GROUPCALL_AGENT_WORKER_KEY) process.env.GROUPCALL_AGENT_WORKER_KEY = envVal(workerEnv, 'GROUPCALL_AGENT_WORKER_KEY'); + const base = String(process.env.GROUPCALL_URL || envVal(workerEnv, 'GROUPCALL_URL') || '').replace(/\/+$/, ''); + if (!process.env.GROUPCALL_STATUS_URL && base && process.env.GROUPCALL_AGENT_WORKER_KEY) { + process.env.GROUPCALL_STATUS_URL = statusUrlFor(base, a.pin); + } + } if ((a._[0] !== 'call' && !join) || !a._[1] || typeof a.goal !== 'string') { console.error('Usage: agent-phone call --goal "what to achieve" [--agent riotcoder] [--max-minutes 10] [--json]'); process.exit(2); diff --git a/test/agent-phone.test.mjs b/test/agent-phone.test.mjs index 9fd8e58..8bcbae0 100644 --- a/test/agent-phone.test.mjs +++ b/test/agent-phone.test.mjs @@ -316,6 +316,17 @@ test('a host that has gone quiet is still introduced, so it keeps coordinating', assert.equal(transcript.some((l) => l.who === 'cha0spunk' && !l.history), false); }); +test('a joined leg can work out where the room status lives', () => { + // Without this a leg is blind twice over: followRoom never runs, so it guesses + // its co-host's voice from audio instead of reading the room's own labelled + // transcript, and roomWatch falls back to "twenty quiet minutes" - which is why + // episodes 8 and 9 each left two legs dialled into an empty room after the + // guest hung up. + assert.equal(worker.statusUrlFor('https://group.call.pit.world', '8625'), 'https://group.call.pit.world/api/v1/agent-queue/rooms/8625'); + // A trailing slash on the configured base is not a double slash in the path. + assert.equal(worker.statusUrlFor('https://group.call.pit.world/', '8625'), 'https://group.call.pit.world/api/v1/agent-queue/rooms/8625'); +}); + 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: '' });