You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
TL;DR
中断(interrupt)后恢复会话时,rc.2 的构建先追加了合成闭包
step/end+turn/end{interrupted}和session/end-seed,紧接着的续跑事件却按"闭包之前"的 log 长度编号,于是 seq 整段复用(复用宽度 = 闭包条数),日志从此不可加载。和 #10429 描述的形状不同:那里是陈旧的活写者交回一条真
tool/result;这里续跑出来的是同一 turn/step 的全新一段运行(assistant/message → tool/call → tool/result → step/end → step/start → …),而且三次落盘跨越 16.5 秒、同进程内三种长度视图。所以它不是跨写者租约能覆盖的竞态,而是恢复路径自己的 seq 基准算错了。三个坏档都在盘上留了可判读的指纹,我按指纹修好并验证了(见末节)。
盘上指纹
设中断时刻的已提交前缀为
persisted(条数n),闭包条数c = 2(step/end+turn/end,无待回填的 tool call),则三个实例都满足:persisted末条step/end→ seqnturn/end {interrupted}→ seqn+1session/end-seed {}→ seqn+2(分离落盘,落后约 6~9 秒)n(= 第一个闭包的 seq),而不是n+3实例(均为
~/.dsh/sessions/…/session.v4.jsonl.zstd,行号含 header):session-18b40e1e…session-f6e85f0c…session-27cc3b7a…18b40e1e的原始片段(相对时间):注意 step 20 在中断时是刚
step/start就被打断(还没有assistant/message),续跑从这一步的模型调用重新开始——这说明续跑继承的是persisted这个快照,而不是persisted + closers那个快照。Current behavior
打开历史时报:
packages/session/session-persistence-jsonl/src/format.ts:510+packages/session/session-format-v1-to-v2/src/codec.ts:106。此后该会话永久不可加载,且会从会话列表里消失,普通用户没有任何修复入口。Expected behavior
恢复流程写下的闭包与它随后续写的实时事件,必须共用同一个 seq 基准;即续跑的第一条事件的 seq 必须是
persisted.length + closers.length + 1(上面例子里应为 1507,而不是 1504)。环境
0.2.1-rc.2(常驻进程 PID 49648,今天 08:05 才重启到 rc.5;这三个会话的最后写入时间分别是 10-30 17:33、10-31 01:46、10-31 23:58,全部早于升级,所以确系 rc.2 产出)session.v4.jsonl.zstd(多帧 zstd)根因指向
packages/core/agent-loop/src/index.ts的 resume 路径:session/end-seed是packages/core/session/src/index.ts:696-697在构造时补的,所以它拿到了n+2(正确)。但从:900之后进入 live 的会话,第一条事件的 seq 退到了n。可疑点是用"条数"做游标:
:899的storedCount取的是persisted.length + closers.length,而handle的真实长度已经被end-seed推进到n+3;如果 live 会话的事件数组是按persisted播种、appendUnstoredSuffix又按索引切后缀,两套编号就会在这里错开一格,再被后续 append 放大成整段复用。我倾向于认为 live 会话(用于续跑的那个)持有的日志数组里没有闭包,于是seq = log.length从n起算。与既有报告的差别
SessionWriteLease。本例的新信号是:没有第二个写者,续跑是同一进程继续跑同一 step;如果 rc.2 已带
SessionWriteLease,那它对这条路径无效——问题出在恢复流程内部的 seq 基准,而不是互斥。已验证的修复方法(3/3 生效)
判据:找到"
step/end→turn/end{interrupted}→session/end-seed"三连,且其后第一条事件的 seq 等于第一个闭包的 seq(差值为c),则这三条闭包是多余的,续跑才是真实经过。步骤(对 18b40e1e、f6e85f0c 两条直接适用,无需任何语义改动):
seq = 0..n-1;scanLog()严格校验。结果:
18b40e1e:删 3 条 → 1897 事件,scanLogOK,末事件turn/endseq 1896 turn 32;f6e85f0c:删 3 条 → 2060 事件,scanLogOK,末事件turn/endseq 2059 turn 18;27cc3b7a:形态更大(交错两条链),保留用户实际经历的 turn 2..5 主线,丢弃重跑的 34+4 条,577 事件,scanLogOK。原文件全部保留了
.pre-repair/.bak-corrupt备份。建议加一道写前不变量
append落盘前断言event.seq === handle 当前长度,不匹配就拒绝写入并立刻重读日志(而不是只在读取侧靠scanLog事后发现)。读取侧的校验只能保证"坏档不被静默吞掉",挡不住把好档写成坏档——这类损坏一旦提交,用户侧没有任何自愈路径。如果需要,我可以把这套判据 + 修复脚本整理成一个最小复现器(构造
persisted + closers + 续跑的单进程时序),方便你们加回归测试。All reactions