Typed Memory & Extraction Pipelines · module 1 of 8 · The Write Path in Earnest · lesson 14 of 84 · ~40 min
$
uv run python curriculum/intermediate/typed-memory-model/lab/lab.py
The Typed Memory Model¶
In one line. Beginner used the type as a label; here it becomes the thing that decides whether a memory can go stale, what happens when two disagree, and what a correct update looks like.
Where this sits¶
Stage: store · Level: intermediate · ~40 min
You need first: Watching It Fail
Concepts assumed: Semantic Memory · Episodic Memory · Procedural Memory · The Memory Record
This unlocks: Extraction Pipelines
The problem¶
You are about to build conflict detection, and the first question is which memories can even conflict.
Naively, any two. In practice, almost none. Priya's store holds 36 memories and two of them are:
episodic Priya is leaving Northwind Labs (2025-12-08)
episodic Priya left Northwind Labs last month (2026-01-19)
Those do not contradict. They are two things that happened, both permanently true, and a system that "resolves" them by retiring one has destroyed a fact for no reason. Meanwhile these two do contradict, and nothing currently notices:
semantic Priya does not drink coffee (2025-06-11)
semantic Priya drinks three coffees a day (2026-02-27)
The difference is not in the content, the wording, or the similarity score — embedding-recall already proved similarity cannot tell these cases apart. It is entirely in the type, which Beginner recorded and then never consulted.
Why this isn't RAG¶
A corpus has one type. Every chunk is treated identically because, for documents, that is roughly correct — a passage does not claim to describe the present, so it cannot be falsified by a later passage.
The moment memories make claims about now, they acquire different lifecycles, and the type becomes an executable rule rather than metadata. There is no chunking strategy that expresses "this one may go stale and that one may not", because no document ever needed it.
Mechanism¶
Four types, four rules. The column that matters is the first.
| Type | Can contradict? | Expires? | On conflict | Retrieved by |
|---|---|---|---|---|
| episodic | no | no | keep both — two things happened | time, participants |
| semantic | yes | yes | one must retire | topic |
| procedural | yes | yes | replace wholesale | task, not topic |
| working | no | yes | irrelevant — dies with the session | position |
RULES in memlab.extract.router is that table, and can_contradict() is the predicate every later mechanism gates on. Of Priya's 36 memories, 24 can contradict — 22 semantic plus 2 procedural — and 12 cannot, all of them episodic. Those 12 are structurally incapable of the failure the next four lessons are about, and conflict detection never has to look at them.
Procedural is the interesting middle case. Two versions of a workflow do conflict, but the correct resolution is not "retire one fact" — it is replace the whole procedure, because the steps are not independently updatable. The type says so, and the code reads it rather than special-casing.
flowchart TD
M["memory"] --> T{"type"}
T -->|episodic| E["can_contradict = false<br/><i>never enters conflict detection</i>"]
T -->|semantic| S["can_contradict = true<br/><i>the only type that can go stale</i>"]
T -->|procedural| P["can_contradict = true<br/><i>replaced wholesale</i>"]
T -->|working| W["dies with the session"]
style S fill:#f9e79f,stroke:#b7950b,stroke-width:2px
There is a second rule here, needed by the very next lesson: describes_a_change(). A turn built around a transition verb — leaving, started, moved, was diagnosed — reports an event, and the state it produced has to be derived rather than hoped for. Type routing and change detection are the same judgement seen from two directions.
Design decisions¶
Rules in a table, or behaviour on the type? A table. TypeRule is data, so the whole policy is visible in one place, diffable, and testable without constructing memories. Methods on an enum scatter the policy across the codebase and make "what does semantic actually do differently" unanswerable without reading everything.
Should procedural be its own type at all? Yes, and it is the type most often collapsed into semantic. Its on_conflict differs from every other type — wholesale replacement rather than per-fact retirement — and that difference is exactly what a taught workflow needs. Collapse it and updating one step silently reshuffles the rest.
Add a fifth type for preferences? No. Preferences are semantic memories with a longer half-life; that is a salience question, not a type question. Every additional type multiplies the cross-type conflict matrix, and this one buys nothing.
Lab¶
You'll implement: can_contradict reading the rule table, and partition_by_conflict_risk — split a store into the memories conflict detection must examine and the ones it can ignore.
Run:
uv run python curriculum/intermediate/typed-memory-model/lab/lab.py
Expected output: of 36 memories, 24 can contradict and 12 cannot — by type alone, before a single comparison is made — which cuts the pairwise comparison space from 630 to 276. Then the two Northwind episodes are shown beside the two coffee facts, and the rule table explains why only one pair is a problem.
Stretch: feed the two Northwind episodes to can_contradict and confirm both return False, then read RULES[EPISODIC].on_conflict. A system that lacks this table has no principled way to avoid "fixing" them, and retiring a true episode is a data-loss bug you will never see reported.
What this adds to the capstone¶
memlab.extract.router — TypeRule, RULES, can_contradict, describes_a_change. Nothing calls it yet; the next lesson does.
Failure modes¶
| Symptom | Cause | How to detect | Mitigation |
|---|---|---|---|
| A true episode gets retired | Conflict detection run over all types | Check whether any episodic memory has invalid_at set |
Gate on can_contradict |
| Old preferences never die | Preferences typed episodic, which never expires | Look for two live semantic-shaped facts that disagree | Route claims about now to semantic |
| Updating a workflow shuffles its steps | Procedure typed semantic, updated per-fact | Ask the system to perform a taught procedure after an update | on_conflict = replace wholesale |
| Conflict detection is slow and mostly useless | Comparing every pair regardless of type | Count comparisons vs. eligible pairs | Partition by type first |
Check yourself¶
'Priya is leaving Northwind' and 'Priya left Northwind last month' describe one event. Are they duplicates?
Near-duplicates worth merging in deduplication, but not a conflict — merging and retiring are different operations with different consequences. The type tells you they cannot contradict; whether they are redundant is a separate question, answered by similarity.
24 of 36 memories can contradict. What does that buy?
Correctness before performance. Conflict detection is now structurally unable to touch the 12 episodic records where retiring something would be data loss. The comparison count falling from 630 pairs to 276 is a side benefit, not the point.
Procedural memories can contradict. Why are they not just semantic, then?
Because on_conflict differs. Two versions of a workflow do conflict, and the correct resolution is to replace the procedure wholesale rather than retire one step — steps are not independently updatable. Same answer to "can this be wrong", different answer to "what do I do about it", which is exactly why the rule is a table rather than a boolean.
Why is describes_a_change in the router rather than the extractor?
Because it is a claim about type. "Does this turn report a transition" and "what type is the resulting memory" are the same judgement, and keeping them together stops the two from drifting apart.
Connections¶
Stage: store · Level: intermediate · ~40 min
You need first: Watching It Fail
Concepts assumed: Semantic Memory · Episodic Memory · Procedural Memory · The Memory Record
This unlocks: Extraction Pipelines