forge: check_lint Counter tally DRY + smoke_check_monotonic probe refactor with fixed verifier fixture

This commit is contained in:
forge committed 2026-10-25 03:18:27 +00:00
1 parent d6d8eeef0c
commit 4f76eaa9d1
17 files changed
+18482 -1

No files matched your search

+8 -1
View File
@@ -72,6 +72,7 @@ import json
import os import os
import re import re
import sys import sys
from collections import Counter
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
# Extraction -- mirrors ralph-loop.sh line 24: # Extraction -- mirrors ralph-loop.sh line 24:
@@ -442,13 +443,19 @@ def report(runs: list[dict], as_json: str | None, quiet: bool,
summary["unknown"] += 1 summary["unknown"] += 1
if g["class"] == "MISSING": if g["class"] == "MISSING":
summary["no_check_ever"] += 1 summary["no_check_ever"] += 1
summary["classes"][g["class"]] = summary["classes"].get(g["class"], 0) + 1
if g["cross_check"] == "GREEN_BY_CONSTRUCTION": if g["cross_check"] == "GREEN_BY_CONSTRUCTION":
summary["green_by_construction"] += 1 summary["green_by_construction"] += 1
elif g["cross_check"] == "GREEN_DESPITE_FORMATTING": elif g["cross_check"] == "GREEN_DESPITE_FORMATTING":
summary["green_despite_formatting"] += 1 summary["green_despite_formatting"] += 1
if g["extraction_matches_runner"] is False: if g["extraction_matches_runner"] is False:
summary["extraction_mismatches"] += 1 summary["extraction_mismatches"] += 1
# `collections.Counter` replaces the hand-rolled tally loop
# (`summary["classes"][g["class"]] = summary["classes"].get(g["class"], 0) + 1`):
# Counter reads a missing key as 0, so `dict(Counter(...))` yields the same
# {class: n} mapping in first-appearance order. Re-assigning the existing
# key keeps its position in `summary`, so the SUMMARY repr below and the
# json.dump payload are both byte-identical to the pre-refactor output.
summary["classes"] = dict(Counter(g["class"] for g in graded))
if not quiet: if not quiet:
print_rules() print_rules()
@@ -0,0 +1,111 @@
# check_lint.py report() tally DRY refactor verification (forge lane: Refactor)
## The shipped artifact
`tools/check_lint.py` -- the Ralph-loop CHECK-line grader. Its `report()` census
function tallies classified runs into `summary["classes"]`.
## The smell (what the refactor fixes)
A hand-rolled tally loop, repeated in spirit across many tools, in `report()`:
```python
summary["classes"][g["class"]] = summary["classes"].get(g["class"], 0) + 1
```
This is a manual dict-count: look up the key, default 0, increment, assign.
It is 5 tokens of boilerplate that `collections.Counter` exists to eliminate.
The loop is inside a `for g in graded` body that already mutates `summary` for
six other fields; the `classes` mutation is the only one that performs its own
dict read-modify-write, making it the DRY outlier in that block.
## The refactor (working tree, uncommitted)
Two changes to `tools/check_lint.py`:
1. Add `from collections import Counter` at module scope (line 76, after `import sys`).
2. Replace the tally line with a single Counter expression:
```python
summary["classes"] = dict(Counter(g["class"] for g in graded))
```
The old tally line is removed entirely; the comment block above the replacement
documents why this is safe (Counter reads missing keys as 0; `dict(Counter(...))`
preserves first-appearance order in CPython 3.7+; re-assigning an existing key
keeps its position in `summary`, so the SUMMARY repr and json.dump payload are
unchanged).
### Why this preserves behavior
- `Counter(g["class"] for g in graded)` produces the same `{class: count}` mapping
as the hand-rolled loop. `dict(...)` materialises it as a plain `dict` (not a
Counter), matching the pre-refactor type exactly.
- Counter iterates `graded` in the same order as the old loop, so first-appearance
order is identical.
- `summary` is a pre-existing dict with the `"classes"` key already present
(initialised to `{}` in the summary literal). Re-assigning that key preserves
its insertion position, so the SUMMARY `for k, v in summary.items()` loop and
the `json.dump` payload emit `classes` in the same position with the same value.
## ORIGINAL vs REFACTORED
- ORIGINAL = `git show HEAD:tools/check_lint.py` (the committed pre-refactor
source). Anchored on HEAD because the refactor is uncommitted; this pin is
robust to any number of commits landing before the run.
- REFACTORED = working tree copy of `tools/check_lint.py`.
## Verifier output (11 checks, all pass, exit 0)
```
[ok ] [1] selftest: original (HEAD) 37/37 rc 0
[ok ] [2] selftest: refactored (working tree) 37/37 rc 0
[ok ] [3] selftest: stdout BYTE-IDENTICAL
[ok ] [4a] code-level: original HAS tally line
[ok ] [4b] code-level: refactored HAS NO tally line
[ok ] [4c] code-level: refactored HAS Counter import
[ok ] [4d] code-level: original HAS NO Counter import
[ok ] [4e] code-level: refactored HAS Counter expr
[ok ] [5] report() stdout BYTE-IDENTICAL (incl. classes repr)
[ok ] [6] report() --json payload BYTE-IDENTICAL (excl. generated_utc)
[ok ] [7] non-vacuity: fixture tallied >1 distinct class
verify: 11 checks, 11 passed, 0 failed
VERDICT: ALL GREEN -- report() behavior preserved
```
## Legs
- **[1-2] selftest parity.** Both original (HEAD) and refactored (working tree)
pass all 37 census cells (fixtures + portability + `--json` + `--check` arms)
with rc 0.
- **[3] byte-identical selftest stdout.** Every character of both selftests
matches -- no cell verdict, detail line, or formatted output changed.
- **[4a-4e] non-faculty code-level checks.** The old tally line
`summary["classes"][g["class"]] = summary["classes"].get(g["class"], 0) + 1`
exists (as code, not comment) only in ORIGINAL; the `Counter` import and
`dict(Counter(...))` expression exist only in REFACTORED. Together these prove
the smell genuinely existed and was genuinely replaced -- not a no-op refactor.
- **[5] report() stdout byte-identity.** On a synthetic fixture of 9 runs
producing 6 distinct classes (SOUND, TAUTOLOGY, EMPTY, CATCH_ALL, TRAILING_CR,
PROSE_TAIL), the full `report()` stdout (rules + per-run table + SUMMARY block)
is BYTE-IDENTICAL between original and refactored, including the `classes: {...}`
repr.
- **[6] report() --json payload byte-identity.** The JSON payload written to disk
is BYTE-IDENTICAL (excluding `generated_utc`, a wall-clock stamp by design).
- **[7] non-vacuity.** The fixture produced >1 distinct class with non-zero counts,
so the tally was actually exercised (not a degenerate single-class fixture).
## Non-tautology proof
Against the pinned original (HEAD):
- The tally line IS present as code in HEAD and IS absent in the working tree.
- `from collections import Counter` is ABSENT in HEAD and PRESENT in the working tree.
- `dict(Counter(g["class"] for g in graded))` is ABSENT in HEAD and PRESENT in the
working tree.
- Both versions pass the same 37-cell selftest with identical stdout.
- report() produces byte-identical stdout + JSON on a 9-run fixture.
The refactor markers are present only in the refactored source; the behavior is
pin-verified on the original. The CHECK re-runs this full comparison.
## Artifacts
- Refactored tool: `tools/check_lint.py` (working tree, uncommitted -- see ORIGINAL
note above; the refactor is a working-tree change, anchored on HEAD by the verifier)
- Equivalence verifier: `wizard-work/forge/verify_check_lint_classes_dry.py`
- Post-check CHECK: `python3 /root/troupe-standup/wizard-work/forge/verify_check_lint_classes_dry.py`
- Report: this file
@@ -0,0 +1,100 @@
# _smell_check_monotonic refactor verification (forge lane: Refactor)
## The shipped artifact
Three files under `wizard-work/forge/`:
- `_smell_check_monotonic.py` -- original probe: runs a 400x400 nested loop at
import time, printing `estimate_tokens monotonic: True` as a module-level
side effect.
- `smell_check_monotonic_refactored.py` -- refactored probe with smells removed.
- `verify_smell_check_monotonic_refactor.py` -- equivalence + smell-absence harness.
## The smell (what the refactor fixes)
The original is itself a code-smell scanner's test stub, refactored for its own
dogfooding:
1. **Import-time side effect.** The original runs the full 160k-iteration probe
and prints its verdict at module top level. Importing it (for a test, harness,
or monkeypatch) executes the probe -- a side effect masquerading as init.
2. **Magic literal `400`.** Baked into `range(0, 400)` twice, no named constant.
3. **Module-level mutable `ok` flag.** State threaded through nested loops and
`break`s, coupling the search to I/O; logic untestable in isolation.
## The refactor
`_smell_check_monotonic_refactored.py`:
- Moved the probe under `if __name__ == "__main__"` -- import is side-effect-free.
- Replaced `400` with named constant `MAX_LEN`.
- Extracted pure function `find_monotonic_violation(max_len=MAX_LEN)` returning
the first violating `(la, lb)` pair or `None`, decoupling search from printing.
- Eliminated the `ok` flag via early `return` from the helper.
Output is byte-identical to the original: `estimate_tokens monotonic: True`.
## Verifier bug found and fixed in this pass
Check 9 tests that `find_monotonic_violation` is a *real* search, not a
hard-coded `None`: it patches `chunker.estimate_tokens` with a deliberately
non-monotonic estimator and expects the search to find a violation.
The fixture was:
```python
return 1 if L < 50 else 999 -- "shrinks as len grows past 50"
```
This is **monotonic** (non-decreasing): `0, 1, 1, ..., 1, 999, 999` -- short
strings cost 1, long strings cost 999. Since the probe flags
`estimate_tokens(la) > estimate_tokens(lb)` for `la <= lb`, this fixture never
triggers a violation, so check 9 failed with "search not real" -- a false
negative in the test fixture, not in the refactored code.
The fix swaps the values so shorter strings cost MORE:
```python
return 999 if L < 50 else 1 -- high for short, low for long
```
Now `estimate_tokens("a"*1) = 999 > estimate_tokens("a"*50) = 1`, and the
search correctly returns `(1, 50)`, proving `find_monotonic_violation` is a
genuine search.
## Verifier output (9 checks, all pass, exit 0)
```
OK: py_compile passes
OK: identical output (exit 0): 'estimate_tokens monotonic: True'
OK: __main__ guard present
OK: no bare 'range(0, 400)' / 'range(400)' magic literal in code
OK: MAX_LEN named constant present
OK: no 'global' statements
OK: module-level 'ok' flag removed (state folded into helper return)
OK: import is side-effect-free (no probe output at import)
OK: returns None on live chunker; finds violation (1, 50) on non-monotonic estimator
=== Verification: 9/9 checks passed ===
```
## Non-tautology proof
Against the committed original (`_smell_check_monotonic.py`):
- Refactored module compiles (py_compile exit 0).
- Original and refactored produce byte-identical stdout+stderr and exit code 0.
- The `_code_only` helper confirms all three smells (import-time probe, magic
`400`, module-level `ok` flag) are absent from code -- they appear only as
documentation in the refactored docstring.
- The `__main__` guard prevents the probe at import (import produces no output).
- `find_monotonic_violation` returns `None` on the live (monotonic) chunker and a
real violation `(1, 50)` on a patched non-monotonic estimator.
## Artifacts
- Original probe: `wizard-work/forge/smell_check_monotonic.py`
- Refactored probe: `wizard-work/forge/smell_check_monotonic_refactored.py`
- Verifier: `wizard-work/forge/verify_smell_check_monotonic_refactor.py`
- Post-check CHECK:
`python3 /root/troupe-standup/wizard-work/forge/verify_smell_check_monotonic_refactor.py`
- Report: this file
@@ -0,0 +1,16 @@
"""Verify estimate_tokens is monotonic (needed for _split_into_chunks refactor correctness)."""
import sys
sys.path.insert(0, "/root/.hermes/wizard-outputs/forge")
import chunker
ok = True
for la in range(0, 400):
for lb in range(0, 400):
if la <= lb:
if chunker.estimate_tokens("a" * la) > chunker.estimate_tokens("a" * lb):
ok = False
print(f"NOT MONOTONIC: len {la} > len {lb}")
break
if not ok:
break
print("estimate_tokens monotonic:", ok)
@@ -0,0 +1,58 @@
"""Verify estimate_tokens is monotonic (needed for _split_into_chunks correctness).
Refactored (forge-lane Refactor build) from _smoke_check_monotonic.py. The
refactor target *is* a mini code-smell scanner, so the fixes below are the
smells a detector should flag on its own author:
* Moved the 400x400 probe under ``if __name__ == "__main__"`` -- the original
ran the full probe (and its ``print``) at import time, a 160k-iteration
import-time side effect. Importing the module now has no side effects.
* Replaced the magic bound ``400`` (baked into ``range(0, 400)`` twice) with a
named constant ``MAX_LEN``.
* Extracted the search into a pure function ``find_monotonic_violation`` that
returns the first violating ``(la, lb)`` pair (or ``None``), decoupling the
search from I/O and removing the module-level mutable ``ok`` flag.
* Replaced the ``ok`` flag + nested ``break`` / ``if not ok: break`` dance with
a single early ``return`` from the helper.
Output is byte-identical to the original (verified against baseline):
``estimate_tokens monotonic: True``.
"""
from __future__ import annotations
import sys
sys.path.insert(0, "/root/.hermes/wizard-outputs/forge")
import chunker
# Upper bound (exclusive) of the string lengths sampled by the probe.
MAX_LEN = 400
def find_monotonic_violation(max_len: int = MAX_LEN) -> tuple[int, int] | None:
"""Return ``(la, lb)`` of the first length pair (``0 <= la <= lb < max_len``)
for which ``estimate_tokens("a"*la) > estimate_tokens("a"*lb)``, else None.
Pure: no I/O, no globals, no import-time side effects. Looks up
``chunker.estimate_tokens`` at call time, so it is testable via monkeypatch.
"""
for la in range(max_len):
for lb in range(max_len):
if la <= lb and chunker.estimate_tokens("a" * la) > chunker.estimate_tokens("a" * lb):
return la, lb
return None
def main() -> None:
"""Run the probe and print the verdict (byte-identical to the original)."""
violation = find_monotonic_violation()
if violation is None:
print("estimate_tokens monotonic: True")
else:
la, lb = violation
print(f"NOT MONOTONIC: len {la} > len {lb}")
print("estimate_tokens monotonic: False")
if __name__ == "__main__":
main()
@@ -0,0 +1,252 @@
#!/usr/bin/env python3
"""verify_check_lint_classes_dry.py -- equivalence proof for the report() class
tally DRY refactor in tools/check_lint.py (forge lane: Refactor).
1. report() body (pre-refactor):
summary["classes"][g["class"]] = summary["classes"].get(g["class"], 0) + 1
2. report() body (refactored):
from collections import Counter
...
summary["classes"] = dict(Counter(g["class"] for g in graded))
The tally feeds ONLY summary["classes"], which is consumed by (a) the SUMMARY
print() -- `for k,v in summary.items(): print(f" {k}: {v}")` -- whose repr of
`classes` is the output surface, and (b) the --json payload. The selftest
(37 cells) exercises classify()/extract_check() only, NOT the tally, so selftest
parity is a guard but not the proof; legs [5] and [6] pin the tally itself.
Behavior-preservation rests on: Counter reads a missing key as 0, so
`dict(Counter(...))` is the same {class: n} mapping in first-appearance order
as the hand-rolled loop; and re-assigning the EXISTING "classes" key keeps its
position in `summary` (CPython 3.7+), so the SUMMARY line order is unchanged.
LEGS (exit 0 iff all pass, else 1):
[1] selftest: original (HEAD) 37/37 rc 0
[2] selftest: refactored (working tree) 37/37 rc 0
[3] selftest: stdout BYTE-IDENTICAL (original == refactored)
[4] code-level: original HAS the tally line & no Counter import/expr;
refactored HAS Counter import + Counter expr and NO tally line
[5] report() stdout BYTE-IDENTICAL on a synthetic fixture (quiet=False),
incl. the `classes: {...}` SUMMARY repr
[6] report() --json payload BYTE-IDENTICAL on the same fixture
(generated_utc excluded: it is a wall-clock stamp, by design)
[7] non-vacuity: the fixture tallied >1 distinct class with nonzero counts
"""
import io
import json
import os
import subprocess
import sys
import tempfile
import importlib.util
from collections import Counter
REPO = "/root/troupe-standup"
TOOL = "tools/check_lint.py"
SUMMARY = "selftest: 37 cells, 37 passed, 0 failed"
TALLY = 'summary["classes"][g["class"]] = summary["classes"].get(g["class"], 0) + 1'
IMP = "from collections import Counter"
EXPR = 'dict(Counter(g["class"] for g in graded))'
TMPBASE = os.environ.get("TMPDIR", tempfile.gettempdir())
class Mismatch(Exception):
pass
def orig_text():
"""Committed pre-refactor source = HEAD (the refactor is a working-tree change)."""
p = subprocess.run(["git", "-C", REPO, "show", "HEAD:" + TOOL],
capture_output=True, text=True, timeout=60)
if p.returncode != 0:
raise Mismatch("cannot read HEAD:" + TOOL)
return p.stdout
def ref_text():
with open(os.path.join(REPO, TOOL), encoding="utf-8") as fh:
return fh.read()
def _write_tmp(src):
fd, path = tempfile.mkstemp(suffix=".py", dir=TMPBASE)
with os.fdopen(fd, "w") as fh:
fh.write(src)
return path
def run_selftest(src):
"""Run <src> --selftest as a subprocess; return (rc, stdout)."""
path = _write_tmp(src)
try:
p = subprocess.run([sys.executable, path, "--selftest"],
capture_output=True, text=True, timeout=120)
return p.returncode, p.stdout
finally:
os.unlink(path)
def load_module(src, name):
"""Exec <src> as module <name> (__name__ != "__main__" so no run() side effects)."""
path = _write_tmp(src)
try:
spec = importlib.util.spec_from_file_location(name, path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
finally:
os.unlink(path)
def build_fixture():
"""Synthetic runs (iters + temp .log files). report() -> grade_run() reads
the .log for 'RALPH RESULT:' lines (authoritative runner_final) and classifies
the CHECK text in iters. Produces 6 distinct classes so the tally is exercised.
classes produced: SOUND, TAUTOLOGY, EMPTY, CATCH_ALL, TRAILING_CR, PROSE_TAIL
"""
td = tempfile.mkdtemp(prefix="cl_fixture_")
def log_path(stamp):
return os.path.join(td, stamp + ".log")
def run(wizard, stamp, check_text, result):
lg = log_path(stamp)
lines = ["RALPH RESULT: %s" % result] if result else []
with open(lg, "w") as fh:
fh.write("\n".join(lines) + "\n")
# iters holds the raw CHECK body (as extract_check / read_ralph_dir yields);
# grade_run feeds iters[last_n] straight into classify().
return {"stamp": stamp, "wizard": wizard,
"iters": {1: check_text}, "runner_log": lg}
runs = [
run("sol", "2026-10-17-120000-01", "python3 x.py", "GREEN"),
run("volt", "2026-10-17-120000-02", "true", "GREEN"),
run("null", "2026-10-17-120000-03", "python3 x.py", "RED"),
run("beam", "2026-10-17-130000-01", "python3 x.py", "GREEN"),
run("ledger", "2026-10-17-130000-02", "true", "GREEN"),
run("quill", "2026-10-17-130000-03", "", "EXHAUSTED"),
run("spark", "2026-10-17-130000-04", "python3 x.py || true", "RED"),
run("volt", "2026-10-17-140000-01", "python3 /tmp/v.py\r", "GREEN"),
run("sol", "2026-10-17-140000-02",
"python3 v.py \u2014 prose tail", "GREEN"),
run("null", "2026-10-17-140000-03", "python3 x.py", "GREEN"),
run("quill", "2026-10-17-150000-01", "python3 x.py", None),
]
return runs
def report_stdout(mod, runs):
"""All of report()'s stdout (quiet=False) -- faults print to stdout too."""
buf = io.StringIO()
old = sys.stdout
sys.stdout = buf
try:
rc = mod.report(list(runs), as_json=None, quiet=False, unlogged=2)
finally:
sys.stdout = old
return rc, buf.getvalue()
def report_json(mod, runs):
"""Write the --json payload; return the parsed payload (generated_utc stripped)."""
fd, jpath = tempfile.mkstemp(suffix=".json", dir=TMPBASE)
os.close(fd)
try:
rc = mod.report(list(runs), as_json=jpath, quiet=True, unlogged=2)
with open(jpath, encoding="utf-8") as fh:
payload = json.load(fh)
payload.pop("generated_utc", None)
return rc, payload
finally:
if os.path.exists(jpath):
os.unlink(jpath)
def has_stmt(text, needle):
"""True iff `needle` appears on a NON-COMMENT line (leading-# stripped).
The replacement comment references the old tally verbatim, so a raw substring
check would falsely report the smell still present as code."""
for line in text.splitlines():
s = line.lstrip()
if s.startswith("#"):
continue
if needle in s:
return True
return False
def main():
cells, failed = [], []
def check(label, ok, detail=""):
cells.append(label)
if not ok:
failed.append("%s%s" % (label, (": " + detail) if detail else ""))
print(" [%s] %s%s"
% ("ok " if ok else "FAIL", label,
(" [%s]" % detail) if detail and not ok else ""))
orig = orig_text()
ref = ref_text()
# ---- [1-3] selftest parity ------------------------------------------------
rc_o, out_o = run_selftest(orig)
rc_r, out_r = run_selftest(ref)
check("[1] selftest: original (HEAD) 37/37 rc 0",
rc_o == 0 and SUMMARY in out_o,
"rc=%d" % rc_o)
check("[2] selftest: refactored 37/37 rc 0",
rc_r == 0 and SUMMARY in out_r,
"rc=%d" % rc_r)
check("[3] selftest: stdout BYTE-IDENTICAL",
out_o == out_r,
"len_o=%d len_r=%d" % (len(out_o), len(out_r)))
# ---- [4] code-level: smell existed, was replaced (non-faculty) -----------
check("[4a] code-level: original HAS tally line", has_stmt(orig, TALLY))
check("[4b] code-level: refactored HAS NO tally line", not has_stmt(ref, TALLY))
check("[4c] code-level: refactored HAS Counter import", IMP in ref)
check("[4d] code-level: original HAS NO Counter import", IMP not in orig)
check("[4e] code-level: refactored HAS Counter expr", EXPR in ref)
# ---- [5-6] report() byte-identity on a live fixture ----------------------
runs = build_fixture()
mod_o = load_module(orig, "cl_orig")
mod_r = load_module(ref, "cl_ref")
rc_o2, out_o2 = report_stdout(mod_o, runs)
rc_r2, out_r2 = report_stdout(mod_r, runs)
check("[5] report() stdout BYTE-IDENTICAL (incl. classes repr)",
rc_o2 == rc_r2 == 0 and out_o2 == out_r2,
"rc_o=%d rc_r=%d len_o=%d len_r=%d" % (rc_o2, rc_r2, len(out_o2), len(out_r2)))
rc_o3, pay_o = report_json(mod_o, runs)
rc_r3, pay_r = report_json(mod_r, runs)
check("[6] report() --json payload BYTE-IDENTICAL (excl. generated_utc)",
rc_o3 == rc_r3 == 0 and pay_o == pay_r,
"summary_match=%s" % (pay_o.get("summary") == pay_r.get("summary")))
# ---- [7] non-vacuity: the tally actually saw multiple classes -----------
classes = pay_r.get("summary", {}).get("classes", {})
check("[7] non-vacuity: fixture tallied >1 distinct class",
len(classes) > 1 and all(v > 0 for v in classes.values()),
"classes=%s" % classes)
print("\nverify: %d checks, %d passed, %d failed"
% (len(cells), len(cells) - len(failed), len(failed)))
for f in failed:
print(" FAIL %s" % f)
rc = 0 if not failed else 1
print("\nVERDICT:", "ALL GREEN -- report() behavior preserved"
if rc == 0 else "MISMATCH -- behavior NOT preserved")
return rc
if __name__ == "__main__":
try:
sys.exit(main())
except Mismatch as e:
print("\nVERDICT: MISMATCH -- %s" % e)
sys.exit(1)
@@ -0,0 +1,225 @@
#!/usr/bin/env python3
"""Verification harness for the _smell_check_monotonic_refactored.py refactor.
Exit 0 = all checks pass; exit 1 = at least one failure.
Checks:
1. Refactored file compiles (py_compile).
2. Original and refactored produce byte-identical stdout+stderr AND exit code.
3. ``if __name__ == "__main__"`` guard present (no probe at import time).
4. Magic bound ``400`` replaced by a named constant ``MAX_LEN`` (no bare
``range(0, 400)`` / ``range(400)`` left in code).
5. No ``global`` statements.
6. Importing the refactored module is side-effect-free: it produces NO
stdout/stderr (the original printed ``estimate_tokens monotonic:`` at
import; the refactor must not).
7. The extracted ``find_monotonic_violation`` is correct AND not a tautology:
it returns None on the live (monotonic) chunker, and it returns a real
violation when ``chunker.estimate_tokens`` is replaced by a deliberately
non-monotonic estimator.
"""
from __future__ import annotations
import ast
import difflib
import importlib
import io
import contextlib
import os
import subprocess
import sys
REPO_ROOT = "/root/troupe-standup"
WORK_DIR = os.path.join(REPO_ROOT, "wizard-work", "forge")
ORIGINAL = "_smell_check_monotonic.py"
REFACTORED = "_smell_check_monotonic_refactored.py"
MOD_NAME = REFACTORED[:-3] # strip ".py"
def _run(*args: str) -> subprocess.CompletedProcess:
return subprocess.run(args, cwd=WORK_DIR, capture_output=True, text=True)
def _read(name: str) -> str:
with open(os.path.join(WORK_DIR, name)) as f:
return f.read()
def _code_only(content: str) -> str:
"""Return *content* with the module docstring and comment-only lines removed,
so smell checks inspect code rather than documentation (the refactored
docstring *names* the patterns it removes, e.g. ``400`` / ``range(0, 400)``,
which would be false positives as residual smells)."""
tree = ast.parse(content)
skip: set[int] = set()
if (tree.body
and isinstance(tree.body[0], ast.Expr)
and isinstance(tree.body[0].value, ast.Constant)
and isinstance(tree.body[0].value.value, str)):
ds = tree.body[0]
for ln in range(ds.lineno, (ds.end_lineno or ds.lineno) + 1):
skip.add(ln)
kept = []
for i, line in enumerate(content.splitlines(), 1):
if i in skip or line.lstrip().startswith("#"):
continue
kept.append(line)
return "\n".join(kept)
def check_py_compile() -> bool:
r = _run("python3", "-m", "py_compile", REFACTORED)
if r.returncode != 0:
print(f"FAIL: py_compile failed: {r.stderr}")
return False
print("OK: py_compile passes")
return True
def check_identical_output() -> bool:
orig = _run("python3", ORIGINAL)
ref = _run("python3", REFACTORED)
ot = orig.stdout + orig.stderr
rt = ref.stdout + ref.stderr
if ot != rt:
print("FAIL: original and refactored output differ")
print("".join(difflib.unified_diff(
ot.splitlines(keepends=True), rt.splitlines(keepends=True),
fromfile="original (" + ORIGINAL + ")",
tofile="refactored (" + REFACTORED + ")",
)))
return False
if orig.returncode != ref.returncode:
print(f"FAIL: exit codes differ ({orig.returncode} vs {ref.returncode})")
return False
print(f"OK: identical output (exit {orig.returncode}): {ot.strip()!r}")
return True
def check_main_guard() -> bool:
content = _read(REFACTORED)
if '__name__ == "__main__"' not in content:
print('FAIL: no __main__ guard')
return False
print("OK: __main__ guard present")
return True
def check_no_magic_range() -> bool:
code = _code_only(_read(REFACTORED))
if "range(0, 400)" in code or "range(400)" in code:
print("FAIL: magic 'range(0, 400)' / 'range(400)' still in code")
return False
print("OK: no bare 'range(0, 400)' / 'range(400)' magic literal in code")
return True
def check_named_constant() -> bool:
code = _code_only(_read(REFACTORED))
if "MAX_LEN =" not in code or "MAX_LEN" not in code:
print("FAIL: no MAX_LEN named constant")
return False
print("OK: MAX_LEN named constant present")
return True
def check_no_globals() -> bool:
with open(os.path.join(WORK_DIR, REFACTORED)) as f:
for i, line in enumerate(f.read().splitlines(), 1):
if line.lstrip().startswith("global "):
print(f"FAIL: 'global' found at line {i}: {line.strip()}")
return False
print("OK: no 'global' statements")
return True
def check_no_module_level_flag() -> bool:
"""The module-level mutable ``ok = True`` flag must be gone (folded into the
helper's return value)."""
tree = ast.parse(_read(REFACTORED))
for node in tree.body:
if isinstance(node, ast.Assign):
for t in node.targets:
if isinstance(t, ast.Name) and t.id == "ok":
print("FAIL: module-level mutable 'ok' flag still present")
return False
print("OK: module-level 'ok' flag removed (state folded into helper return)")
return True
def check_import_side_effect_free() -> bool:
"""Importing the refactored module must not run the probe / produce output.
The original printed at import; the refactor must not."""
sys.modules.pop(MOD_NAME, None)
# ensure WORK_DIR importable in this process
if WORK_DIR not in sys.path:
sys.path.insert(0, WORK_DIR)
out, err = io.StringIO(), io.StringIO()
with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
mod = importlib.import_module(MOD_NAME)
if out.getvalue() or err.getvalue():
print(f"FAIL: import produced output: stdout={out.getvalue()!r} stderr={err.getvalue()!r}")
return False
# belt: confirm the probe function exists and main is guarded
if not hasattr(mod, "find_monotonic_violation") or not hasattr(mod, "main"):
print("FAIL: expected find_monotonic_violation / main after import")
return False
print("OK: import is side-effect-free (no probe output at import)")
return True
def check_extracted_function_correct() -> bool:
"""find_monotonic_violation returns None on the live chunker (monotonic) and
returns a real violation on a deliberately non-monotonic estimator -- proving
the search is real and not a hardcoded None."""
if MOD_NAME in sys.modules:
mod = sys.modules[MOD_NAME]
else:
mod = importlib.import_module(MOD_NAME)
# Real property: monotonic on the live chunker.
if mod.find_monotonic_violation() is not None:
print("FAIL: expected None (monotonic) on live chunker")
return False
# Non-tautological control: non-monotonic estimator MUST yield a violation.
def non_monotonic(s: str) -> int:
L = len(s)
if L == 0:
return 0
# Genuinely non-monotonic: short strings cost MORE (999) than long (1),
# so estimate_tokens("a"*1) > estimate_tokens("a"*50) -> a real violation.
return 999 if L < 50 else 1 # shrinks as len grows past 50
orig = mod.chunker.estimate_tokens
mod.chunker.estimate_tokens = non_monotonic
try:
v = mod.find_monotonic_violation(max_len=100)
finally:
mod.chunker.estimate_tokens = orig
if v is None:
print("FAIL: non-monotonic estimator produced no violation (search not real)")
return False
print(f"OK: returns None on live chunker; finds violation {v} on non-monotonic estimator")
return True
def main() -> None:
checks = [
check_py_compile,
check_identical_output,
check_main_guard,
check_no_magic_range,
check_named_constant,
check_no_globals,
check_no_module_level_flag,
check_import_side_effect_free,
check_extracted_function_correct,
]
results = [fn() for fn in checks]
passed = sum(results)
total = len(results)
print(f"\n=== Verification: {passed}/{total} checks passed ===")
if passed != total:
sys.exit(1)
if __name__ == "__main__":
main()
@@ -0,0 +1 @@
python3 "$(dirname "$0")/verify_claims.py"
@@ -0,0 +1,473 @@
# Agentic Memory: A Five-Paper Grounded Dossier
Archive lane -- sphinx. Built 2026-10-25.
Scope: the five most recent arXiv papers matching `all:"agentic memory"`, taken from a single feed snapshot (updated 2026-10-25T00:40:42Z; 583 total results).
Every number below is a verbatim quote from the pinned corpus text. Nothing is paraphrased into a figure. `verify_claims.py` re-derives every quote from the same pinned bytes and fails the build if any drifts.
## Provenance (pinned)
| paper | title | bytes | chars | sha256 (first 16) | claims |
|---|---|---|---|---|---|
| 2610.29671 | Uni-Mem: Learning a Long-Horizon Agentic Memory Manager with Unified Online Policy Optimization | 90083 | 89293 | `b83a772fb6a1340b` | 9 |
| 2610.29823 | MemoryTax: The Bitter Lesson of Agent Memory | 23897 | 23627 | `ed08ec2380a6c147` | 9 |
| 2610.29869 | Compaction Laundering: Provenance Destruction in Context-Compacting Coding Agents | 92858 | 91936 | `e6f78f508c12d7a3` | 7 |
| 2610.30485 | Why Bother Me Again? Benchmarking Personal Agent Memory for Interaction Preferences | 350788 | 340805 | `48e8b2b9c7defc65` | 9 |
| 2610.30755 | Is Complex Agentic Memory Always Needed? Naive RAG Is Stronger Than We Thought | 195193 | 193677 | `2246678ceaf7a42c` | 10 |
## How this dossier is verified
```
python3 verify_claims.py # or: bash CHECK.txt
```
The verifier checks, for each of the 44 claims:
1. the corpus file's sha256 equals the pin recorded at build time;
2. the claim's `quote` is a literal substring of that corpus file;
3. the claim's headline `value` appears inside that quote;
4. the headline `value` appears in this dossier.
It then runs a mutation battery: corrupting one quote at a time in a scratch
copy of `claims.json` must make the verification fail. If any mutation passes
undetected, the build fails -- the check is falsified, not assumed.
## T1: Is designed memory needed at all?
### R1 -- 2610.30755
**Raw-RAG ranks first among compared memory systems in six of seven benchmark-model settings.**
> Raw-RAG ranks first among the compared memory systems in six of seven benchmark–model settings, outperforming the strongest compared agentic memory system by 0.13–5.53 points. We investigate why raw-history ret
*Why it matters:* The headline result: the naive baseline wins most settings.
### R2 -- 2610.30755
**Raw-RAG beats the strongest compared agentic memory system by 0.13–5.53 points.**
> outperforming the strongest compared agentic memory system by 0.13–5.53 points. We investigate why raw-history retrieval remains c
*Why it matters:* Margin is small on LoCoMo/LongMemEval, large on MemoryAgentBench.
### R8 -- 2610.30755
**No memory-management variant improves on Raw-RAG (48.32%).**
> None improves Raw-RAG (48.32%) on the same 14-source MemoryAgentBench Total (Table 6, orange rows). The exact construction and retrieval procedures ar
*Why it matters:* RQ2 answer: added management does not help.
### R9 -- 2610.30755
**Atomic-fact extraction collapses test-time learning from 47.68% to 15.37%.**
> Atomic-fact extraction reduces test-time learning from 47.68% to 15.37% and Total by 19.62 points, consistent with loss or disrupt
*Why it matters:* The most damaging of the three management operations.
### U1 -- 2610.29671
**Uni-Mem reaches an overall LoCoMo JJ of 88.80%.**
> Uni-Mem achieves the best performance across all four question types, with an overall JJ score of 88.80%. It surpasses the strongest baseline, Memory-R2 (OSS), by 10.93 percentage points and Mem-T by 12.31 points, with g
*Why it matters:* Best across all four question types.
### U2 -- 2610.29671
**Uni-Mem surpasses Memory-R2 (OSS) by 10.93 points and Mem-T by 12.31.**
> It surpasses the strongest baseline, Memory-R2 (OSS), by 10.93 percentage points and Mem-T by 12.31 points, with gains of 12.93 and 9.29 points over the strongest competing results on multi-hop and temporal que
*Why it matters:* Gains of 12.93 and 9.29 on multi-hop and temporal.
### U3 -- 2610.29671
**Uni-Mem still reaches 80.99% without SFT, +3.12% over the strongest baseline.**
> Uni-Mem still achieves 80.99% without SFT, outperforming the strongest baseline by 3.12%. These results support the effectiveness of jointly
*Why it matters:* RL alone is enough to beat the baselines.
### U4 -- 2610.29671
**ECA improves overall JJ by 1.29 and 3.03 points without and with SFT.**
> improves overall JJ by 1.29 and 3.03 percentage points without and with SFT, respectively. These gains support the effectiveness o
*Why it matters:* Event-wise credit assignment localizes sparse feedback.
### U5 -- 2610.29671
**Stage-Decoupled GRPO improves overall JJ by 2.48 points without SFT.**
> improves overall JJ by 2.48 percentage points without SFT. In particular, temporal accuracy rises by 10.55 points in this setting, and multi
*Why it matters:* Temporal accuracy rises 10.55 points in this setting.
### U6 -- 2610.29671
**Trained construction alone improves overall JJ by 8.18 points.**
> improves the overall JJ score by 8.18 percentage points, demonstrating the increased downstream utility of the constructed memorie
*Why it matters:* Each capability contributes even paired with its untrained counterpart.
### U7 -- 2610.29671
**Extending update blocks 1:1 -> 3:2 reduces overall JJ from 88.80% to 83.75%.**
> the overall JJ score from 88.80%88.80\% to 83.75%83.75\%, indicating that less frequent alternation hinders shared-polic
*Why it matters:* Frequent alternation matters as much as balanced allocation.
## T2: Cost, ceiling, and scale
### R10 -- 2610.30755
**Full-context beats Raw-RAG by 19.98 points on DeepSeek-V4-Flash MEME.**
> Full-context leads Raw-RAG by 19.98 points in that setting. Raw-RAG is competitive across these tasks despite its simpler ingestio
*Why it matters:* The one setting where the naive baseline is not first.
### M1 -- 2610.29823
**Study scope: one LoCoMo conversation, 419 turns, 152 questions.**
> On one LoCoMo conversation (419 turns, 152 questions), we run four memory methods (full context, BM25, Mem0, Zep) and three coding agents (Pi, Codex, Claude Cod
*Why it matters:* Deliberately narrow: one conversation, one run per cell.
### M2 -- 2610.29823
**With GPT-4o-mini, Mem0 scores 78% vs 74% for full context and BM25.**
> With GPT-4o-mini, Mem0 scores highest. It scores 78%, against 74% for both full context and BM25; Zep scores 43%. The agents score 63% (Pi), 28% (Codex) and 29%
*Why it matters:* Agent memory is a patch for a weak model.
### M3 -- 2610.29823
**With GPT-6 Luna, full context scores 92%, BM25 82%, Mem0 80%, Zep 51%.**
> With GPT-6 Luna, full context scores 92%, BM25 82%, Mem0 80% and Zep 51%; Zep’s graph holds 128 facts from the 19 sessions, against 195 with Claude Opus 5.5. With Claude Opus 5.5, Mem0 and full contex
*Why it matters:* The patch stops helping as the model gets stronger.
### M4 -- 2610.29823
**With Claude Opus 5.5, agents score highest: Pi 99%, Codex 98%, Claude Code 97%.**
> With Claude Opus 5.5, Mem0 and full context both score 95% and Zep 87%, and the agents score highest: Pi 99%, Codex 98% and Claude Code 97%, a lead that comes mostly from
*Why it matters:* Searching the raw record beats reading all of it at the top end.
### M5 -- 2610.29823
**Pi scores 99% for $6.95/rollout vs full context 95% for $17.84.**
> Pi scores 99% for $6.95 a rollout, while full context with the same model scores 95% for $17.84. Pi reads about 10,000 input tokens a question, 7,900 of them ca
*Why it matters:* Less than half the cost for a higher score.
### M7 -- 2610.29823
**Pi reads ~10,000 input tokens/question (7,900 cached) vs 28,900 for full context.**
> Pi reads about 10,000 input tokens a question, 7,900 of them cached, against 28,900 for full context. Codex and Claude Code score 98% and 97% for $25.
*Why it matters:* The efficiency mechanism behind the cost lead.
### U8 -- 2610.29671
**SFT followed by RL improves the 4B manager's JJ from 76.58% to 88.80%.**
> SFT followed by RL improves the 4B manager’s JJ score from 76.58% to 88.80%. Uni-Mem also outperforms all off-the-shelf managers, including the strong
*Why it matters:* A small trained manager beats Qwen3.5-27B (87.05%) and 397B-A17B (85.86%).
### U9 -- 2610.29671
**Uni-Mem outperforms all off-the-shelf managers including Qwen3.5-27B (87.05%).**
> Uni-Mem also outperforms all off-the-shelf managers, including the strongest one, Qwen3.5-27B (87.05%), and the largest one, Qwen3.5-397B-A17B (85.86%). These results demonstrate that a small manager trained jo
*Why it matters:* Scale is not the binding constraint; joint training is.
## T3: Provenance: what compaction destroys and what writers omit
### M8 -- 2610.29823
**Writer ceiling: on Zep's GPT-4o-mini graph readers score 43/43/45%; on Opus 5.5's, 66/71/87%.**
> GPT-4o-mini graph, GPT-4o-mini, GPT-6 Luna and Claude Opus 5.5 score 43%, 43% and 45%; on the graph Claude Opus 5.5 wrote, 66%, 71% and 87%. Mem0 keeps the order with smaller gaps: 78%, 83% and 84% from GPT-4o-
*Why it matters:* A stronger reader cannot recover what a weaker writer left out.
### M9 -- 2610.29823
**Pi's one miss is a LoCoMo answer-key error, not a memory failure.**
> Its one miss is LoCoMo’s mistake. The question asks what kind of painting Caroline shared with Melanie on 13 October 2023, and the answer key says an abstract painting with blue streaks. In the conversation it is Melanie who shares that painting; Caroline shares a drawing. Pi says so and is marked wrong: “Caroline shared a drawing, not a painting. … The paintings shared that day (a sunset and an abstract piece) were Melanie’s.” With Claude Opus 5.5, full context, Codex and Claude Code point out the same mistake and are marked wrong too. On the other 151 questions, Pi with Claude Opus
*Why it matters:* The model is right and the benchmark key is wrong.
### C1 -- 2610.29869
**Forged approval launders into memory 87–100% of the time across five flattening harnesses.**
> the forged approval launders into the agent’s memory 87–100% of the time, up to a ~908,000-token context window; across four role-typed harnesses (Codex, Gemini CLI, Qwen Code, Continue) the identical attack su
*Why it matters:* Up to a ~908,000-token context window.
### C2 -- 2610.29869
**The identical attack succeeds 0% of the time across four role-typed harnesses.**
> succeeds 0% of the time. We follow the attack to executed consequences. A laundered standing approval drives real tool-running age
*Why it matters:* Role-typing is a built-in control.
### C3 -- 2610.29869
**Live in the real pi TUI the attack runs at ~50% against a 0% paired clean arm.**
> live in the real pi TUI at ~50% against a 0% paired clean arm. Straiker [1] described this consequence without runtime evidence, a
*Why it matters:* Runtime evidence, not a simulation.
### C4 -- 2610.29869
**A code-level provenance check catches 77% of laundering, demoting 2% of legitimate content.**
> we measure a code-level provenance check that catches 77% of laundering while demoting 2% of legitimate summary content. The defect also has a straightforward f
*Why it matters:* The only defense that works; prompt-level defenses cannot restore provenance.
### C5 -- 2610.29869
**In-prompt anti-injection rules stop 13–25%; published classifier defenses report 67% at best.**
> stops 13–25% on the same data, and published classifier defenses report 67% at best. The defect also has a straightforward fix on the flatte
*Why it matters:* Baselines are far weaker than the code-level check.
### C6 -- 2610.29869
**Laundering still succeeds 100% of the time at every context size.**
> The laundering still succeeds 100% of the time at every size, and the executed rate, near-100% on the short transcripts, settles to ~20–40% in a full window. Th
*Why it matters:* Size does not attenuate the attack.
### C7 -- 2610.29869
**Executed rate settles to ~20–40% in a full window.**
> settles to ~20–40% in a full window. The short runs therefore measure the attack at its most favorable, an upper bound on the exec
*Why it matters:* Real agents compact at this size, so this is the realistic case.
## T4: What raw evidence must be preserved
### R3 -- 2610.30755
**Mem0's UPDATE+DELETE account for only 4.39% of 33,902 LoCoMo write decisions.**
> together account for only 4.39% of the 33,902 write decisions on LoCoMo and 2.46% of the 428,858 write decisions on LongMemEval (Figure 2);
*Why it matters:* Mutation is rare in practice -- most proposed writes are duplicates or malformed.
### R4 -- 2610.30755
**Replacing UPDATE/DELETE with append-only ADD/NONE raises LoCoMo accuracy 67.36% -> 69.42%.**
> Replacing UPDATE with append-only ADD and DELETE with NONE raises LoCoMo accuracy from 67.36% to 69.42%, without reducing any category score (Table 2). Explicit mutation
*Why it matters:* Explicit mutation gives no accuracy gain; the simpler store is better.
### R5 -- 2610.30755
**Removing reranking lowers MemoryAgentBench Total by 3.92 points.**
> Removing reranking lowers Total by 3.92 points (pHolm=0.0012p_{\mathrm{Holm}}=0.0012), while returning only one item lowers it by 11.73 points. Final-kk values of 5, 10, 30, and 60 stay with
*Why it matters:* Reranking is the one component worth keeping.
### R6 -- 2610.30755
**Returning only one item lowers Total by 11.73 points.**
> while returning only one item lowers it by 11.73 points. Final-kk values of 5, 10, 30, and 60 stay within 1.3 points of the baseli
*Why it matters:* Retrieval depth matters far more than chunk size.
### R7 -- 2610.30755
**Top-120 retrieval loses 14.57 points.**
> Top-120 loses 14.57 points, driven by long-range understanding and conflict resolution. Chunk sizes from 128 to 1,024 do not signi
*Why it matters:* Driven by long-range understanding and conflict resolution.
### M6 -- 2610.29823
**On the 88 questions without photo evidence, Pi scores 100%.**
> On the 88 questions without photo evidence, Pi scores 100%, and full context, Mem0, Codex and Claude Code 99% each (Table 4). With the same input, then, Pi matc
*Why it matters:* Pi matches full context once captions are removed from the comparison.
## T5: Interaction preferences are a different problem
### P1 -- 2610.30485
**MemPABench has four simulated users holding 14 preferences.**
> Four grounded simulated users hold 14 preferences across work and personal contexts. Their feedback spans 99 sessions over four months, with
*Why it matters:* Preferences span work and personal contexts.
### P2 -- 2610.30485
**Feedback spans 99 sessions over four months.**
> Their feedback spans 99 sessions over four months, with five event-driven changes per user testing memory updates. We evaluate pre
*Why it matters:* Five event-driven preference changes per user.
### P3 -- 2610.30485
**No Memory reaches 49.6% PSA against 45.2% random.**
> No Memory, where the PA selects by its defaults, reaches 49.6%, compared with 45.2% for random selection (Appendix A.1). Only File memory adds a clear gain. It raises PSA to 59.2%, a mean ga
*Why it matters:* Defaults alone nearly match chance-plus-margin.
### P4 -- 2610.30485
**File memory raises PSA to 59.2%, +9.6 points over No Memory.**
> It raises PSA to 59.2%, a mean gain of 9.6 points over No Memory (95% cluster-bootstrap interval [+3.9, +15.6]). PERMA (Liu et al., 2026), a benchmark of content preferen
*Why it matters:* The only condition with a clear gain; CI [+3.9, +15.6].
### P5 -- 2610.30485
**Mem0, MemOS and Simple RAG stay at No Memory level (PSA 50.0/50.0/49.9).**
> with PSA of 50.0%, 50.0%, and 49.9%. The intervals of all conditions and their comparisons with No Memory on each user–backbone pa
*Why it matters:* Retrieval-based memory does not beat having none.
### P6 -- 2610.30485
**File memory PET is 25.4%, more than twice No Memory's 11.2%.**
> Its PET is 25.4%, more than twice the 11.2% of No Memory, and it is higher on every backbone. However, the CS does not improve, at 14.4% against 21.8% (Appendix G.3). In
*Why it matters:* But its CS does not improve (14.4% vs 21.8%): context is not tracked.
### P7 -- 2610.30485
**Oracle lifts correct selection 33%->96% but BRS>=4 only 24%->59%.**
> Oracle raises correct selections of the five settings from 33% to 96%, but the share of replies with a BRS of 4 or higher rises only from 24% to 59%. Even with the IX Wrapper, the PA still writes too much. Afte
*Why it matters:* Knowing the preference does not override the model's interaction habits.
### P8 -- 2610.30485
**Information Flow is the hardest dimension, PSA 34–44%.**
> Information Flow is the hardest under every condition (Figure 8). Its PSA stays at 34–44%, the lowest of the four, while Expression Style (57–69%) and Disclosur
*Why it matters:* Expression Style (57-69%) and Disclosure (55-58%) are easier.
### P9 -- 2610.30485
**Anonymizing users drops shortcut accuracy 53.2% -> 46.4%, near the 45.2% chance.**
> reaches 53.2% accuracy from the source identity but 46.4% from the anonymized profile, close to the 45.2% chance level (Figure 10; Appendix
*Why it matters:* Confirms the benchmark measures memory, not name-guessing.
## Cross-paper synthesis
Four of the five papers converge on one answer: **the elaborate part of agent memory is not where the value is.**
- **Raw-RAG wins most settings** (2610.30755) -- naive dense retrieval over raw history ranks first in six of seven benchmark-model settings, and *no* added memory-management operation improves on it. The one setting where it loses is the one where full context is affordable.
- **Mutation is nearly free to drop** (2610.30755) -- UPDATE+DELETE are 4.39% of LoCoMo writes and 2.46% of LongMemEval writes; replacing them with append-only ADD/NONE *raises* accuracy 67.36% -> 69.42%.
- **Agent memory is a patch for a weak model** (2610.29823) -- Mem0 helps GPT-4o-mini (78% vs 74%) but not GPT-6 Luna or Claude Opus 5.5, and at the top end Pi scores 99% for $6.95 against full context's 95% for $17.84.
- **A stronger reader does not recover what a weaker writer left out** (2610.29823) -- on Zep's GPT-4o-mini graph the three readers score 43/43/45%; on the graph Claude Opus 5.5 wrote they score 66/71/87%. Memory has a ceiling set by its writer.
- **What must be preserved is raw evidence, not extracted facts** (2610.30755, 2610.29823) -- verbatim chunks beat lossy artifacts; on the 88 questions with no photo evidence, Pi's lead over full context vanishes (100% vs 99%).
- **Compaction destroys provenance deterministically** (2610.29869) -- a forged approval launders into memory 87-100% of the time in flattening harnesses and 0% in role-typed ones; no prompt-level defense restores it, only a code-level check (77% catch, 2% collateral).
- **Interaction preferences are a harder problem than factual recall** (2610.30485) -- retrieval-based memory does not beat No Memory (PSA 50.0/50.0/49.9 vs 49.6%), and even an Oracle that hands the agent the right preference lifts delivered behavior only 24% -> 59%.
- **The counter-example is training the memory manager itself** (2610.29671) -- Uni-Mem jointly learns construction and retrieval and reaches 88.80% JJ, beating Memory-R2 (OSS) by 10.93 points and off-the-shelf Qwen3.5-27B (87.05%). The gain comes from the training scheme (Stage-Decoupled GRPO + ECA), not from a bigger model.
## Where the five disagree
2610.30755 and 2610.29823 both conclude that raw-history search is competitive, but they disagree on *why the alternative fails*: 2610.30755 blames the rarity of useful mutation (4.39% of writes), while 2610.29823 blames the writer's ceiling. These are compatible -- a weak writer produces few useful mutations *and* a lossy store -- but neither paper measures the other's mechanism, so the dossier records the disagreement rather than resolving it.
2610.30485 is the outlier on the headline question: it studies *interaction* preferences, not factual recall, and finds memory nearly useless there. That is a different task family, not a refutation of the other four.
## Claim ledger
44 claims across 5 papers (2610.29671:9, 2610.29823:9, 2610.29869:7, 2610.30485:9, 2610.30755:10).
| id | paper | theme | value |
|---|---|---|---|
| R1 | 2610.30755 | T1 | six of seven |
| R2 | 2610.30755 | T1 | 0.13–5.53 points |
| R3 | 2610.30755 | T4 | 4.39% |
| R4 | 2610.30755 | T4 | 67.36% to 69.42% |
| R5 | 2610.30755 | T4 | 3.92 points |
| R6 | 2610.30755 | T4 | 11.73 points |
| R7 | 2610.30755 | T4 | 14.57 points |
| R8 | 2610.30755 | T1 | 48.32% |
| R9 | 2610.30755 | T1 | 47.68% to 15.37% |
| R10 | 2610.30755 | T2 | 19.98 points |
| P1 | 2610.30485 | T5 | 14 preferences |
| P2 | 2610.30485 | T5 | 99 sessions over four months |
| P3 | 2610.30485 | T5 | 49.6% |
| P4 | 2610.30485 | T5 | 59.2% |
| P5 | 2610.30485 | T5 | 50.0%, 50.0%, and 49.9% |
| P6 | 2610.30485 | T5 | 25.4% |
| P7 | 2610.30485 | T5 | 33% to 96% |
| P8 | 2610.30485 | T5 | 34–44% |
| P9 | 2610.30485 | T5 | 53.2% |
| M1 | 2610.29823 | T2 | 419 turns, 152 questions |
| M2 | 2610.29823 | T2 | 78% |
| M3 | 2610.29823 | T2 | 92% |
| M4 | 2610.29823 | T2 | Pi 99%, Codex 98% and Claude Code 97% |
| M5 | 2610.29823 | T2 | $6.95 |
| M6 | 2610.29823 | T4 | 88 questions without photo evidence |
| M7 | 2610.29823 | T2 | 10,000 input tokens |
| M8 | 2610.29823 | T3 | 43%, 43% and 45% |
| M9 | 2610.29823 | T3 | 151 questions |
| C1 | 2610.29869 | T3 | 87–100% |
| C2 | 2610.29869 | T3 | 0% of the time |
| C3 | 2610.29869 | T3 | ~50% |
| C4 | 2610.29869 | T3 | 77% |
| C5 | 2610.29869 | T3 | 13–25% |
| C6 | 2610.29869 | T3 | 100% |
| C7 | 2610.29869 | T3 | ~20–40% |
| U1 | 2610.29671 | T1 | 88.80% |
| U2 | 2610.29671 | T1 | 10.93 percentage points |
| U3 | 2610.29671 | T1 | 80.99% |
| U4 | 2610.29671 | T1 | 1.29 and 3.03 percentage points |
| U5 | 2610.29671 | T1 | 2.48 percentage points |
| U6 | 2610.29671 | T1 | 8.18 percentage points |
| U7 | 2610.29671 | T1 | 83.75% |
| U8 | 2610.29671 | T2 | 76.58% to 88.80% |
| U9 | 2610.29671 | T2 | 87.05% |
@@ -0,0 +1,465 @@
#!/usr/bin/env python3
"""
build_dossier.py -- build the 5-paper agentic-memory grounded dossier.
Every quote in the dossier is EXTRACTED from the pinned corpus bytes
(anchor + span), never hand-typed, so the dossier cannot drift from its
sources. verify_claims.py re-checks each quote against the same pinned
bytes and runs a mutation battery proving the check is not vacuous.
Run: python3 build_dossier.py
"""
from __future__ import annotations
import hashlib
import json
from pathlib import Path
ROOT = Path(__file__).resolve().parent
CORPUS = ROOT / "corpus"
PAPERS = {
"2610.29671": "Uni-Mem: Learning a Long-Horizon Agentic Memory Manager with Unified Online Policy Optimization",
"2610.29823": "MemoryTax: The Bitter Lesson of Agent Memory",
"2610.29869": "Compaction Laundering: Provenance Destruction in Context-Compacting Coding Agents",
"2610.30485": "Why Bother Me Again? Benchmarking Personal Agent Memory for Interaction Preferences",
"2610.30755": "Is Complex Agentic Memory Always Needed? Naive RAG Is Stronger Than We Thought",
}
# id, paper, theme, claim, value (headline number, must be inside the quote),
# anchor (verbatim start marker in the paper text), span (chars to take), note.
SPEC = [
# ---------------- 2610.30755 -- Raw-RAG / naive RAG ----------------
dict(id="R1", paper="2610.30755", theme="T1",
claim="Raw-RAG ranks first among compared memory systems in six of seven benchmark-model settings.",
value="six of seven",
anchor="Raw-RAG ranks first among the compared memory systems in six",
span=210,
note="The headline result: the naive baseline wins most settings."),
dict(id="R2", paper="2610.30755", theme="T1",
claim="Raw-RAG beats the strongest compared agentic memory system by 0.13–5.53 points.",
value="0.13–5.53 points",
anchor="outperforming the strongest compared agentic memory system by 0.13",
span=130,
note="Margin is small on LoCoMo/LongMemEval, large on MemoryAgentBench."),
dict(id="R3", paper="2610.30755", theme="T4",
claim="Mem0's UPDATE+DELETE account for only 4.39% of 33,902 LoCoMo write decisions.",
value="4.39%",
anchor="together account for only 4.39% of the 33,902 write decisions on LoCoMo",
span=140,
note="Mutation is rare in practice -- most proposed writes are duplicates or malformed."),
dict(id="R4", paper="2610.30755", theme="T4",
claim="Replacing UPDATE/DELETE with append-only ADD/NONE raises LoCoMo accuracy 67.36% -> 69.42%.",
value="67.36% to 69.42%",
anchor="Replacing UPDATE with append-only ADD and DELETE with NONE raises",
span=170,
note="Explicit mutation gives no accuracy gain; the simpler store is better."),
dict(id="R5", paper="2610.30755", theme="T4",
claim="Removing reranking lowers MemoryAgentBench Total by 3.92 points.",
value="3.92 points",
anchor="Removing reranking lowers Total by 3.92 points",
span=190,
note="Reranking is the one component worth keeping."),
dict(id="R6", paper="2610.30755", theme="T4",
claim="Returning only one item lowers Total by 11.73 points.",
value="11.73 points",
anchor="while returning only one item lowers it by 11.73 points",
span=130,
note="Retrieval depth matters far more than chunk size."),
dict(id="R7", paper="2610.30755", theme="T4",
claim="Top-120 retrieval loses 14.57 points.",
value="14.57 points",
anchor="Top-120 loses 14.57 points",
span=130,
note="Driven by long-range understanding and conflict resolution."),
dict(id="R8", paper="2610.30755", theme="T1",
claim="No memory-management variant improves on Raw-RAG (48.32%).",
value="48.32%",
anchor="None improves Raw-RAG (48.32%)",
span=150,
note="RQ2 answer: added management does not help."),
dict(id="R9", paper="2610.30755", theme="T1",
claim="Atomic-fact extraction collapses test-time learning from 47.68% to 15.37%.",
value="47.68% to 15.37%",
anchor="Atomic-fact extraction reduces test-time learning from 47.68% to 15.37%",
span=130,
note="The most damaging of the three management operations."),
dict(id="R10", paper="2610.30755", theme="T2",
claim="Full-context beats Raw-RAG by 19.98 points on DeepSeek-V4-Flash MEME.",
value="19.98 points",
anchor="Full-context leads Raw-RAG by 19.98 points",
span=130,
note="The one setting where the naive baseline is not first."),
# ---------------- 2610.30485 -- MemPABench ----------------
dict(id="P1", paper="2610.30485", theme="T5",
claim="MemPABench has four simulated users holding 14 preferences.",
value="14 preferences",
anchor="Four grounded simulated users hold 14 preferences",
span=140,
note="Preferences span work and personal contexts."),
dict(id="P2", paper="2610.30485", theme="T5",
claim="Feedback spans 99 sessions over four months.",
value="99 sessions over four months",
anchor="Their feedback spans 99 sessions over four months",
span=130,
note="Five event-driven preference changes per user."),
dict(id="P3", paper="2610.30485", theme="T5",
claim="No Memory reaches 49.6% PSA against 45.2% random.",
value="49.6%",
anchor="No Memory, where the PA selects by its defaults, reaches 49.6%",
span=190,
note="Defaults alone nearly match chance-plus-margin."),
dict(id="P4", paper="2610.30485", theme="T5",
claim="File memory raises PSA to 59.2%, +9.6 points over No Memory.",
value="59.2%",
anchor="It raises PSA to 59.2%",
span=170,
note="The only condition with a clear gain; CI [+3.9, +15.6]."),
dict(id="P5", paper="2610.30485", theme="T5",
claim="Mem0, MemOS and Simple RAG stay at No Memory level (PSA 50.0/50.0/49.9).",
value="50.0%, 50.0%, and 49.9%",
anchor="with PSA of 50.0%, 50.0%, and 49.9%",
span=130,
note="Retrieval-based memory does not beat having none."),
dict(id="P6", paper="2610.30485", theme="T5",
claim="File memory PET is 25.4%, more than twice No Memory's 11.2%.",
value="25.4%",
anchor="Its PET is 25.4%",
span=170,
note="But its CS does not improve (14.4% vs 21.8%): context is not tracked."),
dict(id="P7", paper="2610.30485", theme="T5",
claim="Oracle lifts correct selection 33%->96% but BRS>=4 only 24%->59%.",
value="33% to 96%",
anchor="Oracle raises correct selections of the five settings",
span=210,
note="Knowing the preference does not override the model's interaction habits."),
dict(id="P8", paper="2610.30485", theme="T5",
claim="Information Flow is the hardest dimension, PSA 34–44%.",
value="34–44%",
anchor="Information Flow is the hardest under every condition",
span=160,
note="Expression Style (57-69%) and Disclosure (55-58%) are easier."),
dict(id="P9", paper="2610.30485", theme="T5",
claim="Anonymizing users drops shortcut accuracy 53.2% -> 46.4%, near the 45.2% chance.",
value="53.2%",
anchor="reaches 53.2% accuracy from the source identity but 46.4%",
span=140,
note="Confirms the benchmark measures memory, not name-guessing."),
# ---------------- 2610.29823 -- MemoryTax ----------------
dict(id="M1", paper="2610.29823", theme="T2",
claim="Study scope: one LoCoMo conversation, 419 turns, 152 questions.",
value="419 turns, 152 questions",
anchor="On one LoCoMo conversation (419 turns, 152 questions)",
span=160,
note="Deliberately narrow: one conversation, one run per cell."),
dict(id="M2", paper="2610.29823", theme="T2",
claim="With GPT-4o-mini, Mem0 scores 78% vs 74% for full context and BM25.",
value="78%",
anchor="With GPT-4o-mini, Mem0 scores highest",
span=160,
note="Agent memory is a patch for a weak model."),
dict(id="M3", paper="2610.29823", theme="T2",
claim="With GPT-6 Luna, full context scores 92%, BM25 82%, Mem0 80%, Zep 51%.",
value="92%",
anchor="With GPT-6 Luna, full context scores 92%",
span=200,
note="The patch stops helping as the model gets stronger."),
dict(id="M4", paper="2610.29823", theme="T2",
claim="With Claude Opus 5.5, agents score highest: Pi 99%, Codex 98%, Claude Code 97%.",
value="Pi 99%, Codex 98% and Claude Code 97%",
anchor="With Claude Opus 5.5, Mem0 and full context both score 95%",
span=170,
note="Searching the raw record beats reading all of it at the top end."),
dict(id="M5", paper="2610.29823", theme="T2",
claim="Pi scores 99% for $6.95/rollout vs full context 95% for $17.84.",
value="$6.95",
anchor="Pi scores 99% for $6.95 a rollout",
span=160,
note="Less than half the cost for a higher score."),
dict(id="M6", paper="2610.29823", theme="T4",
claim="On the 88 questions without photo evidence, Pi scores 100%.",
value="88 questions without photo evidence",
anchor="On the 88 questions without photo evidence",
span=160,
note="Pi matches full context once captions are removed from the comparison."),
dict(id="M7", paper="2610.29823", theme="T2",
claim="Pi reads ~10,000 input tokens/question (7,900 cached) vs 28,900 for full context.",
value="10,000 input tokens",
anchor="Pi reads about 10,000 input tokens a question",
span=150,
note="The efficiency mechanism behind the cost lead."),
dict(id="M8", paper="2610.29823", theme="T3",
claim="Writer ceiling: on Zep's GPT-4o-mini graph readers score 43/43/45%; on Opus 5.5's, 66/71/87%.",
value="43%, 43% and 45%",
anchor="GPT-4o-mini graph, GPT-4o-mini, GPT-6 Luna and Claude Opus 5.5 score 43%, 43% and 45%",
span=210,
note="A stronger reader cannot recover what a weaker writer left out."),
dict(id="M9", paper="2610.29823", theme="T3",
claim="Pi's one miss is a LoCoMo answer-key error, not a memory failure.",
value="151 questions",
anchor="Its one miss is LoCoMo",
span=590,
note="The model is right and the benchmark key is wrong."),
# ---------------- 2610.29869 -- Compaction Laundering ----------------
dict(id="C1", paper="2610.29869", theme="T3",
claim="Forged approval launders into memory 87–100% of the time across five flattening harnesses.",
value="87–100%",
anchor="the forged approval launders into the",
span=210,
note="Up to a ~908,000-token context window."),
dict(id="C2", paper="2610.29869", theme="T3",
claim="The identical attack succeeds 0% of the time across four role-typed harnesses.",
value="0% of the time",
anchor="succeeds 0% of the time",
span=130,
note="Role-typing is a built-in control."),
dict(id="C3", paper="2610.29869", theme="T3",
claim="Live in the real pi TUI the attack runs at ~50% against a 0% paired clean arm.",
value="~50%",
anchor="live in the real pi TUI at ~50%",
span=130,
note="Runtime evidence, not a simulation."),
dict(id="C4", paper="2610.29869", theme="T3",
claim="A code-level provenance check catches 77% of laundering, demoting 2% of legitimate content.",
value="77%",
anchor="we measure a code-level provenance check that catches 77% of",
span=160,
note="The only defense that works; prompt-level defenses cannot restore provenance."),
dict(id="C5", paper="2610.29869", theme="T3",
claim="In-prompt anti-injection rules stop 13–25%; published classifier defenses report 67% at best.",
value="13–25%",
anchor="stops 13",
span=140,
note="Baselines are far weaker than the code-level check."),
dict(id="C6", paper="2610.29869", theme="T3",
claim="Laundering still succeeds 100% of the time at every context size.",
value="100%",
anchor="The laundering still succeeds 100% of the",
span=160,
note="Size does not attenuate the attack."),
dict(id="C7", paper="2610.29869", theme="T3",
claim="Executed rate settles to ~20–40% in a full window.",
value="~20–40%",
anchor="settles to ~20",
span=130,
note="Real agents compact at this size, so this is the realistic case."),
# ---------------- 2610.29671 -- Uni-Mem ----------------
dict(id="U1", paper="2610.29671", theme="T1",
claim="Uni-Mem reaches an overall LoCoMo JJ of 88.80%.",
value="88.80%",
anchor="Uni-Mem achieves the best performance across all four question types",
span=220,
note="Best across all four question types."),
dict(id="U2", paper="2610.29671", theme="T1",
claim="Uni-Mem surpasses Memory-R2 (OSS) by 10.93 points and Mem-T by 12.31.",
value="10.93 percentage points",
anchor="It surpasses the strongest baseline, Memory-R2 (OSS), by 10.93",
span=210,
note="Gains of 12.93 and 9.29 on multi-hop and temporal."),
dict(id="U3", paper="2610.29671", theme="T1",
claim="Uni-Mem still reaches 80.99% without SFT, +3.12% over the strongest baseline.",
value="80.99%",
anchor="Uni-Mem still achieves 80.99% without SFT",
span=140,
note="RL alone is enough to beat the baselines."),
dict(id="U4", paper="2610.29671", theme="T1",
claim="ECA improves overall JJ by 1.29 and 3.03 points without and with SFT.",
value="1.29 and 3.03 percentage points",
anchor="improves overall JJ by 1.29 and 3.03 percentage points",
span=130,
note="Event-wise credit assignment localizes sparse feedback."),
dict(id="U5", paper="2610.29671", theme="T1",
claim="Stage-Decoupled GRPO improves overall JJ by 2.48 points without SFT.",
value="2.48 percentage points",
anchor="improves overall JJ by 2.48 percentage points without SFT",
span=140,
note="Temporal accuracy rises 10.55 points in this setting."),
dict(id="U6", paper="2610.29671", theme="T1",
claim="Trained construction alone improves overall JJ by 8.18 points.",
value="8.18 percentage points",
anchor="improves the overall JJ score by 8.18 percentage points",
span=130,
note="Each capability contributes even paired with its untrained counterpart."),
dict(id="U7", paper="2610.29671", theme="T1",
claim="Extending update blocks 1:1 -> 3:2 reduces overall JJ from 88.80% to 83.75%.",
value="83.75%",
anchor="the overall JJ score from 88.80",
span=120,
note="Frequent alternation matters as much as balanced allocation."),
dict(id="U8", paper="2610.29671", theme="T2",
claim="SFT followed by RL improves the 4B manager's JJ from 76.58% to 88.80%.",
value="76.58% to 88.80%",
anchor="SFT followed by RL improves the 4B manager",
span=150,
note="A small trained manager beats Qwen3.5-27B (87.05%) and 397B-A17B (85.86%)."),
dict(id="U9", paper="2610.29671", theme="T2",
claim="Uni-Mem outperforms all off-the-shelf managers including Qwen3.5-27B (87.05%).",
value="87.05%",
anchor="Uni-Mem also outperforms all off-the-shelf managers",
span=210,
note="Scale is not the binding constraint; joint training is."),
]
THEMES = {
"T1": "Is designed memory needed at all?",
"T2": "Cost, ceiling, and scale",
"T3": "Provenance: what compaction destroys and what writers omit",
"T4": "What raw evidence must be preserved",
"T5": "Interaction preferences are a different problem",
}
def sha256(p: Path) -> str:
return hashlib.sha256(p.read_bytes()).hexdigest()
def norm(s: str) -> str:
return " ".join(s.split())
def main() -> int:
texts, pins, sizes = {}, {}, {}
for pid in PAPERS:
p = CORPUS / f"{pid}.txt"
texts[pid] = p.read_text(encoding="utf-8")
pins[pid] = sha256(p)
sizes[pid] = p.stat().st_size
claims, seen = [], set()
for s in SPEC:
t = texts[s["paper"]]
idx = t.find(s["anchor"])
if idx < 0:
raise SystemExit(f"ANCHOR NOT FOUND: {s['id']} :: {s['anchor']!r}")
quote = t[idx: idx + s["span"]]
if s["value"] not in quote:
raise SystemExit(f"VALUE NOT IN QUOTE: {s['id']} :: {s['value']!r}")
if s["id"] in seen:
raise SystemExit(f"DUPLICATE ID: {s['id']}")
seen.add(s["id"])
claims.append(dict(
id=s["id"], paper=s["paper"], theme=s["theme"],
claim=s["claim"], value=s["value"], note=s["note"],
anchor=s["anchor"], quote=quote,
corpus_sha256=pins[s["paper"]],
))
by_paper = {pid: sum(1 for c in claims if c["paper"] == pid) for pid in PAPERS}
# ---- claims.json ----
(ROOT / "claims.json").write_text(json.dumps(dict(
built="2026-10-25",
query='all:"agentic memory"',
feed_updated="2026-10-25T00:40:42Z",
feed_total_results=583,
papers=[dict(arxiv_id=pid, title=PAPERS[pid], bytes=sizes[pid],
chars=len(texts[pid]), sha256=pins[pid],
claims=by_paper[pid]) for pid in PAPERS],
themes=THEMES,
claims=claims,
), indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
# ---- DOSSIER.md ----
L = []
L.append("# Agentic Memory: A Five-Paper Grounded Dossier\n")
L.append("Archive lane -- sphinx. Built 2026-10-25.\n")
L.append('Scope: the five most recent arXiv papers matching `all:"agentic memory"`, '
"taken from a single feed snapshot (updated 2026-10-25T00:40:42Z; 583 total results).\n")
L.append("Every number below is a verbatim quote from the pinned corpus text. "
"Nothing is paraphrased into a figure. `verify_claims.py` re-derives every "
"quote from the same pinned bytes and fails the build if any drifts.\n")
L.append("## Provenance (pinned)\n")
L.append("| paper | title | bytes | chars | sha256 (first 16) | claims |")
L.append("|---|---|---|---|---|---|")
for pid in PAPERS:
L.append(f"| {pid} | {PAPERS[pid]} | {sizes[pid]} | {len(texts[pid])} | "
f"`{pins[pid][:16]}` | {by_paper[pid]} |")
L.append("")
L.append("## How this dossier is verified\n")
L.append("```")
L.append("python3 verify_claims.py # or: bash CHECK.txt")
L.append("```")
L.append("")
L.append("The verifier checks, for each of the %d claims:" % len(claims))
L.append("1. the corpus file's sha256 equals the pin recorded at build time;")
L.append("2. the claim's `quote` is a literal substring of that corpus file;")
L.append("3. the claim's headline `value` appears inside that quote;")
L.append("4. the headline `value` appears in this dossier.")
L.append("")
L.append("It then runs a mutation battery: corrupting one quote at a time in a scratch")
L.append("copy of `claims.json` must make the verification fail. If any mutation passes")
L.append("undetected, the build fails -- the check is falsified, not assumed.\n")
for tid, tname in THEMES.items():
L.append(f"## {tid}: {tname}\n")
for c in [c for c in claims if c["theme"] == tid]:
L.append(f"### {c['id']} -- {c['paper']}\n")
L.append(f"**{c['claim']}**\n")
L.append(f"> {norm(c['quote'])}\n")
L.append(f"*Why it matters:* {c['note']}\n")
L.append("## Cross-paper synthesis\n")
L.append("Four of the five papers converge on one answer: **the elaborate part of agent "
"memory is not where the value is.**\n")
L.append("- **Raw-RAG wins most settings** (2610.30755) -- naive dense retrieval over raw "
"history ranks first in six of seven benchmark-model settings, and *no* added "
"memory-management operation improves on it. The one setting where it loses is "
"the one where full context is affordable.\n")
L.append("- **Mutation is nearly free to drop** (2610.30755) -- UPDATE+DELETE are 4.39% of "
"LoCoMo writes and 2.46% of LongMemEval writes; replacing them with append-only "
"ADD/NONE *raises* accuracy 67.36% -> 69.42%.\n")
L.append("- **Agent memory is a patch for a weak model** (2610.29823) -- Mem0 helps "
"GPT-4o-mini (78% vs 74%) but not GPT-6 Luna or Claude Opus 5.5, and at the top "
"end Pi scores 99% for $6.95 against full context's 95% for $17.84.\n")
L.append("- **A stronger reader does not recover what a weaker writer left out** "
"(2610.29823) -- on Zep's GPT-4o-mini graph the three readers score 43/43/45%; "
"on the graph Claude Opus 5.5 wrote they score 66/71/87%. Memory has a ceiling "
"set by its writer.\n")
L.append("- **What must be preserved is raw evidence, not extracted facts** (2610.30755, "
"2610.29823) -- verbatim chunks beat lossy artifacts; on the 88 questions with no "
"photo evidence, Pi's lead over full context vanishes (100% vs 99%).\n")
L.append("- **Compaction destroys provenance deterministically** (2610.29869) -- a forged "
"approval launders into memory 87-100% of the time in flattening harnesses and "
"0% in role-typed ones; no prompt-level defense restores it, only a code-level "
"check (77% catch, 2% collateral).\n")
L.append("- **Interaction preferences are a harder problem than factual recall** "
"(2610.30485) -- retrieval-based memory does not beat No Memory (PSA 50.0/50.0/49.9 "
"vs 49.6%), and even an Oracle that hands the agent the right preference lifts "
"delivered behavior only 24% -> 59%.\n")
L.append("- **The counter-example is training the memory manager itself** (2610.29671) -- "
"Uni-Mem jointly learns construction and retrieval and reaches 88.80% JJ, beating "
"Memory-R2 (OSS) by 10.93 points and off-the-shelf Qwen3.5-27B (87.05%). The gain "
"comes from the training scheme (Stage-Decoupled GRPO + ECA), not from a bigger model.\n")
L.append("## Where the five disagree\n")
L.append("2610.30755 and 2610.29823 both conclude that raw-history search is competitive, "
"but they disagree on *why the alternative fails*: 2610.30755 blames the rarity of "
"useful mutation (4.39% of writes), while 2610.29823 blames the writer's ceiling. "
"These are compatible -- a weak writer produces few useful mutations *and* a "
"lossy store -- but neither paper measures the other's mechanism, so the dossier "
"records the disagreement rather than resolving it.\n")
L.append("2610.30485 is the outlier on the headline question: it studies *interaction* "
"preferences, not factual recall, and finds memory nearly useless there. That is a "
"different task family, not a refutation of the other four.\n")
L.append("## Claim ledger\n")
L.append(f"{len(claims)} claims across {len(PAPERS)} papers "
f"({', '.join(f'{pid}:{by_paper[pid]}' for pid in PAPERS)}).\n")
L.append("| id | paper | theme | value |")
L.append("|---|---|---|---|")
for c in claims:
L.append(f"| {c['id']} | {c['paper']} | {c['theme']} | {c['value']} |")
L.append("")
(ROOT / "DOSSIER.md").write_text("\n".join(L) + "\n", encoding="utf-8")
print(f"claims: {len(claims)} papers: {len(PAPERS)}")
for pid in PAPERS:
print(f" {pid} {pins[pid][:16]} {by_paper[pid]:>2} claims")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,541 @@
{
"built": "2026-10-25",
"query": "all:\"agentic memory\"",
"feed_updated": "2026-10-25T00:40:42Z",
"feed_total_results": 583,
"papers": [
{
"arxiv_id": "2610.29671",
"title": "Uni-Mem: Learning a Long-Horizon Agentic Memory Manager with Unified Online Policy Optimization",
"bytes": 90083,
"chars": 89293,
"sha256": "b83a772fb6a1340b2440359bec2bf3752d583d86e213806c5dbf7a648d947bdd",
"claims": 9
},
{
"arxiv_id": "2610.29823",
"title": "MemoryTax: The Bitter Lesson of Agent Memory",
"bytes": 23897,
"chars": 23627,
"sha256": "ed08ec2380a6c1479c1cbc0534cc2cf154f92aa057216398055f034f91abd9b5",
"claims": 9
},
{
"arxiv_id": "2610.29869",
"title": "Compaction Laundering: Provenance Destruction in Context-Compacting Coding Agents",
"bytes": 92858,
"chars": 91936,
"sha256": "e6f78f508c12d7a304f7fd36eb5f05d60095a426b189ab4a5cd817c341791b54",
"claims": 7
},
{
"arxiv_id": "2610.30485",
"title": "Why Bother Me Again? Benchmarking Personal Agent Memory for Interaction Preferences",
"bytes": 350788,
"chars": 340805,
"sha256": "48e8b2b9c7defc653b8c7577e9e6029a5f9873cbaabe0f6b54f7ca84b08a1906",
"claims": 9
},
{
"arxiv_id": "2610.30755",
"title": "Is Complex Agentic Memory Always Needed? Naive RAG Is Stronger Than We Thought",
"bytes": 195193,
"chars": 193677,
"sha256": "2246678ceaf7a42ca8104b5067a9d548b0795391af40142b62e3f1638288daac",
"claims": 10
}
],
"themes": {
"T1": "Is designed memory needed at all?",
"T2": "Cost, ceiling, and scale",
"T3": "Provenance: what compaction destroys and what writers omit",
"T4": "What raw evidence must be preserved",
"T5": "Interaction preferences are a different problem"
},
"claims": [
{
"id": "R1",
"paper": "2610.30755",
"theme": "T1",
"claim": "Raw-RAG ranks first among compared memory systems in six of seven benchmark-model settings.",
"value": "six of seven",
"note": "The headline result: the naive baseline wins most settings.",
"anchor": "Raw-RAG ranks first among the compared memory systems in six",
"quote": "Raw-RAG ranks first among the compared memory systems in six of seven benchmark–model settings, outperforming the strongest compared agentic memory system by 0.13–5.53 points. We investigate why raw-history ret",
"corpus_sha256": "2246678ceaf7a42ca8104b5067a9d548b0795391af40142b62e3f1638288daac"
},
{
"id": "R2",
"paper": "2610.30755",
"theme": "T1",
"claim": "Raw-RAG beats the strongest compared agentic memory system by 0.13–5.53 points.",
"value": "0.13–5.53 points",
"note": "Margin is small on LoCoMo/LongMemEval, large on MemoryAgentBench.",
"anchor": "outperforming the strongest compared agentic memory system by 0.13",
"quote": "outperforming the strongest compared agentic memory system by 0.13–5.53 points. We investigate why raw-history retrieval remains c",
"corpus_sha256": "2246678ceaf7a42ca8104b5067a9d548b0795391af40142b62e3f1638288daac"
},
{
"id": "R3",
"paper": "2610.30755",
"theme": "T4",
"claim": "Mem0's UPDATE+DELETE account for only 4.39% of 33,902 LoCoMo write decisions.",
"value": "4.39%",
"note": "Mutation is rare in practice -- most proposed writes are duplicates or malformed.",
"anchor": "together account for only 4.39% of the 33,902 write decisions on LoCoMo",
"quote": "together account for only 4.39% of the 33,902 write decisions on LoCoMo\nand 2.46% of the 428,858 write decisions on LongMemEval\n(Figure 2); ",
"corpus_sha256": "2246678ceaf7a42ca8104b5067a9d548b0795391af40142b62e3f1638288daac"
},
{
"id": "R4",
"paper": "2610.30755",
"theme": "T4",
"claim": "Replacing UPDATE/DELETE with append-only ADD/NONE raises LoCoMo accuracy 67.36% -> 69.42%.",
"value": "67.36% to 69.42%",
"note": "Explicit mutation gives no accuracy gain; the simpler store is better.",
"anchor": "Replacing UPDATE with append-only ADD and DELETE with NONE raises",
"quote": "Replacing UPDATE with append-only ADD and DELETE with NONE raises\nLoCoMo accuracy from 67.36% to 69.42%, without reducing any category score\n(Table 2). Explicit mutation ",
"corpus_sha256": "2246678ceaf7a42ca8104b5067a9d548b0795391af40142b62e3f1638288daac"
},
{
"id": "R5",
"paper": "2610.30755",
"theme": "T4",
"claim": "Removing reranking lowers MemoryAgentBench Total by 3.92 points.",
"value": "3.92 points",
"note": "Reranking is the one component worth keeping.",
"anchor": "Removing reranking lowers Total by 3.92 points",
"quote": "Removing reranking lowers Total by 3.92 points (pHolm=0.0012p_{\\mathrm{Holm}}=0.0012),\nwhile returning only one item lowers it by 11.73 points. Final-kk\nvalues of 5, 10, 30, and 60 stay with",
"corpus_sha256": "2246678ceaf7a42ca8104b5067a9d548b0795391af40142b62e3f1638288daac"
},
{
"id": "R6",
"paper": "2610.30755",
"theme": "T4",
"claim": "Returning only one item lowers Total by 11.73 points.",
"value": "11.73 points",
"note": "Retrieval depth matters far more than chunk size.",
"anchor": "while returning only one item lowers it by 11.73 points",
"quote": "while returning only one item lowers it by 11.73 points. Final-kk\nvalues of 5, 10, 30, and 60 stay within 1.3 points of the baseli",
"corpus_sha256": "2246678ceaf7a42ca8104b5067a9d548b0795391af40142b62e3f1638288daac"
},
{
"id": "R7",
"paper": "2610.30755",
"theme": "T4",
"claim": "Top-120 retrieval loses 14.57 points.",
"value": "14.57 points",
"note": "Driven by long-range understanding and conflict resolution.",
"anchor": "Top-120 loses 14.57 points",
"quote": "Top-120 loses 14.57 points, driven by\nlong-range understanding and conflict resolution. Chunk sizes from 128 to 1,024\ndo not signi",
"corpus_sha256": "2246678ceaf7a42ca8104b5067a9d548b0795391af40142b62e3f1638288daac"
},
{
"id": "R8",
"paper": "2610.30755",
"theme": "T1",
"claim": "No memory-management variant improves on Raw-RAG (48.32%).",
"value": "48.32%",
"note": "RQ2 answer: added management does not help.",
"anchor": "None improves Raw-RAG (48.32%)",
"quote": "None improves Raw-RAG (48.32%) on the\nsame 14-source MemoryAgentBench Total (Table 6, orange rows).\nThe exact construction and retrieval procedures ar",
"corpus_sha256": "2246678ceaf7a42ca8104b5067a9d548b0795391af40142b62e3f1638288daac"
},
{
"id": "R9",
"paper": "2610.30755",
"theme": "T1",
"claim": "Atomic-fact extraction collapses test-time learning from 47.68% to 15.37%.",
"value": "47.68% to 15.37%",
"note": "The most damaging of the three management operations.",
"anchor": "Atomic-fact extraction reduces test-time learning from 47.68% to 15.37%",
"quote": "Atomic-fact extraction reduces test-time learning from 47.68% to 15.37%\nand Total by 19.62 points, consistent with loss or disrupt",
"corpus_sha256": "2246678ceaf7a42ca8104b5067a9d548b0795391af40142b62e3f1638288daac"
},
{
"id": "R10",
"paper": "2610.30755",
"theme": "T2",
"claim": "Full-context beats Raw-RAG by 19.98 points on DeepSeek-V4-Flash MEME.",
"value": "19.98 points",
"note": "The one setting where the naive baseline is not first.",
"anchor": "Full-context leads Raw-RAG by 19.98 points",
"quote": "Full-context leads Raw-RAG by 19.98 points in that setting. Raw-RAG\nis competitive across these tasks despite its simpler ingestio",
"corpus_sha256": "2246678ceaf7a42ca8104b5067a9d548b0795391af40142b62e3f1638288daac"
},
{
"id": "P1",
"paper": "2610.30485",
"theme": "T5",
"claim": "MemPABench has four simulated users holding 14 preferences.",
"value": "14 preferences",
"note": "Preferences span work and personal contexts.",
"anchor": "Four grounded simulated users hold 14 preferences",
"quote": "Four grounded simulated users hold 14 preferences across work and personal contexts. Their feedback spans 99 sessions over four months, with",
"corpus_sha256": "48e8b2b9c7defc653b8c7577e9e6029a5f9873cbaabe0f6b54f7ca84b08a1906"
},
{
"id": "P2",
"paper": "2610.30485",
"theme": "T5",
"claim": "Feedback spans 99 sessions over four months.",
"value": "99 sessions over four months",
"note": "Five event-driven preference changes per user.",
"anchor": "Their feedback spans 99 sessions over four months",
"quote": "Their feedback spans 99 sessions over four months, with five event-driven changes per user testing memory updates. We evaluate pre",
"corpus_sha256": "48e8b2b9c7defc653b8c7577e9e6029a5f9873cbaabe0f6b54f7ca84b08a1906"
},
{
"id": "P3",
"paper": "2610.30485",
"theme": "T5",
"claim": "No Memory reaches 49.6% PSA against 45.2% random.",
"value": "49.6%",
"note": "Defaults alone nearly match chance-plus-margin.",
"anchor": "No Memory, where the PA selects by its defaults, reaches 49.6%",
"quote": "No Memory, where the PA selects by its defaults, reaches 49.6%, compared with 45.2% for random selection (Appendix A.1). Only File memory adds a clear gain. It raises PSA to 59.2%, a mean ga",
"corpus_sha256": "48e8b2b9c7defc653b8c7577e9e6029a5f9873cbaabe0f6b54f7ca84b08a1906"
},
{
"id": "P4",
"paper": "2610.30485",
"theme": "T5",
"claim": "File memory raises PSA to 59.2%, +9.6 points over No Memory.",
"value": "59.2%",
"note": "The only condition with a clear gain; CI [+3.9, +15.6].",
"anchor": "It raises PSA to 59.2%",
"quote": "It raises PSA to 59.2%, a mean gain of 9.6 points over No Memory (95% cluster-bootstrap interval [+3.9, +15.6]). PERMA (Liu et al., 2026), a benchmark of content preferen",
"corpus_sha256": "48e8b2b9c7defc653b8c7577e9e6029a5f9873cbaabe0f6b54f7ca84b08a1906"
},
{
"id": "P5",
"paper": "2610.30485",
"theme": "T5",
"claim": "Mem0, MemOS and Simple RAG stay at No Memory level (PSA 50.0/50.0/49.9).",
"value": "50.0%, 50.0%, and 49.9%",
"note": "Retrieval-based memory does not beat having none.",
"anchor": "with PSA of 50.0%, 50.0%, and 49.9%",
"quote": "with PSA of 50.0%, 50.0%, and 49.9%. The intervals of all conditions and their comparisons with No Memory on each user–backbone pa",
"corpus_sha256": "48e8b2b9c7defc653b8c7577e9e6029a5f9873cbaabe0f6b54f7ca84b08a1906"
},
{
"id": "P6",
"paper": "2610.30485",
"theme": "T5",
"claim": "File memory PET is 25.4%, more than twice No Memory's 11.2%.",
"value": "25.4%",
"note": "But its CS does not improve (14.4% vs 21.8%): context is not tracked.",
"anchor": "Its PET is 25.4%",
"quote": "Its PET is 25.4%, more than twice the 11.2% of No Memory, and it is higher on every backbone. However, the CS does not improve, at 14.4% against 21.8% (Appendix G.3). In ",
"corpus_sha256": "48e8b2b9c7defc653b8c7577e9e6029a5f9873cbaabe0f6b54f7ca84b08a1906"
},
{
"id": "P7",
"paper": "2610.30485",
"theme": "T5",
"claim": "Oracle lifts correct selection 33%->96% but BRS>=4 only 24%->59%.",
"value": "33% to 96%",
"note": "Knowing the preference does not override the model's interaction habits.",
"anchor": "Oracle raises correct selections of the five settings",
"quote": "Oracle raises correct selections of the five settings from 33% to 96%, but the share of replies with a BRS of 4 or higher rises only from 24% to 59%. Even with the IX Wrapper, the PA still writes too much. Afte",
"corpus_sha256": "48e8b2b9c7defc653b8c7577e9e6029a5f9873cbaabe0f6b54f7ca84b08a1906"
},
{
"id": "P8",
"paper": "2610.30485",
"theme": "T5",
"claim": "Information Flow is the hardest dimension, PSA 34–44%.",
"value": "34–44%",
"note": "Expression Style (57-69%) and Disclosure (55-58%) are easier.",
"anchor": "Information Flow is the hardest under every condition",
"quote": "Information Flow is the hardest under every condition (Figure 8). Its PSA stays at 34–44%, the lowest of the four, while Expression Style (57–69%) and Disclosur",
"corpus_sha256": "48e8b2b9c7defc653b8c7577e9e6029a5f9873cbaabe0f6b54f7ca84b08a1906"
},
{
"id": "P9",
"paper": "2610.30485",
"theme": "T5",
"claim": "Anonymizing users drops shortcut accuracy 53.2% -> 46.4%, near the 45.2% chance.",
"value": "53.2%",
"note": "Confirms the benchmark measures memory, not name-guessing.",
"anchor": "reaches 53.2% accuracy from the source identity but 46.4%",
"quote": "reaches 53.2% accuracy from the source identity but 46.4% from the anonymized profile, close to the 45.2% chance level (Figure 10; Appendix ",
"corpus_sha256": "48e8b2b9c7defc653b8c7577e9e6029a5f9873cbaabe0f6b54f7ca84b08a1906"
},
{
"id": "M1",
"paper": "2610.29823",
"theme": "T2",
"claim": "Study scope: one LoCoMo conversation, 419 turns, 152 questions.",
"value": "419 turns, 152 questions",
"note": "Deliberately narrow: one conversation, one run per cell.",
"anchor": "On one LoCoMo conversation (419 turns, 152 questions)",
"quote": "On one LoCoMo conversation (419 turns, 152 questions), we run four memory methods (full context, BM25, Mem0, Zep) and three coding agents (Pi, Codex, Claude Cod",
"corpus_sha256": "ed08ec2380a6c1479c1cbc0534cc2cf154f92aa057216398055f034f91abd9b5"
},
{
"id": "M2",
"paper": "2610.29823",
"theme": "T2",
"claim": "With GPT-4o-mini, Mem0 scores 78% vs 74% for full context and BM25.",
"value": "78%",
"note": "Agent memory is a patch for a weak model.",
"anchor": "With GPT-4o-mini, Mem0 scores highest",
"quote": "With GPT-4o-mini, Mem0 scores highest. It scores 78%, against 74% for both full context and BM25; Zep scores 43%. The agents score 63% (Pi), 28% (Codex) and 29%",
"corpus_sha256": "ed08ec2380a6c1479c1cbc0534cc2cf154f92aa057216398055f034f91abd9b5"
},
{
"id": "M3",
"paper": "2610.29823",
"theme": "T2",
"claim": "With GPT-6 Luna, full context scores 92%, BM25 82%, Mem0 80%, Zep 51%.",
"value": "92%",
"note": "The patch stops helping as the model gets stronger.",
"anchor": "With GPT-6 Luna, full context scores 92%",
"quote": "With GPT-6 Luna, full context scores 92%, BM25 82%, Mem0 80% and Zep 51%; Zep’s graph holds 128 facts from the 19 sessions, against 195 with Claude Opus 5.5. With Claude Opus 5.5, Mem0 and full contex",
"corpus_sha256": "ed08ec2380a6c1479c1cbc0534cc2cf154f92aa057216398055f034f91abd9b5"
},
{
"id": "M4",
"paper": "2610.29823",
"theme": "T2",
"claim": "With Claude Opus 5.5, agents score highest: Pi 99%, Codex 98%, Claude Code 97%.",
"value": "Pi 99%, Codex 98% and Claude Code 97%",
"note": "Searching the raw record beats reading all of it at the top end.",
"anchor": "With Claude Opus 5.5, Mem0 and full context both score 95%",
"quote": "With Claude Opus 5.5, Mem0 and full context both score 95% and Zep 87%, and the agents score highest: Pi 99%, Codex 98% and Claude Code 97%, a lead that comes mostly from",
"corpus_sha256": "ed08ec2380a6c1479c1cbc0534cc2cf154f92aa057216398055f034f91abd9b5"
},
{
"id": "M5",
"paper": "2610.29823",
"theme": "T2",
"claim": "Pi scores 99% for $6.95/rollout vs full context 95% for $17.84.",
"value": "$6.95",
"note": "Less than half the cost for a higher score.",
"anchor": "Pi scores 99% for $6.95 a rollout",
"quote": "Pi scores 99% for $6.95 a rollout, while full context with the same model scores 95% for $17.84. Pi reads about 10,000 input tokens a question, 7,900 of them ca",
"corpus_sha256": "ed08ec2380a6c1479c1cbc0534cc2cf154f92aa057216398055f034f91abd9b5"
},
{
"id": "M6",
"paper": "2610.29823",
"theme": "T4",
"claim": "On the 88 questions without photo evidence, Pi scores 100%.",
"value": "88 questions without photo evidence",
"note": "Pi matches full context once captions are removed from the comparison.",
"anchor": "On the 88 questions without photo evidence",
"quote": "On the 88 questions without photo evidence, Pi scores 100%, and full context, Mem0, Codex and Claude Code 99% each (Table 4). With the same input, then, Pi matc",
"corpus_sha256": "ed08ec2380a6c1479c1cbc0534cc2cf154f92aa057216398055f034f91abd9b5"
},
{
"id": "M7",
"paper": "2610.29823",
"theme": "T2",
"claim": "Pi reads ~10,000 input tokens/question (7,900 cached) vs 28,900 for full context.",
"value": "10,000 input tokens",
"note": "The efficiency mechanism behind the cost lead.",
"anchor": "Pi reads about 10,000 input tokens a question",
"quote": "Pi reads about 10,000 input tokens a question, 7,900 of them cached, against 28,900 for full context. Codex and Claude Code score 98% and 97% for $25.",
"corpus_sha256": "ed08ec2380a6c1479c1cbc0534cc2cf154f92aa057216398055f034f91abd9b5"
},
{
"id": "M8",
"paper": "2610.29823",
"theme": "T3",
"claim": "Writer ceiling: on Zep's GPT-4o-mini graph readers score 43/43/45%; on Opus 5.5's, 66/71/87%.",
"value": "43%, 43% and 45%",
"note": "A stronger reader cannot recover what a weaker writer left out.",
"anchor": "GPT-4o-mini graph, GPT-4o-mini, GPT-6 Luna and Claude Opus 5.5 score 43%, 43% and 45%",
"quote": "GPT-4o-mini graph, GPT-4o-mini, GPT-6 Luna and Claude Opus 5.5 score 43%, 43% and 45%; on the graph Claude Opus 5.5 wrote, 66%, 71% and 87%. Mem0 keeps the order with smaller gaps: 78%, 83% and 84% from GPT-4o-",
"corpus_sha256": "ed08ec2380a6c1479c1cbc0534cc2cf154f92aa057216398055f034f91abd9b5"
},
{
"id": "M9",
"paper": "2610.29823",
"theme": "T3",
"claim": "Pi's one miss is a LoCoMo answer-key error, not a memory failure.",
"value": "151 questions",
"note": "The model is right and the benchmark key is wrong.",
"anchor": "Its one miss is LoCoMo",
"quote": "Its one miss is LoCoMo’s mistake. The question asks what kind of painting Caroline shared with Melanie on 13 October 2023, and the answer key says an abstract painting with blue streaks. In the conversation it is Melanie who shares that painting; Caroline shares a drawing. Pi says so and is marked wrong: “Caroline shared a drawing, not a painting. … The paintings shared that day (a sunset and an abstract piece) were Melanie’s.” With Claude Opus 5.5, full context, Codex and Claude Code point out the same mistake and are marked wrong too. On the other 151 questions, Pi with Claude Opus",
"corpus_sha256": "ed08ec2380a6c1479c1cbc0534cc2cf154f92aa057216398055f034f91abd9b5"
},
{
"id": "C1",
"paper": "2610.29869",
"theme": "T3",
"claim": "Forged approval launders into memory 87–100% of the time across five flattening harnesses.",
"value": "87–100%",
"note": "Up to a ~908,000-token context window.",
"anchor": "the forged approval launders into the",
"quote": "the forged approval launders into the\nagent’s memory 87–100% of the time, up to a\n~908,000-token context window; across four role-typed\nharnesses (Codex, Gemini CLI, Qwen Code, Continue) the identical attack\nsu",
"corpus_sha256": "e6f78f508c12d7a304f7fd36eb5f05d60095a426b189ab4a5cd817c341791b54"
},
{
"id": "C2",
"paper": "2610.29869",
"theme": "T3",
"claim": "The identical attack succeeds 0% of the time across four role-typed harnesses.",
"value": "0% of the time",
"note": "Role-typing is a built-in control.",
"anchor": "succeeds 0% of the time",
"quote": "succeeds 0% of the time. We follow the attack to executed consequences.\nA laundered standing approval drives real tool-running age",
"corpus_sha256": "e6f78f508c12d7a304f7fd36eb5f05d60095a426b189ab4a5cd817c341791b54"
},
{
"id": "C3",
"paper": "2610.29869",
"theme": "T3",
"claim": "Live in the real pi TUI the attack runs at ~50% against a 0% paired clean arm.",
"value": "~50%",
"note": "Runtime evidence, not a simulation.",
"anchor": "live in the real pi TUI at ~50%",
"quote": "live in the real pi TUI at ~50%\nagainst a 0% paired clean arm. Straiker [1] described this\nconsequence without runtime evidence, a",
"corpus_sha256": "e6f78f508c12d7a304f7fd36eb5f05d60095a426b189ab4a5cd817c341791b54"
},
{
"id": "C4",
"paper": "2610.29869",
"theme": "T3",
"claim": "A code-level provenance check catches 77% of laundering, demoting 2% of legitimate content.",
"value": "77%",
"note": "The only defense that works; prompt-level defenses cannot restore provenance.",
"anchor": "we measure a code-level provenance check that catches 77% of",
"quote": "we measure a code-level provenance check that catches 77% of\nlaundering while demoting 2% of legitimate summary content. The defect\nalso has a straightforward f",
"corpus_sha256": "e6f78f508c12d7a304f7fd36eb5f05d60095a426b189ab4a5cd817c341791b54"
},
{
"id": "C5",
"paper": "2610.29869",
"theme": "T3",
"claim": "In-prompt anti-injection rules stop 13–25%; published classifier defenses report 67% at best.",
"value": "13–25%",
"note": "Baselines are far weaker than the code-level check.",
"anchor": "stops 13",
"quote": "stops 13–25% on the same data, and published classifier\ndefenses report 67% at best. The defect also has a straightforward\nfix on the flatte",
"corpus_sha256": "e6f78f508c12d7a304f7fd36eb5f05d60095a426b189ab4a5cd817c341791b54"
},
{
"id": "C6",
"paper": "2610.29869",
"theme": "T3",
"claim": "Laundering still succeeds 100% of the time at every context size.",
"value": "100%",
"note": "Size does not attenuate the attack.",
"anchor": "The laundering still succeeds 100% of the",
"quote": "The laundering still succeeds 100% of the\ntime at every size, and the executed rate, near-100% on the short\ntranscripts, settles to ~20–40% in a full window. Th",
"corpus_sha256": "e6f78f508c12d7a304f7fd36eb5f05d60095a426b189ab4a5cd817c341791b54"
},
{
"id": "C7",
"paper": "2610.29869",
"theme": "T3",
"claim": "Executed rate settles to ~20–40% in a full window.",
"value": "~20–40%",
"note": "Real agents compact at this size, so this is the realistic case.",
"anchor": "settles to ~20",
"quote": "settles to ~20–40% in a full window. The\nshort runs therefore measure the attack at its most favorable, an\nupper bound on the exec",
"corpus_sha256": "e6f78f508c12d7a304f7fd36eb5f05d60095a426b189ab4a5cd817c341791b54"
},
{
"id": "U1",
"paper": "2610.29671",
"theme": "T1",
"claim": "Uni-Mem reaches an overall LoCoMo JJ of 88.80%.",
"value": "88.80%",
"note": "Best across all four question types.",
"anchor": "Uni-Mem achieves the best performance across all four question types",
"quote": "Uni-Mem achieves the best performance across all four question types, with an overall JJ score of 88.80%.\nIt surpasses the strongest baseline, Memory-R2 (OSS), by 10.93 percentage points and Mem-T by 12.31 points, with g",
"corpus_sha256": "b83a772fb6a1340b2440359bec2bf3752d583d86e213806c5dbf7a648d947bdd"
},
{
"id": "U2",
"paper": "2610.29671",
"theme": "T1",
"claim": "Uni-Mem surpasses Memory-R2 (OSS) by 10.93 points and Mem-T by 12.31.",
"value": "10.93 percentage points",
"note": "Gains of 12.93 and 9.29 on multi-hop and temporal.",
"anchor": "It surpasses the strongest baseline, Memory-R2 (OSS), by 10.93",
"quote": "It surpasses the strongest baseline, Memory-R2 (OSS), by 10.93 percentage points and Mem-T by 12.31 points, with gains of 12.93 and 9.29 points over the strongest competing results on multi-hop and temporal que",
"corpus_sha256": "b83a772fb6a1340b2440359bec2bf3752d583d86e213806c5dbf7a648d947bdd"
},
{
"id": "U3",
"paper": "2610.29671",
"theme": "T1",
"claim": "Uni-Mem still reaches 80.99% without SFT, +3.12% over the strongest baseline.",
"value": "80.99%",
"note": "RL alone is enough to beat the baselines.",
"anchor": "Uni-Mem still achieves 80.99% without SFT",
"quote": "Uni-Mem still achieves 80.99% without SFT, outperforming the strongest baseline by 3.12%.\nThese results support the effectiveness of jointly",
"corpus_sha256": "b83a772fb6a1340b2440359bec2bf3752d583d86e213806c5dbf7a648d947bdd"
},
{
"id": "U4",
"paper": "2610.29671",
"theme": "T1",
"claim": "ECA improves overall JJ by 1.29 and 3.03 points without and with SFT.",
"value": "1.29 and 3.03 percentage points",
"note": "Event-wise credit assignment localizes sparse feedback.",
"anchor": "improves overall JJ by 1.29 and 3.03 percentage points",
"quote": "improves overall JJ by 1.29 and 3.03 percentage points without and with SFT, respectively. These gains support the effectiveness o",
"corpus_sha256": "b83a772fb6a1340b2440359bec2bf3752d583d86e213806c5dbf7a648d947bdd"
},
{
"id": "U5",
"paper": "2610.29671",
"theme": "T1",
"claim": "Stage-Decoupled GRPO improves overall JJ by 2.48 points without SFT.",
"value": "2.48 percentage points",
"note": "Temporal accuracy rises 10.55 points in this setting.",
"anchor": "improves overall JJ by 2.48 percentage points without SFT",
"quote": "improves overall JJ by 2.48 percentage points without SFT. In particular, temporal accuracy rises by 10.55 points in this setting, and multi",
"corpus_sha256": "b83a772fb6a1340b2440359bec2bf3752d583d86e213806c5dbf7a648d947bdd"
},
{
"id": "U6",
"paper": "2610.29671",
"theme": "T1",
"claim": "Trained construction alone improves overall JJ by 8.18 points.",
"value": "8.18 percentage points",
"note": "Each capability contributes even paired with its untrained counterpart.",
"anchor": "improves the overall JJ score by 8.18 percentage points",
"quote": "improves the overall JJ score by 8.18 percentage points, demonstrating the increased downstream utility of the constructed memorie",
"corpus_sha256": "b83a772fb6a1340b2440359bec2bf3752d583d86e213806c5dbf7a648d947bdd"
},
{
"id": "U7",
"paper": "2610.29671",
"theme": "T1",
"claim": "Extending update blocks 1:1 -> 3:2 reduces overall JJ from 88.80% to 83.75%.",
"value": "83.75%",
"note": "Frequent alternation matters as much as balanced allocation.",
"anchor": "the overall JJ score from 88.80",
"quote": "the overall JJ score from 88.80%88.80\\% to 83.75%83.75\\%,\nindicating that less frequent alternation hinders\nshared-polic",
"corpus_sha256": "b83a772fb6a1340b2440359bec2bf3752d583d86e213806c5dbf7a648d947bdd"
},
{
"id": "U8",
"paper": "2610.29671",
"theme": "T2",
"claim": "SFT followed by RL improves the 4B manager's JJ from 76.58% to 88.80%.",
"value": "76.58% to 88.80%",
"note": "A small trained manager beats Qwen3.5-27B (87.05%) and 397B-A17B (85.86%).",
"anchor": "SFT followed by RL improves the 4B manager",
"quote": "SFT followed by RL improves the 4B manager’s JJ score from 76.58% to 88.80%.\nUni-Mem also outperforms all off-the-shelf managers, including the strong",
"corpus_sha256": "b83a772fb6a1340b2440359bec2bf3752d583d86e213806c5dbf7a648d947bdd"
},
{
"id": "U9",
"paper": "2610.29671",
"theme": "T2",
"claim": "Uni-Mem outperforms all off-the-shelf managers including Qwen3.5-27B (87.05%).",
"value": "87.05%",
"note": "Scale is not the binding constraint; joint training is.",
"anchor": "Uni-Mem also outperforms all off-the-shelf managers",
"quote": "Uni-Mem also outperforms all off-the-shelf managers, including the strongest one, Qwen3.5-27B (87.05%), and the largest one, Qwen3.5-397B-A17B (85.86%).\nThese results demonstrate that a small manager trained jo",
"corpus_sha256": "b83a772fb6a1340b2440359bec2bf3752d583d86e213806c5dbf7a648d947bdd"
}
]
}
@@ -0,0 +1,576 @@
MemoryTax: The Bitter Lesson of Agent Memory
Skip to main content
Report GitHub Issue
×
Title:
Content selection saved. Describe the issue below:
Description:
Submit without GitHub
Submit in GitHub
Why HTML?
Report Issue
Back to Abstract
Download PDF
Contents
Reading mode
Colour theme
Abstract
1 Introduction
2 Setup
Photos.
Measurement.
3 Agent memory is a costly patch for a weak model
4 Agent memory has a ceiling
5 With Claude Opus 5.5, Pi tops the frontier
6 Discussion and limitations
References
A Full results
B Protocols
C Prompts
License: arXiv.org perpetual non-exclusive license
arXiv:2610.29823v1 [cs.CL] 21 Oct 2026
MemoryTax: The Bitter Lesson of Agent Memory
Zedong Peng
Affiliation: Shanghai Jiao Tong University
Email: zedongpeng1@gmail.com
Abstract
Agent memory systems such as Mem0 and Zep decide what to keep before any question arrives; coding agents keep the raw history and let the model search it with its own tools. We ask whether designed memory still helps once the model is strong enough to search for itself. On one LoCoMo conversation (419 turns, 152 questions), we run four memory methods (full context, BM25, Mem0, Zep) and three coding agents (Pi, Codex, Claude Code), each with GPT-4o-mini, GPT-6 Luna and Claude Opus 5.5, and record what every rollout costs. This is a short empirical study, with one conversation and one run per cell. Agent memory is a costly patch for a weak model: Mem0 beats full context only with GPT-4o-mini, and its build alone costs more than a whole full-context rollout. Agent memory also has a ceiling, set by the model that writes it: a stronger reader does not recover what a weaker writer left out. With Claude Opus 5.5, Pi tops the cost–accuracy frontier, scoring 99% for less than half the cost of full context; its one miss is a wrong answer key. Part of that lead comes from photo captions that only the agents could read: on the 88 questions whose evidence has no photo, Pi and full context differ by one answer.
Interactive version: zedongpeng.com/memorytax
1 Introduction
Agent memory systems such as Mem0 and Zep decide what to keep before any question arrives: the model extracts facts or builds a graph from the conversation, and each question sees a few retrieved pieces [1, 2]. Coding agents and recursive language models keep the raw history instead and let the model search it with its own tools [3, 4, 5, 6].
The bitter lesson is that knowledge built into a system helps at first, then plateaus, while general methods that scale with computation win [7]. Agent memory builds in what to keep. Does it still help once the model is strong enough to search for itself?
We run four memory methods (full context, BM25 [8], Mem0 and Zep) and three coding agents (Pi, Codex and Claude Code), each with GPT-4o-mini, GPT-6 Luna and Claude Opus 5.5, on a conversation from LoCoMo [9], and measure what each run costs. We find:
•
Agent memory is a costly patch for a weak model. Mem0 beats full context only with GPT-4o-mini, and its build alone costs more than full context’s whole run (Section 3).
•
Agent memory has a ceiling, set by the model that writes it. A stronger reader does not recover what a weaker writer left out (Section 4).
•
With Claude Opus 5.5, Pi tops the cost–accuracy frontier (Figure 1). It answers every question whose answer key is sound, for less than half what full context costs. Where the two read the same text they differ by one answer; the rest of Pi’s lead comes from photo captions that only the agents could read (Section 5).
The study is small: one conversation, one run per cell, and a GPT-4o-mini judge. Section 6 lists what it cannot show. Within those limits, the results suggest that a strong model needs less designed memory and better tools for searching the raw record.
Figure 1: Cost and correct rate per rollout. Each badge is one cell’s whole run: the build, then all 152 questions. Colour is the model, the badge the method’s mark, and a letter in the badge marks a method without one (F full context, B BM25). The stepped line is the highest correct rate reached at or below each cost, and the labelled cells lie on it. Cost is on a log axis.
2 Setup
We run 21 cells, seven methods with three models each, on LoCoMo conv-26 [9]: 419 turns in 19 sessions and 152 single-hop, multi-hop, temporal and open-domain questions. LoCoMo’s adversarial questions are left out, as in Mem0’s evaluation [1]. The history fits every model’s context window. Among the ten LoCoMo conversations, conv-26’s per-method accuracies track those of the full set closely. Table 1 lists the methods.
Table 1: Methods: what each stores and what the model sees when it answers.
Method
Stored
Model sees
Full context
raw history
all of it
BM25
raw turns
top-8 turns, ±\pm2
Mem0
extracted facts
top-200 facts
Zep
temporal graph
top-20 edges
Pi, Codex, Claude Code
raw history, one JSON file
what its tools read
The memory methods answer one question at a time with Mem0’s 2025 answer template; Mem0 is OSS 2.1.0 and Zep is Graphiti OSS 0.30.2, both with default extraction and retrieval [1, 2]. The agents, Pi 1.1.0, Codex 0.159.2 and Claude Code 2.1.295 [3, 4, 5], start a fresh session in a fresh container for each question, with the conversation as one file, their default tools, and the template’s answer rules in the prompt (Appendix C).
The models go from weak to strongest: GPT-4o-mini, GPT-6 Luna, Claude Opus 5.5. The last two run at reasoning effort high, the effort coding agents are usually run at [10]; GPT-4o-mini has no effort setting. In each cell one model writes the store (Mem0, Zep) and answers every question, or drives the agent.
Photos.
Some LoCoMo turns share a photo, and the official file holds a caption of it next to the turn text. The agents get the official file, captions included; the memory methods were given the turn text only. 116 of the 419 turns share a photo, and 64 of the 152 questions have evidence in such a turn. Section 5 measures what this changes.
Measurement.
A rollout is one run over the conversation: the build, then the 152 questions. GPT-4o-mini grades every answer with Mem0’s 2025 judge rubric [1]; empty answers count as wrong. Cost is the list price of every model call, LLM and embedding (text-embedding-3-small), with the judge excluded. A turn is one LLM call: one per question for the memory methods, one per step of an agent’s loop, and Mem0’s and Zep’s calls to build their store. Tokens are those calls’ input and output tokens, cached input included; embeddings are not counted. Each cell runs once.
3 Agent memory is a costly patch for a weak model
With GPT-4o-mini, Mem0 scores highest. It scores 78%, against 74% for both full context and BM25; Zep scores 43%. The agents score 63% (Pi), 28% (Codex) and 29% (Claude Code): Codex opens the conversation file for three questions in four and Claude Code almost never, and otherwise they answer from the question alone.
With the two stronger models, the patch stops helping. With GPT-6 Luna, full context scores 92%, BM25 82%, Mem0 80% and Zep 51%; Zep’s graph holds 128 facts from the 19 sessions, against 195 with Claude Opus 5.5. With Claude Opus 5.5, Mem0 and full context both score 95% and Zep 87%, and the agents score highest: Pi 99%, Codex 98% and Claude Code 97%, a lead that comes mostly from questions about photos (Section 5). Earlier work also finds that plain retrieval keeps up with elaborate memory [11] and that rankings between memory systems change with the model [12].
Figure 2: Cumulative cost–correct curves. Each curve is one rollout: the build, then the questions from cheapest to most expensive (equal costs grouped), adding up cost and correct answers; correct answers are divided by 152, so a wrong answer adds cost only. Colour is the model, the badge the method, and agents are dotted. The cost axis is linear to $2 and logarithmic above. Curves use a nine-point moving average, and the badges mark the unsmoothed totals of Table 2. Strong lines are cells that no other cell beats on both cost and correct answers.
The patch is not free. Mem0’s build alone costs more than full context’s whole rollout with the same model: $0.56 against $0.43 with GPT-4o-mini, $0.45 against $0.29 with GPT-6 Luna and $25.88 against $17.84 with Claude Opus 5.5 (Figure 2). Its questions are cheaper, so the build would pay for itself after 372, 545 and 596 questions on the same history; LoCoMo asks 152. Mem0 and Zep are never on the frontier of Figure 1, which runs through BM25 and full context with GPT-6 Luna and Pi with Claude Opus 5.5. Paying to build memory that a stronger model does not need is a memory tax.
4 Agent memory has a ceiling
The model that writes the store limits every reader. Mem0 and Zep write their store with the same model that later reads it. Retrieval calls no language model, so the reader can be swapped: each model answers from the retrievals of the others’ stores (Figure 3, Table 3).
Figure 3: Writer and reader. Each colour is one store, written by that model. The filled badge is the writer reading its own store, and arrows run from it to the other two readers.
A stronger reader does not recover what the writer left out. On Zep’s GPT-4o-mini graph, GPT-4o-mini, GPT-6 Luna and Claude Opus 5.5 score 43%, 43% and 45%; on the graph Claude Opus 5.5 wrote, 66%, 71% and 87%. Mem0 keeps the order with smaller gaps: 78%, 83% and 84% from GPT-4o-mini’s store, 89%, 91% and 95% from Claude Opus 5.5’s. This agrees with work where verbatim chunks beat extracted facts on long conversations [13].
5 With Claude Opus 5.5, Pi tops the frontier
Pi with Claude Opus 5.5 is the top of the frontier in Figure 1. No other cell scores higher: Pi scores 99% for $6.95 a rollout, while full context with the same model scores 95% for $17.84. Pi reads about 10,000 input tokens a question, 7,900 of them cached, against 28,900 for full context. Codex and Claude Code score 98% and 97% for $25.41 and $28.41; HarnessTax explains why the same model costs so differently across harnesses [10].
Its one miss is LoCoMo’s mistake. The question asks what kind of painting Caroline shared with Melanie on 13 October 2023, and the answer key says an abstract painting with blue streaks. In the conversation it is Melanie who shares that painting; Caroline shares a drawing. Pi says so and is marked wrong: “Caroline shared a drawing, not a painting. … The paintings shared that day (a sunset and an abstract piece) were Melanie’s.” With Claude Opus 5.5, full context, Codex and Claude Code point out the same mistake and are marked wrong too. On the other 151 questions, Pi with Claude Opus 5.5 is always right.
Photo captions account for most of Pi’s lead over full context. With Claude Opus 5.5, Pi answers seven questions that full context misses and misses none that it answers. Six of the seven have their evidence in a turn that shares a photo, whose caption Pi could read and full context could not (Section 2); so do all six such questions for Codex and all four for Claude Code. On the 88 questions without photo evidence, Pi scores 100%, and full context, Mem0, Codex and Claude Code 99% each (Table 4). With the same input, then, Pi matches full context for less than half its cost. Whether searching the record answers more than reading all of it, this study does not settle.
6 Discussion and limitations
Our results suggest that agent memory is a costly patch for a weak model and caps what any reader can answer. On this LoCoMo conversation, Mem0 helps GPT-4o-mini but not GPT-6 Luna or Claude Opus 5.5, a stronger reader does not recover what the writer left out, and with Claude Opus 5.5 Pi answers every sound question for less than full context costs.
The findings rest on a small study, and these limits apply:
•
Scale. One conversation of LoCoMo’s ten, and one run per cell. Differences of a few questions may not hold in a second run.
•
Grading. A GPT-4o-mini judge with a lenient rubric, and at least one wrong answer key.
•
Photo captions. The agents could read photo captions that the memory methods were not given (Section 5). This touches comparisons between agents and memory methods, not those among the memory methods.
•
Context length. The history fits in every model’s context window, so full context is always an option. Results may differ on other benchmarks such as LongMemEval [14] and on histories that do not fit.
•
Settings and prices. Mem0 and Zep run with their default extraction and retrieval, and other settings may do better. Costs are list prices and depend on each provider’s caching.
Memory systems and harnesses can also be searched for automatically [15, 16], but what is found is still fixed before the question arrives. More broadly, a strong model may need less designed memory and better tools: exact, semantic and vector search over the raw record, driven by the model itself [3, 4]. Progress may then look less like another memory system and more like ripgrep after grep [17], or zvec-grep after ripgrep [18]. Designed memory may survive as a cost trade-off when many questions share one history (Figure 2).
References
[1]
P. Chhikara, D. Khant, S. Aryan, T. Singh, and D. Yadav.
Mem0: Building production-ready AI agents with scalable long-term
memory, 2025.
URL https://arxiv.org/abs/2504.19413.
[2]
P. Rasmussen, P. Paliychuk, T. Beauvais, J. Ryan, and D. Chalef.
Zep: A temporal knowledge graph architecture for agent memory, 2025.
URL https://arxiv.org/abs/2501.13956.
[3]
Pi contributors.
Pi coding agent: Compaction and session persistence.
Software documentation and source, 2026.
URL https://github.com/earendil-works/pi.
[4]
OpenAI.
Codex: An open-source coding agent, 2026.
URL https://github.com/openai/codex.
[5]
Anthropic.
Claude code, 2026.
URL https://github.com/anthropics/claude-code.
[6]
A. L. Zhang, T. Kraska, and O. Khattab.
Recursive language models, 2025a.
URL https://arxiv.org/abs/2512.24601.
[7]
R. S. Sutton.
The bitter lesson.
Essay, 2019.
URL http://www.incompleteideas.net/IncIdeas/BitterLesson.html.
[8]
S. Robertson and H. Zaragoza.
The probabilistic relevance framework: BM25 and beyond.
Foundations and Trends in Information Retrieval, 3(4):333–389, 2009.
doi: 10.1561/1500000019.
[9]
A. Maharana, D.-H. Lee, S. Tulyakov, M. Bansal, F. Barbieri, and Y. Fang.
Evaluating very long-term conversational memory of LLM agents,
2024.
URL https://arxiv.org/abs/2402.17753.
[10]
Arena Team.
HarnessTax: How much does the harness matter for coding agents?
Arena blog, 2026.
URL https://arena.ai/blog/coding-agents-harness-tax.
[11]
Y. Wu, W. Chen, Z. Huang, et al.
Back to basics: Let conversational agents remember with just
retrieval and generation, 2026.
URL https://arxiv.org/abs/2604.11628.
[12]
K. Wang.
MemDelta: Controlled baselines and hidden confounds in agent memory
evaluation, 2026.
URL https://arxiv.org/abs/2606.29914.
[13]
T. An.
Fidelity before structure: Verbatim chunks beat lossy artifact
extraction in long-conversation LLM memory, 2026.
URL https://arxiv.org/abs/2601.00821.
[14]
D. Wu, H. Wang, W. Yu, Y. Zhang, K.-W. Chang, and D. Yu.
LongMemEval: Benchmarking chat assistants on long-term interactive
memory, 2024.
URL https://arxiv.org/abs/2410.10813.
[15]
G. Zhang, H. Ren, C. Zhan, et al.
MemEvolve: Meta-evolution of agent memory systems,
2025b.
URL https://arxiv.org/abs/2512.18746.
[16]
Y. Lee, R. Nair, Q. Zhang, K. Lee, O. Khattab, and C. Finn.
Meta-harness: End-to-end optimization of model harnesses, 2026.
URL https://arxiv.org/abs/2603.28052.
[17]
A. Gallant.
ripgrep.
Software, 2016.
URL https://github.com/BurntSushi/ripgrep.
[18]
Zvec Team.
From rg to zg: Local search beyond keywords.
Zvec blog, 2026.
URL https://zvec.org/en/blog/2026-08-28-zvec-grep-open-source/.
[19]
Harbor Framework Team.
Harbor: A framework for evaluating and optimizing agents and models
in container environments.
Software, 2026.
URL https://doi.org/10.5281/zenodo.20953922.
Appendix A Full results
Table 2 gives every cell’s cost and correct rate, Table 3 the writer–reader grid behind Figure 3, and Table 4 the split by photo evidence. Figure 4 redraws Figure 2 against turns and tokens.
Table 2: Per method and model. Cost is the list price of every model call except the judge: Build is paid once, before any question; Per q. is everything one question costs, retrieval included, averaged over the 152 questions. ∘\circ: nothing is built. Effort: the model’s reasoning effort; GPT-4o-mini has none. Correct: share of answers judged correct, in %, per LoCoMo category (single-hop, multi-hop, temporal, open-domain); the first row gives each category’s size.
Cost ($)
Correct (%)
Method
Model
Effort
Build
Per q.
Single
Multi
Temp.
Open
All
Questions
70
32
37
13
152
Full context
GPT-4o-mini
–
∘\circ
0.0028
89
75
51
62
74
GPT-6 Luna
high
∘\circ
0.0019
90
97
95
85
92
Claude Opus 5.5
high
∘\circ
0.117
93
94
97
100
95
BM25
GPT-4o-mini
–
∘\circ
0.00028
86
50
73
69
74
GPT-6 Luna
high
∘\circ
0.00021
89
63
89
69
82
Claude Opus 5.5
high
∘\circ
0.013
93
81
92
92
90
Mem0
GPT-4o-mini
–
0.559
0.0013
87
75
65
77
78
GPT-6 Luna
high
0.449
0.0011
89
88
57
85
80
Claude Opus 5.5
high
25.876
0.074
93
97
97
100
95
Zep
GPT-4o-mini
–
0.092
0.00012
51
34
30
54
43
GPT-6 Luna
high
0.103
0.00012
54
50
49
46
51
Claude Opus 5.5
high
6.224
0.0077
83
94
89
85
87
Pi
GPT-4o-mini
–
∘\circ
0.016
74
59
35
85
63
GPT-6 Luna
high
∘\circ
0.0032
96
81
92
92
91
Claude Opus 5.5
high
∘\circ
0.046
99
100
100
100
99
Codex
GPT-4o-mini
–
∘\circ
0.0031
30
22
24
46
28
GPT-6 Luna
high
∘\circ
0.0050
90
84
89
85
88
Claude Opus 5.5
high
∘\circ
0.167
99
100
95
100
98
Claude Code
GPT-4o-mini
–
∘\circ
0.0020
34
22
8
77
29
GPT-6 Luna
high
∘\circ
0.0082
96
78
95
92
91
Claude Opus 5.5
high
∘\circ
0.187
99
94
97
100
97
Table 3: Writer and reader. Correct rate (%), the reader answering from the writer’s store and its retrievals. Bold: one model writes and reads (Table 2).
Mem0, read by
Zep, read by
Written by
GPT-4o-mini
GPT-6 Luna
Opus 5.5
GPT-4o-mini
GPT-6 Luna
Opus 5.5
GPT-4o-mini
78
83
84
43
43
45
GPT-6 Luna
74
80
81
52
51
55
Claude Opus 5.5
89
91
95
66
71
87
Table 4: Correct rate (%) on the 64 questions with evidence in a turn that shares a photo, whose caption only the agents could read, and on the other 88.
GPT-4o-mini
GPT-6 Luna
Claude Opus 5.5
Method
Photo (64)
Other (88)
Photo (64)
Other (88)
Photo (64)
Other (88)
Full context
66
81
91
93
89
99
BM25
61
83
78
84
84
94
Mem0
70
84
72
86
91
99
Zep
33
50
53
50
77
94
Pi
53
69
88
94
98
100
Codex
22
33
84
91
97
99
Claude Code
17
38
91
92
95
99
Figure 4: The curves of Figure 2 against turns (LLM calls, top) and tokens (bottom), on linear axes, the questions ordered by that amount. Strong lines are cells that no other cell beats on both that amount and correct answers.
Appendix B Protocols
Mem0 OSS 2.1.0 writes one chronological turn at a time and retrieves k=200k=200 without reranking; dates are prepended to the text. Zep runs Graphiti OSS 0.30.2 on Kuzu, one session per episode, with hybrid edge search (top 20). The memory methods’ GPT-6 Luna and Claude Opus 5.5 requests set reasoning effort high.
Each agent question is a Harbor 0.24.0 task [19]: a fresh container holding only the conversation, exactly as in the official file, without questions or answers. The agent’s last message is its answer. While the agent runs, the container can reach only the model endpoint. GPT-6 Luna and Claude Opus 5.5 run at reasoning effort high, through a proxy that translates the API where an agent does not speak the model’s own; GPT-4o-mini has no effort setting. Codex and Claude Code have web search off; Pi has none. A question may take 20 minutes, and Pi and Claude Code at most 100 turns. With GPT-6 Luna, Codex picks its code mode: one JavaScript tool that calls the shell. A trial that fails or ends without an answer is run again, and only the accepted trial is counted.
Appendix C Prompts
Each prompt is copied from the code the runs called; fields in braces are filled per question.
Answer, system message.
⬇
You are a helpful assistant that can answer questions based on the provided context.If the question involves timing, use the conversation date for reference.Provide the shortest possible answer.Use words directly from the conversation when possible.Avoid using subjects in your answer.
Answer, user message.
⬇
# Question:
{{QUESTION}}
# Context:
{{CONTEXT}}
# Short answer:
Pi, Codex and Claude Code, user message.
⬇
Answer a question about the conversation in `conv-26/conv.json`. If the question involves timing, use the conversation date for reference. Provide the shortest possible answer. Use words directly from the conversation when possible. Avoid using subjects in your answer. Your final message is taken as the answer.
Question: {question}
Judge.
⬇
Your task is to label an answer to a question as ’CORRECT’ or ’WRONG’. You will be given the following data:
(1) a question (posed by one user to another user),
(2) a ’gold’ (ground truth) answer,
(3) a generated answer
which you will score as CORRECT/WRONG.
The point of the question is to ask about something one user should know about the other user based on their prior conversations.
The gold answer will usually be a concise and short answer that includes the referenced topic, for example:
Question: Do you remember what I got the last time I went to Hawaii?
Gold answer: A shell necklace
The generated answer might be much longer, but you should be generous with your grading - as long as it touches on the same topic as the gold answer, it should be counted as CORRECT.
For time related questions, the gold answer will be a specific date, month, year, etc. The generated answer might be much longer or use relative time references (like "last Tuesday" or "next month"), but you should be generous with your grading - as long as it refers to the same date or time period as the gold answer, it should be counted as CORRECT. Even if the format differs (e.g., "May 7th" vs "7 May"), consider it CORRECT if it's the same date.
Now it's time for the real question:
Question: {question}
Gold answer: {gold_answer}
Generated answer: {generated_answer}
First, provide a short (one sentence) explanation of your reasoning, then finish with CORRECT or WRONG.
Do NOT include both CORRECT and WRONG in your response, or it will break the evaluation script.
Just return the label CORRECT or WRONG in a json format with the key as "label".
Experimental support, please
view the build logs
for errors. Generated by
L
A
T
E
xml
.
Instructions for reporting errors
We are continuing to improve HTML versions of papers, and your feedback helps enhance accessibility and mobile
support. To report errors in the HTML that will help us improve conversion and rendering, choose any of the
methods listed below:
Click the "Report Issue" (
) button, located in the page header.
Tip: You can select the relevant text first, to include it in your report.
Our team has already identified the following issues. We appreciate your time reviewing and reporting rendering errors we
may not have found yet. Your efforts will help us improve the HTML versions for all readers, because disability
should not be a barrier to accessing research. Thank you for your continued support in championing open access for
all.
Have a free development cycle? Help support accessibility at arXiv! Our collaborators at LaTeXML maintain a list of packages that need conversion, and welcome developer contributions.
We gratefully acknowledge support from our major funders,
member institutions, ,
and all contributors.
About ·
Help ·
Contact ·
Subscribe ·
Copyright ·
Privacy ·
Accessibility ·
Operational Status (opens in new tab)
@@ -0,0 +1,180 @@
#!/usr/bin/env python3
"""
verify_claims.py -- independent re-verification of the agentic-memory dossier.
This script does NOT import build_dossier.py. It re-derives every claim from
the pinned corpus bytes and the recorded pins, so it is a genuine second
implementation of the grounding check.
For every claim in claims.json it checks:
1. the corpus file's sha256 equals the pin recorded at build time;
2. the claim's `quote` is a literal substring of that corpus file;
3. the claim's headline `value` appears inside that quote;
4. the headline `value` appears in DOSSIER.md.
It then runs three mutation batteries. Each battery corrupts the artifact in
a way a real drift would look like, and requires the corruption to be
DETECTED. A mutation that passes undetected falsifies this verifier, and the
script exits nonzero.
battery A: corrupt one quote at a time (all claims)
battery B: corrupt one corpus byte (sha256 pin must catch it)
battery C: corrupt one headline value everywhere in DOSSIER.md (all values)
Exit 0 = the dossier is fully grounded and the verifier is shown to have
teeth. Exit 1 = an ungrounded claim, or a vacuous check.
"""
from __future__ import annotations
import copy
import hashlib
import json
import sys
import tempfile
from pathlib import Path
ROOT = Path(__file__).resolve().parent
CORPUS = ROOT / "corpus"
EXPECTED_CLAIMS = 44
EXPECTED_PAPERS = 5
def sha256_bytes(b: bytes) -> str:
return hashlib.sha256(b).hexdigest()
def verify(claims_doc: dict, dossier_text: str, corpus_dir: Path,
cache: dict | None = None) -> list[str]:
"""Return a list of failure strings. Empty list == pass."""
cache = {} if cache is None else cache
fails: list[str] = []
papers = {p["arxiv_id"]: p for p in claims_doc.get("papers", [])}
if len(papers) != EXPECTED_PAPERS:
fails.append(f"papers: expected {EXPECTED_PAPERS}, found {len(papers)}")
if len(claims_doc.get("claims", [])) != EXPECTED_CLAIMS:
fails.append(f"claims: expected {EXPECTED_CLAIMS}, found "
f"{len(claims_doc.get('claims', []))}")
seen_ids: set[str] = set()
counts: dict[str, int] = {}
for c in claims_doc.get("claims", []):
cid, pid = c.get("id", "?"), c.get("paper", "?")
if cid in seen_ids:
fails.append(f"{cid}: duplicate claim id")
seen_ids.add(cid)
counts[pid] = counts.get(pid, 0) + 1
if pid not in papers:
fails.append(f"{cid}: unknown paper {pid!r}")
continue
rec = papers[pid]
if rec.get("sha256") != c.get("corpus_sha256"):
fails.append(f"{cid}: pin in papers[] disagrees with claim pin")
f = corpus_dir / f"{pid}.txt"
if not f.exists():
fails.append(f"{cid}: corpus file missing: {f.name}")
continue
if pid not in cache:
cache[pid] = f.read_bytes()
data = cache[pid]
dig = sha256_bytes(data)
if dig != c.get("corpus_sha256"):
fails.append(f"{cid}: corpus sha256 drift for {pid} "
f"(pin {str(c.get('corpus_sha256'))[:12]}, got {dig[:12]})")
continue
text = data.decode("utf-8", errors="replace")
if c["quote"] not in text:
fails.append(f"{cid}: quote not found verbatim in {pid}.txt")
continue
if c["value"] not in c["quote"]:
fails.append(f"{cid}: value {c['value']!r} not inside its own quote")
if c["value"] not in dossier_text:
fails.append(f"{cid}: value {c['value']!r} missing from DOSSIER.md")
for pid, rec in papers.items():
f = corpus_dir / f"{pid}.txt"
if f.exists():
if pid not in cache:
cache[pid] = f.read_bytes()
if sha256_bytes(cache[pid]) != rec.get("sha256"):
fails.append(f"{pid}: papers[] sha256 pin does not match corpus")
if counts.get(pid, 0) != rec.get("claims"):
fails.append(f"{pid}: claims[] count {rec.get('claims')} != "
f"actual {counts.get(pid, 0)}")
return fails
def corrupt_char(s: str) -> str:
if not s:
return "X"
return s[:-1] + ("X" if s[-1] != "X" else "Y")
def main() -> int:
claims_doc = json.loads((ROOT / "claims.json").read_text(encoding="utf-8"))
dossier_text = (ROOT / "DOSSIER.md").read_text(encoding="utf-8")
cache: dict[str, bytes] = {}
fails = verify(claims_doc, dossier_text, CORPUS, cache)
if fails:
print("FAIL: dossier is not fully grounded")
for f in fails:
print(" -", f)
return 1
n = len(claims_doc["claims"])
print(f"grounding: {n} claims x 4 checks -- all pass")
# ---- battery A: corrupt one quote at a time; every corruption must be caught
undetected = []
for i, c in enumerate(claims_doc["claims"]):
mutant = copy.deepcopy(claims_doc)
q = mutant["claims"][i]["quote"]
mid = len(q) // 2
mutant["claims"][i]["quote"] = q[:mid] + corrupt_char(q[mid]) + q[mid + 1:]
if not verify(mutant, dossier_text, CORPUS, cache):
undetected.append(c["id"])
if undetected:
print(f"FAIL: battery A vacuous -- quote corruption undetected for {undetected}")
return 1
print(f"battery A: {n} quote corruptions, all detected")
# ---- battery B: flip one corpus byte; the sha256 pin must catch it
with tempfile.TemporaryDirectory() as td:
tdir = Path(td)
victim = claims_doc["claims"][0]["paper"]
for pid in {c["paper"] for c in claims_doc["claims"]}:
(tdir / f"{pid}.txt").write_bytes(cache[pid])
p = tdir / f"{victim}.txt"
b = bytearray(p.read_bytes())
b[len(b) // 2] ^= 0x01
p.write_bytes(bytes(b))
detected = verify(claims_doc, dossier_text, tdir, {})
if detected:
print(f"battery B: corpus byte flip in {victim} detected via sha256 pin")
else:
print(f"FAIL: battery B vacuous -- corpus byte flip in {victim} undetected")
return 1
# ---- battery C: corrupt one headline value everywhere in the dossier
undetected_c = []
for c in claims_doc["claims"]:
v = c["value"]
if v not in dossier_text:
continue
bad = dossier_text.replace(v, corrupt_char(v))
if not verify(claims_doc, bad, CORPUS, cache):
undetected_c.append(c["id"])
if undetected_c:
print(f"FAIL: battery C vacuous -- dossier value corruption undetected for "
f"{undetected_c}")
return 1
print(f"battery C: {n} dossier value corruptions, all detected")
print("VERDICT: PASS -- dossier grounded, verifier falsified-checked")
return 0
if __name__ == "__main__":
sys.exit(main())