## Release v0.51.254 — Release HV (stage-r2)
Phase-2 medium wave 1 — 4 PRs (UI/mobile/cancel fixes + an un-held model dedup).
### Fixed
| Issue/PR | Author | Fix |
|----------|--------|-----|
| #3528 | @franksong2702 | Render partial tool calls after cancel — interrupted turns keep their `_partial_tool_calls` rows in the transcript + fallback tool-cards. (Codex confirmed it stays render-only, not forwarded to the provider API.) |
| #3550 | @lurebat | Android offline recovery soft-reattaches the live stream instead of hard-reloading the page on a transient background/disconnect. |
| #3479 | @mvanhorn | iOS Safari no longer snaps the conversation to the top when a handoff/compression card is inserted mid-stream or on `refreshSession()`. |
| #3478 | @JayC-L | **Un-held:** named custom providers (`@custom:name:model`) dedup against bare model IDs without regressing Ollama multi-colon tags (`qwen2.5:7b-instruct-q4`). Only `@custom:` IDs strip the two-segment prefix. |
### Hold-sweep note
#3478/#3489 was held yesterday for an Ollama multi-colon-tag regression risk (a blanket `lastIndexOf` would lose the model). The author pushed a scoped fix (only `@custom:` IDs use `lastIndexOf`); I verified `_normId` in node against the regression cases — Ollama bare tags are preserved. Un-held + shipped.
### Gate
- Full pytest suite: **7588 passed, 0 failed**
- ESLint: CLEAN · ruff: CLEAN · browser-smoke: CLEAN
- Codex (regression): **SAFE TO SHIP** — #3552 partial-tool-calls verified render-only (no `_API_SAFE_MSG_KEYS` leak / no 400-on-strict-provider, the v0.50.251 #1375 trap); #3551 no EventSource double-subscribe; #3541 no regression vs the #3525 scroll-follow shipped in v0.51.253; #3489 no over-dedup.
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
Co-authored-by: lurebat <lurebat@users.noreply.github.com>
Co-authored-by: mvanhorn <mvanhorn@users.noreply.github.com>
Co-authored-by: JayC-L <JayC-L@users.noreply.github.com>
Fixes introduced when absorbing PR #959 (fast conversation switching):
- _accepts_gzip() now uses getattr() to tolerate _FakeHandler and any
synthesised handler that lacks a .headers attribute (fixes 2 test failures
in test_sprint46.py)
- test_issue401: updated assertion to accept both minified and reformatted
forms of the tool_calls fallback guard (PR reformatted the code)
- test_regressions: updated activeStreamId assertion — PR refactored
data.session references to S.session for direct state access
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
* fix: correct tool call card rendering on session load
Two bugs caused duplicate/incorrect tool call cards when loading
sessions (especially after context compaction):
1. loadSession() sanitized messages (B9 filter) but did NOT update
the session-level tool_calls array's assistant_msg_idx references.
Since compact() returns only sanitized messages and recomputes
tool_calls with indices into the compacted array, the original
assistant_msg_idx values became stale/misaligned.
2. loadSession() then assigned the broken session-level tool_calls
directly to S.toolCalls. This prevented renderMessages()'s fallback
path (which derives tool_calls from per-message tool_calls using
correct sanitized-array indices) from ever running.
Fix:
- Keep full sanitization loop with index remapping for session-level
tool_calls (in case they're needed by other code paths).
- Instead of assigning broken session-level tool_calls to S.toolCalls,
set S.toolCalls=[] so renderMessages() uses the fallback derivation
from per-message tool_calls, which already have correct indices.
* test: add 8 regression tests for issue #401 tool call index remapping
* docs: v0.50.29 release — version badge and CHANGELOG
---------
Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>