From 5a801b109dedffaed1379c2601114df548dfa192 Mon Sep 17 00:00:00 2001 From: Ant39140 Date: Fri, 24 Jul 2026 18:45:02 +0800 Subject: [PATCH 1/3] fix(ai):strip output-only statuses from Responses replay --- packages/ai/src/utils.ts | 10 +++--- .../openai-responses-history-payload.test.ts | 35 ++++++++++++++++--- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/packages/ai/src/utils.ts b/packages/ai/src/utils.ts index 9f52c1385..977989e37 100644 --- a/packages/ai/src/utils.ts +++ b/packages/ai/src/utils.ts @@ -203,9 +203,12 @@ function sanitizeOpenAIResponsesHistoryItemForReplay( if (item.type === "image_generation_call") return sanitizeOpenAIResponsesImageGenerationCallForReplay(item); if (item.type === "reasoning") return sanitizeOpenAIResponsesReasoningItemForReplay(item); - // Provider payload stores raw output items. Computer calls retain their stable - // provider item ID; other replay items strip IDs and normalize call_id. + // Provider output lifecycle metadata is not accepted when these items are + // replayed through the Responses input array. const { id: _id, ...sanitizedItem } = item; + if (item.type === "message" || item.type === "function_call" || item.type === "custom_tool_call") { + delete sanitizedItem.status; + } if (item.type === "computer_call" && typeof item.id === "string") sanitizedItem.id = item.id; if (typeof item.call_id === "string") { sanitizedItem.call_id = normalizeReplayedResponsesHistoryCallId(item.call_id, normalizedCallIds); @@ -224,9 +227,6 @@ function sanitizeOpenAIResponsesReasoningItemForReplay(item: Record { const payload = (await captureResponsesPayload(model, incrementalContext)) as { input?: unknown[] }; expect(payload.input).toEqual([ { role: "user", content: [{ type: "input_text", text: "first question" }] }, - ...incrementalItems1.map(({ id: _id, ...item }) => item), + ...incrementalItems1.map(({ id: _id, status: _status, ...item }) => item), { role: "user", content: [{ type: "input_text", text: "second question" }] }, - ...incrementalItems2.map(({ id: _id, ...item }) => item), + ...incrementalItems2.map(({ id: _id, status: _status, ...item }) => item), { role: "user", content: [{ type: "input_text", text: "third question" }] }, ]); }); @@ -1125,13 +1125,15 @@ describe("OpenAI responses history payload", () => { ]); }); - it("strips replay-only ids and item references while preserving paired call_id values", async () => { + it("strips output-only replay metadata while preserving paired call_id values", async () => { const opaqueReasoningId = `item_${"copilot/reasoning+token=".repeat(8)}`; const opaqueMessageId = `item_${"copilot/message+opaque=".repeat(8)}`; const opaqueCallId = `call_${"copilot/tool-call+opaque/=".repeat(8)}`; const opaqueFunctionItemId = `item_${"copilot/function-item+opaque/=".repeat(8)}`; + const opaqueCustomCallId = `call_${"copilot/custom-call+opaque/=".repeat(8)}`; + const opaqueCustomItemId = `item_${"copilot/custom-item+opaque/=".repeat(8)}`; const replayHistoryItems: Array> = [ - { type: "reasoning", id: opaqueReasoningId, encrypted_content: "enc_opaque" }, + { type: "reasoning", id: opaqueReasoningId, encrypted_content: "enc_opaque", status: "completed" }, { type: "message", role: "assistant", @@ -1148,6 +1150,19 @@ describe("OpenAI responses history payload", () => { status: "completed", }, { type: "function_call_output", id: "fco_should_be_removed", call_id: opaqueCallId, output: "72F" }, + { + type: "custom_tool_call", + id: opaqueCustomItemId, + call_id: opaqueCustomCallId, + name: "apply_patch", + input: "*** Begin Patch\n*** End Patch\n", + status: "completed", + }, + { + type: "custom_tool_call_output", + call_id: opaqueCustomCallId, + output: "patch applied", + }, { type: "item_reference", id: opaqueMessageId }, ]; const context: Context = { @@ -1163,6 +1178,7 @@ describe("OpenAI responses history payload", () => { const messageItem = findResponsesInputItem(payload.input, "message"); const functionCallItem = findResponsesInputItem(payload.input, "function_call"); const functionCallOutputItem = findResponsesInputItem(payload.input, "function_call_output"); + const customToolCallItem = findResponsesInputItem(payload.input, "custom_tool_call"); const itemReference = findResponsesInputItem(payload.input, "item_reference"); const expectedCallId = truncateResponseItemId(opaqueCallId, "call"); @@ -1170,11 +1186,16 @@ describe("OpenAI responses history payload", () => { expect(messageItem).toBeDefined(); expect(functionCallItem).toBeDefined(); expect(functionCallOutputItem).toBeDefined(); + expect(customToolCallItem).toBeDefined(); expect(reasoningItem?.id).toBeUndefined(); expect(messageItem?.id).toBeUndefined(); expect(functionCallItem?.id).toBeUndefined(); expect(functionCallOutputItem?.id).toBeUndefined(); expect(itemReference).toBeUndefined(); + expect(reasoningItem).not.toHaveProperty("status"); + expect(messageItem).not.toHaveProperty("status"); + expect(functionCallItem).not.toHaveProperty("status"); + expect(customToolCallItem).not.toHaveProperty("status"); expect( (payload.input ?? []).some( item => item && typeof item === "object" && "id" in (item as Record), @@ -1184,15 +1205,19 @@ describe("OpenAI responses history payload", () => { expect(functionCallItem).toBeDefined(); expect(functionCallItem!.call_id).toBe(expectedCallId); expect(functionCallOutputItem?.call_id).toBe(expectedCallId); + expect(customToolCallItem?.call_id).toBe(truncateResponseItemId(opaqueCustomCallId, "call")); expect((functionCallItem!.call_id as string).length).toBeLessThanOrEqual(64); expect(containsAssistantOutputText(payload.input, "Sanitized assistant answer")).toBe(true); expect(replayHistoryItems[0]?.id).toBe(opaqueReasoningId); expect(replayHistoryItems[1]?.id).toBe(opaqueMessageId); expect(replayHistoryItems[2]?.id).toBe(opaqueFunctionItemId); expect(replayHistoryItems[2]?.call_id).toBe(opaqueCallId); + expect(replayHistoryItems[1]?.status).toBe("completed"); + expect(replayHistoryItems[2]?.status).toBe("completed"); expect(replayHistoryItems[3]?.id).toBe("fco_should_be_removed"); expect(replayHistoryItems[3]?.call_id).toBe(opaqueCallId); - expect(replayHistoryItems[4]?.id).toBe(opaqueMessageId); + expect(replayHistoryItems[4]?.status).toBe("completed"); + expect(replayHistoryItems[6]?.id).toBe(opaqueMessageId); }); it("backward compat: old full-snapshot payloads still replace history for legacy same-provider assistant turns", async () => { From 3688b43e189310abd2cb1a984d7c08f40083521a Mon Sep 17 00:00:00 2001 From: Ant39140 Date: Fri, 24 Jul 2026 20:15:18 +0800 Subject: [PATCH 2/3] fix(ai): harden Responses replay status stripping - strip status from generic replay items - preserve computer and image-generation status handling - cover web-search replay and add the changelog entry --- packages/ai/CHANGELOG.md | 4 ++++ packages/ai/src/utils.ts | 7 +++---- packages/ai/test/openai-responses-history-payload.test.ts | 3 ++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 1f722e5fc..57db5b7e6 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed OpenAI Responses native history replay sending output-only `status` fields back as input, preventing `input[N].status` failures in long-running sessions. ([#6513](https://github.com/can1357/oh-my-pi/pull/6513) by [@Ant39140](https://github.com/Ant39140)) + ## [17.1.1] - 2026-07-24 ### Added diff --git a/packages/ai/src/utils.ts b/packages/ai/src/utils.ts index 977989e37..71c6eaece 100644 --- a/packages/ai/src/utils.ts +++ b/packages/ai/src/utils.ts @@ -203,12 +203,11 @@ function sanitizeOpenAIResponsesHistoryItemForReplay( if (item.type === "image_generation_call") return sanitizeOpenAIResponsesImageGenerationCallForReplay(item); if (item.type === "reasoning") return sanitizeOpenAIResponsesReasoningItemForReplay(item); - // Provider output lifecycle metadata is not accepted when these items are + // Only computer calls require exact status replay here; image generation + // calls are handled above. Other output lifecycle statuses are rejected when // replayed through the Responses input array. const { id: _id, ...sanitizedItem } = item; - if (item.type === "message" || item.type === "function_call" || item.type === "custom_tool_call") { - delete sanitizedItem.status; - } + if (item.type !== "computer_call") delete sanitizedItem.status; if (item.type === "computer_call" && typeof item.id === "string") sanitizedItem.id = item.id; if (typeof item.call_id === "string") { sanitizedItem.call_id = normalizeReplayedResponsesHistoryCallId(item.call_id, normalizedCallIds); diff --git a/packages/ai/test/openai-responses-history-payload.test.ts b/packages/ai/test/openai-responses-history-payload.test.ts index 2c2df6f42..348cb0ad2 100644 --- a/packages/ai/test/openai-responses-history-payload.test.ts +++ b/packages/ai/test/openai-responses-history-payload.test.ts @@ -933,7 +933,8 @@ describe("OpenAI responses history payload", () => { : undefined; const webSearchItem = findResponsesInputItem(input, "web_search_call"); - expect(webSearchItem).toMatchObject({ type: "web_search_call", status: "completed" }); + expect(webSearchItem).toMatchObject({ type: "web_search_call" }); + expect(webSearchItem).not.toHaveProperty("status"); expect(webSearchItem?.id).toBeUndefined(); expect(containsAssistantOutputText(input, "ignored")).toBe(false); expect(containsUserInputText(input, followUp)).toBe(true); From 1182657f41a3bdb7b62f68fd12d9b38dbbe3cc27 Mon Sep 17 00:00:00 2001 From: Ant39140 Date: Fri, 24 Jul 2026 20:36:27 +0800 Subject: [PATCH 3/3] fix(ai): preserve required replay statuses - restrict status stripping to rejected replay item types - retain required status on hosted built-in tool items --- packages/ai/src/utils.ts | 9 +++++---- .../ai/test/openai-responses-history-payload.test.ts | 3 +-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/ai/src/utils.ts b/packages/ai/src/utils.ts index 71c6eaece..d96661040 100644 --- a/packages/ai/src/utils.ts +++ b/packages/ai/src/utils.ts @@ -203,11 +203,12 @@ function sanitizeOpenAIResponsesHistoryItemForReplay( if (item.type === "image_generation_call") return sanitizeOpenAIResponsesImageGenerationCallForReplay(item); if (item.type === "reasoning") return sanitizeOpenAIResponsesReasoningItemForReplay(item); - // Only computer calls require exact status replay here; image generation - // calls are handled above. Other output lifecycle statuses are rejected when - // replayed through the Responses input array. + // Strip status only from item types whose replay input rejects output + // lifecycle metadata. Hosted built-in tool items require status for replay. const { id: _id, ...sanitizedItem } = item; - if (item.type !== "computer_call") delete sanitizedItem.status; + if (item.type === "message" || item.type === "function_call" || item.type === "custom_tool_call") { + delete sanitizedItem.status; + } if (item.type === "computer_call" && typeof item.id === "string") sanitizedItem.id = item.id; if (typeof item.call_id === "string") { sanitizedItem.call_id = normalizeReplayedResponsesHistoryCallId(item.call_id, normalizedCallIds); diff --git a/packages/ai/test/openai-responses-history-payload.test.ts b/packages/ai/test/openai-responses-history-payload.test.ts index 348cb0ad2..2c2df6f42 100644 --- a/packages/ai/test/openai-responses-history-payload.test.ts +++ b/packages/ai/test/openai-responses-history-payload.test.ts @@ -933,8 +933,7 @@ describe("OpenAI responses history payload", () => { : undefined; const webSearchItem = findResponsesInputItem(input, "web_search_call"); - expect(webSearchItem).toMatchObject({ type: "web_search_call" }); - expect(webSearchItem).not.toHaveProperty("status"); + expect(webSearchItem).toMatchObject({ type: "web_search_call", status: "completed" }); expect(webSearchItem?.id).toBeUndefined(); expect(containsAssistantOutputText(input, "ignored")).toBe(false); expect(containsUserInputText(input, followUp)).toBe(true);