




















1+/** File-backed implementation for plugin host-owned session-state cleanup. */
2+import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
3+import { normalizeSessionEntrySlotKey } from "../../plugins/session-entry-slot-keys.js";
4+import { updateSessionStore } from "./store.js";
5+import type { SessionEntry } from "./types.js";
6+7+/** Cleanup variants owned by plugin host lifecycle paths. */
8+export type PluginHostSessionCleanupMode = "plugin-owned-state" | "promoted-slots";
9+10+export type PluginHostSessionCleanupStoreParams = {
11+/** Cleanup mode chosen by the plugin host lifecycle reason. */
12+mode: PluginHostSessionCleanupMode;
13+/** Plugin owner to clear. Omit only for session-scoped all-plugin cleanup. */
14+pluginId?: string;
15+/** Optional canonical key, alias, or runtime session id filter. */
16+sessionKey?: string;
17+/** Promoted SessionEntry slots declared by the plugin registry. */
18+sessionEntrySlotKeys?: ReadonlySet<string>;
19+/** Per-store file-backed transaction boundary. */
20+storePath: string;
21+/** Cancels the cleanup before persistence when host lifecycle state changes. */
22+shouldCleanup?: () => boolean;
23+};
24+25+function collectStoredSessionEntrySlotKeys(entry: SessionEntry, pluginId?: string): Set<string> {
26+const slotKeys = new Set<string>();
27+const storedSlotKeys = entry.pluginExtensionSlotKeys;
28+if (!storedSlotKeys) {
29+return slotKeys;
30+}
31+const records =
32+pluginId === undefined
33+ ? Object.values(storedSlotKeys)
34+ : storedSlotKeys[pluginId]
35+ ? [storedSlotKeys[pluginId]]
36+ : [];
37+for (const record of records) {
38+for (const slotKey of Object.values(record)) {
39+const normalized = normalizeSessionEntrySlotKey(slotKey);
40+if (normalized.ok) {
41+slotKeys.add(normalized.key);
42+}
43+}
44+}
45+return slotKeys;
46+}
47+48+function collectPromotedSessionEntrySlotKeys(
49+entry: SessionEntry,
50+pluginId?: string,
51+sessionEntrySlotKeys?: ReadonlySet<string>,
52+): Set<string> {
53+const slotKeys = collectStoredSessionEntrySlotKeys(entry, pluginId);
54+for (const slotKey of sessionEntrySlotKeys ?? []) {
55+slotKeys.add(slotKey);
56+}
57+return slotKeys;
58+}
59+60+function clearPromotedSessionEntrySlots(
61+entry: SessionEntry,
62+pluginId?: string,
63+sessionEntrySlotKeys?: ReadonlySet<string>,
64+options: { includeStoredSlotKeys?: boolean; pruneSlotOwnership?: boolean } = {},
65+): void {
66+const slotKeys =
67+options.includeStoredSlotKeys === false && sessionEntrySlotKeys
68+ ? new Set(sessionEntrySlotKeys)
69+ : collectPromotedSessionEntrySlotKeys(entry, pluginId, sessionEntrySlotKeys);
70+const entryRecord = entry as Record<string, unknown>;
71+for (const slotKey of slotKeys) {
72+delete entryRecord[slotKey];
73+}
74+if (!options.pruneSlotOwnership || !entry.pluginExtensionSlotKeys) {
75+return;
76+}
77+// Restart cleanup prunes only ownership for slot keys that disappeared from the new registry.
78+const pruneRecord = (record: Record<string, string>): void => {
79+for (const [namespace, slotKey] of Object.entries(record)) {
80+const normalized = normalizeSessionEntrySlotKey(slotKey);
81+if (normalized.ok && slotKeys.has(normalized.key)) {
82+delete record[namespace];
83+}
84+}
85+};
86+if (pluginId) {
87+const record = entry.pluginExtensionSlotKeys[pluginId];
88+if (record) {
89+pruneRecord(record);
90+if (Object.keys(record).length === 0) {
91+delete entry.pluginExtensionSlotKeys[pluginId];
92+}
93+}
94+} else {
95+for (const record of Object.values(entry.pluginExtensionSlotKeys)) {
96+pruneRecord(record);
97+}
98+for (const [ownerPluginId, record] of Object.entries(entry.pluginExtensionSlotKeys)) {
99+if (Object.keys(record).length === 0) {
100+delete entry.pluginExtensionSlotKeys[ownerPluginId];
101+}
102+}
103+}
104+if (Object.keys(entry.pluginExtensionSlotKeys).length === 0) {
105+delete entry.pluginExtensionSlotKeys;
106+}
107+}
108+109+/** Clears plugin-owned extension state from one session entry. */
110+export function clearPluginOwnedSessionState(
111+entry: SessionEntry,
112+pluginId?: string,
113+sessionEntrySlotKeys?: ReadonlySet<string>,
114+): void {
115+clearPromotedSessionEntrySlots(entry, pluginId, sessionEntrySlotKeys);
116+if (!pluginId) {
117+delete entry.pluginExtensions;
118+delete entry.pluginExtensionSlotKeys;
119+delete entry.pluginNextTurnInjections;
120+return;
121+}
122+if (entry.pluginExtensions) {
123+delete entry.pluginExtensions[pluginId];
124+if (Object.keys(entry.pluginExtensions).length === 0) {
125+delete entry.pluginExtensions;
126+}
127+}
128+if (entry.pluginExtensionSlotKeys) {
129+delete entry.pluginExtensionSlotKeys[pluginId];
130+if (Object.keys(entry.pluginExtensionSlotKeys).length === 0) {
131+delete entry.pluginExtensionSlotKeys;
132+}
133+}
134+if (entry.pluginNextTurnInjections) {
135+delete entry.pluginNextTurnInjections[pluginId];
136+if (Object.keys(entry.pluginNextTurnInjections).length === 0) {
137+delete entry.pluginNextTurnInjections;
138+}
139+}
140+}
141+142+function hasPromotedSessionEntrySlot(
143+entry: SessionEntry,
144+pluginId?: string,
145+sessionEntrySlotKeys?: ReadonlySet<string>,
146+): boolean {
147+const slotKeys = collectPromotedSessionEntrySlotKeys(entry, pluginId, sessionEntrySlotKeys);
148+if (slotKeys.size === 0) {
149+return false;
150+}
151+const entryRecord = entry as Record<string, unknown>;
152+for (const slotKey of slotKeys) {
153+if (Object.hasOwn(entryRecord, slotKey)) {
154+return true;
155+}
156+}
157+return false;
158+}
159+160+function hasPluginOwnedSessionState(
161+entry: SessionEntry,
162+pluginId?: string,
163+sessionEntrySlotKeys?: ReadonlySet<string>,
164+): boolean {
165+if (hasPromotedSessionEntrySlot(entry, pluginId, sessionEntrySlotKeys)) {
166+return true;
167+}
168+if (!pluginId) {
169+return Boolean(
170+entry.pluginExtensions || entry.pluginExtensionSlotKeys || entry.pluginNextTurnInjections,
171+);
172+}
173+return Boolean(
174+entry.pluginExtensions?.[pluginId] ||
175+entry.pluginExtensionSlotKeys?.[pluginId] ||
176+entry.pluginNextTurnInjections?.[pluginId],
177+);
178+}
179+180+function matchesCleanupSession(
181+entryKey: string,
182+entry: SessionEntry,
183+sessionKey?: string,
184+): boolean {
185+const normalizedSessionKey = normalizeLowercaseStringOrEmpty(sessionKey);
186+if (!normalizedSessionKey) {
187+return true;
188+}
189+return (
190+normalizeLowercaseStringOrEmpty(entryKey) === normalizedSessionKey ||
191+normalizeLowercaseStringOrEmpty(entry.sessionId) === normalizedSessionKey
192+);
193+}
194+195+function shouldSkipCleanupStore(params: PluginHostSessionCleanupStoreParams): boolean {
196+if (!params.pluginId && !params.sessionKey) {
197+return true;
198+}
199+return params.mode === "promoted-slots" && (params.sessionEntrySlotKeys?.size ?? 0) === 0;
200+}
201+202+function hasCleanupTarget(
203+entry: SessionEntry,
204+params: PluginHostSessionCleanupStoreParams,
205+): boolean {
206+if (params.mode === "promoted-slots") {
207+return hasPromotedSessionEntrySlot(entry, params.pluginId, params.sessionEntrySlotKeys);
208+}
209+return hasPluginOwnedSessionState(entry, params.pluginId, params.sessionEntrySlotKeys);
210+}
211+212+function clearCleanupTarget(
213+entry: SessionEntry,
214+params: PluginHostSessionCleanupStoreParams,
215+): void {
216+if (params.mode === "promoted-slots") {
217+clearPromotedSessionEntrySlots(entry, params.pluginId, params.sessionEntrySlotKeys, {
218+includeStoredSlotKeys: false,
219+pruneSlotOwnership: true,
220+});
221+return;
222+}
223+clearPluginOwnedSessionState(entry, params.pluginId, params.sessionEntrySlotKeys);
224+}
225+226+/** Clears plugin host-owned session state in one store transaction. */
227+export async function cleanupPluginHostSessionStore(
228+params: PluginHostSessionCleanupStoreParams,
229+): Promise<number> {
230+if (shouldSkipCleanupStore(params) || (params.shouldCleanup && !params.shouldCleanup())) {
231+return 0;
232+}
233+return await updateSessionStore(
234+params.storePath,
235+(store) => {
236+if (params.shouldCleanup && !params.shouldCleanup()) {
237+return 0;
238+}
239+let clearedInStore = 0;
240+const now = Date.now();
241+for (const [entryKey, entry] of Object.entries(store)) {
242+if (
243+!matchesCleanupSession(entryKey, entry, params.sessionKey) ||
244+!hasCleanupTarget(entry, params)
245+) {
246+continue;
247+}
248+clearCleanupTarget(entry, params);
249+entry.updatedAt = now;
250+clearedInStore += 1;
251+}
252+return clearedInStore;
253+},
254+{
255+skipSaveWhenResult: (clearedInStore) => clearedInStore === 0,
256+takeCacheOwnership: true,
257+},
258+);
259+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。