Skip to content

fix(responses-continuation): fail closed on a collector-truncated, empty output array - #12460

Merged
diegosouzapw merged 2 commits into
cheaper-inference:release/v3.8.51from
hartmark:fix-continuation-store-truncated-empty-output
Sep 3, 2026
Merged

diegosouzapw merged 2 commits into
cheaper-inference:release/v3.8.51from
hartmark:fix-continuation-store-truncated-empty-output

Conversation

@hartmark

@hartmark hartmark commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

What Problem This Solves

Ping's conversations kept breaking mid-turn with the upstream provider rejecting the request outright: [400]: Input required: specify "prompt" or "messages". Traced two example failures directly (both traced back to the same root request via a shared correlation_id): #12448 continuation store correctly resolved a previous_response_id, but the response it resolved from had a genuinely empty stored output array.

Why This Change Was Made

A huge/reasoning-heavy response can blow past createStructuredSSECollector's own event-count cap mid-stream. The stored clientResponse then carries _truncated: true and summary.status: "in_progress" (never reached "completed") with an empty summary.output — not a bounded array with the existing _omniroute_truncated_array sentinel (that only covers an array capped mid-array, not a collector that stopped before populating output at all).

resolvePreviousResponseState's existing checks didn't cover this shape — an empty array isn't a truncated-marker array, so it passed through as a valid (if empty) prior response, and the next turn reconstructed to zero real messages.

Measured against a live sample of 40 recent successful (200) responses: 9 of them (22.5%) carried _truncated: true. Not a rare edge case.

Fix: fail closed (return null, same as every other unresolvable case in this function) when clientResponse._truncated === true, or when the resolved output array is empty regardless of the flag. This makes the caller ask the client to resend full history — exactly like a real previous_response_not_found from OpenAI, which OpenClaw's client already handles gracefully.

User Impact

Stops a real, frequent (~1 in 4 sampled) cause of mid-conversation hard failures for continuation-mode clients.

Evidence

  • Root-caused against two real production log artifacts (linked/traced by the reporting user), confirmed _truncated: true / empty output directly in the stored artifact.
  • Two regression tests added to tests/unit/responses-continuation-store.test.ts (existing pattern), both proven RED against unfixed code and GREEN after.
  • Full responses-continuation-*.test.ts suite: 15/15 passing, no regressions.

🤖 Generated with Claude Code

…pty output array

Live incident (2026-09-02): a huge/reasoning-heavy response blew past
createStructuredSSECollector's own event-count cap mid-stream. The stored
clientResponse then carries `_truncated: true` and `summary.status:
"in_progress"` (never reached "completed") with a genuinely EMPTY
`summary.output` -- not a bounded array with the existing
`_omniroute_truncated_array` sentinel (that only covers an array capped
mid-array, not a collector that stopped before ever populating output at
all).

resolvePreviousResponseState's existing checks didn't cover this shape: an
empty array is not a truncated-marker array, so it passed through as a
valid (if empty) prior response. The next turn's request then reconstructed
to zero real messages, which the upstream provider rejected outright
("[400]: Input required: specify \"prompt\" or \"messages\""), breaking the
conversation with no client-visible continuation path -- OpenClaw saw a
hard request failure with no way to recover mid-turn.

Measured against a live sample of 40 recent successful (200) Ping
responses: 9 of them (22.5%) carried `_truncated: true`. Not a rare edge
case.

Fix: fail closed (return null, same as every other unresolvable case here)
when the stored clientResponse carries `_truncated: true`, or when the
resolved output array is empty regardless of the flag (belt-and-suspenders
for a collector that truncated without setting it, or any other path that
somehow logs zero output items for a response the client actually
received as real). A response the client received as real always has at
least one output item. Failing closed makes the caller ask the client to
resend full history instead of silently reconstructing an empty one --
exactly like a real previous_response_not_found from OpenAI itself, which
OpenClaw's client already handles gracefully.

Two regression tests added to tests/unit/responses-continuation-store.test.ts,
following the file's existing pattern; both proven RED against unfixed code
(returned the empty-output object instead of null) and GREEN after. Full
responses-continuation-*.test.ts suite: 15/15 passing, no regressions.
isGenuineContinuationTurn (and its parent-link helpers) have no
callers in this branch — they belong to cheaper-inference#12448. Leaving them here
trips the new-code dead-code gate. Keep only the fail-closed
_truncated / empty-output checks this PR is about.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
@diegosouzapw
diegosouzapw merged commit 0019a47 into cheaper-inference:release/v3.8.51 Sep 3, 2026
15 of 16 checks passed
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…pty output array (cheaper-inference#12460)

Validado em lote numa worktree combinada com os 9 PRs desta leva sobre o tip de `release/v3.8.51` (já com a leva anterior dentro): os nove boardaram **sem um único conflito**, `typecheck:core` limpo e **80/80** nos 6 arquivos de teste que os PRs trazem.

O crescimento de arquivo próprio da leva foi rebaselinado num registro datado (`_rebaseline_2026_09_03_hartmark_batch`): `combos/page.tsx` 5012→5018 (cheaper-inference#12355, tratar o estado degradado quando o bundling de tiktoken de um provider sem relação falha) e `open-sse/services/combo.ts` 4023→4036 (cheaper-inference#12338, os fixes do universal-handoff). As violações restantes (`codex.ts`, `stream.ts`) foram medidas também no tip puro e são drift da base, não desta leva.

Obrigado, @hartmark.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants