fix: scope global model results

This commit is contained in:
Shakker
2026-08-02 20:26:32 +01:00
parent 4d1ce9b890
commit 3f3136ad5a
2 changed files with 53 additions and 1 deletions
+17 -1
View File
@@ -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,
+36
View File
@@ -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<Awaited<ReturnType<ExecuteSlashCommand>>>();
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<Awaited<ReturnType<ExecuteSlashCommand>>>();
executeSlashCommandMock.mockImplementationOnce(() => command.promise);