Skip to content
aujurd22Public

About

Long-term memory layer for personal AI agents: chunked semantic+lexical recall, time decay, semantic dedup, model-driven supersede. Drosophila Hopfield origins.

Resources

Stars

3 stars

Watchers

0 watching

Forks

Latest commit

 

History

240 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

FlyMemory

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.

FlyMemory in plain language

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.

Results at a glance

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 ladder

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.

L1 overlay + L6 anchoring

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).

Memory judgment

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.

Eviction mirror

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.

How it works

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

The write path, end to end

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
Loading

The recall path

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"]
Loading

An entry's lifecycle

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
Loading

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.

MCP server + hook (the integration that makes it "just work")

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/mcp

MCP 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.

LongMemEval-oracle retrieval benchmark

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).

Cross-encoder rerank

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.

Where do decay and source weights act? Not after fusion.

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.

Memory Judgment Benchmark (Phase 1)

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.

TOOL2: time-scoped agentic search (56% strict)

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.

Granularity A/B: consolidation must OVERLAY, never replace

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.

Engine state-fidelity audit (found and fixed a real dedup bug)

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.

Timeline overlay: the structured form wins (v4 lever validated)

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.

Lesson channel (never re-hit the same pitfall)

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.

Aggregation protocol (caller-side, experimental)

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):

  1. Collect wide — top-30 by production recall (width beats every smart collector; turn-recall 0.384 -> 0.683).
  2. 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.
  3. Reduce — answer from the atomic-fact list only; arithmetic over extracted numbers is the reduce step's job.
  4. 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.

Design positioning

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.

The memory rules

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

Install

0. Prerequisites

  • 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

1. Clone and install

git clone https://github.com/aujurd22/flymemory.git
cd flymemory
pip install -r requirements.txt

or install as a package (adds a flymemory-server command):

pip install git+https://github.com/aujurd22/flymemory.git

requirements.txt covers torch, sentence-transformers, mcp (pinned >=1.30,<2), uvicorn, numpy, pytest.

2. Run the tests (also downloads the embedding model)

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"

3. Start the server

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.py

Without --http the server speaks MCP over stdio (for clients that spawn it per-session).

4. Register the MCP server

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).

5. (Recommended) mechanical capture hooks

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 on compact): 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.

6. (Optional) keep-alive

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.

7. Data & configuration

  • 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 (default cpu, set cuda to use the GPU), FLYMEMORY_MODEL (default sentence-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.

Troubleshooting

  • Nothing listens on 127.0.0.1:8765 → wait ~20 s (model warm-up), then check the newest flymemory/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 .pkl and re-import, or switch FLYMEMORY_MODEL back).

Benchmarks

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.

Two-stage retrieval (Hamming prefilter + dense rerank)

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)

Usage preconditions — read before enabling

  1. 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.
  2. 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 with recall(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 in bench_two_stage_scale.py) — at 20k entries, 45% of dense answers leak past the prefilter.
  3. 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.
  4. Memory overhead: +512 B per entry for the packed codes.
  5. 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.
  6. A C-extension popcount or a real binary ANN (faiss/hnswlib) would change the latency picture, but fidelity remains the binding constraint.

Measured verdict (2026-09-22, diverse 10k-entry library)

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.

Contradiction-resolution benchmark

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.

API sketch

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 query

v1/v2 Hopfield experiments

The 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.

Status

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.

License

MIT

About

Long-term memory layer for personal AI agents: chunked semantic+lexical recall, time decay, semantic dedup, model-driven supersede. Drosophila Hopfield origins.

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages