Replies: 3 comments 1 reply
|
你在开头关联了 #6010,但这条还有一个更近的同胞帖子没被串进来:#5952——同列车(0.1.2-rc.1)、同为「中断 → 合成 closer 落盘 → 真实内容按过期基线重号」:那帖是工具结果晚到时从活跃会话未推进的游标重复追加了 51695–51698,你这帖是恢复路径回卷到 91853 重放。两个独立案例合起来,等于把「两条写端共用/回滚同一 seq 基线」这个缺陷从两个方向都钉死了,给维护者的证据比单帖强得多。 补一点源码侧背景(当时为验证 #5952 对照过 rc.1 编译产物): 给后面照做「删合成行」的人一个安全清单(#5952 那次我们踩过的边界):
另外你说「无会话日志备份机制」——这正是我做 dsh 备份插件的出发点,doctor 能检出这类 seq 重号、宿主升级/迁移前也会自动快照。当然你提的自愈/截断才是根治,希望维护者按你建议的方向收。 |
|
TL;DR (EN): I traced this against public master ( 感谢这份报告——帧级证据、seq 区间、两次独立复发、以及「删除哪三行可以完整恢复」都写得很具体,我按它逐条在公开 master( 一、这不是「游标回卷」,是「两次写入」——而这不是抠字眼报告的系统模型是「内存中的事件游标回卷到 91852+1,然后从 91853 重新追加」。但 关键后果是:合成 closer 一旦进入内存中的
而你记录的两次事故都是这一支:
这个判别很重要,因为它排掉了一整类修复:「让重放保持 seq 单调递增、由读端理解替换语义」不能修这个 bug,反而要求它存在。在保持 closer 的前提下,重放只能从 91856 起(也正因如此它才会撞上 二、我复现了校验侧,逐位对齐
// packages/session/session-format-v1-to-v2/src/codec.ts:104
if (event.seq !== eventCount) { … `… has seq gap (expected ${eventCount}, got ${event.seq})` … }v3 codec 包住它( 把你们的行号代进去完全吻合:0..91852 扫完后 也就是说:损坏是两个不同事件占同一个 seq(并因此也占同一格 三、写侧:这个类在 master 上已经不可提交这是我要给的最实质的增量。master 上唯一的耐用 closer 追加点是 resume: // packages/core/agent-loop/src/index.ts:892-893
const closers = interruptedTurnClosers(persisted)
if (closers.length > 0) await handle.append(closers)而它下游的每一次耐用追加都要过: // packages/session/session-persistence-jsonl/src/storage.ts:319-323
private async persistContiguous(batch: readonly SessionEvent[]): Promise<void> {
…
assertContiguous(this.id, batch, this.state.cursor) // 先于任何字节落盘
还有一个确认信号没人在这个串里提过:你贴的 5147 个 zstd 帧全部可解压、7191 行 JSON 全部合法,而报告的报错是「committed region seq gap」而不是「torn record」。这本身就证明了崩溃假设不成立 —— torn tail 的报错文案是另一条(complete frame contains a torn JSONL record)。用「崩溃」解释这个串里的问题,方向是反的。 需要说明的一点边界(不削弱上面的结论,但会影响正确修复的验收): 四、三条修复建议:两条有害
五、插件可挂载面(已扫)结论:这个缺口在 core,插件是受害者,已归档 upstream-fix 候选。 理由不是「没有缝」,而是结构性的:
所以我没有把它做成插件;但顺手把它变成了一个可用的检测器:我已经发布过 你们报告里那条非常好的判据——「重放取代行的 seq vs 子会话 六、所以,今天真正要测的是什么在 master 上重跑你的两次事故,预期是写侧拒绝( 这次我们走到同一个方向上了——你那句「目前一个 bug 会让用户整个会话永久不可用且无自助恢复入口」是对的,而且比第 4 节的任一条都更值得先修。欢迎把帧级解码器 + 修复脚本贴出来,那个对别人做同类救援有直接用处。 |
|
核实结论:部分属实,且该缺陷对当前 master 已在写侧封堵。逐条说:
下一步:请你在最新版上按原复现重试;若仍复现,按 argszero 提示优先排查「第二个进程持锁打开」分支。修复建议 1(截断 + 重放合并为单步原子提交)方向有效;建议 3 的「优先保留后写行」在同形不同因场景会挑错行,慎用。 |
Uh oh!
There was an error while loading. Please reload this page.
TL;DR (EN): When a turn waiting on a background job is interrupted,
interruptedTurnClosers()synthetic closers are durably appended (seq continues fromlast.seq + 1). When the tool's real outcome later arrives and the interrupted step is resumed, the resume path re-appends events from a rewound seq (91853) without truncating the already-committed closers rows. Seqs 91853–91855 now appear twice with different content, andSessionLogScannerhard-fails the whole session at every subsequent load. The data is 100% intact; deleting the 3 superseded synthetic rows fully restores the session.环境 / Environment
@deepseek-ai/dsh0.1.2-rc.1(npm 安装,npx @deepseek-ai/dsh web)现象 / Symptom
重启后 DSH 无法加载该会话,报错:
该会话从此每次启动都无法恢复。日志文件本身无任何物理损坏(2.7 MB,5147 个 zstd 帧全部可解压,7191 行 JSON 全部合法)。
日志证据 / Evidence
解压日志后,报错行(0 基 event 行号 3597)附近的记录:
{"type":"assistant/message","seq":91851,"time":…,"data":{"turn":5,"step":10}} {"type":"tool/call","seq":91852,"time":…,"data":{"turn":5,"step":10, …}} // 等待后台 job 的工具调用 {"type":"tool/result","seq":91853,"time":1789194234652, … "interrupted-tool-result-…", isError:true, "text":"The tool call was interrupted after it was recorded, but no result was durably recorded. Its outcome is unknown. …"} // 合成 closer {"type":"step/end","seq":91854,"time":1789194234652, …} {"type":"turn/end","seq":91855,"time":1789194234652,"data":{"turn":5,"reason":{"kind":"interrupted"}}} // —— 2 分钟后 —— {"type":"agent/inbox/spliced","seq":91853,"time":1789194351514, …} // ← seq 回卷!真实结果返回 {"type":"tool/result","seq":91854,"time":1789194351951, … "settled":true,"waitedSeconds":117,…} // 真实结果 {"type":"step/end","seq":91855,"time":1789194351952, …} {"type":"agent/inbox/spliced","seq":91856,"time":1789194351952,"removedCount":1,…} {"type":"step/start","seq":91857,"time":1789194351982,"data":{"turn":5,"step":11}} // 之后一切正常,直至 seq 184148seq 91853–91855 出现了两次且内容不同;此后 8 个 turn 正常完成。
根因分析 / Root cause
合成 closers 被持久化:turn 5 step 10 的工具调用被中断后,
@deepseek-ai/dsh-session的interruptedTurnClosers()生成合成关闭事件(seq = last.seq + 1,即 91853–91855),经dsh-session-persistence的commitPrepared()→dsh-session-persistence-jsonl的commitRepair()/appendLines()追加落盘。日志尾部变为"已闭合"。恢复重放回卷 seq 但不截断:约 2 分钟后后台 job 完成,恢复路径把(内存中的)事件游标回卷到 91852+1,从 91853 开始重新追加真实事件(
agent/inbox/spliced+ 真实tool/result+step/end)。appendLines()是纯追加,对已提交区域没有任何截断路径(repair(truncateTo)只处理尾部撕裂帧)。全文件surfaceOp仅有append,无替换语义。加载校验拒绝:
SessionLogScanner.consumeEventLine()要求已提交区域每个事件seq === events.length(严格连续);违规后记 sticky issue,一旦后续行出现turn/end即硬抛错。因为回卷点之后还有 8 个已完成的 turn,该会话永远无法加载,且无内置恢复手段(也无会话日志备份机制)。已验证的恢复方法 / Verified workaround
删除被重放取代的 3 行合成 closer(seq 91853–91855 的 interrupted 占位 trio),重新编码日志。用
@deepseek-ai/dsh-session真实的decodeStorageRecord复刻 scanner 全量校验:通过,184,149 个事件(seq 0–184148)完整,最后一行为 turn 12 正常结束。数据零丢失(重放内容是合成 closer 的严格超集)。复发 / Recurrence
次日同一台机器上第二个会话(turn 2 step 58)以同机制损坏:中断 closers(seq 176064–176066)落盘 11 秒后,重放直接以真实
tool/result从 seq 176064 重新追加(本次无 splice 行,且重放后 turn 继续、step/start占用了原turn/end的 seq 176066)。两次事故发生在同一分钟内(06:24 前后),疑似同一次用户中断/崩溃触发。删除同样形态的 3 行合成 closer 后完整恢复(239,160 个事件无损)。全盘扫描另外 66 个会话日志无此类损坏。修复建议 / Suggested fixes
repair(truncateTo),但作用于已提交尾部);或Happy to share the frame-level decoder + repair script used to diagnose and fix this.
All reactions