Replies: 2 comments
|
Independent reproduction on DSH 0.1.1-rc.2 (macOS, web via launchd + Desktop shell sharing ~/.dsh/sessions) — same corruption signature, second confirmation of your root-cause analysis: Damage fingerprint (both of our affected sessions, 100% identical)
User-visible impact noteThe loader's refusal is technically correct but permanent: once a session log has one duplicate seq, the session can never be opened again via the UI ("history read failed … seq gap in committed region"), which users perceive as silently losing a conversation after any restart. We did a survey-style check: with two instances (web + shell) both enabled, the corruption recurs whenever a restart happens while the other instance holds a recently-used session. Recovery (we recovered both sessions with zero event loss — may be worth including in a future test fixture)Using the same published package's own codecs (no foreign rewrite of the zstd frames):
Original corrupted files kept as SHA-256-pinned backups before the fix. Suggestion for the maintainers: since a duplicate (or short) seq inside the committed region is exactly the deterministic artifact of this concurrency bug, the loader could detect the "exactly-one-duplicate-seq here + rest contiguous" pattern during load and either self-heal (with a warning) or at least show a clear "this session was affected by the concurrent-writer bug; run " message instead of a generic permanent refusal. Supporting the upstream fix: the Reference: local recovery record + this discussion's dual-language note. Happy to provide the full recovery script or more log fingerprints on request. |
|
Excellent analysis, and the concurrent-writer asymmetry is real. I independently re-read the alpha.2 source ( Append path — no concurrent-writer guard ( The let state = this.states.get(id)
if (state === undefined) state = await this.adopt(id)
for (const [i, event] of events.entries()) {
if (event.seq !== state.cursor + i) {
throw new Error(`append seq mismatch for "${id}": ...`)
}
}
await this.backend.appendBatch(state.meta, events, state.materialized)
Prepare path — guarded ( By contrast, the resume path verifies the durable log before committing: if (!await this.isPreparedSourceCurrent(source)) return undefined
// ...
async isPreparedSourceCurrent(source, signal?) {
return await this.backend.readStoredRevision(source.inspection.meta.id, signal) === source.revision
}So the asymmetry is structural and exactly where you place it: the load side has a revision guard, the append side does not. Two processes sharing a sessions root each advance their own cursor; a second process's committed batch is invisible to the first until the next On the fix direction — revalidating This plugs into a known family (#1333/#1452/#1497/#1473/#1586/#2167/#2342/#2839/#2900/#3198/#3450/#4274/#4811/#5059/#5103/#5160) where several "corrupt session log" reports trace back to cross-process or replay-ordering writers. A durable-revision guard on append is the missing invariant those reports point at. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Symptom
Two of my sessions became permanently unloadable after two live lifecycles wrote the same session log. The loader correctly refused them:
One log had a duplicate
session/end-seed/agent/inbox/splicedpair at seq 3; the other had a closing batch two seqs behind the committed prefix. The data was already destroyed at write time — the load refusal was correct, but late.Root cause
PersistenceCoordinator.appendCorevalidates a batch's seqs against its in-memory cursor only, and the backendappendBatchwrites without comparing anything to what it last observed. A second process sharing the same sessions root (its own coordinator state) can commit events the first process never sees; the first process's next batch then writes seqs that overlap the committed region.The prepare/load path already handles concurrent writers —
commitPreparedverifiesisPreparedSourceCurrentand retries. The append path had no equivalent guard; that asymmetry is what made the corruption possible.Fix (branch on my fork)
appendCorenow revalidates the durable revision before trusting the cursor:SessionStaterecords the durable revision (state.revision) its cursor was established against — set at every state-establishment site (commitPrepared, HMRadoptLivePrefix, and after every successful append).appendCorere-readsreadStoredRevision; when a different writer changed the log, it reconciles the cursor from a freshloadStoredand lets the existing contiguity check decide:session "<id>" durable log changed under this process (stored N events, this batch starts at seq M); reload the session before appending;Residual (documented in the note): the check-then-act window between the revision read and the file write still exists; closing it needs per-log locking the format deliberately avoids. The window is now bounded per batch instead of always-open.
Branch:
fix/session-persistence-append-revision-guard·Compare against master
Rebased onto
origin/master(dd6322d604). Verified locally: fullbuild:lib:host(types),session-persistencesuite 403/403,session-persistence-jsonlsuite 248/248 (new revision-guard cases in the shared coordinator contract),tool-cordis11/11,doc-sync32/32. The commit carries a full bilingual Agent Note.Workaround until fixed
Avoid two processes sharing one sessions root with live sessions — the corruption needs a second writer committing into a log this process still has open.
中文版
症状
两个活动生命周期写同一份会话日志后,我的两个会话永久无法加载。加载器正确地拒绝了它们:
其中一份日志在 seq 3 处出现重复的
session/end-seed/agent/inbox/spliced;另一份的关闭批次比已提交前缀落后两个 seq。数据在写入时就已经被毁——加载器的拒绝是对的,但为时已晚。根因
PersistenceCoordinator.appendCore只用内存游标校验批次 seq,后端appendBatch写盘时也不与它上次观察到的状态做任何比对。第二个进程共享同一会话根目录(持有自己的协调器状态)时,可以提交本进程从未见过的事件;本进程的下一个批次随后写入与已提交区域重叠的 seq。prepare/load 路径已经处理了并发写入者——
commitPrepared校验isPreparedSourceCurrent并重试。append 路径没有等价防护;正是这个不对称让损坏成为可能。修复(我 fork 上的分支)
appendCore现在在信任游标之前重验持久化修订号:SessionState记录其游标建立时所对应的持久化修订(state.revision)——在每个状态建立点设置(commitPrepared、HMRadoptLivePrefix、以及每次成功 append 之后)。appendCore重读readStoredRevision;发现别的写入者改过日志时,先从新的loadStored对齐游标,再交给既有的连续性检查裁决:session "<id>" durable log changed under this process (stored N events, this batch starts at seq M); reload the session before appending;残留问题(note 里有记录):修订读取与文件写入之间的检查-再行动窗口仍然存在;彻底关闭它需要格式刻意回避的 per-log 锁。该窗口从"永远敞开"收窄为"每批次有界"。
分支:
fix/session-persistence-append-revision-guard·与 master 对比
已 rebase 到
origin/master(dd6322d604)。本地验证:完整build:lib:host(类型)、session-persistence套件 403/403、session-persistence-jsonl套件 248/248(共享协调器契约里新增了修订守卫用例)、tool-cordis11/11、doc-sync32/32。提交带有完整的双语 Agent Note。修复之前的规避方法
避免两个进程带着活动会话共享同一会话根目录——损坏需要第二个写入者提交进本进程仍打开着的日志。
相关报告
#5243 报告了同类损坏症状(
seq gap in committed region),但根因不同(中断轮次恢复时回卷 seq);两者都会让加载器永久拒绝该日志。All reactions