Repository navigation
Description
Description
When the desktop client holds a reference to a session the server does not know, it issues GET /api/session/<id>, receives a standard, correctly-tagged 404 SessionNotFoundError, and keeps retrying that permanent error forever with exponential backoff capped at 30 s. The retries surface a recurring "Session not found" toast/modal. A restart of the desktop renderer stops it immediately, without touching the background opencode serve.
This is distinct from the existing reports (see duplicate/related check below): regardless of why the id does not exist (a short-lived create/delete race, a stale snapshot resurrection, or a survivor UI reference), a 404 for a session is a terminal condition and should not be retried on a loop. There should be a defence in the retry/error layer that is decoupled from how the phantom id was produced.
Observed behaviour (measured from the local server access log):
-
One specific id
ses_e78950f37ffe…was requested 94 times over 43 minutes (11:28:54 → 12:12:28) after it was already returning 404. -
Retry cadence is exponential backoff
+1s, +2s, +4s, +8s, +16s, +30sand then stays at one request every 30 s indefinitely. -
Across the day there were 103
GET /api/session/<missing-id>→ 404 responses, 42 of them from a single id. -
The error body is the canonical V2 tagged shape, i.e. it is recognised as session-not-found rather than a generic error:
{"tag":"SessionNotFoundError","sessionID":"ses…","message":"Session not found: ses_…"}
Workaround that worked: restarting only the desktop renderer stopped it — in the 60 s after restart the 404 rate fell from ~2/min to 0, while the background opencode serve kept running and all real sessions/history were preserved. This localises the loop to renderer/client state rather than the server.
Expected behavior:
A not-found for a session is a terminal condition:
- stop retrying that id (no unbounded backoff for a deterministic 404);
- drop the id from the tab/list/recent references that keep requesting it;
- do not raise a user-visible toast/modal for a session the client itself can prune.
Transient failures (network/5xx) can keep the existing retry policy; only the permanent SessionNotFoundError class should short-circuit.
Actual behavior:
The client retries the deterministic 404 forever (capped cadence 30 s) and keeps notifying the user with a "Session not found" toast/modal.
How the phantom ids arose in this case (context, partly inferred): the retry loop above is fully measured from logs; the origin of the ids is less certain and likely overlaps the existing create/delete-race and stale-snapshot issues. The desktop client appears to create sessions optimistically (a client-generated ses_ id followed by an async POST); a failed POST can leave an id the server never knew. A bulk delete of hundreds of test sessions also left some UI references (drafts / closed tabs / layout) pointing at deleted sessions. Most phantom ids are requested only 1–2 times; a single id entered the long loop — i.e. "producing a phantom id" is common, "retrying it forever" is the rarer, more damaging bug this report focuses on.
Suggested fix area (not implemented): in the client data/query layer, classify SessionNotFoundError (any shape the existing predicates already accept) as a terminal error for that session key — cancel the retry chain, prune references, and suppress the notification. Keep retrying only on non-terminal transport/5xx errors. A focused regression would assert that a 404 response to session.sync(id) produces no second GET and removes the id.
Related but distinct:
- desktop: recurring Session-not-found modal for session IDs that never existed (v2.0.32, Windows) #57026 recurring session-not-found modal for ids that never existed (create/delete race); reproduced — covers where the phantom id comes from.
- client: delayed metadata and family reads resurrect deleted sessions in the client cache #56736 delayed metadata/family reads resurrect a deleted session in the client cache; reproduced.
- Session-not-found errors are only detected in the exact V1 shape, other wrappers are missed #48012 isSessionNotFoundError only matching the exact V1 shape — covers whether the error shape is detected. Here the canonical tagged shape IS already detected and is still retried, so the predicate gap does not explain this case.
- tui: stale subagent sessions cause SessionNotFoundError unhandled rejection bursts #47516 / References to a deleted session outlive it: address bar, recent-tab pointer and persisted handoff #47683 / desktop: sessions missing from server cannot be deleted (DELETE 404 not handled) #51940 stale subagent rejections, survivor references, missing-session handling on delete.
This report is specifically about unbounded retry of a permanent 404 after it has already been correctly recognised.
Plugins
Local third-party plugins are present, but the retry loop is client-side renderer behaviour (it stops on renderer restart while the service and plugins keep running), so it is not attributable to a plugin.
OpenCode version
2.2.1
Steps to reproduce
- Use the macOS desktop app connected to the shared local background
opencode serve. - Leave the client holding a reference to a session id that does not exist server-side (this happens naturally after an optimistic create whose POST fails, or after bulk-deleting sessions while tabs/drafts/layout still reference them).
- Observe
GET /api/session/<id>returning the canonical 404 SessionNotFoundError in the server log. - The client keeps retrying that deterministic 404 with +1/+2/+4/+8/+16/+30 s backoff, then every 30 s indefinitely (one id observed: 94 requests over 43 minutes), repeatedly raising a "Session not found" notification.
Screenshot
No response
Operating System
macOS (darwin, arm64)
Terminal
Desktop app (Electron), connected to a shared local background opencode serve
Activity
zr988888 commentedon Oct 30, 2026
Small formatting correction to my report: the error body is literally
{"_tag":"SessionNotFoundError","sessionID":"ses_…","message":"Session not found: ses_…"}(i.e.
_tag, not*tag— the underscore was eaten by Markdown outside a code fence). No change to the claim: it is the canonical V2 tagged shape that is already recognised as SessionNotFoundError and still retried indefinitely.opencodejr commentedon Oct 30, 2026
Thanks for the detailed logs. I tried to reproduce this on 2.2.1 but couldn't get the retry loop.
What I tried:
GET /api/session/<id>with the same 404{"_tag":"SessionNotFoundError",…}body.session.sync(id)failed once and was never retried.opencode pair(it serves the same UI the desktop app uses).I couldn't run the macOS desktop app itself here. Any of these would help narrow it down:
GET /api/session/<id>lines, were there any other requests for the same id (e.g./message,/inbox,/permission,/form,/view) at the same times? That would show which part of the app is asking.Related issues, most likely first:
opencodejr commentedon Nov 2, 2026
I'm closing this for now because we haven't heard back. On 2.2.1 I couldn't make a missing session get requested over and over: the client fetched it once or twice, got the 404 and stopped, and the tab showed the "This session cannot be found" page instead of a repeating toast. I needed more detail from your setup to go further.
If it happens again, please reply here and we can reopen it. These would help most:
/message,/inbox,/permission,/form,/view) around the loopingGET /api/session/<id>lines.These issues are closely related and may be worth following: #57026, #60396 and #56736.