Stale encrypted compaction/reasoning breaks entire thread after key rotation (invalid_encrypted_content + array_above_max_length)
Stale encrypted compaction/reasoning breaks entire thread after key rotation (invalid_encrypted_content + array_above_max_length)
Summary
A thread that contains content written under an older Codex build becomes permanently unusable after a key rotation / tool-call format migration. Every turn fails at the Responses API with a 400, and the thread cannot recover without manual surgery on the rollout file. This report documents the full troubleshooting path: two distinct errors surfaced in sequence, each masking the next, with three layers of corrupted content ultimately identified and removed.
This affects the Codex bundled inside the ChatGPT desktop app on macOS. Related issues: #25290 (encrypted reasoning), #13724 (old threads broken after update), #35334 (cmp_ compaction error). This report consolidates the full failure chain, adds a second error (array_above_max_length) not previously reported, and includes the step-by-step diagnosis so maintainers can reproduce the investigation.
Initial bug report (as first observed)
When resuming an older Codex thread, every turn fails immediately with:
The encrypted content for item cmp_3c7b644077da41f498a7059c1316e20b could not be verified. Reason: Encrypted content could not be decrypted or parsed.
The thread becomes completely unusable — no turn can run. The error mentions a cmp_ item id which looks like a connector/credential id at first glance, but is actually a compaction item. After manually removing the corrupted compaction, a second error surfaced:
[ArrayParam] [input[5].content] [array_above_max_length] Invalid 'input[5].content': array too long. Expected an array with maximum length 0, but got an array with length 1 instead.
Both errors are HTTP 400 from the Responses API, returned before the model runs.
Environment
- Codex: bundled in ChatGPT.app, framework version
150.0.7871.182 - Platform: macOS (Apple Silicon)
- Shell: fish 4.6.0
- Model:
gpt-5.6-luna(also reproduced withgpt-5.6-sol) - Thread id:
019f8e04-cc6c-7a33-92f0-128aa3d05d20 - Thread cwd:
/Users/hungpham/research/clientsecret-noti - Thread created: 2026-07-23
- First failure: 2026-07-24 ~06:30 UTC (coincides with an app/Codex update)
- Last failure before fix: 2026-08-03 08:00 UTC
history_mode:legacy(fromstate_5.sqlitetablethreads)- Local relay:
127.0.0.1:10100(POST /v1/responses)
Errors observed (in order of appearance)
Error 1 — invalid_encrypted_content (compaction) — 2026-07-24 onward
{
"type": "error",
"error": {
"type": "invalid_request_error",
"code": "invalid_encrypted_content",
"message": "The encrypted content for item cmp_3c7b644077da41f498a7059c1316e20b could not be verified. Reason: Encrypted content could not be decrypted or parsed."
},
"status": 400
}
Error 2 — array_above_max_length (function_call serialization) — 2026-08-03, surfaced after error 1 was cleared
{
"type": "error",
"error": {
"type": "invalid_request_error",
"message": "[ArrayParam] [input[5].content] [array_above_max_length] Invalid 'input[5].content': array too long. Expected an array with maximum length 0, but got an array with length 1 instead."
},
"status": 400
}
Reproduction
I could not reproduce on a fresh thread (fresh threads work normally). To reproduce the failure you need a thread that:
- was created under an older Codex version (so it has
function_callitems and/orocx1/Fernet-encrypted content), and - is resumed after an update that rotates the encryption key and/or migrates the tool-call format.
The failure is deterministic: every turn on such a thread returns 400 before the model runs. Concretely, on the affected thread:
- Open the thread in Codex.
- Send any user message.
- Observe the turn fails immediately with
invalid_encrypted_content(if theocx1:compaction is still present) orarray_above_max_length(if the compaction has been removed but old-formatfunction_callitems remain).
To reproduce the investigation from scratch on a known-broken thread, the queries and commands in the Troubleshooting progress section below can be re-run against ~/.codex/logs_2.sqlite and the thread's rollout file.
Troubleshooting progress
Step 1 — Identified the error source
Searched ~/.codex/logs_2.sqlite (table logs, column feedback_log_body) for the error text:
SELECT datetime(ts,'unixepoch'), substr(feedback_log_body,1,1200)
FROM logs
WHERE feedback_log_body LIKE '%cmp_3c7b644077da41f498a7059c1316e20b%'
ORDER BY ts DESC LIMIT 3;
Found the error was a Codex turn error (invalid_encrypted_content) returned from the local relay at 127.0.0.1:10100 (HTTP 400), recurring on the same thread 019f8e04-cc6c-7a33-92f0-128aa3d05d20 since 2026-07-24. The cmp_ prefix was initially mistaken for a Composio connector ID; investigation ruled that out (Composio is not referenced anywhere in ~/.codex config, and op/composio CLIs are not installed).
Step 2 — Located the corrupted item in the rollout
Searched all files under ~/.codex for the cmp_ id. Found it only in the thread's rollout file:
~/.codex/sessions/2026/07/23/rollout-2026-07-23T15-08-27-019f8e04-cc6c-7a33-92f0-128aa3d05d20.jsonl
Inspected the matching line (line 48) and found a "type": "compacted" event whose payload.replacement_history contained a compaction item with encrypted_content prefixed ocx1: (opencodex v1 format). Decoding the base64 payload confirmed it was a summary of the thread's earliest messages.
Step 3 — Confirmed the error was isolated to one thread
SELECT DISTINCT substr(feedback_log_body, instr(feedback_log_body,'thread.id=')+10, 36)
FROM logs
WHERE feedback_log_body LIKE '%invalid_encrypted_content%'
AND feedback_log_body LIKE '%cmp_3c7b644077%';
-- returned exactly one thread id: 019f8e04-cc6c-7a33-92f0-128aa3d05d20
Step 4 — Surgery 1: removed the corrupted compaction event
Backed up the rollout file, then removed the single compacted event (line 48) that owned the ocx1:-encrypted blob. Validated the result: 355 lines, 0 parse errors. Confirmed no external index/state referenced the compaction id or its window ids (the change was self-contained).
Step 5 — Second error surfaced
After resuming the thread, a new error appeared: array_above_max_length at input[5].content. The compaction error had been masking this second error (the relay rejected the compaction blob before the API ever validated the input array).
Step 6 — Mapped the input items
Enumerated response_item entries from the rollout in order to find input[5]. Found input[5] mapped to a function_call item — but it had no content field in storage. This meant Codex was adding a content array during serialization, or the input indexing differed from file order.
Step 7 — Checked for other encrypted blobs
Scanned the rollout for all encrypted_content fields by prefix:
ocx1:(compaction format): 0 remaining (surgery 1 worked)gAAAA...(Fernet): 33 items — allreasoningitems withencrypted_contentset and nocontentfield- other: 0
The 4 earliest reasoning items (lines 9, 19, 29, 37) had encrypted_content: null explicitly present alongside content: [reasoning_text] (length 1) — also a schema problem, since the Responses API enforces content: [] (maxItems=0) when the encrypted_content field is present, even if null.
Step 8 — Surgery 2: removed the 33 encrypted reasoning items
Removed all reasoning items with non-null encrypted_content. Result: 348 lines, 0 parse errors, 0 Fernet blobs remaining. The thread still failed with the same array_above_max_length error.
Step 9 — Surgery 3: removed the 4 remaining unencrypted reasoning items
Removed the 4 reasoning items with encrypted_content: null (they carried content: [reasoning_text] which the schema rejects when encrypted_content is present even as null). Result: 348 lines, 0 parse errors, 0 reasoning items. The thread still failed with the same error.
Step 10 — Compared with a working thread
Inspected a recent working thread's rollout (rollout-2026-08-03T15-01-55-...jsonl). Key finding: the working thread used only custom_tool_call / custom_tool_call_output items (new format, with input string and output array). The broken thread had 12 old-format function_call + 12 function_call_output items (with name, arguments JSON string, call_id) from an older Codex version.
Step 11 — Surgery 4: removed the 24 old-format function_call items
Removed all function_call and function_call_output items. Result: 324 lines, 0 parse errors. Item types now: message (68) + custom_tool_call (23) + custom_tool_call_output (23) — matching the working thread format. All custom_tool_call/output pairs matched (0 orphaned).
Step 12 — Verified recovery
Re-mapped the input after all surgeries: input[5] is now a standard message (role=assistant, content=[output_text]) — same format as the working thread. The thread resumed successfully.
Root cause analysis
The rollout file contained three classes of content that became invalid after the update:
1. Compaction summary (ocx1: format) — 1 item
The compacted event holds an encrypted_content blob prefixed ocx1: (opencodex v1). After the key rotation, this blob can no longer be decrypted by the local relay. Because the compaction replaced the thread's earliest raw messages, those early messages are unrecoverable. The compaction is injected into every subsequent turn's input, so every turn fails with invalid_encrypted_content.
2. Encrypted reasoning items (gAAAA... Fernet format) — 33 items
response_item entries of type: "reasoning" with encrypted_content set (Fernet token) and no content field. Same key-rotation problem: undecryptable. When serialized they appear to be sent with a content array the API rejects. (Same class of issue as #25290.)
3. Old-format function_call / function_call_output items — 24 items (12 + 12)
The affected thread was created under an older Codex that produced function_call items with name, arguments (JSON string), call_id. A current working thread uses custom_tool_call / custom_tool_call_output items with input (string) and output (array). After the migration, the old function_call items get serialized with a content field the Responses API schema rejects (content has maxItems: 0 for function_call), producing the array_above_max_length error at input[5].content.
Why it only affects one thread
Compaction and encrypted reasoning are per-thread. Newer threads have no compaction, or post-rotation compactions/reasoning that decrypt fine. Only threads that existed before the key rotation and contain pre-rotation encrypted content are affected. The old-format function_call items only exist on threads created before the tool-call format migration.
Expected behavior
- An update that rotates the encryption key should not leave existing threads permanently broken. Options:
- On key rotation, re-summarize (or drop) old compactions instead of shipping an undecryptable blob.
- Detect undecryptable
encrypted_contentat load time and drop/replace the item (with a placeholder or a fresh re-summarization) rather than failing the whole turn. - Optionally, attempt one-time decryption with the old key and re-encrypt under the new key on first load after rotation.
- A format migration for tool calls should transparently migrate old
function_call/function_call_outputitems to the newcustom_tool_call/custom_tool_call_outputshape (or drop them) instead of serializing them in a way the API rejects.
Actual behavior
The entire thread becomes unusable. Every turn returns HTTP 400. There is no in-app recovery path. The user is forced to either abandon the thread or manually edit the rollout JSONL on disk (destructive, loses the compaction summary and the affected reasoning/tool items).
Workaround applied (for reference)
Manual surgery on ~/.codex/sessions/.../<rollout>.jsonl, after quitting the app, with a backup:
- Removed the
compactedevent whosereplacement_historycontained theocx1:-encryptedcmp_...blob. - Removed all 33
reasoningitems with non-nullencrypted_content. - Removed the 4 remaining
reasoningitems withencrypted_content: null(they also carriedcontent: [reasoning_text], which the schema rejects whenencrypted_contentis present even as null). - Removed all 24 old-format
function_call/function_call_outputitems.
After this, the thread uses only message + custom_tool_call / custom_tool_call_output (matching working threads) and turns succeed again. The cost: the early compaction summary, all reasoning summaries, and the early tool-call I/O are lost (assistant messages about those tool calls remain).
Final verified state
324 lines, 0 parse errors
0 encrypted blobs (ocx1=0, fernet=0)
0 old-format function_call items
0 reasoning items
23 custom_tool_call + 23 custom_tool_call_output (all paired, 0 orphaned)
68 messages
INTEGRITY: OK — matches working thread format
Diagnostic data
- Rollout path:
~/.codex/sessions/<date>/rollout-<ts>-<thread-id>.jsonl - Encrypted content formats found pre-fix:
ocx1:(1 compaction) andgAAAA...(33 reasoning items) - Old-format tool items found pre-fix:
function_call(12) +function_call_output(12) - Errors were logged to
~/.codex/logs_2.sqlite(tablelogs, columnfeedback_log_body) - Thread row in
~/.codex/state_5.sqlitetablethreadshadhistory_mode = 'legacy' - Local relay:
127.0.0.1:10100(POST /v1/responses)
Happy to provide redacted excerpts from the rollout or logs if useful.
Severity
High for affected users: any thread that crosses a key-rotation or format-migration boundary is silently bricked with no recovery path short of manual file surgery.
Suggested fix (upstream)
- On startup after an update, scan each thread's rollout for
encrypted_contentthat fails to decrypt under the current key and replace those items with a benign placeholder (or trigger a re-summarization) instead of including them in the request. - Migrate legacy
function_call/function_call_outputitems to the currentcustom_tool_call/custom_tool_call_outputrepresentation (or drop them) at load time, so they never reach the API in the old shape. - Improve the error surfaced to the user:
invalid_encrypted_contentandarray_above_max_lengthwith no guidance are hard to act on. A "this thread was created by an older version and contains content that can no longer be decoded — start a new thread or click here to repair" prompt would help. - Detect at load time when a thread's
history_modeor item formats are stale relative to the current Codex build and offer an automatic repair/migration path before the user hits a 400.
3 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
I’m testing Codex Rescue
0.1.0a3against real persisted-history corruption cases. This one is useful because the rollout remains parseable while replay fails on stale encrypted/legacy records.Rescue does not decrypt stale blobs, migrate legacy tool calls, or rewrite the original rollout. What I want to validate is whether it detects the damaged boundary conservatively and produces a safe bounded handoff instead of guessing a repair.
If you still have a pre-surgery backup of the affected rollout, would you be willing to try:
Sanitized
doctor/salvage --fork/verifyoutput would be useful. Please don’t post raw encrypted content, rollout text, prompts, DBs, credentials, or private paths.https://github.com/shleder/codex-rescue
I think the
ocx1:portion of this report has a different cause than OpenAI key rotation.ocx1:is an OpenCodex proxy format, not native OpenAI encryption. OpenCodex defines it as:ocx1:+ base64-encoded plaintext compaction summarySource: https://github.com/lidge-jun/opencodex/blob/main/src/responses/compaction.ts
OpenCodex also uses port
10100, matching the local relay in this report. Its source says anocx1:item must be translated before forwarding to native OpenAI, because OpenAI cannot decrypt it and will reject it.So the first error appears more likely to be an OpenCodex translation/forwarding failure than a stale native OpenAI encryption key. The later
array_above_max_lengtherror may still be a separate legacy serialization problem.