diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json
index b3349e27d056..01fa1c14bfcc 100644
--- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json
+++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json
@@ -2273,6 +2273,16 @@
               ],
               "writeOnly": true
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "phase": {
               "anyOf": [
                 {
@@ -2313,6 +2323,16 @@
               },
               "type": "array"
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "recipient": {
               "type": "string"
             },
@@ -2351,6 +2371,16 @@
                 "null"
               ]
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "summary": {
               "items": {
                 "$ref": "#/definitions/ReasoningItemReasoningSummary"
@@ -2392,6 +2422,16 @@
               ],
               "writeOnly": true
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "status": {
               "$ref": "#/definitions/LocalShellStatus"
             },
@@ -2426,6 +2466,16 @@
               ],
               "writeOnly": true
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "name": {
               "type": "string"
             },
@@ -2471,6 +2521,16 @@
               ],
               "writeOnly": true
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "status": {
               "type": [
                 "string",
@@ -2498,6 +2558,16 @@
             "call_id": {
               "type": "string"
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "output": {
               "$ref": "#/definitions/FunctionCallOutputBody"
             },
@@ -2532,6 +2602,16 @@
             "input": {
               "type": "string"
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "name": {
               "type": "string"
             },
@@ -2563,6 +2643,16 @@
             "call_id": {
               "type": "string"
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "name": {
               "type": [
                 "string",
@@ -2599,6 +2689,16 @@
             "execution": {
               "type": "string"
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "status": {
               "type": "string"
             },
@@ -2642,6 +2742,16 @@
               ],
               "writeOnly": true
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "status": {
               "type": [
                 "string",
@@ -2667,6 +2777,16 @@
             "id": {
               "type": "string"
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "result": {
               "type": "string"
             },
@@ -2701,6 +2821,16 @@
             "encrypted_content": {
               "type": "string"
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "type": {
               "enum": [
                 "compaction"
@@ -2718,6 +2848,16 @@
         },
         {
           "properties": {
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "type": {
               "enum": [
                 "compaction_trigger"
@@ -2740,6 +2880,16 @@
                 "null"
               ]
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "type": {
               "enum": [
                 "context_compaction"
@@ -2772,6 +2922,17 @@
         }
       ]
     },
+    "ResponseItemMetadata": {
+      "properties": {
+        "turn_id": {
+          "type": [
+            "string",
+            "null"
+          ]
+        }
+      },
+      "type": "object"
+    },
     "ResponsesApiWebSearchAction": {
       "oneOf": [
         {
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 4aca0831c0db..f5a5abfea4cf 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
@@ -14426,6 +14426,16 @@
                 ],
                 "writeOnly": true
               },
+              "metadata": {
+                "anyOf": [
+                  {
+                    "$ref": "#/definitions/v2/ResponseItemMetadata"
+                  },
+                  {
+                    "type": "null"
+                  }
+                ]
+              },
               "phase": {
                 "anyOf": [
                   {
@@ -14466,6 +14476,16 @@
                 },
                 "type": "array"
               },
+              "metadata": {
+                "anyOf": [
+                  {
+                    "$ref": "#/definitions/v2/ResponseItemMetadata"
+                  },
+                  {
+                    "type": "null"
+                  }
+                ]
+              },
               "recipient": {
                 "type": "string"
               },
@@ -14504,6 +14524,16 @@
                   "null"
                 ]
               },
+              "metadata": {
+                "anyOf": [
+                  {
+                    "$ref": "#/definitions/v2/ResponseItemMetadata"
+                  },
+                  {
+                    "type": "null"
+                  }
+                ]
+              },
               "summary": {
                 "items": {
                   "$ref": "#/definitions/v2/ReasoningItemReasoningSummary"
@@ -14545,6 +14575,16 @@
                 ],
                 "writeOnly": true
               },
+              "metadata": {
+                "anyOf": [
+                  {
+                    "$ref": "#/definitions/v2/ResponseItemMetadata"
+                  },
+                  {
+                    "type": "null"
+                  }
+                ]
+              },
               "status": {
                 "$ref": "#/definitions/v2/LocalShellStatus"
               },
@@ -14579,6 +14619,16 @@
                 ],
                 "writeOnly": true
               },
+              "metadata": {
+                "anyOf": [
+                  {
+                    "$ref": "#/definitions/v2/ResponseItemMetadata"
+                  },
+                  {
+                    "type": "null"
+                  }
+                ]
+              },
               "name": {
                 "type": "string"
               },
@@ -14624,6 +14674,16 @@
                 ],
                 "writeOnly": true
               },
+              "metadata": {
+                "anyOf": [
+                  {
+                    "$ref": "#/definitions/v2/ResponseItemMetadata"
+                  },
+                  {
+                    "type": "null"
+                  }
+                ]
+              },
               "status": {
                 "type": [
                   "string",
@@ -14651,6 +14711,16 @@
               "call_id": {
                 "type": "string"
               },
+              "metadata": {
+                "anyOf": [
+                  {
+                    "$ref": "#/definitions/v2/ResponseItemMetadata"
+                  },
+                  {
+                    "type": "null"
+                  }
+                ]
+              },
               "output": {
                 "$ref": "#/definitions/v2/FunctionCallOutputBody"
               },
@@ -14685,6 +14755,16 @@
               "input": {
                 "type": "string"
               },
+              "metadata": {
+                "anyOf": [
+                  {
+                    "$ref": "#/definitions/v2/ResponseItemMetadata"
+                  },
+                  {
+                    "type": "null"
+                  }
+                ]
+              },
               "name": {
                 "type": "string"
               },
@@ -14716,6 +14796,16 @@
               "call_id": {
                 "type": "string"
               },
+              "metadata": {
+                "anyOf": [
+                  {
+                    "$ref": "#/definitions/v2/ResponseItemMetadata"
+                  },
+                  {
+                    "type": "null"
+                  }
+                ]
+              },
               "name": {
                 "type": [
                   "string",
@@ -14752,6 +14842,16 @@
               "execution": {
                 "type": "string"
               },
+              "metadata": {
+                "anyOf": [
+                  {
+                    "$ref": "#/definitions/v2/ResponseItemMetadata"
+                  },
+                  {
+                    "type": "null"
+                  }
+                ]
+              },
               "status": {
                 "type": "string"
               },
@@ -14795,6 +14895,16 @@
                 ],
                 "writeOnly": true
               },
+              "metadata": {
+                "anyOf": [
+                  {
+                    "$ref": "#/definitions/v2/ResponseItemMetadata"
+                  },
+                  {
+                    "type": "null"
+                  }
+                ]
+              },
               "status": {
                 "type": [
                   "string",
@@ -14820,6 +14930,16 @@
               "id": {
                 "type": "string"
               },
+              "metadata": {
+                "anyOf": [
+                  {
+                    "$ref": "#/definitions/v2/ResponseItemMetadata"
+                  },
+                  {
+                    "type": "null"
+                  }
+                ]
+              },
               "result": {
                 "type": "string"
               },
@@ -14854,6 +14974,16 @@
               "encrypted_content": {
                 "type": "string"
               },
+              "metadata": {
+                "anyOf": [
+                  {
+                    "$ref": "#/definitions/v2/ResponseItemMetadata"
+                  },
+                  {
+                    "type": "null"
+                  }
+                ]
+              },
               "type": {
                 "enum": [
                   "compaction"
@@ -14871,6 +15001,16 @@
           },
           {
             "properties": {
+              "metadata": {
+                "anyOf": [
+                  {
+                    "$ref": "#/definitions/v2/ResponseItemMetadata"
+                  },
+                  {
+                    "type": "null"
+                  }
+                ]
+              },
               "type": {
                 "enum": [
                   "compaction_trigger"
@@ -14893,6 +15033,16 @@
                   "null"
                 ]
               },
+              "metadata": {
+                "anyOf": [
+                  {
+                    "$ref": "#/definitions/v2/ResponseItemMetadata"
+                  },
+                  {
+                    "type": "null"
+                  }
+                ]
+              },
               "type": {
                 "enum": [
                   "context_compaction"
@@ -14925,6 +15075,17 @@
           }
         ]
       },
+      "ResponseItemMetadata": {
+        "properties": {
+          "turn_id": {
+            "type": [
+              "string",
+              "null"
+            ]
+          }
+        },
+        "type": "object"
+      },
       "ResponsesApiWebSearchAction": {
         "oneOf": [
           {
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 0249f2e07fdc..6f425c982d9b 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
@@ -10899,6 +10899,16 @@
               ],
               "writeOnly": true
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "phase": {
               "anyOf": [
                 {
@@ -10939,6 +10949,16 @@
               },
               "type": "array"
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "recipient": {
               "type": "string"
             },
@@ -10977,6 +10997,16 @@
                 "null"
               ]
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "summary": {
               "items": {
                 "$ref": "#/definitions/ReasoningItemReasoningSummary"
@@ -11018,6 +11048,16 @@
               ],
               "writeOnly": true
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "status": {
               "$ref": "#/definitions/LocalShellStatus"
             },
@@ -11052,6 +11092,16 @@
               ],
               "writeOnly": true
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "name": {
               "type": "string"
             },
@@ -11097,6 +11147,16 @@
               ],
               "writeOnly": true
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "status": {
               "type": [
                 "string",
@@ -11124,6 +11184,16 @@
             "call_id": {
               "type": "string"
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "output": {
               "$ref": "#/definitions/FunctionCallOutputBody"
             },
@@ -11158,6 +11228,16 @@
             "input": {
               "type": "string"
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "name": {
               "type": "string"
             },
@@ -11189,6 +11269,16 @@
             "call_id": {
               "type": "string"
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "name": {
               "type": [
                 "string",
@@ -11225,6 +11315,16 @@
             "execution": {
               "type": "string"
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "status": {
               "type": "string"
             },
@@ -11268,6 +11368,16 @@
               ],
               "writeOnly": true
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "status": {
               "type": [
                 "string",
@@ -11293,6 +11403,16 @@
             "id": {
               "type": "string"
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "result": {
               "type": "string"
             },
@@ -11327,6 +11447,16 @@
             "encrypted_content": {
               "type": "string"
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "type": {
               "enum": [
                 "compaction"
@@ -11344,6 +11474,16 @@
         },
         {
           "properties": {
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "type": {
               "enum": [
                 "compaction_trigger"
@@ -11366,6 +11506,16 @@
                 "null"
               ]
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "type": {
               "enum": [
                 "context_compaction"
@@ -11398,6 +11548,17 @@
         }
       ]
     },
+    "ResponseItemMetadata": {
+      "properties": {
+        "turn_id": {
+          "type": [
+            "string",
+            "null"
+          ]
+        }
+      },
+      "type": "object"
+    },
     "ResponsesApiWebSearchAction": {
       "oneOf": [
         {
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 2d954061f18a..fdd40dae6106 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json
@@ -384,6 +384,16 @@
               ],
               "writeOnly": true
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "phase": {
               "anyOf": [
                 {
@@ -424,6 +434,16 @@
               },
               "type": "array"
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "recipient": {
               "type": "string"
             },
@@ -462,6 +482,16 @@
                 "null"
               ]
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "summary": {
               "items": {
                 "$ref": "#/definitions/ReasoningItemReasoningSummary"
@@ -503,6 +533,16 @@
               ],
               "writeOnly": true
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "status": {
               "$ref": "#/definitions/LocalShellStatus"
             },
@@ -537,6 +577,16 @@
               ],
               "writeOnly": true
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "name": {
               "type": "string"
             },
@@ -582,6 +632,16 @@
               ],
               "writeOnly": true
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "status": {
               "type": [
                 "string",
@@ -609,6 +669,16 @@
             "call_id": {
               "type": "string"
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "output": {
               "$ref": "#/definitions/FunctionCallOutputBody"
             },
@@ -643,6 +713,16 @@
             "input": {
               "type": "string"
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "name": {
               "type": "string"
             },
@@ -674,6 +754,16 @@
             "call_id": {
               "type": "string"
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "name": {
               "type": [
                 "string",
@@ -710,6 +800,16 @@
             "execution": {
               "type": "string"
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "status": {
               "type": "string"
             },
@@ -753,6 +853,16 @@
               ],
               "writeOnly": true
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "status": {
               "type": [
                 "string",
@@ -778,6 +888,16 @@
             "id": {
               "type": "string"
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "result": {
               "type": "string"
             },
@@ -812,6 +932,16 @@
             "encrypted_content": {
               "type": "string"
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "type": {
               "enum": [
                 "compaction"
@@ -829,6 +959,16 @@
         },
         {
           "properties": {
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "type": {
               "enum": [
                 "compaction_trigger"
@@ -851,6 +991,16 @@
                 "null"
               ]
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "type": {
               "enum": [
                 "context_compaction"
@@ -883,6 +1033,17 @@
         }
       ]
     },
+    "ResponseItemMetadata": {
+      "properties": {
+        "turn_id": {
+          "type": [
+            "string",
+            "null"
+          ]
+        }
+      },
+      "type": "object"
+    },
     "ResponsesApiWebSearchAction": {
       "oneOf": [
         {
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 42505ac24e6b..7ba2a0a64696 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json
@@ -455,6 +455,16 @@
               ],
               "writeOnly": true
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "phase": {
               "anyOf": [
                 {
@@ -495,6 +505,16 @@
               },
               "type": "array"
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "recipient": {
               "type": "string"
             },
@@ -533,6 +553,16 @@
                 "null"
               ]
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "summary": {
               "items": {
                 "$ref": "#/definitions/ReasoningItemReasoningSummary"
@@ -574,6 +604,16 @@
               ],
               "writeOnly": true
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "status": {
               "$ref": "#/definitions/LocalShellStatus"
             },
@@ -608,6 +648,16 @@
               ],
               "writeOnly": true
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "name": {
               "type": "string"
             },
@@ -653,6 +703,16 @@
               ],
               "writeOnly": true
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "status": {
               "type": [
                 "string",
@@ -680,6 +740,16 @@
             "call_id": {
               "type": "string"
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "output": {
               "$ref": "#/definitions/FunctionCallOutputBody"
             },
@@ -714,6 +784,16 @@
             "input": {
               "type": "string"
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "name": {
               "type": "string"
             },
@@ -745,6 +825,16 @@
             "call_id": {
               "type": "string"
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "name": {
               "type": [
                 "string",
@@ -781,6 +871,16 @@
             "execution": {
               "type": "string"
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "status": {
               "type": "string"
             },
@@ -824,6 +924,16 @@
               ],
               "writeOnly": true
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "status": {
               "type": [
                 "string",
@@ -849,6 +959,16 @@
             "id": {
               "type": "string"
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "result": {
               "type": "string"
             },
@@ -883,6 +1003,16 @@
             "encrypted_content": {
               "type": "string"
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "type": {
               "enum": [
                 "compaction"
@@ -900,6 +1030,16 @@
         },
         {
           "properties": {
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "type": {
               "enum": [
                 "compaction_trigger"
@@ -922,6 +1062,16 @@
                 "null"
               ]
             },
+            "metadata": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/ResponseItemMetadata"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
             "type": {
               "enum": [
                 "context_compaction"
@@ -954,6 +1104,17 @@
         }
       ]
     },
+    "ResponseItemMetadata": {
+      "properties": {
+        "turn_id": {
+          "type": [
+            "string",
+            "null"
+          ]
+        }
+      },
+      "type": "object"
+    },
     "ResponsesApiWebSearchAction": {
       "oneOf": [
         {
diff --git a/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts b/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts
index b90963db8020..5bd9a1032bae 100644
--- a/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts
+++ b/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts
@@ -9,10 +9,11 @@ import type { LocalShellStatus } from "./LocalShellStatus";
 import type { MessagePhase } from "./MessagePhase";
 import type { ReasoningItemContent } from "./ReasoningItemContent";
 import type { ReasoningItemReasoningSummary } from "./ReasoningItemReasoningSummary";
+import type { ResponseItemMetadata } from "./ResponseItemMetadata";
 import type { WebSearchAction } from "./WebSearchAction";
 
-export type ResponseItem = { "type": "message", role: string, content: Array<ContentItem>, phase?: MessagePhase, } | { "type": "agent_message", author: string, recipient: string, content: Array<AgentMessageInputContent>, } | { "type": "reasoning", summary: Array<ReasoningItemReasoningSummary>, content?: Array<ReasoningItemContent>, encrypted_content: string | null, } | { "type": "local_shell_call",
+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",
 /**
  * Set when using the Responses API.
  */
-call_id: string | null, status: LocalShellStatus, action: LocalShellAction, } | { "type": "function_call", name: string, namespace?: string, arguments: string, call_id: string, } | { "type": "tool_search_call", call_id: string | null, status?: string, execution: string, arguments: unknown, } | { "type": "function_call_output", call_id: string, output: FunctionCallOutputBody, } | { "type": "custom_tool_call", status?: string, call_id: string, name: string, input: string, } | { "type": "custom_tool_call_output", call_id: string, name?: string, output: FunctionCallOutputBody, } | { "type": "tool_search_output", call_id: string | null, status: string, execution: string, tools: unknown[], } | { "type": "web_search_call", status?: string, action?: WebSearchAction, } | { "type": "image_generation_call", id: string, status: string, revised_prompt?: string, result: string, } | { "type": "compaction", encrypted_content: string, } | { "type": "compaction_trigger" } | { "type": "context_compaction", encrypted_content?: string, } | { "type": "other" };
+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, 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" };
diff --git a/codex-rs/app-server-protocol/schema/typescript/ResponseItemMetadata.ts b/codex-rs/app-server-protocol/schema/typescript/ResponseItemMetadata.ts
new file mode 100644
index 000000000000..f7b69a6dfe17
--- /dev/null
+++ b/codex-rs/app-server-protocol/schema/typescript/ResponseItemMetadata.ts
@@ -0,0 +1,5 @@
+// GENERATED CODE! DO NOT MODIFY BY HAND!
+
+// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
+export type ResponseItemMetadata = { turn_id?: string, };
diff --git a/codex-rs/app-server-protocol/schema/typescript/index.ts b/codex-rs/app-server-protocol/schema/typescript/index.ts
index ad6376fa58b1..c9ad437e587e 100644
--- a/codex-rs/app-server-protocol/schema/typescript/index.ts
+++ b/codex-rs/app-server-protocol/schema/typescript/index.ts
@@ -66,6 +66,7 @@ export type { Resource } from "./Resource";
 export type { ResourceContent } from "./ResourceContent";
 export type { ResourceTemplate } from "./ResourceTemplate";
 export type { ResponseItem } from "./ResponseItem";
+export type { ResponseItemMetadata } from "./ResponseItemMetadata";
 export type { ReviewDecision } from "./ReviewDecision";
 export type { ServerNotification } from "./ServerNotification";
 export type { ServerRequest } from "./ServerRequest";
diff --git a/codex-rs/app-server-protocol/src/protocol/thread_history.rs b/codex-rs/app-server-protocol/src/protocol/thread_history.rs
index f499e568fef2..e19a19e1e325 100644
--- a/codex-rs/app-server-protocol/src/protocol/thread_history.rs
+++ b/codex-rs/app-server-protocol/src/protocol/thread_history.rs
@@ -3381,6 +3381,7 @@ mod tests {
                     text: "plain text".into(),
                 }],
                 phase: None,
+                metadata: None,
             }),
             RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
                 turn_id: "turn-a".into(),
diff --git a/codex-rs/app-server/tests/suite/v2/compaction.rs b/codex-rs/app-server/tests/suite/v2/compaction.rs
index 08ddfa4c6199..e949daa33254 100644
--- a/codex-rs/app-server/tests/suite/v2/compaction.rs
+++ b/codex-rs/app-server/tests/suite/v2/compaction.rs
@@ -136,9 +136,11 @@ async fn auto_compaction_remote_emits_started_and_completed_items() -> Result<()
                 text: "REMOTE_COMPACT_SUMMARY".to_string(),
             }],
             phase: None,
+            metadata: None,
         },
         ResponseItem::Compaction {
             encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(),
+            metadata: None,
         },
     ];
     let compact_mock = responses::mount_compact_json_once(
diff --git a/codex-rs/app-server/tests/suite/v2/thread_inject_items.rs b/codex-rs/app-server/tests/suite/v2/thread_inject_items.rs
index 809358cc8464..21dae50d6f17 100644
--- a/codex-rs/app-server/tests/suite/v2/thread_inject_items.rs
+++ b/codex-rs/app-server/tests/suite/v2/thread_inject_items.rs
@@ -60,6 +60,7 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history() -> Resu
             text: injected_text.to_string(),
         }],
         phase: None,
+        metadata: None,
     };
 
     let inject_req = mcp
@@ -197,6 +198,7 @@ async fn thread_inject_items_adds_raw_response_items_after_a_turn() -> Result<()
             text: "Injected after first turn".to_string(),
         }],
         phase: None,
+        metadata: None,
     };
     let injected_value = serde_json::to_value(&injected_item)?;
 
diff --git a/codex-rs/app-server/tests/suite/v2/thread_resume.rs b/codex-rs/app-server/tests/suite/v2/thread_resume.rs
index cf797ff6be45..e67184bf01d3 100644
--- a/codex-rs/app-server/tests/suite/v2/thread_resume.rs
+++ b/codex-rs/app-server/tests/suite/v2/thread_resume.rs
@@ -2449,6 +2449,7 @@ async fn thread_resume_rejects_history_when_thread_is_running() -> Result<()> {
                     text: "history override".to_string(),
                 }],
                 phase: None,
+                metadata: None,
             }]),
             ..Default::default()
         })
@@ -3456,6 +3457,7 @@ async fn thread_resume_supports_history_and_overrides() -> Result<()> {
             text: history_text.to_string(),
         }],
         phase: None,
+        metadata: None,
     }];
 
     // Resume with explicit history and override the model.
diff --git a/codex-rs/codex-api/src/endpoint/responses_websocket.rs b/codex-rs/codex-api/src/endpoint/responses_websocket.rs
index 202a574167a2..4ea564f139aa 100644
--- a/codex-rs/codex-api/src/endpoint/responses_websocket.rs
+++ b/codex-rs/codex-api/src/endpoint/responses_websocket.rs
@@ -815,6 +815,7 @@ mod tests {
                     text: "hello".to_string(),
                 }],
                 phase: None,
+                metadata: None,
             }],
             tools: vec![json!({
                 "type": "function",
diff --git a/codex-rs/codex-api/src/endpoint/search.rs b/codex-rs/codex-api/src/endpoint/search.rs
index 860f4f283e66..1c231d6bef85 100644
--- a/codex-rs/codex-api/src/endpoint/search.rs
+++ b/codex-rs/codex-api/src/endpoint/search.rs
@@ -161,6 +161,7 @@ mod tests {
                             },
                         ],
                         phase: None,
+                        metadata: None,
                     }])),
                     commands: Some(SearchCommands {
                         search_query: Some(vec![SearchQuery {
diff --git a/codex-rs/codex-api/tests/clients.rs b/codex-rs/codex-api/tests/clients.rs
index 34284634f7c8..9855971f3480 100644
--- a/codex-rs/codex-api/tests/clients.rs
+++ b/codex-rs/codex-api/tests/clients.rs
@@ -424,6 +424,7 @@ async fn azure_default_store_attaches_ids_and_headers() -> Result<()> {
             role: "user".into(),
             content: vec![ContentItem::InputText { text: "hi".into() }],
             phase: None,
+            metadata: None,
         }],
         tools: Vec::new(),
         tool_choice: "auto".into(),
diff --git a/codex-rs/context-fragments/src/fragment.rs b/codex-rs/context-fragments/src/fragment.rs
index bd0e06772b09..38714e251244 100644
--- a/codex-rs/context-fragments/src/fragment.rs
+++ b/codex-rs/context-fragments/src/fragment.rs
@@ -83,6 +83,7 @@ pub trait ContextualUserFragment {
                 text: self.render(),
             }],
             phase: None,
+            metadata: None,
         }
     }
 
@@ -94,6 +95,7 @@ pub trait ContextualUserFragment {
                 text: self.render(),
             }],
             phase: None,
+            metadata: None,
         }
     }
 
diff --git a/codex-rs/core/src/agent/control/spawn.rs b/codex-rs/core/src/agent/control/spawn.rs
index dcfa1b83a9ed..9c12d60e6abf 100644
--- a/codex-rs/core/src/agent/control/spawn.rs
+++ b/codex-rs/core/src/agent/control/spawn.rs
@@ -51,7 +51,7 @@ fn keep_forked_rollout_item(item: &RolloutItem, preserve_reference_context_item:
             | ResponseItem::WebSearchCall { .. }
             | ResponseItem::ImageGenerationCall { .. }
             | ResponseItem::Compaction { .. }
-            | ResponseItem::CompactionTrigger
+            | ResponseItem::CompactionTrigger { .. }
             | ResponseItem::ContextCompaction { .. }
             | ResponseItem::Other,
         ) => false,
diff --git a/codex-rs/core/src/agent/control_tests.rs b/codex-rs/core/src/agent/control_tests.rs
index ac42984b5857..410e1beaf685 100644
--- a/codex-rs/core/src/agent/control_tests.rs
+++ b/codex-rs/core/src/agent/control_tests.rs
@@ -71,6 +71,7 @@ fn assistant_message(text: &str, phase: Option<MessagePhase>) -> ResponseItem {
             text: text.to_string(),
         }],
         phase,
+        metadata: None,
     }
 }
 
@@ -90,6 +91,7 @@ fn spawn_agent_call(call_id: &str) -> ResponseItem {
         namespace: None,
         arguments: "{}".to_string(),
         call_id: call_id.to_string(),
+        metadata: None,
     }
 }
 
@@ -861,6 +863,7 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
                         text: "Parent root guidance.".to_string(),
                     }],
                     phase: None,
+                    metadata: None,
                 },
                 ResponseItem::Message {
                     id: None,
@@ -869,6 +872,7 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
                         text: "Parent subagent guidance.".to_string(),
                     }],
                     phase: None,
+                    metadata: None,
                 },
                 assistant_message("parent commentary", Some(MessagePhase::Commentary)),
                 assistant_message("parent final answer", Some(MessagePhase::FinalAnswer)),
@@ -878,6 +882,7 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
                     summary: Vec::new(),
                     content: None,
                     encrypted_content: None,
+                    metadata: None,
                 },
                 trigger_message.to_response_input_item().into(),
                 spawn_agent_call(&parent_spawn_call_id),
@@ -903,7 +908,6 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
         .flush_rollout()
         .await
         .expect("parent rollout should flush");
-
     let child_thread_id = harness
         .control
         .spawn_agent_with_metadata(
@@ -941,6 +945,7 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
                 text: "parent seed context".to_string(),
             }],
             phase: None,
+            metadata: None,
         },
         assistant_message("parent final answer", Some(MessagePhase::FinalAnswer)),
         ResponseItem::Message {
@@ -950,6 +955,7 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
                 text: "Child subagent guidance.".to_string(),
             }],
             phase: None,
+            metadata: None,
         },
     ];
     assert_eq!(
@@ -1080,6 +1086,7 @@ async fn spawn_agent_fork_strips_parent_usage_hints_from_compacted_history() {
                 text: "compacted parent summary".to_string(),
             }],
             phase: None,
+            metadata: None,
         },
         ResponseItem::Message {
             id: None,
@@ -1088,6 +1095,7 @@ async fn spawn_agent_fork_strips_parent_usage_hints_from_compacted_history() {
                 text: "Parent root guidance.".to_string(),
             }],
             phase: None,
+            metadata: None,
         },
     ];
     parent_thread
@@ -1385,6 +1393,7 @@ async fn spawn_agent_fork_last_n_turns_drops_parent_startup_prefix_when_under_li
                     text: "parent startup developer context".to_string(),
                 }],
                 phase: None,
+                metadata: None,
             }],
         )
         .await;
@@ -1506,6 +1515,7 @@ async fn spawn_agent_fork_last_n_turns_strips_parent_usage_hints() {
                         text: "Parent root guidance.".to_string(),
                     }],
                     phase: None,
+                    metadata: None,
                 },
                 spawn_agent_call(&parent_spawn_call_id),
             ],
diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs
index 2c8b380bacc6..4ea8c0ff1fb8 100644
--- a/codex-rs/core/src/client.rs
+++ b/codex-rs/core/src/client.rs
@@ -782,7 +782,10 @@ impl ModelClient {
         responses_metadata: &CodexResponsesMetadata,
     ) -> Result<ResponsesApiRequest> {
         let instructions = &prompt.base_instructions.text;
-        let input = prompt.get_formatted_input_for_request(model_info.use_responses_lite);
+        let mut input = prompt.get_formatted_input_for_request(model_info.use_responses_lite);
+        if !self.state.provider.info().is_openai() {
+            input.iter_mut().for_each(ResponseItem::clear_metadata);
+        }
         let tools = create_tools_json_for_responses_api(&prompt.tools)?;
         let reasoning = Self::build_reasoning(model_info, effort, summary);
         let include = if reasoning.is_some() {
@@ -1057,9 +1060,15 @@ impl ModelClientSession {
             trace!("incremental request failed, items didn't match");
             return None;
         };
-        let response_items =
-            last_response.map_or(&[][..], |response| response.items_added.as_slice());
-        let Some(incremental_items) = after_previous_input.strip_prefix(response_items) else {
+        let mut response_items =
+            last_response.map_or_else(Vec::new, |response| response.items_added.clone());
+        if !self.client.state.provider.info().is_openai() {
+            response_items
+                .iter_mut()
+                .for_each(ResponseItem::clear_metadata);
+        }
+        let Some(incremental_items) = after_previous_input.strip_prefix(response_items.as_slice())
+        else {
             trace!("incremental request failed, items didn't match");
             return None;
         };
diff --git a/codex-rs/core/src/client_common.rs b/codex-rs/core/src/client_common.rs
index 71d102598d73..624501db462c 100644
--- a/codex-rs/core/src/client_common.rs
+++ b/codex-rs/core/src/client_common.rs
@@ -98,7 +98,7 @@ fn strip_image_details(items: &mut [ResponseItem]) {
             | ResponseItem::WebSearchCall { .. }
             | ResponseItem::ImageGenerationCall { .. }
             | ResponseItem::Compaction { .. }
-            | ResponseItem::CompactionTrigger
+            | ResponseItem::CompactionTrigger { .. }
             | ResponseItem::ContextCompaction { .. }
             | ResponseItem::Other => {}
         }
diff --git a/codex-rs/core/src/client_common_tests.rs b/codex-rs/core/src/client_common_tests.rs
index 4b218e490cff..b71acc398984 100644
--- a/codex-rs/core/src/client_common_tests.rs
+++ b/codex-rs/core/src/client_common_tests.rs
@@ -20,6 +20,7 @@ fn prompt_with_image_outputs() -> Prompt {
                     detail: Some(ImageDetail::Original),
                 }],
                 phase: None,
+                metadata: None,
             },
             ResponseItem::FunctionCallOutput {
                 call_id: "function-call".to_string(),
@@ -29,6 +30,7 @@ fn prompt_with_image_outputs() -> Prompt {
                         detail: Some(ImageDetail::High),
                     },
                 ]),
+                metadata: None,
             },
             ResponseItem::CustomToolCallOutput {
                 call_id: "custom-call".to_string(),
@@ -39,6 +41,7 @@ fn prompt_with_image_outputs() -> Prompt {
                         detail: Some(ImageDetail::Auto),
                     },
                 ]),
+                metadata: None,
             },
         ],
         ..Default::default()
@@ -63,6 +66,7 @@ fn responses_lite_request_copies_strip_image_details() {
                     detail: None,
                 }],
                 phase: None,
+                metadata: None,
             },
             ResponseItem::FunctionCallOutput {
                 call_id: "function-call".to_string(),
@@ -72,6 +76,7 @@ fn responses_lite_request_copies_strip_image_details() {
                         detail: None,
                     },
                 ]),
+                metadata: None,
             },
             ResponseItem::CustomToolCallOutput {
                 call_id: "custom-call".to_string(),
@@ -82,6 +87,7 @@ fn responses_lite_request_copies_strip_image_details() {
                         detail: None,
                     },
                 ]),
+                metadata: None,
             },
         ]
     );
