Area
Proxy and routing
What happened
Switching a live Codex Desktop thread from a native ChatGPT model (gpt-6-astra) to a routed provider model (anthropic/claude-fable-5-1) breaks the thread permanently with:
Routed V2 worker task is encrypted for the native ChatGPT backend and cannot be read by
the selected provider. Use plaintext V2 agent-message delivery or select a native ChatGPT model.
A thread that is started on the routed model never hits this. Only threads whose history already contains a backend-minted encrypted_content agent message do, and once present, every later turn replays it, so the thread can never be continued on the routed model.
The important part: agentTaskRecovery is enabled and still never runs on this path. Recovery is gated on isThreadSpawnRequest(req.headers), and a mid-thread model switch is not a thread spawn, so the request fails closed without any recovery attempt.
Reproduction (no auth, loopback only)
Two identical requests to a running proxy, differing only by the spawn header. The body is one agent_message carrying a routing header plus a structurally valid Fernet-shaped encrypted_content slot, targeting a routed provider.
# A) no spawn header -> mid-thread model switch shape
curl -sS -X POST http://127.0.0.1:10100/v1/responses \
-H 'Content-Type: application/json' -H 'originator: codex_cli_rs' \
--data @probe.json
# B) same body + spawn header
curl -sS -X POST http://127.0.0.1:10100/v1/responses \
-H 'Content-Type: application/json' -H 'originator: codex_cli_rs' \
-H 'x-openai-subagent: collab_spawn' \
--data @probe.json
Observed:
// A) no recovery_reason field at all -> recovery was never attempted
{"error":{"message":"Routed V2 worker task is encrypted ...","type":"invalid_request_error",
"code":"unreadable_encrypted_agent_task"}}
// B) recovery_reason present -> recovery WAS attempted
{"error":{"message":"Routed V2 worker task is encrypted ...","type":"invalid_request_error",
"code":"unreadable_encrypted_agent_task","recovery_reason":"unsupported_envelope"}}
The presence or absence of recovery_reason is the discriminator. unsupported_envelope in (B) only reflects the synthetic envelope used for the probe; the point is that (B) entered the recovery path and (A) did not.
Code pointers
src/server/responses/core.ts — the direct (non-combo) recovery block is gated on threadSpawn:
inboundWire === "responses" && threadSpawn && agentTaskRecovery && !isCanonicalOpenAiForwardProvider(route.provider) && ...
src/server/effort-policy.ts — isThreadSpawnRequest() is true only for x-openai-subagent: collab_spawn or turn metadata subagent_kind === "thread_spawn".
src/server/responses/core.ts — canPassThroughEncryptedV2AgentTask() requires authMode === "key" plus allowEncryptedV2AgentTasks and an openai-responses wire, so an OAuth-mode routed provider has no passthrough option either.
Expected
A thread that can be continued should be continuable. Either the recovery path should be reachable for non-spawn requests that carry an unreadable encrypted agent task, or the client should be told the switch is impossible before the thread is bricked.
Suggested direction
- Let
agentTaskRecovery run for any request whose input contains an unreadable encrypted agent envelope, not only thread-spawn requests. The admission checks in recoveryAdmission() (native ChatGPT bearer, matching chatgpt-account-id, Codex originator, no inbound API key) already bound the trust surface; threadSpawn looks like an additional narrowing rather than the security boundary.
- Alternatively, expose the failure earlier: when a thread's history contains backend-minted ciphertext, reject or warn on the model switch itself instead of failing every subsequent turn.
restoreCachedEncryptedAgentTasks() is similarly gated today, so a proxy restart also loses any chance of reusing a previously recovered plaintext for that thread.
Environment
@bitkyc08/opencodex 2.48.0
- macOS, Codex Desktop, routed provider is an OAuth-mode Anthropic-adapter provider on loopback
agentTaskRecovery = {"enabled": true, "model": "gpt-5.6-sol", "timeoutMs": 45000, "cacheEntries": 200}, verified live via the startup warning
Related
This report is about the non-spawn, same-thread model-switch path, which none of the above cover.
Workaround
Do not switch a native-model thread to a routed provider. Start a new thread on the routed model instead.
Area
Proxy and routing
What happened
Switching a live Codex Desktop thread from a native ChatGPT model (
gpt-6-astra) to a routed provider model (anthropic/claude-fable-5-1) breaks the thread permanently with:A thread that is started on the routed model never hits this. Only threads whose history already contains a backend-minted
encrypted_contentagent message do, and once present, every later turn replays it, so the thread can never be continued on the routed model.The important part:
agentTaskRecoveryis enabled and still never runs on this path. Recovery is gated onisThreadSpawnRequest(req.headers), and a mid-thread model switch is not a thread spawn, so the request fails closed without any recovery attempt.Reproduction (no auth, loopback only)
Two identical requests to a running proxy, differing only by the spawn header. The body is one
agent_messagecarrying a routing header plus a structurally valid Fernet-shapedencrypted_contentslot, targeting a routed provider.Observed:
The presence or absence of
recovery_reasonis the discriminator.unsupported_envelopein (B) only reflects the synthetic envelope used for the probe; the point is that (B) entered the recovery path and (A) did not.Code pointers
src/server/responses/core.ts— the direct (non-combo) recovery block is gated onthreadSpawn:inboundWire === "responses" && threadSpawn && agentTaskRecovery && !isCanonicalOpenAiForwardProvider(route.provider) && ...src/server/effort-policy.ts—isThreadSpawnRequest()is true only forx-openai-subagent: collab_spawnor turn metadatasubagent_kind === "thread_spawn".src/server/responses/core.ts—canPassThroughEncryptedV2AgentTask()requiresauthMode === "key"plusallowEncryptedV2AgentTasksand anopenai-responseswire, so an OAuth-mode routed provider has no passthrough option either.Expected
A thread that can be continued should be continuable. Either the recovery path should be reachable for non-spawn requests that carry an unreadable encrypted agent task, or the client should be told the switch is impossible before the thread is bricked.
Suggested direction
agentTaskRecoveryrun for any request whose input contains an unreadable encrypted agent envelope, not only thread-spawn requests. The admission checks inrecoveryAdmission()(native ChatGPT bearer, matchingchatgpt-account-id, Codex originator, no inbound API key) already bound the trust surface;threadSpawnlooks like an additional narrowing rather than the security boundary.restoreCachedEncryptedAgentTasks()is similarly gated today, so a proxy restart also loses any chance of reusing a previously recovered plaintext for that thread.Environment
@bitkyc08/opencodex2.48.0agentTaskRecovery = {"enabled": true, "model": "gpt-5.6-sol", "timeoutMs": 45000, "cacheEntries": 200}, verified live via the startup warningRelated
This report is about the non-spawn, same-thread model-switch path, which none of the above cover.
Workaround
Do not switch a native-model thread to a routed provider. Start a new thread on the routed model instead.