openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
257 lines (256 loc) • 11 kB
JavaScript
import { c as isRecord } from "./record-coerce-DItp3I4t.js";
import { t as openNodeSqliteDatabase } from "./node-sqlite-BpQX3W0e.js";
import { l as createPrivateSqliteTempDirectorySync } from "./sqlite-readonly-location-BC9PgENz.js";
import "./session-manager-codec-DIXSw5vP.js";
import { n as isSessionTranscriptLeafControl, s as scanSessionTranscriptNavigation, t as isCanonicalSessionTranscriptEntry } from "./transcript-tree-BH69pMSi.js";
import { i as isIndexedSessionEntry } from "./session-entry-codec-6GPwHDjQ.js";
import { a as transcriptRepairUserKey, n as normalizeLegacyOpenAICodexTranscriptMetadata } from "./legacy-transcript-repair-nrJd98DH.js";
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
import { hash } from "node:crypto";
//#region src/config/sessions/session-accessor.sqlite-import-stage.ts
function withSqliteSessionImportStage(run) {
const directory = createPrivateSqliteTempDirectorySync(os.tmpdir(), "openclaw-session-import-");
let database;
try {
const filename = path.join(directory, "transcripts.sqlite");
fs.closeSync(fs.openSync(filename, "wx", 384));
database = openNodeSqliteDatabase(filename);
database.exec(`
PRAGMA cache_size = -2048;
PRAGMA temp_store = FILE;
CREATE TABLE rows (
source INTEGER NOT NULL, seq INTEGER NOT NULL, event_json TEXT NOT NULL,
created_at INTEGER, PRIMARY KEY (source, seq)
) WITHOUT ROWID;
CREATE TABLE seen (hash BLOB NOT NULL, event_json TEXT NOT NULL);
CREATE INDEX seen_hash ON seen(hash);
CREATE TABLE tree (id TEXT PRIMARY KEY, node_json TEXT NOT NULL) WITHOUT ROWID;
CREATE TABLE tree_sets (kind TEXT NOT NULL, id TEXT NOT NULL, PRIMARY KEY(kind, id)) WITHOUT ROWID;
CREATE TABLE selected (id TEXT PRIMARY KEY, seq INTEGER NOT NULL, parent_id TEXT, visible INTEGER NOT NULL) WITHOUT ROWID;
CREATE TABLE user_keys (id TEXT PRIMARY KEY, visible_key TEXT, stripped_key TEXT) WITHOUT ROWID;
CREATE INDEX user_keys_visible ON user_keys(visible_key);
BEGIN;
`);
return run(new SqliteSessionImportStage(database));
} finally {
try {
database?.close();
} finally {
fs.rmSync(directory, {
recursive: true,
force: true
});
}
}
}
var SqliteSessionImportStage = class {
constructor(database) {
this.database = database;
this.rejected = false;
this.insert = database.prepare("INSERT INTO rows VALUES (?, ?, ?, ?)");
this.read = database.prepare("SELECT seq, event_json AS eventJson, created_at AS createdAt FROM rows WHERE source = ? ORDER BY seq");
this.findSeen = database.prepare("SELECT 1 FROM seen WHERE hash = ? AND event_json = ? LIMIT 1");
this.insertSeen = database.prepare("INSERT INTO seen VALUES (?, ?)");
}
append(source, seq, eventJson, createdAt) {
this.insert.run(source, seq, eventJson, createdAt);
}
rows(source) {
return this.read.iterate(source);
}
resetSeen() {
this.database.exec("DELETE FROM seen");
this.rejected = false;
}
*iterateUnseenEvents(source) {
for (const row of this.rows(source)) {
const eventHash = hash("sha256", row.eventJson, "buffer");
if (this.findSeen.get(eventHash, row.eventJson) !== void 0) continue;
if (yield JSON.parse(row.eventJson)) this.insertSeen.run(eventHash, row.eventJson);
else this.rejected = true;
}
}
contains(eventJson) {
return this.findSeen.get(hash("sha256", eventJson, "buffer"), eventJson) !== void 0;
}
get complete() {
return !this.rejected;
}
addSeen(eventJson) {
this.insertSeen.run(hash("sha256", eventJson, "buffer"), eventJson);
}
/** Plan branch repair on disk; only one transcript payload is decoded at a time. */
repairLegacyTranscript(source) {
this.database.exec("DELETE FROM tree; DELETE FROM tree_sets; DELETE FROM selected; DELETE FROM user_keys;");
const put = this.database.prepare("INSERT OR REPLACE INTO tree VALUES (?, ?)");
const get = this.database.prepare("SELECT node_json FROM tree WHERE id = ?");
const lookup = (id) => {
const row = get.get(id);
return row ? JSON.parse(String(row.node_json)) : void 0;
};
const setInsert = this.database.prepare("INSERT OR IGNORE INTO tree_sets VALUES (?, ?)");
const setHas = this.database.prepare("SELECT 1 FROM tree_sets WHERE kind = ? AND id = ?");
const setClear = this.database.prepare("DELETE FROM tree_sets WHERE kind = ?");
const diskSet = (kind) => ({
add: (id) => {
setInsert.run(kind, id);
},
has: (id) => setHas.get(kind, id) !== void 0,
clear: () => {
setClear.run(kind);
}
});
const repeatedRows = diskSet("repeated");
const user = this.database.prepare("INSERT OR REPLACE INTO user_keys VALUES (?, ?, ?)");
const readRow = this.database.prepare("SELECT event_json FROM rows WHERE source = ? AND seq = ?");
const update = this.database.prepare("UPDATE rows SET event_json = ? WHERE source = ? AND seq = ?");
let changed = false;
let recognized = true;
let headerSeq;
let lastEntry;
let terminalControl;
const rows = this.rows(source);
function* entries() {
for (const row of rows) {
const entry = JSON.parse(row.eventJson);
let eventJson = row.eventJson;
if (!isRecord(entry)) {
recognized = false;
continue;
}
if (normalizeLegacyOpenAICodexTranscriptMetadata([entry]) > 0) {
eventJson = JSON.stringify(entry);
update.run(eventJson, source, row.seq);
changed = true;
}
if (entry.type === "session") {
if (headerSeq !== void 0 || typeof entry.id !== "string") recognized = false;
headerSeq ??= row.seq;
continue;
}
const visibleKey = transcriptRepairUserKey(entry, false);
const strippedKey = transcriptRepairUserKey(entry, true);
if (typeof entry.id === "string" && (visibleKey || strippedKey)) user.run(entry.id, visibleKey?.slice(visibleKey.indexOf("\0") + 1) ?? null, strippedKey ?? null);
const indexed = isIndexedSessionEntry(entry);
const leafControl = isSessionTranscriptLeafControl(entry);
if (!indexed && !leafControl) recognized = false;
if (typeof entry.id === "string" && (indexed || leafControl)) {
const previous = lookup(entry.id);
if (previous) {
const previousRow = readRow.get(source, Number(previous.entry.importSeq));
if (previousRow && String(previousRow.event_json) === eventJson) {
repeatedRows.add(String(row.seq));
changed = true;
continue;
}
}
}
const metadata = { ...entry };
delete metadata.message;
const navigation = {};
for (const key of [
"type",
"id",
"parentId",
"targetId",
"appendParentId",
"appendMode"
]) if (Object.hasOwn(metadata, key)) navigation[key] = metadata[key];
lastEntry = navigation;
yield {
...navigation,
importSeq: row.seq
};
}
}
const navigation = scanSessionTranscriptNavigation(entries(), {
byId: {
get: lookup,
has: (id) => get.get(id) !== void 0,
set: (id, node) => {
if (get.get(id)) recognized = false;
put.run(id, JSON.stringify(node));
}
},
addNode: (node) => {
if (node.leafId !== void 0) terminalControl = isSessionTranscriptLeafControl(node.entry) ? node : void 0;
},
resetDescendantIds: diskSet("reset"),
invalidLeafControlIds: diskSet("invalid")
});
this.database.prepare(`DELETE FROM rows WHERE source = ? AND CAST(seq AS TEXT) IN (
SELECT id FROM tree_sets WHERE kind = 'repeated'
)`).run(source);
const select = this.database.prepare("INSERT OR REPLACE INTO selected VALUES (?, ?, ?, ?)");
const selected = this.database.prepare("SELECT 1 FROM selected WHERE id = ?");
const walk = (leaf, visible) => {
const seen = diskSet("walk");
seen.clear();
let id = leaf;
let child;
while (id !== null) {
if (seen.has(id)) return false;
seen.add(id);
const node = lookup(id);
if (!node) {
recognized = false;
break;
}
if (!visible && (selected.get(id) || isCanonicalSessionTranscriptEntry(node.entry))) break;
if (!isSessionTranscriptLeafControl(node.entry)) {
if (child) select.run(child.id, Number(child.entry.importSeq), node.id, visible ? 1 : 0);
child = node;
}
id = navigation.hasExplicitLeafUpdate ? node.parentId : typeof node.entry.parentId === "string" && node.entry.parentId.trim() ? node.entry.parentId : null;
}
if (child) select.run(child.id, Number(child.entry.importSeq), visible ? null : navigation.leafId, visible ? 1 : 0);
return true;
};
const valid = walk(navigation.hasExplicitLeafUpdate ? navigation.leafId : typeof lastEntry?.id === "string" ? lastEntry.id : null, true);
if (valid && this.database.prepare(`
SELECT 1 FROM user_keys inactive
JOIN user_keys active
JOIN selected s ON s.id = active.id AND s.visible = 1
AND inactive.stripped_key = COALESCE(s.parent_id, '') || char(0) || active.visible_key
WHERE NOT EXISTS (SELECT 1 FROM selected WHERE id = inactive.id) LIMIT 1
`).get() !== void 0 && headerSeq !== void 0) {
if (navigation.hasExplicitLeafUpdate) {
if (!walk(navigation.appendParentId, false)) recognized = false;
}
const chosen = this.database.prepare("SELECT seq, parent_id FROM selected ORDER BY seq");
for (const selectedRow of chosen.iterate()) {
const row = readRow.get(source, selectedRow.seq);
const event = JSON.parse(String(row.event_json));
if (navigation.hasExplicitLeafUpdate) event.parentId = selectedRow.parent_id;
update.run(JSON.stringify(event), source, selectedRow.seq);
}
let controlSeq = null;
if (terminalControl) {
controlSeq = Number(terminalControl.entry.importSeq);
const last = this.database.prepare("SELECT id FROM selected ORDER BY seq DESC LIMIT 1").get();
const row = readRow.get(source, controlSeq);
const event = JSON.parse(String(row.event_json));
event.parentId = last?.id ?? null;
event.appendParentId = navigation.appendParentId === null ? null : selected.get(navigation.appendParentId) ? navigation.appendParentId : last?.id ?? null;
update.run(JSON.stringify(event), source, controlSeq);
const next = this.database.prepare("SELECT MAX(seq) + 1 AS seq FROM rows WHERE source = ?").get(source);
this.database.prepare("UPDATE rows SET seq = ? WHERE source = ? AND seq = ?").run(next.seq, source, controlSeq);
controlSeq = Number(next.seq);
}
this.database.prepare(`DELETE FROM rows WHERE source = ? AND seq <> ?
AND (? IS NULL OR seq <> ?) AND seq NOT IN (SELECT seq FROM selected)`).run(source, headerSeq, controlSeq, controlSeq);
changed = true;
}
if (!valid || navigation.hasInvalidLeafControl || headerSeq === void 0) recognized = false;
const count = this.database.prepare("SELECT COUNT(*) AS count FROM rows WHERE source = ?").get(source);
return {
repaired: changed,
events: Number(count.count),
recognized
};
}
};
//#endregion
export { withSqliteSessionImportStage as t };