Summary
On Linux, opencode (TUI) intermittently hangs forever during instance bootstrap: a git child spawned during init exits but is never reaped (it stays <defunct> for the life of the process), the bootstrap await never settles, and the window renders normally — accepts typing — but Enter can never start a session because the server side never finished booting. No error is shown or logged; the process idles in epoll indefinitely.
Environment
- opencode 1.18.15, Linux 6.8.0-124-generic (Ubuntu), TUI launched in a terminal
- Heavily concurrent host: multiple opencode instances (interactive +
opencode run) active in the same projects
Evidence (from two live hung instances, same day)
Log (shared ~/.local/share/opencode/log/opencode.log) — the hung instance run stops mid-bootstrap and never logs another line:
Instance A stopped at init:
16:33:51.880 bootstrapping directory=/home/oren/projects/kk/billie
16:33:52.020 message="all LSPs are disabled"
16:33:52.021 message="all formatters are disabled"
16:33:52.021 message=init
<- nothing, ever again (14 lines total, 0 sessions)
Instance B stopped one phase later:
16:40:13.397 message=init
16:40:16.163 message="watcher backend" ... backend=inotify
16:40:16.165 message="project copy refresh started" projectID=1d4fe22...
<- never "done", nothing again (0 sessions)
A healthy boot of the SAME project in the SAME minute continues: init → watcher backend → project copy refresh started/done → init count=281 → sessions created on Enter.
Process table — the discriminator. Each hung instance holds exactly one unreaped git child; seven healthy same-host instances hold zero:
$ ps --ppid <hung-tui-pid> -o pid,stat,args
3377914 Sl+ python3 .../mcp_server.py (local MCP, alive, fine)
3378581 Zs [git] <defunct> (spawned during bootstrap, never reaped)
The zombies' spawn times match the last log line to the second (e.g. git spawned 16:40:16 local; project copy refresh started logged 16:40:16.165Z). Parent state: wchan=ep_poll, i.e. the event loop is alive and waiting — an await whose completion event was lost, not a deadlock on a sync call.
Trigger correlation
Both hangs happened when the window was opened into a project where other opencode instances were concurrently running git against the same shared snapshot store (~/.local/share/opencode/snapshot/<projectID>/ — visible as concurrent tracking hash=... log lines from the other instances). It is a race: a boot into the same busy project 90 s earlier survived. Historical scan of our log: ~30 instance runs since 2026-08-07 (of 895) logged creating instance but never reached watcher backend, clustered in bursts of concurrent activity.
What it is not
- Not input/keybind/TTY related — termios verified sane; the failure is server-side, before any input matters.
- Not plugins spawning the git — our plugins only spawn git synchronously (
execFileSync, which reaps its own child). The unreaped child is core's.
- Not resolved by any key — only closing and reopening the window.
Expected
Bootstrap either completes or fails loudly. A spawned git child's exit should always settle the awaiting promise (success or error), under any interleaving — including exiting very fast, or failing on index.lock contention from a sibling instance.
Suspected area
The boot-time snapshot/project-copy git spawn: child exit (possibly an immediate/fast exit, or a nonzero exit on lock contention) is not observed — either the exit handler is registered after the exit event has already fired (Bun subprocess reaping race) or an error path never rejects the awaited promise. Happy to run an instrumented build or provide more process-level captures; reproduction should be possible by booting a TUI into a project while a loop hammers git operations against that project's snapshot dir.
Summary
On Linux,
opencode(TUI) intermittently hangs forever during instance bootstrap: agitchild spawned during init exits but is never reaped (it stays<defunct>for the life of the process), the bootstrap await never settles, and the window renders normally — accepts typing — but Enter can never start a session because the server side never finished booting. No error is shown or logged; the process idles inepollindefinitely.Environment
opencode run) active in the same projectsEvidence (from two live hung instances, same day)
Log (shared
~/.local/share/opencode/log/opencode.log) — the hung instance run stops mid-bootstrap and never logs another line:Instance A stopped at
init:Instance B stopped one phase later:
A healthy boot of the SAME project in the SAME minute continues:
init→watcher backend→project copy refresh started/done→init count=281→ sessions created on Enter.Process table — the discriminator. Each hung instance holds exactly one unreaped git child; seven healthy same-host instances hold zero:
The zombies' spawn times match the last log line to the second (e.g. git spawned 16:40:16 local;
project copy refresh startedlogged 16:40:16.165Z). Parent state:wchan=ep_poll, i.e. the event loop is alive and waiting — an await whose completion event was lost, not a deadlock on a sync call.Trigger correlation
Both hangs happened when the window was opened into a project where other opencode instances were concurrently running git against the same shared snapshot store (
~/.local/share/opencode/snapshot/<projectID>/— visible as concurrenttracking hash=...log lines from the other instances). It is a race: a boot into the same busy project 90 s earlier survived. Historical scan of our log: ~30 instance runs since 2026-08-07 (of 895) loggedcreating instancebut never reachedwatcher backend, clustered in bursts of concurrent activity.What it is not
execFileSync, which reaps its own child). The unreaped child is core's.Expected
Bootstrap either completes or fails loudly. A spawned git child's exit should always settle the awaiting promise (success or error), under any interleaving — including exiting very fast, or failing on
index.lockcontention from a sibling instance.Suspected area
The boot-time snapshot/project-copy git spawn: child exit (possibly an immediate/fast exit, or a nonzero exit on lock contention) is not observed — either the exit handler is registered after the exit event has already fired (Bun subprocess reaping race) or an error path never rejects the awaited promise. Happy to run an instrumented build or provide more process-level captures; reproduction should be possible by booting a TUI into a project while a loop hammers git operations against that project's snapshot dir.