From 3f3136ad5a9a9bca87f2a44499ea85a8ff2731d7 Mon Sep 17 00:00:00 2001 From: Shakker Date: Sun, 2 Aug 2026 20:26:32 +0100 Subject: [PATCH] fix: scope global model results --- ui/src/pages/chat/chat-commands.ts | 18 ++++++++++++++- ui/src/pages/chat/chat-send.test.ts | 36 +++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/ui/src/pages/chat/chat-commands.ts b/ui/src/pages/chat/chat-commands.ts index b8bc4467dd8e..624bb524072f 100644 --- a/ui/src/pages/chat/chat-commands.ts +++ b/ui/src/pages/chat/chat-commands.ts @@ -20,6 +20,7 @@ import { } from "../../lib/sessions/index.ts"; import { areUiSessionKeysEquivalent, + isUiGlobalSessionKey, resolveUiDefaultAgentId, type UiSessionDefaultsHost, } from "../../lib/sessions/session-key.ts"; @@ -137,6 +138,21 @@ function isChatCommandTargetCurrent(host: ChatCommandHost, target: ChatCommandTa ); } +function isChatCommandModelCacheOwnerCurrent( + host: ChatCommandHost, + target: ChatCommandTarget, +): boolean { + if (!isChatCommandConnectionCurrent(host, target)) { + return false; + } + // The selected-agent global session shares one UI cache key across agents. + // Keep delayed results out when that selection changes on the same Gateway. + return ( + !isUiGlobalSessionKey(target.sessionKey) || + scopedAgentIdForSession(host, target.sessionKey) === target.agentId + ); +} + export function readChatResetTargetAccess( host: ChatCommandHost, target: ChatCommandTarget, @@ -461,7 +477,7 @@ export async function dispatchChatSlashCommand( if (result.sessionPatch && "modelOverride" in result.sessionPatch) { // A route switch on the same Gateway still owns the originating session's // cache. A replacement connection must not consume this late command result. - if (isChatCommandConnectionCurrent(host, target)) { + if (isChatCommandModelCacheOwnerCurrent(host, target)) { host.sessions.setModelOverride( target.sessionKey, result.sessionPatch.modelOverride?.value ?? null, diff --git a/ui/src/pages/chat/chat-send.test.ts b/ui/src/pages/chat/chat-send.test.ts index 392450b9a154..d7eaf4ebedc1 100644 --- a/ui/src/pages/chat/chat-send.test.ts +++ b/ui/src/pages/chat/chat-send.test.ts @@ -4490,6 +4490,42 @@ describe("handleSendChat", () => { expect(host.sessions.state.modelOverrides[item.sessionKey]).toBeUndefined(); }); + it("does not apply a late global model result after the selected agent changes", async () => { + const command = createDeferred>>(); + executeSlashCommandMock.mockImplementationOnce(() => command.promise); + + const item = createQueuedLocalCommand("switched-global-model-command", "/model gpt-5-mini", { + sessionKey: "global", + }); + const host = makeHost({ + requestHandlers: { + "chat.history": () => idleChatHistory(item.sessionKey), + }, + assistantAgentId: "work", + agentsList: { defaultId: "main" }, + chatQueue: [item], + sessionKey: item.sessionKey, + }); + const setModelOverride = vi.spyOn(host.sessions, "setModelOverride"); + expect(admitQueuedMessageForSession(host, item.sessionKey, item)).toBe(true); + + const draining = retryReconnectableQueuedChatSends(host); + await waitForFast(() => expect(executeSlashCommandMock).toHaveBeenCalledTimes(1)); + + host.assistantAgentId = "main"; + command.resolve({ + action: "refresh", + content: "Model set to `gpt-5-mini`.", + sessionPatch: { + modelOverride: { kind: "qualified", value: "openai/gpt-5-mini" }, + }, + }); + await draining; + + expect(setModelOverride).not.toHaveBeenCalled(); + expect(host.sessions.state.modelOverrides[item.sessionKey]).toBeUndefined(); + }); + it("does not borrow a replacement connection error for a stale queued command", async () => { const command = createDeferred>>(); executeSlashCommandMock.mockImplementationOnce(() => command.promise);