Observation notebook · Anya · 2026 · tooling branch
mem0-processor
Systemmem0-processor · 119 local models · summarization eval
Statuspipeline working daily · backend mid-migration · revised 2026-06-21
Source831 summaries · 23 ground-truthed sessions · z-scored

I set out to build agent memory and built a model-evaluation harness instead. The memory is the use-case it was born from; the eval layer is the part that's reusable — and it surfaced a finding the pipeline was never built to look for.

§1One inversion is the whole architecture

Ask an agent to write its own memories during a conversation, and the memory-writing competes with the user's task for the model's attention. On every turn the model splits effort between answering you and persisting state, and persistence is the lower-urgency job, so in practice it gets a perfunctory sentence or gets skipped. So invert it: don't instrument the live session, mine the logs offline.

Claude Code already writes complete JSONL transcripts of every session it runs. After the fact, a local model batch-summarizes those transcripts, the summaries are scored against ground truth, and the survivors are uploaded as searchable memory. ETL, not live-write. That single inversion is the entire architecture; everything else is making it reliable.

The appeal: the source of truth is data you already have and never had to contaminate the live interaction to collect. The cost: you've now made writing good memory into a summarization-quality problem, which means you have to know which local model actually summarizes well, and that turned out to be a real question with no published answer. That cost is the pivot. Chasing "which model summarizes well" turned mem0-processor into a model-evaluation system that happens to emit memories as its output.

~/.claude/projects/**/*.jsonl raw session transcripts — the source of truth │ parse + segment lib/transcript.mjs ▼ local-LLM summarization llama-server, per-model from a ~145-entry registry │ ▼ scoring + ranking hand-written ground-truth fact sets; z-score across sessions │ ▼ memory backend searchable, deduped by session id (Mem0 today)

§2The core system: a summarization eval harness

Picking which 1–9B local model writes your memories is a model-selection problem, so the pipeline grew a benchmark, and the benchmark grew teeth. It carries 119 models scored (62 in the quality tier, 31 with enough shared sessions to rank head-to-head), evaluated on the exact task they'd be used for: not MMLU, not vibes, but "did the summary capture the facts this session actually established." Three disciplines do the work:

§3What "capturing the facts" actually means

The scored target is not the shape of the session or its headline events. Those are close to free: Claude Code's own Haiku compaction pass already emits a high-quality summary of what happened. What this benchmark scores is the layer above that, decision attribution, meaning who made each call, who caught what, and whether a decision was contested or silently assumed. That's the layer later sessions actually need (the why outlives the what), and the layer models fall down on.

Session 2503c51c is the canonical hard case: three distractors wrapped around one real bug. Mid-session, Claude proposed new Date() for a summary's timestamp header; the user read the code and caught that this stamps processing time rather than session time, and redirected the fix to pull startedAt from the transcript JSONL. The bug never threw — it would have silently corrupted the timestamp on every future summary, undetectable until someone ran temporal analysis. The two nearby exchanges are traps: a double-timestamp worry and a timezone question were both cases where the user misunderstood and Claude was correct. A model that pattern-matches "user raised a concern → there was a bug" inverts all three.

A summary scores well here only if it gets the attribution right, not the content: Claude was wrong, the user caught it by reviewing code, and the failure mode was silent corruption. That last clause is second-order (it requires reasoning about a downstream failure that never visibly happened), and it was hit by 0 of 232 local-model runs as of the 2026-04 scoring epoch.

The cliff is not a small-vs-large one. Scored on the same segment, real frontier models fared no better — and the one that got it right only did so when fed a summary that had already extracted the decision:

RunWhat its summary did with the catchTier
232 local-model runsnone surfaced the silent-corruption risk (0 hits)✗ T4
Haiku 4.5asserts the timestamp work was "already completed"✗ T4
Claude Sonnet 4.6treats the timestamp work as skipped✗ T4
Sonnet 4.6 · continuationinherits a compaction note → relays the catch + corruption risk correctly✓ restated
sourced Haiku/Sonnet T4 reproduced on-disk via the session scorer draft flag a fuller frontier gradient (Gemini/ChatGPT also missing it, Opus 4.6 clearing both) is recollection — pull the cloud-scoring record before publishing beyond Haiku/Sonnet

§4Why raw averages lie

What this catches isn't hypothetical. In the current dataset, ranking by raw fact-recall puts qwen35-9b-os-auto-thinking 4th, but it only drew a handful of easy sessions (n=7), and per-session z-scoring drops it to 26th. The mirror case is a 2-bit quant, qwen3-6-27b-iq2-m: 15th on raw average, rising to 2nd on z-score, because it happened to be tested on harder sessions where its real margin showed. Raw and normalized rankings here don't just shift, they invert, and the 2-bit model outranks the raw-average favorite once difficulty is normalized out.

↳ the left chart is the one people publish; the right chart is the one that's true

Why raw averages lie Connectors ranked by raw mean fact-recall on the left, where overlapping error bars leave the order muddy because each model saw a differently-hard session mix, and by per-session z-score on the right, which normalizes out session difficulty and sharpens the ranking. Two connectors cross hardest between the two views — os-auto-thinking falling from raw #4 to z #26, and iq2-m rising from #15 to #2. Raw averages are an illusion: the ranking reorders once difficulty is normalized 31 rank-eligible summarizers · left rail = raw mean fact-recall · right rail = per-session z-score RANKED BY RAW MEAN RANKED BY z-SCORE the naive chart the normalized chart #1 → #1 top stays put qwen35-9b-os-auto-thinking #4 raw · n=7, easy mix #26 z difficulty normalized out qwen3-6-27b-iq2-m #15 raw · n=10, harder mix · 2-bit #2 z real margin shows on hard sessions Crossing lines are rank inversions — the steeper the cross, the more the raw average was lying.
Why raw averages lie Connectors ranked by raw mean fact-recall on the left, where overlapping error bars leave the order muddy because each model saw a differently-hard session mix, and by per-session z-score on the right, which normalizes out session difficulty and sharpens the ranking. Two connectors cross hardest between the two views — os-auto-thinking falling from raw #4 to z #26, and iq2-m rising from #15 to #2. Raw averages lie: the ranking reorders once you normalize 31 rank-eligible summarizers · same models, both rails BY RAW MEAN BY z-SCORE #1→#1 #4 #26 #15 #2 qwen35-9b-os-auto-thinking #4 → #26 · n=7, easy mix qwen3-6-27b-iq2-m #15 → #2 · n=10, harder mix · 2-bit Bigger cross = the raw average was lying harder.
Fig 1 — why raw averages lie. Ranked by raw mean fact-recall (left), the order is muddy with overlapping error bars because each model saw a different, differently-hard session mix. Ranked by per-session z-score (right), which normalizes out session difficulty, the ranking sharpens and reorders — the two hardest-crossing connectors are os-auto-thinking (raw #4 → z #26) and iq2-m (raw #15 → z #2).
source dataset.db → score_normed_quality, rank-eligible n≥5type fully data-groundedsafety viridis + markers, no red/green; the crossing is dash + marker coded
There is no stable ranking of small local models for summarization without controlling for sampler config and session difficulty. That was an accidental result — the pipeline was built to pick a summarizer, not to study model evaluation — but it's the transferable one. The headline the benchmark produced · §4

The benchmark is, quietly, the more broadly useful half of the project. "Which small local model should write structured summaries" is a question a lot of people building local agent tooling have, and there's very little measured data on it — same gap as the quant benchmark, same reason (nobody runs the equal-environment harness). The dataset (dataset.db, SQLite) and the scoring code stand on their own, independent of whether you ever upload a memory.

§5Status — the honest version

Working daily: transcript discovery, llama-server orchestration from the registry, summarization, scoring, the upload, and session-id dedupe. In progress: a sampler-config schema (v9.4) and dataset epoch tagging. Planned: a migration off the current Mem0 backend to an AWS-native one (DynamoDB + Lambda), because Mem0's search reliability is the pipeline's weakest link (retrieval is unreliable enough that the practical workaround is fetching memories by direct UUID rather than trusting search).

The architecture (the ETL inversion and the eval layer) is the stable part; the memory backend is explicitly the swappable part, and is being swapped. That's worth stating plainly rather than hiding, because the weakness is the roadmap: the project knows its retrieval layer is the soft spot, has diagnosed why, and has a concrete migration that addresses it.

Caveats & reproduction

The methodology holds up here (z-scored against ground truth, not raw averages), but the model roster is smaller and shakier than the raw counts imply, and each grader is only as good as the wording patterns it was tuned on.

Build notes & method

The z-score discipline here is the same one that keeps the layer-surgery lab's leaderboards honest — refuse the raw average, normalize the mix, log the config. Same reflex, different substrate: see also the quant benchmark and duologue. Built and run locally on a 16 GB Mac Mini M4 (llama.cpp / LM Studio for inference, Node for the pipeline, SQLite for the dataset, Mem0 now → DynamoDB+Lambda planned); harness in scoring/ + tools/, dataset in dataset.db. The eval/scoring path runs without the upload path.