mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-28 02:33:08 +00:00
fix: migrate active memory toggles
This commit is contained in:
@@ -458,6 +458,79 @@ describe("state migrations", () => {
|
||||
await expectMissingPath(legacyPluginStatePath);
|
||||
});
|
||||
|
||||
it("imports legacy Active Memory session toggles into unified plugin state", async () => {
|
||||
const root = await createTempDir();
|
||||
const stateDir = path.join(root, ".openclaw");
|
||||
const env = createEnv(stateDir);
|
||||
const cfg = createConfig();
|
||||
const legacyTogglePath = path.join(
|
||||
stateDir,
|
||||
"plugins",
|
||||
"active-memory",
|
||||
"session-toggles.json",
|
||||
);
|
||||
await fs.mkdir(path.dirname(legacyTogglePath), { recursive: true });
|
||||
await fs.writeFile(
|
||||
legacyTogglePath,
|
||||
`${JSON.stringify(
|
||||
{
|
||||
sessions: {
|
||||
"agent:main:disabled": { disabled: true, updatedAt: 111 },
|
||||
"agent:main:enabled": { disabled: false, updatedAt: 222 },
|
||||
" ": { disabled: true, updatedAt: 333 },
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const detected = await detectLegacyStateMigrations({
|
||||
cfg,
|
||||
env,
|
||||
homedir: () => root,
|
||||
});
|
||||
|
||||
expect(detected.preview).toEqual([
|
||||
`- Active Memory session toggles: ${legacyTogglePath} → SQLite`,
|
||||
]);
|
||||
|
||||
const result = await runLegacyStateMigrations({
|
||||
detected,
|
||||
now: () => 1234,
|
||||
});
|
||||
|
||||
expect(result.warnings).toStrictEqual([]);
|
||||
expect(result.changes).toEqual([
|
||||
"Imported 1 Active Memory session toggle(s) into SQLite plugin state",
|
||||
]);
|
||||
|
||||
const stateDatabase = openOpenClawStateDatabase({ env });
|
||||
const db = getNodeSqliteKysely<PluginStateTestDatabase>(stateDatabase.db);
|
||||
const rows = executeSqliteQuerySync(
|
||||
stateDatabase.db,
|
||||
db
|
||||
.selectFrom("plugin_state_entries")
|
||||
.select(["plugin_id", "namespace", "entry_key", "value_json", "created_at", "expires_at"])
|
||||
.orderBy("plugin_id", "asc")
|
||||
.orderBy("namespace", "asc")
|
||||
.orderBy("entry_key", "asc"),
|
||||
).rows;
|
||||
|
||||
expect(rows).toEqual([
|
||||
{
|
||||
plugin_id: "active-memory",
|
||||
namespace: "session-toggles",
|
||||
entry_key: "agent:main:disabled",
|
||||
value_json: '{"version":1,"disabled":true,"updatedAt":111}',
|
||||
created_at: 111,
|
||||
expires_at: null,
|
||||
},
|
||||
]);
|
||||
await expectMissingPath(legacyTogglePath);
|
||||
});
|
||||
|
||||
it("migrates legacy sessions for every configured agent", async () => {
|
||||
const root = await createTempDir();
|
||||
const stateDir = path.join(root, ".openclaw");
|
||||
|
||||
@@ -128,6 +128,12 @@ type MigrationSourceReport = {
|
||||
recordCount?: number;
|
||||
};
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function parseJsonlEvents(filePath: string): unknown[] {
|
||||
const raw = fs.readFileSync(filePath, "utf-8");
|
||||
const events: unknown[] = [];
|
||||
@@ -1373,6 +1379,33 @@ function collectCoreLegacyStateMigrationPlans(params: {
|
||||
},
|
||||
});
|
||||
}
|
||||
const activeMemorySessionTogglesPath = resolveLegacyActiveMemorySessionTogglesPath(
|
||||
params.stateDir,
|
||||
);
|
||||
if (fileExists(activeMemorySessionTogglesPath)) {
|
||||
plans.push({
|
||||
kind: "custom",
|
||||
label: "Active Memory session toggles",
|
||||
sourcePath: activeMemorySessionTogglesPath,
|
||||
targetTable: "plugin_state_entries",
|
||||
recordCount: countLegacyActiveMemorySessionToggleRecords(activeMemorySessionTogglesPath),
|
||||
apply: () => {
|
||||
const result = importLegacyActiveMemorySessionTogglesToSqlite(
|
||||
activeMemorySessionTogglesPath,
|
||||
params.env,
|
||||
);
|
||||
return {
|
||||
changes:
|
||||
result.imported > 0
|
||||
? [
|
||||
`Imported ${result.imported} Active Memory session toggle(s) into SQLite plugin state`,
|
||||
]
|
||||
: [],
|
||||
warnings: result.warnings,
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
const acpxGatewayInstancePath = path.join(params.stateDir, "gateway-instance-id");
|
||||
if (fileExists(acpxGatewayInstancePath)) {
|
||||
plans.push({
|
||||
@@ -1891,11 +1924,23 @@ const CLAWHUB_SKILL_STATE_OWNER_ID = "core:clawhub-skills";
|
||||
const CLAWHUB_SKILL_STATE_NAMESPACE = "skill-installs";
|
||||
const DEFAULT_CLAWHUB_URL = "https://clawhub.ai";
|
||||
const LEGACY_PLUGIN_STATE_SIDECAR_SUFFIXES = ["", "-shm", "-wal"] as const;
|
||||
const ACTIVE_MEMORY_PLUGIN_STATE_OWNER_ID = "active-memory";
|
||||
const ACTIVE_MEMORY_SESSION_TOGGLES_NAMESPACE = "session-toggles";
|
||||
const LEGACY_ACTIVE_MEMORY_SESSION_TOGGLES_FILE = "session-toggles.json";
|
||||
|
||||
function resolveLegacyPluginStateSqlitePath(stateDir: string): string {
|
||||
return path.join(stateDir, "plugin-state", "state.sqlite");
|
||||
}
|
||||
|
||||
function resolveLegacyActiveMemorySessionTogglesPath(stateDir: string): string {
|
||||
return path.join(
|
||||
stateDir,
|
||||
"plugins",
|
||||
ACTIVE_MEMORY_PLUGIN_STATE_OWNER_ID,
|
||||
LEGACY_ACTIVE_MEMORY_SESSION_TOGGLES_FILE,
|
||||
);
|
||||
}
|
||||
|
||||
function readLegacyPluginStateSqliteRows(sourcePath: string): {
|
||||
rows: PluginStateMigrationRow[];
|
||||
warnings: string[];
|
||||
@@ -1974,6 +2019,102 @@ function importLegacyPluginStateSqliteToUnified(
|
||||
return { imported: result.rows.length, warnings: [] };
|
||||
}
|
||||
|
||||
function readLegacyActiveMemorySessionToggleRows(sourcePath: string): {
|
||||
rows: PluginStateMigrationRow[];
|
||||
warnings: string[];
|
||||
} {
|
||||
let fallbackUpdatedAt = 0;
|
||||
try {
|
||||
fallbackUpdatedAt = Math.trunc(fs.statSync(sourcePath).mtimeMs);
|
||||
} catch {
|
||||
// keep deterministic zero fallback when stat fails
|
||||
}
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(fs.readFileSync(sourcePath, "utf8")) as unknown;
|
||||
} catch (error) {
|
||||
return {
|
||||
rows: [],
|
||||
warnings: [`Failed reading legacy Active Memory toggles (${sourcePath}): ${String(error)}`],
|
||||
};
|
||||
}
|
||||
|
||||
const sessions = asRecord(parsed)?.sessions;
|
||||
if (!sessions || typeof sessions !== "object" || Array.isArray(sessions)) {
|
||||
return { rows: [], warnings: [] };
|
||||
}
|
||||
|
||||
const rows: PluginStateMigrationRow[] = [];
|
||||
for (const [rawSessionKey, rawValue] of Object.entries(sessions)) {
|
||||
const sessionKey = rawSessionKey.trim();
|
||||
const value = asRecord(rawValue);
|
||||
if (!sessionKey || value?.disabled !== true) {
|
||||
continue;
|
||||
}
|
||||
const updatedAt =
|
||||
typeof value.updatedAt === "number" && Number.isFinite(value.updatedAt)
|
||||
? Math.trunc(value.updatedAt)
|
||||
: fallbackUpdatedAt;
|
||||
rows.push({
|
||||
plugin_id: ACTIVE_MEMORY_PLUGIN_STATE_OWNER_ID,
|
||||
namespace: ACTIVE_MEMORY_SESSION_TOGGLES_NAMESPACE,
|
||||
entry_key: sessionKey,
|
||||
value_json: JSON.stringify({
|
||||
version: 1,
|
||||
disabled: true,
|
||||
updatedAt,
|
||||
}),
|
||||
created_at: updatedAt,
|
||||
expires_at: null,
|
||||
});
|
||||
}
|
||||
return { rows, warnings: [] };
|
||||
}
|
||||
|
||||
function countLegacyActiveMemorySessionToggleRecords(sourcePath: string): number | undefined {
|
||||
const result = readLegacyActiveMemorySessionToggleRows(sourcePath);
|
||||
return result.warnings.length === 0 ? result.rows.length : undefined;
|
||||
}
|
||||
|
||||
function importLegacyActiveMemorySessionTogglesToSqlite(
|
||||
sourcePath: string,
|
||||
env: NodeJS.ProcessEnv,
|
||||
): { imported: number; warnings: string[] } {
|
||||
const result = readLegacyActiveMemorySessionToggleRows(sourcePath);
|
||||
if (result.warnings.length > 0) {
|
||||
return { imported: 0, warnings: result.warnings };
|
||||
}
|
||||
|
||||
if (result.rows.length > 0) {
|
||||
runOpenClawStateWriteTransaction(
|
||||
(stateDatabase) => {
|
||||
const db = getNodeSqliteKysely<PluginStateMigrationDatabase>(stateDatabase.db);
|
||||
for (const row of result.rows) {
|
||||
executeSqliteQuerySync(
|
||||
stateDatabase.db,
|
||||
db
|
||||
.insertInto("plugin_state_entries")
|
||||
.values(row)
|
||||
.onConflict((conflict) =>
|
||||
conflict.columns(["plugin_id", "namespace", "entry_key"]).doUpdateSet({
|
||||
value_json: (eb) => eb.ref("excluded.value_json"),
|
||||
created_at: (eb) => eb.ref("excluded.created_at"),
|
||||
expires_at: (eb) => eb.ref("excluded.expires_at"),
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
{ env },
|
||||
);
|
||||
}
|
||||
|
||||
fs.rmSync(sourcePath, { force: true });
|
||||
removeDirIfEmpty(path.dirname(sourcePath));
|
||||
return { imported: result.rows.length, warnings: [] };
|
||||
}
|
||||
|
||||
function collectLegacyMigrationSources(detected: LegacyStateDetection): MigrationSourceReport[] {
|
||||
const sources: MigrationSourceReport[] = [];
|
||||
const add = (source: MigrationSourceReport | null | undefined) => {
|
||||
|
||||
Reference in New Issue
Block a user