SendMessage cannot resume an orphaned background subagent whose transcript exceeds ~5 MiB: "No transcript found for agent ID"
Summary
Resuming an orphaned background subagent via SendMessage after a Claude Code restart fails with
Agent "aXXXXXXXXXXXXXXXX" could not be resumed: No transcript found for agent ID: aXXXXXXXXXXXXXXXX
even though the transcript file exists and is intact. It reproduces reliably once the agent's transcript~/.claude/projects/<project>/<session>/subagents/agent-<id>.jsonl grows past roughly 5 MiB; below that
size the identical flow resumes fine.
The restart notification that instructs you to do this appears to check only that the transcript file exists
and is non-empty, so the harness advertises a recovery path that cannot actually be honored for any
long-running agent — and the longer (i.e. more valuable) the orphaned agent's context, the more certain the
advertised recovery is to fail.
Environment
- Claude Code 2.1.220 (also reproduces on 2.1.218), native installer, Linux x64.
Repro sketch
- Launch a background subagent (
Agenttool) and let it run long enough that
subagents/agent-<id>.jsonl exceeds ~5 MiB (a few hours of tool-heavy work; mine was 663 lines / 14.1 MiB).
- Kill or exit the Claude Code process while the agent is still running.
- Restart. The orphan notification says:
> No completion record was found for background agent "…" from the previous session. It may have been
> stopped, or it may have been running when the previous Claude Code process exited — either way its
> transcript is saved on disk, so its progress is not lost. Resume it by sending it a message with
> SendMessage, or check its worktree/output for partial work before assuming the task landed.
SendMessage(to: "<agentId>", …)→could not be resumed: No transcript found for agent ID: <agentId>.
Control: the same flow with a transcript under ~5 MiB resumes normally
(Agent X had no active task; resumed from transcript in the background with your message.).
Evidence for the size threshold
In one workstream, four background agents were orphaned by the same process exit and I attempted to resume
them the same way. Sizes below are the transcript sizes at the moment of the resume attempt, reconstructed
from the per-line timestamps in each .jsonl:
| transcript size at resume | result |
|---|---|
| 0.93 MiB | resumed ✅ |
| 0.97 MiB | resumed ✅ |
| 3.1 MiB | resumed ✅ |
| 14.1 MiB | failed ❌ "No transcript found for agent ID" |
Nothing else distinguished them: same project, same session directory, same agentType, all had their.meta.json present, all had live worktrees, all were spawned by the Agent tool. Only size correlates.
Nothing appears to be wrong with the failing file itself — it is well-formed JSONL and parses cleanly.
Impact
- The advertised
SendMessagerecovery is a dead end exactly for the agents whose lost context matters most. - The automatic orphan re-dispatch on startup appears to go through the same resume path, so it silently
fails for these agents too.
- There is no in-product way to recover the context: the transcript lookup is keyed by agent id and session
directory, so the file cannot be relocated or re-registered.
Workaround
Starting Claude Code with CLAUDE_CODE_DISABLE_PRECOMPACT_SKIP=1 makes the resume succeed for these
large transcripts. That suggests the large-transcript loading fast path is what discards the agent's
messages — a subagent transcript consists entirely of sidechain messages, which may be a case that path
does not expect. (The tradeoff is losing the large-transcript loading optimization process-wide.)
Failing that, the only recovery is to read subagents/agent-<id>.jsonl manually and hand the relevant
content to a fresh agent.
Suggested fix
- Make the large-transcript loading path handle an all-sidechain transcript (or skip that optimization
entirely for subagents/agent-*.jsonl, which is all-sidechain by construction).
- Have the orphan notification validate resumability with the same loader the resume path uses, so it stops
promising SendMessage in cases where the loader will return nothing.
3 Comments
Confirming this on 2.1.223, and I think I can supply the exact mechanism — including why your
CLAUDE_CODE_DISABLE_PRECOMPACT_SKIP=1finding works and why your sidechain hunch is right. I traced it through the shipped bundle. Symbol names below are the minified ones from the 2.1.223 artifact.The threshold is exactly 5 MiB, and it's a hard file-size gate
In the transcript loader (
RMe):This is a pure function of file size, which is why it's perfectly deterministic and survives restarts. My failing transcript was 5,251,451 bytes — 8,571 bytes over the line. A control agent in the same session directory, same
agentType, same spawn mechanism, at 2,711,993 bytes resumed fine. That 8.5 KB was the only difference.Why the fast path returns nothing for a subagent
pDbis the >5 MiB scanner. It picks its walk root like this:R[n]is true when linencontains"isSidechain":true. So it scans backwards for the last non-sidechain line to use as the root of the chain walk. Exactly as you suspected: every line of a subagent transcript isisSidechain:true, so that scan never matches. In my file, 1508 of 1508 lines carry the marker.oetherefore stays-1,ae(undefined)walks nothing, andWends up an empty but non-nullSet. That last part is the actual defect — becauseWis non-null,B = W ?? new Set(C)does not fall back, and pass 2 parses zero lines.I reimplemented the selection logic and ran it against the real file:
The two escape hatches that normally save this are both absent from a subagent transcript by construction: 0
last-promptentries (which would be force-added viafor(let oe of L)B.add(oe)) and 0compact_boundaryentries (which would setM=trueand bypass pruning entirely).Empty map →
Dvtreturnsnullat itsif(i.length===0)return nullguard → the resume path throwsNo transcript found for agent ID. The message is literally true about what the loader returned, which is what makes it so misleading: the file is intact, it just never got parsed.Why it's invisible in telemetry
Worth flagging separately, because it's why this is hard to catch: the loader returns empty rather than throwing.
RMehas real error handling for unreadable/permission cases, but "selected zero lines" isn't an error condition — it's a successful load of nothing. On my side theSendMessagetool call was even recorded as"success":"true"with a 33 ms duration; the failure surfaces only as tool content. I checked my local OTEL log and found zero occurrences of thesubagent_resume_transcript_missingtelemetry code that the throw site emits.The one-line fix — and there's already a precedent for it in the same file
pDbshould fall back to loading everything when the walk root is never found:W=nullmakesB=W??new Set(C)select every line, which is the correct conservative behavior.pDbalready does exactly this one branch over, for the phantom-parent case:So the fix is the existing bail-out extended to one more unsatisfiable-root condition.
Independently, the sibling in-memory truncator
O2p— same job, buffer instead of file — already guards this exact case:O2preturns the entire input when it can't find a root.pDbreturns nothing. Same intent, and only one of the two is safe — which suggestspDbjust missed the guard rather than there being a design reason for the difference.O2palso has a second sanity checkpDblacks (if(g-S<g>>1)return e— bail out if pruning wouldn't drop at least half the file).Scope
This isn't an edge case that needs an unusual transcript — it's every subagent over the threshold, since all-sidechain is structural for
subagents/agent-*.jsonl. In one session of mine, 25 of 550 subagent transcripts were over 5 MiB; every one I sampled showed the identical fingerprint (all lines sidechain, nolast-prompt, nocompact_boundary). They're all unresumable once the in-memory mirror is gone.That in-memory mirror is worth noting as the reason this only bites after a restart. Immediately above the throw there's a fallback:
Within a single process that rescues the resume and the disk bug stays hidden. Restart and the mirror is gone, so the broken disk path is all that's left.
I can confirm the workaround recovers real data, not just a clean error: with the full-read path forced, my file parses to 1508 entries, and the chain walk from the selected leaf reaches 1498 messages. So the content was fully recoverable the whole time.
Two smaller notes in the same area:
_Beconstant is shared with the session-loader path in #55700 (closed), which reports a different failure mode — pre-boundary scrollback loss — from the same threshold. Different code path and different symptom, but a fix that only touches the session loader wouldn't address this one.pDbmakes the existing notification honest, so validating with the real loader is a good belt-and-braces measure rather than the primary fix.Independent confirmation on macOS with native runtime 2.1.237, with additional evidence for the mechanism described above. I also inspected the installed official 2.1.247 runtime and found the same selection logic; I have not end-to-end retested that version.
Three useful additions:
SendMessage.A minimal synthetic text-only fixture establishes the exact boundary:
| Fixture | Bytes | Original records selected | Candidate records selected |
| --- | ---: | ---: | ---: |
| Two-record, all-sidechain transcript | 5,242,880 | 2 | 2 |
| Same shape, one byte larger | 5,242,881 | 0 | 2 |
| Above-threshold main-conversation transcript | 5,242,881 | 2 | 2 |
| Above-threshold sidechain transcript with preserved compaction | 5,242,881 | 3 | 3 |
The fixture consists of a root user record and its child assistant record, with ordinary UUID/timestamp/parent linkage; the user text is padded to the specified serialized JSONL byte length. It contains no images. The preserved-compaction case adds a boundary record. This isolates serialized size and leaf selection from image handling, provider behavior, or permissions.
Nine synthetic cases passed after the in-memory correction: exactly at/above the threshold, explicit keep-all mode, normal main chains, rewind pruning, trailing sidechains, both compaction forms, and last-prompt metadata. Existing main-chain pruning behavior remained unchanged in those cases. This is parser-level proof, not a patched live-app or full cold-resume test.
The owner-level fix should make the streaming selector preserve records when it has no eligible main-conversation leaf, matching the buffered selector's existing behavior. It should not require truncating user history, changing permissions, or disabling the optimization process-wide. Please include native cold-resume coverage after completed-task eviction as well as restart coverage.
No original session/agent IDs, transcript contents, local paths, or configuration are included here.
Corroborating data for the ~5 MiB threshold, from one project over 2026-08-29 → 2026-09-01.
Seven background subagents were each sent a follow-up via
SendMessageafter they had finished and reported. Resume outcome vs. the size of the agent's ownsubagents/agent-<id>.jsonlat the time:| Transcript size | Image-heavy? (many
Readcalls on PNG/JPG) |SendMessageresume ||---|---|---|
| 0.46 MB | no | resumed |
| 1.37 MB | no | resumed |
| 4.49 MB | no | resumed |
| 6.31 MB | yes |
No transcript found for agent ID|| 8.21 MB | yes |
No transcript found for agent ID|| 10.13 MB | yes |
No transcript found for agent ID|| 17.27 MB | yes | (not attempted — pattern already clear; committed the agent's worktree directly) |
Every transcript under 5,242,880 bytes resumed; every one over it failed with the "no transcript" message, though each file was present and intact on disk. Size was the only predictor we found — the failing agents were not orphaned by a restart; they had simply finished, in the same session, minutes earlier. All three large ones got there by reading images (rendered map crops during a georeferencing task), so their token usage was ordinary; the file size was not.
Two things that would have saved us time:
Environment: Linux, native install; the observations above were on the release current as of 2026-09-01 (we have since updated to 2.1.258 and have not yet re-tested on it).