diff --git a/codex-rs/core/src/client_tests.rs b/codex-rs/core/src/client_tests.rs
index 29f397d5cebc..06074a5d2024 100644
--- a/codex-rs/core/src/client_tests.rs
+++ b/codex-rs/core/src/client_tests.rs
@@ -225,6 +225,7 @@ fn output_message(id: &str, text: &str) -> ResponseItem {
             text: text.to_string(),
         }],
         phase: None,
+        metadata: None,
     }
 }
 
diff --git a/codex-rs/core/src/codex_delegate_tests.rs b/codex-rs/core/src/codex_delegate_tests.rs
index c881823d8044..f97e81c6e8f5 100644
--- a/codex-rs/core/src/codex_delegate_tests.rs
+++ b/codex-rs/core/src/codex_delegate_tests.rs
@@ -81,6 +81,7 @@ async fn forward_events_cancelled_while_send_blocked_shuts_down_delegate() {
                     call_id: "call-1".to_string(),
                     name: "tool".to_string(),
                     input: "{}".to_string(),
+                    metadata: None,
                 },
             }),
         })
diff --git a/codex-rs/core/src/codex_thread.rs b/codex-rs/core/src/codex_thread.rs
index 3b8d972d761c..451b497827bd 100644
--- a/codex-rs/core/src/codex_thread.rs
+++ b/codex-rs/core/src/codex_thread.rs
@@ -437,6 +437,7 @@ impl CodexThread {
             role: "user".to_string(),
             content: vec![ContentItem::InputText { text: message }],
             phase: None,
+            metadata: None,
         };
         self.codex
             .session
diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index 6ffb779d3899..52b2ef4ef501 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -32,6 +32,7 @@ use codex_protocol::items::TurnItem;
 use codex_protocol::models::ContentItem;
 use codex_protocol::models::ResponseInputItem;
 use codex_protocol::models::ResponseItem;
+use codex_protocol::models::ResponseItemMetadata;
 use codex_protocol::protocol::CompactedItem;
 use codex_protocol::protocol::EventMsg;
 use codex_protocol::protocol::TurnStartedEvent;
@@ -443,7 +444,13 @@ pub fn content_items_to_text(content: &[ContentItem]) -> Option<String> {
     }
 }
 
-pub(crate) fn collect_user_messages(items: &[ResponseItem]) -> Vec<String> {
+#[derive(Clone, Debug, PartialEq)]
+pub(crate) struct CompactedUserMessage {
+    message: String,
+    metadata: Option<ResponseItemMetadata>,
+}
+
+pub(crate) fn collect_user_messages(items: &[ResponseItem]) -> Vec<CompactedUserMessage> {
     items
         .iter()
         .filter_map(|item| match crate::event_mapping::parse_turn_item(item) {
@@ -451,7 +458,13 @@ pub(crate) fn collect_user_messages(items: &[ResponseItem]) -> Vec<String> {
                 if is_summary_message(&user.message()) {
                     None
                 } else {
-                    Some(user.message())
+                    Some(CompactedUserMessage {
+                        message: user.message(),
+                        metadata: match item {
+                            ResponseItem::Message { metadata, .. } => metadata.clone(),
+                            _ => None,
+                        },
+                    })
                 }
             }
             _ => None,
@@ -522,7 +535,7 @@ pub(crate) fn insert_initial_context_before_last_real_user_or_summary(
 
 pub(crate) fn build_compacted_history(
     initial_context: Vec<ResponseItem>,
-    user_messages: &[String],
+    user_messages: &[CompactedUserMessage],
     summary_text: &str,
 ) -> Vec<ResponseItem> {
     build_compacted_history_with_limit(
@@ -535,24 +548,28 @@ pub(crate) fn build_compacted_history(
 
 fn build_compacted_history_with_limit(
     mut history: Vec<ResponseItem>,
-    user_messages: &[String],
+    user_messages: &[CompactedUserMessage],
     summary_text: &str,
     max_tokens: usize,
 ) -> Vec<ResponseItem> {
-    let mut selected_messages: Vec<String> = Vec::new();
+    let mut selected_messages: Vec<CompactedUserMessage> = Vec::new();
     if max_tokens > 0 {
         let mut remaining = max_tokens;
         for message in user_messages.iter().rev() {
             if remaining == 0 {
                 break;
             }
-            let tokens = approx_token_count(message);
+            let tokens = approx_token_count(&message.message);
             if tokens <= remaining {
                 selected_messages.push(message.clone());
                 remaining = remaining.saturating_sub(tokens);
             } else {
-                let truncated = truncate_text(message, TruncationPolicy::Tokens(remaining));
-                selected_messages.push(truncated);
+                let truncated =
+                    truncate_text(&message.message, TruncationPolicy::Tokens(remaining));
+                selected_messages.push(CompactedUserMessage {
+                    message: truncated,
+                    metadata: message.metadata.clone(),
+                });
                 break;
             }
         }
@@ -564,9 +581,10 @@ fn build_compacted_history_with_limit(
             id: None,
             role: "user".to_string(),
             content: vec![ContentItem::InputText {
-                text: message.clone(),
+                text: message.message.clone(),
             }],
             phase: None,
+            metadata: message.metadata.clone(),
         });
     }
 
@@ -581,6 +599,7 @@ fn build_compacted_history_with_limit(
         role: "user".to_string(),
         content: vec![ContentItem::InputText { text: summary_text }],
         phase: None,
+        metadata: None,
     });
 
     history
diff --git a/codex-rs/core/src/compact_remote.rs b/codex-rs/core/src/compact_remote.rs
index a3da98cc8ecf..eca14af80e3b 100644
--- a/codex-rs/core/src/compact_remote.rs
+++ b/codex-rs/core/src/compact_remote.rs
@@ -344,7 +344,7 @@ pub(crate) fn should_keep_compacted_history_item(item: &ResponseItem) -> bool {
         ResponseItem::Message { .. } => false,
         ResponseItem::AgentMessage { .. } => true,
         ResponseItem::Compaction { .. } | ResponseItem::ContextCompaction { .. } => true,
-        ResponseItem::CompactionTrigger => false,
+        ResponseItem::CompactionTrigger { .. } => false,
         ResponseItem::Reasoning { .. }
         | ResponseItem::LocalShellCall { .. }
         | ResponseItem::FunctionCall { .. }
@@ -403,29 +403,38 @@ pub(crate) fn trim_function_call_history_to_fit_context_window(
 
 fn rewritten_output_for_context_window(item: &ResponseItem) -> Option<ResponseItem> {
     Some(match item {
-        ResponseItem::FunctionCallOutput { call_id, output } => ResponseItem::FunctionCallOutput {
+        ResponseItem::FunctionCallOutput {
+            call_id,
+            output,
+            metadata,
+        } => ResponseItem::FunctionCallOutput {
             call_id: call_id.clone(),
             output: truncated_output_payload(output),
+            metadata: metadata.clone(),
         },
         ResponseItem::CustomToolCallOutput {
             call_id,
             name,
             output,
+            metadata,
         } => ResponseItem::CustomToolCallOutput {
             call_id: call_id.clone(),
             name: name.clone(),
             output: truncated_output_payload(output),
+            metadata: metadata.clone(),
         },
         ResponseItem::ToolSearchOutput {
             call_id,
             status,
             execution,
+            metadata,
             ..
         } => ResponseItem::ToolSearchOutput {
             call_id: call_id.clone(),
             status: status.clone(),
             execution: execution.clone(),
             tools: Vec::new(),
+            metadata: metadata.clone(),
         },
         _ => return None,
     })
diff --git a/codex-rs/core/src/compact_remote_v2.rs b/codex-rs/core/src/compact_remote_v2.rs
index 047c2527cb82..d13aa9406e38 100644
--- a/codex-rs/core/src/compact_remote_v2.rs
+++ b/codex-rs/core/src/compact_remote_v2.rs
@@ -231,7 +231,7 @@ async fn run_remote_compact_task_inner_impl(
     )
     .await?;
     let mut input = prompt_input.clone();
-    input.push(ResponseItem::CompactionTrigger);
+    input.push(ResponseItem::CompactionTrigger { metadata: None });
     let prompt = Prompt {
         input,
         tools: tool_router.model_visible_specs(),
@@ -515,6 +515,7 @@ fn truncate_message_text_to_token_budget(
         role,
         content,
         phase,
+        metadata,
     } = item
     else {
         return Some(item);
@@ -553,6 +554,7 @@ fn truncate_message_text_to_token_budget(
         role,
         content: truncated_content,
         phase,
+        metadata,
     })
 }
 
@@ -573,6 +575,7 @@ mod tests {
                 text: text.to_string(),
             }],
             phase,
+            metadata: None,
         }
     }
 
@@ -604,13 +607,16 @@ mod tests {
                 namespace: None,
                 arguments: "{}".to_string(),
                 call_id: "call_1".to_string(),
+                metadata: None,
             },
             ResponseItem::Compaction {
                 encrypted_content: "old".to_string(),
+                metadata: None,
             },
         ];
         let output = ResponseItem::Compaction {
             encrypted_content: "new".to_string(),
+            metadata: None,
         };
 
         let (history, _) = build_v2_compacted_history(&input, output.clone());
@@ -638,6 +644,7 @@ mod tests {
         ];
         let output = ResponseItem::Compaction {
             encrypted_content: "new".to_string(),
+            metadata: None,
         };
 
         let (history, _) = build_v2_compacted_history(&input, output.clone());
@@ -664,9 +671,11 @@ mod tests {
                 },
             ],
             phase: None,
+            metadata: None,
         }];
         let output = ResponseItem::Compaction {
             encrypted_content: "new".to_string(),
+            metadata: None,
         };
 
         let (_, retained_image_count) = build_v2_compacted_history(&input, output);
@@ -714,6 +723,7 @@ mod tests {
                 },
             ],
             phase: None,
+            metadata: None,
         };
 
         let truncated =
@@ -737,6 +747,7 @@ mod tests {
                     },
                 ],
                 phase: None,
+                metadata: None,
             }]
         );
     }
@@ -751,6 +762,7 @@ mod tests {
                 detail: None,
             }],
             phase: None,
+            metadata: None,
         };
         let newest = message("user", "new", /*phase*/ None);
         let retained = vec![
@@ -775,6 +787,7 @@ mod tests {
                 detail: None,
             }],
             phase: None,
+            metadata: None,
         };
         let newest = message("user", "new", /*phase*/ None);
         let retained = vec![image_only_message, newest.clone()];
@@ -789,6 +802,7 @@ mod tests {
     async fn collect_compaction_output_accepts_additional_output_items() {
         let compaction = ResponseItem::Compaction {
             encrypted_content: "encrypted".to_string(),
+            metadata: None,
         };
         let stream = response_stream(vec![
             Ok(ResponseEvent::OutputItemDone(message(
diff --git a/codex-rs/core/src/compact_tests.rs b/codex-rs/core/src/compact_tests.rs
index 550a7ec8467c..a52e70a7ba1e 100644
--- a/codex-rs/core/src/compact_tests.rs
+++ b/codex-rs/core/src/compact_tests.rs
@@ -2,6 +2,7 @@ use super::*;
 use codex_model_provider_info::ModelProviderInfo;
 use codex_model_provider_info::WireApi;
 use codex_protocol::models::DEFAULT_IMAGE_DETAIL;
+use codex_protocol::models::ResponseItemMetadata;
 use pretty_assertions::assert_eq;
 
 async fn process_compacted_history_with_test_session(
@@ -31,6 +32,14 @@ fn user_message(text: &str) -> ResponseItem {
             text: text.to_string(),
         }],
         phase: None,
+        metadata: None,
+    }
+}
+
+fn compacted_user_message(text: &str) -> CompactedUserMessage {
+    CompactedUserMessage {
+        message: text.to_string(),
+        metadata: None,
     }
 }
 
@@ -75,6 +84,7 @@ fn collect_user_messages_extracts_user_text_only() {
                 text: "ignored".to_string(),
             }],
             phase: None,
+            metadata: None,
         },
         ResponseItem::Message {
             id: Some("user".to_string()),
@@ -83,13 +93,14 @@ fn collect_user_messages_extracts_user_text_only() {
                 text: "first".to_string(),
             }],
             phase: None,
+            metadata: None,
         },
         ResponseItem::Other,
     ];
 
     let collected = collect_user_messages(&items);
 
-    assert_eq!(vec!["first".to_string()], collected);
+    assert_eq!(vec![compacted_user_message("first")], collected);
 }
 
 #[test]
@@ -107,6 +118,7 @@ do things
                     .to_string(),
             }],
             phase: None,
+            metadata: None,
         },
         ResponseItem::Message {
             id: None,
@@ -115,6 +127,7 @@ do things
                 text: "<ENVIRONMENT_CONTEXT>cwd=/tmp</ENVIRONMENT_CONTEXT>".to_string(),
             }],
             phase: None,
