Skip to content

desktop: recurring Session-not-found modal for session IDs that never existed (v2.0.32, Windows) #57026

Description

@1757010249

Summary

The desktop app periodically (observed every ~20 minutes, as two interleaved cadences ~2 minutes apart) issues GET /api/session/<id> for session IDs that never existed in the server's store, receives 404 SessionNotFoundError, and surfaces a modal error (请求失败 / Session not found: ses_…). Impact: recurring modal interruption while working; no data loss observed.

Environment

  • opencode version: 2.0.32 (desktop @opencodedesktop 2.0.32.0, staged CLI 2.0.32, npm global @opencode/[email protected])
  • OS: Windows 11, 10.0.26200.9711 (win32 x64)
  • Terminal: Desktop app (Electron) — not applicable
  • Shell: C:\WINDOWS\system32\cmd.exe (PowerShell 5.1.26100.9711)
  • Install/channel: latest; background service at http://127.0.0.1:45278
  • Active plugins: opencode-feishu-plugin 0.2.30 (from ~/.config/opencode/opencode.json), local TUI plugin plugins/tui-tps (from cli.json)

Reproduction

  1. Start the desktop app connected to the local background service, with at least one real session open (a TUI/CLI client was also connected on the same machine).
  2. Leave it running idle; no user interaction is required.
  3. Within ~20 minutes a modal appears: 请求失败 / Session not found: ses_<id>; it recurs on a ~20-minute cadence. Two interleaved series were observed, e.g. at :49:53 / :09:53 and :51:52 / :11:52 (server log clock).
  4. The same second, the server log shows (verbatim):
timestamp=2026-10-19T08:09:53.912Z level=INFO run=80c266f5 message="Sent HTTP response" http.span=2 role=server http.method=GET http.url=/api/session/ses_eacca402fffeN7u3VJ0c4Amhrr http.status=404
timestamp=2026-10-19T08:09:53.914Z level=INFO run=80c266f5 message="Sent HTTP response" http.span=1 role=server http.method=GET http.url=/api/session/ses_eacca402fffeN7u3VJ0c4Amhrr http.status=404
timestamp=2026-10-19T08:09:53.916Z level=INFO run=80c266f5 message="Sent HTTP response" http.span=1 role=server http.method=GET http.url=/api/session/ses_eacca402fffeN7u3VJ0c4Amhrr http.status=404
timestamp=2026-10-19T08:09:54.364Z level=INFO run=80c266f5 message="Sent HTTP response" http.span=1 role=server http.method=GET http.url=/api/session/ses_eacca402fffeN7u3VJ0c4Amhrr http.status=404
timestamp=2026-10-19T08:11:52.684Z level=INFO run=80c266f5 message="Sent HTTP response" http.span=3 role=server http.method=GET http.url=/api/session/ses_eacc87069ffeJISQHT2U1BD7oA http.status=404
timestamp=2026-10-19T08:11:52.695Z level=INFO run=80c266f5 message="Sent HTTP response" http.span=3 role=server http.method=GET http.url=/api/session/ses_eacc87069ffeJISQHT2U1BD7oA http.status=404

