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
37 changes: 24 additions & 13 deletions app/api/session/stop/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
isValidConnectionRoomId,
} from '@/lib/connection-room-id';
import {
executeRoomInputStopsSequentially,
resolveRoomInputStopUrls as resolveConfiguredRoomInputStopUrls,
resolveLiveKitHttpUrl,
} from '@/lib/session-stop';
Expand Down Expand Up @@ -158,18 +159,14 @@ function resolveRoomInputStopUrls(): string[] {
'ROOM_VISION_INPUT_DEVICE',
'NEXT_PUBLIC_ROOM_VISION_INPUT_DEVICE'
),
roomAudioInputUrl: readStopEnv('ROOM_AUDIO_INPUT_URL'),
roomVisionInputUrl: readStopEnv('ROOM_VISION_INPUT_URL'),
roomInputUrl: readStopEnv('ROOM_INPUT_URL'),
frontdeskInputParticipantUrl: readStopEnv('FRONTDESK_INPUT_PARTICIPANT_URL'),
faceServiceUrl: readStopEnv('FACE_SERVICE_URL'),
genericCameraParticipantUrl: readStopEnv('GENERIC_CAMERA_PARTICIPANT_URL'),
videoProcessorUrl: readStopEnv('VIDEO_PROCESSOR_URL'),
edgeMediaUrl: readStopEnv('EDGE_MEDIA_URL'),
});
}

