Repository navigation
Description
Bug Description
When an API 403 error triggers automatic retries, the retry mechanism re-sends the user message with an incorrect parentUuid, pointing to a subagent's progress message instead of the main conversation chain. Upon session resume, the conversation tree is rebuilt from this broken branch, causing all prior conversation history to be permanently lost from context.
This is not context compression — the JSONL file retains the full history, but the model receives none of it because the parent chain is severed.
Reproduction Steps
- Have a long-running conversation session with subagents (progress messages present)
- Send a message that triggers a 403 API error (
{"error":{"type":"forbidden","message":"Request not allowed"}}) - Claude Code automatically retries the same message multiple times
- Close the terminal window and reopen / resume the session
- The model now only sees the last message — all prior history is gone
Root Cause Analysis
From the JSONL file (a6de1f0c-178d-4085-9fd0-0f7abf13cc70.jsonl, ~53K lines):
First send (correct parent):
L53699 [system/turn_duration] uuid=9ef20ec5-101..
L53701 [user] parentUuid=9ef20ec5-101.. uuid=de92f26a-e76.. ← correct, points to main chain
L53702 [assistant] 403 error
Retries (broken parent):
L53703-53734 [progress] ← subagent progress messages, chain ends at uuid=558d5e73-82e..
L53736 [user] parentUuid=558d5e73-82e.. ← WRONG! Points to progress msg, not main chain
L53737 [assistant] 403 error
...
L53745 [user] parentUuid=558d5e73-82e.. ← same broken parent, retry #3
L53746 [assistant] 403 error
...
L53754 [user] parentUuid=558d5e73-82e.. ← retry #4, finally succeeds
L53756 [assistant] normal response
The retries all use parentUuid=558d5e73-82e.. which is a progress message from a subagent sidechain, not the main conversation's turn_duration system message (9ef20ec5-101..).
When the session is resumed, the conversation tree is walked from L53754 backwards via parentUuid, which goes into the progress sidechain and never reaches the actual conversation history.
Expected Behavior
- Retries after 403 should preserve the original
parentUuidfrom the first (failed) attempt - The parent should always point to the main conversation chain, never to progress/sidechain messages
- Session resume should not lose conversation history due to retry-induced parent chain corruption
Impact
- Severity: Data loss — entire conversation context silently disappears
- User-visible: After resume, the model has no memory of prior conversation (not compression, complete loss)
- JSONL intact: The full history exists in the JSONL file but is unreachable via the broken parent chain
Environment
- Claude Code CLI (latest as of 2026-03-22)
- Model: claude-opus-4-6
- OS: Arch Linux
- Terminal: Ghostty
Activity
Loong0x00 commentedon Mar 22, 2026
Additional Finding: No subagent was active when this occurred
Upon further investigation, the user did not have any subagents running at the time of the bug. There was only a single main conversation window.
The
progressmessages (L53703-53734) that corrupted the parentUuid chain were delayed writes from the previous conversation turn's tool calls — they were flushed into the JSONL after theturn_durationsystem message (L53699) had already marked that turn as complete.Revised timeline:
turn_durationwritten, marking the previous turn as "done"parentUuidpointing to L53699)progresstype, containing web search results, file reads, etc.) are asynchronously flushed into the JSONL after their parent turn already endedprogressmessage) asparentUuid→ chain brokenImplications
This means the bug is not limited to users who explicitly use subagents. Any conversation where internal tool calls (which are implemented as subagents under the hood) have async I/O can trigger this. The
turn_durationmarker does notawaitall pending writes before being emitted, so there is a window where stale progress messages can land between the turn boundary and the next user message.This significantly increases the blast radius — it can affect any session that hits a 403 during retry while prior tool-call results are still being flushed.
github-actions commentedon Mar 22, 2026
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
Loong0x00 commentedon Mar 22, 2026
This is not a duplicate of the linked issues, though it shares the same root cause family.
This issue adds a distinct trigger and deeper root cause analysis:
turn_durationmarker doesn't await pending writes — this is the architectural root cause that enables all variants of this bugThe fix for all four issues is the same: the parentUuid chain walker needs to either (a) treat progress entries as sidechain, or (b) retry logic must preserve the original parentUuid. Both should probably be done.
Requesting this not be auto-closed as duplicate — the trigger path and root cause detail here are complementary to the existing reports.
Loong0x00 commentedon Mar 22, 2026
Source Code Analysis — Root Cause Found
Decompiled the CC binary (
2.1.81). The bug is in the tip-finding function used during session resume.The tip selector (
ug$in minified source):This selects the message with the latest timestamp that passes the filter.
The call site in
giH(current session loader):Filter:
!isSidechain. That's it.The problem:
progressmessages haveisSidechain: false, so they pass the filter. When progress messages are written asynchronously (e.g., from tool calls in the previous turn), their timestamps are later than the last real conversation message. The tip selector picks the progress message as the chain tip, thenwalkChaintraces back viaparentUuidthrough the progress branch, and the entire main conversation chain becomes unreachable.The fix is trivial:
Either fix is a one-line change. Both should probably be applied.
Why this wasn't caught:
The
SH_(resume from file) andYb(session list) code paths useleafUuidsset for filtering, which may exclude progress messages depending on how leafUuids is populated. ButgiH(the live session loader) uses only the!isSidechaincheck — meaning this bug specifically affects live session resume (close window → reopen), not cold resume from file.This bug affects every session that has tool calls and gets resumed, not just subagent sessions or 403 retry scenarios. The 403 in my case just made the timing window wider, making it easier to trigger.
This is the same root cause behind #35024, #33651, and #24304. One-line fix, open for 6+ weeks.
junaidtitan commentedon Mar 22, 2026
We open-sourced cozempic which addresses part of this — it uses atomic writes (write→fsync→replace) so session files can't be partially corrupted during resume, and it never touches uuid/parentUuid fields so the conversation DAG stays intact. Feedback welcome.
github-actions commentedon Apr 21, 2026
Closing for now — inactive for too long. Please open a new issue if this is still relevant.
github-actions commentedon Apr 30, 2026
This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.