diff --git a/src/features/recent-projects/main/adapters/output/sources/CodexRecentProjectsSourceAdapter.ts b/src/features/recent-projects/main/adapters/output/sources/CodexRecentProjectsSourceAdapter.ts index b8fd002e..fa0af4a9 100644 --- a/src/features/recent-projects/main/adapters/output/sources/CodexRecentProjectsSourceAdapter.ts +++ b/src/features/recent-projects/main/adapters/output/sources/CodexRecentProjectsSourceAdapter.ts @@ -13,14 +13,15 @@ import type { RecentProjectIdentityResolver } from '@features/recent-projects/ma import type { ServiceContext } from '@main/services'; const CODEX_THREAD_LIMIT = 40; +const CODEX_INITIALIZE_TIMEOUT_MS = 6_000; const CODEX_LIVE_FETCH_TIMEOUT_MS = 4_500; const CODEX_ARCHIVED_FETCH_TIMEOUT_MS = 2_500; const CODEX_SESSION_OVERHEAD_TIMEOUT_MS = 1_500; const CODEX_TOTAL_FETCH_TIMEOUT_MS = - CODEX_LIVE_FETCH_TIMEOUT_MS + CODEX_ARCHIVED_FETCH_TIMEOUT_MS + CODEX_SESSION_OVERHEAD_TIMEOUT_MS; + CODEX_INITIALIZE_TIMEOUT_MS + CODEX_LIVE_FETCH_TIMEOUT_MS + CODEX_SESSION_OVERHEAD_TIMEOUT_MS; const CODEX_SOURCE_TIMEOUT_MS = CODEX_TOTAL_FETCH_TIMEOUT_MS + 500; const CODEX_LIVE_ONLY_FALLBACK_TOTAL_TIMEOUT_MS = - CODEX_LIVE_FETCH_TIMEOUT_MS + CODEX_SESSION_OVERHEAD_TIMEOUT_MS + 1_500; + CODEX_INITIALIZE_TIMEOUT_MS + CODEX_LIVE_FETCH_TIMEOUT_MS + CODEX_SESSION_OVERHEAD_TIMEOUT_MS; function isInteractiveSource(source: unknown): boolean { return source === 'vscode' || source === 'cli'; @@ -88,6 +89,7 @@ export class CodexRecentProjectsSourceAdapter implements RecentProjectsSourcePor limit: CODEX_THREAD_LIMIT, liveRequestTimeoutMs: CODEX_LIVE_FETCH_TIMEOUT_MS, archivedRequestTimeoutMs: CODEX_ARCHIVED_FETCH_TIMEOUT_MS, + initializeTimeoutMs: CODEX_INITIALIZE_TIMEOUT_MS, totalTimeoutMs: CODEX_TOTAL_FETCH_TIMEOUT_MS, }); @@ -137,6 +139,7 @@ export class CodexRecentProjectsSourceAdapter implements RecentProjectsSourcePor const liveFallback = await this.deps.appServerClient.listRecentLiveThreads(binaryPath, { limit: CODEX_THREAD_LIMIT, requestTimeoutMs: CODEX_LIVE_FETCH_TIMEOUT_MS, + initializeTimeoutMs: CODEX_INITIALIZE_TIMEOUT_MS, totalTimeoutMs: CODEX_LIVE_ONLY_FALLBACK_TOTAL_TIMEOUT_MS, }); diff --git a/src/features/recent-projects/main/infrastructure/codex/CodexAppServerClient.ts b/src/features/recent-projects/main/infrastructure/codex/CodexAppServerClient.ts index 10013c01..56985538 100644 --- a/src/features/recent-projects/main/infrastructure/codex/CodexAppServerClient.ts +++ b/src/features/recent-projects/main/infrastructure/codex/CodexAppServerClient.ts @@ -2,6 +2,7 @@ import type { JsonRpcSession, JsonRpcStdioClient } from './JsonRpcStdioClient'; const DEFAULT_REQUEST_TIMEOUT_MS = 3_000; const DEFAULT_TOTAL_TIMEOUT_MS = 8_000; +const DEFAULT_INITIALIZE_TIMEOUT_MS = 6_000; const MIN_SESSION_OVERHEAD_TIMEOUT_MS = 1_500; const SUPPRESSED_NOTIFICATION_METHODS = [ 'thread/started', @@ -52,6 +53,7 @@ export interface CodexRecentThreadsResult { interface ThreadListSessionOptions { binaryPath: string; requestTimeoutMs: number; + initializeTimeoutMs: number; totalTimeoutMs: number; label: string; } @@ -64,19 +66,25 @@ export class CodexAppServerClient { options: { limit: number; requestTimeoutMs?: number; + initializeTimeoutMs?: number; totalTimeoutMs?: number; } ): Promise { const requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS; + const initializeTimeoutMs = Math.max( + options.initializeTimeoutMs ?? DEFAULT_INITIALIZE_TIMEOUT_MS, + requestTimeoutMs + ); const totalTimeoutMs = Math.max( options.totalTimeoutMs ?? DEFAULT_TOTAL_TIMEOUT_MS, - requestTimeoutMs + MIN_SESSION_OVERHEAD_TIMEOUT_MS + initializeTimeoutMs + requestTimeoutMs + MIN_SESSION_OVERHEAD_TIMEOUT_MS ); return this.#withThreadListSession( { binaryPath, requestTimeoutMs, + initializeTimeoutMs, totalTimeoutMs, label: 'codex app-server thread/list live', }, @@ -104,21 +112,27 @@ export class CodexAppServerClient { limit: number; liveRequestTimeoutMs?: number; archivedRequestTimeoutMs?: number; + initializeTimeoutMs?: number; totalTimeoutMs?: number; } ): Promise { const liveRequestTimeoutMs = options.liveRequestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS; const archivedRequestTimeoutMs = options.archivedRequestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS; const sessionRequestTimeoutMs = Math.max(liveRequestTimeoutMs, archivedRequestTimeoutMs); + const initializeTimeoutMs = Math.max( + options.initializeTimeoutMs ?? DEFAULT_INITIALIZE_TIMEOUT_MS, + sessionRequestTimeoutMs + ); const totalTimeoutMs = Math.max( options.totalTimeoutMs ?? DEFAULT_TOTAL_TIMEOUT_MS, - liveRequestTimeoutMs + archivedRequestTimeoutMs + MIN_SESSION_OVERHEAD_TIMEOUT_MS + initializeTimeoutMs + sessionRequestTimeoutMs + MIN_SESSION_OVERHEAD_TIMEOUT_MS ); return this.#withThreadListSession( { binaryPath, requestTimeoutMs: sessionRequestTimeoutMs, + initializeTimeoutMs, totalTimeoutMs, label: 'codex app-server thread/list', }, @@ -193,7 +207,7 @@ export class CodexAppServerClient { optOutNotificationMethods: SUPPRESSED_NOTIFICATION_METHODS, }, }, - options.requestTimeoutMs + options.initializeTimeoutMs ); await session.notify('initialized'); diff --git a/test/features/recent-projects/main/infrastructure/CodexAppServerClient.test.ts b/test/features/recent-projects/main/infrastructure/CodexAppServerClient.test.ts index 549c8845..3dbc8b74 100644 --- a/test/features/recent-projects/main/infrastructure/CodexAppServerClient.test.ts +++ b/test/features/recent-projects/main/infrastructure/CodexAppServerClient.test.ts @@ -53,7 +53,7 @@ describe('CodexAppServerClient', () => { expect.objectContaining({ binaryPath: '/usr/local/bin/codex', requestTimeoutMs: 4500, - totalTimeoutMs: 8500, + totalTimeoutMs: 12000, }), expect.any(Function) ); @@ -135,7 +135,7 @@ describe('CodexAppServerClient', () => { expect(withSession).toHaveBeenCalledWith( expect.objectContaining({ - totalTimeoutMs: 8500, + totalTimeoutMs: 12000, }), expect.any(Function) ); @@ -171,7 +171,7 @@ describe('CodexAppServerClient', () => { expect.objectContaining({ binaryPath: '/usr/local/bin/codex', requestTimeoutMs: 4500, - totalTimeoutMs: 6000, + totalTimeoutMs: 12000, label: 'codex app-server thread/list live', }), expect.any(Function) @@ -180,4 +180,32 @@ describe('CodexAppServerClient', () => { threads: [{ id: 'live-1', cwd: '/Users/test/live-project', source: 'cli' }], }); }); + + it('uses the longer initialize timeout for app-server startup', async () => { + const request = vi.fn().mockImplementation((method: string, _params?: unknown, timeoutMs?: number) => { + if (method === 'initialize') { + expect(timeoutMs).toBe(6000); + return Promise.resolve({}); + } + + if (method === 'thread/list') { + return Promise.resolve({ data: [] }); + } + + return Promise.reject(new Error(`Unexpected method: ${method}`)); + }); + + const session = createSession(request); + const withSession = vi.fn().mockImplementation((_options, handler) => handler(session)); + const client = new CodexAppServerClient({ withSession } as unknown as JsonRpcStdioClient); + + await client.listRecentThreads('/usr/local/bin/codex', { + limit: 40, + liveRequestTimeoutMs: 4500, + archivedRequestTimeoutMs: 2500, + totalTimeoutMs: 4500, + }); + + expect(request).toHaveBeenCalled(); + }); });