Repository navigation
INVALID_REPLAY_STATE permanently wedges a session when a pi-ai response is cut by the output cap #6525
SeekerCodes
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.
INVALID_REPLAY_STATEpermanently wedges a session when a pi-ai response is cut by the output capSummary
When an
llm-pi-airoute's assistant response is truncated at the output-token cap(
stopReason: "length") while a tool call is in flight, the persisted replay ledgerrecords a block that never reaches the durable message content. Every subsequent turn
in that session then fails the replay invariant with:
The session becomes unusable through normal means — no data is lost, but the log
carries a permanently invalid ledger.
Environment
0.1.0-rc.5(local checkout; the failure was observed live on arunning Web instance against the same log)
llm-pi-aiprovider on the OpenAI chat-completions protocolpointing at the official DeepSeek endpoint
deepseek-v4-flash(response modeldeepseek-flash)maxTokenson the model, so the endpoint's own default (8192) capped theresponse
Steps to reproduce
llm-pi-airoute as above and run a session on it.the output cap (here: 8192 tokens, the endpoint default when the request carries no
explicit
max_tokens).stopReason: "length"and the truncated text; the tool call thatwas being generated never materialises in the message content.
INVALID_REPLAY_STATE— and keeps failing on every later turn.Evidence
The stored assistant message (abridged) — the ledger has 2 blocks, the durable
content has 1:
{ "type": "assistant/message", "data": { "turn": 88, "message": { "role": "assistant", "content": [{ "type": "text", "text": "…" }], "source": { "kind": "model", "provider": "deepseek", "model": "deepseek-v4-flash", "replayState": { "kind": "pi-ai", "version": 1, "api": "openai-completions", "provider": "deepseek", "model": "deepseek-v4-flash", "responseModel": "deepseek-flash", "responseId": "87614cec-478d-4b27-a99e-2f81fe8edf6c", "stopReason": "length", "blocks": [{ "type": "text" }, { "type": "tool-call" }] } } }, "usage": { "inputTokens": 158, "outputTokens": 8192, "cacheReadTokens": 412032 } } }outputTokens: 8192equals the endpoint default cap, andstopReason: "length"confirmsthe cut. The second ledger entry (
tool-call) has no counterpart incontent.A sweep of all 30 sessions in
$DSH_HOME/sessionsfound exactly one affected session(105 pi-ai ledger messages scanned); the single mismatch above is the whole blast radius
in that library.
Root cause
The validation that fails is intentional and correct in itself:
packages/llm/llm-pi-ai/src/replay.ts:164The divergent write is the bug:
toPiReplayState()buildsblocksfrom the nativepi-ai assistant message as it completed, while the durable message persists only the
content blocks that survived the truncation. A block that was still forming when the cap
hit (a
toolCallhere) lands in the ledger but not in the content, so ledger and contentdisagree by exactly the trailing, unpersisted blocks.
Suggested fixes (any one removes the wedge)
replayState.blocksfrom the messagecontent that is actually being persisted, so the two can never disagree. Signatures can
be joined back by index/type.
state.blocks.length > message.content.lengthand
stopReasonislength/aborted, align by prefix instead of throwing. This alsolets already-broken logs recover without hand-editing, which matters because a fix to
the write path alone leaves every existing wedged session broken.
toolCallas a real content block(possibly marked incomplete) so both sides agree.
Impact
A wedged session fails every subsequent turn, so the user must abandon it or repair it
by hand. Because the trigger (hitting the output cap mid-tool-call) is reachable by
ordinary long work, the failure surfaces as an unexplained hard stop well after the
turn that actually caused it.
Workarounds (verified locally)
replayState(e.g.llm-deepseek). The session resumes immediately; the invalidledger simply goes unread. Switching back to the pi-ai route re-triggers the failure.
source.replayState.blocksto match its content (drop the trailing, unpersistedblock). Multi-frame zstd framing must be preserved (frame 1 = header line only).
maxTokenson the model so the requestcarries the cap instead of inheriting the endpoint default:
Notes
packages/llm/llm-pi-ai/tests/convert.spec.ts:473already asserts this error message,so the validation is by design; the uncovered path is how a ledger can diverge
(truncated/aborted streams), not the check itself.
way existing affected logs recover without manual surgery.
All reactions