#18679: fix(agents): always downgrade orphaned OpenAI reasoning blocks
agents
stale
size: XS
Cluster:
OpenAI Reasoning Enhancements
## Summary
- Removes the `modelChanged` gate from `downgradeOpenAIReasoningBlocks()` in `sanitizeSessionHistory()` so orphaned `rs_*` reasoning blocks are stripped unconditionally for OpenAI Responses API models
- Previously the downgrade only ran on model switches (`modelChanged === true`), leaving orphaned reasoning blocks in initial sessions (no prior snapshot) and same-model compaction cycles — causing OpenAI to return 400 errors
- Adds regression test for the null-snapshot scenario (the actual reported bug)
Fixes #17019
## Test plan
- [x] Existing test updated: same-model session now asserts downgrade happens (was: skipped)
- [x] Existing test renamed: model-change scenario still downgrades (unchanged behavior)
- [x] New test added: no prior snapshot (initial session) — orphaned blocks are stripped
- [x] All 9 sanitize-session-history tests pass
- [x] All 4 downgrade-openai-reasoning helper tests pass
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- greptile_comment -->
<h3>Greptile Summary</h3>
Removes the `modelChanged` gate from `downgradeOpenAIReasoningBlocks()` in `sanitizeSessionHistory()` so orphaned OpenAI `rs_*` reasoning blocks are stripped unconditionally for OpenAI Responses API models. Previously, the downgrade only ran when `modelChanged === true`, which meant orphaned reasoning blocks persisted in two scenarios: (1) initial sessions with no prior model snapshot, and (2) same-model compaction cycles — both causing OpenAI 400 errors.
- **Bug fix in `google.ts`**: The condition `isOpenAIResponsesApi && modelChanged` is simplified to just `isOpenAIResponsesApi`. The `downgradeOpenAIReasoningBlocks` function is safe to call unconditionally since it only drops orphaned reasoning blocks (those without following non-thinking content); valid blocks are preserved. The `modelChanged` variable remains in use for the `appendModelSnapshot` logic.
- **Test coverage**: Three scenarios are now covered — same-model (updated from asserting no-op to asserting downgrade), model-change (renamed for clarity, behavior unchanged), and no-prior-snapshot (new regression test for the reported bug).
<h3>Confidence Score: 5/5</h3>
- This PR is safe to merge — it's a minimal, well-targeted bug fix with comprehensive test coverage.
- The change is a 4-line simplification that removes an incorrect gate condition. The `downgradeOpenAIReasoningBlocks` function is idempotent and safe to call unconditionally (it only strips orphaned reasoning blocks). The `modelChanged` variable is not dead code — it's still used for snapshot persistence. All three relevant scenarios are tested. No risk of regression.
- No files require special attention.
<sub>Last reviewed commit: 7b7e558</sub>
<!-- greptile_other_comments_section -->
<!-- /greptile_comment -->
Most Similar PRs
#19407: fix(agents): strip thinking blocks on cross-provider model switch (...
by lailoo · 2026-02-17
85.1%
#16464: fix: harden OpenAI reasoning replay sanitization
by Swader · 2026-02-14
76.6%
#21847: fix(session): /new and /reset no longer carry over model overrides
by hydro13 · 2026-02-20
75.5%
#22503: docs: proposed fix for reasoning array error (#18480)
by Shuai-DaiDai · 2026-02-21
75.0%
#10097: fix: add empty thinking blocks to tool call messages when thinking is…
by cyxer000 · 2026-02-06
74.7%
#6673: fix: preserve allowAny flag in createModelSelectionState for custom...
by tenor0 · 2026-02-01
74.7%
#17914: Resolve merge conflicts in PR #13361
by SovranAMR · 2026-02-16
74.2%
#17455: fix: strip content before orphan closing think tags
by jwt625 · 2026-02-15
74.1%
#22064: fix(failover): bypass models allowlist for configured fallback models
by winston-bepresent · 2026-02-20
73.9%
#16930: fix: treat empty baseUrl as non-direct for OpenAI Responses
by OiPunk · 2026-02-15
73.9%