Response body (retrieved from the desktop renderer's Chromium HTTP cache):

{"_tag":"SessionNotFoundError","sessionID":"ses_eae1dfbb5ffe69zYeau2G1dAkc","message":"Session not found: ses_eae1dfbb5ffe69zYeau2G1dAkc"}

Expected Behavior

No modal error for internal/ephemeral session IDs. Either the request should not be issued, or a 404 for a session the UI itself did not create should be handled silently (e.g. dropped from the tab/list) instead of raising a modal dialog.

Actual Behavior

A modal error 请求失败 / Session not found: ses_… with a dismiss (×) button, recurring roughly every 20 minutes for the duration of the app session.

Additional Context

The requested IDs never existed server-side. I queried every table and text column of the live opencode.db (25 tables) plus the migration backup of the same DB — 0 rows for each offending ID. They appear only in session transcripts (self-reference from discussion of the bug). Session rows increased normally across the same period (240 → 245), so this is not lost data.

The list endpoint is healthy. GET /api/session?limit=5000&order=desc&parentID=null returns 191 sessions and each one fetches fine individually — so it is not a "list contains unreadable rows" problem.

The desktop renderer is the requester (at least for one series): the 404 responses are cached under %APPDATA%\ai.opencode.desktop\Cache\Cache_Data\data_1 for URLs http://127.0.0.1:45278/api/session/ses_….

The desktop app's own persisted UI state does not contain these IDs: drafts.sqlite (state.tabs, tabs.closed, tabs.recent, and per-session documents) and the Local Storage leveldb only reference real, existing sessions. So the IDs are not restored from local state.

Hypothesis (unconfirmed): a temporary/ephemeral session is created and then deleted quickly, and a UI component races the deletion. For example, the installed Feishu plugin implements:

const created = await doFetch(`${base}/api/session`, { method: "POST", ... })
const tempID  = extractSessionID(await created.json())
try   { await doFetch(`${base}/api/session/${tempID}/generate`, { method: "POST", ... }) }
finally { await doFetch(`${base}/api/session/${tempID}`, { method: "DELETE" }) }

Any client that reacts to the corresponding session.created event by fetching session details will hit 404 after the DELETE. I could not confirm which component creates the sessions in this case (the plugin's summary path ran only 7 times overall, mostly a day earlier, so it does not match the 20-minute cadence).

Workarounds tried, none helped: closing/restarting the desktop app; restarting the background service; clearing the app's HTTP cache; removing a stale draft tab from drafts.sqlite; disabling an unrelated plugin watchdog.

Frequency: consistent (~every 20 min) while the app is open.

Note: the machine clock jumped by ~5 hours during the investigation, so wall-clock times above should be read from the server log's UTC timestamps.

Activity

  1. opencode-agent commented on Oct 19, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report and logs. I reproduced what looks like the cause.

    The desktop app can't run in our sandbox, so I tested the client data layer it uses (createData in packages/client) against opencode serve 2.0.32:

    1. Connect the client to the server's event stream.
    2. Create a session with POST /api/session, then DELETE it right away. This is the same pattern your Feishu plugin uses for temporary sessions.
    3. The client receives the session.created event and fetches the new session with GET /api/session/<id>. By then the session is gone, so the request returns 404 Session not found: ses_…, and the error goes to the client's error handler. In the desktop app, that handler shows the "Request failed" toast you see. The server log has the same GET /api/session/ses_… 404 lines as yours.

    It still happens on the latest dev (ba06ff9). The script below reproduces it.

    So any session that is created and then deleted quickly can trigger the modal, whatever creates it. I couldn't tell which component creates sessions every ~20 minutes on your machine, but your guess about short-lived sessions matches what I saw.

    Related: #48012 (session-not-found errors are only detected in one shape) and #51940 (missing sessions are not handled when deleting).

    Reproduction script (repro-57026.ts)
    // Reproduction for #57026: the shared client data layer (used by the desktop app with an onError that
    // shows the "Request failed" toast) reacts to `session.created` by GET /api/session/<id>. If the session
    // is deleted right after creation (e.g. a plugin making a temporary session), that GET 404s and onError fires.
    // Usage: OC_URL=http://127.0.0.1:45278 OC_PASSWORD=... bun run repro-57026.ts   (from packages/client)
    import { createRoot } from "solid-js"
    import { OpenCode, type OpenCodeEvent } from "./src/promise/index.ts"
    import { createClientConnection, createData } from "./src/solid/index.ts"
    
    const url = process.env.OC_URL ?? "http://127.0.0.1:45278"
    const headers: Record<string, string> = process.env.OC_PASSWORD
      ? { authorization: "Basic " + Buffer.from(`opencode:${process.env.OC_PASSWORD}`).toString("base64") }
      : {}
    const api = OpenCode.make({ baseUrl: url, headers })
    const errors: unknown[] = []
    
    createRoot(() => {
      const handlers = new Map<string, Set<(e: any) => void>>()
      const all = new Set<(e: any) => void>()
      const connection = createClientConnection(api, {
        onEvent(event: OpenCodeEvent) {
          handlers.get(event.type)?.forEach((h) => h(event))
          all.forEach((h) => h({ name: event.type, details: event }))
        },
      })
      createData({
        api: () => api,
        directory: process.cwd(),
        connection,
        event: {
          on: (type: string, h: any) => (handlers.get(type) ?? handlers.set(type, new Set()).get(type)!).add(h) && (() => handlers.get(type)!.delete(h)),
          listen: (h: any) => (all.add(h), () => all.delete(h)),
        } as any,
        // Same hook the desktop app uses to show the "Request failed" toast.
        onError(error) {
          errors.push(error)
          console.log("onError (desktop would show 'Request failed' modal):", (error as any)?.message ?? error)
        },
      })
      ;(async () => {
        while (connection.status() !== "connected") await Bun.sleep(100)
        await Bun.sleep(500)
        // Temporary session: create, then delete straight away (like the plugin pattern in the report).
        const created = await fetch(`${url}/api/session`, {
          method: "POST",
          headers: { ...headers, "content-type": "application/json" },
          body: JSON.stringify({ location: { directory: process.cwd() } }),
        })
        const body: any = await created.json()
        const id = body.id ?? body.sessionID ?? body.data?.id
        console.log("created", created.status, id)
        const del = await fetch(`${url}/api/session/${id}`, { method: "DELETE", headers })
        console.log("deleted", del.status)
        await Bun.sleep(3000)
        console.log(errors.length ? `REPRODUCED: ${errors.length} onError call(s)` : "NOT reproduced: no onError")
        process.exit(errors.length ? 1 : 0)
      })()
    })

    Run it: bun repro-57026.ts

  2. zr988888 commented on Oct 30, 2026

    @zr988888

    Cross-platform datapoint + one extra trigger, on macOS desktop 2.2.1 with a shared background opencode serve.

    • Same symptom: GET /api/session/<id> → canonical {"_tag":"SessionNotFoundError",...} for ids that don't exist server-side, recurring as a "Session not found" notification. Not Windows-only.
    • Additional trigger beyond the short-lived create/delete race: after bulk-deleting hundreds of test sessions, renderer references (drafts / closed tabs / layout) kept pointing at deleted ids and produced a burst of 404s.
    • Once a missing id is referenced, the deterministic 404 is retried indefinitely: one id was fetched 94 times over 43 minutes with +1/+2/+4/+8/+16/+30s then a fixed 30s cadence. I filed that retry-policy piece separately as client: a permanent 404 SessionNotFoundError is retried indefinitely with exponential backoff (one id: 94 requests / 43 min) #60413 — a defence independent of where the phantom id originates.
    • Restarting only the desktop renderer (not the service) dropped the 404 rate to 0 within a minute, consistent with the loop living in client/renderer state.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions