diff --git a/ops/agent-phone.mjs b/ops/agent-phone.mjs index 9f68737..2d8191b 100755 --- a/ops/agent-phone.mjs +++ b/ops/agent-phone.mjs @@ -1136,7 +1136,19 @@ async function main() { const last = [...transcript].reverse().find((t) => !t.history && t.who !== agent); return Boolean(last && last.who === partnerAgent); })(); - if (join && a.interview && !firstTurn) { + /** + * Co-host coordination, on EVERY turn including the first one. + * + * This used to be gated on `!firstTurn`, and `firstTurn` is true + * whenever the agent has not spoken for three minutes. The hosts dial in + * and then wait in an empty room while the guest is rung, which is nearly + * always longer than that - so on the guest's very first words both hosts + * counted as introducing themselves, skipped the stagger, the reservation + * and the do-not-answer-your-co-host rule, and talked over each other in + * the first ten seconds of the show. Episode 9 ended twenty seconds after + * Anthony said hello and got two overlapping introductions. + */ + if (join && a.interview) { // Never answer a question the co-host put to the guest. if (partnerSpokeLast && !mine) return; if (!mine) { @@ -1213,7 +1225,13 @@ async function main() { const lastHeard = recentSpeech(); const mine = addressed(agent, lastHeard); const otherNamed = Object.keys(NAME_ALIASES).some((o) => o !== agent && addressed(o, lastHeard)) && !mine; - const introduced = !shouldIntroduce(agent, transcript, Date.now()); + /** + * Whether this agent has spoken on this call at all - NOT whether it + * spoke recently. These guards stop two hosts answering the same line, + * and tying them to introduction staleness turned them off after any + * three-minute lull: exactly the long guest answer the format asks for. + */ + const introduced = transcript.some((t) => t.who === agent && !t.history); if (introduced && kind !== 'hangup' && otherNamed) return; // The other agent already answered what we are answering: leave it, unless it was ours by name. const lineAt = Date.parse([...transcript].reverse().find((t) => t.who === 'them')?.at || '') || 0; diff --git a/test/agent-phone.test.mjs b/test/agent-phone.test.mjs index 3facc87..9fd8e58 100644 --- a/test/agent-phone.test.mjs +++ b/test/agent-phone.test.mjs @@ -297,6 +297,25 @@ test('a host saying its own name is not the guest, however the transcriber spell assert.equal(worker.echoesFloor('We run our own security and nothing else', 'rootkey handles the scanning for us'), false); }); +test('a host that has gone quiet is still introduced, so it keeps coordinating', () => { + // shouldIntroduce() is about the PROMPT: say hello if you have not spoken for + // three minutes. It used to gate co-host coordination too, and the hosts wait + // in an empty room while the guest is rung - always longer than three minutes. + // So on the guest's first words both hosts counted as introducing themselves, + // skipped the stagger and the reservation, and talked over each other. Episode + // 9 ended twenty seconds after Anthony said hello. + const transcript = [ + { who: 'riotcoder', text: 'Welcome to Off The Record.', at: new Date(now - 4 * minute).toISOString() }, + { who: 'them', text: 'Hello? This is Anthony.', at: new Date(now).toISOString() }, + ]; + // Four minutes quiet: the prompt should still ask for a hello... + assert.equal(worker.shouldIntroduce('riotcoder', transcript, now), true); + // ...but having spoken on this call is a different fact, and the one the + // coordination guards now use. + assert.equal(transcript.some((l) => l.who === 'riotcoder' && !l.history), true); + assert.equal(transcript.some((l) => l.who === 'cha0spunk' && !l.history), 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: '' });