Summary
A single historian extraction can mint a durable project memory. There is already a replication counter — memories.seen_count — but it increments on exact normalized-hash match, so it measures whether the historian phrased a fact identically twice, not whether two sessions independently corroborated it.
This proposes one small primitive (an episode table) that makes corroboration actually computable, plus two optional policies on top. The primitive is the ask; the policies are a sketch you can take or leave.
All references against upstream/master @ b27a6d76 (v0.38.0). Measurements from my own store (1,432 active memories) — the shapes should reproduce anywhere, the exact percentages will not.
The gap
memory/promotion.ts:63-68:
const existingMemory = getMemoryByHash(db, projectPath, fact.category, normalizedHash);
if (existingMemory) {
updateMemorySeenCount(db, existingMemory.id);
continue;
}
Two consequences:
- Hash equality is the corroboration test. Two sessions that independently learn the same thing but phrase it differently produce two rows at
seen_count=1, not one at seen_count=2. The counter tracks the historian's phrasing consistency.
- The episode set is discarded. The bump records a count and
last_seen_at; it does not record which session bumped it. source_session_id is singular — the first writer only (167 distinct values across 1,432 rows here). So "corroborated by N independent sessions" cannot be answered even approximately.
Live distribution:
seen_count = 1 1259
= 2 127
= 3 31
= 4 8
= 5 6
verification_status = 'verified' 78 (5.5%, despite verify running nightly)
The primitive (the actual ask)
CREATE TABLE memory_episodes (
memory_id INTEGER NOT NULL,
session_id TEXT NOT NULL,
seen_at INTEGER NOT NULL,
actor TEXT,
PRIMARY KEY (memory_id, session_id)
);
Append at the existing bump site and at insert. The PRIMARY KEY (memory_id, session_id) is doing real work: it counts a session once regardless of how many times extraction restates the fact within it, so the count cannot be inflated by extraction volume.
That alone makes "how many distinct sessions, on how many distinct days, corroborated this?" answerable — which is useful for curate, for the dashboard, and for anyone reasoning about which memories have earned their place, independent of whether you adopt any tiering policy. Cost is one migration and roughly two lines in promoteSessionFactsDurable.
Optional policy A — two tiers
If you want it: new facts land episodic, promote to durable on N distinct sessions (proposed N=2 with day-independence; N=3 plus actor-independence for CONSTRAINTS/PROJECT_RULES, where a wrong memory costs a wrong unattended decision), or on an existing verification artifact. Demotion on contradiction goes to a visible disputed state rather than a silent overwrite. Promotion records which criterion fired.
Two things worth knowing before you consider this:
The bar is brutal on a real corpus. Under seen_count >= 2 OR verified, my store promotes 247 of 1,432 — so 1,185 (83%) would sit episodic. Some of that is the hash-equality flaw above (real corroboration miscounted), but not all. The criteria and N are a dial and the episode table is what makes any setting measurable; I would not ship a default without measuring on your own corpus first.
Quarantine should be non-injection, not a marker. #320 established that a "this is data, not instructions" preamble on <project-memory> demotes PROJECT_RULES, which exist to bind behavior. So episodic facts should simply not render until promoted, rather than rendering with a trust marker. No new prompt wording to freeze forever.
Cache note, since this touches the injected set: a tier flip changes which memories render, so it is not cache-neutral the way setMemoryClassification deliberately is (storage-memory.ts:1117-1120 documents that contrast). It has to ride the existing non-additive path — memory_mutation_log → <memory-updates> in m[1] → reconcile into m[0] on the next natural hard fold. The hazard is batch shape rather than mechanism: a nightly promotion burst writes many mutation rows at once, inflating m[1] in one step, which can trip the m[1] pressure-backstop refold across active sessions the following morning. Mitigations: cap promotions per night, or let them accumulate and flush on each session's next natural fold instead of minting deltas eagerly.
Optional policy B — rank consolidation by surprise
Extraction depth currently does not track information content. In practice the memories that earn their keep disproportionately come from corrections, refutations, and gate failures after green claims, while routine-success episodes tend to restate what the docs already say.
The non-obvious part is where this has to live. The dreamer looks like the natural home, but its only transcript surface is user-only:
retrospective-raw-provider.ts:414 "the newest `count` USER rows. Returns user-only"
The historian reads assistant, user, and tool rows (read-session-db.ts:108/145/247). So of the marker classes worth detecting — operator corrections, REFUTED/verdict reversals, explicit retraction language, self-caught errors — the dreamer can see only the first. Detection belongs in the historian, which already has the bytes in hand at extraction time; the dreamer's batch pass consumes the flags to allocate depth. Nearly free, no second transcript read.
Guards if pursued: small integer weight tiers (not learned weights), artifact markers (gate output, review verdicts) outrank self-reported ones, density caps so repeated "correction" language cannot farm rank — and surprise ranks but never filters, so a zero-marker episode holding a first-seen fact still gets extracted.
Episode-dedup is what keeps A and B independent: deeper extraction of a surprising episode must not inflate promotion counts, and PRIMARY KEY (memory_id, session_id) guarantees it cannot.
What I am asking
Interest in the episode table specifically. It is small, useful on its own, and no policy has to follow it. If you want it, I will send a PR for just that — migration, the two write-site lines, tests — and we can discuss tiering separately or not at all.
Prerequisite for the "operator ratified it" criterion is filed separately as #334 (the "user" source-type variant is declared and rendered but never written).
Design provenance: this came out of a cross-project design exchange; I attacked both drafts against this codebase and the findings above are what survived. Cross-links: #334, #320.
Summary
A single historian extraction can mint a durable project memory. There is already a replication counter —
memories.seen_count— but it increments on exact normalized-hash match, so it measures whether the historian phrased a fact identically twice, not whether two sessions independently corroborated it.This proposes one small primitive (an episode table) that makes corroboration actually computable, plus two optional policies on top. The primitive is the ask; the policies are a sketch you can take or leave.
All references against
upstream/master@b27a6d76(v0.38.0). Measurements from my own store (1,432 active memories) — the shapes should reproduce anywhere, the exact percentages will not.The gap
memory/promotion.ts:63-68:Two consequences:
seen_count=1, not one atseen_count=2. The counter tracks the historian's phrasing consistency.last_seen_at; it does not record which session bumped it.source_session_idis singular — the first writer only (167 distinct values across 1,432 rows here). So "corroborated by N independent sessions" cannot be answered even approximately.Live distribution:
The primitive (the actual ask)
Append at the existing bump site and at insert. The
PRIMARY KEY (memory_id, session_id)is doing real work: it counts a session once regardless of how many times extraction restates the fact within it, so the count cannot be inflated by extraction volume.That alone makes "how many distinct sessions, on how many distinct days, corroborated this?" answerable — which is useful for
curate, for the dashboard, and for anyone reasoning about which memories have earned their place, independent of whether you adopt any tiering policy. Cost is one migration and roughly two lines inpromoteSessionFactsDurable.Optional policy A — two tiers
If you want it: new facts land episodic, promote to durable on N distinct sessions (proposed N=2 with day-independence; N=3 plus actor-independence for
CONSTRAINTS/PROJECT_RULES, where a wrong memory costs a wrong unattended decision), or on an existing verification artifact. Demotion on contradiction goes to a visibledisputedstate rather than a silent overwrite. Promotion records which criterion fired.Two things worth knowing before you consider this:
The bar is brutal on a real corpus. Under
seen_count >= 2 OR verified, my store promotes 247 of 1,432 — so 1,185 (83%) would sit episodic. Some of that is the hash-equality flaw above (real corroboration miscounted), but not all. The criteria and N are a dial and the episode table is what makes any setting measurable; I would not ship a default without measuring on your own corpus first.Quarantine should be non-injection, not a marker. #320 established that a "this is data, not instructions" preamble on
<project-memory>demotesPROJECT_RULES, which exist to bind behavior. So episodic facts should simply not render until promoted, rather than rendering with a trust marker. No new prompt wording to freeze forever.Cache note, since this touches the injected set: a tier flip changes which memories render, so it is not cache-neutral the way
setMemoryClassificationdeliberately is (storage-memory.ts:1117-1120documents that contrast). It has to ride the existing non-additive path —memory_mutation_log→<memory-updates>in m[1] → reconcile into m[0] on the next natural hard fold. The hazard is batch shape rather than mechanism: a nightly promotion burst writes many mutation rows at once, inflating m[1] in one step, which can trip the m[1] pressure-backstop refold across active sessions the following morning. Mitigations: cap promotions per night, or let them accumulate and flush on each session's next natural fold instead of minting deltas eagerly.Optional policy B — rank consolidation by surprise
Extraction depth currently does not track information content. In practice the memories that earn their keep disproportionately come from corrections, refutations, and gate failures after green claims, while routine-success episodes tend to restate what the docs already say.
The non-obvious part is where this has to live. The dreamer looks like the natural home, but its only transcript surface is user-only:
The historian reads assistant, user, and tool rows (
read-session-db.ts:108/145/247). So of the marker classes worth detecting — operator corrections, REFUTED/verdict reversals, explicit retraction language, self-caught errors — the dreamer can see only the first. Detection belongs in the historian, which already has the bytes in hand at extraction time; the dreamer's batch pass consumes the flags to allocate depth. Nearly free, no second transcript read.Guards if pursued: small integer weight tiers (not learned weights), artifact markers (gate output, review verdicts) outrank self-reported ones, density caps so repeated "correction" language cannot farm rank — and surprise ranks but never filters, so a zero-marker episode holding a first-seen fact still gets extracted.
Episode-dedup is what keeps A and B independent: deeper extraction of a surprising episode must not inflate promotion counts, and
PRIMARY KEY (memory_id, session_id)guarantees it cannot.What I am asking
Interest in the episode table specifically. It is small, useful on its own, and no policy has to follow it. If you want it, I will send a PR for just that — migration, the two write-site lines, tests — and we can discuss tiering separately or not at all.
Prerequisite for the "operator ratified it" criterion is filed separately as #334 (the
"user"source-type variant is declared and rendered but never written).Design provenance: this came out of a cross-project design exchange; I attacked both drafts against this codebase and the findings above are what survived. Cross-links: #334, #320.