diff --git a/src/gateway/server-methods/sessions.ts b/src/gateway/server-methods/sessions.ts index dcf1ade212f..851360a088b 100644 --- a/src/gateway/server-methods/sessions.ts +++ b/src/gateway/server-methods/sessions.ts @@ -103,12 +103,46 @@ import { assertValidParams } from "./validation.js"; type SessionsRuntimeModule = typeof import("./sessions.runtime.js"); let sessionsRuntimeModulePromise: Promise | undefined; +let loggedSlowSessionsListCatalog = false; + +const SESSIONS_LIST_MODEL_CATALOG_TIMEOUT_MS = 750; function loadSessionsRuntimeModule(): Promise { sessionsRuntimeModulePromise ??= import("./sessions.runtime.js"); return sessionsRuntimeModulePromise; } +async function loadOptionalSessionsListModelCatalog( + context: GatewayRequestContext, +): Promise> | undefined> { + let timeout: NodeJS.Timeout | undefined; + const timedOut = Symbol("sessions-list-model-catalog-timeout"); + const timeoutPromise = new Promise((resolve) => { + timeout = setTimeout(() => resolve(timedOut), SESSIONS_LIST_MODEL_CATALOG_TIMEOUT_MS); + timeout.unref?.(); + }); + try { + const result = await Promise.race([ + context.loadGatewayModelCatalog().catch(() => undefined), + timeoutPromise, + ]); + if (result === timedOut) { + if (!loggedSlowSessionsListCatalog) { + loggedSlowSessionsListCatalog = true; + context.logGateway.debug( + `sessions.list continuing without model catalog after ${SESSIONS_LIST_MODEL_CATALOG_TIMEOUT_MS}ms`, + ); + } + return undefined; + } + return Array.isArray(result) ? result : undefined; + } finally { + if (timeout) { + clearTimeout(timeout); + } + } +} + function requireSessionKey(key: unknown, respond: RespondFn): string | null { const raw = typeof key === "string" @@ -613,8 +647,7 @@ export const sessionsHandlers: GatewayRequestHandlers = { const p = params; const cfg = context.getRuntimeConfig(); const { storePath, store } = loadCombinedSessionStoreForGateway(cfg); - const loadedCatalog = await context.loadGatewayModelCatalog().catch(() => undefined); - const modelCatalog = Array.isArray(loadedCatalog) ? loadedCatalog : undefined; + const modelCatalog = await loadOptionalSessionsListModelCatalog(context); const result = listSessionsFromStore({ cfg, storePath, diff --git a/src/gateway/server.sessions.gateway-server-sessions-a.test.ts b/src/gateway/server.sessions.gateway-server-sessions-a.test.ts index 4f57498c7ca..552130daa9f 100644 --- a/src/gateway/server.sessions.gateway-server-sessions-a.test.ts +++ b/src/gateway/server.sessions.gateway-server-sessions-a.test.ts @@ -46,6 +46,16 @@ async function getSessionsHandlers() { return (await import("./server-methods/sessions.js")).sessionsHandlers; } +function createDeferred() { + let resolve!: (value: T | PromiseLike) => void; + let reject!: (reason?: unknown) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve, reject }; +} + const sessionCleanupMocks = vi.hoisted(() => ({ clearSessionQueues: vi.fn((keys: Array) => { const clearedKeys = Array.from( @@ -832,6 +842,58 @@ describe("gateway server sessions", () => { ); }); + test("sessions.list does not block on slow model catalog discovery", async () => { + await createSessionStoreDir(); + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-main", + updatedAt: Date.now(), + }, + }, + }); + + vi.useFakeTimers(); + try { + const deferredCatalog = createDeferred(); + const respond = vi.fn(); + const sessionsHandlers = await getSessionsHandlers(); + const { getRuntimeConfig } = await getGatewayConfigModule(); + const request = sessionsHandlers["sessions.list"]({ + req: { + type: "req", + id: "req-sessions-list-slow-catalog", + method: "sessions.list", + params: {}, + }, + params: {}, + respond, + client: null, + isWebchatConnect: () => false, + context: { + getRuntimeConfig, + loadGatewayModelCatalog: vi.fn(() => deferredCatalog.promise), + logGateway: { + debug: vi.fn(), + }, + } as never, + }); + + await vi.advanceTimersByTimeAsync(800); + await request; + + expect(respond).toHaveBeenCalledWith( + true, + expect.objectContaining({ + sessions: expect.arrayContaining([expect.objectContaining({ key: "agent:main:main" })]), + }), + undefined, + ); + } finally { + vi.useRealTimers(); + } + }); + test("sessions.changed mutation events include live usage metadata", async () => { const { dir } = await createSessionStoreDir(); await fs.writeFile(