Skip to content

fix(diff): stop destroying memories on CONFLICT and cosine-only UPDATE#84

Open
astrologist wants to merge 1 commit into
mnemon-dev:masterfrom
astrologist:fix/diff-conflict-clobber
Open

fix(diff): stop destroying memories on CONFLICT and cosine-only UPDATE#84
astrologist wants to merge 1 commit into
mnemon-dev:masterfrom
astrologist:fix/diff-conflict-clobber

Conversation

@astrologist

Copy link
Copy Markdown

Problem

mnemon remember silently soft-deletes an existing memory whenever the built-in diff classifies the new content as CONFLICT or UPDATE. Both paths misfire on long technical notes, destroying unrelated memories:

  1. classifySuggestion returns CONFLICT when similarity >= 0.7 and either text contains a negation word ("replaced", "no longer", "never", "deprecated", …). Change-log style memories almost always contain these words, and same-domain notes easily reach 0.7 similarity through shared vocabulary plus the cosine >= 0.85 hoist.
  2. A CONFLICT is precisely the case where auto-deleting one side is wrong — the texts disagree, so the caller should decide which one wins.

We hit this three times in production use (v0.1.13): storing a new memory about topic A silently deleted an unrelated memory about topic B in the same entity cluster. Each time the only reason no data was lost permanently is that the replaced_id in the response was noticed in-session.

Reproduction

Store two same-domain notes describing different facts, both containing "no longer" (any two realistic incident write-ups about the same system will do). The second remember returns action: updated, diff_suggestion: CONFLICT and soft-deletes the first.

Fix

  • remember: CONFLICT now keeps both memories. Detection is unchanged and the response still carries diff_suggestion: CONFLICT, so callers can merge or forget deliberately.
  • remember: UPDATE only auto-replaces when the top match has TokenSimilarity >= 0.6. Cosine-only similarity (same-domain embeddings cluster at 0.85+) is not enough evidence to destroy a memory.
  • classify: near-verbatim re-statements (token similarity > 0.9) are classified DUPLICATE before the negation scan — previously an exact duplicate containing a negation word was classified CONFLICT (masked by the old replace behavior; exposed once CONFLICT keeps both).
  • classify: a new text >25% longer than its match is never DUPLICATE. Previously a superset extension (old text + a new sentence) was cosine-hoisted above 0.9, classified DUPLICATE, and skipped — silently dropping the new content.

Testing

  • go test ./internal/search passes (call sites updated for the new tokenSim parameter).
  • Manual scenario matrix in an isolated MNEMON_DATA_DIR (with Ollama embeddings active):
scenario before after
same-domain, different facts, negation words CONFLICT → old memory deleted CONFLICT → both kept
exact re-statement containing negation words CONFLICT (replace) DUPLICATE → skipped
superset extension (old + new sentence) DUPLICATE → skipped, new info lost UPDATE → replaced
unrelated new content ADD ADD

🤖 Generated with Claude Code

The diff engine silently soft-deletes an existing memory whenever the new
content is classified CONFLICT or UPDATE. Both paths misfire on long
technical notes and destroy unrelated memories:

1. classifySuggestion returns CONFLICT when similarity >= 0.7 and EITHER
   text contains a negation word ("replaced", "no longer", "never",
   "deprecated", ...). Change-log style memories almost always contain
   these words, and same-domain notes easily reach 0.7 through shared
   vocabulary plus the cosine >= 0.85 hoist. Observed three times in
   production: a new memory about topic A silently deleted an unrelated
   memory about topic B in the same entity cluster.

2. A CONFLICT is precisely the case where auto-deleting one side is
   wrong: the texts disagree, so the caller should decide which one wins.

Changes:

- remember: CONFLICT now keeps both memories. Detection is unchanged and
  the response still carries diff_suggestion=CONFLICT, so callers can
  merge or forget deliberately.
- remember: UPDATE only auto-replaces when the top match has
  TokenSimilarity >= 0.6. Cosine-only similarity (same-domain embeddings
  cluster at 0.85+) is not enough evidence to destroy a memory.
- classify: near-verbatim re-statements (token similarity > 0.9) are
  classified DUPLICATE before the negation scan — a text can no longer
  "conflict" with an identical copy of itself and get re-added forever.
- classify: a new text >25% longer than its match is never DUPLICATE.
  Previously a superset extension was cosine-hoisted above 0.9,
  classified DUPLICATE, and skipped — silently dropping the new content.

Repro (before): store two same-domain notes describing different facts,
both containing "no longer"; the second remember returns
action=updated and soft-deletes the first. After: both are kept with
diff_suggestion=CONFLICT.

go test ./internal/search passes; existing table-driven cases unchanged
(call sites updated for the new tokenSim parameter).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant