A boundary the caller cannot drop

memtomem

A markdown-first long-term memory for coding agents whose scope rule is a SQL fragment that is never empty, so no read surface can lose it by treating an empty filter as no filter — with a registry test that fails when a new surface takes a scope without calling the gate, and an id-restricted recall that still refuses another project's rows.

Carries 3 of 7 rubric mechanisms. Most systems here carry none or one (41%), and a dash means the mechanism was not found at this commit — not that the system needed it. Each mark is one LLM reviewer's reading of the code at this commit rather than a run of it — known limits.

  • Tombstone
  • Trust state
  • Bi-temporal
  • Scope enforced
  • Mutation audit
  • Human review
  • Negative evals

1. Executive Summary

memtomem is long-term memory for coding agents built on markdown files the user keeps: the files are the authority, SQLite is an index over them, and the README's promise is that "your files stay yours, and core usage is hook-free by default." The core package is version 0.6.2 under Apache-2.0 — the workspace carries 1,996 commits since 28 March 2026, with 157,951 lines of Python in the core against 297,851 lines across 520 test files — and the README labels the 0.x line alpha.

The thing worth the visit is how it enforces scope.

Memory lives in three tiers — user, project_shared, project_local — and the enforcement is a SQL fragment, scope_context_sql, composed into every chunk query. The design decision that makes it hold is stated in the docstring: the fragment "is non-empty in every case — never returns ("", []) — so callers cannot accidentally drop the context rule by treating an empty fragment as 'no filter'." With no project context it pins scope = 'user' and excludes every project tier, with the reasoning written down — the caller did not pin to a project, so the store cannot tell whose project_shared chunks would be safe to show. A caller's own ScopeFilter is layered on as intent and can only narrow, or explicitly opt into a cross-project read.

Three things then stop that rule eroding.

neighbor_visible re-checks scope, system namespaces and temporal validity on a chunk reached by adjacency rather than by the query — the seam where a graph-expanding retriever usually leaks — and its docstring is careful about the asymmetry: an explicit filter widens neighbour visibility and never narrows it, "[t]hat asymmetry is what makes this a visibility rule rather than a second copy of the selection query."

test_scope_vocabulary_architectural_guard.py keeps a registry of every function that accepts a scope, asserts that each declared surface actually calls the vocabulary gate, and — the part that matters — test_no_unclassified_scope_sinks fails when a new scope sink appears in neither the validated nor the permissive set. A future read surface cannot quietly skip the gate; the test breaks first.

And test_recall_by_chunk_ids_still_honors_the_project_boundary pins the rule that most id-addressed fetches get wrong. A caller holding all three chunk ids gets back exactly the two in scope, because the id-restricted path routes through recall_chunks rather than a raw batch getter. Its docstring says it plainly: "'I know its id' is not authorization."

Around that sit two guards worth naming. Consolidation refuses to write a summary onto a virtual path occupied by chunks it does not own — origin != ORIGIN_CONSOLIDATION_POLICY means "a real file or another writer's chunks appear to occupy it" — and refuses outright when the path holds too many chunks to establish ownership. And the write path runs content through a credential-redaction guard whose 19 secret-class patterns are documented as byte-identical to a named commit of a sibling project, with an asymmetric sync rule for which side may lead.

The limits are stated by the code rather than discovered against it. The consent that clears a write into a git-tracked directory is a boolean parameter, and emit_project_shared_confirmation says what its audit line means: "a human (or an agent acting for one) authorised a git-tracked write" — so this is a recorded consent, not human review, and no mark follows from it. The self-export HMAC marker "proves local provenance … NOT that every chunk passed redaction", and the docstring lists exactly which rows can slip through: legacy pre-guard rows, prior force_unsafe writes, and folder-indexed content from before that path had a gate.

Three marks: scope_enforced, bitemporal, negative_eval.

2. Mental Model

A chunk is a slice of a markdown file with its heading hierarchy, a namespace, tags, a scope tier, a project root, and optionally a validity window declared in the file's frontmatter.

A scope is user, project_shared or project_local. The first is shared across projects; the other two belong to a project identified by an absolute project_root, so one user-local database can hold several worktrees of the same project without colliding.

A context boundary is the always-on rule: out-of-project reads see user only; in-project reads see user plus that project's tiers. It is not a default the caller overrides — it is the floor the caller's filter narrows.

An as-of instant gates valid_from_unix and valid_to_unix, which come from the file, not from the indexer.

