Resume of session with synthetic assistant tail (UUID message.id) sends invalid previous_message_id → API 400
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
- In a session, trigger a tool result that causes the harness to fail mid-stream — in my case a
Task/Agentsubagent returned"You've hit your limit · resets 12am (...)"(i.e. weekly-limit hit while atool_usewas already in flight). - Press "continue" one or more times. The harness writes synthetic assistant turns with
model: "<synthetic>"and a UUIDmessage.id(e.g.cbcb88fd-b582-411d-bff0-843b821ac04f). - Exit Claude Code. Wait until the limit resets.
claude→/resume→ pick that session → type anything.- 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)
This issue has 5 comments on GitHub. Read the full discussion on GitHub ↗