Skip to content

OpenCode agent fails on OpenCode v2: "Timeout waiting for server to start after 15000ms" (v1 SDK banner + auth assumptions) #117

Open

Description

@som4tress

OpenCode agent fails on OpenCode v2: "Timeout waiting for server to start after 15000ms" (v1 SDK banner + auth assumptions)

Summary

The OpenCode integration in Shellular CLI (shellular@0.1.0, bundled @opencode-ai/sdk@1.18.18) cannot launch OpenCode v2.x servers. Tapping OpenCode in the app always fails with:

timeout waiting to start after 15000 ms

The server itself actually starts fine in ~1 second — the CLI just never recognizes it, because its launch code assumes OpenCode v1 behavior in two ways, and both changed intentionally in OpenCode v2:

  1. Ready-line banner changed. The SDK's createOpencodeServer() scans stdout for a line starting with opencode server listening and parses the URL from it, otherwise kills the process after timeout ms. OpenCode v2.1.4 prints a different banner:

    server listening on http://127.0.0.1:40201
    server password V4kTahCjdou5…
    

    No opencode prefix → the match never fires → "Timeout waiting for server to start after 15000ms".

  2. Servers now enforce a per-server generated password. Even if the banner matched, the client would get 401s: Shellular creates its OpenCode client with a random Basic auth header (Basic base64(NAME:random32)) and never reads the printed password. That worked against v1 localhost servers, but v2 rejects it. Verified against a v2.1.4 server on loopback — GET /api/info returns 401 with no auth, wrong Basic auth, Basic with the generated password, and Bearer — the v2 client SDK uses its own (different) auth flow.

Environment

  • Shellular CLI 0.1.0 (via npx shellular), Linux x86_64
  • Shellular iOS app 0.0.43 (43), iPhone — pairing itself works fine
  • OpenCode CLI v2.1.4 (opencode serve / background service)
  • Bundled @opencode-ai/sdk 1.18.18

Reproduction

  1. Machine has OpenCode v2.1.4.
  2. npx shellular, pair + approve the iOS app.
  3. Tap OpenCode in the app.
  4. Every attempt: the CLI spawns opencode serve --hostname=127.0.0.1 --port=0, the server binds successfully, the SDK never matches the ready-line, kills the process at 15 s → "timeout waiting to start after 15000 ms". Retries repeat every ~30 s.

Evidence from the spawned server run manually (it comes up fine):

$ opencode serve --hostname=127.0.0.1 --port=0
server listening on http://127.0.0.1:40201      # ← no "opencode " prefix anymore
server password V4kTahCjdou5…                   # ← per-server password, enforced

The relevant code paths:

  • @opencode-ai/sdk/dist/v2/server.js → createOpencodeServer():
    if (line.startsWith("opencode server listening")) — v1-only banner.
  • Shellular's OpenCode agent (zt=class n extends P in dist/main.js):
    createOpencodeServer({ hostname: "127.0.0.1", port: 0, timeout: 15000 }), then
    createOpencodeClient({ baseUrl, headers: { Authorization: <random Basic> } }).

Observed spawn pattern in the OpenCode server log (repeats on each app retry):

cli starting  args=["serve","--hostname=127.0.0.1","--port=0"]
…nothing further… (killed after 15 s)

(A opencode serve --stdio --port 0 process is also spawned and stays alive, but the app's launch path clearly uses the HTTP SDK path, since the error is the SDK's 15 s timeout message.)

Suggested fix

Upgrade the OpenCode integration to the v2 SDK generation (@opencode-ai/sdk v2 / @opencode/client@2.x), which:

  • matches the new server listening on <url> ready-line, and
  • handles the new per-server password (or passes a known password via OPENCODE_SERVER_PASSWORD / config when spawning).

OpenCode v2's server API changed intentionally (see their v2 docs/migration notes), so this likely affects any v1-era SDK usage in the CLI.

Notes

  • Everything else in Shellular (terminal, files, localhost ports) works great against this machine — only the OpenCode agent integration is affected.
  • OpenCode v2.1.4's opencode serve --stdio (ACP-ish) mode exists and spawns, in case a stdio transport is a viable interim path.

