A schema that deleted its own dead fields, with receipts

memory-ts

Seven metadata fields removed in one version, each with the evidence — "580 variants, never used", "always empty", "no logic implemented" — recorded in the schema's own history.

Carries 1 of 7 rubric mechanisms. Most systems here carry none or one (43%), 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

memory-ts is a memory layer for Claude Code and Gemini CLI — hooks, a curator that extracts memories from the session transcript, semantic surfacing, and a session primer ("Last session: 2 hours ago. We implemented embeddings…").

The artifact worth the report is src/types/schema.ts's version history, which records what the schema removed and why: There is no licence file in the tree, so the default applies and all rights are reserved.

"v3: Consolidated metadata — removed fragmented/unused fields: knowledge_domain (overlaps with project_id + domain); emotional_resonance (580 variants, never used); component (always empty); expires_after_sessions (never used); parent_id/child_ids (no logic implemented); retrieval_weight (retrieval uses importance_weight); temporal_relevance (replaced by temporal_class)."

This atlas spends a great deal of its time finding declared-and-unread fields — audit models with no writer, valid_from columns no query touches, status enums nothing sets. This is a project that went looking for its own and deleted them, and left the evidence for each. "580 variants, never used" is a measurement someone took. "No logic implemented" is an admission. "Always empty" is a query someone ran against their own store.

A schema that shrinks is rare; a schema that documents why each field failed to earn its place is rarer, and it is the single most useful thing a maintainer can leave for the next person — including themselves, when they are tempted to add emotional_resonance again.

And v4 is a good idea in its own right — section 5's two-tier memory.

2. Mental Model

A session ends, a curator reads the transcript and proposes memories. Each carries a status, a confidence, a context type from a closed enum, and a headline separate from its body. Retrieval surfaces headlines, filters to active, and expands the body only when a rule says it matters.

Diagram — a headline is always shown and the full content expands only on an explicit rule, so the surfacing decision is separate from the retrieval one
Diagram source
%% caption: a headline is always shown and the full content expands only on an explicit rule, so the surfacing decision is separate from the retrieval one
flowchart TD
    S["session ends"] --> CU["curator — reads the transcript,<br/>proposes memories with a session summary<br/>and an interaction tone"]
    CU --> M["memory: headline + content,<br/>confidence_score, context_type (11 values),<br/>status, importance_weight,<br/>sessions_since_surfaced = 0"]
    Q["you type something"] --> R["semantic retrieval"]
    M --> R
    R --> F{"status === 'active'?"}
    F -->|no| HIDE["not surfaced"]
    F -->|yes| HEAD["headline shown —<br/>1–2 lines, always"]
    HEAD --> AX{"auto-expand rule?<br/>action_required · awaiting_decision · 5+ signals"}
    AX -->|yes| FULL["full structured content"]
    AX -->|no| STAY["headline only"]
    SUP["a memory replaces another"] --> SB["superseded_by → the replacement"]
    SB --> ST["old memory status = superseded"]
    DEC["not surfaced this session"] --> CNT["sessions_since_surfaced += 1"]
    LNK["linked / replacement lookups"] --> EXC["skipped when status is<br/>'archived' or 'deprecated'"]

3. Architecture

src/ splits into core (curator, retrieval, store, session-parser), server, cli, types (schema, memory), migrations, utils. Hooks and skills directories sit beside it, and the package installs globally through Bun.

The dual-CLI support is the integration claim — "use your preferred AI coding assistant, or even both simultaneously. The memory server handles concurrent sessions from different CLIs seamlessly" — which makes the memory the shared thing rather than the tool.

11,600 lines of TypeScript. There is a migrations/ directory, which follows from having a schema that changes.

4. Essential Implementation Paths

The schema and its history — src/types/schema.ts (the v2–v4 changelog :20-34, confidence_score :43, context_type :46, status :65, sessions_since_surfaced :72, superseded_by :84).

Retrieve — src/core/retrieval.ts (the active filter :158, :812, the replacement and linked-memory guards :491, :646).

