Supersession is now a first-class signal
The store could say what was true, and not what changed. It held 36 entries — 36 active, 0 deprecated, 0 supersedes links — while dec-013 stated it replaced the old additive-only sync and con-006 carried "(was additive-only)" inline. deprecate_entry had supported superseded_by since v0.10 and had never been used once. The capability wasn't missing: nothing asked at the moment the answer was known, and nothing spent the link afterwards.
Write time. record_entry now names active same-kind entries covering the same subject — possible supersession of <id>: <summary>. Scored on shared tags and whole-component scope overlap, deliberately not the text overlap similar_entries already computes: a replacement often shares almost no wording with what it replaces while addressing exactly the same thing. It never links, never blocks the write, never touches the older entry — a heuristic edge silently demotes a rule that may still be in force.
Read time. get_context prepends one compact line for the immediate predecessor — what it said, and why it changed. One level deep. Under budget pressure the line is dropped and a predecessor_id trail kept, never the entry. The remote Worker emits a byte-identical line, so history reads the same over stdio and HTTP.
Backfill. scripts/survey_supersessions.py proposes links read-only — opens no store for writing, calls no lifecycle tool.
con-011 was false, and is now enforced by construction
The constraint requires every surface deciding "does this scope cover this file" to agree. There were five surfaces with four implementations, two of which provably disagreed — and score_entry still used the raw substring test the rule exists to forbid, so querying scope="hooks/" gave a webhooks/-scoped entry the same boost as the real one.
scope_rules.py is now the only implementation, importing nothing so the PreToolUse hook can load it under con-010. Ranking gains partial credit as a side effect: hooks/ against hooks/scope_guard.py is a real 0.5 overlap, which substring matching could never express.
Retrieval has a number, and it holds still
A 59-case golden set over a frozen 7-store corpus — 44 positive, 9 unanswerable, 6 asking for superseded history — with every question written from the problem an entry solves rather than reworded from its summary.
| arm | recall@5 | hit@5 | MRR |
|---|---|---|---|
| lexical | 0.417 | 0.500 | 0.370 |
| embedding (w=150) | 0.644 | 0.700 | 0.553 |
Two findings worth stating: history is unreachable (recall@5 = 0.000 in both arms — supersession is first-class at write and read time and retrieval still can't find a predecessor when asked directly), and abstention misses in-domain negatives (6 of 9). Both are pinned by tests so a fix can't land unnoticed either.
The corpus is frozen because the first pin broke within a day without a line of ranking code changing — it read live stores.
Internals
server.py 4,270 → 3,787 lines (ranking.py, mojibake.py, quality_checks.py, all re-exported). verify_quality's seven inlined checks became a registry. The test suite split from one 5,213-line file into six themed ones. 544 tests pass; tools/list unchanged at 2955/3150 tokens.
New
📊 jarmstrong158.github.io/context-keeper — a project page whose every number is generated from committed measurement output, with a test that fails if the page and the measurement disagree.
pip install context-keeper-mcp · Claude Desktop: download the .mcpb below