Skip to content

Commit 1a14e94

Browse files
safishamsiclaude
andcommitted
fix(reflect): dedupe dead-ends and corrections by question
Saving the same Q&A more than once duplicated lines in the "known dead ends" and "corrections" sections: both lists were appended per memory doc with no key, while node scoring already dedups by node. They now collapse by question, keeping the most recent entry (docs are processed oldest-first, so a re-corrected question shows its latest correction). Output stays deterministic, ordered by (date, question). Applied to both the flat lists and the per-community buckets. Found by a user running it on a 104-file Go codebase. Added a regression test covering the dedupe and the recency-wins correction. Full suite 2389 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent d193b62 commit 1a14e94

3 files changed

Lines changed: 34 additions & 3 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ Full release notes with details on each version: [GitHub Releases](https://githu
44

55
## Unreleased
66

7+
- Fix: `graphify reflect` no longer duplicates lines in the "known dead ends" and "corrections" sections when the same Q&A is saved more than once. Those lists were appended per memory doc with no key (node scoring already dedups by node, but these two did not); they now collapse by question, keeping the most recent entry — so a re-corrected question shows its latest correction. Output stays deterministic (ordered by date then question).
78
- Fix: the work-memory loop no longer depends on the git hook. The skill now tells the agent to run `graphify reflect --if-stale` itself at the start of graph work (cheap, deterministic, a no-op when no outcomes have been saved), then read `LESSONS.md`. Previously a skill-only install (without `graphify hook install`) would keep recording outcomes via `save-result` but never regenerate `LESSONS.md`, so the lessons never surfaced. The post-commit hook is now an optimization for between-session freshness rather than a requirement. The new `--if-stale` flag skips the run when `LESSONS.md` is already newer than every input (the memory docs and the graph), so when the hook just refreshed it the agent's session-start run costs almost nothing.
89

910
## 0.8.47 (2026-06-24)

‎graphify/reflect.py‎

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,20 @@ def _finalize_sources(bucket: dict[str, Any],
328328
return {"preferred": preferred, "tentative": tentative, "contested": contested}
329329

330330

331+
def _dedupe_by_question(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
332+
"""Collapse repeated questions to one entry. Docs are processed oldest-first, so
333+
the last write per question wins (recency — e.g. the most recent correction text).
334+
Output is deterministically ordered by (date, question). Without this, saving the
335+
same Q&A twice duplicated lines in the dead-ends / corrections lists, even though
336+
node scoring already dedups by node.
337+
"""
338+
latest: dict[str, dict[str, Any]] = {}
339+
for it in items:
340+
latest[it.get("question", "")] = it
341+
return sorted(latest.values(),
342+
key=lambda it: (it.get("date", ""), it.get("question", "")))
343+
344+
331345
def aggregate_lessons(docs: list[dict[str, Any]],
332346
node_community: dict[str, str] | None = None,
333347
*,
@@ -384,7 +398,8 @@ def aggregate_lessons(docs: list[dict[str, Any]],
384398
if node_community:
385399
community_out = {
386400
label: {"counts": b["counts"], **_finalize_sources(b, min_corroboration),
387-
"dead_ends": b["dead_ends"], "corrections": b["corrections"]}
401+
"dead_ends": _dedupe_by_question(b["dead_ends"]),
402+
"corrections": _dedupe_by_question(b["corrections"])}
388403
for label, b in by_community.items()
389404
}
390405

@@ -393,8 +408,8 @@ def aggregate_lessons(docs: list[dict[str, Any]],
393408
"counts": overall["counts"],
394409
"min_corroboration": min_corroboration,
395410
**_finalize_sources(overall, min_corroboration),
396-
"dead_ends": overall["dead_ends"],
397-
"corrections": overall["corrections"],
411+
"dead_ends": _dedupe_by_question(overall["dead_ends"]),
412+
"corrections": _dedupe_by_question(overall["corrections"]),
398413
"by_community": community_out,
399414
}
400415

‎tests/test_reflect.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,3 +631,18 @@ def test_cli_reflect_if_stale_skips_when_fresh(tmp_path):
631631
ran = _run(["reflect", "--if-stale"], tmp_path)
632632
assert ran.returncode == 0
633633
assert "up to date" not in (ran.stdout + ran.stderr).lower()
634+
635+
636+
def test_dead_ends_and_corrections_dedupe_by_question():
637+
"""Saving the same Q&A more than once must not duplicate lines in the dead-ends
638+
/ corrections lists; for a re-corrected question the most recent text wins."""
639+
docs = [
640+
_doc("dead_end", question="ws server?", date="2026-01-01"),
641+
_doc("dead_end", question="ws server?", date="2026-01-02"), # duplicate
642+
_doc("corrected", question="hash?", correction="SHA-1", date="2026-01-01"),
643+
_doc("corrected", question="hash?", correction="SHA-256", date="2026-01-03"), # newer
644+
]
645+
agg = aggregate_lessons(docs, now=_NOW)
646+
assert [d["question"] for d in agg["dead_ends"]] == ["ws server?"]
647+
assert len(agg["corrections"]) == 1
648+
assert agg["corrections"][0]["correction"] == "SHA-256" # recency wins

0 commit comments

Comments
 (0)