Resume of session with synthetic assistant tail (UUID message.id) sends invalid previous_message_id → API 400

Resolved 💬 5 comments Opened May 12, 2026 by zjc-enigma Closed Jun 26, 2026

Summary

When a session JSONL ends with <synthetic> assistant entries (UUID-style message.id, not msg_...), /resume followed by any input fails with:

API Error: 400 diagnostics.previous_message_id: must be the `id` from a prior /v1/messages response (starts with `msg_`)

The resume path appears to pick the last assistant message.id from the JSONL as previous_message_id without filtering out synthetic entries.

Environment

  • Claude Code: 2.1.131
  • OS: Linux 6.18.2-zen2-gentoo-dist
  • Shell: zsh

Reproduction

  1. In a session, trigger a tool result that causes the harness to fail mid-stream — in my case a Task/Agent subagent returned "You've hit your limit · resets 12am (...)" (i.e. weekly-limit hit while a tool_use was already in flight).
  2. Press "continue" one or more times. The harness writes synthetic assistant turns with model: "<synthetic>" and a UUID message.id (e.g. cbcb88fd-b582-411d-bff0-843b821ac04f).
  3. Exit Claude Code. Wait until the limit resets.
  4. claude/resume → pick that session → type anything.
  5. Observe API Error: 400 diagnostics.previous_message_id: must be the id from a prior /v1/messages response (starts with msg_).

What the session JSONL looks like

Tail of the broken file (annotated):

[465] type=assistant  msgid=msg_01MsrY...    ← last REAL assistant (has pending tool_use=Agent)
[466] type=last-prompt
[467] type=ai-title
[468] type=permission-mode
[469] type=user        content=[tool_result] ← real tool_result: "You've hit your limit · resets 12am (Asia/Shanghai)"
[470] type=assistant   model=<synthetic> msgid=d1e01673-...   ← synthetic begins
[472] type=user        text="continue"
[485] type=assistant   model=<synthetic> msgid=c7a85e6a-...
...
[552] type=assistant   model=<synthetic> msgid=cbcb88fd-...  ← last assistant in file, picked up as previous_message_id

All four sessions in this project: only the one with a synthetic tail 400s on resume. The other three end on real msg_... IDs and resume cleanly.

Workaround

Truncate the JSONL to the last entry preceding the first synthetic assistant. After truncation /resume continues correctly with previous_message_id = msg_01MsrY....

Suggested fix

When computing previous_message_id on resume, skip entries where message.model == "<synthetic>" (or, equivalently, where message.id does not match ^msg_). Ideally also avoid persisting synthetic entries with a message.id field that overlaps the API ID namespace — give them a distinct shape so they cannot accidentally be sent as anchors on the next API call.

Related (different roots, similar shape)

  • #53260 (synthetic "No response requested." on auto-resume)
  • #58320 (synthetic PDF-validation literal persists across resume)
  • #55811 (closed — 429 mid-send leaves branch unrecoverable)

View original on GitHub ↗

This issue has 5 comments on GitHub. Read the full discussion on GitHub ↗