---
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-01
---

# 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

```sh
# Project dir = main repo, NOT a worktree. Worktree sessions still live under
# the repo's project dir, indexed by the cwd Claude Code was launched in.
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:

```sh
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:

```sh
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

```sh
BK="${SF}.bak-$(date +%Y%m%d-%H%M%S)"
cp "$SF" "$BK"
# Delete the orphan pair — replace 95 and 99 with YOUR line numbers
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

```sh
# Orphan content blocks should now be zero. The id may still appear in
# 1–2 *error-log text* lines (the 400 message Claude Code recorded) —
# that's harmless plain text.
grep -cE '"(server_tool_use|advisor_tool_result|tool_result)"[^}]*srvtoolu_<id>|srvtoolu_<id>[^}]*"(server_tool_use|advisor_tool_result|tool_result)"' "$SF"
# Expect: 0
```

### Step 7 — Re-resume

```sh
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).
