From 2b8782de85736508140132bf3440750305d3d401 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sun, 17 May 2026 01:27:00 +0900 Subject: [PATCH] fix(claude-code-session-state): clear session-agent map on delete and sync cleanup External review on PR #4074 noted that adding setSessionAgent for child sessions left sessionAgentMap holding entries after the session was deleted or after the sync call_omo_agent executor cleaned up other owned state. The map only grows; entries never get reused but they do accumulate across long-running plugin instances. Close both gaps: - BackgroundManager.handleEvent for session.deleted now calls clearSessionAgent for the deleted session id on both the early-return no-task branch and the cascade tail. This pairs with the existing clearDelegatedChildSessionBootstrap and SessionCategoryRegistry.remove so all owned session state is dropped together. - sync-executor finally for createdSessionForExecution now calls clearSessionAgent alongside the existing subagentSessions, syncSubagentSessions, and deleteSessionTools cleanup so sessions this executor created cannot leak their agent mapping. Adds focused tests: - BackgroundManager.handleEvent - session.deleted cascade > should clear session agent state for deleted sessions to prevent map leak - executeSync > registers child-session bootstrap and tracked prompt state before sync prompt dispatch (extended assertion for cleanup) --- src/features/background-agent/manager.test.ts | 21 +++++++++++++++++++ src/features/background-agent/manager.ts | 2 ++ .../call-omo-agent/sync-executor.test.ts | 2 ++ src/tools/call-omo-agent/sync-executor.ts | 3 ++- 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/features/background-agent/manager.test.ts b/src/features/background-agent/manager.test.ts index edb79de57..3a219b45e 100644 --- a/src/features/background-agent/manager.test.ts +++ b/src/features/background-agent/manager.test.ts @@ -5063,6 +5063,27 @@ describe("BackgroundManager.handleEvent - session.deleted cascade", () => { manager.shutdown() }) + + test("should clear session agent state for deleted sessions to prevent map leak", async () => { + //#given + const { setSessionAgent } = await import("../claude-code-session-state") + resetClaudeCodeSessionState() + const manager = createBackgroundManager() + const sessionID = "session-deleted-agent-leak" + setSessionAgent(sessionID, "sisyphus-junior") + expect(getSessionAgent(sessionID)).toBe("sisyphus-junior") + + //#when + manager.handleEvent({ + type: "session.deleted", + properties: { info: { id: sessionID } }, + }) + + //#then + expect(getSessionAgent(sessionID)).toBeUndefined() + + manager.shutdown() + }) }) describe("BackgroundManager.handleEvent - session.error", () => { diff --git a/src/features/background-agent/manager.ts b/src/features/background-agent/manager.ts index a93a7f6b6..384246396 100644 --- a/src/features/background-agent/manager.ts +++ b/src/features/background-agent/manager.ts @@ -1660,6 +1660,7 @@ The fallback retry session is now created and can be inspected directly. if (tasksToCancel.size === 0) { this.clearTaskHistoryWhenParentTasksGone(sessionID) + clearSessionAgent(sessionID) return } @@ -1698,6 +1699,7 @@ The fallback retry session is now created and can be inspected directly. this.rootDescendantCounts.delete(sessionID) clearDelegatedChildSessionBootstrap(sessionID) + clearSessionAgent(sessionID) SessionCategoryRegistry.remove(sessionID) } diff --git a/src/tools/call-omo-agent/sync-executor.test.ts b/src/tools/call-omo-agent/sync-executor.test.ts index 58d56a16d..bdbfd7340 100644 --- a/src/tools/call-omo-agent/sync-executor.test.ts +++ b/src/tools/call-omo-agent/sync-executor.test.ts @@ -394,6 +394,8 @@ describe("executeSync", () => { expect(observed[0]?.bootstrap?.tools?.question).toBe(false) expect(observed[0]?.bootstrap?.fallbackChain?.[0]?.model).toBe("gpt-5.4") expect(getDelegatedChildSessionBootstrap("ses-call-bootstrap")).toBeUndefined() + // session-agent state for a sync session we created must be cleared after dispatch + expect(getSessionAgent("ses-call-bootstrap")).toBeUndefined() } finally { clearAllDelegatedChildSessionBootstrap() clearSessionTools() diff --git a/src/tools/call-omo-agent/sync-executor.ts b/src/tools/call-omo-agent/sync-executor.ts index 59a802263..98d698fde 100644 --- a/src/tools/call-omo-agent/sync-executor.ts +++ b/src/tools/call-omo-agent/sync-executor.ts @@ -1,5 +1,5 @@ import type { PluginInput } from "@opencode-ai/plugin" -import { setSessionAgent, subagentSessions, syncSubagentSessions } from "../../features/claude-code-session-state" +import { clearSessionAgent, setSessionAgent, subagentSessions, syncSubagentSessions } from "../../features/claude-code-session-state" import { promptAsyncAfterSessionIdle } from "../../hooks/shared/prompt-async-gate" import { getAgentToolRestrictions, log } from "../../shared" import { getAgentDisplayName, stripAgentListSortPrefix } from "../../shared/agent-display-names" @@ -187,6 +187,7 @@ export async function executeSync( subagentSessions.delete(sessionID) syncSubagentSessions.delete(sessionID) deleteSessionTools(sessionID) + clearSessionAgent(sessionID) } } }