Skip to content

fix(core): preserve reasoning_content in rewind, compression, and merge paths (#3579)#3737

Merged
tanzhenxin merged 3 commits into
QwenLM:mainfrom
fyc09:fix/issue-3579-missed-paths
Apr 30, 2026
Merged

fix(core): preserve reasoning_content in rewind, compression, and merge paths (#3579)#3737
tanzhenxin merged 3 commits into
QwenLM:mainfrom
fyc09:fix/issue-3579-missed-paths

Conversation

@fyc09

@fyc09 fyc09 commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR fixes three remaining paths where reasoning_content could be silently dropped during DeepSeek thinking mode sessions, completing the set of fixes for #3579.

After #3590 (session resume / idle) and #3682 (model switch / load_history), these paths were still missing:

  • Rewind (AppContainer.tsx): /rewind and double-ESC called stripThoughtsFromHistory() after truncating history
  • Chat compression (chatCompressionService.ts): The compressed summary turn was created without a thought part, breaking DeepSeek's required reasoning chain continuity
  • Message merge (converter.ts): mergeConsecutiveAssistantMessages discarded reasoning_content when merging adjacent assistant messages

Second commit removes the now-unused stripThoughtsFromHistory() and stripThoughtsFromHistoryKeepRecent() methods from GeminiClient and GeminiChat, along with their test blocks and stale mock entries.

This aligns with the maintainer decision: remove all strip-on-event sites.

Changes

packages/cli/src/ui/AppContainer.tsx

  • Remove stripThoughtsFromHistory() call in the rewind handler
  • Replace "strip stale thinking blocks" comment with explanation of why thought parts must be preserved

packages/core/src/services/chatCompressionService.ts

  • Check if any model turn in the preserved history has a thought part
  • If so, append a { text: ' ', thought: true } part to the compressed summary turn
  • This maintains reasoning chain continuity for providers like DeepSeek

packages/core/src/core/openaiContentGenerator/converter.ts

  • When merging consecutive assistant messages, combine their reasoning_content fields
  • Handles cases where one or both messages have reasoning_content (string or null)

packages/core/src/core/client.ts

  • Remove stripThoughtsFromHistory() wrapper method (dead code)

packages/core/src/core/geminiChat.ts

  • Remove stripThoughtsFromHistory() method (dead code)
  • Remove stripThoughtsFromHistoryKeepRecent() method (dead code)

Testing

  • npm run build — passes
  • npm run typecheck — passes
  • All core tests pass (6219 passed, 11 failed — pre-existing Windows symlink EPERM errors)

Closes #3579

@fyc09 fyc09 marked this pull request as ready for review April 29, 2026 10:41
Bojun-Vvibe added a commit to Bojun-Vvibe/oss-contributions that referenced this pull request Apr 29, 2026
…(5 PRs)

- BerriAI/litellm#26764 merge-after-nits: multipart user_config/tags JSON decode
- BerriAI/litellm#26769 request-changes: FuturMix add bundles unrelated deletions
- google-gemini/gemini-cli#26184 merge-after-nits: deleteSession fail-loud on missing file
- QwenLM/qwen-code#3737 merge-after-nits: drop strip-thoughts helpers, preserve reasoning_content
- aaif-goose/goose#8781 needs-discussion: ACP per-connection inline→spawn ordering question
@tanzhenxin tanzhenxin added the type/bug Something isn't working as expected label Apr 29, 2026

@tanzhenxin tanzhenxin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review

Thanks for the careful follow-up on #3579 — really appreciate the effort here.

A bit of context on how we fixed the recent DeepSeek HTTP 400s: DeepSeek can legitimately return an assistant tool-call turn with empty reasoning_content. Our pipeline filters that empty thought out when storing the turn, so when we rebuild the next request the replayed assistant turn has reasoning_content absent — which DeepSeek then rejects, since thinking-mode requires the field on every prior tool-call turn. #3729 addresses this at the canonical layer: ensureReasoningContentOnToolCalls in provider/deepseek.ts runs on every outgoing assistant message and injects reasoning_content: '' when a turn carries tool_calls but the field is missing. That's scoped to the only shape DeepSeek actually rejects.

This PR is a valuable enhancement on top of that — the rewind handler change and the dead-method cleanup look good and we're happy to take those. But two of the changes don't apply given the canonical fix, and we'd prefer to revert them:

  1. chatCompressionService.ts — the compressed ack is plain text with no tool_calls, so DeepSeek's constraint doesn't apply. The { text: ' ', thought: true } injection is defensive but unnecessary, and the inline comment overstates the requirement.
  2. mergeConsecutiveAssistantMessages in converter.ts — even if a merged turn ends up with tool_calls, #3729's normalizer fixes it before sending. Duplicating reasoning-merge logic in the converter is redundant and risks drift if the canonical contract changes. If a concrete failure case shows up that isn't covered by ensureReasoningContentOnToolCalls, we'd rather extend that one place.

Verdict

REQUEST_CHANGES — once those two are reverted, we're ready to merge. Thanks again!

…nLM#3737)

Per tanzhenxin's review: the compressed ack is plain text without
tool_calls so the thought-part injection is unnecessary, and the
converter reasoning merge is redundant given QwenLM#3729's canonical
ensureReasoningContentOnToolCalls in the deepseek provider.

Both paths are now handled at the request boundary, not in history
transformation.

@tanzhenxin tanzhenxin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review

Thanks for the round-2 changes — reverting the chatCompressionService.ts and converter.ts modifications was the right call. The maintainer's #3729 normalizer (ensureReasoningContentOnToolCalls) handles compression and replay at the request boundary, so an overlapping fix here would have just caused merge friction. The PR is now appropriately narrow: one behavioral line in the rewind handler plus symmetric dead-code removal.

Verdict

APPROVE — production code is correct, scope reduction is right, dead-code removal is symmetric.

@tanzhenxin tanzhenxin merged commit 2f1b52d into QwenLM:main Apr 30, 2026
12 of 13 checks passed
xaelistic pushed a commit to xaelistic/qwen-code that referenced this pull request Jun 7, 2026
…ge paths (QwenLM#3579) (QwenLM#3737)

* fix(core): preserve reasoning_content in rewind, compression, and merge paths (QwenLM#3579)

* chore(core): remove dead stripThoughtsFromHistory methods (QwenLM#3579)

* revert(pr): remove redundant reasoning merge per review feedback (QwenLM#3737)

Per tanzhenxin's review: the compressed ack is plain text without
tool_calls so the thought-part injection is unnecessary, and the
converter reasoning merge is redundant given QwenLM#3729's canonical
ensureReasoningContentOnToolCalls in the deepseek provider.

Both paths are now handled at the request boundary, not in history
transformation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type/bug Something isn't working as expected

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG: DeepSeek API 400 error — reasoning_content in thinking mode must be passed back

2 participants