Skip to content

'--continue' flag causes 'Unexpected server error' toast at boot (sessionID placeholder is literal 'dummy') #34144

Open

Description

@eddy-lau

Version: 21 (bundled binary, macOS arm64)

Steps:

  1. Run opencode --continue (or -c)
  2. Toast appears: "Unexpected server error. Check server logs for details."

Log evidence (~/.local/share/opencode/log/opencode.log):

level=ERROR message=failed error="Error: Expected a string starting with \"ses\", got \"dummy\""

Root cause — TUI bundle sets a hardcoded placeholder "dummy" as the initial route sessionID when --continue is passed:

get initialRoute(){
  return U.args.continue
    ? {type:"session", sessionID:"dummy"}
    : void 0
}

Session-ID validation expects the ses_ prefix, so "dummy" throws before the subsequent useEffect hook navigates to the actual last session. Cosmetic — functionality still works — but the error toast is alarming to users.

Fix suggestion: pass undefined instead of "dummy", or skip initial-route validation when the session has not loaded yet.

Activity

  1. changed the title [-] flag causes toast at boot (sessionID placeholder is literal "dummy")[/-] [+]'--continue' flag causes 'Unexpected server error' toast at boot (sessionID placeholder is literal 'dummy')[/+] on Jun 27, 2026
  2. satwareAG-ironMike commented on Aug 17, 2026

    @satwareAG-ironMike

    Corroborating from an independent report (#43020, closed as dup of this): same Error: Expected a string starting with "ses", got "dummy" on 1.18.18, ~1 occurrence/day since ~Aug 6 2026. Context per log: the failing run immediately performs watcher-backend + project-copy-refresh work (background project indexing), concurrent real ses_* streams unaffected, stacks resolve only into bundled code (//root/chunk-*.js). Consistent with the hardcoded-placeholder-before-real-session-ID root cause.

  3. vilherda commented on Oct 6, 2026

    @vilherda

    Still reproducible on the V2 line: confirmed on 2.0.18 and 2.0.23 (Homebrew tap anomalyco/tap/opencode-v2, macOS arm64). The placeholder is still present in packages/tui/src/app.tsx on tags v2.0.18, v2.0.23, v2.0.24 and on dev:

    input.args.continue
      ? {
          type: "session",
          sessionID: "dummy",
        }
      : undefined

    Steps

    1. opencode -c in a project that already has sessions
    2. Error shown at boot: Expected a string starting with "ses" at ["sessionID"]
    3. After dismissing it, the last session loads and works normally

    Server log on 2.0.23: six requests fail in the same millisecond, right at boot, and none afterwards:

    GET /api/session/dummy             400
    GET /api/session/dummy/message     400
    GET /api/session/dummy/inbox       400
    GET /api/session/dummy/permission  400
    GET /api/session/dummy/form        400
    GET /api/session?parentID=dummy    400
    level=WARN message="schema rejection" kind=Params reason="Expected a string starting with \"ses\"\n  at [\"sessionID\"]"
    level=WARN message="schema rejection" kind=Query  reason="Expected a string starting with \"ses\"\n  at [\"parentID\"]"
    

    On 2.0.18, GET /api/experimental/session/dummy/terminal was also requested.

    Observations

    • Some TUI code paths already guard against the placeholder (sessionID !== "dummy" checks around session tabs and the view/idle tracking), but the session view data loaders (messages, inbox, permissions, forms, child sessions) do not, so they hit the API before the real session ID is resolved.
    • opencode -s <ses_id> opens the same session without the error, so the issue is specific to the --continue initial route.
    • Workaround for users: opencode session list, then opencode -s <ses_id>.

    Possible fixes: use undefined / a dedicated "resolving" route state instead of a fake session ID, or skip session data loading while the route still holds the placeholder.

  4. bornmw commented on Oct 10, 2026

    @bornmw

    Reproduced on v0.0.0-pr-54107-202610102051 (branch pr-54107, 2026-10-10, Linux x64) with opencode -c.

    Client: opencode -c in a directory with existing sessions.

    Server log (the rejected placeholder fetches — one per session data endpoint):

    level=WARN message="schema rejection" kind=Params reason="Expected a string starting with \"ses\"\n  at [\"sessionID\"]"
    level=INFO http.method=GET http.url=/api/session/dummy http.status=400
    level=INFO http.method=GET http.url=/api/session/dummy/inbox http.status=400
    level=INFO http.method=GET http.url=/api/session/dummy/message http.status=400
    level=INFO http.method=GET http.url=/api/session/dummy/permission http.status=400
    level=INFO http.method=GET http.url=/api/session/dummy/form http.status=400
    level=INFO http.method=GET http.url=/api/session?parentID=dummy http.status=400
    

    Direct repro against the live server (same error the user sees):

    $ curl -s -u opencode:<password> http://127.0.0.1:<port>/api/session/dummy
    {"_tag":"InvalidRequestError","message":"Expected a string starting with \"ses\"\n  at [\"sessionID\"]","kind":"Params"}
    

    The placeholder is still sessionID: "dummy" in packages/tui/src/app.tsx (input.args.continue ? { type: "session", sessionID: "dummy" } : undefined), and the session route mounts against it before the continue effect resolves the real session, so every data endpoint gets a 400 on boot.

    I have a local fix in progress: it keeps the placeholder route but skips all session data syncs (session get/messages/inbox/permission/form + child-list parentID) while sessionID === "dummy", so no request is made until the real session ID is known. Preparing a PR; will link here.

  5. DEAD1nsane commented on Oct 22, 2026

    @DEAD1nsane

    Still present in 2.1.3 (Termux musl build). Tight correlation: 2.1.3 installed 2026-10-22 08:30, and the first schema rejection in the server log lands at 08:37 boot with args ["-c"] — zero occurrences in the entire prior log under 2.1.2.

    Server side, v2.1.3 background service:

    WARN  "schema rejection" kind=Params reason="Expected a string starting with \"ses\"\n  at [\"sessionID\"]"
    INFO  "Sent HTTP response" http.url=/api/session/dummy http.status=400
    INFO  "Sent HTTP response" http.url=/api/session/dummy/inbox http.status=400
    INFO  "Sent HTTP response" http.url=/api/session/dummy/message http.status=400
    INFO  "Sent HTTP response" http.url=/api/experimental/session/dummy/terminal http.status=400
    INFO  "Sent HTTP response" http.url=/api/session?parentID=dummy http.status=400
    INFO  "Sent HTTP response" http.url=/api/session/dummy/permission http.status=400
    INFO  "Sent HTTP response" http.url=/api/session/dummy/form http.status=400
    

    Surface error in the TUI: Expected a string starting with "ses" x at ["sessionID"]/. After the burst the run never issues a single real-session request, so the last session is not resumed (TUI falls back to a fresh session).

    Workaround that does work: opencode -s ses_<id> (explicit --session) boots clean — no dummy requests, no rejections.

  6. opencodejr commented on Oct 29, 2026

    @opencodejr
    Contributor

    Thanks for the detailed report and root-cause analysis. I could reproduce this on 2.2.0.

    Steps:

    1. Create a session in a project.
    2. Start opencode --continue while the TUI's "latest session" lookup is slow. The script below simulates this with a proxy that delays that one request by 3s.

    What happens: the TUI first opens a placeholder session dummy and requests it (/api/session/dummy, /message, /permission, /form, …). The server rejects these, so a toast appears (Expected a string starting with "ses" at ["sessionID"]) and the TUI briefly drops to the home screen. A moment later it opens the real last session, as you described. The recording and screenshot below show it.

    Latest dev (f43563b): the dummy placeholder is still in the code, and the same invalid requests are sent on every --continue start. The toast itself didn't appear in my 3 runs there, most likely because of startup timing, so it may show up only some of the time.

    Related: #47556 (closed as a duplicate of this issue; it reports that OPENCODE_FAST_BOOT=1 makes it easier to trigger) and #43020 (same got "dummy" error in the logs).

    recording.mp4

    Screenshot

    Reproduction script (drive.ts)
    // Reproduces #34144: `opencode --continue` shows a session-ID validation error toast at boot
    // because the TUI seeds the session route with sessionID "dummy".
    //
    // Run: OPENCODE_DRIVE_MEDIA_DIR=./media opencode-drive run ./drive.ts
    // (set OC_DEV=/path/to/opencode/checkout to run from source instead of `opencode` on PATH)
    //
    // 1. The first TUI creates a session by submitting a prompt.
    // 2. A second, recorded TUI is launched with `--continue` against the same server.
    //    It goes through a small proxy that delays the "latest session" lookup by 3s
    //    (as on a slow machine or with a large history), so the placeholder route mounts first.
    import { Effect } from "effect"
    import { OpenCodeDriver } from "opencode-drive"
    import { chmodSync, mkdirSync, rmSync, writeFileSync } from "node:fs"
    
    const dir = "/tmp/opencode/oc34144"
    const wrapper = `${dir}/oc-continue.sh`
    const flag = `${dir}/continue.flag`
    mkdirSync(dir, { recursive: true })
    rmSync(flag, { force: true })
    writeFileSync(`${dir}/slow-proxy.ts`, '// Forwards to the real server, delaying the session list request the TUI makes for --continue\n// (as on a slow machine or with a large session history), so the placeholder route mounts first.\nconst target = process.argv[2]!\nconst port = Number(process.argv[3])\nBun.serve({\n  port,\n  idleTimeout: 0,\n  async fetch(req) {\n    const url = new URL(req.url)\n    const upstream = new URL(url.pathname + url.search, target)\n    if (process.env.PROXY_LOG) console.error(req.method, url.pathname + url.search)\n    if (url.searchParams.get("limit") === "1" || url.pathname.endsWith("/session/list")) await Bun.sleep(3000)\n    const headers = new Headers(req.headers)\n    headers.delete("host")\n    return fetch(upstream, {\n      method: req.method,\n      headers,\n      body: req.method === "GET" || req.method === "HEAD" ? undefined : await req.arrayBuffer(),\n      redirect: "manual",\n      // @ts-ignore\n      decompress: false,\n    })\n  },\n})\n')
    writeFileSync(wrapper, '#!/bin/sh\n# Runs opencode (installed, or the OC_DEV checkout from source) and adds\n# --continue when launched as a TUI (i.e. with --server) once the drive script\n# has created the flag file (so the first TUI starts normally).\n# The TUI is pointed at slow-proxy.ts, which delays the latest-session lookup.\nif [ -n "$OC_DEV" ]; then\n  set -- bun --conditions=browser --preload="$OC_DEV/packages/tui/node_modules/@opentui/solid/scripts/preload.js" "$OC_DEV/packages/cli/src/index.ts" "$@"\nelse\n  set -- opencode "$@"\nfi\nif [ -f /tmp/opencode/oc34144/continue.flag ]; then\n  case " $* " in *" --server "*)\n    # Route this TUI through a proxy that delays the "latest session" lookup by 3s.\n    port=$(( 40000 + $$ % 20000 ))\n    out=""; prev=""\n    for a in "$@"; do\n      if [ "$prev" = "--server" ]; then\n        bun /tmp/opencode/oc34144/slow-proxy.ts "$a" "$port" >>/tmp/opencode/oc34144/proxy.log 2>&1 &\n        a="http://127.0.0.1:$port"\n      fi\n      out="$out \'$a\'"; prev="$a"\n    done\n    eval "set -- $out"\n    sleep 1\n    set -- "$@" --continue ;;\n  esac\nfi\nexec "$@"\n')
    chmodSync(wrapper, 0o755)
    
    export default OpenCodeDriver.use(
      {
        opencode: { command: [wrapper] },
        project: { git: true, files: { "README.md": "hello\n" } },
      },
      ({ ui, tuis }) =>
        Effect.gen(function* () {
          yield* ui.submit("Say hello")
          yield* ui.waitFor("Say hello")
          yield* ui.screenshot("first-tui")
    
          // From now on, TUIs are launched with --continue (through the slow proxy).
          yield* Effect.sync(() => writeFileSync(flag, ""))
          const second = yield* tuis.launch({ recording: true, viewport: { cols: 120, rows: 40 } })
          yield* second.recording!.mark("opencode --continue")
          yield* second.ui
            .waitFor("Expected a string starting with", { timeout: 10000 })
            .pipe(Effect.catchTag("UiWaitTimeoutError", () => Effect.log("NO ERROR TOAST SEEN")))
          yield* second.ui.screenshot("continue-toast")
          yield* Effect.sleep(4000)
          yield* second.ui.screenshot("continue-after")
        }),
    )

    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.

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions