fix(codex): preserve startup diagnostics on initialize timeout (#115161)

* fix(codex): preserve app-server startup diagnostics

* chore: leave release notes to release automation
This commit is contained in:
Peter Steinberger
2026-07-28 08:08:12 -04:00
committed by GitHub
parent bb634261cb
commit c0df3cf0b9
4 changed files with 60 additions and 3 deletions
@@ -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=<redacted>"',
);
const requestMethods = (await fs.readFile(requestLogPath, "utf8")).trim().split(/\r?\n/u);
expect(requestMethods).toEqual(["initialize"]);
@@ -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;
@@ -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=\\"<redacted>\\""',
);
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=<redacted>"',
);
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);
@@ -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<T>(
timeoutMs: number, // First: fail before the caller starts its promise argument.
promise: Promise<T>,
signal?: AbortSignal,
timeoutMessage = "codex app-server initialize timed out",
timeoutMessage = CODEX_APP_SERVER_INITIALIZE_TIMEOUT_MESSAGE,
timeoutErrorFactory?: () => CodexAppServerStartupError,
): Promise<T> {
if (signal?.aborted) {
throw new CodexAppServerStartupError("aborted", "codex app-server initialize aborted");
@@ -666,7 +671,7 @@ async function withCodexAppServerAcquireDeadline<T>(
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<T>(
});
}
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();