A purge that later writes collide with

Titen

A Bun and SQLite agent memory with no model and no embedding provider on any path, whose claims carry a database-checked status, trust level and visibility, whose reads gate a validity window and an ABAC predicate composed at seventy-five call sites, and whose purge record makes a later claim citing that evidence fail on a primary key rather than be refused in application code.

Carries 5 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

Titen is agent memory that runs with no API key, no LLM and no embedding provider: Bun and SQLite, FTS5 for retrieval, MCP served over stdio in-process with no outbound network call on the default path. Apache-2.0, version 0.10.0, 277 commits since 29 July 2026, 26,504 lines of TypeScript in src against 11,889 lines across 38 test files. Its own summary line is a good description of what it models: "Every memory keeps its source, who may read it, and the evidence that contradicts it."

Three things make it worth reading.

The invariants are in the schema. A claim's status is checked against ('active','disputed','superseded','expired','revoked') and its trust against ('unverified','asserted','verified','policy_approved'), both by database CHECK. Visibility is checked the same way. The scope index is (org_id, subject_id, status), so status is part of how rows are found rather than a column consulted afterwards, and assertTrustCeiling refuses a write that asserts a level above the writing principal's.

The tombstone is enforced by a primary key. This is the most unusual mechanism here. claim_sources is keyed (claim_id, observation_id, relation). Before inserting a new claim's sources, the write path adds purgedEvidenceGuardStatement, which inserts a claim_sources row for the cited observation only WHERE EXISTS a record_history row whose change_kind is purge for that observation. If the evidence was purged, that speculative row lands first and the legitimate insert that follows collides on the primary key, aborting the transaction. A purged observation therefore cannot be cited again, and the enforcement is a constraint rather than a check a future code path might forget to call.

The access predicate travels with the query. recordAccessSql emits one SQL fragment — organization-visible, or private and yours, or team with a live membership — ANDed with an ownership-or-grant clause that respects revoked_at and an unexpired expires_at. It is referenced at seventy-five call sites across eighteen files.

Reads also gate time: both retrieval lanes add valid_from <= ? and (valid_to IS NULL OR valid_to > ?), so a claim true over a past interval is answerable for that interval.

Two things to be precise about. The hero image's alt text states "dependencies empty"; package.json at this pin declares two runtime dependencies — @simplewebauthn/browser and @simplewebauthn/server — plus a sqlite-vec peer. The substantive half of the claim holds: they are passkey authentication for the dashboard, not memory machinery, and nothing here calls a model or an embedding provider. The dependency count is simply not zero.

And the same alt text publishes the benchmark's degradation: recall@1 of 0.880 on LongMemEval-S in the per-instance scoped condition, "which falls to 0.524, 0.364, 0.308 and 0.246 as the store pools to 1k, 5k, 10k and 19,829 sessions." Putting the curve that undercuts your headline number in the headline image is rare enough to be worth naming.

Five marks: trust_state, scope_enforced, bitemporal, tombstone, audit_log.

2. Mental Model

An observation is what was seen, with its source. It can be redacted or purged.

A claim is what is believed: a subject, a kind, a statement, a confidence, a trust level, a visibility, a status, a version and a validity window, with a canonical hash for idempotency.

Evidence joins them: a claim_sources row typed supports, contradicts or qualifies. A claim can therefore carry the evidence against it.

Trust is a ladder — unverified, asserted, verified, policy_approved — and a principal cannot assert above their own rung.

Visibility is private, team or organization, and the read predicate resolves it against live memberships and unexpired grants.

History is a record_history row per change, with a snapshot hash. The purge guard reads it.

Diagram — a purge record turns a later citation into a primary-key collision; every read carries the access predicate and the validity window
Diagram source
%% caption: a purge record turns a later citation into a primary-key collision; every read carries the access predicate and the validity window
flowchart TB
    AGENT["agent over MCP stdio<br/>nine reference tool names"] --> OBS[("observations<br/>content + source")]
    OBS --> CLAIMW["write claim"]
    CLAIMW --> CEIL{"assertTrustCeiling<br/>level <= principal's"}
    CEIL -->|"above"| REJ["refused"]
    CEIL -->|"ok"| GUARD["purgedEvidenceGuardStatement:<br/>INSERT claim_sources row<br/>WHERE EXISTS a record_history<br/>purge for this observation"]
    GUARD --> INS["INSERT claims + claim_sources"]
    INS --> PK{"claim_sources PRIMARY KEY<br/>(claim_id, observation_id, relation)"}
    PK -->|"guard row already there"| ABORT["collision — transaction aborts<br/>purged evidence cannot be cited"]
    PK -->|"clear"| C[("claims<br/>kind, statement, confidence,<br/>trust, visibility, status,<br/>version, valid_from/valid_to,<br/>canonical_hash")]
    INS --> HIST[("record_history<br/>record_type, record_id, version,<br/>change_kind, actor_id, snapshot_hash")]
    HIST -.->|"read by the guard"| GUARD
    PURGE["evidence purge"] --> HIST
    Q["retrieval — FTS5, no model"] --> VIS["recordAccessSql<br/>organization | private+owner |<br/>team+live membership<br/>AND owner-or-unexpired-grant"]
    C --> VIS
    VIS --> TIME["valid_from <= ? AND<br/>(valid_to IS NULL OR valid_to > ?)"]
    TIME --> STAT["status via (org_id, subject_id, status)"]
    STAT --> OUT["returned"]

3. Architecture

Area Role
src/core/migrations.ts The schema and every CHECK — the fastest way to read the model
src/core/claims.ts The claim write path, the trust ceiling, the guard call
src/core/writes.ts historyStatement, outboxStatement, purgedEvidenceGuardStatement
src/core/authorization.ts recordAccessSql and its organization-wide variant
src/core/retrieval.ts The FTS lanes and the validity window
src/core/observations.ts, evidence.ts Observations, redaction, evidence relations
src/core/lifecycle.ts, maintenance.ts Status transitions, retirement, the outbox drain
src/core/audit.ts, atlas.ts The audit command, including over other vendors' stores
src/core/mcp.ts The reference-server-compatible tool surface
tests/contract, tests/integration Backend contracts (Bun SQLite, D1) and end-to-end behaviour

4. Essential Implementation Paths

  • src/core/migrations.ts:68-113 — claims, claim_sources, record_history.
  • src/core/writes.ts:49-66 — the guard, and why it is shaped that way.
  • src/core/claims.ts:283-300 — where the guard is pushed before the insert.
  • src/core/authorization.ts:18-48 — the access predicate.
  • src/core/retrieval.ts:155-215 — both lanes and their temporal clause.
  • src/core/validate.ts:3-40 — trust levels, ranks, visibilities, relations.

5. Memory Data Model

The separation of observation from claim is the spine: what was seen is not what is believed, and the join between them is typed, so contradicts is representable rather than implied by absence. A claim's confidence is constrained > 0 AND <= 1 at the database, its version starts at 1, and its canonical_hash supports idempotent re-writes.

Six claim kinds — semantic_fact, episodic_event, preference, procedural, decision, relationship — are enumerated in a CHECK rather than left to convention.

6. Retrieval Mechanics

FTS5 with unicode61 remove_diacritics 2 tokenisation over both observations and claims, with a migration comment noting that both FTS tables are rebuilt together "so observation and claim tokenization cannot drift". Vector support is an optional peer dependency rather than a requirement. Every read composes the access predicate and the validity window.

7. Write Mechanics

Writes are idempotent by request key and canonical JSON hash. The trust ceiling is checked against the principal. The purge guard runs before the source inserts. Each mutation appends history with a snapshot hash, and an outbox row drives indexing, which a test asserts happens "without anyone calling the drain endpoint".

8. Agent Integration

