Session rename mid-server-tool-call injects a turn that permanently corrupts the transcript (400 on every future prompt)
Summary
Renaming a session (custom title) while a server_tool_use call (e.g. the built-in advisor tool) is in flight injects a system-reminder as a synthetic user turn in the transcript, landing it between the server_tool_use block and its matching advisor_tool_result block. Because server-tool call + result must live in the same API message, this splits them across two messages on the next resume/request, and the Anthropic API rejects the entire history with a 400 on every subsequent turn — permanently breaking the session until the transcript file is hand-edited.
Reproduction
- Start a session and trigger a server-side tool call that returns asynchronously (in our case, the
advisortool). - While the call is in flight, rename the session (set a custom title) — e.g. via whatever UI/flow assigns
customTitle/agent-name. - Let the tool call resolve and continue the conversation, then send further prompts.
- Every subsequent prompt now fails with:
``tool_use_id
API Error: 400 messages.N.content.0: unexpected found in advisor_tool_result blocks: srvtoolu_XXXX. Each advisor_tool_result block must have a corresponding server_tool_use block before it.``
Root cause (confirmed by inspecting the session .jsonl)
In the broken transcript, the three lines around the failure were:
{"type":"assistant", ..., "uuid":"4320b59f-...", "message":{"content":[{"type":"server_tool_use","id":"srvtoolu_01YZBP7b9c3YoD3NXiLgXsVb","name":"advisor","input":{}}]}}
{"type":"user","isMeta":true, "parentUuid":"4320b59f-...", "message":{"role":"user","content":"<system-reminder>\nThe user named this session \"...\". This may indicate the session's focus or intent.\n</system-reminder>"}}
{"type":"assistant", "parentUuid":"<the user line above>", "message":{"content":[{"type":"advisor_tool_result","tool_use_id":"srvtoolu_01YZBP7b9c3YoD3NXiLgXsVb", ...}]}}
When the harness replays/merges the transcript into the API messages array, consecutive same-role entries are folded into one logical message (confirmed by other server_tool_use/advisor_tool_result pairs in the same file that have no interruption and work fine). The injected isMeta user turn breaks that fold here, producing:
- message A (assistant):
[..., server_tool_use] - message B (user):
[system-reminder text] - message C (assistant):
[advisor_tool_result, ...]
advisor_tool_result in message C no longer has its server_tool_use in the same message → 400 from the API → every future turn in the session fails identically, since the full (corrupted) history is resent each time.
Impact
- Session becomes permanently unusable for every future prompt, not just the one where the race happened.
- No user-facing recovery path — we had to manually locate the offending lines in
~/.claude/projects/<project>/<session>.jsonl, delete the injectedisMetasystem-reminder line, and repoint the following message'sparentUuidto restore a valid chain, then re-verify the merged message structure by hand.
Suggested fix
- Don't inject
system-reminder(or any synthetic) user turns while a server-side tool call is awaiting its result — queue the reminder and deliver it only once the current turn (tool_use + its result) is fully closed. - Defensively, on session load/resume, detect and either coalesce or drop any zero-content-value synthetic turn that would split a
server_tool_use/matching result pair, rather than sending it to the API and hard-failing.
Happy to share the (redacted) transcript snippet if useful.
Showing cached comments. Read the full discussion on GitHub ↗
8 Comments
Full disclosure: I maintain cozempic.
The forensic detail here is unusually clear — you've traced the exact injected line (
isMetasystem-reminder betweenserver_tool_useandadvisor_tool_result), confirmed the failure mechanism, and already done the repair manually. Worth knowing thatcozempic doctorhas an orphaned-tool-results check targeting exactly this structural break: atool_resultblock (oradvisor_tool_result) that no longer has a correspondingtool_usein the same API message context.Running it on the broken session file should detect the orphaned result automatically and walk through the repair rather than requiring manual JSONL surgery. There's also a
doctor --fix unresumable-sessionpath that targets sessions the API rejects on every turn — which is exactly the state your session ended up in.It won't prevent the race condition (that's CC's fix to ship), but it covers the recovery step you had to do by hand. Worth trying before editing session files manually in future occurrences.
Ran into the same failure mode and wanted to leave a recovery recipe for anyone whose session is already 400-bricked, since there's no in-product path yet.
Confirming your root cause on my side. I reconstructed your three-line pattern (the
server_tool_use, the injectedisMetasystem-reminderuser turn, and theadvisor_tool_result) alongside an uninterrupted healthy pair in the same file, and the failure is exactly the fold break you describe: theisMetaturn between theserver_tool_useand its result forces the result into a separatemessagesentry, so on replay theadvisor_tool_resulthas noserver_tool_usein the same message →400 unexpected tool_use_id. The healthy pair with no interruption folds fine, which is the tell.Automated recovery. This does what you did by hand — finds an
isMetauser turn that sits between aserver_tool_useand its matching result, drops it, and repoints the result'sparentUuidback to theserver_tool_useline. It only touches a turn that actually splits a matching pair, so uninterrupted pairs and ordinaryisMetareminders are left alone.Usage (back up first):
cp session.jsonl session.jsonl.bak && python3 repair.py ~/.claude/projects/<project>/<session>.jsonlthen swap in the.fixedfile. On my reconstruction it removed only the contaminated turn, restored the chain so every*_tool_resultagain shares a message with itsserver_tool_use, and left the healthy pair byte-for-byte. Caveat: I validated against the reconstructed pattern from your report, not your actual file — verify the.fixedoutput before replacing.Detection before it bricks the session. The same match condition works as a
SessionStartcheck that scans~/.claude/projects/**/<session>.jsonlfor anisMetaturn splitting aserver_tool_use/result pair and warns with the offending line, so you can repair before the next prompt 400s instead of discovering it mid-work.Agree the real fix is on the harness side — either queue the
system-reminderuntil the in-flight tool turn closes (your first suggestion), or coalesce/drop any zero-value synthetic turn that would split aserver_tool_use/result pair on load. Detection + repair only limits the blast radius after the fact.@yurukusa that recovery script is excellent — you arrived at the exact same structural diagnosis independently, which validates the root cause. The three-line pattern you reconstructed (server_tool_use → injected isMeta → advisor_tool_result separated across message boundaries) is precisely what the repair needs to target.
For what it's worth, cozempic ships an auto-repair for this same break:
cozempic doctor --fix unresumable-sessiondetects an advisor_tool_result or server_tool_result whose matching server_tool_use isn't in the same message context, and repairs the parentUuid chain — which is structurally the same thing your script does. It idle-gates on mtime (won't race a live write), and the guard daemon runs it automatically on session start before the first prompt can 400.Your SessionStart detection hook idea is also the same approach our guard uses — scan the JSONL at session start, warn before the bad turn triggers, repair if it can. Nice to see two independent implementations converge on the same design.
The repair script you posted is a solid standalone option if someone doesn't want to install anything — good to have it documented in this thread for people who find it later.
Confirming the same failure independently, third report converging on the identical root cause.
Hit this today:
advisor()in flight, session got auto-named ("The user named this session ...") mid-call, and every subsequent turn 400'd with the exact error. Inspecting the transcript found the identical three-line pattern already documented here:One data point worth adding: the repair did not require dropping the injected reminder line or rewriting the file into a
.fixedcopy — re-pointing only theadvisor_tool_resultrecord'sparentUuiddirectly to theserver_tool_userecord's ownuuid(leaving the injected reminder line in place, just no longer on the path between the pair) was sufficient. Verified before/after with a script that walks everyserver_tool_use/advisor_tool_resultpair in the file and assertsresult.parentUuid == server_tool_use.uuid: 1 broken pair found, 0 after the one-field edit, all 8 other pairs in the same session untouched.Adds confidence to the suggested fix direction: the harness only needs to avoid letting a zero-content synthetic turn sit on the path between a
server_tool_useand its result — it doesn't need to be excluded from the transcript entirely, just not injected as an intervening parent while that pair is open.Hit this again today, independent confirmation of the root cause and fix already documented here.
advisor()call in flight, session got auto-titled mid-call, every prompt after that 400'd with the exactunexpected tool_use_id found in advisor_tool_result blockserror.Transcript matched the documented pattern, but with one extra wrinkle: the injected content wasn't a single
isMetauser line, it was three lines — two"type":"system"framing entries plus theusersystem-reminder itself, all sitting between the pair:A single-field patch (repoint
advisor_tool_result.parentUuidstraight toserver_tool_use.uuid, per @jcfernandez-890825) isn't quite enough here on its own: the line that originally followed the result (the next assistant turn) still points itsparentUuidat the result. If you only fix the one field, that next turn is left pointing at aparentUuidthat's still mid-chain instead of at the new tail, forking the tree into two children at that node instead of a linear chain.Full repair that worked: move the result so it's an immediate child of
server_tool_use, then chain the framing/reminder lines after it in their original relative order, then repoint the turn that used to follow the result so it now follows the last framing/reminder line instead. Net effect: same three-field reparenting, just generalized to N intervening lines instead of assuming exactly one.+1 for the harness-side fix — queue/defer the system-reminder (and any framing it carries) until the in-flight server-tool turn is fully closed. This is at least 4 independent reports of the same race now, and the "exactly one isMeta line" assumption in the manual/scripted repairs floating in this thread won't hold for every occurrence.
Still reproducing on 2.1.220 (npm latest as of 2026-07-30), and the trigger does not require a user-initiated rename.
Two independent sessions on the same machine bricked within 24h. In both, the injected records were
isMetauserrecords carrying the "The user named this session ..." system-reminder — but in one of them the name was never changed by the user: it was set once at session start, and the harness re-asserted the identical value repeatedly (52custom-title/ 61agent-namerecords over the session). The reminder pair still landed mid-flight.server_tool_useat T+0 → two identical reminders at T+95s (1 ms apart) →advisor_tool_resultat T+106sserver_tool_useat T+0 → two identical reminders at T+73s (1 ms apart) →advisor_tool_resultat T+112sIn both cases the call and the result share the same
message.id, so the pair ends up split across two persisted assistant records with the reminder records in between, and every later request fails identically:Deterministic: retry, a different prompt and
/compactall fail the same way, since the same history is resent every turn.Notes that may help others hitting this:
parentUuidat the call record'suuid. Anchor on uuid, not line numbers — the file keeps being appended to while you work.claude --resume <id> --fork-session -p ...verifies the repaired history without touching the real session.Is buffering injected reminders until the server-tool turn completes (the way
pause_turnresumption already requires the assistant message to be replayed unchanged) viable? The call/result pair looks like it has to be persisted atomically.Same race, different server tool:
tool_search_tool_regex→tool_search_tool_result. Every report in this thread so far isadvisor(); this machine has never called advisor — 183/183server_tool_userecords across 223 transcripts aretool_search_tool_regex— so the defect is in the shared injection/fold path rather than anything advisor-specific, and it reaches the tool-loading calls the harness issues on its own initiative.Occurrence (2.1.220, macOS 25.4.0, CLI, user-initiated rename):
Lines 22 and 24 carry the same
message.id— the fold the API requires is exactly the one the reminder breaks. Every subsequent prompt then fails with:Exposure window was ~1.9s (call → result); the reminder landed 0.8s in.
The repair scripts posted in this thread miss this variant. @yurukusa's matcher keys on
tool_search_tool_resultis not in that tuple, so it prints "No contamination found; file left untouched." on a file that is bricked. Suggest matching any*_tool_resultblock whosetool_use_idstarts withsrvtoolu_instead of an allowlist of names — server tools will keep being added, and the allowlist silently fails closed-eyed on each new one.Otherwise the shape matches the original report exactly: a single
isMeta: trueuser record, notype:"system"framing lines (so @alies-dev's three-line variant is not universal). Minimal repair sufficed — drop the injected record, repoint the result record'sparentUuidto theserver_tool_userecord'suuid— verified by asserting that no non-assistant record sits between anyserver_tool_useand its matching result, swept across all 223 transcripts on the machine: 1 broken pair before, 0 after, every other pair untouched.Base rate here: 223 transcripts, 183 server-tool call/result pairs, 1 split by an injected reminder → 1 permanently bricked session.
Correction: the repair script I posted above does nothing on the exact case @ungeism reported.
Both of those cases have two identical reminders (1 ms apart) between the
server_tool_useand theadvisor_tool_result. My script walks forward from anisMetaturn but inspects only the first record whoseparentUuidmatches, thenbreaks. With two chained reminders that first record is the second reminder, not the result — no id match, loop exits, nothing repaired. It only ever worked for a single injected turn, which is the shape I reconstructed from the original report. So anyone who ran it on a session bricked this way got "No contamination found" on a genuinely contaminated file.Fixed by anchoring on the pair instead of on the reminder: walk
server_tool_useids, find the record carrying the matching result id, and drop what is between them only if every intervening record is anisMetauser turn. Any number of reminders is handled; anything else in between is left for a human instead of being silently deleted.What I verified, and what I could not. I synthesised the Case A/B shape (call → N identical reminders → result) in a file that also contains an uninterrupted pair and an ordinary
isMetareminder splitting nothing, then ran both versions:| injected reminders | old | new |
|---|---|---|
| 1 | repairs | repairs |
| 2 (Case A/B) | does nothing | repairs |
| 3 | does nothing | repairs |
The new one leaves the healthy pair's
parentUuidunchanged, keeps the non-splittingisMeta, and drops exactly the injected turns.I could not test against a real corrupted transcript. Scanning this machine found 819 session files, 240
server_tool_use/result pairs, and zero with anything between a call and its result — it has never fired here, and I don't rename sessions, which fits @ungeism's point that the reminder storm is what puts a pair at risk. So this is verified against the reported shape, not against a live brick: check the.fixedoutput before swapping it in.