fix(codex): preserve final_answer responses replay (#1965)

Integrated into release/v3.7.9
This commit is contained in:
Alexander Averyanov
2026-05-05 15:08:27 +03:00
committed by GitHub
parent 64b1a20010
commit 2082ffbad5
2 changed files with 67 additions and 3 deletions

View File

@@ -1,4 +1,5 @@
type JsonRecord = Record<string, unknown>;
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[] {

View File

@@ -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")