| name | claude-code-session-jsonl-orphan-advisor-tool-result |
| description | Repair a Claude Code session whose transcript JSONL has an orphan
`server_tool_use` / `*_tool_result` content block, causing every API call
(including `/compact` and `claude --resume`) to fail with HTTP 400
"unexpected tool_use_id found in advisor_tool_result blocks: srvtoolu_...
Each ... block must have a corresponding server_tool_use block before it".
Use when: (1) a Claude Code session dies mid-advisor call with "socket
connection was closed unexpectedly", (2) resume / compact returns the 400
above naming `srvtoolu_...`, (3) `/remote-control` (or any system event)
activated mid-stream while the assistant was emitting an advisor response.
Covers diagnosis via grouping JSONL lines by shared `message.id`, surgical
deletion of the orphan pair (both halves), and the line-number-drift
gotcha that occurs when the file grows between inspection and edit.
|
| author | Claude Code |
| version | 1.0.1 |
| date | "2026-06-01T00:00:00.000Z" |
Claude Code session JSONL — orphan advisor tool_use_id repair
Problem
A Claude Code session becomes permanently stuck: every API request — typing a
new prompt, /compact, or even claude --resume <id> — returns:
API Error: 400 messages.<N>.content.0: unexpected `tool_use_id` found in
`advisor_tool_result` blocks: srvtoolu_<id>. Each `advisor_tool_result`
block must have a corresponding `server_tool_use` block before it.
The session can no longer be used. The transcript file holds the conversation
hostage.
Context / Trigger Conditions
All of the following are common triggers:
- The previous turn called
advisor() and the message stream was interrupted
("API Error: The socket connection was closed unexpectedly").
/remote-control (or any other system command) activated while the assistant
was mid-stream emitting the advisor response, splitting the writes across
more JSONL lines than usual.
- A subagent died mid-call and the parent kept writing.
- You resumed the session, got the 400, tried
/compact, also got 400.
The Anthropic Messages API requires that a server_tool_use block AND its
corresponding *_tool_result block live in the same assistant message's
content array. Claude Code's JSONL stores each "delta" of an assistant
response on a separate line and groups them back into one message at send
time using the shared message.id. The bug happens when the grouping logic
emits them as two separate messages — the _tool_result lands in a
message whose content[0] has no preceding server_tool_use → 400.
Solution
Step 1 — Tell the user to quit any open stuck Claude Code window
The corrupted session is still loaded in their terminal. It can't write
anything (every API call 400s), so it's effectively read-only, but quit it
before editing to be safe.
Step 2 — Locate the session file
SF=~/.claude/projects/-Users-<user>-<repo-path>/<sessionId>.jsonl
ls -la "$SF"
If claude --resume <id> reports "No conversation found", you are running it
from a directory whose project dir doesn't contain the session. Resume from
the directory the session was originally launched from (often the main repo
root, even if the session later worked inside a worktree). Note the encoded
project-dir name collapses both / and _ to -, so a folder like
...-worktrees-cute-tab may actually be the worktree cute_tab — git worktree list reveals the real path (see Notes for the false-MISSING gotcha).
Step 3 — Find the orphan id and the lines carrying it
Grab the srvtoolu_<id> from the 400 error message, then:
grep -n "srvtoolu_<id>" "$SF" | awk -F: '{print $1}'
You will typically see 2–4 line numbers. The actionable ones are lines whose
content contains either:
"type":"server_tool_use" with that id, OR
"type":"advisor_tool_result" (or tool_result) with "tool_use_id":"<id>".
Any other lines that mention the id are usually just error-log strings (e.g.,
the 400 response itself recorded back into the JSONL) — leave those alone.
Step 4 — Confirm shared message.id and inspect neighbours
Both halves should share message.id. Find the full group:
grep -n "msg_<messageId>" "$SF" | awk -F: '{print $1}'
Check the surrounding lines — what's the message structure (thinking,
text, tool_use, etc.) when you remove the orphan pair? The remaining
blocks must still form a valid message (at minimum: one non-empty content
block). For a typical advisor mid-stream, the message looks like:
thinking, text, server_tool_use, [advisor_tool_result], thinking, text, tool_use(next_skill). Dropping the middle two leaves a valid skeleton.
Step 5 — Back up, then delete BOTH lines
BK="${SF}.bak-$(date +%Y%m%d-%H%M%S)"
cp "$SF" "$BK"
awk 'NR!=95 && NR!=99' "$BK" > "$SF.tmp" && mv "$SF.tmp" "$SF"
Critical: delete BOTH halves of the orphan pair, not just the
*_tool_result. If you delete only the result, the server_tool_use becomes
the new orphan (tool_use without tool_result) and the API will reject it
just as forcefully.
Step 6 — Verify
grep -cE '"(server_tool_use|advisor_tool_result|tool_result)"[^}]*srvtoolu_<id>|srvtoolu_<id>[^}]*"(server_tool_use|advisor_tool_result|tool_result)"' "$SF"
Step 7 — Re-resume
cd <directory-where-session-was-originally-launched>
claude --resume <sessionId>
Derive that cwd from git worktree list, do NOT construct it by decoding
the encoded project-dir name — the encoding is lossy (/ AND _ both →
-), so a name like ...-my-app-propensity--claude-worktrees-feature-v7
can decode to the wrong path: the repo is really my_app_propensity
(underscore) and the --claude-worktrees- segment is /.claude/worktrees/.
Handing the user a literally-decoded cd path (e.g. the hyphen spelling) will
fail with cd: no such file or directory even though the session is fine.
This applies when telling the user where to resume, not just when locating
the JSONL — match the encoded <name> against git worktree list output by
normalising _/-, then quote the real on-disk path.
The 400 is gone. The advisor consultation text is lost from history, but
every other turn, todo, commit, and tool result is intact. Prompt the
assistant to continue from where it was ("continue with Task N" or similar).
Verification
- File line count decreased by exactly the number of orphan-block lines you
removed (typically 2).
- The orphan id no longer appears in any
server_tool_use or _tool_result
content block (only — possibly — in error-log text strings).
claude --resume <id> opens normally and accepts a new prompt without
returning 400.
⚠️ Line-number drift gotcha
Re-grep for the current line numbers immediately before running the awk
delete. If any time has passed since your initial inspection — especially
if the user attempted /compact or another resume that got logged as an
error — the JSONL has GROWN, and the line numbers you noted earlier no
longer point at the orphans. Deleting the old numbers will corrupt random
unrelated lines and is hard to detect because there's no immediate error.
Symptom that you hit this: after your delete, grep -c for the orphan id
in server_tool_use|*_tool_result content blocks still returns > 0.
Recovery: restore from the .bak file you (hopefully) created, re-grep for
current line numbers, and try again.
Example
Session 97f294f0-f54b-42d1-9f51-a23c3bee0a68, file at 345 lines:
$ grep -n "srvtoolu_01PqQwhhaakjWqL3YpqAjo7H" "$SF" | awk -F: '{print $1}'
95
99
328
334
Inspect each:
- 95 →
"type":"server_tool_use" with that id → orphan, delete
- 99 →
"type":"advisor_tool_result","tool_use_id":"<id>" → orphan, delete
- 328 → inside a
<local-command-stderr> text block (the /compact error
Claude Code recorded) → leave alone
- 334 → inside a synthetic assistant
text block (the second 400 echoed
back) → leave alone
All four lines (95, 99, 328, 334) share message.id
msg_013PHxJ8qHe88WcYWgEhKXNR plus its neighbours (93, 94, 100, 101, 102),
confirming 95 and 99 are halves of one assistant response. Backup, awk 'NR!=95 && NR!=99', file → 343 lines, orphan content blocks → 0, resume
succeeds.
Notes
- The transcript JSONL lives at
~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl. The <encoded-cwd>
is the launch cwd with both / AND _ replaced by - (the encoding is
lossy — underscores and slashes both collapse to hyphens). Worktree sessions
usually live under the main repo's encoded path (because the session was
launched from the main repo, then cd'd), not under a worktree-specific
path. If claude --resume <id> says "No conversation found", you are running
it from the wrong cwd.
- Don't decode the project-dir name literally to check if the launch cwd
still exists. Because
_ → -, an encoded segment like
...-worktrees-cute-tab can map to a real directory named cute_tab (or
cute-tab) — you can't tell which from the encoded name alone. An ls of
the hyphen spelling will report MISSING even though the underscore-named
worktree is alive. To find the true launch cwd, run git worktree list (or
check sibling dirs) and match by normalising _/-, rather than trusting a
single literal ls.
- Don't try to "merge" the two lines into a single assistant message line
manually. Claude Code's JSONL parser tolerates the split format on read
but the merge format on write is undocumented; an attempt risks
destabilising the whole session. Deleting the pair is the safe move and
only costs the advisor's text content for that one consultation.
- Same fix works for any orphan
<X>_tool_use / <X>_tool_result pair
(web_search, code_execution, etc.), not just advisor — the API contract
is identical.
- This is distinct from
worktree-index-corrupt-async-post-commit-hook and
git-amend-hits-async-post-commit-hook-commit, which corrupt the git
index rather than the Claude Code session transcript.
References
- Anthropic Messages API: server tools and tool result pairing requirements
(a
server_tool_use content block and its corresponding *_tool_result
content block must appear in the same assistant message's content
array — splitting them across two assistant messages returns HTTP 400).