A bootstrap for local, embedded, long-term memory for AI coding agents (built for Claude Code).
Your agent's text is embedded with a local model and stored as a vector in a local Turso (SQLite-compatible) database, so it recalls memories by meaning — with an IDF-weighted keyword credit so exact ids and error strings still surface. Everything runs on your machine: no cloud, no API cost, no data leaving your laptop. The database is the single source of truth; a daily job backs it up locally, and a multiprocess flag lets several agent instances share it at once.
- Semantic recall — hybrid vector + keyword search over everything the agent has learned.
- Two memory types — timeless, revisable semantic facts and time-anchored, append-only episodic events.
- Configurable episodic policy — pick, at setup, how aggressively events are logged (every prompt → only when you ask).
- Fully local & private — a local embedding model (Ollama) and a local database; nothing is sent to a cloud service.
- Multi-instance safe — several agent sessions can share one database concurrently.
- Graceful degradation — if the embedding model is offline, recall falls back to keyword-only and the DB stays usable.
- Sleep & deep sleep — user-triggered consolidation: sift events into facts, then (deep) purge what you agree is dead, merge near-duplicates, and derive patterns and trends across the whole event log. All of it read by subagents, so the cost doesn't land in your session's context.
- Coworkers — optional named AI personas with scoped memory and trust-gated autonomy.
- Backups built in — validated, gzipped daily dumps with daily + weekly retention.
Neither dependency is bundled — install both before setup.
Turso 0.7.0+ — the Rust rewrite of SQLite with native vector support. 0.7.2 is what this project targets, and what the behaviour documented here was verified against; 0.7.0 remains the floor, not a tested version.
curl -sSL tur.so/install | shtursodb is always opened with --experimental-multiprocess-wal, and on Windows
with --vfs experimental_win_iocp beside it (see
Concurrency).
Ollama serves the embedding model locally.
# macOS
brew install ollama
brew services start ollama
# Pull the embedding model (multilingual, 1024-dim)
ollama pull bge-m3By default it must be reachable at http://localhost:11434. bge-m3 is the default model; one embedding model per database — the writer refuses to mix models in the same DB.
Python 3 (standard library only — no pip install needed) and bash.
instructions/SETUP.md is a runbook Claude Code can execute for you. In a session
started in this repo, just say "run SETUP.md" — the agent verifies/installs the
two dependencies (asking before each install), pulls the embedding model, asks
where to store the database and which episodic-memory policy to use,
activates the instructions, creates the DB, and finally offers to set up coworkers.
-
Install the dependencies above.
-
Clone this repository somewhere local (not a cloud-synced folder — see below).
-
Choose where the live database file lives and export it (keep it local; add the export to your shell profile so every session and script agrees):
export SUPERCHARGED_MEMORY_TURSO_PATH="${XDG_DATA_HOME:-$HOME/.local/share}/turso/supercharged-memory.db" # your choice
-
Register Turso as a Claude Code MCP server named
turso, launched astursodb "$SUPERCHARGED_MEMORY_TURSO_PATH" --mcp --experimental-multiprocess-wal(on Windows append--vfs experimental_win_iocp— see Concurrency; this applies to everytursodbcommand below too). -
Activate the memory instructions in your agent, baking in your database path and episodic policy:
SUPERCHARGED_MEMORY_TURSO_PATH="$SUPERCHARGED_MEMORY_TURSO_PATH" EPISODIC_MODE=major-events bash scripts/install-claude-md.shThis renders
CLAUDE.md.templateinto~/.claude/CLAUDE.mdbetween managed markers, substitutingBASE_PATHandEPISODIC_MODE. Re-running replaces the managed block and keeps everything outside it — and, because both of those values are rendered into the block, a re-run reads them back out of it rather than resetting them: a barebash scripts/install-claude-md.shpreserves this machine's episodic mode, and refuses outright if it would repoint the block at a differentBASE_PATH(see Configuration). Restart your session to pick it up. -
Create the database and check availability (the scripts refuse to silently create an empty DB, so build the schema first):
tursodb "$SUPERCHARGED_MEMORY_TURSO_PATH" --experimental-multiprocess-wal < schema.sql # +`--vfs experimental_win_iocp` on Windows python3 scripts/recall.py --status # MISSING | EMPTY | DEGRADED n | READY n
Scripts read these environment variables (defaults in scripts/memlib.py):
| Variable | Default | Notes |
|---|---|---|
SUPERCHARGED_MEMORY_TURSO_PATH |
${XDG_DATA_HOME:-~/.local/share}/turso/supercharged-memory.db |
The live DB. Keep it local — never in a cloud-synced folder; cloud sync corrupts live SQLite. |
TURSO_BIN |
~/.turso/tursodb |
Path to the tursodb binary. |
OLLAMA_URL |
http://localhost:11434 |
Ollama endpoint. |
EMBED_MODEL |
bge-m3 |
Embedding model; one per DB. |
RECALL_ALPHA |
0.15 |
Keyword weight in recall ranking. Corpus-calibrated — see below. |
BACKUP_DIR |
./Backups |
Where daily dumps are written. |
TURSO_VFS |
experimental_win_iocp on Windows, else unset |
tursodb IO backend, paired with --experimental-multiprocess-wal. Set it to none (or empty) to drop --vfs entirely. See instructions/SETUP.md, Windows. |
SUPERCHARGED_MEMORY_EVAL_DIR |
<db parent>/eval |
Query-embedding cache for the eval harness. Derived data; the cases themselves live in the DB. |
install-claude-md.sh reads three more env vars. bash scripts/install-claude-md.sh --help
is the authoritative copy of this list; the table below is the prose one.
| Variable | Default | Notes |
|---|---|---|
EPISODIC_MODE |
major-events, but recovered from the managed block being replaced when there is one |
Episodic-storage policy (see below), rendered into the block. Validated to one of the four keys. A block whose policy line can't be read is refused rather than silently defaulted. |
BASE_PATH |
repo root (the parent of the folder holding the script) | Points at this repo; rendered into every path in the block. If the existing block was rendered from a different BASE_PATH, the run is refused — a stray run from a temporary clone or git worktree would otherwise repoint a live config at it. |
ALLOW_BASE_PATH_CHANGE |
unset | Set to 1 to allow that repoint (the repo genuinely moved). |
PYTHON_BIN |
python3, python on Windows |
Interpreter rendered into the command prefix. Windows has no python3 — the name is a Store alias stub that prints "Python was not found" and EXITS 0, so a rendered python3 command reads as a successful empty result. Takes a command (python, py -3) or a path; a path with spaces (C:/Program Files/Python314/python.exe) is quoted for you, and whitespace that is neither is refused rather than rendered into a prefix that splits at the space. |
TURSO_VFS |
unset; experimental_win_iocp on Windows |
VFS passed to every tursodb open. Windows' default IO backend refuses --experimental-multiprocess-wal without it. Set TURSO_VFS=none to pass no --vfs at all, should a later release rename the backend. |
SUPERCHARGED_MEMORY_TURSO_PATH is read too, and falls back to the value in
~/.claude/settings.json before the XDG default, but the template stopped
referencing it in 05b1967 — so today it is substituted into nothing and only shows
up in the installer's closing report. The path that matters at runtime is the one in
~/.claude/settings.json (see above).
Every value's source — env, recovered from the block, or default — is named in that closing report, so a re-install is auditable after the fact.
Chosen at setup and written into the agent's instructions, this controls how aggressively episodic events are logged (it does not affect semantic facts, gotchas, or corrections — those are always stored autonomously):
EPISODIC_MODE |
Behavior |
|---|---|
every-prompt |
Store an episodic note for every prompt / turn. |
major-actions |
Store every substantive action; skip quick questions and clarifications. |
major-events |
Store only major events — feature done, bug resolved, decision, milestone, incident. (Default.) |
manual |
Store episodic memory only when you explicitly ask. |
Change it later by re-running install-claude-md.sh with a different
EPISODIC_MODE.
If you were running Claude Code with ordinary file-based memory before installing
this — a curated ~/.claude/CLAUDE.md, a ~/.claude/memory/*.md set,
project-scoped ~/.claude/projects/<slug>/memory/*.md — none of it is in the
database, and the setup runbook's candidate check will not find it: that looks for
a Turso DB or a backup dump, which is a different thing.
instructions/MIGRATE-EXISTING-MEMORY.md imports those files as semantic memory
and then runs the deep-sleep phases that apply (compaction, the required
topic-index rebuild, the Verify pass — imported memory is old by definition).
Setup Step 7 offers it; you can also ask for it later ("migrate my memory").
scripts/find-existing-memory.py— read-only scan, needs neither the database nor Ollama. Reports what exists per file:kind(memory/index/claude/empty),scope(global/project), char count. Counts of what there is to import coverkind: memoryonly, so a plainCLAUDE.mdis not reported as memory to migrate. Everything it leaves out lands in a reported field instead of vanishing —over_max_text({path, chars, reason}, flagged against the 1800-char effective budget, not the raw 2000-charMAX_TEXT, becauseremember.pyappends--keywordsinto the same field the cap counts — so a 1900-char file is flagged here rather than refused mid-import),empty(nothing to import; the writer refuses an empty--text),unreadable(unreadable file or directory, broken symlink, symlink loop) andexcluded_dirs(.git/agentsat a memory root, or a symlink pointing above it).SETUP.mdStep 7 gates on the computednothing_to_do— no importable memory and nothing skipped — rather than onn_memory_files, which is 0 for a config dir whose only memory file is unreadable.- Additive, not reversible. No source file is deleted or edited, and every row
carries
source='migration'plus afile_referenceback to its file. There is no bulk undo of the imported rows, though: the source files are always the way back, a pre-import backup restores a database that already held rows, and on a fresh (EMPTY) one — the common case — there is no pre-import dump because there is nothing to lose, so the reset is a rebuild fromschema.sql. - Classification runs in subagents, the same contract the sleep runbooks use; the orchestrator never reads the corpus in bulk.
The runtime state lives outside this repo — the database, ~/.claude/CLAUDE.md,
~/.claude/settings.json — so pulling new commits does not update a machine on its
own, and some commits need a manual step. instructions/UPDATE.md is the runbook
that closes that gap. In a session:
update the memory system
It reads the sync stamp, pulls, applies any pending migration notes, shows you what
changed in the instructions, and re-renders ~/.claude/CLAUDE.md. Nothing is applied
without asking.
install-claude-md.sh writes the repo commit it rendered from as the first line
inside the managed block:
<!-- BEGIN agentic-memory (managed by install-claude-md.sh) -->
<!-- supercharged-memory: synced-at 9f3c1ab… -->
That one line is the whole ledger — the repo's git history supplies the rest, so
there is no separate state file to drift out of sync. git log <stamp>..HEAD yields
both the template changes worth explaining and the migrations still pending. Two
special values: a -dirty suffix means the install was rendered from an uncommitted
working tree, and unknown means BASE_PATH was not a git work tree (a plain file
copy). An install predating this feature has no stamp at all; UPDATE.md then lists
every migration note and asks which ones already apply, rather than guessing a base
commit.
Every change that breaks an existing install ships a note here: schema changes,
script CLI changes, renamed env vars or paths, changed instruction contracts,
anything needing a manual step. One file per change,
migration-steps/YYYY-MM-DD-<slug>.md:
| Part | Purpose |
|---|---|
commit: <sha> (first line) |
The commit that introduced the break. UPDATE.md classifies notes with git merge-base --is-ancestor, so a missing or unreachable sha is reported as malformed instead of silently applied. |
| What broke | Schema / CLI / instruction contract / on-disk runtime state. |
| How to resolve | Concrete commands, in order. |
| Verification | What to run afterwards, and what output proves it worked. |
Because a commit cannot reference its own sha, the note lands in the commit right
after the breaking one and points back at it — pushed together, so no git pull
can leave a machine between the break and its instructions.
Memory operations go through the helper scripts in scripts/ (a 1024-float
vector is clumsy to inline into MCP calls). The Turso MCP server is for ad-hoc
SQL and inspection only — revisions go through the scripts.
# Recall (hybrid vector + keyword; add --coworker to scope to a persona)
python3 scripts/recall.py "how do I run a single test?" [--table semantic|episodic|both] [--project <id>] [--k N]
python3 scripts/recall.py --baseline # always-apply rules, loaded every session
python3 scripts/recall.py --status # availability check
python3 scripts/recall.py --candidates # other memory DBs/backups found on this machine
# Store one memory (see the script docstrings for the full flag set)
python3 scripts/remember.py --table semantic --topic <t> --category <c> \
--keywords "id, error string, synonyms" --source <s> --model <your-model-id> --text "..."
python3 scripts/remember.py --table semantic --supersedes <old-id> ... # revise a fact
python3 scripts/remember.py --table episodic --event-type bug_fix --importance notable ...
# Bulk import a directory as a fresh start (one file = one memory)
python3 scripts/backfill.py --dir <path> [--glob '*.md'] [--category project]On session start the agent runs recall.py --status first:
MISSING— nothing at the configured path → stop and ask the user (see below).EMPTY— DB exists but holds no memories → check--candidates, then offer to backfill from a directory.DEGRADED n— Ollama down, DB fine → still load baseline and use keyword-only recall, but don't store new memories.ERROR— fall back to the agent's built-in memory store.READY n— load baseline and proceed.nis a liveness signal, not a corpus size: it counts allsemantic_memoryrows (superseded and retired included) plus allepisodic_memoryrows. Never compare it against a current-rows count such assleep.py --staleness'sn_current, and never diff it across time — the DB is shared, so it moves on its own.
The most likely cause of MISSING is a wrong path, not lost data — and the two
are indistinguishable unless you look. An agent that responds by creating a fresh DB
silently strands the real one; an agent that restores a backup over a live DB loses
everything since that backup. So nothing in this system ever creates, restores, or
overwrites a database on its own:
memlib.require_db()refuses to let any script auto-create an empty DB.--status MISSINGprints the configured path, where that path came from (env var vs. built-in default), and any other memory DB or backup it found — each with its memory count.--candidatesruns that search on demand, at any time.- The agent instructions require it to work through wrong path → candidate DB → ask the user before restore is even considered, and creating an empty DB is the last resort with explicit confirmation.
A path that silently defaults is the root of this whole failure mode, so note that
SUPERCHARGED_MEMORY_TURSO_PATH must be set where Claude Code can see it —
~/.claude/settings.json under env. A shell profile alone is not enough: the
agent's Bash tool runs non-interactively and never sources ~/.zshrc or ~/.bashrc.
scripts/memlib.py is the shared core every script imports: config, embedding
(with a dimension assert), compact vector literals, SQL escaping, and a robust
tursodb runner — a failure is a non-zero exit, an error on stderr, or a stdout
that is nothing but a diagnostic, and the busy backoff fires on the unanchored
phrase database is busy|locked in either stream (see
Concurrency). The rest are thin CLIs on top:
remember.py— one memory = one row (no chunking). Folds--keywordsinto the text, embeds it, inserts. Guards: baseline needs--confirm-baseline; semantic refuses a near-duplicate (cosine < 0.10) unless--force(episodic is exempt — events recur);--supersedes <id>inserts a revision and marks the old row superseded in one call; refuses a DB embedded with a different model; refuses an empty or malformed--created-at(YYYY-MM-DD HH:MM:SS, or a bareYYYY-MM-DDnormalised to midnight — anything else is kept verbatim by SQLite and read as NULL by every date function), before paying for the embedding. Its over-cap refusal splits the total into your--textand the appended keyword line, since the cap counts both.recall.py— hybrid search, scoreddist - RECALL_ALPHA * kw(lower wins): brute-forcevector_distance_cosminus an IDF-weighted keyword credit, normalised to 0..1. IDF is measured per query against the rows being ranked, so common words demote themselves — there is no stopword list, and the 8 rarest query tokens are the ones that count. Degrades to keyword-only if Ollama is down. Also--baseline,--status,--count.backfill.py— import a directory of files for a fresh start (skips files over the cap).seed.py— scaffold for a one-time bootstrap load (empty by default).sleep.py— the mechanical write primitives a sleep pass needs, no judgment of its own:--mark-processed,--retire,--rebuild-topics, plus deep sleep's--purge(guarded hard delete) and--cluster(read-only similarity grouping).supercharged-memory-backup.sh— the daily backup (below).restore.py— replay a dump into a fresh DB and verify it, splitting on real statement boundaries because piping a dump intotursodbrestores only a fraction of the rows (below). Refuses an existing target; exits non-zero on any row mismatch.install-claude-md.sh— render the template into~/.claude/CLAUDE.md, stamping the repo commit it rendered from (see Staying up to date). Recovers the values already in the block it is about to replace, and refuses aBASE_PATHrepoint.coworkers.py— manage AI personas (below).
Two tables (see schema.sql), one row per memory. Both embed memory_text into
embedding and search by brute-force vector_distance_cos. Hard caps are
enforced via CHECK (SQLite ignores declared VARCHAR sizes): memory_text ≤
2000; project/topic/source/model/embed_model ≤ 128. Enums are
CHECK-enforced. Keywords are not a column — they live inside memory_text
(appended by remember.py --keywords), so they're both embedded and searchable.
id, created_at, updated_at, project (NULL = global; a tracking-tool
work-item id), topic, category ∈
baseline|user|feedback|project|reference|pattern, source, model,
embed_model, memory_text, file_reference, embedding F32_BLOB(1024),
superseded_by (current truth = WHERE superseded_by IS NULL), retired_at
(soft-delete set by a sleep pass — current truth also requires
retired_at IS NULL).
baseline = must-always-apply rules, loaded every session start
(recall.py --baseline), not by search. Storing a baseline memory requires
explicit user confirmation (--confirm-baseline). None are seeded by default.
pattern = derived, written only by a deep sleep pass: a recurrence or trend
found across episodic events, carrying the episodic ids it came from so the claim
can be re-checked rather than trusted. Revised through the normal supersede chain
when the count changes.
A hard DELETE happens in exactly one place — deep sleep's purge gate, on rows
the user selects, after a backup (see Deep sleep above). Everywhere else,
obsolescence is retired_at or a supersede.
id, created_at (event time), project, topic, event_type ∈
project_start|bug_fix|feature_complete|decision|milestone|incident|note,
importance ∈ routine|notable|major, source, model, embed_model,
memory_text, file_reference, embedding F32_BLOB(1024), processed_at
(set once a sleep pass has sifted this row; NULL = not yet processed).
Optional named AI personas with scoped memory and trust-gated autonomy — full
design in docs/2026-07-23-ai-coworkers-design.md, and see NEW-COWORKER.md
for how to construct a coherent personality. Three tables, no changes to
semantic_memory/episodic_memory: coworkers (name/expertise/personality/
trust_level/active), memory_coworkers (many-to-many — untagged = global,
tagged = visible only when that coworker is loaded), appraisals (one current
row per coworker, history via superseded_by).
scripts/coworkers.py— writes only:--add,--appraise,--set-trust,--retire/--reactivate. Reads (list coworkers, load a coworker's state) are ad-hoc SQL via the Turso MCP — no embedding involved.remember.py --coworker name[,name...]— tag a memory to one or more coworkers instead of leaving it global; dedup scopes to that coworker's visible set.recall.py --coworker name— scope a semantic search to memories visible to that coworker (untagged ∪ tagged-to-them).trust_level(supervised|trusted|autonomous) never loosens a global baseline rule — it's a hard floor regardless of which coworker is active.
A user-triggered ("sleep" / "go to sleep") consolidation pass — never
scheduled automatically. Full procedure in instructions/SLEEP.md; no new
memory tables, just three additions.
CLAUDE.md.template deliberately carries only a one-line pointer to that
file, not a summary of the procedure — sleep happens rarely, so its steps are
read on demand and cost nothing in the sessions that never sleep. The pointer is
an absolute path baked in at install time ({{BASE_PATH}}), so it resolves
without the agent hunting for the repo. Same reasoning for backups (below).
Keep the policy in the template (user-triggered only) and the procedure here.
episodic_memory.processed_at— sleep sifts unprocessed rows, keeping only ones where something was actually learned (discarding bare event sequences like "x happened, then y happened"), promotes the kept ones intosemantic_memory(viaremember.py, same as any other store/supersede), then stampsprocessed_aton every row it looked at — kept or discarded — so nothing gets re-sifted forever.semantic_memory.retired_at— sleep also sweeps current semantic memory for near-duplicates to consolidate (remember.py --supersedes) and obsolete facts to retire. Retirement is soft-delete only (scripts/sleep.py --retire, setsretired_at) — same never-hard-delete philosophy as coworkers'active=0. "Obsolete" has no fixed rule; ask the user when it's not clear-cut.topic_keywords(new table, see schema below) — a curated topic → keywords index, rebuilt wholesale each sleep (scripts/sleep.py --rebuild-topics: full DELETE + re-INSERT, never accumulated). Loaded in full at every session start (recall.py --topics, same slot as--baseline) so a session knows what topics have memory without having to guess a search term first. This puts pressure back on sleep's consolidation step to keep the topic count bounded —recall.py --topicswarns past 50 topics as a nudge to merge harder next sleep, since that many would cost real context budget every session.
Both sleep runbooks push all bulk reading into subagents. The orchestrating
agent queries skinny metadata (ids, topics, dates) and never holds memory_text
in bulk — that cost scales with the corpus, which is precisely what a memory
system is supposed to grow. Workers get self-contained prompts (their ids, the
read command, the judgment rule, the exact remember.py call) and report one line
per row.
Who may write is split on risk: episodic-sift workers write via remember.py
themselves (high volume, low stakes), while compaction and pattern workers only
propose and the user approves in one batch. --mark-processed stays with the
orchestrator and covers only ids a worker actually reported back — a worker that
dies leaves its rows unprocessed, which is the recoverable state.
A second, deeper pass — instructions/DEEP-SLEEP.md, reached by saying "deep
sleep" or by answering yes to the offer at the end of a normal sleep. Also
user-triggered only. It does the three things a normal pass deliberately doesn't:
| Phase | What it does |
|---|---|
| D0 | Normal sleep (prerequisite) + recall.py --status; stops on DEGRADED — nothing can be embedded with Ollama down. |
| D1 | Backup. Mandatory: D2 is the only operation in this system that destroys a memory. |
| D2 | Purge gate. Lists every superseded/retired semantic row and hard-deletes the ones the user selects. No default policy — asked every run. |
| D3 | Compaction. Clusters all current semantic memory and merges same-topic near-duplicates. |
| D4 | Pattern mining. Derives recurrences and trends from the whole episodic log. |
| D5 | Rebuild topic_keywords, report. |
| D6 | Recall check. Validate eval_cases, report recall@1/@5/MRR vs the last eval_runs row, propose up to 3 new cases, and — only on a flagged regression — sweep RECALL_ALPHA and ask whether to change it. |
| D6.5 | Memory-quality metrics. Age profile of current rows plus an adjudicated contradiction rate — the two numbers ranking metrics cannot express. Reported, never persisted. |
| D7 | Verify. Lists current rows quoting a checkable artifact and has workers check each one against the repo as it stands. Staleness is invisible to retrieval metrics. |
D6 runs after D2 and D3 for a reason: those two are what break an eval set. A purged
row is gone and a merged row is superseded, so a case pointing at either looks exactly
like a ranking regression while being nothing of the sort — validation has to happen
after them, not before. D7 is the one phase that follows D6, and only because it
must: it is the sole remaining phase that can retire a row, so putting it earlier
would reintroduce the very problem D6's placement solves. The price is that a
retirement in D7 has to be followed by re-running eval-harness.py --validate by
hand.
Two things a consolidation pass structurally cannot do are split out into their own
phases. D6.5 reports the memory-quality metrics that ranking cannot express: the
age profile of current rows (sleep.py --staleness) and a contradiction rate
adjudicated by workers over a mechanically-built shortlist
(sleep.py --contradiction-candidates, which reuses the --cluster grouping at a
tighter 0.15 threshold — just above remember.py's own 0.10 dedup cutoff, because
two rows that disagree about the same subject are near-identical in vector space
without being duplicates). D7 is the Verify stage: staleness of a stored fact is
invisible to retrieval metrics, since a row can rank first and still name a renamed
flag, so sleep.py --verify-candidates lists current rows quoting a checkable
artifact — a script path, a CLI flag, an env var, a version — oldest first with
artifact-class count as tiebreak, and workers check each artifact against the repo as
it stands. Both primitives are read-only and decide nothing; retiring or superseding
stays a user call, exactly as in D2 and D3. The patterns are deliberately narrow:
measured on the live corpus, loose first drafts flagged 502 of 561 rows (89%), which
makes the tiebreak meaningless — tightened, 236 of 561 (42%).
Ordering candidates by how often a row is actually recalled would be the better
signal — a frequently-retrieved stale row is the worst case — but the only way to
know that is to increment a counter on every search, which makes recall.py a writer
on the read path. Keeping recall a pure reader won that trade; age plus artifact
density is the accepted proxy.
The eval set is runtime state, not repo content — its expect values are live row
ids — so it lives in the DB, in eval_cases (authored cases, soft-deleted via
retired_at) and eval_runs (one row per run). That puts it inside the .sql dump,
which is deliberate: cases are authored and cannot be regenerated from the corpus, and
a past corpus cannot be re-measured. The only file left on disk is the query-embedding
cache <db parent>/eval/qvec.json (SUPERCHARGED_MEMORY_EVAL_DIR overrides) — pure
derived data, excluded on purpose. There is no auto-generated eval set: an LLM writing a
query while looking at the row it should retrieve produces lexical overlap a real user
never produces, which would bias every future alpha upward. Cases are drafted from a
row's topic and keywords only, never its text, and the user approves each one.
RECALL_ALPHA is never changed unattended. The harness reports the recall@5 plateau
and whether the configured value still sits inside it; if it doesn't, the agent asks.
Per-machine calibration belongs in env in ~/.claude/settings.json, not in the
repo default.
Purge (sleep.py --purge <ids> --confirm-purge) is the sole exception to the
never-hard-delete rule, and it is fenced: it refuses a current row, an unknown id,
a missing --confirm-purge, any id whose deletion would strand a surviving row
that points at it, and any run where the newest backup is older than the DB file.
It also clears the rows' memory_coworkers entries — that table has no FK on
memory_id, so an orphan would silently re-scope a future memory that reuses the
id. Episodic memory is never purged. Pass all ids in one call: a purge is
itself a write, so a second call in the same pass fails the backup gate.
Clustering (sleep.py --cluster [--table semantic|episodic] [--threshold N])
is mechanical — no LLM, no new embeddings. It runs pairwise
vector_distance_cos over the vectors already stored and emits connected
components as JSON, plus any rows with no embedding, listed separately rather than
silently skipped. Semantic defaults to 0.22, episodic to 0.25 — both measured
against a real ~230-row corpus, not guessed. The useful range is much tighter than
intuition suggests, because connected components chain: if a–b are close and
b–c are close, a and c land in the same cluster even when they have nothing to do
with each other. On this corpus 0.22 yields ~11 pairs and triples, 0.30
already produces a 33-row blob, and 0.35 collapses 154 of ~200 rows into one
"cluster". The script therefore refuses to present that as a finding: when the
largest cluster exceeds 12 rows and a quarter of all clustered rows, the JSON
carries a warning telling you to lower the threshold. Merges then apply via
remember.py --supersedes <id1,id2,id3> — one insert, all listed rows pointed at
it, one transaction.
Pattern mining is two-stage, because there are two kinds of pattern and no
single worker can see both. A map stage partitioned by episodic embedding
cluster — not by exact topic, since episodic topics are near-unique per ticket
and exact grouping would yield all singletons — catches within-topic recurrence
("that service restarted four times"). Then one reduce worker reads only the tiny
per-row digests and catches cross-topic shapes ("a whole class of bug keeps coming
back") that a topic-scoped worker structurally cannot see. A pattern needs ≥3
supporting events, a trend claim also ≥2 distinct calendar months, and its
evidence_ids go into the memory text so a later session can verify the claim
instead of trusting it.
topic (primary key), keywords, updated_at. Deliberately unlinked to
semantic_memory/episodic_memory — no FK, no embedding, substring-searched
via plain LIKE.
tursodb takes an exclusive file lock — a second process can't even read —
unless every opener passes --experimental-multiprocess-wal. With the flag
on all openers (MCP, scripts, backup): multiple agent instances share the DB,
reads run concurrently, and writes serialize (a clash returns database is busy;
scripts retry with backoff). A process without the flag is refused. The flag is
experimental — that's the trade for concurrency.
On Windows the flag needs --vfs experimental_win_iocp alongside it, or the open
fails with experimental multiprocess WAL is not supported by the active IO backend. The scripts add it themselves (TURSO_VFS, see the table above); every
tursodb command you type by hand needs both flags. Dropping the WAL flag is not
an alternative — that is what re-introduces the exclusive lock.
How exec_sql decides what failed and what to retry. A failure is a non-zero
exit, an error on stderr, or a stdout that carries a tursodb diagnostic and
nothing else — SQL-level errors go to stdout with an empty stderr, so stdout has
to count, but only structurally: this corpus stores tursodb's own error messages
as memories, so a successful SELECT memory_text can print lines byte-identical
to a diagnostic, and what tells them apart is that a real failure prints no rows
around them (a successful write prints nothing; a successful read always prints at
least one non-diagnostic line). Only a failure then retries, and only when the
phrase database is busy|locked appears anywhere in stderr or stdout.
That search is unanchored on purpose — deliberately not restricted to
diagnostic-shaped lines ( × …, x … under miette's ASCII theme on a legacy
Windows codepage, a wrapped │ … continuation, a line-initial Error:),
because contention is the one error that carries no prefix at all: a contended
write prints exactly database is busy, so the prefix gate meant the backoff
never fired. Reading all of stdout is safe because it is read only once the run
has already failed — nothing landed, so a retry cannot duplicate a write, and a
row body that merely quotes the phrase costs a few seconds of pointless backoff
before the same exception.
-
Daily backup — schedule
scripts/supercharged-memory-backup.sh(e.g. a launchd/cron job) to produce a validated gzipped SQL dumpYYYY-MM-DD-supercharged-memory.sql.gzinBackups/(concurrent-safe reader; checks non-empty + has INSERTs + gzip intact). Retains 3 daily + 4 weekly (Monday) copies, and runs even while a session is open. -
Manual backup:
bash scripts/supercharged-memory-backup.sh— this is also what the agent runs when the user asks for a backup (CLAUDE.md.templatepoints at the script by absolute path). Dumps always land in this repo'sBackups/, theBACKUP_DIRdefault;recall.py --candidateslooks there for restorable dumps, so keep them together rather than scattering them per-machine. Taking a backup is additive and non-destructive; restoring is neither — never restore without the user's explicit ok. -
Restore —
scripts/restore.py, into a fresh DB file:python3 scripts/restore.py --out /path/to/new.db # newest backup by default python3 scripts/restore.py --dump Backups/<file>.sql.gz --out /path/to/new.db
It refuses a target that already exists (pass
--forceto override), reads.sqland.sql.gzalike, and prints a per-tablein dumpvsrestoredcomparison, exiting non-zero if any row is missing. A restore that was not counted is not a restore.Do not pipe a dump into
tursodb, and do not use its.read. The CLI splits its input on line boundaries, butmemory_textcontains newlines, so most of a dump's lines are continuations inside string literals — in one measured dump, 4378 of 5034. The first multi-lineINSERTends its statement mid-value and everything after it is parsed as garbage until something happens to parse again. The result is an arbitrary fraction of the rows (98 of 389 in the observed case) together with a misleadingParse error: table 'eval_cases' does not exist— a partial restore that looks like it mostly worked. The dumps themselves are fine; only the replay was broken. -
Rebuild empty schema — pipe the file; don't pass it as a SQL argument (
tursodb "$(cat schema.sql)"fails: the leading--comment parses as a CLI flag):tursodb "$SUPERCHARGED_MEMORY_TURSO_PATH" --experimental-multiprocess-wal < schema.sql # +`--vfs experimental_win_iocp` on Windows python3 scripts/seed.py # empty by default — add entries first if you want a seeded start
-
New machine: run
instructions/SETUP.md(or manually: installtursodb+ Ollama,ollama pull bge-m3, choose/exportSUPERCHARGED_MEMORY_TURSO_PATH, restore the latest dump, register the MCP with the flag, and re-runinstall-claude-md.shwith yourSUPERCHARGED_MEMORY_TURSO_PATHandEPISODIC_MODE).
Never store PII — anywhere. Anonymize before calling
remember.py/backfill.py. Store pointers (ticket/case ids, role labels)
instead of personal data. This applies to the memory text and anything sent to
Ollama to embed.