Diagram — the scope fragment is never empty and the adjacency path re-checks it; an id-restricted recall routes through the same rule, so naming an id is not authorization
Diagram source
%% caption: the scope fragment is never empty and the adjacency path re-checks it; an id-restricted recall routes through the same rule, so naming an id is not authorization
flowchart TB
    MD[("markdown files<br/>user tier + project tiers<br/>the authority")] --> IDX["indexing"]
    ADD["mm add / mem_add"] --> GATEB{"scope = project_shared?"}
    GATEB -->|"yes"| CONF{"confirm_project_shared"}
    CONF -->|"absent"| REFUSE["refused — git-tracked dir"]
    CONF -->|"present"| REC["emit_project_shared_confirmation<br/>records the consent, not the write"]
    REC --> GATEA
    GATEB -->|"no"| GATEA{"enforce_write_guard<br/>19 secret-class patterns<br/>whole content, any offset"}
    GATEA -->|"blocked"| STOP["write rejected"]
    GATEA -->|"clear"| MD
    IDX --> CH[("chunks in SQLite<br/>namespace, scope, project_root,<br/>valid_from_unix, valid_to_unix,<br/>origin, redaction_count")]
    Q["search / recall / ask / timeline"] --> VOCAB["validate_scope_vocabulary<br/>every user-facing surface"]
    VOCAB --> FRAG["scope_context_sql<br/>NEVER returns an empty fragment"]
    FRAG -->|"no project context"| USERONLY["scope = 'user'"]
    FRAG -->|"in project"| UNION["scope = 'user' OR project_root = ?"]
    FRAG --> SEL["selection query"]
    CH --> SEL
    SEL --> ADJ["adjacency expansion"]
    ADJ --> NV{"neighbor_visible<br/>system prefix? scope? valid at as_of?"}
    NV -->|"fails any"| HIDE["not shown"]
    NV -->|"passes"| OUT["returned"]
    BYID["recall by chunk_ids"] --> FRAG
    CONS["mem_consolidate_apply"] --> OWN{"every chunk at the summary path<br/>origin == CONSOLIDATION_POLICY?"}
    OWN -->|"no"| REFUSE2["refuses: another writer occupies it"]
    OWN -->|"yes"| WRITE["replace the summary"]

3. Architecture

Area Role
storage/sqlite_scope.py scope_context_sql — the always-on boundary
search/visibility.py neighbor_visible, chunk_valid_at, the adjacency fence
models.py ChunkMetadata, NamespaceFilter, ScopeFilter and their parsers
memory_scope.py The scope-tier to canonical-directory resolver shared by CLI and server
privacy.py The redaction guard, its pattern provenance and the consent audit
provenance.py The HMAC self-export marker and its threat model
tools/consolidation_engine.py Summary-path ownership and the refusal to consolidate over foreign chunks
server/tools/ The MCP surface: mem_add, mem_search, mem_recall, mem_consolidate*
quality/ Experiment, replay, gate, fingerprints — a retrieval-quality harness
packages/memtomem-* Claude Code plugins, Kimi skills, an OpenCode integration

4. Essential Implementation Paths

  • storage/sqlite_scope.py:29-69 — the fragment, and why it is never empty.
  • search/visibility.py:50-92 — validity and the adjacency rule.
  • models.py:281-340ScopeFilter, and where vocabulary is enforced.
  • server/tools/memory_crud.py:700-760 — Gate B then Gate A, in that order.
  • tools/consolidation_engine.py:385-408 — the ownership refusal.
  • provenance.py:1-40 — what the marker proves and what it does not.

5. Memory Data Model

A chunk carries retrieval structure (headings, overlap, parent and file context), classification (namespace, tags, chunk type), scope (tier plus project root), temporal validity, and provenance (origin, source_span_hash, redaction_count, source_read_only).

There is no status field. Correction happens in the file: edit it, and the index follows. That is coherent for a markdown-first design and it means the store has no notion of a claim being superseded — the diff in the user's repository is the record.

The origin field is the exception worth noting, and the comment explains the choice: ownership over the virtual summary path is decided on origin "rather than on a namespace/tag combination a user chunk can reproduce." The key is one a user write cannot forge.

6. Retrieval Mechanics

Hybrid BM25 and vector search over the indexed chunks, with the scope fragment and namespace filter composed in, an optional as-of instant, and an adjacency expansion guarded separately. validate_scope_vocabulary runs on every user-facing read — HTTP, MCP and CLI alike — so a misspelled tier gives the same answer everywhere, with run_search repeating it as a backstop for in-process callers.

7. Write Mechanics

Two gates in a fixed order. Gate B requires confirm_project_shared=True before a write into a git-tracked directory and records that consent with the surface that obtained it. Gate A is the content chokepoint: the redaction scan covers the whole content regardless of length, "a secret pasted past any byte offset still trips it", and it hard-refuses force_unsafe=True when the scope is project_shared.

8. Agent Integration

An MCP server with the memory CRUD, search, consolidation and timeline tools; a CLI; an HTTP API with a dashboard; and packaged plugins for Claude Code, Kimi and OpenCode. Core usage is hook-free by default, which is the README's answer to memory layers that require a harness change.

9. Reliability, Safety, and Trust

The pattern across this codebase is that each guarantee is accompanied by its own limits, in the file that implements it. That is worth saying plainly because it is rare: the redaction module records the exact sibling commit its patterns match and which side may lead; the provenance module states that a valid marker proves the bundle is a self-export and not that its contents passed redaction, and enumerates the three categories of row that can slip through; the consent emitter says its line means a human or an agent acting for one authorised the write.

Two consequences follow for this report's marks. human_review is withheld: the confirmation is a parameter the calling agent can set, and the code says so. audit_log is withheld: the audit lines cover the consent and bypass gates, not every mutation, and the memory itself is a file whose history lives in the user's version control rather than in the store.

The remaining risk is inherent to the design rather than a slip. With no status on a chunk, a stale memory is corrected by changing the file. That is the right answer for a markdown-first system and it means everything this atlas asks about supersession, tombstones and belief is answered by git rather than by memtomem.

10. Tests, Evals, and Benchmarks

520 test files, nearly twice the source in lines, plus a quality/ package with experiments, replay, fingerprints and gates for retrieval quality.

Two test shapes are worth copying. The architectural guard converts "every read surface must call the gate" from a convention into a failing test when a new surface appears. And test_search_scope_filter.py pairs SQL-fragment unit cases with end-to-end recalls that assert exact content sets, including the id-addressed case.

11. For Your Own Build

Steal

  • Make the scope fragment impossible to drop. Returning a non-empty SQL fragment in every case, and saying in the docstring that this is why, removes the failure mode where a caller treats "no filter" as "no restriction".
  • Register your scope sinks and test the registry. A test that fails when an unclassified sink appears is worth more than any number of per-surface tests, because it covers the surface nobody has written yet.
  • Re-check visibility on the adjacency path, and let an explicit filter only widen it. The distinction between a visibility rule and a second selection query is the one most graph retrievers blur.
  • Decide ownership on a field a user write cannot reproduce. origin set only by the consolidation policy beats any namespace or tag convention.
  • Document what each guarantee does not prove. The provenance module's "what the marker proves — and does not" section is the model.

Avoid

  • Calling an agent-settable boolean a human confirmation. memtomem does not — but the pattern invites it, and the audit line only means as much as the hand that set the flag.

Fit

Reach for this if you want memory that stays in your own markdown with real per-project separation and a write path that refuses secrets. Look elsewhere if you need the store itself to model belief, supersession or forgetting rather than delegating that to your repository's history.

12. Open Questions

  • Is there a path to a consent signal that an agent cannot set for itself — a terminal prompt, or a host-side approval — for project_shared writes?
  • The self-export exemption skips the redaction re-scan on bundles that may contain pre-guard rows. Is a per-chunk redaction provenance still deferred?
  • With validity windows in frontmatter, is a knowledge-time axis — when the index learned a fact — planned, or is file history considered sufficient?

Appendix: File Index

Path What to read it for
storage/sqlite_scope.py The always-on boundary and its three cases
search/visibility.py The adjacency fence and validity check
models.py Chunk metadata and the two filters
privacy.py The redaction guard and its cross-project sync rule
provenance.py The HMAC marker and an honest threat model
tools/consolidation_engine.py Summary-path ownership
tests/test_scope_vocabulary_architectural_guard.py The registry test
tests/test_search_scope_filter.py The boundary, including the id case

History

2026-09-1695baf628… — first reading, at a commit dated 16 September 2026. Screened before opening, from a shallow clone: seventeen files, one auto-run surface (a .claude-plugin/ directory), two build-time execution points, two unpinned surfaces, seven dependency files inside the cooldown, and CLAUDE.md read as data. Nothing was installed, built or run.