[BUG] v2→v3 会话迁移对插件自定义的 message source kind 直接拒载,导致旧会话永久无法加载 #6311
Replies: 1 comment 2 replies
源级复核:你的核心判断成立,而且比"缺功能"更强 —— v2→v3 是唯一违反仓库自己已写明策略的那条边
1. 你引用的三处,逐字复核为真
2. 决定性事实(报告里没有):这不是"缺一个 kind",而是策略不一致
类型上刻意只声明已知臂、把未知留给插件 —— 所以"插件会写入自定义 kind"不是意外用法,而是设计意图。 更关键的是:仓库已经写明了历史迁移遇到未知 source kind 该怎么办,而且另一条边就是照做的。
⇒ 结论:v0→v1 与 v2→v3 这两条历史边对同一个判别式采取相反策略,而写在 note 与 dispositions 文档里的那条是"保留未知为 owner-opaque"。 3. 而这条白名单是可证明地过宽:它的唯一工作对象只有一个 kindv2→v3 的全部 source 改写就是 function renameMessageSource(message) {
const source = message['source']
if (source['kind'] !== 'plugin' || source['plugin'] !== 'tools-code-mode') return message
return { ...message, source: { ...source, plugin: 'tools-ptc' } }
}调用点只有 ⇒ 未知 kind 永远不可能是改写目标(它不是 顺带一个旁证:同一份 4. 所以你的建议 ① 与 ② 要分开看
5. 落地障碍(你没提,且它决定了这事的性质)
it.each([... , event('user/message', { ...user(), source: { kind: 'future', seq: 0 } }, 'append')])(
'rejects unaudited payloads %j', (bad) => {
expect(() => migrate([...opening(), bad])).toThrow(/unclassified|unexpected/)
})那个向量 6. 一个立刻能拿到的、无争议的改进:诊断信息你说"报错既没有指出出问题的 kind,也没有指出事件类型或 seq,只能手工解 zstd 数直方图"——这条完全成立,而且同一个文件的兄弟分支已经把该做的做了:内容 kind 的报错( throw new SessionFormatUnsupportedMigrationError(label + ': cannot safely transform unclassified message content kind ' + JSON.stringify(kind))带 label、带 kind。而 source 这条是裸字符串( 7. 插件可挂载面判定(我这边的配额)缺口在 core 面,插件是受害者,修不了。 依据:白名单与 8. 边界
EN TL;DR — Your diagnosis holds, and it is stronger than a missing feature: v2→v3 is the only historical edge that violates the repository's own documented policy. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
环境
dsh0.1.5-rc.1(web profile)@deepseek-ai/dsh-session-format/-v2-to-v3/-catalog/dsh-session-persistence-jsonl— 0.1.5-rc.2@wingsky-1/dsh-mcp-manager@0.1.14摘要
由旧版本写入、durable 产物仍停留在 format v0 的会话,在升级后变得永久无法加载。
GUI 报错如下:
由于没有生成
session.v3.jsonl.zstd,此后每一次加载尝试都会以同样方式失败。根因
@deepseek-ai/dsh-session-format-v2-to-v3/lib/index.js的assertSource()会用一份封闭的硬编码白名单(
SOURCE_KINDS,共 15 项,第 14–30 行)校验每一条surface 消息的
source.kind。对user/message而言,它由assertEvent()第 98 行进入:
受影响的产物里有 133 条
user/message事件的source.kind === "mcp-catalog"—— 由第三方 MCP 管理器插件写入(即其能力目录 /<available_mcp_servers>注入,announceCatalog默认为 true)。mcp-catalog不在SOURCE_KINDS中,且在任何@deepseek-ai/*包里都不存在。为什么这更像是核心内部的不一致,而不只是插件的问题
原生 v3 读取路径根本不做这项校验。
assertV3Event()(第 300–340 行)从不校验
message.source;按其自身注释(第 295–296 行)所述,"Unclassified metadata is deferred to vocabulary-aware restoration"。
于是同一个 kind 在原生 v3 会话里被接受,在迁移时却被拒:
session-917465aa-…—— 仅有原生 v3 产物,含 38 条mcp-catalogsource → 加载正常。session-f4e42048-…—— v0 产物,含 133 条mcp-catalogsource → 迁移中止。也就是说:迁移路径比它所迁移到的读取器更严格。任何当前可用的插件自定义
source kind,都会让升级前的会话变成不可恢复。
复现步骤
source.kind的user/message的插件(本例为@wingsky-1/dsh-mcp-manager)。session.jsonl.zstd旁边不会出现session.v3.jsonl.zstd。而不含该自定义 kind 的同级会话迁移正常(已实测:同一项目目录下 4 个会话中有 3 个正常生成了
session.v3.jsonl.zstd)。预期
含有插件自定义 source kind 的旧会话应当完成迁移(目标格式本身容忍它们),
或至少给出可据以定位的报错信息。
实际
整个会话无法加载,且报错既没有指出出问题的 kind,也没有指出事件类型或 seq。
为了定位,只能把多帧 zstd 原始日志解压出来,手工统计 source kind 直方图。
建议
① 【主】让 v2→v3 与 v0→v1 对齐,按仓库已写明的策略把未知 source kind 作为
owner-opaque JSON 透传。
这不是新增功能,而是消除一条孤立偏离。仓库内已有明文策略:
.agents/notes/implemented/architecture/2026-08-31-alpha-historical-unknown-event-refusal.md的 Decision:"unknown content-block types, message-source kinds, assistant
finish-reason kinds, and turn-ending reason kinds are preserved as owner-opaque
JSON, while known arms receive structural validation."
packages/session/session-format-v0-to-v1/src/dispositions.ts:28-35—— "Nested merge-extensible discriminants validate known variants and preserve
unknown variants as owner-opaque JSON."
payload-validation.ts:548-629对source['kind']做switch,default:分支只要求 kind 是非空字符串即放行(:626-628)。(我在已安装的
0.1.5-rc.2bundle 上复核过同一逻辑:dsh-session-format-v0-to-v1/lib/index.js:914-916。)packages/llm/llm/src/message.ts:98-107把MessageSourceMap明确标注为 "Merge-extensible sum type — plugins add their ownkinds." —— 即"插件会写入自定义 kind"是设计意图,不是意外用法。由此:v2→v3 是历史迁移链上唯一违反该策略的一条边;
SOURCE_KINDS白名单不是"待补全的功能",而是与文档相悖的额外收紧。且该白名单可证明过宽:
v2→v3 中唯一的 source 改写是
renameMessageSource(
packages/session/session-format-v2-to-v3/src/migration.ts:169-172,plugin+tools-code-mode→tools-ptc),未知 kind 永远不可能是改写目标,因此没有任何一处需要"看懂"一个未知的 source kind。
落地障碍(决定了这是策略变更而非补丁):
packages/session/session-format-v2-to-v3/tests/migration.spec.ts:211用向量{ kind: 'future', seq: 0 }把"拒收未知 source kind"钉死为期望行为。按本项改动会让该断言变红,需同步更新该测试,并明确"未知 source 作为
owner-opaque JSON 透传"的边界(例如仍须保证 lossless JSON)。
文档说保留、测试说拒收,两者必须先对齐。
② 【独立、可立即落地,零策略变更】把诊断信息补齐到与兄弟分支同等。
同一个文件里,内容 kind 的报错(
payload.ts:171)带 label 也带 kind:而 source 这条(
payload.ts:113)是裸字符串,既无 kind,也无事件类型、无 seq。建议补上
source.kind+ 事件类型 +seq。这一项不触碰白名单策略,可以先独立推进;也能让后来者不必像我这样解压多帧 zstd 日志手工统计直方图。
③ 【配套】提供受支持的修复入口(例如
dsh session repair <id>),让用户无需手工编辑 durable 日志即可救回搁浅的会话。
关于已撤回的「登记 kind / 注册扩展点」方案:其一,逐个登记是打地鼠 ——
下个插件的 kind 照样撞墙,正是本报告"任何当前可用的插件自定义 kind 都会让
升级前会话不可恢复"的情形;其二,"注册"这个形状在本仓库已有明确否决先例 ——
packages/core/session/src/known-event-types.ts:15-20拒绝事件名注册,理由是"event-name registration was rejected because it does not classify omission safety
and would make reads composition-dependent",同一理由亦见于上述 note 的
Alternatives。给 source kind 加表等于把被拒的形状搬到判别式上。
变通方案(供其他受影响的用户参考)
在一个用完即弃的内存模块实例里,把
mcp-catalog加进SOURCE_KINDS,然后跑真正的迁移链;编码 v3 产物时严格照抄
JsonlSessionPersistence.encodeMaterialization的方式(header 单独一帧 +事件一整帧,
ZSTD_c_checksumFlag);最后用未打补丁的官方 catalog 回读校验(
readHeader→status: "current",并用Session.fromRestore做全量回放)。v0 原件保持字节级不变。
附:
在翻issue时找到了类似的帖子#6101,但是只贴了报错信息没写其他环境和原因,所以还是决定新发一个帖子,不过能确定上面的帖子也是同样的报错原因
All reactions