Skip to content

client: a permanent 404 SessionNotFoundError is retried indefinitely with exponential backoff (one id: 94 requests / 43 min) #60413

Closed as not planned

Description

@zr988888

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, +30s and 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:

  1. stop retrying that id (no unbounded backoff for a deterministic 404);
  2. drop the id from the tab/list/recent references that keep requesting it;
  3. 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:

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

  1. Use the macOS desktop app connected to the shared local background opencode serve.
  2. 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).
  3. Observe GET /api/session/<id> returning the canonical 404 SessionNotFoundError in the server log.
  4. 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

  1. zr988888 commented on Oct 30, 2026

    @zr988888
    Author

    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.

  2. opencodejr commented on Oct 30, 2026

    @opencodejr
    Contributor

    Thanks for the detailed logs. I tried to reproduce this on 2.2.1 but couldn't get the retry loop.

    What I tried:

    • Shared client data layer: a mock server answered GET /api/session/<id> with the same 404 {"_tag":"SessionNotFoundError",…} body. session.sync(id) failed once and was never retried.
    • The app UI on 2.2.1: I connected a browser to the background service with opencode pair (it serves the same UI the desktop app uses).
      • Opening a tab on a session id that never existed caused 2 GETs, both 404. After that there were no more requests for over a minute, and the tab showed the "This session cannot be found" page with a Close Tab button, not a repeating toast.
      • Opening a real session in a tab, deleting it on the server and reloading caused a few 404s, then nothing.
    • Code: I didn't find anything in the app or client code at 2.2.1 that retries a session read without a limit. The 1/2/4/8/16/30 s spacing you measured matches the default retry delay of the query library the app uses, but the app's own queries are limited to a few retries.

    I couldn't run the macOS desktop app itself here. Any of these would help narrow it down:

    1. Plugins: does the loop still happen with the local third-party plugins disabled? Desktop/GUI plugins run in the renderer, so a renderer restart would stop them too.
    2. The exact UI state: when the loop was running, which view showed the stale id (an open tab, the sidebar or session list, the inbox/titlebar, the command palette, a draft)? A screenshot of the toast or modal would also help.
    3. Other requests: in the server log near the looping 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:

  3. opencodejr commented on Nov 2, 2026

    @opencodejr
    Contributor

    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:

    1. Whether it still happens with your local third-party plugins disabled.
    2. Which view held the stale id (tab, sidebar or session list, inbox or titlebar, palette, draft), plus a screenshot of the toast or modal if you can.
    3. Any other server-log requests for the same id (/message, /inbox, /permission, /form, /view) around the looping GET /api/session/<id> lines.

    These issues are closely related and may be worth following: #57026, #60396 and #56736.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions