fix: dedupe overlapping archive segments in the rotated corpus - #8488
fix: dedupe overlapping archive segments in the rotated corpus#8488buluoray wants to merge 1 commit into
Conversation
Rotation writes the archive segment before rewriting the live file, and that order is deliberate: until the archive lands, the live file is the only copy of those rows. A crash between the two steps therefore leaves the archived prefix in the live file as well, so the next rotation computes its own dropped set from a live file that still begins with those rows and archives them again as the following segment. read_rotated_messages concatenated segments with no cross-segment identity check, so those rows were served twice. That is not cosmetic: this corpus is the index space before/next_before cursors and the fork index path resolve a rendered row's position against, so one duplicated row shifts every index above it, silently -- a reader pages positions that no longer mean what they meant, and an index-addressed fork copies a different cutoff than the one on screen. drop_persisted_tail_prefix already owns this identity rule for the archive/live seam; apply it one level down at the segment boundary. Both producers persist a prefix, never an interior slice, so the duplication is always a segment's head against what the corpus already ends with.
Design Review (Fable 5) — ✅ PASSDesign-level review of Design-Verdict: PASS Right layer, right rule: the one function assembling the rotated corpus reuses the existing seam helper, and reader-side dedup also heals corpora already damaged on disk. The reader-side placement is the correct root-cause choice, not a symptom patch: a writer-side guard in [DESIGN-REVIEWED] 4056e2b |
Opus 4.8 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
First Principles Review (Fable 5) — ✅ PASSPremise-level review of All claims verified: the writer ordering in First-Principles-Verdict: PASS A reported crash-window defect fixed at the one shared reader with the existing helper — no new surface, no new identity rule, tests pin both directions. What this change shipsIntent: after a crash-interrupted rotation, serve each archived row once so pagination cursors and fork cutoffs land on the row on screen — a FIX.
No new config key, flag, or public symbol; the guard reuses WatchOne counted sibling of the root cause: grepped [FIRST-PRINCIPLES-REVIEWED] 4056e2b |
GPT 5.6 Review — 🔴 changes requested (blocking)GPT 5.6 found at least one blocking issue that must be resolved before merging This comment is updated in place on each push. BLOCKING -- src/kiro_crew/history_projection.py:471 -- Reused message IDs delete distinct archived rows
|
Closes #8484. Split out of #7916 under a human override: the finding was real, but low-probability, so it did not hold that PR.
The seam
read_rotated_messagesconcatenated archive segments with a barerows.append(row)and no cross-segment identity check.drop_persisted_tail_prefixguards the archive↔live seam — it cannot reach this one, because the duplicate is already insiderotatedbefore that comparison runs.How two archive segments come to overlap
HistoryRewriter._maybe_rotatewrites the archive before rewriting the live file, deliberately: until the archive lands, the live file is the only copy of those rows, so archiving is a precondition of the rewrite rather than a best-effort side effect.A hard crash between those two steps leaves
droppedin both places:dropped' = message_lines[:-keep_count], which starts with that same prefix, and archives it again as segment N+1;The header-damage branch a few lines above already named this hazard — "a retry rotation can archive the same rows successfully, and then a partial read of this segment duplicates them" — but nothing guarded it.
Why a duplicate is worse than showing a row twice
read_rotated_messagesis the pagination index space:before/next_beforecursors and the fork index path both resolve a rendered row's position against it. A duplicated row shifts every index above it, silently. A reader pages positions that no longer mean what they meant, and an index-addressed fork copies a different cutoff than the one on screen. Nothing raises.The fix
Accumulate each segment's rows, then apply
drop_persisted_tail_prefixagainst the corpus so far before extending. Same rule, same helper, one level down — no new identity logic. Both producers persist a prefix, never an interior slice, so the duplication is always a segment's head against what the corpus already ends with, which is exactly the shape that helper was written for.Test power — verified in both directions
test/test_archive_segment_overlap.py, 6 cases. Three pin the fix and three pin its limits, so the suite fails if the guard is missing or too greedy:rows.extend(seg_rows))wholly_duplicated→['1','2','1','2','3'],partially_duplicated→['1','2','2','3'],overlap_matched_by_stable_id(ts, role)onlydistinct_rows_sharing_ts_and_role_are_both_keptThe three upper-bound cases stay green under the first mutation by design — they assert the guard does not eat rows — which is why the second mutation is what proves they are not vacuous.
non_overlapping_segments_are_all_servedkeeps a healthy archive intact;an_interior_repeat_is_not_treated_as_overlappins that a row reappearing mid-segment is the reader's own repeated message, not a re-archived prefix.Cost
The helper's
all(...)short-circuits on the first mismatch, so the healthy no-overlap path costs one comparison per candidate offset, and the whole read is cached on the segment files' stat signature — it runs once per archive change, not once per page.Checks run locally
pyteston the new file plus the five adjacent archive/fork/pagination suites (51 passed),isort,flake8,mypy --platform linux, and the black gate. The full suite is left to CI.Why no screenshot: backend-only change to a transcript read projection. There is no user-visible surface in this diff — the observable effect is the absence of duplicated rows in a corpus that only appears after a crash-interrupted rotation, which is not reproducible in a screenshot.