fix: retire model selection ownership

This commit is contained in:
Shakker
2026-08-02 22:18:03 +01:00
parent 3940d1f825
commit 5bcebc3267
6 changed files with 88 additions and 1 deletions
@@ -28,6 +28,7 @@ function createSessionCapability(client: GatewayBrowserClient): SessionCapabilit
create: async () => null,
patch: (key: string, patch: SessionPatch, options: SessionPatchOptions = {}) =>
request("sessions.patch", { key, agentId: options.agentId, ...patch }),
setModelOverride: () => undefined,
delete: async () => false,
deleteMany: async () => ({ deleted: [], errors: [], preservedWorktrees: [] }),
reset: async () => true,
@@ -176,6 +177,7 @@ describe("executeSlashCommand directives", () => {
expect.objectContaining({
agentId: "work",
deferModelOverride: true,
ownsModelOverride: expect.any(Function),
}),
);
expect(setModelOverride).toHaveBeenCalledWith("global", "openai/gpt-5-mini");
@@ -316,6 +316,7 @@ async function executeModel(
},
{
deferModelOverride: true,
ownsModelOverride: context.ownsModelOverride,
reconcile: async (result) => {
const resolvedModel = result.resolved?.model ?? requestedModel;
let resolvedValue = resolvePreferredServerChatModelValue(
+10
View File
@@ -14,6 +14,7 @@ import { resolveSessionKey } from "../../lib/sessions/index.ts";
import {
buildAgentMainSessionKey,
canonicalUiSessionKeyForPersistence,
isUiGlobalSessionKey,
parseAgentSessionKey,
resolveUiConfiguredMainKey,
uiSessionEventMatches,
@@ -26,6 +27,7 @@ import { resolveAssistantAttachmentAuthToken } from "./chat-pane-state.ts";
import { markQueuedChatSendsWaitingForReconnect } from "./chat-queue.ts";
import { stopChatRealtimeTalk } from "./chat-realtime.ts";
import { retryReconnectableQueuedChatSends } from "./chat-send-actions.ts";
import { retireChatModelSelectionOwnership } from "./chat-session.ts";
import {
invalidateChatMetadataCache,
refreshChatModelAuthStatus,
@@ -169,6 +171,7 @@ export abstract class ChatPaneContext extends ChatPaneLifecycle {
}
const previousMediaAuthToken = resolveAssistantAttachmentAuthToken(state);
const wasConnected = state.connected;
const previousAssistantAgentId = state.assistantAgentId;
const previousSidebarSessionKey = canonicalUiSessionKeyForPersistence(state, state.sessionKey);
const connectionLifecycle = (this.gatewayConnectionLifecycle ??=
createGatewayConnectionLifecycle({
@@ -216,6 +219,13 @@ export abstract class ChatPaneContext extends ChatPaneLifecycle {
this.resetOlderMessagesViewport();
state.chatLoading = false;
}
if (
sourceChanged ||
(previousAssistantAgentId !== snapshot.assistantAgentId &&
isUiGlobalSessionKey(state.sessionKey))
) {
retireChatModelSelectionOwnership(state);
}
state.client = snapshot.client;
state.connected = snapshot.phase === "connected";
state.connectionEpoch = this.connectionGeneration;
@@ -920,6 +920,51 @@ describe("chat pane connection lifecycle", () => {
expect(state.chatIsProgrammaticScroll).toBe(false);
});
it("retires pending model selection state when the Gateway owner changes", () => {
const client = { request: vi.fn() } as unknown as GatewayBrowserClient;
const setModelOverride = vi.fn();
const sessions = { setModelOverride } as unknown as SessionCapability;
const { pane, state } = createTestChatPane({ client, sessions });
state.sessionKey = "global";
state.chatModelSwitchPromises = {
global: new Promise<boolean>(() => {}),
};
pane.applyGatewaySnapshot({
...pane.context.gateway.snapshot,
client,
phase: "reconnecting",
hello: null,
});
expect(state.chatModelSwitchPromises).toEqual({});
expect(setModelOverride).toHaveBeenCalledWith("global", undefined);
});
it("retires pending global model selection state when the selected agent changes", () => {
const client = { request: vi.fn() } as unknown as GatewayBrowserClient;
const setModelOverride = vi.fn();
const sessions = { setModelOverride } as unknown as SessionCapability;
const { pane, state } = createTestChatPane({ client, sessions });
const snapshot = {
...pane.context.gateway.snapshot,
client,
phase: "connected" as const,
assistantAgentId: "work",
};
pane.applyGatewaySnapshot(snapshot);
state.sessionKey = "global";
state.chatModelSwitchPromises = {
global: new Promise<boolean>(() => {}),
};
setModelOverride.mockClear();
pane.applyGatewaySnapshot({ ...snapshot, assistantAgentId: "main" });
expect(state.chatModelSwitchPromises).toEqual({});
expect(setModelOverride).toHaveBeenCalledWith("global", undefined);
});
it("refreshes the transcript before secondary hydration after a same-client reconnect", () => {
const request = vi.fn(() => new Promise<never>(() => {}));
const client = {
+19
View File
@@ -54,6 +54,25 @@ type ChatIdleSessionReconciliationHost = SessionScopeHost & {
sessionsResult?: SessionsListResult | null;
};
export function retireChatModelSelectionOwnership(
host: Pick<
ChatModelSettingsHost,
"chatModelSwitchPromises" | "requestUpdate" | "sessionKey" | "sessions"
>,
): void {
const hasPendingSwitch = Object.keys(host.chatModelSwitchPromises ?? {}).length > 0;
const hasModelOverride = Object.hasOwn(
host.sessions.state?.modelOverrides ?? {},
host.sessionKey,
);
if (!hasPendingSwitch && !hasModelOverride) {
return;
}
host.chatModelSwitchPromises = {};
host.sessions.setModelOverride(host.sessionKey, undefined);
host.requestUpdate?.();
}
function buildChatSessionListOptions(
state: ChatSessionListHost,
options: { offset?: number; append?: boolean; search?: string | null } = {},
+11 -1
View File
@@ -4,7 +4,10 @@ import { fetchAssistantIdentity } from "../../app/assistant-identity.ts";
import type { ApplicationContext } from "../../app/context.ts";
import { loadLocalUserIdentity, loadSettings, patchSettings } from "../../app/settings.ts";
import { resolveSafeExternalUrl } from "../../lib/open-external-url.ts";
import { canonicalUiSessionKeyForPersistence } from "../../lib/sessions/session-key.ts";
import {
canonicalUiSessionKeyForPersistence,
isUiGlobalSessionKey,
} from "../../lib/sessions/session-key.ts";
import { resolveAgentIdForSession } from "./chat-avatar.ts";
import { removeQueuedMessage } from "./chat-queue.ts";
import { attachChatRealtimeActions, createInitialChatRealtimeState } from "./chat-realtime.ts";
@@ -14,6 +17,7 @@ import {
steerQueuedChatMessage,
} from "./chat-send-actions.ts";
import { handleSendChat } from "./chat-send-submit.ts";
import { retireChatModelSelectionOwnership } from "./chat-session.ts";
import type { ChatPageHost } from "./chat-state-host.ts";
import {
handleChatDraftChange,
@@ -86,6 +90,12 @@ async function loadPageAssistantIdentity(
) {
return;
}
if (
state.assistantAgentId !== (identity.agentId ?? null) &&
isUiGlobalSessionKey(state.sessionKey)
) {
retireChatModelSelectionOwnership(state);
}
state.assistantName = identity.name;
state.assistantAvatar = identity.avatar;
state.assistantAvatarSource = identity.avatarSource ?? null;