From efdce6754d005c83f1623ae3941d15b52692b4bd Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 12 Jul 2026 15:22:37 +0800 Subject: [PATCH] fix(ci): align scheduled e2e checks with sqlite sessions --- .../openclaw-live-and-e2e-checks-reusable.yml | 6 +- .../e2e/lib/live-plugin-tool/assertions.mjs | 58 +++++++- .../e2e/lib/upgrade-survivor/assertions.mjs | 82 +++++++---- scripts/e2e/live-plugin-tool-docker.sh | 8 +- .../session-runtime-context-docker-client.ts | 32 +++-- .../live-plugin-tool-assertions.test.ts | 77 ++++++++++- test/scripts/release-no-push-workflow.test.ts | 17 +++ .../upgrade-survivor-assertions.test.ts | 127 ++++++++++-------- 8 files changed, 295 insertions(+), 112 deletions(-) diff --git a/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml b/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml index e20890d1518f..d5df124c0de7 100644 --- a/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml +++ b/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml @@ -1197,7 +1197,7 @@ jobs: export OPENCLAW_DOCKER_ALL_TIMINGS_FILE=".artifacts/docker-tests/release-${DOCKER_E2E_CHUNK}-timings.json" export OPENCLAW_DOCKER_ALL_PNPM_COMMAND="$(command -v pnpm)" if [[ "${{ steps.plan.outputs.needs_live_image }}" == "1" ]]; then - OPENCLAW_DOCKER_BUILD_ON_MISSING=1 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" bash .release-harness/scripts/test-live-build-docker.sh + OPENCLAW_SKIP_DOCKER_BUILD=0 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" bash .release-harness/scripts/test-live-build-docker.sh fi node .release-harness/scripts/test-docker-all.mjs @@ -1525,7 +1525,7 @@ jobs: export OPENCLAW_DOCKER_ALL_TIMINGS_FILE=".artifacts/docker-tests/targeted-${ARTIFACT_SUFFIX}-timings.json" export OPENCLAW_DOCKER_ALL_PNPM_COMMAND="$(command -v pnpm)" if [[ "${{ steps.plan.outputs.needs_live_image }}" == "1" ]]; then - OPENCLAW_DOCKER_BUILD_ON_MISSING=1 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" bash .release-harness/scripts/test-live-build-docker.sh + OPENCLAW_SKIP_DOCKER_BUILD=0 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" bash .release-harness/scripts/test-live-build-docker.sh fi export OPENCLAW_DOCKER_ALL_BUILD=0 @@ -1717,7 +1717,7 @@ jobs: export OPENCLAW_DOCKER_ALL_TIMINGS_FILE=".artifacts/docker-tests/release-openwebui-timings.json" export OPENCLAW_DOCKER_ALL_PNPM_COMMAND="$(command -v pnpm)" if [[ "${{ steps.plan.outputs.needs_live_image }}" == "1" ]]; then - OPENCLAW_DOCKER_BUILD_ON_MISSING=1 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" bash .release-harness/scripts/test-live-build-docker.sh + OPENCLAW_SKIP_DOCKER_BUILD=0 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" bash .release-harness/scripts/test-live-build-docker.sh fi node .release-harness/scripts/test-docker-all.mjs diff --git a/scripts/e2e/lib/live-plugin-tool/assertions.mjs b/scripts/e2e/lib/live-plugin-tool/assertions.mjs index dcc6c0a714cc..7312f1cfe2f1 100644 --- a/scripts/e2e/lib/live-plugin-tool/assertions.mjs +++ b/scripts/e2e/lib/live-plugin-tool/assertions.mjs @@ -1,6 +1,7 @@ // Assertions for live plugin tool E2E scenarios. import fs from "node:fs"; import path from "node:path"; +import { DatabaseSync } from "node:sqlite"; import { extractAgentReplyTexts } from "../agent-turn-output.mjs"; import { readPluginInstallRecords } from "../plugin-index-sqlite.mjs"; import { readTextFileTail, tailText } from "../text-file-utils.mjs"; @@ -33,6 +34,7 @@ const AGENT_OUTPUT_MAX_BYTES = readPositiveIntEnv( 1024 * 1024, ); const SESSION_FILE_LIST_LIMIT = 20; +const LIVE_PLUGIN_TOOL_SESSION_ID = "live-plugin-tool"; const SESSION_SCAN_MAX_ENTRIES = readPositiveIntEnv( "OPENCLAW_LIVE_PLUGIN_TOOL_SESSION_SCAN_MAX_ENTRIES", 50_000, @@ -373,6 +375,41 @@ function scanSessionTranscripts(sessionsDir, toolName, expected) { return { checkedFiles, filesChecked, found: false, missingDir: false }; } +function scanSqliteSessionTranscript(databasePath, sessionId, toolName, expected) { + if (!fs.existsSync(databasePath)) { + return { eventsChecked: 0, found: false }; + } + const database = new DatabaseSync(databasePath, { readOnly: true }); + try { + const table = database + .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'transcript_events'") + .get(); + if (!table) { + return { eventsChecked: 0, found: false }; + } + const rows = database + .prepare("SELECT event_json FROM transcript_events WHERE session_id = ? ORDER BY seq LIMIT ?") + .all(sessionId, SESSION_SCAN_MAX_ENTRIES + 1); + if (rows.length > SESSION_SCAN_MAX_ENTRIES) { + throw new Error(`session transcript scan exceeded ${SESSION_SCAN_MAX_ENTRIES} SQLite events`); + } + + const tracker = createToolEvidenceTracker(toolName, expected); + for (const row of rows) { + if (typeof row.event_json !== "string") { + continue; + } + const message = transcriptMessageFromLine(row.event_json); + if (message && tracker.recordMessage(message)) { + return { eventsChecked: rows.length, found: true }; + } + } + return { eventsChecked: rows.length, found: false }; + } finally { + database.close(); + } +} + function realPathMaybe(filePath) { try { return fs.realpathSync(filePath); @@ -610,13 +647,22 @@ function assertAgentTurn() { `live agent reply did not contain tool slug ${expected}:\nstdout tail=${tailText(stdout, ERROR_DETAIL_TAIL_BYTES)}\nstderr tail=${stderrTail}`, ); } - const sessionsDir = path.join(stateDir(), "agents", "main", "sessions"); - const scan = scanSessionTranscripts(sessionsDir, toolName, expected); - if (!scan.found) { - const checkedFiles = scan.checkedFiles.length > 0 ? scan.checkedFiles.join(", ") : ""; - const missingDir = scan.missingDir ? " sessions directory was missing." : ""; + const agentStateDir = path.join(stateDir(), "agents", "main"); + const sqliteScan = scanSqliteSessionTranscript( + path.join(agentStateDir, "agent", "openclaw-agent.sqlite"), + LIVE_PLUGIN_TOOL_SESSION_ID, + toolName, + expected, + ); + const fileScan = sqliteScan.found + ? { checkedFiles: [], filesChecked: 0, found: false, missingDir: false } + : scanSessionTranscripts(path.join(agentStateDir, "sessions"), toolName, expected); + if (!sqliteScan.found && !fileScan.found) { + const checkedFiles = + fileScan.checkedFiles.length > 0 ? fileScan.checkedFiles.join(", ") : ""; + const missingDir = fileScan.missingDir ? " sessions directory was missing." : ""; throw new Error( - `session transcript did not show ${toolName} returning ${expected}; missing causal tool-result evidence after checking ${scan.filesChecked} jsonl file(s): ${checkedFiles}.${missingDir}`, + `session transcript did not show ${toolName} returning ${expected}; missing causal tool-result evidence after checking ${sqliteScan.eventsChecked} SQLite event(s) and ${fileScan.filesChecked} jsonl file(s): ${checkedFiles}.${missingDir}`, ); } } diff --git a/scripts/e2e/lib/upgrade-survivor/assertions.mjs b/scripts/e2e/lib/upgrade-survivor/assertions.mjs index 9e479250e147..7094191a3015 100644 --- a/scripts/e2e/lib/upgrade-survivor/assertions.mjs +++ b/scripts/e2e/lib/upgrade-survivor/assertions.mjs @@ -478,20 +478,11 @@ function assertSessionMetadataMigrated(stateDir) { const legacyStorePath = path.join(stateDir, "sessions", "sessions.json"); const agentSessionsDir = path.join(stateDir, "agents", "main", "sessions"); const targetStorePath = path.join(agentSessionsDir, "sessions.json"); + const dbPath = path.join(stateDir, "agents", "main", "agent", "openclaw-agent.sqlite"); 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 = readMigratedSessionStore(stateDir, targetStorePath); const main = store["agent:main:main"]; @@ -500,18 +491,44 @@ function assertSessionMetadataMigrated(stateDir) { assert(main?.sessionId === LEGACY_SESSION_MAIN_ID, "main legacy session row missing"); assert(direct?.sessionId === LEGACY_SESSION_DIRECT_ID, "direct legacy session row missing"); assert(group?.sessionId === LEGACY_SESSION_GROUP_ID, "channel legacy session row missing"); - 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", - ); + const migratedSessionIds = [ + LEGACY_SESSION_MAIN_ID, + LEGACY_SESSION_DIRECT_ID, + LEGACY_SESSION_GROUP_ID, + ]; + if (fs.existsSync(dbPath)) { + const db = new DatabaseSync(dbPath, { readOnly: true }); + try { + const count = db.prepare( + "SELECT COUNT(*) AS count FROM transcript_events WHERE session_id = ?", + ); + for (const sessionId of migratedSessionIds) { + const row = count.get(sessionId); + assert( + Number(row?.count ?? 0) > 0, + `legacy session transcript was not imported for ${sessionId}`, + ); + } + } finally { + db.close(); + } + } else { + for (const [sessionId, entry] of [ + [LEGACY_SESSION_MAIN_ID, main], + [LEGACY_SESSION_DIRECT_ID, direct], + [LEGACY_SESSION_GROUP_ID, group], + ]) { + const expectedPath = path.join(agentSessionsDir, `${sessionId}.jsonl`); + assert( + fs.existsSync(expectedPath), + `legacy session transcript was not moved for ${sessionId}`, + ); + assert( + entry?.sessionFile === expectedPath, + `legacy session row still points at the old sessions directory for ${sessionId}`, + ); + } + } assert( main.skillsSnapshot?.prompt === "legacy prompt survives as metadata", "legacy session metadata prompt was not preserved", @@ -533,15 +550,28 @@ function readMigratedSessionStore(stateDir, targetStorePath) { let db; try { db = new DatabaseSync(dbPath, { readOnly: true }); - const rows = db - .prepare("SELECT key, value_json FROM cache_entries WHERE scope = ?") - .all("session_entries"); + const hasSessionEntries = db + .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'session_entries'") + .get(); + const rows = hasSessionEntries + ? db + .prepare( + `SELECT se.session_key AS key, sr.session_id, se.entry_json AS value_json + FROM session_entries AS se + INNER JOIN session_routes AS sr ON sr.session_key = se.session_key`, + ) + .all() + : db + .prepare("SELECT key, value_json FROM cache_entries WHERE scope = ?") + .all("session_entries"); const store = {}; for (const row of rows) { if (typeof row?.key !== "string" || typeof row?.value_json !== "string") { continue; } - store[row.key] = JSON.parse(row.value_json); + const entry = JSON.parse(row.value_json); + store[row.key] = + typeof row.session_id === "string" ? { ...entry, sessionId: row.session_id } : entry; } return store; } finally { diff --git a/scripts/e2e/live-plugin-tool-docker.sh b/scripts/e2e/live-plugin-tool-docker.sh index 37a51d367052..70e1f51fdf3e 100755 --- a/scripts/e2e/live-plugin-tool-docker.sh +++ b/scripts/e2e/live-plugin-tool-docker.sh @@ -150,7 +150,7 @@ openclaw_e2e_enable_openclaw_cli_timeout fixture_dir="$(mktemp -d /tmp/openclaw-live-plugin-tool.XXXXXX)" plugin_dir="$fixture_dir/package" mkdir -p "$plugin_dir" -node scripts/e2e/lib/live-plugin-tool/assertions.mjs write-fixture "$plugin_dir" +node --disable-warning=ExperimentalWarning scripts/e2e/lib/live-plugin-tool/assertions.mjs write-fixture "$plugin_dir" (cd "$plugin_dir" && npm pack --pack-destination "$fixture_dir" --silent) \ >/tmp/openclaw-live-plugin-tool-pack.log 2>&1 plugin_tgzs=() @@ -166,11 +166,11 @@ plugin_tgz="${plugin_tgzs[0]}" echo "Installing fixture plugin from npm-pack: $plugin_tgz" openclaw plugins install "npm-pack:$plugin_tgz" --force >/tmp/openclaw-plugin-install.log 2>&1 -node scripts/e2e/lib/live-plugin-tool/assertions.mjs configure +node --disable-warning=ExperimentalWarning scripts/e2e/lib/live-plugin-tool/assertions.mjs configure openclaw plugins enable "$PLUGIN_ID" >/tmp/openclaw-plugin-enable.log 2>&1 openclaw plugins list --json >/tmp/openclaw-plugins-list.json openclaw plugins inspect "$PLUGIN_ID" --runtime --json >/tmp/openclaw-plugin-inspect.json -node scripts/e2e/lib/live-plugin-tool/assertions.mjs assert-installed +node --disable-warning=ExperimentalWarning scripts/e2e/lib/live-plugin-tool/assertions.mjs assert-installed echo "Running live OpenAI agent turn that must call $TOOL_NAME..." openclaw agent --local \ @@ -182,7 +182,7 @@ openclaw agent --local \ --timeout "${OPENCLAW_LIVE_PLUGIN_TOOL_TIMEOUT_SECONDS:-300}" \ --json >/tmp/openclaw-agent.json 2>/tmp/openclaw-agent.err -node scripts/e2e/lib/live-plugin-tool/assertions.mjs assert-agent-turn +node --disable-warning=ExperimentalWarning scripts/e2e/lib/live-plugin-tool/assertions.mjs assert-agent-turn echo "Live plugin tool Docker E2E passed" EOF diff --git a/scripts/e2e/session-runtime-context-docker-client.ts b/scripts/e2e/session-runtime-context-docker-client.ts index 561c46625ee3..61f3828bd3ec 100644 --- a/scripts/e2e/session-runtime-context-docker-client.ts +++ b/scripts/e2e/session-runtime-context-docker-client.ts @@ -5,7 +5,9 @@ import { spawnSync } from "node:child_process"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; +import { DatabaseSync } from "node:sqlite"; import { SessionManager } from "openclaw/plugin-sdk/agent-sessions"; +import { readSessionTranscriptEvents } from "openclaw/plugin-sdk/session-transcript-runtime"; import { buildRuntimeContextCustomMessage, resolveRuntimeContextPromptParts, @@ -32,14 +34,6 @@ function setEnvValue(key: string, value: string): void { Reflect.set(process.env, key, value); } -async function readJsonl(filePath: string): Promise { - const raw = await fs.readFile(filePath, "utf-8"); - return raw - .split(/\r?\n/) - .filter(Boolean) - .map((line) => JSON.parse(line) as TranscriptEntry); -} - function messageText(content: unknown): string { if (typeof content === "string") { return content; @@ -91,7 +85,7 @@ async function verifyRuntimeContextTranscriptShape(root: string) { timestamp: Date.now() + 1, }); - const entries = await readJsonl(sessionFile); + const entries = sessionManager.getEntries() as TranscriptEntry[]; const customEntry = entries.find((entry) => entry.type === "custom_message"); assert(!customEntry, "runtime custom message should not be persisted without its user turn"); assert( @@ -218,7 +212,25 @@ async function verifyDoctorRepair(root: string) { result.status === 0, `doctor --fix failed\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`, ); - const entries = await readJsonl(sessionFile); + const databasePath = path.join(stateDir, "agents", "main", "agent", "openclaw-agent.sqlite"); + const database = new DatabaseSync(databasePath, { readOnly: true }); + let migratedSessionId: string | undefined; + try { + const row = database + .prepare("SELECT session_id FROM session_routes WHERE session_key = ?") + .get("agent:main:qa:docker-runtime-context"); + if (typeof row?.session_id === "string") { + migratedSessionId = row.session_id; + } + } finally { + database.close(); + } + assert(migratedSessionId, "doctor did not migrate session"); + const entries = (await readSessionTranscriptEvents({ + agentId: "main", + sessionId: migratedSessionId, + sessionKey: "agent:main:qa:docker-runtime-context", + })) as TranscriptEntry[]; const ids = entries.map((entryValue) => (entryValue as { id?: string }).id).filter(Boolean); assert( JSON.stringify(ids) === diff --git a/test/scripts/live-plugin-tool-assertions.test.ts b/test/scripts/live-plugin-tool-assertions.test.ts index 19d55e7488dd..0357a428f693 100644 --- a/test/scripts/live-plugin-tool-assertions.test.ts +++ b/test/scripts/live-plugin-tool-assertions.test.ts @@ -3,6 +3,7 @@ import { spawnSync } from "node:child_process"; import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; +import { DatabaseSync } from "node:sqlite"; import { describe, expect, it } from "vitest"; const ASSERTIONS_SCRIPT = "scripts/e2e/lib/live-plugin-tool/assertions.mjs"; @@ -68,7 +69,7 @@ describe("live plugin tool assertions", () => { OPENCLAW_LIVE_PLUGIN_TOOL_TIMEOUT_SECONDS: "240", }); - expect(result.status).toBe(0); + expect(result.status, result.stderr).toBe(0); const config = JSON.parse(readFileSync(path.join(root, "state", "openclaw.json"), "utf8")); expect(config.models.providers.openai.timeoutSeconds).toBe(240); expect(config.agents.defaults.timeoutSeconds).toBe(240); @@ -115,7 +116,75 @@ describe("live plugin tool assertions", () => { const result = runAssertion(root); - expect(result.status).toBe(0); + expect(result.status, result.stderr).toBe(0); + expect(result.stderr).toBe(""); + } finally { + rmSync(root, { force: true, recursive: true }); + } + }); + + it("reads causal tool evidence from the canonical SQLite transcript", () => { + const root = mkdtempSync(path.join(tmpdir(), "openclaw-live-plugin-tool-")); + const databasePath = path.join( + root, + "state", + "agents", + "main", + "agent", + "openclaw-agent.sqlite", + ); + + try { + writeJson(path.join(root, "agent.json"), { + payloads: [{ text: "live-plugin-slug" }], + }); + mkdirSync(path.dirname(databasePath), { recursive: true }); + const database = new DatabaseSync(databasePath); + try { + database.exec(` + CREATE TABLE transcript_events ( + session_id TEXT NOT NULL, + seq INTEGER NOT NULL, + event_json TEXT NOT NULL + ) + `); + const insert = database.prepare( + "INSERT INTO transcript_events (session_id, seq, event_json) VALUES (?, ?, ?)", + ); + insert.run( + "live-plugin-tool", + 1, + JSON.stringify({ + message: { + role: "assistant", + content: [ + { + type: "tool_use", + id: "call-live-plugin-tool", + name: "e2e_slug_probe", + }, + ], + }, + }), + ); + insert.run( + "live-plugin-tool", + 2, + JSON.stringify({ + message: { + role: "tool", + tool_call_id: "call-live-plugin-tool", + content: "live-plugin-slug", + }, + }), + ); + } finally { + database.close(); + } + + const result = runAssertion(root); + + expect(result.status, result.stderr).toBe(0); expect(result.stderr).toBe(""); } finally { rmSync(root, { force: true, recursive: true }); @@ -162,7 +231,7 @@ describe("live plugin tool assertions", () => { expect(result.status).not.toBe(0); expect(result.stderr).toContain("session transcript did not show"); - expect(result.stderr).toContain("after checking 2 jsonl file(s)"); + expect(result.stderr).toContain("0 SQLite event(s) and 2 jsonl file(s)"); } finally { rmSync(root, { force: true, recursive: true }); } @@ -322,7 +391,7 @@ describe("live plugin tool assertions", () => { expect(result.status).not.toBe(0); expect(result.stderr).toContain("session transcript did not show"); - expect(result.stderr).toContain("after checking 1 jsonl file(s)"); + expect(result.stderr).toContain("0 SQLite event(s) and 1 jsonl file(s)"); expect(result.stderr).toContain("session.jsonl"); expect(result.stderr).not.toContain("DO_NOT_DUMP_SESSION_CONTENT"); } finally { diff --git a/test/scripts/release-no-push-workflow.test.ts b/test/scripts/release-no-push-workflow.test.ts index 215995380751..b19575da14fe 100644 --- a/test/scripts/release-no-push-workflow.test.ts +++ b/test/scripts/release-no-push-workflow.test.ts @@ -188,6 +188,23 @@ function expectReadOnlyPackagePermission(workflowJob: WorkflowJob): void { } describe("release validation no-push transport", () => { + it("builds planned live images locally without entering pull fallback", () => { + const workflow = readWorkflow(LIVE_E2E); + for (const jobName of [ + "validate_docker_e2e", + "validate_docker_lanes", + "validate_docker_openwebui", + ]) { + const job = workflow.jobs?.[jobName]; + const runStep = job?.steps?.find((candidate) => + candidate.run?.includes("test-live-build-docker.sh"), + ); + + expect(runStep?.run, jobName).toContain("OPENCLAW_SKIP_DOCKER_BUILD=0"); + expect(runStep?.run, jobName).not.toContain("OPENCLAW_DOCKER_BUILD_ON_MISSING=1"); + } + }); + it("keeps every local reusable-workflow permission request within its caller ceiling", () => { const readOnlyCalls = [ [PLUGIN_PRERELEASE, "plugin-prerelease-docker-suite"], diff --git a/test/scripts/upgrade-survivor-assertions.test.ts b/test/scripts/upgrade-survivor-assertions.test.ts index 48770bdec27a..7b43ce75f164 100644 --- a/test/scripts/upgrade-survivor-assertions.test.ts +++ b/test/scripts/upgrade-survivor-assertions.test.ts @@ -15,79 +15,88 @@ function writeJson(path: string, value: unknown): void { function writeMigratedSessionState(stateDir: string): void { const agentSessionsDir = join(stateDir, "agents", "main", "sessions"); const agentDbDir = join(stateDir, "agents", "main", "agent"); - const mainSessionFile = join(agentSessionsDir, "upgrade-main-session.jsonl"); - const directSessionFile = join(agentSessionsDir, "upgrade-direct-session.jsonl"); - const groupSessionFile = join(agentSessionsDir, "upgrade-group-session.jsonl"); mkdirSync(agentSessionsDir, { recursive: true }); mkdirSync(agentDbDir, { recursive: true }); - writeFileSync(mainSessionFile, '{"type":"main"}\n'); - writeFileSync(directSessionFile, '{"type":"direct"}\n'); - writeFileSync(groupSessionFile, '{"type":"group"}\n'); - writeJson(join(agentSessionsDir, "sessions.json"), { - "agent:main:main": { - sessionFile: mainSessionFile, - sessionId: "upgrade-main-session", - skillsSnapshot: { - prompt: "legacy prompt survives as metadata", - }, - }, - "agent:main:+15551234567": { - sessionFile: directSessionFile, - sessionId: "upgrade-direct-session", - }, - "agent:main:slack:channel:cupgrade": { - sessionFile: groupSessionFile, - sessionId: "upgrade-group-session", - }, - }); const db = new DatabaseSync(join(agentDbDir, "openclaw-agent.sqlite")); try { db.exec(` - CREATE TABLE IF NOT EXISTS cache_entries ( - scope TEXT NOT NULL, - key TEXT NOT NULL, - value_json TEXT, - blob BLOB, - expires_at INTEGER, + CREATE TABLE sessions ( + session_id TEXT PRIMARY KEY, + session_key TEXT NOT NULL, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + ); + CREATE TABLE session_routes ( + session_key TEXT PRIMARY KEY, + session_id TEXT NOT NULL, updated_at INTEGER NOT NULL, - PRIMARY KEY (scope, key) + FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE + ); + CREATE TABLE session_entries ( + session_key TEXT PRIMARY KEY, + session_id TEXT NOT NULL, + entry_json TEXT NOT NULL, + updated_at INTEGER NOT NULL, + FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE + ); + CREATE TABLE transcript_events ( + session_id TEXT NOT NULL, + seq INTEGER NOT NULL, + event_json TEXT NOT NULL, + created_at INTEGER NOT NULL, + PRIMARY KEY (session_id, seq), + FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE ); `); - const insert = db.prepare(` - INSERT INTO cache_entries (scope, key, value_json, updated_at) + const insertSession = db.prepare(` + INSERT INTO sessions (session_id, session_key, created_at, updated_at) VALUES (?, ?, ?, ?) `); - insert.run( - "session_entries", - "agent:main:main", - JSON.stringify({ - sessionFile: mainSessionFile, - sessionId: "upgrade-main-session", - skillsSnapshot: { - prompt: "legacy prompt survives as metadata", + const insertRoute = db.prepare(` + INSERT INTO session_routes (session_key, session_id, updated_at) + VALUES (?, ?, ?) + `); + const insertEntry = db.prepare(` + INSERT INTO session_entries (session_key, session_id, entry_json, updated_at) + VALUES (?, ?, ?, ?) + `); + const insertTranscript = db.prepare(` + INSERT INTO transcript_events (session_id, seq, event_json, created_at) + VALUES (?, ?, ?, ?) + `); + const migratedSessions = [ + { + entry: { + skillsSnapshot: { + prompt: "legacy prompt survives as metadata", + }, }, - }), - 1710000000000, - ); - insert.run( - "session_entries", - "agent:main:+15551234567", - JSON.stringify({ - sessionFile: directSessionFile, + sessionId: "upgrade-main-session", + sessionKey: "agent:main:main", + }, + { + entry: {}, sessionId: "upgrade-direct-session", - }), - 1710000000100, - ); - insert.run( - "session_entries", - "agent:main:slack:channel:cupgrade", - JSON.stringify({ - sessionFile: groupSessionFile, + sessionKey: "agent:main:+15551234567", + }, + { + entry: {}, sessionId: "upgrade-group-session", - }), - 1710000000200, - ); + sessionKey: "agent:main:slack:channel:cupgrade", + }, + ]; + for (const { entry, sessionId, sessionKey } of migratedSessions) { + insertSession.run(sessionId, sessionKey, 1710000000000, 1710000000000); + insertRoute.run(sessionKey, sessionId, 1710000000000); + insertEntry.run(sessionKey, sessionId, JSON.stringify(entry), 1710000000000); + insertTranscript.run( + sessionId, + 1, + JSON.stringify({ type: "session", id: sessionId }), + 1710000000000, + ); + } } finally { db.close(); }