Skip to content

feat(consolidation): semantic dedup of near-duplicate observations (create + update)#1977

Merged
nicoloboschi merged 1 commit into
mainfrom
investigate/flash-lite-dups
Jun 5, 2026
Merged

feat(consolidation): semantic dedup of near-duplicate observations (create + update)#1977
nicoloboschi merged 1 commit into
mainfrom
investigate/flash-lite-dups

Conversation

@nicoloboschi

@nicoloboschi nicoloboschi commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator

Problem

Weak consolidation models (e.g. gemini-2.5-flash-lite) emit near-duplicate observations even when the twin is shown to them. Interleave-fusion recall (#1907) fixed this for strong models, but at 1/4 scale on the English hermes benchmark flash-lite still left ~7% residual near-duplicates (cosine ≥0.97).

Root cause

Instrumented the consolidation create path (HNSW probe vs an exact Python cosine over every observation) and ran flash-lite at 1/4 scale: 0 probe misses across the entire run — the create-time probe never fails to surface an existing twin. The residual duplicates are instead born on the UPDATE path: _execute_update_action rewrites an observation's text and re-embeds it to fold in a new fact, and the rewritten vector can drift to ≥0.97 of a different existing observation. Nothing reconciled that — dedup only ran on CREATE. Every residual pair had ≥1 member that had been updated (history>0/proof_count>1).

It is not a recall, HNSW-recall, pgvector-freshness, transaction-visibility, or caching problem (all ruled out with data: deterministic embeddings, autocommit connections, per-bank HNSW index present from row 0).

Fix

consolidation_dedup_threshold (default 1.0 = disabled, Postgres only). When set below 1.0, an observation that is ≥ the threshold cosine to an existing one is reconciled via a focused 1-by-1 LLM "merge or keep" call (anchored on the observation text — the correct obs↔obs comparison — scope consolidation_dedup; the model reads both texts so a number/negation/entity difference is respected):

  • CREATE path — on "merge", fold the new source facts + synthesized text into the existing twin and skip the insert.
  • UPDATE path (the fix for the residual) — after the rewrite+re-embed, probe the new vector (excluding the row itself); on "merge", fold the updated observation's sources into the twin, persist the merged text, and delete the now-redundant updated row.

Shared probe+adjudicate logic is factored into _dedup_adjudicate; _execute_update_action returns its freshly-computed embedding so the update-path probe doesn't re-embed.

Result

English hermes obs benchmark, flash-lite, 1/4 scale: residual ≥0.97 near-dups ~7% → 0–1% (UPDATE-path merges observed firing alongside CREATE-path merges). Default 1.0 keeps behaviour unchanged.

Tests

tests/test_consolidation_dedup.py (13, no LLM/DB): exact-text guard, create-path (no-twin / keep / merge-folds / picks-highest), update-path (merge folds+deletes the updated row / keep / self-exclusion / no-twin).

@nicoloboschi nicoloboschi force-pushed the investigate/flash-lite-dups branch from babee52 to fc3c476 Compare June 5, 2026 07:50
@nicoloboschi nicoloboschi changed the title feat(consolidation): create-time semantic dedup of near-duplicate observations feat(consolidation): semantic dedup of near-duplicate observations (create + update) Jun 5, 2026
…reate + update)

Weak consolidation models (e.g. gemini-2.5-flash-lite) emit near-duplicate
observations even when the twin is in context, and an UPDATE that rewrites +
re-embeds an observation can drift it into a near-twin of a different existing
observation. When consolidation_dedup_threshold < 1.0, an observation that is
>= the threshold cosine to an existing one is reconciled by a focused 1-by-1 LLM
"merge or keep" call (anchored on the observation text, not the source fact, so
it is the correct obs<->obs comparison):

- CREATE path: on "merge", fold the new source facts + synthesized text into the
  existing twin and skip the insert.
- UPDATE path: after the rewrite+re-embed, probe the new vector (excluding the
  row itself); on "merge", fold the updated observation's sources into the twin
  and delete the now-redundant updated row.

Default 1.0 disables it (no behaviour change). Postgres only. On the English
hermes obs benchmark with flash-lite at 1/4 scale, residual >=0.97 near-dups
drop from ~7% to 0-1%.
@nicoloboschi nicoloboschi force-pushed the investigate/flash-lite-dups branch from fc3c476 to 929bfff Compare June 5, 2026 08:03
@nicoloboschi nicoloboschi merged commit 61f9bc8 into main Jun 5, 2026
80 checks passed
@nicoloboschi nicoloboschi deleted the investigate/flash-lite-dups branch June 5, 2026 08:19
nicoloboschi added a commit that referenced this pull request Jun 5, 2026
… on Oracle (#2000)

The create+update semantic dedup added in #1977 shipped opt-in (threshold 1.0).
Enable it by default at 0.97 so observations are deduplicated out of the box.

The merge path uses Postgres-only SQL, so consolidation skips dedup entirely on
Oracle (via _dedup_active) — it behaves exactly as before there, regardless of
the configured threshold. This is what lets the default flip without breaking
Oracle deployments.

Also fix MockLLM to return a valid keep-decision for the consolidation_dedup
scope, so mock-LLM consolidation tests (which now exercise the enabled-by-default
path) don't crash on the structured response and never spuriously merge.
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