UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

257 lines (256 loc) 11 kB
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 };