diff --git a/apps/desktop/src/codexAppServerManager.test.ts b/apps/desktop/src/codexAppServerManager.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..796aa2aedd99aa3551d1a903bfeff1e1c399d8be --- /dev/null +++ b/apps/desktop/src/codexAppServerManager.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from "vitest"; + +import { classifyCodexStderrLine } from "./codexAppServerManager"; + +describe("classifyCodexStderrLine", () => { + it("ignores empty lines", () => { + expect(classifyCodexStderrLine(" ")).toBeNull(); + }); + + it("ignores non-error structured codex logs", () => { + const line = + "2026-02-08T04:24:19.241256Z WARN codex_core::features: unknown feature key in config: skills"; + expect(classifyCodexStderrLine(line)).toBeNull(); + }); + + it("ignores known benign rollout path errors", () => { + const line = + "\u001b[2m2026-02-08T04:24:20.085687Z\u001b[0m \u001b[31mERROR\u001b[0m \u001b[2mcodex_core::rollout::list\u001b[0m: state db missing rollout path for thread 019c3b6c-46b8-7b70-ad23-82f824d161fb"; + expect(classifyCodexStderrLine(line)).toBeNull(); + }); + + it("keeps unknown structured errors", () => { + const line = + "2026-02-08T04:24:20.085687Z ERROR codex_core::runtime: unrecoverable failure"; + expect(classifyCodexStderrLine(line)).toEqual({ + message: line, + }); + }); + + it("keeps plain stderr messages", () => { + const line = "fatal: permission denied"; + expect(classifyCodexStderrLine(line)).toEqual({ + message: line, + }); + }); +}); diff --git a/apps/desktop/src/codexAppServerManager.ts b/apps/desktop/src/codexAppServerManager.ts index ba21e340293b7c39df7e06d0f00fc4f3f4184cd0..442bd5314467cc6dc7f1b17aa2003e04a7949f0c 100644 --- a/apps/desktop/src/codexAppServerManager.ts +++ b/apps/desktop/src/codexAppServerManager.ts @@ -51,6 +51,41 @@ interface JsonRpcNotification { params?: unknown; } +const ANSI_ESCAPE_CHAR = String.fromCharCode(27); +const ANSI_ESCAPE_REGEX = new RegExp(`${ANSI_ESCAPE_CHAR}\\[[0-9;]*m`, "g"); +const CODEX_STDERR_LOG_REGEX = + /^\d{4}-\d{2}-\d{2}T\S+\s+(TRACE|DEBUG|INFO|WARN|ERROR)\s+\S+:\s+(.*)$/; +const BENIGN_ERROR_LOG_SNIPPETS = [ + "state db missing rollout path for thread", + "state db record_discrepancy: find_thread_path_by_id_str_in_subdir, falling_back", +]; + +export function classifyCodexStderrLine( + rawLine: string, +): { message: string } | null { + const line = rawLine.replaceAll(ANSI_ESCAPE_REGEX, "").trim(); + if (!line) { + return null; + } + + const match = line.match(CODEX_STDERR_LOG_REGEX); + if (match) { + const level = match[1]; + if (level && level !== "ERROR") { + return null; + } + + const isBenignError = BENIGN_ERROR_LOG_SNIPPETS.some((snippet) => + line.includes(snippet), + ); + if (isBenignError) { + return null; + } + } + + return { message: line }; +} + export interface CodexAppServerManagerEvents { event: [event: ProviderEvent]; } @@ -267,12 +302,16 @@ export class CodexAppServerManager extends EventEmitter { - const message = chunk.toString().trim(); - if (!message) { - return; - } + const raw = chunk.toString(); + const lines = raw.split(/\r?\n/g); + for (const rawLine of lines) { + const classified = classifyCodexStderrLine(rawLine); + if (!classified) { + continue; + } - this.emitErrorEvent(context, "process/stderr", message); + this.emitErrorEvent(context, "process/stderr", classified.message); + } }); context.child.on("error", (error) => {