diff --git a/extensions/codex/src/app-server/attempt-startup.test.ts b/extensions/codex/src/app-server/attempt-startup.test.ts index 27866355373f..375e95ce728f 100644 --- a/extensions/codex/src/app-server/attempt-startup.test.ts +++ b/extensions/codex/src/app-server/attempt-startup.test.ts @@ -597,6 +597,7 @@ describe("startCodexAttemptThread", () => { 'import readline from "node:readline";', "const [requestLogPath, pidPath] = process.argv.slice(2);", 'fs.writeFileSync(pidPath, String(process.pid), "utf8");', + 'process.stderr.write("Error: failed to initialize sqlite state runtime token=secret-value\\n");', "const lines = readline.createInterface({ input: process.stdin });", 'lines.on("line", (line) => {', " const message = JSON.parse(line);", @@ -623,7 +624,9 @@ describe("startCodexAttemptThread", () => { skipStartSpy: true, }); - await expect(run).rejects.toThrow("codex app-server initialize timed out"); + await expect(run).rejects.toThrow( + 'codex app-server initialize timed out; stderr="Error: failed to initialize sqlite state runtime token="', + ); const requestMethods = (await fs.readFile(requestLogPath, "utf8")).trim().split(/\r?\n/u); expect(requestMethods).toEqual(["initialize"]); diff --git a/extensions/codex/src/app-server/client.ts b/extensions/codex/src/app-server/client.ts index bc5874ab5639..fb8963d1fbf8 100644 --- a/extensions/codex/src/app-server/client.ts +++ b/extensions/codex/src/app-server/client.ts @@ -355,6 +355,11 @@ export class CodexAppServerClient { return this.runtimeIdentity ? { ...this.runtimeIdentity } : undefined; } + /** Returns a bounded, redacted stderr diagnostic from the app-server process. */ + getStderrDiagnostic(): string | undefined { + return redactCodexAppServerLinePreview(this.stderrTail) || undefined; + } + /** Stable generation id for this exact physical client instance. */ getInstanceId(): string { return this.instanceId; diff --git a/extensions/codex/src/app-server/shared-client.test.ts b/extensions/codex/src/app-server/shared-client.test.ts index 2ebb7d38e98d..8651d6ac31fc 100644 --- a/extensions/codex/src/app-server/shared-client.test.ts +++ b/extensions/codex/src/app-server/shared-client.test.ts @@ -715,6 +715,22 @@ describe("shared Codex app-server client", () => { expect(startSpy).toHaveBeenCalledTimes(2); }); + it("includes redacted app-server stderr when shared initialize times out", async () => { + const harness = createClientHarness(); + vi.spyOn(CodexAppServerClient, "start").mockReturnValue(harness.client); + + const models = listCodexAppServerModels({ timeoutMs: 100 }); + await vi.waitFor(() => expect(harness.writes.length).toBeGreaterThanOrEqual(1)); + harness.process.stderr.write( + 'Error: failed to initialize sqlite state runtime token="secret-value"\n', + ); + + await expect(models).rejects.toThrow( + 'codex app-server initialize timed out; stderr="Error: failed to initialize sqlite state runtime token=\\"\\""', + ); + expect(harness.process.stdin.destroyed).toBe(true); + }); + it("keeps shared startup alive for a caller with a longer initialize timeout", async () => { const harness = createClientHarness(); const startSpy = vi.spyOn(CodexAppServerClient, "start").mockReturnValue(harness.client); @@ -798,6 +814,20 @@ describe("shared Codex app-server client", () => { expect(harness.process.stdin.destroyed).toBe(true); }); + it("includes redacted app-server stderr when isolated initialize times out", async () => { + const harness = createClientHarness(); + vi.spyOn(CodexAppServerClient, "start").mockReturnValue(harness.client); + + const client = createIsolatedCodexAppServerClient({ timeoutMs: 100 }); + await vi.waitFor(() => expect(harness.writes.length).toBeGreaterThanOrEqual(1)); + harness.process.stderr.write("state database is locked access_token=secret-value\n"); + + await expect(client).rejects.toThrow( + 'codex app-server initialize timed out; stderr="state database is locked access_token="', + ); + expect(harness.process.stdin.destroyed).toBe(true); + }); + it("includes isolated auth application in the total startup deadline", async () => { const harness = createClientHarness(); vi.spyOn(CodexAppServerClient, "start").mockReturnValue(harness.client); diff --git a/extensions/codex/src/app-server/shared-client.ts b/extensions/codex/src/app-server/shared-client.ts index 675925a3eca0..f8afb125911e 100644 --- a/extensions/codex/src/app-server/shared-client.ts +++ b/extensions/codex/src/app-server/shared-client.ts @@ -39,6 +39,8 @@ import { withTimeout } from "./timeout.js"; export type { CodexAppServerPreparedAuth } from "./auth-bridge.js"; +const CODEX_APP_SERVER_INITIALIZE_TIMEOUT_MESSAGE = "codex app-server initialize timed out"; + type SharedCodexAppServerClientEntry = { client?: CodexAppServerClient; startup?: SharedCodexAppServerClientStartup; @@ -619,6 +621,8 @@ async function acquireSharedCodexAppServerClient( remainingTimeoutMs, startup.initialized, options?.abandonSignal, + CODEX_APP_SERVER_INITIALIZE_TIMEOUT_MESSAGE, + () => buildCodexAppServerInitializeTimeoutError(entry.client), ); const client = await withCodexAppServerAcquireDeadline( timeoutMs, @@ -657,7 +661,8 @@ async function withCodexAppServerAcquireDeadline( timeoutMs: number, // First: fail before the caller starts its promise argument. promise: Promise, signal?: AbortSignal, - timeoutMessage = "codex app-server initialize timed out", + timeoutMessage = CODEX_APP_SERVER_INITIALIZE_TIMEOUT_MESSAGE, + timeoutErrorFactory?: () => CodexAppServerStartupError, ): Promise { if (signal?.aborted) { throw new CodexAppServerStartupError("aborted", "codex app-server initialize aborted"); @@ -666,7 +671,7 @@ async function withCodexAppServerAcquireDeadline( promise, timeoutMs, timeoutMessage, - () => new CodexAppServerStartupError("timed_out", timeoutMessage), + () => timeoutErrorFactory?.() ?? new CodexAppServerStartupError("timed_out", timeoutMessage), ); if (!signal) { return await timed; @@ -679,6 +684,18 @@ async function withCodexAppServerAcquireDeadline( }); } +function buildCodexAppServerInitializeTimeoutError( + client: CodexAppServerClient | undefined, +): CodexAppServerStartupError { + const stderr = client?.getStderrDiagnostic(); + return new CodexAppServerStartupError( + "timed_out", + stderr + ? `${CODEX_APP_SERVER_INITIALIZE_TIMEOUT_MESSAGE}; stderr=${JSON.stringify(stderr)}` + : CODEX_APP_SERVER_INITIALIZE_TIMEOUT_MESSAGE, + ); +} + function resolveRemainingAcquireTimeout(timeoutMs: number, startedAt: number): number { if (!(timeoutMs > 0)) { return timeoutMs; @@ -847,6 +864,8 @@ async function startInitializedCodexAppServerClient(params: { resolveRemainingAcquireTimeout(timeoutMs, acquireStartedAt), (initialize = client.initialize()), params.abandonSignal, + CODEX_APP_SERVER_INITIALIZE_TIMEOUT_MESSAGE, + () => buildCodexAppServerInitializeTimeoutError(client), ); } catch (error) { client.close();