Skip to content

Repository files navigation

persisted-memory

Consolidate an append-only agent-memory store into evolving, fixed-size digests — so memory gets denser as it grows, not just longer.

A small, zero-install (Bun) experiment in keeping long-running agent memory cheap to read. It maintains one evolving digest per (project, type) cell and folds new observations into it with a recursive merge:

digest(n) = merge( digest(n-1), new observations since the last run )

The store stays append-only. The understanding gets rewritten. An append log can only get longer; a digest that is rewritten each run stays roughly the same size while getting smarter.

Built against claude-mem (a local memory plugin for Claude Code), but the schema assumptions are small and the technique is tool-agnostic.


The problem

An append-only memory store grows without bound. A common "inject the 50 most recent notes" strategy then fails two ways at once:

  • Cost climbs with history — more past work means more tokens re-read every session.
  • Old context silently drops — a decision from weeks ago falls off the back the moment 50 newer notes pile in front of it.

You end up paying more over time to remember less of what matters.

The idea

Stop injecting the log. Inject an evolving summary. Key it by category, not timestamphow-it-works, gotcha, decision, pattern, and so on (eight cells per project). Each run, hand the model the current digest plus only the observations newer than a stored watermark, and ask it to rewrite: merge duplicates, prefer newer information, drop what's superseded.

Because a project has a fixed, small number of categories, the digest size is bounded — it tracks how many kinds of things you know, not how much you wrote down.


Quickstart (offline, no API, no private data)

Requires Bun (built-in SQLite, nothing to install).

# 1. Seed a synthetic store (two fake projects, lopsided history)
bun seed-example.mjs

# 2. Fold it — MERGE_MODEL=mock uses a deterministic, no-LLM merge
CLAUDE_MEM_DB=./example.db DIGEST_DB=./demo.db MERGE_MODEL=mock bun build-digests.mjs

# 3. Read a project's digest block + token economics
CLAUDE_MEM_DB=./example.db DIGEST_DB=./demo.db bun read-digests.mjs widgets-api

mock is plumbing only. It dedupes and truncates to budget — it does not semantically consolidate, so its compression is the worst case (~3x). It exists to prove the watermark / drain / resume / economics path with no provider and no cost. Real compression comes from the LLM merge below.

Real usage (against a live store, LLM merge)

The merge shells out to claude -p over your Claude subscription (no API key, no metered cost). Point it at your store and run:

# Default model is sonnet; haiku is cheaper/faster for big backfills
bun build-digests.mjs my-project
MERGE_MODEL=haiku bun build-digests.mjs my-project

bun read-digests.mjs my-project          # the injectable block + economics
bun cluster-observations.mjs             # read-only: inspect the data shape first

Config (env)

Var Default Meaning
CLAUDE_MEM_DB ~/.claude-mem/claude-mem.db source observation store (read-only)
DIGEST_DB ./digests.db digest output DB
MERGE_MODEL sonnet sonnet · haiku · mock

Expected source schema

A SQLite table observations(project, concepts, title, narrative, facts, created_at_epoch), where concepts and facts are JSON-array strings. concepts carries the category tags the digest keys on. (seed-example.mjs shows the exact shape.)


Results

Measured on a personal memory store of seven projects (token estimates = chars/4; project names anonymized). The point is the shape, not the digits.

project     observations   raw tokens   digest tokens   ratio
project-a          1640      587,602        4,288       137.0x
project-b           753      310,395        6,489        47.8x
project-c           507      190,597        6,054        31.5x
project-d           127       50,824        4,557        11.2x
project-e            46       21,437        3,555         6.0x
project-f            34       11,588        2,344         4.9x
project-g            29       11,046        4,091         2.7x   (floor)
─────────────────────────────────────────────────────────────
TOTAL              3136    1,183,489       31,378        37.7x

Two things this shows:

  • Digest size is roughly flat (~2.3–6.5K tokens) across projects from 29 to 1,640 observations. It's bounded by cells × budget.
  • So compression scales with history. A near-empty project barely benefits (project-g, 2.7x — the floor, because eight cells have a fixed cost). A deep one collapses by two orders of magnitude (project-a, 137x). The technique pays off precisely where you need it.

A session-start read injects one project's block — a few thousand tokens, flat — covering its entire history instead of a recent slice.


How it works

  • Key by (project, type), UNIQUE — a small fixed cell set per project is the whole reason cost stays flat.
  • Watermark (source_max_epoch) per cell — only observations newer than the watermark are folded. Re-running with nothing new is a no-op (idempotent).
  • Drain mode — fold in MAX_NEW_PER_RUN-sized batches until the cell is current, persisting after every batch. An interrupted run resumes cleanly from the last committed watermark; finished cells are never recomputed.
  • Swappable seammergeDigest() is the only provider-coupled function. Replace its body to use any model or local LLM.
  • Read-only source — the source store is opened {readonly:true}; a bug in this tool physically cannot corrupt your memory.

Known limitations

  • Token figures are estimates (chars/4), not exact count_tokens calls — order-of-magnitude, not to the digit.
  • The size budget is advisory. It's enforced in the prompt; a model treats it as a strong suggestion. A real guarantee needs a hard post-merge truncate.
  • Small projects barely benefit — below ~100 observations, eight digest cells have a fixed floor cost, so compression can approach a wash.
  • This is a read-only sidecar, not part of claude-mem. It reads the store and writes its own digests.db. See docs/upstream-notes.md for what an in-schema, provider-routed upstream contribution would require.
  • claude -p and ANTHROPIC_API_KEY: if that key is set, the CLI bills API credits instead of your subscription — and an empty key still wins its slot. The tool removes it from the child process environment; if you adapt the merge, preserve that.

Relationship to claude-mem

This is an independent experiment that interoperates with claude-mem by reading its observation store; it does not fork or modify it. Credit for the underlying memory system — session distillation, the observation model, the worker — belongs to thedotmack/claude-mem. An upstream discussion about folding consolidation into the plugin itself is the intended next step; see docs/upstream-notes.md.

License

Apache-2.0 © 2026 Eric Tetzlaff

About

Recursive-fold consolidation for append-only agent memory — evolving fixed-size digests so memory gets denser as it grows, not just longer. Built to interoperate with claude-mem.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages