Skip to content

fix(prune): never delete records that are recorded dedup survivors (#80)#95

Merged
edheltzel merged 2 commits into
mainfrom
worktree-fix+80-prune-dedup-lineage-guard
Jun 18, 2026
Merged

fix(prune): never delete records that are recorded dedup survivors (#80)#95
edheltzel merged 2 commits into
mainfrom
worktree-fix+80-prune-dedup-lineage-guard

Conversation

@edheltzel

Copy link
Copy Markdown
Owner

Closes #80

What

recall prune deleted records with no awareness of dedup_lineage. Pruning a record that is a recorded survivor orphaned every duplicate marked under it — the duplicates stayed hidden from search while their visible representative was gone. Same visible-survivor decay class as #63, through a different door.

The fix is the conservative guard option from the issue, consistent with PR #79's destructive-mode survivor guard and the stance recorded in ADR-0003 ("prune must not silently orphan a survivor's marked duplicates"). I did not choose re-point/unmark: that is #63's option (b) chain-compression, which PR #79 explicitly deferred, and unmarking would mutate dedup state as a side-effect of prune.

Changes

  • src/lib/dedup.tsnotRecordedSurvivorSql(tableExpr, idExpr), the survivor-side mirror of notMarkedDuplicateSql. Active lineage is status IN ('marked','deleted') — the same set loadRecordedSurvivors treats as binding (reverted carries no obligation).
  • src/commands/prune.ts — appends the guard to the three dedup-able pruned tables (messages, decisions, breadcrumbs) on both the count and the DELETE, so the reported count matches what is removed. Withheld rows are surfaced in the summary (N kept as dedup survivors) — never silent (mirrors PR fix(dedup): make recorded survivors sticky across runs #79's stickySkipped). sessions/extraction_* are not dedup tables; loa_entries/learnings are never pruned.
  • docs/cli-reference.md — completes the ADR-0003 user-facing dedup-safety prose: (a) mark→delete is non-escalating; (b) the new prune survivor guard; (c) the semantic-pass asymmetry — a sticky survivor acquires no new semantic duplicates after fix(dedup): make recorded survivors sticky across runs #79, exact pass unaffected.
  • tests/commands/prune.test.ts (new) — pins the behavior.

Tests

tests/commands/prune.test.ts (5 tests):

  • protects a recorded survivor message and still prunes a non-survivor — survivor kept, plain pruned, duplicate never orphaned, summary reports kept as dedup survivors
  • protects a survivor decision and prunes a non-survivor decision
  • 'deleted'-status lineage also protects its survivor (breadcrumbs)
  • 'reverted' lineage carries no obligation — a former survivor is prunable
  • dry-run reports protected survivors without deleting anything

Verification

  • bun run lint clean.
  • bun test tests/commands/prune.test.ts: 5 pass; tests/commands/dedup.test.ts: 22 pass (no regression from the new helper).
  • Mutation check: neutering the guard (NOT EXISTS over an empty set) fails 4 of 5 prune tests (the reverted test correctly still passes — reverted is never protected). Restored → green.
  • Full suite: the only failures are 10 pre-existing opencode/pi MCP-config tests, confirmed failing on the clean base (47bc38b) before this branch — unrelated to prune/dedup.

`recall prune` deleted records with no awareness of `dedup_lineage`. Pruning a
record that is a recorded survivor orphaned every duplicate marked under it —
the duplicates stayed hidden from search while their visible representative was
gone (the same visible-survivor decay class as #63, via a different door).

Fix (conservative guard, consistent with PR #79's destructive-mode survivor
guard and ADR-0003): prune now excludes recorded survivors (active
`dedup_lineage`, status `marked`/`deleted`) from deletion for the three
dedup-able pruned tables — messages, decisions, breadcrumbs. Withheld rows are
counted and surfaced in the summary ("N kept as dedup survivors"), so the
exclusion is never silent. `notRecordedSurvivorSql` is the survivor-side mirror
of `notMarkedDuplicateSql`, appended to BOTH the count and DELETE so the
reported count matches what is removed.

Docs: completes the ADR-0003 user-facing dedup-safety prose — (a) mark→delete
is non-escalating; (b) the prune survivor guard; (c) the semantic-pass
asymmetry (a sticky survivor acquires no new semantic duplicates after #79;
the exact pass is unaffected).

Tests: tests/commands/prune.test.ts pins survivor protection (status marked and
deleted), non-survivor pruning, reverted-carries-no-obligation, and the
never-silent summary; mutation-verified.

Closes #80
@edheltzel edheltzel merged commit f20fa16 into main Jun 18, 2026
2 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.

prune: deletes survivors with no dedup_lineage awareness — orphans marked duplicates

1 participant