function resolveLocalLiveKitServerLogPath(): string {
function resolveLocalAgentWorkerLogPath(): string {
const runLogDir = process.env.LEXVOICE_RUN_LOG_DIR?.trim();
return runLogDir ? path.join(runLogDir, 'server.log') : '';
return runLogDir ? path.join(runLogDir, 'live.log') : '';
}

function sleep(ms: number): Promise<void> {
Expand All @@ -187,7 +184,7 @@ async function fileExists(filePath: string): Promise<boolean> {
}
}

async function readAgentWorkerStateFromServerLog(
async function readAgentWorkerStateFromLocalLog(
logPath: string,
agentName: string
): Promise<AgentWorkerState> {
Expand All @@ -214,15 +211,15 @@ async function waitForLocalAgentWorkerReadiness(): Promise<StopResult> {
return { target: 'agent_worker_readiness', ok: true, skipped: true };
}

const logPath = resolveLocalLiveKitServerLogPath();
const logPath = resolveLocalAgentWorkerLogPath();
const agentName = readStopAgentName();
if (!logPath || !(await fileExists(logPath))) {
return { target: 'agent_worker_readiness', ok: true, skipped: true };
}

const deadline = Date.now() + AGENT_WORKER_READINESS_TIMEOUT_MS;
while (Date.now() <= deadline) {
const state = await readAgentWorkerStateFromServerLog(logPath, agentName);
const state = await readAgentWorkerStateFromLocalLog(logPath, agentName);
if (state === 'available') {
return { target: 'agent_worker_readiness', ok: true };
}
Expand Down Expand Up @@ -341,12 +338,26 @@ async function postRoomInputStop(
}

async function stopRoomInput(roomName: string, sessionId: string): Promise<StopResult[]> {
const stopUrls = resolveRoomInputStopUrls();
let stopUrls: string[];
try {
stopUrls = resolveRoomInputStopUrls();
} catch (error) {
return [
{
target: 'room_input_configuration',
ok: false,
fatal: true,
error: error instanceof Error ? error.message : String(error),
},
];
}
if (stopUrls.length === 0) {
return [{ target: 'room_input', ok: true, skipped: true }];
}

return Promise.all(stopUrls.map((stopUrl) => postRoomInputStop(stopUrl, roomName, sessionId)));
return executeRoomInputStopsSequentially(stopUrls, (stopUrl) =>
postRoomInputStop(stopUrl, roomName, sessionId)
);
}

async function runRemoteSessionCleanup(
Expand Down
11 changes: 11 additions & 0 deletions lib/agent-worker-readiness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,19 @@ export function readAgentWorkerStateFromLog(source: string, agentName: string):
const agentNamePattern = new RegExp(`"agentName"\\s*:\\s*"${escapeRegExp(agentName)}"`);
const availablePattern = /"status"\s*:\s*"WS_AVAILABLE"/;
const unavailablePattern = /"status"\s*:\s*"WS_FULL"/;
// The run-scoped live.log contains one local worker; these SDK capacity lines omit agentName.
const localAvailablePattern = /worker is below capacity, marking as available/;
const localUnavailablePattern = /worker is at full capacity, marking as unavailable/;

for (const line of source.split(/\r?\n/)) {
if (localAvailablePattern.test(line)) {
state = 'available';
continue;
}
if (localUnavailablePattern.test(line)) {
state = 'unavailable';
continue;
}
if (!agentNamePattern.test(line)) {
continue;
}
Expand Down
70 changes: 28 additions & 42 deletions lib/session-stop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,8 @@ export interface ResolveRoomInputStopUrlsOptions {
inputSource?: string | null;
audioInputDevice?: string | null;
visionInputDevice?: string | null;
/**
* Room-input control URLs are configured as base endpoint paths. The
* normalizer intentionally strips query/hash fragments when switching
* between /start and /stop so stop calls do not inherit start-only params.
*/
roomAudioInputUrl?: string | null;
roomVisionInputUrl?: string | null;
roomInputUrl?: string | null;
frontdeskInputParticipantUrl?: string | null;
faceServiceUrl?: string | null;
genericCameraParticipantUrl?: string | null;
videoProcessorUrl?: string | null;
edgeMediaUrl?: string | null;
}

export function resolveLiveKitHttpUrl(liveKitUrl?: string | null): string | undefined {
Expand All @@ -33,13 +24,6 @@ export function resolveLiveKitHttpUrl(liveKitUrl?: string | null): string | unde
return normalized;
}

function addRoomInputStopUrl(urls: Set<string>, rawUrl?: string | null): void {
const stopUrl = normalizeRoomInputControlUrl(rawUrl || '', 'stop');
if (stopUrl) {
urls.add(stopUrl);
}
}

export function normalizeRoomInputControlUrl(
rawUrl: string,
action: RoomInputControlAction
Expand Down Expand Up @@ -77,16 +61,23 @@ export function normalizeRoomInputControlUrl(
}
}

export async function executeRoomInputStopsSequentially<T>(
stopUrls: readonly string[],
stop: (stopUrl: string) => Promise<T>
): Promise<T[]> {
const results: T[] = [];
for (const stopUrl of stopUrls) {
results.push(await stop(stopUrl));
}
return results;
}

export function resolveRoomInputStopUrls({
inputSource,
audioInputDevice,
visionInputDevice,
roomAudioInputUrl,
roomVisionInputUrl,
roomInputUrl,
frontdeskInputParticipantUrl,
faceServiceUrl,
genericCameraParticipantUrl,
videoProcessorUrl,
edgeMediaUrl,
}: ResolveRoomInputStopUrlsOptions): string[] {
const {
audioInputDevice: resolvedAudioInputDevice,
Expand All @@ -97,28 +88,23 @@ export function resolveRoomInputStopUrls({
visionInputDevice,
});

const urls = new Set<string>();
const selectedServerDevices = new Set<string>();

if (usesServerRoomInputDevice(resolvedAudioInputDevice)) {
selectedServerDevices.add(resolvedAudioInputDevice);
addRoomInputStopUrl(urls, roomAudioInputUrl || roomInputUrl);
}
if (usesServerRoomInputDevice(resolvedVisionInputDevice)) {
selectedServerDevices.add(resolvedVisionInputDevice);
addRoomInputStopUrl(urls, roomVisionInputUrl || roomInputUrl);
}
if (selectedServerDevices.size === 0) {
const usesServerInput =
usesServerRoomInputDevice(resolvedAudioInputDevice) ||
usesServerRoomInputDevice(resolvedVisionInputDevice);
if (!usesServerInput) {
return [];
}

if (selectedServerDevices.has('xunfei')) {
addRoomInputStopUrl(urls, frontdeskInputParticipantUrl);
addRoomInputStopUrl(urls, faceServiceUrl);
const videoProcessorStopUrl = normalizeRoomInputControlUrl(videoProcessorUrl || '', 'stop');
const edgeMediaStopUrl = normalizeRoomInputControlUrl(edgeMediaUrl || '', 'stop');
if (!videoProcessorStopUrl || !edgeMediaStopUrl) {
throw new Error('VIDEO_PROCESSOR_URL and EDGE_MEDIA_URL are required for server room input');
}
if (selectedServerDevices.has('generic')) {
addRoomInputStopUrl(urls, genericCameraParticipantUrl);
if (videoProcessorStopUrl === edgeMediaStopUrl) {
throw new Error(
'VIDEO_PROCESSOR_URL and EDGE_MEDIA_URL must resolve to distinct stop endpoints'
);
}

return [...urls];
return [videoProcessorStopUrl, edgeMediaStopUrl];
}
Loading
Loading