Bug: session log corrupted on resume - last 4 committed events re-appended with fresh timestamps (seq gap in committed region) #2167
Replies: 2 comments
|
Outstanding report — the 4-log evidence, the exact 4-behind signature, and the manual-repair detail make this one of the cleanest corruption-family write-ups we've had. I verified the cited sites and want to add the family context and a refinement to your fix ordering. 1. Verified
2. Family context: this is the same-process sibling of the #1333 cross-process seq thesis On #1333/#1452 I established that seq = 3. Refinement to your fix ordering Your write-side invariant (1) is right, but make it cheap and strict: 4. One addition to your repair playbook Your manual repair is exactly right, and it's worth noting this is the 6th distinct corruption mechanism in the family now: #1333 (cross-process seq duplication) / #1452 (same) / #1497 (torn tail) / #1473 (append vs read race) / #1586 (recovery-writer vs legacy-writer collision) / #2167 (same-process stale-view re-append). The common thread across all six: the append path has no durable ownership/validation contract. A unified hardening — append watermark + durable seq assertion + writer lease (wellorbetter) — would close the whole family, and I've tracked exactly this as a PR-ready workstream (#1333/#1452/#1497/#1473/#1586, now + #2167). Thanks for the precise evidence — this makes the stale-view trigger a first-class member of the family rather than a one-off. |
|
I reproduced the corruption mechanism and implemented a candidate fix against Root causeThe concrete failure is below AgentLoop replay: The resume/HMR path can expose the stale coordinator, but scanning the durable event count immediately before append is not sufficient by itself: another writer can commit between that scan and the write. The comparison and mutation need one writer transaction. Fix
Additional findings fixed while exercising the race
Regression evidence
Implementation: commit |
Uh oh!
There was an error while loading. Please reload this page.
Summary
Resuming a persisted session can append a stale 4-event prefix to its JSONL session log: the last four committed events are re-appended verbatim (same
seq, same payload, freshtime), producing a seq gap in the committed region. The reader then hard-fails withcorrupt session log: seq gap in committed region, the session history becomes permanently unloadable in the Web GUI, and the log requires manual surgery to repair.Affected on 2026-08-15, dsh
0.1.0-rc.5, Windows, JSONL+zstd persistence backend (session.jsonl.zstd), 4 sessions with an identical signature.Environment
0.1.0-rc.5(running profile packages and checkout HEAD47f9438all0.1.0-rc.5)session-persistence-jsonlwith zstd compression (session.jsonl.zstd)Symptom
Opening the session in the Web GUI:
Evidence (4 real logs, all same signature)
session-9173e707-2020-4fba-96cd-55f056b74a9fF:\Git\dsh-pluginssession-2503d35c-ad47-4811-9ae6-f944c1dad356F:\CommonTaskssession-6f6ddc38-9705-4cfd-b132-506c5881b5dfF:\Git\dsh-pluginssession-88728413-fa95-4993-8f10-5158eb91a881F:\Git\dsh-pluginsEvery gap is exactly 4 events behind (
got == expected - 4), and the duplicated events are exactly the last 4 events of the committed prefix, with identical types/payloads but fresh timestamps (~2 minutes after the originals). The file resumes at the correct seq immediately after the duplicate prefix and is otherwise perfectly contiguous to EOF (e.g.9173e707continues 93137?132739 with zero further discontinuities, ending in a completedturn/end).Detailed trace for
session-9173e707(lines 4984-4992, 1 event per line)The re-emitted sequence is exactly what an agent loop resuming an interrupted step emits first: re-deliver the in-flight
tool/result, close the step, open the next step, stream the firstassistant/chunk- but its seq counter was 4 behind the durable log (as if the session view was missing the last 4 committed events).Root cause analysis
appendLines()insession-persistence-jsonl/src/index.tsencodes the batch and appends+fsyncs blindly; nothing checks that the batch's first eventseqcontinues the file's committed prefix.Session.appendonly validates against the in-memory log, so a stale in-memory/snapshot view writes duplicate seqs without any error.commitRepairwrites synthetic closers - not this shape).SessionLogScannertreats a seq gap inside the committed region as damage, and since aturn/endexists after the gap it throws (format.tsline ~368). Torn-tail truncation does not apply because the gap is not at EOF. Result: the session is permanently unloadable until the log is manually rewritten.Suggested fixes
appendLines(or the coordinator append path) must verify the batch's firstevent.seqequals the durable committed event count (obtained via the scanner/committed prefix) and refuse - or truncate-and-repair - instead of writing a duplicate prefix. This turns the corruption into a loud error at write time, or prevents it entirely.time), the scanner could drop the duplicates and keep the file readable - but prevention (1)/(2) is the real fix.Manual repair performed (for reference)
Repair script dropped exactly the 4 duplicate lines (frames containing them were re-encoded; all other zstd frames kept byte-identical) and re-verified full contiguity with the harness scanner semantics (seq 0..N contiguous, ends
turn/end, no torn frames). All 4 logs repaired; original corrupt bytes kept assession.jsonl.zstd.corrupt-bakbeside each log. No real conversation content was lost (duplicates only).All reactions