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.
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.
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.
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.
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.
| Setting | What changes | Measured |
|---|---|---|
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 |
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.
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:
// 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?
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.
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
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.
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.
| Stage | What it decides |
|---|---|
| Embed | 384-dimension vector from concept and content, reused immediately as the novelty signal |
| Salience | 0.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 |
| Reinforce | A 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 |
| Supersede | A 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. |
| Connections | Queued, 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.
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.
| Variable | Default | Effect |
|---|---|---|
| Q1 — how generous the proposal is | ||
AWM_RERANK_POOL | max(limit·4, 40) | How many candidates reach the decider. Recall against latency; the dial the attribution study moved. |
AWM_TOPN_MULT | 8 | Candidate breadth carried into the graph walk and rerank |
AWM_SIM_FLOOR_TARGETED | 0.50 | Raw cosine floor for vector eligibility on targeted queries; 0.35 in exploratory mode |
| Q2 — what the decider gets to read | ||
AWM_RERANK_WINDOW | query | Place the 400-character passage on the densest query-term region rather than the prefix |
AWM_RERANK_TAGS | 1 | Include structured tags in the rerank passage |
AWM_RERANK2 | 1 | Second-stage ordering by the cross-encoder's own score |
| Q3 — when to say nothing | ||
require_confidence | 0.05 | Per-call shape gate. Read the trap above before raising it. |
AWM_ABSTAIN_GATE_K | 5 | How many post-rerank results the channel-agreement guard judges. Decouples pool width from precision. |
| Clock | ||
now | wall clock | Per-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.