Governance as the product

SuperLocalMemory

A hash-chained audit log on its own database connection, so the record survives corruption of the store it audits — and an EU AI Act module that refuses to certify compliance.

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

SuperLocalMemory positions itself as "the governed memory layer for AI agents: local-first, auditable, and built for the compliance obligations teams now actually carry". AGPL-3.0, roughly 399,000 lines of Python including tests, over SQLite, with twenty-eight top-level subpackages of which compliance/, trust/, access/ and attribution/ are the distinguishing ones.

This is the governance corner of the atlas, and two design decisions in it are worth taking elsewhere.

The audit chain runs on its own database connection. compliance/audit.py implements a SHA-256 hash chain from a genesis hash with verify_integrity(), and the module header states the architectural reason: "The audit chain uses its OWN sqlite3 connection (not shared DB manager) for independence — audit must survive even if the main DB is corrupted."

Several systems in this atlas hash-chain an audit log. This is the only one that asks what happens to the evidence when the thing it is evidence about fails, and answers by decoupling them. An audit log sharing a connection, a transaction and a file with the store it audits is a record that dies with its subject.

The EU AI Act module refuses to certify. compliance/eu_ai_act.py is "technical deployment-posture reporting", and its docstring draws the line itself:

An operating mode can establish technical facts such as locality and use of generative AI. It cannot determine the Act's risk classification or certify legal compliance without the system's intended purpose and deployment context.

A compliance feature whose first act is to say what it cannot conclude is rarer than it should be, and it is the same distinction this atlas draws when it declines to issue a conformance statement of its own.

The temporal axis carries two clocks and both are queryable, independently: known_as_of asks what this instance had learned by a timestamp, valid_at asks what was true at one, and the docstring states the separation outright — "The axes are independent and may be supplied separately or together."

2. Mental Model

A memory is decomposed into atomic facts, each typed episodic, semantic, opinion or temporal under a CHECK constraint, each carrying a profile_id, a scope (defaulting to personal), an optional shared_with, confidence, importance, evidence count, emotional valence and arousal, and a lifecycle zone.

Governance wraps the store rather than living inside the fact. A write passes through a trust gate before it lands; access passes through ABAC; expiry is decided by named retention rules bound to a profile; and every operation lands in the audit chain.

Diagram — a trust gate precedes attribute-based access control on writes and deletes while reads always pass it, and every operation appends to a hash chain on its own connection
Diagram source
%% caption: a trust gate precedes attribute-based access control on writes and deletes while reads always pass it, and every operation appends to a hash chain on its own connection
flowchart TD
    W["write / delete"] --> TG{"trust gate:<br/>score above threshold?"}
    TG -->|no| DENY["refused"]
    TG -->|yes| AB{"ABAC: agent, profile, action"}
    R["read"] -->|"always passes"| AB
    AB -->|deny policy matches| DENY
    AB --> OP["operation executes"]
    OP --> AC["audit_chain: sha256 over prev_hash + entry<br/>separate sqlite connection"]
    OP --> LC["lifecycle: active → warm → cold → archived"]
    RR["named retention rule, per profile"] --> ACT{"action"}
    ACT -->|archive| ARCH["lifecycle = archived"]
    ACT -->|tombstone| TS["lifecycle = archived<br/>archive_status = tombstoned, purgeable"]
    ACT -->|notify| N["count surfaced only — an operator acts"]

The three retention actions are the model in miniature: two mutate and one deliberately does not.

3. Architecture

One SQLite database for memory, a second for the audit chain, and twenty-eight subpackages: compliance, trust, access, attribution, retrieval, vector, graph, code_graph, learning, evolution, dynamics, math, mesh, optimize, evaluation, parameterization, ingestion, encoding, loops, hooks, mcp, server, cli, ui, storage, infra, migrations, llm.

Nine framework integrations ship as separate packages under ide/integrations/ — LangChain, LangGraph, LlamaIndex, CrewAI, AutoGen, Semantic Kernel, Google ADK, OpenAI Agents, Agent Framework — each with its own pyproject.toml and test suite.

The operator cost is a Python install and a directory; the conceptual cost is the surface area, which is the largest in this batch relative to what a single user needs.

4. Essential Implementation Paths

Writetrust/gate.py (minimum trust score for write and delete; "Read operations always pass but are logged for audit purposes") → compliance/abac.py → storage → compliance/audit.py.

Retentioncompliance/retention.py: rules in their own table bound to a profile_id with days, a framework tag (GDPR, HIPAA, custom), an action and an applies_to JSON selector; enforce moves expired facts.

Retrievalretrieval/engine.py, entity_channel.py, vector_store.py, each carrying WHERE profile_id = ?, with an explicit query over edge_type IN ('contradiction', 'supersedes').

Trusttrust/scorer.py, signals.py, provenance.py.

5. Memory Data Model

atomic_facts is thirty-plus columns. The governance-bearing ones: profile_id NOT NULL DEFAULT 'default' with a foreign key and a cascade, scope, shared_with, fact_type under a four-value CHECK, lifecycle under a four-value CHECK, archive_status, and the four date fields.

The retention module's handling of the lifecycle constraint is worth quoting because it is a workaround with its reasoning attached: atomic_facts.lifecycle "CHECK allows only active/warm/cold/archived, so both retention actions land in the 'archived' lifecycle zone. 'tombstone' is additionally flagged purgeable via archive_status so a purge job can find it — this avoids a risky rebuild of the (large) atomic_facts CHECK constraint."

That is the right way to record a compromise: what was wanted, what the schema allowed, what was done instead, and why the clean fix was rejected. A reader grepping for a tombstoned lifecycle value would otherwise conclude the feature was missing.

Beside the facts sit Fisher-information fields (fisher_mean, fisher_variance, fisher_last_applied_access) driving a delta-based maintenance update, and a langevin_position — the dynamics machinery is substantial and is not what this report is about.

6. Retrieval Mechanics

Multi-channel retrieval — an entity channel, a vector channel and community summaries — with a fact_importance table and typed graph edges including contradiction and supersedes, queried explicitly so a contradicted fact can be surfaced as such rather than silently outranked.

Scope is real. profile_id is NOT NULL with a foreign key, appears in every index including the composite (profile_id, lifecycle) and (profile_id, fact_type), and appears as WHERE profile_id = ? in the retrieval engine, the entity channel and the vector store. The indexes matching the query shape is the sign that the scoping is load-bearing rather than decorative.

ABAC sits above it as a second layer, and its default is worth stating plainly: "Default: allow all (open access). Policies restrict specific agents." It is a deny-list, so an unconfigured deployment has no access control — which is a reasonable default for a local single-user tool and the wrong one for the compliance posture the README describes. Anyone deploying this for a team should treat writing policies as step one.

The read path filters on both clocks, and the design decisions around it are the interesting part. get_strict_temporal_inadmissible_fact_ids takes known_as_of and valid_at, and a companion filter excludes any candidate whose event-time window fails to contain the reference instant — expired when valid_until <= ref, not-yet-valid when valid_from > as_of, with a comment naming the half-open interval [valid_from, valid_until) and the boundary bug it fixes. Four properties are worth copying together:

  • Bounded, never a scan. Both filters operate only on the already-retrieved candidate pool, keyed on the fact_id primary key and chunked at 900 to stay under SQLite's bound-parameter limit.
  • Zero-regression by default. A fact with valid_until = NULL is open-ended and never returned; a fact with no temporal row at all is never returned. With as_of=None the filter returns the empty set, so adding it demoted nothing.
  • Legacy is explicit, not silent. A fact predating the write invariant is legacy_unknown, and strict time-travel excludes it unless the caller asks for unknown history — the safe direction, and named.
  • Fail-open. A database error logs a warning and returns an empty set, so retrieval cannot break because a validity lookup did.

The filter is wired through engine.py:1108 into recall_pipeline.py:1197 and the recall worker, so the parameter reaches the query from the public surface. bitemporal is earned.

7. Write Mechanics

The trust gate is the write mechanic and its asymmetry is deliberate: writes and deletes require a minimum trust score, reads always pass "but are logged for audit purposes". Gating mutation and auditing access is the correct division — the damage from a bad write is durable, the damage from a bad read is disclosure, and disclosure is what an audit trail addresses.

Retention is per-profile and per-framework, which is the shape a compliance obligation actually has: a GDPR rule and a HIPAA rule can coexist on one store with different periods and different applies_to selectors.

There is no rejected-value record. A fact archived by retention or superseded by an edge can be written again; the audit chain will record both, and nothing refuses the second.

8. Agent Integration

An MCP server, a CLI, a browser dashboard with lifecycle and compliance views, and nine framework integration packages. The dashboard surfaces the lifecycle distribution — active, warm, cold, archived, tombstoned — and the compliance view lets an operator choose the action on a retention rule.

