fix(cli): re-apply dead-stream detection to SSE endpoints to prevent memory leak on Windows - #8952
Merged
Conversation
…memory leak on Windows The upstream OpenCode v1.3.0 merge rewrote SSE routes with AsyncQueue but dropped our dead-stream detection. On Windows, stream.onAbort() may never fire after client disconnect (IOCP delays TCP RST detection), leaking a GlobalBus listener, heartbeat interval, and AsyncQueue per dead connection. Wrap writeSSE in try/catch to clean up eagerly on write failure.
Contributor
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Files Reviewed (3 files)
|
imanolmzd-svg
approved these changes
Apr 15, 2026
imanolmzd-svg
suggested changes
Apr 15, 2026
imanolmzd-svg
self-requested a review
April 15, 2026 08:39
imanolmzd-svg
approved these changes
Apr 15, 2026
jliounis
pushed a commit
to jliounis/kilocode
that referenced
this pull request
May 18, 2026
…memory leak on Windows (Kilo-Org#8952) * fix(cli): re-apply dead-stream detection to SSE endpoints to prevent memory leak on Windows The upstream OpenCode v1.3.0 merge rewrote SSE routes with AsyncQueue but dropped our dead-stream detection. On Windows, stream.onAbort() may never fire after client disconnect (IOCP delays TCP RST detection), leaking a GlobalBus listener, heartbeat interval, and AsyncQueue per dead connection. Wrap writeSSE in try/catch to clean up eagerly on write failure. * fix(cli): log dead-stream cleanup in SSE endpoints
t7tran
pushed a commit
to t7tran/kilocode
that referenced
this pull request
Aug 14, 2026
…memory leak on Windows (Kilo-Org#8952) * fix(cli): re-apply dead-stream detection to SSE endpoints to prevent memory leak on Windows The upstream OpenCode v1.3.0 merge rewrote SSE routes with AsyncQueue but dropped our dead-stream detection. On Windows, stream.onAbort() may never fire after client disconnect (IOCP delays TCP RST detection), leaking a GlobalBus listener, heartbeat interval, and AsyncQueue per dead connection. Wrap writeSSE in try/catch to clean up eagerly on write failure. * fix(cli): log dead-stream cleanup in SSE endpoints
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Re-apply dead-stream detection to SSE endpoints that was lost during the OpenCode v1.3.0 upstream merge, fixing a memory leak on Windows that is significantly amplified by Agent Manager with large diffs.
Investigation & Findings
Symptom
Memory leak on Windows only. Fairly slow on a single session, but spikes fast with Agent Manager open. Reproducible by having a large diff on the main repo and creating a worktree in Agent Manager that also has a large diff. Does not happen on macOS.
How we narrowed it down
v7.2.0 (April 7) was the last GA release with no leak. The leak appeared in builds after v7.2.0. Between v7.2.0 and v7.2.6, the major changes were OpenCode upstream merges: v1.2.25 through v1.3.17.
The upstream merge v1.3.0 included commit
0540751897("fix(core): use a queue to process events in event routes") which completely rewrote the SSE event routes inglobal.tsandevent.tsto use anAsyncQueue-basedstreamEvents()pattern. This rewrite replaced the code that contained our dead-stream detection fix (81a0d87c25), effectively deleting it during merge conflict resolution.Root cause
The SSE endpoints (
/global/event,/global/sync-event,/event) usestream.onAbort(stop)as the sole mechanism to detect disconnected clients and clean up resources (GlobalBus listener, heartbeat interval, AsyncQueue).On macOS,
onAbortfires within milliseconds of client disconnect (kqueue-based socket notification). On Windows, it can take minutes or never fire (IOCP with default 2-hour TCP keepalive timers).When the VS Code extension's
SdkSSEAdapterdetects a stale connection (15s heartbeat timeout) and reconnects, the old server-side SSE handler stays alive on Windows:GlobalBus.on("event", handler)listener remains registered permanentlysetIntervalheartbeat (10s) keeps pushing to theAsyncQueueforeverAsyncQueuegrows without bound as events keep arriving but nobody reads themWhy "Windows only" excludes other candidates
During investigation we identified several other memory concerns. The Windows-only constraint eliminates all of them as the primary cause:
diffFullCached— module-level cache of up to 100FileDiff[]arrays (kilocode/snapshot/index.ts:130)messageSessionIdsByMessageId— unbounded Map in connection-service.ts:75formatPatchwithcontext: MAX_SAFE_INTEGER— inflated patch strings (snapshot/index.ts:634)Bus.subscribeorphaned fibers in kilo-sessions.tsretryAbortControllersnot cleaned in KiloProvider.dispose()These are filed as follow-up issues (#8949, #8950, #8951) for general memory optimization but are not the Windows leak.
Why "large diffs + worktree" reproduces it faster
Large diffs amplify the leak because
Session.Event.Diffpublishes fullFileDiff[]arrays (withcontext: MAX_SAFE_INTEGERpatches — entire file contents, not just changed lines) throughGlobalBus→ every SSE handler including leaked dead ones. Each leaked connection'sAsyncQueueaccumulates these multi-MB payloads. Two large diffs (main repo + worktree) double the event rate with massive payloads, making memory growth visible within minutes.Why Agent Manager makes it worse
Agent Manager tracks multiple sessions across worktrees, producing higher SSE event volume. More events = leaked connections accumulate data faster. Additionally,
retainContextWhenHidden: trueon the Agent Manager panel means the webview and its state persist even when the tab is hidden.Changes
1. Dead-stream detection in
global.tsandevent.tsWrap
stream.writeSSE()in try/catch inside thefor awaitloop. When a write fails (broken TCP socket), callstop()immediately — cleaning up the GlobalBus listener, heartbeat interval, and AsyncQueue — instead of waiting foronAbortwhich may never fire on Windows.2.
GlobalBus.setMaxListeners(50)inglobal.ts(bus)Safety net. Normal operation uses ~2 listeners. If this warning ever fires in logs, it signals leaked connections are accumulating again. Does not limit functionality — only controls when Node.js emits a warning.
Related
81a0d87c250540751897