-
Notifications
You must be signed in to change notification settings - Fork 11.2k
Description
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
SIGHUPto the process group - No SIGHUP handler exists → signal is ignored by the Bun event loop
stdinis now a dead file descriptor but the TUI event loop doesn't detect thiscli.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()inweb_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 (
WaiterThreadinprocess.zig) only handles children spawned viaBun.spawn(), not the parent process lifecycle