Continuum Engine
Milestone 2 · Sept 2026 -
Cortex, Activation & the Event Plane
Milestone 1 built a store that can be looked up. Milestone 2 is making it recall, and building the harness to measure the difference.
Overview
Lookup returns what matches. Recall returns what would come to mind.
A lookup ranks by similarity to a query. Recall ranks by how often something has been used, how recently, how strongly it associates with what is currently in play, and who vouched for it. That is the ACT-R activation equation, and it cannot be bolted onto a search index.
Fitting it honestly turned out to require three things M1 never needed: a ranking function specified against the primary sources rather than summarised from them, an evaluation harness that cannot be gamed by its own corpus, and an event plane that records what actually happened, because every time-decayed term in the equation is a question about the past, and answering it with a guess produces a number that looks like evidence.
The scorer is built, fitted and unit-tested. Nothing calls it. There is no retrieval path joining candidate generation, graph expansion and event windows into one pass, so the activation model is not reachable from the running system, and the figures quoted below come from an offline harness that touches none of the four stores. That is the honest state of this milestone, and it is on the page because a milestone page that only lists wins is marketing.
Background reading: the five-part activation explainer and the laws of the context store.
The Fourth Plane
Three stores answer what is. None of them answers what happened.
Every decay term in the activation equation is a question about the past. The three tiers hold current state; they cannot tell you when something was last used, or how often, or in what context. So M2 adds a plane that does.
what is claimed
Truth. Five tables, append-only lineage.
M1
what is like what
Vectors. The entry point into the graph.
M1
what connects to what
Edges. Spreading activation walks here.
M1
what happened, and when
Events. Append-only, not derived from the rest.
M2
The event plane is deliberately not derived from the other three and not rebuildable from them. Delete it and the base-level term has no history to decay over. It carries no content field, ever - the events record that something was referenced, not what it said, which keeps a GDPR erasure cascade from having to reach into the ledger.
The ledger, its schema, the idempotent initializer and integration tests against a live ClickHouse are all built. No producer writes to it yet. Until one does, every time-decayed term still runs on fabricated or absent evidence.
What the Fit Exposed
Mistakes that were invisible until measured.
The activation function had been specified for months. Fitting it for the first time found that most of the interesting failures were in the measurement, not the model.
The base-level prior put the floor above the signal
β was set to
0.05, which put the activation
floor at ln(0.05) ≈ -3.0 -
above the entire real signal range of
-8.35 … -5.68. Every entry
clamped to the floor. Base level was inert in 42 of 42 swept cells, and nothing
about the output looked broken. The fitted value is
0.002.
The root cause was a conflation: β
and ACT-R's :blc are different
parameters. ln(β + Σ)
is a floor; ln(Σ) + blc is a
shift. They compose, and treating one as the other is how a constant ends up
25 times too large.
Fitting on recall alone silently disabled the gate
Recall is monotone in a lower bar. Fit on it and the optimiser drives the threshold down until the gate stops firing entirely - the headline metric improves while the mechanism underneath it dies. The first pass did exactly this. The fix is a balanced objective that scores correct abstention as well as recall, so "return everything" stops being a winning strategy.
Invariants are now applied as admissibility filters over the parameter grid rather than checked by eye. Cold-start reachability rejected the top-scoring cell by 0.08 units. As a judgement call it would have gone the other way.
The evaluation corpus was contaminated for an entire session
Documents describing the harness were scoring as gold results against the harness. The system was being graded on how well it could retrieve its own exam paper.
The fix is enforced in code, not by a maintained exclusion list: a run fails if any corpus section mentions an arm or probe identifier. Exclusion lists do not work here for a structural reason - the contaminating document is always written after the list. The guard caught a new contaminating document immediately.
A 39% invalid-citation rate came from an example ordinal in the prompt
Nearly two in five answers cited a block that did not exist, which read as a
serious retrieval defect. It was an example ordinal in the prompt - the model
was faithfully copying the [^2]
it had been shown, including on turns that injected fewer than two blocks. Fixing the prompt took it from
39% to 0%.
A metric that looks like a model failure is often a harness failure, and the harness is the thing nobody instruments.
Where the threshold sits, and what it costs
The scoring ranks better than cosine - 69% against 62% on the same candidates. Adding the threshold drops that to 38% and buys 80% correct refusal on questions the corpus cannot answer, where plain search confabulates on all ten. False refusal runs at 31%.
That trade rests on 23 probes, and the harness that produced it runs offline against an in-memory corpus - it touches none of the four stores, so none of these figures describe what this system retrieves. Widening the probe set and rerunning against a real retrieval path is the next experiment, once a retrieval path exists.
Progress
A component with no callers is not a working system.
The pieces below exist and are tested in isolation. Most of them are not wired to anything, so the integration items on the right are the ones that decide whether this milestone worked.
Built
- ✓ Context store reshaped to five tables with recordable provenance
- ✓ Activation specified against the primary ACT-R sources, with per-term provenance
- ✓
ActivationCalculator- pure, unit-tested. No callers - ✓
ContextVectorRepo- semantic candidate generation. No callers - ✓ ClickHouse event ledger, integration-tested live. No producer, no reader
- ✓ Constants fitted in simulation, and the spec corrected to match the code
- ✓ Offline scoring harness - arms, probes, balanced objective, contamination check in code
- ✓ Model provenance - an agent-authored claim names the model and the config that produced it
- ✓
docs/act-r/- 23-file cited literature review
Not Yet
- ○
RecallAsync- one retrieval path joining candidates, graph, events and the scorer. Nothing above is reachable from the running system until this exists - ○ An end-to-end test - ingest a document, assert Postgres, the vector store and the graph agree, re-ingest and assert supersession propagates. No such test exists
- ○ A producer that writes to the event plane - the ledger exists, nothing feeds it
- ○ The retrieval threshold decision
- ○ Assembly cascade implementation - carried from M1
- ○
ProjectEntryWorkflow- entry → embedding → Qdrant → FalkorDB - ○
ActivationTracein the dashboard, including "why was this NOT retrieved" - ○ Demo B - context-enhanced inference, measured against the no-context arm
Stretch
- ○
Continuum.Analyzers- Roslyn diagnostics enforcing the laws (CTX0001-CTX0008) - ○
context_adjudications- append-only human judgement, anchored on content hash - ○ Re-embed workflow for model changes
Design Decisions
Why it's built this way.
▶ Why a 40-year-old psychology model instead of a learned ranker?
A learned ranker needs labelled relevance data. At the start of this milestone there was none, and the only way to get it was to ship a ranking and watch what people used - which requires a ranking.
ACT-R gives a defensible prior with no training data at all. Anderson's base-level equation has four decades of empirical fit behind it, every term is interpretable, and the constants that matter are published. It is a starting point you can defend on the first day rather than the thousandth.
It is also inspectable in a way a learned model is not. Every retrieval decomposes into named terms, which is what makes "why was this not retrieved" answerable. A gradient-boosted ranker would score better eventually and explain nothing.
▶ Why compute activation at query time instead of caching it?
Because a written-back score is a lie with a timestamp. Activation depends on now - the decay term is a function of elapsed time since each past use. Persist it and it is stale the instant after the write, and the staleness is invisible.
It is also a law. The First Law forbids destroying or altering a claim, and a decayed value written onto the row is exactly that: an irreversible edit derived from a model that may later be found wrong. Recomputing from events means a corrected constant re-scores the entire store for free.
The cost is arithmetic per candidate per query. That is cheap, and it buys a store where fixing the ranking does not require a migration.
▶ Why a separate event plane instead of columns on the entry?
A reference_count column is
a ratchet with no memory of when its members happened. Old evidence never
fades, so a heavily-used entry becomes permanently hot and the decay term
stops meaning anything. The base-level equation needs individual
timestamps, not a total.
Events are also a fundamentally different shape of data - high-volume, append-only, never updated, queried in time ranges. That is a columnar workload, which is why it is ClickHouse and not another Postgres table.
The plane carries no content field, only references. That keeps a GDPR erasure cascade confined to Postgres, and it means the ledger can be retained longer than the material it describes without retaining the material.
▶ Why was the first reinforcement loop removed after it worked?
It recorded a reference every time assembly returned an entry. That sounds right and is not: the loop reinforced whatever the retriever already favoured and compounded it every turn - a ranking that promotes its own output until nothing else can surface.
Being shown something is not evidence you used it. The replacement signal has to come from the model actually citing an entry in its answer, which is why citation markers are enforced rather than requested.
It shipped working and was deleted a day later. The failure mode is invisible in any single retrieval and only shows up as drift.
▶ Why read the papers again when the specification was already written?
Four primary sources had been worked from search summaries rather than
the PDFs. Installing pdftotext
and re-reading them overturned five claims - three about our own
activation spec.
Two of the corrections were structural. Petrov's hybrid, which the spec
treated as a novel addition, is ACT-R's
:ol optimized learning -
and the project's tail term was a worse version of something canonical.
And the multi-hop spreading the spec described, attributed to ACT-R for
months, is Soar's design.
The resulting rule is now a hard constraint: read the primary source, do not summarise it. A search summary is not a citation.
Previous milestone
← Milestone 1 - Cortex: Context as a ServiceGoing deeper
Activation in practice →