Replies: 2 comments
|
Confirmed, and your diagnosis holds all the way down. Thank you for repairing your own logs and reporting that the bytes were intact — that is what makes this unambiguously a validation asymmetry rather than data loss. Where the empty id gets in
id: partial.toolCallId ?? CallId(`call-${index}`)
Fix, writer-sideTreat empty exactly like absent, at the single point blocks leave the assembler: function withCallId(block: ContentBlock, index: number): ContentBlock {
if (block.type !== 'tool-call' || block.id !== '') return block
return { ...block, id: CallId(`call-${index}`) }
}There is a second path that is easy to miss — I missed it on the first attempt. On your suggestion 2 (reader-side tolerance)I would argue against loosening the reader, and for the writer fix alone. The correlation between Your suggestion 3 (a migration channel) stays the open question — this fix stops new logs from being damaged but does nothing for the ones already on disk, which still need the in-place repair you describe. Related but separate#4671 looks like the same bug and is not: there an adapter discards the id and the name when continuation frames send explicit nulls. The assembler fallback above does not help it, because it restores an id while the empty name is what makes dispatch fail with |
|
Session-unreadability is the nastiest tail of a well-measured family — cross-adapter datapoint: We fault-injected the real pi-ai openai-completions accumulator (client/SSE parser/assembly unmodified; only the wire bytes synthetic) with this exact delta family — empty-string id/name continuations, late identity, parallel calls with no ids at all (index-keyed interleave), and index-less id-keyed deltas: all assemble correctly. The semantics that make it immune: blocks are keyed by That measured rule (index-first keying + first-non-empty-wins) is a ready-made semantic for the |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Opening older history sessions in the Web UI (
@deepseek-ai/dsh web, v0.1.1-rc.2, developer preview) fails with:Root cause (diagnosed from the persisted log)
The session log contains a single
tool/resultevent produced by an empty tool call: the model emitted a tool call with an empty id/name, the dispatcher raisedToolNotFoundError: unknown tool "", and the writer persisted the failure with empty call ids:{ "type": "tool/result", "data": { "message": { "role": "user", "source": { "kind": "tool", "callId": "" }, "content": [{ "type": "tool-result", "toolCallId": "", "content": [{ "type": "text", "text": "Error: unknown tool \"\"" }], "isError": true }] }, "error": { "name": "ToolNotFoundError", "code": "UNKNOWN_TOOL" } } }At load time
assertMessageEventShaperequirestool/resultto carry a non-emptysource.callIdmatching the precedingtool/call, so ONE such record rejects the entire session (hundreds of thousands of events). Restarting does not help — the bytes on disk are unchanged. There is no migration path: the storage format version is pinned to 0 and older records are hard-rejected.Suggested fixes
tool/resultfor an empty tool call, generate a synthetic non-empty callId (or drop the failure record).UNKNOWN_TOOLrecords with empty callId (warn, don't fail the whole session).Note
I already repaired the affected local sessions in place (backfilled
unknown-tool-<seq>callIds on thetool/call+tool/resultpair, re-encoded with the same zstd framing) and all affected sessions load again — the underlying data was intact; this is purely a validation/schema-drift issue. Happy to contribute the writer-side guard (or reader-side tolerance) as a PR if maintainers agree on an approach.Environment: Windows 10 22H2 (build 19045, 64-bit) · Node v24.13.0 · npm 11.12.1 ·
@deepseek-ai/dsh@0.1.1-rc.2(npm global).All reactions