class

Engram::Store

Inherits Reference < Object

Owns the per-clone SQLite cache (normally .git/engram.db): schema creation and every read/write needed to keep it in sync with the memory migration files on disk. The database is a disposable cache — deleting it and running sync again fully rebuilds it from the working tree.

Constants

BUSY_TIMEOUT_MS = 5000

Busy-wait window handed to sqlite's own retry loop (via PRAGMA busy_timeout) before a lock contention gives up and raises "database is locked" — long enough to ride out another local process's (a post-checkout hook, a running engram mcp server) brief write, short enough that a truly stuck lock still fails within one interactive command.

Constructors

new(db_path : String)

Opens (creating if needed) the sqlite database at db_path, ensures its schema exists, and self-heals a corrupted cache file: since .git/engram.db is a disposable cache of the migration files in .agents/memories/, a corrupted or truncated file is deleted and replaced with a fresh empty database here, and the next sync fully repopulates it. A permission or lock failure opening the file is a different, non-disposable problem and always propagates — only a genuine corruption signature triggers a rebuild. The actual open-and-repair logic lives in self.open_and_repair (a class method working on locals, not ivars) so this assignment stays a single, unconditional statement: Crystal's definite-assignment check for instance variables treats anything assigned inside a begin/rescue as potentially skipped unless it's also assigned in the rescue branch, which would otherwise force @db/@db_path through every retry path here too.

Source

Class methods

connection_uri(path : String, busy_timeout_ms : Int32 = BUSY_TIMEOUT_MS) : URI

Builds the sqlite3:// connection URI the underlying driver expects for path, percent-encoding the filename so a real repo pathname containing +, ?, #, %, or a space is treated as literal filename bytes rather than decoded into a space (+), parsed as the start of query params (?) or a URI fragment (#) — a naive "sqlite3://#{path}" interpolation hands the driver a different, mis-decoded path, or makes DB.open raise outright. Also sets a busy_timeout pragma so a concurrent sync/engram mcp connection retries instead of hard-failing with "database is locked". Public so every other opener of this same sqlite file (Search, the CLI's DB integrity check) can build the identical, safe URI instead of re-interpolating the path themselves.

Source
corruption_error?(ex : SQLite3::Exception) : Bool

True for the sqlite error strings a corrupted or truncated file surfaces (e.g. "file is not a database", "database disk image is malformed"). Deliberately does NOT match lock/busy errors ("database is locked") or anything else — those must always propagate, never be treated as a green light to delete a perfectly fine database out from under a concurrent writer.

Source
open_and_repair(path : String) : DB::Database

Opens path, ensures its schema exists, and repairs a corrupted (but readable) cache file by deleting and recreating it — retried exactly once, so a second failure is a real, non-corruption error and propagates. A permission or lock failure opening the file surfaces as DB::ConnectionRefused (not SQLite3::Exception) and always propagates untouched, never mistaken for disposable-cache corruption.

Source

Instance methods

all_ids

Returns every memory id currently stored, for sync's set-diff against the working tree.

Source
close

Closes the underlying database connection.

Source
counts

Returns the number of active (not superseded) and superseded memories.

Source
delete_memory(id : Int64) : Nil

Deletes a memory row if present; the FTS index is kept in sync by a trigger. A no-op if id isn't stored.

Source
get(id : Int64) : MemoryRecord | Nil

Fetches the full record for id, or nil if no such memory is stored.

Source
insert_memory(id : Int64, slug : String, title : String, topics : Array(String), author : String | Nil, body : String, supersedes : Array(Int64), file_path : String, embedding : Bytes | Nil = nil, applied_at : Time = Time.utc) : Nil

Inserts a new memory row; the FTS index is kept in sync by a trigger. Raises on a duplicate id.

Source
meta(key : String) : String | Nil

Reads a value from engram_meta, or nil if the key has never been set.

Source
set_meta(key : String, value : String) : Nil

Upserts a key/value pair into engram_meta.

Source
set_superseded_by(id : Int64, superseded_by : Int64 | Nil) : Nil

Sets (or clears, with nil) which memory id supersedes id. Used to recompute demotion links after a sync.

Source
transaction

Runs block inside a single sqlite transaction on one connection, so callers (e.g. Sync) can batch several CRUD calls on this Store atomically: either all of them land or (on an exception, which re-raises after rollback) none do. Do not call embed() or any other network I/O inside block — it would hold sqlite's write lock across the network for as long as the request takes.

Source
update_memory(id : Int64, slug : String, title : String, topics : Array(String), author : String | Nil, body : String, supersedes : Array(Int64), file_path : String, embedding : Bytes | Nil = nil, applied_at : Time = Time.utc) : Nil

Updates an existing memory row in place; the FTS index is kept in sync by a trigger. Raises Engram::MemoryNotFoundError if id is absent.

Source