English | 简体中文
Last updated: 2026-10-03 · v4.0 — plain-language intro, results charts, aggregation protocol, dream (idle-time consolidation) now runs silently on a schedule
A long-term memory layer for personal AI agents: hybrid retrieval (dense + lexical → RRF, optional cross-encoder) + a memory state machine (supersede lineage, evidence-linked consolidation, power-law decay, rehearsal, directed forgetting) + model-driven judgment — the server maintains state, the calling model decides.
FlyMemory gives a coding agent a persistent, self-managed memory: every user message is captured by a hook, stored as per-sentence chunks, deduplicated, and recalled into later conversations with age and provenance stamps. Stale facts are not deleted — the calling model marks them superseded, so history stays queryable while current state stays clean.
What problem does it solve? AI assistants forget everything between conversations. FlyMemory gives one a notebook that survives: every message you send is automatically noted down, and later conversations get the relevant notes injected back — with a timestamp and where they came from.
How does it work, in one paragraph? Think of a well-kept paper notebook. Every sentence you say gets written on its own line (so topics don't blur together). If you say something almost identical to an existing line, the notebook just underlines the old one harder instead of copying it twice. If you correct something ("actually I moved to Hangzhou"), the old note is crossed out — but not torn out, you can still peek at crossed-out notes when you ask about history. Every night-ish, short scribbles get condensed into clean summary notes, and anything untouched for months slowly fades. Finding notes works two ways at once: by meaning (even if the wording differs) and by exact keywords (part numbers, file paths, IDs).
Who does the thinking? The notebook itself never thinks. A small
mechanical program captures and fetches notes; your AI (the LLM you
already use) makes every judgment call — what's important, what's
outdated, what can be forgotten — through a small set of tools
(flymemory_remember, flymemory_supersede, flymemory_forget, ...).
This split — dumb, reliable storage; smart, accountable model — is
the design's core bet, and every benchmark below exists to test it.
Do I need the research sections? No. Everything up to Install is enough to use it. The rest is the measured evidence: benchmarks, ablations, and a long list of things we tried that did not work (kept on purpose — negative results are part of the record).
Reader's map — I want to use it → Install. I want to know if it
works → Results at a glance and the benchmark sections.
I want to know what was tried and failed → every section marked "measured
negative". I want the research laws → the law ledger at
research/RESEARCH.md.
Note on the name: v1 of this project was a pure Hopfield associative memory inspired by the Drosophila mushroom body (kept below as v1/v2). In v3 the production recall path is semantic + lexical retrieval; the Hopfield matrix survives only as an experimental associative-expansion layer, and measurements (see Benchmarks) currently argue against enabling it.
Every number below is one-command reproducible and carries its artifact path in the corresponding section. Four charts summarize the measured story; the negative results are as load-bearing as the positive ones.
Retrieval on the 9,729-entry LongMemEval-S oracle store. Hybrid fusion (RRF) and the cross-encoder each buy real points; the lexical channel is what carries part numbers and IDs that embeddings cannot see.
Left: consolidation must overlay, never replace (L1) — replace-style abstraction is 10 points WORSE than storing nothing. Right: the anchoring law (L6) — the same 48 turns through two collection strategies; anchored judgment is near-mechanical (pre-registered paired experiment, discordant 13:0, McNemar p ~ 2e-4).
Can the calling model actually OPERATE the state machine? 168-case judgment benchmark, execution-aware scoring. The balanced block measures response bias: real changes are never missed (12/12), unconfirmed intents occasionally mutate (9/12) — the bias is over-eager, not conservative.
Storage policy on the production store: whatever eviction frees it perturbs. High-coverage entries are both the redundant region (easy to recover: orange) and the retrieval hubs (rank stability: blue). LRU wins the pair; the redundancy dividend was already collected by upstream dedup.
user message
↓ UserPromptSubmit hook (mechanical: capture + recall + store)
chunked store: one block per sentence (multi-topic messages stay separable)
↓
semantic dedup, length-tiered thresholds
> 0.95 (short) / 0.92 (long) → strengthen existing entry
> 0.85 (short) / 0.75 (long) → merge (longer text wins; V4: old text
becomes a superseded TOMBSTONE with
valid_from/valid_to, current gets
updated_at)
else → new entry (source=hook)
↓
model precision-store: the calling agent stores conclusions it judges
important via flymemory_remember (source=model; optional state_key/
state_value -- I1 active-unique: the same key auto-supersedes older states)
recall (per query):
query-type ROUTING (V4): state / history / temporal / aggregation /
lookup -- history questions auto-include superseded entries; state
questions answerable via state_lookup(key) without competing in top-k
dense ranking: multilingual embedding cosine, max over query chunks
+ lexical ranking: IDF boost (part numbers, paths, IDs — invisible to embeddings)
↓ RRF fusion (k=60, pool 200) → candidate ORDER
- superseded entries excluded (include_superseded=True for history queries)
optional: cross-encoder rerank of the fused top-10 (enable_rerank=True)
decay R(t) = (1 + t/τ)^-0.5 is a MAINTENANCE signal (drives cleanup and
rehearsal), not a ranking feature — ordering by it was measured to lose to
plain RRF (see the LongMemEval and state-aware sections below).
idle-time consolidation ("dreaming", hourly task):
RECENT window (90 min) → LLM distill → per-entry faithfulness audit
→ overlay via flymemory_remember (compartment-scoped supported)
-- see dream.py and v4-rfc.md §9
flowchart TD
A["user message"] -->|"UserPromptSubmit hook<br/>(mechanical, ~8 s budget)"| B["split_chunks<br/>one block per sentence"]
B --> C{"semantic dedup<br/>length-tiered thresholds"}
C -->|"cos ≥ 0.95 / 0.92"| D["STRENGTHEN<br/>existing entry, access_count++"]
C -->|"cos ≥ 0.85 / 0.75"| E["MERGE<br/>longer text wins in place<br/>old text → superseded TOMBSTONE"]
C -->|"below"| F["NEW entry<br/>source=hook"]
G["calling model"] -->|"flymemory_remember<br/>(state_key → I1 auto-supersede)"| H["NEW entry, source=model"]
D --> K[("flymemory_v3.pkl<br/>one file, no database")]
E --> K
F --> K
H --> K
flowchart TD
Q["query"] --> R{"classify_query<br/>state / history / temporal /<br/>aggregation / lookup"}
R -->|"history"| S1["include_superseded = True"]
R -->|"state"| S2["state_lookup(key)<br/>no top-k competition"]
R -->|"others"| S3["default top-k"]
S3 --> T["dense: multilingual cosine<br/>max over query chunks"]
S3 --> U["lexical: IDF boost<br/>(IDs, paths, part numbers)"]
T --> V["RRF fusion (k=60, pool 200)"]
U --> V
V --> W["drop superseded unless history"]
W --> X["optional cross-encoder<br/>rerank of fused top-10"]
X --> Y["[#id · state · age · source · decay]<br/>hook-injected into context"]
stateDiagram-v2
[*] --> active: remember (hook or model)
active --> active: strengthen / merge-in-place (updated_at set)
active --> superseded: supersede(old, new)<br/>or I1 same-state-key write
superseded --> superseded: recoverable via include_superseded
superseded --> [*]: forget (evidence links cleaned too)
active --> [*]: forget (wrong fact) / decay_cleanup (faded)
note right of superseded
tombstones keep valid_from / valid_to
history stays queryable; current state stays clean
end note
The pipeline has one hard rule: the server never runs an LLM. Every semantic decision — what to consolidate, whether a fact is outdated, what is worth forgetting — belongs to the calling model, which sees ids, ages and provenance and drives the state machine through MCP tools. The server owns everything mechanical: chunking, dedup, fusion, decay math, lineage hygiene (evidence links cleaned on forget and on decay cleanup), and persistence.
Decay is a power law, not an exponential half-life: R(τ) ≈ 0.707 and the true half-life is 3τ (90 days at the default τ = 30 days). The heavy tail is intentional — old memories fade slowly instead of vanishing.
Why semantic consolidation is not threshold-based: on a multilingual
embedder, a true paraphrase pair can score 0.62 while a same-structure pair
differing only in a detail ("meeting at 15:00 tomorrow" vs "today") scores
0.93. No threshold can merge the first without merging the second, so
near-identical dedup is automatic but meaning-level consolidation is left to
the calling model via flymemory_supersede(old_id, new_id) — judgment where
the judgment capability lives.
The server runs as a persistent stateless streamable-HTTP MCP service, so restarting it never breaks connected sessions:
python flymemory/mcp_v3.py --http # serves http://127.0.0.1:8765/mcpMCP registration (ZCode ~/.zcode/cli/config.json, adjust paths):
{
"mcp": {
"servers": {
"flymemory": {
"type": "http",
"url": "http://127.0.0.1:8765/mcp",
"timeoutMs": 120000
}
}
}
}A UserPromptSubmit hook forwards every user message to the server and
injects the recall results back into the conversation — capture does not
depend on the model remembering to call a tool:
{
"hooks": {
"enabled": true,
"events": {
"UserPromptSubmit": [
{ "hooks": [ { "type": "process",
"command": "C:/path/to/python.exe",
"args": ["D:/path/to/flymemory/flymemory/hook_auto.py"],
"timeoutMs": 10000 } ] }
]
}
}
}flymemory/flymemory_supervisor.pyw keeps the server alive (restart on crash,
idle when healthy, never exits); run it headless at login via a Startup
shortcut or a scheduled task.
bench_longmemeval.py adapts the LongMemEval oracle edition (500 questions,
940 evidence-haystack sessions, 10,866 turns, 2021-2024) to FlyMemory: all
sessions ingested at turn granularity into one shared memory, questions
answered by recall, scored by evidence-session hit@3 (retrieval-level metric;
answer generation + LLM judging not included).
| policy | evidence-hit@3 |
|---|---|
| RRF fusion — production recall | 339/500 = 67.8% |
| RRF + cross-encoder rerank (optional) | 360/500 = 72.0% |
| BM25-only (IDF lexical) | 314/500 = 63% |
| legacy full scoring (sim × decay × source ordering — retired, see below) | 303/500 = 61% |
| recency-only (newest turns) | 1/500 = 0% (dates span 3 years — recency is uninformative here) |
The bold rows are the current production path (recall() = RRF fusion of the
dense and lexical rankings; rerank is opt-in). The "legacy full" row is the
original eff-ordering pipeline this benchmark first measured, kept for
continuity — ordering by decay × source was later measured to lose to plain
RRF (see "Where do decay and source weights act?" below), so it is no longer
the shipped ranking.
Per ability (legacy path, kept as the per-type breakdown): knowledge-update 83% (the supersede/lineage strong suit), single-session-assistant 98%, multi-session 56%, single-session-user 54%, temporal-reasoning 44%, single-session-preference 40%.
Honest reading, updated: on the retired eff-ordering path BM25-only statistically tied the pipeline (63% vs 61%) — exact-token overlap carried most retrieval weight on chit-chat style sessions. The shipped RRF fusion fixes that: 67.8% vs BM25's 63% (+4.8pp), and cross-encoder rerank adds another +4.2pp on top. The multilingual embedder still earns less on English casual text than on Chinese technical content, but the hybrid no longer depends on that gap. n=500, retrieval-only, no LLM layer: indicative, not comparable to published end-to-end LongMemEval scores (which include an answering LLM).
bench_rerank.py scores the fused RRF top-10 with a cross-encoder
(ms-marco-MiniLM-L-6-v2) and returns the top-3 after reranking. On the
oracle edition above (500 questions, 9,729 entries):
| policy | evidence-hit@3 |
|---|---|
| RRF fusion top-3 (no rerank) | 338/500 = 67.6% |
| cross-encoder rerank of RRF top-10 | 360/500 = 72.0% |
| oracle: answer anywhere in RRF top-10 | 397/500 = 79.4% |
Rerank converts about a third of the top-10 ceiling into top-3 hits (+4.4pp); it can also drop an answer that raw RRF had surfaced — the oracle row shows what a perfect pool-level fix would be worth. Enable per library:
mem = SmartMemory(enable_rerank=True, rerank_pool=10)Off by default: one CE pass costs ~0.3 s CPU per recall at pool 10. The reranker loads lazily and is never persisted. Superseded entries are excluded before pooling so a dead entry cannot burn a rerank slot.
The gain transfers to production scale. bench_rerank_full.py on the
S-edition library (500 questions, 199,509 turn-granularity entries):
| policy | evidence-hit@3 |
|---|---|
| dense (max-over-chunks cosine) | 137/500 = 27.4% |
| full production scoring (sims × decay × source) | 145/500 = 29.0% |
| BM25-only (IDF lexical) | 168/500 = 33.6% |
| RRF fusion top-3 | 204/500 = 40.8% |
| cross-encoder rerank of RRF top-10 | 229/500 = 45.8% |
| oracle: answer anywhere in RRF top-10 | 277/500 = 55.4% |
Same +5.0pp from rerank, same ~1/3 ceiling capture — the effect is stable
across a 20× library-size change. Two forensics notes from this run: an
earlier "production scoring = 0/500" row was a bench artifact (the
S-edition date parser silently returned None for every session, NaN-ing all
decay weights; fixed in parse_lme_date + repair_lme_s_timestamps.py), and
bench_lex_ab.py shows the lexical channel's ratio normalization vs
cumulative IDF is a wash inside RRF (339 = 339/500) — _lex_scores needs
no change. Thread-cap note: on a host running a full-core training job,
torch's default thread count livelocks the CE forward (220s+ vs 0.3s
capped to 1-4 threads); benches and the reranker itself cap threads.
recall() ranks by RRF over the raw dense and lexical channels; the decay ×
source × eff score is an auxiliary column, not the ordering key. Reranking
the fused pool by eff makes things worse — bench_state_oracle.py
(oracle edition, 500 questions):
| policy | evidence-hit@3 |
|---|---|
| RRF top-3 (production order) | 339/500 = 67.8% |
| RRF top-10 reranked by eff | 335/500 = 67.0% |
| RRF top-20 reranked by eff | 329/500 = 65.8% |
The eff score is a maintenance signal (what survives decay cleanup, what
the rehearsal refresh touches), not a ranking signal: applying it to an
already-fused candidate pool trades retrieval quality for recency bias. The
same eff ordering run over the WHOLE library is the "full" arm above — and it
loses to plain RRF by ~12pp at S scale. Production recall therefore keeps
RRF order, and bench_rerank_full.py carries the state10/state20 arms as a
standing regression guard for this decision. S-edition spot check (first
150 questions, run under a saturated host): rrf 21.3%, state10 22.0%,
state20 20.7% — within noise of plain RRF, same ordering by pool size.
Can the AI actually operate the notebook correctly — update what changed, stay silent when nothing did, forget only what it should? This section grades exactly that, as a test the model takes.
Retrieval metrics answer "can the evidence be found"; they say nothing about
whether an LLM placed in front of the state machine will operate it
correctly. bench_memory_judgment.py measures exactly that, with an offline
JSON protocol (the actor sees the memory snapshot + the new user message and
emits strict JSON; the harness executes and validates). The engine is frozen
while this benchmark exists. Mechanical validity (id existence, active
supersede target) is checked in code; unsupported inference in consolidation
conclusions goes to an LLM judge.
Dataset v1.5 (168 cases, five batches): 62 supersede (14 from the contradiction scenarios + 20 from the state-fidelity pairs + 28 fresh themes), 30 adversarial no-ops ("I fixed something on my old Windows VM" must NOT supersede the Fedora entry), 14 consolidation (summary requests over topic fragments; 2 with an outdated-distractor to test stale leakage), 8 forget (6 wrong-fact deletions + 2 traps where the right action is supersede, never forget), and batch 4 adding four NEW capability dimensions (5 each): multi-update (two prior states both need superseding), reversal (value reverts -- must create a new entry, never resurrect the superseded one), temporary state (a time-boxed fact is new info, not an update), and partial correction (one field of a compound entry changes). Generators: gen_dataset_batch2.py / batch3.py / batch4.py.
Batch 5 adds balanced stale/new detection (12 + 12 cases) borrowed from intuition-mechanism P32-h and the STALE benchmark's Premise Resistance axis: in the STALE half the fact really changed (correct = supersede onto the new value); in the RESIST half the fact is UNCHANGED and the user's message merely embeds a stale premise or an unconfirmed intent to switch ("I'm thinking of going back to Notion"). The balanced design measures response bias -- over-conservative (missing real changes) vs over-eager (mutating on talk) -- which single-sided P/R cannot see.
First results, 2026-09-24 (oracle = gold replay, harness sanity check, all
1.0 with zero mechanical errors; actor = deepseek-chat, temperature 0;
supersede/forget scores are execution-aware — an operation that was decided
but failed to execute leaves the state unchanged and cannot count as a hit):
| metric | value |
|---|---|
| supersede precision / recall | 1.00 / 1.00 (n=77, incl. multi-update/reversal/partial-correction dimensions) |
| forget precision / recall | 1.00 / 1.00 (n=8) |
| unnecessary mutation rate | 0/36 (all adversarial no-ops held) |
| consolidation evidence exact-match | 8/14 |
| unsupported inference 0/14; stale leakage 1/14 (LLM judge, the known | |
| cons_08 case -- judge calibration: 2 distilled sessions manually verified | |
| faithful) |
Failure modes worth keeping: (1) one supersede was issued without the
required remember entry holding the new state — the state transition was
understood but the protocol contract was missed; (2) one consolidation
included an already-superseded detail (the old printer cartridge) — faithful
to the evidence, but stale leakage into a "current state" summary; the
unsupported-inference judge does not catch this, it is a separate axis.
Oracle vs autonomous shows zero judgment gap on supersede and forget at
this scale; the gap concentrates in consolidation timing and evidence
selection — the target for the next iteration.
Re-run at v1.5 (2026-09-28, 168 cases): supersede 0.99 / 1.00, unnecessary mutation 0/36 on the classic no-ops, and the batch-5 balanced block scores STALE 12/12 (a real change is never missed -- zero conservative bias) vs RESIST 9/12. The three RESIST failures split into two distinct behaviors: one genuine Premise-Resistance breach ("I'm switching back to VS Code" superseded the Zed entry outright -- an unconfirmed intent treated as a state change), and two boundary remembers that correctly left the state alone but stored the consideration itself as a new entry ("user is considering switching to pour-over") -- arguably true, but noise under a strict no-op gold. The bias direction is over-eager, not conservative (contrast: a doubao-based judge in the P32-h-style setup reported NEW only 7/40); it concentrates in intent-statements, and the model's own reasons show it has the considered-vs-confirmed distinction yet still writes 2/12 times.
Arm-S transplant (negative result, 2026-09-28): intuition-mechanism
P32-i showed its novelty deficit is an EXTRACTION failure -- supplying the
cue-extraction procedure took the blind judge from 20/40 to 39/40. A
two-step protocol (--two-step: mandatory assertion extraction tagged
fact/intent/question before deciding, ACTOR_SYSTEM_TWO) was transplanted
here as a pre-registered arm. Verdict: NOT SUPPORTED -- the flagged
intent-supersede (bal_resist_01) persisted because the model labels
"I'm switching back to X" as a fact, i.e. the failure is in the
classification discretion, not extraction; RESIST mutations went 3 -> 4
(bal_resist_08 fixed, bal_resist_11/12 newly written consideration entries
the protocol text itself permits), one classic no-op gained a mutation
(0/36 -> 1/36), one supersede recall slipped (sup_06), and consolidation
timing shifted on 3 cases. supersede R 1.00 -> 0.989 overall. The
transplant failure is itself informative: P32-i's procedure worked because
its sufficient statistic is objective (numeric comparisons against 6/20/70
plus sign-flip); this schema (fact/intent/question) supplies no comparable
mechanical sufficient statistic -- what counts as an update and what is
worth keeping are, so far, semantic discretion. That is a statement about
this one schema, not a proof that judgment cannot be decomposed into
objective sub-problems; hunting for such a decomposition (assertion
extraction / state-change detection / memory-worthiness, benchmarked
separately) is the next phase. Judgment really does live in the caller, and cannot be
restored by pipeline scaffolding. The flag is kept as a regression
sentinel, not adopted into the production prompt.
Atomic decomposition (bench_atomic_judgment.py, 2026-09-28): the
monolithic memory-policy prompt was split into three primitives benchmarked
separately, each with its own system prompt and gold labels:
| primitive | input -> output | score | confusion |
|---|---|---|---|
| A assertion extraction | turn -> kind (fact/intent/question) | 81.2% (26/32) | ALL errors bias toward fact (1/5 intent, 5/7 question) |
| B state-change detection | (entry, assertion) -> CHANGED/UNCHANGED/UNKNOWN | 100% (30/30) | none |
| C memory-worthiness | (assertion, context) -> KEEP/DISCARD/EPHEMERAL | 86.7% (26/30) | 4 one-off events -> EPHEMERAL (harmless) |
The decomposition localizes the end-to-end failures with unusual clarity: given an anchor (a concrete stored value to compare the assertion against), state-change detection is perfectly solvable -- the three "considering switching back" assertions that caused the Arm-S failures are all judged UNCHANGED here. The unsolvable-feeling part is A: classifying bare linguistic behavior (declaration vs consideration vs question) with no anchor, where every confusion flows toward fact. The round-9 review's framing holds: the bottleneck has narrowed from memory management to semantic boundary detection, and within that, from "no sufficient statistic" to "no anchor -- the missing ingredient is the thing to compare against". Engineering prescription to explore next: always ask the B-question per stored entry (anchored, mechanical-adjacent) instead of asking the model to free-classify the turn first.
The prescription was then tested as a pre-registered paired experiment
(P-ANCH-1, bench_anchor_pairing.py; criteria committed before running --
279e9aa): the SAME 48 turns under both conditions, one dependent variable
(does this message invalidate the stored state?).
| condition | input | acc |
|---|---|---|
| U unanchored (kind + naive rule) | turn alone | 34/48 = 70.8% (structural ceiling 75%) |
| A anchored | (stored entry, turn) | 47/48 = 97.9% |
U's errors landed exactly where predicted: all 12 noop facts-about-old-objects became false positives (structural), plus 2 resist; discordant pairs went 13:0 for anchoring (exact McNemar p ~ 2e-4). The single anchored miss is "I'm switching back to VS Code" -- the pre-declared declarative-intent residual, i.e. an annotation-boundary disagreement, not an anchoring failure. Pre-registered verdict: SUPPORTED-with-scope. L6 is upgraded from candidate to established (single-model scope) in the program doc.
P-PHASE-1 (bench_phase_boundary.py) then asked WHERE the anchored
advantage degrades, borrowing P47's phase-boundary method: the same
anchored verdict on a textual-delta ladder (digit-swap / reworded /
re-domain updates, plus near-collision distractors). Result: 36/36 --
flat 6/6 at every level, zero false positives even when the message is
topically CLOSER to a distractor than to the anchor. No phase boundary
in the tested range; the small-edit fragility of L4 (the engine's dedup
dropping 5/20 digit/date edits) does NOT reproduce at the judgment
layer -- the model reads value swaps perfectly when the question is
anchored and binary. L4 stays mechanism-layer; L6's anchor flattens the
gradient down to delta_emb ~ 0.2. Verdict per pre-registration:
P1 PARTIAL / P2 NEGATIVE (gap 0 < 15pp band; no FP concentration).
Phase 1.5 (real tool-calling, bench_memory_judgment_tools.py) repeats
the same 38 cases through the actual tool surface — the model must handle the
id flow itself (remember first, take the returned id, then supersede). Same
model, same data, execution-aware scoring: supersede 1.00 / 0.81,
forget 1.00 / 1.00, unnecessary mutation 0/10. The recall gap is 3 cases
where the model's remember of the new state was merged into the old
entry (sim > 0.75, longer text wins: the entry text is rewritten to the
new state in place) — the model then issued a redundant supersede
(old == new) that was correctly rejected. Replaying all three shows the
final store state is CORRECT in every case: the merge was the state
transition. Strict P/R therefore undercounts; the honest statement is that
2/16 supersedes needed the explicit supersede tool and 3/16 were absorbed by
merge semantics, and the model cannot distinguish "stored as new #N" from
"strengthened/merged into existing #N" from the tool's return text — the one
genuine protocol gap found at this layer.
Phase 2 (end-to-end, five arms, bench_e2e_answer.py): the same 30 state
cases, scored at the ANSWER level — apply a maintenance policy, retrieve,
deepseek-chat answers the question from the recalled entries, LLM judge
classifies the answer. The arms separate retrieval quality from state
maintenance:
| arm | retrieval | maintenance | current | stale | unknown (correct) |
|---|---|---|---|---|---|
| no memory | – | – | 0% | 0% | 28/30 (93%) |
| dense naive RAG | cosine top-3 | store-only | 26/30 = 87% | 4/30 = 13% | 0 |
| RRF naive (store-only) | production RRF | store-only | 25/30 = 83% | 5/30 = 17% | 0 |
| FlyMemory + oracle state | production RRF | gold ops | 26/30 = 87% | 0% | 4/30 = 13% |
| FlyMemory + autonomous state | production RRF | DeepSeek ops | 26/30 = 87% | 0% | 4/30 = 13% |
The 5 naive-RAG stales are exactly the deleted-wrong-fact cases (frt_01–04):
without forget, the assistant keeps confidently answering with facts the
user explicitly retracted. Autonomous matches the oracle ceiling exactly —
zero judgment gap end-to-end — and the stale contamination that motivates
the state machine disappears entirely under it. Judge calibration: the LLM
verdicts agree with a mechanical keyword check on all 150 arm-answer pairs
(150/150) -- the judge is neither stricter nor looser than the observable
evidence.
Note on the no-memory arm: the blind model is 3% current with a 10% stale answer rate and 20% outright wrong -- it confabulates where it should abstain. With a naive store it reaches 83-87% current but still leaks 17% stale; only the state-maintenance arms reach 0% stale while staying at the 87% ceiling.
Some questions need more than one lookup. This is what we measured when the assistant was allowed to search repeatedly with a time filter.
bench_toolanswer.py --tool2 (TOOL2): the answer model's search_memory
gains a time_range parameter ("YYYY-MM..YYYY-MM") and the system prompt
instructs scoping for time-bounded questions. Same 50 questions:
| answer mode | strict (n=50 / n=500) | weighted |
|---|---|---|
| single-round top-5 (three-layer) | 40.0% / 40.0% | 40.0% |
| TOOL1 agentic (free-form search) | 46.0% / 43.2%* | 48.6%* |
| TOOL2 agentic + time_range | 56.0% / 44.6% | 63.0% / 50.0% |
| TOOL3 + calculator (n=50) | 54.0% | 58.0% -- no gain, dropped |
(*TOOL1 was only run at n=50; at n=500 TOOL2 scores 44.6% strict / 50.0% weighted vs turn-only 37.0% / 39.6% and prose overlay 42.8% / 44.9% -- agentic time-scoped retrieval is the best configuration at both scales. By type at n=500: temporal-reasoning remains the weakest slice at 50/133 correct; multi-session 56/133 correct.)
The registered prediction (research/RESEARCH.md, TOOL2) confirmed with a large margin: temporal-reasoning wrongs dropped 67% and no other type regressed (+10pp strict overall). Time-scoped retrieval is a first-order lever and ships as the default tool shape. TOOL3 probe: adding a calculator tool on top gave no further gain (54.0%, within noise) -- with time-scoped search the residual bottleneck is evidence collection (reading across many turns), not arithmetic.
Cross-model check (external-review suggestion): the same 50 questions answered by a second model (GLM, via interactive session) score 30% strict / 31% weighted vs DeepSeek's 32% / 35% -- the ~50% answer-conversion ceiling over retrieved evidence is model-agnostic, confirming it is a task-structure limit (multi-turn aggregation), not a DeepSeek quirk. Grading script: bench_glm_answers.py.
bench_lme_e2e.py extends the protocol to the public LongMemEval-oracle
questions (all 500): recall top-5 → deepseek answers → judge vs gold.
Strict correct 37.0% (185/500), weighted with partials 39.6%. Full-scale
attribution: retrieval hit@5 = 73.2%; answers are 43% correct when evidence
was retrieved vs 22% when not (the 22% are questions answerable without the
specific evidence -- generic or inferable). Raising top-k to 10 changed
nothing on a 50-question sample. The conversion losses (43% not 100% after a
hit) concentrate on multi-turn aggregation questions (arithmetic over two
turns, counting across sessions) — turn-granularity retrieval cannot answer
them, which is the concrete argument for session-level consolidation
(below). Not comparable to official LongMemEval end-to-end scores: they feed
the full haystack (long-context setting), this is a memory-augmented top-k
setting.
When short notes get condensed into summaries, should the originals be thrown away? Measured answer: never throw them away — keep both. Replacing lost 10 accuracy points; keeping both gained 6.
bench_granularity.py distills each of the 940 evidence sessions into 1-3
DeepSeek-consolidated durable-fact entries and reruns the same 50 questions
under two ingestion policies:
| store | strict correct | weighted score |
|---|---|---|
| turn entries only (baseline, n=500) | 185/500 = 37.0% | 0.396 |
| consolidated entries only (replace, n=50) | 11/50 = 22% | 0.270 |
| turn + consolidated (overlay, n=500) | 214/500 = 42.8% | 0.449 |
Replacing raw turns with summaries loses the concrete details most questions ask about (22% -- worse than baseline). Overlaying the consolidated entries on the untouched turns gains +5.8pp strict at full scale (verified on all 500 questions, not just the 50-question sample; per-question verdict flips: 40 improved vs 11 regressed, net +29). The summaries act as retrieval entry points while the turns keep the details. This is the measurement behind the design rule "raw entries are never deleted -- consolidation adds abstraction without loss".
Full hallucination audit (bench_consolidation_audit.py): all 703
successfully consolidated sessions judged against their source conversations
— 5 flagged on first pass, 4 still flagged after a head+tail re-check
(0.57--0.71% session-level). MANUAL REVIEW of all four: 2 are judge
artifacts (long conversations truncated at 6000 chars; the D&D stat-block
details and the 400k-dollar mortgage are verifiably in the source), 1 is a
mild over-inference ("user prefers romantic lyrics" inferred from one
revision request -- judge correctly flagged), 1 remains ambiguous without
the full conversation. Adjusted true hallucination rate: ~0--0.14%
(0--1 session out of 703). LLM-consolidated entries are SAFE to ingest at
this reliability level; the audit judge needs a sliding-window protocol
for long conversations.
bench_state_fidelity.py pushes 20 realistic single-edit state updates
("server is 192.168.1.50" → "…1.99", "meeting at 15:00" → "…16:00") through
the dedup ladder and checks whether the store ends up holding the NEW state.
Pre-fix: 5/20 updates were silently dropped — two strengthened at
sim > 0.92 (text untouched, only the access time refreshed) and three merged
without rewrite because the new text was not longer than the stored one. The
affected updates are exactly the most common kind: changed numbers, times,
names. Fix (in remember()): a restatement carrying tokens the stored entry
lacks, and any differing text in the merge zone, now rewrites the entry in
place — pure restatements keep the old behavior. Post-fix: 20/20 updates
end with the correct state; all 51 behavior tests, the contradiction
benchmark (stale top-1 still 0/14) and the QA benchmark (17/20 before AND
after — the three misses are its pre-existing baseline) confirm no
regression.
Known trade-off, CLOSED in V4: 6/20 of those updates rewrite the entry in
place (merge) -- the V4 rewriting merge parks the old text as a superseded
tombstone with valid_from/valid_to, and include_superseded=True recovers
history uniformly (verified 6/6 pairs). Cost: +1 entry per rewriting merge.
Query routing (V4): recall classifies each question (state / history /
temporal / aggregation / lookup, mechanical heuristics) and routes
accordingly -- history questions automatically include superseded entries;
state questions are answerable via state_lookup(key) without competing in
top-k.
Also open from the end-to-end run: a full attribution of the 265 overlay-wrong answers — 114 had the evidence turn in top-5 (no consolidated entry hit), 53 had BOTH the evidence turn and the consolidated entry and still failed (answer-side multi-turn aggregation), 13 hit only the consolidated entry (summary lacked the detail), 85 were full retrieval misses. By question type, temporal-reasoning dominates (106/265 wrongs; 80% of that type), followed by multi-session (92). The next lever is answer-side multi-turn aggregation for temporal/multi-session questions, and preference/user retrieval quality for the 32 missing ones. Manual review of 8 sampled wrongs confirms the mechanism: 6 of 8 require cross-turn arithmetic (subtracting membership durations, summing trip mileage, counting flights) with the evidence turns ALL present in top-5; the model answers "I don't know" because no single entry states the derived number. The concrete v4 lever is structured timelines (entity+date tables) produced at consolidation time, or a calculation tool at answer time. A 6-sample review of the 85 full-miss wrongs shows their best evidence sits at rank 1708--11867 with sim 0.18--0.64: aggregation/statistics questions ("how many doctors", "total gift spend", "what did I do with Rachel two months ago") whose answers span many turns -- structurally out of reach for top-k semantic retrieval, confirming they need aggregated entities or answer-side tools.
bench_timeline.py re-runs the granularity A/B with TIMELINE-form
consolidated entries ("YYYY-MM: plain fact with numbers/names",
chronological) instead of prose summaries -- same 50 questions (seed 7):
| store (n=50, seed 7) | strict correct | weighted score |
|---|---|---|
| turn entries only | 16/50 = 32% | 0.350 |
| prose-summary overlay | 19/50 = 38% | 0.400 |
| timeline overlay (50-question sample) | 22/50 = 44% | 0.440 |
| turn + entity-state overlay (same-source records) | 18/50 = 36% | 0.390 |
| turn + timeline + prose (dual overlay) | 21/50 = 42% | 0.420 |
Dual overlay (timelines + prose stacked) does NOT beat timeline-only: the top-5 pool is finite, and prose entries compete for slots without helping the temporal questions timelines answer. Timeline-only is the optimal single configuration; stacking more consolidated forms dilutes it.
(Entity-state records -- "entity.attribute = current value (changed from old)" -- generated from the same 940 sessions: above turn-only, below timeline. Measured spectrum: narrative (turns) -> prose summary -> timeline (optimum) -> key-value records.)
At FULL SCALE (n=500) the timeline overlay scores 41.6% strict / 43.9% weighted vs the prose overlay's 42.8% / 44.9% -- statistically a wash. Honest reading: the 50-question sample overestimated the timeline form by ~2pp; what survives at full scale is the overlay principle itself (either consolidated form beats turn-only by ~+4.6pp). Timelines for all 940 sessions are cached in reports/timeline_entries.json. The memory format shapes answer quality, but format choice between prose and timeline is second-order; the first-order lever was overlaying consolidation on top of turns at all.
Format matrix completion (same 50 questions, seed 7): a fourth form -- ENTITY-STATE records ("entity.attribute = current value (changed from ...)") generated from the same 940 sessions -- scores 36% strict / 39% weighted, statistically indistinguishable from turn-only. Reading: structured timeline lines keep a date plus a narrative fact (retrievable), while heavily compressed key-value records lose the contextual surface the embedder needs, so they neither retrieve nor answer better. Consolidation formats are now a measured spectrum: narrative (turns) -> prose summary -> timeline -> key-value (entity-state), with the optimum at timeline.
Recurring mistakes have a dedicated mechanism (P-LESSON). Pitfall
lessons live in a separate lessons compartment and follow a strict
form: they start with their trigger words — the environment/tool
names whose presence should recall them — because plain text similarity
does not connect "a new task in X" with "don't do Y under X"; exact
keywords do. The capture hook injects the top-2 lessons on EVERY
message, in addition to normal recall, so lessons never compete for
regular slots. Registering one:
flymemory_remember(
text="PowerShell/Git Bash: inline quoting breaks -- write a script
file instead",
compartment="lessons", tags="lesson")Rules for the caller: check flymemory_recall(compartment="lessons")
before bug-fix/environment tasks; register every fixed pitfall. Honest
limits: the FIRST occurrence of a pitfall is always hit (no lesson
exists yet), and reading a lesson does not guarantee applying it.
For questions that aggregate across many turns ("how many times...", "how much in total..."), single-shot top-5 retrieval structurally under-collects evidence (the L5 aggregation-miss family), and widening the pool alone does not fix answers -- the answer layer must decompose. The measured protocol (P-ANSWER + P-AUDIT, strict 6% -> 16% on the multi-session set, unsupported-claim rate 0/50 after the gate):
- Collect wide — top-30 by production recall (width beats every smart collector; turn-recall 0.384 -> 0.683).
- Extract per entry — for each entry, ask for the atomic facts that bear on the question, copied VERBATIM (no inference); skip entries with none. This converts an intractable cross-turn aggregation into tractable per-turn lookups.
- Reduce — answer from the atomic-fact list only; arithmetic over extracted numbers is the reduce step's job.
- Audit — unsupported-claims check (answer vs fact list) before trusting the answer; the audit is what passed the gate at 0/50.
This is a CALLER-side protocol: the server stays LLM-free. Cost is ~31 short calls per question — route it explicitly (aggregation-class queries), never on the per-message hook path.
Where FlyMemory sits among the alternatives, and the deliberate things it refuses to do.
FlyMemory is an explicit, inspectable memory state machine — not a
model-driven memory synthesizer. Every state transition is mechanical and
auditable: superseded_by lineage, source provenance, power-law decay,
directed forgetting, evidence-linked consolidation, and a recovery pack after
context compaction. Capture is mechanical (hook), judgment is the calling
model's (precision-store, supersede, consolidate, forget) — the server never
runs an LLM, and everything is readable in one small file.
That is a deliberate contrast with hosted "memory synthesis" approaches
(background model-side consolidation of raw chats, e.g. ChatGPT's memory):
those optimize synthesis quality at service scale; FlyMemory optimizes
inspectability, verifiability and data locality for a personal agent.
Raw entries are never deleted — consolidation adds higher-order entries with
evidence_ids back-links (abstraction without loss), and superseded states
remain queryable via include_superseded=True.
| Rule | Mechanism |
|---|---|
| Capture everything mechanically | hook → flymemory_auto, silent no-op when the server is down |
| Don't lose novel facts | length-tiered dedup prefers not merging |
| Don't resurrect stale states | flymemory_supersede marks them; default recall skips them |
| Distinguish "captured" from "judged important" | source: hook / model, stamped on every entry and shown in recall output |
| Recall exact identifiers | IDF lexical channel (part numbers, file paths, IDs) |
| Forget slowly, never abruptly | power-law decay (tau doubled for model-stored entries -- dopamine-gated) + rehearsal; flymemory_cleanup prunes below threshold |
| Rehearsal stays scarce | only entries injected into context refresh; a wide sim>0.5 rule measured 100% of entries pinned at retention 1.0 (immortal chatter, decay inert) |
| Multi-topic messages stay separable | per-sentence chunking on store, per-chunk max on query |
| Fragmented knowledge gets abstracted | flymemory_consolidate(ids, conclusion) builds a higher-order entry with evidence_ids back-links; raw entries kept as evidence |
- Python 3.10+ (3.13 tested), Git
- ~5 GB disk for dependencies (torch is the big one) + a ~470 MB embedding model
- OS: developed on Windows; POSIX should work (pure Python + uvicorn), feedback welcome
- A GPU is not required — CPU inference is the default and is fast enough
git clone https://github.com/aujurd22/flymemory.git
cd flymemory
pip install -r requirements.txtor install as a package (adds a flymemory-server command):
pip install git+https://github.com/aujurd22/flymemory.gitrequirements.txt covers torch, sentence-transformers, mcp (pinned >=1.30,<2),
uvicorn, numpy, pytest.
python -m pytest tests/55 behavioral rule tests. The first run downloads
paraphrase-multilingual-MiniLM-L12-v2 (~470 MB) from HuggingFace into the
standard HF cache; after that everything works offline. If HuggingFace is
unreachable from your network, set a mirror endpoint first:
export HF_ENDPOINT=https://hf-mirror.com # PowerShell: $env:HF_ENDPOINT="https://hf-mirror.com"python flymemory/mcp_v3.py --http # or: flymemory-server --http
# → serves the MCP endpoint at http://127.0.0.1:8765/mcp
# → logs to flymemory/server.log (or server.<pid>.log if that file is locked)Smoke test (writes one test entry into a fresh library, and reads it back):
python flymemory/test_http_client.pyWithout --http the server speaks MCP over stdio (for clients that spawn it
per-session).
Any streamable-HTTP MCP client works. ZCode example
(~/.zcode/cli/config.json, adjust paths):
{
"mcp": {
"servers": {
"flymemory": {
"type": "http",
"url": "http://127.0.0.1:8765/mcp",
"timeoutMs": 120000
}
}
}
}Restart the client afterwards; you should see the flymemory_* tools
(remember / recall / auto / supersede / consolidate / forget / cleanup /
stats / session_pack / find_conflicts / insights / recall_index /
get_memory; remember & recall also take an optional compartment).
Two optional hooks make memory work without the model having to remember anything. Same config file:
{
"hooks": {
"enabled": true,
"events": {
"UserPromptSubmit": [
{ "hooks": [ { "type": "process",
"command": "C:/path/to/python.exe",
"args": ["D:/path/to/flymemory/flymemory/hook_auto.py"],
"timeoutMs": 10000 } ] }
],
"SessionStart": [
{ "matcher": "compact",
"hooks": [ { "type": "process",
"command": "C:/path/to/python.exe",
"args": ["D:/path/to/flymemory/flymemory/hook_compact.py"],
"timeoutMs": 15000 } ] }
]
}
}
}hook_auto.py(UserPromptSubmit): stores every user message and injects recall results into the turn.hook_compact.py(SessionStart oncompact): injects a recovery pack (recent trail + latest conclusions) right after the client compresses the conversation.
Both are silent no-ops when the server is down.
flymemory/flymemory_supervisor.pyw restarts the server if it dies and idles
while it is healthy. Run it headless at login — on Windows, a Startup-folder
shortcut to pythonw.exe flymemory_supervisor.pyw, or a per-user scheduled
task every few minutes as a second layer of protection.
- The library lives next to the code as
flymemory/flymemory_v3.pkl(git-ignored — your conversation content never leaves the machine unless you copy it). - Environment variables:
FLYMEMORY_DEVICE(defaultcpu, setcudato use the GPU),FLYMEMORY_MODEL(defaultsentence-transformers/paraphrase-multilingual-MiniLM-L12-v2),HF_HUB_OFFLINE(auto-set when the model cache is detected). - Schema upgrades are automatic: new code loads older library files in place.
- Nothing listens on
127.0.0.1:8765→ wait ~20 s (model warm-up), then check the newestflymemory/server*.log. - First tool call right after startup can block for a few seconds while the embedder loads — that is the warm-up gate, not a hang.
- Library embedded with a different model name → the server logs a warning;
re-embed (delete the
.pkland re-import, or switchFLYMEMORY_MODELback).
Measured on a real 1370-entry library, CPU (see bench_recall_speed.py,
bench_hopfield.py):
| Path | Result |
|---|---|
| Query encoding (embedder, CPU) | ~10.6 ms/query — dominates end-to-end at this scale |
| Scoring: per-entry Python loop | 8.5 ms/query |
| Scoring: vectorized matmul (shipped) | 1.2 ms/query (7×) |
| End-to-end recall | ~11.5 ms/query |
| Hopfield associative expansion vs pure vector recall | Recall@5 0.189 → 0.043 — hurts; disabled by default |
REVISITED 2026-10-03: the v1 Hopfield path was broken from day one --
the original "N=1370 crosstalk" reading was wrong. The v1 hash encoder
(encoder.py) has a k-WTA boundary bug: a short text hashes to ~200
non-zero bits, but k=204 (5% of 4096) makes np.partition(code, -k)[-k]
land on 0.0, so code >= 0.0 sets every zero bit to 1 as well -- every
text encodes to the same all-ones vector (reproduced 2026-10-03: three
unrelated sentences, bit-identical 4096/4096 codes). The 2026-09-22
"N=1370 patterns exceed capacity" verdict therefore measured 3 identical
all-ones codes, not capacity limits. Secondary: even with a fixed sparse
code, sign(W·s) iteration converges to the all +1 attractor
(classic unbalanced-Hebbian bias); centered Hebbian and view-normalized
variants do not rescue it. The production path (v3 embed+RRF) never used
this encoder and is unaffected. The biology-correct reading is one-step
linear readout (MBON-style: fixed synaptic weights, no recurrent
dynamics) -- 200/200 perfect recall in the committee bench.
For large libraries, recall can run in two stages: a Hamming-distance prefilter over packed 4096-bit sparse codes (bitwise ops, 512 B/entry), then dense cosine rerank on the top-C candidates (default C=100).
mem = SmartMemory(two_stage=True, hamming_candidates=100)
# or per-call: mem.recall(q, two_stage=True)- Library size N ≥ ~5000. Below that, dense scoring is already ~1 ms (measured 1.2 ms at N≈1.4k via BLAS matmul) and the prefilter + rerank overhead makes two-stage a net loss. The pay-off region is 10⁴–10⁵+ entries, where full-matrix scoring approaches 0.1 s/query.
- Code stability must hold on YOUR data. The quality premise comes from
the FlyPoet sparse-code retrieval experiment: Hamming lookup hit@1 0.123 vs
dense 0.128 (Δ ≈ 0.005) on trained char-level codes with a measured
address stability of 1.52×. FlyMemory's codes come from a random
projection — before trusting the prefilter, verify on your own library:
recall(q, two_stage=True)must agree withrecall(q, two_stage=False)on realistic queries (the tests pin this only for the small-N case). Measured fidelity also degrades with N (0.73 @1k → 0.55 @20k inbench_two_stage_scale.py) — at 20k entries, 45% of dense answers leak past the prefilter. - Keep fraction matters — a lot. The prefilter code sparsity follows a
U-curve like FlyPoet's channel sparsity: measured fidelity@100 was 0.691
at the biology-derived 5%, 0.849 at 25%, 0.861 at 50% (adopted default,
code_keep=0.5). The biology-derived 5% was too sparse for retrieval. - Memory overhead: +512 B per entry for the packed codes.
- Rehearsal side effect is scoped to returned candidates in prefilter
mode (non-candidates are never scored, so their access counters do not
refresh). With
include_superseded=True, history mode stays decay-immune as in the dense path. - A C-extension popcount or a real binary ANN (faiss/hnswlib) would change the latency picture, but fidelity remains the binding constraint.
| N | prefilter fidelity@100 | Hamming scan | dense scan |
|---|---|---|---|
| 1 000 | 0.922 | 6.35 ms | 0.21 ms |
| 2 000 | 0.965 | 11.90 ms | 0.34 ms |
| 5 000 | 0.903 | 31.94 ms | 0.56 ms |
| 10 000 | 0.878 | 55.93 ms | 0.94 ms |
The numpy Hamming scan is ~60x slower than the BLAS dense matmul at every scale (no SIMD popcount in numpy; BLAS is heavily optimized), and fidelity decays as N grows. Two-stage retrieval is therefore rejected for the numpy implementation at every scale -- the dense path is both faster and more faithful. The code stays behind the flag as a documented negative result; the sensible scale path is a real ANN index (faiss/hnswlib) at N > 1e5.
bench_contradiction.py — 14 temporal scenarios ("user runs Windows" →
"user switched to Fedora"), each with adversarial traps: a newer off-hand
mention of the old state ("the old Windows VM is slow" — not a state change),
old states that paraphrase the query better than the new state, and new states
missing the attribute keyword. Five policies share one embedder and one
chunker; supersede markings simulate the calling model's judgment (the
architecture under test: judgment in the caller, mechanical resolution in the
server).
| policy | current@1 | current@3 | stale top1 |
|---|---|---|---|
| dense (plain RAG) | 2/14 | 14/14 | 12/14 |
| dense + recency | 3/14 | 14/14 | 0/14 |
| bm25 | 4/14 | 14/14 | 9/14 |
| bm25 + recency | 10/14 | 14/14 | 0/14 |
| flymemory (full) | 6/14 | 13/14 | 0/14 |
Reading:
- Plain similarity retrieval puts a superseded fact first in 9–12 of 14 current-state queries — the mechanical origin of the "stale memory confusion" agents suffer from.
- Recency heuristics avoid stale picks but get baited by recent non-state mentions (dense+recency: 3/14); a recency-ordered BM25 is the strongest mechanical top-1 here (10/14) — an honest result we document, not hide.
- FlyMemory's design intent is model-resolution from a stamped top-3, not
mechanical top-1. Measured: 13/14 of the correct current answers are present
in the model-facing top-3 (with age + provenance stamps); a superseded
fact never appears at rank 1 (0/14), and full history is recovered on
demand (
include_superseded=True, 6/6 — archived, not forgotten, immune to decay in history mode). Note: the benchmark measures presence in top-3, not an actual LLM resolution step. - This is a system-level comparison: the FlyMemory arm runs the whole pipeline (supersede + decay + source weighting + lexical + chunking + dedup) while baselines run bare similarity. Component ablation is future work.
- n=14: indicative, not statistical.
from flymemory.v3 import SmartMemory
mem = SmartMemory() # decay_tau=30d, Hopfield off
mem.remember_text("今天讨论了X。还决定了Y。", source="hook")
mem.remember("重要结论:Y 优于 X", source="model")
mem.supersede(old_id, new_id) # judgment made by the caller
mem.recall("X 的结论是什么") # hybrid semantic+lexical, decay-weighted
mem.recall("以前是不是用过 X", include_superseded=True) # history queryThe original design (kept in flymemory/v2.py, hopfield.py, encoder.py,
memory_store.py, demo*.py): text → 5% sparse binary code → multi-
compartment Hopfield network (W += s^T s, recall by iterating
s ← sign(W·s)), motivated by the fly mushroom body's sparse coding and
recurrent loops. Its claims (20% cue → 100% recovery, O(1) recall) hold only
for that small-N experimental setting, not for the production v3 path above.
Personal, single-user, evolving. Scale target: the exact vectorized scan is fine to ~10⁵ entries; beyond that, add an ANN index. Not a Mem0/Letta competitor — it is the minimal mechanism set that made one agent's memory reliable across months of daily use.
MIT



