Skip to content

Commit 4d226da

Browse files
committed
fix(consolidation): make the exact-dup guard case-sensitive
_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).
1 parent ebfb48b commit 4d226da

2 files changed

Lines changed: 20 additions & 6 deletions

File tree

hindsight-api-slim/hindsight_api/engine/consolidation/consolidator.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,14 @@
5454

5555

5656
def _norm_obs_text(text: str) -> str:
57-
"""Whitespace/case-normalised observation text for exact-duplicate matching."""
58-
return " ".join((text or "").split()).strip().lower()
57+
"""Whitespace-normalised observation text for exact-duplicate matching.
58+
59+
Collapses runs of whitespace only; case is preserved. The reconciliation guard
60+
drops a CREATE on the premise that an exact-text match loses no information — but
61+
case-folding would also drop a create differing only in case (e.g. "TLS" vs "tls"),
62+
which *does* lose information, so we match case-sensitively.
63+
"""
64+
return " ".join((text or "").split()).strip()
5965

6066

6167
def _duplicate_create_target(

hindsight-api-slim/tests/test_consolidation_dedup.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,27 @@ def _shown(*observations: _FakeObs) -> dict[str, _FakeObs]:
2020
return {_norm_obs_text(o.text): o for o in observations}
2121

2222

23-
def test_norm_obs_text_collapses_whitespace_and_case() -> None:
24-
assert _norm_obs_text(" The User likes BASIL.\n") == "the user likes basil."
23+
def test_norm_obs_text_collapses_whitespace_preserves_case() -> None:
24+
# Whitespace (incl. newlines) collapses; case is preserved.
25+
assert _norm_obs_text(" The User likes BASIL.\n") == "The User likes BASIL."
2526
assert _norm_obs_text(None) == ""
2627

2728

2829
def test_create_matching_shown_observation_is_duplicate() -> None:
2930
shown = _shown(_FakeObs(id="11111111-aaaa", text="User waters the herbs early in the morning."))
30-
# Same text with different whitespace/case still matches.
31-
target = _duplicate_create_target("user waters the herbs early in the morning.", shown, set())
31+
# Same text with only-whitespace differences still matches.
32+
target = _duplicate_create_target("User waters the herbs early in the morning.", shown, set())
3233
assert target is not None
3334
assert target.startswith("shown observation 11111111")
3435

3536

37+
def test_create_differing_only_in_case_is_not_duplicate() -> None:
38+
# Case-folding would lose information (e.g. acronyms), so a case-only difference
39+
# is treated as novel rather than silently dropped.
40+
shown = _shown(_FakeObs(id="22222222-bbbb", text="The user prefers TLS."))
41+
assert _duplicate_create_target("The user prefers tls.", shown, set()) is None
42+
43+
3644
def test_create_matching_inresponse_update_is_duplicate() -> None:
3745
update_texts = {_norm_obs_text("Mint is kept in its own separate bed.")}
3846
target = _duplicate_create_target("Mint is kept in its own separate bed.", {}, update_texts)

0 commit comments

Comments
 (0)