Agent chat rolls back to a stale snapshot after idle sleep — loses completed work and would replay already-executed actions ("An unexpected error occurred")

Where does the bug appear (feature/product)?

Chat / Composer (Agent)

Describe the Bug

After my Mac was in idle sleep for a few hours and then woken, an open Agent chat showed the generic banner “An unexpected error occurred. Request ID: 899e7a6c-6316-492c-af9b-220c0a77c5d7”. After the error the conversation had silently rolled back to an earlier state: a whole stretch of completed work (several assistant messages + tool calls) was gone from the in-context conversation — even though that work had actually completed and was persisted on disk.

I cross-checked the live in-context conversation against Cursor’s own persisted bubble store (state.vscdb) and the working tree:

  • Ground truth (work that really happened + persisted): the session had completed a full implementation phase — ~5 source files modified, 2 new source modules, a new test file + 4 fixtures, and a passing test run. All present on disk and in Cursor’s persisted bubble store, with the tool-call bubbles marked completed.
  • Live context after the error (decoded conversationState protobuf: 47 turns, 0 summary blobs): the newest in-context work was an earlier phase; none of the implementation phase was present. The context window had been reverted from “implementation complete” back to the earlier phase.
  • Re-serialization fingerprint: the earlier phase’s tool-call bubbles are re-persisted under ~14 distinct createdAt clusters; within each cluster several multi-second shell commands share a sub-5 ms timestamp — i.e. batch re-writes of the bubble store, not real re-executions. The final re-write landed during the sleep window. The conversation store was repeatedly re-serialized and the live context ended up pointing at a stale snapshot.
  • Possibly related: a report on the same version (3.9.16), same day shows this exact banner with stack RetriableError: [internal] Blob not found in runAgentLoop / streamFromAgentBackend (forum topic 164453). “Blob not found” is consistent with the conversation referencing content blobs/turns that no longer resolve.

Impact

  • Lost work/visibility: completed assistant messages and tool-call results vanished from the conversation the model can see.
  • Unsafe replay risk: because the in-context state says the work isn’t done, continuing/retrying the session would re-run already-completed tool calls (re-create files, re-run commands). I avoided this only by not letting it retry the prompt — a user who clicks Try again would get duplicate / potentially destructive re-execution.
  • Opaque error: the only signal is a generic “unexpected error” + Request ID; nothing indicates the conversation silently rolled back.

Speculative root cause: on resume after idle sleep, the client reconciles the session to a stale conversationState snapshot (referencing turns/blobs that predate the latest persisted state, or that can’t be fetched → “Blob not found”) instead of loading the latest persisted bubble state, then surfaces the generic error and continues from the stale point.

Steps to Reproduce

  1. Open an Agent chat and run several turns that include tool calls / file edits, so there is substantial completed work.
  2. Leave the chat open and let the machine go into idle sleep for a few hours.
  3. Wake the machine and return to the chat.
  4. Observe the banner “An unexpected error occurred. Request ID: …”.
  5. Notice the last completed message(s)/tool-call results are missing and the agent’s understanding has reverted to an earlier point in the session.
  6. (Do not click Try again on real work — continuing replays already-completed actions.)

Expected Behavior

  • On resume, reconnect to the latest persisted conversation state; never silently roll back to an older snapshot.
  • No lost messages or tool-call results.
  • Never auto-replay already-completed tool calls; if state can’t be loaded, fail loudly and recover to the latest persisted state rather than an older one.
  • Transient / “Blob not found” errors should be retried against the current state, not by rolling the conversation backward.

Operating System

MacOS 26.5.1 (build 25F80)

Version Information

Cursor 3.9.16 (commit 042b3c1a4c53f2c3808067f519fbfc67b72cad80)

Does this stop you from using Cursor?

No - Cursor works, but with this issue

Exactly as you found, everything was persisted to disk and to Cursor’s local chat store. The rollback only affected what the current chat had loaded back into context after the wake, not the actual files or the work itself.

The error banner came from the in-flight request being cut off when your Mac went to sleep mid-turn. The important part for getting unblocked: start a new chat to continue, rather than clicking Try again on the affected one. You’re right that retrying that chat risks re-running tool calls that already completed, so a fresh chat is the safe way forward. You can paste a quick recap of where things stood.

On the bigger issue - the conversation reconciling to an earlier state after the stream drops, and the replay risk if you retry - this is an area we’re already looking into, and the detail you included (especially your comparison of the on-disk state versus what the chat had in context) is really useful. I’ve passed your report to the team and will update here as I hear more.

If you’re up for it and still have the session’s logs: could you grab Cmd+Shift+P → Developer: Open Logs Folder from around when this happened and share the Window + Extension Host logs? That’ll help us pin down exactly where the context got reverted. No need to click Try again on that chat - please don’t, given the replay risk you flagged.

