Files
openclaw/extensions/active-memory/doctor-contract-api.ts
Peter Steinberger eafe2a8d0b refactor: consolidate duplicated plugin state and doctor migration plumbing onto SDK seams (#99850)
* refactor(plugin-sdk): add createPersistentDedupeCache and migrate channel presence caches

* refactor(matrix): adopt SDK approval reaction target store

* refactor(plugin-sdk): share doctor legacy-state migration fs helpers

* refactor(memory-core): dedupe qmd cache entry envelope validation

* chore(plugin-sdk): pin surface budgets for shared dedupe and doctor helpers

* test(matrix): use future approval expiry fixtures for reaction targets

* test(matrix): use future approval expiry fixtures for reaction targets
2026-07-04 01:51:03 -07:00

125 lines
4.3 KiB
TypeScript

/**
* Doctor migration contract for Active Memory state. It moves legacy per-session
* toggle JSON into the plugin state keyed store used by current runtimes.
*/
import crypto from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import {
archiveLegacyStateSource,
type PluginDoctorStateMigration,
} from "openclaw/plugin-sdk/runtime-doctor";
type ActiveMemoryToggleEntry = {
sessionKey: string;
disabled: boolean;
updatedAt: number;
};
const TOGGLE_STATE_FILE = "session-toggles.json";
const SESSION_TOGGLES_NAMESPACE = "session-toggles";
const MAX_TOGGLE_ENTRIES = 10_000;
function resolveToggleStatePath(stateDir: string): string {
return path.join(stateDir, "plugins", "active-memory", TOGGLE_STATE_FILE);
}
function activeMemoryToggleKey(sessionKey: string): string {
return crypto.createHash("sha256").update(sessionKey, "utf8").digest("hex");
}
async function readLegacyToggleEntries(filePath: string): Promise<ActiveMemoryToggleEntry[]> {
try {
const parsed = JSON.parse(await fs.readFile(filePath, "utf8")) as unknown;
if (!parsed || typeof parsed !== "object") {
return [];
}
const sessions = (parsed as { sessions?: unknown }).sessions;
if (!sessions || typeof sessions !== "object" || Array.isArray(sessions)) {
return [];
}
const entries: ActiveMemoryToggleEntry[] = [];
for (const [sessionKey, value] of Object.entries(sessions)) {
if (!sessionKey.trim() || !value || typeof value !== "object" || Array.isArray(value)) {
continue;
}
if ((value as { disabled?: unknown }).disabled !== true) {
continue;
}
const updatedAt =
typeof (value as { updatedAt?: unknown }).updatedAt === "number"
? (value as { updatedAt: number }).updatedAt
: Date.now();
entries.push({ sessionKey, disabled: true, updatedAt });
}
return entries;
} catch {
return [];
}
}
/** State migrations exposed to OpenClaw doctor for Active Memory. */
export const stateMigrations: PluginDoctorStateMigration[] = [
{
id: "active-memory-session-toggles-json-to-plugin-state",
label: "Active Memory session toggles",
async detectLegacyState(params) {
const filePath = resolveToggleStatePath(params.stateDir);
const entries = await readLegacyToggleEntries(filePath);
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: string[] = [];
const warnings: string[] = [];
const filePath = resolveToggleStatePath(params.stateDir);
const entries = await readLegacyToggleEntries(filePath);
if (entries.length === 0) {
return { changes, warnings };
}
const store = params.context.openPluginStateKeyedStore<ActiveMemoryToggleEntry>({
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 archiveLegacyStateSource({
filePath,
label: "Active Memory session toggles",
changes,
warnings,
});
return { changes, warnings };
},
},
];