Skip to content

tui: false "Session location unavailable" prompt after network reconnect #55250

Description

@Mattheinrichs

Summary

When the TUI reconnects after a brief network interruption, it can replace the composer
with the "Session location unavailable — Choose another directory to continue this session"
recovery prompt even though the session's directory is valid and unchanged. Selecting the
same directory resumes the session. No restart or upgrade is involved; any single failed
request in the post-reconnect location sync is enough to surface the false prompt.

Environment

  • opencode version: 2.0.27 (client TUI and server)
  • OS: Ubuntu 26.04.1 LTS, x86_64 (kernel 7.0.0-38-generic)
  • Terminal: Unavailable: remote TUI over SSH; terminal app not discoverable from server
  • Shell: bash
  • Install/channel: latest
  • Active plugins: superpowers@git+https://github.com/obra/superpowers.git
  • Setup: opencode serve in Docker behind a reverse proxy + forward auth; client runs
    opencode attach https://<host> --dir <workdir> on the same host. Four remote MCP
    servers configured.

Reproduction

  1. Attach a TUI to a remote server and open a session in an existing directory.
  2. Briefly interrupt network/proxy connectivity so the event stream drops and reconnects.
  3. On reconnect the client re-runs the full location sync; if any one request fails
    transiently, the composer is replaced by "Session location unavailable — Choose
    another directory to continue this session."
  4. Choose directory → select the current directory → session resumes normally.

Intermittent: depends on whether all requests in the post-reconnect burst succeed.

Expected Behavior

Reconnecting should not report a valid, unchanged directory as unavailable. Transient
sync failures should retry or stay non-blocking; the recovery prompt should only appear
when the location is actually missing or inaccessible.

Actual Behavior

Full-screen recovery prompt for a valid directory; the only offered action moves the
session to the same directory.

Additional Context

Root cause trace (v2.0.27; verified identical on dev on 2026-10-13):

  • Every reconnect re-runs the location sync:
    packages/tui/src/context/location.tsx:48 —
    onCleanup(client.event.on("server.connected", () => sync(ref()))).
  • While disconnected, packages/client/src/solid/data.ts invalidates all cached syncs
    (createEffect → sync.invalidate() at the end of createData), so the reconnect
    re-runs the full fan-out.
  • location.sync() (packages/client/src/solid/data.ts) awaits location.get, then
    Promise.all over 12 location-scoped loads (vcs, agent, command, integration, mcp
    servers, mcp resources, model, provider, reference, skill, shells, global forms).
    Any single rejection rejects the whole sync.
  • LocationProvider stores that rejection as error; the session route matches
    currentLocation.error?.location.directory === session()!.location.directory and
    renders SessionLocationMissing (packages/tui/src/routes/session/index.tsx:1519-1531).
  • A transient data-refresh failure after reconnect is therefore presented as "location
    unavailable", even though the directory exists (session.move to the same directory
    then works).

Related: #49352 (same false prompt, upgrade/version-skew trigger), #43415 (service
restart trigger, non-dismissible modal), #54943 (plugin model crash), #46322 (closed as
duplicate). Prior attempts to separate loading from sync failures: #45601, #46961 (closed).

Fix direction: treat location sync as degraded rather than unavailable on transient
failure (retry/backoff), and only escalate when location.get itself identifies a
missing/inaccessible directory.

Frequency: intermittent, several times over recent weeks; always recoverable by
choosing the same directory.

Activity

  1. opencode-agent commented on Oct 13, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed trace. I could reproduce this on 2.0.27, and it still happens on the latest dev (46e4695).

    Setup: a simulated model, plus a small HTTP reverse proxy between the TUI and opencode serve.

    1. Open a session and get a reply.
    2. Drop all connections for about 3 seconds so the event stream disconnects, then restore the network.
    3. While the TUI re-runs its location sync, the proxy answers 502 to GET /api/agent for about a second. Every other request goes through normally.
    4. The composer is replaced by Session location unavailable · Choose another directory to continue this session, even though the directory is unchanged. A "Data migration failed · UnexpectedStatus: 500" toast also appears.

    Failing only the first such request wasn't always enough. A second location sync sometimes started right after and succeeded, so the failure has to land on the sync that ends up current. That fits the intermittent behaviour you described. The recording and screenshot below show the run from start to the prompt, and the script below reproduces it.

    Related, ordered by likelihood:

    recording.mp4

    Screenshot

    Reproduction script (drive.ts)
    // Repro for anomalyco/opencode#55250: after a brief network outage, the TUI
    // reconnects and re-runs the location sync. If a single request in that burst
    // fails transiently (here: one 502 from a reverse proxy on GET /api/agent), the
    // composer is replaced by "Session location unavailable" although the
    // directory is valid and unchanged.
    //
    // Setup: TUI -> small HTTP "reverse proxy" (wrapper below) -> drive's chaos
    // TCP proxy -> opencode server. The wrapper replaces the TUI's --server URL.
    import { Effect } from "effect"
    import { Llm, OpenCodeDriver } from "opencode-drive"
    import { writeFileSync, chmodSync } from "node:fs"
    
    const wrapper = `#!/usr/bin/env bun
    const args = process.argv.slice(2)
    const i = args.indexOf("--server")
    if (i >= 0) {
      const upstream = args[i + 1].replace(/\\/$/, "")
      let streams = 0
      let lastStream = 0
      const server = Bun.serve({
        port: 0,
        idleTimeout: 0,
        async fetch(req) {
          const url = new URL(req.url)
          if (url.pathname === "/api/event") { streams++; lastStream = Date.now() }
          // A short upstream hiccup right after the event stream reconnects:
          // GET /api/agent answers 502 for ~1s; every other request is proxied.
          if (streams >= 2 && Date.now() - lastStream < 1000 && url.pathname === "/api/agent") {
            return new Response("Bad Gateway", { status: 502 })
          }
          const headers = new Headers(req.headers)
          headers.delete("host")
          return fetch(upstream + url.pathname + url.search, {
            method: req.method,
            headers,
            body: req.method === "GET" || req.method === "HEAD" ? undefined : await req.arrayBuffer(),
            decompress: false,
          })
        },
      })
      args[i + 1] = "http://127.0.0.1:" + server.port
    }
    const real = (process.env.REAL_OPENCODE ?? "opencode").split("\\u0001")
    const child = Bun.spawn([...real, ...args], { stdio: ["inherit", "inherit", "inherit"] })
    process.exit(await child.exited)
    `
    const wrapperPath = "/tmp/opencode/oc-55250-wrapper.ts"
    writeFileSync(wrapperPath, wrapper)
    chmodSync(wrapperPath, 0o755)
    
    const dev = process.env.OC_DEV
    const real = dev
      ? [
          process.execPath,
          "--conditions=browser",
          `--preload=${dev}/packages/tui/node_modules/@opentui/solid/scripts/preload.js`,
          `${dev}/packages/cli/src/index.ts`,
        ]
      : ["opencode"]
    
    export default OpenCodeDriver.use(
      {
        opencode: {
          command: [process.execPath, wrapperPath],
          env: { REAL_OPENCODE: real.join("\u0001") },
        },
        network: true,
        project: { git: true, files: { "README.md": "hello\n" } },
        tui: { recording: true },
      },
      ({ ui, llm, network }) =>
        Effect.gen(function* () {
          yield* llm.queue(Llm.text("Hello there."))
          yield* ui.submit("Say hello")
          yield* ui.waitFor("Hello there.")
          yield* ui.screenshot("01-session-ok")
    
          // Brief network outage: drop connections, refuse new ones for 3s.
          yield* network.set({ refuseNew: true })
          yield* network.killConnections()
          yield* Effect.sleep("3 seconds")
          yield* network.clear()
    
          // On reconnect, the location sync runs again; one GET /api/agent gets a 502.
          yield* ui.waitFor("Session location unavailable", { timeout: 15_000 })
          yield* Effect.sleep("1500 millis")
          yield* ui.screenshot("02-location-unavailable")
        }),
    )

    Run it with opencode-drive: opencode-drive run ./drive.ts. It drives opencode from your PATH; set OC_DEV to the path of an OpenCode checkout to run that from source instead.

  2. Finn763 commented on Oct 13, 2026

    @Finn763
    Contributor

    Taking this one. The fix belongs in the post-reconnect location sync: a single transient failure shouldn't surface the recovery prompt -- I'll add bounded retry/verification to that path and cover it with a test.

  3. Finn763 commented on Oct 13, 2026

    @Finn763
    Contributor

    PR is up: #55258 — the post-reconnect sync now retries a bounded number of times before surfacing the recovery prompt, so one transient 5xx no longer tears down the session view. The sync logic also moves into a createLocation creator (same pattern as context/session-retention) so this is pinned by tests; the transient-failure case is red on the base.

  4. robertn702 commented on Oct 14, 2026

    @robertn702

    Observed the same false prompt on 2.0.28 on Linux, in an Orca-managed Git worktree connected to the local shared OpenCode service. It appeared while returning from a review subagent and interrupted the workflow.

    The directory and .git were readable, the TUI process cwd matched the worktree, and the stored session mappings pointed to the correct directory. The shared server did not restart at the time of the banner. The session later recovered in the original directory without choosing another location and completed its work.

    The investigation found the same broad location-sync error handling described above. We did not capture the rejected subrequest, so this observation does not establish a network interruption as the trigger. Another invocation installed 2.0.29 during this period, but both the existing TUI and shared server remained on 2.0.28. We cannot attribute the failure to that upgrade either.

  5. yentsun commented on Oct 16, 2026

    @yentsun

    Same false prompt on Windows, in a case where no interruption was involved.

    • opencode 2.0.30 (opencode --version), CLI/TUI in Windows Terminal, Windows 10 x64 (10.0.19045.7807), local shared service at http://127.0.0.1:45278.
    • The directory in the prompt (F:\Projects\personal\<repo>) is a local fixed NTFS volume (F:, DriveType 3). I verified it exists and is readable while the prompt was up, so the server's report of a missing location did not match the disk.
    • The prompt appeared mid-session, with no network drop, proxy hiccup, or service restart that I noticed, so I cannot tie it to the reconnect path traced above.
    • ~/.local/share/opencode/log/opencode.log has no Location not found / LocationNotFoundError entries for that period, but that log also records no server.connected events, so its silence is inconclusive rather than evidence that the client failed the location sync on its own.

    What the occurrence did correlate with is CPU saturation: the machine was sitting at 100% CPU with a large number of Node.js processes competing for a few cores, and connectivity did not change. Hypothesis: a location-scoped request in the sync times out under CPU starvation, and because one rejection rejects the whole sync, it surfaces as the same false prompt as the reconnect case.

    Selecting the current directory resumed the session, same as in @robertn702's report: valid directory, no captured failing subrequest. As of writing, #55258 is still open, so 2.0.30 predates the bounded-retry change.

    If the intended fix is tolerating transient failures in the post-reconnect sync, it is worth knowing that the prompt can also surface with no reconnect at all, so a local resource stall (CPU saturation here) may be enough to trigger it.

  6. WilliamAGH commented on Oct 19, 2026

    @WilliamAGH

    Same false prompt on 2.0.32 (macOS, local shared service) after a service restart, with the failing request pinned down.

    After the restart, six attached TUIs (each in a different directory) re-ran their location sync at once. GET /api/model waited on plugin activation for 5 s, then returned 503 Location plugins did not finish activating. Every other request in the sync succeeded:

    http.span=5087 http.method=GET http.url=/api/model http.status=503
    

    Activation runs once per location: 0.9–3.2 s each with opencode models --standalone, so six together pass 5 s on a busy host.

    For #55258: a 503 from the plugin gate means "not ready yet", so waiting for activation (or backing off) seems safer than a fixed 3 × 500 ms. Ideally the location prompt would show only on a real 404 LocationNotFoundError.

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