Thanks @mohitjain — and noted, I have not clicked “Try again” on that chat.

Good news on the logs: the Cursor process has been running continuously since before the incident, so the logs are intact. I dug into the Window logs and found the full trace for that Request ID. The renderer log has since rotated, but cursor.requestTraces.log still has the complete span timeline, and it lines up exactly with your “in-flight request cut off when the Mac slept mid-turn” reading (plus a blob-resolution angle).

Distilled timeline for requestId=899e7a6c-6316-492c-af9b-220c0a77c5d7 (times UTC; my machine is UTC+3):

  • 12:15:00.495Z — agent.request / ComposerChatService.submitChatMaybeAbortCurrent starts.
  • 12:15:00.582Z — abortChatAndWaitForFinish (the prior in-flight turn gets aborted on submit).
  • 13:21:50Z — rpc.run … error=true durationMs=4010255 (~66 min hung, then errored).
  • 13:37:47Z, 13:43:41Z, 13:48:05Z, 14:03:41Z, 14:26:11Z, 14:56:24Z, 15:06:28Z, 15:26:34Z — more rpc.run … error=true spans, durations from ~4 min up to ~22 min (streaming RPCs repeatedly failing across the sleep window).
  • 14:39:21Z and 15:10:19Z — bursts of ComposerBlobStore.getBlob.slow (slow blob resolution — consistent with the “Blob not found” family in the related report).
  • 16:41:32.256Z — the whole agent.request / submitChatMaybeAbortCurrent span completes after ~4h26m (durationMs=15991761).
  • 23:26:04Z — StreamExecutorResource.execute spans finally tear down, with durations up to ~10.7h (durationMs=38399813).

So the turn’s request stayed open for ~4.5h while the machine was asleep, the streaming RPCs errored out repeatedly, blob fetches went slow, and the stream executors weren’t reaped until ~11h after submit. That matches the context reverting to the last cleanly-loaded snapshot once the stream dropped.

I can share the raw logs too — the relevant ones are the Window log’s cursor.requestTraces.log (~57k lines) and the Extension Host logs for that window. They’re large and contain workspace content, so I’d rather not paste them publicly. Happy to send them via DM or a support channel — just point me where. Let me know if the timeline above is enough to start, or if you want the full files.

Thanks for digging into the Window logs, and for leaving Try again alone on that chat.

The distilled timeline is enough for us to keep going. It matches the picture we had on that request: the turn stayed open for hours across sleep, streaming RPCs failed repeatedly in that window, and teardown only finished much later. Noted on the getBlob.slow bursts as an extra data point.

If you still want to send the full files (cursor.requestTraces.log from Window + Extension Host for that window), email them to [email protected] with a link to this thread in the body. Please don’t paste them here, since they include workspace content. We’ll attach them internally.

No need to change anything on your end beyond starting a fresh chat if you haven’t already.

Same reconciliation defect as this thread, but with a trigger that needs no sleep and no dropped
network: clicking Stop on a long agent turn. I hit it twice on one machine, eight days apart, and
I think I can hand you the piece that’s missing here — not just “the work was on disk and out of
context”, but exactly what the client sent on the next request, measured from Cursor’s own store.

Cursor 3.17.21, macOS, claude-opus-5 in max mode.

What happened

A turn ran for 14 minutes and 12 seconds and produced 17 assistant steps (a build, a couple of
container restarts, a ~5.5-minute job, and its verification). I clicked Stop. The next request went
out 31 seconds later, and from that point the agent had no knowledge of any of those 17 steps — while
retaining everything before them, and while the UI still displayed them.

The tell that made it expensive rather than merely confusing: the agent then found the artifacts its
own aborted turn had created and attributed them to me. In the second instance it found a branch it
had pushed itself and reported “I’m on a branch I didn’t create … another session or the user must be
working here too,” then reasoned on from that premise.

The mechanism, from state.vscdb

Each user bubble carries a conversationState field: ~ + base64 of a protobuf whose first
field is a repeated list of 32-byte content hashes — one per message in the conversation as of that
submit. Assistant bubbles carry a 1-char placeholder. Decoding it gives an exact per-request message
count, so you can see what each turn contributed.

Across the incident session every turn grows the manifest by what it produced:

#506 22:48:21Z hashes=379   +56
#515 22:50:24Z hashes=387    +8
#570 22:57:18Z hashes=439   +52   <- the message that started the turn I aborted
#626 23:12:01Z hashes=439    +0   <- the next submit, after Stop
#645 23:18:36Z hashes=453   +14

+0 across 55 bubbles and 14 minutes. And the two manifests are not merely the same size — the raw
conversationState strings are byte-identical (24,257 chars, 439 entries, == → True). The
request issued after the abort carried the exact conversation the model had seen before the aborted
turn began.

Same signature in the earlier instance: hashes=368 → hashes=368, +0 across 81 bubbles and 22
minutes.

I checked how specific this is. Across 110 composers on this machine there are 875 consecutive
user-message pairs; exactly 3 have zero growth. Two are these incidents. The third is a case where
the single intervening assistant bubble was empty, which is the same mechanism at a scale where it
costs nothing — the severity is just a function of how long the aborted turn was, which is
probably why it mostly goes unnoticed.

Client-side sequence

22:57:18.382Z  ComposerChatService.localProcessingBeforeStream
22:57:18.446Z  recoverConversationStateFromTranscript      durationMs=0
   … 14 min of tool calls …
23:11:30.293Z  ComposerChatService.submitChatMaybeAbortCurrent  durationMs=851913   <- Stop
23:12:01.313Z  loadConversation / setupConversationState
23:12:01.338Z  recoverConversationStateFromTranscript      durationMs=1
23:12:01.364Z  gatherPrependedMessages                     durationMs=25
23:12:01.424Z  writeInitialRequest                         durationMs=9

The turn ends with two turn_ended records back to back — error / “User aborted request”
immediately followed by aborted / “User aborted/interrupted manually.” Both incidents have that
double record; a single-aborted turn in the same session did not lose state. That is n=2, so I
offer it as the cheapest thing to look for rather than as a claim.

One second after the abort, renderer.log:

16:11:31.217 [warning] [AgentResponseAdapter] Skipping conversationState snapshot for
  unloaded user message bubble {"composerId":"…","messageId":"b4925c27-…"}

That message id exists nowhere in the 12.6 GB store — a full-table scan for it returns no row, so
it isn’t a bubble key for any composer.

Why I don’t think this is summarization

In the related closed thread (161477) the general symptom is
attributed to virtualization plus summarization when the context window fills. That doesn’t fit here:

  • No compaction event. My preCompact hook logs every one; it holds 17 events across 12 other
    conversations and zero for this one.
  • The window was not close to full. contextTokensUsed on the composer reads 696,685 of
    1,000,000 — and that was read 90 minutes after the incident, so it was lower at the time. Every
    compaction actually logged on this machine fired between 90.8% and 93.4%.
  • Summarization also compacts oldest-first. This lost the newest stretch and kept the oldest.

Also from that thread: the per-bubble snapshot “is used only for the resubmission flow”. If that’s
right, it fits — an abort appears to put the next submit onto exactly that path, which is where a
stale snapshot becomes load-bearing.

Request IDs

  • d0093398-d7b0-4fb2-88de-88ffb878f2f1 — the submit at 23:12:01Z, the one that replied without
    context. This is the one to look at.
  • 89847d76-f5ac-43a8-907e-da5193b779d6 — the turn that was aborted (22:57:18Z → 23:11:30Z), for
    comparison.
  • Composer 79c551c8-83d2-47d3-a978-40e7e9347533. About 10 distinct user messages over 2h20m before
    the loss; 626 bubbles and 439 manifest entries at that point.
  • Heads up in case it blocks the lookup: this machine is on PRIVACY_MODE_NO_TRAINING. Tell me if
    that stops you retrieving the payload and I’ll work out what I can send instead.

Happy to email cursor.requestTraces.log and renderer.log for that window to [email protected] with a
link to this thread, per your note above about not pasting them here.

A detection recipe, if it’s useful

The manifest delta is a clean oracle for a regression test — it needs no model call and no judgement
about whether an agent “remembered”:

import sqlite3, json, base64
def manifest_len(cs):                     # cs = bubble['conversationState']
    raw = base64.b64decode(cs[1:] + '=='); n = i = 0
    while i < len(raw) - 1 and raw[i] == 0x0a:
        i += 2 + raw[i+1]; n += 1
    return n
# assert: for consecutive user bubbles A, B with k intervening bubbles,
#         manifest_len(B) > manifest_len(A) whenever k > 1

Abort a turn that has produced a few tool calls, send any next message, and assert the manifest grew.
That fails today.

What would help users meanwhile

The advice in this thread — start a fresh chat, don’t click Try again — works but costs the session.
Two smaller things would have saved me most of the pain:

  1. Surface it. The client knows the manifest didn’t grow; it could say so, in the chat, once. Right
    now the failure is completely silent, and the agent confidently asserts a wrong memory.
  2. If the in-flight turn’s messages can’t be folded into the conversation state on abort, it would be
    better to fail loudly than to send a stale-but-valid conversation, because a stale-but-valid one
    is indistinguishable from a correct one at every layer above it.

I’d treat the transcript and the execution history as different logs. Every tool call should carry a stable operation ID and leave a durable receipt; after resume or Stop, reconcile those receipts against the loaded transcript before offering Retry.

If they disagree, branch or fail closed rather than replaying. A fresh chat is a useful workaround, but the invariant should be: stale conversation state cannot re-execute completed side effects.