Repository navigation
Description
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:
-
Ready-line banner changed. The SDK's
createOpencodeServer()scans stdout for a line starting withopencode server listeningand parses the URL from it, otherwise kills the process aftertimeoutms. OpenCode v2.1.4 prints a different banner:server listening on http://127.0.0.1:40201 server password V4kTahCjdou5…No
opencodeprefix → the match never fires → "Timeout waiting for server to start after 15000ms". -
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/inforeturns 401 with no auth, wrong Basic auth, Basic with the generated password, andBearer— 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/sdk1.18.18
Reproduction
- Machine has OpenCode v2.1.4.
npx shellular, pair + approve the iOS app.- Tap OpenCode in the app.
- 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 Pindist/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
som4tress commentedon Oct 27, 2026
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:v2 prints
server listening on http://127.0.0.1:40201, so the match never fires and the SDK kills the server aftertimeoutms.2. Per-server password
Confirmed. The CLI built its client with
Basic base64(<NAME>:<random32>)and never read the printedserver password <pw>. Against v2 that's a 401.Worth noting the auth scheme: Basic with username
opencodeis what v2 accepts.Bearer <password>and Basic with any other username both return 401. This was easy to get wrong, so: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:
That string comes from the SDK's response interceptor in
dist/v2/client.js:The CLI calls
experimental.session.list, which requests/experimental/session. On v2.1.4 that path serves the web UI, not the API:The JSON API is
/api/session. So even with the banner and auth fixed, session listing would still fail.Payload shape also differs
/api/sessionreturns a shape that doesn't match what the CLI'sOpenCodeSessionSchemaexpects:/api/sessiondirectorylocation.directoryslugversionprojectprojectIDonly)summarytime.created/updatedThe 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:OPENCODE_SERVER_URL→ default port4096probe →~/.config/opencode/service.json(written byopencode serve --service, and it contains the password). Server started viaopencode serve --serviceis very common, so this covers the common case without spawning at all.externaland are never killed indestroy(); only servers we spawn are closed. (The old code would have killed a user's pre-existing server on teardown.)service.jsonfor Basic auth./api/sessionwhen 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 --noEmitclean,biome checkclean,pnpm run buildsucceedstext/htmlCaveat 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 opencodefirst is enough to force it.