Skip to content

refactor(voice): LiveAgentRuntime — durable-channel wiring + SessionRecorder (steps 5a-2, 5a-3) - #1965

Merged
qingyun-wu merged 3 commits into
mainfrom
feat/step5a2-runtime-skeleton
Jul 7, 2026
Merged

qingyun-wu merged 3 commits into
mainfrom
feat/step5a2-runtime-skeleton

Conversation

@qingyun-wu

Copy link
Copy Markdown
Collaborator

The LiveAgentRuntime lands with its two reusable pieces, both moved VERBATIM from voice-agent main() and gated by the #1964 behavior anchors:

5a-2 — wireDurableChannels(session, opts): the durable-work-channel ↔ live-session glue — context drops, ambient note-viewing (incl. the GOODBYE-trigger metadata-only guard with its incident history), and task results (1.5s/3s retry, Discord-DM + optional Cartesia stuck-session fallback). This is what every live adapter needs identically: web voice today, phone in 5b-2, MatrixRTC later. Media frames never touch the module (three-channel rule). Equivalence: 15/15 template literals (one interpolation variable renamed, same runtime values).

5a-3 — createSessionRecorder(source, sessionId): per-session metrics state, idempotent flush (leaked-ticker invariant preserved: ticker stopped BEFORE the write-once guard), realtime usage-ticker management, and wasFlushed for the reconnect-reset path (same metricsWritten semantics incl. phantom-session_end protection).

voice-agent.ts is now: config module (5a-1, merged) + runtime calls + web-voice-specific session wiring. Next: 5b-1 extraction (phone config; tripwires already on feat/step5b-phone-anchors), then 5b-2 (phone onto this runtime).

Verification: tsc clean, behavior anchors 11/11, task-bridge suites green. Live smoke call with the owner scheduled before the 5a stack is behavior-effective (service restart).

— @sutando-qingyun-001

qingyun-wu and others added 2 commits July 6, 2026 18:46
…ts (step 5a-2)

The LiveAgentRuntime's first real content: the durable-work-channel wiring
(context drops, ambient note-viewing with the GOODBYE-trigger metadata-only
guard, task results with the 1.5s/3s retry + Discord-DM/Cartesia stuck-
session fallback) moved VERBATIM from voice-agent main(). This is the
piece every live adapter needs identically — web voice today, phone in
5b-2, MatrixRTC later. Adapter-specifics (Cartesia key + synth) arrive
via DurableChannelOptions.

Equivalence: 15/15 template literals identical (the one diff is the
variable rename in a log-line interpolation — same runtime values);
behavior anchors 11/11; tsc clean. Media frames never touch this module
(three-channel rule).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t-runtime (step 5a-3)

The per-session metrics state (events/toolCalls/transcript arrays), the
idempotent flush into conversation-store (ticker stopped FIRST, before the
guard — the leaked-ticker invariant preserved verbatim), and the realtime
usage-ticker management move from voice-agent main() into
createSessionRecorder(source, sessionId). Callbacks push into
recorder.events/toolCalls/transcript exactly as they pushed into the old
module-level arrays; the reconnect reset keys off recorder.wasFlushed
(same metricsWritten semantics, incl. the phantom-session_end protection).
Phone (5b-2) reuses the recorder with source: 'phone'.

tsc clean, anchors 11/11, task-bridge suites green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Coverage Gate

✅ No Python changes — nothing to gate (bar: 95% on changed lines).

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Review: LGTM — faithful, behavior-preserving extraction. ✅

I read the full diff against the "verbatim move" claim and checked the load-bearing invariants specifically. They hold:

SessionRecorder (5a-3):

  • reset() clears the arrays in place (events.length = 0, not events = []). This is the subtle one: the recorder exposes events/toolCalls/transcript as the same array references the onSessionStart/onToolCall callbacks push into, and flush() reads their .length. In-place clearing keeps those exported references valid across a reset — a reassignment here would have silently split the references and made post-reconnect sessions record zero tools/transcript. Correct.
  • flush() stops the ticker BEFORE the write-once guard (ticker?.stop(); ticker = null; then if (metricsWritten) return) — the leaked-ticker invariant is preserved exactly, so a leaked ticker can't emit phantom voice.seconds past a flush. Comment carried over verbatim.
  • wasFlushed → metricsWritten, and the reconnect path's if (metricsWritten) → if (recorder.wasFlushed) map 1:1. startTicker() stops-then-starts as before.

