UNPKG

trellis

Version:

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

213 lines (208 loc) 6.09 kB
import { canonicalOpBodyFromOp, init_canonical_op } from "./chunk-RUMOVKR4.js"; import { __require } from "./chunk-2ESYSVXG.js"; // src/core/persist/better-sqlite-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 BetterSqliteKernelBackend = class { db; _stmts; _initialized = false; constructor(dbPath) { try { const createRequire = __require("module").createRequire; const req = createRequire(__filename); const Database = req("better-sqlite3"); this.db = new Database(dbPath); } catch (e) { throw new Error( `Failed to initialize SQLite backend: ${e instanceof Error ? e.message : "Unknown error"}. Ensure better-sqlite3 is installed: npm install better-sqlite3` ); } } _prepareStatements() { const db = this.db; this._stmts = { insert: db.prepare( "INSERT OR REPLACE INTO ops (hash, kind, timestamp, agent_id, previous_hash, payload) VALUES (?, ?, ?, ?, ?, ?)" ), readAll: db.prepare("SELECT * FROM ops ORDER BY timestamp ASC"), readUntil: db.prepare( "SELECT * FROM ops WHERE rowid <= (SELECT rowid FROM ops WHERE hash = ?) ORDER BY rowid ASC" ), readAfter: db.prepare( "SELECT * FROM ops WHERE rowid > (SELECT rowid FROM ops WHERE hash = ?) ORDER BY rowid ASC" ), getByHash: db.prepare("SELECT * FROM ops WHERE hash = ?"), getLast: db.prepare("SELECT * FROM ops ORDER BY timestamp DESC LIMIT 1"), count: db.prepare("SELECT COUNT(*) as count FROM ops"), saveSnapshot: db.prepare( "INSERT INTO snapshots (last_op_hash, data, created_at) VALUES (?, ?, ?)" ), loadLatestSnapshot: db.prepare( "SELECT * FROM snapshots ORDER BY id DESC LIMIT 1" ), putBlob: db.prepare( "INSERT OR REPLACE INTO blobs (hash, content) VALUES (?, ?)" ), getBlob: db.prepare("SELECT content FROM blobs WHERE hash = ?"), hasBlob: db.prepare("SELECT 1 FROM blobs WHERE hash = ?") }; } init() { if (this._initialized) return; this.db.exec(SCHEMA_SQL); this._prepareStatements(); this._initialized = true; } append(op) { const payload = canonicalOpBodyFromOp(op); this._stmts.insert.run( op.hash, op.kind, op.timestamp, op.agentId, op.previousHash ?? null, payload ); } appendBatch(ops) { for (const op of ops) { this.append(op); } } readAll() { const rows = this._stmts.readAll.all(); return rows.map(this._rowToOp); } readUntil(opHash) { const row = this._stmts.getByHash.get(opHash); if (!row) return []; const rows = this._stmts.readUntil.all(opHash); return rows.map(this._rowToOp); } readUntilTimestamp(isoTimestamp) { const rows = this.db.prepare("SELECT * FROM ops WHERE timestamp <= ? ORDER BY timestamp ASC").all(isoTimestamp); return rows.map(this._rowToOp); } readAfter(opHash) { const row = this._stmts.getByHash.get(opHash); if (!row) return this.readAll(); const rows = this._stmts.readAfter.all(opHash); return rows.map(this._rowToOp); } getByHash(hash) { return this.getOpByHash(hash); } getOpByHash(hash) { const row = this._stmts.getByHash.get(hash); return row ? this._rowToOp(row) : void 0; } getLastOp() { const row = this._stmts.getLast.get(); return row ? this._rowToOp(row) : void 0; } getOpCount() { const row = this._stmts.count.get(); return row?.count ?? 0; } count() { return this.getOpCount(); } saveSnapshot(lastOpHash, data) { this._stmts.saveSnapshot.run( lastOpHash, JSON.stringify(data), (/* @__PURE__ */ new Date()).toISOString() ); } loadLatestSnapshot() { const row = this._stmts.loadLatestSnapshot.get(); if (!row) return void 0; return { lastOpHash: row.last_op_hash, data: JSON.parse(row.data) }; } putBlob(hash, content) { this._stmts.putBlob.run(hash, Buffer.from(content)); } getBlob(hash) { const row = this._stmts.getBlob.get(hash); if (!row) return void 0; return new Uint8Array(row.content); } hasBlob(hash) { const row = this._stmts.hasBlob.get(hash); return !!row; } findCommonAncestor(hashA, hashB) { const ancestorsA = /* @__PURE__ */ new Set(); let cursor = hashA; while (cursor) { ancestorsA.add(cursor); const op = this.getOpByHash(cursor); cursor = op?.previousHash; } cursor = hashB; while (cursor) { if (ancestorsA.has(cursor)) { return this.getOpByHash(cursor); } const op = this.getOpByHash(cursor); cursor = op?.previousHash; } return void 0; } close() { this.db?.close(); } _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 { BetterSqliteKernelBackend };