9. Reliability, Safety, and Trust

Audit log — awarded, in the strongest form found in this atlas. A hash chain from a named genesis, verify_integrity() to walk it, thread-safe, and — the part that distinguishes it — an independent connection so the audit survives corruption of the store. Every operation including reads is logged.

Scope — awarded, per section 6.

Trust state — withheld, and the distinction matters. trust/ scores operations and agents, not memories. The gate answers "may this actor write", not "is this fact believed". A fact carries confidence (a float) and evidence_count (an integer), neither of which is a discrete epistemic status. Naming a subpackage trust and having no trust state on the record is exactly the confusion the mark exists to separate.

Human review — withheld. The notify retention action "changes nothing — it only surfaces the count so an operator can act", which is a display, and the compliance dashboard configures rules rather than adjudicating facts.

Bitemporal — awarded, per section 6.

Tombstone — no, and the near-miss is now a real one. Two mechanisms wear the name. archive_status = 'tombstoned' marks a row purgeable and is consulted by a purge job, which is housekeeping. The second is closer: projection_tombstones is read on the store path by _fact_is_tombstoned (core/store_pipeline.py:427-437), so an erased fact genuinely is checked for before it is re-projected. What it is keyed on is fact_id, and a fact id is a UUID — uuid.uuid4().hex[:16] in the consolidator, v4 elsewhere. Re-asserting the same sentence mints a new id and passes the check. The mark wants the key to be the value; here it is the row, which makes this a durable record of which row was erased rather than of what was refused.

Negative eval — no, among the test suites found.

The most transferable safety judgement here is the EU AI Act docstring. A module that reports deployment posture and states in its own header that it cannot classify risk or certify compliance is doing the thing every compliance feature should do and most do not: bounding its own claim.

10. Tests, Evals, and Benchmarks

No paper. Substantial test infrastructure — tests/ with subdirectories for adapters, API, binary, code graph, learning, optimize, process health and release, plus a test suite inside each of the nine integration packages.

I ran nothing. The screen flagged fifteen dependency manifests inside the seven-day cooldown and build-time execution in eighteen conftest.py files and two Makefile/package.json targets — the widest execution surface in this batch.

There is an evaluation/ subpackage in the source tree. No committed benchmark result, dataset or scorecard was found, and no retrieval-quality number appears in the README. For a project whose pitch is governance rather than recall quality, the absence is coherent — the claims made are about auditability, and auditability is checkable by reading audit.py rather than by running a benchmark.

11. For Your Own Build

Steal

  • Give the audit log its own connection. "Audit must survive even if the main DB is corrupted" is a one-line argument that changes the design, and the cost is one file handle.
  • Chain from a named genesis and ship the verifier. _GENESIS_HASH = "genesis" plus verify_integrity() means the chain can be checked by anyone, not just trusted.
  • Log reads, gate writes. Reads always pass the trust gate and are recorded; writes and deletes must clear a threshold. The two failure modes are different and deserve different treatment.
  • Bind retention rules to a profile and tag them with a framework. A GDPR rule and a HIPAA rule on one store, with different periods and different applies_to selectors, is what a real obligation looks like.
  • Give retention a notify action. An action that changes nothing and surfaces the count lets an operator dry-run a policy before arming it.
  • Record the workaround and why the clean fix was rejected. The archive_status note explains that rebuilding a large CHECK constraint was judged riskier — a reader grepping for the missing lifecycle value would otherwise report a gap.
  • Make your compliance module say what it cannot conclude. The EU AI Act docstring is the model: establish technical facts, refuse the legal classification.

Avoid

  • Do not default an access-control layer to allow-all in a system sold on governance. ABAC's deny-list default is right for a local tool and wrong for the deployment the README describes, and nothing in the code forces the distinction.
  • Do not name a subsystem trust when it scores actors rather than claims. A reader will assume the memories carry a trust state; they carry a float.
  • Do not store four date columns and query none of them. The temporal model costs write bandwidth and buys nothing at read time.
  • Do not let the surface outgrow the story. Twenty-eight subpackages including Langevin dynamics and Fisher information, around a memory whose distinguishing claim is auditability.

Fit

This is for a team that has a compliance obligation and wants agent memory that can be audited against it — the audit chain, the retention engine and the per-profile scoping are genuinely built for that, and AGPL-3.0 is workable for internal deployment.