+            metadata: None,
         },
         ResponseItem::Message {
             id: None,
@@ -123,12 +136,13 @@ do things
                 text: "real user message".to_string(),
             }],
             phase: None,
+            metadata: None,
         },
     ];
 
     let collected = collect_user_messages(&items);
 
-    assert_eq!(vec!["real user message".to_string()], collected);
+    assert_eq!(vec![compacted_user_message("real user message")], collected);
 }
 
 #[test]
@@ -148,7 +162,7 @@ fn collect_user_messages_filters_legacy_warnings() {
 
     let collected = collect_user_messages(&items);
 
-    assert_eq!(vec!["real user message".to_string()], collected);
+    assert_eq!(vec![compacted_user_message("real user message")], collected);
 }
 
 #[test]
@@ -157,9 +171,10 @@ fn build_token_limited_compacted_history_truncates_overlong_user_messages() {
     // that oversized user content is truncated.
     let max_tokens = 16;
     let big = "word ".repeat(200);
+    let user_message = compacted_user_message(&big);
     let history = super::build_compacted_history_with_limit(
         Vec::new(),
-        std::slice::from_ref(&big),
+        std::slice::from_ref(&user_message),
         "SUMMARY",
         max_tokens,
     );
@@ -196,7 +211,7 @@ fn build_token_limited_compacted_history_truncates_overlong_user_messages() {
 #[test]
 fn build_token_limited_compacted_history_appends_summary_message() {
     let initial_context: Vec<ResponseItem> = Vec::new();
-    let user_messages = vec!["first user message".to_string()];
+    let user_messages = vec![compacted_user_message("first user message")];
     let summary_text = "summary text";
 
     let history = build_compacted_history(initial_context, &user_messages, summary_text);
@@ -215,6 +230,23 @@ fn build_token_limited_compacted_history_appends_summary_message() {
     assert_eq!(summary, summary_text);
 }
 
+#[test]
+fn build_compacted_history_preserves_user_message_metadata() {
+    let history = build_compacted_history(
+        Vec::new(),
+        &[CompactedUserMessage {
+            message: "first user message".to_string(),
+            metadata: Some(ResponseItemMetadata {
+                turn_id: Some("turn-1".to_string()),
+            }),
+        }],
+        "summary text",
+    );
+
+    assert_eq!(history[0].turn_id(), Some("turn-1"));
+    assert_eq!(history[1].turn_id(), None);
+}
+
 #[test]
 fn should_use_remote_compact_task_for_azure_provider() {
     let provider = ModelProviderInfo {
@@ -249,6 +281,7 @@ async fn process_compacted_history_replaces_developer_messages() {
                 text: "stale permissions".to_string(),
             }],
             phase: None,
+            metadata: None,
         },
         ResponseItem::Message {
             id: None,
@@ -257,6 +290,7 @@ async fn process_compacted_history_replaces_developer_messages() {
                 text: "summary".to_string(),
             }],
             phase: None,
+            metadata: None,
         },
         ResponseItem::Message {
             id: None,
@@ -265,6 +299,7 @@ async fn process_compacted_history_replaces_developer_messages() {
                 text: "stale personality".to_string(),
             }],
             phase: None,
+            metadata: None,
         },
     ];
     let (refreshed, mut expected) = process_compacted_history_with_test_session(
@@ -279,6 +314,7 @@ async fn process_compacted_history_replaces_developer_messages() {
             text: "summary".to_string(),
         }],
         phase: None,
+        metadata: None,
     });
     assert_eq!(refreshed, expected);
 }
@@ -292,6 +328,7 @@ async fn process_compacted_history_reinjects_full_initial_context() {
             text: "summary".to_string(),
         }],
         phase: None,
+        metadata: None,
     }];
     let (refreshed, mut expected) = process_compacted_history_with_test_session(
         compacted_history,
@@ -305,6 +342,7 @@ async fn process_compacted_history_reinjects_full_initial_context() {
             text: "summary".to_string(),
         }],
         phase: None,
+        metadata: None,
     });
     assert_eq!(refreshed, expected);
 }
@@ -324,6 +362,7 @@ keep me updated
                     .to_string(),
             }],
             phase: None,
+            metadata: None,
         },
         ResponseItem::Message {
             id: None,
@@ -336,6 +375,7 @@ keep me updated
                     .to_string(),
             }],
             phase: None,
+            metadata: None,
         },
         ResponseItem::Message {
             id: None,
@@ -348,6 +388,7 @@ keep me updated
                     .to_string(),
             }],
             phase: None,
+            metadata: None,
         },
         ResponseItem::Message {
             id: None,
@@ -356,6 +397,7 @@ keep me updated
                 text: "summary".to_string(),
             }],
             phase: None,
+            metadata: None,
         },
         ResponseItem::Message {
             id: None,
@@ -364,6 +406,7 @@ keep me updated
                 text: "stale developer instructions".to_string(),
             }],
             phase: None,
+            metadata: None,
         },
     ];
     let (refreshed, mut expected) = process_compacted_history_with_test_session(
@@ -378,6 +421,7 @@ keep me updated
             text: "summary".to_string(),
         }],
         phase: None,
+        metadata: None,
     });
     assert_eq!(refreshed, expected);
 }
@@ -417,6 +461,7 @@ async fn process_compacted_history_inserts_context_before_last_real_user_message
                 text: "older user".to_string(),
             }],
             phase: None,
+            metadata: None,
         },
         ResponseItem::Message {
             id: None,
@@ -425,6 +470,7 @@ async fn process_compacted_history_inserts_context_before_last_real_user_message
                 text: format!("{SUMMARY_PREFIX}\nsummary text"),
             }],
             phase: None,
+            metadata: None,
         },
         ResponseItem::Message {
             id: None,
@@ -433,6 +479,7 @@ async fn process_compacted_history_inserts_context_before_last_real_user_message
                 text: "latest user".to_string(),
             }],
             phase: None,
+            metadata: None,
         },
     ];
 
@@ -449,6 +496,7 @@ async fn process_compacted_history_inserts_context_before_last_real_user_message
                 text: "older user".to_string(),
             }],
             phase: None,
+            metadata: None,
         },
         ResponseItem::Message {
             id: None,
@@ -457,6 +505,7 @@ async fn process_compacted_history_inserts_context_before_last_real_user_message
                 text: format!("{SUMMARY_PREFIX}\nsummary text"),
             }],
             phase: None,
+            metadata: None,
         },
     ];
     expected.extend(initial_context);
@@ -467,6 +516,7 @@ async fn process_compacted_history_inserts_context_before_last_real_user_message
             text: "latest user".to_string(),
         }],
         phase: None,
+        metadata: None,
     });
     assert_eq!(refreshed, expected);
 }
@@ -480,6 +530,7 @@ async fn process_compacted_history_reinjects_model_switch_message() {
             text: "summary".to_string(),
         }],
         phase: None,
+        metadata: None,
     }];
     let previous_turn_settings = PreviousTurnSettings {
         model: "previous-regular-model".to_string(),
@@ -510,6 +561,7 @@ async fn process_compacted_history_reinjects_model_switch_message() {
             text: "summary".to_string(),
         }],
         phase: None,
+        metadata: None,
     });
     assert_eq!(refreshed, expected);
 }
@@ -524,6 +576,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
                 text: "older user".to_string(),
             }],
             phase: None,
+            metadata: None,
         },
         ResponseItem::Message {
             id: None,
@@ -532,6 +585,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
                 text: "latest user".to_string(),
             }],
             phase: None,
+            metadata: None,
         },
         ResponseItem::Message {
             id: None,
@@ -540,6 +594,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
                 text: format!("{SUMMARY_PREFIX}\nsummary text"),
             }],
             phase: None,
+            metadata: None,
         },
     ];
     let initial_context = vec![ResponseItem::Message {
@@ -549,6 +604,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
             text: "fresh permissions".to_string(),
         }],
         phase: None,
+        metadata: None,
     }];
 
     let refreshed =
@@ -561,6 +617,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
                 text: "older user".to_string(),
             }],
             phase: None,
+            metadata: None,
         },
         ResponseItem::Message {
             id: None,
@@ -569,6 +626,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
                 text: "fresh permissions".to_string(),
             }],
             phase: None,
+            metadata: None,
         },
         ResponseItem::Message {
             id: None,
@@ -577,6 +635,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
                 text: "latest user".to_string(),
             }],
             phase: None,
+            metadata: None,
         },
         ResponseItem::Message {
             id: None,
@@ -585,6 +644,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
                 text: format!("{SUMMARY_PREFIX}\nsummary text"),
             }],
             phase: None,
+            metadata: None,
         },
     ];
     assert_eq!(refreshed, expected);
@@ -594,6 +654,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
 fn insert_initial_context_before_last_real_user_or_summary_keeps_compaction_last() {
     let compacted_history = vec![ResponseItem::Compaction {
         encrypted_content: "encrypted".to_string(),
+        metadata: None,
     }];
     let initial_context = vec![ResponseItem::Message {
         id: None,
@@ -602,6 +663,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_compaction_last
             text: "fresh permissions".to_string(),
         }],
         phase: None,
+        metadata: None,
     }];
 
     let refreshed =
@@ -614,9 +676,11 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_compaction_last
                 text: "fresh permissions".to_string(),
             }],
             phase: None,
+            metadata: None,
         },
         ResponseItem::Compaction {
             encrypted_content: "encrypted".to_string(),
+            metadata: None,
         },
     ];
     assert_eq!(refreshed, expected);
diff --git a/codex-rs/core/src/context_manager/history.rs b/codex-rs/core/src/context_manager/history.rs
index 479ccc89c6c6..47f37a32f9c7 100644
--- a/codex-rs/core/src/context_manager/history.rs
+++ b/codex-rs/core/src/context_manager/history.rs
@@ -338,23 +338,25 @@ impl ContextManager {
     fn process_item(&self, item: &ResponseItem, policy: TruncationPolicy) -> ResponseItem {
         let policy_with_serialization_budget = policy * 1.2;
         match item {
-            ResponseItem::FunctionCallOutput { call_id, output } => {
-                ResponseItem::FunctionCallOutput {
-                    call_id: call_id.clone(),
-                    output: truncate_function_output_payload(
-                        output,
-                        policy_with_serialization_budget,
-                    ),
-                }
-            }
+            ResponseItem::FunctionCallOutput {
+                call_id,
+                output,
+                metadata,
+            } => ResponseItem::FunctionCallOutput {
+                call_id: call_id.clone(),
+                output: truncate_function_output_payload(output, policy_with_serialization_budget),
+                metadata: metadata.clone(),
+            },
             ResponseItem::CustomToolCallOutput {
                 call_id,
                 name,
                 output,
+                metadata,
             } => ResponseItem::CustomToolCallOutput {
                 call_id: call_id.clone(),
                 name: name.clone(),
                 output: truncate_function_output_payload(output, policy_with_serialization_budget),
+                metadata: metadata.clone(),
             },
             ResponseItem::Message { .. }
             | ResponseItem::AgentMessage { .. }
@@ -367,7 +369,7 @@ impl ContextManager {
             | ResponseItem::ImageGenerationCall { .. }
             | ResponseItem::CustomToolCall { .. }
             | ResponseItem::Compaction { .. }
-            | ResponseItem::CompactionTrigger
+            | ResponseItem::CompactionTrigger { .. }
             | ResponseItem::ContextCompaction { .. }
             | ResponseItem::Other => item.clone(),
         }
@@ -459,7 +461,7 @@ fn is_api_message(message: &ResponseItem) -> bool {
         | ResponseItem::ImageGenerationCall { .. }
         | ResponseItem::Compaction { .. }
         | ResponseItem::ContextCompaction { .. } => true,
-        ResponseItem::CompactionTrigger => false,
+        ResponseItem::CompactionTrigger { .. } => false,
         ResponseItem::Other => false,
     }
 }
@@ -511,9 +513,11 @@ fn estimate_response_item_model_visible_bytes(item: &ResponseItem) -> i64 {
         }
         | ResponseItem::Compaction {
             encrypted_content: content,
+            ..
         }
         | ResponseItem::ContextCompaction {
             encrypted_content: Some(content),
+            ..
         } => i64::try_from(estimate_reasoning_length(content.len())).unwrap_or(i64::MAX),
         item => {
             let raw = serde_json::to_string(item)
@@ -689,7 +693,7 @@ fn is_model_generated_item(item: &ResponseItem) -> bool {
         | ResponseItem::LocalShellCall { .. }
         | ResponseItem::Compaction { .. }
         | ResponseItem::ContextCompaction { .. } => true,
-        ResponseItem::CompactionTrigger => false,
+        ResponseItem::CompactionTrigger { .. } => false,
         ResponseItem::FunctionCallOutput { .. }
         | ResponseItem::ToolSearchOutput { .. }
         | ResponseItem::CustomToolCallOutput { .. }
diff --git a/codex-rs/core/src/context_manager/history_tests.rs b/codex-rs/core/src/context_manager/history_tests.rs
index f4bc87f5b8e8..a98061b5b097 100644
--- a/codex-rs/core/src/context_manager/history_tests.rs
+++ b/codex-rs/core/src/context_manager/history_tests.rs
@@ -14,6 +14,7 @@ use codex_protocol::models::LocalShellExecAction;
 use codex_protocol::models::LocalShellStatus;
 use codex_protocol::models::ReasoningItemContent;
 use codex_protocol::models::ReasoningItemReasoningSummary;
+use codex_protocol::models::ResponseItemMetadata;
 use codex_protocol::openai_models::InputModality;
 use codex_protocol::openai_models::default_input_modalities;
 use codex_protocol::protocol::AskForApproval;
@@ -41,6 +42,7 @@ fn assistant_msg(text: &str) -> ResponseItem {
             text: text.to_string(),
         }],
         phase: None,
+        metadata: None,
     }
 }
 
@@ -59,6 +61,7 @@ fn inter_agent_assistant_msg(text: &str) -> ResponseItem {
             text: serde_json::to_string(&communication).unwrap(),
         }],
         phase: None,
+        metadata: None,
     }
 }
 
@@ -78,6 +81,7 @@ fn user_msg(text: &str) -> ResponseItem {
             text: text.to_string(),
         }],
         phase: None,
+        metadata: None,
     }
 }
 
@@ -89,6 +93,7 @@ fn user_input_text_msg(text: &str) -> ResponseItem {
             text: text.to_string(),
         }],
         phase: None,
+        metadata: None,
     }
 }
 
@@ -100,6 +105,7 @@ fn developer_msg(text: &str) -> ResponseItem {
             text: text.to_string(),
         }],
         phase: None,
+        metadata: None,
     }
 }
 
@@ -114,6 +120,7 @@ fn developer_msg_with_fragments(texts: &[&str]) -> ResponseItem {
             })
             .collect(),
         phase: None,
+        metadata: None,
     }
 }
 
@@ -145,6 +152,7 @@ fn custom_tool_call_output(call_id: &str, output: &str) -> ResponseItem {
         call_id: call_id.to_string(),
         name: None,
         output: FunctionCallOutputPayload::from_text(output.to_string()),
+        metadata: None,
     }
 }
 
@@ -158,6 +166,7 @@ fn reasoning_msg(text: &str) -> ResponseItem {
             text: text.to_string(),
         }]),
         encrypted_content: None,
+        metadata: None,
     }
 }
 
@@ -169,6 +178,7 @@ fn reasoning_with_encrypted_content(len: usize) -> ResponseItem {
         }],
         content: None,
         encrypted_content: Some("a".repeat(len)),
+        metadata: None,
     }
 }
 
@@ -192,6 +202,7 @@ fn filters_non_api_messages() {
             text: "ignored".to_string(),
         }],
         phase: None,
+        metadata: None,
     };
     let reasoning = reasoning_msg("thinking...");
     h.record_items([&system, &reasoning, &ResponseItem::Other], policy);
@@ -214,6 +225,7 @@ fn filters_non_api_messages() {
                     text: "thinking...".to_string(),
                 }]),
                 encrypted_content: None,
+                metadata: None,
             },
             ResponseItem::Message {
                 id: None,
@@ -222,6 +234,7 @@ fn filters_non_api_messages() {
                     text: "hi".to_string()
                 }],
                 phase: None,
+                metadata: None,
             },
             ResponseItem::Message {
                 id: None,
@@ -230,6 +243,7 @@ fn filters_non_api_messages() {
                     text: "hello".to_string()
                 }],
                 phase: None,
+                metadata: None,
             }
         ]
     );
@@ -379,6 +393,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() {
                 },
             ],
             phase: None,
+            metadata: None,
         },
         ResponseItem::FunctionCall {
             id: None,
@@ -386,6 +401,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() {
             namespace: None,
             arguments: "{}".to_string(),
             call_id: "call-1".to_string(),
+            metadata: None,
         },
         ResponseItem::FunctionCallOutput {
             call_id: "call-1".to_string(),
@@ -398,6 +414,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() {
                     detail: Some(DEFAULT_IMAGE_DETAIL),
                 },
             ]),
+            metadata: None,
         },
         ResponseItem::CustomToolCall {
             id: None,
@@ -405,6 +422,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() {
             call_id: "tool-1".to_string(),
             name: "js_repl".to_string(),
             input: "view_image".to_string(),
+            metadata: None,
         },
         ResponseItem::CustomToolCallOutput {
             call_id: "tool-1".to_string(),
@@ -418,6 +436,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() {
                     detail: Some(DEFAULT_IMAGE_DETAIL),
                 },
             ]),
+            metadata: None,
         },
     ];
     let history = create_history_with_items(items);
@@ -441,6 +460,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() {
                 },
             ],
             phase: None,
+            metadata: None,
         },
         ResponseItem::FunctionCall {
             id: None,
@@ -448,6 +468,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() {
             namespace: None,
             arguments: "{}".to_string(),
             call_id: "call-1".to_string(),
+            metadata: None,
         },
         ResponseItem::FunctionCallOutput {
             call_id: "call-1".to_string(),
@@ -460,6 +481,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() {
                         .to_string(),
                 },
             ]),
+            metadata: None,
         },
         ResponseItem::CustomToolCall {
             id: None,
@@ -467,6 +489,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() {
             call_id: "tool-1".to_string(),
             name: "js_repl".to_string(),
             input: "view_image".to_string(),
+            metadata: None,
         },
         ResponseItem::CustomToolCallOutput {
             call_id: "tool-1".to_string(),
@@ -480,6 +503,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() {
                         .to_string(),
                 },
             ]),
+            metadata: None,
         },
     ];
     assert_eq!(stripped, expected);
@@ -499,6 +523,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() {
             },
         ],
         phase: None,
+        metadata: None,
     }]);
     let preserved = with_images.for_prompt(&modalities);
     assert_eq!(preserved.len(), 1);
@@ -518,6 +543,7 @@ fn for_prompt_preserves_image_generation_calls_when_images_are_supported() {
             status: "generating".to_string(),
             revised_prompt: Some("lobster".to_string()),
             result: "Zm9v".to_string(),
+            metadata: None,
         },
         ResponseItem::Message {
             id: None,
@@ -526,6 +552,7 @@ fn for_prompt_preserves_image_generation_calls_when_images_are_supported() {
                 text: "hi".to_string(),
             }],
             phase: None,
+            metadata: None,
         },
     ]);
 
@@ -537,6 +564,7 @@ fn for_prompt_preserves_image_generation_calls_when_images_are_supported() {
                 status: "generating".to_string(),
                 revised_prompt: Some("lobster".to_string()),
                 result: "Zm9v".to_string(),
+                metadata: None,
             },
             ResponseItem::Message {
                 id: None,
@@ -545,6 +573,7 @@ fn for_prompt_preserves_image_generation_calls_when_images_are_supported() {
                     text: "hi".to_string(),
                 }],
                 phase: None,
+                metadata: None,
             }
         ]
     );
@@ -560,12 +589,14 @@ fn for_prompt_clears_image_generation_result_when_images_are_unsupported() {
                 text: "generate a lobster".to_string(),
             }],
             phase: None,
+            metadata: None,
         },
         ResponseItem::ImageGenerationCall {
             id: "ig_123".to_string(),
             status: "completed".to_string(),
             revised_prompt: Some("lobster".to_string()),
             result: "Zm9v".to_string(),
+            metadata: None,
         },
     ]);
 
@@ -579,12 +610,14 @@ fn for_prompt_clears_image_generation_result_when_images_are_unsupported() {
                     text: "generate a lobster".to_string(),
                 }],
                 phase: None,
+                metadata: None,
             },
             ResponseItem::ImageGenerationCall {
                 id: "ig_123".to_string(),
                 status: "completed".to_string(),
                 revised_prompt: Some("lobster".to_string()),
                 result: String::new(),
+                metadata: None,
             },
         ]
     );
@@ -621,10 +654,12 @@ fn remove_first_item_removes_matching_output_for_function_call() {
             namespace: None,
             arguments: "{}".to_string(),
             call_id: "call-1".to_string(),
+            metadata: None,
         },
         ResponseItem::FunctionCallOutput {
             call_id: "call-1".to_string(),
             output: FunctionCallOutputPayload::from_text("ok".to_string()),
+            metadata: None,
         },
     ];
     let mut h = create_history_with_items(items);
@@ -638,6 +673,7 @@ fn remove_first_item_removes_matching_call_for_output() {
         ResponseItem::FunctionCallOutput {
             call_id: "call-2".to_string(),
             output: FunctionCallOutputPayload::from_text("ok".to_string()),
+            metadata: None,
         },
         ResponseItem::FunctionCall {
             id: None,
@@ -645,6 +681,7 @@ fn remove_first_item_removes_matching_call_for_output() {
             namespace: None,
             arguments: "{}".to_string(),
             call_id: "call-2".to_string(),
+            metadata: None,
         },
     ];
     let mut h = create_history_with_items(items);
@@ -667,6 +704,7 @@ fn replace_last_turn_images_replaces_tool_output_images() {
                 ]),
                 success: Some(true),
             },
+            metadata: None,
         },
     ];
     let mut history = create_history_with_items(items);
@@ -687,6 +725,7 @@ fn replace_last_turn_images_replaces_tool_output_images() {
                     ]),
                     success: Some(true),
                 },
+                metadata: None,
             },
         ]
     );
@@ -702,6 +741,7 @@ fn replace_last_turn_images_does_not_touch_user_images() {
             detail: Some(DEFAULT_IMAGE_DETAIL),
         }],
         phase: None,
+        metadata: None,
     }];
     let mut history = create_history_with_items(items.clone());
 
@@ -723,10 +763,12 @@ fn remove_first_item_handles_local_shell_pair() {
                 env: None,
                 user: None,
             }),
+            metadata: None,
         },
         ResponseItem::FunctionCallOutput {
             call_id: "call-3".to_string(),
             output: FunctionCallOutputPayload::from_text("ok".to_string()),
+            metadata: None,
         },
     ];
     let mut h = create_history_with_items(items);
@@ -948,11 +990,13 @@ fn remove_first_item_handles_custom_tool_pair() {
             call_id: "tool-1".to_string(),
             name: "my_tool".to_string(),
             input: "{}".to_string(),
+            metadata: None,
         },
         ResponseItem::CustomToolCallOutput {
             call_id: "tool-1".to_string(),
             name: None,
             output: FunctionCallOutputPayload::from_text("ok".to_string()),
+            metadata: None,
         },
     ];
     let mut h = create_history_with_items(items);
@@ -974,10 +1018,12 @@ fn normalization_retains_local_shell_outputs() {
                 env: None,
                 user: None,
             }),
+            metadata: None,
         },
         ResponseItem::FunctionCallOutput {
             call_id: "shell-1".to_string(),
             output: FunctionCallOutputPayload::from_text("Total output lines: 1\n\nok".to_string()),
+            metadata: None,
         },
     ];
 
@@ -1001,6 +1047,9 @@ fn record_items_truncates_function_call_output_content() {
             body: FunctionCallOutputBody::Text(long_output.clone()),
             success: Some(true),
         },
+        metadata: Some(ResponseItemMetadata {
+            turn_id: Some("turn-1".to_string()),
+        }),
     };
 
     history.record_items([&item], policy);
@@ -1021,6 +1070,7 @@ fn record_items_truncates_function_call_output_content() {
         }
         other => panic!("unexpected history item: {other:?}"),
     }
+    assert_eq!(history.items[0].turn_id(), Some("turn-1"));
 }
 
 #[test]
@@ -1033,6 +1083,7 @@ fn record_items_truncates_custom_tool_call_output_content() {
         call_id: "tool-200".to_string(),
         name: None,
         output: FunctionCallOutputPayload::from_text(long_output.clone()),
+        metadata: None,
     };
 
     history.record_items([&item], policy);
@@ -1066,6 +1117,7 @@ fn record_items_respects_custom_token_limit() {
             body: FunctionCallOutputBody::Text(long_output),
             success: Some(true),
         },
+        metadata: None,
     };
 
     history.record_items([&item], policy);
@@ -1185,6 +1237,7 @@ fn normalize_adds_missing_output_for_function_call() {
         namespace: None,
         arguments: "{}".to_string(),
         call_id: "call-x".to_string(),
+        metadata: None,
     }];
     let mut h = create_history_with_items(items);
 
@@ -1199,10 +1252,12 @@ fn normalize_adds_missing_output_for_function_call() {
                 namespace: None,
                 arguments: "{}".to_string(),
                 call_id: "call-x".to_string(),
+                metadata: None,
             },
             ResponseItem::FunctionCallOutput {
                 call_id: "call-x".to_string(),
                 output: FunctionCallOutputPayload::from_text("aborted".to_string()),
+                metadata: None,
             },
         ]
     );
@@ -1217,6 +1272,7 @@ fn normalize_adds_missing_output_for_custom_tool_call() {
         call_id: "tool-x".to_string(),
         name: "custom".to_string(),
         input: "{}".to_string(),
+        metadata: None,
     }];
     let mut h = create_history_with_items(items);
 
@@ -1231,11 +1287,13 @@ fn normalize_adds_missing_output_for_custom_tool_call() {
                 call_id: "tool-x".to_string(),
                 name: "custom".to_string(),
                 input: "{}".to_string(),
+                metadata: None,
             },
             ResponseItem::CustomToolCallOutput {
                 call_id: "tool-x".to_string(),
                 name: None,
                 output: FunctionCallOutputPayload::from_text("aborted".to_string()),
+                metadata: None,
             },
         ]
     );
@@ -1255,6 +1313,7 @@ fn normalize_adds_missing_output_for_local_shell_call_with_id() {
             env: None,
             user: None,
         }),
+        metadata: None,
     }];
     let mut h = create_history_with_items(items);
 
@@ -1274,10 +1333,12 @@ fn normalize_adds_missing_output_for_local_shell_call_with_id() {
                     env: None,
                     user: None,
                 }),
+                metadata: None,
             },
             ResponseItem::FunctionCallOutput {
                 call_id: "shell-1".to_string(),
                 output: FunctionCallOutputPayload::from_text("aborted".to_string()),
+                metadata: None,
             },
         ]
     );
@@ -1289,6 +1350,7 @@ fn normalize_removes_orphan_function_call_output() {
     let items = vec![ResponseItem::FunctionCallOutput {
         call_id: "orphan-1".to_string(),
         output: FunctionCallOutputPayload::from_text("ok".to_string()),
+        metadata: None,
     }];
     let mut h = create_history_with_items(items);
 
@@ -1304,6 +1366,7 @@ fn normalize_removes_orphan_custom_tool_call_output() {
         call_id: "orphan-2".to_string(),
         name: None,
         output: FunctionCallOutputPayload::from_text("ok".to_string()),
+        metadata: None,
     }];
     let mut h = create_history_with_items(items);
 
@@ -1323,11 +1386,13 @@ fn normalize_mixed_inserts_and_removals() {
             namespace: None,
             arguments: "{}".to_string(),
             call_id: "c1".to_string(),
+            metadata: None,
         },
         // Orphan output that should be removed
         ResponseItem::FunctionCallOutput {
             call_id: "c2".to_string(),
             output: FunctionCallOutputPayload::from_text("ok".to_string()),
+            metadata: None,
         },
         // Will get an inserted custom tool output
         ResponseItem::CustomToolCall {
@@ -1336,6 +1401,7 @@ fn normalize_mixed_inserts_and_removals() {
             call_id: "t1".to_string(),
             name: "tool".to_string(),
             input: "{}".to_string(),
+            metadata: None,
         },
         // Local shell call also gets an inserted function call output
         ResponseItem::LocalShellCall {
@@ -1349,6 +1415,7 @@ fn normalize_mixed_inserts_and_removals() {
                 env: None,
                 user: None,
             }),
+            metadata: None,
         },
     ];
     let mut h = create_history_with_items(items);
@@ -1364,10 +1431,12 @@ fn normalize_mixed_inserts_and_removals() {
                 namespace: None,
                 arguments: "{}".to_string(),
                 call_id: "c1".to_string(),
+                metadata: None,
             },
             ResponseItem::FunctionCallOutput {
                 call_id: "c1".to_string(),
                 output: FunctionCallOutputPayload::from_text("aborted".to_string()),
+                metadata: None,
             },
             ResponseItem::CustomToolCall {
                 id: None,
@@ -1375,11 +1444,13 @@ fn normalize_mixed_inserts_and_removals() {
                 call_id: "t1".to_string(),
                 name: "tool".to_string(),
                 input: "{}".to_string(),
+                metadata: None,
             },
             ResponseItem::CustomToolCallOutput {
                 call_id: "t1".to_string(),
                 name: None,
                 output: FunctionCallOutputPayload::from_text("aborted".to_string()),
+                metadata: None,
             },
             ResponseItem::LocalShellCall {
                 id: None,
@@ -1392,10 +1463,12 @@ fn normalize_mixed_inserts_and_removals() {
                     env: None,
                     user: None,
                 }),
+                metadata: None,
             },
             ResponseItem::FunctionCallOutput {
                 call_id: "s1".to_string(),
                 output: FunctionCallOutputPayload::from_text("aborted".to_string()),
+                metadata: None,
             },
         ]
     );
@@ -1409,6 +1482,7 @@ fn normalize_adds_missing_output_for_function_call_inserts_output() {
         namespace: None,
         arguments: "{}".to_string(),
         call_id: "call-x".to_string(),
+        metadata: None,
     }];
     let mut h = create_history_with_items(items);
     h.normalize_history(&default_input_modalities());
@@ -1421,10 +1495,12 @@ fn normalize_adds_missing_output_for_function_call_inserts_output() {
                 namespace: None,
                 arguments: "{}".to_string(),
                 call_id: "call-x".to_string(),
+                metadata: None,
             },
             ResponseItem::FunctionCallOutput {
                 call_id: "call-x".to_string(),
                 output: FunctionCallOutputPayload::from_text("aborted".to_string()),
+                metadata: None,
             },
         ]
     );
@@ -1438,6 +1514,7 @@ fn normalize_adds_missing_output_for_tool_search_call() {
         status: Some("completed".to_string()),
         execution: "client".to_string(),
         arguments: "{}".into(),
+        metadata: None,
     }];
     let mut h = create_history_with_items(items);
 
@@ -1452,12 +1529,14 @@ fn normalize_adds_missing_output_for_tool_search_call() {
                 status: Some("completed".to_string()),
                 execution: "client".to_string(),
                 arguments: "{}".into(),
+                metadata: None,
             },
             ResponseItem::ToolSearchOutput {
                 call_id: Some("search-call-x".to_string()),
                 status: "completed".to_string(),
                 execution: "client".to_string(),
                 tools: Vec::new(),
+                metadata: None,
             },
         ]
     );
@@ -1473,6 +1552,7 @@ fn normalize_adds_missing_output_for_custom_tool_call_panics_in_debug() {
         call_id: "tool-x".to_string(),
         name: "custom".to_string(),
         input: "{}".to_string(),
+        metadata: None,
     }];
     let mut h = create_history_with_items(items);
     h.normalize_history(&default_input_modalities());
@@ -1493,6 +1573,7 @@ fn normalize_adds_missing_output_for_local_shell_call_with_id_panics_in_debug()
             env: None,
             user: None,
         }),
+        metadata: None,
     }];
     let mut h = create_history_with_items(items);
     h.normalize_history(&default_input_modalities());
@@ -1505,6 +1586,7 @@ fn normalize_removes_orphan_function_call_output_panics_in_debug() {
     let items = vec![ResponseItem::FunctionCallOutput {
         call_id: "orphan-1".to_string(),
         output: FunctionCallOutputPayload::from_text("ok".to_string()),
+        metadata: None,
     }];
     let mut h = create_history_with_items(items);
     h.normalize_history(&default_input_modalities());
@@ -1518,6 +1600,7 @@ fn normalize_removes_orphan_custom_tool_call_output_panics_in_debug() {
         call_id: "orphan-2".to_string(),
         name: None,
         output: FunctionCallOutputPayload::from_text("ok".to_string()),
+        metadata: None,
     }];
     let mut h = create_history_with_items(items);
     h.normalize_history(&default_input_modalities());
@@ -1531,6 +1614,7 @@ fn normalize_removes_orphan_client_tool_search_output() {
         status: "completed".to_string(),
         execution: "client".to_string(),
         tools: Vec::new(),
+        metadata: None,
     }];
     let mut h = create_history_with_items(items);
 
@@ -1548,6 +1632,7 @@ fn normalize_removes_orphan_client_tool_search_output_panics_in_debug() {
         status: "completed".to_string(),
         execution: "client".to_string(),
         tools: Vec::new(),
+        metadata: None,
     }];
     let mut h = create_history_with_items(items);
     h.normalize_history(&default_input_modalities());
@@ -1560,6 +1645,7 @@ fn normalize_keeps_server_tool_search_output_without_matching_call() {
         status: "completed".to_string(),
         execution: "server".to_string(),
         tools: Vec::new(),
+        metadata: None,
     }];
     let mut h = create_history_with_items(items);
 
@@ -1572,6 +1658,7 @@ fn normalize_keeps_server_tool_search_output_without_matching_call() {
             status: "completed".to_string(),
             execution: "server".to_string(),
             tools: Vec::new(),
+            metadata: None,
         }]
     );
 }
@@ -1587,10 +1674,12 @@ fn normalize_mixed_inserts_and_removals_panics_in_debug() {
             namespace: None,
             arguments: "{}".to_string(),
             call_id: "c1".to_string(),
+            metadata: None,
         },
         ResponseItem::FunctionCallOutput {
             call_id: "c2".to_string(),
             output: FunctionCallOutputPayload::from_text("ok".to_string()),
+            metadata: None,
         },
         ResponseItem::CustomToolCall {
             id: None,
@@ -1598,6 +1687,7 @@ fn normalize_mixed_inserts_and_removals_panics_in_debug() {
             call_id: "t1".to_string(),
             name: "tool".to_string(),
             input: "{}".to_string(),
+            metadata: None,
         },
         ResponseItem::LocalShellCall {
             id: None,
@@ -1610,6 +1700,7 @@ fn normalize_mixed_inserts_and_removals_panics_in_debug() {
                 env: None,
                 user: None,
             }),
+            metadata: None,
         },
     ];
     let mut h = create_history_with_items(items);
@@ -1633,6 +1724,7 @@ fn image_data_url_payload_does_not_dominate_message_estimate() {
             },
         ],
         phase: None,
+        metadata: None,
     };
     let text_only_item = ResponseItem::Message {
         id: None,
@@ -1641,6 +1733,7 @@ fn image_data_url_payload_does_not_dominate_message_estimate() {
             text: "Here is the screenshot".to_string(),
         }],
         phase: None,
+        metadata: None,
     };
 
     let raw_len = serde_json::to_string(&image_item).unwrap().len() as i64;
@@ -1668,6 +1761,7 @@ fn image_data_url_payload_does_not_dominate_function_call_output_estimate() {
                 detail: Some(DEFAULT_IMAGE_DETAIL),
             },
         ]),
+        metadata: None,
     };
 
     let raw_len = serde_json::to_string(&item).unwrap().len() as i64;
@@ -1694,6 +1788,7 @@ fn image_data_url_payload_does_not_dominate_custom_tool_call_output_estimate() {
                 detail: Some(DEFAULT_IMAGE_DETAIL),
             },
         ]),
+        metadata: None,
     };
 
     let raw_len = serde_json::to_string(&item).unwrap().len() as i64;
@@ -1714,6 +1809,7 @@ fn non_base64_image_urls_are_unchanged() {
             detail: Some(DEFAULT_IMAGE_DETAIL),
         }],
         phase: None,
+        metadata: None,
     };
     let function_output_item = ResponseItem::FunctionCallOutput {
         call_id: "call-1".to_string(),
@@ -1723,6 +1819,7 @@ fn non_base64_image_urls_are_unchanged() {
                 detail: Some(DEFAULT_IMAGE_DETAIL),
             },
         ]),
+        metadata: None,
     };
 
     assert_eq!(
@@ -1745,6 +1842,7 @@ fn encrypted_function_output_uses_plaintext_byte_estimate() {
                 encrypted_content: encrypted_content.clone(),
             },
         ]),
+        metadata: None,
     };
 
     let raw_len = serde_json::to_string(&item).unwrap().len() as i64;
@@ -1765,6 +1863,7 @@ fn data_url_without_base64_marker_is_unchanged() {
             detail: Some(DEFAULT_IMAGE_DETAIL),
         }],
         phase: None,
+        metadata: None,
     };
 
     assert_eq!(
@@ -1785,6 +1884,7 @@ fn non_image_base64_data_url_is_unchanged() {
                 detail: Some(DEFAULT_IMAGE_DETAIL),
             },
         ]),
+        metadata: None,
     };
 
     let raw_len = serde_json::to_string(&item).unwrap().len() as i64;
@@ -1805,6 +1905,7 @@ fn mixed_case_data_url_markers_are_adjusted() {
             detail: Some(DEFAULT_IMAGE_DETAIL),
         }],
         phase: None,
+        metadata: None,
     };
 
     let raw_len = serde_json::to_string(&item).unwrap().len() as i64;
@@ -1837,6 +1938,7 @@ fn multiple_inline_images_apply_multiple_fixed_costs() {
             },
         ],
         phase: None,
+        metadata: None,
     };
 
     let raw_len = serde_json::to_string(&item).unwrap().len() as i64;
@@ -1870,6 +1972,7 @@ fn original_detail_images_scale_with_dimensions() {
                 detail: Some(ImageDetail::Original),
             },
         ]),
+        metadata: None,
     };
 
     let raw_len = serde_json::to_string(&item).unwrap().len() as i64;
@@ -1900,6 +2003,7 @@ fn original_detail_images_are_capped_at_max_patch_count() {
                 detail: Some(ImageDetail::Original),
             },
         ]),
+        metadata: None,
     };
 
     let raw_len = serde_json::to_string(&item).unwrap().len() as i64;
@@ -1933,6 +2037,7 @@ fn original_detail_webp_images_scale_with_dimensions() {
                 detail: Some(ImageDetail::Original),
             },
         ]),
+        metadata: None,
     };
 
     let raw_len = serde_json::to_string(&item).unwrap().len() as i64;
@@ -1951,6 +2056,7 @@ fn text_only_items_unchanged() {
             text: "Hello world, this is a response.".to_string(),
         }],
         phase: None,
+        metadata: None,
     };
 
     let estimated = estimate_response_item_model_visible_bytes(&item);
diff --git a/codex-rs/core/src/context_manager/normalize.rs b/codex-rs/core/src/context_manager/normalize.rs
index cbfe6f305eff..1da58b61924f 100644
--- a/codex-rs/core/src/context_manager/normalize.rs
+++ b/codex-rs/core/src/context_manager/normalize.rs
@@ -49,6 +49,7 @@ pub(crate) fn ensure_call_outputs_present(items: &mut Vec<ResponseItem>) {
                     ResponseItem::FunctionCallOutput {
                         call_id: call_id.clone(),
                         output: FunctionCallOutputPayload::from_text("aborted".to_string()),
+                        metadata: None,
                     },
                 ));
             }
@@ -64,6 +65,7 @@ pub(crate) fn ensure_call_outputs_present(items: &mut Vec<ResponseItem>) {
                         status: "completed".to_string(),
                         execution: "client".to_string(),
                         tools: Vec::new(),
+                        metadata: None,
                     },
                 ));
             }
@@ -79,6 +81,7 @@ pub(crate) fn ensure_call_outputs_present(items: &mut Vec<ResponseItem>) {
                         call_id: call_id.clone(),
                         name: None,
                         output: FunctionCallOutputPayload::from_text("aborted".to_string()),
+                        metadata: None,
                     },
                 ));
             }
@@ -95,6 +98,7 @@ pub(crate) fn ensure_call_outputs_present(items: &mut Vec<ResponseItem>) {
                     ResponseItem::FunctionCallOutput {
                         call_id: call_id.clone(),
                         output: FunctionCallOutputPayload::from_text("aborted".to_string()),
+                        metadata: None,
                     },
                 ));
             }
diff --git a/codex-rs/core/src/context_manager/updates.rs b/codex-rs/core/src/context_manager/updates.rs
index d7302bbbf8ab..0ce02240b007 100644
--- a/codex-rs/core/src/context_manager/updates.rs
+++ b/codex-rs/core/src/context_manager/updates.rs
@@ -203,6 +203,7 @@ fn build_text_message(role: &str, text_sections: Vec<String>) -> Option<Response
         role: role.to_string(),
         content,
         phase: None,
+        metadata: None,
     })
 }
 
diff --git a/codex-rs/core/src/event_mapping.rs b/codex-rs/core/src/event_mapping.rs
index e9383aef4159..11e0294c33e6 100644
--- a/codex-rs/core/src/event_mapping.rs
+++ b/codex-rs/core/src/event_mapping.rs
@@ -199,6 +199,7 @@ pub fn parse_turn_item(item: &ResponseItem) -> Option<TurnItem> {
             status,
             revised_prompt,
             result,
+            ..
         } => Some(TurnItem::ImageGeneration(
             codex_protocol::items::ImageGenerationItem {
                 id: id.clone(),
diff --git a/codex-rs/core/src/event_mapping_tests.rs b/codex-rs/core/src/event_mapping_tests.rs
index 6f9dad64bb2f..ad5b4c877d13 100644
--- a/codex-rs/core/src/event_mapping_tests.rs
+++ b/codex-rs/core/src/event_mapping_tests.rs
@@ -61,6 +61,7 @@ fn parses_user_message_with_text_and_two_images() {
             },
         ],
         phase: None,
+        metadata: None,
     };
 
     let turn_item = parse_turn_item(&item).expect("expected user message turn item");
@@ -110,6 +111,7 @@ fn skips_local_image_label_text() {
             },
         ],
         phase: None,
+        metadata: None,
     };
 
     let turn_item = parse_turn_item(&item).expect("expected user message turn item");
@@ -142,6 +144,7 @@ fn parses_assistant_message_input_text_for_backward_compatibility() {
                 .to_string(),
         }],
         phase: None,
+        metadata: None,
     };
 
     let turn_item = parse_turn_item(&item).expect("expected assistant message turn item");
@@ -191,6 +194,7 @@ fn skips_unnamed_image_label_text() {
             },
         ],
         phase: None,
+        metadata: None,
     };
 
     let turn_item = parse_turn_item(&item).expect("expected user message turn item");
@@ -223,7 +227,7 @@ fn skips_user_instructions_and_env() {
                     text: "# AGENTS.md instructions for test_directory\n\n<INSTRUCTIONS>\ntest_text\n</INSTRUCTIONS>".to_string(),
                 }],
             phase: None,
-            },
+                metadata: None,},
             ResponseItem::Message {
                 id: None,
                 role: "user".to_string(),
@@ -231,7 +235,7 @@ fn skips_user_instructions_and_env() {
                     text: "<environment_context>test_text</environment_context>".to_string(),
                 }],
             phase: None,
-            },
+                metadata: None,},
             ResponseItem::Message {
                 id: None,
                 role: "user".to_string(),
@@ -239,7 +243,7 @@ fn skips_user_instructions_and_env() {
                     text: "# AGENTS.md instructions for test_directory\n\n<INSTRUCTIONS>\ntest_text\n</INSTRUCTIONS>".to_string(),
                 }],
             phase: None,
-            },
+                metadata: None,},
             ResponseItem::Message {
                 id: None,
                 role: "user".to_string(),
@@ -248,7 +252,7 @@ fn skips_user_instructions_and_env() {
                         .to_string(),
                 }],
             phase: None,
-            },
+                metadata: None,},
             ResponseItem::Message {
                 id: None,
                 role: "user".to_string(),
@@ -256,7 +260,7 @@ fn skips_user_instructions_and_env() {
                     text: "<user_shell_command>echo 42</user_shell_command>".to_string(),
                 }],
             phase: None,
-            },
+                metadata: None,},
             ResponseItem::Message {
                 id: None,
                 role: "user".to_string(),
@@ -271,7 +275,7 @@ fn skips_user_instructions_and_env() {
                     },
                 ],
                 phase: None,
