Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
7152df1
perf: Prewarm LiveAvatar browser sessions
jiejuncai-ly Jul 13, 2026
425c8fe
fix: Scope VAD assets to sandbox sessions
jiejuncai-ly Jul 14, 2026
cac16fc
fix: preserve per-caller dispatch readiness
jiejuncai-ly Jul 14, 2026
b9e0ac0
fix: retain dispatch state through readiness waits
jiejuncai-ly Jul 14, 2026
7f64d87
test: share session registry instance in CI
jiejuncai-ly Jul 14, 2026
e2cedc5
test: verify shared dispatch lifecycle
jiejuncai-ly Jul 14, 2026
9d160e4
fix: align prewarm and client session identity
jiejuncai-ly Jul 14, 2026
d813831
fix: cancel dispatch when local media fails
jiejuncai-ly Jul 14, 2026
6a8c8f9
fix: preserve dispatch timeout boundaries
jiejuncai-ly Jul 17, 2026
9863a25
test: verify repeated prewarm reuse
jiejuncai-ly Jul 17, 2026
d2300d2
fix: consume prewarm authorization once
jiejuncai-ly Jul 17, 2026
7153320
test: share prewarm guard module instance
jiejuncai-ly Jul 17, 2026
afc1dd1
fix: share prewarm guard across server chunks
jiejuncai-ly Jul 17, 2026
e2ddad2
fix: report concurrent media capture failures
jiejuncai-ly Jul 17, 2026
a22db10
test: verify dispatch timeout behavior
jiejuncai-ly Jul 17, 2026
91e5494
fix: preserve shared dispatch timeout budget
jiejuncai-ly Jul 17, 2026
ab4ce82
fix: honor late shared dispatch deadline
jiejuncai-ly Jul 17, 2026
0a86192
fix: share the prewarm timeout budget
jiejuncai-ly Jul 17, 2026
8ffb46b
fix: share session coordination across chunks
jiejuncai-ly Jul 17, 2026
d2abb62
fix: support dynamic sandbox proxy paths
jiejuncai-ly Jul 22, 2026
97196a6
fix: bound prewarm cold-start readiness
jiejuncai-ly Jul 24, 2026
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
4 changes: 2 additions & 2 deletions app-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,9 +220,9 @@ export const APP_CONFIG_DEFAULTS: AppConfig = {
remoteVideoHeight: 480,
remoteVideoFps: 25,

logo: '/lk-logo.png',
logo: 'lk-logo.png',
accent: '#002cf2',
logoDark: '/lk-logo-dark.png',
logoDark: 'lk-logo-dark.png',
accentDark: '#1fd5f9',
startButtonText: 'Start call',

Expand Down
129 changes: 129 additions & 0 deletions app/api/session/agent-worker-readiness.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { readFile } from 'node:fs/promises';
import path from 'node:path';

const DEFAULT_READY_TIMEOUT_MS = 30_000;
const DEFAULT_READY_POLL_MS = 200;

type AgentWorkerReadyMarker = {
version: number;
agentName: string;
workerId: string;
registeredAt: string;
};

export type WaitForAgentWorkerReadyOptions = {
readyFile?: string;
timeoutMs?: number;
maxWaitMs?: number;
pollMs?: number;
readFile?: (filePath: string) => Promise<string>;
sleep?: (ms: number) => Promise<unknown>;
};

export type AgentWorkerReadiness =
| {
state: 'ready';
agentName: string;
workerId: string;
registeredAt: string;
waitedMs: number;
}
| {
state: 'skipped';
agentName: string;
reason: 'not_sandbox';
waitedMs: 0;
};

export function resolveAgentWorkerReadyFile(env: NodeJS.ProcessEnv = process.env): string {
const configured = (env.LIVEAVATAR_AGENT_WORKER_READY_FILE || '').trim();
if (configured) {
return configured;
}
if ((env.LIVEAVATAR_RUNTIME_MODE || '').trim().toLowerCase() !== 'sandbox') {
return '';
}
const workspaceDataDir = (env.LIVEAVATAR_SANDBOX_WORKSPACE_DATA_DIR || '/workspace/data').trim();
return workspaceDataDir
? path.join(workspaceDataDir, 'logs', 'sandbox', 'agent-worker-ready.json')
: '';
}

export function resolveAgentWorkerReadyTimeoutMs(env: NodeJS.ProcessEnv = process.env): number {
return readPositiveInt(env.LIVEAVATAR_AGENT_WORKER_READY_TIMEOUT_MS, DEFAULT_READY_TIMEOUT_MS);
}

export async function waitForAgentWorkerReady(
agentName: string,
options: WaitForAgentWorkerReadyOptions = {}
): Promise<AgentWorkerReadiness> {
const readyFile = options.readyFile ?? resolveAgentWorkerReadyFile();
if (!readyFile) {
return { state: 'skipped', agentName, reason: 'not_sandbox', waitedMs: 0 };
}

const configuredTimeoutMs = options.timeoutMs ?? resolveAgentWorkerReadyTimeoutMs();
const timeoutMs =
options.maxWaitMs === undefined
? configuredTimeoutMs
: Math.max(0, Math.min(configuredTimeoutMs, options.maxWaitMs));
if (timeoutMs <= 0) {
throw new Error(`agent worker did not register before prewarm timeout: ${agentName}`);
}
const pollMs =
options.pollMs ??
readPositiveInt(process.env.LIVEAVATAR_AGENT_WORKER_READY_POLL_MS, DEFAULT_READY_POLL_MS);
const readMarkerFile = options.readFile || ((filePath: string) => readFile(filePath, 'utf8'));
const sleepFn = options.sleep || sleep;
const startedAt = Date.now();
const deadline = startedAt + timeoutMs;

do {
const marker = await readReadyMarker(readyFile, readMarkerFile);
if (marker?.agentName === agentName && marker.workerId) {
return {
state: 'ready',
agentName,
workerId: marker.workerId,
registeredAt: marker.registeredAt,
waitedMs: Date.now() - startedAt,
};
}

const waitMs = Math.min(pollMs, deadline - Date.now());
if (waitMs > 0) {
await sleepFn(waitMs);
}
} while (Date.now() < deadline);

throw new Error(`agent worker did not register before prewarm timeout: ${agentName}`);
}

async function readReadyMarker(
readyFile: string,
readMarkerFile: (filePath: string) => Promise<string>
): Promise<AgentWorkerReadyMarker | null> {
try {
const payload = JSON.parse(await readMarkerFile(readyFile)) as Partial<AgentWorkerReadyMarker>;
if (
payload.version !== 1 ||
typeof payload.agentName !== 'string' ||
typeof payload.workerId !== 'string' ||
typeof payload.registeredAt !== 'string'
) {
return null;
}
return payload as AgentWorkerReadyMarker;
} catch {
return null;
}
}

function readPositiveInt(value: string | undefined, fallback: number): number {
const parsed = Number.parseInt(String(value || ''), 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}

function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
Loading
Loading