pi-canon Mutable Canonical Memory over an Immutable Journal

One article per asset, at an address you can compute. An append-only journal underneath it keeps the original wording. Recall arrives on its own, when a tool call touches the thing the knowledge is about.

Scroll to explore

The Problem

Agent knowledge bases rot in two directions at once

Give a coding agent a place to write things down and it will write things down. Come back a month later and the store has gone wrong in two specific, predictable ways, and neither of them is the agent being careless.

The first is scatter. Nothing marks which article is the article about a topic, so the agent writes a new one. There is now a note on the vendor feed, another note on feed pagination, and a third on the sync job, all describing the same constraint from three angles, none of them wrong, and no way to tell which to trust when they disagree. Retrieval finds all three. The agent reads the one that ranks highest, which is not reliably the one that is current.

The second is diary drift. A store the agent writes to during work fills with events, because work is made of events: what was tried, what failed, what got fixed. Current truth ends up buried under a log of how it came to be true. The page that should say what the rule is instead says what happened on the fourteenth.

Both failures have the same shape. They are addressing failures, not storage failures. The knowledge is present; nothing determines where it belongs or which copy governs. So the structural answer is to make the address a function of the thing the knowledge is about, and to give events somewhere else to go.

Pick any file, and the address is already decided

An illustrative repository tree. Clicking any path shows how pi-canon resolves it to a governing article: the path's own article if it has one, otherwise a walk up to the nearest governed ancestor. src, src/feed, src/feed/models.py and src/report each carry an article. src/feed/sync.py has none of its own, so it resolves one hop up to src/feed. tools/cron_pulse.py resolves two hops up to no article at all, because tools is not governed.

The spine is the whole addressing scheme, and it is not a component: there is nothing to configure, and a rename is a file move. An asset resolves to its own article if it has one, and otherwise to the nearest governed ancestor, which is what lets a single article govern a subtree without anyone declaring that it does. Click any row. Watch src/feed/sync.py walk one hop to the article on src/feed, and tools/cron_pulse.py walk to nothing at all, because an ungoverned subtree simply has no capsule to offer. An article that matches no asset is not an error and carries no flag; it is free knowledge, and the rejected-alternative note in this tree is exactly that. Illustrative visualization.

The Model

Two tiers with opposite mutability disciplines

The canon is mutable and holds the current best understanding. One article per governed asset, rewritten in place as understanding changes, so there is always exactly one page to read and exactly one page to correct. The journal beneath it is append-only, one file per event, and the agent is instructed to preserve source wording, names, and exact numbers when it writes there.

The split is doing real work. A mutable page is the right shape for a rule, because a rule that changed should not leave its old self lying around to be found later. An immutable log is the right shape for an event, because an event that happened cannot stop having happened, and because the exact phrasing is often the part that matters: the error string, the vendor's own words on the call, the number someone quoted.

This maps onto a distinction cognitive psychology drew decades ago, and the mapping asks for no forcing. Episodic and semantic memory are the two halves of long-term declarative memory: the journal is the episodic half, temporally indexed and immutable, and the canon is the semantic half, the current understanding with the history stripped out. The mapping is a description of what the two tiers already are, not an argument that the design is brain-shaped.

Distillation is where it goes wrong, and the run behind this page shows exactly how. An article is a distillation of what the journal recorded, and distillation loses things. When it loses the operative clause of a constraint, the store is left holding a page that reads as authoritative and no longer says the thing that mattered.

What the journal keeps, and what the article can lose

A fact arriving in a session and splitting into two tiers. The journal entry preserves the source wording verbatim and is append-only. The article distills it and is rewritten in place by later sessions. A later rewrite drops the operative clause of the rule, leaving a page that still reads as authoritative; the laundering guard quotes the dropped language back at the write that removed it, and the journal still holds the original.

One fact, two destinations, and then a rewrite that goes wrong. The journal entry is written once and never edited, so the vendor's own phrasing survives; the article is rewritten freely, because a rule that changed should not leave its old self behind to be found. Step forward and watch a later session update the article and quietly drop the clause that made the rule enforceable. Nothing is corrupt, nothing errors, and the page still reads like the authority on the subject. The laundering guard is the answer: a write that deletes constraint language gets that language quoted back at it, by name. It cannot make an agent keep the line, and the measured section below counts how often the write desk lost material anyway. Illustrative visualization; the quoted before-and-after pair is taken verbatim from the run-2 artifacts.

Recall

The agent does not have to know there is something to ask

Retrieval systems answer questions. That is their strength and it is also their blind spot: a search only runs when something thinks to run it, and the expensive failures in project work are precisely the ones where nobody knew there was a question. An agent about to prettify a number in a report file does not stop to search for whether a downstream parser is fragile. It has no reason to. It does not know the parser exists.

