From 2082ffbad536b5f4718108e18f987fb4a9ae2d41 Mon Sep 17 00:00:00 2001 From: Alexander Averyanov Date: Tue, 5 May 2026 15:08:27 +0300 Subject: [PATCH] fix(codex): preserve final_answer responses replay (#1965) Integrated into release/v3.7.9 --- open-sse/services/responsesInputSanitizer.ts | 7 ++- tests/unit/executor-codex.test.ts | 63 ++++++++++++++++++++ 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/open-sse/services/responsesInputSanitizer.ts b/open-sse/services/responsesInputSanitizer.ts index 31a540feb8..ba2fef19e7 100644 --- a/open-sse/services/responsesInputSanitizer.ts +++ b/open-sse/services/responsesInputSanitizer.ts @@ -1,4 +1,5 @@ type JsonRecord = Record; +const INTERNAL_ASSISTANT_PHASES = new Set(["commentary"]); function toRecord(value: unknown): JsonRecord | null { return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : null; @@ -15,9 +16,9 @@ export function isInternalAssistantMessage(record: JsonRecord): boolean { const phase = typeof record.phase === "string" ? record.phase.trim().toLowerCase() : ""; if (!phase) return false; - // OpenCode can send assistant-side commentary/analysis frames in Responses - // shape. Those frames are local runtime state, not durable conversation turns. - return phase !== "final"; + // Drop only known internal runtime frames. Visible assistant turns such as + // `final` and `final_answer` must survive replay for Codex/OpenCode follow-ups. + return INTERNAL_ASSISTANT_PHASES.has(phase); } export function sanitizeResponsesInputItems(items: readonly unknown[], clone = true): unknown[] { diff --git a/tests/unit/executor-codex.test.ts b/tests/unit/executor-codex.test.ts index e7bd377127..efeab8749f 100644 --- a/tests/unit/executor-codex.test.ts +++ b/tests/unit/executor-codex.test.ts @@ -468,6 +468,57 @@ test("CodexExecutor.transformRequest does not replay internal assistant commenta assert.equal(result.input[3].type, "function_call_output"); }); +test("CodexExecutor.transformRequest preserves replayed assistant final_answer messages", () => { + const executor = new CodexExecutor(); + rememberResponseConversationState( + "resp_prev_final_answer_123", + [ + { + type: "message", + role: "user", + content: [{ type: "input_text", text: "9+10?" }], + }, + { + type: "message", + role: "assistant", + phase: "final_answer", + content: [{ type: "output_text", text: "19" }], + }, + ], + [] + ); + + const result = executor.transformRequest( + "gpt-5.5-low", + { + _nativeCodexPassthrough: true, + previous_response_id: "resp_prev_final_answer_123", + input: [ + { + type: "message", + role: "user", + content: [{ type: "input_text", text: "did you answered?" }], + }, + ], + stream: false, + }, + false, + { requestEndpointPath: "/responses" } + ); + + assert.equal( + result.input.some((item) => JSON.stringify(item).includes('"text":"19"')), + true + ); + assert.equal( + result.input.some((item) => { + if (!item || typeof item !== "object" || Array.isArray(item)) return false; + return item.role === "assistant" && item.phase === "final_answer"; + }), + true + ); +}); + test("CodexExecutor.transformRequest strips raw internal assistant commentary without dropping useful Responses items", () => { const executor = new CodexExecutor(); const body = { @@ -490,6 +541,12 @@ test("CodexExecutor.transformRequest strips raw internal assistant commentary wi phase: "final", content: [{ type: "output_text", text: "Visible final assistant answer." }], }, + { + type: "message", + role: "assistant", + phase: "final_answer", + content: [{ type: "output_text", text: "Visible final_answer assistant answer." }], + }, { type: "message", role: "assistant", @@ -526,6 +583,12 @@ test("CodexExecutor.transformRequest strips raw internal assistant commentary wi result.input.some((item) => JSON.stringify(item).includes("Visible final assistant answer")), true ); + assert.equal( + result.input.some((item) => + JSON.stringify(item).includes("Visible final_answer assistant answer") + ), + true + ); assert.equal( result.input.some((item) => JSON.stringify(item).includes("Visible assistant history without phase")