Skip to content
91 changes: 91 additions & 0 deletions benchmarks/ANALYSIS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# ANALYSIS.md — Phase 4 (learn from the frozen baseline)

**Baseline run:** `gyst-baseline` (LongMemEval-S, 500 questions). Judge + answerer **sonnet-4.5**. Vector OFF (sqlite-vec not installed). Dataset sha256 `d6f21ea9…c3a442`.
**Tool:** `src/analysis/analyze-run.ts` (deterministic classification from each evaluation's `searchResults`).

---

## 1. Headline (the only number that travels: end-to-end answer accuracy)

| Metric | Value |
|---|---|
| **Accuracy** | **13.8% (69/500)** |
| Avg context tokens / q | 280 |
| Mean search latency | 26 ms |
| Mean answer latency | 5,220 ms |

> Reminder (guardrail #1): this is **answer accuracy** under a sonnet-4.5 judge. It is NOT a retrieval metric and must never be reported as one. Because the judge is sonnet-4.5 (not gpt-4o, due to key availability), this sits beside supermemory's **sonnet-4-class** numbers, not their gpt-4o headline. The gpt-4o-matched run is a documented TODO.

## 2. Category ranking (worst → best)

| Rank | Category | Correct/Total | Accuracy |
|---|---|---|---|
| 1 (worst) | single-session-assistant | 1/56 | **1.8%** |
| 2 | temporal-reasoning | 8/133 | **6.0%** |
| 3 | multi-session | 9/133 | 6.8% |
| 4 | knowledge-update | 14/78 | 17.9% |
| 5 | single-session-user | 21/70 | 30.0% |
| 6 (best) | single-session-preference | 16/30 | 53.3% |

The gradient is exactly what the mechanism analysis predicts: single-session **preference/user** (one chunk, salient keywords) survive; everything requiring **cross-session** gathering, **temporal** reasoning, or **oblique reference** to a past assistant turn collapses.

## 3. Retrieval-miss vs answer-miss split (decides where the fix lives)

A failure is a **retrieval-miss** if `searchResults` was empty (the answer model got nothing), an **answer-miss** if context was returned but the answer was still wrong.

| Scope | Retrieval-miss | Answer-miss |
|---|---|---|
| **Overall (431 incorrect)** | **414 (96.1%)** | 17 (3.9%) |
| single-session-assistant (55) | 55 (100%) | 0 |
| temporal-reasoning (125) | 123 (98.4%) | 2 |

**Conclusion: the loss is almost entirely retrieval.** 96% of failures never showed the answer model any context. Only 3.9% of failures are "context was there, model blew it" — and those are out of Gyst's control (answer-stage). **Fixing Gyst's retrieval is both necessary and nearly sufficient.**

## 4. Retrieval ceiling (upper bound — what's even worth chasing)

If every retrieval-miss were converted to a correct answer:

| Scenario | Ceiling |
|---|---|
| baseline | 13.8% |
| 50% of retrieval-misses recovered | **55.2%** |
| 100% of retrieval-misses recovered | **96.6%** |

The retrieval ceiling is **96.6%** — i.e., retrieval, not the answer model, is the entire game here. Even recovering *half* the empty retrievals roughly **quadruples** accuracy. This is an optimistic upper bound (a recovered retrieval doesn't guarantee a correct answer), but it sizes the prize and justifies spending all Phase-5 effort on retrieval.

## 5. Why retrieval returns empty — ranked, mechanism-grounded hypotheses

(From the parallel mechanism investigation; each tied to a real Gyst code path. Full evidence in WORKLOG / agent reports.)

| # | Hypothesis | Mechanism & evidence | Targets | Expected leverage |
|---|---|---|---|---|
| **H1** | **Enable embeddings** (install sqlite-vec, backfill, GYST_SQLITE_PATH) | Vector OFF: `entry_vectors` virtual table never created without sqlite-vec (`embeddings.ts:142`), so `searchByVector→[]`. Gyst's own `decisions/006` measured enabling it: complete-misses **6/50→0/50**, Recall@5 0.81→0.98. | the whole empty-retrieval class (vocabulary mismatch) | **Highest.** Semantic match recovers "vintage cameras" ↔ "old film cameras" where BM25-AND fails. |
| **H2** | **OR-mode BM25 fallback** when AND yields 0 | `expandQuery` joins terms with spaces → FTS5 **implicit-AND** (`query-expansion.ts:138`). "What is the name of my dog?" → `what AND name AND my AND dog` → 0. No OR fallback exists. ~30 LOC. | natural-language questions where some terms are absent | High, low-risk; complements H1 if embeddings can't be installed. |
| **H3** | **Strip question-words + pronouns** before MATCH | `FTS5_PROBLEM_WORDS` removes "is/the/of" but keeps "what/which/my" (`query-expansion.ts:30`), inflating the AND burden. ~10 LOC. | reduces AND-miss rate broadly | Medium-high, ~10 LOC, low risk. |
| **H4** | **Conversational entity extraction** (noun-phrase / proper-noun) | `extractEntities` is camelCase/`function`/`def`-only (`entities.ts`); conversational text → **0 entity tags → 0 graph edges** → `searchByGraph` dead (multi-session 0%). | multi-session, cross-session linking | Medium; larger change, enables graph + auto-linking. |
| **H5** | **Broaden temporal trigger** to comparative/ordinal phrasing | `parseTimeReference` needs explicit "yesterday/last week" (`temporal.ts:62`); "which did I start **first**" → `null` → `[]`. Temporal is a re-ranker, can't rescue empty BM25. | temporal-reasoning | Medium for one category; risk of over-trigger. |

**Ruled out:** the **0.15 confidence floor** is NOT a cause — new entries are seeded at 0.5 (`ingest`/`learn` confidence=0.5), comfortably above the floor. Empty results are genuine "nothing matched", not over-filtering.

## 6. Phase-5 plan (one change at a time, measure each against the frozen 13.8%)

Sequencing matters because H1 and H2 attack the **same** failure class — running both at once would make the delta unattributable.

1. **H2 first** (OR-mode BM25 fallback) — cheapest, pure-local, no system deps; isolates "how much does relaxing AND alone buy?"
2. **H3** (stopword expansion) — stack on H2, tiny.
3. **H1** (enable embeddings) — the big lever; measure on top, and *also* in isolation vs baseline to attribute cleanly.
4. **H4 / H5** — category-targeted, only if budget remains; watch for cross-category regressions (always read the whole profile).

**Overfitting guard (committed now):** hold out a random slice of question-ids that I will NOT inspect failures on; after the final change, run it once. If the gain doesn't replicate there, we overfit. (Implementation: the harness supports `-l`/sampling; I'll reserve a fixed id set.)

**Note on the 3.9% answer-misses:** 17 questions where context was returned but the answer was wrong — these are answer-stage, outside Gyst's retrieval. Not a Phase-5 target; reported for honesty.

---

## Exit gate 4 checklist
- [x] All six categories broken out and ranked worst→best (§2)
- [x] Weakest-two categories: retrieval-vs-answer split (§3)
- [x] Ranked hypotheses, each tied to a real Gyst mechanism (§5)
- [x] Retrieval ceiling estimate (§4)

**STOP — awaiting your review before Phase 5 (Iterate).**
53 changes: 53 additions & 0 deletions benchmarks/IMPROVEMENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# IMPROVEMENTS.md — Phase 5 changelog

Each change is measured on the **same 100-question subset** (`gyst-base100` reference = **15.0%**), one change at a time, against a frozen reference. Full-500 re-runs only for changes with a real subset gain. Judge + answerer = sonnet-4.5. Vector OFF unless noted.

**Headline rule:** accuracy is reported with **avg context tokens** and latency — never alone. A recall win bought with a large token cost is a *move along the cost/quality curve*, not a free lunch.

| # | Hypothesis | Diff summary | Subset acc (base→after) | Target category | Avg tokens (base→after) | Decision |
|---|---|---|---|---|---|---|
| baseline | — | unchanged Gyst, vector OFF | 15.0% | — | 293 | reference |
| **H2** | OR-mode BM25 fallback | `search.ts`: when implicit-AND returns 0 rows, retry once with terms OR-joined (plain-term queries only) | **15.0% → 65.0% (+50.0)** | all (empty-retrieval class) | 293 → **8,695** | **KEEP** ✅ (huge accuracy gain; large token cost noted) |

## H2 detail
- **Empty-retrieval rate: 89% → 1%** (leading indicator) — OR-fallback returns candidates for nearly every natural-language query.
- **Per-category (base→H2):** ss-assistant 0/9→9/9, ss-user 3/13→12/13, temporal 2/29→16/29, knowledge-update 4/17→13/17, multi-session 2/24→9/24, ss-preference 4/8→6/8.
- **Cost:** avg context tokens 293→8,695 (~30×). OR-mode + returning ~10 full session chunks per query. Genuine recall, but precision/token cost is the tradeoff — motivates H1 (semantic ranking) to deliver the same recall with fewer, better chunks.
- **Tests:** TDD RED→GREEN (`tests/store/search-or-fallback.test.ts`, 3 tests); `tests/store/` regression 203/203 pass.
- **Faithfulness:** change is inside Gyst's real `searchByBM25`, so both the production `recall` tool and the benchmark adapter use it — not an adapter-only trick.
- **TODO:** full-500 re-run to confirm at scale; consider a token-cost mitigation (fewer/shorter chunks or better ranking) once H1 lands.

## ⛔ BLOCKER (2026-06-26): Anthropic API usage limit reached
- Stored error from the haiku reference run: *"You have reached your specified API usage limits. You will regain access on 2026-07-01 at 00:00 UTC."*
- Cause: the key hit its spend cap. H2's ~30× context blowup (8,695 tok/q) accelerated it. ALL calls now fail regardless of model (haiku reference failed 20/20 on the first batch).
- Impact: **no benchmark runs (answer/judge) possible until the limit resets (Jul 1) or is raised / a different key is supplied.** Local code + tests are unaffected.

## Model regime
- Iteration loop switched to **haiku-4.5** (cost). New reference run `gyst-ref-haiku-100` was launched but DIED on the API limit before producing a report → no haiku reference yet.
- Final headline stays **sonnet-4.5** (+ opus-4.5 robustness).

| H3 | strip question-words/pronouns/auxiliaries before MATCH | `query-expansion.ts`: add ~40 function words to FTS5_PROBLEM_WORDS (aligns code with module doc) | folded into haiku anchor (below) | precision / token cost | — | KEEP (207/207 tests) |

### Haiku regime (cost-saving loop; sonnet reserved for final headline)
| Config (haiku, 100q subset) | Accuracy | Avg tokens | Search latency | Note |
|---|---|---|---|---|
| H2+H3 (vector off) — **anchor** | 55% (55/100) | 8,384 | 30 ms | headline config |
| H1 (vector on, sqlite-vec) | 60% (60/100) | 9,785 | 1,316 ms | **variant** |

**H1 (embeddings) verdict — VARIANT, not headline.** +5 pts overall (within 100q noise), but the *direction* is clear: helps semantic/cross-session categories (multi-session 8→13, knowledge-update 10→13), slightly hurts exact-recall (ss-user 12→10, ss-assistant 9→8). Costs: search latency 30ms→1,316ms (per-query ONNX embedding) and tokens +1,400. Enabled via a Bun `--preload` that runs Gyst's custom-SQLite probe before the harness opens a DB (fixes canLoadExtensions=false), plus `initVectorStore` per container. Verified 68/69 entry_vectors written. Kept as a reported variant; **headline stays H2+H3** (clean, ~30ms search).

### Overfitting guard (held-out, never inspected)
Ran the headline config (H2+H3, vector off, haiku) on questions **400–500** — a slice 100% disjoint from the first-100 tuning set, whose failures I never inspected.

| H2+H3 (haiku) | Accuracy | Avg tokens |
|---|---|---|
| first-100 (tuning set) | 55% | 8,384 |
| **held-out 400–500 (unseen)** | **68%** | 8,680 |

Generalization gap **+13 pts in the favorable direction** (held-out ≥ tuning). No overfitting — the H2/H3 mechanism transfers to unseen questions. (The first-100 simply had a harder category mix.)

## Pending (all benchmark-gated on API access)
- Measure H3 delta (haiku) once API returns; re-run haiku reference first.
- H1 — enable embeddings (install sqlite-vec, backfill). **Local setup can be done offline now**; only the accuracy delta needs API.
- Full-500 confirmation of H2(+H3) on sonnet-4.5.
- Held-out slice (never inspected) — run once after the final change (overfitting guard).
118 changes: 118 additions & 0 deletions benchmarks/METHODOLOGY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Gyst on MemoryBench / LongMemEval — Methodology

**Date:** 2026-06-26
**What this measures:** Gyst's **retrieval substrate** (the hybrid BM25 + graph + temporal + file-path engine, fused by RRF, with an optional sqlite-vec semantic strategy) on **conversational** question-answering, scored end-to-end by an LLM judge. It does **not** measure ghost knowledge or code mining — those do not fire on conversational data and are validated separately (CodeMemBench). See §"Honest framing".

This number is **answer accuracy**, not a retrieval metric (Hit@k / MRR). The two are never reported on the same axis.

---

## Harness & dataset

- **Harness:** [supermemoryai/memorybench](https://github.com/supermemoryai/memorybench) @ commit `118209a746d97d0d85e5a7234267f0b6962857e9`.
- **Benchmark:** LongMemEval — **S (small), cleaned** variant.
- Source: `https://huggingface.co/datasets/xiaowu0162/longmemeval-cleaned/resolve/main/longmemeval_s_cleaned.json`
- **sha256:** `d6f21ea9d60a0d56f34a05b609c79c88a451d2ae03597821ea3d5a9678c3a442`
- size 277,383,467 bytes; 500 questions; 6 categories (single-session-user/-assistant/-preference, multi-session, temporal-reasoning, knowledge-update).
- **Pipeline (run every time):** ingest → index → search → answer → evaluate → report.

## Models (part of the eval, external for every provider)

- **Answering model + Judge (primary):** `claude-sonnet-4-5-20250929` (sonnet-4.5).
- The brief targets gpt-4o to match supermemory's published headline; no OpenAI key was available, so a Claude judge/answerer was used. **Consequence:** these numbers sit beside supermemory's *Claude-judged* runs, not their gpt-4o headline. A gpt-4o-matched run is an open TODO.
- Note: `claude-sonnet-4-20250514` (the `sonnet-4` alias) was retired/inaccessible for the key used; `sonnet-4.5` was substituted.
- **Iteration loop:** `claude-haiku-4-5-20251001` (haiku-4.5), ~3× cheaper, used to measure deltas; the final/headline numbers use sonnet-4.5.
- **Judge prompts:** harness defaults, **unmodified** (per-category: default / abstention / temporal / knowledge-update / preference). No prompt was tuned.
- **Answer prompt:** harness default (`buildDefaultAnswerPrompt`). The GystProvider supplies **no** custom `prompts` hook, for an apples-to-apples number.

## Gyst — fully local

- Ingest and search make **zero outbound network calls** (the embedding model is a one-time local download, then cached). Only the answer + judge models are external — as they are for every provider.
- **Embeddings model (when enabled):** `Xenova/all-MiniLM-L6-v2`, 384-dim, via `@huggingface/transformers` + `sqlite-vec` `vec0` (L2 distance). (Note: Gyst's prose elsewhere says `bge-small-en-v1.5`; the code uses all-MiniLM-L6-v2 — the latter is what ran.)

## Adapter (GystProvider)

- Lives in the memorybench fork at `src/providers/gyst/` (provider commit `85582c8`). Thin: it **composes Gyst's real exported functions** (`searchByBM25`, `reciprocalRankFusion`, `persistEntry`, `fetchEntriesByIds`, `searchByVector`, …) — it does not reimplement retrieval.
- **Isolation:** one SQLite file per harness `containerTag` (= one LongMemEval question). `clear` deletes the file.
- **Ingest:** each conversation session → one or more `learning` entries (chunked ≤5000 chars), timestamped at the session date so the temporal strategy has a real recency signal. No LLM extraction (stays offline).
- **Embeddings (variant):** enabled with a Bun `--preload` hook that runs Gyst's custom-SQLite probe before the harness opens any DB, plus `initVectorStore` per container.

## Gyst commits under test

- Baseline: `1ab6367` (pre-changes).
- **H2** OR-mode BM25 fallback: `0ee010d`.
- **H3** strip question-words/pronouns/auxiliaries: `e512699`.
- Confidence floor, RRF k=60, and all other parameters: unchanged.

## Hardware

- Apple Silicon (Darwin arm64), Bun 1.3.12. Embeddings ran on CPU (ONNX fp32).

---

## Exact commands

Baseline (unchanged Gyst, vector off):
```
bun run src/index.ts run -p gyst -b longmemeval -j sonnet-4.5 -m sonnet-4.5 -r gyst-baseline
```
Subset iteration (haiku loop):
```
bun run src/index.ts run -p gyst -b longmemeval -j haiku-4.5 -m haiku-4.5 -l 100 -r <id>
```
Embeddings variant (H1):
```
GYST_SQLITE_PATH=/opt/homebrew/opt/sqlite/lib/libsqlite3.dylib \
bun --preload ./src/providers/gyst/preload-sqlite.ts src/index.ts run \
-p gyst -b longmemeval -j haiku-4.5 -m haiku-4.5 -l 100 -r gyst-h1-haiku-100
```
Held-out guard (never-inspected slice):
```
bun run src/index.ts run -p gyst -b longmemeval -j haiku-4.5 -m haiku-4.5 --offset 400 -l 100 -r gyst-heldout-haiku
```

---

## Results

### Primary lever — H2 (OR-mode BM25 fallback), sonnet-4.5, same 100 questions
| | Accuracy | Empty-retrieval | Avg ctx tokens |
|---|---|---|---|
| Baseline (vector off) | 15.0% (15/100) | 89% | 293 |
| **+H2** | **65.0% (65/100)** | 1% | 8,695 |

Full-500 baseline (sonnet-4.5, vector off): **13.8% (69/500)** — corroborates the subset baseline. The H2 win is bought with a large context-token increase (OR-mode returns ~10 session chunks); reported, not hidden.

### Overfitting guard (held-out, never inspected) — H2+H3, haiku
| | Accuracy |
|---|---|
| first-100 (tuning set) | 55% |
| **held-out Q400–500 (unseen)** | **68%** |

Held-out ≥ tuning ⇒ the gain generalizes; no overfitting.

### Embeddings variant — H1, haiku
| | Accuracy | Avg tokens | Search latency |
|---|---|---|---|
| H2+H3 (vector off) | 55% | 8,384 | 30 ms |
| H1 (vector on) | 60% | 9,785 | 1,316 ms |

H1 helps semantic/cross-session categories (multi-session, knowledge-update) at a real latency cost; kept as a **variant**, not the headline.

### Retrieval-vs-answer split (full-500 baseline)
96.1% of failures were **retrieval-misses** (empty context); only 3.9% answer-misses. Retrieval ceiling ≈ 96.6%.

---

## Honest framing / limitations

- This is **answer accuracy** under a **sonnet-4.5** judge, on **conversational** data. Not comparable to supermemory's gpt-4o headline; comparable to Claude-judged runs.
- It measures Gyst's **retrieval substrate**, not ghost knowledge or code mining (out of scope here; validated on CodeMemBench).
- **No full-500 sonnet "after" run** was completed (API budget). The headline after-number is a **100-question sonnet subset** (15%→65%) with a haiku held-out generalization check (55%→68%); the full-500 sonnet confirmation is a documented TODO.
- A **second-judge robustness run** (e.g. opus-4.5) is a TODO (budget).
- Per-category counts on the 100q subset are small (single-digit per category) — treat category-level deltas as directional.

## Reproduce
1. Clone memorybench @ the commit above; `bun install`; add `ANTHROPIC_API_KEY` to `.env.local`.
2. Add the GystProvider (provider commit `85582c8`) and point it at a Gyst checkout at the commit you're testing.
3. Run the commands above. Reports land in `data/runs/<id>/report.json`; slimmed summaries for these runs are in `./reports/`.
Loading
Loading