diff --git a/docs/refactor/database-first.md b/docs/refactor/database-first.md index 7d6988a1432..259f5aac3cb 100644 --- a/docs/refactor/database-first.md +++ b/docs/refactor/database-first.md @@ -574,11 +574,12 @@ Completed consolidation/deletion highlights: - Channel session runtime types now expose `{agentId, sessionKey}` for updated-at reads, inbound metadata, and last-route updates. The old `saveSessionStore(storePath, store)` compatibility type is gone. -- Plugin runtime, extension API, root library, and `config/sessions` barrel - surfaces no longer export `resolveStorePath`; plugin code uses SQLite-backed - session row helpers. The old `resolveLegacySessionStorePath` helper is gone; - legacy `sessions.json` path construction is now local to migration and test - fixtures. +- Plugin runtime, extension API, and `config/sessions` barrel surfaces now steer + plugin code to SQLite-backed session row helpers. Root library compatibility + exports (`loadSessionStore`, `saveSessionStore`, `resolveStorePath`) remain as + deprecated shims for existing consumers. The old + `resolveLegacySessionStorePath` helper is gone; legacy `sessions.json` path + construction is now local to migration and test fixtures. - `src/config/sessions/session-entries.sqlite.ts` now stores canonical session entries in the per-agent database and has row-level read/upsert/delete patch support. Runtime upsert/patch/delete no longer scans for case variants or @@ -1768,6 +1769,8 @@ runtime contract: `patchSessionEntry`, `deleteSessionEntry`, and `listSessionEntries`. - Whole-store rewrite helpers, file writers, queue tests, alias pruning, and legacy-key deletion parameters are gone from runtime. +- Deprecated root-package compatibility exports still adapt canonical + `sessions.json` paths onto the SQLite row APIs. - `sessions.json` parsing remains only in doctor migration/import code and doctor tests. - Runtime lifecycle fallback reads SQLite transcript headers, not JSONL first diff --git a/src/index.ts b/src/index.ts index b91ade3423b..63304ed42d9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -30,14 +30,17 @@ export let getSessionEntry: LibraryExports["getSessionEntry"]; export let handlePortError: LibraryExports["handlePortError"]; export let listSessionEntries: LibraryExports["listSessionEntries"]; export let loadConfig: LibraryExports["loadConfig"]; +export let loadSessionStore: LibraryExports["loadSessionStore"]; export let monitorWebChannel: LibraryExports["monitorWebChannel"]; export let normalizeE164: LibraryExports["normalizeE164"]; export let patchSessionEntry: LibraryExports["patchSessionEntry"]; export let PortInUseError: LibraryExports["PortInUseError"]; export let promptYesNo: LibraryExports["promptYesNo"]; export let resolveSessionKey: LibraryExports["resolveSessionKey"]; +export let resolveStorePath: LibraryExports["resolveStorePath"]; export let runCommandWithTimeout: LibraryExports["runCommandWithTimeout"]; export let runExec: LibraryExports["runExec"]; +export let saveSessionStore: LibraryExports["saveSessionStore"]; export let upsertSessionEntry: LibraryExports["upsertSessionEntry"]; export let waitForever: LibraryExports["waitForever"]; @@ -72,14 +75,17 @@ if (!isMain) { handlePortError, listSessionEntries, loadConfig, + loadSessionStore, monitorWebChannel, normalizeE164, patchSessionEntry, PortInUseError, promptYesNo, resolveSessionKey, + resolveStorePath, runCommandWithTimeout, runExec, + saveSessionStore, upsertSessionEntry, waitForever, } = await import("./library.js")); diff --git a/src/infra/backup-create.ts b/src/infra/backup-create.ts index c02374b237b..8cd11b9c641 100644 --- a/src/infra/backup-create.ts +++ b/src/infra/backup-create.ts @@ -706,12 +706,20 @@ export async function createBackupArchive( }); await publishTempArchive({ tempArchivePath, outputPath }); if (manifest && result.assets.some((asset) => asset.kind === "state")) { - recordOpenClawStateBackupRun({ - createdAt: nowMs, - archivePath: outputPath, - status: "completed", - manifest: manifest as unknown as Record, - }); + try { + recordOpenClawStateBackupRun({ + createdAt: nowMs, + archivePath: outputPath, + status: "completed", + manifest: manifest as unknown as Record, + }); + } catch (error) { + opts.log?.( + `Backup created, but recording backup history failed: ${ + error instanceof Error ? error.message : String(error) + }`, + ); + } } } finally { await fs.rm(tempArchivePath, { force: true }).catch(() => undefined); diff --git a/src/library.test.ts b/src/library.test.ts index 72957119465..3720aea601a 100644 --- a/src/library.test.ts +++ b/src/library.test.ts @@ -1,5 +1,6 @@ import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; +import * as library from "./library.js"; const libraryPath = new URL("./library.ts", import.meta.url); const lazyRuntimeSpecifiers = [ @@ -38,3 +39,11 @@ describe("library module imports", () => { } }); }); + +describe("root library compatibility exports", () => { + it("keeps deprecated session-store shims available", () => { + expect(library.loadSessionStore).toEqual(expect.any(Function)); + expect(library.saveSessionStore).toEqual(expect.any(Function)); + expect(library.resolveStorePath).toEqual(expect.any(Function)); + }); +}); diff --git a/src/library.ts b/src/library.ts index c016ce213d4..ac5630a05b9 100644 --- a/src/library.ts +++ b/src/library.ts @@ -18,6 +18,11 @@ import { handlePortError, PortInUseError, } from "./infra/ports.js"; +import { + loadSessionStore, + resolveStorePath, + saveSessionStore, +} from "./plugin-sdk/session-store-runtime.js"; import type { monitorWebChannel as monitorWebChannelRuntime } from "./plugins/runtime/runtime-web-channel-plugin.js"; import type { runCommandWithTimeout as runCommandWithTimeoutRuntime, @@ -84,6 +89,7 @@ export { describePortOwner, ensurePortAvailable, handlePortError, + loadSessionStore, loadConfig, getSessionEntry, listSessionEntries, @@ -91,6 +97,8 @@ export { patchSessionEntry, PortInUseError, resolveSessionKey, + resolveStorePath, + saveSessionStore, upsertSessionEntry, waitForever, }; diff --git a/src/plugin-sdk/session-store-runtime.test.ts b/src/plugin-sdk/session-store-runtime.test.ts index 9d8cbf9f76f..8a343a01ec4 100644 --- a/src/plugin-sdk/session-store-runtime.test.ts +++ b/src/plugin-sdk/session-store-runtime.test.ts @@ -7,6 +7,7 @@ import { loadSessionStore, readSessionUpdatedAt, resolveAndPersistSessionFile, + resolveSessionTranscriptPathInDir, saveSessionStore, updateSessionStore, upsertSessionEntry, @@ -21,6 +22,15 @@ describe("session-store-runtime compatibility", () => { return { ...process.env, OPENCLAW_STATE_DIR: stateDir }; } + it("rejects reserved checkpoint session IDs for transcript paths", () => { + expect(() => + resolveSessionTranscriptPathInDir( + "sess.checkpoint.11111111-1111-4111-8111-111111111111", + "/tmp/sessions", + ), + ).toThrow(/Invalid session ID/); + }); + it("rejects custom store paths instead of falling back to the default agent", async () => { await withOpenClawTestState( { diff --git a/src/plugin-sdk/session-store-runtime.ts b/src/plugin-sdk/session-store-runtime.ts index 103759b672c..7b67c378d7a 100644 --- a/src/plugin-sdk/session-store-runtime.ts +++ b/src/plugin-sdk/session-store-runtime.ts @@ -5,6 +5,7 @@ import type { MsgContext } from "../auto-reply/templating.js"; import { resolveStateDir } from "../config/paths.js"; import { loadSqliteSessionEntries } from "../config/sessions/session-entries.sqlite.js"; import { normalizeSessionEntries } from "../config/sessions/session-entry-normalize.js"; +import { validateSessionId } from "../config/sessions/session-id.js"; import { resolveAndPersistSessionTranscriptScope } from "../config/sessions/session-scope.js"; import { resolveSessionRowEntry } from "../config/sessions/store-entry.js"; import { @@ -73,8 +74,6 @@ type SaveSessionStoreOptions = { type CompatSessionEntry = SessionEntry & { sessionFile?: string }; -const SAFE_SESSION_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/i; - function optionsWithEnv(agentId: string, env?: NodeJS.ProcessEnv): SessionRowOptions { return env ? { agentId, env } : { agentId }; } @@ -177,10 +176,7 @@ export function resolveSessionTranscriptPathInDir( sessionsDir: string, topicId?: string | number, ): string { - const trimmed = sessionId.trim(); - if (!SAFE_SESSION_ID_RE.test(trimmed)) { - throw new Error(`Invalid session ID: ${sessionId}`); - } + const trimmed = validateSessionId(sessionId); const safeTopicId = typeof topicId === "string" ? encodeURIComponent(topicId)