Stale encrypted compaction/reasoning breaks entire thread after key rotation (invalid_encrypted_content + array_above_max_length)

Open 💬 3 comments Opened Aug 3, 2026 by harrypham2000
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

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 with gpt-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 (from state_5.sqlite table threads)
  • 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_call items and/or ocx1/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:

  1. Open the thread in Codex.
  2. Send any user message.
  3. Observe the turn fails immediately with invalid_encrypted_content (if the ocx1: compaction is still present) or array_above_max_length (if the compaction has been removed but old-format function_call items 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 — all reasoning items with encrypted_content set and no content field
  • 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_content at 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_output items to the new custom_tool_call / custom_tool_call_output shape (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:

  1. Removed the compacted event whose replacement_history contained the ocx1:-encrypted cmp_... blob.
  2. Removed all 33 reasoning items with non-null encrypted_content.
  3. Removed the 4 remaining reasoning items with encrypted_content: null (they also carried content: [reasoning_text], which the schema rejects when encrypted_content is present even as null).
  4. Removed all 24 old-format function_call / function_call_output items.

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) and gAAAA... (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 (table logs, column feedback_log_body)
  • Thread row in ~/.codex/state_5.sqlite table threads had history_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)

  1. On startup after an update, scan each thread's rollout for encrypted_content that 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.
  2. Migrate legacy function_call / function_call_output items to the current custom_tool_call / custom_tool_call_output representation (or drop them) at load time, so they never reach the API in the old shape.
  3. Improve the error surfaced to the user: invalid_encrypted_content and array_above_max_length with 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.
  4. Detect at load time when a thread's history_mode or item formats are stale relative to the current Codex build and offer an automatic repair/migration path before the user hits a 400.

View original on GitHub ↗

3 Comments

github-actions[bot] contributor · 25 days ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #36551
  • #36662

Powered by Codex Action

shleder · 14 days ago

I’m testing Codex Rescue 0.1.0a3 against 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:

pipx install codex-rescue==0.1.0a3
codex-rescue doctor --latest

Sanitized doctor / salvage --fork / verify output would be useful. Please don’t post raw encrypted content, rollout text, prompts, DBs, credentials, or private paths.

https://github.com/shleder/codex-rescue

darlingm contributor · 11 days ago

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 summary

Source: 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 an ocx1: 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_length error may still be a separate legacy serialization problem.