diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json
index ca247f2d5503..f3983e31f41c 100644
--- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json
+++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json
@@ -2683,6 +2683,12 @@
             "name": {
               "type": "string"
             },
+            "namespace": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "status": {
               "type": [
                 "string",
@@ -3631,6 +3637,13 @@
         "ephemeral": {
           "type": "boolean"
         },
+        "lastTurnId": {
+          "description": "Optional last turn id to fork through, inclusive.\n\nWhen specified, turns after `last_turn_id` are omitted from the fork. The referenced turn cannot be in progress.",
+          "type": [
+            "string",
+            "null"
+          ]
+        },
         "model": {
           "description": "Configuration overrides for the forked thread, if any.",
           "type": [
@@ -3673,13 +3686,6 @@
             }
           ],
           "description": "Optional client-supplied analytics source classification for this forked thread."
-        },
-        "lastTurnId": {
-          "description": "Optional last turn id to fork through, inclusive.\n\nWhen specified, turns after `last_turn_id` are omitted from the fork. The referenced turn cannot be in progress.",
-          "type": [
-            "string",
-            "null"
-          ]
         }
       },
       "required": [
@@ -6873,4 +6879,4 @@
     }
   ],
   "title": "ClientRequest"
-}
+}
\ No newline at end of file
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 341c325b1d28..b77b512b36e3 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
@@ -15510,6 +15510,12 @@
               "name": {
                 "type": "string"
               },
+              "namespace": {
+                "type": [
+                  "string",
+                  "null"
+                ]
+              },
               "status": {
                 "type": [
                   "string",
@@ -17279,6 +17285,13 @@
           "ephemeral": {
             "type": "boolean"
           },
+          "lastTurnId": {
+            "description": "Optional last turn id to fork through, inclusive.\n\nWhen specified, turns after `last_turn_id` are omitted from the fork. The referenced turn cannot be in progress.",
+            "type": [
+              "string",
+              "null"
+            ]
+          },
           "model": {
             "description": "Configuration overrides for the forked thread, if any.",
             "type": [
@@ -17321,13 +17334,6 @@
               }
             ],
             "description": "Optional client-supplied analytics source classification for this forked thread."
-          },
-        "lastTurnId": {
-          "description": "Optional last turn id to fork through, inclusive.\n\nWhen specified, turns after `last_turn_id` are omitted from the fork. The referenced turn cannot be in progress.",
-            "type": [
-              "string",
-              "null"
-            ]
           }
         },
         "required": [
@@ -20874,4 +20880,4 @@
   },
   "title": "CodexAppServerProtocol",
   "type": "object"
-}
+}
\ No newline at end of file
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 488bd6016e0b..bdd61d6b9436 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
@@ -11914,6 +11914,12 @@
             "name": {
               "type": "string"
             },
+            "namespace": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "status": {
               "type": [
                 "string",
@@ -15058,6 +15064,13 @@
         "ephemeral": {
           "type": "boolean"
         },
+        "lastTurnId": {
+          "description": "Optional last turn id to fork through, inclusive.\n\nWhen specified, turns after `last_turn_id` are omitted from the fork. The referenced turn cannot be in progress.",
+          "type": [
+            "string",
+            "null"
+          ]
+        },
         "model": {
           "description": "Configuration overrides for the forked thread, if any.",
           "type": [
@@ -15100,13 +15113,6 @@
             }
           ],
           "description": "Optional client-supplied analytics source classification for this forked thread."
-        },
-        "lastTurnId": {
-          "description": "Optional last turn id to fork through, inclusive.\n\nWhen specified, turns after `last_turn_id` are omitted from the fork. The referenced turn cannot be in progress.",
-          "type": [
-            "string",
-            "null"
-          ]
         }
       },
       "required": [
@@ -18652,4 +18658,4 @@
   },
   "title": "CodexAppServerProtocolV2",
   "type": "object"
-}
+}
\ No newline at end of file
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 01ac63c268d2..dad669b139da 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json
@@ -751,6 +751,12 @@
             "name": {
               "type": "string"
             },