It is not a small component and it is not a retrieval story. If you want one thing from it, take compliance/audit.py: about the size of a single file, and the best answer in this atlas to "what happens to my audit log when my database breaks".

12. Open Questions

  • What produces a trust score? trust/scorer.py and signals.py exist; what feeds them, and what a fresh agent's score is, decides whether the gate admits everything by default the way ABAC does.
  • Does anything verify the audit chain on a schedule? verify_integrity() exists; a scheduled check and an alarm on failure were not found.
  • Is the four-date model used anywhere? observation_date and referenced_date may feed ranking rather than filtering; no consumer was traced.
  • What purges a tombstoned row? archive_status = 'tombstoned' is described as purgeable "so a purge job can find it"; the job was not located.

Appendix: File Index

Auditsrc/superlocalmemory/compliance/audit.py (the independent connection rationale :10-13, _GENESIS_HASH :28, the audit_chain schema :30, verify_integrity)

Compliancesrc/superlocalmemory/compliance/retention.py (the rules table :24, the lifecycle-CHECK workaround :38-47, enforce :327-413), abac.py, gdpr.py, eu_ai_act.py (the refusal to certify :5-9), lifecycle.py, scheduler.py

Trustsrc/superlocalmemory/trust/gate.py (the read/write asymmetry :6-8), scorer.py, signals.py, provenance.py

Schemasrc/superlocalmemory/storage/schema.py:147-215 (atomic_facts, the CHECK constraints, the profile indexes)

Retrievalsrc/superlocalmemory/retrieval/engine.py, entity_channel.py (the contradiction/supersedes edge query :842), vector_store.py, src/superlocalmemory/graph/

Integrationsrc/superlocalmemory/mcp/, server/, cli/, ui/, ide/integrations/ (nine framework packages)

Dynamicssrc/superlocalmemory/dynamics/, math/, learning/, evolution/, optimize/

Appendix: Recorded Searches

Run from the root of the checkout at the pinned commit.

Claim Command Result at this pin
Both clocks reach a query read src/superlocalmemory/storage/database.py:2536-2700 known_as_of and valid_at as independent parameters; a half-open [valid_from, valid_until) exclusion beside them
The filter is wired from the public surface grep -rn "known_as_of" --include="*.py" src engine.py:1108, recall_pipeline.py:1197, recall_worker.py:80, worker_pool.py:105
The tombstone is keyed on a row, not a value grep -rn "fact_id = |uuid" --include="*.py" src | grep -i "fact_id" uuid.uuid4().hex[:16] in the consolidator; v4 elsewhere; no content hash
Scope reaches every read grep -rn "profile_id" --include="*.py" . | grep -icE "WHERE profile_id|AND profile_id" 1,074 predicates
Tree and suite size find src -name "*.py" | xargs wc -l | tail -1; find . -name "test_*.py" | wc -l 203,891 lines under src/; 924 test files

History

2026-09-1107a431ed… — re-read, 857 files and 101,066 insertions past the previous pin in a single commit. bitemporal added, closing the risk the previous edition led on. That edition said "Four temporal columns are stored and no read path filters on the interval" and that the columns were "one predicate away" from the mark. They are past it: get_strict_temporal_inadmissible_fact_ids takes known_as_of for transaction time and valid_at for event time as independent clocks, a companion filter excludes candidates outside the half-open [valid_from, valid_until) window at a reference instant, and both are wired from engine.py:1108 through the recall pipeline and worker. The engineering around it is careful in ways worth recording: bounded to the retrieved candidate pool and chunked at 900 parameters, returning the empty set when no as_of is supplied so adding it demoted nothing, treating a fact with no temporal row as legacy_unknown and excluding it from strict time-travel unless asked, and failing open so a validity lookup cannot break retrieval. audit_log and scope_enforced re-verified. tombstone stays withheld, on a sharper near-miss than before: projection_tombstones is consulted on the store path by _fact_is_tombstoned, so an erased fact is genuinely checked for — but the key is fact_id, and a fact id is a UUID, so the same sentence re-asserted mints a new id and passes. Tree 203,891 lines under src/ with 924 test files. Screened before reading: a plugin manifest, a configured smudge filter in .gitattributes, and several conftest.py files executing on collection; nothing was installed or run.

2026-08-09f47c9c3b… — first reading. Screened before reading: one auto-run surface, build-time execution in eighteen conftest.py files plus a Makefile and an npm manifest, and fifteen dependency manifests inside the seven-day cooldown. The tree was read, never installed, and no test was run.