-            },
+                metadata: None,},
         ];
 
     for item in items {
@@ -321,7 +325,7 @@ fn parses_hook_prompt_and_hides_other_contextual_fragments() {
             },
         ],
         phase: None,
-    };
+        metadata: None,};
 
     let turn_item = parse_turn_item(&item).expect("expected hook prompt turn item");
 
@@ -353,6 +357,7 @@ fn internal_model_context_does_not_parse_as_visible_turn_item() {
             .render(),
         }],
         phase: None,
+        metadata: None,
     };
 
     assert!(parse_turn_item(&item).is_none());
@@ -367,6 +372,7 @@ fn parses_agent_message() {
             text: "Hello from Codex".to_string(),
         }],
         phase: None,
+        metadata: None,
     };
 
     let turn_item = parse_turn_item(&item).expect("expected agent message turn item");
@@ -398,6 +404,7 @@ fn parses_reasoning_summary_and_raw_content() {
             text: "raw details".to_string(),
         }]),
         encrypted_content: None,
+        metadata: None,
     };
 
     let turn_item = parse_turn_item(&item).expect("expected reasoning turn item");
@@ -430,6 +437,7 @@ fn parses_reasoning_including_raw_content() {
             },
         ]),
         encrypted_content: None,
+        metadata: None,
     };
 
     let turn_item = parse_turn_item(&item).expect("expected reasoning turn item");
@@ -455,6 +463,7 @@ fn parses_web_search_call() {
             query: Some("weather".to_string()),
             queries: None,
         }),
+        metadata: None,
     };
 
     let turn_item = parse_turn_item(&item).expect("expected web search turn item");
@@ -483,6 +492,7 @@ fn parses_web_search_open_page_call() {
         action: Some(WebSearchAction::OpenPage {
             url: Some("https://example.com".to_string()),
         }),
+        metadata: None,
     };
 
     let turn_item = parse_turn_item(&item).expect("expected web search turn item");
@@ -511,6 +521,7 @@ fn parses_web_search_find_in_page_call() {
             url: Some("https://example.com".to_string()),
             pattern: Some("needle".to_string()),
         }),
+        metadata: None,
     };
 
     let turn_item = parse_turn_item(&item).expect("expected web search turn item");
@@ -537,6 +548,7 @@ fn parses_partial_web_search_call_without_action_as_other() {
         id: Some("ws_partial".to_string()),
         status: Some("in_progress".to_string()),
         action: None,
+        metadata: None,
     };
 
     let turn_item = parse_turn_item(&item).expect("expected web search turn item");
diff --git a/codex-rs/core/src/guardian/tests.rs b/codex-rs/core/src/guardian/tests.rs
index 7b269bd33928..222639d1342f 100644
--- a/codex-rs/core/src/guardian/tests.rs
+++ b/codex-rs/core/src/guardian/tests.rs
@@ -254,6 +254,7 @@ async fn seed_guardian_parent_history(session: &Arc<Session>, turn: &Arc<TurnCon
                             .to_string(),
                     }],
                     phase: None,
+                    metadata: None,
                 },
                 ResponseItem::FunctionCall {
                     id: None,
@@ -261,12 +262,14 @@ async fn seed_guardian_parent_history(session: &Arc<Session>, turn: &Arc<TurnCon
                     namespace: None,
                     arguments: "{\"repo\":\"openai/codex\"}".to_string(),
                     call_id: "call-1".to_string(),
+                    metadata: None,
                 },
                 ResponseItem::FunctionCallOutput {
                     call_id: "call-1".to_string(),
                     output: codex_protocol::models::FunctionCallOutputPayload::from_text(
                         "repo visibility: public".to_string(),
                     ),
+                    metadata: None,
                 },
                 ResponseItem::Message {
                     id: None,
@@ -276,6 +279,7 @@ async fn seed_guardian_parent_history(session: &Arc<Session>, turn: &Arc<TurnCon
                             .to_string(),
                     }],
                     phase: None,
+                    metadata: None,
                 },
             ],
         )
@@ -482,6 +486,7 @@ async fn build_guardian_prompt_delta_mode_preserves_original_numbering() -> anyh
                         text: "Please also push the second docs fix.".to_string(),
                     }],
                     phase: None,
+                    metadata: None,
                 },
                 ResponseItem::Message {
                     id: None,
@@ -490,6 +495,7 @@ async fn build_guardian_prompt_delta_mode_preserves_original_numbering() -> anyh
                         text: "I need approval for the second push.".to_string(),
                     }],
                     phase: None,
+                    metadata: None,
                 },
             ],
         )
@@ -612,6 +618,7 @@ async fn build_guardian_prompt_stale_delta_version_falls_back_to_full_prompt() -
                         text: "Compacted retained user request.".to_string(),
                     }],
                     phase: None,
+                    metadata: None,
                 },
                 ResponseItem::Message {
                     id: None,
@@ -620,6 +627,7 @@ async fn build_guardian_prompt_stale_delta_version_falls_back_to_full_prompt() -
                         text: "Compacted summary of earlier guardian context.".to_string(),
                     }],
                     phase: None,
+                    metadata: None,
                 },
             ],
             /*reference_context_item*/ None,
@@ -636,6 +644,7 @@ async fn build_guardian_prompt_stale_delta_version_falls_back_to_full_prompt() -
                         text: "Please push after the compaction.".to_string(),
                     }],
                     phase: None,
+                    metadata: None,
                 },
                 ResponseItem::Message {
                     id: None,
@@ -644,6 +653,7 @@ async fn build_guardian_prompt_stale_delta_version_falls_back_to_full_prompt() -
                         text: "I need approval for the post-compaction push.".to_string(),
                     }],
                     phase: None,
+                    metadata: None,
                 },
             ],
         )
@@ -691,6 +701,7 @@ fn collect_guardian_transcript_entries_skips_contextual_user_messages() {
                 text: "<environment_context>\n<cwd>/tmp</cwd>\n</environment_context>".to_string(),
             }],
             phase: None,
+            metadata: None,
         },
         ResponseItem::Message {
             id: None,
@@ -699,6 +710,7 @@ fn collect_guardian_transcript_entries_skips_contextual_user_messages() {
                 text: "hello".to_string(),
             }],
             phase: None,
+            metadata: None,
         },
     ];
 
@@ -726,6 +738,7 @@ fn collect_guardian_transcript_entries_keeps_manual_approval_developer_message()
                 text: "ordinary developer context".to_string(),
             }],
             phase: None,
+            metadata: None,
         },
         ResponseItem::Message {
             id: None,
@@ -734,6 +747,7 @@ fn collect_guardian_transcript_entries_keeps_manual_approval_developer_message()
                 text: approval_text.clone(),
             }],
             phase: None,
+            metadata: None,
         },
     ];
 
@@ -758,6 +772,7 @@ fn collect_guardian_transcript_entries_includes_recent_tool_calls_and_output() {
                 text: "check the repo".to_string(),
             }],
             phase: None,
+            metadata: None,
         },
         ResponseItem::FunctionCall {
             id: None,
@@ -765,12 +780,14 @@ fn collect_guardian_transcript_entries_includes_recent_tool_calls_and_output() {
             namespace: None,
             arguments: "{\"path\":\"README.md\"}".to_string(),
             call_id: "call-1".to_string(),
+            metadata: None,
         },
         ResponseItem::FunctionCallOutput {
             call_id: "call-1".to_string(),
             output: codex_protocol::models::FunctionCallOutputPayload::from_text(
                 "repo is public".to_string(),
             ),
+            metadata: None,
         },
         ResponseItem::Message {
             id: None,
@@ -779,6 +796,7 @@ fn collect_guardian_transcript_entries_includes_recent_tool_calls_and_output() {
                 text: "I need to push a fix".to_string(),
             }],
             phase: None,
+            metadata: None,
         },
     ];
 
@@ -1813,6 +1831,7 @@ async fn guardian_reuses_prompt_cache_key_and_appends_prior_reviews() -> anyhow:
                         text: "Please push the second docs fix too.".to_string(),
                     }],
                     phase: None,
+                    metadata: None,
                 },
                 ResponseItem::Message {
                     id: None,
@@ -1821,6 +1840,7 @@ async fn guardian_reuses_prompt_cache_key_and_appends_prior_reviews() -> anyhow:
                         text: "I need approval for the second docs fix.".to_string(),
                     }],
                     phase: None,
+                    metadata: None,
                 },
             ],
         )
@@ -1858,6 +1878,7 @@ async fn guardian_reuses_prompt_cache_key_and_appends_prior_reviews() -> anyhow:
                         text: "Please push the third docs fix too.".to_string(),
                     }],
                     phase: None,
+                    metadata: None,
                 },
                 ResponseItem::Message {
                     id: None,
@@ -1866,6 +1887,7 @@ async fn guardian_reuses_prompt_cache_key_and_appends_prior_reviews() -> anyhow:
                         text: "I need approval for the third docs fix.".to_string(),
                     }],
                     phase: None,
+                    metadata: None,
                 },
             ],
         )
@@ -2605,7 +2627,7 @@ async fn guardian_ephemeral_retry_preserves_parallel_trunk_and_fork_history() ->
                             text: "Please inspect pending changes before pushing.".to_string(),
                         }],
                         phase: None,
-                    },
+                        metadata: None,},
                     ResponseItem::Message {
                         id: None,
                         role: "assistant".to_string(),
@@ -2613,7 +2635,7 @@ async fn guardian_ephemeral_retry_preserves_parallel_trunk_and_fork_history() ->
                             text: "I need approval to run git diff.".to_string(),
                         }],
                         phase: None,
-                    },
+                        metadata: None,},
                 ],
             )
             .await;
@@ -2672,7 +2694,7 @@ async fn guardian_ephemeral_retry_preserves_parallel_trunk_and_fork_history() ->
                             text: "Now inspect whether pushing is safe.".to_string(),
                         }],
                         phase: None,
-                    },
+                        metadata: None,},
                     ResponseItem::Message {
                         id: None,
                         role: "assistant".to_string(),
@@ -2680,7 +2702,7 @@ async fn guardian_ephemeral_retry_preserves_parallel_trunk_and_fork_history() ->
                             text: "I need approval to push after the diff check.".to_string(),
                         }],
                         phase: None,
-                    },
+                        metadata: None,},
                 ],
             )
             .await;
diff --git a/codex-rs/core/src/image_preparation.rs b/codex-rs/core/src/image_preparation.rs
index d992adb8093d..d30dfbb5cad9 100644
--- a/codex-rs/core/src/image_preparation.rs
+++ b/codex-rs/core/src/image_preparation.rs
@@ -62,7 +62,7 @@ pub(crate) fn prepare_response_items(items: &mut [ResponseItem]) {
             | ResponseItem::WebSearchCall { .. }
             | ResponseItem::ImageGenerationCall { .. }
             | ResponseItem::Compaction { .. }
-            | ResponseItem::CompactionTrigger
+            | ResponseItem::CompactionTrigger { .. }
             | ResponseItem::ContextCompaction { .. }
             | ResponseItem::Other => {}
         }
diff --git a/codex-rs/core/src/image_preparation_tests.rs b/codex-rs/core/src/image_preparation_tests.rs
index 1d54973cc242..01d512c75862 100644
--- a/codex-rs/core/src/image_preparation_tests.rs
+++ b/codex-rs/core/src/image_preparation_tests.rs
@@ -49,6 +49,7 @@ fn preparation_preserves_small_image_bytes_and_non_data_urls() {
             },
         ],
         phase: None,
+        metadata: None,
     }];
 
     prepare_response_items(&mut items);
@@ -85,6 +86,7 @@ fn detail_policies_apply_the_expected_budgets() {
             role: "user".to_string(),
             content: vec![ContentItem::InputImage { image_url, detail }],
             phase: None,
+            metadata: None,
         }];
 
         prepare_response_items(&mut items);
@@ -130,6 +132,7 @@ fn preparation_replaces_only_failed_tool_images_and_preserves_metadata() {
             ]),
             success: Some(true),
         },
+        metadata: None,
     }];
 
     prepare_response_items(&mut items);
@@ -160,6 +163,7 @@ fn preparation_replaces_only_failed_tool_images_and_preserves_metadata() {
                 ]),
                 success: Some(true),
             },
+            metadata: None,
         }]
     );
 }
diff --git a/codex-rs/core/src/realtime_context_tests.rs b/codex-rs/core/src/realtime_context_tests.rs
index 8c3aa8d97e5a..f04282a65f21 100644
--- a/codex-rs/core/src/realtime_context_tests.rs
+++ b/codex-rs/core/src/realtime_context_tests.rs
@@ -74,6 +74,7 @@ fn message(role: &str, content: ContentItem) -> ResponseItem {
         role: role.to_string(),
         content: vec![content],
         phase: None,
+        metadata: None,
     }
 }
 
diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs
index 8e67020883f7..675a0408e8e3 100644
--- a/codex-rs/core/src/session/mod.rs
+++ b/codex-rs/core/src/session/mod.rs
@@ -181,6 +181,7 @@ use uuid::Uuid;
 
 use crate::client::ModelClient;
 use crate::codex_thread::ThreadConfigSnapshot;
+#[cfg(test)]
 use crate::compact::collect_user_messages;
 use crate::config::Config;
 use crate::config::Constrained;
diff --git a/codex-rs/core/src/session/rollout_reconstruction.rs b/codex-rs/core/src/session/rollout_reconstruction.rs
index c844a630fce1..34e7807a9a5a 100644
--- a/codex-rs/core/src/session/rollout_reconstruction.rs
+++ b/codex-rs/core/src/session/rollout_reconstruction.rs
@@ -296,7 +296,7 @@ impl Session {
                         // prompt shape.
                         // TODO(ccunningham): if we drop support for None replacement_history compaction items,
                         // we can get rid of this second loop entirely and just build `history` directly in the first loop.
-                        let user_messages = collect_user_messages(history.raw_items());
+                        let user_messages = compact::collect_user_messages(history.raw_items());
                         let rebuilt = compact::build_compacted_history(
                             Vec::new(),
                             &user_messages,
diff --git a/codex-rs/core/src/session/rollout_reconstruction_tests.rs b/codex-rs/core/src/session/rollout_reconstruction_tests.rs
index d243946761ee..d53e3866c993 100644
--- a/codex-rs/core/src/session/rollout_reconstruction_tests.rs
+++ b/codex-rs/core/src/session/rollout_reconstruction_tests.rs
@@ -20,6 +20,7 @@ fn user_message(text: &str) -> ResponseItem {
             text: text.to_string(),
         }],
         phase: None,
+        metadata: None,
     }
 }
 
@@ -31,6 +32,7 @@ fn assistant_message(text: &str) -> ResponseItem {
             text: text.to_string(),
         }],
         phase: None,
+        metadata: None,
     }
 }
 
@@ -49,6 +51,7 @@ fn inter_agent_assistant_message(text: &str) -> ResponseItem {
             text: serde_json::to_string(&communication).unwrap(),
         }],
         phase: None,
+        metadata: None,
     }
 }
 
diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs
index 810398e97166..9bfbd9613b9a 100644
--- a/codex-rs/core/src/session/tests.rs
+++ b/codex-rs/core/src/session/tests.rs
@@ -103,6 +103,7 @@ use codex_protocol::config_types::Settings;
 use codex_protocol::models::BaseInstructions;
 use codex_protocol::models::ContentItem;
 use codex_protocol::models::ResponseItem;
+use codex_protocol::models::ResponseItemMetadata;
 use codex_protocol::protocol::AskForApproval;
 use codex_protocol::protocol::CodexErrorInfo;
 use codex_protocol::protocol::CompactedItem;
@@ -189,6 +190,7 @@ fn user_message(text: &str) -> ResponseItem {
             text: text.to_string(),
         }],
         phase: None,
+        metadata: None,
     }
 }
 
@@ -200,6 +202,7 @@ fn assistant_message(text: &str) -> ResponseItem {
             text: text.to_string(),
         }],
         phase: None,
+        metadata: None,
     }
 }
 
@@ -259,6 +262,7 @@ fn skill_message(text: &str) -> ResponseItem {
             text: text.to_string(),
         }],
         phase: None,
+        metadata: None,
     }
 }
 
@@ -1597,6 +1601,9 @@ async fn reconstruct_history_uses_replacement_history_verbatim() {
             text: "summary".to_string(),
         }],
         phase: None,
+        metadata: Some(ResponseItemMetadata {
+            turn_id: Some("compact-turn".to_string()),
+        }),
     };
     let replacement_history = vec![
         summary_item.clone(),
@@ -1607,6 +1614,7 @@ async fn reconstruct_history_uses_replacement_history_verbatim() {
                 text: "stale developer instructions".to_string(),
             }],
             phase: None,
+            metadata: None,
         },
     ];
     let rollout_items = vec![RolloutItem::Compacted(CompactedItem {
@@ -1668,32 +1676,35 @@ async fn resize_all_images_prepares_failures_before_history_insertion() {
             ]),
             success: Some(true),
         },
+        metadata: None,
     };
 
     session
         .record_conversation_items(turn_context.as_ref(), std::slice::from_ref(&item))
         .await;
 
+    let expected = vec![ResponseItem::FunctionCallOutput {
+        call_id: "call-1".to_string(),
+        output: FunctionCallOutputPayload {
+            body: FunctionCallOutputBody::ContentItems(vec![
+                FunctionCallOutputContentItem::InputText {
+                    text: "before".to_string(),
+                },
+                FunctionCallOutputContentItem::InputText {
+                    text: "image content omitted because it could not be processed".to_string(),
+                },
+                FunctionCallOutputContentItem::InputImage {
+                    image_url: "https://example.com/image.png".to_string(),
+                    detail: Some(ImageDetail::High),
+                },
+            ]),
+            success: Some(true),
+        },
+        metadata: None,
+    }];
     assert_eq!(
         session.state.lock().await.clone_history().raw_items(),
-        &[ResponseItem::FunctionCallOutput {
-            call_id: "call-1".to_string(),
-            output: FunctionCallOutputPayload {
-                body: FunctionCallOutputBody::ContentItems(vec![
-                    FunctionCallOutputContentItem::InputText {
-                        text: "before".to_string(),
-                    },
-                    FunctionCallOutputContentItem::InputText {
-                        text: "image content omitted because it could not be processed".to_string(),
-                    },
-                    FunctionCallOutputContentItem::InputImage {
-                        image_url: "https://example.com/image.png".to_string(),
-                        detail: Some(ImageDetail::High),
-                    },
-                ]),
-                success: Some(true),
-            },
-        }]
+        expected.as_slice()
     );
 }
 
@@ -1720,6 +1731,7 @@ async fn resize_all_images_prepares_resumed_history_before_installing_it() {
             },
         ],
         phase: None,
+        metadata: None,
     };
 
     session
@@ -1744,6 +1756,7 @@ async fn resize_all_images_prepares_resumed_history_before_installing_it() {
                 },
             ],
             phase: None,
+            metadata: None,
         }]
     );
 }
@@ -1859,7 +1872,8 @@ async fn resumed_history_injects_initial_context_on_first_context_update_only()
     session
         .record_context_updates_and_set_reference_context_item(&turn_context)
         .await;
-    expected.extend(session.build_initial_context(&turn_context).await);
+    let initial_context = session.build_initial_context(&turn_context).await;
+    expected.extend(initial_context);
     let history_after_seed = session.clone_history().await;
     assert_eq!(expected, history_after_seed.raw_items());
 
@@ -7652,6 +7666,7 @@ async fn build_initial_context_omits_default_image_save_location_with_image_hist
                 status: "completed".to_string(),
                 revised_prompt: Some("a tiny blue square".to_string()),
                 result: "Zm9v".to_string(),
+                metadata: None,
             }],
             /*reference_context_item*/ None,
         )
@@ -7904,6 +7919,7 @@ async fn handle_output_item_done_records_image_save_history_message() {
         status: "completed".to_string(),
         revised_prompt: Some("a tiny blue square".to_string()),
         result: "Zm9v".to_string(),
+        metadata: None,
     };
 
     let mut ctx = HandleOutputCtx {
@@ -7934,7 +7950,8 @@ async fn handle_output_item_done_records_image_save_history_message() {
             image_output_path.display(),
         ),
     );
-    assert_eq!(history.raw_items(), &[image_message, item]);
+    let expected = vec![image_message, item];
+    assert_eq!(history.raw_items(), expected.as_slice());
     assert_eq!(
         std::fs::read(&expected_saved_path).expect("saved file"),
         b"foo"
@@ -7959,6 +7976,7 @@ async fn handle_output_item_done_skips_image_save_message_when_save_fails() {
         status: "completed".to_string(),
         revised_prompt: Some("broken payload".to_string()),
         result: "_-8".to_string(),
+        metadata: None,
     };
 
     let mut ctx = HandleOutputCtx {
@@ -7975,7 +7993,8 @@ async fn handle_output_item_done_skips_image_save_message_when_save_fails() {
         .expect("image generation item should still complete");
 
     let history = session.clone_history().await;
-    assert_eq!(history.raw_items(), &[item]);
+    let expected = vec![item];
+    assert_eq!(history.raw_items(), expected.as_slice());
     assert!(!expected_saved_path.exists());
 }
 
@@ -8118,6 +8137,7 @@ async fn record_context_updates_and_set_reference_context_item_reinjects_full_co
             text: format!("{}\nsummary", crate::compact::SUMMARY_PREFIX),
         }],
         phase: None,
+        metadata: None,
     };
     session
         .record_conversation_items(&turn_context, std::slice::from_ref(&compacted_summary))
@@ -8142,7 +8162,8 @@ async fn record_context_updates_and_set_reference_context_item_reinjects_full_co
 
     let history = session.clone_history().await;
     let mut expected_history = vec![compacted_summary];
-    expected_history.extend(session.build_initial_context(&turn_context).await);
+    let initial_context = session.build_initial_context(&turn_context).await;
+    expected_history.extend(initial_context);
     assert_eq!(history.raw_items().to_vec(), expected_history);
 }
 
@@ -8715,6 +8736,7 @@ async fn task_finish_emits_turn_item_lifecycle_for_leftover_pending_user_input()
             text: "late pending input".to_string(),
         }],
         phase: None,
+        metadata: None,
     };
     assert!(
         history.raw_items().iter().any(|item| item == &expected),
@@ -9147,6 +9169,7 @@ async fn abort_empty_active_turn_preserves_pending_input() {
             text: "late pending input".to_string(),
         }],
         phase: None,
