Description
The serve and web commands have no graceful shutdown. When the process receives SIGTERM or SIGINT, child resources (MCP subprocesses, LSP servers, file watchers, SQLite connections) are never cleaned up because Instance.disposeAll() is never called.
Separately, the SSE /event endpoint leaks a setInterval heartbeat and a Bus.subscribeAll subscription when the server closes a stream (on InstanceDisposed), because cleanup only runs in onAbort — which Hono only fires on client disconnect, not on stream.close().
Bug 1: Dead server.stop() in serve.ts and web.ts
serve.ts:17-18:
await new Promise(() => {}) // blocks forever — never resolves
await server.stop() // unreachable
Same pattern in web.ts:78-79. No signal handlers are registered, so SIGTERM/SIGINT use the kernel default (immediate kill, no cleanup).
The correct pattern already exists in worker.ts:137-147:
async shutdown() {
if (eventStream.abort) eventStream.abort.abort()
await Promise.race([
Instance.disposeAll(),
new Promise((resolve) => { setTimeout(resolve, 5000) }),
])
if (server) server.stop(true)
}
Bug 2: SSE heartbeat interval leaks on server-initiated stream close
server.ts:513-539 — clearInterval(heartbeat) and unsub() are inside stream.onAbort(). Hono's onAbort only fires on client disconnect (via ReadableStream.cancel()). When the server closes the stream on InstanceDisposed via stream.close(), the onAbort callback never fires.
Each server-closed SSE client leaks:
- One
setInterval (10s heartbeat) writing to a closed stream
- One
Bus.subscribeAll subscription processing every bus event
Steps to reproduce
Bug 1:
bun dev serve --port 14096 &
SERVE_PID=$!
sleep 3
kill -TERM $SERVE_PID
wait $SERVE_PID
echo "exit code: $?"
Exit code is 143 (SIGTERM default), zero shutdown logs, child processes survive. Each orphaned process holds ~200-280MB RSS and they accumulate on every restart.
Bug 2:
Connect an SSE client, trigger InstanceDisposed — heartbeat interval keeps firing after stream.close().
OpenCode version
Current dev branch
Operating System
Linux (also affects macOS — any platform running serve or web)
Description
The
serveandwebcommands have no graceful shutdown. When the process receives SIGTERM or SIGINT, child resources (MCP subprocesses, LSP servers, file watchers, SQLite connections) are never cleaned up becauseInstance.disposeAll()is never called.Separately, the SSE
/eventendpoint leaks asetIntervalheartbeat and aBus.subscribeAllsubscription when the server closes a stream (onInstanceDisposed), because cleanup only runs inonAbort— which Hono only fires on client disconnect, not onstream.close().Bug 1: Dead
server.stop()in serve.ts and web.tsserve.ts:17-18:Same pattern in
web.ts:78-79. No signal handlers are registered, so SIGTERM/SIGINT use the kernel default (immediate kill, no cleanup).The correct pattern already exists in
worker.ts:137-147:Bug 2: SSE heartbeat interval leaks on server-initiated stream close
server.ts:513-539—clearInterval(heartbeat)andunsub()are insidestream.onAbort(). Hono'sonAbortonly fires on client disconnect (viaReadableStream.cancel()). When the server closes the stream onInstanceDisposedviastream.close(), the onAbort callback never fires.Each server-closed SSE client leaks:
setInterval(10s heartbeat) writing to a closed streamBus.subscribeAllsubscription processing every bus eventSteps to reproduce
Bug 1:
Exit code is 143 (SIGTERM default), zero shutdown logs, child processes survive. Each orphaned process holds ~200-280MB RSS and they accumulate on every restart.
Bug 2:
Connect an SSE client, trigger
InstanceDisposed— heartbeat interval keeps firing afterstream.close().OpenCode version
Current
devbranchOperating System
Linux (also affects macOS — any platform running
serveorweb)