Skip to content

fix(opencode): prevent resource leaks in serve/web shutdown and SSE stream cleanup#14092

Closed
s2bomb wants to merge 2 commits intoanomalyco:devfrom
s2bomb:fix/orphaned-serve-processes
Closed

fix(opencode): prevent resource leaks in serve/web shutdown and SSE stream cleanup#14092
s2bomb wants to merge 2 commits intoanomalyco:devfrom
s2bomb:fix/orphaned-serve-processes

Conversation

@s2bomb
Copy link
Copy Markdown

@s2bomb s2bomb commented Feb 18, 2026

What does this PR do?

Fixes #14091

Two resource cleanup bugs in the serve/web commands and the SSE event endpoint:

  1. serve.ts and web.tsserver.stop() is dead code (unreachable after await new Promise(() => {})), and no signal handlers exist. On SIGTERM/SIGINT, Instance.disposeAll() never runs, so MCP subprocesses, LSP servers, and other child resources are orphaned.

  2. server.ts — The SSE /event endpoint puts clearInterval(heartbeat) and unsub() inside stream.onAbort(), which Hono only calls on client disconnect. When the server closes the stream on InstanceDisposed via stream.close(), cleanup never runs and the interval + subscription leak.

Changes

serve.ts and web.ts — Add signal handlers following the existing pattern from worker.ts:137-147:

const shutdown = async () => {
  await Promise.race([Instance.disposeAll(), new Promise((resolve) => setTimeout(resolve, 5000))])
  server.stop(true)
  process.exit(0)
}
process.on("SIGTERM", shutdown)
process.on("SIGINT", shutdown)

server.ts — Add clearInterval(heartbeat) and unsub() to the InstanceDisposed handler before stream.close(). Both calls are idempotent, so the existing onAbort handler remains as a fallback for normal client disconnects.

How did you verify your code works?

  • Full test suite passes (996 tests, 0 failures)
  • Manual: bun dev serve, then kill -TERM <pid> — process now exits cleanly (exit 0) instead of being killed by signal (exit 143)
  • Code review: the shutdown pattern is identical to the existing worker.ts implementation that already handles this correctly

server.stop() after await new Promise(() => {}) is unreachable dead code.
No signal handlers exist, so SIGTERM/SIGINT use the kernel default and
Instance.disposeAll() never runs — orphaning MCP subprocesses, LSP servers,
file watchers, and SQLite connections.

Add signal handlers following the existing pattern from worker.ts:137-147:
timeout-bounded Instance.disposeAll() + server.stop(true) + process.exit(0).
clearInterval(heartbeat) and unsub() only run inside stream.onAbort(),
which Hono only fires on client disconnect. When the server closes the
stream on InstanceDisposed via stream.close(), the interval and bus
subscription leak. Add cleanup before stream.close() in the
InstanceDisposed handler. Both calls are idempotent so the onAbort
fallback for normal client disconnects still works.
@github-actions
Copy link
Copy Markdown
Contributor

Closing this pull request because it has had no updates for more than 60 days. If you plan to continue working on it, feel free to reopen or open a new PR.

@github-actions github-actions Bot closed this Apr 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Memory leak: serve and web commands leak child processes on exit; SSE heartbeat interval leaks on server-initiated close

1 participant