Skip to content
Harness Engineering2026-09-108 min readRev. 2026-09-30

Measure Stale Agent Memory as Recall@k, Then Retire It by Source Revision

Similarity search ranks by meaning, not currency, so a note that used to be true can outrank the one that is. Measure it as stale@1 and recall@k of the current note, then retire by source revision.

Key takeaways

  • An embedding ranks by similarity of meaning; a superseded note can match a question as well as, or better than, the current one.
  • Measure staleness as two retrieval metrics: stale@1, the share of queries whose top note is superseded, and recall@k of the current note.
  • Repetition hurts: if nine old observations and one new note score alike, the new note reaches the top three only 30% of the time.
  • Retire a superseded note from current selection by source revision, and keep it as history.
  • Scope invalidation to the fact that changed only when you have a per-fact revision or a trustworthy change list.

Rod Rivera

Author

Measure Stale Agent Memory as Recall@k, Then Retire It by Source Revision

Rod's note: read with a pencil; the margins are for you.

The note that keeps winning

A research agent answers questions from a memory built from weeks of experiment logs. For a month, the logs said the math eval runs 4-shot, and the agent stored that observation many times: in run summaries, in debugging notes, in a comparison table. Then the eval harness changed to 0-shot in revision v18, and one new note records it.

Someone asks, "How many shots does the math eval use?" The store embeds the question and returns the nearest notes. Most of them say 4-shot, because most of the notes about the math eval were written before the change. Every one is a strong semantic match. None is current.

The retrieval did its job. It found notes about the math eval's shot count. It was never asked whether they are still true.

Measure it before you fix it

This is a retrieval-quality problem, so measure it with retrieval metrics. Chapter 5 derives recall@k and reciprocal rank; two follow directly for staleness:

  • stale@1: the share of queries whose top-ranked note is superseded. It is the rate at which a top-1 answer would be wrong.
  • recall@k of the current note: the share of queries whose current note appears in the top k. For an agent this is the one that matters: a current note outside the top k never reaches the model.
python
def recall_at_k(ranked, relevant, k):
    return len(set(ranked[:k]) & relevant) / len(relevant)


def stale_at_1(ranked, superseded):
    return ranked[0] in superseded


# One query, "how many shots does the math eval use?", ranked by some embedding model.
ranked = ["obs-03", "obs-11", "obs-07", "harness-v18", "obs-01"]  # obs-* say 4-shot (superseded)
current = {"harness-v18"}  # says 0-shot, from the harness's current revision
superseded = {"obs-01", "obs-03", "obs-07", "obs-11"}

print("recall@3 unfiltered", recall_at_k(ranked, current, 3), "stale@1", stale_at_1(ranked, superseded))
filtered = [d for d in ranked if d not in superseded]
print("recall@3 filtered  ", recall_at_k(filtered, current, 3), "stale@1", stale_at_1(filtered, superseded))

# If m stale copies and the one fresh note score about alike, the fresh note's rank is
# uniform over 1..m+1, so P(fresh in top k) = k / (m + 1).
for m in (1, 4, 9):
    print(f"m={m}: P(fresh in top 3) = {min(1, 3 / (m + 1)):.2f}")
text
recall@3 unfiltered 0.0 stale@1 True
recall@3 filtered   1.0 stale@1 False
m=1: P(fresh in top 3) = 1.00
m=4: P(fresh in top 3) = 0.60
m=9: P(fresh in top 3) = 0.30

I ran it with Python 3.12 on September 30, 2026. The ranking is a hand-written example, not a model's output. The last three lines are arithmetic, and they are the useful part. Suppose the old fact was stored m times and the new one once, and all m + 1 notes are about equally similar to the question. Then the current note's rank is equally likely to be any of 1 through m + 1, and the chance it lands in the top k is k / (m + 1). With nine old observations and k = 3, that is 3 / 10 = 0.30. Repetition, which feels like evidence, crowds the current fact out.

The assumption of equal similarity is a simplification. The new note may be phrased closer to the question, or further from it. That is exactly what a measurement with a real embedding model settles.

The protocol, with real embeddings

This article reports no measured numbers. Here is the experiment, sized so it runs in a Colab notebook on a small open embedding model; Chapter 6 measures all-minilm and shows how to store each vector with the model that made it.

Build a store with known supersessions

Take 30 facts from a real project's history, each with an old value and a new value from a later source revision. For each, write between one and nine notes stating the old value, in the varied phrasing real logs have, and one note stating the new value. Record for every note its fact_id, source_revision and status.

Write the questions before you rank anything

Three questions per fact, including one paraphrase that shares no content word with any note. Label the current note as the only relevant answer.

Rank with and without the revision filter

Embed notes and questions with the same model. Rank by cosine similarity over all notes, then again over notes whose revision matches the source's current revision.

Report stale@1 and recall@3 with intervals

Pool the 90 questions per condition and give each rate a Wilson interval. Plot recall@3 against m, the number of old copies, next to the k / (m + 1) line: points above the line mean the model favors the new phrasing, points below mean it favors the old.

What you expect, stated as a hypothesis to test rather than a result: unfiltered stale@1 rises with m; filtered stale@1 is zero by construction; filtered recall@3 is limited only by how well the current note matches the question, which is Chapter 6's paraphrase problem.

Retire by revision, keep the history

The fix is not "delete old memories more often." Deleting loses the record of what the team believed and when, and it can throw away something still true. Give every note enough structure that a program, not a similarity score, decides whether it may answer a present-tense question.

FieldPurpose
fact_idThe exact fact, such as eval.math.shots
source_idThe authoritative system: the harness config, not a chat log
source_revisionThe revision of that source the note reflects
recorded_atWhen the note itself was written
statuscurrent, superseded or disputed

When eval.math.shots moves from v17 to v18, the v17 notes become superseded, and a new note is written at v18 with status: current. Current-answer retrieval filters on status and cross-checks the revision against the source's live revision. The old notes stay, so "what did we assume in August" still has an answer. Chapter 6's vector index does the same in its "keep only the current revision" section: search reads only current rows, and the superseded row stays on record.

Deciding whether a stored note can answer a current question. Question about the present; Retrieve candidate notes; Compare source_revision with the live source; Match: use as current; Mismatch: withhold and re-fetch
Deciding whether a stored note can answer a current question
A vector match is not a currency check

A high similarity score says the note answers the same kind of question. It says nothing about whether the source under it has changed. Pair every present-tense retrieval with a revision check.

Scope the invalidation to what changed

Wiping everything the agent knows on every source change is the opposite mistake. If the harness revision bumps because the math eval's shot count changed, the code eval's timeout did not move, and re-verifying it wastes work and can leave a window with no answer at all.

Scoped invalidation needs a per-fact revision or a trustworthy change list. If v18 is only a global revision of the harness config, a mismatch says something changed, not what. In that case revalidate every fact that depends on that source, or get the change list first. Do not claim fact-level precision from a global version number.

Tag every note with its source revision when it is written

Not as a later migration.

Compare revisions before serving a current answer

If the note's revision is behind the source's, the note may explain the past but may not answer the present.

Withhold until the replacement is checked

Fetch and validate the new value before publishing a new current note, and move the current pointer in one transaction. If the source is unreachable, say the current value is unavailable.

Scope to the affected fact when you can prove it

Per-fact revisions or a change list narrow the work; a global revision means revalidating everything that depends on it.

After validating the replacement, one datastore transaction retires the old row, inserts the new row and moves the current pointer.. eval.math.shots revision changes; Fetch and validate replacement; Begin datastore transaction; Retire old row and insert new row; Move current pointer; Commit together
After validating the replacement, one datastore transaction retires the old row, inserts the new row and moves the current pointer.

Someone has to own the trigger. Leaving it to "the agent will notice from context" is how stale notes survive. A small ownership record per fact domain is enough: fact_id, authoritative_source, updater, invalidation_trigger (for example, on every harness config commit) and escalation_when_missing. Giving every memory an owner and an expiry rule works through ownership and conflict escalation in detail.

Quick check — Retrieval returns a note that closely matches 'how many shots does the math eval use?' It was written three weeks ago, and the harness config has had a new revision since. Should the agent use it for a present-tense answer?

Check the decision

Why this matters in a lab

Retrieval evaluation is core applied-AI work, and memory staleness is a retrieval failure that ordinary benchmarks do not measure, because their corpora do not change under them. An agent that works in a research repository sees its sources change constantly: configs, harnesses, datasets, baselines. Being able to say "our memory's stale@1 is this, with this interval, and the revision filter takes it to zero at this recall cost" is the kind of claim a team can act on.

In an interview, the derivation above is a good test of whether you think in distributions: why does repetition hurt, what is the probability the current note makes the top k, and what assumption makes that formula exact? The Tools, Memory, and Multi-Agent Systems course covers the surrounding memory architectures.

What would prove this wrong

The revision filter fails if the source has no reliable revision and you fall back on recorded_at: a timestamp says when a copy was written, not whether anything changed upstream afterward. Use a content hash, a re-fetch, or a documented freshness interval instead, and say which.

Two writers can race to publish different current values; the store needs a uniqueness rule on the current pointer and a conditional update, which an in-memory example does not test. And the recall argument fails if, on your model and your notes, the current note outranks the old ones regardless of m. Run the protocol and find out.

Read Chapter 6: embeddings, vector search, and keeping only the current revision

Measure a real embedding model against BM25 with recall@3, then store vectors with their provenance and search only current rows.

Learn the mechanism. Test the claim.

Join the Prof Rod newsletter for one educational lesson a week, with worked examples attached. It is free to register for and separate from the Zero Employee community.