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 4070bd91..dea0542d 100644 --- a/src/features/recent-projects/main/adapters/output/sources/CodexRecentProjectsSourceAdapter.ts +++ b/src/features/recent-projects/main/adapters/output/sources/CodexRecentProjectsSourceAdapter.ts @@ -15,7 +15,10 @@ import type { ServiceContext } from '@main/services'; const CODEX_THREAD_LIMIT = 40; const CODEX_LIVE_FETCH_TIMEOUT_MS = 4_500; const CODEX_ARCHIVED_FETCH_TIMEOUT_MS = 2_500; -const CODEX_SOURCE_TIMEOUT_MS = 5_200; +const CODEX_SESSION_OVERHEAD_TIMEOUT_MS = 1_500; +const CODEX_TOTAL_FETCH_TIMEOUT_MS = + CODEX_LIVE_FETCH_TIMEOUT_MS + CODEX_SESSION_OVERHEAD_TIMEOUT_MS; +const CODEX_SOURCE_TIMEOUT_MS = CODEX_TOTAL_FETCH_TIMEOUT_MS + 500; function isInteractiveSource(source: unknown): boolean { return source === 'vscode' || source === 'cli'; @@ -83,7 +86,7 @@ export class CodexRecentProjectsSourceAdapter implements RecentProjectsSourcePor limit: CODEX_THREAD_LIMIT, liveRequestTimeoutMs: CODEX_LIVE_FETCH_TIMEOUT_MS, archivedRequestTimeoutMs: CODEX_ARCHIVED_FETCH_TIMEOUT_MS, - totalTimeoutMs: CODEX_LIVE_FETCH_TIMEOUT_MS, + totalTimeoutMs: CODEX_TOTAL_FETCH_TIMEOUT_MS, }); this.deps.logger.info('codex recent-projects thread lists loaded', { diff --git a/src/features/recent-projects/main/infrastructure/codex/CodexAppServerClient.ts b/src/features/recent-projects/main/infrastructure/codex/CodexAppServerClient.ts index 2a43a960..b58b870f 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 { JsonRpcStdioClient } from './JsonRpcStdioClient'; const DEFAULT_REQUEST_TIMEOUT_MS = 3_000; const DEFAULT_TOTAL_TIMEOUT_MS = 8_000; +const MIN_SESSION_OVERHEAD_TIMEOUT_MS = 1_500; const SUPPRESSED_NOTIFICATION_METHODS = [ 'thread/started', 'thread/status/changed', @@ -63,7 +64,10 @@ export class CodexAppServerClient { const liveRequestTimeoutMs = options.liveRequestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS; const archivedRequestTimeoutMs = options.archivedRequestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS; const sessionRequestTimeoutMs = Math.max(liveRequestTimeoutMs, archivedRequestTimeoutMs); - const totalTimeoutMs = options.totalTimeoutMs ?? DEFAULT_TOTAL_TIMEOUT_MS; + const totalTimeoutMs = Math.max( + options.totalTimeoutMs ?? DEFAULT_TOTAL_TIMEOUT_MS, + sessionRequestTimeoutMs + MIN_SESSION_OVERHEAD_TIMEOUT_MS + ); return this.rpcClient.withSession( { diff --git a/test/features/recent-projects/main/infrastructure/CodexAppServerClient.test.ts b/test/features/recent-projects/main/infrastructure/CodexAppServerClient.test.ts index bb92552d..76b39c08 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: 4500, + totalTimeoutMs: 6000, }), expect.any(Function) ); @@ -107,4 +107,37 @@ describe('CodexAppServerClient', () => { error: 'JSON-RPC request timed out: thread/list', }); }); + + it('raises the session timeout budget above the longest request timeout', async () => { + const session = createSession( + vi.fn().mockImplementation((method: string, params?: { archived?: boolean }) => { + if (method === 'initialize') { + return Promise.resolve({}); + } + + if (method === 'thread/list') { + return Promise.resolve({ data: [] }); + } + + return Promise.reject(new Error(`Unexpected method: ${method}`)); + }) + ); + + 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(withSession).toHaveBeenCalledWith( + expect.objectContaining({ + totalTimeoutMs: 6000, + }), + expect.any(Function) + ); + }); });