UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

114 lines (113 loc) 4.14 kB
import path from "node:path"; import fs from "node:fs/promises"; import crypto from "node:crypto"; //#region extensions/active-memory/doctor-contract-api.ts /** * Doctor migration contract for Active Memory state. It moves legacy per-session * toggle JSON into the plugin state keyed store used by current runtimes. */ const TOGGLE_STATE_FILE = "session-toggles.json"; const SESSION_TOGGLES_NAMESPACE = "session-toggles"; const MAX_TOGGLE_ENTRIES = 1e4; function resolveToggleStatePath(stateDir) { return path.join(stateDir, "plugins", "active-memory", TOGGLE_STATE_FILE); } function activeMemoryToggleKey(sessionKey) { return crypto.createHash("sha256").update(sessionKey, "utf8").digest("hex"); } async function fileExists(filePath) { try { return (await fs.stat(filePath)).isFile(); } catch { return false; } } async function readLegacyToggleEntries(filePath) { try { const parsed = JSON.parse(await fs.readFile(filePath, "utf8")); if (!parsed || typeof parsed !== "object") return []; const sessions = parsed.sessions; if (!sessions || typeof sessions !== "object" || Array.isArray(sessions)) return []; const entries = []; for (const [sessionKey, value] of Object.entries(sessions)) { if (!sessionKey.trim() || !value || typeof value !== "object" || Array.isArray(value)) continue; if (value.disabled !== true) continue; const updatedAt = typeof value.updatedAt === "number" ? value.updatedAt : Date.now(); entries.push({ sessionKey, disabled: true, updatedAt }); } return entries; } catch { return []; } } async function archiveLegacySource(params) { const archivedPath = `${params.filePath}.migrated`; if (await fileExists(archivedPath)) { params.warnings.push(`Left migrated ${params.label} source in place because ${archivedPath} already exists`); return; } try { await fs.rename(params.filePath, archivedPath); params.changes.push(`Archived ${params.label} legacy source -> ${archivedPath}`); } catch (err) { params.warnings.push(`Failed archiving ${params.label} legacy source: ${String(err)}`); } } /** State migrations exposed to OpenClaw doctor for Active Memory. */ const stateMigrations = [{ id: "active-memory-session-toggles-json-to-plugin-state", label: "Active Memory session toggles", async detectLegacyState(params) { const entries = await readLegacyToggleEntries(resolveToggleStatePath(params.stateDir)); if (entries.length === 0) return null; return { preview: [`- Active Memory session toggles: ${entries.length} ${entries.length === 1 ? "entry" : "entries"} -> plugin state (${SESSION_TOGGLES_NAMESPACE})`] }; }, async migrateLegacyState(params) { const changes = []; const warnings = []; const filePath = resolveToggleStatePath(params.stateDir); const entries = await readLegacyToggleEntries(filePath); if (entries.length === 0) return { changes, warnings }; const store = params.context.openPluginStateKeyedStore({ namespace: SESSION_TOGGLES_NAMESPACE, maxEntries: MAX_TOGGLE_ENTRIES }); const existingKeys = new Set((await store.entries()).map((entry) => entry.key)); const missingEntries = entries.filter((entry) => !existingKeys.has(activeMemoryToggleKey(entry.sessionKey))); if (missingEntries.length > MAX_TOGGLE_ENTRIES - existingKeys.size) { warnings.push(`Skipped Active Memory session toggle migration because plugin state has room for ${MAX_TOGGLE_ENTRIES - existingKeys.size} of ${missingEntries.length} missing entries; left legacy source in place`); return { changes, warnings }; } let imported = 0; for (const entry of entries) { const key = activeMemoryToggleKey(entry.sessionKey); if (existingKeys.has(key)) continue; await store.register(key, entry); existingKeys.add(key); imported++; } if (imported > 0) changes.push(`Migrated ${imported} Active Memory session toggle ${imported === 1 ? "entry" : "entries"} -> plugin state`); await archiveLegacySource({ filePath, label: "Active Memory session toggles", changes, warnings }); return { changes, warnings }; } }]; //#endregion export { stateMigrations };