Replies: 2 comments
|
核验通过(对照 main/master HEAD 代码逐条确认:
修复优先级(同意你的 A/B/C,微调排序):
家族归类:并入家族 3(坏工件隔离)"截断 tool-call 参数"子族(与 #3232 崩溃恢复、#1047 单坏日志同原则:隔离 + 可见 + 可修复才修复)。需要的话我可以把 C 的验证点写成回归测试(截断 argumentsDelta → assembler finalize 拒绝/落盘正常形态)。 |
|
I verified the durable failure chain against https://sandbaseai.github.io/deepseek-harness-handbook/poisoned-session-recovery.html The practical recovery sequence is:
One current-source nuance: at The runbook includes a read-only detector that parses the outer event row and then parses |
Uh oh!
There was an error while loading. Please reload this page.
Summary
When a model streams a tool call whose
argumentspayload arrives truncated / incomplete JSON (the delta stream stops mid-string, or a closing"/}never arrives before the block ends), the harness persists the malformed string verbatim into the append-only session log.From then on, every request in that session rebuilds its history from the log,
serializeAssistantreserializes the same brokenargumentsunmodified intotool_calls[].function.arguments, and the DeepSeek (Python) endpoint fails tojson.loadsit, returning:The turn is marked failed and the GUI shows the terminal turn-error. Because the log is append-only and the bad record is never healed, the same session fails on every follow-up turn, indefinitely. Only starting a new conversation (fresh log, no bad history) recovers.
Environment
@deepseek-ai/dsh-llm-deepseek(DeepSeek chat-completions compatible endpoint, streaming)session.jsonl.zstd(append-only JSONL back-end)Steps to reproduce
read,grep, andpwsh; likely any tool) where the streamedargumentsis cut off mid-JSON — thetool-call-deltastops before the closing"}andblock-endcloses the block anyway.argumentsis persisted as atool/callevent in the session log.400 INVALID_REQUEST: Unterminated string starting at: line 1 column N (char N).Expected behavior
argumentsdo not form valid JSON should either be rejected/retried at ingest time, or persisted in a sane form (e.g.{}), so that a single truncated stream does not permanently brick the session history.argumentsto a schema-strict upstream and turn it into a permanent400.load()for malformed persistedarguments.Actual behavior
The ingest path deliberately tolerates malformed arguments, but the resend path is strict, and nothing validates or heals in between:
BlockAssemblerconcatenatestool-call-delta.argumentsDeltaonto the accumulating arguments (@deepseek-ai/dsh-llm,assembler.ts) without ever checking that the result is valid JSON.dsh-agent-loop'sparseArgumentsswallows invalid JSON (catch { return raw }) and runs the tool with the raw truncated string — no error surfaces.session.append("tool/call", { arguments: block.arguments })persists the raw truncated string as the authoritative record.dsh-llm-deepseek'sserializeAssistantwritestool_calls[].function.arguments = block.argumentsverbatim into the wire request; the DeepSeek server (Python)json.loadsit and 400s.dsh-host-apiproxy'sviewFor/backscanArgsalso do a bareJSON.parse(raw), throwingUnterminated string ...again — this is what emits the console noiseapi-proxy: presenter failed for tool/call, falling back to generic: ....load()only synthesizes missingtool/result/turn/endclosers; it does not scan or heal malformedarguments. So the poisoned record is permanent.Evidence (from real
~/.dshsession logs)Example 1 — persisted
tool/callwith truncatedreadarguments:(note: missing closing
"}), immediately followed by:{"type":"assistant/chunk","seq":696,"data":{"chunk":{"type":"finish","reason":{"kind":"error","failure":{"message":"Unterminated string starting at: line 1 column 15 (char 14)","code":"INVALID_REQUEST","status":400}}}} {"type":"turn/end","seq":698,"data":{"reason":{"kind":"error","error":{"message":"Unterminated string starting at: line 1 column 15 (char 14)","code":"INVALID_REQUEST","status":400}}}}Example 2 — persisted
tool/callwith truncatedgreparguments:followed by two
turn/enderrors (seq 175,seq 183) withUnterminated string starting at: line 1 column 45 (char 44).The streamed deltas confirm the truncation is upstream: for example the
readcall received a single partialargumentsDeltaand thenblock-endwithout a closing"}", while adjacentglobcalls in the same assistant message did receive their closing"}"deltas — so the harness faithfully accumulated a payload the provider never finished.Suggested fixes
A (minimal, unblocks) — in
dsh-llm-deepseek'sserializeAssistant(and any other place that resends tool-callarguments), validate withJSON.parseand substitute{}for invalid values, so a poisoned history stops producing permanent400s:B (quiet the noise) — wrap the bare
JSON.parse(raw)indsh-host-apiproxyviewFor/backscanArgsin try/catch (mirroringparseArgumentselsewhere), removing thepresenter failed ... falling back to genericconsole spam.C (root cause) — validate the accumulated tool-call
arguments(valid JSON) before persisting thetool/callevent (inBlockAssemblerfinalize orappendToolCall); drop or replace invalid arguments so bad history never enters the log. Optionally add a recovery/heal pass onload()for already-persisted malformed records.Impact
INVALID_REQUESTuntil the user branches or starts a new conversation.presenter failedJSON-parse errors on the same poisoned record.Issue drafted from a diagnosis that inspected the compiled packages in the npm-cache checkout and decoded the local
~/.dsh/sessions/.../session.jsonl.zstdlogs to confirm the persisted corruptargumentsand the recurringINVALID_REQUESTturn errors.All reactions