Skip to content

fix(cli): re-apply dead-stream detection to SSE endpoints to prevent memory leak on Windows - #8952

Merged
marius-kilocode merged 2 commits into
mainfrom
romantic-dryer
Apr 15, 2026
Merged

fix(cli): re-apply dead-stream detection to SSE endpoints to prevent memory leak on Windows#8952
marius-kilocode merged 2 commits into
mainfrom
romantic-dryer

Conversation

@marius-kilocode

@marius-kilocode marius-kilocode commented Apr 15, 2026

Copy link
Copy Markdown
Collaborator

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 in global.ts and event.ts to use an AsyncQueue-based streamEvents() 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) use stream.onAbort(stop) as the sole mechanism to detect disconnected clients and clean up resources (GlobalBus listener, heartbeat interval, AsyncQueue).

On macOS, onAbort fires 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 SdkSSEAdapter detects 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 permanently
  • setInterval heartbeat (10s) keeps pushing to the AsyncQueue forever
  • The AsyncQueue grows without bound as events keep arriving but nobody reads them

Why "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:

Candidate Why excluded
diffFullCached — module-level cache of up to 100 FileDiff[] arrays (kilocode/snapshot/index.ts:130) Pure JS Map, platform-independent — would leak equally on Mac
messageSessionIdsByMessageId — unbounded Map in connection-service.ts:75 Pure JS Map in extension process, identical on all platforms
formatPatch with context: MAX_SAFE_INTEGER — inflated patch strings (snapshot/index.ts:634) Inflates data size but is platform-independent
Effect Bus.subscribe orphaned fibers in kilo-sessions.ts Effect-ts runtime, platform-independent
retryAbortControllers not cleaned in KiloProvider.dispose() Pure JS, same on both platforms

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.Diff publishes full FileDiff[] arrays (with context: MAX_SAFE_INTEGER patches — entire file contents, not just changed lines) through GlobalBus → every SSE handler including leaked dead ones. Each leaked connection's AsyncQueue accumulates 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: true on the Agent Manager panel means the webview and its state persist even when the tab is hidden.

Changes

1. Dead-stream detection in global.ts and event.ts

Wrap stream.writeSSE() in try/catch inside the for await loop. When a write fails (broken TCP socket), call stop() immediately — cleaning up the GlobalBus listener, heartbeat interval, and AsyncQueue — instead of waiting for onAbort which may never fire on Windows.

2. GlobalBus.setMaxListeners(50) in global.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

…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.
@kilo-code-bot

kilo-code-bot Bot commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (3 files)
  • packages/opencode/src/bus/global.ts
  • packages/opencode/src/server/routes/event.ts
  • packages/opencode/src/server/routes/global.ts

@imanolmzd-svg imanolmzd-svg left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move logic to opencode/src/kilocode as much as possible to prevent upstream conflicts

@imanolmzd-svg
imanolmzd-svg self-requested a review April 15, 2026 08:39
@marius-kilocode
marius-kilocode merged commit 7939a02 into main Apr 15, 2026
16 checks passed
@marius-kilocode
marius-kilocode deleted the romantic-dryer branch April 15, 2026 08:40
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
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.

2 participants