AgentWorkingMemory · retrieval internals

How AWM decides what to say

A vector store answers what is similar? A memory has to answer something harder: what should I say, and should I say anything at all? That is three questions, each answered by a different mechanism, over a store that changes with time.

bge-small-en-v1.5 (384d) · ms-marco-MiniLM-L-6-v2 cross-encoder · SQLite + FTS5 · all local, in-process
Prefer it without the internals? One memory, followed end to end · The decision-maker brief

The whole system in one rule

Every stage in AWM is either a proposer or a decider, and nothing is allowed to be both.

Proposers are cheap, wide and generous: their only job is to avoid losing the answer. Deciders are expensive, narrow and final: they are the only stages permitted to rank, cut or refuse. Hold that distinction and the rest of the design follows from it — why the shortlist handed to the reranker is more than ten times the size of the answer, why keyword search is never allowed to break a tie, why an empty result is a decision rather than an absence.

Q1 PROPOSE Q2 DECIDE Q3 SPEAK? keyword + vector union, few hundred composite score relevance x liveness graph walk neighbours vote shortlist of ~40 cross-encoder reads query + memory together the only stage that ranks for real do the channels agree? is this query in-domain is the set shaped like an answer? sharpness · cliff · floor answer or ∅
Three questions, answered in order. Q1 may only gather and narrow cheaply; it is never the final word. Q2 is the single decisive ranking. Q3 has two independent ways to return nothing.
The rule, stated once

A cheap stage that is allowed to decide will quietly cost you the answer, because it is cheap precisely by not reading anything carefully. Every default below exists to keep proposers generous and deciders late.

Underneath all three questions sits a clock. Memories carry an activation that decays with time and strengthens with use, so the store you query today is not the store you queried in March. That is the fourth thing to hold, and it explains a class of behaviour nothing above accounts for.

Q1

What could possibly match?

Proposer · cheap · wide · the only eligibility gate

Two channels run against the store and their results are unioned. Keyword (BM25 over FTS5) catches exact strings — a ticket number, a table name, a file path, an error message. Vector (cosine against 384-dimension embeddings) catches the same idea in different words. Neither is sufficient alone, which is the entire reason both exist: identifiers embed badly, and paraphrases share no tokens.

What survives is scored by the composite — a blend of text match, vector match, ACT-R activation, Hebbian edge strength, graph centrality, stored confidence and past feedback. Relevance gates the rest of that blend, so a popular, heavily-linked memory cannot muscle in on popularity alone. Then a beam walk of depth two over the association graph lets shortlisted memories pull in their neighbours, under a hard cap, so the graph can nudge an ordering but never invent one.

The output is a shortlist of about forty for a three-result query. That ratio is the point.

Where that ratio came from

An instrumented study traced, query by query, the stage at which the correct memory was lost. It was not the search — the answer cleared candidate generation almost always — and it was not the reranker, which lifted the right memory by three ranks on average whenever it got to see it. Half of all losses happened when the composite ranked a perfectly retrievable memory just below the shortlist cutoff, so the reranker never saw it at all. A weak ranker was gatekeeping a strong one.

That study ran on a benchmark retired in 0.13.x, and its absolute scores no longer describe this system. The structural lesson is what survived, and it is the rule above: widen the shortlist, demote the cheap ranker to a filter, let the expensive one decide. Current measurements are further down.

Q2

Which one actually answers it?

Decider · expensive · narrow · the precision cut

Everything to this point compared the query and a memory separately — each reduced to a vector or a bag of terms, then measured against the other. A cross-encoder does not do that. It reads the query and the memory as a single input and scores the pair directly, which is why it can tell that a memory containing your identifier is nonetheless about something else entirely.

It is also around two orders of magnitude more expensive per candidate, which is the whole reason for the funnel in front of it. Most of warm recall latency is this stage, and that is why the result limit is a cost dial as much as a breadth dial.

Three settings materially improve what the reranker gets to read. All three ship default-off so an existing install never changes behaviour under its owner, and all three are switched on by awm setup from 0.14.6 on a fresh install.

SettingWhat changesMeasured
AWM_RERANK_WINDOW=query The 400-character passage is cut from the densest region of query terms rather than the start of the memory 25% → 87.5%
on long memories
AWM_RERANK_TAGS=1 Structured tags join the passage, so ticket=10003 is readable by the stage that decides +7.4pp
AWM_RERANK2=1 Second-stage ordering by the cross-encoder's own score rather than the blend enable together
A consequence worth internalising

The embedding is built from concept and content only. A word that exists solely as a tag is therefore invisible to the vector channel no matter what else you tune — keyword search indexes it, and the reranker sees it only with AWM_RERANK_TAGS=1. Tag it and write it into the body. This is the highest-leverage habit when writing memories, and no retrieval setting rescues a memory that never names its own subject.

Q3

Should it say anything at all?

Decider · two independent guards · why this is safe to wire into reasoning

A confident wrong memory costs more than an admitted gap, because the agent will act on it. So the last thing AWM decides is whether to answer, and it decides that twice, on two different questions.

Guard one — do the channels agree this query is in-domain?

Keyword, vector and reranker each vote on whether they found anything convincing, judged over the post-rerank top five rather than the whole shortlist. Two of three must agree, or three of three when the caller explicitly asked for abstention. Fall below that with no strong semantic match anywhere and recall returns empty. Judging the returned five rather than the wide pool is what let the shortlist grow without one deep distractor propping up a bad answer.

Guard two — is this result set shaped like a good answer?

This guard ignores content entirely and looks only at the distribution of final scores. Three signals, combined as a weighted geometric mean, so any one of them near zero drags the whole result down rather than being averaged away:

Confident sharpness highcliff highfloor high answer Noise sharpness ~0cliff ~0floor mid withhold Best of a bad set sharpness highcliff highfloor LOW withhold
Sharpness is top-1 against the mean of the top five. Cliff is the drop from first to tenth. Floor is the top score in absolute terms. The third case is the interesting one: a clear winner among candidates that are all mediocre.
// src/engine/confidence.ts - weighted geometric mean, so a weak signal cannot be averaged away
confidence = sharpness^0.4 · cliff^0.3 · floor^0.3

// sharpness = top1 / mean(top5)    is there a standout?
// cliff     = (top1 - top10)/top1  does it drop off after the winner?
// floor     = top1                 is the winner good in absolute terms?
The trap this creates, and why the default is 0.05

Because floor is an absolute score, a narrow question with exactly one correct answer can score modestly and still be right. Measured on the live store: two precise identifier queries produced confidence of 0.26 and 0.38 and were withheld at a 0.25 threshold, while a vague prompt matching a broad cloud of loosely-related memories sailed through at 0.92.

Confidence measures the shape of an answer, not its correctness, and that shape is easiest to achieve on questions too general to be useful. Raising this threshold therefore silences your most specific queries first — exactly the ones worth asking. The default is a light 0.05. Treat 0.10 as the ceiling, and use it only for pushed recall such as prompt priming, where nobody asked.

When guard two fires, the reply says RECALL ABSTAINED and reports how many results were withheld. An empty result without that line is genuine absence. The distinction matters: the first means reformulate with the exact identifier, the second means go and read the source.

CLOCK

The store changes while you are not looking

Borrowed from ACT-R · why nobody has to curate this

Every memory carries a base-level activation computed from how often it has been retrieved and how long ago. Retrieval touches a memory and resets its clock, so what keeps proving useful stays sharp and the one-offs sink without ever being deleted.

// src/core/decay.ts
B = ln(n + 1) - d · ln(age / (n + 1))
// n = retrieval count, age = time since last touch, d = 0.3-0.6 adapted to the query
time since last retrieval activation retrieved often - persists never retrieved - fades
Practice effects, not a time-to-live. The content of long-unretrieved memories fades while their cue pathways — concept, tags, embedding — stay intact, so an old memory remains findable by the thing it was about.

A consolidation pass runs in the background on session end: cluster, strengthen what co-activates, bridge across clusters, decay and forget on an age and access gate, prune redundancy, cap per-node edge weight. Measured to improve retrieval as the store grows, by removing keyword-similar noise that would otherwise bury the memory you want.

What the clock does to measurement

A benchmark that replays a frozen snapshot against a live wall clock is measuring the wall clock. AWM's own identifier benchmark read 70.0% one month and 67.0% the next from this alone, on an unchanged commit. Recall now accepts a pinned evaluation time and the harness pins it to the snapshot. If you build an evaluation against a decaying store, pin the clock before you trust a delta.

The write path is the same rule, mirrored

Storage is not an insert. It is propose-then-decide running in the other direction, and it is the reason recall stays sharp as the store grows: most of what an agent offers never competes at retrieval time at all.

StageWhat it decides
Embed384-dimension vector from concept and content, reused immediately as the novelty signal
Salience0.45·novelty + 0.15·surprise + 0.15·decision + 0.15·causal + 0.1·effort plus an event-type bonus, into one of three dispositions: active at 0.4 and above, staging at 0.2, otherwise discarded
ReinforceA close match to an existing memory strengthens it — confidence up, retrieval count up, content merged — rather than creating a near-duplicate that would split the vote at recall
SupersedeA correction marks the old memory superseded and down-ranks it hard, so recall returns the current answer while the transition stays queryable. Retraction is the separate, stronger action for something that was simply wrong.
ConnectionsQueued, drained during consolidation, forming the semantic and entity edges that Q1's graph walk later traverses

Roughly a third of what is offered is kept at full confidence. The rest is staged or dropped. A notes file has two paragraphs that read with equal authority; AWM knows which one won.

What the model lets you predict

The test of a mental model is whether it tells you what will happen before you measure it. Six things this one predicts.

Tighten abstention and your best questions go dark first

Precise queries with one correct answer score modestly in absolute terms, and the absolute term carries three tenths of the confidence weight. Q3 · floor — raise the threshold only for recall nobody asked for.

A memory that never names its subject cannot be found by it

No amount of retrieval tuning recovers a word that was never written down, and a word that lives only in a tag is invisible to the embedding. Q1 · channels — write the category noun next to the identifier, in the body.

The same question answers differently in June than in March

Not a bug and not index drift — activation moved, and a memory nobody touched fell behind one that keeps earning its place. Clock — pin it before you evaluate anything.

Growth does not degrade recall the way it degrades a vector store

Selective retention, reinforcement instead of duplication, and consolidation all attack the same failure: a store in which everything is a little bit relevant. Write path · consolidation — measured on a real 30,000-memory store.

The association graph rarely changes the final answer

It nudges an ordering under a hard cap; the reranker decides. Stated plainly because a model that over-claims its own graph would mislead you about where to spend effort. Q1 proposes, Q2 decides — measured across 300 real queries.

Empty means two different things that need opposite responses

RECALL ABSTAINED with a withheld count means reformulate using the exact identifier. A plain empty result means it was never stored, so go and read the source. Q3 · both guards — never retry by lowering the threshold.

Where it currently lands

Measured against a frozen copy of a real 30,000-memory store rather than synthetic fixtures, with the decay clock pinned to the snapshot and every query scoped to its own memory pool, at three results per query.

92.7%
Right memory first, identifier queries (300)
92.7%
Right memory first, topic queries (450)
90.0%
Correctly silent on facts never stored
500ms
Median warm recall, local

Two fixtures built independently agree within a point. Both were corrected upward twice in September 2026, when the instrument rather than the engine turned out to be wrong — once for the unpinned clock above, once for a runner that queried every memory as the wrong agent. In October 2026 they moved once more, this time for an actual engine change: the Rocchio pseudo-relevance pass was measured for the first time, bought nothing, and was defaulted off. The method and its history are in benchmarks.md, which deliberately keeps only current readings; the superseded numbers and what moved them are recorded in CHANGELOG.md and recall-latency.md.

The knobs, grouped by what they affect

Every default below is a measured value. The grouping is the useful part: it tells you which dial to reach for when a behaviour surprises you.

VariableDefaultEffect
Q1 — how generous the proposal is
AWM_RERANK_POOLmax(limit·4, 40)How many candidates reach the decider. Recall against latency; the dial the attribution study moved.
AWM_TOPN_MULT8Candidate breadth carried into the graph walk and rerank
AWM_SIM_FLOOR_TARGETED0.50Raw cosine floor for vector eligibility on targeted queries; 0.35 in exploratory mode
Q2 — what the decider gets to read
AWM_RERANK_WINDOWqueryPlace the 400-character passage on the densest query-term region rather than the prefix
AWM_RERANK_TAGS1Include structured tags in the rerank passage
AWM_RERANK21Second-stage ordering by the cross-encoder's own score
Q3 — when to say nothing
require_confidence0.05Per-call shape gate. Read the trap above before raising it.
AWM_ABSTAIN_GATE_K5How many post-rerank results the channel-agreement guard judges. Decouples pool width from precision.
Clock
nowwall clockPer-call pin for the decay clock. Required for any reproducible evaluation.

The complete surface, including the experiments that were tried and rejected, is in reference.md. memory_whoami prints which of these a running process actually has, which is worth checking before concluding that a setting did nothing.

The division of labour

AWM is the precise, fast, local memory layer. The judgement — what to chain, what to verify, when to act — stays with the agent.

That boundary is deliberate, and it is why the abstention gate exists at all. A memory system that always answers pushes its uncertainty into the agent's reasoning, where it becomes indistinguishable from knowledge. One that is willing to return nothing hands back a signal the agent can act on: ask differently, or go and look at the source. Build against that division and the thing scales.