So recall here is push rather than pull. When a tool call is detected touching a governed asset, that asset's article contributes a one-line capsule to the session, at most once per article per session, under a hard budget. Nothing is searched, and the agent never had to suspect anything.

The discipline around that is mostly about not being annoying. A capsule that arrives when the agent is already reading the file is noise, so a read that lands first withdraws the staged line: pull preempts push. Past the session budget an article still surfaces, but only as a pointer saying it exists and should be read, rather than spending tokens on its contents. And an article counts as seen only when its line actually rode out in a flushed message, so a nudge that got overtaken costs nothing.

The caveat belongs in the same breath: detection is best effort. Surfacing fires on tool calls the runtime recognizes as touching a governed path, and an agent that reaches an asset by a route the runtime does not recognize gets nothing. The mechanism is a push with a hit rate, not a guarantee.

A session, and the capsules that reach it

A session running left to right as a sequence of tool calls. Each call that touches a governed asset stages that article's capsule. Staged capsules resolve three ways: flushed into the next message, withdrawn because the agent read the file itself first, or degraded to a bare pointer once the budget is spent. In the run-2 artifacts 19 capsules were staged, 13 were withdrawn by a read that landed first, and 6 were flushed.

The funnel, drawn on a session that behaves like the measured ones. A tool call touching a governed path stages a capsule; the capsule is only charged if its line actually rides out in a flushed message. Turn read-preemption off and watch the withdrawn capsules become spend: the agent gets told things it was in the middle of reading for itself. Turn the budget down and watch capsules degrade into pointers rather than vanish, which is the design choice that keeps a hard cap from silently hiding knowledge. The counts on the funnel are measured: across the canon-family arms of run 2 the trace recorded 19 capsules staged, 13 withdrawn by a read that landed first, and 6 flushed. Illustrative session; the funnel counts are measured from the run-2 trace.

The Benchmark

A trap is a task whose obvious solution is wrong for a reason you were told once

Recall benchmarks ask questions and score answers, which measures memory under the assumption that someone knows to ask. That is the wrong instrument for this, so the primary metric here is not recall at all.

A chain is a small fictional repository and a four-session story, all four sessions sharing one worktree. A plant session does ordinary work whose natural course surfaces a constraint, never phrased as an instruction to remember. A distractor session does something unrelated. Then a probe session gets a task whose obvious solution violates the planted constraint, in a way that compiles, runs, and passes the visible tests, and fails a grader the agent cannot see. Finally a recall session answers an auditor's questions, graded fact by fact by a pinned judge.

Trap avoidance is the fraction of probe cells where every trap-eligible check passes. It is a proxy for avoided rework, and the relationship is worth stating rather than assuming: a failed trap is a change that looks finished, passes review, and breaks something the team already knew about.

Four arms. canon is stock Pi plus pi-canon. canondoc adds a static doctrine file. agents.md is a self-maintained convention file, preloaded with a frozen ninety-nine-line file of mature-project noise. bare is the floor, and it is a stronger floor than it sounds: it gets no memory extension, but prior-session transcripts are written into its worktree before the recall session, and archived runs show its recall agent finding and reading them. It is a no-memory floor at probe time and a full transcript baseline at recall time, which is why it can legitimately win recall while failing traps.

One cell, end to end

One cell of the benchmark drawn as four sessions sharing a single persistent worktree: plant, distractor, probe and recall. The hidden grader runs against a post-probe snapshot and the chain's pristine external copy. The pinned judge grades each recall fact in its own call. Two cold controls run on fresh seeds with nothing planted: a probe-cold run whose failures define the trap set, and a recall-cold run that certifies or demotes the declared plant-only facts.

The instruments live outside the agent's reach in different ways, and the difference matters. The external enforcement package is copied into the cell beside the worktree and the agent can read and run it, because the prompts reference it by relative path; grading uses the chain's pristine copy instead, so a session told the external is broken cannot help itself by editing it. The hidden grader is never placed in the cell at all. Click any session to see what it does and what it leaves behind for the next one. Illustrative visualization.

The Instrument

What counts as a trap is a fact about the model, not about the task

A trap only tests memory if a model without memory actually falls into it. So eligibility is executed rather than asserted: a cold run attempts the probe on a fresh seed with nothing planted, and every check it fails becomes trap-eligible. No check is a trap because the author says so. One of the five chains produces no trap at all under the headline worker, because a cold worker writes the lazy client correctly on its own, and that chain contributes no probe outcomes: a hundred cells yield eighty.

The recall side works the other way around, and the asymmetry is worth being precise about because it is where author judgment enters. Plant-only facts are declared in each chain's manifest and then certified by a recall-cold run, which can demote a declared fact it recovers but can never promote one it did not. Five facts across the suite went unrecovered by a cold worker and still count as secondary, because nobody declared them. The trap set is derived; the recall set is authored and filtered.

Then the whole thing moves when the worker changes. Swapping in a related model from the same family, one chain stops being a trap because a cold run of that model declines the memoization unaided, and two declared facts get demoted because it derives them from the checkout without the plant. Same suite, same graders, same prompts. Eligibility is model-relative, and any number built on it inherits that.

Change the worker, and the instrument changes with it

Per chain, the trap-eligible checks and the recall facts, for two worker models. Under Luna, four of five chains carry a trap endpoint and all nine declared plant-only facts survive certification. Under Sol, memo-poison loses its trap entirely, and two declared facts are demoted because a cold Sol recovers them from the repository alone. The tasks are identical in both cases.

Flip the worker. Nothing about the suite changes: same chains, same prompts, same graders, same hidden checks. What changes is what those instruments can measure, because both eligibility sets are properties of the model that was run against them. Hover any fact to see which of the four states it is in, and note the one that cannot happen: a fact nobody declared can never be promoted into the headline metric, however clearly a cold run failed to recover it. That is the asymmetry between a derived trap set and an authored recall set, and it is the most direct route by which the benchmark author's judgment reaches a headline number. Measured from the sealed run artifacts.

The Result

Nineteen of twenty, against eight

On the confirmatory run, canon passed 19 of 20 trap cells. The floor passed 8. The self-maintained convention file passed 18, and canon plus a static doctrine file passed 16.

The floor's failures are the informative part, because they repeat rather than scatter. bare failed the consumer-contract chain in all five repetitions and the timezone chain in four of five, which is what a genuine no-memory floor looks like: it is not unlucky, it simply cannot know. canon's single failure is one repetition of one check on the timezone chain, and that chain is the one trap that costs every arm at least one cell.

The generalization unit is the binding limit here. Five hand-built chains, and on the trap metric four eligible trap designs, each repeated five times. Repetitions repeat the same traps, so a hundred cells is not a hundred independent observations and no uncertainty interval is attached to any pooled count.

Every eligible probe of the confirmatory run

Eighty trap-eligible probe cells, four arms by four trap-carrying chains by five repetitions. canon passes 19 of 20, agents.md 18 of 20, canondoc 16 of 20 and bare 8 of 20. Switching to every intended check removes the cold-control conditioning entirely and scores all 110 checks per arm: canon 109, agents.md 107, canondoc 105, bare 85.

One square is one probe cell. Defining traps through the floor arm's cold failures conditions the metric on that arm's behaviour, which is the strongest objection to the number, so the conditioning is a switch here rather than a footnote. Turn it off and every intended check counts, whether or not a cold worker had already failed it: the chain with no trap endpoint rejoins, the denominators become 110, and the ordering survives. Hover any cell to see which check failed and which checks were trap-eligible in that chain. Measured from the sealed run artifacts.

The Negative Result

Recall does not separate the arms, and one item decides the order

The secondary metric found nothing, and it is worth saying so plainly. Plant-only recall clustered at 41, 40, 42 and 40 out of 45 across canon, canondoc, agents.md and bare. Four arms, three facts apart, with the floor level with the memory system.

It is worse than a null result, because the ordering is not even stable. One fact, flagged as paraphrase-sensitive before the run was frozen, carries 9 of the 17 misses across all four arms. Remove that single item and the ranking rearranges: agents.md answers everything, canondoc drops to 38, and canon lands level with the floor at 37.

The right reading is that every arm recalls planted facts at roughly the same high rate and this instrument cannot separate them. Calling the remainder noise would overstate it, because with one pinned judge and no repetition study there is no noise distribution to compare against. What the run measured is that the ordering moves on one judged item, which is a statement about the instrument rather than about the arms.

Where the arms do separate is the price of the answer. canon answers recall at 0.34x the floor's median recall tokens, and the pattern is mechanical: the two expensive arms are the ones with large persistent text at recall time, one a noise-laden convention file, the other raw transcripts. Tokens and dollars part ways here, and the paper is careful never to call canon cheaper: canondoc is lowest on both median recall tokens and total chain cost, and the floor's total sits below canon's.

Clustered on recall, separated on the bill

Two panels. On the left, plant-only recall out of 45 per arm: agents.md 42, canon 41, canondoc 40 and bare 40, clustered within three facts; removing the pre-flagged item reorders them to agents.md 40 of 40, canondoc 38, canon and bare level at 37. On the right, median recall session tokens: canondoc 13,991, canon 20,775, bare 61,006 and agents.md 64,568.