+        metadata: None,
     };
     let turn_state = {
         let mut active = sess.active_turn.lock().await;
@@ -9405,6 +9428,7 @@ async fn tool_calls_reopen_mailbox_delivery_for_current_turn() {
         namespace: None,
         arguments: "{}".to_string(),
         call_id: "call-1".to_string(),
+        metadata: None,
     };
     let mut ctx = HandleOutputCtx {
         sess: Arc::clone(&sess),
@@ -9531,6 +9555,7 @@ async fn fatal_tool_error_stops_turn_and_reports_error() {
         call_id: "call-1".to_string(),
         name: "shell_command".to_string(),
         input: "{}".to_string(),
+        metadata: None,
     };
 
     let call = ToolRouter::build_tool_call(item.clone())
@@ -9615,6 +9640,7 @@ async fn sample_rollout(
             text: "first user".to_string(),
         }],
         phase: None,
+        metadata: None,
     };
     live_history.record_items(
         std::iter::once(&user1),
@@ -9629,6 +9655,7 @@ async fn sample_rollout(
             text: "assistant reply one".to_string(),
         }],
         phase: None,
+        metadata: None,
     };
     live_history.record_items(
         std::iter::once(&assistant1),
@@ -9656,6 +9683,7 @@ async fn sample_rollout(
             text: "second user".to_string(),
         }],
         phase: None,
+        metadata: None,
     };
     live_history.record_items(
         std::iter::once(&user2),
@@ -9670,6 +9698,7 @@ async fn sample_rollout(
             text: "assistant reply two".to_string(),
         }],
         phase: None,
+        metadata: None,
     };
     live_history.record_items(
         std::iter::once(&assistant2),
@@ -9697,6 +9726,7 @@ async fn sample_rollout(
             text: "third user".to_string(),
         }],
         phase: None,
+        metadata: None,
     };
     live_history.record_items(
         std::iter::once(&user3),
@@ -9711,6 +9741,7 @@ async fn sample_rollout(
             text: "assistant reply three".to_string(),
         }],
         phase: None,
+        metadata: None,
     };
     live_history.record_items(
         std::iter::once(&assistant3),
@@ -9856,6 +9887,7 @@ while :; do sleep 1; done"#,
         })
         .to_string(),
         call_id: "shell-cleanup-call".to_string(),
+        metadata: None,
     };
     let call = ToolRouter::build_tool_call(item)?
         .expect("shell command response item should build a tool call");
diff --git a/codex-rs/core/src/session/tests/guardian_tests.rs b/codex-rs/core/src/session/tests/guardian_tests.rs
index 0d3c9c033b8f..a2cffe2de310 100644
--- a/codex-rs/core/src/session/tests/guardian_tests.rs
+++ b/codex-rs/core/src/session/tests/guardian_tests.rs
@@ -536,6 +536,7 @@ async fn process_compacted_history_preserves_separate_guardian_developer_message
                     text: "stale developer message".to_string(),
                 }],
                 phase: None,
+                metadata: None,
             },
             ResponseItem::Message {
                 id: None,
@@ -544,6 +545,7 @@ async fn process_compacted_history_preserves_separate_guardian_developer_message
                     text: "summary".to_string(),
                 }],
                 phase: None,
+                metadata: None,
             },
         ],
         InitialContextInjection::BeforeLastUserMessage,
diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs
index 1abf8bb5d178..46d24520b2b3 100644
--- a/codex-rs/core/src/session/turn.rs
+++ b/codex-rs/core/src/session/turn.rs
@@ -1971,7 +1971,7 @@ async fn try_run_sampling_request(
                     | ResponseItem::WebSearchCall { .. }
                     | ResponseItem::ImageGenerationCall { .. }
                     | ResponseItem::Compaction { .. }
-                    | ResponseItem::CompactionTrigger
+                    | ResponseItem::CompactionTrigger { .. }
                     | ResponseItem::ContextCompaction { .. }
                     | ResponseItem::Other => false,
                 };
diff --git a/codex-rs/core/src/session/turn_tests.rs b/codex-rs/core/src/session/turn_tests.rs
index 3511c03bda9d..7c9f003cb4a9 100644
--- a/codex-rs/core/src/session/turn_tests.rs
+++ b/codex-rs/core/src/session/turn_tests.rs
@@ -33,6 +33,7 @@ fn assistant_output_text(text: &str) -> ResponseItem {
             text: text.to_string(),
         }],
         phase: None,
+        metadata: None,
     }
 }
 
diff --git a/codex-rs/core/src/stream_events_utils.rs b/codex-rs/core/src/stream_events_utils.rs
index 9ae20df74dfa..265d7b450fea 100644
--- a/codex-rs/core/src/stream_events_utils.rs
+++ b/codex-rs/core/src/stream_events_utils.rs
@@ -626,6 +626,7 @@ pub(crate) fn response_input_to_response_item(input: &ResponseInputItem) -> Opti
             Some(ResponseItem::FunctionCallOutput {
                 call_id: call_id.clone(),
                 output: output.clone(),
+                metadata: None,
             })
         }
         ResponseInputItem::CustomToolCallOutput {
@@ -636,12 +637,14 @@ pub(crate) fn response_input_to_response_item(input: &ResponseInputItem) -> Opti
             call_id: call_id.clone(),
             name: name.clone(),
             output: output.clone(),
+            metadata: None,
         }),
         ResponseInputItem::McpToolCallOutput { call_id, output } => {
             let output = output.as_function_call_output_payload();
             Some(ResponseItem::FunctionCallOutput {
                 call_id: call_id.clone(),
                 output,
+                metadata: None,
             })
         }
         ResponseInputItem::ToolSearchOutput {
@@ -654,6 +657,7 @@ pub(crate) fn response_input_to_response_item(input: &ResponseInputItem) -> Opti
             status: status.clone(),
             execution: execution.clone(),
             tools: tools.clone(),
+            metadata: None,
         }),
         _ => None,
     }
diff --git a/codex-rs/core/src/stream_events_utils_tests.rs b/codex-rs/core/src/stream_events_utils_tests.rs
index c151a46fb355..b5fb950869c3 100644
--- a/codex-rs/core/src/stream_events_utils_tests.rs
+++ b/codex-rs/core/src/stream_events_utils_tests.rs
@@ -42,6 +42,7 @@ fn assistant_output_text_with_phase(text: &str, phase: Option<MessagePhase>) ->
             text: text.to_string(),
         }],
         phase,
+        metadata: None,
     }
 }
 
@@ -52,6 +53,7 @@ fn external_context_pollution_items_include_web_search_and_tool_search() {
             id: None,
             status: Some("completed".to_string()),
             action: None,
+            metadata: None,
         },
         ResponseItem::ToolSearchCall {
             id: None,
@@ -59,12 +61,14 @@ fn external_context_pollution_items_include_web_search_and_tool_search() {
             status: None,
             execution: "client".to_string(),
             arguments: serde_json::json!({"query": "calendar"}),
+            metadata: None,
         },
         ResponseItem::ToolSearchOutput {
             call_id: Some("search-1".to_string()),
             status: "completed".to_string(),
             execution: "client".to_string(),
             tools: Vec::new(),
+            metadata: None,
         },
     ];
 
@@ -89,6 +93,7 @@ fn external_context_pollution_items_exclude_local_tool_calls() {
                 env: None,
                 user: None,
             }),
+            metadata: None,
         },
         ResponseItem::FunctionCall {
             id: None,
@@ -96,10 +101,12 @@ fn external_context_pollution_items_exclude_local_tool_calls() {
             namespace: None,
             arguments: "{}".to_string(),
             call_id: "call-1".to_string(),
+            metadata: None,
         },
         ResponseItem::FunctionCallOutput {
             call_id: "call-1".to_string(),
             output: FunctionCallOutputPayload::from_text("ok".to_string()),
+            metadata: None,
         },
         ResponseItem::CustomToolCall {
             id: None,
@@ -107,11 +114,13 @@ fn external_context_pollution_items_exclude_local_tool_calls() {
             call_id: "custom-1".to_string(),
             name: "apply_patch".to_string(),
             input: "*** Begin Patch\n*** End Patch\n".to_string(),
+            metadata: None,
         },
         ResponseItem::CustomToolCallOutput {
             call_id: "custom-1".to_string(),
             name: Some("apply_patch".to_string()),
             output: FunctionCallOutputPayload::from_text("ok".to_string()),
+            metadata: None,
         },
         assistant_output_text("plain assistant text"),
     ];
@@ -412,6 +421,7 @@ fn completed_item_defers_mailbox_delivery_for_image_generation_calls() {
         status: "completed".to_string(),
         revised_prompt: None,
         result: "Zm9v".to_string(),
+        metadata: None,
     };
 
     assert!(completed_item_defers_mailbox_delivery_to_next_turn(
diff --git a/codex-rs/core/src/tasks/mod.rs b/codex-rs/core/src/tasks/mod.rs
index 61f698015680..b2f337ec42f2 100644
--- a/codex-rs/core/src/tasks/mod.rs
+++ b/codex-rs/core/src/tasks/mod.rs
@@ -109,6 +109,7 @@ pub(crate) fn interrupted_turn_history_marker(
                     text: marker.render(),
                 }],
                 phase: None,
+                metadata: None,
             })
         }
     }
diff --git a/codex-rs/core/src/tasks/review.rs b/codex-rs/core/src/tasks/review.rs
index 0d9e3521dcd3..bb8670237e20 100644
--- a/codex-rs/core/src/tasks/review.rs
+++ b/codex-rs/core/src/tasks/review.rs
@@ -247,6 +247,7 @@ pub(crate) async fn exit_review_mode(
                 role: "user".to_string(),
                 content: vec![ContentItem::InputText { text: user_message }],
                 phase: None,
+                metadata: None,
             }],
         )
         .await;
@@ -267,6 +268,7 @@ pub(crate) async fn exit_review_mode(
                     text: assistant_message,
                 }],
                 phase: None,
+                metadata: None,
             },
         )
         .await;
diff --git a/codex-rs/core/src/thread_manager_tests.rs b/codex-rs/core/src/thread_manager_tests.rs
index a5670b05333a..4a7890511bc4 100644
--- a/codex-rs/core/src/thread_manager_tests.rs
+++ b/codex-rs/core/src/thread_manager_tests.rs
@@ -42,6 +42,7 @@ fn user_msg(text: &str) -> ResponseItem {
             text: text.to_string(),
         }],
         phase: None,
+        metadata: None,
     }
 }
 fn assistant_msg(text: &str) -> ResponseItem {
@@ -52,6 +53,7 @@ fn assistant_msg(text: &str) -> ResponseItem {
             text: text.to_string(),
         }],
         phase: None,
+        metadata: None,
     }
 }
 
@@ -80,6 +82,7 @@ fn truncates_before_requested_user_message() {
             }],
             content: None,
             encrypted_content: None,
+            metadata: None,
         },
         ResponseItem::FunctionCall {
             id: None,
@@ -87,6 +90,7 @@ fn truncates_before_requested_user_message() {
             name: "tool".to_string(),
             namespace: None,
             arguments: "{}".to_string(),
+            metadata: None,
         },
         assistant_msg("a4"),
     ];
diff --git a/codex-rs/core/src/thread_rollout_truncation_tests.rs b/codex-rs/core/src/thread_rollout_truncation_tests.rs
index 652682776910..bb6c3ac03e7b 100644
--- a/codex-rs/core/src/thread_rollout_truncation_tests.rs
+++ b/codex-rs/core/src/thread_rollout_truncation_tests.rs
@@ -15,6 +15,7 @@ fn user_msg(text: &str) -> ResponseItem {
             text: text.to_string(),
         }],
         phase: None,
+        metadata: None,
     }
 }
 
@@ -26,6 +27,7 @@ fn assistant_msg(text: &str) -> ResponseItem {
             text: text.to_string(),
         }],
         phase: None,
+        metadata: None,
     }
 }
 
@@ -37,6 +39,7 @@ fn developer_msg(text: &str) -> ResponseItem {
             text: text.to_string(),
         }],
         phase: None,
+        metadata: None,
     }
 }
 
@@ -76,6 +79,7 @@ fn truncates_rollout_from_start_before_nth_user_only() {
             }],
             content: None,
             encrypted_content: None,
+            metadata: None,
         },
         ResponseItem::FunctionCall {
             id: None,
@@ -83,6 +87,7 @@ fn truncates_rollout_from_start_before_nth_user_only() {
             name: "tool".to_string(),
             namespace: None,
             arguments: "{}".to_string(),
+            metadata: None,
         },
         assistant_msg("a4"),
     ];
diff --git a/codex-rs/core/src/tools/code_mode/delegate.rs b/codex-rs/core/src/tools/code_mode/delegate.rs
index 06bb71a28072..15177944ca0c 100644
--- a/codex-rs/core/src/tools/code_mode/delegate.rs
+++ b/codex-rs/core/src/tools/code_mode/delegate.rs
@@ -302,6 +302,7 @@ impl CoreTurnHost {
                 call_id,
                 name: Some(PUBLIC_TOOL_NAME.to_string()),
                 output: FunctionCallOutputPayload::from_text(text),
+                metadata: None,
             }])
             .await
             .map_err(|_| {
diff --git a/codex-rs/core/src/tools/handlers/extension_tools.rs b/codex-rs/core/src/tools/handlers/extension_tools.rs
index 33625317fe40..f0522c7c1763 100644
--- a/codex-rs/core/src/tools/handlers/extension_tools.rs
+++ b/codex-rs/core/src/tools/handlers/extension_tools.rs
@@ -324,6 +324,7 @@ mod tests {
                 text: "extension history".to_string(),
             }],
             phase: None,
+            metadata: None,
         };
         session
             .record_conversation_items(&turn, std::slice::from_ref(&history_item))
diff --git a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs
index 88a8f07dcb08..8c3ea14325ac 100644
--- a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs
+++ b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs
@@ -2797,6 +2797,7 @@ async fn resume_agent_restores_closed_agent_and_accepts_send_input() {
                     text: "materialized".to_string(),
                 }],
                 phase: None,
+                metadata: None,
             })]),
             AuthManager::from_auth_for_testing(CodexAuth::from_api_key("dummy")),
             /*parent_trace*/ None,
diff --git a/codex-rs/core/src/tools/router_tests.rs b/codex-rs/core/src/tools/router_tests.rs
index c85c15a50388..f729c736c181 100644
--- a/codex-rs/core/src/tools/router_tests.rs
+++ b/codex-rs/core/src/tools/router_tests.rs
@@ -156,6 +156,7 @@ async fn build_tool_call_uses_namespace_for_registry_name() -> anyhow::Result<()
         namespace: Some("mcp__codex_apps__calendar".to_string()),
         arguments: "{}".to_string(),
         call_id: "call-namespace".to_string(),
+        metadata: None,
     })?
     .expect("function_call should produce a tool call");
 
@@ -335,6 +336,7 @@ async fn extension_tool_executors_are_model_visible_and_dispatchable() -> anyhow
             text: "extension history".to_string(),
         }],
         phase: None,
+        metadata: None,
     };
     session
         .record_conversation_items(&turn, std::slice::from_ref(&history_item))
@@ -369,6 +371,7 @@ async fn extension_tool_executors_are_model_visible_and_dispatchable() -> anyhow
         namespace: Some("extension/".to_string()),
         arguments: json!({ "message": "hello" }).to_string(),
         call_id: "call-extension".to_string(),
+        metadata: None,
     })?
     .expect("function_call should produce a tool call");
     let result = router
diff --git a/codex-rs/core/src/turn_timing.rs b/codex-rs/core/src/turn_timing.rs
index 380e246fae0b..8a93a91833f8 100644
--- a/codex-rs/core/src/turn_timing.rs
+++ b/codex-rs/core/src/turn_timing.rs
@@ -378,7 +378,7 @@ fn response_item_records_turn_ttft(item: &ResponseItem) -> bool {
         | ResponseItem::ImageGenerationCall { .. }
         | ResponseItem::Compaction { .. }
         | ResponseItem::ContextCompaction { .. } => true,
-        ResponseItem::CompactionTrigger => false,
+        ResponseItem::CompactionTrigger { .. } => false,
         ResponseItem::FunctionCallOutput { .. }
         | ResponseItem::CustomToolCallOutput { .. }
         | ResponseItem::ToolSearchOutput { .. }
diff --git a/codex-rs/core/src/turn_timing_tests.rs b/codex-rs/core/src/turn_timing_tests.rs
index 7146f537be6f..af5a54fec8b7 100644
--- a/codex-rs/core/src/turn_timing_tests.rs
+++ b/codex-rs/core/src/turn_timing_tests.rs
@@ -112,6 +112,7 @@ fn response_item_records_turn_ttft_for_first_output_signals() {
             namespace: None,
             arguments: "{}".to_string(),
             call_id: "call-1".to_string(),
+            metadata: None,
         }
     ));
     assert!(response_item_records_turn_ttft(
@@ -121,6 +122,7 @@ fn response_item_records_turn_ttft_for_first_output_signals() {
             call_id: "call-2".to_string(),
             name: "custom".to_string(),
             input: "echo hi".to_string(),
+            metadata: None,
         }
     ));
     assert!(response_item_records_turn_ttft(&ResponseItem::Message {
@@ -130,6 +132,7 @@ fn response_item_records_turn_ttft_for_first_output_signals() {
             text: "hello".to_string(),
         }],
         phase: None,
+        metadata: None,
     }));
 }
 
@@ -142,11 +145,13 @@ fn response_item_records_turn_ttft_ignores_empty_non_output_items() {
             text: String::new(),
         }],
         phase: None,
+        metadata: None,
     }));
     assert!(!response_item_records_turn_ttft(
         &ResponseItem::FunctionCallOutput {
             call_id: "call-1".to_string(),
             output: FunctionCallOutputPayload::from_text("ok".to_string()),
+            metadata: None,
         }
     ));
 }
diff --git a/codex-rs/core/tests/common/responses.rs b/codex-rs/core/tests/common/responses.rs
index c917436b6d19..6e5405e4b3ed 100644
--- a/codex-rs/core/tests/common/responses.rs
+++ b/codex-rs/core/tests/common/responses.rs
@@ -685,6 +685,7 @@ pub fn user_message_item(text: &str) -> ResponseItem {
             text: text.to_string(),
         }],
         phase: None,
+        metadata: None,
     }
 }
 
diff --git a/codex-rs/core/tests/responses_headers.rs b/codex-rs/core/tests/responses_headers.rs
index cda5ffcdb0e2..b5bc2b0d4440 100644
--- a/codex-rs/core/tests/responses_headers.rs
+++ b/codex-rs/core/tests/responses_headers.rs
@@ -140,6 +140,7 @@ async fn responses_stream_includes_subagent_header_on_review() {
             text: "hello".into(),
         }],
         phase: None,
+        metadata: None,
     }];
 
     let mut stream = client_session
@@ -270,6 +271,7 @@ async fn responses_stream_includes_subagent_header_on_other() {
             text: "hello".into(),
         }],
         phase: None,
+        metadata: None,
     }];
 
     let mut stream = client_session
@@ -386,6 +388,7 @@ async fn responses_respects_model_info_overrides_from_config() {
             text: "hello".into(),
         }],
         phase: None,
+        metadata: None,
     }];
 
     let mut stream = client_session
diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs
index c41593f5163f..144daeffdb91 100644
--- a/codex-rs/core/tests/suite/client.rs
+++ b/codex-rs/core/tests/suite/client.rs
@@ -169,6 +169,57 @@ fn assert_codex_client_metadata(
     );
 }
 
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn non_openai_responses_requests_omit_item_turn_metadata() {
+    let server = MockServer::start().await;
+    let response_mock = mount_sse_once(
+        &server,
+        sse(vec![ev_response_created("resp1"), ev_completed("resp1")]),
+    )
+    .await;
+    let mut provider =
+        built_in_model_providers(/* openai_base_url */ /*openai_base_url*/ None)["openai"].clone();
+    provider.name = "Test Responses".to_string();
+    provider.base_url = Some(format!("{}/v1", server.uri()));
+    provider.supports_websockets = false;
+    let codex = test_codex()
+        .with_config(move |config| {
+            config.model_provider_id = provider.name.clone();
+            config.model_provider = provider;
+        })
+        .build(&server)
+        .await
+        .unwrap()
+        .codex;
+
+    codex
+        .submit(Op::UserInput {
+            items: vec![UserInput::Text {
+                text: "hello".into(),
+                text_elements: Vec::new(),
+            }],
+            final_output_json_schema: None,
+            responsesapi_client_metadata: None,
+            additional_context: Default::default(),
+            thread_settings: Default::default(),
+        })
+        .await
+        .unwrap();
+    wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
+
+    let body = response_mock.single_request().body_json();
+    let input = body["input"]
+        .as_array()
+        .expect("request should include input items");
+    assert!(!input.is_empty(), "request should include input items");
+    for item in input {
+        assert!(
+            item.get("metadata").is_none(),
+            "input item should omit metadata: {item}"
+        );
+    }
+}
+
 /// 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)]
@@ -352,6 +403,7 @@ async fn resume_includes_initial_messages_and_sends_prior_items() {
             text: "resumed user message".to_string(),
         }],
         phase: None,
+        metadata: None,
     };
     let prior_user_json = serde_json::to_value(&prior_user).unwrap();
     writeln!(
@@ -373,6 +425,7 @@ async fn resume_includes_initial_messages_and_sends_prior_items() {
             text: "resumed system instruction".to_string(),
         }],
         phase: None,
+        metadata: None,
     };
     let prior_system_json = serde_json::to_value(&prior_system).unwrap();
     writeln!(
@@ -394,6 +447,7 @@ async fn resume_includes_initial_messages_and_sends_prior_items() {
             text: "resumed assistant message".to_string(),
         }],
         phase: Some(MessagePhase::Commentary),
+        metadata: None,
     };
     let prior_item_json = serde_json::to_value(&prior_item).unwrap();
     writeln!(
@@ -536,6 +590,7 @@ async fn resume_replays_legacy_js_repl_image_rollout_shapes() {
         call_id: "legacy-js-call".to_string(),
         name: "js_repl".to_string(),
         input: "console.log('legacy image flow')".to_string(),
+        metadata: None,
     };
     let legacy_image_url = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==";
     let rollout = vec![
@@ -565,6 +620,7 @@ async fn resume_replays_legacy_js_repl_image_rollout_shapes() {
                 call_id: "legacy-js-call".to_string(),
                 name: None,
                 output: FunctionCallOutputPayload::from_text("legacy js_repl stdout".to_string()),
+                metadata: None,
             }),
         },
         RolloutLine {
@@ -577,6 +633,7 @@ async fn resume_replays_legacy_js_repl_image_rollout_shapes() {
                     detail: Some(DEFAULT_IMAGE_DETAIL),
                 }],
                 phase: None,
