From 1b62e1043f4f98b62575690877546c7dfd1e0c95 Mon Sep 17 00:00:00 2001 From: opencode Date: Sat, 29 Aug 2026 09:12:26 +0200 Subject: [PATCH] fix(everything): cleanup subscriptions on session disconnect Fixes #4710. The subscriptions Map retained sessionIds after disconnect because cleanup() only cleared intervals and task store. Export removeSubscriber(sessionId) that iterates the map, deletes the session from each Set and drops empty entries, and call it from server factory cleanup() (used by streamableHttp and sse onclose). The reference server is the example clients copy; this prevents unbounded growth from clients that subscribe then disconnect without unsubscribing. --- src/everything/resources/subscriptions.ts | 19 +++++++++++++++++++ src/everything/server/index.ts | 2 ++ 2 files changed, 21 insertions(+) diff --git a/src/everything/resources/subscriptions.ts b/src/everything/resources/subscriptions.ts index 854a8633a2..7e7982a9b2 100644 --- a/src/everything/resources/subscriptions.ts +++ b/src/everything/resources/subscriptions.ts @@ -166,3 +166,22 @@ export const stopSimulatedResourceUpdates = (sessionId?: string) => { subsUpdateIntervals.delete(sessionId); } }; + +/** + * Removes a disconnected session from all subscription Sets. + * + * Called from the server factory's `cleanup()` when a transport closes so + * that a client that subscribed and then disconnects without unsubscribing + * does not leave its sessionId in every Set it joined for the life of the + * process. Drops empty entries to avoid unbounded map growth. + * + * @param sessionId - The session ID to remove (can be undefined for stdio). + */ +export const removeSubscriber = (sessionId?: string) => { + for (const [uri, subscribers] of subscriptions.entries()) { + subscribers.delete(sessionId); + if (subscribers.size === 0) { + subscriptions.delete(uri); + } + } +}; diff --git a/src/everything/server/index.ts b/src/everything/server/index.ts index f1459cc812..da130ce391 100644 --- a/src/everything/server/index.ts +++ b/src/everything/server/index.ts @@ -6,6 +6,7 @@ import { import { setSubscriptionHandlers, stopSimulatedResourceUpdates, + removeSubscriber, } from "../resources/subscriptions.js"; import { registerConditionalTools, registerTools } from "../tools/index.js"; import { registerResources, readInstructions } from "../resources/index.js"; @@ -110,6 +111,7 @@ export const createServer: () => ServerFactoryResponse = () => { // Stop any simulated logging or resource updates that may have been initiated. stopSimulatedLogging(sessionId); stopSimulatedResourceUpdates(sessionId); + removeSubscriber(sessionId); // Clean up task store timers taskStore.cleanup(); if (initializeTimeout) clearTimeout(initializeTimeout);