mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-24 11:25:50 -06:00
fix: retire model selection ownership
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 } = {},
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user