Bug Report
Summary
When a controlling terminal is destroyed (SSH disconnect, tmux pane close, terminal emulator crash), opencode processes become orphaned and continue running indefinitely, each consuming ~4.7 GB of RAM. This is because opencode has no SIGHUP handler, and the TUI event loop blocks on stdin that will never produce input, preventing process.exit() in the finally block of index.ts from ever executing.
Observed Impact
On a system running opencode via oh-my-opencode with tmux, 14 orphaned opencode processes accumulated over time, consuming ~66 GB of RAM total. All had PPID=1 (adopted by init), no controlling terminal, and no alive children.
Root Cause
In packages/opencode/src/index.ts, the main flow is:
try {
await cli.parse() // blocks on TUI promise → stdin
} catch (e) { ... } finally {
process.exit() // only reached when cli.parse() resolves
}
When the TUI command runs (thread.ts), cli.parse() awaits the TUI promise (line 185), which waits for interactive terminal input. When the terminal dies:
- Kernel sends
SIGHUP to the process group
- No SIGHUP handler exists → signal is ignored by the Bun event loop
stdin is now a dead file descriptor but the TUI event loop doesn't detect this
cli.parse() never resolves → process.exit() never runs
- Process continues consuming memory indefinitely as an orphan (
PPID=1)
Environment
- opencode v1.2.10
- Bun 1.3.6
- Linux (Arch)
- oh-my-opencode v3.7.4 (manages tmux sessions)
Reproduction
- Start opencode in a tmux pane
- Kill the tmux pane or session externally (e.g.
tmux kill-pane)
- Observe the opencode process continues running with
PPID=1
- Repeat — each orphaned process retains ~4.7 GB
Proposed Fix
Add signal handlers before the main try block in index.ts:
for (const sig of ["SIGHUP", "SIGTERM", "SIGPIPE"] as const) {
process.on(sig, () => {
process.exit(1)
})
}
- SIGHUP: Terminal death (SSH disconnect, tmux pane close)
- SIGTERM: Graceful shutdown requests
- SIGPIPE: Broken pipe when output destination is gone
This ensures process.exit() is called immediately on terminal death, which triggers the existing subprocess cleanup logic and prevents orphaned processes.
Additional Context
The existing comment on lines 198-200 of index.ts already acknowledges that subprocesses can hang:
Some subprocesses don't react properly to SIGTERM and similar signals. Most notably, some docker-container-based MCP servers don't handle such signals unless run using docker run --init.
The same class of problem affects the parent opencode process itself — it doesn't react to SIGHUP at all.
Investigation Notes
- Bun Workers are threads (via
std.Thread.spawn() in web_worker.zig), not processes — they are not the source of zombie child processes
- The orphaned processes were full opencode instances that lost their controlling terminal
- Bun's process reaping (
WaiterThread in process.zig) only handles children spawned via Bun.spawn(), not the parent process lifecycle
Bug Report
Summary
When a controlling terminal is destroyed (SSH disconnect, tmux pane close, terminal emulator crash), opencode processes become orphaned and continue running indefinitely, each consuming ~4.7 GB of RAM. This is because opencode has no
SIGHUPhandler, and the TUI event loop blocks on stdin that will never produce input, preventingprocess.exit()in thefinallyblock ofindex.tsfrom ever executing.Observed Impact
On a system running opencode via oh-my-opencode with tmux, 14 orphaned opencode processes accumulated over time, consuming ~66 GB of RAM total. All had
PPID=1(adopted by init), no controlling terminal, and no alive children.Root Cause
In
packages/opencode/src/index.ts, the main flow is:When the TUI command runs (
thread.ts),cli.parse()awaits the TUI promise (line 185), which waits for interactive terminal input. When the terminal dies:SIGHUPto the process groupstdinis now a dead file descriptor but the TUI event loop doesn't detect thiscli.parse()never resolves →process.exit()never runsPPID=1)Environment
Reproduction
tmux kill-pane)PPID=1Proposed Fix
Add signal handlers before the main
tryblock inindex.ts:This ensures
process.exit()is called immediately on terminal death, which triggers the existing subprocess cleanup logic and prevents orphaned processes.Additional Context
The existing comment on lines 198-200 of
index.tsalready acknowledges that subprocesses can hang:The same class of problem affects the parent opencode process itself — it doesn't react to SIGHUP at all.
Investigation Notes
std.Thread.spawn()inweb_worker.zig), not processes — they are not the source of zombie child processesWaiterThreadinprocess.zig) only handles children spawned viaBun.spawn(), not the parent process lifecycle