+            "namespace": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "status": {
               "type": [
                 "string",
diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json
index 7a00e3803ad6..76278b106ba3 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json
@@ -126,6 +126,13 @@
     "ephemeral": {
       "type": "boolean"
     },
+    "lastTurnId": {
+      "description": "Optional last turn id to fork through, inclusive.\n\nWhen specified, turns after `last_turn_id` are omitted from the fork. The referenced turn cannot be in progress.",
+      "type": [
+        "string",
+        "null"
+      ]
+    },
     "model": {
       "description": "Configuration overrides for the forked thread, if any.",
       "type": [
@@ -168,13 +175,6 @@
         }
       ],
       "description": "Optional client-supplied analytics source classification for this forked thread."
-    },
-    "lastTurnId": {
-      "description": "Optional last turn id to fork through, inclusive.\n\nWhen specified, turns after `last_turn_id` are omitted from the fork. The referenced turn cannot be in progress.",
-      "type": [
-        "string",
-        "null"
-      ]
     }
   },
   "required": [
@@ -182,4 +182,4 @@
   ],
   "title": "ThreadForkParams",
   "type": "object"
-}
+}
\ No newline at end of file
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 af078af68ef2..0cd7d9ae7b1f 100644
--- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json
+++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json
@@ -821,6 +821,12 @@
             "name": {
               "type": "string"
             },
+            "namespace": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "status": {
               "type": [
                 "string",
diff --git a/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts b/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts
index 62547d3e4055..769b1b08fc83 100644
--- a/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts
+++ b/codex-rs/app-server-protocol/schema/typescript/ResponseItem.ts
@@ -20,4 +20,4 @@ id?: string,
 /**
  * Set when using the Responses API.
  */
-call_id: string | null, status: LocalShellStatus, action: LocalShellAction, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "function_call", id?: string, name: string, namespace?: string, arguments: string, call_id: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "tool_search_call", id?: string, call_id: string | null, status?: string, execution: string, arguments: unknown, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "function_call_output", id?: string, call_id: string, output: FunctionCallOutputBody, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "custom_tool_call", id?: string, status?: string, call_id: string, name: string, input: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "custom_tool_call_output", id?: string, call_id: string, name?: string, output: FunctionCallOutputBody, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "tool_search_output", id?: string, call_id: string | null, status: string, execution: string, tools: unknown[], internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "web_search_call", id?: string, status?: string, action?: WebSearchAction, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "image_generation_call", id?: string, status: string, revised_prompt?: string, result: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "compaction", id?: string, encrypted_content: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "compaction_trigger", } | { "type": "context_compaction", id?: string, encrypted_content?: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "other" };
+call_id: string | null, status: LocalShellStatus, action: LocalShellAction, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "function_call", id?: string, name: string, namespace?: string, arguments: string, call_id: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "tool_search_call", id?: string, call_id: string | null, status?: string, execution: string, arguments: unknown, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "function_call_output", id?: string, call_id: string, output: FunctionCallOutputBody, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "custom_tool_call", id?: string, status?: string, call_id: string, name: string, namespace?: string, input: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "custom_tool_call_output", id?: string, call_id: string, name?: string, output: FunctionCallOutputBody, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "tool_search_output", id?: string, call_id: string | null, status: string, execution: string, tools: unknown[], internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "web_search_call", id?: string, status?: string, action?: WebSearchAction, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "image_generation_call", id?: string, status: string, revised_prompt?: string, result: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "compaction", id?: string, encrypted_content: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "compaction_trigger", } | { "type": "context_compaction", id?: string, encrypted_content?: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "other" };
diff --git a/codex-rs/core/src/codex_delegate_tests.rs b/codex-rs/core/src/codex_delegate_tests.rs
index 2791c887e6cb..9513dcc56cf1 100644
--- a/codex-rs/core/src/codex_delegate_tests.rs
+++ b/codex-rs/core/src/codex_delegate_tests.rs
@@ -80,6 +80,7 @@ async fn forward_events_cancelled_while_send_blocked_shuts_down_delegate() {
                     status: None,
                     call_id: "call-1".to_string(),
                     name: "tool".to_string(),
+                    namespace: None,
                     input: "{}".to_string(),
                     internal_chat_message_metadata_passthrough: None,
                 },
diff --git a/codex-rs/core/src/context_manager/history_tests.rs b/codex-rs/core/src/context_manager/history_tests.rs
index 5c320b798d09..ad46f7be7b11 100644
--- a/codex-rs/core/src/context_manager/history_tests.rs
+++ b/codex-rs/core/src/context_manager/history_tests.rs
@@ -513,6 +513,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() {
             status: None,
             call_id: "tool-1".to_string(),
             name: "js_repl".to_string(),
+            namespace: None,
             input: "view_image".to_string(),
             internal_chat_message_metadata_passthrough: None,
         },
@@ -582,6 +583,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() {
             status: None,
             call_id: "tool-1".to_string(),
             name: "js_repl".to_string(),
+            namespace: None,
             input: "view_image".to_string(),
             internal_chat_message_metadata_passthrough: None,
         },
@@ -1090,6 +1092,7 @@ fn remove_first_item_handles_custom_tool_pair() {
             status: None,
             call_id: "tool-1".to_string(),
             name: "my_tool".to_string(),
+            namespace: None,
             input: "{}".to_string(),
             internal_chat_message_metadata_passthrough: None,
         },
@@ -1378,6 +1381,7 @@ fn normalize_adds_missing_output_for_custom_tool_call() {
         status: None,
         call_id: "tool-x".to_string(),
         name: "custom".to_string(),
+        namespace: None,
         input: "{}".to_string(),
         internal_chat_message_metadata_passthrough: None,
     }];
@@ -1393,6 +1397,7 @@ fn normalize_adds_missing_output_for_custom_tool_call() {
                 status: None,
                 call_id: "tool-x".to_string(),
                 name: "custom".to_string(),
+                namespace: None,
                 input: "{}".to_string(),
                 internal_chat_message_metadata_passthrough: None,
             },
@@ -1512,6 +1517,7 @@ fn normalize_mixed_inserts_and_removals() {
             status: None,
             call_id: "t1".to_string(),
             name: "tool".to_string(),
+            namespace: None,
             input: "{}".to_string(),
             internal_chat_message_metadata_passthrough: None,
         },
@@ -1556,6 +1562,7 @@ fn normalize_mixed_inserts_and_removals() {
                 status: None,
                 call_id: "t1".to_string(),
                 name: "tool".to_string(),
+                namespace: None,
                 input: "{}".to_string(),
                 internal_chat_message_metadata_passthrough: None,
             },
@@ -1668,6 +1675,7 @@ fn normalize_adds_missing_output_for_custom_tool_call_panics_in_debug() {
         status: None,
         call_id: "tool-x".to_string(),
         name: "custom".to_string(),
+        namespace: None,
         input: "{}".to_string(),
         internal_chat_message_metadata_passthrough: None,
     }];
@@ -1810,6 +1818,7 @@ fn normalize_mixed_inserts_and_removals_panics_in_debug() {
             status: None,
             call_id: "t1".to_string(),
             name: "tool".to_string(),
+            namespace: None,
             input: "{}".to_string(),
             internal_chat_message_metadata_passthrough: None,
         },
diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs
index d6336acd57a8..ece9d446b57e 100644
--- a/codex-rs/core/src/session/tests.rs
+++ b/codex-rs/core/src/session/tests.rs
@@ -10482,6 +10482,7 @@ async fn fatal_tool_error_stops_turn_and_reports_error() {
         status: None,
         call_id: "call-1".to_string(),
         name: "shell_command".to_string(),
+        namespace: None,
         input: "{}".to_string(),
         internal_chat_message_metadata_passthrough: None,
     };
diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs
index 2e948b9c1ed7..f16b525002fc 100644
--- a/codex-rs/core/src/session/turn.rs
+++ b/codex-rs/core/src/session/turn.rs
@@ -2080,8 +2080,14 @@ async fn try_run_sampling_request(
                 }
             }
             ResponseEvent::OutputItemAdded(item) => {
-                if let ResponseItem::CustomToolCall { call_id, name, .. } = &item {
-                    let tool_name = ToolName::plain(name.as_str());
+                if let ResponseItem::CustomToolCall {
+                    call_id,
+                    name,
+                    namespace,
+                    ..
+                } = &item
+                {
+                    let tool_name = ToolName::new(namespace.clone(), name.as_str());
                     active_tool_argument_diff_consumer = tool_runtime
                         .create_diff_consumer(&tool_name)
                         .map(|consumer| (call_id.clone(), consumer));
diff --git a/codex-rs/core/src/stream_events_utils_tests.rs b/codex-rs/core/src/stream_events_utils_tests.rs
index 78a219add655..b3312ad49d28 100644
--- a/codex-rs/core/src/stream_events_utils_tests.rs
+++ b/codex-rs/core/src/stream_events_utils_tests.rs
@@ -116,6 +116,7 @@ fn external_context_pollution_items_exclude_local_tool_calls() {
             status: None,
             call_id: "custom-1".to_string(),
             name: "apply_patch".to_string(),
+            namespace: None,
             input: "*** Begin Patch\n*** End Patch\n".to_string(),
             internal_chat_message_metadata_passthrough: None,
         },
diff --git a/codex-rs/core/src/tools/router.rs b/codex-rs/core/src/tools/router.rs
index 4bdd121f6ea2..aca86b124b3a 100644
--- a/codex-rs/core/src/tools/router.rs
+++ b/codex-rs/core/src/tools/router.rs
@@ -25,7 +25,7 @@ use tracing::instrument;
 
 pub use crate::tools::context::ToolCallSource;
 
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, PartialEq)]
 pub struct ToolCall {
     pub tool_name: ToolName,
     pub call_id: String,
@@ -147,11 +147,12 @@ impl ToolRouter {
             ResponseItem::ToolSearchCall { .. } => Ok(None),
             ResponseItem::CustomToolCall {
                 name,
+                namespace,
                 input,
                 call_id,
                 ..
             } => Ok(Some(ToolCall {
-                tool_name: ToolName::plain(name),
+                tool_name: ToolName::new(namespace, name),
                 call_id,
                 payload: ToolPayload::Custom { input },
             })),
diff --git a/codex-rs/core/src/tools/router_tests.rs b/codex-rs/core/src/tools/router_tests.rs
index 6f399fd5fc85..62391395f826 100644
--- a/codex-rs/core/src/tools/router_tests.rs
+++ b/codex-rs/core/src/tools/router_tests.rs
@@ -179,6 +179,35 @@ async fn build_tool_call_uses_namespace_for_registry_name() -> anyhow::Result<()
     Ok(())
 }
 
+#[tokio::test]
+async fn build_custom_tool_call_uses_namespace_for_registry_name() -> anyhow::Result<()> {
+    let tool_name = "exec".to_string();
+
+    let call = ToolRouter::build_tool_call(ResponseItem::CustomToolCall {
+        id: None,
+        status: None,
+        call_id: "call-namespace".to_string(),
+        name: tool_name.clone(),
+        namespace: Some("mcp__python".to_string()),
+        input: "print('hello')".to_string(),
+        internal_chat_message_metadata_passthrough: None,
+    })?
+    .expect("custom_tool_call should produce a tool call");
+
+    assert_eq!(
+        call,
+        ToolCall {
+            tool_name: ToolName::namespaced("mcp__python", tool_name),
+            call_id: "call-namespace".to_string(),
+            payload: ToolPayload::Custom {
+                input: "print('hello')".to_string(),
+            },
+        }
+    );
+
+    Ok(())
+}
+
 #[tokio::test]
 async fn mcp_parallel_support_uses_handler_data() -> anyhow::Result<()> {
     let (_, turn) = make_session_and_context().await;
diff --git a/codex-rs/core/src/turn_timing_tests.rs b/codex-rs/core/src/turn_timing_tests.rs
index 956468da2371..fcb34b474212 100644
--- a/codex-rs/core/src/turn_timing_tests.rs
+++ b/codex-rs/core/src/turn_timing_tests.rs
@@ -121,6 +121,7 @@ fn response_item_records_turn_ttft_for_first_output_signals() {
             status: None,
             call_id: "call-2".to_string(),
             name: "custom".to_string(),
+            namespace: None,
             input: "echo hi".to_string(),
             internal_chat_message_metadata_passthrough: None,
         }
diff --git a/codex-rs/core/tests/common/responses.rs b/codex-rs/core/tests/common/responses.rs
index 6a605a80b3ae..9569df1c90ec 100644
--- a/codex-rs/core/tests/common/responses.rs
+++ b/codex-rs/core/tests/common/responses.rs
@@ -895,6 +895,24 @@ pub fn ev_custom_tool_call(call_id: &str, name: &str, input: &str) -> Value {
     })
 }
 
+pub fn ev_custom_tool_call_with_namespace(
+    call_id: &str,
+    namespace: &str,
+    name: &str,
+    input: &str,
+) -> Value {
+    serde_json::json!({
+        "type": "response.output_item.done",
+        "item": {
+            "type": "custom_tool_call",
+            "call_id": call_id,
+            "namespace": namespace,
+            "name": name,
+            "input": input
+        }
+    })
+}
+
 pub fn ev_local_shell_call(call_id: &str, status: &str, command: Vec<&str>) -> Value {
     serde_json::json!({
         "type": "response.output_item.done",
diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs
index 4df2bca1c10e..7acf67da5697 100644
--- a/codex-rs/core/tests/suite/client.rs
+++ b/codex-rs/core/tests/suite/client.rs
@@ -785,6 +785,7 @@ async fn resume_replays_legacy_js_repl_image_rollout_shapes() {
         status: None,
         call_id: "legacy-js-call".to_string(),
         name: "js_repl".to_string(),
+        namespace: None,
         input: "console.log('legacy image flow')".to_string(),
         internal_chat_message_metadata_passthrough: None,
     };
@@ -976,6 +977,7 @@ async fn resume_replays_image_tool_outputs_with_detail() {
                 status: Some("completed".to_string()),
                 call_id: custom_call_id.to_string(),
                 name: "js_repl".to_string(),
+                namespace: None,
                 input: "console.log('image flow')".to_string(),
                 internal_chat_message_metadata_passthrough: None,
             }),
@@ -2912,6 +2914,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() {
         status: Some("completed".into()),
         call_id: "custom-tool-call-id".into(),
         name: "custom_tool".into(),
+        namespace: None,
         input: "{}".into(),
         internal_chat_message_metadata_passthrough: None,
     });
diff --git a/codex-rs/core/tests/suite/tools.rs b/codex-rs/core/tests/suite/tools.rs
index 83584b8ec074..5e47eae52e2c 100644
--- a/codex-rs/core/tests/suite/tools.rs
+++ b/codex-rs/core/tests/suite/tools.rs
@@ -20,6 +20,7 @@ use core_test_support::assert_regex_match;
 use core_test_support::responses::ev_assistant_message;
 use core_test_support::responses::ev_completed;
 use core_test_support::responses::ev_custom_tool_call;
+use core_test_support::responses::ev_custom_tool_call_with_namespace;
 use core_test_support::responses::ev_function_call;
 use core_test_support::responses::ev_response_created;
 use core_test_support::responses::mount_sse_once;
@@ -177,6 +178,80 @@ async fn custom_tool_unknown_returns_custom_output_error() -> Result<()> {
     Ok(())
 }
 
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn namespaced_custom_tool_call_preserves_namespace_through_dispatch_and_replay() -> Result<()>
+{
+    skip_if_no_network!(Ok(()));
+
+    let server = start_mock_server().await;
+    let mut builder = test_codex();
+    let test = builder.build(&server).await?;
+
+    let call_id = "custom-namespaced";
+    let namespace = "test_namespace::";
+    let tool_name = "unsupported_tool";
+    let input = "\"payload\"";
+
+    mount_sse_once(
+        &server,
+        sse(vec![
+            ev_response_created("resp-1"),
+            ev_custom_tool_call_with_namespace(call_id, namespace, tool_name, input),
+            ev_completed("resp-1"),
+        ]),
+    )
+    .await;
+    let mock = mount_sse_once(
+        &server,
+        sse(vec![
+            ev_assistant_message("msg-1", "done"),
+            ev_completed("resp-2"),
+        ]),
+    )
+    .await;
+
+    test.submit_turn_with_approval_and_permission_profile(
+        "invoke namespaced custom tool",
+        AskForApproval::Never,
+        PermissionProfile::Disabled,
+    )
+    .await?;
+
+    let request = mock.single_request();
+    let custom_tool_calls = request.inputs_of_type("custom_tool_call");
+    let turn_id = custom_tool_calls
+        .first()
+        .and_then(|item| item.pointer("/internal_chat_message_metadata_passthrough/turn_id"))
+        .and_then(Value::as_str)
+        .map(str::to_string)
+        .expect("custom tool call should include turn metadata");
+    assert_eq!(
+        (custom_tool_calls, request.custom_tool_call_output(call_id),),
+        (
+            vec![json!({
+                "type": "custom_tool_call",
+                "call_id": call_id,
+                "namespace": namespace,
+                "name": tool_name,
+                "input": input,
+                "internal_chat_message_metadata_passthrough": {
+                    "turn_id": turn_id,
+                },
+            })],
+            json!({
+                "type": "custom_tool_call_output",
+                "call_id": call_id,
+                "output": format!("unsupported custom tool call: {namespace}{tool_name}"),
+                "internal_chat_message_metadata_passthrough": {
+                    "turn_id": turn_id,
+                },
+            }),
+        )
+    );
+
+    Ok(())
+}
+
 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
 async fn shell_command_escalated_permissions_rejected_then_ok() -> Result<()> {
     skip_if_no_network!(Ok(()));
diff --git a/codex-rs/ext/image-generation/src/tests.rs b/codex-rs/ext/image-generation/src/tests.rs
index fe915746f9a7..1ec9371214a4 100644
--- a/codex-rs/ext/image-generation/src/tests.rs
+++ b/codex-rs/ext/image-generation/src/tests.rs
@@ -97,6 +97,7 @@ async fn recent_image_fallback_selects_newest_images_in_chronological_order() {
             status: Some("completed".to_string()),
             call_id: "code-mode-call".to_string(),
             name: "exec".to_string(),
+            namespace: None,
             input: String::new(),
             internal_chat_message_metadata_passthrough: None,
         },
diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs
index 2faad8734900..46aecb61b97f 100644
--- a/codex-rs/protocol/src/models.rs
+++ b/codex-rs/protocol/src/models.rs
@@ -1052,6 +1052,9 @@ pub enum ResponseItem {
 
         call_id: String,
         name: String,
+        #[serde(default, skip_serializing_if = "Option::is_none")]
+        #[ts(optional)]
+        namespace: Option<String>,
         input: String,
         #[serde(default, skip_serializing_if = "Option::is_none")]
         #[ts(optional)]
diff --git a/codex-rs/tools/src/tool_payload.rs b/codex-rs/tools/src/tool_payload.rs
index 71af45774321..101d95a278ef 100644
--- a/codex-rs/tools/src/tool_payload.rs
+++ b/codex-rs/tools/src/tool_payload.rs
@@ -3,7 +3,7 @@ use std::borrow::Cow;
 use codex_protocol::models::SearchToolCallParams;
 
 /// Canonical payload shapes accepted by model-visible tool runtimes.
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, PartialEq)]
 pub enum ToolPayload {
     Function { arguments: String },
     ToolSearch { arguments: SearchToolCallParams },
