Skip to content

tool_search results reuse the same namespace name, hard-failing the thread with Duplicate namespace name 'mcp__*' #44378

Open

Description

@cczxuan

What version of the Codex App are you using (From “About Codex” dialog)?

26.903.8094.0 (Windows). Bundled Codex core: codex-cli 0.153.4.

What subscription do you have?

None - this install is configured with a third-party model provider through an API key, not an OpenAI subscription.

What platform is your computer?

Microsoft Windows NT 10.0.26200.0 x64

What issue are you seeing?

Requests fail with:

{"error":{"message":"Duplicate namespace name 'mcp__rednote' in input[10].tools[0]. Namespace names must be unique.","type":"invalid_request_error","param":null,"code":"invalid_request_error"}}

When the model calls tool_search more than once in a single turn, each result is stored in the conversation as a tool_search_output item, and each of those items carries a tool entry of type "namespace" named mcp__. Two searches in one turn therefore put two entries with the same namespace name into a single request, and the whole request is rejected.

This is not a one-off failure. The duplicate entry stays in the thread history, so every later turn in that thread fails with the same error at the same index (input[10]). Restarting the app does not help - only starting a new thread recovers.

What steps can reproduce the bug?

  1. Configure one MCP server in ~/.codex/config.toml so its tools are exposed as deferred mcp__ tools. In my case: rednote (stdio, Node.js). I hit the same bug earlier with playwright.
  2. In a single turn, let the model issue two tool_search calls (two parallel calls with different queries work well - e.g. one searching for "login", another for "get_note_content"). Both results contain the same mcp__rednote namespace.
  3. Send any subsequent request in that thread. It fails with the error above and keeps failing on every following turn.

Session ids:

  • rednote case: session 01a0890d-c295-76d1-9b02-2cdc23fbe30e, turn 01a0890d-c8cc-7710-b387-de0e2cd91d67, error at input[10]
  • playwright case: session 2026-09-10T09-59-20-01a0890a-7be4-70e1-ba54-f1eb5e7d848e, turn 01a0890a-8401-7710-ad47-106c2123eac6, error at input[37], repeated on four later turns

In both cases the two tool_search_output items share the same turn_id and were produced in the same millisecond, which is what makes them parallel tool_search calls.

What is the expected behavior?

Tool-search results should be merged or deduplicated by namespace name before the conversation history is sent, or at minimum the same namespace should not appear twice in one request. It would also help if a rejected request did not permanently poison the thread, so that the session stays usable after the error.

Additional information

Evidence (abridged; the full sanitized log excerpt is attached as codex-issue-attachment-minimal-repro.txt):

Case A - mcp__rednote, two tool_search_output items inside turn 01a0890d-c8cc-7710-b387-de0e2cd91d67 (both at 2026-09-10T02:02:58.677Z):

{"type":"tool_search_output","call_id":"call_00_...","status":"completed","execution":"client","tools":[{"type":"namespace","name":"mcp__rednote","tools":[{"type":"function","name":"login", ...}]}]}
{"type":"tool_search_output","call_id":"call_01_...","status":"completed","execution":"client","tools":[{"type":"namespace","name":"mcp__rednote","tools":[{"type":"function","name":"get_note_comments", ...},{"type":"function","name":"get_note_content", ...}]}]}

Case B - mcp__playwright: same pattern. Once the duplicate was in the history, four later turns in the same thread failed with the index (input[37]) unchanged.

Correlation on this machine: 3 of 3 turns that called tool_search twice ended with this error; no turn with a single tool_search call failed.

Configuration (redacted):

[mcp_servers.rednote]
command = '<node.exe>'
args = ['/rednote-mcp/dist/cli.js', "--stdio"]

[mcp_servers.rednote.env]
APPDATA = ''
NODE_OPTIONS = '--require=/patch_playwright.cjs'
NODE_PATH = '/node_modules'

[mcp_servers.playwright]
command = '<node.exe>'
args = ['/@playwright/mcp/cli.js']

