fix(memory): hide superseded entries from semantic recall - #20
Merged
Conversation
The server never negotiated: handleInitialize discarded its params and always answered with its own protocolVersion. A client moving to a newer MCP revision was therefore unobservable — we would learn about it from a failure rather than from telemetry. Logging only; the response is unchanged and a mismatch is not an error. This is step 1 of MCP-PROTOCOL-MIGRATION-2026-07-28, which turns the migration trigger from a calendar guess into an instrumented signal.
Watching initialize turned out to be blind. Measured 2026-08-10 against a live client: Claude Code reconnects to a restarted HTTP server by going straight to tools/call and never re-sends initialize, which dispatch accepts because no handshake is required. The detector added in 9355a79 therefore never fires for the case it was built for. An unknown method is the reliable signal: a client on revision 2026-07-28 calls server/discover, a mandatory RPC we do not implement, and today that returns method-not-found silently. The line also carries the protocol version from _meta when present, so it says which revision the caller speaks rather than only that something unknown was asked for.
An entry whose superseded_by points at a live successor stayed in recall with its original vector, so the dead copy kept competing with — and often out-ranking — the successor. Downranking via MarkOutdated was not enough. Recall now skips it; List/ListLightweight still return it so the maintenance and temporal-history views are unchanged. The successor is looked up in the cache rather than trusted: Delete does not clear superseded_by on predecessors, and a dangling pointer would otherwise bury the entry forever. MarkOutdated without a successor keeps the pre-existing downrank behaviour. TestMarkOutdatedDownranksMemory pinned the old contract and is renamed and updated accordingly. Also pins issue #19 (plain markdown must classify as docs) with the two regression cases from PR #17; the fix itself already landed in b052f2d. Fixes #18 Fixes #19
This was referenced Aug 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #18, closes #19. Picks up the recall fix from #17 (thanks @ch405canova-sudo) with one behavioural correction, and adds the regression coverage both issues were missing.
Issue #18 — superseded entries in recall
Recallnever looked atsuperseded_by, so a merged/outdated entry stayed in the result set with its original vector and kept competing with its successor.MarkOutdatedonly downranks (importance capped at 0.25), which is not enough when the two embeddings are near-identical.Recall now skips such entries.
List/ListLightweightstill return them, so the temporal-history and maintenance views are unchanged.Difference from #17: the successor is looked up in the cache instead of being trusted.
Deletedoes not clearsuperseded_byon predecessors, so an unconditional skip would bury an entry forever once its successor is deleted. An archived entry beats no entry at all.MarkOutdatedwithout a successor keeps the pre-existing downrank behaviour — nothing replaced the entry, so hiding it would just lose the knowledge.Issue #19 — plain markdown dropped from the index
Already fixed on
mainin b052f2d / bd00d87 (the.mdbranch returnsdocsunconditionally); the fix is simply not in a release yet, which is why v0.9.1 still reproduces it. This PR only adds the two regression cases from #17 so the heuristic cannot come back.Contract change
TestMarkOutdatedDownranksMemorypinned the old "superseded stays in recall, just lower" contract. Renamed toTestMarkOutdatedHidesSupersededMemoryFromRecalland updated.Verification
go build ./...,go vet ./...cleango test ./...— 709 passed, 0 failedTestSupersededMemoryExcludedFromRecallfails on the expected assertion