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