Repository navigation
[Bug] 0.2.1-rc.2: sessions with non-empty surface but no system/message head fail to load (SessionFormatError: requires a protected first surface head) #10619
Replies: 2 comments
|
The rule you hit is real and it is a fold-time invariant, not a corruption check: // packages/session/session-format-v3-to-v4/src/relationships.ts:124-128
foldSurface(event: SessionFormatEvent): void {
if (!SURFACE_TYPES.has(event.type)) return
if (event.type === 'system/message' && this.surface.length > 0 && this.protectedHead === undefined) {
throw new SessionFormatError('system/message requires a protected first surface head')
}The condition is positional: a So the reader is doing what v4 defines; the question is whether the v3→v4 conversion should have produced a head. Since your import synthesizes a v4 log from a v3 shape that never had the invariant, the fix belongs in the migration: when the surface opens with a non-system event, insert the head at that point (or lift the earliest system/message to the front) before the log is written as v4. Enforcing it at read time cannot help, because by then the ordering is already recorded. One thing worth separating in the report: the wrapper message says "stored log is corrupt", but the log is not corrupt — it is valid v3 semantics that violate a v4 invariant. If you want a workaround before a fix, keep the imported sessions on the v3 side instead of converting them — the invariant only exists in v4, so a log that is never converted never has to satisfy it. Reordering events inside an existing v4 log is not a safe workaround: |
|
Still reproduces on 0.2.1-rc.5 (Windows desktop, 2026-11-03): a seeded/imported session whose surface starts with The consequence is now worse than unreadable history: on the next app start the corrupt log was silently rewritten to a prefix of complete zstd frames (729,460 B → 4,129 B; 1,024 of 1,058 events discarded, no backup kept). The placeholder-head repair you described is exactly what allowed recovering the session. Evidence, timeline and the write/truncation details: #12065. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Component: session log validation /
SystemPromptProjectionVersion: DeepSeek Harness
0.2.1-rc.2(bundled inapp.asar, 3 occurrences of the version string)Platform: Windows 11, Electron desktop build (
E:\<install>\resources\app.asar)Model route:
deepseek-flash(DeepSeek-V4.1-Flash-1017)Summary
After importing legacy chat logs (sessions that have user/assistant nodes but no persisted
system/message),opening such a session fails with:
The log itself is well-formed (valid zstd, valid JSONL, monotonic
seq). The failure is a write-pathinvariant violation triggered by the projector, not corrupt input:
SystemPromptProjection.project()unconditionally appends a system node when the surface has none, but the validator rejects appending a
system/messageonce the surface is already non-empty and no protected head exists.Expected vs actual
rendered system prompt at the head is legal.
SessionFormatError, history is unreadable, and the session cannot be continued.Root cause (verified in the shipped bundle)
SystemPromptProjection.project():The validator (
foldSurface/ log validation):So for a legacy log (
turn/start → step/start → user/message → …, nosystem/message):surface.length > 0→ true (user/assistant events),protectedHead === undefined→ true (never set, because no system node was ever appended),append→ validator throws → history load fails.Minimal reproduction
user/message(nosystem/messagebefore it) — e.g. a logproduced by an older exporter, or a log whose first write predates system-prompt persistence.
Observed event order of an affected (but valid) log after the hotfix described below:
Why the
surfaceOp: "append"is not legal hereappendis only legal when it creates the protected head (surface.length === 0). Once the surface containsnodes, the only legal ways to establish a system node are:
The projector currently has no such branch, so the first re-open of a legacy session is guaranteed to fail.
Workaround we applied (works, but should live in the product)
Insert one inert
system/messageatturn=1, step=1before the firstuser/message:{ "type": "system/message", "seq": 2, "data": { "turn": 1, "step": 1, "message": { "role": "system", "content": [], "source": { "kind": "system-prompt" }, "id": "<fresh uuid>" } }, "surfaceOp": "append" }This is byte-for-byte what
createSystemMessage('')produces(
content: text.length === 0 ? [] : [{type:'text',text}],source: { kind: "system-prompt" }).It is inert because the projector drops empty content from the wire:
(Comment in the source: “Empty system and developer nodes retain their surface positions without adding wire
messages.”)
After the placeholder exists,
project()behaves correctly on the next resume:!inHistory || startsSeries || rendered === ""→replace(head.seq, rendered)(head normalized in place), orlatest.text !== rendered→ append, which is now legal because the head is protected.Suggested fix
project(), handlehead === void 0 && surface.length > 0by emitting aninsert-at-head intent (e.g. a replacement whose range is empty at position 0), instead of
append.persist the inert placeholder above as part of a lossless migration, before any projection runs.
Migration must keep a backup of the original log.
(
session,permission/preset,sandbox/mode,approval/policy) is legitimate and should not bereported as corrupt.
system/messageappended while the surface was empty” — otherwise third-party importers cannot target the format safely.
Extra observation (data safety)
Applying the workaround in our environment required rewriting 37 session files in place. Tooling around this
workflow (
fix-sessions.ps1,auto-repair.ps1) only backs upworkspace.json, not the session logs beingreplaced. Recommend the product (and any repair scripts) always snapshot
~/.dsh/sessionsbefore rewriting.中文摘要(供参考)
现象:导入的旧会话(有 user/assistant 节点但没有
system/message)一打开就报SessionFormatError: system/message requires a protected first surface head,历史加载失败。根因:
SystemPromptProjection.project()在“surface 无 system 节点”时无条件append;而校验器规定:surface 非空时再追加
system/message必须有受保护的 head。两条规则冲突,老格式会话首次重开必然失败。临时修复:在
turn=1/step=1插入一条空内容的system/message(content: []、source.kind: "system-prompt",等同createSystemMessage(''))。因为deriveEventMessage对空内容返回null,这条占位不会进入任何模型请求;下次续聊时投影会把它替换成真实系统提示(或合法追加)。建议:产品侧在
project()增加“插入为 head”分支,或在加载时做一次带备份的无损迁移;并区分“空会话”与“损坏会话”。
以上内容由deepseek会话后生成,并指导发送。
All reactions