Release v0.51.295 — stage-3739/3742 (model-pick revert fix #3739 + session-status revert #3742) - #3743
Conversation
…y selection (#3737) When a user changes the model in the composer dropdown and sends, _resolve_compatible_session_model_state previously had no way to distinguish an explicit user pick from stale session state. The profile-aware branch (v0.51.290, PR #3448) and the legacy block both rewrote bare cross-family models to the profile default, and the client unconditionally applied effective_model — silently discarding the user's choice. Backend: accept explicit_model_pick flag (default False) on _resolve_compatible_session_model_state. Guard both the profile-aware branch (routes.py:2024) and the legacy block (routes.py:2124) to skip cross-provider normalization when set. _handle_chat_start extracts the flag and passes it through. Frontend: consult _readPendingSessionModel (sessionStorage, 10-min window) to detect explicit picks and include the flag. Add a toast as defense-in-depth when the server still returns effective_model. Closes #3737
Greptile P2-1: compare model_provider in pending pick detection, not just model name, to avoid false-positive flag when the session provider changes between pick and send. Greptile P2-2: only show the defense-in-depth toast when an explicit pick was actually overridden — stale-session normalizations are expected behavior and should be silent. Add two regression tests for the profile-branch guard: - explicit_model_pick=True → cross-family model survives - explicit_model_pick=False → existing normalization preserved
The manual per-session status labels (Todo / In Progress / Done) added in v0.51.284 (#3570) stored state only in browser localStorage keyed by session id, with no server-side backing — so labels did not persist across browsers or devices (a user who labeled sessions on one machine saw none after moving to a laptop). They also rendered as three flat top-level entries in the session context menu, crowding the root menu. Per maintainer decision, remove the feature entirely for now. It can be reintroduced later with proper server-side persistence and a less intrusive menu treatment. Removes: - JS state/cycle helpers + SESSION_MANUAL_STATUS_KEY (static/sessions.js) - context-menu status entries + sidebar status badge render - .session-manual-status* CSS (static/style.css) - session_status_* locale strings across all locales (static/i18n.js) Full suite: 8084 passed, 0 failed. ESLint runtime gate: clean. reverts #3570
…atch) Codex found the explicit_model_pick flag never engaged in the normal flow: boot.js modelSelect.onchange cleared the pending-pick marker right after /api/session/update, so by the time send() ran _readPendingSessionModel returned null, _explicitPick was false, and the server's profile-provider branch still reverted the cross-family pick (the exact #3737 bug). The flag only worked in the rare race where send beat the session-update round-trip. Fix (Codex prescription): do NOT clear the marker in onchange; clear it in send() immediately after reading a matching pending pick, so it's consumed for that send only. onchange still RECORDS the pick (_rememberPendingSessionModel) — only the premature clear is removed.
…consumes) Static source guards for the Codex clear-timing fix: onchange must record the pending pick and NOT clear it post-session-update; send() must consume (clear) it only after reading a matching _explicitPick, and send the flag only when truthy. Complements the author's resolver-level tests in test_provider_mismatch.py.
…ck clear The Codex clear-timing fix moved the pending-pick clear out of modelSelect.onchange into send() (consume-on-send). test_model_selection_records_pending_state_before_async_session_update asserted the OLD onchange-clears behavior (assert _clearPendingSessionModel in body). Updated to assert the NEW correct behavior (onchange must NOT clear it — it survives to send). The test's core refresh-survives invariant (marker recorded before the async session-update; reapplied on load) is unchanged and still passes; only the stale clear-location assertion is flipped. Not a regression-blessing: the refresh-survives feature is intact, the marker lifecycle is more correct.
|
| Filename | Overview |
|---|---|
| api/routes.py | Adds explicit_model_pick parameter to _resolve_compatible_session_model_state and wires it through _handle_chat_start; both the profile-aware branch (cross-family bare name) and the legacy bare-prefix branch now honor the flag — but slash-qualified cross-family models in the profile branch remain unguarded. |
| static/boot.js | Removes the premature _clearPendingSessionModel call from modelSelect.onchange; the marker now survives until send() consumes it, fixing the timing bug that was the root cause of #3737. |
| static/messages.js | send() now reads and consumes the pending explicit-pick marker, sends explicit_model_pick to the server, and shows a mismatch toast on revert; marker is cleared before the API call (single-use on failure) and the toast string is not yet i18n-wrapped. |
| static/sessions.js | Clean deletion of the manual-status localStorage state, cycle logic, context-menu entries, and sidebar badge render from #3570 — no logic concerns. |
| static/i18n.js | Removes the four session_status_* locale keys from all 12 locales; deletion is symmetric and complete across all locale blocks. |
| tests/test_issue3737_explicit_pick_client_wiring.py | New static source-structure tests guard the onchange-not-clear and send-consumes-marker invariants; assertions are precise and lock the exact fix from regressing silently. |
| tests/test_provider_mismatch.py | Two new resolver tests verify that explicit_model_pick=True preserves a cross-family bare model and explicit_model_pick=False still normalizes; clean and targeted. |
| tests/test_model_selection_refresh_persistence.py | Updated the existing assertion to now assert _clearPendingSessionModel is absent from onchange; correctly reflects the intent of the fix. |
| static/style.css | Removes the five session-manual-status CSS rules; clean deletion, no concerns. |
| CHANGELOG.md | Adds a Removed entry for the session-status revert; description is accurate and thorough. |
Sequence Diagram
sequenceDiagram
participant U as User
participant DS as modelSelect.onchange (boot.js)
participant SS as /api/session/update
participant SN as send() (messages.js)
participant CS as /api/chat/start
participant R as _resolve_compatible_session_model_state
U->>DS: picks cross-family model (e.g. gpt-5.5)
DS->>DS: _rememberPendingSessionModel(sid, model, provider)
DS->>SS: POST /api/session/update
Note over DS: marker NOT cleared here (fix for #3737)
U->>SN: submits message
SN->>SN: _readPendingSessionModel(sid)
SN->>SN: "_explicitPick = (pending matches current model)"
SN->>SN: _clearPendingSessionModel(sid) [consumed]
SN->>CS: "POST {model, explicit_model_pick: true}"
CS->>R: "_resolve_compatible_session_model_state(..., explicit_model_pick=True)"
alt profile-aware branch — cross-family bare name
R-->>CS: (model, profile_provider, False) honored
else legacy bare-prefix branch
R-->>CS: (model, requested_provider, False) honored
end
CS-->>SN: "{effective_model, ...}"
alt "effective_model != sentModel"
SN->>U: showToast(model changed — profile provider mismatch)
end
Comments Outside Diff (1)
-
api/routes.py, line 2044-2046 (link)Slash-qualified cross-family model not guarded by explicit_model_pick
The
explicit_model_pickguard at line 2023 only fires when the model name starts with a bare prefix (gpt,claude,gemini) andmodel_familyis detected. For a slash-qualified model likeopenai/gpt-5.4-minipicked explicitly under an anthropic profile,model_familyresolves to""(the bare-prefix loop doesn't fire since the string starts with"openai/"), so execution falls through to this block and the model is still reverted to the profile default — the same silent revert bug bug(models): picked model reverts to previous/default model on send (likely #3448 profile-aware resolution, v0.51.290) #3737 fixed for bare names. Slash-qualified cross-provider sessions would remain broken after this PR.
Reviews (1): Last reviewed commit: "test(#3737): realign refresh-persistence..." | Re-trigger Greptile
| if(startData.effective_model && S.session){ | ||
| const _sentModel=_modelState.model; | ||
| if(_explicitPick && _sentModel && startData.effective_model!==_sentModel && typeof showToast==='function'){ | ||
| showToast('Model '+_sentModel+' changed to '+startData.effective_model+' — profile provider mismatch', 5000); |
There was a problem hiding this comment.
The mismatch toast message is a raw English string with string concatenation and is not wrapped with the
t() i18n helper. The PR notes this as a known follow-up, but worth tracking as a concrete anchor. The t('model_mismatch_toast') key would also need to be added to i18n.js for all locales.
| showToast('Model '+_sentModel+' changed to '+startData.effective_model+' — profile provider mismatch', 5000); | |
| showToast((t('model_mismatch_toast')||'Model {0} changed to {1} — profile provider mismatch').replace('{0}',_sentModel).replace('{1}',startData.effective_model), 5000); |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| // session-update) so it survives the normal pick→update→send flow; clear it here | ||
| // once read so a later send of an unchanged dropdown isn't treated as an explicit | ||
| // pick. (#3739/#3737, Codex catch) | ||
| if(_explicitPick && typeof _clearPendingSessionModel==='function') _clearPendingSessionModel(activeSid); |
There was a problem hiding this comment.
Marker consumed before API call — explicit pick is single-use on failure
_clearPendingSessionModel is called before await api('/api/chat/start', …), so if the network request fails or the server returns an error, the pending-pick marker is already gone. A retry send() would compute _explicitPick = false and the server would silently revert the cross-family model back to the profile default. The PR notes this as an intentional "single-use per send" trade-off with a future server-persist path, but it could confuse users who retry after a transient failure.
+ session-status revert nesquena#3742) (nesquena#3743) * fix: honor explicit model pick, suppress silent revert on cross-family selection (nesquena#3737) When a user changes the model in the composer dropdown and sends, _resolve_compatible_session_model_state previously had no way to distinguish an explicit user pick from stale session state. The profile-aware branch (v0.51.290, PR nesquena#3448) and the legacy block both rewrote bare cross-family models to the profile default, and the client unconditionally applied effective_model — silently discarding the user's choice. Backend: accept explicit_model_pick flag (default False) on _resolve_compatible_session_model_state. Guard both the profile-aware branch (routes.py:2024) and the legacy block (routes.py:2124) to skip cross-provider normalization when set. _handle_chat_start extracts the flag and passes it through. Frontend: consult _readPendingSessionModel (sessionStorage, 10-min window) to detect explicit picks and include the flag. Add a toast as defense-in-depth when the server still returns effective_model. Closes nesquena#3737 * fix: tighten explicit-pick detection and add regression tests (nesquena#3737) Greptile P2-1: compare model_provider in pending pick detection, not just model name, to avoid false-positive flag when the session provider changes between pick and send. Greptile P2-2: only show the defense-in-depth toast when an explicit pick was actually overridden — stale-session normalizations are expected behavior and should be silent. Add two regression tests for the profile-branch guard: - explicit_model_pick=True → cross-family model survives - explicit_model_pick=False → existing normalization preserved * revert(sidebar): remove manual session status labels (nesquena#3570) The manual per-session status labels (Todo / In Progress / Done) added in v0.51.284 (nesquena#3570) stored state only in browser localStorage keyed by session id, with no server-side backing — so labels did not persist across browsers or devices (a user who labeled sessions on one machine saw none after moving to a laptop). They also rendered as three flat top-level entries in the session context menu, crowding the root menu. Per maintainer decision, remove the feature entirely for now. It can be reintroduced later with proper server-side persistence and a less intrusive menu treatment. Removes: - JS state/cycle helpers + SESSION_MANUAL_STATUS_KEY (static/sessions.js) - context-menu status entries + sidebar status badge render - .session-manual-status* CSS (static/style.css) - session_status_* locale strings across all locales (static/i18n.js) Full suite: 8084 passed, 0 failed. ESLint runtime gate: clean. reverts nesquena#3570 * fix(nesquena#3737): keep explicit-pick marker until send consumes it (Codex catch) Codex found the explicit_model_pick flag never engaged in the normal flow: boot.js modelSelect.onchange cleared the pending-pick marker right after /api/session/update, so by the time send() ran _readPendingSessionModel returned null, _explicitPick was false, and the server's profile-provider branch still reverted the cross-family pick (the exact nesquena#3737 bug). The flag only worked in the rare race where send beat the session-update round-trip. Fix (Codex prescription): do NOT clear the marker in onchange; clear it in send() immediately after reading a matching pending pick, so it's consumed for that send only. onchange still RECORDS the pick (_rememberPendingSessionModel) — only the premature clear is removed. * test(nesquena#3737): lock client clear-timing wiring (onchange records, send consumes) Static source guards for the Codex clear-timing fix: onchange must record the pending pick and NOT clear it post-session-update; send() must consume (clear) it only after reading a matching _explicitPick, and send the flag only when truthy. Complements the author's resolver-level tests in test_provider_mismatch.py. * test(nesquena#3737): realign refresh-persistence test to the moved pending-pick clear The Codex clear-timing fix moved the pending-pick clear out of modelSelect.onchange into send() (consume-on-send). test_model_selection_records_pending_state_before_async_session_update asserted the OLD onchange-clears behavior (assert _clearPendingSessionModel in body). Updated to assert the NEW correct behavior (onchange must NOT clear it — it survives to send). The test's core refresh-survives invariant (marker recorded before the async session-update; reapplied on load) is unchanged and still passes; only the stale clear-location assertion is flipped. Not a regression-blessing: the refresh-survives feature is intact, the marker lifecycle is more correct. --------- Co-authored-by: John Doe <johndoe@example.com> Co-authored-by: nesquena-hermes <[email protected]>
Release stage for v0.51.295 — two small independent fixes batched (no file overlap).
#3739 → fixes #3737 — model picker silently reverts explicit cross-family pick
v0.51.290 regression (#3448 profile-aware resolution). @someaka. Honors an explicit pick across BOTH resolver branches + a toast on genuine mismatch.
Deep-review notes (this stage):
explicit_model_pickflag never engaged in the normal flow (boot.js onchange cleared the pending-pick marker before send() could read it). Fixed: marker is recorded in onchange, kept, and consumed once in send().gpt-5.5under an anthropic profile; explicit=False still repairs to default.#3742 → reverts #3570 — manual per-session status labels
Maintainer-authored, pure deletion (+4/−118). localStorage-only (no cross-device persistence, read as a bug) + crowded the root context menu.
Gate (all green)
Tests added:
test_issue3737_explicit_pick_client_wiring.py(4), author's resolver tests intest_provider_mismatch.py(2), refresh-persistence test realigned.Known non-blocking follow-ups: mismatch toast string not yet i18n-wrapped; explicit-pick is single-use per send (net improvement, server-persist is a future enhancement). Closes #3737. Reverts #3570.