From 944c2952c72a01b2bf293fce6b9a0b28a0075ebb Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 21 Aug 2026 12:57:20 +0800 Subject: [PATCH 1/6] fix: wait for managed input before activating session --- app/api/session/dispatch/route.ts | 9 +++++++++ hooks/useRoom.ts | 4 +++- lib/session-dispatch-client.ts | 6 ++++++ tests/session-dispatch-client.test.mjs | 6 +++++- tests/session-start-dispatch.test.mjs | 11 +++++++++++ 5 files changed, 34 insertions(+), 2 deletions(-) diff --git a/app/api/session/dispatch/route.ts b/app/api/session/dispatch/route.ts index 982817d87..a3f51ae5b 100644 --- a/app/api/session/dispatch/route.ts +++ b/app/api/session/dispatch/route.ts @@ -20,6 +20,10 @@ export async function POST(req: Request) { agent_name?: string; sessionId?: string; session_id?: string; + requireAgentSessionReady?: boolean; + require_agent_session_ready?: boolean; + requireRoomInputParticipantsReady?: boolean; + require_room_input_participants_ready?: boolean; requireRoomVideoInputReady?: boolean; require_room_video_input_ready?: boolean; }; @@ -57,6 +61,11 @@ export async function POST(req: Request) { sessionId, agentName, readiness: { + requireAgentSessionReady: + body.requireAgentSessionReady === true || body.require_agent_session_ready === true, + requireRoomInputParticipantsReady: + body.requireRoomInputParticipantsReady === true || + body.require_room_input_participants_ready === true, requireRoomVideoInputReady: body.requireRoomVideoInputReady === true || body.require_room_video_input_ready === true, }, diff --git a/hooks/useRoom.ts b/hooks/useRoom.ts index d44922f1d..f79cf2526 100644 --- a/hooks/useRoom.ts +++ b/hooks/useRoom.ts @@ -212,7 +212,6 @@ export function useRoom(appConfig: AppConfig) { await recoverFromStartError(error); }; - setIsSessionActive(true); beginFrontendObservabilitySession(room); const dispatchAgentSession = async () => { @@ -220,6 +219,8 @@ export function useRoom(appConfig: AppConfig) { dispatchSessionId = sessionId; const signal = beginAgentSessionStart(room.name, sessionId); const dispatchPromise = requestAgentSessionDispatch(appConfig.agentName, sessionId, { + requireAgentSessionReady: usesManagedRoomInput, + requireRoomInputParticipantsReady: usesManagedRoomInput, requireRoomVideoInputReady: requiresRoomVideoInputReady(appConfig), signal, }); @@ -310,6 +311,7 @@ export function useRoom(appConfig: AppConfig) { if (!usesSandboxConcurrentStartup) { await dispatchAgentSession(); } + setIsSessionActive(true); } catch (error) { await handleStartError(error); } diff --git a/lib/session-dispatch-client.ts b/lib/session-dispatch-client.ts index 0d6b7ef1b..10812ba40 100644 --- a/lib/session-dispatch-client.ts +++ b/lib/session-dispatch-client.ts @@ -1,5 +1,7 @@ type DispatchOptions = { signal?: AbortSignal; + requireAgentSessionReady?: boolean; + requireRoomInputParticipantsReady?: boolean; requireRoomVideoInputReady?: boolean; }; @@ -27,6 +29,10 @@ export async function requestAgentSessionDispatch( body: JSON.stringify({ agentName: normalizedAgentName, sessionId: normalizedSessionId, + ...(options.requireAgentSessionReady ? { requireAgentSessionReady: true } : {}), + ...(options.requireRoomInputParticipantsReady + ? { requireRoomInputParticipantsReady: true } + : {}), ...(options.requireRoomVideoInputReady ? { requireRoomVideoInputReady: true } : {}), }), signal: options.signal, diff --git a/tests/session-dispatch-client.test.mjs b/tests/session-dispatch-client.test.mjs index 09bb1e45a..b57f06077 100644 --- a/tests/session-dispatch-client.test.mjs +++ b/tests/session-dispatch-client.test.mjs @@ -43,7 +43,7 @@ test('agent session dispatch sends only canonical session id to Next API', async } }); -test('agent session dispatch can require room video input readiness', async () => { +test('agent session dispatch can require managed room input readiness', async () => { const originalFetch = globalThis.fetch; let postedBody; globalThis.fetch = async (_url, init) => { @@ -55,12 +55,16 @@ test('agent session dispatch can require room video input readiness', async () = const { requestAgentSessionDispatch } = await loadSessionDispatchClientModule(); await requestAgentSessionDispatch('agent-a', '11111111-2222-4333-8444-555555555555', { + requireAgentSessionReady: true, + requireRoomInputParticipantsReady: true, requireRoomVideoInputReady: true, }); assert.deepEqual(postedBody, { agentName: 'agent-a', sessionId: '11111111-2222-4333-8444-555555555555', + requireAgentSessionReady: true, + requireRoomInputParticipantsReady: true, requireRoomVideoInputReady: true, }); } finally { diff --git a/tests/session-start-dispatch.test.mjs b/tests/session-start-dispatch.test.mjs index dca58fa7e..7af9d3f38 100644 --- a/tests/session-start-dispatch.test.mjs +++ b/tests/session-start-dispatch.test.mjs @@ -177,6 +177,10 @@ test('session dispatch route only accepts anonymous LiveKit agent fallback after ); assert.match(routeSource, /requireRoomVideoInputReady/); assert.match(routeSource, /require_room_video_input_ready/); + assert.match(routeSource, /requireAgentSessionReady/); + assert.match(routeSource, /require_agent_session_ready/); + assert.match(routeSource, /requireRoomInputParticipantsReady/); + assert.match(routeSource, /require_room_input_participants_ready/); assert.match(readinessSource, /type AgentParticipantMatchOptions/); assert.match(readinessSource, /type ReusableAgentParticipantOptions/); assert.match(readinessSource, /allowAnonymousLiveKitAgentFallback/); @@ -211,6 +215,13 @@ test('start call dispatches the agent with a cancellable room session id', async useRoomSource, /requireRoomVideoInputReady: requiresRoomVideoInputReady\(appConfig\)/ ); + assert.match(useRoomSource, /requireAgentSessionReady: usesManagedRoomInput/); + assert.match(useRoomSource, /requireRoomInputParticipantsReady: usesManagedRoomInput/); + assert.ok( + useRoomSource.lastIndexOf('setIsSessionActive(true)') > + useRoomSource.lastIndexOf('await dispatchAgentSession()'), + 'session view must become active only after dispatch readiness completes' + ); assert.doesNotMatch(useRoomSource, /requestAgentSessionDispatch\(\s*room\.name,/); }); From deda8fa7cec594dbafaa9fe6b824ada064e79bcd Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 21 Aug 2026 13:18:06 +0800 Subject: [PATCH 2/6] fix: wait for configured session inputs --- app/api/session/session-dispatch-service.ts | 2 +- hooks/useRoom.ts | 10 ++++++++-- lib/input-device-config.ts | 10 ++++++++++ tests/local-dispatch-config.test.mjs | 9 +++++++++ tests/session-prewarm.test.mjs | 2 +- tests/session-start-dispatch.test.mjs | 6 +++++- 6 files changed, 34 insertions(+), 5 deletions(-) diff --git a/app/api/session/session-dispatch-service.ts b/app/api/session/session-dispatch-service.ts index 067468acc..d05b9ea5b 100644 --- a/app/api/session/session-dispatch-service.ts +++ b/app/api/session/session-dispatch-service.ts @@ -75,7 +75,7 @@ const globalForInFlightDispatches = globalThis as typeof globalThis & { const inFlightDispatches = globalForInFlightDispatches.__liveavatarInFlightDispatches ?? (globalForInFlightDispatches.__liveavatarInFlightDispatches = new Map()); -const DEFAULT_AGENT_DISPATCH_TIMEOUT_MS = 8_000; +const DEFAULT_AGENT_DISPATCH_TIMEOUT_MS = 30_000; const DEFAULT_PREWARM_TOTAL_TIMEOUT_MS = 45_000; export type PrewarmPhase = 'room' | 'worker_readiness' | 'dispatch_readiness'; diff --git a/hooks/useRoom.ts b/hooks/useRoom.ts index f79cf2526..9a5aa90fd 100644 --- a/hooks/useRoom.ts +++ b/hooks/useRoom.ts @@ -6,7 +6,10 @@ import { useBrowserSourceClient } from '@/hooks/useBrowserSourceClient'; import { getVoiceSessionId, resetVoiceSessionId } from '@/lib/browser-room-session'; import { readConnectionDetailsResponse } from '@/lib/connection-details-response'; import { isValidConnectionRoomId } from '@/lib/connection-room-id'; -import { usesServerRoomInputDevice } from '@/lib/input-device-config'; +import { + usesBothServerRoomInputParticipants, + usesServerRoomInputDevice, +} from '@/lib/input-device-config'; import { FRONTEND_EVENTS, beginFrontendObservabilitySession, @@ -220,7 +223,10 @@ export function useRoom(appConfig: AppConfig) { const signal = beginAgentSessionStart(room.name, sessionId); const dispatchPromise = requestAgentSessionDispatch(appConfig.agentName, sessionId, { requireAgentSessionReady: usesManagedRoomInput, - requireRoomInputParticipantsReady: usesManagedRoomInput, + requireRoomInputParticipantsReady: usesBothServerRoomInputParticipants( + appConfig.audioInputDevice, + appConfig.visionInputDevice + ), requireRoomVideoInputReady: requiresRoomVideoInputReady(appConfig), signal, }); diff --git a/lib/input-device-config.ts b/lib/input-device-config.ts index 7807150fc..e89d7bd21 100644 --- a/lib/input-device-config.ts +++ b/lib/input-device-config.ts @@ -42,6 +42,16 @@ export function usesServerRoomInputDevice(inputDevice: string): boolean { return SERVER_ROOM_INPUT_DEVICES.has(inputDevice); } +export function usesBothServerRoomInputParticipants( + audioInputDevice?: string | null, + visionInputDevice?: string | null +): boolean { + return ( + usesServerRoomInputDevice(audioInputDevice || '') && + usesServerRoomInputDevice(visionInputDevice || '') + ); +} + export function resolveRoleInputDevices({ inputSource, audioInputDevice, diff --git a/tests/local-dispatch-config.test.mjs b/tests/local-dispatch-config.test.mjs index 15c9a703d..abf44fd18 100644 --- a/tests/local-dispatch-config.test.mjs +++ b/tests/local-dispatch-config.test.mjs @@ -2,6 +2,8 @@ import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; import { test } from 'node:test'; +const { usesBothServerRoomInputParticipants } = await import('../lib/input-device-config.ts'); + async function loadAppConfigModule() { return import('../app-config.ts'); } @@ -136,6 +138,13 @@ test('frontend resolves mixed xunfei audio with browser vision role devices', as assert.equal(config.showDefaultCameraPreview, false); }); +test('room participant readiness follows the configured server-owned inputs', () => { + assert.equal(usesBothServerRoomInputParticipants('xunfei', 'generic'), true); + assert.equal(usesBothServerRoomInputParticipants('xunfei', 'browser'), false); + assert.equal(usesBothServerRoomInputParticipants('browser', 'generic'), false); + assert.equal(usesBothServerRoomInputParticipants('browser', 'browser'), false); +}); + test('frontend normalizes invalid mixed output devices to the base role input device', async () => { const { resolveInputDeviceConfig } = await loadAppConfigModule(); diff --git a/tests/session-prewarm.test.mjs b/tests/session-prewarm.test.mjs index 9a44fdb32..1eff3fc6b 100644 --- a/tests/session-prewarm.test.mjs +++ b/tests/session-prewarm.test.mjs @@ -384,7 +384,7 @@ test('regular dispatch keeps its 8s timeout while prewarm gets the default 45s t ), /agent dispatch failed/ ); - assert.equal(now - regularStartedAt, 8_000); + assert.equal(now - regularStartedAt, 30_000); const prewarmStartedAt = now; await assert.rejects( diff --git a/tests/session-start-dispatch.test.mjs b/tests/session-start-dispatch.test.mjs index 7af9d3f38..35a87e0b1 100644 --- a/tests/session-start-dispatch.test.mjs +++ b/tests/session-start-dispatch.test.mjs @@ -216,7 +216,11 @@ test('start call dispatches the agent with a cancellable room session id', async /requireRoomVideoInputReady: requiresRoomVideoInputReady\(appConfig\)/ ); assert.match(useRoomSource, /requireAgentSessionReady: usesManagedRoomInput/); - assert.match(useRoomSource, /requireRoomInputParticipantsReady: usesManagedRoomInput/); + assert.match( + useRoomSource, + /requireRoomInputParticipantsReady: usesBothServerRoomInputParticipants\(/ + ); + assert.match(useRoomSource, /await Promise\.allSettled\(\[/); assert.ok( useRoomSource.lastIndexOf('setIsSessionActive(true)') > useRoomSource.lastIndexOf('await dispatchAgentSession()'), From 50c624ce5a4f2b545917339d202ee7771f9b59da Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 21 Aug 2026 13:22:38 +0800 Subject: [PATCH 3/6] fix: use agent session readiness as startup gate --- app/api/session/dispatch/route.ts | 5 ----- hooks/useRoom.ts | 9 +-------- lib/input-device-config.ts | 10 ---------- lib/session-dispatch-client.ts | 4 ---- tests/local-dispatch-config.test.mjs | 9 --------- tests/session-dispatch-client.test.mjs | 4 +--- tests/session-start-dispatch.test.mjs | 6 ------ 7 files changed, 2 insertions(+), 45 deletions(-) diff --git a/app/api/session/dispatch/route.ts b/app/api/session/dispatch/route.ts index a3f51ae5b..641fb02ad 100644 --- a/app/api/session/dispatch/route.ts +++ b/app/api/session/dispatch/route.ts @@ -22,8 +22,6 @@ export async function POST(req: Request) { session_id?: string; requireAgentSessionReady?: boolean; require_agent_session_ready?: boolean; - requireRoomInputParticipantsReady?: boolean; - require_room_input_participants_ready?: boolean; requireRoomVideoInputReady?: boolean; require_room_video_input_ready?: boolean; }; @@ -63,9 +61,6 @@ export async function POST(req: Request) { readiness: { requireAgentSessionReady: body.requireAgentSessionReady === true || body.require_agent_session_ready === true, - requireRoomInputParticipantsReady: - body.requireRoomInputParticipantsReady === true || - body.require_room_input_participants_ready === true, requireRoomVideoInputReady: body.requireRoomVideoInputReady === true || body.require_room_video_input_ready === true, }, diff --git a/hooks/useRoom.ts b/hooks/useRoom.ts index 9a5aa90fd..1206c8c5a 100644 --- a/hooks/useRoom.ts +++ b/hooks/useRoom.ts @@ -6,10 +6,7 @@ import { useBrowserSourceClient } from '@/hooks/useBrowserSourceClient'; import { getVoiceSessionId, resetVoiceSessionId } from '@/lib/browser-room-session'; import { readConnectionDetailsResponse } from '@/lib/connection-details-response'; import { isValidConnectionRoomId } from '@/lib/connection-room-id'; -import { - usesBothServerRoomInputParticipants, - usesServerRoomInputDevice, -} from '@/lib/input-device-config'; +import { usesServerRoomInputDevice } from '@/lib/input-device-config'; import { FRONTEND_EVENTS, beginFrontendObservabilitySession, @@ -223,10 +220,6 @@ export function useRoom(appConfig: AppConfig) { const signal = beginAgentSessionStart(room.name, sessionId); const dispatchPromise = requestAgentSessionDispatch(appConfig.agentName, sessionId, { requireAgentSessionReady: usesManagedRoomInput, - requireRoomInputParticipantsReady: usesBothServerRoomInputParticipants( - appConfig.audioInputDevice, - appConfig.visionInputDevice - ), requireRoomVideoInputReady: requiresRoomVideoInputReady(appConfig), signal, }); diff --git a/lib/input-device-config.ts b/lib/input-device-config.ts index e89d7bd21..7807150fc 100644 --- a/lib/input-device-config.ts +++ b/lib/input-device-config.ts @@ -42,16 +42,6 @@ export function usesServerRoomInputDevice(inputDevice: string): boolean { return SERVER_ROOM_INPUT_DEVICES.has(inputDevice); } -export function usesBothServerRoomInputParticipants( - audioInputDevice?: string | null, - visionInputDevice?: string | null -): boolean { - return ( - usesServerRoomInputDevice(audioInputDevice || '') && - usesServerRoomInputDevice(visionInputDevice || '') - ); -} - export function resolveRoleInputDevices({ inputSource, audioInputDevice, diff --git a/lib/session-dispatch-client.ts b/lib/session-dispatch-client.ts index 10812ba40..1635966dc 100644 --- a/lib/session-dispatch-client.ts +++ b/lib/session-dispatch-client.ts @@ -1,7 +1,6 @@ type DispatchOptions = { signal?: AbortSignal; requireAgentSessionReady?: boolean; - requireRoomInputParticipantsReady?: boolean; requireRoomVideoInputReady?: boolean; }; @@ -30,9 +29,6 @@ export async function requestAgentSessionDispatch( agentName: normalizedAgentName, sessionId: normalizedSessionId, ...(options.requireAgentSessionReady ? { requireAgentSessionReady: true } : {}), - ...(options.requireRoomInputParticipantsReady - ? { requireRoomInputParticipantsReady: true } - : {}), ...(options.requireRoomVideoInputReady ? { requireRoomVideoInputReady: true } : {}), }), signal: options.signal, diff --git a/tests/local-dispatch-config.test.mjs b/tests/local-dispatch-config.test.mjs index abf44fd18..15c9a703d 100644 --- a/tests/local-dispatch-config.test.mjs +++ b/tests/local-dispatch-config.test.mjs @@ -2,8 +2,6 @@ import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; import { test } from 'node:test'; -const { usesBothServerRoomInputParticipants } = await import('../lib/input-device-config.ts'); - async function loadAppConfigModule() { return import('../app-config.ts'); } @@ -138,13 +136,6 @@ test('frontend resolves mixed xunfei audio with browser vision role devices', as assert.equal(config.showDefaultCameraPreview, false); }); -test('room participant readiness follows the configured server-owned inputs', () => { - assert.equal(usesBothServerRoomInputParticipants('xunfei', 'generic'), true); - assert.equal(usesBothServerRoomInputParticipants('xunfei', 'browser'), false); - assert.equal(usesBothServerRoomInputParticipants('browser', 'generic'), false); - assert.equal(usesBothServerRoomInputParticipants('browser', 'browser'), false); -}); - test('frontend normalizes invalid mixed output devices to the base role input device', async () => { const { resolveInputDeviceConfig } = await loadAppConfigModule(); diff --git a/tests/session-dispatch-client.test.mjs b/tests/session-dispatch-client.test.mjs index b57f06077..375aa80cb 100644 --- a/tests/session-dispatch-client.test.mjs +++ b/tests/session-dispatch-client.test.mjs @@ -43,7 +43,7 @@ test('agent session dispatch sends only canonical session id to Next API', async } }); -test('agent session dispatch can require managed room input readiness', async () => { +test('agent session dispatch can require authoritative session readiness', async () => { const originalFetch = globalThis.fetch; let postedBody; globalThis.fetch = async (_url, init) => { @@ -56,7 +56,6 @@ test('agent session dispatch can require managed room input readiness', async () await requestAgentSessionDispatch('agent-a', '11111111-2222-4333-8444-555555555555', { requireAgentSessionReady: true, - requireRoomInputParticipantsReady: true, requireRoomVideoInputReady: true, }); @@ -64,7 +63,6 @@ test('agent session dispatch can require managed room input readiness', async () agentName: 'agent-a', sessionId: '11111111-2222-4333-8444-555555555555', requireAgentSessionReady: true, - requireRoomInputParticipantsReady: true, requireRoomVideoInputReady: true, }); } finally { diff --git a/tests/session-start-dispatch.test.mjs b/tests/session-start-dispatch.test.mjs index 35a87e0b1..404e665bc 100644 --- a/tests/session-start-dispatch.test.mjs +++ b/tests/session-start-dispatch.test.mjs @@ -179,8 +179,6 @@ test('session dispatch route only accepts anonymous LiveKit agent fallback after assert.match(routeSource, /require_room_video_input_ready/); assert.match(routeSource, /requireAgentSessionReady/); assert.match(routeSource, /require_agent_session_ready/); - assert.match(routeSource, /requireRoomInputParticipantsReady/); - assert.match(routeSource, /require_room_input_participants_ready/); assert.match(readinessSource, /type AgentParticipantMatchOptions/); assert.match(readinessSource, /type ReusableAgentParticipantOptions/); assert.match(readinessSource, /allowAnonymousLiveKitAgentFallback/); @@ -216,10 +214,6 @@ test('start call dispatches the agent with a cancellable room session id', async /requireRoomVideoInputReady: requiresRoomVideoInputReady\(appConfig\)/ ); assert.match(useRoomSource, /requireAgentSessionReady: usesManagedRoomInput/); - assert.match( - useRoomSource, - /requireRoomInputParticipantsReady: usesBothServerRoomInputParticipants\(/ - ); assert.match(useRoomSource, /await Promise\.allSettled\(\[/); assert.ok( useRoomSource.lastIndexOf('setIsSessionActive(true)') > From 18db439cd6569fa4bf7a7e6571eefe8fe89aa9d5 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 21 Aug 2026 13:24:57 +0800 Subject: [PATCH 4/6] fix: keep video readiness out of voice startup --- app/api/session/dispatch/route.ts | 4 ---- hooks/useRoom.ts | 8 -------- lib/session-dispatch-client.ts | 2 -- tests/session-dispatch-client.test.mjs | 2 -- tests/session-start-dispatch.test.mjs | 6 ------ 5 files changed, 22 deletions(-) diff --git a/app/api/session/dispatch/route.ts b/app/api/session/dispatch/route.ts index 641fb02ad..59661c172 100644 --- a/app/api/session/dispatch/route.ts +++ b/app/api/session/dispatch/route.ts @@ -22,8 +22,6 @@ export async function POST(req: Request) { session_id?: string; requireAgentSessionReady?: boolean; require_agent_session_ready?: boolean; - requireRoomVideoInputReady?: boolean; - require_room_video_input_ready?: boolean; }; try { body = await req.json(); @@ -61,8 +59,6 @@ export async function POST(req: Request) { readiness: { requireAgentSessionReady: body.requireAgentSessionReady === true || body.require_agent_session_ready === true, - requireRoomVideoInputReady: - body.requireRoomVideoInputReady === true || body.require_room_video_input_ready === true, }, }); return NextResponse.json({ status: 'dispatched', roomName, agentName, sessionId, dispatch }); diff --git a/hooks/useRoom.ts b/hooks/useRoom.ts index 1206c8c5a..25e023a88 100644 --- a/hooks/useRoom.ts +++ b/hooks/useRoom.ts @@ -6,7 +6,6 @@ import { useBrowserSourceClient } from '@/hooks/useBrowserSourceClient'; import { getVoiceSessionId, resetVoiceSessionId } from '@/lib/browser-room-session'; import { readConnectionDetailsResponse } from '@/lib/connection-details-response'; import { isValidConnectionRoomId } from '@/lib/connection-room-id'; -import { usesServerRoomInputDevice } from '@/lib/input-device-config'; import { FRONTEND_EVENTS, beginFrontendObservabilitySession, @@ -27,12 +26,6 @@ import { waitForAgentSessionStop, } from '@/lib/session-stop-client'; -function requiresRoomVideoInputReady(appConfig: AppConfig) { - return appConfig.visionInputDevice - ? usesServerRoomInputDevice(appConfig.visionInputDevice) - : false; -} - export function useRoom(appConfig: AppConfig) { const aborted = useRef(false); const sessionIdRef = useRef(null); @@ -220,7 +213,6 @@ export function useRoom(appConfig: AppConfig) { const signal = beginAgentSessionStart(room.name, sessionId); const dispatchPromise = requestAgentSessionDispatch(appConfig.agentName, sessionId, { requireAgentSessionReady: usesManagedRoomInput, - requireRoomVideoInputReady: requiresRoomVideoInputReady(appConfig), signal, }); registerAgentSessionDispatch(room.name, sessionId, dispatchPromise); diff --git a/lib/session-dispatch-client.ts b/lib/session-dispatch-client.ts index 1635966dc..06fa7c93d 100644 --- a/lib/session-dispatch-client.ts +++ b/lib/session-dispatch-client.ts @@ -1,7 +1,6 @@ type DispatchOptions = { signal?: AbortSignal; requireAgentSessionReady?: boolean; - requireRoomVideoInputReady?: boolean; }; export class AgentSessionDispatchCancelledError extends Error { @@ -29,7 +28,6 @@ export async function requestAgentSessionDispatch( agentName: normalizedAgentName, sessionId: normalizedSessionId, ...(options.requireAgentSessionReady ? { requireAgentSessionReady: true } : {}), - ...(options.requireRoomVideoInputReady ? { requireRoomVideoInputReady: true } : {}), }), signal: options.signal, }); diff --git a/tests/session-dispatch-client.test.mjs b/tests/session-dispatch-client.test.mjs index 375aa80cb..3e0c9a8c2 100644 --- a/tests/session-dispatch-client.test.mjs +++ b/tests/session-dispatch-client.test.mjs @@ -56,14 +56,12 @@ test('agent session dispatch can require authoritative session readiness', async await requestAgentSessionDispatch('agent-a', '11111111-2222-4333-8444-555555555555', { requireAgentSessionReady: true, - requireRoomVideoInputReady: true, }); assert.deepEqual(postedBody, { agentName: 'agent-a', sessionId: '11111111-2222-4333-8444-555555555555', requireAgentSessionReady: true, - requireRoomVideoInputReady: true, }); } finally { globalThis.fetch = originalFetch; diff --git a/tests/session-start-dispatch.test.mjs b/tests/session-start-dispatch.test.mjs index 404e665bc..eba6d783f 100644 --- a/tests/session-start-dispatch.test.mjs +++ b/tests/session-start-dispatch.test.mjs @@ -175,8 +175,6 @@ test('session dispatch route only accepts anonymous LiveKit agent fallback after serviceSource, /const alreadyJoined = await findReusableAgentParticipant\(\s*roomClient,\s*roomName,\s*agentName,\s*reusableAgentOptions\s*\);/ ); - assert.match(routeSource, /requireRoomVideoInputReady/); - assert.match(routeSource, /require_room_video_input_ready/); assert.match(routeSource, /requireAgentSessionReady/); assert.match(routeSource, /require_agent_session_ready/); assert.match(readinessSource, /type AgentParticipantMatchOptions/); @@ -209,10 +207,6 @@ test('start call dispatches the agent with a cancellable room session id', async assert.match(useRoomSource, /isExpectedStartCancellation/); assert.match(useRoomSource, /waitForAgentSessionStop/); assert.match(useRoomSource, /requestAgentSessionDispatch\(\s*appConfig\.agentName,\s*sessionId,/); - assert.match( - useRoomSource, - /requireRoomVideoInputReady: requiresRoomVideoInputReady\(appConfig\)/ - ); assert.match(useRoomSource, /requireAgentSessionReady: usesManagedRoomInput/); assert.match(useRoomSource, /await Promise\.allSettled\(\[/); assert.ok( From 5ac7bad3de9d8bee0abcf35de4e238ba76a8e2e2 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 21 Aug 2026 13:29:21 +0800 Subject: [PATCH 5/6] fix: recognize server serialized readiness attributes --- lib/session-dispatch-readiness.ts | 7 ++++++- tests/session-dispatch-readiness.test.mjs | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/lib/session-dispatch-readiness.ts b/lib/session-dispatch-readiness.ts index 2c6026cc3..0b55e95b1 100644 --- a/lib/session-dispatch-readiness.ts +++ b/lib/session-dispatch-readiness.ts @@ -16,6 +16,7 @@ export type ReusableAgentParticipantOptions = AgentParticipantMatchOptions & { }; export const AGENT_SESSION_READY_ATTRIBUTE = 'liveavatar.agent.session_ready'; +const AGENT_SESSION_READY_ATTRIBUTE_CAMEL = 'liveavatarAgentSessionReady'; const ROOM_AUDIO_INPUT_IDENTITY = 'room_audio_input'; const ROOM_VIDEO_INPUT_IDENTITY = 'room_video_input'; @@ -127,7 +128,11 @@ function isExpectedAgentParticipant(participant: ParticipantInfo, agentName: str } function isAgentSessionReady(participant: ParticipantInfo) { - return participant.attributes?.[AGENT_SESSION_READY_ATTRIBUTE] === 'true'; + const attributes = participant.attributes ?? {}; + return ( + attributes[AGENT_SESSION_READY_ATTRIBUTE] === 'true' || + attributes[AGENT_SESSION_READY_ATTRIBUTE_CAMEL] === 'true' + ); } function isAnonymousLiveKitAgentParticipant(participant: ParticipantInfo) { diff --git a/tests/session-dispatch-readiness.test.mjs b/tests/session-dispatch-readiness.test.mjs index 7977a5d74..93567c1d6 100644 --- a/tests/session-dispatch-readiness.test.mjs +++ b/tests/session-dispatch-readiness.test.mjs @@ -62,6 +62,24 @@ test('dispatch can require room video input readiness before reusing an agent', ); }); +test('dispatch accepts the server SDK camel-cased agent ready attribute', () => { + const agent = participant({ + identity: 'agent-AJ_ready', + kind: ParticipantInfo_Kind.AGENT, + attributes: { + lkAgentName: 'frontdesk-agent', + liveavatarAgentSessionReady: 'true', + }, + }); + + assert.equal( + findReusableAgentParticipant([agent], 'frontdesk-agent', { + requireAgentSessionReady: true, + }), + agent + ); +}); + test('dispatch can reuse an active agent once room video input is publishing', () => { const agent = participant({ identity: 'agent-AJ_running', From 5e4c87c1c4cd3137325e1f5b73f04b7e57d13ef4 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 21 Aug 2026 13:57:28 +0800 Subject: [PATCH 6/6] fix: gate prewarm on authoritative session readiness --- app/api/session/session-dispatch-service.ts | 1 - lib/session-dispatch-readiness.ts | 1 + tests/session-prewarm.test.mjs | 6 +++--- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/api/session/session-dispatch-service.ts b/app/api/session/session-dispatch-service.ts index d05b9ea5b..44075ff1d 100644 --- a/app/api/session/session-dispatch-service.ts +++ b/app/api/session/session-dispatch-service.ts @@ -302,7 +302,6 @@ export async function prewarmRoomSession( ...request, readiness: { requireAgentSessionReady: true, - requireRoomInputParticipantsReady: true, }, }, { diff --git a/lib/session-dispatch-readiness.ts b/lib/session-dispatch-readiness.ts index 0b55e95b1..51cb58e47 100644 --- a/lib/session-dispatch-readiness.ts +++ b/lib/session-dispatch-readiness.ts @@ -16,6 +16,7 @@ export type ReusableAgentParticipantOptions = AgentParticipantMatchOptions & { }; export const AGENT_SESSION_READY_ATTRIBUTE = 'liveavatar.agent.session_ready'; +// livekit-server-sdk maps protobuf attribute keys to camelCase object keys. const AGENT_SESSION_READY_ATTRIBUTE_CAMEL = 'liveavatarAgentSessionReady'; const ROOM_AUDIO_INPUT_IDENTITY = 'room_audio_input'; diff --git a/tests/session-prewarm.test.mjs b/tests/session-prewarm.test.mjs index 1eff3fc6b..243ac835e 100644 --- a/tests/session-prewarm.test.mjs +++ b/tests/session-prewarm.test.mjs @@ -331,7 +331,7 @@ test('missing LiveKit configuration fails before registering a room session', as } }); -test('regular dispatch keeps its 8s timeout while prewarm gets the default 45s total budget', async () => { +test('regular dispatch keeps its 30s timeout while prewarm gets the default 45s total budget', async () => { const originalNow = Date.now; const originalTimeout = process.env.AGENT_DISPATCH_TIMEOUT_MS; const originalPrewarmTimeout = process.env.LIVEAVATAR_PREWARM_TOTAL_TIMEOUT_MS; @@ -1322,7 +1322,7 @@ test('shared dispatch token stays active through per-caller readiness waits', as assert.doesNotMatch(readinessSource, /beginRoomSessionDispatch|finishRoomSessionDispatch/); }); -test('prewarm waits for the agent session and both room input participants', async () => { +test('prewarm completes when the agent session is ready without waiting for optional video input', async () => { const agentName = 'frontdesk-browser-agent-readiness'; let roomCreated = false; let workerReady = false; @@ -1395,7 +1395,7 @@ test('prewarm waits for the agent session and both room input participants', asy assert.deepEqual(result.readiness, { agentSessionReady: true, audioParticipantReady: true, - visionParticipantReady: true, + visionParticipantReady: false, }); });