PROJECT · 02

branchmem

Branch-aware visibility for derived memory in tree-shaped histories

View on GitHub →Open source · Research in progress
Fig. 1
BRANCH ABRANCH Bedit →e1 cat named Mochie2 notede3 moving to Chengdue4 try Gaoxine5 moving to Hangzhoue6 try Yuhange7 remind: pet carrier

path: e1 → e2 → e5 → e6 → e7

VISIBILITY TRACEanchor = e7
  1. n1has a cat, Mochi
    ← e1· ⊂ path→ visible
  2. n2moving to Chengdu
    ← e3· e3 ∉ path→ hidden
  3. n3summary: cat Mochi, moving to Chengdu
    ← e1,e2,e3· e3 ∉ path→ COLLATERAL
  4. n4moving to Hangzhou
    ← e5· ⊂ path→ visible
  5. n5todo: carrier for Mochi
    ← e7,e1· ⊂ path→ visible
leaks 0 · wrongly hidden 0 · collateral 1
Figure 1. One tree, three readers. The user edited e3 “moving to Chengdu” into e5 “moving to Hangzhou”; while each branch was current, the memory system extracted notes n1–n5. All three readers give the model only the raw messages on the current path; they differ on derived memory. Right and wrong on the right-hand side ignore every reader’s rule and look only at which events each note’s content depends on. (a) Raw filter only: every note is kept, so the sibling’s facts reach the context. (b) Branch labels: this is the naive scheme, tagging a note with the branch that was current when it was written. Extraction is asynchronous; when n1 was written the user was still on A, so n1, derived only from the shared prefix, is labeled A and disappears on B. Copying shared-prefix labels to both branches at the fork fixes this example, but not the underlying problem: by the time a note is written, the current branch may already have changed. (c) Provenance: a note is visible only if all its source events lie on the anchor’s ancestor path. The cost is granularity: n3 put the shared prefix and branch A into one summary, so on B the whole note is hidden, the cat included. Click e4 to switch back: only the anchor moves, and A’s memories return on their own. This figure demonstrates the rule; it is not evidence. The evidence is the answer-scored benchmark: on the test split, branchmem leaks on 5.9% of leak probes, Mem0 with a raw filter on 50.0%.

Chat and agent histories are trees, not logs. A user regenerates a reply, edits an earlier message, or an agent framework forks from a checkpoint, and the old continuation becomes a sibling branch. For raw messages, filtering to the current branch is enough. Long-term memory is the problem: summaries, facts, profile entries, and graph edges were derived while the old branch was still current, and they stay in the store after it is abandoned. The user edited “moving to Chengdu” into “moving to Hangzhou”; memory still says Chengdu.

branchmem has one rule: a derived memory is visible only when every one of its source events lies on the ancestor path of the current anchor. Visibility never depends on labels like branch_id or inactive — labels go stale, source event IDs do not. One source on a sibling hides the whole note. Switching back only moves the anchor; nothing is marked, deleted, or rewritten, so the old branch’s memories simply come back. Without an anchor, strict mode returns nothing rather than defaulting to the whole tree.

The rule is simple; the hard parts are where it doesn’t reach. First, global invalidation and branch conflicts: when the user says “don’t bring this up again,” should that retraction apply to the current branch only, or to the whole tree? When a retraction and the memory it retracts sit on different branches, which one wins? This part is not done yet. Second, silent failure in production: a missing source event, legacy data without topology, a request without an anchor — if any link breaks, the rule raises no error; it just quietly lets too much through or hides too much. So strict mode fails closed whenever information is missing, and compat mode lets legacy data through but counts and reports every case separately.

The companion branchbench has 30 synthetic Chinese dialogues and 98 probes across five branch operations: editing a user message, regenerating, forking from a checkpoint, switching back, and correcting within a branch. The test split (20 scenarios, 69 probes) is frozen with SHA-256 hashes, and reported results use it only; scoring is keyword-based with no LLM judge, so it is reproducible. Same answer model, same prompt, temperature 0, three runs: full history leaks on 52.0% of leak probes, a rolling summary 40.2%, Mem0 OSS 60.8%, and branchmem 5.9% — equal to the “current-branch raw only” upper bound — which also shows the current scenarios are short and do not yet demonstrate why derived memory is needed. Its correct rate is 95.7%, against 70–80% for the baselines.

The Mem0 pair makes the point. Feeding it only the current branch’s raw messages lowers its leak rate from 60.8% to just 50.0%, because the sibling-branch facts are already in Mem0’s own memories. The same filter on full history blocks 89%. Filtering raw messages is not enough; whatever is derived from them has to trace back to its sources.

The code was extracted from Autarkia’s memory layer and checked differentially against the original: on 400 trees with shuffled arrival, about 107k visibility decisions matched exactly. The limits are stated in the repository: the scenarios are synthetic, there are only 34 leak probes, and a single answer model was used. Two probes had the forbidden word in the question itself, found after the run; the frozen set was left unchanged and a sensitivity analysis without them is reported alongside.

01Highlights

Visibility predicatea derived memory is visible only if all its source events are on the current anchor’s ancestor path — no branch labels involved
Deterministic topologythe same event set yields the same tree, segments, and IDs regardless of arrival order, duplicates, or children arriving first
Strict and compat modesstrict fails closed without an anchor; compat lets pre-topology history through but counts and reports it separately
Corezero dependencies, never calls a model; optional note store, gated BM25 / vector retrieval, and extraction pipeline
branchbench30 scenarios / 98 probes over five branch operations, a validator that checks each probe tests what it claims, and a frozen test split
Answer-level baselinesfull history, rolling summary, Mem0 OSS, Mem0 + raw filter, branchmem, and the upper bound, plus a pre-registered conflict-prompt robustness check

02Stack

PythonpytestBM25Mem0LLM