From 009bd870cd794c4c867576d306717b6a5ca70857 Mon Sep 17 00:00:00 2001
From: pakrym-oai <pakrym@openai.com>
Date: Wed, 17 Jun 2026 16:43:53 -0700
Subject: [PATCH 01/14] assign response item IDs when recording history

---
 .../schema/json/ClientRequest.json            |  80 ++++++++++++-
 .../codex_app_server_protocol.schemas.json    |  80 ++++++++++++-
 .../codex_app_server_protocol.v2.schemas.json |  80 ++++++++++++-
 .../RawResponseItemCompletedNotification.json |  80 ++++++++++++-
 .../schema/json/v2/ThreadResumeParams.json    |  80 ++++++++++++-
 .../schema/typescript/ResponseItem.ts         |  10 +-
 codex-rs/core/config.schema.json              |   6 +
 codex-rs/core/src/compact_remote_v2.rs        |   5 +-
 codex-rs/core/src/session/mod.rs              |  39 ++++++-
 codex-rs/core/src/session/tests.rs            |  18 ++-
 codex-rs/core/tests/suite/client.rs           |  78 +++++++++++++
 codex-rs/features/src/lib.rs                  |   8 ++
 codex-rs/protocol/src/models.rs               | 108 ++++++------------
 13 files changed, 575 insertions(+), 97 deletions(-)

diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json
index 749899498067..7c2c5f55649d 100644
--- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json
+++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json
@@ -2289,6 +2289,12 @@
               },
               "type": "array"
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -2339,6 +2345,12 @@
               },
               "type": "array"
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -2387,6 +2399,12 @@
                 "null"
               ]
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -2430,6 +2448,13 @@
                 "null"
               ]
             },
+            "id": {
+              "description": "Legacy id field retained for compatibility with older payloads.",
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -2467,6 +2492,12 @@
             "call_id": {
               "type": "string"
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -2515,6 +2546,12 @@
             "execution": {
               "type": "string"
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -2552,6 +2589,12 @@
             "call_id": {
               "type": "string"
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -2586,6 +2629,12 @@
             "call_id": {
               "type": "string"
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "input": {
               "type": "string"
             },
@@ -2630,6 +2679,12 @@
             "call_id": {
               "type": "string"
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -2676,6 +2731,12 @@
             "execution": {
               "type": "string"
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -2722,6 +2783,12 @@
                 }
               ]
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -2755,7 +2822,6 @@
         {
           "properties": {
             "id": {
-              "description": "Existing provider ID retained on serialized history for compatibility.",
               "type": [
                 "string",
                 "null"
@@ -2804,6 +2870,12 @@
             "encrypted_content": {
               "type": "string"
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -2863,6 +2935,12 @@
                 "null"
               ]
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json
index 5b0c59c42555..b0e5972e7679 100644
--- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json
+++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json
@@ -14791,6 +14791,12 @@
                 },
                 "type": "array"
               },
+              "id": {
+                "type": [
+                  "string",
+                  "null"
+                ]
+              },
               "metadata": {
                 "anyOf": [
                   {
@@ -14841,6 +14847,12 @@
                 },
                 "type": "array"
               },
+              "id": {
+                "type": [
+                  "string",
+                  "null"
+                ]
+              },
               "metadata": {
                 "anyOf": [
                   {
@@ -14889,6 +14901,12 @@
                   "null"
                 ]
               },
+              "id": {
+                "type": [
+                  "string",
+                  "null"
+                ]
+              },
               "metadata": {
                 "anyOf": [
                   {
@@ -14932,6 +14950,13 @@
                   "null"
                 ]
               },
+              "id": {
+                "description": "Legacy id field retained for compatibility with older payloads.",
+                "type": [
+                  "string",
+                  "null"
+                ]
+              },
               "metadata": {
                 "anyOf": [
                   {
@@ -14969,6 +14994,12 @@
               "call_id": {
                 "type": "string"
               },
+              "id": {
+                "type": [
+                  "string",
+                  "null"
+                ]
+              },
               "metadata": {
                 "anyOf": [
                   {
@@ -15017,6 +15048,12 @@
               "execution": {
                 "type": "string"
               },
+              "id": {
+                "type": [
+                  "string",
+                  "null"
+                ]
+              },
               "metadata": {
                 "anyOf": [
                   {
@@ -15054,6 +15091,12 @@
               "call_id": {
                 "type": "string"
               },
+              "id": {
+                "type": [
+                  "string",
+                  "null"
+                ]
+              },
               "metadata": {
                 "anyOf": [
                   {
@@ -15088,6 +15131,12 @@
               "call_id": {
                 "type": "string"
               },
+              "id": {
+                "type": [
+                  "string",
+                  "null"
+                ]
+              },
               "input": {
                 "type": "string"
               },
@@ -15132,6 +15181,12 @@
               "call_id": {
                 "type": "string"
               },
+              "id": {
+                "type": [
+                  "string",
+                  "null"
+                ]
+              },
               "metadata": {
                 "anyOf": [
                   {
@@ -15178,6 +15233,12 @@
               "execution": {
                 "type": "string"
               },
+              "id": {
+                "type": [
+                  "string",
+                  "null"
+                ]
+              },
               "metadata": {
                 "anyOf": [
                   {
@@ -15224,6 +15285,12 @@
                   }
                 ]
               },
+              "id": {
+                "type": [
+                  "string",
+                  "null"
+                ]
+              },
               "metadata": {
                 "anyOf": [
                   {
@@ -15257,7 +15324,6 @@
           {
             "properties": {
               "id": {
-                "description": "Existing provider ID retained on serialized history for compatibility.",
                 "type": [
                   "string",
                   "null"
@@ -15306,6 +15372,12 @@
               "encrypted_content": {
                 "type": "string"
               },
+              "id": {
+                "type": [
+                  "string",
+                  "null"
+                ]
+              },
               "metadata": {
                 "anyOf": [
                   {
@@ -15365,6 +15437,12 @@
                   "null"
                 ]
               },
+              "id": {
+                "type": [
+                  "string",
+                  "null"
+                ]
+              },
               "metadata": {
                 "anyOf": [
                   {
diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json
index 45cfbd95c21f..c2f0d7e5d4ae 100644
--- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json
+++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json
@@ -11236,6 +11236,12 @@
               },
               "type": "array"
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -11286,6 +11292,12 @@
               },
               "type": "array"
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -11334,6 +11346,12 @@
                 "null"
               ]
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -11377,6 +11395,13 @@
                 "null"
               ]
             },
+            "id": {
+              "description": "Legacy id field retained for compatibility with older payloads.",
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -11414,6 +11439,12 @@
             "call_id": {
               "type": "string"
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -11462,6 +11493,12 @@
             "execution": {
               "type": "string"
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -11499,6 +11536,12 @@
             "call_id": {
               "type": "string"
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -11533,6 +11576,12 @@
             "call_id": {
               "type": "string"
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "input": {
               "type": "string"
             },
@@ -11577,6 +11626,12 @@
             "call_id": {
               "type": "string"
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -11623,6 +11678,12 @@
             "execution": {
               "type": "string"
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -11669,6 +11730,12 @@
                 }
               ]
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -11702,7 +11769,6 @@
         {
           "properties": {
             "id": {
-              "description": "Existing provider ID retained on serialized history for compatibility.",
               "type": [
                 "string",
                 "null"
@@ -11751,6 +11817,12 @@
             "encrypted_content": {
               "type": "string"
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -11810,6 +11882,12 @@
                 "null"
               ]
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
diff --git a/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json
index f77a68dc1d71..22385f816f87 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json
@@ -377,6 +377,12 @@
               },
               "type": "array"
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -427,6 +433,12 @@
               },
               "type": "array"
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -475,6 +487,12 @@
                 "null"
               ]
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -518,6 +536,13 @@
                 "null"
               ]
             },
+            "id": {
+              "description": "Legacy id field retained for compatibility with older payloads.",
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -555,6 +580,12 @@
             "call_id": {
               "type": "string"
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -603,6 +634,12 @@
             "execution": {
               "type": "string"
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -640,6 +677,12 @@
             "call_id": {
               "type": "string"
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -674,6 +717,12 @@
             "call_id": {
               "type": "string"
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "input": {
               "type": "string"
             },
@@ -718,6 +767,12 @@
             "call_id": {
               "type": "string"
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -764,6 +819,12 @@
             "execution": {
               "type": "string"
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -810,6 +871,12 @@
                 }
               ]
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -843,7 +910,6 @@
         {
           "properties": {
             "id": {
-              "description": "Existing provider ID retained on serialized history for compatibility.",
               "type": [
                 "string",
                 "null"
@@ -892,6 +958,12 @@
             "encrypted_content": {
               "type": "string"
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -951,6 +1023,12 @@
                 "null"
               ]
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json
index e09dd2c6fc75..528076c6ae28 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json
@@ -448,6 +448,12 @@
               },
               "type": "array"
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -498,6 +504,12 @@
               },
               "type": "array"
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -546,6 +558,12 @@
                 "null"
               ]
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -589,6 +607,13 @@
                 "null"
               ]
             },
+            "id": {
+              "description": "Legacy id field retained for compatibility with older payloads.",
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -626,6 +651,12 @@
             "call_id": {
               "type": "string"
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -674,6 +705,12 @@
             "execution": {
               "type": "string"
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -711,6 +748,12 @@
             "call_id": {
               "type": "string"
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -745,6 +788,12 @@
             "call_id": {
               "type": "string"
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "input": {
               "type": "string"
             },
@@ -789,6 +838,12 @@
             "call_id": {
               "type": "string"
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -835,6 +890,12 @@
             "execution": {
               "type": "string"
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -881,6 +942,12 @@
                 }
               ]
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -914,7 +981,6 @@
         {
           "properties": {
             "id": {
-              "description": "Existing provider ID retained on serialized history for compatibility.",
               "type": [
                 "string",
                 "null"
@@ -963,6 +1029,12 @@
             "encrypted_content": {
               "type": "string"
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
@@ -1022,6 +1094,12 @@
                 "null"
               ]
             },
+            "id": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "metadata": {
               "anyOf": [
                 {
diff --git a/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts b/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts
index f0cd1c8ee661..fc399b925f59 100644
--- a/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts
+++ b/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts
@@ -12,12 +12,12 @@ import type { ReasoningItemReasoningSummary } from "./ReasoningItemReasoningSumm
 import type { ResponseItemMetadata } from "./ResponseItemMetadata";
 import type { WebSearchAction } from "./WebSearchAction";
 
-export type ResponseItem = { "type": "message", role: string, content: Array<ContentItem>, phase?: MessagePhase, metadata?: ResponseItemMetadata, } | { "type": "agent_message", author: string, recipient: string, content: Array<AgentMessageInputContent>, metadata?: ResponseItemMetadata, } | { "type": "reasoning", summary: Array<ReasoningItemReasoningSummary>, content?: Array<ReasoningItemContent>, encrypted_content: string | null, metadata?: ResponseItemMetadata, } | { "type": "local_shell_call",
+export type ResponseItem = { "type": "message", id?: string, role: string, content: Array<ContentItem>, phase?: MessagePhase, metadata?: ResponseItemMetadata, } | { "type": "agent_message", id?: string, author: string, recipient: string, content: Array<AgentMessageInputContent>, metadata?: ResponseItemMetadata, } | { "type": "reasoning", id?: string, summary: Array<ReasoningItemReasoningSummary>, content?: Array<ReasoningItemContent>, encrypted_content: string | null, metadata?: ResponseItemMetadata, } | { "type": "local_shell_call",
 /**
- * Set when using the Responses API.
+ * Legacy id field retained for compatibility with older payloads.
  */
-call_id: string | null, status: LocalShellStatus, action: LocalShellAction, metadata?: ResponseItemMetadata, } | { "type": "function_call", name: string, namespace?: string, arguments: string, call_id: string, metadata?: ResponseItemMetadata, } | { "type": "tool_search_call", call_id: string | null, status?: string, execution: string, arguments: unknown, metadata?: ResponseItemMetadata, } | { "type": "function_call_output", call_id: string, output: FunctionCallOutputBody, metadata?: ResponseItemMetadata, } | { "type": "custom_tool_call", status?: string, call_id: string, name: string, input: string, metadata?: ResponseItemMetadata, } | { "type": "custom_tool_call_output", call_id: string, name?: string, output: FunctionCallOutputBody, metadata?: ResponseItemMetadata, } | { "type": "tool_search_output", call_id: string | null, status: string, execution: string, tools: unknown[], metadata?: ResponseItemMetadata, } | { "type": "web_search_call", status?: string, action?: WebSearchAction, metadata?: ResponseItemMetadata, } | { "type": "image_generation_call",
+id?: string,
 /**
- * Existing provider ID retained on serialized history for compatibility.
+ * Set when using the Responses API.
  */
-id?: string, status: string, revised_prompt?: string, result: string, metadata?: ResponseItemMetadata, } | { "type": "compaction", encrypted_content: string, metadata?: ResponseItemMetadata, } | { "type": "compaction_trigger", metadata?: ResponseItemMetadata, } | { "type": "context_compaction", encrypted_content?: string, metadata?: ResponseItemMetadata, } | { "type": "other" };
+call_id: string | null, status: LocalShellStatus, action: LocalShellAction, metadata?: ResponseItemMetadata, } | { "type": "function_call", id?: string, name: string, namespace?: string, arguments: string, call_id: string, metadata?: ResponseItemMetadata, } | { "type": "tool_search_call", id?: string, call_id: string | null, status?: string, execution: string, arguments: unknown, metadata?: ResponseItemMetadata, } | { "type": "function_call_output", id?: string, call_id: string, output: FunctionCallOutputBody, metadata?: ResponseItemMetadata, } | { "type": "custom_tool_call", id?: string, status?: string, call_id: string, name: string, input: string, metadata?: ResponseItemMetadata, } | { "type": "custom_tool_call_output", id?: string, call_id: string, name?: string, output: FunctionCallOutputBody, metadata?: ResponseItemMetadata, } | { "type": "tool_search_output", id?: string, call_id: string | null, status: string, execution: string, tools: unknown[], metadata?: ResponseItemMetadata, } | { "type": "web_search_call", id?: string, status?: string, action?: WebSearchAction, metadata?: ResponseItemMetadata, } | { "type": "image_generation_call", id?: string, status: string, revised_prompt?: string, result: string, metadata?: ResponseItemMetadata, } | { "type": "compaction", id?: string, encrypted_content: string, metadata?: ResponseItemMetadata, } | { "type": "compaction_trigger", metadata?: ResponseItemMetadata, } | { "type": "context_compaction", id?: string, encrypted_content?: string, metadata?: ResponseItemMetadata, } | { "type": "other" };
diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json
index 9944c2994cd2..19c6f1a56539 100644
--- a/codex-rs/core/config.schema.json
+++ b/codex-rs/core/config.schema.json
@@ -523,6 +523,9 @@
             "in_app_browser": {
               "type": "boolean"
             },
+            "item_ids": {
+              "type": "boolean"
+            },
             "js_repl": {
               "type": "boolean"
             },
@@ -4684,6 +4687,9 @@
         "in_app_browser": {
           "type": "boolean"
         },
+        "item_ids": {
+          "type": "boolean"
+        },
         "js_repl": {
           "type": "boolean"
         },
diff --git a/codex-rs/core/src/compact_remote_v2.rs b/codex-rs/core/src/compact_remote_v2.rs
index 6c3a939024a6..3b79789c02ba 100644
--- a/codex-rs/core/src/compact_remote_v2.rs
+++ b/codex-rs/core/src/compact_remote_v2.rs
@@ -231,10 +231,7 @@ async fn run_remote_compact_task_inner_impl(
     )
     .await?;
     let mut input = prompt_input.clone();
-    input.push(ResponseItem::CompactionTrigger {
-        id: None,
-        metadata: None,
-    });
+    input.push(ResponseItem::CompactionTrigger { metadata: None });
     let prompt = Prompt {
         input,
         tools: tool_router.model_visible_specs(),
diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs
index cf191235919d..e9fa300c1f8e 100644
--- a/codex-rs/core/src/session/mod.rs
+++ b/codex-rs/core/src/session/mod.rs
@@ -2634,17 +2634,46 @@ impl Session {
         turn_context: &TurnContext,
         items: &'a [ResponseItem],
     ) -> Cow<'a, [ResponseItem]> {
-        if !turn_context
+        let mut items = Cow::Borrowed(items);
+        if turn_context
             .config
             .features
             .enabled(Feature::ResizeAllImages)
         {
-            return Cow::Borrowed(items);
+            prepare_response_items(items.to_mut());
+        }
+        if turn_context.config.features.enabled(Feature::ItemIds) {
+            Self::assign_missing_response_item_ids(&mut items);
         }
+        items
+    }
 
-        let mut prepared_items = items.to_vec();
-        prepare_response_items(&mut prepared_items);
-        Cow::Owned(prepared_items)
+    fn assign_missing_response_item_ids(items: &mut Cow<'_, [ResponseItem]>) {
+        if items.iter().all(|item| item.id().is_some()) {
+            return;
+        }
+        for item in items.to_mut() {
+            if item.id().is_some() {
+                continue;
+            }
+            let prefix = match item {
+                ResponseItem::Message { .. } => "msg",
+                ResponseItem::AgentMessage { .. } => "amsg",
+                ResponseItem::Reasoning { .. } => "rs",
+                ResponseItem::LocalShellCall { .. } => "lsh",
+                ResponseItem::FunctionCall { .. } => "fc",
+                ResponseItem::ToolSearchCall { .. } => "tsc",
+                ResponseItem::FunctionCallOutput { .. } => "fco",
+                ResponseItem::CustomToolCall { .. } => "ctc",
+                ResponseItem::CustomToolCallOutput { .. } => "ctco",
+                ResponseItem::ToolSearchOutput { .. } => "tso",
+                ResponseItem::WebSearchCall { .. } => "ws",
+                ResponseItem::ImageGenerationCall { .. } => "ig",
+                ResponseItem::Compaction { .. } | ResponseItem::ContextCompaction { .. } => "cmp",
+                ResponseItem::CompactionTrigger { .. } | ResponseItem::Other => continue,
+            };
+            item.set_id(format!("{prefix}_{}", Uuid::now_v7()));
+        }
     }
 
     pub(crate) fn response_item_from_user_input(
diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs
index 724c35f2e815..d0b7149d87d7 100644
--- a/codex-rs/core/src/session/tests.rs
+++ b/codex-rs/core/src/session/tests.rs
@@ -1660,6 +1660,7 @@ async fn resize_all_images_prepares_failures_before_history_insertion() {
         Vec::new(),
         |config| {
             let _ = config.features.enable(Feature::ResizeAllImages);
+            let _ = config.features.enable(Feature::ItemIds);
         },
     )
     .await;
@@ -1689,8 +1690,18 @@ async fn resize_all_images_prepares_failures_before_history_insertion() {
         .record_conversation_items(turn_context.as_ref(), std::slice::from_ref(&item))
         .await;
 
+    let history = session.state.lock().await.clone_history();
+    let id = history.raw_items()[0]
+        .id()
+        .expect("history item should have an ID")
+        .to_string();
+    let uuid = id
+        .strip_prefix("fco_")
+        .expect("function call output ID should have the Responses API prefix");
+    let parsed_id = Uuid::parse_str(uuid).expect("history item should have a UUID ID");
+    assert_eq!(parsed_id.get_version(), Some(uuid::Version::SortRand));
     let expected = vec![ResponseItem::FunctionCallOutput {
-        id: None,
+        id: Some(id),
         call_id: "call-1".to_string(),
         output: FunctionCallOutputPayload {
             body: FunctionCallOutputBody::ContentItems(vec![
@@ -1709,10 +1720,7 @@ async fn resize_all_images_prepares_failures_before_history_insertion() {
         },
         metadata: None,
     }];
-    assert_eq!(
-        session.state.lock().await.clone_history().raw_items(),
-        expected.as_slice()
-    );
+    assert_eq!(history.raw_items(), expected.as_slice());
 }
 
 #[tokio::test]
diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs
index 54e24d49cbe8..357e6c429d82 100644
--- a/codex-rs/core/tests/suite/client.rs
+++ b/codex-rs/core/tests/suite/client.rs
@@ -134,6 +134,22 @@ fn message_input_text_contains(request: &ResponsesRequest, role: &str, needle: &
         .any(|text| text.contains(needle))
 }
 
+fn response_message_item_id(request: &ResponsesRequest, role: &str, text: &str) -> String {
+    request
+        .inputs_of_type("message")
+        .into_iter()
+        .find(|item| {
+            item.get("role").and_then(serde_json::Value::as_str) == Some(role)
+                && message_input_texts(item).contains(&text)
+        })
+        .and_then(|item| {
+            item.get("id")
+                .and_then(serde_json::Value::as_str)
+                .map(str::to_string)
+        })
+        .unwrap_or_else(|| panic!("missing item ID for {role} message {text:?}"))
+}
+
 fn assert_codex_client_metadata(
     request_body: &serde_json::Value,
     installation_id: &str,
@@ -216,9 +232,71 @@ async fn non_openai_responses_requests_omit_item_turn_metadata() {
             item.get("metadata").is_none(),
             "input item should omit metadata: {item}"
         );
+        assert!(
+            item.get("id").is_none(),
+            "input item should omit generated IDs: {item}"
+        );
     }
 }
 
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn response_item_ids_persist_across_resume_and_preserve_server_ids() -> anyhow::Result<()> {
+    let server = MockServer::start().await;
+    let response_mock = mount_sse_sequence(
+        &server,
+        vec![
+            sse(vec![
+                ev_response_created("resp-1"),
+                ev_assistant_message("msg_server", "first reply"),
+                ev_completed("resp-1"),
+            ]),
+            sse(vec![ev_response_created("resp-2"), ev_completed("resp-2")]),
+        ],
+    )
+    .await;
+    let mut builder = test_codex().with_config(|config| {
+        let _ = config.features.enable(Feature::ItemIds);
+    });
+    let initial = builder.build(&server).await?;
+    let home = Arc::clone(&initial.home);
+    let rollout_path = initial
+        .session_configured
+        .rollout_path
+        .clone()
+        .expect("rollout path");
+
+    initial.submit_turn("before resume").await?;
+    initial.codex.submit(Op::Shutdown).await?;
+    wait_for_event(&initial.codex, |event| {
+        matches!(event, EventMsg::ShutdownComplete)
+    })
+    .await;
+
+    let resumed = builder.resume(&server, home, rollout_path).await?;
+    resumed.submit_turn("after resume").await?;
+
+    let requests = response_mock.requests();
+    assert_eq!(requests.len(), 2);
+    let user_id = response_message_item_id(&requests[0], "user", "before resume");
+    let user_uuid = user_id
+        .strip_prefix("msg_")
+        .expect("message ID should have the Responses API prefix");
+    assert_eq!(
+        Uuid::parse_str(user_uuid)?.get_version(),
+        Some(uuid::Version::SortRand)
+    );
+    assert_eq!(
+        response_message_item_id(&requests[1], "user", "before resume"),
+        user_id
+    );
+    assert_eq!(
+        response_message_item_id(&requests[1], "assistant", "first reply"),
+        "msg_server"
+    );
+
+    Ok(())
+}
+
 /// Writes an `auth.json` into the provided `codex_home` with the specified parameters.
 /// Returns the fake JWT string written to `tokens.id_token`.
 #[expect(clippy::unwrap_used)]
diff --git a/codex-rs/features/src/lib.rs b/codex-rs/features/src/lib.rs
index b887237a18cb..1267239cecd9 100644
--- a/codex-rs/features/src/lib.rs
+++ b/codex-rs/features/src/lib.rs
@@ -189,6 +189,8 @@ pub enum Feature {
     ImageGenExt,
     /// Resize all inline data-URL images before recording them in history.
     ResizeAllImages,
+    /// Generate Responses API item IDs for client-created history items.
+    ItemIds,
     /// Allow prompting and installing missing MCP dependencies.
     SkillMcpDependencyInstall,
     /// Removed compatibility flag for deleted skill env var dependency prompting.
@@ -1113,6 +1115,12 @@ pub const FEATURES: &[FeatureSpec] = &[
         stage: Stage::UnderDevelopment,
         default_enabled: false,
     },
+    FeatureSpec {
+        id: Feature::ItemIds,
+        key: "item_ids",
+        stage: Stage::UnderDevelopment,
+        default_enabled: false,
+    },
     FeatureSpec {
         id: Feature::SkillMcpDependencyInstall,
         key: "skill_mcp_dependency_install",
diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs
index 3c0753cb43ab..ead2d5330b14 100644
--- a/codex-rs/protocol/src/models.rs
+++ b/codex-rs/protocol/src/models.rs
@@ -918,9 +918,8 @@ pub struct ResponseItemMetadata {
 #[serde(tag = "type", rename_all = "snake_case")]
 pub enum ResponseItem {
     Message {
-        #[serde(default, skip_serializing)]
-        #[ts(skip)]
-        #[schemars(skip)]
+        #[serde(default, skip_serializing_if = "Option::is_none")]
+        #[ts(optional)]
         id: Option<String>,
         role: String,
         content: Vec<ContentItem>,
@@ -935,9 +934,8 @@ pub enum ResponseItem {
         metadata: Option<ResponseItemMetadata>,
     },
     AgentMessage {
-        #[serde(default, skip_serializing)]
-        #[ts(skip)]
-        #[schemars(skip)]
+        #[serde(default, skip_serializing_if = "Option::is_none")]
+        #[ts(optional)]
         id: Option<String>,
         author: String,
         recipient: String,
@@ -947,9 +945,8 @@ pub enum ResponseItem {
         metadata: Option<ResponseItemMetadata>,
     },
     Reasoning {
-        #[serde(default, skip_serializing)]
-        #[ts(skip)]
-        #[schemars(skip)]
+        #[serde(default, skip_serializing_if = "Option::is_none")]
+        #[ts(optional)]
         id: Option<String>,
         summary: Vec<ReasoningItemReasoningSummary>,
         #[serde(default, skip_serializing_if = "should_serialize_reasoning_content")]
@@ -962,9 +959,8 @@ pub enum ResponseItem {
     },
     LocalShellCall {
         /// Legacy id field retained for compatibility with older payloads.
-        #[serde(default, skip_serializing)]
-        #[ts(skip)]
-        #[schemars(skip)]
+        #[serde(default, skip_serializing_if = "Option::is_none")]
+        #[ts(optional)]
         id: Option<String>,
         /// Set when using the Responses API.
         call_id: Option<String>,
@@ -975,9 +971,8 @@ pub enum ResponseItem {
         metadata: Option<ResponseItemMetadata>,
     },
     FunctionCall {
-        #[serde(default, skip_serializing)]
-        #[ts(skip)]
-        #[schemars(skip)]
+        #[serde(default, skip_serializing_if = "Option::is_none")]
+        #[ts(optional)]
         id: Option<String>,
         name: String,
         #[serde(default, skip_serializing_if = "Option::is_none")]
@@ -993,9 +988,8 @@ pub enum ResponseItem {
         metadata: Option<ResponseItemMetadata>,
     },
     ToolSearchCall {
-        #[serde(default, skip_serializing)]
-        #[ts(skip)]
-        #[schemars(skip)]
+        #[serde(default, skip_serializing_if = "Option::is_none")]
+        #[ts(optional)]
         id: Option<String>,
         call_id: Option<String>,
         #[serde(default, skip_serializing_if = "Option::is_none")]
@@ -1014,9 +1008,8 @@ pub enum ResponseItem {
     //   - an array of structured content items (`content_items`)
     // We keep this behavior centralized in `FunctionCallOutputPayload`.
     FunctionCallOutput {
-        #[serde(default, skip_serializing)]
-        #[ts(skip)]
-        #[schemars(skip)]
+        #[serde(default, skip_serializing_if = "Option::is_none")]
+        #[ts(optional)]
         id: Option<String>,
         call_id: String,
         #[ts(as = "FunctionCallOutputBody")]
@@ -1027,9 +1020,8 @@ pub enum ResponseItem {
         metadata: Option<ResponseItemMetadata>,
     },
     CustomToolCall {
-        #[serde(default, skip_serializing)]
-        #[ts(skip)]
-        #[schemars(skip)]
+        #[serde(default, skip_serializing_if = "Option::is_none")]
+        #[ts(optional)]
         id: Option<String>,
         #[serde(default, skip_serializing_if = "Option::is_none")]
         #[ts(optional)]
@@ -1046,9 +1038,8 @@ pub enum ResponseItem {
     // `function_call_output.output` so freeform tools can return either plain
     // text or structured content items.
     CustomToolCallOutput {
-        #[serde(default, skip_serializing)]
-        #[ts(skip)]
-        #[schemars(skip)]
+        #[serde(default, skip_serializing_if = "Option::is_none")]
+        #[ts(optional)]
         id: Option<String>,
         call_id: String,
         #[serde(default, skip_serializing_if = "Option::is_none")]
@@ -1062,9 +1053,8 @@ pub enum ResponseItem {
         metadata: Option<ResponseItemMetadata>,
     },
     ToolSearchOutput {
-        #[serde(default, skip_serializing)]
-        #[ts(skip)]
-        #[schemars(skip)]
+        #[serde(default, skip_serializing_if = "Option::is_none")]
+        #[ts(optional)]
         id: Option<String>,
         call_id: Option<String>,
         status: String,
@@ -1084,9 +1074,8 @@ pub enum ResponseItem {
     //   "action": {"type":"search","query":"weather: San Francisco, CA"}
     // }
     WebSearchCall {
-        #[serde(default, skip_serializing)]
-        #[ts(skip)]
-        #[schemars(skip)]
+        #[serde(default, skip_serializing_if = "Option::is_none")]
+        #[ts(optional)]
         id: Option<String>,
         #[serde(default, skip_serializing_if = "Option::is_none")]
         #[ts(optional)]
@@ -1108,7 +1097,6 @@ pub enum ResponseItem {
     //   "result":"..."
     // }
     ImageGenerationCall {
-        /// Existing provider ID retained on serialized history for compatibility.
         #[serde(default, skip_serializing_if = "Option::is_none")]
         #[ts(optional)]
         id: Option<String>,
@@ -1123,28 +1111,24 @@ pub enum ResponseItem {
     },
     #[serde(alias = "compaction_summary")]
     Compaction {
-        #[serde(default, skip_serializing)]
-        #[ts(skip)]
-        #[schemars(skip)]
+        #[serde(default, skip_serializing_if = "Option::is_none")]
+        #[ts(optional)]
         id: Option<String>,
         encrypted_content: String,
         #[serde(default, skip_serializing_if = "Option::is_none")]
         #[ts(optional)]
         metadata: Option<ResponseItemMetadata>,
     },
+    // Compaction triggers are request controls, and the Responses API does not
+    // accept an `id` field for them.
     CompactionTrigger {
-        #[serde(default, skip_serializing)]
-        #[ts(skip)]
-        #[schemars(skip)]
-        id: Option<String>,
         #[serde(default, skip_serializing_if = "Option::is_none")]
         #[ts(optional)]
         metadata: Option<ResponseItemMetadata>,
     },
     ContextCompaction {
-        #[serde(default, skip_serializing)]
-        #[ts(skip)]
-        #[schemars(skip)]
+        #[serde(default, skip_serializing_if = "Option::is_none")]
+        #[ts(optional)]
         id: Option<String>,
         #[serde(default, skip_serializing_if = "Option::is_none")]
         #[ts(optional)]
@@ -1179,9 +1163,8 @@ impl ResponseItem {
             | Self::Reasoning { id, .. }
             | Self::ImageGenerationCall { id, .. }
             | Self::Compaction { id, .. }
-            | Self::CompactionTrigger { id, .. }
             | Self::ContextCompaction { id, .. } => id.as_deref().filter(|id| !id.is_empty()),
-            Self::Other => None,
+            Self::CompactionTrigger { .. } | Self::Other => None,
         }
     }
 
@@ -1201,9 +1184,8 @@ impl ResponseItem {
             | Self::Reasoning { id, .. }
             | Self::ImageGenerationCall { id, .. }
             | Self::Compaction { id, .. }
-            | Self::CompactionTrigger { id, .. }
             | Self::ContextCompaction { id, .. } => *id = Some(new_id),
-            Self::Other => {}
+            Self::CompactionTrigger { .. } | Self::Other => {}
         }
     }
 
@@ -3022,10 +3004,7 @@ mod tests {
 
     #[test]
     fn serializes_compaction_trigger_without_payload() -> Result<()> {
-        let item = ResponseItem::CompactionTrigger {
-            id: None,
-            metadata: None,
-        };
+        let item = ResponseItem::CompactionTrigger { metadata: None };
 
         assert_eq!(
             serde_json::to_value(item)?,
@@ -3038,10 +3017,7 @@ mod tests {
 
     #[test]
     fn serializes_stamped_compaction_trigger_metadata() -> Result<()> {
-        let mut item = ResponseItem::CompactionTrigger {
-            id: None,
-            metadata: None,
-        };
+        let mut item = ResponseItem::CompactionTrigger { metadata: None };
         item.stamp_turn_id_if_missing("turn-1");
 
         assert_eq!(
@@ -3062,13 +3038,7 @@ mod tests {
 
         let item: ResponseItem = serde_json::from_str(json)?;
 
-        assert_eq!(
-            item,
-            ResponseItem::CompactionTrigger {
-                id: None,
-                metadata: None,
-            }
-        );
+        assert_eq!(item, ResponseItem::CompactionTrigger { metadata: None });
         Ok(())
     }
 
@@ -3109,7 +3079,6 @@ mod tests {
                     queries: Some(vec!["weather seattle".into(), "seattle weather now".into()]),
                 }),
                 Some("completed".into()),
-                true,
             ),
             (
                 r#"{
@@ -3125,7 +3094,6 @@ mod tests {
                     url: Some("https://example.com".into()),
                 }),
                 Some("open".into()),
-                true,
             ),
             (
                 r#"{
@@ -3143,7 +3111,6 @@ mod tests {
                     pattern: Some("installation".into()),
                 }),
                 Some("in_progress".into()),
-                true,
             ),
             (
                 r#"{
@@ -3154,12 +3121,10 @@ mod tests {
                 Some("ws_partial".into()),
                 None,
                 Some("in_progress".into()),
-                false,
             ),
         ];
 
-        for (json_literal, expected_id, expected_action, expected_status, expect_roundtrip) in cases
-        {
+        for (json_literal, expected_id, expected_action, expected_status) in cases {
             let parsed: ResponseItem = serde_json::from_str(json_literal)?;
             let expected = ResponseItem::WebSearchCall {
                 id: expected_id.clone(),
@@ -3170,10 +3135,7 @@ mod tests {
             assert_eq!(parsed, expected);
 
             let serialized = serde_json::to_value(&parsed)?;
-            let mut expected_serialized: serde_json::Value = serde_json::from_str(json_literal)?;
-            if !expect_roundtrip && let Some(obj) = expected_serialized.as_object_mut() {
-                obj.remove("id");
-            }
+            let expected_serialized: serde_json::Value = serde_json::from_str(json_literal)?;
             assert_eq!(serialized, expected_serialized);
         }
 

From c2b77d7d580cda225c35b73f0b73689cad7c8d6f Mon Sep 17 00:00:00 2001
From: pakrym-oai <pakrym@openai.com>
Date: Thu, 18 Jun 2026 11:01:38 -0700
Subject: [PATCH 02/14] codex: address PR review feedback (#28814)

---
 codex-rs/codex-api/src/endpoint/responses.rs  |  19 ++-
 codex-rs/codex-api/tests/clients.rs           |  22 +++-
 codex-rs/core/src/client.rs                   |  11 +-
 codex-rs/core/src/client_common.rs            |   4 +
 codex-rs/core/src/compact.rs                  |   2 +
 codex-rs/core/src/compact_remote.rs           |   2 +
 codex-rs/core/src/compact_remote_v2.rs        |   2 +
 codex-rs/core/src/session/mod.rs              |   3 +-
 codex-rs/core/src/session/turn.rs             |   1 +
 codex-rs/core/tests/suite/client.rs           | 121 ++++++++++++++++++
 .../core/tests/suite/client_websockets.rs     |  29 +++++
 codex-rs/protocol/src/models.rs               |  21 +++
 codex-rs/protocol/src/protocol.rs             |   8 +-
 codex-rs/rollout-trace/src/inference.rs       |   1 +
 14 files changed, 237 insertions(+), 9 deletions(-)

diff --git a/codex-rs/codex-api/src/endpoint/responses.rs b/codex-rs/codex-api/src/endpoint/responses.rs
index ad79adf33dff..8d120618f232 100644
--- a/codex-rs/codex-api/src/endpoint/responses.rs
+++ b/codex-rs/codex-api/src/endpoint/responses.rs
@@ -37,6 +37,7 @@ pub struct ResponsesOptions {
     pub extra_headers: HeaderMap,
     pub compression: Compression,
     pub turn_state: Option<Arc<OnceLock<String>>>,
+    pub include_item_ids: bool,
 }
 
 impl<T: HttpTransport> ResponsesClient<T> {
@@ -80,13 +81,27 @@ impl<T: HttpTransport> ResponsesClient<T> {
             extra_headers,
             compression,
             turn_state,
+            include_item_ids,
         } = options;
 
-        let body = if request.store && self.session.provider().is_azure_responses_endpoint() {
+        let attach_azure_item_ids =
+            request.store && self.session.provider().is_azure_responses_endpoint();
+        let body = if !include_item_ids || attach_azure_item_ids {
             let mut body = serde_json::to_value(&request).map_err(|e| {
                 ApiError::Stream(format!("failed to encode responses request: {e}"))
             })?;
-            attach_item_ids(&mut body, &request.input);
+            if !include_item_ids
+                && let Some(items) = body.get_mut("input").and_then(Value::as_array_mut)
+            {
+                for item in items {
+                    if let Some(item) = item.as_object_mut() {
+                        item.remove("id");
+                    }
+                }
+            }
+            if attach_azure_item_ids {
+                attach_item_ids(&mut body, &request.input);
+            }
             EncodedJsonBody::encode(&body)
         } else {
             EncodedJsonBody::encode(&request)
diff --git a/codex-rs/codex-api/tests/clients.rs b/codex-rs/codex-api/tests/clients.rs
index d8489ed7487f..824f60d04bd3 100644
--- a/codex-rs/codex-api/tests/clients.rs
+++ b/codex-rs/codex-api/tests/clients.rs
@@ -301,7 +301,7 @@ async fn responses_client_uses_responses_path() -> Result<()> {
 }
 
 #[tokio::test]
-async fn responses_client_stream_request_preserves_exact_json_body() -> Result<()> {
+async fn responses_client_stream_request_gates_item_ids() -> Result<()> {
     let state = RecordingState::default();
     let transport = RecordingTransport::new(state.clone());
     let client = ResponsesClient::new(transport, provider("openai"), Arc::new(NoAuth));
@@ -330,12 +330,25 @@ async fn responses_client_stream_request_preserves_exact_json_body() -> Result<(
     let expected = serde_json::to_vec(&request)?;
 
     let _stream = client
-        .stream_request(request, ResponsesOptions::default())
+        .stream_request(request.clone(), ResponsesOptions::default())
+        .await?;
+    let _stream = client
+        .stream_request(
+            request,
+            ResponsesOptions {
+                include_item_ids: true,
+                ..Default::default()
+            },
+        )
         .await?;
 
     let requests = state.take_stream_requests();
-    assert_eq!(requests.len(), 1);
-    let prepared = requests[0]
+    assert_eq!(requests.len(), 2);
+    let body_without_ids: serde_json::Value =
+        serde_json::from_slice(request_body_bytes(&requests[0]))?;
+    assert_eq!(body_without_ids["input"][0].get("id"), None);
+
+    let prepared = requests[1]
         .prepare_body_for_send()
         .expect("body should prepare");
     assert_eq!(prepared.body.as_deref(), Some(expected.as_slice()));
@@ -542,6 +555,7 @@ async fn azure_default_store_attaches_ids_and_headers() -> Result<()> {
                 extra_headers,
                 compression: Compression::None,
                 turn_state: None,
+                include_item_ids: false,
             },
         )
         .await?;
diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs
index 4ea8c0ff1fb8..04e45475bca3 100644
--- a/codex-rs/core/src/client.rs
+++ b/codex-rs/core/src/client.rs
@@ -508,7 +508,7 @@ impl ModelClient {
             RequestRouteTelemetry::for_endpoint(RESPONSES_COMPACT_ENDPOINT),
             self.state.auth_env_telemetry.clone(),
         );
-        let request = self.build_responses_request(
+        let mut request = self.build_responses_request(
             &client_setup.api_provider,
             prompt,
             model_info,
@@ -517,6 +517,9 @@ impl ModelClient {
             settings.service_tier,
             responses_metadata,
         )?;
+        if !prompt.include_item_ids {
+            request.input.iter_mut().for_each(ResponseItem::clear_id);
+        }
         let ResponsesApiRequest {
             model,
             instructions,
@@ -1033,6 +1036,7 @@ impl ModelClientSession {
             },
             compression,
             turn_state: Some(Arc::clone(&self.turn_state)),
+            include_item_ids: false,
         }
     }
 
@@ -1298,6 +1302,7 @@ impl ModelClientSession {
                     model_info.use_responses_lite,
                 )
                 .await;
+            options.include_item_ids = prompt.include_item_ids;
 
             let request = self.client.build_responses_request(
                 &client_setup.api_provider,
@@ -1467,6 +1472,10 @@ impl ModelClientSession {
 
             let (mut ws_request, previous_response_id_from_untraced_warmup) =
                 self.prepare_websocket_request(ws_payload, &request);
+            if !prompt.include_item_ids {
+                let ResponsesWsRequest::ResponseCreate(payload) = &mut ws_request;
+                payload.input.iter_mut().for_each(ResponseItem::clear_id);
+            }
             let inference_trace_attempt = if warmup {
                 // Prewarm sends `generate=false`; it is connection setup, not a
                 // model inference attempt that should appear in rollout traces.
diff --git a/codex-rs/core/src/client_common.rs b/codex-rs/core/src/client_common.rs
index e0c2ff03c302..41edf4add8f4 100644
--- a/codex-rs/core/src/client_common.rs
+++ b/codex-rs/core/src/client_common.rs
@@ -19,6 +19,9 @@ pub struct Prompt {
     /// Conversation context input items.
     pub input: Vec<ResponseItem>,
 
+    /// Whether Responses API item IDs should be included on the wire.
+    pub include_item_ids: bool,
+
     /// Tools available to the model, including additional tools sourced from
     /// external MCP servers.
     pub(crate) tools: Vec<ToolSpec>,
@@ -39,6 +42,7 @@ impl Default for Prompt {
     fn default() -> Self {
         Self {
             input: Vec::new(),
+            include_item_ids: false,
             tools: Vec::new(),
             parallel_tool_calls: false,
             base_instructions: BaseInstructions::default(),
diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index 9d79138e87ce..943b4a50f9a3 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -25,6 +25,7 @@ use codex_analytics::CompactionStatus;
 use codex_analytics::CompactionStrategy;
 use codex_analytics::CompactionTrigger;
 use codex_analytics::now_unix_seconds;
+use codex_features::Feature;
 use codex_protocol::error::CodexErr;
 use codex_protocol::error::Result as CodexResult;
 use codex_protocol::items::ContextCompactionItem;
@@ -238,6 +239,7 @@ async fn run_compact_task_inner_impl(
         let turn_input_len = turn_input.len();
         let prompt = Prompt {
             input: turn_input,
+            include_item_ids: turn_context.config.features.enabled(Feature::ItemIds),
             base_instructions: sess.get_base_instructions().await,
             ..Default::default()
         };
diff --git a/codex-rs/core/src/compact_remote.rs b/codex-rs/core/src/compact_remote.rs
index c88d8062d602..115106b936b3 100644
--- a/codex-rs/core/src/compact_remote.rs
+++ b/codex-rs/core/src/compact_remote.rs
@@ -23,6 +23,7 @@ use codex_analytics::CompactionPhase;
 use codex_analytics::CompactionReason;
 use codex_analytics::CompactionTrigger;
 use codex_app_server_protocol::AuthMode;
+use codex_features::Feature;
 use codex_protocol::error::CodexErr;
 use codex_protocol::error::Result as CodexResult;
 use codex_protocol::items::ContextCompactionItem;
@@ -225,6 +226,7 @@ async fn run_remote_compact_task_inner_impl(
     .await?;
     let prompt = Prompt {
         input: prompt_input,
+        include_item_ids: turn_context.config.features.enabled(Feature::ItemIds),
         tools: tool_router.model_visible_specs(),
         parallel_tool_calls: turn_context.model_info.supports_parallel_tool_calls,
         base_instructions,
diff --git a/codex-rs/core/src/compact_remote_v2.rs b/codex-rs/core/src/compact_remote_v2.rs
index 3b79789c02ba..864a814ef626 100644
--- a/codex-rs/core/src/compact_remote_v2.rs
+++ b/codex-rs/core/src/compact_remote_v2.rs
@@ -27,6 +27,7 @@ use codex_analytics::CompactionImplementation;
 use codex_analytics::CompactionPhase;
 use codex_analytics::CompactionReason;
 use codex_analytics::CompactionTrigger;
+use codex_features::Feature;
 use codex_protocol::error::CodexErr;
 use codex_protocol::error::Result as CodexResult;
 use codex_protocol::items::ContextCompactionItem;
@@ -234,6 +235,7 @@ async fn run_remote_compact_task_inner_impl(
     input.push(ResponseItem::CompactionTrigger { metadata: None });
     let prompt = Prompt {
         input,
+        include_item_ids: turn_context.config.features.enabled(Feature::ItemIds),
         tools: tool_router.model_visible_specs(),
         parallel_tool_calls: turn_context.model_info.supports_parallel_tool_calls,
         base_instructions,
diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs
index e9fa300c1f8e..f91ccbd09402 100644
--- a/codex-rs/core/src/session/mod.rs
+++ b/codex-rs/core/src/session/mod.rs
@@ -2717,7 +2717,7 @@ impl Session {
     pub(crate) async fn record_inter_agent_communication(
         &self,
         turn_context: &TurnContext,
-        communication: InterAgentCommunication,
+        mut communication: InterAgentCommunication,
     ) {
         let response_item = communication.to_model_input_item();
         let items = self.prepare_conversation_items_for_history(
@@ -2725,6 +2725,7 @@ impl Session {
             std::slice::from_ref(&response_item),
         );
         let items = items.as_ref();
+        communication.id = items.first().and_then(ResponseItem::id).map(str::to_owned);
         {
             let mut state = self.state.lock().await;
             state.record_items(
diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs
index 4beef792bb30..31b5671231fc 100644
--- a/codex-rs/core/src/session/turn.rs
+++ b/codex-rs/core/src/session/turn.rs
@@ -1026,6 +1026,7 @@ pub(crate) fn build_prompt(
 ) -> Prompt {
     Prompt {
         input,
+        include_item_ids: turn_context.config.features.enabled(Feature::ItemIds),
         tools: router.model_visible_specs(),
         parallel_tool_calls: turn_context.model_info.supports_parallel_tool_calls,
         base_instructions,
diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs
index 357e6c429d82..95a285015197 100644
--- a/codex-rs/core/tests/suite/client.rs
+++ b/codex-rs/core/tests/suite/client.rs
@@ -19,6 +19,7 @@ use codex_model_provider_info::built_in_model_providers;
 use codex_models_manager::bundled_models_response;
 use codex_otel::SessionTelemetry;
 use codex_otel::TelemetryAuthMode;
+use codex_protocol::AgentPath;
 use codex_protocol::ThreadId;
 use codex_protocol::config_types::CollaborationMode;
 use codex_protocol::config_types::ModeKind;
@@ -42,6 +43,7 @@ use codex_protocol::models::ResponseItem;
 use codex_protocol::models::WebSearchAction;
 use codex_protocol::openai_models::ReasoningEffort;
 use codex_protocol::protocol::EventMsg;
+use codex_protocol::protocol::InterAgentCommunication;
 use codex_protocol::protocol::Op;
 use codex_protocol::protocol::RolloutItem;
 use codex_protocol::protocol::RolloutLine;
@@ -150,6 +152,19 @@ fn response_message_item_id(request: &ResponsesRequest, role: &str, text: &str)
         .unwrap_or_else(|| panic!("missing item ID for {role} message {text:?}"))
 }
 
+fn response_agent_message_item_id(request: &ResponsesRequest, text: &str) -> String {
+    request
+        .inputs_of_type("agent_message")
+        .into_iter()
+        .find(|item| message_input_texts(item).contains(&text))
+        .and_then(|item| {
+            item.get("id")
+                .and_then(serde_json::Value::as_str)
+                .map(str::to_string)
+        })
+        .unwrap_or_else(|| panic!("missing item ID for agent message {text:?}"))
+}
+
 fn assert_codex_client_metadata(
     request_body: &serde_json::Value,
     installation_id: &str,
@@ -239,6 +254,38 @@ async fn non_openai_responses_requests_omit_item_turn_metadata() {
     }
 }
 
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn response_item_ids_are_omitted_when_feature_is_disabled() -> anyhow::Result<()> {
+    let server = MockServer::start().await;
+    let response_mock = mount_sse_sequence(
+        &server,
+        vec![
+            sse(vec![
+                ev_response_created("resp-1"),
+                ev_assistant_message("msg_server", "first reply"),
+                ev_completed("resp-1"),
+            ]),
+            sse(vec![ev_response_created("resp-2"), ev_completed("resp-2")]),
+        ],
+    )
+    .await;
+    let test = test_codex().build(&server).await?;
+
+    test.submit_turn("first turn").await?;
+    test.submit_turn("second turn").await?;
+
+    let requests = response_mock.requests();
+    assert_eq!(requests.len(), 2);
+    for item in requests[1].input() {
+        assert!(
+            item.get("id").is_none(),
+            "input item should omit IDs while the feature is disabled: {item}"
+        );
+    }
+
+    Ok(())
+}
+
 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
 async fn response_item_ids_persist_across_resume_and_preserve_server_ids() -> anyhow::Result<()> {
     let server = MockServer::start().await;
@@ -272,6 +319,9 @@ async fn response_item_ids_persist_across_resume_and_preserve_server_ids() -> an
     })
     .await;
 
+    builder = builder.with_config(|config| {
+        let _ = config.features.enable(Feature::ItemIds);
+    });
     let resumed = builder.resume(&server, home, rollout_path).await?;
     resumed.submit_turn("after resume").await?;
 
@@ -297,6 +347,77 @@ async fn response_item_ids_persist_across_resume_and_preserve_server_ids() -> an
     Ok(())
 }
 
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn inter_agent_response_item_ids_persist_across_resume() -> anyhow::Result<()> {
+    let server = MockServer::start().await;
+    let response_mock = mount_sse_sequence(
+        &server,
+        vec![
+            sse(vec![
+                ev_response_created("resp-mail"),
+                ev_completed("resp-mail"),
+            ]),
+            sse(vec![ev_response_created("resp-2"), ev_completed("resp-2")]),
+        ],
+    )
+    .await;
+    let mut builder = test_codex().with_config(|config| {
+        let _ = config.features.enable(Feature::ItemIds);
+    });
+    let initial = builder.build(&server).await?;
+    let home = Arc::clone(&initial.home);
+    let rollout_path = initial
+        .session_configured
+        .rollout_path
+        .clone()
+        .expect("rollout path");
+
+    initial
+        .codex
+        .submit(Op::InterAgentCommunication {
+            communication: InterAgentCommunication::new(
+                AgentPath::try_from("/root/worker").expect("worker path should parse"),
+                AgentPath::root(),
+                Vec::new(),
+                "child update".to_string(),
+                /*trigger_turn*/ true,
+            ),
+        })
+        .await?;
+    wait_for_event(&initial.codex, |event| {
+        matches!(event, EventMsg::TurnComplete(_))
+    })
+    .await;
+    initial.codex.submit(Op::Shutdown).await?;
+    wait_for_event(&initial.codex, |event| {
+        matches!(event, EventMsg::ShutdownComplete)
+    })
+    .await;
+
+    builder = builder.with_config(|config| {
+        let _ = config.features.enable(Feature::ItemIds);
+    });
+    let resumed = builder.resume(&server, home, rollout_path).await?;
+    resumed.submit_turn("after resume").await?;
+
+    let requests = response_mock.requests();
+    assert_eq!(requests.len(), 2);
+    let agent_message_id = response_agent_message_item_id(&requests[0], "child update");
+    let agent_message_uuid = agent_message_id
+        .strip_prefix("amsg_")
+        .expect("agent message ID should have the Responses API prefix");
+    assert_eq!(
+        Uuid::parse_str(agent_message_uuid)?.get_version(),
+        Some(uuid::Version::SortRand)
+    );
+    assert_eq!(
+        response_agent_message_item_id(&requests[1], "child update"),
+        agent_message_id
+    );
+
+    Ok(())
+}
+
 /// Writes an `auth.json` into the provided `codex_home` with the specified parameters.
 /// Returns the fake JWT string written to `tokens.id_token`.
 #[expect(clippy::unwrap_used)]
diff --git a/codex-rs/core/tests/suite/client_websockets.rs b/codex-rs/core/tests/suite/client_websockets.rs
index cee54e24b310..223477034975 100755
--- a/codex-rs/core/tests/suite/client_websockets.rs
+++ b/codex-rs/core/tests/suite/client_websockets.rs
@@ -1513,6 +1513,35 @@ async fn responses_websocket_connection_limit_error_reconnects_and_completes() {
     server.shutdown().await;
 }
 
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn responses_websocket_gates_item_ids() {
+    skip_if_no_network!();
+
+    let server = start_websocket_server(vec![vec![
+        vec![ev_response_created("resp-1"), ev_completed("resp-1")],
+        vec![ev_response_created("resp-2"), ev_completed("resp-2")],
+    ]])
+    .await;
+
+    let harness = websocket_harness(&server).await;
+    let mut client_session = harness.client.new_session();
+    let prompt_without_ids = prompt_with_input(vec![assistant_message_item("msg-1", "first")]);
+    let mut prompt_with_ids = prompt_with_input(vec![assistant_message_item("msg-2", "second")]);
+    prompt_with_ids.include_item_ids = true;
+
+    stream_until_complete(&mut client_session, &harness, &prompt_without_ids).await;
+    stream_until_complete(&mut client_session, &harness, &prompt_with_ids).await;
+
+    let connection = server.single_connection();
+    assert_eq!(connection.len(), 2);
+    let first = connection.first().expect("missing request").body_json();
+    let second = connection.get(1).expect("missing request").body_json();
+    assert_eq!(first["input"][0].get("id"), None);
+    assert_eq!(second["input"][0]["id"].as_str(), Some("msg-2"));
+
+    server.shutdown().await;
+}
+
 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
 async fn responses_websocket_uses_incremental_create_on_prefix() {
     skip_if_no_network!();
diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs
index ead2d5330b14..6911e65846c7 100644
--- a/codex-rs/protocol/src/models.rs
+++ b/codex-rs/protocol/src/models.rs
@@ -1189,6 +1189,27 @@ impl ResponseItem {
         }
     }
 
+    /// Clears the Responses API item ID for variants that carry one.
+    pub fn clear_id(&mut self) {
+        match self {
+            Self::Message { id, .. }
+            | Self::AgentMessage { id, .. }
+            | Self::LocalShellCall { id, .. }
+            | Self::FunctionCall { id, .. }
+            | Self::ToolSearchCall { id, .. }
+            | Self::FunctionCallOutput { id, .. }
+            | Self::CustomToolCall { id, .. }
+            | Self::CustomToolCallOutput { id, .. }
+            | Self::ToolSearchOutput { id, .. }
+            | Self::WebSearchCall { id, .. }
+            | Self::Reasoning { id, .. }
+            | Self::ImageGenerationCall { id, .. }
+            | Self::Compaction { id, .. }
+            | Self::ContextCompaction { id, .. } => *id = None,
+            Self::CompactionTrigger { .. } | Self::Other => {}
+        }
+    }
+
     /// Returns the non-empty turn ID stamped onto this item, if present.
     pub fn turn_id(&self) -> Option<&str> {
         self.metadata()
diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs
index a0e5b24d4704..341f56049e9c 100644
--- a/codex-rs/protocol/src/protocol.rs
+++ b/codex-rs/protocol/src/protocol.rs
@@ -676,6 +676,9 @@ impl From<Vec<UserInput>> for Op {
 
 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)]
 pub struct InterAgentCommunication {
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
+    pub id: Option<String>,
     pub author: AgentPath,
     pub recipient: AgentPath,
     #[serde(default)]
@@ -699,6 +702,7 @@ impl InterAgentCommunication {
         trigger_turn: bool,
     ) -> Self {
         Self {
+            id: None,
             author,
             recipient,
             other_recipients,
@@ -717,6 +721,7 @@ impl InterAgentCommunication {
         trigger_turn: bool,
     ) -> Self {
         Self {
+            id: None,
             author,
             recipient,
             other_recipients,
@@ -762,7 +767,7 @@ impl InterAgentCommunication {
             }],
         };
         ResponseItem::AgentMessage {
-            id: None,
+            id: self.id.clone(),
             author: self.author.to_string(),
             recipient: self.recipient.to_string(),
             content,
@@ -4251,6 +4256,7 @@ mod tests {
     #[test]
     fn inter_agent_communication_response_input_item_preserves_commentary_phase() {
         let communication = InterAgentCommunication {
+            id: None,
             author: AgentPath::root(),
             recipient: AgentPath::root().join("reviewer").expect("recipient path"),
             other_recipients: vec![AgentPath::root().join("worker").expect("recipient path")],
diff --git a/codex-rs/rollout-trace/src/inference.rs b/codex-rs/rollout-trace/src/inference.rs
index 7329fd3cf219..7afdcf584727 100644
--- a/codex-rs/rollout-trace/src/inference.rs
+++ b/codex-rs/rollout-trace/src/inference.rs
@@ -515,6 +515,7 @@ mod tests {
             traced,
             json!({
                 "type": "reasoning",
+                "id": "rs-1",
                 "summary": [{"type": "summary_text", "text": "summary"}],
                 "content": [{"type": "text", "text": "raw reasoning"}],
                 "encrypted_content": "encoded",

From 9d198cf1938a24412d6388f1f90afd09266c9fd5 Mon Sep 17 00:00:00 2001
From: pakrym-oai <pakrym@openai.com>
Date: Thu, 18 Jun 2026 11:13:33 -0700
Subject: [PATCH 03/14] codex: preserve legacy image generation IDs (#28814)

---
 codex-rs/codex-api/src/endpoint/responses.rs | 25 +++++++++-----------
 codex-rs/core/src/client.rs                  | 10 ++++++--
 codex-rs/protocol/src/models.rs              |  9 +++----
 3 files changed, 24 insertions(+), 20 deletions(-)

diff --git a/codex-rs/codex-api/src/endpoint/responses.rs b/codex-rs/codex-api/src/endpoint/responses.rs
index 8d120618f232..ea68acaf1f82 100644
--- a/codex-rs/codex-api/src/endpoint/responses.rs
+++ b/codex-rs/codex-api/src/endpoint/responses.rs
@@ -15,6 +15,7 @@ use codex_client::EncodedJsonBody;
 use codex_client::HttpTransport;
 use codex_client::RequestCompression;
 use codex_client::RequestTelemetry;
+use codex_protocol::models::ResponseItem;
 use codex_protocol::protocol::SessionSource;
 use http::HeaderMap;
 use http::HeaderValue;
@@ -71,7 +72,7 @@ impl<T: HttpTransport> ResponsesClient<T> {
     )]
     pub async fn stream_request(
         &self,
-        request: ResponsesApiRequest,
+        mut request: ResponsesApiRequest,
         options: ResponsesOptions,
     ) -> Result<ResponseStream, ApiError> {
         let ResponsesOptions {
@@ -86,22 +87,18 @@ impl<T: HttpTransport> ResponsesClient<T> {
 
         let attach_azure_item_ids =
             request.store && self.session.provider().is_azure_responses_endpoint();
-        let body = if !include_item_ids || attach_azure_item_ids {
+        let azure_item_ids = attach_azure_item_ids.then(|| request.input.clone());
+        if !include_item_ids {
+            request
+                .input
+                .iter_mut()
+                .for_each(ResponseItem::clear_feature_gated_id);
+        }
+        let body = if let Some(azure_item_ids) = azure_item_ids {
             let mut body = serde_json::to_value(&request).map_err(|e| {
                 ApiError::Stream(format!("failed to encode responses request: {e}"))
             })?;
-            if !include_item_ids
-                && let Some(items) = body.get_mut("input").and_then(Value::as_array_mut)
-            {
-                for item in items {
-                    if let Some(item) = item.as_object_mut() {
-                        item.remove("id");
-                    }
-                }
-            }
-            if attach_azure_item_ids {
-                attach_item_ids(&mut body, &request.input);
-            }
+            attach_item_ids(&mut body, &azure_item_ids);
             EncodedJsonBody::encode(&body)
         } else {
             EncodedJsonBody::encode(&request)
diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs
index 04e45475bca3..3c10210553e4 100644
--- a/codex-rs/core/src/client.rs
+++ b/codex-rs/core/src/client.rs
@@ -518,7 +518,10 @@ impl ModelClient {
             responses_metadata,
         )?;
         if !prompt.include_item_ids {
-            request.input.iter_mut().for_each(ResponseItem::clear_id);
+            request
+                .input
+                .iter_mut()
+                .for_each(ResponseItem::clear_feature_gated_id);
         }
         let ResponsesApiRequest {
             model,
@@ -1474,7 +1477,10 @@ impl ModelClientSession {
                 self.prepare_websocket_request(ws_payload, &request);
             if !prompt.include_item_ids {
                 let ResponsesWsRequest::ResponseCreate(payload) = &mut ws_request;
-                payload.input.iter_mut().for_each(ResponseItem::clear_id);
+                payload
+                    .input
+                    .iter_mut()
+                    .for_each(ResponseItem::clear_feature_gated_id);
             }
             let inference_trace_attempt = if warmup {
                 // Prewarm sends `generate=false`; it is connection setup, not a
diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs
index 6911e65846c7..194a1ceef9ff 100644
--- a/codex-rs/protocol/src/models.rs
+++ b/codex-rs/protocol/src/models.rs
@@ -1189,8 +1189,10 @@ impl ResponseItem {
         }
     }
 
-    /// Clears the Responses API item ID for variants that carry one.
-    pub fn clear_id(&mut self) {
+    /// Clears IDs whose serialization is controlled by the `item_ids` feature.
+    ///
+    /// Image generation call IDs predate that feature and remain serialized for compatibility.
+    pub fn clear_feature_gated_id(&mut self) {
         match self {
             Self::Message { id, .. }
             | Self::AgentMessage { id, .. }
@@ -1203,10 +1205,9 @@ impl ResponseItem {
             | Self::ToolSearchOutput { id, .. }
             | Self::WebSearchCall { id, .. }
             | Self::Reasoning { id, .. }
-            | Self::ImageGenerationCall { id, .. }
             | Self::Compaction { id, .. }
             | Self::ContextCompaction { id, .. } => *id = None,
-            Self::CompactionTrigger { .. } | Self::Other => {}
+            Self::ImageGenerationCall { .. } | Self::CompactionTrigger { .. } | Self::Other => {}
         }
     }
 

From f2e69e58fa468c3ba570bdbecb796e787626c233 Mon Sep 17 00:00:00 2001
From: pakrym-oai <pakrym@openai.com>
Date: Thu, 18 Jun 2026 11:28:46 -0700
Subject: [PATCH 04/14] codex: keep item ID serialization unconditional
 (#28814)

---
 codex-rs/codex-api/src/endpoint/responses.rs  | 18 ++-------
 codex-rs/codex-api/tests/clients.rs           | 22 ++---------
 codex-rs/core/src/client.rs                   | 17 +--------
 codex-rs/core/src/client_common.rs            |  4 --
 codex-rs/core/src/compact.rs                  |  2 -
 codex-rs/core/src/compact_remote.rs           |  2 -
 codex-rs/core/src/compact_remote_v2.rs        |  2 -
 codex-rs/core/src/session/turn.rs             |  1 -
 codex-rs/core/tests/suite/client.rs           | 38 -------------------
 .../core/tests/suite/client_websockets.rs     | 29 --------------
 codex-rs/protocol/src/models.rs               | 22 -----------
 11 files changed, 8 insertions(+), 149 deletions(-)

diff --git a/codex-rs/codex-api/src/endpoint/responses.rs b/codex-rs/codex-api/src/endpoint/responses.rs
index ea68acaf1f82..ad79adf33dff 100644
--- a/codex-rs/codex-api/src/endpoint/responses.rs
+++ b/codex-rs/codex-api/src/endpoint/responses.rs
@@ -15,7 +15,6 @@ use codex_client::EncodedJsonBody;
 use codex_client::HttpTransport;
 use codex_client::RequestCompression;
 use codex_client::RequestTelemetry;
-use codex_protocol::models::ResponseItem;
 use codex_protocol::protocol::SessionSource;
 use http::HeaderMap;
 use http::HeaderValue;
@@ -38,7 +37,6 @@ pub struct ResponsesOptions {
     pub extra_headers: HeaderMap,
     pub compression: Compression,
     pub turn_state: Option<Arc<OnceLock<String>>>,
-    pub include_item_ids: bool,
 }
 
 impl<T: HttpTransport> ResponsesClient<T> {
@@ -72,7 +70,7 @@ impl<T: HttpTransport> ResponsesClient<T> {
     )]
     pub async fn stream_request(
         &self,
-        mut request: ResponsesApiRequest,
+        request: ResponsesApiRequest,
         options: ResponsesOptions,
     ) -> Result<ResponseStream, ApiError> {
         let ResponsesOptions {
@@ -82,23 +80,13 @@ impl<T: HttpTransport> ResponsesClient<T> {
             extra_headers,
             compression,
             turn_state,
-            include_item_ids,
         } = options;
 
-        let attach_azure_item_ids =
-            request.store && self.session.provider().is_azure_responses_endpoint();
-        let azure_item_ids = attach_azure_item_ids.then(|| request.input.clone());
-        if !include_item_ids {
-            request
-                .input
-                .iter_mut()
-                .for_each(ResponseItem::clear_feature_gated_id);
-        }
-        let body = if let Some(azure_item_ids) = azure_item_ids {
+        let body = if request.store && self.session.provider().is_azure_responses_endpoint() {
             let mut body = serde_json::to_value(&request).map_err(|e| {
                 ApiError::Stream(format!("failed to encode responses request: {e}"))
             })?;
-            attach_item_ids(&mut body, &azure_item_ids);
+            attach_item_ids(&mut body, &request.input);
             EncodedJsonBody::encode(&body)
         } else {
             EncodedJsonBody::encode(&request)
diff --git a/codex-rs/codex-api/tests/clients.rs b/codex-rs/codex-api/tests/clients.rs
index 824f60d04bd3..d8489ed7487f 100644
--- a/codex-rs/codex-api/tests/clients.rs
+++ b/codex-rs/codex-api/tests/clients.rs
@@ -301,7 +301,7 @@ async fn responses_client_uses_responses_path() -> Result<()> {
 }
 
 #[tokio::test]
-async fn responses_client_stream_request_gates_item_ids() -> Result<()> {
+async fn responses_client_stream_request_preserves_exact_json_body() -> Result<()> {
     let state = RecordingState::default();
     let transport = RecordingTransport::new(state.clone());
     let client = ResponsesClient::new(transport, provider("openai"), Arc::new(NoAuth));
@@ -330,25 +330,12 @@ async fn responses_client_stream_request_gates_item_ids() -> Result<()> {
     let expected = serde_json::to_vec(&request)?;
 
     let _stream = client
-        .stream_request(request.clone(), ResponsesOptions::default())
-        .await?;
-    let _stream = client
-        .stream_request(
-            request,
-            ResponsesOptions {
-                include_item_ids: true,
-                ..Default::default()
-            },
-        )
+        .stream_request(request, ResponsesOptions::default())
         .await?;
 
     let requests = state.take_stream_requests();
-    assert_eq!(requests.len(), 2);
-    let body_without_ids: serde_json::Value =
-        serde_json::from_slice(request_body_bytes(&requests[0]))?;
-    assert_eq!(body_without_ids["input"][0].get("id"), None);
-
-    let prepared = requests[1]
+    assert_eq!(requests.len(), 1);
+    let prepared = requests[0]
         .prepare_body_for_send()
         .expect("body should prepare");
     assert_eq!(prepared.body.as_deref(), Some(expected.as_slice()));
@@ -555,7 +542,6 @@ async fn azure_default_store_attaches_ids_and_headers() -> Result<()> {
                 extra_headers,
                 compression: Compression::None,
                 turn_state: None,
-                include_item_ids: false,
             },
         )
         .await?;
diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs
index 3c10210553e4..4ea8c0ff1fb8 100644
--- a/codex-rs/core/src/client.rs
+++ b/codex-rs/core/src/client.rs
@@ -508,7 +508,7 @@ impl ModelClient {
             RequestRouteTelemetry::for_endpoint(RESPONSES_COMPACT_ENDPOINT),
             self.state.auth_env_telemetry.clone(),
         );
-        let mut request = self.build_responses_request(
+        let request = self.build_responses_request(
             &client_setup.api_provider,
             prompt,
             model_info,
@@ -517,12 +517,6 @@ impl ModelClient {
             settings.service_tier,
             responses_metadata,
         )?;
-        if !prompt.include_item_ids {
-            request
-                .input
-                .iter_mut()
-                .for_each(ResponseItem::clear_feature_gated_id);
-        }
         let ResponsesApiRequest {
             model,
             instructions,
@@ -1039,7 +1033,6 @@ impl ModelClientSession {
             },
             compression,
             turn_state: Some(Arc::clone(&self.turn_state)),
-            include_item_ids: false,
         }
     }
 
@@ -1305,7 +1298,6 @@ impl ModelClientSession {
                     model_info.use_responses_lite,
                 )
                 .await;
-            options.include_item_ids = prompt.include_item_ids;
 
             let request = self.client.build_responses_request(
                 &client_setup.api_provider,
@@ -1475,13 +1467,6 @@ impl ModelClientSession {
 
             let (mut ws_request, previous_response_id_from_untraced_warmup) =
                 self.prepare_websocket_request(ws_payload, &request);
-            if !prompt.include_item_ids {
-                let ResponsesWsRequest::ResponseCreate(payload) = &mut ws_request;
-                payload
-                    .input
-                    .iter_mut()
-                    .for_each(ResponseItem::clear_feature_gated_id);
-            }
             let inference_trace_attempt = if warmup {
                 // Prewarm sends `generate=false`; it is connection setup, not a
                 // model inference attempt that should appear in rollout traces.
diff --git a/codex-rs/core/src/client_common.rs b/codex-rs/core/src/client_common.rs
index 41edf4add8f4..e0c2ff03c302 100644
--- a/codex-rs/core/src/client_common.rs
+++ b/codex-rs/core/src/client_common.rs
@@ -19,9 +19,6 @@ pub struct Prompt {
     /// Conversation context input items.
     pub input: Vec<ResponseItem>,
 
-    /// Whether Responses API item IDs should be included on the wire.
-    pub include_item_ids: bool,
-
     /// Tools available to the model, including additional tools sourced from
     /// external MCP servers.
     pub(crate) tools: Vec<ToolSpec>,
@@ -42,7 +39,6 @@ impl Default for Prompt {
     fn default() -> Self {
         Self {
             input: Vec::new(),
-            include_item_ids: false,
             tools: Vec::new(),
             parallel_tool_calls: false,
             base_instructions: BaseInstructions::default(),
diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index 943b4a50f9a3..9d79138e87ce 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -25,7 +25,6 @@ use codex_analytics::CompactionStatus;
 use codex_analytics::CompactionStrategy;
 use codex_analytics::CompactionTrigger;
 use codex_analytics::now_unix_seconds;
-use codex_features::Feature;
 use codex_protocol::error::CodexErr;
 use codex_protocol::error::Result as CodexResult;
 use codex_protocol::items::ContextCompactionItem;
@@ -239,7 +238,6 @@ async fn run_compact_task_inner_impl(
         let turn_input_len = turn_input.len();
         let prompt = Prompt {
             input: turn_input,
-            include_item_ids: turn_context.config.features.enabled(Feature::ItemIds),
             base_instructions: sess.get_base_instructions().await,
             ..Default::default()
         };
diff --git a/codex-rs/core/src/compact_remote.rs b/codex-rs/core/src/compact_remote.rs
index 115106b936b3..c88d8062d602 100644
--- a/codex-rs/core/src/compact_remote.rs
+++ b/codex-rs/core/src/compact_remote.rs
@@ -23,7 +23,6 @@ use codex_analytics::CompactionPhase;
 use codex_analytics::CompactionReason;
 use codex_analytics::CompactionTrigger;
 use codex_app_server_protocol::AuthMode;
-use codex_features::Feature;
 use codex_protocol::error::CodexErr;
 use codex_protocol::error::Result as CodexResult;
 use codex_protocol::items::ContextCompactionItem;
@@ -226,7 +225,6 @@ async fn run_remote_compact_task_inner_impl(
     .await?;
     let prompt = Prompt {
         input: prompt_input,
-        include_item_ids: turn_context.config.features.enabled(Feature::ItemIds),
         tools: tool_router.model_visible_specs(),
         parallel_tool_calls: turn_context.model_info.supports_parallel_tool_calls,
         base_instructions,
diff --git a/codex-rs/core/src/compact_remote_v2.rs b/codex-rs/core/src/compact_remote_v2.rs
index 864a814ef626..3b79789c02ba 100644
--- a/codex-rs/core/src/compact_remote_v2.rs
+++ b/codex-rs/core/src/compact_remote_v2.rs
@@ -27,7 +27,6 @@ use codex_analytics::CompactionImplementation;
 use codex_analytics::CompactionPhase;
 use codex_analytics::CompactionReason;
 use codex_analytics::CompactionTrigger;
-use codex_features::Feature;
 use codex_protocol::error::CodexErr;
 use codex_protocol::error::Result as CodexResult;
 use codex_protocol::items::ContextCompactionItem;
@@ -235,7 +234,6 @@ async fn run_remote_compact_task_inner_impl(
     input.push(ResponseItem::CompactionTrigger { metadata: None });
     let prompt = Prompt {
         input,
-        include_item_ids: turn_context.config.features.enabled(Feature::ItemIds),
         tools: tool_router.model_visible_specs(),
         parallel_tool_calls: turn_context.model_info.supports_parallel_tool_calls,
         base_instructions,
diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs
index 31b5671231fc..4beef792bb30 100644
--- a/codex-rs/core/src/session/turn.rs
+++ b/codex-rs/core/src/session/turn.rs
@@ -1026,7 +1026,6 @@ pub(crate) fn build_prompt(
 ) -> Prompt {
     Prompt {
         input,
-        include_item_ids: turn_context.config.features.enabled(Feature::ItemIds),
         tools: router.model_visible_specs(),
         parallel_tool_calls: turn_context.model_info.supports_parallel_tool_calls,
         base_instructions,
diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs
index 95a285015197..8e808c76c7e7 100644
--- a/codex-rs/core/tests/suite/client.rs
+++ b/codex-rs/core/tests/suite/client.rs
@@ -254,38 +254,6 @@ async fn non_openai_responses_requests_omit_item_turn_metadata() {
     }
 }
 
-#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
-async fn response_item_ids_are_omitted_when_feature_is_disabled() -> anyhow::Result<()> {
-    let server = MockServer::start().await;
-    let response_mock = mount_sse_sequence(
-        &server,
-        vec![
-            sse(vec![
-                ev_response_created("resp-1"),
-                ev_assistant_message("msg_server", "first reply"),
-                ev_completed("resp-1"),
-            ]),
-            sse(vec![ev_response_created("resp-2"), ev_completed("resp-2")]),
-        ],
-    )
-    .await;
-    let test = test_codex().build(&server).await?;
-
-    test.submit_turn("first turn").await?;
-    test.submit_turn("second turn").await?;
-
-    let requests = response_mock.requests();
-    assert_eq!(requests.len(), 2);
-    for item in requests[1].input() {
-        assert!(
-            item.get("id").is_none(),
-            "input item should omit IDs while the feature is disabled: {item}"
-        );
-    }
-
-    Ok(())
-}
-
 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
 async fn response_item_ids_persist_across_resume_and_preserve_server_ids() -> anyhow::Result<()> {
     let server = MockServer::start().await;
@@ -319,9 +287,6 @@ async fn response_item_ids_persist_across_resume_and_preserve_server_ids() -> an
     })
     .await;
 
-    builder = builder.with_config(|config| {
-        let _ = config.features.enable(Feature::ItemIds);
-    });
     let resumed = builder.resume(&server, home, rollout_path).await?;
     resumed.submit_turn("after resume").await?;
 
@@ -394,9 +359,6 @@ async fn inter_agent_response_item_ids_persist_across_resume() -> anyhow::Result
     })
     .await;
 
-    builder = builder.with_config(|config| {
-        let _ = config.features.enable(Feature::ItemIds);
-    });
     let resumed = builder.resume(&server, home, rollout_path).await?;
     resumed.submit_turn("after resume").await?;
 
diff --git a/codex-rs/core/tests/suite/client_websockets.rs b/codex-rs/core/tests/suite/client_websockets.rs
index 223477034975..cee54e24b310 100755
--- a/codex-rs/core/tests/suite/client_websockets.rs
+++ b/codex-rs/core/tests/suite/client_websockets.rs
@@ -1513,35 +1513,6 @@ async fn responses_websocket_connection_limit_error_reconnects_and_completes() {
     server.shutdown().await;
 }
 
-#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
-async fn responses_websocket_gates_item_ids() {
-    skip_if_no_network!();
-
-    let server = start_websocket_server(vec![vec![
-        vec![ev_response_created("resp-1"), ev_completed("resp-1")],
-        vec![ev_response_created("resp-2"), ev_completed("resp-2")],
-    ]])
-    .await;
-
-    let harness = websocket_harness(&server).await;
-    let mut client_session = harness.client.new_session();
-    let prompt_without_ids = prompt_with_input(vec![assistant_message_item("msg-1", "first")]);
-    let mut prompt_with_ids = prompt_with_input(vec![assistant_message_item("msg-2", "second")]);
-    prompt_with_ids.include_item_ids = true;
-
-    stream_until_complete(&mut client_session, &harness, &prompt_without_ids).await;
-    stream_until_complete(&mut client_session, &harness, &prompt_with_ids).await;
-
-    let connection = server.single_connection();
-    assert_eq!(connection.len(), 2);
-    let first = connection.first().expect("missing request").body_json();
-    let second = connection.get(1).expect("missing request").body_json();
-    assert_eq!(first["input"][0].get("id"), None);
-    assert_eq!(second["input"][0]["id"].as_str(), Some("msg-2"));
-
-    server.shutdown().await;
-}
-
 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
 async fn responses_websocket_uses_incremental_create_on_prefix() {
     skip_if_no_network!();
diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs
index 194a1ceef9ff..ead2d5330b14 100644
--- a/codex-rs/protocol/src/models.rs
+++ b/codex-rs/protocol/src/models.rs
@@ -1189,28 +1189,6 @@ impl ResponseItem {
         }
     }
 
-    /// Clears IDs whose serialization is controlled by the `item_ids` feature.
-    ///
-    /// Image generation call IDs predate that feature and remain serialized for compatibility.
-    pub fn clear_feature_gated_id(&mut self) {
-        match self {
-            Self::Message { id, .. }
-            | Self::AgentMessage { id, .. }
-            | Self::LocalShellCall { id, .. }
-            | Self::FunctionCall { id, .. }
-            | Self::ToolSearchCall { id, .. }
-            | Self::FunctionCallOutput { id, .. }
-            | Self::CustomToolCall { id, .. }
-            | Self::CustomToolCallOutput { id, .. }
-            | Self::ToolSearchOutput { id, .. }
-            | Self::WebSearchCall { id, .. }
-            | Self::Reasoning { id, .. }
-            | Self::Compaction { id, .. }
-            | Self::ContextCompaction { id, .. } => *id = None,
-            Self::ImageGenerationCall { .. } | Self::CompactionTrigger { .. } | Self::Other => {}
-        }
-    }
-
     /// Returns the non-empty turn ID stamped onto this item, if present.
     pub fn turn_id(&self) -> Option<&str> {
         self.metadata()

From 2ea01cc1a36034c6b24b6788c11a51922448a216 Mon Sep 17 00:00:00 2001
From: pakrym-oai <pakrym@openai.com>
Date: Thu, 18 Jun 2026 11:50:34 -0700
Subject: [PATCH 05/14] codex: update item ID fixtures (#28814)

---
 codex-rs/core/tests/suite/client.rs                            | 2 ++
 codex-rs/core/tests/suite/compact.rs                           | 3 +++
 ..._manual_compact_api_auth_prompt_cache_key_request_diff.snap | 2 ++
 ...hatgpt_auth_service_tier_prompt_cache_key_request_diff.snap | 2 ++
 4 files changed, 9 insertions(+)

diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs
index 8e808c76c7e7..5289117c6d79 100644
--- a/codex-rs/core/tests/suite/client.rs
+++ b/codex-rs/core/tests/suite/client.rs
@@ -3477,6 +3477,7 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() {
         },
         {
             "type": "message",
+            "id": "msg-1",
             "role": "assistant",
             "content": [{"type":"output_text","text":"Hey there!\n"}]
         },
@@ -3487,6 +3488,7 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() {
         },
         {
             "type": "message",
+            "id": "msg-1",
             "role": "assistant",
             "content": [{"type":"output_text","text":"Hey there!\n"}]
         },
diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs
index 3b709d54a206..bf268cee9412 100644
--- a/codex-rs/core/tests/suite/compact.rs
+++ b/codex-rs/core/tests/suite/compact.rs
@@ -1305,6 +1305,7 @@ async fn multiple_auto_compact_per_task_runs_after_token_limit_hit() {
       {
         "content": null,
         "encrypted_content": encrypted_content_1,
+        "id": "m1",
         "summary": [
           {
             "text": "I will create a react app",
@@ -1405,6 +1406,7 @@ async fn multiple_auto_compact_per_task_runs_after_token_limit_hit() {
       {
         "content": null,
         "encrypted_content": encrypted_content_2,
+        "id": "m3",
         "summary": [
           {
             "text": "I will create a node app",
@@ -1505,6 +1507,7 @@ async fn multiple_auto_compact_per_task_runs_after_token_limit_hit() {
       {
         "content": null,
         "encrypted_content": encrypted_content_3,
+        "id": "m6",
         "summary": [
           {
             "text": "I will create a python app",
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_api_auth_prompt_cache_key_request_diff.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_api_auth_prompt_cache_key_request_diff.snap
index 0df96a7c064b..748ac7914863 100644
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_api_auth_prompt_cache_key_request_diff.snap
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_api_auth_prompt_cache_key_request_diff.snap
@@ -26,6 +26,7 @@ Scenario: After five varied API-key-auth turns, remote manual compaction omits s
 +        }
 +      ],
 +      "encrypted_content": "YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYnR1cm4gZml2ZSByYXcgY29udGVudA==",
++      "id": "turn-five-reasoning",
 +      "summary": [
 +        {
 +          "text": "TURN_FIVE_REASONING",
@@ -41,6 +42,7 @@ Scenario: After five varied API-key-auth turns, remote manual compaction omits s
 +          "type": "output_text"
 +        }
 +      ],
++      "id": "turn-five-assistant",
 +      "role": "assistant",
 +      "type": "message"
 -  "service_tier": "priority",
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_chatgpt_auth_service_tier_prompt_cache_key_request_diff.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_chatgpt_auth_service_tier_prompt_cache_key_request_diff.snap
index d959d526cc92..f997de046652 100644
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_chatgpt_auth_service_tier_prompt_cache_key_request_diff.snap
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_chatgpt_auth_service_tier_prompt_cache_key_request_diff.snap
@@ -26,6 +26,7 @@ Scenario: After five varied ChatGPT-auth turns, remote manual compaction reuses
 +        }
 +      ],
 +      "encrypted_content": "YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYnR1cm4gZml2ZSByYXcgY29udGVudA==",
++      "id": "turn-five-reasoning",
 +      "summary": [
 +        {
 +          "text": "TURN_FIVE_REASONING",
@@ -41,6 +42,7 @@ Scenario: After five varied ChatGPT-auth turns, remote manual compaction reuses
 +          "type": "output_text"
 +        }
 +      ],
++      "id": "turn-five-assistant",
 +      "role": "assistant",
 +      "type": "message"
 -  "store": false,

From 20d146f59535391ca5e9b13d4f0c4b936d1039d5 Mon Sep 17 00:00:00 2001
From: pakrym-oai <pakrym@openai.com>
Date: Thu, 18 Jun 2026 11:56:54 -0700
Subject: [PATCH 06/14] codex: defer inter-agent item IDs (#28814)

---
 codex-rs/core/src/session/mod.rs    |  3 +-
 codex-rs/core/tests/suite/client.rs | 83 -----------------------------
 codex-rs/protocol/src/protocol.rs   |  8 +--
 3 files changed, 2 insertions(+), 92 deletions(-)

diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs
index f91ccbd09402..e9fa300c1f8e 100644
--- a/codex-rs/core/src/session/mod.rs
+++ b/codex-rs/core/src/session/mod.rs
@@ -2717,7 +2717,7 @@ impl Session {
     pub(crate) async fn record_inter_agent_communication(
         &self,
         turn_context: &TurnContext,
-        mut communication: InterAgentCommunication,
+        communication: InterAgentCommunication,
     ) {
         let response_item = communication.to_model_input_item();
         let items = self.prepare_conversation_items_for_history(
@@ -2725,7 +2725,6 @@ impl Session {
             std::slice::from_ref(&response_item),
         );
         let items = items.as_ref();
-        communication.id = items.first().and_then(ResponseItem::id).map(str::to_owned);
         {
             let mut state = self.state.lock().await;
             state.record_items(
diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs
index 5289117c6d79..ae8c6c1ebe3e 100644
--- a/codex-rs/core/tests/suite/client.rs
+++ b/codex-rs/core/tests/suite/client.rs
@@ -19,7 +19,6 @@ use codex_model_provider_info::built_in_model_providers;
 use codex_models_manager::bundled_models_response;
 use codex_otel::SessionTelemetry;
 use codex_otel::TelemetryAuthMode;
-use codex_protocol::AgentPath;
 use codex_protocol::ThreadId;
 use codex_protocol::config_types::CollaborationMode;
 use codex_protocol::config_types::ModeKind;
@@ -43,7 +42,6 @@ use codex_protocol::models::ResponseItem;
 use codex_protocol::models::WebSearchAction;
 use codex_protocol::openai_models::ReasoningEffort;
 use codex_protocol::protocol::EventMsg;
-use codex_protocol::protocol::InterAgentCommunication;
 use codex_protocol::protocol::Op;
 use codex_protocol::protocol::RolloutItem;
 use codex_protocol::protocol::RolloutLine;
@@ -152,19 +150,6 @@ fn response_message_item_id(request: &ResponsesRequest, role: &str, text: &str)
         .unwrap_or_else(|| panic!("missing item ID for {role} message {text:?}"))
 }
 
-fn response_agent_message_item_id(request: &ResponsesRequest, text: &str) -> String {
-    request
-        .inputs_of_type("agent_message")
-        .into_iter()
-        .find(|item| message_input_texts(item).contains(&text))
-        .and_then(|item| {
-            item.get("id")
-                .and_then(serde_json::Value::as_str)
-                .map(str::to_string)
-        })
-        .unwrap_or_else(|| panic!("missing item ID for agent message {text:?}"))
-}
-
 fn assert_codex_client_metadata(
     request_body: &serde_json::Value,
     installation_id: &str,
@@ -312,74 +297,6 @@ async fn response_item_ids_persist_across_resume_and_preserve_server_ids() -> an
     Ok(())
 }
 
-#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
-async fn inter_agent_response_item_ids_persist_across_resume() -> anyhow::Result<()> {
-    let server = MockServer::start().await;
-    let response_mock = mount_sse_sequence(
-        &server,
-        vec![
-            sse(vec![
-                ev_response_created("resp-mail"),
-                ev_completed("resp-mail"),
-            ]),
-            sse(vec![ev_response_created("resp-2"), ev_completed("resp-2")]),
-        ],
-    )
-    .await;
-    let mut builder = test_codex().with_config(|config| {
-        let _ = config.features.enable(Feature::ItemIds);
-    });
-    let initial = builder.build(&server).await?;
-    let home = Arc::clone(&initial.home);
-    let rollout_path = initial
-        .session_configured
-        .rollout_path
-        .clone()
-        .expect("rollout path");
-
-    initial
-        .codex
-        .submit(Op::InterAgentCommunication {
-            communication: InterAgentCommunication::new(
-                AgentPath::try_from("/root/worker").expect("worker path should parse"),
-                AgentPath::root(),
-                Vec::new(),
-                "child update".to_string(),
-                /*trigger_turn*/ true,
-            ),
-        })
-        .await?;
-    wait_for_event(&initial.codex, |event| {
-        matches!(event, EventMsg::TurnComplete(_))
-    })
-    .await;
-    initial.codex.submit(Op::Shutdown).await?;
-    wait_for_event(&initial.codex, |event| {
-        matches!(event, EventMsg::ShutdownComplete)
-    })
-    .await;
-
-    let resumed = builder.resume(&server, home, rollout_path).await?;
-    resumed.submit_turn("after resume").await?;
-
-    let requests = response_mock.requests();
-    assert_eq!(requests.len(), 2);
-    let agent_message_id = response_agent_message_item_id(&requests[0], "child update");
-    let agent_message_uuid = agent_message_id
-        .strip_prefix("amsg_")
-        .expect("agent message ID should have the Responses API prefix");
-    assert_eq!(
-        Uuid::parse_str(agent_message_uuid)?.get_version(),
-        Some(uuid::Version::SortRand)
-    );
-    assert_eq!(
-        response_agent_message_item_id(&requests[1], "child update"),
-        agent_message_id
-    );
-
-    Ok(())
-}
-
 /// Writes an `auth.json` into the provided `codex_home` with the specified parameters.
 /// Returns the fake JWT string written to `tokens.id_token`.
 #[expect(clippy::unwrap_used)]
diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs
index 341f56049e9c..a0e5b24d4704 100644
--- a/codex-rs/protocol/src/protocol.rs
+++ b/codex-rs/protocol/src/protocol.rs
@@ -676,9 +676,6 @@ impl From<Vec<UserInput>> for Op {
 
 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)]
 pub struct InterAgentCommunication {
-    #[serde(default, skip_serializing_if = "Option::is_none")]
-    #[ts(optional)]
-    pub id: Option<String>,
     pub author: AgentPath,
     pub recipient: AgentPath,
     #[serde(default)]
@@ -702,7 +699,6 @@ impl InterAgentCommunication {
         trigger_turn: bool,
     ) -> Self {
         Self {
-            id: None,
             author,
             recipient,
             other_recipients,
@@ -721,7 +717,6 @@ impl InterAgentCommunication {
         trigger_turn: bool,
     ) -> Self {
         Self {
-            id: None,
             author,
             recipient,
             other_recipients,
@@ -767,7 +762,7 @@ impl InterAgentCommunication {
             }],
         };
         ResponseItem::AgentMessage {
-            id: self.id.clone(),
+            id: None,
             author: self.author.to_string(),
             recipient: self.recipient.to_string(),
             content,
@@ -4256,7 +4251,6 @@ mod tests {
     #[test]
     fn inter_agent_communication_response_input_item_preserves_commentary_phase() {
         let communication = InterAgentCommunication {
-            id: None,
             author: AgentPath::root(),
             recipient: AgentPath::root().join("reviewer").expect("recipient path"),
             other_recipients: vec![AgentPath::root().join("worker").expect("recipient path")],

From 92d942d981879648f9a02c1c7afbed3828bbdc05 Mon Sep 17 00:00:00 2001
From: pakrym-oai <pakrym@openai.com>
Date: Thu, 18 Jun 2026 12:25:35 -0700
Subject: [PATCH 07/14] Gate response item IDs in outbound requests

---
 codex-rs/codex-api/src/endpoint/compact.rs    |  7 +-
 codex-rs/codex-api/src/endpoint/responses.rs  | 19 ++---
 .../src/endpoint/responses_websocket.rs       | 57 +++++++++++--
 codex-rs/codex-api/src/endpoint/search.rs     |  9 +-
 codex-rs/codex-api/src/requests/mod.rs        |  2 +-
 codex-rs/codex-api/src/requests/responses.rs  | 82 ++++++++++++++-----
 codex-rs/codex-api/tests/clients.rs           | 59 +++++++++++--
 codex-rs/core/src/client.rs                   |  6 ++
 codex-rs/core/src/client_tests.rs             |  2 +
 codex-rs/core/src/session/session.rs          |  1 +
 codex-rs/core/src/session/tests.rs            |  3 +
 codex-rs/core/tests/responses_headers.rs      |  3 +
 codex-rs/core/tests/suite/client.rs           |  6 ++
 .../core/tests/suite/client_websockets.rs     |  1 +
 ...pi_auth_prompt_cache_key_request_diff.snap |  2 -
 ...ce_tier_prompt_cache_key_request_diff.snap |  2 -
 16 files changed, 211 insertions(+), 50 deletions(-)

diff --git a/codex-rs/codex-api/src/endpoint/compact.rs b/codex-rs/codex-api/src/endpoint/compact.rs
index c8730235fd5f..b2c9bc5b433b 100644
--- a/codex-rs/codex-api/src/endpoint/compact.rs
+++ b/codex-rs/codex-api/src/endpoint/compact.rs
@@ -3,6 +3,7 @@ use crate::common::CompactionInput;
 use crate::endpoint::session::EndpointSession;
 use crate::error::ApiError;
 use crate::provider::Provider;
+use crate::requests::strip_response_item_ids;
 use codex_client::HttpTransport;
 use codex_client::RequestTelemetry;
 use codex_protocol::models::ResponseItem;
@@ -75,9 +76,13 @@ impl<T: HttpTransport> CompactClient<T> {
         extra_headers: HeaderMap,
         request_timeout: Duration,
         turn_state: Option<&OnceLock<String>>,
+        include_item_ids: bool,
     ) -> Result<Vec<ResponseItem>, ApiError> {
-        let body = to_value(input)
+        let mut body = to_value(input)
             .map_err(|e| ApiError::Stream(format!("failed to encode compaction input: {e}")))?;
+        if !include_item_ids {
+            strip_response_item_ids(&mut body);
+        }
         self.compact(body, extra_headers, request_timeout, turn_state)
             .await
     }
diff --git a/codex-rs/codex-api/src/endpoint/responses.rs b/codex-rs/codex-api/src/endpoint/responses.rs
index ad79adf33dff..399a84f8b410 100644
--- a/codex-rs/codex-api/src/endpoint/responses.rs
+++ b/codex-rs/codex-api/src/endpoint/responses.rs
@@ -5,10 +5,10 @@ use crate::endpoint::session::EndpointSession;
 use crate::error::ApiError;
 use crate::provider::Provider;
 use crate::requests::Compression;
-use crate::requests::attach_item_ids;
 use crate::requests::headers::build_session_headers;
 use crate::requests::headers::insert_header;
 use crate::requests::headers::subagent_header;
+use crate::requests::strip_response_item_ids;
 use crate::sse::spawn_response_stream;
 use crate::telemetry::SseTelemetry;
 use codex_client::EncodedJsonBody;
@@ -37,6 +37,7 @@ pub struct ResponsesOptions {
     pub extra_headers: HeaderMap,
     pub compression: Compression,
     pub turn_state: Option<Arc<OnceLock<String>>>,
+    pub include_item_ids: bool,
 }
 
 impl<T: HttpTransport> ResponsesClient<T> {
@@ -80,18 +81,16 @@ impl<T: HttpTransport> ResponsesClient<T> {
             extra_headers,
             compression,
             turn_state,
+            include_item_ids,
         } = options;
 
-        let body = if request.store && self.session.provider().is_azure_responses_endpoint() {
-            let mut body = serde_json::to_value(&request).map_err(|e| {
-                ApiError::Stream(format!("failed to encode responses request: {e}"))
-            })?;
-            attach_item_ids(&mut body, &request.input);
-            EncodedJsonBody::encode(&body)
-        } else {
-            EncodedJsonBody::encode(&request)
+        let mut body = serde_json::to_value(&request)
+            .map_err(|e| ApiError::Stream(format!("failed to encode responses request: {e}")))?;
+        if !include_item_ids {
+            strip_response_item_ids(&mut body);
         }
-        .map_err(|e| ApiError::Stream(format!("failed to encode responses request: {e}")))?;
+        let body = EncodedJsonBody::encode(&body)
+            .map_err(|e| ApiError::Stream(format!("failed to encode responses request: {e}")))?;
 
         let mut headers = extra_headers;
         if let Some(ref thread_id) = thread_id {
diff --git a/codex-rs/codex-api/src/endpoint/responses_websocket.rs b/codex-rs/codex-api/src/endpoint/responses_websocket.rs
index 4ea564f139aa..cba9d09202f2 100644
--- a/codex-rs/codex-api/src/endpoint/responses_websocket.rs
+++ b/codex-rs/codex-api/src/endpoint/responses_websocket.rs
@@ -5,6 +5,7 @@ use crate::common::ResponsesWsRequest;
 use crate::error::ApiError;
 use crate::provider::Provider;
 use crate::rate_limits::parse_rate_limit_event;
+use crate::requests::strip_response_item_ids;
 use crate::sse::ResponsesStreamEvent;
 use crate::sse::process_responses_event;
 use crate::telemetry::WebsocketTelemetry;
@@ -216,6 +217,7 @@ impl ResponsesWebsocketConnection {
         request: ResponsesWsRequest,
         connection_reused: bool,
         turn_state: Option<Arc<OnceLock<String>>>,
+        include_item_ids: bool,
     ) -> Result<ResponseStream, ApiError> {
         let (tx_event, rx_event) =
             mpsc::channel::<std::result::Result<ResponseEvent, ApiError>>(1600);
@@ -225,7 +227,7 @@ impl ResponsesWebsocketConnection {
         let models_etag = self.models_etag.clone();
         let server_model = self.server_model.clone();
         let telemetry = self.telemetry.clone();
-        let request_text = serialize_websocket_request(&request)?;
+        let request_text = serialize_websocket_request(&request, include_item_ids)?;
 
         let current_span = Span::current();
         tokio::spawn(
@@ -787,8 +789,16 @@ async fn send_websocket_request(
     Ok(())
 }
 
-fn serialize_websocket_request(request: &ResponsesWsRequest) -> Result<String, ApiError> {
-    serde_json::to_string(request)
+fn serialize_websocket_request(
+    request: &ResponsesWsRequest,
+    include_item_ids: bool,
+) -> Result<String, ApiError> {
+    let mut payload = serde_json::to_value(request)
+        .map_err(|err| ApiError::Stream(format!("failed to encode websocket request: {err}")))?;
+    if !include_item_ids {
+        strip_response_item_ids(&mut payload);
+    }
+    serde_json::to_string(&payload)
         .map_err(|err| ApiError::Stream(format!("failed to encode websocket request: {err}")))
 }
 
@@ -839,14 +849,51 @@ mod tests {
         });
 
         let previous_payload = serde_json::to_value(&request).expect("serialize previous payload");
-        let request_text =
-            serialize_websocket_request(&request).expect("serialize websocket request");
+        let request_text = serialize_websocket_request(&request, /*include_item_ids*/ true)
+            .expect("serialize websocket request");
         let wire_payload =
             serde_json::from_str::<Value>(&request_text).expect("parse websocket request");
 
         assert_eq!(wire_payload, previous_payload);
     }
 
+    #[test]
+    fn websocket_serialization_strips_item_ids_by_default() {
+        let request = ResponsesWsRequest::ResponseCreate(ResponseCreateWsRequest {
+            model: "gpt-test".to_string(),
+            instructions: "Use the available tools.".to_string(),
+            previous_response_id: None,
+            input: vec![ResponseItem::Message {
+                id: Some("msg-1".to_string()),
+                role: "user".to_string(),
+                content: vec![ContentItem::InputText {
+                    text: "hello".to_string(),
+                }],
+                phase: None,
+                metadata: None,
+            }],
+            tools: Vec::new(),
+            tool_choice: "auto".to_string(),
+            parallel_tool_calls: true,
+            reasoning: None,
+            store: false,
+            stream: true,
+            include: Vec::new(),
+            service_tier: None,
+            prompt_cache_key: None,
+            text: None,
+            generate: None,
+            client_metadata: None,
+        });
+
+        let request_text = serialize_websocket_request(&request, /*include_item_ids*/ false)
+            .expect("serialize websocket request");
+        let wire_payload =
+            serde_json::from_str::<Value>(&request_text).expect("parse websocket request");
+
+        assert_eq!(wire_payload["input"][0].get("id"), None);
+    }
+
     #[test]
     fn websocket_config_enables_permessage_deflate() {
         let config = websocket_config();
diff --git a/codex-rs/codex-api/src/endpoint/search.rs b/codex-rs/codex-api/src/endpoint/search.rs
index 1c231d6bef85..abf06777b10b 100644
--- a/codex-rs/codex-api/src/endpoint/search.rs
+++ b/codex-rs/codex-api/src/endpoint/search.rs
@@ -2,6 +2,8 @@ use crate::auth::SharedAuthProvider;
 use crate::endpoint::session::EndpointSession;
 use crate::error::ApiError;
 use crate::provider::Provider;
+use crate::requests::strip_response_item_ids;
+use crate::search::SearchInput;
 use crate::search::SearchRequest;
 use crate::search::SearchResponse;
 use codex_client::HttpTransport;
@@ -37,8 +39,11 @@ impl<T: HttpTransport> SearchClient<T> {
         request: &SearchRequest,
         extra_headers: HeaderMap,
     ) -> Result<SearchResponse, ApiError> {
-        let body = to_value(request)
+        let mut body = to_value(request)
             .map_err(|e| ApiError::Stream(format!("failed to encode search request: {e}")))?;
+        if matches!(&request.input, Some(SearchInput::Items(_))) {
+            strip_response_item_ids(&mut body);
+        }
         let resp = self
             .session
             .execute(Method::POST, Self::path(), extra_headers, Some(body))
@@ -149,7 +154,7 @@ mod tests {
                     model: "gpt-test".to_string(),
                     reasoning: None,
                     input: Some(SearchInput::Items(vec![ResponseItem::Message {
-                        id: None,
+                        id: Some("msg_search".to_string()),
                         role: "user".to_string(),
                         content: vec![
                             ContentItem::InputText {
diff --git a/codex-rs/codex-api/src/requests/mod.rs b/codex-rs/codex-api/src/requests/mod.rs
index 1c357b2a613b..37dd1304db77 100644
--- a/codex-rs/codex-api/src/requests/mod.rs
+++ b/codex-rs/codex-api/src/requests/mod.rs
@@ -2,4 +2,4 @@ pub(crate) mod headers;
 pub(crate) mod responses;
 
 pub use responses::Compression;
-pub(crate) use responses::attach_item_ids;
+pub(crate) use responses::strip_response_item_ids;
diff --git a/codex-rs/codex-api/src/requests/responses.rs b/codex-rs/codex-api/src/requests/responses.rs
index 836f525c49b7..3ac618aafe13 100644
--- a/codex-rs/codex-api/src/requests/responses.rs
+++ b/codex-rs/codex-api/src/requests/responses.rs
@@ -1,4 +1,3 @@
-use codex_protocol::models::ResponseItem;
 use serde_json::Value;
 
 #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
@@ -8,30 +7,69 @@ pub enum Compression {
     Zstd,
 }
 
-pub(crate) fn attach_item_ids(payload_json: &mut Value, original_items: &[ResponseItem]) {
-    let Some(input_value) = payload_json.get_mut("input") else {
-        return;
-    };
-    let Value::Array(items) = input_value else {
+pub(crate) fn strip_response_item_ids(payload_json: &mut Value) {
+    let Some(Value::Array(items)) = payload_json.get_mut("input") else {
         return;
     };
 
-    for (value, item) in items.iter_mut().zip(original_items.iter()) {
-        if let ResponseItem::Reasoning { id: Some(id), .. }
-        | ResponseItem::Message { id: Some(id), .. }
-        | ResponseItem::WebSearchCall { id: Some(id), .. }
-        | ResponseItem::FunctionCall { id: Some(id), .. }
-        | ResponseItem::ToolSearchCall { id: Some(id), .. }
-        | ResponseItem::LocalShellCall { id: Some(id), .. }
-        | ResponseItem::CustomToolCall { id: Some(id), .. } = item
-        {
-            if id.is_empty() {
-                continue;
-            }
-
-            if let Some(obj) = value.as_object_mut() {
-                obj.insert("id".to_string(), Value::String(id.clone()));
-            }
+    for item in items {
+        if let Value::Object(object) = item {
+            object.remove("id");
         }
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use pretty_assertions::assert_eq;
+    use serde_json::json;
+
+    #[test]
+    fn strip_response_item_ids_removes_ids_from_input_items() {
+        let mut payload = json!({
+            "model": "gpt-test",
+            "id": "request-id",
+            "input": [
+                {
+                    "type": "message",
+                    "id": "msg_1",
+                    "content": [
+                        {"type": "input_text", "text": "hello"},
+                        {"type": "input_image", "id": "img_1", "image_url": "https://example.com/image.png"}
+                    ]
+                },
+                {"type": "function_call_output", "id": "fco_1", "call_id": "call_1", "output": "done"}
+            ]
+        });
+
+        strip_response_item_ids(&mut payload);
+
+        assert_eq!(
+            payload,
+            json!({
+                "model": "gpt-test",
+                "id": "request-id",
+                "input": [
+                    {
+                        "type": "message",
+                        "content": [
+                            {"type": "input_text", "text": "hello"},
+                            {"type": "input_image", "id": "img_1", "image_url": "https://example.com/image.png"}
+                        ]
+                    },
+                    {"type": "function_call_output", "call_id": "call_1", "output": "done"}
+                ]
+            })
+        );
+    }
+
+    #[test]
+    fn strip_response_item_ids_ignores_missing_input() {
+        let mut payload = json!({"id": "request-id"});
+
+        strip_response_item_ids(&mut payload);
+
+        assert_eq!(payload, json!({"id": "request-id"}));
+    }
+}
diff --git a/codex-rs/codex-api/tests/clients.rs b/codex-rs/codex-api/tests/clients.rs
index d8489ed7487f..625dd348baa3 100644
--- a/codex-rs/codex-api/tests/clients.rs
+++ b/codex-rs/codex-api/tests/clients.rs
@@ -301,7 +301,7 @@ async fn responses_client_uses_responses_path() -> Result<()> {
 }
 
 #[tokio::test]
-async fn responses_client_stream_request_preserves_exact_json_body() -> Result<()> {
+async fn responses_client_stream_request_preserves_item_ids_when_enabled() -> Result<()> {
     let state = RecordingState::default();
     let transport = RecordingTransport::new(state.clone());
     let client = ResponsesClient::new(transport, provider("openai"), Arc::new(NoAuth));
@@ -327,10 +327,16 @@ async fn responses_client_stream_request_preserves_exact_json_body() -> Result<(
         text: None,
         client_metadata: None,
     };
-    let expected = serde_json::to_vec(&request)?;
+    let expected = serde_json::to_value(&request)?;
 
     let _stream = client
-        .stream_request(request, ResponsesOptions::default())
+        .stream_request(
+            request,
+            ResponsesOptions {
+                include_item_ids: true,
+                ..Default::default()
+            },
+        )
         .await?;
 
     let requests = state.take_stream_requests();
@@ -338,7 +344,10 @@ async fn responses_client_stream_request_preserves_exact_json_body() -> Result<(
     let prepared = requests[0]
         .prepare_body_for_send()
         .expect("body should prepare");
-    assert_eq!(prepared.body.as_deref(), Some(expected.as_slice()));
+    let body: serde_json::Value =
+        serde_json::from_slice(prepared.body.as_deref().expect("body should be JSON"))?;
+    assert_eq!(body, expected);
+    assert_eq!(body["input"][0]["id"], "msg_1");
     assert_eq!(
         prepared.headers.get(http::header::CONTENT_TYPE),
         Some(&HeaderValue::from_static("application/json"))
@@ -346,6 +355,45 @@ async fn responses_client_stream_request_preserves_exact_json_body() -> Result<(
     Ok(())
 }
 
+#[tokio::test]
+async fn responses_client_stream_request_strips_item_ids_by_default() -> Result<()> {
+    let state = RecordingState::default();
+    let transport = RecordingTransport::new(state.clone());
+    let client = ResponsesClient::new(transport, provider("openai"), Arc::new(NoAuth));
+    let request = ResponsesApiRequest {
+        model: "gpt-test".into(),
+        instructions: "Say hi".into(),
+        input: vec![ResponseItem::Message {
+            id: Some("msg_1".into()),
+            role: "user".into(),
+            content: vec![ContentItem::InputText { text: "hi".into() }],
+            phase: None,
+            metadata: None,
+        }],
+        tools: Vec::new(),
+        tool_choice: "auto".into(),
+        parallel_tool_calls: false,
+        reasoning: None,
+        store: false,
+        stream: true,
+        include: Vec::new(),
+        service_tier: None,
+        prompt_cache_key: None,
+        text: None,
+        client_metadata: None,
+    };
+
+    let _stream = client
+        .stream_request(request, ResponsesOptions::default())
+        .await?;
+
+    let requests = state.take_stream_requests();
+    assert_eq!(requests.len(), 1);
+    let body: serde_json::Value = serde_json::from_slice(request_body_bytes(&requests[0]))?;
+    assert_eq!(body["input"][0].get("id"), None);
+    Ok(())
+}
+
 #[tokio::test]
 async fn streaming_client_adds_auth_headers() -> Result<()> {
     let state = RecordingState::default();
@@ -502,7 +550,7 @@ async fn streaming_client_does_not_retry_auth_build_error() -> Result<()> {
 }
 
 #[tokio::test]
-async fn azure_default_store_attaches_ids_and_headers() -> Result<()> {
+async fn azure_store_sends_ids_when_enabled_and_headers() -> Result<()> {
     let state = RecordingState::default();
     let transport = RecordingTransport::new(state.clone());
     let client = ResponsesClient::new(transport, provider("azure"), Arc::new(NoAuth));
@@ -542,6 +590,7 @@ async fn azure_default_store_attaches_ids_and_headers() -> Result<()> {
                 extra_headers,
                 compression: Compression::None,
                 turn_state: None,
+                include_item_ids: true,
             },
         )
         .await?;
diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs
index 4ea8c0ff1fb8..417634e2359d 100644
--- a/codex-rs/core/src/client.rs
+++ b/codex-rs/core/src/client.rs
@@ -178,6 +178,7 @@ struct ModelClientState {
     enable_request_compression: bool,
     include_timing_metrics: bool,
     beta_features_header: Option<String>,
+    item_ids_enabled: bool,
     include_attestation: bool,
     attestation_provider: Option<Arc<dyn AttestationProvider>>,
     disable_websockets: AtomicBool,
@@ -378,6 +379,7 @@ impl ModelClient {
         enable_request_compression: bool,
         include_timing_metrics: bool,
         beta_features_header: Option<String>,
+        item_ids_enabled: bool,
         attestation_provider: Option<Arc<dyn AttestationProvider>>,
     ) -> Self {
         let model_provider = create_model_provider(provider_info, auth_manager);
@@ -398,6 +400,7 @@ impl ModelClient {
                 enable_request_compression,
                 include_timing_metrics,
                 beta_features_header,
+                item_ids_enabled,
                 include_attestation,
                 attestation_provider,
                 disable_websockets: AtomicBool::new(false),
@@ -572,6 +575,7 @@ impl ModelClient {
                 extra_headers,
                 compact_request_timeout,
                 turn_state.as_deref(),
+                /*include_item_ids*/ self.state.item_ids_enabled,
             )
             .await
             .map_err(map_api_error);
@@ -1033,6 +1037,7 @@ impl ModelClientSession {
             },
             compression,
             turn_state: Some(Arc::clone(&self.turn_state)),
+            include_item_ids: self.client.state.item_ids_enabled,
         }
     }
 
@@ -1496,6 +1501,7 @@ impl ModelClientSession {
                     ws_request,
                     self.websocket_session.connection_reused(),
                     Some(Arc::clone(&self.turn_state)),
+                    /*include_item_ids*/ self.client.state.item_ids_enabled,
                 )
                 .await
                 .map_err(|err| {
diff --git a/codex-rs/core/src/client_tests.rs b/codex-rs/core/src/client_tests.rs
index 06074a5d2024..1d0c4c48c29a 100644
--- a/codex-rs/core/src/client_tests.rs
+++ b/codex-rs/core/src/client_tests.rs
@@ -76,6 +76,7 @@ fn test_model_client(session_source: SessionSource) -> ModelClient {
         /*enable_request_compression*/ false,
         /*include_timing_metrics*/ false,
         /*beta_features_header*/ None,
+        /*item_ids_enabled*/ false,
         /*attestation_provider*/ None,
     )
 }
@@ -566,6 +567,7 @@ fn model_client_with_counting_attestation(
         /*enable_request_compression*/ false,
         /*include_timing_metrics*/ false,
         /*beta_features_header*/ None,
+        /*item_ids_enabled*/ false,
         Some(Arc::new(CountingAttestationProvider {
             calls: attestation_calls.clone(),
         })),
diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs
index c4bc064a473f..e3686baffc01 100644
--- a/codex-rs/core/src/session/session.rs
+++ b/codex-rs/core/src/session/session.rs
@@ -1027,6 +1027,7 @@ impl Session {
                     config.features.enabled(Feature::EnableRequestCompression),
                     config.features.enabled(Feature::RuntimeMetrics),
                     Self::build_model_client_beta_features_header(config.as_ref()),
+                    /*item_ids_enabled*/ config.features.enabled(Feature::ItemIds),
                     attestation_provider,
                 )
                 .with_prompt_cache_key_override(
diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs
index d0b7149d87d7..c804f398975d 100644
--- a/codex-rs/core/src/session/tests.rs
+++ b/codex-rs/core/src/session/tests.rs
@@ -437,6 +437,7 @@ fn test_model_client_session() -> crate::client::ModelClientSession {
         /*enable_request_compression*/ false,
         /*include_timing_metrics*/ false,
         /*beta_features_header*/ None,
+        /*item_ids_enabled*/ false,
         /*attestation_provider*/ None,
     )
     .new_session()
@@ -5047,6 +5048,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
             config.features.enabled(Feature::EnableRequestCompression),
             config.features.enabled(Feature::RuntimeMetrics),
             Session::build_model_client_beta_features_header(config.as_ref()),
+            /*item_ids_enabled*/ config.features.enabled(Feature::ItemIds),
             /*attestation_provider*/ None,
         ),
         code_mode_service: crate::tools::code_mode::CodeModeService::new(),
@@ -7090,6 +7092,7 @@ where
             config.features.enabled(Feature::EnableRequestCompression),
             config.features.enabled(Feature::RuntimeMetrics),
             Session::build_model_client_beta_features_header(config.as_ref()),
+            /*item_ids_enabled*/ config.features.enabled(Feature::ItemIds),
             /*attestation_provider*/ None,
         ),
         code_mode_service: crate::tools::code_mode::CodeModeService::new(),
diff --git a/codex-rs/core/tests/responses_headers.rs b/codex-rs/core/tests/responses_headers.rs
index b5bc2b0d4440..cf9b9a939fcd 100644
--- a/codex-rs/core/tests/responses_headers.rs
+++ b/codex-rs/core/tests/responses_headers.rs
@@ -127,6 +127,7 @@ async fn responses_stream_includes_subagent_header_on_review() {
         /*enable_request_compression*/ false,
         /*include_timing_metrics*/ false,
         /*beta_features_header*/ None,
+        /*item_ids_enabled*/ false,
         /*attestation_provider*/ None,
     );
     let responses_metadata = test_turn_responses_metadata(&client, thread_id, &session_source);
@@ -258,6 +259,7 @@ async fn responses_stream_includes_subagent_header_on_other() {
         /*enable_request_compression*/ false,
         /*include_timing_metrics*/ false,
         /*beta_features_header*/ None,
+        /*item_ids_enabled*/ false,
         /*attestation_provider*/ None,
     );
     let responses_metadata = test_turn_responses_metadata(&client, thread_id, &session_source);
@@ -375,6 +377,7 @@ async fn responses_respects_model_info_overrides_from_config() {
         /*enable_request_compression*/ false,
         /*include_timing_metrics*/ false,
         /*beta_features_header*/ None,
+        /*item_ids_enabled*/ false,
         /*attestation_provider*/ None,
     );
     let responses_metadata = test_turn_responses_metadata(&client, thread_id, &session_source);
diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs
index ae8c6c1ebe3e..f06c9723dc1c 100644
--- a/codex-rs/core/tests/suite/client.rs
+++ b/codex-rs/core/tests/suite/client.rs
@@ -272,6 +272,9 @@ async fn response_item_ids_persist_across_resume_and_preserve_server_ids() -> an
     })
     .await;
 
+    builder = builder.with_config(|config| {
+        let _ = config.features.enable(Feature::ItemIds);
+    });
     let resumed = builder.resume(&server, home, rollout_path).await?;
     resumed.submit_turn("after resume").await?;
 
@@ -1100,6 +1103,7 @@ async fn send_provider_auth_request(server: &MockServer, auth: ModelProviderAuth
         /*enable_request_compression*/ false,
         /*include_timing_metrics*/ false,
         /*beta_features_header*/ None,
+        /*item_ids_enabled*/ config.features.enabled(Feature::ItemIds),
         /*attestation_provider*/ None,
     );
     let responses_metadata = test_turn_responses_metadata(&client, thread_id);
@@ -2560,6 +2564,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() {
     let mut config = load_default_config_for_test(&codex_home).await;
     config.model_provider_id = provider.name.clone();
     config.model_provider = provider.clone();
+    let _ = config.features.enable(Feature::ItemIds);
     let effort = config.model_reasoning_effort.clone();
     let summary = config.model_reasoning_summary;
     let model = codex_core::test_support::get_model_offline(config.model.as_deref());
@@ -2592,6 +2597,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() {
         /*enable_request_compression*/ false,
         /*include_timing_metrics*/ false,
         /*beta_features_header*/ None,
+        /*item_ids_enabled*/ config.features.enabled(Feature::ItemIds),
         /*attestation_provider*/ None,
     );
     let responses_metadata = test_turn_responses_metadata(&client, thread_id);
diff --git a/codex-rs/core/tests/suite/client_websockets.rs b/codex-rs/core/tests/suite/client_websockets.rs
index cee54e24b310..996b7632ccf0 100755
--- a/codex-rs/core/tests/suite/client_websockets.rs
+++ b/codex-rs/core/tests/suite/client_websockets.rs
@@ -2189,6 +2189,7 @@ async fn websocket_harness_with_provider_options(
         /*enable_request_compression*/ false,
         runtime_metrics_enabled,
         /*beta_features_header*/ None,
+        /*item_ids_enabled*/ config.features.enabled(Feature::ItemIds),
         /*attestation_provider*/ None,
     );
 
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_api_auth_prompt_cache_key_request_diff.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_api_auth_prompt_cache_key_request_diff.snap
index 748ac7914863..0df96a7c064b 100644
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_api_auth_prompt_cache_key_request_diff.snap
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_api_auth_prompt_cache_key_request_diff.snap
@@ -26,7 +26,6 @@ Scenario: After five varied API-key-auth turns, remote manual compaction omits s
 +        }
 +      ],
 +      "encrypted_content": "YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYnR1cm4gZml2ZSByYXcgY29udGVudA==",
-+      "id": "turn-five-reasoning",
 +      "summary": [
 +        {
 +          "text": "TURN_FIVE_REASONING",
@@ -42,7 +41,6 @@ Scenario: After five varied API-key-auth turns, remote manual compaction omits s
 +          "type": "output_text"
 +        }
 +      ],
-+      "id": "turn-five-assistant",
 +      "role": "assistant",
 +      "type": "message"
 -  "service_tier": "priority",
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_chatgpt_auth_service_tier_prompt_cache_key_request_diff.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_chatgpt_auth_service_tier_prompt_cache_key_request_diff.snap
index f997de046652..d959d526cc92 100644
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_chatgpt_auth_service_tier_prompt_cache_key_request_diff.snap
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_chatgpt_auth_service_tier_prompt_cache_key_request_diff.snap
@@ -26,7 +26,6 @@ Scenario: After five varied ChatGPT-auth turns, remote manual compaction reuses
 +        }
 +      ],
 +      "encrypted_content": "YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYnR1cm4gZml2ZSByYXcgY29udGVudA==",
-+      "id": "turn-five-reasoning",
 +      "summary": [
 +        {
 +          "text": "TURN_FIVE_REASONING",
@@ -42,7 +41,6 @@ Scenario: After five varied ChatGPT-auth turns, remote manual compaction reuses
 +          "type": "output_text"
 +        }
 +      ],
-+      "id": "turn-five-assistant",
 +      "role": "assistant",
 +      "type": "message"
 -  "store": false,

From a5cd238565f89e5d1e07b80ba6df7c0e944c3255 Mon Sep 17 00:00:00 2001
From: pakrym-oai <pakrym@openai.com>
Date: Thu, 18 Jun 2026 13:19:14 -0700
Subject: [PATCH 08/14] codex: fix CI failure on PR #28814

---
 codex-rs/memories/write/src/runtime.rs | 1 +
 1 file changed, 1 insertion(+)

diff --git a/codex-rs/memories/write/src/runtime.rs b/codex-rs/memories/write/src/runtime.rs
index 8dae6ca61704..74d8445b7566 100644
--- a/codex-rs/memories/write/src/runtime.rs
+++ b/codex-rs/memories/write/src/runtime.rs
@@ -235,6 +235,7 @@ impl MemoryStartupContext {
             config.features.enabled(Feature::EnableRequestCompression),
             config.features.enabled(Feature::RuntimeMetrics),
             /*beta_features_header*/ None,
+            config.features.enabled(Feature::ItemIds),
             /*attestation_provider*/ None,
         );
 

From de68ecbd0d36a9de5ee09a279fe5377dff96e921 Mon Sep 17 00:00:00 2001
From: pakrym-oai <pakrym@openai.com>
Date: Thu, 18 Jun 2026 13:33:59 -0700
Subject: [PATCH 09/14] codex: update default item ID expectations

---
 codex-rs/core/tests/suite/compact.rs         | 3 ---
 codex-rs/core/tests/suite/model_switching.rs | 8 ++++----
 2 files changed, 4 insertions(+), 7 deletions(-)

diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs
index bf268cee9412..3b709d54a206 100644
--- a/codex-rs/core/tests/suite/compact.rs
+++ b/codex-rs/core/tests/suite/compact.rs
@@ -1305,7 +1305,6 @@ async fn multiple_auto_compact_per_task_runs_after_token_limit_hit() {
       {
         "content": null,
         "encrypted_content": encrypted_content_1,
-        "id": "m1",
         "summary": [
           {
             "text": "I will create a react app",
@@ -1406,7 +1405,6 @@ async fn multiple_auto_compact_per_task_runs_after_token_limit_hit() {
       {
         "content": null,
         "encrypted_content": encrypted_content_2,
-        "id": "m3",
         "summary": [
           {
             "text": "I will create a node app",
@@ -1507,7 +1505,6 @@ async fn multiple_auto_compact_per_task_runs_after_token_limit_hit() {
       {
         "content": null,
         "encrypted_content": encrypted_content_3,
-        "id": "m6",
         "summary": [
           {
             "text": "I will create a python app",
diff --git a/codex-rs/core/tests/suite/model_switching.rs b/codex-rs/core/tests/suite/model_switching.rs
index 22363eabea3a..ef7a305bd762 100644
--- a/codex-rs/core/tests/suite/model_switching.rs
+++ b/codex-rs/core/tests/suite/model_switching.rs
@@ -649,8 +649,8 @@ async fn generated_image_is_replayed_for_image_capable_models() -> Result<()> {
     );
     assert_eq!(
         image_generation_calls[0]["id"].as_str(),
-        Some("ig_123"),
-        "expected the original image generation call id to be preserved"
+        None,
+        "expected the image generation call id to be omitted"
     );
     assert_eq!(
         image_generation_calls[0]["result"].as_str(),
@@ -766,8 +766,8 @@ async fn model_change_from_generated_image_to_text_preserves_prior_generated_ima
     );
     assert_eq!(
         image_generation_calls[0]["id"].as_str(),
-        Some("ig_123"),
-        "second request should preserve the original generated image call id"
+        None,
+        "second request should omit the generated image call id"
     );
     assert_eq!(
         image_generation_calls[0]["result"].as_str(),

From 949475bda25bd59dabcc3be52291e73eb89a141a Mon Sep 17 00:00:00 2001
From: pakrym-oai <pakrym@openai.com>
Date: Thu, 18 Jun 2026 13:41:45 -0700
Subject: [PATCH 10/14] codex: omit IDs from default history fixture

---
 codex-rs/core/tests/suite/client.rs | 2 --
 1 file changed, 2 deletions(-)

diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs
index f06c9723dc1c..e5c8a0ed3da2 100644
--- a/codex-rs/core/tests/suite/client.rs
+++ b/codex-rs/core/tests/suite/client.rs
@@ -3400,7 +3400,6 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() {
         },
         {
             "type": "message",
-            "id": "msg-1",
             "role": "assistant",
             "content": [{"type":"output_text","text":"Hey there!\n"}]
         },
@@ -3411,7 +3410,6 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() {
         },
         {
             "type": "message",
-            "id": "msg-1",
             "role": "assistant",
             "content": [{"type":"output_text","text":"Hey there!\n"}]
         },

From f5750e75cd9e2d639c989fde27e691c39da6efbb Mon Sep 17 00:00:00 2001
From: pakrym-oai <pakrym@openai.com>
Date: Thu, 18 Jun 2026 14:26:54 -0700
Subject: [PATCH 11/14] codex: address PR review feedback (#28814)

---
 codex-rs/codex-api/src/endpoint/compact.rs    |  8 ++-----
 codex-rs/codex-api/src/endpoint/responses.rs  |  7 ++----
 .../src/endpoint/responses_websocket.rs       |  7 ++----
 codex-rs/codex-api/src/lib.rs                 |  1 +
 codex-rs/codex-api/src/requests/mod.rs        |  1 +
 codex-rs/codex-api/src/requests/responses.rs  | 20 ++++++++++++++---
 codex-rs/core/src/client.rs                   | 14 +++++++-----
 codex-rs/core/src/session/mod.rs              |  5 +++--
 codex-rs/core/src/session/tests.rs            | 22 +++++++++++++++++++
 9 files changed, 59 insertions(+), 26 deletions(-)

diff --git a/codex-rs/codex-api/src/endpoint/compact.rs b/codex-rs/codex-api/src/endpoint/compact.rs
index b2c9bc5b433b..9631a5f018d3 100644
--- a/codex-rs/codex-api/src/endpoint/compact.rs
+++ b/codex-rs/codex-api/src/endpoint/compact.rs
@@ -3,14 +3,13 @@ use crate::common::CompactionInput;
 use crate::endpoint::session::EndpointSession;
 use crate::error::ApiError;
 use crate::provider::Provider;
-use crate::requests::strip_response_item_ids;
+use crate::requests::response_request_json;
 use codex_client::HttpTransport;
 use codex_client::RequestTelemetry;
 use codex_protocol::models::ResponseItem;
 use http::HeaderMap;
 use http::Method;
 use serde::Deserialize;
-use serde_json::to_value;
 use std::sync::Arc;
 use std::sync::OnceLock;
 use std::time::Duration;
@@ -78,11 +77,8 @@ impl<T: HttpTransport> CompactClient<T> {
         turn_state: Option<&OnceLock<String>>,
         include_item_ids: bool,
     ) -> Result<Vec<ResponseItem>, ApiError> {
-        let mut body = to_value(input)
+        let body = response_request_json(input, include_item_ids)
             .map_err(|e| ApiError::Stream(format!("failed to encode compaction input: {e}")))?;
-        if !include_item_ids {
-            strip_response_item_ids(&mut body);
-        }
         self.compact(body, extra_headers, request_timeout, turn_state)
             .await
     }
diff --git a/codex-rs/codex-api/src/endpoint/responses.rs b/codex-rs/codex-api/src/endpoint/responses.rs
index 399a84f8b410..205fe91f6d92 100644
--- a/codex-rs/codex-api/src/endpoint/responses.rs
+++ b/codex-rs/codex-api/src/endpoint/responses.rs
@@ -8,7 +8,7 @@ use crate::requests::Compression;
 use crate::requests::headers::build_session_headers;
 use crate::requests::headers::insert_header;
 use crate::requests::headers::subagent_header;
-use crate::requests::strip_response_item_ids;
+use crate::requests::response_request_json;
 use crate::sse::spawn_response_stream;
 use crate::telemetry::SseTelemetry;
 use codex_client::EncodedJsonBody;
@@ -84,11 +84,8 @@ impl<T: HttpTransport> ResponsesClient<T> {
             include_item_ids,
         } = options;
 
-        let mut body = serde_json::to_value(&request)
+        let body = response_request_json(&request, include_item_ids)
             .map_err(|e| ApiError::Stream(format!("failed to encode responses request: {e}")))?;
-        if !include_item_ids {
-            strip_response_item_ids(&mut body);
-        }
         let body = EncodedJsonBody::encode(&body)
             .map_err(|e| ApiError::Stream(format!("failed to encode responses request: {e}")))?;
 
diff --git a/codex-rs/codex-api/src/endpoint/responses_websocket.rs b/codex-rs/codex-api/src/endpoint/responses_websocket.rs
index cba9d09202f2..d954d3761674 100644
--- a/codex-rs/codex-api/src/endpoint/responses_websocket.rs
+++ b/codex-rs/codex-api/src/endpoint/responses_websocket.rs
@@ -5,7 +5,7 @@ use crate::common::ResponsesWsRequest;
 use crate::error::ApiError;
 use crate::provider::Provider;
 use crate::rate_limits::parse_rate_limit_event;
-use crate::requests::strip_response_item_ids;
+use crate::requests::response_request_json;
 use crate::sse::ResponsesStreamEvent;
 use crate::sse::process_responses_event;
 use crate::telemetry::WebsocketTelemetry;
@@ -793,11 +793,8 @@ fn serialize_websocket_request(
     request: &ResponsesWsRequest,
     include_item_ids: bool,
 ) -> Result<String, ApiError> {
-    let mut payload = serde_json::to_value(request)
+    let payload = response_request_json(request, include_item_ids)
         .map_err(|err| ApiError::Stream(format!("failed to encode websocket request: {err}")))?;
-    if !include_item_ids {
-        strip_response_item_ids(&mut payload);
-    }
     serde_json::to_string(&payload)
         .map_err(|err| ApiError::Stream(format!("failed to encode websocket request: {err}")))
 }
diff --git a/codex-rs/codex-api/src/lib.rs b/codex-rs/codex-api/src/lib.rs
index df6723f023f6..f9f7c10e7997 100644
--- a/codex-rs/codex-api/src/lib.rs
+++ b/codex-rs/codex-api/src/lib.rs
@@ -78,6 +78,7 @@ pub use crate::provider::Provider;
 pub use crate::provider::RetryConfig;
 pub use crate::provider::is_azure_responses_provider;
 pub use crate::requests::Compression;
+pub use crate::requests::response_request_json;
 pub use crate::search::AllowedCaller;
 pub use crate::search::ApproximateLocation;
 pub use crate::search::ClickOperation;
diff --git a/codex-rs/codex-api/src/requests/mod.rs b/codex-rs/codex-api/src/requests/mod.rs
index 37dd1304db77..8cd81169c2b2 100644
--- a/codex-rs/codex-api/src/requests/mod.rs
+++ b/codex-rs/codex-api/src/requests/mod.rs
@@ -2,4 +2,5 @@ pub(crate) mod headers;
 pub(crate) mod responses;
 
 pub use responses::Compression;
+pub use responses::response_request_json;
 pub(crate) use responses::strip_response_item_ids;
diff --git a/codex-rs/codex-api/src/requests/responses.rs b/codex-rs/codex-api/src/requests/responses.rs
index 3ac618aafe13..8e3bb7193e86 100644
--- a/codex-rs/codex-api/src/requests/responses.rs
+++ b/codex-rs/codex-api/src/requests/responses.rs
@@ -1,3 +1,4 @@
+use serde::Serialize;
 use serde_json::Value;
 
 #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
@@ -7,6 +8,18 @@ pub enum Compression {
     Zstd,
 }
 
+/// Serializes a Responses-shaped request using the item ID mode shared by transport and tracing.
+pub fn response_request_json(
+    request: &impl Serialize,
+    include_item_ids: bool,
+) -> Result<Value, serde_json::Error> {
+    let mut payload = serde_json::to_value(request)?;
+    if !include_item_ids {
+        strip_response_item_ids(&mut payload);
+    }
+    Ok(payload)
+}
+
 pub(crate) fn strip_response_item_ids(payload_json: &mut Value) {
     let Some(Value::Array(items)) = payload_json.get_mut("input") else {
         return;
@@ -26,8 +39,8 @@ mod tests {
     use serde_json::json;
 
     #[test]
-    fn strip_response_item_ids_removes_ids_from_input_items() {
-        let mut payload = json!({
+    fn response_request_json_removes_ids_from_input_items() {
+        let request = json!({
             "model": "gpt-test",
             "id": "request-id",
             "input": [
@@ -43,7 +56,8 @@ mod tests {
             ]
         });
 
-        strip_response_item_ids(&mut payload);
+        let payload = response_request_json(&request, /*include_item_ids*/ false)
+            .expect("request should serialize");
 
         assert_eq!(
             payload,
diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs
index 417634e2359d..18c1c5036752 100644
--- a/codex-rs/core/src/client.rs
+++ b/codex-rs/core/src/client.rs
@@ -61,6 +61,7 @@ use codex_api::auth_header_telemetry;
 use codex_api::build_session_headers;
 use codex_api::create_text_param_for_request;
 use codex_api::response_create_client_metadata;
+use codex_api::response_request_json;
 use codex_app_server_protocol::AuthMode;
 use codex_login::AuthManager;
 use codex_login::CodexAuth;
@@ -1315,7 +1316,9 @@ impl ModelClientSession {
             )?;
             let inference_trace_attempt = inference_trace.start_attempt();
             inference_trace_attempt.add_request_headers(&mut options.extra_headers);
-            inference_trace_attempt.record_started(&request);
+            let trace_request =
+                response_request_json(&request, self.client.state.item_ids_enabled)?;
+            inference_trace_attempt.record_started(&trace_request);
             let client = ApiResponsesClient::new(
                 transport,
                 client_setup.api_provider,
@@ -1480,14 +1483,15 @@ impl ModelClientSession {
                 inference_trace.start_attempt()
             };
             stamp_ws_stream_request_start_ms(&mut ws_request);
-            if previous_response_id_from_untraced_warmup {
+            let trace_request = if previous_response_id_from_untraced_warmup {
                 // The transport can reuse an untraced warmup response id and omit the
                 // already-sent input, but rollout replay needs the logical model-visible
                 // request rather than the compressed websocket delta.
-                inference_trace_attempt.record_started(&request);
+                response_request_json(&request, self.client.state.item_ids_enabled)?
             } else {
-                inference_trace_attempt.record_started(&ws_request);
-            }
+                response_request_json(&ws_request, self.client.state.item_ids_enabled)?
+            };
+            inference_trace_attempt.record_started(&trace_request);
             self.websocket_session.last_request = Some(request);
             self.websocket_session.last_response_from_untraced_warmup = warmup;
             let websocket_connection =
diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs
index e9fa300c1f8e..abac00a439fd 100644
--- a/codex-rs/core/src/session/mod.rs
+++ b/codex-rs/core/src/session/mod.rs
@@ -2658,7 +2658,6 @@ impl Session {
             }
             let prefix = match item {
                 ResponseItem::Message { .. } => "msg",
-                ResponseItem::AgentMessage { .. } => "amsg",
                 ResponseItem::Reasoning { .. } => "rs",
                 ResponseItem::LocalShellCall { .. } => "lsh",
                 ResponseItem::FunctionCall { .. } => "fc",
@@ -2670,7 +2669,9 @@ impl Session {
                 ResponseItem::WebSearchCall { .. } => "ws",
                 ResponseItem::ImageGenerationCall { .. } => "ig",
                 ResponseItem::Compaction { .. } | ResponseItem::ContextCompaction { .. } => "cmp",
-                ResponseItem::CompactionTrigger { .. } | ResponseItem::Other => continue,
+                ResponseItem::AgentMessage { .. }
+                | ResponseItem::CompactionTrigger { .. }
+                | ResponseItem::Other => continue,
             };
             item.set_id(format!("{prefix}_{}", Uuid::now_v7()));
         }
diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs
index c804f398975d..1a3745a4e80d 100644
--- a/codex-rs/core/src/session/tests.rs
+++ b/codex-rs/core/src/session/tests.rs
@@ -42,6 +42,7 @@ use codex_protocol::config_types::ServiceTier;
 use codex_protocol::config_types::TrustLevel;
 use codex_protocol::exec_output::ExecToolCallOutput;
 use codex_protocol::models::ActivePermissionProfile;
+use codex_protocol::models::AgentMessageInputContent;
 use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
 use codex_protocol::models::FileSystemPermissions;
 use codex_protocol::models::FunctionCallOutputBody;
@@ -197,6 +198,27 @@ fn user_message(text: &str) -> ResponseItem {
     }
 }
 
+#[test]
+fn assign_missing_response_item_ids_skips_agent_messages() {
+    let mut items = Cow::Owned(vec![
+        ResponseItem::AgentMessage {
+            id: None,
+            author: "worker".to_string(),
+            recipient: "root".to_string(),
+            content: vec![AgentMessageInputContent::InputText {
+                text: "done".to_string(),
+            }],
+            metadata: None,
+        },
+        user_message("hello"),
+    ]);
+
+    Session::assign_missing_response_item_ids(&mut items);
+
+    assert_eq!(items[0].id(), None);
+    assert!(items[1].id().is_some_and(|id| id.starts_with("msg_")));
+}
+
 fn assistant_message(text: &str) -> ResponseItem {
     ResponseItem::Message {
         id: None,

From 1abdae46d80b72e80d814999b9e746d09bc7495c Mon Sep 17 00:00:00 2001
From: pakrym-oai <pakrym@openai.com>
Date: Thu, 18 Jun 2026 14:37:03 -0700
Subject: [PATCH 12/14] codex: preserve IDs for stored requests

---
 codex-rs/core/src/client.rs         | 16 +++++++++++-----
 codex-rs/core/tests/suite/client.rs |  3 +--
 2 files changed, 12 insertions(+), 7 deletions(-)

diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs
index 18c1c5036752..f2e8073ceb5e 100644
--- a/codex-rs/core/src/client.rs
+++ b/codex-rs/core/src/client.rs
@@ -835,6 +835,10 @@ impl ModelClient {
         Ok(request)
     }
 
+    fn include_item_ids_for_request(&self, request: &ResponsesApiRequest) -> bool {
+        self.state.item_ids_enabled || request.store
+    }
+
     /// Returns whether the Responses-over-WebSocket transport is active for this session.
     ///
     /// WebSocket use is controlled by provider capability and session-scoped fallback state.
@@ -1314,10 +1318,11 @@ impl ModelClientSession {
                 service_tier.clone(),
                 responses_metadata,
             )?;
+            let include_item_ids = self.client.include_item_ids_for_request(&request);
+            options.include_item_ids = include_item_ids;
             let inference_trace_attempt = inference_trace.start_attempt();
             inference_trace_attempt.add_request_headers(&mut options.extra_headers);
-            let trace_request =
-                response_request_json(&request, self.client.state.item_ids_enabled)?;
+            let trace_request = response_request_json(&request, include_item_ids)?;
             inference_trace_attempt.record_started(&trace_request);
             let client = ApiResponsesClient::new(
                 transport,
@@ -1421,6 +1426,7 @@ impl ModelClientSession {
                 service_tier.clone(),
                 responses_metadata,
             )?;
+            let include_item_ids = self.client.include_item_ids_for_request(&request);
             let mut client_metadata = self
                 .client
                 .build_ws_client_metadata(responses_metadata, model_info.use_responses_lite);
@@ -1487,9 +1493,9 @@ impl ModelClientSession {
                 // The transport can reuse an untraced warmup response id and omit the
                 // already-sent input, but rollout replay needs the logical model-visible
                 // request rather than the compressed websocket delta.
-                response_request_json(&request, self.client.state.item_ids_enabled)?
+                response_request_json(&request, include_item_ids)?
             } else {
-                response_request_json(&ws_request, self.client.state.item_ids_enabled)?
+                response_request_json(&ws_request, include_item_ids)?
             };
             inference_trace_attempt.record_started(&trace_request);
             self.websocket_session.last_request = Some(request);
@@ -1505,7 +1511,7 @@ impl ModelClientSession {
                     ws_request,
                     self.websocket_session.connection_reused(),
                     Some(Arc::clone(&self.turn_state)),
-                    /*include_item_ids*/ self.client.state.item_ids_enabled,
+                    include_item_ids,
                 )
                 .await
                 .map_err(|err| {
diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs
index e5c8a0ed3da2..0c7844d55ee4 100644
--- a/codex-rs/core/tests/suite/client.rs
+++ b/codex-rs/core/tests/suite/client.rs
@@ -2564,7 +2564,6 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() {
     let mut config = load_default_config_for_test(&codex_home).await;
     config.model_provider_id = provider.name.clone();
     config.model_provider = provider.clone();
-    let _ = config.features.enable(Feature::ItemIds);
     let effort = config.model_reasoning_effort.clone();
     let summary = config.model_reasoning_summary;
     let model = codex_core::test_support::get_model_offline(config.model.as_deref());
@@ -2597,7 +2596,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() {
         /*enable_request_compression*/ false,
         /*include_timing_metrics*/ false,
         /*beta_features_header*/ None,
-        /*item_ids_enabled*/ config.features.enabled(Feature::ItemIds),
+        /*item_ids_enabled*/ false,
         /*attestation_provider*/ None,
     );
     let responses_metadata = test_turn_responses_metadata(&client, thread_id);

From d5070ac8cc2b61048f48cf2304a4ab4d4ee0680e Mon Sep 17 00:00:00 2001
From: pakrym-oai <pakrym@openai.com>
Date: Thu, 18 Jun 2026 14:56:13 -0700
Subject: [PATCH 13/14] codex: centralize response item ID request policy

---
 codex-rs/codex-api/src/endpoint/compact.rs    |  4 +-
 codex-rs/codex-api/src/endpoint/responses.rs  |  7 +-
 .../src/endpoint/responses_websocket.rs       | 54 ++----------
 codex-rs/codex-api/src/endpoint/search.rs     |  8 +-
 codex-rs/codex-api/src/lib.rs                 |  1 -
 codex-rs/codex-api/src/requests/mod.rs        |  2 -
 codex-rs/codex-api/src/requests/responses.rs  | 83 -------------------
 codex-rs/codex-api/tests/clients.rs           | 52 +-----------
 codex-rs/core/src/client.rs                   | 41 +++++----
 codex-rs/core/src/session/mod.rs              |  2 +-
 .../core/tests/suite/client_websockets.rs     |  4 +-
 codex-rs/ext/web-search/src/history.rs        | 16 ++--
 codex-rs/protocol/src/models.rs               | 12 ++-
 13 files changed, 58 insertions(+), 228 deletions(-)

diff --git a/codex-rs/codex-api/src/endpoint/compact.rs b/codex-rs/codex-api/src/endpoint/compact.rs
index 9631a5f018d3..fd3fd17dbca7 100644
--- a/codex-rs/codex-api/src/endpoint/compact.rs
+++ b/codex-rs/codex-api/src/endpoint/compact.rs
@@ -3,7 +3,6 @@ use crate::common::CompactionInput;
 use crate::endpoint::session::EndpointSession;
 use crate::error::ApiError;
 use crate::provider::Provider;
-use crate::requests::response_request_json;
 use codex_client::HttpTransport;
 use codex_client::RequestTelemetry;
 use codex_protocol::models::ResponseItem;
@@ -75,9 +74,8 @@ impl<T: HttpTransport> CompactClient<T> {
         extra_headers: HeaderMap,
         request_timeout: Duration,
         turn_state: Option<&OnceLock<String>>,
-        include_item_ids: bool,
     ) -> Result<Vec<ResponseItem>, ApiError> {
-        let body = response_request_json(input, include_item_ids)
+        let body = serde_json::to_value(input)
             .map_err(|e| ApiError::Stream(format!("failed to encode compaction input: {e}")))?;
         self.compact(body, extra_headers, request_timeout, turn_state)
             .await
diff --git a/codex-rs/codex-api/src/endpoint/responses.rs b/codex-rs/codex-api/src/endpoint/responses.rs
index 205fe91f6d92..804f0027ff84 100644
--- a/codex-rs/codex-api/src/endpoint/responses.rs
+++ b/codex-rs/codex-api/src/endpoint/responses.rs
@@ -8,7 +8,6 @@ use crate::requests::Compression;
 use crate::requests::headers::build_session_headers;
 use crate::requests::headers::insert_header;
 use crate::requests::headers::subagent_header;
-use crate::requests::response_request_json;
 use crate::sse::spawn_response_stream;
 use crate::telemetry::SseTelemetry;
 use codex_client::EncodedJsonBody;
@@ -37,7 +36,6 @@ pub struct ResponsesOptions {
     pub extra_headers: HeaderMap,
     pub compression: Compression,
     pub turn_state: Option<Arc<OnceLock<String>>>,
-    pub include_item_ids: bool,
 }
 
 impl<T: HttpTransport> ResponsesClient<T> {
@@ -81,12 +79,9 @@ impl<T: HttpTransport> ResponsesClient<T> {
             extra_headers,
             compression,
             turn_state,
-            include_item_ids,
         } = options;
 
-        let body = response_request_json(&request, include_item_ids)
-            .map_err(|e| ApiError::Stream(format!("failed to encode responses request: {e}")))?;
-        let body = EncodedJsonBody::encode(&body)
+        let body = EncodedJsonBody::encode(&request)
             .map_err(|e| ApiError::Stream(format!("failed to encode responses request: {e}")))?;
 
         let mut headers = extra_headers;
diff --git a/codex-rs/codex-api/src/endpoint/responses_websocket.rs b/codex-rs/codex-api/src/endpoint/responses_websocket.rs
index d954d3761674..4ea564f139aa 100644
--- a/codex-rs/codex-api/src/endpoint/responses_websocket.rs
+++ b/codex-rs/codex-api/src/endpoint/responses_websocket.rs
@@ -5,7 +5,6 @@ use crate::common::ResponsesWsRequest;
 use crate::error::ApiError;
 use crate::provider::Provider;
 use crate::rate_limits::parse_rate_limit_event;
-use crate::requests::response_request_json;
 use crate::sse::ResponsesStreamEvent;
 use crate::sse::process_responses_event;
 use crate::telemetry::WebsocketTelemetry;
@@ -217,7 +216,6 @@ impl ResponsesWebsocketConnection {
         request: ResponsesWsRequest,
         connection_reused: bool,
         turn_state: Option<Arc<OnceLock<String>>>,
-        include_item_ids: bool,
     ) -> Result<ResponseStream, ApiError> {
         let (tx_event, rx_event) =
             mpsc::channel::<std::result::Result<ResponseEvent, ApiError>>(1600);
@@ -227,7 +225,7 @@ impl ResponsesWebsocketConnection {
         let models_etag = self.models_etag.clone();
         let server_model = self.server_model.clone();
         let telemetry = self.telemetry.clone();
-        let request_text = serialize_websocket_request(&request, include_item_ids)?;
+        let request_text = serialize_websocket_request(&request)?;
 
         let current_span = Span::current();
         tokio::spawn(
@@ -789,13 +787,8 @@ async fn send_websocket_request(
     Ok(())
 }
 
-fn serialize_websocket_request(
-    request: &ResponsesWsRequest,
-    include_item_ids: bool,
-) -> Result<String, ApiError> {
-    let payload = response_request_json(request, include_item_ids)
-        .map_err(|err| ApiError::Stream(format!("failed to encode websocket request: {err}")))?;
-    serde_json::to_string(&payload)
+fn serialize_websocket_request(request: &ResponsesWsRequest) -> Result<String, ApiError> {
+    serde_json::to_string(request)
         .map_err(|err| ApiError::Stream(format!("failed to encode websocket request: {err}")))
 }
 
@@ -846,51 +839,14 @@ mod tests {
         });
 
         let previous_payload = serde_json::to_value(&request).expect("serialize previous payload");
-        let request_text = serialize_websocket_request(&request, /*include_item_ids*/ true)
-            .expect("serialize websocket request");
+        let request_text =
+            serialize_websocket_request(&request).expect("serialize websocket request");
         let wire_payload =
             serde_json::from_str::<Value>(&request_text).expect("parse websocket request");
 
         assert_eq!(wire_payload, previous_payload);
     }
 
-    #[test]
-    fn websocket_serialization_strips_item_ids_by_default() {
-        let request = ResponsesWsRequest::ResponseCreate(ResponseCreateWsRequest {
-            model: "gpt-test".to_string(),
-            instructions: "Use the available tools.".to_string(),
-            previous_response_id: None,
-            input: vec![ResponseItem::Message {
-                id: Some("msg-1".to_string()),
-                role: "user".to_string(),
-                content: vec![ContentItem::InputText {
-                    text: "hello".to_string(),
-                }],
-                phase: None,
-                metadata: None,
-            }],
-            tools: Vec::new(),
-            tool_choice: "auto".to_string(),
-            parallel_tool_calls: true,
-            reasoning: None,
-            store: false,
-            stream: true,
-            include: Vec::new(),
-            service_tier: None,
-            prompt_cache_key: None,
-            text: None,
-            generate: None,
-            client_metadata: None,
-        });
-
-        let request_text = serialize_websocket_request(&request, /*include_item_ids*/ false)
-            .expect("serialize websocket request");
-        let wire_payload =
-            serde_json::from_str::<Value>(&request_text).expect("parse websocket request");
-
-        assert_eq!(wire_payload["input"][0].get("id"), None);
-    }
-
     #[test]
     fn websocket_config_enables_permessage_deflate() {
         let config = websocket_config();
diff --git a/codex-rs/codex-api/src/endpoint/search.rs b/codex-rs/codex-api/src/endpoint/search.rs
index abf06777b10b..864f7ec9b0a8 100644
--- a/codex-rs/codex-api/src/endpoint/search.rs
+++ b/codex-rs/codex-api/src/endpoint/search.rs
@@ -2,8 +2,6 @@ use crate::auth::SharedAuthProvider;
 use crate::endpoint::session::EndpointSession;
 use crate::error::ApiError;
 use crate::provider::Provider;
-use crate::requests::strip_response_item_ids;
-use crate::search::SearchInput;
 use crate::search::SearchRequest;
 use crate::search::SearchResponse;
 use codex_client::HttpTransport;
@@ -39,11 +37,8 @@ impl<T: HttpTransport> SearchClient<T> {
         request: &SearchRequest,
         extra_headers: HeaderMap,
     ) -> Result<SearchResponse, ApiError> {
-        let mut body = to_value(request)
+        let body = to_value(request)
             .map_err(|e| ApiError::Stream(format!("failed to encode search request: {e}")))?;
-        if matches!(&request.input, Some(SearchInput::Items(_))) {
-            strip_response_item_ids(&mut body);
-        }
         let resp = self
             .session
             .execute(Method::POST, Self::path(), extra_headers, Some(body))
@@ -233,6 +228,7 @@ mod tests {
                 "model": "gpt-test",
                 "input": [{
                     "type": "message",
+                    "id": "msg_search",
                     "role": "user",
                     "content": [
                         {"type": "input_text", "text": "find this"},
diff --git a/codex-rs/codex-api/src/lib.rs b/codex-rs/codex-api/src/lib.rs
index f9f7c10e7997..df6723f023f6 100644
--- a/codex-rs/codex-api/src/lib.rs
+++ b/codex-rs/codex-api/src/lib.rs
@@ -78,7 +78,6 @@ pub use crate::provider::Provider;
 pub use crate::provider::RetryConfig;
 pub use crate::provider::is_azure_responses_provider;
 pub use crate::requests::Compression;
-pub use crate::requests::response_request_json;
 pub use crate::search::AllowedCaller;
 pub use crate::search::ApproximateLocation;
 pub use crate::search::ClickOperation;
diff --git a/codex-rs/codex-api/src/requests/mod.rs b/codex-rs/codex-api/src/requests/mod.rs
index 8cd81169c2b2..abe57a886dce 100644
--- a/codex-rs/codex-api/src/requests/mod.rs
+++ b/codex-rs/codex-api/src/requests/mod.rs
@@ -2,5 +2,3 @@ pub(crate) mod headers;
 pub(crate) mod responses;
 
 pub use responses::Compression;
-pub use responses::response_request_json;
-pub(crate) use responses::strip_response_item_ids;
diff --git a/codex-rs/codex-api/src/requests/responses.rs b/codex-rs/codex-api/src/requests/responses.rs
index 8e3bb7193e86..9a16ceb1c44c 100644
--- a/codex-rs/codex-api/src/requests/responses.rs
+++ b/codex-rs/codex-api/src/requests/responses.rs
@@ -1,89 +1,6 @@
-use serde::Serialize;
-use serde_json::Value;
-
 #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
 pub enum Compression {
     #[default]
     None,
     Zstd,
 }
-
-/// Serializes a Responses-shaped request using the item ID mode shared by transport and tracing.
-pub fn response_request_json(
-    request: &impl Serialize,
-    include_item_ids: bool,
-) -> Result<Value, serde_json::Error> {
-    let mut payload = serde_json::to_value(request)?;
-    if !include_item_ids {
-        strip_response_item_ids(&mut payload);
-    }
-    Ok(payload)
-}
-
-pub(crate) fn strip_response_item_ids(payload_json: &mut Value) {
-    let Some(Value::Array(items)) = payload_json.get_mut("input") else {
-        return;
-    };
-
-    for item in items {
-        if let Value::Object(object) = item {
-            object.remove("id");
-        }
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use pretty_assertions::assert_eq;
-    use serde_json::json;
-
-    #[test]
-    fn response_request_json_removes_ids_from_input_items() {
-        let request = json!({
-            "model": "gpt-test",
-            "id": "request-id",
-            "input": [
-                {
-                    "type": "message",
-                    "id": "msg_1",
-                    "content": [
-                        {"type": "input_text", "text": "hello"},
-                        {"type": "input_image", "id": "img_1", "image_url": "https://example.com/image.png"}
-                    ]
-                },
-                {"type": "function_call_output", "id": "fco_1", "call_id": "call_1", "output": "done"}
-            ]
-        });
-
-        let payload = response_request_json(&request, /*include_item_ids*/ false)
-            .expect("request should serialize");
-
-        assert_eq!(
-            payload,
-            json!({
-                "model": "gpt-test",
-                "id": "request-id",
-                "input": [
-                    {
-                        "type": "message",
-                        "content": [
-                            {"type": "input_text", "text": "hello"},
-                            {"type": "input_image", "id": "img_1", "image_url": "https://example.com/image.png"}
-                        ]
-                    },
-                    {"type": "function_call_output", "call_id": "call_1", "output": "done"}
-                ]
-            })
-        );
-    }
-
-    #[test]
-    fn strip_response_item_ids_ignores_missing_input() {
-        let mut payload = json!({"id": "request-id"});
-
-        strip_response_item_ids(&mut payload);
-
-        assert_eq!(payload, json!({"id": "request-id"}));
-    }
-}
diff --git a/codex-rs/codex-api/tests/clients.rs b/codex-rs/codex-api/tests/clients.rs
index 625dd348baa3..488f6dc67436 100644
--- a/codex-rs/codex-api/tests/clients.rs
+++ b/codex-rs/codex-api/tests/clients.rs
@@ -301,7 +301,7 @@ async fn responses_client_uses_responses_path() -> Result<()> {
 }
 
 #[tokio::test]
-async fn responses_client_stream_request_preserves_item_ids_when_enabled() -> Result<()> {
+async fn responses_client_stream_request_preserves_item_ids() -> Result<()> {
     let state = RecordingState::default();
     let transport = RecordingTransport::new(state.clone());
     let client = ResponsesClient::new(transport, provider("openai"), Arc::new(NoAuth));
@@ -330,13 +330,7 @@ async fn responses_client_stream_request_preserves_item_ids_when_enabled() -> Re
     let expected = serde_json::to_value(&request)?;
 
     let _stream = client
-        .stream_request(
-            request,
-            ResponsesOptions {
-                include_item_ids: true,
-                ..Default::default()
-            },
-        )
+        .stream_request(request, ResponsesOptions::default())
         .await?;
 
     let requests = state.take_stream_requests();
@@ -355,45 +349,6 @@ async fn responses_client_stream_request_preserves_item_ids_when_enabled() -> Re
     Ok(())
 }
 
-#[tokio::test]
-async fn responses_client_stream_request_strips_item_ids_by_default() -> Result<()> {
-    let state = RecordingState::default();
-    let transport = RecordingTransport::new(state.clone());
-    let client = ResponsesClient::new(transport, provider("openai"), Arc::new(NoAuth));
-    let request = ResponsesApiRequest {
-        model: "gpt-test".into(),
-        instructions: "Say hi".into(),
-        input: vec![ResponseItem::Message {
-            id: Some("msg_1".into()),
-            role: "user".into(),
-            content: vec![ContentItem::InputText { text: "hi".into() }],
-            phase: None,
-            metadata: None,
-        }],
-        tools: Vec::new(),
-        tool_choice: "auto".into(),
-        parallel_tool_calls: false,
-        reasoning: None,
-        store: false,
-        stream: true,
-        include: Vec::new(),
-        service_tier: None,
-        prompt_cache_key: None,
-        text: None,
-        client_metadata: None,
-    };
-
-    let _stream = client
-        .stream_request(request, ResponsesOptions::default())
-        .await?;
-
-    let requests = state.take_stream_requests();
-    assert_eq!(requests.len(), 1);
-    let body: serde_json::Value = serde_json::from_slice(request_body_bytes(&requests[0]))?;
-    assert_eq!(body["input"][0].get("id"), None);
-    Ok(())
-}
-
 #[tokio::test]
 async fn streaming_client_adds_auth_headers() -> Result<()> {
     let state = RecordingState::default();
@@ -550,7 +505,7 @@ async fn streaming_client_does_not_retry_auth_build_error() -> Result<()> {
 }
 
 #[tokio::test]
-async fn azure_store_sends_ids_when_enabled_and_headers() -> Result<()> {
+async fn azure_store_sends_ids_and_headers() -> Result<()> {
     let state = RecordingState::default();
     let transport = RecordingTransport::new(state.clone());
     let client = ResponsesClient::new(transport, provider("azure"), Arc::new(NoAuth));
@@ -590,7 +545,6 @@ async fn azure_store_sends_ids_when_enabled_and_headers() -> Result<()> {
                 extra_headers,
                 compression: Compression::None,
                 turn_state: None,
-                include_item_ids: true,
             },
         )
         .await?;
diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs
index f2e8073ceb5e..d8f9df2f5382 100644
--- a/codex-rs/core/src/client.rs
+++ b/codex-rs/core/src/client.rs
@@ -61,7 +61,6 @@ use codex_api::auth_header_telemetry;
 use codex_api::build_session_headers;
 use codex_api::create_text_param_for_request;
 use codex_api::response_create_client_metadata;
-use codex_api::response_request_json;
 use codex_app_server_protocol::AuthMode;
 use codex_login::AuthManager;
 use codex_login::CodexAuth;
@@ -524,7 +523,7 @@ impl ModelClient {
         let ResponsesApiRequest {
             model,
             instructions,
-            input,
+            mut input,
             tools,
             parallel_tool_calls,
             reasoning,
@@ -533,6 +532,7 @@ impl ModelClient {
             text,
             ..
         } = request;
+        self.prepare_response_items_for_request(&mut input, /*store*/ false);
         let payload = ApiCompactionInput {
             model: &model,
             input: &input,
@@ -576,7 +576,6 @@ impl ModelClient {
                 extra_headers,
                 compact_request_timeout,
                 turn_state.as_deref(),
-                /*include_item_ids*/ self.state.item_ids_enabled,
             )
             .await
             .map_err(map_api_error);
@@ -835,8 +834,14 @@ impl ModelClient {
         Ok(request)
     }
 
-    fn include_item_ids_for_request(&self, request: &ResponsesApiRequest) -> bool {
-        self.state.item_ids_enabled || request.store
+    fn prepare_response_items_for_request(&self, input: &mut [ResponseItem], store: bool) {
+        if self.state.item_ids_enabled || store {
+            return;
+        }
+
+        for item in input {
+            item.set_id(None);
+        }
     }
 
     /// Returns whether the Responses-over-WebSocket transport is active for this session.
@@ -1042,7 +1047,6 @@ impl ModelClientSession {
             },
             compression,
             turn_state: Some(Arc::clone(&self.turn_state)),
-            include_item_ids: self.client.state.item_ids_enabled,
         }
     }
 
@@ -1309,7 +1313,7 @@ impl ModelClientSession {
                 )
                 .await;
 
-            let request = self.client.build_responses_request(
+            let mut request = self.client.build_responses_request(
                 &client_setup.api_provider,
                 prompt,
                 model_info,
@@ -1318,12 +1322,12 @@ impl ModelClientSession {
                 service_tier.clone(),
                 responses_metadata,
             )?;
-            let include_item_ids = self.client.include_item_ids_for_request(&request);
-            options.include_item_ids = include_item_ids;
+            let store = request.store;
+            self.client
+                .prepare_response_items_for_request(&mut request.input, store);
             let inference_trace_attempt = inference_trace.start_attempt();
             inference_trace_attempt.add_request_headers(&mut options.extra_headers);
-            let trace_request = response_request_json(&request, include_item_ids)?;
-            inference_trace_attempt.record_started(&trace_request);
+            inference_trace_attempt.record_started(&request);
             let client = ApiResponsesClient::new(
                 transport,
                 client_setup.api_provider,
@@ -1426,7 +1430,6 @@ impl ModelClientSession {
                 service_tier.clone(),
                 responses_metadata,
             )?;
-            let include_item_ids = self.client.include_item_ids_for_request(&request);
             let mut client_metadata = self
                 .client
                 .build_ws_client_metadata(responses_metadata, model_info.use_responses_lite);
@@ -1489,15 +1492,18 @@ impl ModelClientSession {
                 inference_trace.start_attempt()
             };
             stamp_ws_stream_request_start_ms(&mut ws_request);
-            let trace_request = if previous_response_id_from_untraced_warmup {
+            let ResponsesWsRequest::ResponseCreate(ws_payload) = &mut ws_request;
+            let store = ws_payload.store;
+            self.client
+                .prepare_response_items_for_request(&mut ws_payload.input, store);
+            if previous_response_id_from_untraced_warmup {
                 // The transport can reuse an untraced warmup response id and omit the
                 // already-sent input, but rollout replay needs the logical model-visible
                 // request rather than the compressed websocket delta.
-                response_request_json(&request, include_item_ids)?
+                inference_trace_attempt.record_started(&request);
             } else {
-                response_request_json(&ws_request, include_item_ids)?
-            };
-            inference_trace_attempt.record_started(&trace_request);
+                inference_trace_attempt.record_started(&ws_request);
+            }
             self.websocket_session.last_request = Some(request);
             self.websocket_session.last_response_from_untraced_warmup = warmup;
             let websocket_connection =
@@ -1511,7 +1517,6 @@ impl ModelClientSession {
                     ws_request,
                     self.websocket_session.connection_reused(),
                     Some(Arc::clone(&self.turn_state)),
-                    include_item_ids,
                 )
                 .await
                 .map_err(|err| {
diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs
index abac00a439fd..7c725fa151d6 100644
--- a/codex-rs/core/src/session/mod.rs
+++ b/codex-rs/core/src/session/mod.rs
@@ -2673,7 +2673,7 @@ impl Session {
                 | ResponseItem::CompactionTrigger { .. }
                 | ResponseItem::Other => continue,
             };
-            item.set_id(format!("{prefix}_{}", Uuid::now_v7()));
+            item.set_id(Some(format!("{prefix}_{}", Uuid::now_v7())));
         }
     }
 
diff --git a/codex-rs/core/tests/suite/client_websockets.rs b/codex-rs/core/tests/suite/client_websockets.rs
index 996b7632ccf0..1ccbd302d8a4 100755
--- a/codex-rs/core/tests/suite/client_websockets.rs
+++ b/codex-rs/core/tests/suite/client_websockets.rs
@@ -157,7 +157,8 @@ async fn responses_websocket_streams_request() {
 
     let harness = websocket_harness(&server).await;
     let mut client_session = harness.client.new_session();
-    let prompt = prompt_with_input(vec![message_item("hello")]);
+    let mut prompt = prompt_with_input(vec![message_item("hello")]);
+    prompt.input[0].set_id(Some("msg_existing".to_string()));
 
     stream_until_complete(&mut client_session, &harness, &prompt).await;
 
@@ -169,6 +170,7 @@ async fn responses_websocket_streams_request() {
     assert_eq!(body["model"].as_str(), Some(MODEL));
     assert_eq!(body["stream"], serde_json::Value::Bool(true));
     assert_eq!(body["input"].as_array().map(Vec::len), Some(1));
+    assert_eq!(body["input"][0].get("id"), None);
     let handshake = server.single_handshake();
     assert_eq!(
         handshake.header(OPENAI_BETA_HEADER),
diff --git a/codex-rs/ext/web-search/src/history.rs b/codex-rs/ext/web-search/src/history.rs
index 3065ed178b10..80bc6dd32405 100644
--- a/codex-rs/ext/web-search/src/history.rs
+++ b/codex-rs/ext/web-search/src/history.rs
@@ -29,7 +29,9 @@ pub(crate) fn recent_input(items: &[ResponseItem]) -> Option<SearchInput> {
 fn push_visible_message(messages: &mut Vec<ResponseItem>, item: &ResponseItem) {
     match item {
         ResponseItem::Message { role, .. } if role == ASSISTANT_ROLE => {
-            messages.push(item.clone());
+            let mut message = item.clone();
+            message.set_id(None);
+            messages.push(message);
         }
         ResponseItem::AgentMessage {
             author,
@@ -50,7 +52,7 @@ fn push_visible_message(messages: &mut Vec<ResponseItem>, item: &ResponseItem) {
             }
         }
         ResponseItem::Message {
-            id,
+            id: _,
             role,
             content,
             phase,
@@ -65,7 +67,7 @@ fn push_visible_message(messages: &mut Vec<ResponseItem>, item: &ResponseItem) {
                 .collect::<Vec<_>>();
             if !content.is_empty() {
                 messages.push(ResponseItem::Message {
-                    id: id.clone(),
+                    id: None,
                     role: role.clone(),
                     content,
                     phase: phase.clone(),
@@ -108,11 +110,15 @@ mod tests {
 
     #[test]
     fn keeps_current_user_and_previous_visible_turn() {
+        let mut previous_user = message(USER_ROLE, "previous user");
+        previous_user.set_id(Some("msg_previous_user".to_string()));
+        let mut previous_assistant = message(ASSISTANT_ROLE, "previous assistant");
+        previous_assistant.set_id(Some("msg_previous_assistant".to_string()));
         let items = vec![
             message("system", "system"),
             message(USER_ROLE, "old user"),
             message(ASSISTANT_ROLE, "old assistant"),
-            message(USER_ROLE, "previous user"),
+            previous_user,
             ResponseItem::FunctionCall {
                 id: None,
                 name: "tool".to_string(),
@@ -121,7 +127,7 @@ mod tests {
                 call_id: "call-1".to_string(),
                 metadata: None,
             },
-            message(ASSISTANT_ROLE, "previous assistant"),
+            previous_assistant,
             message("developer", "developer"),
             message(USER_ROLE, "current user"),
             message(ASSISTANT_ROLE, "current commentary"),
diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs
index ead2d5330b14..dc97e7c55d0a 100644
--- a/codex-rs/protocol/src/models.rs
+++ b/codex-rs/protocol/src/models.rs
@@ -1168,8 +1168,8 @@ impl ResponseItem {
         }
     }
 
-    /// Sets the Responses API item ID for variants that carry one.
-    pub fn set_id(&mut self, new_id: String) {
+    /// Sets or clears the Responses API item ID for variants that carry one.
+    pub fn set_id(&mut self, new_id: Option<String>) {
         match self {
             Self::Message { id, .. }
             | Self::AgentMessage { id, .. }
@@ -1184,7 +1184,7 @@ impl ResponseItem {
             | Self::Reasoning { id, .. }
             | Self::ImageGenerationCall { id, .. }
             | Self::Compaction { id, .. }
-            | Self::ContextCompaction { id, .. } => *id = Some(new_id),
+            | Self::ContextCompaction { id, .. } => *id = new_id,
             Self::CompactionTrigger { .. } | Self::Other => {}
         }
     }
@@ -2132,9 +2132,13 @@ mod tests {
         let mut item = response_item_with_metadata(/*metadata*/ None);
         assert_eq!(item.id(), None);
 
-        item.set_id("msg_test".to_string());
+        item.set_id(Some("msg_test".to_string()));
 
         assert_eq!(item.id(), Some("msg_test"));
+
+        item.set_id(None);
+
+        assert_eq!(item.id(), None);
     }
 
     fn response_item_with_metadata(metadata: Option<ResponseItemMetadata>) -> ResponseItem {

From 5233e4da7db737cb2cf1fb5aeca7dbca22946506 Mon Sep 17 00:00:00 2001
From: pakrym-oai <pakrym@openai.com>
Date: Thu, 18 Jun 2026 15:05:58 -0700
Subject: [PATCH 14/14] codex: fix CI failure on PR #28814

---
 codex-rs/core/src/client.rs            | 2 +-
 codex-rs/ext/web-search/src/history.rs | 2 +-
 codex-rs/protocol/src/models.rs        | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs
index d8f9df2f5382..fe290c16bf2b 100644
--- a/codex-rs/core/src/client.rs
+++ b/codex-rs/core/src/client.rs
@@ -840,7 +840,7 @@ impl ModelClient {
         }
 
         for item in input {
-            item.set_id(None);
+            item.set_id(/*new_id*/ None);
         }
     }
 
diff --git a/codex-rs/ext/web-search/src/history.rs b/codex-rs/ext/web-search/src/history.rs
index 80bc6dd32405..df3faf123c3a 100644
--- a/codex-rs/ext/web-search/src/history.rs
+++ b/codex-rs/ext/web-search/src/history.rs
@@ -30,7 +30,7 @@ fn push_visible_message(messages: &mut Vec<ResponseItem>, item: &ResponseItem) {
     match item {
         ResponseItem::Message { role, .. } if role == ASSISTANT_ROLE => {
             let mut message = item.clone();
-            message.set_id(None);
+            message.set_id(/*new_id*/ None);
             messages.push(message);
         }
         ResponseItem::AgentMessage {
diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs
index dc97e7c55d0a..b0d373cfb053 100644
--- a/codex-rs/protocol/src/models.rs
+++ b/codex-rs/protocol/src/models.rs
@@ -2136,7 +2136,7 @@ mod tests {
 
         assert_eq!(item.id(), Some("msg_test"));
 
-        item.set_id(None);
+        item.set_id(/*new_id*/ None);
 
         assert_eq!(item.id(), None);
     }
