import { chmodSync, existsSync, mkdirSync } from "node:fs"; import { homedir } from "node:os"; import { dirname, join } from "node:path"; import { DatabaseSync } from "node:sqlite"; import type { CommittedBatch, WorkflowState } from "../domain/types.ts"; export interface StoreStats { path: string; checkpoints: number; } function defaultDatabasePath(): string { const root = process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state"); return join(root, "intent-petri", "intent-petri.sqlite"); } export class SqliteStore { readonly path: string; private readonly db: DatabaseSync; constructor(path = defaultDatabasePath()) { this.path = path; const directory = dirname(path); mkdirSync(directory, { recursive: true, mode: 0o700 }); chmodSync(directory, 0o700); this.db = new DatabaseSync(path); chmodSync(path, 0o600); this.db.exec("PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON; PRAGMA busy_timeout = 2000;"); this.db.exec(` CREATE TABLE IF NOT EXISTS checkpoint_audit ( session_id TEXT NOT NULL, tool_call_id TEXT NOT NULL, parent_entry_id TEXT, patch_id TEXT NOT NULL, revision INTEGER NOT NULL, snapshot_hash TEXT NOT NULL, committed_at TEXT NOT NULL, state_json TEXT NOT NULL, batch_json TEXT NOT NULL, PRIMARY KEY (session_id, tool_call_id) ); CREATE INDEX IF NOT EXISTS checkpoint_audit_session_revision ON checkpoint_audit(session_id, revision); `); this.secureSidecars(); } record( sessionId: string, toolCallId: string, parentEntryId: string | null, batch: CommittedBatch, state: WorkflowState, ): void { const existing = this.db .prepare("SELECT patch_id, snapshot_hash FROM checkpoint_audit WHERE session_id = ? AND tool_call_id = ?") .get(sessionId, toolCallId) as { patch_id: string; snapshot_hash: string } | undefined; if (existing) { if (existing.patch_id === batch.patch.patchId && existing.snapshot_hash === batch.snapshotHash) return; throw new Error(`SQLite audit collision for ${sessionId}/${toolCallId}`); } this.db .prepare(` INSERT INTO checkpoint_audit (session_id, tool_call_id, parent_entry_id, patch_id, revision, snapshot_hash, committed_at, state_json, batch_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `) .run( sessionId, toolCallId, parentEntryId, batch.patch.patchId, batch.revision, batch.snapshotHash, batch.committedAt, JSON.stringify(state), JSON.stringify(batch), ); this.secureSidecars(); } stats(sessionId: string): StoreStats { const row = this.db .prepare("SELECT COUNT(*) AS count FROM checkpoint_audit WHERE session_id = ?") .get(sessionId) as { count: number }; return { path: this.path, checkpoints: Number(row.count) }; } close(): void { this.db.close(); } private secureSidecars(): void { for (const file of [this.path, `${this.path}-wal`, `${this.path}-shm`]) { if (existsSync(file)) chmodSync(file, 0o600); } } }