From 5fd7873c7c10728d2e8152d8cfbef707146cceec Mon Sep 17 00:00:00 2001 From: tbounsiar Date: Tue, 24 Mar 2026 22:19:02 +0100 Subject: [PATCH] fix: dispose SettingsManager on session close to prevent resource leaks (#454) ## Summary - `SettingsManager.dispose()` was never called when a session was closed via `unstable_closeSession` or when the Claude Agent process died unexpectedly - Each leaked session left up to 4 active `fs.watch()` file descriptors and a pending debounce timer - Added `session.settingsManager.dispose()` in both cleanup paths and updated test mocks to assert the call ## Test plan - [x] `npm run build` compiles clean - [x] `npm run lint` passes - [x] Existing `session/close` tests updated to verify `dispose()` is called - [x] All 4 session/close tests pass --- src/acp-agent.ts | 2 ++ src/tests/acp-agent.test.ts | 7 ++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/acp-agent.ts b/src/acp-agent.ts index 972c28f8..992d3ee8 100644 --- a/src/acp-agent.ts +++ b/src/acp-agent.ts @@ -811,6 +811,7 @@ export class ClaudeAcpAgent implements Agent { message.includes("Failed to write to process stdin") ) { this.logger.error(`Session ${params.sessionId}: Claude Agent process died: ${message}`); + session.settingsManager.dispose(); session.input.end(); delete this.sessions[params.sessionId]; throw RequestError.internalError( @@ -858,6 +859,7 @@ export class ClaudeAcpAgent implements Agent { } await this.cancel({ sessionId: params.sessionId }); + session.settingsManager.dispose(); session.abortController.abort(); delete this.sessions[params.sessionId]; diff --git a/src/tests/acp-agent.test.ts b/src/tests/acp-agent.test.ts index ec479629..bebac4bd 100644 --- a/src/tests/acp-agent.test.ts +++ b/src/tests/acp-agent.test.ts @@ -1336,7 +1336,7 @@ describe("stop reason propagation", () => { currentModelId: "default", availableModels: [], }, - settingsManager: {} as any, + settingsManager: { dispose: vi.fn() } as any, accumulatedUsage: { inputTokens: 0, outputTokens: 0, @@ -1470,7 +1470,7 @@ describe("stop reason propagation", () => { currentModelId: "default", availableModels: [], }, - settingsManager: {} as any, + settingsManager: { dispose: vi.fn() } as any, accumulatedUsage: { inputTokens: 0, outputTokens: 0, @@ -1543,7 +1543,7 @@ describe("session/close", () => { currentModelId: "default", availableModels: [], }, - settingsManager: {} as any, + settingsManager: { dispose: vi.fn() } as any, accumulatedUsage: { inputTokens: 0, outputTokens: 0, @@ -1570,6 +1570,7 @@ describe("session/close", () => { expect(result).toEqual({}); expect(agent.sessions["session-1"]).toBeUndefined(); expect(session.query.interrupt).toHaveBeenCalled(); + expect(session.settingsManager.dispose).toHaveBeenCalled(); }); it("should abort the session's abort controller", async () => {