Sprint 11 closed: memory that forgets relevance, not facts
Sprint 11 is closed. Meronq's project memory now has a lifecycle: knowledge gains strength when it is used, cools when it is not, and is archived rather than deleted when it disappears from the codebase. ADR-0009 (Knowledge Lifecycle and Vitality) moved from Proposed to Accepted.
The principle we were building toward: Meronq does not forget facts. It forgets relevance.
The sprint started from three bug reports filed at the end of Sprint 10 — strength_delta was written but never read, access history was wiped on every sync, and entity search ranked alphabetically. They read like three small fixes. Two of them were not.
One bug, three causes
The headline bug was that a full sync wiped usage history. The obvious culprit was an explicit DELETE FROM access_events. Removing it fixes nothing, because the foreign keys do the same damage independently:
access_events.entity_id TEXT REFERENCES entities(id) ON DELETE CASCADE
With PRAGMA foreign_keys = ON, the DELETE FROM entities that follows destroys the usage history anyway. Full-replace had to become upsert; there was no one-line fix.
Worse, handshake runs a sync on every session start. So usage history never survived a single session — which is precisely why the event substrate from ADR-0005 looked inert. It was collecting signal and throwing it away before anything could read it.
Tombstones need stable identity
Once rows are archived instead of deleted, anything with an unstable identity accumulates forever. And our identities were unstable: defineRelation and defineEvidence fell back to crypto.randomUUID(), so every scan produced a structurally identical but freshly-identified graph.
The first run against a real database made this obvious — it archived all 37 relations and all 22 evidence rows, then inserted 29 and 14 new ones. Every sync would have done that again, forever.
The fix is content-addressed identity in the core model: a relation is identified by its edge, evidence by its kind and source. The second identical sync then archived nothing. Stable identity is also the prerequisite for strength to accumulate against a relation at all — you cannot reinforce something that gets a new name every time you look at it.
Presence is not usage
With history finally durable, aggregation was still meaningless, because almost everything in the event tables is machine bookkeeping rather than human interest:
- Every sync wrote a reinforcement event against the project entity. That means "the scan saw this project", not "someone used this knowledge".
- Every handshake recorded an access event for every entity in the snapshot. A bulk sweep like that makes all knowledge equally hot and equally recent — differentiating nothing.
Both are now classified as presence and excluded from derived strength, while remaining in the tables as an audit trail. Sync bookkeeping is recorded under a sync: source prefix; bulk sweeps are excluded from the usage counters.
That left a new problem: with bookkeeping excluded, nothing reinforced individual entities, so strength was zero everywhere except the project itself. So memory_search hits now reinforce the entities they return. Searching for knowledge is using it, and retrieval that improves itself is the point of the whole exercise.
Relevance still beats popularity
Ranking by strength alone would be a trap: search for scanner and get whatever happens to be popular. So relevance decides the tier — exact id, then id, then name, then summary, then properties — and vitality only reorders hits within a tier. The tiers are spaced wider than the maximum vitality boost, which makes the guarantee structural rather than a matter of tuning. There is a test that piles fifty reinforcements onto a weak summary match and asserts a plain id match still wins.
Cooling is a 14-day half-life applied at query time. Nothing is rewritten, no background job mutates state, and no evidence is ever discarded. Decay changes priority, not truth.
Verified on real data, not just tests
The interesting failure modes here only appear on a database with real history, so every risky change was also run against a copy of a real memory.db — never the original:
- The v2 to v3 migration preserved all 47 access events (
47 -> 47, previously47 -> 0). - A repeated sync archived zero rows, confirming identity is stable.
- Searching
appswith no usage returned everythingcoldin name order. After usingpackages/scanner, it ranked first ashot.
The nicest part was unplanned: real historical usage surfaced on its own. Two ADRs came back warm purely from genuine memory_search calls in earlier sessions. The memory had been recording our attention all along; it just had no way to report it.
Final state: 2 hot, 2 warm, 25 cold entities, reported as a Knowledge Vitality metric in memory_stats and as a single line in every handshake.
What we deliberately did not build
ADR-0009 describes more than one sprint of model. Left out on purpose: a background decay pass (query-time weighting covers ranking without rewriting state), confidence and explicit decay-rate attributes, per-project thresholds, and the archived-recovery and evidence-reuse metrics. Thresholds are global constants living in one module, which is what will make per-project configuration cheap when there is evidence anyone needs it.
160 tests, 0 failures, 15 packages.