Skip to content

fix(core): omit truncated reasoning-only history on recovery - #61439

Open
HaiYangBG1 wants to merge 3 commits into
anomalyco:devfrom
HaiYangBG1:reasoning-recovery
Open

HaiYangBG1 wants to merge 3 commits into
anomalyco:devfrom
HaiYangBG1:reasoning-recovery

Conversation

@HaiYangBG1

@HaiYangBG1 HaiYangBG1 commented Nov 3, 2026 •

Copy link
Copy Markdown

Issue for this PR

Fixes #60292

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

After a reasoning-only response hits the output limit, the recovery request can replay an assistant message with empty content. Strict OpenAI-compatible providers reject it with 400.

Omit that message from model history only when its finish reason is length and all meaningful parts are reasoning. Keep the original reasoning in stored history and send the existing recovery prompt. Responses containing text or tool calls, and normally completed reasoning, keep their existing handling.

This is separate from #56942's empty-content guard: nonempty reasoning survives that guard. Porting its production filtering logic to the same current baseline still reproduces the 400; this comparison does not claim to test the whole older PR branch.

How did you verify your code works?

macOS ARM64, Bun 1.4.4, frozen install; base ce8ca3084eac.

  • The issue's local HTTP fixture, adapted to run the source CLI in standalone mode, returns 400 / exit 1 before this change and succeeds / exits 0 after it. No real model calls.
  • The updated runner assertion fails before the fix. Runner/message suites: 273 passed. Text and reasoning output-limit cases repeated ten times each under full-suite load: 20 passed. The test also checks that reasoning remains in stored history.
  • bun run check: lint has no errors (warnings remain), all 35 typecheck tasks passed. git diff --check passed.
  • Full Core: 7,185 passed / 35 skipped / 4 failed across 311 files. The full suite did not pass.

Failure details:

Failed case Comparison
ShellTool conditional process substitution > conditional substitutions respect once Also fails with this patch removed.
ShellTool conditional process substitution > conditional substitutions respect always Also fails with this patch removed.
real-shell compound syntax: Bash function containing case inside command substitution (test/shell-scan/compound-parity.test.ts:125) Also fails with this patch removed.
RepositoryCache > persists the daily throttle across cache recreation and serializes competing refreshes Timed out at 30s in the full run. Passes on isolated reruns both without and with the patch; cause remains undetermined.

The three affected files on the clean same-SHA baseline produced 643 passed / 11 skipped / 3 failed, with the same three Shell/Bash failures. After restoring the patch, the cache case and two recovery cases passed 3/3. No assertions, skips or timeouts in the failing tests were changed. Reruns do not replace the full-run result.

To run the focused suite: cd packages/core && bun run script/test.ts test/session-runner.test.ts test/session-runner-message.test.ts.

Linux, Windows, real Kimi service behavior and upstream CI have not been verified locally.

Visual

<svg xmlns="http://www.w3.org/2000/svg" width="240" height="72" viewBox="0 0 240 72" fill="none">
  <style>@keyframes ready{0%,100%{opacity:1}50%{opacity:.35}}.ready{animation:ready 2.4s ease-in-out infinite}@media(prefers-reduced-motion:reduce){.ready{animation:none}}</style>
  <g stroke="currentColor" stroke-width="2">
    <rect x="8" y="14" width="76" height="44" rx="7" opacity=".5"/>
    <path d="M22 27h48M22 36h35M22 45h24" opacity=".6"/>
    <path d="M96 36h36m-8-8 8 8-8 8"/>
    <g class="ready"><rect x="146" y="14" width="86" height="44" rx="7"/><path d="m162 36 8 8 16-18M196 29h22M196 38h16"/></g>
  </g>
</svg>

Screenshots / recordings

No UI change. The before/after check uses the real CLI against a local simulated provider.

Checklist

  • I have tested my changes locally within the scope above.
  • I have not included unrelated changes in this PR.

Added by the opencode bot (AI-generated).

Related: #46881

Upstream sync on 2026-11-05

Head 203be51d5ec52ab4e5eb7fb59fa8883f9181178b merges dev at 8a12d6b4207a2bef758a20fa922a189fe9dea78d and preserves the PR's existing behavior. The session runner and message conversion suites passed 274 tests. On macOS ARM64 with Bun 1.4.4 and frozen dependencies, bun run check passed lint with no errors and all 35 typecheck tasks. The normal pre-push hook passed, and the fork branch SHA was read back after the fast-forward push.

Earlier runtime and visual evidence above remains tied to its stated commits. These local results do not establish hosted CI approval or release validation.

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix works. With the #60292 reproduction (a fake strict OpenAI-compatible server, opencode run --standalone), the base sends assistant(content="", reasoning_content=…) in the recovery request, gets a 400 and exits with code 1. With this change, the recovery request is [system, user, user(nudge)] and the run exits with code 0. The runner and message suites pass (273/273).

The check is narrow: only turns that stopped at the output limit and contain nothing but reasoning are left out. Their tool results are still returned, and the turn stays in stored history, so the repeated-length guard in llm.ts is unaffected. Anthropic, Bedrock and Gemini allow dropping a thinking block from a turn without tool use, so signed reasoning replay doesn't break.

Public API: unchanged.

No blocking concerns.

Related: #46881

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new commit only merges dev; the change itself is the same as before. I re-ran the #60292 reproduction on the new base: the base still gets the 400 and exits with code 1, and with this PR the recovery request contains no empty assistant message and the run exits with code 0. The runner and message suites pass (274/274), and tsc -b is clean.

Public API: unchanged.

No new concerns.

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new commit only merges dev; the change itself is unchanged. I re-ran the #60292 reproduction on the new base: the base still gets the 400 and exits with code 1, and with this PR the recovery request contains no empty assistant message and the run exits with code 0. The runner and message suites pass (274/274), and tsc -b is clean.

Public API: unchanged.

No new concerns.

This branch has not been deployed

No deployments
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.x: recovery after a reasoning-only turn that stops with length replays an empty assistant message (400 "must not be empty" on strict providers)

1 participant