UNPKG

trellis

Version:

Agentic State Engine — event-sourced causal graph with branching, decision traces, and realtime sync for AI-native applications

304 lines (301 loc) 9.05 kB
import { canonicalOpBodyFromOp, init_canonical_op } from "./chunk-RUMOVKR4.js"; import { __require } from "./chunk-2ESYSVXG.js"; // src/core/persist/sqljs-backend.ts init_canonical_op(); var SCHEMA_SQL = ` CREATE TABLE IF NOT EXISTS ops ( hash TEXT PRIMARY KEY, kind TEXT NOT NULL, timestamp TEXT NOT NULL, agent_id TEXT NOT NULL, previous_hash TEXT, payload TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS snapshots ( id INTEGER PRIMARY KEY AUTOINCREMENT, last_op_hash TEXT NOT NULL, data TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS blobs ( hash TEXT PRIMARY KEY, content BLOB NOT NULL ); CREATE INDEX IF NOT EXISTS idx_ops_kind ON ops(kind); CREATE INDEX IF NOT EXISTS idx_ops_timestamp ON ops(timestamp); CREATE INDEX IF NOT EXISTS idx_ops_agent ON ops(agent_id); CREATE INDEX IF NOT EXISTS idx_ops_previous ON ops(previous_hash); CREATE INDEX IF NOT EXISTS idx_snapshots_op ON snapshots(last_op_hash); `; var SqlJsKernelBackend = class _SqlJsKernelBackend { constructor(opts) { this.opts = opts; this.flushEvery = opts.autoFlushEvery ?? 50; } db; stmts; writes = 0; flushEvery; initialized = false; /** * Async factory — sql.js WASM init is async, but the resulting backend * exposes the synchronous KernelBackend surface, so it slots into the * existing kernel without interface changes. */ static async create(opts) { const backend = new _SqlJsKernelBackend(opts); await backend.bootstrap(); return backend; } async bootstrap() { let initSqlJs; try { const mod = await import("sql.js"); initSqlJs = mod.default ?? mod; } catch (e) { throw new Error( 'SqlJsKernelBackend requires the optional dependency "sql.js". Install it: npm install sql.js' ); } let sqljsDistDir = null; if (typeof window === "undefined") { try { const moduleMod = await import("module"); const pathMod = await import("path"); const req = moduleMod.createRequire(import.meta.url); const sqlJsEntry = req.resolve("sql.js"); sqljsDistDir = pathMod.dirname(sqlJsEntry); } catch { sqljsDistDir = null; } } const SQL = await initSqlJs({ locateFile: (file) => { if (typeof window !== "undefined") return `/sql-wasm/${file}`; if (sqljsDistDir) return `${sqljsDistDir}/${file}`; return file; } }); const existing = this.loadFromDisk(); this.db = existing ? new SQL.Database(existing) : new SQL.Database(); } loadFromDisk() { if (this.opts.dbPath === ":memory:") return null; try { const fs = __require("fs"); if (!fs.existsSync(this.opts.dbPath)) return null; return new Uint8Array(fs.readFileSync(this.opts.dbPath)); } catch { return null; } } flushToDisk() { if (this.opts.dbPath === ":memory:") return; try { const fs = __require("fs"); const path = __require("path"); const data = this.db.export(); fs.mkdirSync(path.dirname(this.opts.dbPath), { recursive: true }); const tmp = `${this.opts.dbPath}.tmp`; fs.writeFileSync(tmp, Buffer.from(data)); fs.renameSync(tmp, this.opts.dbPath); } catch { } } init() { if (this.initialized) return; this.db.exec(SCHEMA_SQL); this.prepareStatements(); this.initialized = true; } prepareStatements() { this.stmts = { insert: this.db.prepare( `INSERT OR IGNORE INTO ops (hash, kind, timestamp, agent_id, previous_hash, payload) VALUES ($hash, $kind, $timestamp, $agentId, $previousHash, $payload)` ), readAll: this.db.prepare( `SELECT hash, kind, timestamp, agent_id, previous_hash, payload FROM ops ORDER BY rowid ASC` ), readUntil: this.db.prepare( `SELECT hash, kind, timestamp, agent_id, previous_hash, payload FROM ops WHERE rowid <= (SELECT rowid FROM ops WHERE hash = $hash) ORDER BY rowid ASC` ), readAfter: this.db.prepare( `SELECT hash, kind, timestamp, agent_id, previous_hash, payload FROM ops WHERE rowid > (SELECT rowid FROM ops WHERE hash = $hash) ORDER BY rowid ASC` ), getByHash: this.db.prepare( `SELECT hash, kind, timestamp, agent_id, previous_hash, payload FROM ops WHERE hash = $hash` ), getLast: this.db.prepare( `SELECT hash, kind, timestamp, agent_id, previous_hash, payload FROM ops ORDER BY rowid DESC LIMIT 1` ), count: this.db.prepare(`SELECT COUNT(*) AS cnt FROM ops`), saveSnapshot: this.db.prepare( `INSERT INTO snapshots (last_op_hash, data) VALUES ($lastOpHash, $data)` ), loadSnapshot: this.db.prepare( `SELECT last_op_hash, data FROM snapshots ORDER BY id DESC LIMIT 1` ), putBlob: this.db.prepare( `INSERT OR IGNORE INTO blobs (hash, content) VALUES ($hash, $content)` ), getBlob: this.db.prepare( `SELECT content FROM blobs WHERE hash = $hash` ), hasBlob: this.db.prepare( `SELECT 1 AS present FROM blobs WHERE hash = $hash` ) }; } append(op) { const payload = canonicalOpBodyFromOp(op); this.stmts.insert.run({ $hash: op.hash, $kind: op.kind, $timestamp: op.timestamp, $agentId: op.agentId, $previousHash: op.previousHash ?? null, $payload: payload }); this.stmts.insert.reset(); this.tickFlush(); } readAll() { return this.runAll(this.stmts.readAll); } readUntil(hash) { return this.runAll(this.stmts.readUntil, { $hash: hash }); } readAfter(hash) { return this.runAll(this.stmts.readAfter, { $hash: hash }); } readUntilTimestamp(iso) { const stmt = this.db.prepare( `SELECT hash, kind, timestamp, agent_id, previous_hash, payload FROM ops WHERE timestamp <= $ts ORDER BY rowid ASC` ); const rows = this.runAll(stmt, { $ts: iso }); stmt.free(); return rows; } getByHash(hash) { return this.runOne(this.stmts.getByHash, { $hash: hash }); } getLastOp() { return this.runOne(this.stmts.getLast); } getOpCount() { this.stmts.count.bind({}); const has = this.stmts.count.step(); const row = has ? this.stmts.count.getAsObject() : { cnt: 0 }; this.stmts.count.reset(); return Number(row.cnt ?? 0); } saveSnapshot(lastOpHash, data) { this.stmts.saveSnapshot.run({ $lastOpHash: lastOpHash, $data: typeof data === "string" ? data : JSON.stringify(data) }); this.stmts.saveSnapshot.reset(); this.tickFlush(); } loadLatestSnapshot() { this.stmts.loadSnapshot.bind({}); const has = this.stmts.loadSnapshot.step(); if (!has) { this.stmts.loadSnapshot.reset(); return void 0; } const row = this.stmts.loadSnapshot.getAsObject(); this.stmts.loadSnapshot.reset(); return { lastOpHash: row.last_op_hash, data: row.data }; } putBlob(hash, content) { this.stmts.putBlob.run({ $hash: hash, $content: content }); this.stmts.putBlob.reset(); this.tickFlush(); } getBlob(hash) { this.stmts.getBlob.bind({ $hash: hash }); const has = this.stmts.getBlob.step(); if (!has) { this.stmts.getBlob.reset(); return void 0; } const row = this.stmts.getBlob.getAsObject(); this.stmts.getBlob.reset(); if (!row.content) return void 0; return row.content instanceof Uint8Array ? row.content : new Uint8Array(row.content); } hasBlob(hash) { this.stmts.hasBlob.bind({ $hash: hash }); const has = this.stmts.hasBlob.step(); this.stmts.hasBlob.reset(); return !!has; } close() { try { this.flushToDisk(); } finally { for (const s of Object.values(this.stmts ?? {})) s?.free?.(); this.db?.close?.(); } } /** Force a write of the in-memory DB image to disk. */ flush() { this.flushToDisk(); } runAll(stmt, params = {}) { stmt.bind(params); const rows = []; while (stmt.step()) rows.push(rowToOp(stmt.getAsObject())); stmt.reset(); return rows; } runOne(stmt, params = {}) { stmt.bind(params); const has = stmt.step(); const row = has ? stmt.getAsObject() : void 0; stmt.reset(); return row ? rowToOp(row) : void 0; } tickFlush() { if (this.flushEvery === 0) return; if (++this.writes % this.flushEvery === 0) this.flushToDisk(); } }; function rowToOp(row) { const payload = JSON.parse(row.payload); return { hash: row.hash, kind: row.kind, timestamp: row.timestamp, agentId: row.agent_id, previousHash: row.previous_hash ?? void 0, facts: payload.facts, links: payload.links, deleteFacts: payload.deleteFacts, deleteLinks: payload.deleteLinks, // ADR 0021: `v` absent ⇒ legacy v1 op, never reverified. v: payload.v, provenance: payload.provenance ?? void 0 }; } export { SqlJsKernelBackend };