fix: harden legacy session SQLite migration

This commit is contained in:
Vincent Koc
2026-06-09 14:37:41 +09:00
parent 5e1fbca3cb
commit 2f02bbcbb3
3 changed files with 967 additions and 57 deletions

View File

@@ -1,8 +1,11 @@
// Assertions for upgrade-survivor E2E scenarios.
import fs from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
import { readPluginInstallIndex } from "../plugin-index-sqlite.mjs";
const require = createRequire(import.meta.url);
const command = process.argv[2];
const SCENARIOS = new Set([
"base",
@@ -23,6 +26,10 @@ const PERSONA_FILES = new Map([
["MEMORY.md", "# Existing Memory\n\nUpgrade reports came from real users.\n"],
]);
const LEGACY_SESSION_MAIN_ID = "upgrade-main-session";
const LEGACY_SESSION_DIRECT_ID = "upgrade-direct-session";
const LEGACY_SESSION_GROUP_ID = "upgrade-group-session";
function requireEnv(name) {
const value = process.env[name];
if (!value) {
@@ -83,6 +90,71 @@ function assert(condition, message) {
}
}
function readSessionRowsFromAgentSqlite(stateDir, agentId = "main") {
const dbPath = path.join(stateDir, "agents", agentId, "agent", "openclaw-agent.sqlite");
assert(fs.existsSync(dbPath), `agent SQLite session database missing: ${dbPath}`);
const { DatabaseSync } = require("node:sqlite");
const db = new DatabaseSync(dbPath, { readOnly: true });
try {
const rows = db
.prepare("SELECT key, value_json FROM cache_entries WHERE scope = ? ORDER BY key ASC")
.all("session_entries");
return Object.fromEntries(
rows.map((row) => {
assert(typeof row.key === "string", "session SQLite row key was not a string");
assert(
typeof row.value_json === "string",
`session SQLite row ${String(row.key)} had no JSON payload`,
);
return [row.key, JSON.parse(row.value_json)];
}),
);
} finally {
db.close();
}
}
function seedLegacySessionMetadata(stateDir) {
const legacySessionsDir = path.join(stateDir, "sessions");
writeJson(path.join(legacySessionsDir, "sessions.json"), {
main: {
sessionId: LEGACY_SESSION_MAIN_ID,
sessionFile: path.join(legacySessionsDir, `${LEGACY_SESSION_MAIN_ID}.jsonl`),
provider: "openai",
model: "gpt-5.5",
updatedAt: 1710000000000,
skillsSnapshot: {
prompt: "legacy prompt survives as metadata",
resolvedSkills: [
{
name: "legacy-heavy-skill-cache",
filePath: "/tmp/openclaw-old-package/skills/legacy-heavy-skill-cache/SKILL.md",
},
],
},
},
"+15551234567": {
sessionId: LEGACY_SESSION_DIRECT_ID,
sessionFile: path.join(legacySessionsDir, `${LEGACY_SESSION_DIRECT_ID}.jsonl`),
provider: "openai",
model: "gpt-5.5",
updatedAt: 1710000000100,
},
"slack:channel:CUPGRADE": {
sessionId: LEGACY_SESSION_GROUP_ID,
sessionFile: path.join(legacySessionsDir, `${LEGACY_SESSION_GROUP_ID}.jsonl`),
provider: "openai",
model: "gpt-5.5",
updatedAt: 1710000000200,
lastChannel: "slack",
lastTo: "CUPGRADE",
},
});
write(path.join(legacySessionsDir, `${LEGACY_SESSION_MAIN_ID}.jsonl`), '{"type":"main"}\n');
write(path.join(legacySessionsDir, `${LEGACY_SESSION_DIRECT_ID}.jsonl`), '{"type":"direct"}\n');
write(path.join(legacySessionsDir, `${LEGACY_SESSION_GROUP_ID}.jsonl`), '{"type":"group"}\n');
}
function getScenario() {
const scenario = process.env.OPENCLAW_UPGRADE_SURVIVOR_SCENARIO || "base";
assert(SCENARIOS.has(scenario), `unknown upgrade survivor scenario: ${scenario}`);
@@ -139,6 +211,7 @@ function seedState() {
agentId: "main",
title: "Existing user session",
});
seedLegacySessionMetadata(stateDir);
const runtimeRoot = path.join(stateDir, "plugin-runtime-deps");
for (const plugin of ["discord", "telegram", "whatsapp"]) {
@@ -357,12 +430,15 @@ function assertStateSurvived() {
const stateDir = requireEnv("OPENCLAW_STATE_DIR");
const workspace = requireEnv("OPENCLAW_TEST_WORKSPACE_DIR");
const scenario = getScenario();
const stage = process.env.OPENCLAW_UPGRADE_SURVIVOR_ASSERT_STAGE || "survival";
assert(fs.existsSync(path.join(workspace, "IDENTITY.md")), "workspace identity file missing");
assert(
fs.existsSync(path.join(stateDir, "agents", "main", "sessions", "legacy-session.json")),
"legacy session file missing",
);
const stage = process.env.OPENCLAW_UPGRADE_SURVIVOR_ASSERT_STAGE || "survival";
if (stage !== "baseline") {
assertSessionMetadataMigratedToSqlite(stateDir);
}
const legacyRuntimeRoot = path.join(stateDir, "plugin-runtime-deps");
if (stage === "baseline") {
if (fs.existsSync(legacyRuntimeRoot)) {
@@ -406,6 +482,59 @@ function assertStateSurvived() {
}
}
function assertSessionMetadataMigratedToSqlite(stateDir) {
const legacyStorePath = path.join(stateDir, "sessions", "sessions.json");
const agentSessionsDir = path.join(stateDir, "agents", "main", "sessions");
assert(
!fs.existsSync(legacyStorePath),
`legacy sessions.json survived migration: ${legacyStorePath}`,
);
for (const sessionId of [
LEGACY_SESSION_MAIN_ID,
LEGACY_SESSION_DIRECT_ID,
LEGACY_SESSION_GROUP_ID,
]) {
assert(
fs.existsSync(path.join(agentSessionsDir, `${sessionId}.jsonl`)),
`legacy session transcript was not moved for ${sessionId}`,
);
}
const store = readSessionRowsFromAgentSqlite(stateDir);
const main = store["agent:main:main"];
const direct = store["agent:main:+15551234567"];
const group = store["agent:main:slack:channel:cupgrade"];
assert(main?.sessionId === LEGACY_SESSION_MAIN_ID, "main legacy session row missing from SQLite");
assert(
direct?.sessionId === LEGACY_SESSION_DIRECT_ID,
"direct legacy session row missing from SQLite",
);
assert(
group?.sessionId === LEGACY_SESSION_GROUP_ID,
"channel legacy session row missing from SQLite",
);
assert(
main?.sessionFile === path.join(agentSessionsDir, `${LEGACY_SESSION_MAIN_ID}.jsonl`),
"main legacy session row still points at the old sessions directory",
);
assert(
direct?.sessionFile === path.join(agentSessionsDir, `${LEGACY_SESSION_DIRECT_ID}.jsonl`),
"direct legacy session row still points at the old sessions directory",
);
assert(
group?.sessionFile === path.join(agentSessionsDir, `${LEGACY_SESSION_GROUP_ID}.jsonl`),
"channel legacy session row still points at the old sessions directory",
);
assert(
main.skillsSnapshot?.prompt === "legacy prompt survives as metadata",
"legacy session metadata prompt was not preserved",
);
assert(
main.skillsSnapshot?.resolvedSkills === undefined,
"heavy resolvedSkills cache was persisted into SQLite session metadata",
);
}
function readInstalledPluginIndex() {
const stateDir = requireEnv("OPENCLAW_STATE_DIR");
const index = readPluginInstallIndex({ stateDir });