You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Instance bootstrap can hang indefinitely — plugin.init() npm install has no timeout and the per-directory boot promise is cached #33905
Every instance-scoped HTTP request (GET /session/:id, GET /session, etc.) hangs forever when the first request triggers a lazy instance bootstrap whose npm dependency install stalls (e.g. slow/unreachable registry on a cold or resumed machine). The server logs status=started for the request and never logs status=completed; no error is thrown. The client eventually gives up on its own timeout (e.g. undici's 300s headersTimeout). Subsequent requests for the same directory hang identically and indefinitely.
Root cause
InstanceMiddleware resolves a per-directory instance via Instance.provide({ init: () => AppRuntime.runPromise(InstanceBootstrap), fn: () => next() }) (packages/opencode/src/server/routes/instance/middleware.ts).
Instance.provide (packages/opencode/src/project/instance.ts) caches one boot promise per directory:
constcache=newMap<string,Promise<InstanceContext>>()// ...letexisting=cache.get(directory)if(!existing)existing=track(directory,boot({directory, init, ... }))constctx=awaitexisting// every request awaits the SAME cached promise
InstanceBootstrap (packages/opencode/src/project/bootstrap.ts) awaits plugin.init()inline, before the forked services. plugin.init() → Config.waitForDependencies() joins config dependency fibers that run Npm.add / Npm.install (@opencode-ai/core/npm) to install config-declared plugins and their runtime deps — with no timeout.
track() only deletes the cache entry when the promise settles. A boot that rejects clears the entry and the next request retries. A boot that hangs never settles, so it is never evicted or retried, and every subsequent request for that directory parks on the same dead promise permanently.
This produces a permanent, recurring, error-less wedge: the process stays alive (health and other directories' requests are served in ms), but one directory's instance is stuck forever.
Suggested fixes
Bound InstanceBootstrap (and the npm install in particular) with a timeout.
Evict/replace the cached boot promise when a boot stalls, so a single stuck bootstrap cannot poison all future requests for that directory.
Consider moving dependency installs off the request-blocking bootstrap path (install in the background and serve once ready, or fail fast).
Flagging a possible mis-triage, plus some fresh static evidence that the mechanism described here is still present in 1.18.3.
This was closed in favour of #32361, but #32361 is "TUI hangs on black screen at boot (OpenTUI) since v1.17.0 — Ubuntu/Wayland, all terminals, no plugins", which appears to be an unrelated rendering path. The failure described in this issue — plugin.init() awaiting Config.waitForDependencies(), which joins npm install fibers that carry no timeout — looks like a different bug, and closing it here leaves that mechanism untracked.
An independent read of the shipped 1.18.3 bundle matches this issue's description and adds two details. The custom-tool loader does let Q = (yield* o.directories()).flatMap(io => Zt.scanSync("{tool,tools}/*.{js,ts}", { cwd: io, absolute: !0, dot: !0, symlink: !0 })); if (Q.length) yield* o.waitForDependencies();. First, that wait is gated purely on Q.length — the mere existence of a tool file, before any import is attempted — so the install is awaited even when dependencies are already present and even when the tool file is broken and would never have loaded. Second, the install being joined is forked unconditionally per writable config directory via forkDetach, and the reify it eventually calls runs Arborist with progress: false and no timeout. The result is that a foreground startup can sit behind a silent background install with no progress output and no log line naming the tool subsystem, so the last line before the stall comes from an unrelated subsystem and points debugging in the wrong direction.
Symptom observed on macOS with 1.18.3: opencode run did not return, and the last line in ~/.local/share/opencode/log/opencode.log was message=init. I want to be clear about the evidence tier — I did not reproduce that on demand. This machine's npm cache was already warm, which is consistent with the mechanism but is not a demonstration of it. Treat the symptom as a credible report and the paragraph above as the static analysis it is.
A maintainer should be able to force the condition deterministically by making the install unbounded: remove node_modules, package.json and package-lock.json from the config directory, point npm at a registry that does not answer (npm config set registry http://127.0.0.1:9), then run with at least one custom tool file present.
If this is reopened or refiled, the narrow fix suggested by the above is to move the gate from "a tool file exists" to "a tool file failed to import", and to bound waitForDependencies() with a timeout plus a single log line naming the directory being installed into. Related and adjacent: #37317, #34933 and #35963 cover the neighbouring case where a broken tool file's import error escapes initialization rather than being isolated.
Activity
simonklee commentedon Jun 25, 2026
Thanks for the details, closing in favor of #32361
willmcginnis commentedon Aug 10, 2026
Flagging a possible mis-triage, plus some fresh static evidence that the mechanism described here is still present in 1.18.3.
This was closed in favour of #32361, but #32361 is "TUI hangs on black screen at boot (OpenTUI) since v1.17.0 — Ubuntu/Wayland, all terminals, no plugins", which appears to be an unrelated rendering path. The failure described in this issue —
plugin.init()awaitingConfig.waitForDependencies(), which joins npm install fibers that carry no timeout — looks like a different bug, and closing it here leaves that mechanism untracked.An independent read of the shipped 1.18.3 bundle matches this issue's description and adds two details. The custom-tool loader does
let Q = (yield* o.directories()).flatMap(io => Zt.scanSync("{tool,tools}/*.{js,ts}", { cwd: io, absolute: !0, dot: !0, symlink: !0 })); if (Q.length) yield* o.waitForDependencies();. First, that wait is gated purely onQ.length— the mere existence of a tool file, before any import is attempted — so the install is awaited even when dependencies are already present and even when the tool file is broken and would never have loaded. Second, the install being joined is forked unconditionally per writable config directory viaforkDetach, and the reify it eventually calls runs Arborist withprogress: falseand no timeout. The result is that a foreground startup can sit behind a silent background install with no progress output and no log line naming the tool subsystem, so the last line before the stall comes from an unrelated subsystem and points debugging in the wrong direction.Symptom observed on macOS with 1.18.3:
opencode rundid not return, and the last line in~/.local/share/opencode/log/opencode.logwasmessage=init. I want to be clear about the evidence tier — I did not reproduce that on demand. This machine's npm cache was already warm, which is consistent with the mechanism but is not a demonstration of it. Treat the symptom as a credible report and the paragraph above as the static analysis it is.A maintainer should be able to force the condition deterministically by making the install unbounded: remove
node_modules,package.jsonandpackage-lock.jsonfrom the config directory, point npm at a registry that does not answer (npm config set registry http://127.0.0.1:9), then run with at least one custom tool file present.If this is reopened or refiled, the narrow fix suggested by the above is to move the gate from "a tool file exists" to "a tool file failed to import", and to bound
waitForDependencies()with a timeout plus a single log line naming the directory being installed into. Related and adjacent: #37317, #34933 and #35963 cover the neighbouring case where a broken tool file's import error escapes initialization rather than being isolated.