Skip to content

opencode run hangs forever (and silently) when the provider's baseURL refuses the TCP connection — unbounded retry schedule + non-interactive mode never surfaces the retry status #40330

Closed as not planned

Description

@Martossien

Summary

When a configured openai-compatible provider's baseURL points to a port with nothing
listening (TCP ECONNREFUSED — e.g. a local llama.cpp server that is not started yet, or
crashed), opencode run produces no output at all and never exits. No error, no retry
notice, no timeout — from the outside it is indistinguishable from a deadlock (~50 Bun
threads parked in futex, zero open sockets, because the refused socket closes instantly
and the event loop sleeps on the backoff timer 29.9 s out of every 30).

A live-but-slow endpoint works fine. The trigger is strictly connection refused.
Reproduced on 1.17.4, 1.17.13, 1.17.14 and 1.18.11, with a minimal isolated config
(fresh XDG_CONFIG_HOME/XDG_DATA_HOME, a single custom provider, no plugins).

Reproduction (no GPU, ~30 seconds)

ISO=$(mktemp -d); mkdir -p $ISO/opencode $ISO/data
cat > $ISO/opencode/opencode.json <<'EOF'
{
  "$schema": "https://opencode.ai/config.json",
  "provider": {
    "local": {
      "npm": "@ai-sdk/openai-compatible",
      "name": "local repro",
      "options": { "baseURL": "http://127.0.0.1:59999/v1" },
      "models": { "m": { "name": "local repro" } }
    }
  }
}
EOF
time timeout 40 env XDG_CONFIG_HOME=$ISO XDG_DATA_HOME=$ISO/data \
  opencode run -m local/m "say pong"
# → 40s, zero bytes of output, killed by `timeout` (exit 124). Expected: a prompt,
#   clear error within seconds ("cannot connect to http://127.0.0.1:59999/v1").

Root cause (from reading v1.18.11 source)

  1. @ai-sdk/provider-utils handleFetchError marks every network-level failure
    (including ECONNREFUSED) as APICallError { isRetryable: true }.
  2. opencode propagates that flag untouched: packages/opencode/src/session/message-v2.ts:676-704
    and packages/opencode/src/provider/error.ts:181. There is no branch for
    ECONNREFUSED/ENOTFOUND/EHOSTUNREACH
    (contrast with the explicit ECONNRESET
    handling at message-v2.ts:625-637).
  3. The retry policy never gives up: packages/opencode/src/session/retry.ts:68-79
    (retryable() always returns an object while isRetryable is true) and
    retry.ts:176-200 (policy() — the Schedule has no Schedule.recurs, no time
    budget
    ; only the delay is capped at RETRY_MAX_DELAY_NO_HEADERS = 30_000,
    retry.ts:26-28). Net effect: one attempt every 30 s, forever.
  4. packages/opencode/src/session/processor.ts:660-675 applies
    Effect.retry(SessionRetry.policy(...)) before Effect.catch(halt) — so halt
    is unreachable, session.error is never published, the session never reaches idle.
  5. Non-interactive mode has no exit path and no feedback:
    packages/opencode/src/cli/cmd/run.ts:789-793 only breaks on
    session.status === "idle", and the retry status events (emitted at
    processor.ts:664-672) are consumed by the TUI
    (packages/tui/src/routes/session/index.tsx:352) but nowhere in run.ts — not
    even with --format json. Meanwhile run.ts:862-869 awaits
    client.session.prompt(...) with no client-side timeout.
  6. No workaround via options: the fetch wrapper honors timeout/headerTimeout/
    chunkTimeout (packages/opencode/src/provider/provider.ts:1737-1768) but those only
    arm on an established connection — ECONNREFUSED fails before them — and
    headerTimeout gets a default only for the openai provider (provider.ts:208).
    opencode run has no --timeout/--max-retries flag (run.ts:128-210).

Why it matters

Anyone driving a local model server (llama.cpp, vLLM, Ollama-compatible endpoints)
hits this whenever the server is down or restarting: batch/CI usage of opencode run
freezes forever with zero diagnostics. We have been wrapping every invocation in our own
TCP pre-flight probe + external watchdog to survive it.

Suggested fixes (any subset helps)

  • Treat ECONNREFUSED/ENOTFOUND/EHOSTUNREACH as non-retryable (or retry a small,
    bounded number of times, then fail with the underlying message).
  • Bound the retry schedule (max attempts and/or total time budget).
  • Surface the retry status in run mode (both plain and --format json) so the user
    can at least see what is happening.
  • Expose --max-retries / --timeout on opencode run.

Related: #17516 (opencode run hangs after completing tool calls) is a different hang
(post-completion), but both share the "non-interactive mode can wait forever with no
output" trait.

Activity

  1. github-actions commented on Aug 3, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

    Additionally, #39790 covers a related problem with the same unbounded SessionRetry.policy() — retrying forever on fixed-window quota errors (HTTP 429) — if that analysis is also relevant context.

  2. Martossien commented on Aug 3, 2026

    @Martossien
    Author

    Duplicate of #40319 (filed a few hours earlier, same symptom, same version). Moved the root-cause analysis there: #40319 (comment) — the 30-second repro script in this issue's description remains valid.

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

Metadata

Metadata

Assignees

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