From 15bd2ef349d390939432b27fa8958daa6dc3a876 Mon Sep 17 00:00:00 2001 From: Brandon Woo Date: Mon, 6 Apr 2026 17:07:11 +0900 Subject: [PATCH] fix: recognize missing-rollout Codex resume error as stale session The Codex CLI can return "no rollout found for thread id ..." when resuming a heartbeat thread whose rollout has been garbage-collected. Extend isCodexUnknownSessionError() to match this wording so the existing single-retry path in execute.ts activates correctly. Add parse.test.ts covering the new pattern, existing stale-session wordings, parseCodexJsonl, and a negative case. Co-Authored-By: Paperclip --- .../codex-local/src/server/parse.test.ts | 50 +++++++++++++++++++ .../adapters/codex-local/src/server/parse.ts | 2 +- 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 packages/adapters/codex-local/src/server/parse.test.ts diff --git a/packages/adapters/codex-local/src/server/parse.test.ts b/packages/adapters/codex-local/src/server/parse.test.ts new file mode 100644 index 00000000..b2b33e4a --- /dev/null +++ b/packages/adapters/codex-local/src/server/parse.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from "vitest"; +import { isCodexUnknownSessionError, parseCodexJsonl } from "./parse.js"; + +describe("parseCodexJsonl", () => { + it("captures session id, assistant summary, usage, and error message", () => { + const stdout = [ + JSON.stringify({ type: "thread.started", thread_id: "thread_123" }), + JSON.stringify({ + type: "item.completed", + item: { type: "agent_message", text: "Recovered response" }, + }), + JSON.stringify({ + type: "turn.completed", + usage: { input_tokens: 10, cached_input_tokens: 2, output_tokens: 4 }, + }), + JSON.stringify({ type: "turn.failed", error: { message: "resume failed" } }), + ].join("\n"); + + expect(parseCodexJsonl(stdout)).toEqual({ + sessionId: "thread_123", + summary: "Recovered response", + usage: { + inputTokens: 10, + cachedInputTokens: 2, + outputTokens: 4, + }, + errorMessage: "resume failed", + }); + }); +}); + +describe("isCodexUnknownSessionError", () => { + it("detects the current missing-rollout thread error", () => { + expect( + isCodexUnknownSessionError( + "", + "Error: thread/resume: thread/resume failed: no rollout found for thread id d448e715-7607-4bcc-91fc-7a3c0c5a9632", + ), + ).toBe(true); + }); + + it("still detects existing stale-session wordings", () => { + expect(isCodexUnknownSessionError("unknown thread id", "")).toBe(true); + expect(isCodexUnknownSessionError("", "state db missing rollout path for thread abc")).toBe(true); + }); + + it("does not classify unrelated Codex failures as stale sessions", () => { + expect(isCodexUnknownSessionError("", "model overloaded")).toBe(false); + }); +}); diff --git a/packages/adapters/codex-local/src/server/parse.ts b/packages/adapters/codex-local/src/server/parse.ts index afcf935c..1a7aa6a7 100644 --- a/packages/adapters/codex-local/src/server/parse.ts +++ b/packages/adapters/codex-local/src/server/parse.ts @@ -67,7 +67,7 @@ export function isCodexUnknownSessionError(stdout: string, stderr: string): bool .map((line) => line.trim()) .filter(Boolean) .join("\n"); - return /unknown (session|thread)|session .* not found|thread .* not found|conversation .* not found|missing rollout path for thread|state db missing rollout path/i.test( + return /unknown (session|thread)|session .* not found|thread .* not found|conversation .* not found|missing rollout path for thread|state db missing rollout path|no rollout found for thread id/i.test( haystack, ); }