Git keeps the record. SQLite keeps it quick.
A closed task leaves its who, what, how and why as plain JSON lines in the repository. An optional SQLite file on your machine indexes those lines so an agent can recall them fast. The index is derived: delete it and the next read builds it again. It is never the only copy of anything.
One authority per piece of data. Never two.
There are never two independently edited copies. The task in flight lives in the task state; a closed task lives in the record in git; the index only ever holds a copy it can rebuild.
state.py memory enable, yours| Data | Files mode | Packages mode | SQLite index |
|---|---|---|---|
| The task in flight | state.json, .ai/reports/<id>/ | the same, unchanged | never |
| A closed task | .ai/reports/<id>/, committed | entries in .ai/memory/*.jsonl, in git | derived only |
| Logs and the archived state | .ai/state/logs/<id>/, local | the same | derived only |
| Risks | .ai/project/known-risks.md | risk entries | derived only |
The index file is .ai/local/memory/index.sqlite3, inside the git-ignored .ai/local/. The pre-push check refuses a push whose commits carry any SQLite file under .ai/.
Refresh on every read. Fall back on any error.
Before it answers, the index compares the size and modification time of every source with what it imported last. Only a changed source is hashed and imported again, all in one transaction. If the index is busy or broken, recall and search read the files directly and say so in their header.
Rebuild
A user_version other than the one the code knows deletes the file and builds it again from the sources.
Rebuild
Its entries may also belong to another package, so the whole index is rebuilt rather than patched.
Stale
The index is marked stale. The next read refreshes it, or falls back to the files.
Scan
Search scans every entry. The results are identical, only slower, and memory status says so.
Already on every machine that runs python3.
The plugin uses the Python standard library only, offline, with no account, server or external model. Those constraints chose SQLite before any feature did.
No dependency
sqlite3 ships with Python. Nothing to install, nothing to pin, nothing that can be missing on a teammate's laptop.
One local file
The database and its WAL files sit in .ai/local/, outside git. Each developer's index is their own choice and their own copy.
Safe under concurrency
WAL lets readers work while one writer imports. Writers take BEGIN IMMEDIATE, so a crash leaves the old index or the new one, never half of each.
Full-text search
FTS5 is detected at run time. It only prefilters candidates; the same Python matcher decides in both backends, so results never differ.
Off the hot path
No per-tool-call hook imports the memory code or opens SQLite. The guard's latency budget is untouched.
A clone rebuilds it
A fresh clone builds the index from the packages alone, and shows every task exactly as the clone that exported it.
| Design | Why not |
|---|---|
| SQLite as the record, committed | Binary files do not merge; two writable copies would drift. |
| One JSON line per task | A 20–60 KB line is unreviewable in a PR, and loss becomes all or nothing. |
| Markdown as the record | Readable, but a reviewer's edit can silently break the parse of verbatim answers. |
JSONL plus a committed rendered .md | Two copies that drift, and twice the files. |
| git notes | Not fetched by default, hidden by hosting UIs, and a plain clone would lose the history. |
| An orphan branch | Needs a second push, is not reviewed with the PR, is absent from a default clone. |
| A vector or graph database | A non-goal: recall must be deterministic and work offline with the standard library. |
What was chosen instead: one canonical JSON object per line, ordered, with a seal per task carrying the digest of its entries. Each branch appends to its own batch, so a merge only adds files.
Five tables. One virtual.
packages remembers each source's signature. tasks and entries hold what the record says. entry_files answers "what touched this path", which is what recall asks first. conflicts keeps two seals of one task side by side; nothing is merged silently.
-- .ai/local/memory/index.sqlite3 · user_version 1 PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON; CREATE TABLE packages(path TEXT PRIMARY KEY, size, mtime_ns, sha256, author, period, entries); CREATE TABLE tasks(key TEXT PRIMARY KEY, task_id, started_at, closed_at, result, mode, w, workflow, goal, seal_rid); CREATE TABLE entries(rid TEXT PRIMARY KEY, key, seq, kind, ts, actor, by, runtime, status, superseded, text, json, package, line, origin); CREATE TABLE entry_files(rid REFERENCES entries ON DELETE CASCADE, path, own, PRIMARY KEY(rid, path)); CREATE TABLE conflicts(key, rid_a, rid_b, reason); CREATE TABLE meta(key TEXT PRIMARY KEY, value); -- stale, fts5, keep_rules -- only when this SQLite has FTS5 CREATE VIRTUAL TABLE entries_fts USING fts5(text, content='entries', tokenize='unicode61 remove_diacritics 0');
The files are fast enough. The index is faster.
The index was to be built only if recall over the files passed 300 ms, or search passed 1 s, at 1 000 tasks. The files stayed under both. It ships anyway, opt-in, by the developer's decision of 28 October 2026, for projects that keep years of history.
| Tasks | Recall, files | Search, files | Index build | Recall, index | Search, index |
|---|---|---|---|---|---|
| 100 | 12 ms | 11 ms | 24 ms | 6.5 ms | 2.8 ms |
| 1 000 | 129 ms | 118 ms | 245 ms | 65 ms | 24 ms |
| 10 000 | 1.4 s | 1.3 s | 2.8 s | 0.73 s | 0.31 s |
Storage at 10 000 tasks: 25 MB of packages in git, plus 73 MB of local index. Recall output stays within its token budget at every size. The legacy one-directory-per-task layout is slower still: 1.6 s for recall at 10 000.
Yours to switch, in your terminal.
Packages mode and the index are switched on by the human route: run from an agent, enable exits 5. The mode is per project; the author and the index are per developer, in the git-ignored .ai/local/memory/config.json.
Limits: the FTS5 prefilter is used for ASCII query tokens only, other queries scan. Without fcntl, two closes in one clone are last-writer-wins on the batch. Recall estimates tokens; it does not run a tokenizer.
# packages mode, with the local index state.py memory enable --period quarter --index sqlite git add .ai/policies/memory.json git commit -m "memory: packages mode" # what an agent asks before it edits state.py recall --files "src/Payment/**" --budget 1500 state.py memory search "rounding" --kind risk # the index is disposable state.py memory rebuild state.py memory status # index, fts5, packages, pre-push