wireDurableChannels (5a-2):

  • Context-drop, note-viewing (incl. the GOODBYE-trigger metadata-only guard with the 2026-04-09 incident rationale and the GOODBYE_TRIGGERS regex), and the task-result path all moved verbatim.
  • Result retry timing preserved: inject at T+1.5s, one retry at T+3s (nested setTimeout(…,1500)), then Discord-DM fallback + optional Cartesia — no infinite retry. The "re-check inside the timer" pattern (the 2026-05-20 106ms-setup-race fix) is intact.
  • Three-channel rule honored — the module carries only control/durable-work text into the session; no media frames touch it.

Call-site mapping in voice-agent.ts is 1:1: voiceSessionStart = Date.now(); metricsWritten = false; …length = 0 → recorder.reset(); voiceEvents.push → recorder.events.push; startVoiceUsageTicker() → recorder.startTicker(VOICE_NATIVE_AUDIO_MODEL); writeVoiceMetrics() → recorder.flush(). recordToolCall stays imported (per-tool-call path unchanged; only session-flush moved).

Two minor, non-blocking notes:

  1. The recorder deliberately exposes events/toolCalls/transcript as public mutable arrays (a slightly leaky abstraction). The PR is right to keep it this way for a verbatim step — switching to pushEvent()/pushToolCall() methods would change call-site shape and break the equivalence guarantee. Worth a follow-up step to encapsulate once the anchors relax.
  2. import { recordSession } sits mid-file (below wireDurableChannels) rather than with the top imports. Legal via hoisting and tsc is happy, but consolidating all imports at the top would read cleaner — trivially deferrable.

Nice seam for the phone (5b-2) and MatrixRTC adapters to reuse. No behavior concerns.

— Sutando (@sutando-wu-air.agent)

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

No blocking findings.

I checked the LiveAgentRuntime extraction with attention to the durable-channel paths and the session recorder lifecycle. The moved result/context/note wiring preserves the delayed active-session checks, metadata-only note guard for goodbye-trigger content, and stuck-voice Discord fallback. The recorder also keeps the important ordering where the realtime ticker is stopped before the write-once guard, and reset() mutates the existing arrays so callback references remain live.

Verification run in an isolated worktree:

  • npm exec -- tsc --noEmit
  • npm exec -- tsx --test tests/voice-behavior-anchors.test.ts
  • npm exec -- tsx --test tests/task-bridge-isvoice.test.ts tests/task-bridge-format.test.ts tests/task-bridge-fallthrough-guard.test.ts tests/task-delegation.test.ts

Reviewed by Qingyun's Codex.

@john-the-dev john-the-dev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#1965 cold review — APPROVE

LiveAgentRuntime extraction (steps 5a-2 + 5a-3) is clean. Key checks:

  • Verbatim move: 114 lines removed from voice-agent.ts main(), reappear in wireDurableChannels and createSessionRecorder. Logic identical — no behavior change.
  • GOODBYE-trigger guard preserved exactly: the GOODBYE_TRIGGERS regex + metadata-only fallback with the incident comment (2026-04-09 uiuc-trip-conflicts.md) is carried over verbatim. This is the load-bearing invariant — if the comment moved, the guard still matches behavior.
  • Three-channel rule respected: media frames don't cross the module boundary; only durable-work traffic goes through wireDurableChannels.
  • voice-agent.ts delegation: imports and calls wireDurableChannels(session, { cartesiaApiKey, generateSpeech }) and createSessionRecorder('voice', SESSION_ID) — clean, no inline duplicates left behind.
  • CI: all 6 checks green.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

@cla-assistant check

@qingyun-wu
qingyun-wu merged commit 4c61a9c into main Jul 7, 2026
7 checks passed
@qingyun-wu
qingyun-wu deleted the feat/step5a2-runtime-skeleton branch July 7, 2026 02:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants