From ade438a1359ab7c8cf82f7e65c23d21a56205ba2 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 23 Oct 2026 23:17:30 -0700 Subject: [PATCH 1/2] Claim quest/m1/voice-goodbye-drain Co-Authored-By: Claude Opus 5.5 From 59bef3be0f00c2ab7f969b880d1799c82723e5d0 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 23 Oct 2026 23:33:57 -0700 Subject: [PATCH 2/2] Unmount the voice publisher without a goodbye wait never closes its connection on unmount: it releases a pooled handle, and the shared session keeps sending the cleanly finished transcript track until the pool's 2 s linger expires. The page's 250 ms GOODBYE_MS was redundant. Keep the quest open for the upstream drain on linger expiry and a real-call check. Co-Authored-By: Claude Opus 5.5 --- quest/m1/voice-goodbye-drain.md | 28 ++++++++++++++++++---------- src/components/voice-demo.tsx | 14 +++++--------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/quest/m1/voice-goodbye-drain.md b/quest/m1/voice-goodbye-drain.md index 77c0b23..1840059 100644 --- a/quest/m1/voice-goodbye-drain.md +++ b/quest/m1/voice-goodbye-drain.md @@ -3,17 +3,25 @@ ## Goal When a `/voice` call ends, the page's final `session-ending` record reaches -the agent because the connection drains on close, not because the page waits -a fixed 250 ms (`GOODBYE_MS` in `src/components/voice-demo.tsx`) before -unmounting ``. +the agent because the connection drains on close, not because of a fixed +wait. ## Plan -Decided 2026-10-19: once `@moq/net`'s draining `close()` is available, drop -the wait and close through it. The `moq` submodule now provides it. +The page no longer waits before unmounting ``. Unmounting only +releases the element's pooled connection handle, and the shared session keeps +sending the cleanly finished transcript track until the pool's 2 s linger +(`LINGER_MS` in `moq/js/net/src/connection/pool.ts`) expires. A local relay +test with the real element delivered the record every time; closing the +connection at once lost it every time. -Check how `` closes its connection before relying on it: the -element holds a pooled connection handle, and the pool lingers before -closing, so unmounting may not drain the broadcast's last frames the way an -explicit `close()` does. Prove it with a real call: hang up and confirm the -agent logs the `session-ending` record rather than a timeout. +Remaining: + +- Upstream in moq-dev/moq: when the linger expires, `Reload`'s teardown + (`moq/js/net/src/connection/reload.ts`) calls `Established.abort()`, so the + draining `close()` never runs on this path and the linger is still a fixed + wait. Swapping it for `close()` delivered the record with the connection + closed at once. Bump the `moq` submodule once that lands. +- A real call on new.moq.dev/voice: hang up and confirm the agent logs the + `session-ending` record rather than a timeout. Needs a maintainer (Turnstile + and a mic). diff --git a/src/components/voice-demo.tsx b/src/components/voice-demo.tsx index 61aaf8f..79c6878 100644 --- a/src/components/voice-demo.tsx +++ b/src/components/voice-demo.tsx @@ -72,10 +72,6 @@ const POLL_MS = 2_000; // lands a little before our pickup. const LIMIT_SLACK_SECS = 15; -// Closing the connection drops whatever has not reached the wire, so the goodbye gets a moment -// first, the same as @pipecat-ai/moq-transport. -const GOODBYE_MS = 250; - // The API's refusal codes plus the ones only the page can see. Copy comes from here, never from // the API's error string. type Outcome = @@ -193,7 +189,7 @@ function clock(secs: number): string { export default function VoiceDemo() { const [phase, setPhase] = createSignal("idle"); const [outcome, setOutcome] = createSignal(); - // Mounts the publisher and player; it outlives `current` by the goodbye. + // Mounts the publisher and player. const [mounted, setMounted] = createSignal(); const [remaining, setRemaining] = createSignal(); const [transcript, setTranscript] = createSignal([]); @@ -236,11 +232,11 @@ export default function VoiceDemo() { if (call) { for (const timer of call.timers) clearTimeout(timer); clearInterval(call.ticker); - // The agent hangs up at once on this, instead of waiting for us to redial. + // The agent hangs up at once on this, instead of waiting for us to redial. Unmounting + // right after is safe: only releases its pooled connection, which keeps + // sending the cleanly finished track until the pool's linger closes it. send(call, { label: "moq-transport", type: "session-ending" }); - const unmount = () => setMounted((m) => (m === call ? undefined : m)); - if (call.producer) setTimeout(unmount, GOODBYE_MS); - else unmount(); + setMounted(undefined); } setRemaining(undefined); setOutcome(why);