Activity

  1. som4tress commented on Oct 27, 2026

    @som4tress
    Author

    Opened a fix for this: #118 (branch fix/opencode-v2-support).

    While implementing it I hit a third v2 incompatibility that isn't in the original report, and it's the one that produces a second, separate error after the timeout is solved. Details below in case they're useful to anyone else tripping over this.

    Root causes confirmed

    1. Ready-line banner (the reported 15s timeout)

    Confirmed in the bundled @opencode-ai/sdk@1.18.18, dist/v2/server.js:

    if (line.startsWith("opencode server listening")) { ... }

    v2 prints server listening on http://127.0.0.1:40201, so the match never fires and the SDK kills the server after timeout ms.

    2. Per-server password

    Confirmed. The CLI built its client with Basic base64(<NAME>:<random32>) and never read the printed server password <pw>. Against v2 that's a 401.

    Worth noting the auth scheme: Basic with username opencode is what v2 accepts. Bearer <password> and Basic with any other username both return 401. This was easy to get wrong, so:

    # works
    curl -H "Authorization: Basic $(printf 'opencode:%s' "$PW" | base64)" http://127.0.0.1:PORT/api/info
    
    # 401
    curl -H "Authorization: Bearer $PW" http://127.0.0.1:PORT/api/info

    3. Session list endpoint returns HTML, not JSON

    This one is new and is not mentioned in the original report. After fixing both issues above, tapping OpenCode still failed with a different message:

    Request is not supported by this version of OpenCode Server (Server responded with text/html)

    That string comes from the SDK's response interceptor in dist/v2/client.js:

    client.interceptors.response.use((response) => {
        const contentType = response.headers.get("content-type");
        if (contentType === "text/html")
            throw new Error("Request is not supported by this version of OpenCode Server (Server responded with text/html)");
        return response;
    });

    The CLI calls experimental.session.list, which requests /experimental/session. On v2.1.4 that path serves the web UI, not the API:

    $ curl -i -H "Authorization: Basic ..." http://127.0.0.1:49374/experimental/session
    HTTP/1.1 200
    content-type: text/html
    
    $ curl -i -H "Authorization: Basic ..." http://127.0.0.1:49374/api/session
    HTTP/1.1 200
    content-type: application/json
    

    The JSON API is /api/session. So even with the banner and auth fixed, session listing would still fail.

    Payload shape also differs

    /api/session returns a shape that doesn't match what the CLI's OpenCodeSessionSchema expects:

    Expected by CLI (v1) v2 /api/session
    directory under location.directory
    slug absent
    version absent
    project absent (projectID only)
    summary absent
    time.created/updated present ✅

    The fix normalizes v2 → v1 before the zod parse, so the app-facing shape is unchanged.

    What the fix does

    All in cli/src/agents/opencode.ts:

    • Reuse an already-running server instead of depending on the v1 banner. Resolution order: OPENCODE_SERVER_URL → default port 4096 probe → ~/.config/opencode/service.json (written by opencode serve --service, and it contains the password). Server started via opencode serve --service is very common, so this covers the common case without spawning at all.
    • Servers we discover are marked external and are never killed in destroy(); only servers we spawn are closed. (The old code would have killed a user's pre-existing server on teardown.)
    • Use the real password from service.json for Basic auth.
    • Fall back to /api/session when the SDK call fails, then normalize v2 → v1. v1-shaped payloads pass through untouched, so older servers keep working.

    Verification

    OpenCode v2.1.4, Linux x86_64, iOS app 0.0.43:

    • tsc --noEmit clean, biome check clean, pnpm run build succeeds
    • Pairing succeeds, OpenCode connects, session list loads, sessions open — no timeout, no text/html
      ✓ Client c_... connected
      reusing running opencode server at http://127.0.0.1:49374
    

    Caveat on coverage: my server was already running as a background service, so the discovery/reuse path is the one exercised end-to-end. The spawn path (and therefore the banner handling itself) was not exercised in this test — it remains the fallback when no running server is found. Happy to re-test that path if a maintainer wants evidence before merging; pkill opencode first is enough to force it.

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