fix: forward inter-agent messages as user text so DeepSeek stops rejecting them - #24
Merged
Merged
Conversation
…cting them Codex delivers a task to a child agent as an `agent_message` item whose payload block is typed `encrypted_content`. Two things broke every spawn: * DeepSeek's content enum accepts only input_text/output_text/input_image/input_file, so the raw block failed the request with a 422 before the child ever started. The block is now rewritten as text, and a payload that cannot be read as text (a sealed artifact) is dropped rather than handed to the model as ciphertext. * The item was replayed with role `assistant`, which DeepSeek reads as the model's own prior thinking turn: as soon as the request carries tools it answers "The `reasoning_text` in the thinking mode must be passed back to the API." and the request fails. For a child agent that message is the last item of its first request, so every spawn died; the same 400 also killed the parent's next turn after an inter-agent reply. `user` carries the same text without claiming prior reasoning. Also stop forwarding Codex-internal `internal_chat_message_metadata_passthrough` to DeepSeek.
… types one by one DeepSeek deserializes the content of an input message into a closed enum — input_text, output_text, input_image, input_file — and answers 422 `unknown variant ...` for anything else, taking the whole turn offline. The inter-agent message bug was one instance of that; the next block type we have not been taught yet would be the next outage. Probing every shape Codex actually recorded in its own session rollouts (41 files) shows the item layer is tolerant: custom_tool_call, tool_search_call, web_search_call, tool_search outputs carrying `namespace` tool groups, and input_image all pass through untouched. Content blocks are the only fragile layer, and a fabricated unknown block (`refusal`) still 422s, which confirms where the guarantee belongs. Every content block is now normalised on the way out: known types pass through byte for byte, a block carrying text (encrypted_content, refusal, whatever comes next) is forwarded as input_text, and a block with no readable text is dropped rather than handed to the model as ciphertext.
An encrypted payload only verifies for the provider that issued it. When the root model is served by this router and a GPT model runs a sub-agent, every spawn is a cross-provider model switch, so the child's request replays encrypted fields the ChatGPT endpoint cannot verify and the turn dies. DeepSeek now returns a non-null `encrypted_content`, so the previous "is the field empty?" test never recognised those items as foreign and forwarded them anyway. ChatGPT's own ciphertext is base64 beginning `gAAAAA`, so that is the only encrypted payload let through now; DSCodex-sealed compactions are still unwrapped, and anything else is dropped rather than forwarded for the upstream to reject. Applies to both the HTTP and websocket transports. Verified against the live endpoint: a request carrying a DeepSeek-issued reasoning token in history went 400 -> 200. This does not by itself revive GPT-model sub-agents in that setup — see openai/codex#33267 — where the remaining failure is client side.
fish2lab
pushed a commit
that referenced
this pull request
Sep 20, 2026
… encrypted_content Native GPT reasoning never carries a content array (#23 measured 0 of 7539 official items with one), so a reasoning_text part is foreign on its own and the router no longer keys the decision on what encrypted_content holds. The gAAAAA ciphertext rule from #24 still covers content-less foreign items. Document the #23 / #24 rules in AGENTS.md and both READMEs. Closes #23 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
fish2lab
pushed a commit
that referenced
this pull request
Sep 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Spawning a sub-agent on DeepSeek fails every time (
spawn_agent/ the collaboration tools). Two independent bugs sit in the same code path.1. 422 —
unknown variant 'encrypted_content'Codex delivers a task to another agent as an
agent_messageitem whose payload block is typedencrypted_content. That block is plain text in practice, but DeepSeek's content enum accepts onlyinput_text/output_text/input_image/input_file, so the whole request is rejected before the child agent starts.2. 400 —
The reasoning_text in the thinking mode must be passed back to the API.With (1) fixed the same request still failed: the inter-agent message was replayed with role
assistant, and DeepSeek reads that as the model's own prior thinking turn. Once the request carriestools— which it always does — DeepSeek demands the reasoning_text for that turn. For a child agent this message is the last item of its first request, so every spawn died.Minimal reproductions against the router with real DeepSeek:
developer + user + agent_message + toolsagent_message + toolsusermessage +toolsassistantmessage +toolsBug (2) is not limited to sub-agents: the same 400 also kills the parent's next turn after it receives an inter-agent reply, because that reply is replayed as an
assistantitem too.Fix
encrypted_contentcontent blocks asinput_textso the payload text still reaches the model. A payload that cannot be read as text — a sealed artifact such as adscodex-compaction-v1:blob that fails to open — is dropped instead of being handed to the model as ciphertext.agent_messageitems with roleuser. The message is a task handed to this agent, not a turn the model produced, sousercarries the same text without claiming prior reasoning.internal_chat_message_metadata_passthroughfield to DeepSeek.buildChatGptBodyis untouched, so native ChatGPT/Codex traffic still passesagent_messageitems through unchanged.The same class of bug on the ChatGPT side (third commit)
An encrypted payload only verifies for the provider that issued it, and DeepSeek now returns a non-null
encrypted_content, so the existing "is the field empty?" test no longer recognises foreign items on the GPT path — they were forwarded verbatim and the upstream rejected the turn. ChatGPT-issued ciphertext is base64 beginninggAAAAA, so that is now the only encrypted payload allowed through; DSCodex-sealed compactions are still unwrapped and anything else is dropped, on both the HTTP and websocket transports.Verified against the live endpoint: a request carrying a DeepSeek-issued reasoning token in history went 400 → 200. That is the same root as issue #23.
This does not by itself revive GPT-model sub-agents, which still fail client-side with
Encrypted function output content could not be decrypted or decoded— reported with evidence upstream as openai/codex#33267.Turning the instance into a guarantee
Fixing the inter-agent message by name would only hold until the next block type shows up, so the second commit closes the class instead. Every shape Codex actually recorded in its own session rollouts was enumerated (41 files) and then sent to the router:
custom_tool_call/custom_tool_call_outputpairtool_search_call/tool_search_outputpairweb_search_calltoolscontaining anamespacegroupinput_imagerefusal, fabricated)So the item layer is tolerant and content blocks are the only fragile layer — which is exactly where the inter-agent message bug came from.
convertInputItemnow guarantees the enum on the way out: accepted types pass through byte for byte, any other block that carries text (encrypted_content,refusal, whatever comes next) is forwarded asinput_text, and a block with no readable text is dropped instead of being handed to the model as ciphertext.Verification
node --test test/proxy.test.mjs— 35/35 pass. Two new tests reproduce the exact failing shapes end to end throughcreateProxyServer(red before each change, green after).agent_messageshape and every shape above now returns 200, while a genuineassistantmessage without reasoning still returns 400 — that is DeepSeek's real rule and is deliberately left alone.The pre-existing assertion in
test/proxy.test.mjsthat pinned the replayed role toassistantwas updated to match the corrected role.