Replies: 2 comments
|
Thanks — the asymmetry is real, and sharper than "no validation at the append boundary": the validator that rejects your event already exists, it is just never called on the write path. Where the rule lives vs. where it is enforcedThe throw in your error text is if (sourceRecord['kind'] !== 'tool'
|| typeof sourceRecord['callId'] !== 'string'
|| sourceRecord['callId'] === '') {
throw new Error(`${subject} message must have tool source`)
}That function has exactly two call sites: So exactly one rule is missing on the write side, and Your fix 1 can be smaller than "add validation"
Where the degenerate block is born (not in the report)
id: partial.toolCallId ?? brandString<ToolCallId>(`call-${index}`),
name: partial.toolCallName ?? '',The id has a fallback and the name has none — but that fallback never fires for your case either, because Worse, the harness already detects this shape and declines to reject it. if (chunk.id.length === 0 || chunk.name === '') {
this.records.push({ type: 'chunk', time, chunk })
return timed
}while the coalesced record it is steered away from has a strict counterpart — An invariant for exactly your suggestion 1 exists — it just is not mounted
if (exec.name.length === 0 || String(exec.callId).length === 0) {
fail('tools/result execution must carry non-empty name and callId')
}That is your fix 1, already written as a testable contract. But Two cautions on suggestion 2The shape rules are load-bearing: the For anyone who needs a workaround today
One thing I could not confirm from sourceThe "every start attempt fails" step. |
|
Follow-up: the producer-side mitigation I described now exists as a published plugin, so anyone hitting this today can recover without waiting for the write-boundary fix.
npm install @argszero/cordis-plugin-llm-tool-call-guardMount it in a dsh profile: - insert:
- id: llm-tool-call-guard
name: '@argszero/cordis-plugin-llm-tool-call-guard'New in 0.1.4 is an identity guard. Defaults are on, so mounting is enough:
Two honest caveats, so nobody over-reads this:
I still owe this thread a retest prompt rather than a claim: if you re-run the same provider and the guard is mounted, the session should survive the reload. If it does not, that is a bug in the plugin and I would want the frame. |
Uh oh!
There was an error while loading. Please reload this page.
Summary
@deepseek-ai/dsh@0.1.2-rc.1accepts a degenerate tool call (emptyname/id/arguments) from a model response and persists the result chain as-is. On the next load, the strict message-shape validation (@deepseek-ai/dsh-session, same version) rejects the storedtool/resultand marks the entire session corrupt — the conversation history cannot be opened and the provider can no longer observe/resume the session. There is no recovery path short of manually editing the zstd frame container.This happened on multiple sessions across two machines (Windows + Linux), with 15–16 such chains in a single long session.
Environment
@deepseek-ai/dsh0.1.2-rc.1(also@deepseek-ai/dsh-session0.1.2-rc.1)dsh-llm-pi-ai), modeldeepseek-v4-flash-vision-expserved via local/custom providersWhat gets persisted
A model turn ended with a tool call block carrying empty fields:
{"type": "tool-call", "id": "", "name": "", "arguments": "{}"}The harness then recorded (no validation at the append boundary):
{"type": "tool/call", "seq": 709, "data": {"turn": 2, "step": 3, "callId": "", "name": "", "arguments": "{}"}}{ "type": "tool/result", "seq": 710, "data": { "turn": 2, "step": 3, "message": { "source": {"kind": "tool", "callId": ""}, "content": [{"type": "tool-result", "toolCallId": "", "isError": true, "content": [{"type": "text", "text": "Error: unknown tool \"\""}]}], "role": "user", "id": "1121ab3f-4834-4b6e-be7f-a890cc8c0ddd" }, "error": {"name": "ToolNotFoundError", "code": "UNKNOWN_TOOL"} }, "sourceEventSeqs": [709], "surfaceOp": "append" }What breaks on load
History load (session query) fails with:
because
assertMessageEventShape(dsh-sessionlib/index.js) requirestool/resultmessages to havesource.kind === "tool"and a non-emptysource.callId(plusblock.toolCallId === source.callId). The event was allowed to be written withcallId: ""but can no longer be read back — a write/read validation asymmetry within the same build.Provider resume/observe then fails from the daemon side:
Impact
.jsonl.zstdframe container.Suggested fixes
name(or emptyid), reject it (raise/fail the step or retry once with a steering message) instead of persisting a degeneratetool/call+tool/resultchain. At minimum, never persist atool/resultwith an emptycallId.--repair) would save users manual binary surgery.Repro sketch
name(observed withdeepseek-v4-flash-vision-exp).ToolNotFoundError(unknown tool ""); the chain above is persisted.All reactions