Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions apps/server/src/provider/Layers/CodexAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,42 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {
}),
);

it.effect("maps fatal websocket stderr notifications to runtime.error", () =>
Effect.gen(function* () {
const adapter = yield* CodexAdapter;
const firstEventFiber = yield* Stream.runHead(adapter.streamEvents).pipe(Effect.forkChild);

lifecycleManager.emit("event", {
id: asEventId("evt-process-stderr-websocket"),
kind: "notification",
provider: "codex",
threadId: asThreadId("thread-1"),
createdAt: new Date().toISOString(),
method: "process/stderr",
turnId: asTurnId("turn-1"),
message:
"2026-03-31T18:14:06.833399Z ERROR codex_api::endpoint::responses_websocket: failed to connect to websocket: HTTP error: 503 Service Unavailable, url: wss://chatgpt.com/backend-api/codex/responses",
} satisfies ProviderEvent);

const firstEvent = yield* Fiber.join(firstEventFiber);

assert.equal(firstEvent._tag, "Some");
if (firstEvent._tag !== "Some") {
return;
}
assert.equal(firstEvent.value.type, "runtime.error");
if (firstEvent.value.type !== "runtime.error") {
return;
}
assert.equal(firstEvent.value.turnId, "turn-1");
assert.equal(firstEvent.value.payload.class, "provider_error");
assert.equal(
firstEvent.value.payload.message,
"2026-03-31T18:14:06.833399Z ERROR codex_api::endpoint::responses_websocket: failed to connect to websocket: HTTP error: 503 Service Unavailable, url: wss://chatgpt.com/backend-api/codex/responses",
);
}),
);

it.effect("preserves request type when mapping serverRequest/resolved", () =>
Effect.gen(function* () {
const adapter = yield* CodexAdapter;
Expand Down
35 changes: 27 additions & 8 deletions apps/server/src/provider/Layers/CodexAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,13 @@ function asNumber(value: unknown): number | undefined {
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
}

const FATAL_CODEX_STDERR_SNIPPETS = ["failed to connect to websocket"];

function isFatalCodexProcessStderrMessage(message: string): boolean {
const normalized = message.toLowerCase();
return FATAL_CODEX_STDERR_SNIPPETS.some((snippet) => normalized.includes(snippet));
}

function normalizeCodexTokenUsage(value: unknown): ThreadTokenUsageSnapshot | undefined {
const usage = asObject(value);
const totalUsage = asObject(usage?.total_token_usage ?? usage?.total);
Expand Down Expand Up @@ -1269,15 +1276,27 @@ function mapToRuntimeEvents(
}

if (event.method === "process/stderr") {
const message = event.message ?? "Codex process stderr";
const isFatal = isFatalCodexProcessStderrMessage(message);
return [
{
type: "runtime.warning",
...runtimeEventBase(event, canonicalThreadId),
payload: {
message: event.message ?? "Codex process stderr",
...(event.payload !== undefined ? { detail: event.payload } : {}),
},
},
isFatal
? {
type: "runtime.error",
...runtimeEventBase(event, canonicalThreadId),
payload: {
message,
class: "provider_error" as const,
...(event.payload !== undefined ? { detail: event.payload } : {}),
},
}
: {
type: "runtime.warning",
...runtimeEventBase(event, canonicalThreadId),
payload: {
message,
...(event.payload !== undefined ? { detail: event.payload } : {}),
},
},
];
}

Expand Down
Loading