[BUG] Session unrecoverable after upstream 429: every retry returns 400 previous_message_id

Status Closed — not planned
Reported on v2.1.81
Maintainer reply None cached
Activity 9 comments · opened May 15, 2026 · closed Jun 26, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

When the Anthropic API returns a transient server-side rate-limit (Server is
temporarily limiting requests (not your usage limit) · Rate limited
),
Claude Code's subsequent retry attempts fail permanently with:

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

Every user-initiated retry (continue, resume, free-text prompts, even
/something) returns the same 400. The session becomes unrecoverable; the
only escape is /exit and starting fresh, which loses in-session state.

What Should Happen?

the session should resume after API rate limiting expired

Error Messages/Logs

pass. Thanks for the standard + sweep verdict, made shipping #68 straightforward.
    ⎿  API Error: Server is temporarily limiting requests (not your usage limit) · Rate limited

  ✻ Crunched for 3s · 4 shells still running

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

  ✻ Churned for 0s · 4 shells still running

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

  ✻ Churned for 0s · 4 shells still running

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

  ✻ Crunched for 1s · 4 shells still running

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

  ✻ Worked for 0s · 4 shells still running

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

  ✻ Worked for 0s · 4 shells still running

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

  ✻ Sautéed for 0s · 4 shells still running

Steps to Reproduce

  1. Hold a long-running interactive session.
  2. Issue a prompt that the API throttles with the "Server is temporarily

limiting requests (not your usage limit)" 429 (we hit it under normal
foreground use — no abuse, no usage cap).

  1. Observe the assistant turn does not complete.
  2. Type anything to retry.

Claude Model

Opus

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

2.1.81

Platform

Anthropic API

Operating System

Ubuntu/Debian Linux

Terminal/Shell

Other

Additional Information

claude write-up

Title
Session unrecoverable after upstream 429: every retry returns 400 previous_message_id

Body
## Summary

When the Anthropic API returns a transient server-side rate-limit (Server is
temporarily limiting requests (not your usage limit) · Rate limited
),
Claude Code's subsequent retry attempts fail permanently with:

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

Every user-initiated retry (continue, resume, free-text prompts, even
/something) returns the same 400. The session becomes unrecoverable; the
only escape is /exit and starting fresh, which loses in-session state.

## Reproduction

  1. Hold a long-running interactive session.
  2. Issue a prompt that the API throttles with the "Server is temporarily

limiting requests (not your usage limit)" 429 (we hit it under normal
foreground use — no abuse, no usage cap).

  1. Observe the assistant turn does not complete.
  2. Type anything to retry.

Expected: client retries with the last valid previous_message_id (or no
previous_message_id if the failed turn was never persisted server-side),
the next call succeeds once the throttle clears.

Actual: every subsequent request includes a previous_message_id the API
doesn't recognize and returns a 400. The state never self-heals, regardless
of how long the user waits.

## Hypothesis

The client appears to advance its previous_message_id pointer optimistically
based on the request it sent, not based on a successful response containing
a real msg_… id. When the request fails before producing an assistant
message id, the pointer is left referencing something the server never
issued, and all subsequent calls are rejected.

## Severity

Medium: the workaround (/exit, lose context) is reliable but expensive —
in our setup the session held substantial in-flight context and
inter-agent state. The bug turns a transient server-side blip into a
permanent loss of the session.

## Version

(fill in: output of claude --version and OS — Linux 6.17.0 / Ubuntu in
my case)

## Full transcript of the failure mode

[Paste the literal terminal output from the failed session — the
"continue / resume / what is wrong? / check out the history" sequence
that all returned the same 400.]

View original on GitHub ↗

9 Comments

github-actions[bot] · 3 months ago

Found 2 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/55811
  2. https://github.com/anthropics/claude-code/issues/26699

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

daveCode-dot · 3 months ago

The protocol-level shape of this is worth flagging — it's the same class of bug as keeping a foreign-key constraint pointed at a row that never committed.

When the API returns the transient Server is temporarily limiting requests 429, the assistant turn doesn't complete, so the server never emits an id: msg_… for that turn. The CLI's local session state, however, has already advanced to "expecting to attach the next user message to previous_message_id = <placeholder for the in-flight turn>". From that point on, every subsequent request carries a previous_message_id that the server cannot resolve to a real prior response, and the API correctly returns 400 with the must be the id from a prior /v1/messages response diagnostic.

The recovery path on the client is to rewind the conversation pointer to the last successfully completed assistant turn when an error short-circuits the current turn before a message_stop event lands. Concretely:

  1. Track last_successful_message_id separately from pending_previous_message_id.
  2. On stream/message error (any 4xx/5xx that aborts before message_stop), discard pending_previous_message_id and reset to last_successful_message_id.
  3. The user's retry then attaches to the last completed turn, which is exactly what the user expects when they type continue.

Two extra wrinkles that the fix should cover:

  • 429 with Retry-After and 429 from sustained-usage limits (CHANGELOG entries 819 / 891 / 951 / 997 / 1383) — both should follow the same rewind path. Today the symptom is the same regardless of which 429 fired.
  • Tool-use turns: if the assistant emitted tool calls in the failed turn but those calls had not yet been streamed back to the client, the rewind has to also discard any local "pending tool_result" the user might have queued, otherwise the next retry sends a tool_result referencing a tool_use_id the server doesn't recognize.

The reason /exit followed by claude --resume works is exactly because it bootstraps previous_message_id from disk (where only completed turns were persisted), bypassing the in-memory stale pointer.

Happy to dig into the repro further if it helps — I can reproduce the 400 chain by force-cancelling a messages.stream mid-stream against the API directly, which mirrors the 429 path without needing to wait for a real rate-limit.

atlantsecurity · 3 months ago

I am having the same issue.

delthas · 3 months ago

/rewind to the last message that did not fail fixed it for me.

caldayham · 3 months ago

Hit this on macOS with Claude Code 2.1.133. Triggered by a 529 Overloaded
mid-turn (not a 429, but same failure mode — server-side error before the
API issued a msg_... id). Every subsequent send returned the 400, including
fresh prompts and /resume.

Recovered by truncating the JSONL tail. The session file at
~/.claude/projects/<slug>/<uuid>.jsonl had 29 trailing entries after the
last real "id":"msg_..." line — a mix of two synthetic assistant entries
(model: "<synthetic>", UUID-style id) plus queue-operation, last-prompt,
ai-title, and file-history-snapshot records. Cutting back to the last
msg_... line let /resume latch onto a valid previous_message_id and the
session resumed cleanly with full context.

So this reproduces from 529 as well as 429 — looks like any upstream error
that kills the turn before an assistant msg_ id is issued leaves the
pointer dangling.

PierreFouquet · 3 months ago

This happened to me via the web - I ended up having to resume/teleport the chat into Claude CLI and use /rewind like another user suggested.

RickyPOnline · 3 months ago

GitHub Comment Draft — for posting on #58427 and #59520

Post-ready. Co-authored with Marina (Wave VPS-side). Lightly edited from her draft. Cross-link both issues when posting.

---

+1 hit this in production · session became unrecoverable after a 529 cascade fired during an autonomous loop wakeup. After surgical recovery, sharing one finding worth knowing for whoever ships the upstream fix.

The naive truncation rule is wrong

Several existing community recovery scripts suggest "cut at the last msg_ entry." That matches mid-turn entries (thinking blocks, tool_use calls, multi-part responses) whose stop_reason is "tool_use", not a turn-ending value. Cutting there leaves the session resumed mid-task — which itself causes weird state on --resume.

The correct rule

Cut at the last assistant entry where all three hold:

  • type === "assistant"
  • id starts with msg_
  • stop_reason is one of "end_turn" or "stop_sequence"

Empirical evidence

In one corrupted 8256-line session, the naive rule suggested line 8229 (an api_error log entry) or line 8230 (the first synthetic placeholder). Both cuts would have restarted the session mid-turn. The correct rule identified line 8211 — the last clean end_turn boundary — which produced a perfectly clean restart.

One-liner (grep-style)

grep -nE '"stop_reason":"(end_turn|stop_sequence)"' "$SESSION" \
  | grep '"id":"msg_' \
  | tail -1 \
  | cut -d: -f1

Stricter awk (requires both predicates on the same line, JSON-key-order-agnostic)

awk '/"stop_reason":"end_turn"|"stop_reason":"stop_sequence"/ {
  if (match($0, /"id":"msg_/)) print NR
}' "$SESSION" | tail -1

Sibling note: detector should gate on more than 529

The same cascade pattern surfaces from 429, 503, 504, and 529 — not just 529. A detector worth its salt should also flag any 400 response whose body carries diagnostics.previous_message_id, since that's the smoking gun that the bug has already fired and the session needs cut-point recovery.

Detection regexes that work in production:

# Synthetic-entry signature in JSONL (the corruption)
SYNTHETIC_REGEX='"message":\{[^}]{0,200}"id":"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"'
SYNTHETIC_REGEX_REV='"model":"<synthetic>"'

# Upstream errors that trigger the bug
UPSTREAM_ERROR_REGEX='"status":(429|503|504|529)|"http_status":(429|503|504|529)|"statusCode":(429|503|504|529)'

# Smoking-gun 400 (bug has fired). 2-step match to avoid false-positives on
# documentation that contains the phrase "previous_message_id" in body text:
# require the phrase AND an error-context marker on the same line.
PREV_MSG_ID_BUG_LINE_PATTERN='previous_message_id'
PREV_MSG_ID_BUG_CONTEXT_PATTERN='"type":"error"|"isApiErrorMessage":true|"error":\{[^}]*"status":4[0-9]{2}'

Portable recovery skill

We've packaged the full procedure as a Claude Code skill that works cross-platform (bash for Linux/Mac, PowerShell for Windows) and supports both local and SSH-remote broken sessions. It's available here: https://github.com/RickyPOnline/claude-code-recover-session

The skill includes:

  • Auto-discovery of the broken session file
  • Triple-backup before any destructive op
  • Marina's verbatim find_last_clean_line() algorithm
  • 2-step regex with error-context anchoring (zero false positives on documentation)
  • Process-kill with $$-self-exclusion safety
  • Tmux re-attach handling
  • Three standalone variants (skill, single-file paste, bash script, PowerShell script)

Closing

Filing in case it helps the upstream patch land correctly. Happy to provide more detail or test patches — we've reproduced the bug deterministically and have smoke-test fixtures available.

— Co-authored by Marina (Wave VPS-side, autonomous detector daemon) and Ccode (laptop-side Claude Code recovery skill), via Ricky.

github-actions[bot] · 2 months ago

Closing for now — inactive for too long. Please open a new issue if this is still relevant.

JM-77 · 18 days ago

我遇到這問題時,直接ESC两次,然后選擇到没出問題前的記録,LUCK~