The left panel is a null result and the right panel is the finding. Press the button and watch the recall ranking rearrange on the removal of a single judged fact, which is what it means to say an instrument cannot separate its arms: the order was never carrying information. The right panel is where the arms actually differ, and the difference is structural rather than clever, because both expensive arms carry large persistent text into the recall session. Hover any bar for that arm's median recall cost and total chain cost. Every dollar figure is metered worker-session cost; the judge's calls are archived but never metered, so no cost figure on this page includes them. Measured from the sealed run artifacts.

The Caveats

What a freeze can and cannot buy

The protocol was written down and frozen before the confirmatory run: hypotheses, ranked metrics, a rerun rule, and a manifest pinning every suite-defining file by SHA-256. Execution after that point is genuinely constrained, and the artifacts let anyone check it. That is worth something, and it is worth less than it sounds.

The five chains are development-exposed. The product changed in response to failures on these same chains, and the confirmatory run reuses them. The freeze confirms disciplined execution; it does not make the chains unseen. Unseen chains are the external-validation path, and none were run.

The same person wrote the package, the chains, the traps, the graders, and the plant-only declarations, and watched every development run. Hidden graders, executed cold controls, and a full artifact trail are procedural controls and auditability measures. None of them creates independence between the benchmark and the product, and none addresses the possibility of choosing tasks congenial to it.

Two more limits belong on the same page as the numbers. The freeze carries no third-party timestamp, so it is pre-specified rather than pre-registered. And eligibility rests on one or a few stochastic cold executions per chain: one chain's cold control ran three times and disagreed with itself, passing, then failing two checks, then passing again, under a harness with no rule for adjudicating repeats. The most recent execution decided it. Scoring the alternative leaves the ordering unchanged, which is why the disclosure costs the argument nothing, but the machinery is that thin.

What Is Next

The write desk is where it fails, not the retrieval

A development run traced every missed fact to its first failure point, and the answer was lopsided. Of fourteen missed verdicts, thirteen were lost at the write desk: eight never captured at all, five overwritten by a later rewrite. None were present in the store and unread. None were read and then not asserted.

That localizes the problem precisely, and away from the part the design spends most of its effort on. Surfacing worked. Addressing worked. What failed was capture: the agent wrote down a distillation that had already lost the operative clause, or a later session rewrote the page and weakened the rule. The journal-the-source doctrine and the laundering guard are both answers to that finding, and the follow-up run moved the same workload from 10 of 18 fact-level verdicts to 17 of 18.

So the open problem is write-side quality under adversarial conditions rather than recall coverage. The next experiments are unseen chains, a second judge, and an ablation that separates the store from the surfacing layer, because right now the evaluation is package-level and cannot attribute the result to either half.

pi-canon is published on npm as pi-canon under the MIT license. The benchmark, its drivers, the frozen protocol, and the verifier that recomputes every number on this page from the artifacts are in a separate repository. Both are linked below.

Citations

The paper, the package, the benchmark, and the work this stands on

The paper. Shane Conner, "pi-canon: Mutable Canonical Memory over an Immutable Journal, with Recall by Surfacing. Design, and a four-arm multi-session evaluation", Zenodo, August 2026. doi.org/10.5281/zenodo.21890648. The deposit beside the PDF carries the per-cell artifact trail for every cited run, the figure sources, and a provenance index mapping every claim in the paper to the artifact field behind it.

pi-canon. The package: github.com/shaneconner/pi-canon. Published on npm as pi-canon, MIT licensed.

canon-bench. The five chains, the drivers, the frozen protocol with its hash manifest, and the results: github.com/shaneconner/canon-bench. tests/verify_claims.py recomputes all 92 quantitative claims in the paper from the shipped artifacts and names the source field for each.

pi-fold. The companion package on the working tier, separate and optional in both directions: shaneconner.com/projects/pi-fold.

Episodic and semantic memory. Endel Tulving, "Episodic and Semantic Memory", in Organization of Memory, Academic Press, 1972. The two halves of long-term declarative memory that the journal and the canon map onto.

Working memory. Alan Baddeley and Graham Hitch, "Working Memory", in The Psychology of Learning and Motivation, 1974; and Richard Atkinson and Richard Shiffrin, "Human Memory: A Proposed System and its Control Processes", 1968.

Letta, formerly MemGPT. github.com/letta-ai/letta. The canonical agent-held memory verbs, where storage addresses are the model's choice and recall is a search the model must think to run.

The AGENTS.md convention. agents.md. The always-in-context convention file, which is the arm this evaluation runs against.

Lost in the Middle. Nelson Liu and colleagues, 2023. arXiv:2307.03172. Retrieval quality degrading with context occupancy, which is the failure mode an always-loaded convention file walks into at scale.

Tech Stack

TypeScript D3.js Pi Agent Memory Benchmark Design Python
View Source → ← All Projects