V3 accepts {kind:'plugin'} but V4 refuses it — plugin-authored injected messages have no literal that satisfies both #7556
Replies: 3 comments
|
Correction to the report above — its central claim is wrong, and the correction is the useful part. What I got wrongI said "no literal satisfies both generations". That was read off the V3 source kind whitelist without checking which code path reads it. It does not govern native V3 traffic at all: export function assertEvent(event, version: 2 | 3): void {
if (version === 3) {
assertV3Event(event) // structural validation only
return // ← returns before the source-kind checks below
}
...
if (event.type === 'user/message') assertSource(data) // reached for version 2
if (event.type === 'assistant/message' || event.type === 'tool/result') assertSource(...)
if (event.type === 'agent/inbox/spliced' || …) for (const m of messages) assertSource(m)
}So Measured, not readInstead of reading the validator again, I asked it.
Corrected conclusion
What this means for the reports in #7546Unchanged for the read path: existing rows with the retired wrapper are still converted by the migration, and Verified on our sideFive injection sites here were writing the retired wrapper (all ordinary message slots — Reproducing the matrix needs only the installed catalog path: |
|
A concrete pointer for the "state the migration in the plugin-facing docs" item. At 0.1.7-alpha.2 (0010283), two cookbook pages still teach the retired wrapper that V4 refuses:
An author who follows those pages today writes messages that fail with The literal those examples should show is already defined by the migration's source-kind table in Confirming the portability conclusion in the follow-up above from the other side: 0.1.6-alpha.2's |
|
给这条补三段迁移侧的实测 —— 中央主张你们已经自己更正过(作者 09-23 02:39 那条),剩下的缺口正好在「迁移路径」这一侧,而它没人写过。 一、迁移路径其实认 二、本机读数(全在副本,原件未动):179 个存档 / 三、还有一件文档里肯定没有的(对所有双版本并存的人都是硬知识):V4 是追加发布在旁边(与它自己脚本的自述一致:历史代次不动),但一个目录里一旦出现 判据 / 怎么复算这三段(不需要账号、不需要触屏):在已发布产物里 |
Uh oh!
There was an error while loading. Please reload this page.
V3 validation and V4 admission disagree about plugin-authored message sources, and there is no literal that satisfies both. Every out-of-tree plugin that injects messages the way the V3 tree taught will fail on 0.1.7 with
format v4 message requires a producer-owned source kind. Reporting it with source citations; corrections welcome if I have misread something.The two rules
V3 accepts
'plugin', and refuses anything it does not recognise.packages/session/session-format-v2-to-v3/src/payload.ts:'plugin'is in that closed set;'plugin:<name>'is not. Line 237 of the same file goes further and requires the retired literal for system messages:if (source['kind'] !== 'plugin' || typeof source['plugin'] !== 'string' …) throw new SessionFormatError('system message requires plugin source').V4 refuses
'plugin'—packages/session/session-format-v3-to-v4/src/message-sources.ts, with the rejected set pinned by the spec (null, [], {}, {kind:''}, {kind:1}, {kind:'plugin',plugin:'compact'}):So on a V3 host,
{kind:'plugin', plugin:'X'}is the only accepted form; on a V4 host it is the one refused form, and the accepted form ({kind:'plugin:X'}) fails V3 admission withcannot safely transform unclassified message source. Neither literal is portable.Why this only bites out-of-tree plugins
In the 0.1.6 tree, every producer writes the retired literal — first-party included:
session-title-llm(session-title-llm/src/index.ts:250),plan-mode(:461),cordis-host-runner(five sites),time-context(:217),tmux-context(:259),schedule(runtime.ts:271),hooks-codex/hooks-claude-code(PLUGIN_SOURCE),agent-instructions(state.ts:96),compaction-basic(summarizer.ts:148),tools-ptc(ptc.ts:635), plus the migration paths themselves. A code search of currentmasterfor that literal returns nothing: the repo-wide sweep happened in-tree, and the V3→V4 migration converts V3 rows on read — so nothing in the shipped configuration breaks.An installed third-party plugin has neither benefit. It keeps emitting the retired literal on a V4 host,
encodeCurrentEventrefuses it on the write path, and the user sees exactly the failure reported in #7546 — "本轮运行失败". Same for the read path, whereassertV4SourceRowAdmissionrefuses retired wrappers in every declared message slot before recovery can discard them.Measurement on a real machine (0.1.6-alpha.2)
I scanned this host's session logs for the pattern (multi-frame zstd, so the frames have to be walked — a single-frame decompressor misses everything after the first frame). One session, 12,019 rows: 85 messages still carry
{kind:'plugin'}, from five producers —tool-jobs(76),repeat-tool-reminder(4),user-approval(2),@deepseek-ai/dsh-system-prompt(2),dsh-session-title-llm(1). That file is headerversion: 0, so the migration chain converts those rows on read and the session still opens — which is precisely why this stays invisible until a plugin writes under V4 rather than having its history read.What would close the gap
Two things, either of which is sufficient for plugin authors:
producerKind(): third-party producers becomeplugin:<their name>and drop thepluginfield; the 24 names inRELEASED_SAME_NAME_PRODUCERSkeep their own name. Right now that rule is discoverable only by readingsession-format-v3-to-v4source, and the V3 tree still contains hundreds of counterexamples to imitate.injectedSource(name)) so a plugin asks the host for the version-correct source instead of hardcoding either literal. As long as the source is hand-built, a plugin that supports both harness generations has to branch on the session format version itself, and a plugin that does not branch is silently broken by an upgrade.Happy to test a candidate helper on both generations if that is useful — I have a 0.1.6 install and a 0.1.7 target, and can report what each accepts. Disclosure so the report is not read as finger-pointing: our own organs write the retired literal too, so they are in the affected population; that is how I found this.
All reactions