Skip to content

fix(consolidation): eliminate duplicate observations (interleave dedup recall + tool & benchmark)#1907

Merged
nicoloboschi merged 6 commits into
mainfrom
fix/observation-consolidation-duplication
Jun 3, 2026
Merged

fix(consolidation): eliminate duplicate observations (interleave dedup recall + tool & benchmark)#1907
nicoloboschi merged 6 commits into
mainfrom
fix/observation-consolidation-duplication

Conversation

@nicoloboschi

@nicoloboschi nicoloboschi commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator

Problem

Consolidation could emit duplicate observations — multiple near-identical (sometimes byte-identical) observations of the same fact within a single document/session.

Root cause (investigated end-to-end)

Two distinct mechanisms, confirmed with the new per-bank LLM tracing (#1922):

  1. Byte-identical creates — the LLM occasionally emits a CREATE whose text exactly matches an observation already shown to it (or an UPDATE in the same response).
  2. Recall misses (the dominant residual) — the existing near-identical observation (the merge "twin") is semantic rank Fix dependencies and docker #1 for the new fact, but it shares no source-fact graph link and little lexical overlap, so RRF averaged it below the per-fact recall budget (consolidation_max_tokens=512, ~8 observations) and the LLM never saw it → created a duplicate. The cross-encoder demoted it the same way (semantic Fix dependencies and docker #1 → reranked async operations error #37).

Verified on traced runs: for every residual near-dup, the twin was semantic #1 (cosine 0.84–0.88) yet absent from the consolidation prompt; the LLM's own reason said "not covered by existing observations".

Fix

  • Interleave fusion for consolidation dedup recall. A new round-robin fusion takes each retrieval arm's Fix dependencies and docker #1, then each rename to hindsight #2, … (semantic → bm25 → graph → temporal), de-duplicating, until the budget fills — so every arm's top hit is guaranteed a slot and the semantic-Fix dependencies and docker #1 twin is always shown. The LLM then UPDATEs instead of creating a duplicate.
  • Unified reranking strategy param. Replaced the recall rerank: bool + ad-hoc passthrough with a single named strategy: cross_encoder (default, unchanged for user-facing recall), rrf, interleave. Consolidation uses interleave. For interleave the fusion order is authoritative — the recency/temporal re-sort is skipped (that re-sort is part of what buried the twin).
  • Deterministic post-LLM reconciliation guard for the byte-identical case: a CREATE whose normalised text matches an observation shown to the LLM (or an in-response UPDATE) is dropped (no info loss).
  • reason field on consolidation actions (LLM explains each create/update/delete) — kept for observability.

Results

Measured on an English translation of the hermes transcript (removes a cross-lingual embedding confound: the default bge-small-en can't align Chinese facts ↔ English observations, which inflated loose near-dups):

metric RRF (before) 1/4 interleave (after) 1/4
near-dups @0.97 4 (4%) 0
near-dups @0.92 9 (9%) 0
coverage 89% 94%
both-flags / failures 0 / 0 0 / 0

No false merges: the heavy observations are coherent single themes, and distinct facets (per-language targets, plan phases) stay as separate observations.

Also included

  • hindsight-dev observation-dedup tool (obs_dedup) — re-embeds observations, cosine-clusters near-dups; reused by the benchmark.
  • benchmarks/obs quality benchmark — ingests a dataset, drains consolidation (serial SyncTaskBackend so the drain loop is the sole consolidator), and reports the duplication rate (exact + @0.97/@0.92). --fraction / --dataset / --wipe-bank flags.

Testing

  • tests/test_interleave_fusion.py — round-robin order, dedup, semantic-top-hit-always-first, strictly-decreasing scores.
  • tests/test_consolidation_dedup.py — the reconciliation guard.
  • Default recall path unchanged (cross_encoder); existing recall/scoring tests pass.

@nicoloboschi nicoloboschi force-pushed the fix/observation-consolidation-duplication branch 6 times, most recently from 3cb2942 to 8a1f322 Compare June 3, 2026 14:18
@nicoloboschi nicoloboschi changed the title fix(consolidation): eliminate duplicate observations (+ dedup tool & trace instrumentation) fix(consolidation): eliminate duplicate observations (interleave dedup recall + tool & benchmark) Jun 3, 2026
Enumerates a bank's observations via the API, re-embeds locally, and
clusters near-duplicates by cosine similarity. Includes unit tests and a
registered 'find-duplicate-observations' script.
Consolidation produced duplicate observations two ways: (1) the cross-encoder
reranker demoted a near-identical existing observation below the recall budget,
hiding it from the LLM; (2) even when shown the twin, the model emitted an
identical CREATE anyway (often while also UPDATE-ing it).

Changes:
- recall: add a 'rerank' flag; consolidation recall ranks by RRF (rerank=False)
  so a near-identical existing observation stays near the top instead of being
  demoted by the cross-encoder. Default recall path is unchanged.
- consolidation: drop a CREATE whose normalized text exactly matches an
  observation shown to the LLM, or the text of an UPDATE issued in the same
  response (exact match => no information loss). See _duplicate_create_target;
  dropped creates are logged at WARNING.
- add a 'reason' field to consolidation create/update/delete actions so the
  model explains its choices (surfaced when a duplicate create is dropped).
- tests/eval: deterministic unit test for the dedup guard
  (test_consolidation_dedup.py); the duplicate-observation rate is tracked by a
  new quality benchmark (hindsight-dev/benchmarks/obs) over a synthetic
  herb-garden transcript and the real PII-free hermes session that surfaced the
  bug, reusing the obs-dedup tool — instead of a flaky real-LLM pytest.
The benchmark's explicit run_consolidation_job drain loop raced the default
BrokerTaskBackend's worker poller (which runs retain's auto-submitted
consolidation and the consolidator's own round-limit re-submit), leaving facts
with both consolidated_at and consolidation_failed_at. Use SyncTaskBackend so
all consolidation runs inline/serially and the drain loop is the sole
consolidator; additionally disable auto-consolidation per-bank to confine
consolidation to the drain loop for a clean measurement point.

Add --fraction (scale up 1/4 -> full incrementally), --dataset (filter), and
--wipe-bank (default now persists the bank for inspection) flags.
… always shown

The residual near-duplicate observations were recall misses, not LLM errors:
the existing near-identical observation (the merge 'twin') is semantic rank #1
for the new fact, but shares no source-fact graph link and little lexical
overlap, so RRF averaged it below the per-fact recall budget (512 tokens, ~8
observations) and the LLM never saw it -> created a duplicate. The cross-encoder
demoted it the same way (semantic #1 -> reranked #37).

Add round-robin interleave fusion: take each retrieval arm's #1, then each #2,
... (semantic, bm25, graph, temporal), de-duplicating, until the budget fills.
This guarantees every arm's top hit a slot, so the semantic-#1 twin is always
shown and the LLM UPDATEs instead of creating a duplicate.

Replace the recall 'rerank: bool' / ad-hoc passthrough with a single named
'reranking' strategy: 'cross_encoder' (default), 'rrf', 'interleave'.
Consolidation dedup recall uses 'interleave'. For interleave the fusion order is
authoritative — combined scoring's recency/temporal re-sort is skipped (that
re-sort is what buried the twin).

Measured on an English translation of the hermes transcript (removes the
cross-lingual embedding confound): near-dup observations 4% (@0.97) -> 0% at both
1/10 and 1/4 cuts, coverage 89% -> 94%, no false merges (heavy observations are
coherent single themes; distinct facets stay separate).
…dataset

Wire the observation-dedup benchmark into the continuous-performance-monitor
like perf-test/LoComo:
- obs_benchmark.py: add --output and a headline overall_duplication_rate
  (near-dup redundant / total observations at the strictest threshold).
- scripts/benchmarks/publish-obs-results.sh: enrich a run with commit/workflow
  metadata and push data/obs/<run>.json + data/obs-index.json to the dashboard
  repo's gh-pages (mirrors publish-locomo-results.sh).
- perf-test.yml: add an 'obs' job (VertexAI, embedded pg0, serial SyncTaskBackend)
  that runs the benchmark and publishes; workflow_dispatch inputs obs_skip/
  obs_dataset/obs_fraction. Defaults to the English hermes transcript at full
  fraction — a clean, deterministic signal (the Chinese variant adds a
  cross-lingual embedding confound).
- Add the English-translated hermes dataset (the monolingual signal the
  interleave fix was validated on).

Dashboard frontend (obs.html + nav) lives in the hindsight-continuous-performance-monitor repo.
_norm_obs_text dropped a CREATE whose text matched a shown observation only after
lower-casing. But the guard's premise is that an exact-text match loses no info on
drop — case-folding breaks that (a create differing only in case, e.g. an acronym
like "TLS" vs "tls", carries different information). Collapse whitespace only;
keep case. The guard body stays — it's a zero-cost deterministic backstop for the
verbatim-re-create mode, complementary to interleave recall (which surfaces the twin;
the guard catches the LLM emitting an identical CREATE anyway).
@nicoloboschi nicoloboschi force-pushed the fix/observation-consolidation-duplication branch from 791f0fc to 4d226da Compare June 3, 2026 14:59
@nicoloboschi nicoloboschi merged commit 613a699 into main Jun 3, 2026
150 of 158 checks passed
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