fix(toolsets): only forward staged toolsets from empty composer + clear on session load (#4490 Codex follow-up)

Gate-found: staged _pendingSessionToolsets could leak into a later New Chat
started from an existing session (newSession forwarded it whenever it was an
array; loadSession never cleared it). Gate the forward on !S.session and clear
the staged value when a real session loads. +2 regression tests.

Co-authored-by: claw-io <claw-io@users.noreply.github.com>
This commit is contained in:
nesquena-hermes
2026-06-19 18:24:45 +00:00
parent fce927d5ac
commit 4e8e6714d5
3 changed files with 35 additions and 2 deletions
+1 -1
View File
@@ -7,7 +7,7 @@
### Fixed
- **The composer Tools chip can now stage per-session toolsets before the first conversation exists (#4490).** Applying a custom toolset from the empty composer stores it for the next new session instead of silently no-oping (the documented per-session override #493 was unreachable on a brand-new session), `/api/session/new` now accepts the staged `enabled_toolsets` value with the same structural validation as `/api/session/toolsets`, and staged values are cleared on workspace/profile context switches and when a real session loads so they cannot leak into an unrelated later New Chat. Thanks @claw-io.
- **The composer Tools chip can now stage per-session toolsets before the first conversation exists (#4490).** Applying a custom toolset from the empty composer stores it for the next new session instead of silently no-oping (the documented per-session override #493 was unreachable on a brand-new session), `/api/session/new` now accepts the staged `enabled_toolsets` value with the same structural validation as `/api/session/toolsets`, and staged values are cleared on workspace/profile context switches and when a real session loads so they cannot leak into an unrelated later New Chat. Thanks @franksong2702.
## [v0.51.515] — 2026-06-19 — Release RZ (remote-gateway approvals work again + conversation history threaded)
+8 -1
View File
@@ -692,7 +692,10 @@ async function newSession(flash, options={}){
if(S.session&&S.session.session_id) reqBody.prev_session_id=S.session.session_id;
if(options&&options.worktree) reqBody.worktree=true;
if(_activeProject&&_activeProject!==NO_PROJECT_FILTER) reqBody.project_id=_activeProject;
if(Array.isArray(S._pendingSessionToolsets)) reqBody.enabled_toolsets=S._pendingSessionToolsets;
// Only forward a pre-session toolset override staged on the empty composer
// (no active session). Once a real session is loaded, an abandoned staged
// value must not silently leak into a later New Chat (#4490 follow-up).
if(!S.session && Array.isArray(S._pendingSessionToolsets)) reqBody.enabled_toolsets=S._pendingSessionToolsets;
// Carry the visible picker selection into the new session. Without this,
// /api/session/new falls back to config.yaml defaults (e.g. gpt-5.5) even
// when the user already chose cursor/composer-2.5 in the composer chip.
@@ -1069,6 +1072,10 @@ async function loadSession(sid){
return;
}
S.session=data.session;
// Loading a real existing session abandons any pre-session toolset override
// staged on the empty composer — clear it so it can't leak into a later New
// Chat started from this session (#4490 follow-up).
S._pendingSessionToolsets=null;
if(typeof _hydrateTodosFromSession==='function') _hydrateTodosFromSession(S.session);
S.session._modelResolutionDeferred=true;
S.lastUsage={...(data.session.last_usage||{})};
@@ -97,6 +97,32 @@ def test_new_session_request_consumes_pending_toolsets_once():
assert "S._pendingSessionToolsets=null" in after_assignment
def test_pending_toolsets_only_forwarded_from_empty_composer():
"""The staged override must only be forwarded when there is no active session.
Without the `!S.session` guard, a toolset staged on the empty composer would
leak into a later New Chat started from an existing session (#4490 follow-up).
"""
compact = SESSIONS_JS.replace(" ", "")
post_start = compact.index("api('/api/session/new'")
before_post = compact[:post_start]
# The forwarding line must be gated on the no-session (empty composer) state.
assert "!S.session&&Array.isArray(S._pendingSessionToolsets)" in before_post
def test_load_existing_session_clears_staged_toolsets():
"""Loading a real existing session must clear any abandoned staged override.
loadSession() assigns S.session=data.session on the success path; the staged
value is cleared there so a subsequent New Chat does not inherit it (#4490).
"""
body = _function_body(SESSIONS_JS, "async function loadSession")
compact = body.replace(" ", "")
assign = compact.index("S.session=data.session")
# The clear must accompany the real-session assignment, not only the create path.
assert "S._pendingSessionToolsets=null" in compact[assign : assign + 400]
def test_workspace_and_profile_switches_clear_pending_toolsets():
for marker in (
"function promptWorkspacePath",