You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[bug] Session permanently stuck with 400: tool_addition blocks serialized into a system-role message inside messages[]
#10760
[bug] Session becomes permanently unusable: tool_addition blocks are serialized into a system-role message inside messages[]
Component:dsh-llm-deepseek (Messages API serializer) Version:@deepseek-ai/dsh-desktop-runtime 0.2.1-rc.2 (Windows x64) Provider/model:deepseek-account / deepseek-flash, systemPromptUpdate: "in-history" Severity: high — one failed request permanently bricks the session (no recovery path in UI)
Summary
When the tool registry changes mid-session, DSH writes a developer/message containing N tool-addition blocks. At request time these are converted to Anthropic tool_addition content blocks and placed in a role: "system" message inside the messages array. The provider rejects the request with:
messages.172: `tool_addition` blocks can only be in `system` messages (400, INVALID_REQUEST)
Because the developer message is already persisted in the session log before the request is sent, every subsequent turn rebuilds the same broken request body and fails identically. The session is stuck forever; restarting DSH does not help.
Steps to reproduce
Open a session with a model configured with systemPromptUpdate: "in-history" (default for the shipped catalog).
Change the available tool set mid-conversation — e.g. install/remove an extension, or let the skill catalog change. DSH then appends a developer/message (source: {kind: "tool-registry"}) with one {type: "tool-addition", toolName} block per new tool.
Send any message.
Expected: request succeeds; the model sees the new tools (they are already in the top-level tools array anyway). Actual: HTTP 400 messages.NNN: tool_addition blocks can only be in system messages; the turn ends with turn/end {kind: "error"}; all later turns fail identically.
Observed request ids on one session: c47f2c8f-…, c8a2fe95-…, 402a224a-…, eb47d51c-c484-43b0-bc69-6dd7573a64d0, 45700b48-f4e2-4ffb-8bc1-ec7126a571f2, d89742f4-2024-4500-8307-8aed465cded8. Six consecutive turns, all 400, never retried (correct: INVALID_REQUEST is not retryable).
Root cause
All line numbers below refer to the packaged dsh-llm-deepseek/lib/index.js in 0.2.1-rc.2.
1. The blocks are put into a system role entry inside messages[]
serialize() (line 1590) collects developer messages into systemUpdates:
case "tool-addition": return [{
type: "tool_addition",
tool: { type: "tool_reference", name: block.toolName }
}]; // line 1638-1644
...
if (content.length > 0) systemUpdates.push({
role: "system",
content
}); // line 1655-1658
and flushes them into the messages array:
const flushSystemUpdates = () => {
...
messages.push(...systemUpdates.splice(0)); // line 1628
};
...
if (message.role === "assistant") flushSystemUpdates(); // line 1685
...
flushSystemUpdates(); // line 1694
The top-level system field is built from plain strings only and never receives these blocks:
const system = [options.system, historySystem].filter(Boolean).join("\n\n"); // line 1714
This is the core issue: Anthropic Messages API allows only user and assistant in messages[]; system is a top-level parameter. DSH's interpretation of "system message" (a message whose role is system inside messages[]) differs from the provider's (the top-level system field), which is exactly what the error text says. Note the index mismatch: the serialized array has 174 entries with the system entry at index 173, while the provider reports messages.172 — the preceding user turn. The provider appears to relocate/merge system-role entries before validating blocks, so the tool_addition block ends up in a non-system position.
2. A beta feature is required but its capability is never verified
/** Required opt-in for mid-conversation tool_addition and tool_removal blocks. */
const MESSAGES_TOOL_CHANGES_BETA = "mid-conversation-tool-changes-2026-07-01"; // line 560
const betas = [...,
...body.messages.some((message) => message.content.some((block) =>
block.type === "tool_addition" || block.type === "tool_removal"))
? [MESSAGES_TOOL_CHANGES_BETA] : []];
// line 2216
The header is attached mechanically whenever such a block exists, with no capability negotiation and no fallback. If the endpoint ignores or does not support this beta, every request carrying tool additions fails — and there is no way for the client to recover at runtime.
3. toolUpdate is dead configuration
toolUpdate is declared with default "addition-only" (line 48), exposed in the model schema (line 310), validated and merged into the connection config (lines 367-378, 524) — but serialize()never reads it. The only serialization switch that exists is systemPromptUpdate === "in-history" (line 1592), which only affects plain-text system updates. Users therefore have no supported way to opt out of tool additions, even though the setting suggests they do.
4. The trigger is persisted before the request, so one failure is permanent
The developer/message is appended to the session log at the moment the tool registry changes (surfaceOp: "append"), before any request is made. When the request fails, nothing removes or rewrites it, so the broken body is rebuilt on every later turn. There is no "this message was never delivered" rollback and no way to delete a single event from the UI.
Suggested fixes
Do not emit role: "system" entries inside messages[]. Merge text-only system updates into the top-level system field (or into the allowed position defined by the beta), and place tool_addition / tool_removal only where that beta actually defines them. If the beta requires top-level system content blocks, build the system field as an array instead of a joined string.
Add a fallback or capability probe. Top-level tools already contains the full tool table, so dropping the redundant tool_addition blocks changes nothing semantically. On tool_addition blocks can only be in system messages, retry once with the blocks stripped (or downgrade the developer message to a text block).
Wire or remove toolUpdate. Either honor "in-history" / "addition-only" in serialize() or delete the option; right now it is a misleading no-op.
Roll back (or never persist) a tool-registry developer message whose request failed. At minimum, if a request fails while a tool-change message exists, retry without it and mark it superseded.
Related robustness issue: listArtifacts() silently drops sessions whose log fails structural validation (catch { return { metadata: void 0 } } then continue). The session simply disappears from the sidebar with no diagnostic. Please surface a warning / "damaged session" state instead.
Evidence / recovery (for reference)
The affected session log is sessions/--E-DSH--/session-<id>/session.v4.jsonl.zstd. Recovery that worked without touching DSH code:
Locate the persisted developer/message event carrying content: [{type:"tool-addition", toolName}…].
Replace its content with a single {type:"text", text:"Tool registry updated …"} block and delete its data.headerSeq (the local validator requires headerSeq exactly when tool additions are present).
Rebuild the zstd log without changing frame boundaries: keep bytes up to the frame containing that event and re-compress from that frame's plaintext onward. The first frame must contain exactly one header line (assertZstdHeaderFrame), otherwise the session is treated as corrupt and silently skipped.
After this edit the session loaded and the next turn completed with 67 events and zero errors, confirming the tool-addition blocks were the only cause.
Environment
Windows 11, DSH Desktop 0.2.1-rc.2
Provider deepseek-account, model deepseek-flash, reasoning effort high
Third-party extensions installed (modlens, billion-context, univer-office, dsh-context, dshmarket, …) — tool-registry churn from enabling them is what triggered the first tool-addition write.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
[bug] Session becomes permanently unusable:
tool_additionblocks are serialized into a system-role message insidemessages[]Component:
dsh-llm-deepseek(Messages API serializer)Version:
@deepseek-ai/dsh-desktop-runtime 0.2.1-rc.2(Windows x64)Provider/model:
deepseek-account/deepseek-flash,systemPromptUpdate: "in-history"Severity: high — one failed request permanently bricks the session (no recovery path in UI)
Summary
When the tool registry changes mid-session, DSH writes a
developer/messagecontaining Ntool-additionblocks. At request time these are converted to Anthropictool_additioncontent blocks and placed in arole: "system"message inside themessagesarray. The provider rejects the request with:Because the developer message is already persisted in the session log before the request is sent, every subsequent turn rebuilds the same broken request body and fails identically. The session is stuck forever; restarting DSH does not help.
Steps to reproduce
systemPromptUpdate: "in-history"(default for the shipped catalog).developer/message(source: {kind: "tool-registry"}) with one{type: "tool-addition", toolName}block per new tool.Expected: request succeeds; the model sees the new tools (they are already in the top-level
toolsarray anyway).Actual: HTTP 400
messages.NNN: tool_addition blocks can only be in system messages; the turn ends withturn/end {kind: "error"}; all later turns fail identically.Observed request ids on one session:
c47f2c8f-…,c8a2fe95-…,402a224a-…,eb47d51c-c484-43b0-bc69-6dd7573a64d0,45700b48-f4e2-4ffb-8bc1-ec7126a571f2,d89742f4-2024-4500-8307-8aed465cded8. Six consecutive turns, all 400, never retried (correct: INVALID_REQUEST is not retryable).Root cause
All line numbers below refer to the packaged
dsh-llm-deepseek/lib/index.jsin 0.2.1-rc.2.1. The blocks are put into a
systemrole entry insidemessages[]serialize()(line 1590) collects developer messages intosystemUpdates:and flushes them into the
messagesarray:The top-level
systemfield is built from plain strings only and never receives these blocks:This is the core issue: Anthropic Messages API allows only
userandassistantinmessages[];systemis a top-level parameter. DSH's interpretation of "system message" (a message whose role issysteminsidemessages[]) differs from the provider's (the top-levelsystemfield), which is exactly what the error text says. Note the index mismatch: the serialized array has 174 entries with the system entry at index 173, while the provider reportsmessages.172— the preceding user turn. The provider appears to relocate/merge system-role entries before validating blocks, so thetool_additionblock ends up in a non-system position.2. A beta feature is required but its capability is never verified
The header is attached mechanically whenever such a block exists, with no capability negotiation and no fallback. If the endpoint ignores or does not support this beta, every request carrying tool additions fails — and there is no way for the client to recover at runtime.
3.
toolUpdateis dead configurationtoolUpdateis declared with default"addition-only"(line 48), exposed in the model schema (line 310), validated and merged into the connection config (lines 367-378, 524) — butserialize()never reads it. The only serialization switch that exists issystemPromptUpdate === "in-history"(line 1592), which only affects plain-text system updates. Users therefore have no supported way to opt out of tool additions, even though the setting suggests they do.4. The trigger is persisted before the request, so one failure is permanent
The
developer/messageis appended to the session log at the moment the tool registry changes (surfaceOp: "append"), before any request is made. When the request fails, nothing removes or rewrites it, so the broken body is rebuilt on every later turn. There is no "this message was never delivered" rollback and no way to delete a single event from the UI.Suggested fixes
role: "system"entries insidemessages[]. Merge text-only system updates into the top-levelsystemfield (or into the allowed position defined by the beta), and placetool_addition/tool_removalonly where that beta actually defines them. If the beta requires top-levelsystemcontent blocks, build thesystemfield as an array instead of a joined string.toolsalready contains the full tool table, so dropping the redundanttool_additionblocks changes nothing semantically. Ontool_addition blocks can only be in system messages, retry once with the blocks stripped (or downgrade the developer message to a text block).toolUpdate. Either honor"in-history"/"addition-only"inserialize()or delete the option; right now it is a misleading no-op.listArtifacts()silently drops sessions whose log fails structural validation (catch { return { metadata: void 0 } }thencontinue). The session simply disappears from the sidebar with no diagnostic. Please surface a warning / "damaged session" state instead.Evidence / recovery (for reference)
The affected session log is
sessions/--E-DSH--/session-<id>/session.v4.jsonl.zstd. Recovery that worked without touching DSH code:developer/messageevent carryingcontent: [{type:"tool-addition", toolName}…].{type:"text", text:"Tool registry updated …"}block and delete itsdata.headerSeq(the local validator requiresheaderSeqexactly when tool additions are present).assertZstdHeaderFrame), otherwise the session is treated as corrupt and silently skipped.After this edit the session loaded and the next turn completed with 67 events and zero errors, confirming the tool-addition blocks were the only cause.
Environment
deepseek-account, modeldeepseek-flash, reasoning efforthightool-additionwrite.All reactions