Skip to content

fix: forward inter-agent messages as user text so DeepSeek stops rejecting them - #24

Merged
fish2lab merged 3 commits into
fish2lab:mainfrom
Yilmhi:fix/agent-message-thinking-mode
Sep 20, 2026
Merged

fish2lab merged 3 commits into
fish2lab:mainfrom
Yilmhi:fix/agent-message-thinking-mode

Conversation

@Yilmhi

@Yilmhi Yilmhi commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

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_message item whose payload block is typed encrypted_content. That block is plain text in practice, but DeepSeek's content enum accepts only input_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 carries tools — 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:

input shape before after
developer + user + agent_message + tools 400 200
agent_message + tools 400 200
same text sent as a user message + tools 200 200
a genuine assistant message + tools 400 400 (correct)

Bug (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 assistant item too.

Fix

  • Rewrite encrypted_content content blocks as input_text so the payload text still reaches the model. A payload that cannot be read as text — a sealed artifact such as a dscodex-compaction-v1: blob that fails to open — is dropped instead of being handed to the model as ciphertext.
  • Replay agent_message items with role user. The message is a task handed to this agent, not a turn the model produced, so user carries the same text without claiming prior reasoning.
  • Stop forwarding the Codex-internal internal_chat_message_metadata_passthrough field to DeepSeek.

buildChatGptBody is untouched, so native ChatGPT/Codex traffic still passes agent_message items 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 beginning gAAAAA, 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:

shape DeepSeek
custom_tool_call / custom_tool_call_output pair 200
tool_search_call / tool_search_output pair 200
web_search_call 200
tools containing a namespace group 200
message with input_image 200
message with an unknown content block (refusal, fabricated) 422

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. convertInputItem now 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 as input_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 through createProxyServer (red before each change, green after).
  • Live router probe matrices: every agent_message shape and every shape above now returns 200, while a genuine assistant message without reasoning still returns 400 — that is DeepSeek's real rule and is deliberately left alone.
  • End to end: a spawned child agent received its task and answered.
  • Also exercised end to end: a sub-agent that calls tools, and a sub-agent that spawns its own sub-agent.

The pre-existing assertion in test/proxy.test.mjs that pinned the replayed role to assistant was updated to match the corrected role.

…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
fish2lab merged commit a4346e0 into fish2lab:main Sep 20, 2026
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
Sub-agents on DeepSeek (#24), DeepSeek → GPT switch fixed again (#23),
instant 426 fallback for DeepSeek WebSocket handshakes (#25), voice
documented as client-owned since desktop 26.908 (#21 closed).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Yilmhi
Yilmhi deleted the fix/agent-message-thinking-mode branch September 20, 2026 09:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants