Replies: 5 comments
|
Verified against rc.7 ( 1. The exact divergence confirmed at source.
So within one fresh process the seqs stay in-memory contiguous (end-seed at N, next event at N+1). The divergence appears when the persisted file already contains a tail beyond the seed the constructor was given — exactly your shape: the previous session's interrupted-turn repair left 2. This is the same family shape as #2839, one level up. #2839 (resume seed excludes just-committed placeholders, via stale prepared-source cache) and #2167 (resume-path fresh-load gap) both live on the "resume seed ≠ persisted tail" axis. Yours is the fresh-load, single-instance instance of that axis — the cleanest demonstration yet that the seed boundary itself is the bug, not the cache. The coordinator's 3. Fix directions, in order of leverage:
4. On your "GUI tolerates, persistence rejects" observation — that asymmetry is itself worth a note: the client assembler's tolerance is what makes the session look alive while exports/history fail. It's the same live-vs-durable divergence the family keeps hitting (replay-state coherence #1627/#1703/#2143); the strict scanner is the only integrity backstop, so fixing (c) must preserve strictness within each segment rather than weakening the global check. 12 reports, 10 mechanisms now — and this one comes with decoded evidence and a deterministic repro, which makes it a one-PR-sized fix ((a)+(b) together, or (a)+(c) if you want the healing path). Good find. |
|
Thanks for the confirmation and the fix directions! I implemented (a) + (c) and verified them — the code is ready for the team to pick up: Branch: Fix (a) —
Fix (c) — scan heals a seq break immediately after
Verification: 1205 tests pass across On (b) (resume seed = full persisted tail): the coordinator's A |
|
The patch bundle is here (applies cleanly on master @ rc.7): https://gist.github.com/Max-Null/16257bc912307dc15fb06e0d149305a4 |
|
我拉取了 gist 原文件(275KB,UTF-16)逐文件核对:
所以 这个核验对维护者很关键:如果他们直接应用这个 bundle,只会把 222 个 package.json 版本号"打回 rc.7"而没有任何修复。 |
|
Thanks for the careful verification — you are right on all counts. The patch bundle has been regenerated. What went wrong: the earlier bundle was exported with a wrong commit range — it captured the Regenerated patch (same gist, file replaced):
gist (updated): https://gist.github.com/Max-Null/16257bc912307dc15fb06e0d149305a4 Happy to adjust the (a)/(c) shape or move to (a)+(b) if the maintainers prefer a different combination. |
Uh oh!
There was an error while loading. Please reload this page.
Summary
A session whose log contains a
session/end-seedboundary followed by a resume (retry after an interrupted turn) becomes permanently unreadable: the durable log written by DSH itself fails DSH's ownscanLogvalidation with "corrupt session log: seq gap in committed region".Environment
dsh websession-persistence-jsonl(zstd)Symptom
Loading session history (web GUI history load / Session log export) fails:
The live GUI still displays the same session normally: the client conversation assembler tolerates non-contiguous seqs; only the persistence-layer strict scan rejects them.
Log shape at the gap
Two independent sessions show the identical pattern (events decoded with
decodeStorageRecord):The second session's gap is identical (expected 78435, got 78432).
This reproduced with a single DSH instance (no second instance running when the gap was written), so it is not a multi-process race.
Root cause
Sessionassignsseq = this.log.length(packages/core/session/src/index.ts:629). On construction with a seed (replay / resume / fork),firstLiveSeq = this.log.length(index.ts:539). When the seed's last event is not alreadysession/end-seed, the constructor appends one (index.ts:545). After that, new events restart fromfirstLiveSeq(the seed length) instead of continuing after the end-seed marker.The persistence scanner, meanwhile, requires strictly contiguous seq from 0:
event.seq !== this.events.lengththrows "seq gap in committed region" (packages/session/session-persistence-jsonl/src/format.ts:364-371).So any session that (1) has an interrupted/empty step (e.g. a model request failing or aborted right after
step/start), (2) is then resumed (same-process retry or a later process loading it), and (3) whose resume appends events with seq restarted at the seed length, is written by DSH in a form DSH itself refuses to read later. The write side and the read side disagree about seq semantics across anend-seedboundary.Workaround (no data loss)
Creating a fork of the session while the owning instance still holds it in memory yields a clean copy: fork seeds come from the in-memory event stream (contiguous), so the child session loads fine and the conversation continues with full context. This is the recommended recovery path for affected users. (Rewriting the log to shift seqs after the gap also works but is riskier.)
Suggested fix
Decide the intended semantics of seq across
session/end-seed:firstLiveSeq(current write behavior) and the scanner treatsend-seedas a seq-reset boundary instead of requiring global contiguity.end-seed.seq + 1(keep global contiguity); the constructor should not append the marker atfirstLiveSeqwhen a later resume would restart below it.Either way, writer and strict scanner must agree, and a regression test covering "interrupted turn -> end-seed -> resume -> reload" is needed.
All reactions