Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions ops/agent-phone.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -821,8 +821,25 @@ async function main() {
const roomHeaders = { authorization: `Bearer ${process.env.GROUPCALL_AGENT_WORKER_KEY}` };
if (join && process.env.GROUPCALL_STATUS_URL) {
const r = await fetch(process.env.GROUPCALL_STATUS_URL, { headers: roomHeaders }).then((x) => (x.ok ? x.json() : null)).catch(() => null);
const all = await fetch(`${process.env.GROUPCALL_STATUS_URL}/lines?after=0&wait=0`, { headers: roomHeaders }).then((x) => (x.ok ? x.json() : null)).catch(() => null);
afterId = all?.lines?.at(-1)?.id || 0;
/**
* Read to the END of the room's record, not to the end of its first page.
*
* /lines returns at most 200 at a time, so `after=0` answers with the OLDEST
* two hundred. Taking the last id of that one page left the mark sitting in
* the middle of the log, and the stream then delivered everything after it -
* weeks of earlier episodes, arriving as though the guest had just said
* them. Page until the room has no more to give.
*/
const all = { lines: [] };
for (let page = 0; page < 50; page++) {
const mark = all.lines.at(-1)?.id ?? 0;
const r2 = await fetch(`${process.env.GROUPCALL_STATUS_URL}/lines?after=${mark}&wait=0`, { headers: roomHeaders }).then((x) => (x.ok ? x.json() : null)).catch(() => null);
const lines = r2?.lines || [];
if (!lines.length) break;
all.lines.push(...lines);
if (lines.length < 200) break;
}
afterId = all.lines.at(-1)?.id || 0;
/**
* Recent lines only. The room's record is our memory so that a drop, a crash
* or a deploy picks the conversation up instead of starting over - and a
Expand Down Expand Up @@ -888,6 +905,10 @@ async function main() {
for (const l of r.lines || []) {
afterId = Math.max(afterId, l.id);
if (ended || (l.kind === 'agent' && l.who === agent)) continue;
// Old enough to be another call: never "what was just said". The mark
// can start behind (a short page, a room that was pruned), and a line
// from last week must not arrive as a line from this second.
if (Number(l.at) && Date.now() - Number(l.at) > ROOM_MEMORY_MS) continue;
if (l.kind === 'agent') {
transcript.push({ who: l.who, text: l.text, at: new Date().toISOString() });
try {
Expand Down
30 changes: 30 additions & 0 deletions test/agent-phone.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,36 @@ test("the room's record is memory of this call, not of the last episode", () =>
assert.ok(worker.ROOM_MEMORY_MS >= 10 * minute);
});

test('catching up on a room reads to the end of the log, not the end of a page', () => {
// /lines answers with at most 200, so after=0 returns the OLDEST two hundred
// (room 8625: ids 403-602 of a log reaching far past it). Taking the last id of
// that single page left the mark mid-log and the stream then delivered
// everything after it - weeks of earlier episodes, arriving as though the guest
// had just said them.
const log = Array.from({ length: 450 }, (_, i) => ({ id: 403 + i, at: now - (450 - i) * 1000 }));
const page = (after) => log.filter((l) => l.id > after).slice(0, 200);
const collected = [];
for (let i = 0; i < 50; i++) {
const lines = page(collected.at(-1)?.id ?? 0);
if (!lines.length) break;
collected.push(...lines);
if (lines.length < 200) break;
}
assert.equal(collected.at(-1).id, log.at(-1).id);
// One page would have stopped here, 250 lines short of the end.
assert.equal(page(0).at(-1).id, 602);
});

test('a line older than the memory window is never what was just said', () => {
const fresh = { id: 9, at: now - 60_000 };
const ancient = { id: 2, at: now - 8 * 24 * 60 * minute };
const stale = (l) => Boolean(Number(l.at)) && now - Number(l.at) > worker.ROOM_MEMORY_MS;
assert.equal(stale(fresh), false);
assert.equal(stale(ancient), true);
// A line with no timestamp is not thrown away.
assert.equal(stale({ id: 3, at: 0 }), 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: '' });
Expand Down
Loading