Skip to content

feat(memory): detect and surface conflicting entries on write#88

Merged
Siddhant-K-code merged 1 commit into
mainfrom
feat/conflict-detection
May 9, 2026
Merged

feat(memory): detect and surface conflicting entries on write#88
Siddhant-K-code merged 1 commit into
mainfrom
feat/conflict-detection

Conversation

@Siddhant-K-code

Copy link
Copy Markdown
Owner

What

When a new memory entry is semantically similar to an existing one — but not similar enough to be a duplicate — Distill now flags it as a conflict and returns both entries to the caller.

Why

When new context contradicts something already in memory, Distill previously stored both silently. The agent then operated with conflicting information, producing inconsistent behavior. Now the caller knows about the conflict and can resolve it (supersede the old entry, expire the new one, or keep both).

How it works

Two thresholds define three zones:

cosine distance:  0 ──── 0.15 ──── 0.35 ──── 1.0
                  │  duplicate  │  conflict  │  unrelated  │
                  │  (merged)   │  (flagged)  │  (stored)   │
  • < DedupThreshold (0.15): Exact semantic duplicate → merged, access count bumped
  • DedupThreshold to ConflictThreshold (0.35): Conflict → stored AND flagged in response
  • > ConflictThreshold: Unrelated → stored normally

Conflicts are surfaced, not blocked. The entry is always stored.

Changes

  • Config.ConflictThreshold — configurable conflict zone boundary (default: 0.35)
  • StoreResult.Conflicts — list of Conflict structs with NewID, NewText, ExistingID, ExistingText, Distance
  • Refactored findDuplicatefindSimilar returning both duplicates and conflicts
  • Expired entries excluded from conflict detection
  • 7 new tests

Usage

curl -X POST localhost:8080/v1/memory/store -d '{
  "entries": [{"text": "Auth uses HMAC with HS256", "embedding": [...]}]
}'

# Response when conflict detected:
{
  "stored": 1,
  "conflicts": [{
    "new_id": "abc123",
    "new_text": "Auth uses HMAC with HS256",
    "existing_id": "def456",
    "existing_text": "Auth uses JWT with RS256",
    "distance": 0.23
  }]
}

The caller can then resolve with POST /v1/memory/supersede to replace the old entry.

Closes #77

- Add ConflictThreshold config (default 0.35) for the conflict detection zone
- Entries with cosine distance between DedupThreshold and ConflictThreshold
  are flagged as conflicts but still stored (no silent overwrites)
- StoreResult.Conflicts lists each conflict with both entry IDs and texts
- Expired entries are excluded from conflict detection
- Refactor findDuplicate into findSimilar returning both dups and conflicts
- 7 new tests: similar-not-identical, exact-dup, far-apart, multiple
  conflicts, still-stores, no-embedding, expired-ignored

Closes #77

Co-authored-by: Ona <no-reply@ona.com>
@Siddhant-K-code Siddhant-K-code added the enhancement New feature or request label May 9, 2026
@Siddhant-K-code Siddhant-K-code merged commit 020f678 into main May 9, 2026
2 checks passed
@Siddhant-K-code Siddhant-K-code deleted the feat/conflict-detection branch May 9, 2026 07:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Detect and surface conflicting memory entries

1 participant