From 4e8e6714d56a27daedd6f8060cafe751d6cf82a3 Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Fri, 19 Jun 2026 18:24:45 +0000 Subject: [PATCH] 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 --- CHANGELOG.md | 2 +- static/sessions.js | 9 ++++++- tests/test_issue4490_presession_toolsets.py | 26 +++++++++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b26784e9..82c9e68c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/static/sessions.js b/static/sessions.js index de5cf5457..3e867f029 100644 --- a/static/sessions.js +++ b/static/sessions.js @@ -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||{})}; diff --git a/tests/test_issue4490_presession_toolsets.py b/tests/test_issue4490_presession_toolsets.py index 99fb1dacd..b88b5875c 100644 --- a/tests/test_issue4490_presession_toolsets.py +++ b/tests/test_issue4490_presession_toolsets.py @@ -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",