Titen serves the nine tool names of the reference memory server with the same schemas, answers memory://knowledge-graph, and imports an existing store on first local-mode start — and when it cannot find one it "says on stderr when it found nothing, rather than starting empty in silence". That last detail is a small thing that decides whether a migration is noticed.

9. Reliability, Safety, and Trust

The guard protects the first cited source. purgedEvidenceGuardStatement inserts one row, for observationIds[0], while its WHERE EXISTS tests every cited id. So a claim citing a live observation first and a purged one second produces a guard row keyed on the live id — which the real insert will also write, so the collision still fires. The mechanism holds for the cases this report could construct, but it depends on the guard row's id also appearing in the real insert, which is a subtler contract than "refuse if any cited observation is purged" would be.

The dependency claim is overstated. Two WebAuthn packages and a sqlite-vec peer are declared. Nothing about them undermines the no-model, no-embedding, no-network claims, which are the substantive ones; "dependencies empty" is simply not what package.json says at this pin.

The benchmark is published against itself. Recall@1 falls from 0.880 to 0.246 as the corpus pools from one instance to 19,829 sessions, and that sentence is in the README's hero image. A reader is told the scaling limit before they are told the headline.

10. Tests, Evals, and Benchmarks

38 test files split into backend contracts — Bun SQLite, Cloudflare D1, a D1 harness, vectors — and integration suites for MCP stdio, local mode, audit, maintenance, federation, webhooks, collaboration integrity and runtime hardening.

Two test names are worth repeating because they encode judgement rather than coverage: "audit refuses a file it cannot recognize instead of reporting zeros", and, inside the audit suite, the assertion that "an absent signal is never a failure". A store that reports zero when it cannot read the input is the failure mode those two exist to prevent.

11. For Your Own Build

Steal

  • Enforce the tombstone with a constraint. A guarded insert that collides on a primary key cannot be skipped by a new code path the way an if (isPurged(...)) can.
  • Put the enum in a CHECK. Status, trust, visibility, claim kind and relation are all constrained at the database here, so an invalid value is a write error rather than a filter that silently matches nothing.
  • Compose the access predicate into the query. Seventy-five call sites referencing one fragment is a stronger guarantee than one gate everyone is expected to pass through.
  • Rebuild related FTS tables together. The comment explains the failure it prevents: two tokenizers drifting apart across migrations.
  • Say on stderr when an import found nothing. Starting empty in silence is how a migration is discovered a week later.
  • Publish the curve, not just the peak. The degradation from 0.880 to 0.246 is in the hero image.

Avoid

  • Claiming zero dependencies while declaring some. The interesting claims here — no model, no embedding provider, no outbound call — are true and checkable, and the one that is not true is the one a reader checks first.

Fit

Reach for this if you want deterministic, self-hosted memory with real per-principal visibility and no provider to configure, and you can accept lexical retrieval. Look elsewhere if your corpus will pool to tens of thousands of sessions and you need recall to hold there.

12. Open Questions

  • Should the purge guard emit a row per cited observation rather than one for the first, so the contract is "any purged source aborts" by construction?
  • Trust has four levels and a ceiling on assertion. What promotes a claim from asserted to verified — and is that surface a person's?
  • The degradation curve is published. Is the intended answer scoping (which the per-instance number suggests) or a ranking change?

Appendix: File Index

Path What to read it for
src/core/migrations.ts Every table and every CHECK
src/core/writes.ts The guard, the history row, the outbox
src/core/claims.ts The write path and the trust ceiling
src/core/authorization.ts The one access predicate
src/core/retrieval.ts The lanes and the validity clause
src/core/audit.ts Auditing a store this project does not own
tests/integration/audit.test.ts Refusing to report zeros

History

2026-09-16a68ce063… — first reading, at a commit dated 30 August 2026. Screened before opening, from a shallow clone: thirteen files, two auto-run surfaces (a .claude-plugin/ directory and an MCP server manifest), no build-time execution points, two unpinned surfaces, none inside the cooldown, and AGENTS.md and CLAUDE.md read as data. Nothing was installed, built or run.