fix: preserve sqlite compatibility shims

This commit is contained in:
Peter Steinberger
2026-05-16 07:10:30 +01:00
parent 70cf721d40
commit 4dfcec4b1e
7 changed files with 57 additions and 17 deletions

View File

@@ -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

View File

@@ -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"));

View File

@@ -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<string, unknown>,
});
try {
recordOpenClawStateBackupRun({
createdAt: nowMs,
archivePath: outputPath,
status: "completed",
manifest: manifest as unknown as Record<string, unknown>,
});
} 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);

View File

@@ -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));
});
});

View File

@@ -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,
};

View File

@@ -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(
{

View File

@@ -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)