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
JavaScript
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
};