Repository navigation
v3→v4 session migration in 0.2.1-rc.2 fails to restore 0.1.5-rc.3 historical sessions (history invisible after upgrade) #10050
Replies: 4 comments
Additional root cause (distinct from the source-encoding bug above): single-frame v3 artifacts are silently skipped by
|
从插件作者角度看 0.1.5 → 0.2.1:四处静默破坏,以及一份迁移指南的请求我在这个版本上维护了四个 1.
|
| 行为 | 后果 |
|---|---|
list() 不传 caller 只返回无主任务 |
插件看到 0 个任务,而任务明明在跑 |
read() 的消费游标按任务而非按调用者 |
第二个读取者(如状态监视器)会偷走 agent 的 job_output 输出 |
第二条尤其危险:它让"观察"变成"破坏"。0.2.x 新增的 readAt(id, from)(绝对偏移、不消费)是正确的解,但这一点没有任何地方说明,我是先踩坑、再读类型才发现的。
4. streamOpenTimeoutMs=3000
已在 #3413 报告,这里只作提醒:它是"标志性报错"的来源之一,同样不适合静默。
请求:一份 PLUGIN-MIGRATION.md
不需要很长的文档,但需要可操作。我建议的骨架(每项一行即可):
变更内容 | 旧写法 | 新写法 | 是否静默失败 | 最小示例
具体到本次至少应包含:
- 消息
source形状(kind: "plugin"→kind: "plugin:<pkg>")—— 附producerKind()的规则表; - 会话存储:v4 要求多帧 zstd,header 单独一帧;读取 API 与写入 API 的差异;
ctx.jobs:list/read/readAt的可见性规则,以及"消费 vs 不消费"的语义;ctx.schedule:它已经完整接管了提醒的创建与投递(这是好事!),但旧插件自建 store/engine 的迁移路径没有写;ctx.skills.register、ctx.tools等接口的类型变更。
一句总结
这次升级的设计方向是对的 —— ctx.schedule 接管投递后,我把整个自建引擎(约 45KB:store、scheduler、fire、PowerShell 任务镜像)删掉了,代码从 680 行降到 500 行,行为更可靠。
问题不在改动,而在改动的"发现成本":四处破坏全部是运行时静默的,只能靠读源码或踩坑发现。一份迁移文档能把成本从"数小时"降到"数分钟"。
以上每条都有本机实测证据(会话日志、失败记录、复现脚本);如果需要最小复现,我可以逐条补充。
|
Your second comment is the valuable half, and it holds up line for line at 1. The list-time skip is real — and the open path fails too, with a different errorThe silent drop is exactly as you traced it:
What does not hold is that the open path reads these files correctly. The decode path asserts the same invariant on the same first frame: The two paths differ in error typing, not in acceptance:
So your offline rewrite was not a convenience around a list-only blind spot — with this reader those 87 artifacts are unreadable by both paths. Worth checking before you conclude otherwise: run one of the rewritten originals through the persistence backend's open path (not the format catalog) and see which of the two messages you get. 2. The fix is in the assertion, not in the list fast-pathPatching only
Both pieces already exist: 3. Root cause 1 reads backwards: the wrapper is v3 input, not the broken form
The converter is built for exactly that wrapper. Put together: stored v3 history needs no action from anyone; the migration does the lifting. What a third-party plugin must change is only what it writes at runtime on a v4 session. And the admission rule is narrower than "use Which leaves a question about your 129 sources: if those files really declare v3 in their header and carry producer kinds with no On the four-part migration guideThe shape you propose is the right one, and the mapping table above is the content of item 1 — but it has two audiences and they need opposite sentences. For stored history: nothing to do, converted automatically. For a plugin writing new events: supply a stable non- |
|
Thanks — A and B are both right, and I re-verified A against the shipped bundle before replying. A — confirmed. B — agreed. The invariant to relax is in the assertion ("frame 0 begins with the header line" for pre-v4), gated on C — the on-disk artifacts say otherwise. I ran the shipped catalog migration against an original (pre-my-edit) v3 file before replying. It fails:
The throw is if (source["kind"] !== "plugin" || typeof source["plugin"] !== "string" || source["plugin"].length === 0)
throw new SessionFormatError("system message requires plugin source");So the v3 decoder rejects producer form before the v3→v4 This is not a stray file. Across all 90 original v3 artifacts the plugin sources are uniformly producer form — 1,827 occurrences, 11 distinct plugins, zero wrapper-form:
None of them carries a On your provenance question — it's a fair probe, and I did check. Header Either way, "stored v3 history needs no action" doesn't survive contact with the actual artifacts: as-is they throw during migration, and it is specifically the wrapper normalization that unblocks them. |
Uh oh!
There was an error while loading. Please reload this page.
Impact
After upgrading from
0.1.5-rc.3to0.2.1-rc.2(SESSION_FORMAT_VERSION3 → 4), opening any historical v3 session fails during the on-demand v3→v4 migration. The session list still renders (it reads only file headers), but opening a conversation shows no history — the migration throws and the session is treated as unreadable. The underlying files are intact on disk (94 v3 + 25 v0 + 7 v4, all header-readable); this is a migration failure, not data loss.Root cause 1 — legacy plugin source encoding (systematic: 90/94 v3 sessions)
v3 sessions written by 0.1.5-rc.3 encode plugin message sources as:
{"kind":"plugin:@deepseek-ai/dsh-system-prompt"}i.e. the plugin name is merged into
kindwith aplugin:prefix and there is nopluginfield. The v3 codec in 0.2.1-rc.2 hard-requires the canonical form:{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"}assertSysteminpackages/.../dsh-session-format-v2-to-v3/lib/index.js(~line 263) throwsSessionFormatError("system message requires plugin source")becausesource.kind !== "plugin". This is hit during v3 decode inside the v3→v4 migration, so every v3 session containing a system prompt fails to open.Scan of all 94 v3 files on disk: 90 contain at least one
system/message, and all 129 system sources use the broken{"kind":"plugin:@deepseek-ai/dsh-system-prompt"}shape (0 canonical). In total, 1827 message sources across 11 plugin names use theplugin:<name>form:The
plugin:<name>string also matches the v4 producer-kind fallback indsh-session-format-v3-to-v4(producerKindreturnsplugin:${name}), which suggests 0.1.5-rc.3 was already emitting producer-kind-shaped sources into v3 files.Root cause 2 — v3→v4 migration is stricter than the v3 reader on turn/step lifecycle (3/94 v3 sessions)
Three sessions are readable at the v3 level (verified with
historicalSessionFormatCatalog.createRestore(...).finish(), which succeeds) but are rejected by the v3→v4 migration:turn/endevent for turn 1 (turn 2'sturn/startfollows turn 1's laststep/endwith no interveningturn/end). Migration error:turn/start does not open the expected turn.tool/resultevents (surfaceOp≠append) referencing an already-closed turn, placed after a laterturn/end. Migration error:tool/result is outside an open turn.The lifecycle checks live in
dsh-session-format-v3-to-v4/lib/index.js(the relationship validator, ~line 570requireTurnand ~line 747 forturn/start).Workaround I applied
A one-time script that normalizes
"kind":"plugin:<name>"→{"kind":"plugin","plugin":"<name>"}in the v3 files (backing up the originals first) restores 91/94 sessions. The remaining 3 need the lifecycle-strictness issue addressed (or a targeted per-file repair I chose not to attempt, since it requires dense-seq renumbering with back-reference updates).Suggested fix
{"kind":"plugin:<name>"}source form (treat it as{"kind":"plugin","plugin":"<name>"}, then map throughproducerKindas usual). This recovers the 90 sessions.turn/endwhen aturn/startopens a new turn, or accept retroactive replacement tool/results the way the v3 reader does.Environment: Windows 11,
@deepseek-ai/dsh-session0.2.1-rc.2 (launcherdsh-web-launcher).All reactions