max-tokens responses containing tool calls can permanently break pi-ai session replay #2410
youkongling
started this conversation in
General
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Summary
A successful stream that terminates with
max-tokensand contains a tool call can persist an assistant message whose durable content no longer matches its adapter replay state. On the next request, pi-ai history reconstruction rejects that message with:The request fails during local history reconstruction, before it reaches the provider. Once such a message is stored, every subsequent “continue” for that session fails the same way.
Impact
replayState.blockstherefore have different lengths.I reproduced this with a real session using a pi-ai-backed provider. The truncated block was a large tool call; the durable assistant content retained reasoning/text blocks while replay state retained reasoning/text/tool-call blocks.
Root cause
BlockAssembler.blocks()intentionally removes tool calls on max-token truncation:But its replay-state accessor still returns the original terminal state:
The agent loop then persists:
For example, the stored message can contain
[reasoning, text]while replay state contains[reasoning, text, tool-call].On resume, pi-ai replay validation correctly enforces:
That invariant is right; the writer violated it.
Proposed fix
Patch branch: https://github.com/youkongling/deepseek-harness/tree/fix/max-tokens-replay-state
Patch commit: youkongling@f6bbb24
Raw patch: https://github.com/youkongling/deepseek-harness/commit/f6bbb24a439c10c1901566b8f8b96ce08c50da36.patch
1. Keep new writes aligned
When max-token safety filtering removes at least one tool call,
BlockAssembler.replayStatenow returnsundefined. The persisted assistant content and persisted replay metadata therefore remain consistent.Max-token responses without tool calls retain replay state, as do normally completed responses.
2. Recover the known historical corruption without weakening validation
Sessions written by the old behavior may already contain the mismatch. The pi-ai conversion layer now recognizes only the exact legacy shape:
stopReasonislength;Such a message is translated as provider-neutral history. Other malformed states, provider/model mismatches, unsupported versions, and unrelated block mismatches continue to fail explicitly with
INVALID_REPLAY_STATE.This preserves fail-loud replay validation while unbricking sessions corrupted by this specific writer bug.
Tests added
BlockAssemblerdrops replay state when max-token filtering removes a tool call.BlockAssemblerretains max-token replay state when no tool call was filtered.length+ dropped-tool-call corruption as provider-neutral history.Validation on top of current
master(47f943859bef60e4160492346772ded9b24f765a):The same vulnerable logic is present in current
masterand in the published@deepseek-ai/dsh-llm/@deepseek-ai/dsh-llm-pi-airuntime packages I inspected.中文说明
这是一个 DSH 核心写入路径的持久化一致性 bug。
当模型输出达到
max-tokens且包含 tool-call 时,DSH 会安全过滤掉可能被截断的 tool-call,避免执行不完整工具调用;但BlockAssembler仍会把描述原始 tool-call 的replayState返回给 agent-loop,并随 assistant message 一起持久化。结果是持久化后的 assistant content 与
replayState.blocks数量不一致。例如:下次继续该会话时,pi-ai 历史重建会校验失败:
这个失败发生在本地历史重建阶段,请求不会真正到达模型,因此该会话后续“继续”会反复失败,表现为永久卡死。
修复思路:
max-tokens安全过滤移除 tool-call 时,同步丢弃replayState,保证持久化内容与回放元数据一致。stopReason = length、replay blocks 多于 durable content、durable content 无 tool-call、去掉 replay 中的 tool-call 后两边完全对齐;此时转换为 provider-neutral 历史继续使用。INVALID_REPLAY_STATE。修复分支:
https://github.com/youkongling/deepseek-harness/tree/fix/max-tokens-replay-state
修复提交:
youkongling@f6bbb24
All reactions