Engram::Store
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-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
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.
Class methods
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.
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.
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.
Instance methods
Returns every memory id currently stored, for sync's set-diff against the working tree.
Deletes a memory row if present; the FTS index is kept in sync by a trigger. A no-op if id isn't stored.
Fetches the full record for id, or nil if no such memory is stored.
Inserts a new memory row; the FTS index is kept in sync by a trigger. Raises on a duplicate id.
Reads a value from engram_meta, or nil if the key has never been set.
Sets (or clears, with nil) which memory id supersedes id. Used to recompute demotion links after a sync.
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.
Updates an existing memory row in place; the FTS index is kept in sync by a trigger. Raises Engram::MemoryNotFoundError if id is absent.