Workaround I am using: keep tool_search to a single call per turn and request more tools with a larger limit instead of issuing a second search, and disable MCP servers that are not needed for the current task.

Related issues: #31744, #31746, #31748, #31665 (same namespace-duplication family).

One caveat: this install routes requests to a third-party Responses-compatible endpoint through a local proxy, so I cannot tell whether the namespace validation happens in OpenAI's API or in that endpoint. What I am reporting is the client-side behaviour: two identical namespace entries are serialized into one request's input array.

Activity

  1. added
    appIssues related to the Codex desktop app
    on Sep 10, 2026
  2. added
    bugSomething isn't working
    mcpIssues related to the use of model context protocol (MCP) servers
    tool-callsIssues related to tool calling
    custom-modelIssues related to custom model providers (including local models)
    sessionIssues involving session (thread) management, resuming, forking, naming, archiving
    on Sep 10, 2026
  3. cczxuan commented on Sep 10, 2026

    @cczxuan
    Author

    Attaching the sanitized log excerpt with the raw tool_search_output records and the resulting error.

    codex-issue-attachment-minimal-repro.txt

  4. dashoilai5-lab commented on Sep 10, 2026

    @dashoilai5-lab

    Same failure on macOS with the app's own codex_app namespace — a single tool_search call is enough

    Environment: Codex app 26.901.51231 (macOS), bundled codex-cli 0.153.4, third-party provider over the Responses wire API (no OpenAI subscription).

    I believe this is the same root cause, but with a much lower trigger threshold, and with a namespace the user cannot remove:

    • The session declares a dynamic tool namespace named codex_app (the desktop app's own tools: create_thread, list_threads, handoff_thread, ...). It is present in the session's dynamic_tools.
    • One turn called tool_search exactly once. The resulting tool_search_output item carried two namespaces: mcp__code_review_graph (a user MCP server) and codex_app.
    • The very next request was rejected:
    {"error":{"message":"Duplicate namespace name 'codex_app' in input[913].tools[1]. Namespace names must be unique.","type":"invalid_request_error","param":null,"code":"invalid_request_error"}}
    
    • input[913] is the 914th response_item in the session rollout, which is exactly that tool_search_output item. So the client replays rollout response items 1:1 into input, and the collision is between that item's tools[1] and the namespace already declared for the session.
    • The thread is then permanently poisoned: two consecutive turns failed with the identical message at the identical index, and restarting the app did not help.

    So unlike the two-parallel-searches repro above, a single tool_search that returns a namespace already declared at session scope is enough to kill the thread, and it is not specific to user-configured MCP servers — codex_app is supplied by the desktop app itself.

    Suggested fix: when serializing input, drop or merge any {"type":"namespace", ...} entry in a tool_search_output (or additional-tools) item whose name is already declared by the session's dynamic tool namespaces, in addition to deduping across repeated search results.

  5. RuriLothlorien commented on Sep 10, 2026

    @RuriLothlorien

    @etraut-openai — adding a Windows + third-party-provider data point that matches this exactly, with a single tool_search call being enough (same as @dashoilai5-lab's macOS case), plus a workaround that unblocks the poisoned thread without starting a new one.

    Environment

    • Codex Desktop 26.903.71938 (Windows x64), bundled codex-cli 0.153.4
    • Third-party provider over the Responses wire API (DeepSeek, https://api.deepseek.com) — the colliding namespace is the app's own codex_app, so the user cannot remove it

    What happened

    • Session 01a014e4-291b-7e82-aaf1-6560fd7d0395, turn 01a08a9b-6518-7b01-8da5-1136781468e9:
      {"error":{"message":"Duplicate namespace name 'codex_app' in input[525].tools[4]. Namespace names must be unique.","type":"invalid_request_error",...}}
    • The rollout confirms the mechanism:
      • session_meta.dynamic_tools already declares namespace codex_app with 20 tools (create_thread, fork_thread, wait_threads, handoff_thread, …).
      • One tool_search call produced a tool_search_output item (rollout line 4608) that declares the same namespace codex_app again, this time with 2 tools (open_in_codex, list_archived_threads) — a subset of the base namespace.
      • input[525] is that item; tools[4] is the duplicated codex_app entry (index 4 of 6: mcp__dashscope_webparser, mcp__fetch, mcp__codex_dsh_routing_suite, mcp__context7, codex_app, mcp__espressif_engineering).
    • The item stays in history, so every later turn fails at the same index; restarting the app does not help, only a new thread does.

    Scope on this machine: I scanned all 77 rollout files; exactly 1 is poisoned, and the only collision class present is base dynamic_tools namespaces vs tool_search_output namespaces.

    Workaround that actually unblocks the thread (no capability loss)

    • Remove the duplicate namespace entry from the offending tool_search_output item in the rollout JSONL, keeping the tool_search_call / tool_search_output pairing intact (all other lines byte-identical, verified with per-line SHA-256).
    • The 2 tools dropped were already present in the base codex_app namespace, so nothing becomes unreachable, and the thread resumes normally.

    Suggested fix: when assembling the input, drop tool_search_output namespace entries whose name is already declared by the session's base dynamic_tools, and dedupe namespace names across multiple tool_search_output items produced in the same turn (the original report's case).

    Happy to share sanitized excerpts (namespace lists and the offending item's structure — no prompts, paths, or file contents) if that helps.

  6. argszero commented on Sep 11, 2026

    @argszero

    Root cause confirmed on current main (8e6a44b428) — this is client-side, and there are two distinct collision classes with the same origin: namespace entries that codex replays verbatim out of thread history.

    Where the duplicate entries come from

    ToolSearchEntry::from_spec (codex-rs/tools/src/tool_search.rs:32) wraps every matched spec as a LoadableToolSpec::Namespace carrying only the tools that matched. ToolSearchOutput::to_response_item (codex-rs/core/src/tools/context.rs:216) then serializes that vector straight into the tool_search_output item's tools array, and that is what gets persisted. So each tool_search call adds a new input[n].tools[k] namespace entry — one per result set, not one per namespace.

    Why the existing merge logic does not catch it

    Both merge helpers see only a single tool list each, and neither is applied to the per-item arrays:

    • coalesce_loadable_tool_specs (codex-rs/tools/src/responses_api.rs:90) merges namespaces within one search result — that is why a single result carries one namespace rather than one per tool.
    • merge_into_namespaces (codex-rs/core/src/tools/spec_plan.rs:900) merges the top-level request tool list, via build_model_visible_specs (spec_plan.rs:531, call at :562).

    Neither touches the tools arrays of tool_search_output (or additional_tools) items replayed from history, so the second occurrence survives into the request. Note that the index reported by the server is always an input item (input[10], input[913], input[525]) rather than a top-level tools array — i.e. the namespaces are being sent inside input items. That is consistent with the responses-lite request shape (client.rs:795-807, which fronts the tool list as an additional_tools input item; use_responses_lite is enabled for most current model entries in codex-rs/models-manager/models.json). I mention it because it determines whether the session-scope namespace in class B arrives as an additional_tools item or as a top-level tools array, and the dedup pass has to cover whichever shape is in play.

    Both reported classes follow from this:

    • A — repeated search: two tool_search calls in one turn produce two tool_search_output items with the same namespace name but different tool subsets (cczxuan's mcp__rednote: login in one, get_note_comments/get_note_content in the other; confirmed verbatim in the attached rollout excerpt).
    • B — session-scope collision: a single tool_search result re-declares a namespace that already exists in the session's base tools (dashoilai5-lab and RuriLothlorien, both codex_app, where the base declaration comes from session_meta.dynamic_tools with 20 tools and the search result re-declares 2 of them).

    Why the thread stays poisoned

    The offending item remains in history, so every later request re-sends the same duplicate at the same index indefinitely. A new thread recovers because the payload is dropped when the item is rebuilt: compact_remote_history.rs:158 reconstructs ToolSearchOutput with tools: Vec::new().

    Fix locus

    Prompt::get_formatted_input_for_request (codex-rs/core/src/client_common.rs:59) is already a whole-input pass over every item before the request is built, and its match already has arms for ToolSearchOutput/AdditionalTools. Normalizing namespace names there keeps the change at request-assembly time — no rollout format change, no routing change — and covers both classes.

    One caveat worth settling before implementation, because the two classes do not want the same operation:

    • Class A must union, not drop. Same-named namespaces from different search results carry disjoint tool subsets, so dropping the later entry would silently remove tools the model just searched for.
    • Class B is a subset re-declaration, so merging into the base entry would be semantically fine — but that entry lives in the responses-lite additional_tools item whose id is a v5 hash of its serialized payload (client.rs:796-807, "Hash their visible payloads within the thread so retries and resumed sessions preserve their identity"). Appending tools to it after construction would leave the id inconsistent with its content, so dropping the duplicate on the tool_search_output side avoids touching that item.

    I can implement this: a dedup-by-name pass in get_formatted_input_for_request that unions tools within tool_search_output items and drops entries whose namespace is already declared by additional_tools (first occurrence wins), plus a regression test that builds an input containing two same-namespace search results and asserts one namespace per name. Happy to file it as a PR — just confirm which treatment you want on the Class B side (drop the duplicate vs. merge into the base entry if the id/hash concern is not a real constraint).

  7. liblibKylin commented on Sep 11, 2026

    @liblibKylin

    Confirming this from a first-party install (ChatGPT.app on macOS, same codex-cli 0.153.4 core), which answers the open question in your "caveat": the duplication is client-side and does not require a third-party proxy to surface — the official ChatGPT backend also rejects it with the identical message.

    Evidence from an affected thread (01a08fab-..., 2026-09-11):

    {"error":{"message":"Duplicate namespace name 'mcp__libtv_pre' in input[117].tools[0]. Namespace names must be unique.","type":"invalid_request_error","param":null,"code":"invalid_request_error"}}

    Reproduced 7 times across 4 threads in this install, and it matches your mechanism exactly:

    1. Turn 1: one tool_search call (query "libtv doctor get_capabilities list_projects") -> tool_search_output carrying {"type":"namespace","name":"mcp__libtv_pre","tools":[...defer_loading:true]}
    2. Turn 3: the model issues two tool_search calls in the same turn (different queries, both hitting the same MCP server) -> two tool_search_output items, both declaring mcp__libtv_pre
    3. Every subsequent request in that thread fails at the same input[N] index

    Two additional data points that may help whoever fixes this:

    • The trigger is a model-behaviour interaction, not a server property. In one thread of the same install, 11 tool_search calls spread one-per-turn over many turns all succeeded (no errors), against the same MCP server. Searches issued twice within a single turn are what put two identical namespace entries into one request. From the rollouts: one model spread its searches across turns with real tool calls interleaved and never errored, while another issued several searches back-to-back in one turn and failed on the first such turn.
    • The thread-poisoning effect is confirmed on the first-party path too, as you described for rednote/playwright: the duplicate stays in history, restarting the app does not help, only a new thread recovers.

    Workaround that matches your report (and worked in our successful thread): keep tool_search to a single call per turn and request a larger limit.

    What this does not tell you: whether the fix belongs in request construction (dedupe namespaces before serializing) or in the deferred-tool exposure path ("direct and deferred exposure are mutually exclusive" per the binary's own invariant). Both of your input[N].tools[0] observations and ours point at the former.

  8. liblibKylin commented on Sep 11, 2026

    @liblibKylin

    Related (separate defect, same client-side family): filed #44779 for tool_search calls failing with missing field call_id`` on strict Responses providers — that one blocks tool_search entirely on such providers, so this namespace-duplication bug cannot even be reached there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    appIssues related to the Codex desktop appbugSomething isn't workingcustom-modelIssues related to custom model providers (including local models)mcpIssues related to the use of model context protocol (MCP) serverssessionIssues involving session (thread) management, resuming, forking, naming, archivingtool-callsIssues related to tool calling

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions