From b637414871abbcf2e1d136ac009206878e10b7d3 Mon Sep 17 00:00:00 2001 From: Josh Lehman Date: Thu, 18 Jun 2026 13:29:39 -0700 Subject: [PATCH] fix: add plugin host session cleanup seam (#93733) * fix: add plugin host session cleanup seam (clawdbot-d02.1.9.1.38) * test: update session accessor writer ratchet --- scripts/check-session-accessor-boundary.mjs | 2 + src/config/sessions/plugin-host-cleanup.ts | 259 ++++++++++++++++++ src/config/sessions/session-accessor.ts | 18 ++ .../host-hook-cleanup.session-store.test.ts | 81 ++++++ src/plugins/host-hook-cleanup.ts | 246 ++--------------- .../check-session-accessor-boundary.test.ts | 1 + 6 files changed, 381 insertions(+), 226 deletions(-) create mode 100644 src/config/sessions/plugin-host-cleanup.ts diff --git a/scripts/check-session-accessor-boundary.mjs b/scripts/check-session-accessor-boundary.mjs index 632291cf063f..293a825b0d3a 100644 --- a/scripts/check-session-accessor-boundary.mjs +++ b/scripts/check-session-accessor-boundary.mjs @@ -110,6 +110,7 @@ export const migratedSessionAccessorWriteFiles = new Set([ "src/auto-reply/reply/session-usage.ts", "src/tui/embedded-backend.ts", "src/config/sessions/cleanup-service.ts", + "src/plugins/host-hook-cleanup.ts", ]); export const migratedTranscriptWriterFiles = new Set([ @@ -336,6 +337,7 @@ export async function main() { "src/agents", "src/auto-reply", "src/config/sessions", + "src/plugins", "src/tui", ]); const transcriptWriterSourceRoots = resolveSourceRoots(repoRoot, [ diff --git a/src/config/sessions/plugin-host-cleanup.ts b/src/config/sessions/plugin-host-cleanup.ts new file mode 100644 index 000000000000..6e00d927363d --- /dev/null +++ b/src/config/sessions/plugin-host-cleanup.ts @@ -0,0 +1,259 @@ +/** File-backed implementation for plugin host-owned session-state cleanup. */ +import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; +import { normalizeSessionEntrySlotKey } from "../../plugins/session-entry-slot-keys.js"; +import { updateSessionStore } from "./store.js"; +import type { SessionEntry } from "./types.js"; + +/** Cleanup variants owned by plugin host lifecycle paths. */ +export type PluginHostSessionCleanupMode = "plugin-owned-state" | "promoted-slots"; + +export type PluginHostSessionCleanupStoreParams = { + /** Cleanup mode chosen by the plugin host lifecycle reason. */ + mode: PluginHostSessionCleanupMode; + /** Plugin owner to clear. Omit only for session-scoped all-plugin cleanup. */ + pluginId?: string; + /** Optional canonical key, alias, or runtime session id filter. */ + sessionKey?: string; + /** Promoted SessionEntry slots declared by the plugin registry. */ + sessionEntrySlotKeys?: ReadonlySet; + /** Per-store file-backed transaction boundary. */ + storePath: string; + /** Cancels the cleanup before persistence when host lifecycle state changes. */ + shouldCleanup?: () => boolean; +}; + +function collectStoredSessionEntrySlotKeys(entry: SessionEntry, pluginId?: string): Set { + const slotKeys = new Set(); + const storedSlotKeys = entry.pluginExtensionSlotKeys; + if (!storedSlotKeys) { + return slotKeys; + } + const records = + pluginId === undefined + ? Object.values(storedSlotKeys) + : storedSlotKeys[pluginId] + ? [storedSlotKeys[pluginId]] + : []; + for (const record of records) { + for (const slotKey of Object.values(record)) { + const normalized = normalizeSessionEntrySlotKey(slotKey); + if (normalized.ok) { + slotKeys.add(normalized.key); + } + } + } + return slotKeys; +} + +function collectPromotedSessionEntrySlotKeys( + entry: SessionEntry, + pluginId?: string, + sessionEntrySlotKeys?: ReadonlySet, +): Set { + const slotKeys = collectStoredSessionEntrySlotKeys(entry, pluginId); + for (const slotKey of sessionEntrySlotKeys ?? []) { + slotKeys.add(slotKey); + } + return slotKeys; +} + +function clearPromotedSessionEntrySlots( + entry: SessionEntry, + pluginId?: string, + sessionEntrySlotKeys?: ReadonlySet, + options: { includeStoredSlotKeys?: boolean; pruneSlotOwnership?: boolean } = {}, +): void { + const slotKeys = + options.includeStoredSlotKeys === false && sessionEntrySlotKeys + ? new Set(sessionEntrySlotKeys) + : collectPromotedSessionEntrySlotKeys(entry, pluginId, sessionEntrySlotKeys); + const entryRecord = entry as Record; + for (const slotKey of slotKeys) { + delete entryRecord[slotKey]; + } + if (!options.pruneSlotOwnership || !entry.pluginExtensionSlotKeys) { + return; + } + // Restart cleanup prunes only ownership for slot keys that disappeared from the new registry. + const pruneRecord = (record: Record): void => { + for (const [namespace, slotKey] of Object.entries(record)) { + const normalized = normalizeSessionEntrySlotKey(slotKey); + if (normalized.ok && slotKeys.has(normalized.key)) { + delete record[namespace]; + } + } + }; + if (pluginId) { + const record = entry.pluginExtensionSlotKeys[pluginId]; + if (record) { + pruneRecord(record); + if (Object.keys(record).length === 0) { + delete entry.pluginExtensionSlotKeys[pluginId]; + } + } + } else { + for (const record of Object.values(entry.pluginExtensionSlotKeys)) { + pruneRecord(record); + } + for (const [ownerPluginId, record] of Object.entries(entry.pluginExtensionSlotKeys)) { + if (Object.keys(record).length === 0) { + delete entry.pluginExtensionSlotKeys[ownerPluginId]; + } + } + } + if (Object.keys(entry.pluginExtensionSlotKeys).length === 0) { + delete entry.pluginExtensionSlotKeys; + } +} + +/** Clears plugin-owned extension state from one session entry. */ +export function clearPluginOwnedSessionState( + entry: SessionEntry, + pluginId?: string, + sessionEntrySlotKeys?: ReadonlySet, +): void { + clearPromotedSessionEntrySlots(entry, pluginId, sessionEntrySlotKeys); + if (!pluginId) { + delete entry.pluginExtensions; + delete entry.pluginExtensionSlotKeys; + delete entry.pluginNextTurnInjections; + return; + } + if (entry.pluginExtensions) { + delete entry.pluginExtensions[pluginId]; + if (Object.keys(entry.pluginExtensions).length === 0) { + delete entry.pluginExtensions; + } + } + if (entry.pluginExtensionSlotKeys) { + delete entry.pluginExtensionSlotKeys[pluginId]; + if (Object.keys(entry.pluginExtensionSlotKeys).length === 0) { + delete entry.pluginExtensionSlotKeys; + } + } + if (entry.pluginNextTurnInjections) { + delete entry.pluginNextTurnInjections[pluginId]; + if (Object.keys(entry.pluginNextTurnInjections).length === 0) { + delete entry.pluginNextTurnInjections; + } + } +} + +function hasPromotedSessionEntrySlot( + entry: SessionEntry, + pluginId?: string, + sessionEntrySlotKeys?: ReadonlySet, +): boolean { + const slotKeys = collectPromotedSessionEntrySlotKeys(entry, pluginId, sessionEntrySlotKeys); + if (slotKeys.size === 0) { + return false; + } + const entryRecord = entry as Record; + for (const slotKey of slotKeys) { + if (Object.hasOwn(entryRecord, slotKey)) { + return true; + } + } + return false; +} + +function hasPluginOwnedSessionState( + entry: SessionEntry, + pluginId?: string, + sessionEntrySlotKeys?: ReadonlySet, +): boolean { + if (hasPromotedSessionEntrySlot(entry, pluginId, sessionEntrySlotKeys)) { + return true; + } + if (!pluginId) { + return Boolean( + entry.pluginExtensions || entry.pluginExtensionSlotKeys || entry.pluginNextTurnInjections, + ); + } + return Boolean( + entry.pluginExtensions?.[pluginId] || + entry.pluginExtensionSlotKeys?.[pluginId] || + entry.pluginNextTurnInjections?.[pluginId], + ); +} + +function matchesCleanupSession( + entryKey: string, + entry: SessionEntry, + sessionKey?: string, +): boolean { + const normalizedSessionKey = normalizeLowercaseStringOrEmpty(sessionKey); + if (!normalizedSessionKey) { + return true; + } + return ( + normalizeLowercaseStringOrEmpty(entryKey) === normalizedSessionKey || + normalizeLowercaseStringOrEmpty(entry.sessionId) === normalizedSessionKey + ); +} + +function shouldSkipCleanupStore(params: PluginHostSessionCleanupStoreParams): boolean { + if (!params.pluginId && !params.sessionKey) { + return true; + } + return params.mode === "promoted-slots" && (params.sessionEntrySlotKeys?.size ?? 0) === 0; +} + +function hasCleanupTarget( + entry: SessionEntry, + params: PluginHostSessionCleanupStoreParams, +): boolean { + if (params.mode === "promoted-slots") { + return hasPromotedSessionEntrySlot(entry, params.pluginId, params.sessionEntrySlotKeys); + } + return hasPluginOwnedSessionState(entry, params.pluginId, params.sessionEntrySlotKeys); +} + +function clearCleanupTarget( + entry: SessionEntry, + params: PluginHostSessionCleanupStoreParams, +): void { + if (params.mode === "promoted-slots") { + clearPromotedSessionEntrySlots(entry, params.pluginId, params.sessionEntrySlotKeys, { + includeStoredSlotKeys: false, + pruneSlotOwnership: true, + }); + return; + } + clearPluginOwnedSessionState(entry, params.pluginId, params.sessionEntrySlotKeys); +} + +/** Clears plugin host-owned session state in one store transaction. */ +export async function cleanupPluginHostSessionStore( + params: PluginHostSessionCleanupStoreParams, +): Promise { + if (shouldSkipCleanupStore(params) || (params.shouldCleanup && !params.shouldCleanup())) { + return 0; + } + return await updateSessionStore( + params.storePath, + (store) => { + if (params.shouldCleanup && !params.shouldCleanup()) { + return 0; + } + let clearedInStore = 0; + const now = Date.now(); + for (const [entryKey, entry] of Object.entries(store)) { + if ( + !matchesCleanupSession(entryKey, entry, params.sessionKey) || + !hasCleanupTarget(entry, params) + ) { + continue; + } + clearCleanupTarget(entry, params); + entry.updatedAt = now; + clearedInStore += 1; + } + return clearedInStore; + }, + { + skipSaveWhenResult: (clearedInStore) => clearedInStore === 0, + takeCacheOwnership: true, + }, + ); +} diff --git a/src/config/sessions/session-accessor.ts b/src/config/sessions/session-accessor.ts index 50de517606d7..45a9ba0f6096 100644 --- a/src/config/sessions/session-accessor.ts +++ b/src/config/sessions/session-accessor.ts @@ -22,6 +22,11 @@ import { resolveSessionTranscriptPathInDir, resolveStorePath, } from "./paths.js"; +import { + cleanupPluginHostSessionStore as cleanupFilePluginHostSessionStore, + clearPluginOwnedSessionState, + type PluginHostSessionCleanupStoreParams, +} from "./plugin-host-cleanup.js"; import { resolveAndPersistSessionFile } from "./session-file.js"; import type { ResolvedSessionMaintenanceConfig } from "./store-maintenance.js"; import { @@ -379,6 +384,8 @@ export type DeleteSessionEntryLifecycleParams = { target: SessionLifecycleStoreTarget; }; +export { clearPluginOwnedSessionState }; + /** Returns the entry for a canonical or alias session key, if one exists. */ export function loadSessionEntry(scope: SessionAccessScope): SessionEntry | undefined { if (scope.clone === false) { @@ -702,6 +709,17 @@ export async function purgeDeletedAgentSessionEntries( return await purgeFileDeletedAgentSessionEntries(params); } +/** + * Clears plugin host-owned state inside one resolved session store. + * This is an internal transaction-sized boundary for the storage backend, not + * a Plugin SDK API. + */ +export async function cleanupPluginHostSessionStore( + params: PluginHostSessionCleanupStoreParams, +): Promise { + return await cleanupFilePluginHostSessionStore(params); +} + /** Reads parsed transcript records from an explicit or derived transcript target. */ export async function loadTranscriptEvents( scope: SessionTranscriptReadScope, diff --git a/src/plugins/host-hook-cleanup.session-store.test.ts b/src/plugins/host-hook-cleanup.session-store.test.ts index e563290c8a7b..00aaa58f9244 100644 --- a/src/plugins/host-hook-cleanup.session-store.test.ts +++ b/src/plugins/host-hook-cleanup.session-store.test.ts @@ -101,4 +101,85 @@ describe("plugin host cleanup session stores", () => { }, }); }); + + it("clears plugin-owned session state across resolved stores without touching unrelated rows", async () => { + stateDir = await fs.mkdtemp( + path.join(resolvePreferredOpenClawTmpDir(), "openclaw-host-cleanup-multistore-"), + ); + previousStateDir = process.env.OPENCLAW_STATE_DIR; + process.env.OPENCLAW_STATE_DIR = stateDir; + const firstStorePath = path.join(stateDir, "agent-a", "sessions.json"); + const secondStorePath = path.join(stateDir, "agent-b", "sessions.json"); + const beforeUpdatedAt = 100; + const unrelatedUpdatedAt = Date.now(); + const firstEntry: SessionEntry = { + sessionId: "shared-session", + updatedAt: beforeUpdatedAt, + pluginExtensions: { + cleanup: { state: { active: true } }, + other: { state: { preserved: true } }, + }, + pluginNextTurnInjections: { + cleanup: [ + { + id: "remove", + pluginId: "cleanup", + text: "remove", + placement: "append_context", + createdAt: beforeUpdatedAt, + }, + ], + }, + }; + const secondEntry: SessionEntry = { + sessionId: "shared-session", + updatedAt: beforeUpdatedAt, + pluginExtensions: { + cleanup: { state: { active: true } }, + }, + }; + const unrelatedEntry: SessionEntry = { + sessionId: "unrelated-session", + updatedAt: unrelatedUpdatedAt, + pluginExtensions: { + cleanup: { state: { keep: true } }, + }, + }; + await saveSessionStore( + firstStorePath, + { + "agent:a:main": firstEntry, + "agent:a:unrelated": unrelatedEntry, + }, + { skipMaintenance: true }, + ); + await saveSessionStore( + secondStorePath, + { + "agent:b:other": secondEntry, + }, + { skipMaintenance: true }, + ); + + const result = await runPluginHostCleanup({ + cfg: { session: { store: firstStorePath } }, + registry: createEmptyPluginRegistry(), + pluginId: "cleanup", + reason: "disable", + sessionKey: "shared-session", + sessionStorePaths: [firstStorePath, secondStorePath], + }); + + expect(result).toEqual({ cleanupCount: 2, failures: [] }); + const firstStore = loadSessionStore(firstStorePath, { skipCache: true }); + const secondStore = loadSessionStore(secondStorePath, { skipCache: true }); + expect(firstStore["agent:a:main"]?.pluginExtensions).toEqual({ + other: { state: { preserved: true } }, + }); + expect(firstStore["agent:a:main"]?.pluginNextTurnInjections).toBeUndefined(); + expect(firstStore["agent:a:main"]?.updatedAt).toBeGreaterThan(beforeUpdatedAt); + expect(firstStore["agent:a:unrelated"]).toEqual(unrelatedEntry); + expect(secondStore["agent:b:other"]?.pluginExtensions).toBeUndefined(); + expect(secondStore["agent:b:other"]?.updatedAt).toBeGreaterThan(beforeUpdatedAt); + }); }); diff --git a/src/plugins/host-hook-cleanup.ts b/src/plugins/host-hook-cleanup.ts index e4806e4c6455..51b6cd5f2873 100644 --- a/src/plugins/host-hook-cleanup.ts +++ b/src/plugins/host-hook-cleanup.ts @@ -1,10 +1,11 @@ /** Runs plugin cleanup callbacks and clears host-side plugin session/runtime state. */ import fs from "node:fs"; -import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { getRuntimeConfig } from "../config/config.js"; -import { updateSessionStore } from "../config/sessions/store.js"; +import { + cleanupPluginHostSessionStore, + clearPluginOwnedSessionState, +} from "../config/sessions/session-accessor.js"; import { resolveAllAgentSessionStoreTargetsSync } from "../config/sessions/targets.js"; -import type { SessionEntry } from "../config/sessions/types.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { withPluginHostCleanupTimeout } from "./host-hook-cleanup-timeout.js"; import { @@ -17,6 +18,8 @@ import type { PluginRegistry } from "./registry-types.js"; import { getActivePluginRegistry } from "./runtime.js"; import { normalizeSessionEntrySlotKey } from "./session-entry-slot-keys.js"; +export { clearPluginOwnedSessionState }; + /** Failure captured while running plugin cleanup hooks. */ /** Failure captured while running one plugin cleanup callback. */ export type PluginHostCleanupFailure = { @@ -37,176 +40,6 @@ function shouldCleanPlugin(pluginId: string, filterPluginId?: string): boolean { return !filterPluginId || pluginId === filterPluginId; } -function collectStoredSessionEntrySlotKeys(entry: SessionEntry, pluginId?: string): Set { - const slotKeys = new Set(); - const storedSlotKeys = entry.pluginExtensionSlotKeys; - if (!storedSlotKeys) { - return slotKeys; - } - const records = - pluginId === undefined - ? Object.values(storedSlotKeys) - : storedSlotKeys[pluginId] - ? [storedSlotKeys[pluginId]] - : []; - for (const record of records) { - for (const slotKey of Object.values(record)) { - const normalized = normalizeSessionEntrySlotKey(slotKey); - if (normalized.ok) { - slotKeys.add(normalized.key); - } - } - } - return slotKeys; -} - -function collectPromotedSessionEntrySlotKeys( - entry: SessionEntry, - pluginId?: string, - sessionEntrySlotKeys?: ReadonlySet, -): Set { - const slotKeys = collectStoredSessionEntrySlotKeys(entry, pluginId); - for (const slotKey of sessionEntrySlotKeys ?? []) { - slotKeys.add(slotKey); - } - return slotKeys; -} - -function clearPromotedSessionEntrySlots( - entry: SessionEntry, - pluginId?: string, - sessionEntrySlotKeys?: ReadonlySet, - options: { includeStoredSlotKeys?: boolean; pruneSlotOwnership?: boolean } = {}, -): void { - const slotKeys = - options.includeStoredSlotKeys === false && sessionEntrySlotKeys - ? new Set(sessionEntrySlotKeys) - : collectPromotedSessionEntrySlotKeys(entry, pluginId, sessionEntrySlotKeys); - const entryRecord = entry as Record; - for (const slotKey of slotKeys) { - delete entryRecord[slotKey]; - } - if (!options.pruneSlotOwnership || !entry.pluginExtensionSlotKeys) { - return; - } - // Restart cleanup prunes only ownership for slot keys that disappeared from the new registry. - const pruneRecord = (record: Record): void => { - for (const [namespace, slotKey] of Object.entries(record)) { - const normalized = normalizeSessionEntrySlotKey(slotKey); - if (normalized.ok && slotKeys.has(normalized.key)) { - delete record[namespace]; - } - } - }; - if (pluginId) { - const record = entry.pluginExtensionSlotKeys[pluginId]; - if (record) { - pruneRecord(record); - if (Object.keys(record).length === 0) { - delete entry.pluginExtensionSlotKeys[pluginId]; - } - } - } else { - for (const record of Object.values(entry.pluginExtensionSlotKeys)) { - pruneRecord(record); - } - for (const [ownerPluginId, record] of Object.entries(entry.pluginExtensionSlotKeys)) { - if (Object.keys(record).length === 0) { - delete entry.pluginExtensionSlotKeys[ownerPluginId]; - } - } - } - if (Object.keys(entry.pluginExtensionSlotKeys).length === 0) { - delete entry.pluginExtensionSlotKeys; - } -} - -/** Clears plugin-owned extension state from one session entry. */ -export function clearPluginOwnedSessionState( - entry: SessionEntry, - pluginId?: string, - sessionEntrySlotKeys?: ReadonlySet, -): void { - clearPromotedSessionEntrySlots(entry, pluginId, sessionEntrySlotKeys); - if (!pluginId) { - delete entry.pluginExtensions; - delete entry.pluginExtensionSlotKeys; - delete entry.pluginNextTurnInjections; - return; - } - if (entry.pluginExtensions) { - delete entry.pluginExtensions[pluginId]; - if (Object.keys(entry.pluginExtensions).length === 0) { - delete entry.pluginExtensions; - } - } - if (entry.pluginExtensionSlotKeys) { - delete entry.pluginExtensionSlotKeys[pluginId]; - if (Object.keys(entry.pluginExtensionSlotKeys).length === 0) { - delete entry.pluginExtensionSlotKeys; - } - } - if (entry.pluginNextTurnInjections) { - delete entry.pluginNextTurnInjections[pluginId]; - if (Object.keys(entry.pluginNextTurnInjections).length === 0) { - delete entry.pluginNextTurnInjections; - } - } -} - -function hasPromotedSessionEntrySlot( - entry: SessionEntry, - pluginId?: string, - sessionEntrySlotKeys?: ReadonlySet, -): boolean { - const slotKeys = collectPromotedSessionEntrySlotKeys(entry, pluginId, sessionEntrySlotKeys); - if (slotKeys.size === 0) { - return false; - } - const entryRecord = entry as Record; - for (const slotKey of slotKeys) { - if (Object.hasOwn(entryRecord, slotKey)) { - return true; - } - } - return false; -} - -function hasPluginOwnedSessionState( - entry: SessionEntry, - pluginId?: string, - sessionEntrySlotKeys?: ReadonlySet, -): boolean { - if (hasPromotedSessionEntrySlot(entry, pluginId, sessionEntrySlotKeys)) { - return true; - } - if (!pluginId) { - return Boolean( - entry.pluginExtensions || entry.pluginExtensionSlotKeys || entry.pluginNextTurnInjections, - ); - } - return Boolean( - entry.pluginExtensions?.[pluginId] || - entry.pluginExtensionSlotKeys?.[pluginId] || - entry.pluginNextTurnInjections?.[pluginId], - ); -} - -function matchesCleanupSession( - entryKey: string, - entry: SessionEntry, - sessionKey?: string, -): boolean { - const normalizedSessionKey = normalizeLowercaseStringOrEmpty(sessionKey); - if (!normalizedSessionKey) { - return true; - } - return ( - normalizeLowercaseStringOrEmpty(entryKey) === normalizedSessionKey || - normalizeLowercaseStringOrEmpty(entry.sessionId) === normalizedSessionKey - ); -} - function resolveExistingSessionStorePaths(cfg: OpenClawConfig): string[] { return [ ...new Set( @@ -257,32 +90,14 @@ async function clearPluginOwnedSessionStores(params: { if (params.shouldCleanup && !params.shouldCleanup()) { break; } - cleared += await updateSessionStore( + cleared += await cleanupPluginHostSessionStore({ storePath, - (store) => { - if (params.shouldCleanup && !params.shouldCleanup()) { - return 0; - } - let clearedInStore = 0; - const now = Date.now(); - for (const [entryKey, entry] of Object.entries(store)) { - if ( - !matchesCleanupSession(entryKey, entry, params.sessionKey) || - !hasPluginOwnedSessionState(entry, params.pluginId, params.sessionEntrySlotKeys) - ) { - continue; - } - clearPluginOwnedSessionState(entry, params.pluginId, params.sessionEntrySlotKeys); - entry.updatedAt = now; - clearedInStore += 1; - } - return clearedInStore; - }, - { - skipSaveWhenResult: (clearedInStore) => clearedInStore === 0, - takeCacheOwnership: true, - }, - ); + mode: "plugin-owned-state", + pluginId: params.pluginId, + sessionKey: params.sessionKey, + sessionEntrySlotKeys: params.sessionEntrySlotKeys, + shouldCleanup: params.shouldCleanup, + }); } return cleared; } @@ -305,35 +120,14 @@ async function clearPromotedSessionEntrySlotStores(params: { if (params.shouldCleanup && !params.shouldCleanup()) { break; } - cleared += await updateSessionStore( + cleared += await cleanupPluginHostSessionStore({ storePath, - (store) => { - if (params.shouldCleanup && !params.shouldCleanup()) { - return 0; - } - let clearedInStore = 0; - const now = Date.now(); - for (const [entryKey, entry] of Object.entries(store)) { - if ( - !matchesCleanupSession(entryKey, entry, params.sessionKey) || - !hasPromotedSessionEntrySlot(entry, params.pluginId, params.sessionEntrySlotKeys) - ) { - continue; - } - clearPromotedSessionEntrySlots(entry, params.pluginId, params.sessionEntrySlotKeys, { - includeStoredSlotKeys: false, - pruneSlotOwnership: true, - }); - entry.updatedAt = now; - clearedInStore += 1; - } - return clearedInStore; - }, - { - skipSaveWhenResult: (clearedInStore) => clearedInStore === 0, - takeCacheOwnership: true, - }, - ); + mode: "promoted-slots", + pluginId: params.pluginId, + sessionKey: params.sessionKey, + sessionEntrySlotKeys: params.sessionEntrySlotKeys, + shouldCleanup: params.shouldCleanup, + }); } return cleared; } diff --git a/test/scripts/check-session-accessor-boundary.test.ts b/test/scripts/check-session-accessor-boundary.test.ts index f9fcf56db6bb..942c2b221558 100644 --- a/test/scripts/check-session-accessor-boundary.test.ts +++ b/test/scripts/check-session-accessor-boundary.test.ts @@ -83,6 +83,7 @@ describe("session accessor boundary guard", () => { "src/auto-reply/reply/session-reset-model.ts", "src/auto-reply/reply/session-updates.ts", "src/auto-reply/reply/session-usage.ts", + "src/plugins/host-hook-cleanup.ts", "src/tui/embedded-backend.ts", "src/config/sessions/cleanup-service.ts", ]),