Curate — src/core/curator.ts, src/core/session-parser.ts, test-curation.ts.

5. Memory Data Model

status is a five-value enum — active | pending | superseded | deprecated | archived — with superseded_by pointing at the replacement, and it is read on the retrieval path: if (memory.status && memory.status !== 'active') return false appears at two sites, and the replacement and linked-memory lookups separately skip anything archived or deprecated. That earns trust_state, and the distinction between the five is meaningful: pending is not yet believed, superseded was replaced, deprecated was retired, archived was put away.

The consumer is more finished than the producer, and the gap is the whole correction story. The read side is genuinely worked out: superseded rows are deliberately not filtered in the match phase — "NOTE: Don't filter superseded_by here - we handle redirects in Phase 1" — and retrieval.ts:486-493 instead follows superseded_by ?? resolved_by, loads the replacement, and surfaces it in the superseded memory's place, logging the redirect. A reader who asks about an outdated fact gets the corrected one rather than nothing.

But nothing writes the fields it reads. superseded_by and resolved_by are assigned exactly twice each in the source, both times as null in the two creation paths (store.ts:250, :253, :526, :529), and no literal 'pending', 'superseded', 'deprecated' or 'archived' is assigned to status anywhere. The curator — the component that writes memories at session end — never mentions either. The one surface that can set them is PATCH /memory/:id in server/index.ts:477, which takes status and the curation flags as optional body fields, so a person or an external tool can mark a memory superseded by hand.

The mark stands on that: the status is stored, the filter is on the read path, and a live writer exists. What belongs beside it is that in an unattended install nothing ever moves a memory off active, so the five-state vocabulary has one reachable value and the redirect machinery never fires. pending in particular is a state the pipeline cannot produce, which makes it a description of an intent rather than a state the system enters.

sessions_since_surfaced counts decay in sessions, not in time. That is a better unit for this workload than a wall-clock half-life: a memory that has not been relevant across twenty working sessions is stale whether those took a week or three months, and a project you did not touch for a month should not have its memories decay for the holiday.

v4's two-tier structure is the other idea to take:

"headline: 1-2 line summary (always shown in retrieval); content: full structured template (expand on demand). Auto-expand rules: action_required, awaiting_decision, 5+ signals."

Storing the summary and the body as separate fields — rather than truncating at render time — means retrieval can show twenty headlines for the cost of two full memories, and the auto-expand rules decide when brevity is the wrong default. "Something is awaiting your decision" is exactly the case where the user needs the whole thing unprompted.

6. Retrieval Mechanics

Semantic surfacing with cosine similarity over a filesystem-backed store, no external service. The interface shown in the README puts the confidence and the tags in front of the memory — [🔧 • 0.9] [fsdb, vectors] fsdb has cosineSimilarity — so the model and the user see the system's own uncertainty beside the claim rather than having it applied silently.

project_id and domain exist on a memory; no scope key was found applied as a read predicate, so scope_enforced is not earned.

7. Write Mechanics

Curation runs at session end against the transcript, producing memories plus a session_summary and an interaction_tone. curateWithSessionResume handles a session that was resumed rather than started fresh, which is the case a naive end-of-session hook gets wrong — it would re-curate material already captured.

The curator is a subprocess call to a CLI (getClaudeCommand, getCuratorPromptPath), with the prompt in a separate file. Keeping the extraction prompt as an editable artifact rather than a string literal is right; depending on a CLI binary being present and stable is the cost.

8. Agent Integration

bun install -g @rlabs-inc/memory then memory install for Claude Code or the Gemini CLI equivalent. Hooks, skills, and a server handling concurrent sessions from both CLIs.

9. Reliability, Safety, and Trust

One mark: trust state, per section 5.

Tombstone — no. superseded_by records the replacement and nothing keys on the superseded value, so the same content re-extracted from a later session is a new memory.

Bitemporal, audit log, scope, human review, negative eval — no.

The risk is the curator's dependency shape. Extraction is a subprocess invocation of an external CLI, so the memory layer's quality is downstream of a binary's presence, version and prompt-following. test-curation.ts at the repository root — a hand-run script that prints the result of curating one session — is the only exercise of it found, and it is a debugging aid rather than a test.

For a system whose entire value is what the curator chooses to keep, that is the gap. The schema history shows this project is willing to measure its own fields and delete the ones that fail; the same instinct applied to curation output — what fraction of curated memories are ever surfaced, how many are duplicates of an existing memory — would be the natural next measurement, and sessions_since_surfaced is already the column that would answer the first.

10. Tests, Evals, and Benchmarks

No paper and no benchmark — but there is a test suite, and the first version of this report said there was not. src/core/engine.test.ts is 313 lines of bun:test covering three describes and ten cases: the store round-trip, session tracking, summaries and stats; the retrieval scorer's action_required boost, trigger-phrase matching and maxMemories limit; and two engine cases. It sits beside test-curation.ts, the #!/usr/bin/env bun script that curates one session id and prints the result, which the earlier text called the only exercise of the system.

And its deduplication case cannot fail, in the file's own words. The test is named should deduplicate memories within session, its comments say "Third request - same session, should NOT get same memory again" and "Second query should find the memory, third should not (already injected)", and the assertion is:

console.log(`Result2 memories: ${memory2Count}, Result3 memories: ${memory3Count}`)

// At least verify the deduplication logic is running
expect(result3.memories.length).toBeLessThanOrEqual(result2.memories.length)

<= passes when dedup does nothing and both counts are equal, and passes again when retrieval returns nothing and both are zero. The intended assertion is written down twice in the comments; the code declines to make it, and the third comment admits as much. This is the shape the atlas's test-that-cannot-fail note collects, and an unusually candid instance — the author recorded the gap rather than hiding it behind a passing name.

The schema changelog is, in an unusual sense, the evaluation record this project does have: seven fields removed with a stated reason each is evidence that somebody checked whether the design was carrying its weight. It is retrospective and manual, and it is more self-examination than most repositories in this atlas show.

I ran nothing.

11. For Your Own Build

Steal

  • Record what your schema removed and why. "580 variants, never used", "always empty", "no logic implemented", "overlaps with X + Y" — four different reasons a field failed, all worth knowing, and all lost if the field is quietly dropped.
  • Go looking for your own dead fields. Almost every system in this atlas has one; almost none has deleted one. SELECT COUNT(DISTINCT field) and SELECT COUNT(*) WHERE field IS NOT NULL are the two queries.
  • Store the headline and the body as separate fields. Truncating at render time throws away the choice; two fields let retrieval show twenty summaries for the cost of two full memories.
  • Write down when brevity is wrong. action_required, awaiting_decision and "5+ signals" as auto-expand rules mean the cases that need the whole thing get it without being asked for.
  • Count decay in sessions, not in days. A project untouched for a month should not have its memories age for the holiday; twenty working sessions without a memory surfacing is real evidence of staleness.
  • Give status five values and read it on the retrieval path. pending, superseded, deprecated and archived are four different reasons not to surface something, and the replacement lookup should skip the last two too.
  • Handle the resumed session explicitly. curateWithSessionResume avoids re-curating material a fresh-session hook would capture twice.
  • Keep the extraction prompt in a file. Editable, diffable, and not a string literal in the curator.
  • Show confidence beside the memory in the interface. [🔧 • 0.9] in front of the claim beats applying the number silently.

Avoid

  • Do not leave the curator untested when the curator is the product. A hand-run script that prints one session's output is a debugging aid; what fraction of curated memories are ever surfaced is the measurement, and sessions_since_surfaced already holds the answer.
  • Do not depend on an external CLI binary for your core extraction without saying what happens when it is absent or its output shape changes.
  • Do not let superseded_by be the whole correction story. It records the replacement and nothing prevents the superseded content being re-extracted from a later transcript as a fresh memory.

Fit

A reasonable choice if you switch between Claude Code and Gemini CLI and want one memory behind both — that dual-CLI framing is uncommon, and the two-tier headline/content structure is a genuinely good answer to injection cost.

src/types/schema.ts is worth opening whatever you build. Its first thirty lines are a model of how to document a schema that changed its mind.

12. Open Questions

  • What sets pending? The status is filtered on retrieval; the writer of each state was not traced.
  • Does anything act on sessions_since_surfaced? It is incremented and named a decay counter; the threshold was not found.
  • What happens when the curator CLI is missing? getClaudeCommand resolves it; the failure path was not read.
  • How do concurrent sessions from two CLIs avoid double-curating? The claim is that the server handles it seamlessly.

Appendix: File Index

Schema — src/types/schema.ts (the v2/v3/v4 changelog with the removal reasons :20-34, confidence_score :43, context_type enum :46, status :65, sessions_since_surfaced :72, superseded_by :84, superseded_count :151), src/types/memory.ts (V4_DEFAULTS :167, :203), src/migrations/

Retrieval — src/core/retrieval.ts (the active-status filters :158, :812, the replacement guard :491, the linked-memory guard :646)

Curation — src/core/curator.ts (CuratorConfig, getClaudeCommand, getCuratorPromptPath), src/core/session-parser.ts, src/core/store.ts (:239), test-curation.ts

Surfaces — src/server/, src/cli/, hooks/, skills/

Appendix: Recorded Searches

Checked at the pinned revision, without a clone.

Claim Check Result at this pin
No licence file exists anywhere in the tree GET /repos/<owner>/<repo>/git/trees/<this revision>?recursive=1, filtered for a path matching licen[cs]e or COPYING, and each of LICENSE, LICENSE.md, LICENSE.txt, COPYING and LICENCE fetched directly from raw.githubusercontent.com at this revision Nothing, by both checks. The grant is absent rather than merely unlocated, so the default applies and all rights are reserved.

History

2026-09-19 — 8fcadf6d… — trust_state re-tested at the unchanged pin; the mark holds and the detail the record led with is the one worth leading with. Both retrieval entry points drop a non-active record, but superseded_by is deliberately not filtered there — it is redirected in the first phase to the memory it names, and the code says so twice, in the doc comment above the filter and again inside it, which is a note written to stop a future reader from "fixing" it. That is the difference between a store that forgets a corrected fact and one that forwards the question to the correction. Two details are added: resolved_by takes the same path as superseded_by, and the predicate is written memory.status && memory.status !== 'active', so a record carrying no status at all passes — the permissive value is the absent one. Nothing was installed and no suite was run.

2026-09-18 — 8fcadf6d… — re-read at the same commit. Nothing upstream had moved, so every correction is the atlas's own, and the first reading had read the status field without asking who writes it.

Nothing does. superseded_by and resolved_by are assigned four times between them, all null in the two creation paths, and no literal 'pending', 'superseded', 'deprecated' or 'archived' is assigned to status anywhere in the source; the curator never mentions either. Meanwhile the read side is fully built — superseded rows are deliberately left in the match phase so retrieval.ts:486-493 can follow superseded_by and surface the replacement instead. The only writer is the optional status field on PATCH /memory/:id. trust_state stands, because the status is stored, filtered on the read path and settable by a live surface, and the limit is now in its evidence record: an unattended install never leaves active, so the redirect never fires and pending is a state the pipeline cannot produce.

Section 10 said there was no test directory and that test-curation.ts was the only exercise of the system. src/core/engine.test.ts is 313 lines of bun:test with ten cases over the store, the retrieval scorer and the engine. One of them is worth its own paragraph: the case named should deduplicate memories within session asserts result3.memories.length is less than or equal to result2.memories.length, which passes when deduplication does nothing and passes again when retrieval returns nothing — and the file says so, in three comments that state the assertion the code does not make. stack_source goes from seeded to reviewed.

2026-08-09 — 8fcadf6d… — first reading. Screened before reading; the tree was read, never installed, and no curation was run.