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.
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.
Stop injecting the log. Inject an evolving summary. Key it by category, not timestamp — how-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.
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
mockis 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.
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| 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 |
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.)
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.
- 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 seam —
mergeDigest()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.
- Token figures are estimates (chars/4), not exact
count_tokenscalls — 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. Seedocs/upstream-notes.mdfor what an in-schema, provider-routed upstream contribution would require. claude -pandANTHROPIC_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.
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.
Apache-2.0 © 2026 Eric Tetzlaff