Replies: 1 comment
|
Verified your mechanism against current upstream master (4e84901 = 0.1.2-alpha.4) — every claim holds, and this is the same root cause as the thread opened two days ago: #5255 — "Session logs corrupted by concurrent writers: append path trusts its in-memory cursor" (2026-08-31). Your three-session Windows evidence plus Tazio7's macOS rc.2 recurrence on #2817 are the strongest cross-platform confirmations of that root cause yet, so it's worth consolidating here rather than re-deriving. Source-level confirmation at alpha.4:
Fix direction (writer-side, one seam): at the live-append boundary, re-validate the durable tail before appending — cheapest is a first-line/tail-frame re-read (readers already do bounded tail recovery) or a durable-revision token re-check on every If you can share the raw tail around one overlap (zstd frame boundary + seqs), it would discriminate the two sub-cases (late drain after adopt vs. closer re-write) — useful for the regression fixture. |
Uh oh!
There was an error while loading. Please reload this page.
Summary
Across three real sessions on Windows (dsh
0.1.0-rc.6, zstd-compressed JSONL store), history loading started failing with:All three are the same defect: when a session is interrupted/closed and then resumed, the interrupted-close block that a prior lifecycle persisted (synthetic closers from
interruptedTurnClosers, plus the re-projectedsession/end-seed) is re-written from its starting seq by the resumed writer, whileappendLines()appends at byte-EOF without validating the file's actual tail seq. The seq-continuity contract (event.seq === state.cursor + iinappendCore) is enforced only against the in-memory cursor, so any handoff where the cursor and the durable file diverge produces a durable seq overlap. The strict loader (SessionLogScanner.consumeEventLine) then rejects the entire stored history ("history unavailable" in the web UI).Evidence (frame-level analysis of the real logs)
Large session (16k frames):
turn/end(interrupted)session/end-seedThe resumed batch passes the in-memory cursor check because its writer's cursor was 131087 — a state read before the close block was persisted — while the file already contained 131087..131090.
appendLinesthen appends at EOF (after the marker) and the committed region overlaps.Second session: stored
session/end-seedat seq 3; the resumed writer's first batch starts at seq 3 (agent/inbox/spliced) and continues 3..40421. Same shape, one event of overlap.Third session: identical signature —
session/end-seedat seq 354320 superseded by the resumedagent/inbox/splicedat seq 354320, continuing to 378636. Four occurrences across three days on three sessions, always at an interrupt/resume boundary.Root cause
SessionPersistenceCoordinator.appendCorevalidatesevent.seq === state.cursor + i— in-memory only.JsonlSessionPersistence.appendLinesopens the log with mode"a"and writes; there is no check that the durable tail actually continues at the batch's first seq.Suggested fix (validated locally)
Make the durable boundary seq-aware. In
appendLines, before opening the append handle, decode the log's tail from its bytes (frame granularity for zstd, line granularity for plain) and classify the batch's first seq:firstSeq === lastSeq + 1→ clean append, unchanged behavior (fast path: only the last frame is decoded; measured 3.6 ms on a 4.1 MB / 16k-frame log);firstSeq <= lastSeq→ the file tail is superseded by this writer's branch: truncate at the frame boundary ending atfirstSeq - 1(reusing the existingrepair()truncate+fsync), then append. The writer's in-memory log is the authority for content; this converges the file and makes the corruption self-healing;firstSeq > lastSeq + 1, or the boundary falls inside a packed storage row → refuse loudly (the file stays valid).We run this patch locally (against
0.1.0-rc.6): all three real corruption scenarios are healed automatically on the first post-resume append, clean-append behavior is byte-identical, torn tails are truncated as a side effect, and a 23-case integration suite passes (gap refusal, straddle refusal, idempotent rewrite convergence, zstd + plain layouts). Happy to share the patch or open a PR if contributions are accepted.Until then, affected logs can be repaired offline by byte-level frame surgery: drop the superseded frames between the pre-overlap boundary and the resumed continuation, revalidate strict seq contiguity, and atomically replace the file.
Environment
0.1.0-rc.6, profile web, Windows 11 (win32), Node 22.22.2session.jsonl.zstdper-session files under<home>/.dsh/sessions/<project-slug>/<session-id>/All reactions