Repository navigation
Description
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?
- 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.
- 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.
- 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
cczxuan commentedon Sep 10, 2026
Attaching the sanitized log excerpt with the raw tool_search_output records and the resulting error.
codex-issue-attachment-minimal-repro.txt
dashoilai5-lab commentedon Sep 10, 2026
Same failure on macOS with the app's own
codex_appnamespace — a singletool_searchcall is enoughEnvironment: 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:
codex_app(the desktop app's own tools:create_thread,list_threads,handoff_thread, ...). It is present in the session'sdynamic_tools.tool_searchexactly once. The resultingtool_search_outputitem carried two namespaces:mcp__code_review_graph(a user MCP server) andcodex_app.input[913]is the 914thresponse_itemin the session rollout, which is exactly thattool_search_outputitem. So the client replays rollout response items 1:1 intoinput, and the collision is between that item'stools[1]and the namespace already declared for the session.So unlike the two-parallel-searches repro above, a single
tool_searchthat 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_appis supplied by the desktop app itself.Suggested fix: when serializing
input, drop or merge any{"type":"namespace", ...}entry in atool_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.RuriLothlorien commentedon Sep 10, 2026
@etraut-openai — adding a Windows + third-party-provider data point that matches this exactly, with a single
tool_searchcall being enough (same as @dashoilai5-lab's macOS case), plus a workaround that unblocks the poisoned thread without starting a new one.Environment
26.903.71938(Windows x64), bundledcodex-cli 0.153.4https://api.deepseek.com) — the colliding namespace is the app's owncodex_app, so the user cannot remove itWhat happened
01a014e4-291b-7e82-aaf1-6560fd7d0395, turn01a08a9b-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",...}}session_meta.dynamic_toolsalready declares namespacecodex_appwith 20 tools (create_thread,fork_thread,wait_threads,handoff_thread, …).tool_searchcall produced atool_search_outputitem (rollout line 4608) that declares the same namespacecodex_appagain, 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 duplicatedcodex_appentry (index 4 of 6:mcp__dashscope_webparser,mcp__fetch,mcp__codex_dsh_routing_suite,mcp__context7,codex_app,mcp__espressif_engineering).Scope on this machine: I scanned all 77 rollout files; exactly 1 is poisoned, and the only collision class present is base
dynamic_toolsnamespaces vstool_search_outputnamespaces.Workaround that actually unblocks the thread (no capability loss)
tool_search_outputitem in the rollout JSONL, keeping thetool_search_call/tool_search_outputpairing intact (all other lines byte-identical, verified with per-line SHA-256).codex_appnamespace, so nothing becomes unreachable, and the thread resumes normally.Suggested fix: when assembling the input, drop
tool_search_outputnamespace entries whose name is already declared by the session's basedynamic_tools, and dedupe namespace names across multipletool_search_outputitems 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.
argszero commentedon Sep 11, 2026
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 aLoadableToolSpec::Namespacecarrying only the tools that matched.ToolSearchOutput::to_response_item(codex-rs/core/src/tools/context.rs:216) then serializes that vector straight into thetool_search_outputitem'stoolsarray, and that is what gets persisted. So eachtool_searchcall adds a newinput[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, viabuild_model_visible_specs(spec_plan.rs:531, call at:562).Neither touches the
toolsarrays oftool_search_output(oradditional_tools) items replayed from history, so the second occurrence survives into the request. Note that the index reported by the server is always aninputitem (input[10],input[913],input[525]) rather than a top-leveltoolsarray — i.e. the namespaces are being sent insideinputitems. That is consistent with the responses-lite request shape (client.rs:795-807, which fronts the tool list as anadditional_toolsinputitem;use_responses_liteis enabled for most current model entries incodex-rs/models-manager/models.json). I mention it because it determines whether the session-scope namespace in class B arrives as anadditional_toolsitem or as a top-leveltoolsarray, and the dedup pass has to cover whichever shape is in play.Both reported classes follow from this:
tool_searchcalls in one turn produce twotool_search_outputitems with the same namespace name but different tool subsets (cczxuan'smcp__rednote:loginin one,get_note_comments/get_note_contentin the other; confirmed verbatim in the attached rollout excerpt).tool_searchresult re-declares a namespace that already exists in the session's base tools (dashoilai5-labandRuriLothlorien, bothcodex_app, where the base declaration comes fromsession_meta.dynamic_toolswith 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:158reconstructsToolSearchOutputwithtools: 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 forToolSearchOutput/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:
additional_toolsitem whoseidis 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 thetool_search_outputside avoids touching that item.I can implement this: a dedup-by-name pass in
get_formatted_input_for_requestthat unions tools withintool_search_outputitems and drops entries whose namespace is already declared byadditional_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).liblibKylin commentedon Sep 11, 2026
Confirming this from a first-party install (ChatGPT.app on macOS, same
codex-cli 0.153.4core), 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:
tool_searchcall (query "libtv doctor get_capabilities list_projects") ->tool_search_outputcarrying{"type":"namespace","name":"mcp__libtv_pre","tools":[...defer_loading:true]}tool_searchcalls in the same turn (different queries, both hitting the same MCP server) -> twotool_search_outputitems, both declaringmcp__libtv_preinput[N]indexTwo additional data points that may help whoever fixes this:
tool_searchcalls 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.Workaround that matches your report (and worked in our successful thread): keep
tool_searchto a single call per turn and request a largerlimit.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.liblibKylin commentedon Sep 11, 2026
Related (separate defect, same client-side family): filed #44779 for
tool_searchcalls failing withmissing fieldcall_id`` on strict Responses providers — that one blockstool_searchentirely on such providers, so this namespace-duplication bug cannot even be reached there.