+                metadata: None,
             }),
         },
     ];
@@ -694,6 +751,7 @@ async fn resume_replays_image_tool_outputs_with_detail() {
                 namespace: None,
                 arguments: "{\"path\":\"/tmp/example.webp\"}".to_string(),
                 call_id: function_call_id.to_string(),
+                metadata: None,
             }),
         },
         RolloutLine {
@@ -706,6 +764,7 @@ async fn resume_replays_image_tool_outputs_with_detail() {
                         detail: Some(ImageDetail::Original),
                     },
                 ]),
+                metadata: None,
             }),
         },
         RolloutLine {
@@ -716,6 +775,7 @@ async fn resume_replays_image_tool_outputs_with_detail() {
                 call_id: custom_call_id.to_string(),
                 name: "js_repl".to_string(),
                 input: "console.log('image flow')".to_string(),
+                metadata: None,
             }),
         },
         RolloutLine {
@@ -729,6 +789,7 @@ async fn resume_replays_image_tool_outputs_with_detail() {
                         detail: Some(ImageDetail::Original),
                     },
                 ]),
+                metadata: None,
             }),
         },
     ];
@@ -976,6 +1037,7 @@ async fn send_provider_auth_request(server: &MockServer, auth: ModelProviderAuth
             text: "hello".to_string(),
         }],
         phase: None,
+        metadata: None,
     });
 
     let mut stream = client_session
@@ -2474,6 +2536,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() {
             text: "content".into(),
         }]),
         encrypted_content: None,
+        metadata: None,
     });
     prompt.input.push(ResponseItem::Message {
         id: Some("message-id".into()),
@@ -2482,6 +2545,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() {
             text: "message".into(),
         }],
         phase: None,
+        metadata: None,
     });
     prompt.input.push(ResponseItem::WebSearchCall {
         id: Some("web-search-id".into()),
@@ -2490,6 +2554,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() {
             query: Some("weather".into()),
             queries: None,
         }),
+        metadata: None,
     });
     prompt.input.push(ResponseItem::FunctionCall {
         id: Some("function-id".into()),
@@ -2497,10 +2562,12 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() {
         namespace: None,
         arguments: "{}".into(),
         call_id: "function-call-id".into(),
+        metadata: None,
     });
     prompt.input.push(ResponseItem::FunctionCallOutput {
         call_id: "function-call-id".into(),
         output: FunctionCallOutputPayload::from_text("ok".into()),
+        metadata: None,
     });
     prompt.input.push(ResponseItem::LocalShellCall {
         id: Some("local-shell-id".into()),
@@ -2513,6 +2580,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() {
             env: None,
             user: None,
         }),
+        metadata: None,
     });
     prompt.input.push(ResponseItem::CustomToolCall {
         id: Some("custom-tool-id".into()),
@@ -2520,11 +2588,13 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() {
         call_id: "custom-tool-call-id".into(),
         name: "custom_tool".into(),
         input: "{}".into(),
+        metadata: None,
     });
     prompt.input.push(ResponseItem::CustomToolCallOutput {
         call_id: "custom-tool-call-id".into(),
         name: None,
         output: FunctionCallOutputPayload::from_text("ok".into()),
+        metadata: None,
     });
 
     let mut stream = client_session
diff --git a/codex-rs/core/tests/suite/client_websockets.rs b/codex-rs/core/tests/suite/client_websockets.rs
index 4aac3dcbd8f8..1f9175d7a4f0 100755
--- a/codex-rs/core/tests/suite/client_websockets.rs
+++ b/codex-rs/core/tests/suite/client_websockets.rs
@@ -2057,6 +2057,7 @@ fn message_item(text: &str) -> ResponseItem {
         role: "user".into(),
         content: vec![ContentItem::InputText { text: text.into() }],
         phase: None,
+        metadata: None,
     }
 }
 
@@ -2066,6 +2067,7 @@ fn assistant_message_item(id: &str, text: &str) -> ResponseItem {
         role: "assistant".into(),
         content: vec![ContentItem::OutputText { text: text.into() }],
         phase: None,
+        metadata: None,
     }
 }
 
diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs
index 1d1d387f2c88..56738a09a0f5 100644
--- a/codex-rs/core/tests/suite/compact.rs
+++ b/codex-rs/core/tests/suite/compact.rs
@@ -2003,9 +2003,11 @@ async fn auto_compact_runs_after_resume_when_token_usage_is_over_limit() {
                 text: remote_summary.to_string(),
             }],
             phase: None,
+            metadata: None,
         },
         codex_protocol::models::ResponseItem::Compaction {
             encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(),
+            metadata: None,
         },
     ];
     let compact_mock =
@@ -4009,9 +4011,11 @@ async fn auto_compact_counts_encrypted_reasoning_before_last_user() {
                 text: "REMOTE_COMPACT_SUMMARY".to_string(),
             }],
             phase: None,
+            metadata: None,
         },
         codex_protocol::models::ResponseItem::Compaction {
             encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(),
+            metadata: None,
         },
     ];
     let compact_mock =
@@ -4134,9 +4138,11 @@ async fn auto_compact_runs_when_reasoning_header_clears_between_turns() {
                 text: "REMOTE_COMPACT_SUMMARY".to_string(),
             }],
             phase: None,
+            metadata: None,
         },
         codex_protocol::models::ResponseItem::Compaction {
             encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(),
+            metadata: None,
         },
     ];
     let compact_mock =
diff --git a/codex-rs/core/tests/suite/compact_remote.rs b/codex-rs/core/tests/suite/compact_remote.rs
index 6bca81bfd32e..b6cbb31c401d 100644
--- a/codex-rs/core/tests/suite/compact_remote.rs
+++ b/codex-rs/core/tests/suite/compact_remote.rs
@@ -161,6 +161,7 @@ fn format_labeled_requests_snapshot(
 fn compacted_summary_only_output(summary: &str) -> Vec<ResponseItem> {
     vec![ResponseItem::Compaction {
         encrypted_content: summary_with_prefix(summary),
+        metadata: None,
     }]
 }
 
@@ -329,6 +330,7 @@ async fn remote_compact_replaces_history_for_followups() -> Result<()> {
 
     let compacted_history = vec![ResponseItem::Compaction {
         encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(),
+        metadata: None,
     }];
     let compact_mock = responses::mount_compact_json_once(
         harness.server(),
@@ -2355,6 +2357,7 @@ async fn remote_compact_persists_replacement_history_in_rollout() -> Result<()>
     let compacted_history = vec![
         ResponseItem::Compaction {
             encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(),
+            metadata: None,
         },
         ResponseItem::Message {
             id: None,
@@ -2363,6 +2366,7 @@ async fn remote_compact_persists_replacement_history_in_rollout() -> Result<()>
                 text: "COMPACTED_ASSISTANT_NOTE".to_string(),
             }],
             phase: None,
+            metadata: None,
         },
     ];
     let compact_mock = responses::mount_compact_json_once(
@@ -2411,7 +2415,9 @@ async fn remote_compact_persists_replacement_history_in_rollout() -> Result<()>
             let has_compaction_item = replacement_history.iter().any(|item| {
                 matches!(
                     item,
-                    ResponseItem::Compaction { encrypted_content }
+                    ResponseItem::Compaction {
+                        encrypted_content, ..
+                    }
                         if encrypted_content == "ENCRYPTED_COMPACTION_SUMMARY"
                 )
             });
@@ -2502,9 +2508,11 @@ async fn remote_compact_and_resume_refresh_stale_developer_instructions() -> Res
                 text: stale_developer_message.to_string(),
             }],
             phase: None,
+            metadata: None,
         },
         ResponseItem::Compaction {
             encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(),
+            metadata: None,
         },
     ];
     let compact_mock = responses::mount_compact_json_once(
@@ -2642,9 +2650,11 @@ async fn remote_compact_refreshes_stale_developer_instructions_without_resume()
                 text: stale_developer_message.to_string(),
             }],
             phase: None,
+            metadata: None,
         },
         ResponseItem::Compaction {
             encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(),
+            metadata: None,
         },
     ];
     let compact_mock = responses::mount_compact_json_once(
@@ -4050,6 +4060,7 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_summary_only_reinject
 
     let compacted_history = vec![ResponseItem::Compaction {
         encrypted_content: summary_with_prefix("REMOTE_SUMMARY_ONLY"),
+        metadata: None,
     }];
     let compact_mock = responses::mount_compact_json_once(
         harness.server(),
diff --git a/codex-rs/core/tests/suite/image_rollout.rs b/codex-rs/core/tests/suite/image_rollout.rs
index 199a7d1db6fe..2b18559075b4 100644
--- a/codex-rs/core/tests/suite/image_rollout.rs
+++ b/codex-rs/core/tests/suite/image_rollout.rs
@@ -178,6 +178,7 @@ async fn copy_paste_local_image_persists_rollout_request_shape() -> anyhow::Resu
             },
         ],
         phase: None,
+        metadata: None,
     };
 
     assert_eq!(actual, expected);
@@ -268,6 +269,7 @@ async fn drag_drop_image_persists_rollout_request_shape() -> anyhow::Result<()>
             },
         ],
         phase: None,
+        metadata: None,
     };
 
     assert_eq!(actual, expected);
diff --git a/codex-rs/core/tests/suite/prompt_debug_tests.rs b/codex-rs/core/tests/suite/prompt_debug_tests.rs
index c2ebe3999b8a..5883a8564b14 100644
--- a/codex-rs/core/tests/suite/prompt_debug_tests.rs
+++ b/codex-rs/core/tests/suite/prompt_debug_tests.rs
@@ -49,6 +49,7 @@ async fn build_prompt_input_includes_context_and_user_message() -> Result<()> {
             text: "hello from debug prompt".to_string(),
         }],
         phase: None,
+        metadata: None,
     };
     assert_eq!(input.last(), Some(&expected_user_message));
     assert!(input.iter().any(|item| {
diff --git a/codex-rs/core/tests/suite/realtime_conversation.rs b/codex-rs/core/tests/suite/realtime_conversation.rs
index 85bc1827f043..fc05e98e046e 100644
--- a/codex-rs/core/tests/suite/realtime_conversation.rs
+++ b/codex-rs/core/tests/suite/realtime_conversation.rs
@@ -2129,6 +2129,7 @@ async fn conversation_startup_context_current_thread_selects_many_turns_by_budge
                     role: "user".to_string(),
                     content: vec![ContentItem::InputText { text: user_turn }],
                     phase: None,
+                    metadata: None,
                 }),
                 RolloutItem::ResponseItem(ResponseItem::Message {
                     id: None,
@@ -2137,6 +2138,7 @@ async fn conversation_startup_context_current_thread_selects_many_turns_by_budge
                         text: assistant_turn,
                     }],
                     phase: None,
+                    metadata: None,
                 }),
             ]
         })
diff --git a/codex-rs/core/tests/suite/review.rs b/codex-rs/core/tests/suite/review.rs
index 3fdd5f83efac..6a38872972c3 100644
--- a/codex-rs/core/tests/suite/review.rs
+++ b/codex-rs/core/tests/suite/review.rs
@@ -531,6 +531,7 @@ async fn review_input_isolated_from_parent_history() {
                 text: "parent: earlier user message".to_string(),
             }],
             phase: None,
+            metadata: None,
         };
         let user_json = serde_json::to_value(&user).unwrap();
         let user_line = serde_json::json!({
@@ -550,6 +551,7 @@ async fn review_input_isolated_from_parent_history() {
                 text: "parent: assistant reply".to_string(),
             }],
             phase: None,
+            metadata: None,
         };
         let assistant_json = serde_json::to_value(&assistant).unwrap();
         let assistant_line = serde_json::json!({
diff --git a/codex-rs/core/tests/suite/unified_exec.rs b/codex-rs/core/tests/suite/unified_exec.rs
index 92913710105a..ac1aac0b1250 100644
--- a/codex-rs/core/tests/suite/unified_exec.rs
+++ b/codex-rs/core/tests/suite/unified_exec.rs
@@ -174,6 +174,7 @@ async fn wait_for_raw_unified_exec_output(
             ResponseItem::FunctionCallOutput {
                 call_id: output_call_id,
                 output,
+                ..
             } if output_call_id == call_id => output.text_content().map(str::to_string),
             _ => None,
         },
diff --git a/codex-rs/ext/image-generation/src/tests.rs b/codex-rs/ext/image-generation/src/tests.rs
index f0f483a2aada..449c7beb09ad 100644
--- a/codex-rs/ext/image-generation/src/tests.rs
+++ b/codex-rs/ext/image-generation/src/tests.rs
@@ -76,6 +76,7 @@ async fn recent_image_fallback_selects_newest_images_in_chronological_order() {
                 },
             ],
             phase: None,
+            metadata: None,
         },
         ResponseItem::FunctionCall {
             id: None,
@@ -83,10 +84,12 @@ async fn recent_image_fallback_selects_newest_images_in_chronological_order() {
             namespace: None,
             arguments: "{}".to_string(),
             call_id: "mcp-call".to_string(),
+            metadata: None,
         },
         ResponseItem::FunctionCallOutput {
             call_id: "mcp-call".to_string(),
             output: image_output("mcp"),
+            metadata: None,
         },
         ResponseItem::CustomToolCall {
             id: None,
@@ -94,21 +97,25 @@ async fn recent_image_fallback_selects_newest_images_in_chronological_order() {
             call_id: "code-mode-call".to_string(),
             name: "exec".to_string(),
             input: String::new(),
+            metadata: None,
         },
         ResponseItem::CustomToolCallOutput {
             call_id: "code-mode-call".to_string(),
             name: Some("exec".to_string()),
             output: image_output("code-mode"),
+            metadata: None,
         },
         ResponseItem::ImageGenerationCall {
             id: "generated-call".to_string(),
             status: "completed".to_string(),
             revised_prompt: None,
             result: "generated".to_string(),
+            metadata: None,
         },
         ResponseItem::FunctionCallOutput {
             call_id: "orphan-call".to_string(),
             output: image_output("orphan"),
+            metadata: None,
         },
     ];
 
@@ -196,6 +203,7 @@ async fn recent_image_fallback_requires_requested_count() {
             role: "user".to_string(),
             content: vec![input_image("only-image")],
             phase: None,
+            metadata: None,
         }],
         &[],
     )
diff --git a/codex-rs/ext/image-generation/src/tool.rs b/codex-rs/ext/image-generation/src/tool.rs
index 66bb9ec71c79..62688b47d92a 100644
--- a/codex-rs/ext/image-generation/src/tool.rs
+++ b/codex-rs/ext/image-generation/src/tool.rs
@@ -267,7 +267,7 @@ fn recent_images(history: &[ResponseItem], count: usize) -> Vec<ImageUrl> {
             | ResponseItem::WebSearchCall { .. }
             | ResponseItem::ImageGenerationCall { .. }
             | ResponseItem::Compaction { .. }
-            | ResponseItem::CompactionTrigger
+            | ResponseItem::CompactionTrigger { .. }
             | ResponseItem::ContextCompaction { .. }
             | ResponseItem::Other => {}
         }
@@ -283,9 +283,9 @@ fn recent_images(history: &[ResponseItem], count: usize) -> Vec<ImageUrl> {
                     ContentItem::InputText { .. } | ContentItem::OutputText { .. } => None,
                 }));
             }
-            ResponseItem::FunctionCallOutput { call_id, output }
-                if function_call_ids.contains(call_id.as_str()) =>
-            {
+            ResponseItem::FunctionCallOutput {
+                call_id, output, ..
+            } if function_call_ids.contains(call_id.as_str()) => {
                 image_urls.extend(output_image_urls(output));
             }
             ResponseItem::CustomToolCallOutput {
@@ -308,7 +308,7 @@ fn recent_images(history: &[ResponseItem], count: usize) -> Vec<ImageUrl> {
             | ResponseItem::WebSearchCall { .. }
             | ResponseItem::ImageGenerationCall { .. }
             | ResponseItem::Compaction { .. }
-            | ResponseItem::CompactionTrigger
+            | ResponseItem::CompactionTrigger { .. }
             | ResponseItem::ContextCompaction { .. }
             | ResponseItem::Other => {}
         }
diff --git a/codex-rs/ext/web-search/src/history.rs b/codex-rs/ext/web-search/src/history.rs
index 94f9f3977276..6417a705fe3c 100644
--- a/codex-rs/ext/web-search/src/history.rs
+++ b/codex-rs/ext/web-search/src/history.rs
@@ -32,7 +32,10 @@ fn push_visible_message(messages: &mut Vec<ResponseItem>, item: &ResponseItem) {
             messages.push(item.clone());
         }
         ResponseItem::AgentMessage {
-            author, content, ..
+            author,
+            content,
+            metadata,
+            ..
         } => {
             let text = content
                 .iter()
@@ -50,6 +53,7 @@ fn push_visible_message(messages: &mut Vec<ResponseItem>, item: &ResponseItem) {
                         text: format!("Agent message from {author}:\n{text}"),
                     }],
                     phase: None,
+                    metadata: metadata.clone(),
                 });
             }
         }
@@ -58,6 +62,7 @@ fn push_visible_message(messages: &mut Vec<ResponseItem>, item: &ResponseItem) {
             role,
             content,
             phase,
+            metadata,
         } if role == USER_ROLE
             && matches!(parse_turn_item(item), Some(TurnItem::UserMessage(_))) =>
         {
@@ -72,6 +77,7 @@ fn push_visible_message(messages: &mut Vec<ResponseItem>, item: &ResponseItem) {
                     role: role.clone(),
                     content,
                     phase: phase.clone(),
+                    metadata: metadata.clone(),
                 });
             }
         }
@@ -104,6 +110,7 @@ mod tests {
                 }
             }],
             phase: None,
+            metadata: None,
         }
     }
 
@@ -120,6 +127,7 @@ mod tests {
                 namespace: None,
                 arguments: "{}".to_string(),
                 call_id: "call-1".to_string(),
+                metadata: None,
             },
             message(ASSISTANT_ROLE, "previous assistant"),
             message("developer", "developer"),
@@ -152,6 +160,7 @@ mod tests {
                 },
             ],
             phase: None,
+            metadata: None,
         };
         let items = vec![
             previous_user,
diff --git a/codex-rs/external-agent-sessions/src/export.rs b/codex-rs/external-agent-sessions/src/export.rs
index 05a3004ee9fc..2990c0f5ea49 100644
--- a/codex-rs/external-agent-sessions/src/export.rs
+++ b/codex-rs/external-agent-sessions/src/export.rs
@@ -141,6 +141,7 @@ fn response_item(message: ConversationMessage) -> ResponseItem {
         },
         content: vec![content],
         phase: None,
+        metadata: None,
     }
 }
 
diff --git a/codex-rs/memories/write/src/phase1.rs b/codex-rs/memories/write/src/phase1.rs
index 3edad7ff75db..775e8988745c 100644
--- a/codex-rs/memories/write/src/phase1.rs
+++ b/codex-rs/memories/write/src/phase1.rs
@@ -303,6 +303,7 @@ mod job {
                 )?,
             }],
             phase: None,
+            metadata: None,
         }];
         prompt.base_instructions = BaseInstructions {
             text: crate::stage_one::PROMPT.to_string(),
@@ -428,6 +429,7 @@ mod job {
             role,
             content,
             phase,
+            metadata,
         } = item
         else {
             return should_persist_response_item_for_memories(item).then(|| item.clone());
@@ -455,6 +457,7 @@ mod job {
             role: role.clone(),
             content,
             phase: phase.clone(),
+            metadata: metadata.clone(),
         })
     }
 
@@ -684,6 +687,7 @@ mod tests {
                 },
             ],
             phase: None,
+            metadata: None,
         };
         let skill_message = ResponseItem::Message {
             id: None,
@@ -694,6 +698,7 @@ mod tests {
                         .to_string(),
             }],
             phase: None,
+            metadata: None,
         };
         let subagent_message = ResponseItem::Message {
             id: None,
@@ -703,6 +708,7 @@ mod tests {
                     .to_string(),
             }],
             phase: None,
+            metadata: None,
         };
 
         let serialized = job::serialize_filtered_rollout_response_items(&[
@@ -724,6 +730,7 @@ mod tests {
                             .to_string(),
                     }],
                     phase: None,
+                    metadata: None,
                 },
                 subagent_message,
             ]
@@ -742,6 +749,7 @@ mod tests {
                         ),
                         success: Some(true),
                     },
+                    metadata: None,
                 },
             )])
             .expect("serialize");
diff --git a/codex-rs/memories/write/src/startup_tests.rs b/codex-rs/memories/write/src/startup_tests.rs
index 66b035ed00d2..13ba9da3b774 100644
--- a/codex-rs/memories/write/src/startup_tests.rs
+++ b/codex-rs/memories/write/src/startup_tests.rs
@@ -685,6 +685,7 @@ async fn seed_stage1_candidate(
                 text: "remember this startup test conversation".to_string(),
             }],
             phase: None,
+            metadata: None,
         }),
     };
     let jsonl = serde_json::to_string(&line)?;
diff --git a/codex-rs/otel/src/events/session_telemetry.rs b/codex-rs/otel/src/events/session_telemetry.rs
index 00c13046dd81..8f0471088ffb 100644
--- a/codex-rs/otel/src/events/session_telemetry.rs
+++ b/codex-rs/otel/src/events/session_telemetry.rs
@@ -1230,7 +1230,7 @@ impl SessionTelemetry {
             ResponseItem::WebSearchCall { .. } => "web_search_call".into(),
             ResponseItem::ImageGenerationCall { .. } => "image_generation_call".into(),
             ResponseItem::Compaction { .. } => "compaction".into(),
-            ResponseItem::CompactionTrigger => "compaction_trigger".into(),
+            ResponseItem::CompactionTrigger { .. } => "compaction_trigger".into(),
             ResponseItem::ContextCompaction { .. } => "context_compaction".into(),
             ResponseItem::Other => "other".into(),
         }
diff --git a/codex-rs/protocol/src/items.rs b/codex-rs/protocol/src/items.rs
index ee61dc3949d4..267f73fbdff0 100644
--- a/codex-rs/protocol/src/items.rs
+++ b/codex-rs/protocol/src/items.rs
@@ -391,6 +391,7 @@ pub fn build_hook_prompt_message(fragments: &[HookPromptFragment]) -> Option<Res
         role: "user".to_string(),
         content,
         phase: None,
+        metadata: None,
     })
 }
 
diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs
index 9d4647b30b21..0aaf68299a16 100644
--- a/codex-rs/protocol/src/models.rs
+++ b/codex-rs/protocol/src/models.rs
@@ -750,6 +750,13 @@ pub enum MessagePhase {
     FinalAnswer,
 }
 
+#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)]
+pub struct ResponseItemMetadata {
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
+    pub turn_id: Option<String>,
+}
+
 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
 #[serde(tag = "type", rename_all = "snake_case")]
 pub enum ResponseItem {
@@ -765,11 +772,17 @@ pub enum ResponseItem {
         #[serde(default, skip_serializing_if = "Option::is_none")]
         #[ts(optional)]
         phase: Option<MessagePhase>,
+        #[serde(default, skip_serializing_if = "Option::is_none")]
+        #[ts(optional)]
+        metadata: Option<ResponseItemMetadata>,
     },
     AgentMessage {
         author: String,
         recipient: String,
         content: Vec<AgentMessageInputContent>,
+        #[serde(default, skip_serializing_if = "Option::is_none")]
+        #[ts(optional)]
+        metadata: Option<ResponseItemMetadata>,
     },
     Reasoning {
         #[serde(default, skip_serializing)]
@@ -781,6 +794,9 @@ pub enum ResponseItem {
         #[ts(optional)]
         content: Option<Vec<ReasoningItemContent>>,
         encrypted_content: Option<String>,
+        #[serde(default, skip_serializing_if = "Option::is_none")]
+        #[ts(optional)]
+        metadata: Option<ResponseItemMetadata>,
     },
     LocalShellCall {
         /// Legacy id field retained for compatibility with older payloads.
@@ -791,6 +807,9 @@ pub enum ResponseItem {
         call_id: Option<String>,
         status: LocalShellStatus,
         action: LocalShellAction,
+        #[serde(default, skip_serializing_if = "Option::is_none")]
+        #[ts(optional)]
+        metadata: Option<ResponseItemMetadata>,
     },
     FunctionCall {
         #[serde(default, skip_serializing)]
@@ -805,6 +824,9 @@ pub enum ResponseItem {
         // Session::handle_function_call parse it into a Value.
         arguments: String,
         call_id: String,
+        #[serde(default, skip_serializing_if = "Option::is_none")]
+        #[ts(optional)]
+        metadata: Option<ResponseItemMetadata>,
     },
     ToolSearchCall {
         #[serde(default, skip_serializing)]
@@ -817,6 +839,9 @@ pub enum ResponseItem {
         execution: String,
         #[ts(type = "unknown")]
         arguments: serde_json::Value,
+        #[serde(default, skip_serializing_if = "Option::is_none")]
+        #[ts(optional)]
+        metadata: Option<ResponseItemMetadata>,
     },
     // NOTE: The `output` field for `function_call_output` uses a dedicated payload type with
     // custom serialization. On the wire it is either:
@@ -828,6 +853,9 @@ pub enum ResponseItem {
         #[ts(as = "FunctionCallOutputBody")]
         #[schemars(with = "FunctionCallOutputBody")]
         output: FunctionCallOutputPayload,
+        #[serde(default, skip_serializing_if = "Option::is_none")]
+        #[ts(optional)]
+        metadata: Option<ResponseItemMetadata>,
     },
     CustomToolCall {
         #[serde(default, skip_serializing)]
@@ -840,6 +868,9 @@ pub enum ResponseItem {
         call_id: String,
         name: String,
         input: String,
+        #[serde(default, skip_serializing_if = "Option::is_none")]
+        #[ts(optional)]
+        metadata: Option<ResponseItemMetadata>,
     },
     // `custom_tool_call_output.output` uses the same wire encoding as
     // `function_call_output.output` so freeform tools can return either plain
@@ -852,6 +883,9 @@ pub enum ResponseItem {
         #[ts(as = "FunctionCallOutputBody")]
         #[schemars(with = "FunctionCallOutputBody")]
         output: FunctionCallOutputPayload,
+        #[serde(default, skip_serializing_if = "Option::is_none")]
+        #[ts(optional)]
+        metadata: Option<ResponseItemMetadata>,
     },
     ToolSearchOutput {
         call_id: Option<String>,
@@ -859,6 +893,9 @@ pub enum ResponseItem {
         execution: String,
         #[ts(type = "unknown[]")]
         tools: Vec<serde_json::Value>,
+        #[serde(default, skip_serializing_if = "Option::is_none")]
+        #[ts(optional)]
+        metadata: Option<ResponseItemMetadata>,
     },
     // Emitted by the Responses API when the agent triggers a web search.
     // Example payload (from SSE `response.output_item.done`):
@@ -878,6 +915,9 @@ pub enum ResponseItem {
         #[serde(default, skip_serializing_if = "Option::is_none")]
         #[ts(optional)]
         action: Option<WebSearchAction>,
+        #[serde(default, skip_serializing_if = "Option::is_none")]
+        #[ts(optional)]
+        metadata: Option<ResponseItemMetadata>,
     },
     // Emitted by the Responses API when the agent triggers image generation.
     // Example payload:
@@ -895,16 +935,29 @@ pub enum ResponseItem {
         #[ts(optional)]
         revised_prompt: Option<String>,
         result: String,
+        #[serde(default, skip_serializing_if = "Option::is_none")]
+        #[ts(optional)]
+        metadata: Option<ResponseItemMetadata>,
     },
     #[serde(alias = "compaction_summary")]
     Compaction {
         encrypted_content: String,
+        #[serde(default, skip_serializing_if = "Option::is_none")]
+        #[ts(optional)]
+        metadata: Option<ResponseItemMetadata>,
+    },
+    CompactionTrigger {
+        #[serde(default, skip_serializing_if = "Option::is_none")]
+        #[ts(optional)]
+        metadata: Option<ResponseItemMetadata>,
     },
-    CompactionTrigger,
     ContextCompaction {
         #[serde(default, skip_serializing_if = "Option::is_none")]
         #[ts(optional)]
         encrypted_content: Option<String>,
+        #[serde(default, skip_serializing_if = "Option::is_none")]
+        #[ts(optional)]
+        metadata: Option<ResponseItemMetadata>,
     },
     #[serde(other)]
     Other,
@@ -915,6 +968,75 @@ impl ResponseItem {
     pub fn is_user_message(&self) -> bool {
         matches!(self, Self::Message { role, .. } if role == "user")
     }
+
+    /// Returns the non-empty turn ID stamped onto this item, if present.
+    pub fn turn_id(&self) -> Option<&str> {
+        self.metadata()
+            .and_then(|metadata| metadata.turn_id.as_deref())
+            .filter(|turn_id| !turn_id.is_empty())
+    }
+
+    /// Stamps the item with `turn_id` unless it already has a non-empty turn ID.
+    pub fn stamp_turn_id_if_missing(&mut self, turn_id: &str) {
+        if turn_id.is_empty() || self.turn_id().is_some() {
+            return;
+        }
+        let Some(metadata) = self.metadata_mut() else {
+            return;
+        };
+        metadata
+            .get_or_insert_with(ResponseItemMetadata::default)
+            .turn_id = Some(turn_id.to_string());
+    }
+
+    /// Removes Responses API item metadata before sending to a provider that does not accept it.
+    pub fn clear_metadata(&mut self) {
+        if let Some(metadata) = self.metadata_mut() {
+            *metadata = None;
+        }
+    }
+
+    fn metadata(&self) -> Option<&ResponseItemMetadata> {
+        match self {
+            Self::Message { metadata, .. }
+            | Self::AgentMessage { metadata, .. }
+            | Self::Reasoning { metadata, .. }
+            | Self::LocalShellCall { metadata, .. }
+            | Self::FunctionCall { metadata, .. }
+            | Self::ToolSearchCall { metadata, .. }
+            | Self::FunctionCallOutput { metadata, .. }
+            | Self::CustomToolCall { metadata, .. }
+            | Self::CustomToolCallOutput { metadata, .. }
+            | Self::ToolSearchOutput { metadata, .. }
+            | Self::WebSearchCall { metadata, .. }
+            | Self::ImageGenerationCall { metadata, .. }
+            | Self::Compaction { metadata, .. }
+            | Self::CompactionTrigger { metadata }
+            | Self::ContextCompaction { metadata, .. } => metadata.as_ref(),
+            Self::Other => None,
+        }
+    }
+
+    fn metadata_mut(&mut self) -> Option<&mut Option<ResponseItemMetadata>> {
+        match self {
+            Self::Message { metadata, .. }
+            | Self::AgentMessage { metadata, .. }
+            | Self::Reasoning { metadata, .. }
+            | Self::LocalShellCall { metadata, .. }
+            | Self::FunctionCall { metadata, .. }
+            | Self::ToolSearchCall { metadata, .. }
+            | Self::FunctionCallOutput { metadata, .. }
+            | Self::CustomToolCall { metadata, .. }
+            | Self::CustomToolCallOutput { metadata, .. }
+            | Self::ToolSearchOutput { metadata, .. }
+            | Self::WebSearchCall { metadata, .. }
+            | Self::ImageGenerationCall { metadata, .. }
+            | Self::Compaction { metadata, .. }
+            | Self::CompactionTrigger { metadata }
+            | Self::ContextCompaction { metadata, .. } => Some(metadata),
+            Self::Other => None,
+        }
+    }
 }
 
 pub const BASE_INSTRUCTIONS_DEFAULT: &str = include_str!("prompts/base_instructions/default.md");
@@ -1153,13 +1275,20 @@ impl From<ResponseInputItem> for ResponseItem {
                 content,
                 id: None,
                 phase,
+                metadata: None,
+            },
+            ResponseInputItem::FunctionCallOutput { call_id, output } => Self::FunctionCallOutput {
+                call_id,
+                output,
+                metadata: None,
             },
-            ResponseInputItem::FunctionCallOutput { call_id, output } => {
-                Self::FunctionCallOutput { call_id, output }
-            }
             ResponseInputItem::McpToolCallOutput { call_id, output } => {
                 let output = output.into_function_call_output_payload();
-                Self::FunctionCallOutput { call_id, output }
+                Self::FunctionCallOutput {
+                    call_id,
+                    output,
+                    metadata: None,
+                }
             }
             ResponseInputItem::CustomToolCallOutput {
                 call_id,
@@ -1169,6 +1298,7 @@ impl From<ResponseInputItem> for ResponseItem {
                 call_id,
                 name,
                 output,
+                metadata: None,
             },
             ResponseInputItem::ToolSearchOutput {
                 call_id,
@@ -1180,6 +1310,7 @@ impl From<ResponseInputItem> for ResponseItem {
                 status,
                 execution,
                 tools,
+                metadata: None,
             },
         }
     }
@@ -1718,10 +1849,64 @@ mod tests {
                     text: "still working".to_string(),
                 }],
                 phase: Some(MessagePhase::Commentary),
+                metadata: None,
             }
         );
     }
 
+    #[test]
+    fn response_item_metadata_round_trips_and_stamps_turn_ids() -> Result<()> {
+        let mut item = response_item_with_metadata(Some(response_item_metadata("turn-1")));
+        let round_trip: ResponseItem = serde_json::from_value(serde_json::to_value(&item)?)?;
+        assert_eq!(round_trip, item);
+
+        let unknown_metadata: ResponseItem = serde_json::from_value(serde_json::json!({
+            "type": "message",
+            "role": "user",
+            "content": [{"type": "input_text", "text": "hello"}],
+            "metadata": {
+                "turn_id": "turn-1",
+                "other": "ignored",
+            },
+        }))?;
+        assert_eq!(unknown_metadata, item);
+
+        item.stamp_turn_id_if_missing("turn-2");
+        assert_eq!(item.turn_id(), Some("turn-1"));
+
+        let mut empty_turn_id = response_item_with_metadata(Some(response_item_metadata("")));
+        empty_turn_id.stamp_turn_id_if_missing("turn-1");
+        assert_eq!(empty_turn_id.turn_id(), Some("turn-1"));
+
+        let mut missing_turn_id = response_item_with_metadata(/*metadata*/ None);
+        missing_turn_id.stamp_turn_id_if_missing("");
+        missing_turn_id.stamp_turn_id_if_missing("turn-1");
+        assert_eq!(missing_turn_id.turn_id(), Some("turn-1"));
+
+        let mut other = ResponseItem::Other;
+        other.stamp_turn_id_if_missing("turn-1");
+        assert_eq!(other.turn_id(), None);
+        Ok(())
+    }
+
+    fn response_item_with_metadata(metadata: Option<ResponseItemMetadata>) -> ResponseItem {
+        ResponseItem::Message {
+            id: None,
+            role: "user".to_string(),
+            content: vec![ContentItem::InputText {
+                text: "hello".to_string(),
+            }],
+            phase: None,
+            metadata,
+        }
+    }
+
+    fn response_item_metadata(turn_id: &str) -> ResponseItemMetadata {
+        ResponseItemMetadata {
+            turn_id: Some(turn_id.to_string()),
+        }
+    }
+
     #[test]
     fn image_detail_roundtrips_all_wire_values() -> Result<()> {
         assert_eq!(
@@ -1823,6 +2008,7 @@ mod tests {
                 status: "completed".to_string(),
                 revised_prompt: Some("A small blue square".to_string()),
                 result: "Zm9v".to_string(),
+                metadata: None,
             }
         );
     }
@@ -1844,6 +2030,7 @@ mod tests {
                 status: "completed".to_string(),
                 revised_prompt: None,
                 result: "Zm9v".to_string(),
+                metadata: None,
             }
         );
     }
@@ -2195,6 +2382,7 @@ mod tests {
                 namespace: Some("mcp__codex_apps__gmail".to_string()),
                 arguments: "{\"top_k\":5}".to_string(),
                 call_id: "call-1".to_string(),
+                metadata: None,
             }
         );
     }
@@ -2541,6 +2729,7 @@ mod tests {
             item,
             ResponseItem::Compaction {
                 encrypted_content: "abc".into(),
+                metadata: None,
             }
         );
         Ok(())
@@ -2556,6 +2745,7 @@ mod tests {
             item,
             ResponseItem::ContextCompaction {
                 encrypted_content: Some("abc".into()),
+                metadata: None,
             }
         );
         Ok(())
@@ -2563,12 +2753,29 @@ mod tests {
 
     #[test]
     fn serializes_compaction_trigger_without_payload() -> Result<()> {
-        let item = ResponseItem::CompactionTrigger;
+        let item = ResponseItem::CompactionTrigger { metadata: None };
+
+        assert_eq!(
+            serde_json::to_value(item)?,
+            serde_json::json!({
+                "type": "compaction_trigger",
+            })
+        );
+        Ok(())
+    }
+
+    #[test]
+    fn serializes_stamped_compaction_trigger_metadata() -> Result<()> {
+        let mut item = ResponseItem::CompactionTrigger { metadata: None };
+        item.stamp_turn_id_if_missing("turn-1");
 
         assert_eq!(
             serde_json::to_value(item)?,
             serde_json::json!({
                 "type": "compaction_trigger",
+                "metadata": {
+                    "turn_id": "turn-1",
+                },
             })
         );
         Ok(())
@@ -2580,7 +2787,7 @@ mod tests {
 
         let item: ResponseItem = serde_json::from_str(json)?;
 
-        assert_eq!(item, ResponseItem::CompactionTrigger);
+        assert_eq!(item, ResponseItem::CompactionTrigger { metadata: None });
         Ok(())
     }
 
@@ -2677,6 +2884,7 @@ mod tests {
                 id: expected_id.clone(),
                 status: expected_status.clone(),
                 action: expected_action.clone(),
+                metadata: None,
             };
             assert_eq!(parsed, expected);
 
@@ -2764,6 +2972,7 @@ mod tests {
                     "query": "calendar create",
                     "limit": 1,
                 }),
+                metadata: None,
             }
         );
 
@@ -2824,6 +3033,7 @@ mod tests {
                         "additionalProperties": false,
                     }
                 })],
+                metadata: None,
             }
         );
 
@@ -2877,6 +3087,7 @@ mod tests {
                 arguments: serde_json::json!({
                     "paths": ["crm"],
                 }),
+                metadata: None,
             }
         );
 
@@ -2896,6 +3107,7 @@ mod tests {
                 status: "completed".to_string(),
                 execution: "server".to_string(),
                 tools: vec![],
+                metadata: None,
             }
         );
 
diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs
index 92b8194b4762..cde22ee3f36d 100644
--- a/codex-rs/protocol/src/protocol.rs
+++ b/codex-rs/protocol/src/protocol.rs
@@ -41,6 +41,7 @@ use crate::models::MessagePhase;
 use crate::models::PermissionProfile;
 use crate::models::ResponseInputItem;
 use crate::models::ResponseItem;
+use crate::models::ResponseItemMetadata;
 use crate::models::SandboxEnforcement;
 use crate::models::WebSearchAction;
 use crate::num_format::format_with_separators;
@@ -683,6 +684,9 @@ pub struct InterAgentCommunication {
     #[serde(default, skip_serializing_if = "Option::is_none")]
     #[ts(optional)]
     pub encrypted_content: Option<String>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[ts(optional)]
+    pub metadata: Option<ResponseItemMetadata>,
     pub trigger_turn: bool,
 }
 
@@ -700,6 +704,7 @@ impl InterAgentCommunication {
             other_recipients,
             content,
             encrypted_content: None,
+            metadata: None,
             trigger_turn,
         }
     }
@@ -717,6 +722,7 @@ impl InterAgentCommunication {
             other_recipients,
             content: String::new(),
             encrypted_content: Some(encrypted_content),
+            metadata: None,
             trigger_turn,
         }
     }
@@ -744,6 +750,7 @@ impl InterAgentCommunication {
             author: self.author.to_string(),
             recipient: self.recipient.to_string(),
             content: vec![content],
+            metadata: self.metadata.clone(),
         }
     }
 
@@ -2946,6 +2953,7 @@ impl From<CompactedItem> for ResponseItem {
                 text: value.message,
             }],
             phase: None,
+            metadata: None,
         }
     }
 }
@@ -4231,6 +4239,7 @@ mod tests {
             other_recipients: vec![AgentPath::root().join("worker").expect("recipient path")],
             content: "review the diff".to_string(),
             encrypted_content: None,
+            metadata: None,
             trigger_turn: true,
         };
 
diff --git a/codex-rs/rollout-trace/src/inference.rs b/codex-rs/rollout-trace/src/inference.rs
index a4977b0dc57b..f826d9f47c66 100644
--- a/codex-rs/rollout-trace/src/inference.rs
+++ b/codex-rs/rollout-trace/src/inference.rs
@@ -504,6 +504,7 @@ mod tests {
                 text: "raw reasoning".to_string(),
             }]),
             encrypted_content: Some("encoded".to_string()),
+            metadata: None,
         };
 
         let normal = serde_json::to_value(&item).expect("response item serializes");
diff --git a/codex-rs/rollout-trace/src/reducer/conversation/normalize.rs b/codex-rs/rollout-trace/src/reducer/conversation/normalize.rs
index e67673b96261..e7a3253f6fec 100644
--- a/codex-rs/rollout-trace/src/reducer/conversation/normalize.rs
+++ b/codex-rs/rollout-trace/src/reducer/conversation/normalize.rs
@@ -194,6 +194,7 @@ fn normalize_agent_message_item(
         author,
         recipient,
         content,
+        ..
     } = response_item
     else {
         bail!("item in payload {raw_payload_id} was not an agent_message");
diff --git a/codex-rs/rollout/src/policy.rs b/codex-rs/rollout/src/policy.rs
index 40ca77c97e72..ba2ec23c2c20 100644
--- a/codex-rs/rollout/src/policy.rs
+++ b/codex-rs/rollout/src/policy.rs
@@ -44,7 +44,7 @@ pub fn should_persist_response_item(item: &ResponseItem) -> bool {
         | ResponseItem::ImageGenerationCall { .. }
         | ResponseItem::Compaction { .. }
         | ResponseItem::ContextCompaction { .. } => true,
-        ResponseItem::CompactionTrigger => false,
+        ResponseItem::CompactionTrigger { .. } => false,
         ResponseItem::Other => false,
     }
 }
@@ -66,7 +66,7 @@ pub fn should_persist_response_item_for_memories(item: &ResponseItem) -> bool {
         | ResponseItem::Reasoning { .. }
         | ResponseItem::ImageGenerationCall { .. }
         | ResponseItem::Compaction { .. }
-        | ResponseItem::CompactionTrigger
+        | ResponseItem::CompactionTrigger { .. }
         | ResponseItem::ContextCompaction { .. }
         | ResponseItem::Other => false,
     }
diff --git a/codex-rs/rollout/src/tests.rs b/codex-rs/rollout/src/tests.rs
index e489eaba59a9..3c1fc8bb57f2 100644
--- a/codex-rs/rollout/src/tests.rs
+++ b/codex-rs/rollout/src/tests.rs
@@ -1305,6 +1305,7 @@ async fn test_updated_at_uses_file_mtime() -> Result<()> {
                     text: format!("reply-{idx}"),
                 }],
                 phase: None,
+                metadata: None,
             }),
         };
         writeln!(file, "{}", serde_json::to_string(&response_line)?)?;
diff --git a/codex-rs/state/src/extract.rs b/codex-rs/state/src/extract.rs
index fbb63dceae5e..3e41b44aaaeb 100644
--- a/codex-rs/state/src/extract.rs
+++ b/codex-rs/state/src/extract.rs
@@ -191,6 +191,7 @@ mod tests {
                 text: "hello from response item".to_string(),
             }],
             phase: None,
+            metadata: None,
         });
 
         apply_rollout_item(&mut metadata, &item, "test-provider");
diff --git a/codex-rs/tools/src/response_history.rs b/codex-rs/tools/src/response_history.rs
index cee6f438cb9d..4196d7c3efc9 100644
--- a/codex-rs/tools/src/response_history.rs
+++ b/codex-rs/tools/src/response_history.rs
@@ -95,6 +95,7 @@ mod tests {
                 }
             }],
             phase: None,
+            metadata: None,
         }
     }
 
diff --git a/codex-rs/tui/src/app/side.rs b/codex-rs/tui/src/app/side.rs
index 04831e16e495..61fed28158a8 100644
--- a/codex-rs/tui/src/app/side.rs
+++ b/codex-rs/tui/src/app/side.rs
@@ -453,6 +453,7 @@ impl App {
                 text: SIDE_BOUNDARY_PROMPT.to_string(),
             }],
             phase: None,
+            metadata: None,
         }
     }
 
