mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 11:12:54 +00:00
perf: streamline chat startup metadata (#88825)
* perf: streamline chat startup metadata * fix: defer global queued agent selection * style: format gateway startup refresh
This commit is contained in:
committed by
GitHub
parent
1b10739d60
commit
6b940ed3ca
@@ -288,6 +288,81 @@ describe("gateway server chat", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("chat.history exposes persisted and synthetic session metadata for startup hydration", async () => {
|
||||
await withGatewayChatHarness(async ({ ws, createSessionDir }) => {
|
||||
await connectOk(ws);
|
||||
const sessionDir = await createSessionDir();
|
||||
const updatedAt = Date.now();
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-main",
|
||||
updatedAt,
|
||||
modelProvider: "openai",
|
||||
model: "gpt-5",
|
||||
contextTokens: 128_000,
|
||||
},
|
||||
},
|
||||
});
|
||||
await writeMainSessionTranscript(sessionDir, [
|
||||
JSON.stringify({
|
||||
message: {
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "persisted metadata" }],
|
||||
timestamp: updatedAt,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const persisted = await rpcReq<{
|
||||
defaults?: { modelProvider?: string | null; model?: string | null };
|
||||
sessionInfo?: {
|
||||
key?: string;
|
||||
sessionId?: string;
|
||||
updatedAt?: number | null;
|
||||
modelProvider?: string | null;
|
||||
model?: string | null;
|
||||
contextTokens?: number | null;
|
||||
};
|
||||
}>(ws, "chat.history", { sessionKey: "main" });
|
||||
|
||||
expect(persisted.ok).toBe(true);
|
||||
expect(persisted.payload?.defaults?.modelProvider).toBeTruthy();
|
||||
expect(persisted.payload?.defaults?.model).toBeTruthy();
|
||||
expect(persisted.payload?.sessionInfo).toMatchObject({
|
||||
key: "agent:main:main",
|
||||
sessionId: "sess-main",
|
||||
updatedAt,
|
||||
modelProvider: "openai",
|
||||
model: "gpt-5",
|
||||
contextTokens: 128_000,
|
||||
});
|
||||
|
||||
await writeSessionStore({ entries: {} });
|
||||
const synthetic = await rpcReq<{
|
||||
defaults?: { modelProvider?: string | null; model?: string | null };
|
||||
sessionInfo?: {
|
||||
key?: string;
|
||||
sessionId?: string;
|
||||
updatedAt?: number | null;
|
||||
modelProvider?: string | null;
|
||||
model?: string | null;
|
||||
contextTokens?: number | null;
|
||||
};
|
||||
}>(ws, "chat.history", { sessionKey: "main" });
|
||||
|
||||
expect(synthetic.ok).toBe(true);
|
||||
expect(synthetic.payload?.defaults?.modelProvider).toBeTruthy();
|
||||
expect(synthetic.payload?.defaults?.model).toBeTruthy();
|
||||
expect(synthetic.payload?.sessionInfo?.key).toBe("agent:main:main");
|
||||
expect(synthetic.payload?.sessionInfo?.sessionId).toBeUndefined();
|
||||
expect(synthetic.payload?.sessionInfo?.updatedAt).toBeNull();
|
||||
expect(synthetic.payload?.sessionInfo?.modelProvider).toBeTruthy();
|
||||
expect(synthetic.payload?.sessionInfo?.model).toBeTruthy();
|
||||
expect(synthetic.payload?.sessionInfo?.contextTokens).toEqual(expect.any(Number));
|
||||
});
|
||||
});
|
||||
|
||||
test("chat.send returns in_flight when duplicate attachment send wins parsing race", async () => {
|
||||
const sessionDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-"));
|
||||
const dispatchRelease = createDeferred<void>();
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
resolveThreadParentSessionKey,
|
||||
} from "../sessions/session-key-utils.js";
|
||||
import {
|
||||
agentSessionKeysMatchByRequestKey,
|
||||
buildAgentPeerSessionKey,
|
||||
buildGroupHistoryKey,
|
||||
classifySessionKeyShape,
|
||||
@@ -76,6 +77,14 @@ describe("isUnscopedSessionKeySentinel", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("agentSessionKeysMatchByRequestKey", () => {
|
||||
it("matches canonical agent keys against their request-key aliases", () => {
|
||||
expect(agentSessionKeysMatchByRequestKey("agent:main:main", "main")).toBe(true);
|
||||
expect(agentSessionKeysMatchByRequestKey("agent:ops:incident-42", "incident-42")).toBe(true);
|
||||
expect(agentSessionKeysMatchByRequestKey("agent:ops:incident-42", "main")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("session key backward compatibility", () => {
|
||||
function expectBackwardCompatibleDirectSessionKey(key: string) {
|
||||
expect(classifySessionKeyShape(key)).toBe("agent");
|
||||
|
||||
@@ -92,6 +92,20 @@ export function toAgentRequestSessionKey(storeKey: string | undefined | null): s
|
||||
return parseAgentSessionKey(raw)?.rest ?? raw;
|
||||
}
|
||||
|
||||
export function agentSessionKeysMatchByRequestKey(
|
||||
left: string | undefined | null,
|
||||
right: string | undefined | null,
|
||||
): boolean {
|
||||
const leftRaw = (left ?? "").trim();
|
||||
const rightRaw = (right ?? "").trim();
|
||||
if (!leftRaw || !rightRaw) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
leftRaw === rightRaw || toAgentRequestSessionKey(leftRaw) === toAgentRequestSessionKey(rightRaw)
|
||||
);
|
||||
}
|
||||
|
||||
export function toAgentStoreSessionKey(params: {
|
||||
agentId: string;
|
||||
requestKey: string | undefined | null;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { normalizeOptionalString } from "@openclaw/normalization-core/string-coe
|
||||
import type { SessionsPatchResult } from "../../packages/gateway-protocol/src/index.js";
|
||||
import { resolveSessionInfoModelSelection } from "../agents/model-selection-display.js";
|
||||
import {
|
||||
agentSessionKeysMatchByRequestKey,
|
||||
normalizeAgentId,
|
||||
normalizeMainKey,
|
||||
parseAgentSessionKey,
|
||||
@@ -312,15 +313,8 @@ export function createSessionActions(context: SessionActionContext) {
|
||||
includeUnknown: state.currentSessionKey === "unknown",
|
||||
agentId: listAgentId,
|
||||
});
|
||||
const normalizeMatchKey = (key: string) => parseAgentSessionKey(key)?.rest ?? key;
|
||||
const currentMatchKey = normalizeMatchKey(state.currentSessionKey);
|
||||
const entry = result.sessions.find((row) => {
|
||||
// Exact match
|
||||
if (row.key === state.currentSessionKey) {
|
||||
return true;
|
||||
}
|
||||
// Also match canonical keys like "agent:default:main" against "main"
|
||||
return normalizeMatchKey(row.key) === currentMatchKey;
|
||||
return agentSessionKeysMatchByRequestKey(row.key, state.currentSessionKey);
|
||||
});
|
||||
if (entry?.key && entry.key !== state.currentSessionKey) {
|
||||
updateAgentFromSessionKey(entry.key);
|
||||
|
||||
@@ -1847,6 +1847,43 @@ describe("handleSendChat", () => {
|
||||
expect(host.chatStream).toBeNull();
|
||||
});
|
||||
|
||||
it("defers queued global send agent selection until defaults are known", async () => {
|
||||
const request = vi.fn((method: string) => {
|
||||
if (method === "chat.send") {
|
||||
return Promise.resolve({ runId: "run-work", status: "started" });
|
||||
}
|
||||
throw new Error(`Unexpected request: ${method}`);
|
||||
});
|
||||
const host = makeHost({
|
||||
client: null,
|
||||
connected: false,
|
||||
sessionKey: "global",
|
||||
chatMessage: "send to default later",
|
||||
});
|
||||
|
||||
await handleSendChat(host);
|
||||
|
||||
expect(host.chatQueue[0]).toMatchObject({
|
||||
text: "send to default later",
|
||||
sessionKey: "global",
|
||||
sendState: "waiting-reconnect",
|
||||
});
|
||||
expect(host.chatQueue[0]?.agentId).toBeUndefined();
|
||||
|
||||
host.agentsList = { defaultId: "work" };
|
||||
host.client = { request } as unknown as ChatHost["client"];
|
||||
host.connected = true;
|
||||
await retryReconnectableQueuedChatSends(host);
|
||||
|
||||
const payload = findRequestPayload(
|
||||
request as unknown as MockCallSource,
|
||||
"chat.send",
|
||||
"queued global send payload",
|
||||
);
|
||||
expect(payload.sessionKey).toBe("global");
|
||||
expect(payload.agentId).toBe("work");
|
||||
});
|
||||
|
||||
it("marks saved session queued sends waiting after a disconnect", () => {
|
||||
const host = makeHost({
|
||||
chatQueue: [],
|
||||
|
||||
@@ -45,8 +45,13 @@ import { normalizeBasePath } from "./navigation.ts";
|
||||
import {
|
||||
areUiSessionKeysEquivalent,
|
||||
DEFAULT_AGENT_ID,
|
||||
isUiGlobalSessionKey,
|
||||
normalizeAgentId,
|
||||
parseAgentSessionKey,
|
||||
resolveUiDefaultAgentId,
|
||||
resolveUiGlobalAliasAgentId,
|
||||
resolveUiKnownSelectedGlobalAgentId,
|
||||
resolveUiSelectedGlobalAgentId,
|
||||
} from "./session-key.ts";
|
||||
import { isSessionRunActive } from "./session-run-state.ts";
|
||||
import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "./string-coerce.ts";
|
||||
@@ -219,10 +224,6 @@ function isBtwCommand(text: string) {
|
||||
return /^\/(?:btw|side)(?::|\s|$)/i.test(text.trim());
|
||||
}
|
||||
|
||||
function isGlobalSessionKey(sessionKey: string | undefined | null): boolean {
|
||||
return normalizeLowercaseStringOrEmpty(sessionKey) === "global";
|
||||
}
|
||||
|
||||
function readHelloDefaultAgentId(host: Pick<ChatHost, "hello">): string | undefined {
|
||||
const snapshot = host.hello?.snapshot as
|
||||
| { sessionDefaults?: SessionDefaultsSnapshot }
|
||||
@@ -230,50 +231,10 @@ function readHelloDefaultAgentId(host: Pick<ChatHost, "hello">): string | undefi
|
||||
return snapshot?.sessionDefaults?.defaultAgentId?.trim() || undefined;
|
||||
}
|
||||
|
||||
function readHelloMainKey(host: Pick<ChatHost, "hello">): string | undefined {
|
||||
const snapshot = host.hello?.snapshot as
|
||||
| { sessionDefaults?: SessionDefaultsSnapshot }
|
||||
| undefined;
|
||||
return snapshot?.sessionDefaults?.mainKey?.trim() || undefined;
|
||||
}
|
||||
|
||||
function resolveGlobalAliasAgentId(
|
||||
host: Pick<ChatHost, "agentsList" | "hello">,
|
||||
sessionKey: string | undefined | null,
|
||||
): string | undefined {
|
||||
const parsed = parseAgentSessionKey(sessionKey);
|
||||
if (!parsed) {
|
||||
return undefined;
|
||||
}
|
||||
const rest = normalizeLowercaseStringOrEmpty(parsed.rest);
|
||||
const configuredMainKey = normalizeLowercaseStringOrEmpty(
|
||||
host.agentsList?.mainKey ?? readHelloMainKey(host) ?? "main",
|
||||
);
|
||||
return rest === "global" || rest === "main" || rest === configuredMainKey
|
||||
? normalizeAgentId(parsed.agentId)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function resolveSelectedGlobalAgentId(
|
||||
host: Pick<ChatHost, "assistantAgentId" | "agentsList" | "hello">,
|
||||
): string | undefined {
|
||||
const agentId =
|
||||
host.assistantAgentId?.trim() ||
|
||||
host.agentsList?.defaultId?.trim() ||
|
||||
readHelloDefaultAgentId(host);
|
||||
return agentId ? normalizeAgentId(agentId) : undefined;
|
||||
}
|
||||
|
||||
function resolveDefaultAgentIdForList(host: Pick<ChatHost, "agentsList" | "hello">): string {
|
||||
return normalizeAgentId(
|
||||
host.agentsList?.defaultId ?? readHelloDefaultAgentId(host) ?? DEFAULT_AGENT_ID,
|
||||
);
|
||||
}
|
||||
|
||||
function scopedAgentIdForSession(host: ChatHost, sessionKey: string | undefined | null) {
|
||||
return isGlobalSessionKey(sessionKey)
|
||||
? resolveSelectedGlobalAgentId(host)
|
||||
: resolveGlobalAliasAgentId(host, sessionKey);
|
||||
return isUiGlobalSessionKey(sessionKey)
|
||||
? resolveUiKnownSelectedGlobalAgentId(host)
|
||||
: (resolveUiGlobalAliasAgentId(host, sessionKey) ?? undefined);
|
||||
}
|
||||
|
||||
function visibleSessionMatches(
|
||||
@@ -282,8 +243,8 @@ function visibleSessionMatches(
|
||||
agentId: string | undefined,
|
||||
): boolean {
|
||||
if (host.sessionKey !== sessionKey) {
|
||||
const hostAliasAgentId = resolveGlobalAliasAgentId(host, host.sessionKey);
|
||||
if (!hostAliasAgentId || !isGlobalSessionKey(sessionKey)) {
|
||||
const hostAliasAgentId = resolveUiGlobalAliasAgentId(host, host.sessionKey);
|
||||
if (!hostAliasAgentId || !isUiGlobalSessionKey(sessionKey)) {
|
||||
return false;
|
||||
}
|
||||
const expectedAgentId = agentId ?? host.agentsList?.defaultId ?? readHelloDefaultAgentId(host);
|
||||
@@ -291,10 +252,10 @@ function visibleSessionMatches(
|
||||
? hostAliasAgentId === normalizeAgentId(expectedAgentId)
|
||||
: hostAliasAgentId === normalizeAgentId("main");
|
||||
}
|
||||
if (!isGlobalSessionKey(sessionKey)) {
|
||||
if (!isUiGlobalSessionKey(sessionKey)) {
|
||||
return true;
|
||||
}
|
||||
const selectedAgentId = resolveSelectedGlobalAgentId(host);
|
||||
const selectedAgentId = resolveUiKnownSelectedGlobalAgentId(host);
|
||||
const expectedAgentId = agentId ?? host.agentsList?.defaultId ?? readHelloDefaultAgentId(host);
|
||||
return expectedAgentId
|
||||
? selectedAgentId === normalizeAgentId(expectedAgentId)
|
||||
@@ -305,9 +266,9 @@ export function scopedAgentParamsForSession(
|
||||
host: Pick<ChatHost, "assistantAgentId" | "agentsList" | "hello">,
|
||||
sessionKey: string,
|
||||
) {
|
||||
const agentId = isGlobalSessionKey(sessionKey)
|
||||
? resolveSelectedGlobalAgentId(host)
|
||||
: resolveGlobalAliasAgentId(host, sessionKey);
|
||||
const agentId = isUiGlobalSessionKey(sessionKey)
|
||||
? resolveUiKnownSelectedGlobalAgentId(host)
|
||||
: resolveUiGlobalAliasAgentId(host, sessionKey);
|
||||
return agentId ? { agentId } : {};
|
||||
}
|
||||
|
||||
@@ -320,10 +281,10 @@ export function scopedAgentListParamsForSession(
|
||||
const agentId =
|
||||
parsed?.agentId ??
|
||||
(normalizedSessionKey === "global"
|
||||
? resolveSelectedGlobalAgentId(host)
|
||||
? resolveUiKnownSelectedGlobalAgentId(host)
|
||||
: normalizedSessionKey === "unknown"
|
||||
? undefined
|
||||
: resolveDefaultAgentIdForList(host));
|
||||
: resolveUiDefaultAgentId(host));
|
||||
return agentId ? { agentId: normalizeAgentId(agentId) } : {};
|
||||
}
|
||||
|
||||
@@ -986,8 +947,8 @@ function isHistorySessionInfoForRequestedSession(
|
||||
}
|
||||
return Boolean(
|
||||
historySessionKey &&
|
||||
isGlobalSessionKey(historySessionKey) &&
|
||||
resolveGlobalAliasAgentId(host, requestedSessionKey),
|
||||
isUiGlobalSessionKey(historySessionKey) &&
|
||||
resolveUiGlobalAliasAgentId(host, requestedSessionKey),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -998,8 +959,8 @@ function findSelectedSessionRow(
|
||||
historySessionKey: string | undefined,
|
||||
): GatewaySessionRow | undefined {
|
||||
const requestedGlobalAgentId =
|
||||
historySessionKey && isGlobalSessionKey(historySessionKey)
|
||||
? resolveGlobalAliasAgentId(host, sessionKey)
|
||||
historySessionKey && isUiGlobalSessionKey(historySessionKey)
|
||||
? resolveUiGlobalAliasAgentId(host, sessionKey)
|
||||
: undefined;
|
||||
return sessionsResult?.sessions.find((session) => {
|
||||
if (areUiSessionKeysEquivalent(session.key, sessionKey)) {
|
||||
@@ -1007,7 +968,7 @@ function findSelectedSessionRow(
|
||||
}
|
||||
return (
|
||||
requestedGlobalAgentId != null &&
|
||||
resolveGlobalAliasAgentId(host, session.key) === requestedGlobalAgentId
|
||||
resolveUiGlobalAliasAgentId(host, session.key) === requestedGlobalAgentId
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -1558,8 +1519,8 @@ function resolveAgentIdForSession(host: ChatHost): string | null {
|
||||
if (parsed?.agentId) {
|
||||
return parsed.agentId;
|
||||
}
|
||||
if (isGlobalSessionKey(host.sessionKey)) {
|
||||
return resolveSelectedGlobalAgentId(host) || DEFAULT_AGENT_ID;
|
||||
if (isUiGlobalSessionKey(host.sessionKey)) {
|
||||
return resolveUiSelectedGlobalAgentId(host) || DEFAULT_AGENT_ID;
|
||||
}
|
||||
return readHelloDefaultAgentId(host) || DEFAULT_AGENT_ID;
|
||||
}
|
||||
|
||||
@@ -76,8 +76,12 @@ import type { Tab } from "./navigation.ts";
|
||||
import {
|
||||
areUiSessionKeysEquivalent,
|
||||
buildAgentMainSessionKey,
|
||||
isUiGlobalSessionKey,
|
||||
normalizeAgentId,
|
||||
parseAgentSessionKey,
|
||||
resolveUiDefaultAgentId,
|
||||
resolveUiGlobalAliasAgentId,
|
||||
resolveUiSelectedGlobalAgentId,
|
||||
} from "./session-key.ts";
|
||||
import type { UiSettings } from "./storage.ts";
|
||||
import type {
|
||||
@@ -470,19 +474,8 @@ function resolveMainSessionFallback(host: GatewayHost): string {
|
||||
});
|
||||
}
|
||||
|
||||
function isGlobalSessionKey(sessionKey: string | undefined | null): boolean {
|
||||
return sessionKey?.trim().toLowerCase() === "global";
|
||||
}
|
||||
|
||||
function resolveDefaultAgentId(host: GatewayHost): string {
|
||||
const snapshot = host.hello?.snapshot as
|
||||
| { sessionDefaults?: SessionDefaultsSnapshot }
|
||||
| undefined;
|
||||
return normalizeAgentId(
|
||||
host.agentsList?.defaultId?.trim() ||
|
||||
snapshot?.sessionDefaults?.defaultAgentId?.trim() ||
|
||||
"main",
|
||||
);
|
||||
return resolveUiDefaultAgentId(host);
|
||||
}
|
||||
|
||||
function resolveFreshDefaultAgentId(host: GatewayHost): string | undefined {
|
||||
@@ -499,20 +492,7 @@ function resolveFreshDefaultAgentId(host: GatewayHost): string | undefined {
|
||||
}
|
||||
|
||||
function resolveSelectedGlobalAgentId(host: GatewayHost): string {
|
||||
return normalizeAgentId(host.assistantAgentId?.trim() || resolveDefaultAgentId(host));
|
||||
}
|
||||
|
||||
function resolveGlobalAliasAgentId(host: GatewayHost, sessionKey: string | undefined | null) {
|
||||
const parsed = parseAgentSessionKey(sessionKey ?? "");
|
||||
if (!parsed) {
|
||||
return undefined;
|
||||
}
|
||||
const rest = parsed.rest.trim().toLowerCase();
|
||||
const snapshot = host.hello?.snapshot as
|
||||
| { sessionDefaults?: SessionDefaultsSnapshot }
|
||||
| undefined;
|
||||
const mainKey = snapshot?.sessionDefaults?.mainKey?.trim().toLowerCase() || "main";
|
||||
return rest === "main" || rest === mainKey ? normalizeAgentId(parsed.agentId) : undefined;
|
||||
return resolveUiSelectedGlobalAgentId(host);
|
||||
}
|
||||
|
||||
function resolveSelectedGlobalEventAgentId(
|
||||
@@ -527,12 +507,12 @@ function globalAgentScopeMatches(
|
||||
sessionKey: string | undefined | null,
|
||||
agentId: string | undefined | null,
|
||||
): boolean {
|
||||
if (!isGlobalSessionKey(sessionKey)) {
|
||||
if (!isUiGlobalSessionKey(sessionKey)) {
|
||||
return true;
|
||||
}
|
||||
const selectedAgentId = isGlobalSessionKey(host.sessionKey)
|
||||
const selectedAgentId = isUiGlobalSessionKey(host.sessionKey)
|
||||
? resolveSelectedGlobalAgentId(host)
|
||||
: resolveGlobalAliasAgentId(host, host.sessionKey);
|
||||
: resolveUiGlobalAliasAgentId(host, host.sessionKey);
|
||||
if (!selectedAgentId) {
|
||||
return true;
|
||||
}
|
||||
@@ -550,10 +530,10 @@ function sessionMessageMatchesHost(
|
||||
if (areUiSessionKeysEquivalent(sessionKey, host.sessionKey)) {
|
||||
return true;
|
||||
}
|
||||
const hostAliasAgentId = resolveGlobalAliasAgentId(host, host.sessionKey);
|
||||
const hostAliasAgentId = resolveUiGlobalAliasAgentId(host, host.sessionKey);
|
||||
return Boolean(
|
||||
hostAliasAgentId &&
|
||||
isGlobalSessionKey(sessionKey) &&
|
||||
isUiGlobalSessionKey(sessionKey) &&
|
||||
resolveSelectedGlobalEventAgentId(host, agentId) === hostAliasAgentId,
|
||||
);
|
||||
}
|
||||
@@ -592,7 +572,7 @@ function canRefreshActiveTabBeforeAgents(host: GatewayHost): boolean {
|
||||
if (host.tab !== "chat") {
|
||||
return false;
|
||||
}
|
||||
if (isGlobalSessionKey(host.sessionKey)) {
|
||||
if (isUiGlobalSessionKey(host.sessionKey)) {
|
||||
return false;
|
||||
}
|
||||
const parsed = parseAgentSessionKey(host.sessionKey);
|
||||
@@ -610,9 +590,11 @@ async function loadAgentsThenRefreshActiveTab(host: GatewayHost) {
|
||||
let initialRefreshError: Error | undefined;
|
||||
const refreshBeforeAgents = canRefreshActiveTabBeforeAgents(host);
|
||||
const initialRefresh = refreshBeforeAgents
|
||||
? refreshActiveTab(host as unknown as Parameters<typeof refreshActiveTab>[0]).catch((err: unknown) => {
|
||||
initialRefreshError = normalizeStartupRefreshError(err);
|
||||
})
|
||||
? refreshActiveTab(host as unknown as Parameters<typeof refreshActiveTab>[0]).catch(
|
||||
(err: unknown) => {
|
||||
initialRefreshError = normalizeStartupRefreshError(err);
|
||||
},
|
||||
)
|
||||
: Promise.resolve();
|
||||
let refreshAfterAgents = !refreshBeforeAgents;
|
||||
let agentsError: Error | undefined;
|
||||
|
||||
@@ -491,13 +491,15 @@ export async function refreshActiveTab(host: SettingsHost) {
|
||||
]);
|
||||
break;
|
||||
case "chat": {
|
||||
const modelAuthRefresh = loadModelAuthStatusState(app).catch(() => undefined);
|
||||
await refreshChat(host as unknown as Parameters<typeof refreshChat>[0]);
|
||||
scheduleChatScroll(
|
||||
host as unknown as Parameters<typeof scheduleChatScroll>[0],
|
||||
!host.chatHasAutoScrolled,
|
||||
);
|
||||
void modelAuthRefresh;
|
||||
try {
|
||||
await refreshChat(host as unknown as Parameters<typeof refreshChat>[0]);
|
||||
scheduleChatScroll(
|
||||
host as unknown as Parameters<typeof scheduleChatScroll>[0],
|
||||
!host.chatHasAutoScrolled,
|
||||
);
|
||||
} finally {
|
||||
void loadModelAuthStatusState(app).catch(() => undefined);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "debug":
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { resetToolStream, type CompactionStatus, type FallbackStatus } from "../app-tool-stream.ts";
|
||||
import {
|
||||
areUiSessionKeysEquivalent,
|
||||
DEFAULT_MAIN_KEY,
|
||||
parseAgentSessionKey,
|
||||
} from "../session-key.ts";
|
||||
import { uiSessionRowMatchesSelectedChat } from "../session-key.ts";
|
||||
import { isSessionRunActive } from "../session-run-state.ts";
|
||||
import type { GatewaySessionRow, SessionRunStatus, SessionsListResult } from "../types.ts";
|
||||
|
||||
@@ -280,35 +276,12 @@ export function reconcileChatRunFromCurrentSessionRow(
|
||||
return reconcileChatRunFromSessionRow(host, row, options);
|
||||
}
|
||||
|
||||
function configuredMainKey(host: RunLifecycleHost): string {
|
||||
const snapshot =
|
||||
host.hello?.snapshot && typeof host.hello.snapshot === "object"
|
||||
? (host.hello.snapshot as { sessionDefaults?: { mainKey?: string | null } })
|
||||
: undefined;
|
||||
return (
|
||||
host.agentsList?.mainKey?.trim() ||
|
||||
snapshot?.sessionDefaults?.mainKey?.trim() ||
|
||||
DEFAULT_MAIN_KEY
|
||||
).toLowerCase();
|
||||
}
|
||||
|
||||
function isSessionRowForSelectedChat(
|
||||
host: RunLifecycleHost,
|
||||
rowKey: string,
|
||||
sessionKey: string,
|
||||
): boolean {
|
||||
if (areUiSessionKeysEquivalent(rowKey, sessionKey)) {
|
||||
return true;
|
||||
}
|
||||
if (rowKey !== "global") {
|
||||
return false;
|
||||
}
|
||||
const parsed = parseAgentSessionKey(sessionKey);
|
||||
return (
|
||||
parsed?.rest === "global" ||
|
||||
parsed?.rest === DEFAULT_MAIN_KEY ||
|
||||
parsed?.rest === configuredMainKey(host)
|
||||
);
|
||||
return uiSessionRowMatchesSelectedChat(host, rowKey, sessionKey);
|
||||
}
|
||||
|
||||
export function reconcileChatRunFromSessionRow(
|
||||
|
||||
@@ -6,9 +6,13 @@ import {
|
||||
import type { GatewayBrowserClient, GatewayHelloOk } from "../gateway.ts";
|
||||
import {
|
||||
areUiSessionKeysEquivalent,
|
||||
isUiGlobalSessionKey,
|
||||
isSubagentSessionKey,
|
||||
normalizeAgentId,
|
||||
parseAgentSessionKey,
|
||||
resolveUiDefaultAgentId,
|
||||
resolveUiGlobalAliasAgentId,
|
||||
resolveUiSelectedGlobalAgentId,
|
||||
} from "../session-key.ts";
|
||||
import { isSessionRunActive } from "../session-run-state.ts";
|
||||
import type {
|
||||
@@ -104,47 +108,22 @@ function normalizeSubscriptionKey(value: string | null | undefined): string | nu
|
||||
return normalized ? normalized : null;
|
||||
}
|
||||
|
||||
function isGlobalSessionKey(value: string | null | undefined): boolean {
|
||||
return (value ?? "").trim().toLowerCase() === "global";
|
||||
}
|
||||
|
||||
function resolveConfiguredMainKey(state: SessionsState): string {
|
||||
const snapshot = state.hello?.snapshot as { sessionDefaults?: { mainKey?: string } } | undefined;
|
||||
const mainKey =
|
||||
typeof state.agentsList?.mainKey === "string" && state.agentsList.mainKey.trim()
|
||||
? state.agentsList.mainKey
|
||||
: typeof snapshot?.sessionDefaults?.mainKey === "string" &&
|
||||
snapshot.sessionDefaults.mainKey.trim()
|
||||
? snapshot.sessionDefaults.mainKey
|
||||
: "main";
|
||||
return mainKey.trim().toLowerCase();
|
||||
}
|
||||
|
||||
function resolveSelectedGlobalAliasAgentId(
|
||||
state: SessionsState,
|
||||
key: string | null | undefined,
|
||||
): string | null {
|
||||
const parsed = parseAgentSessionKey(key);
|
||||
if (!parsed?.agentId) {
|
||||
return null;
|
||||
}
|
||||
const rest = parsed.rest.toLowerCase();
|
||||
if (rest === "global") {
|
||||
return normalizeAgentId(parsed.agentId);
|
||||
}
|
||||
const configuredMainKey = resolveConfiguredMainKey(state);
|
||||
if (rest !== "main" && rest !== configuredMainKey) {
|
||||
return null;
|
||||
}
|
||||
const row = state.sessionsResult?.sessions.find((session) => session.key === key);
|
||||
return row?.kind === "global" ? normalizeAgentId(parsed.agentId) : null;
|
||||
return resolveUiGlobalAliasAgentId(state, key, {
|
||||
rowKind: row?.kind,
|
||||
requireGlobalRowForMainAlias: true,
|
||||
});
|
||||
}
|
||||
|
||||
function resolveSelectedSessionMessageSubscriptionAgentId(
|
||||
state: SessionsState,
|
||||
key: string,
|
||||
): string | null {
|
||||
if (isGlobalSessionKey(key)) {
|
||||
if (isUiGlobalSessionKey(key)) {
|
||||
return resolveSelectedGlobalAgentId(state);
|
||||
}
|
||||
return resolveSelectedGlobalAliasAgentId(state, key);
|
||||
@@ -155,23 +134,7 @@ function resolveSelectedGlobalAgentId(state: SessionsState): string {
|
||||
if (parsed?.agentId) {
|
||||
return normalizeAgentId(parsed.agentId);
|
||||
}
|
||||
const snapshot = state.hello?.snapshot as
|
||||
| { sessionDefaults?: { defaultAgentId?: string } }
|
||||
| undefined;
|
||||
const assistantAgentId =
|
||||
typeof state.assistantAgentId === "string" && state.assistantAgentId.trim()
|
||||
? state.assistantAgentId
|
||||
: undefined;
|
||||
const defaultAgentId =
|
||||
typeof state.agentsList?.defaultId === "string" && state.agentsList.defaultId.trim()
|
||||
? state.agentsList.defaultId
|
||||
: undefined;
|
||||
const helloDefaultAgentId =
|
||||
typeof snapshot?.sessionDefaults?.defaultAgentId === "string" &&
|
||||
snapshot.sessionDefaults.defaultAgentId.trim()
|
||||
? snapshot.sessionDefaults.defaultAgentId
|
||||
: undefined;
|
||||
return normalizeAgentId(assistantAgentId ?? defaultAgentId ?? helloDefaultAgentId ?? "main");
|
||||
return resolveUiSelectedGlobalAgentId(state);
|
||||
}
|
||||
|
||||
function resolveChatHistorySessionResultAgentId(
|
||||
@@ -182,21 +145,11 @@ function resolveChatHistorySessionResultAgentId(
|
||||
if (parsed?.agentId) {
|
||||
return normalizeAgentId(parsed.agentId);
|
||||
}
|
||||
return isGlobalSessionKey(row.key) ? resolveSelectedGlobalAgentId(state) : null;
|
||||
return isUiGlobalSessionKey(row.key) ? resolveSelectedGlobalAgentId(state) : null;
|
||||
}
|
||||
|
||||
function resolveDefaultGlobalAgentId(state: SessionsState): string {
|
||||
const snapshot = state.hello?.snapshot as
|
||||
| { sessionDefaults?: { defaultAgentId?: string } }
|
||||
| undefined;
|
||||
const defaultAgentId =
|
||||
typeof state.agentsList?.defaultId === "string" && state.agentsList.defaultId.trim()
|
||||
? state.agentsList.defaultId
|
||||
: typeof snapshot?.sessionDefaults?.defaultAgentId === "string" &&
|
||||
snapshot.sessionDefaults.defaultAgentId.trim()
|
||||
? snapshot.sessionDefaults.defaultAgentId
|
||||
: "main";
|
||||
return normalizeAgentId(defaultAgentId);
|
||||
return resolveUiDefaultAgentId(state);
|
||||
}
|
||||
|
||||
function sessionsChangedGlobalAgentMatches(
|
||||
@@ -204,7 +157,7 @@ function sessionsChangedGlobalAgentMatches(
|
||||
payload: Record<string, unknown>,
|
||||
key: string,
|
||||
): boolean {
|
||||
if (!isGlobalSessionKey(key)) {
|
||||
if (!isUiGlobalSessionKey(key)) {
|
||||
return true;
|
||||
}
|
||||
const eventSession = isRecord(payload.session) ? payload.session : null;
|
||||
@@ -309,7 +262,7 @@ async function unsubscribeSelectedSessionMessageBestEffort(
|
||||
try {
|
||||
await client.request("sessions.messages.unsubscribe", {
|
||||
key,
|
||||
...(isGlobalSessionKey(key) && agentId ? { agentId } : {}),
|
||||
...(isUiGlobalSessionKey(key) && agentId ? { agentId } : {}),
|
||||
});
|
||||
} catch {
|
||||
// Best-effort cleanup for stale async subscription completions.
|
||||
@@ -576,7 +529,7 @@ function sessionRowMatchesChatHistoryRow(
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
isGlobalSessionKey(incoming.key) &&
|
||||
isUiGlobalSessionKey(incoming.key) &&
|
||||
resolveSelectedGlobalAliasAgentId(state, existing.key) === resolveSelectedGlobalAgentId(state)
|
||||
);
|
||||
}
|
||||
@@ -729,7 +682,7 @@ async function runCompactionMutation<T>(
|
||||
});
|
||||
await loadSessions(
|
||||
state,
|
||||
isGlobalSessionKey(key) ? { agentId: resolveSelectedGlobalAgentId(state) } : undefined,
|
||||
isUiGlobalSessionKey(key) ? { agentId: resolveSelectedGlobalAgentId(state) } : undefined,
|
||||
);
|
||||
return result;
|
||||
} catch (err) {
|
||||
@@ -983,7 +936,7 @@ export function applyChatHistorySessionInfo(
|
||||
const applied = applySessionsChangedEvent(state, {
|
||||
session: visibleSession,
|
||||
sessionKey: visibleSession.key,
|
||||
...(isGlobalSessionKey(visibleSession.key)
|
||||
...(isUiGlobalSessionKey(visibleSession.key)
|
||||
? { agentId: resolveSelectedGlobalAgentId(state) }
|
||||
: {}),
|
||||
});
|
||||
@@ -1066,7 +1019,8 @@ export async function syncSelectedSessionMessageSubscription(
|
||||
if (shouldUnsubscribePrevious && previousCanonicalKey) {
|
||||
await client.request("sessions.messages.unsubscribe", {
|
||||
key: previousCanonicalKey,
|
||||
...(isGlobalSessionKey(previousCanonicalKey) && state.chatSessionMessageSubscriptionAgentId
|
||||
...(isUiGlobalSessionKey(previousCanonicalKey) &&
|
||||
state.chatSessionMessageSubscriptionAgentId
|
||||
? { agentId: state.chatSessionMessageSubscriptionAgentId }
|
||||
: {}),
|
||||
});
|
||||
@@ -1087,7 +1041,7 @@ export async function syncSelectedSessionMessageSubscription(
|
||||
const staleKeyChanged =
|
||||
normalizeSubscriptionKey(state.chatSessionMessageSubscriptionKey) !== subscribedKey;
|
||||
const staleAgentChanged =
|
||||
isGlobalSessionKey(subscribedKey) &&
|
||||
isUiGlobalSessionKey(subscribedKey) &&
|
||||
(state.chatSessionMessageSubscriptionAgentId ?? null) !== subscribedAgentId;
|
||||
if (staleKeyChanged || staleAgentChanged) {
|
||||
await unsubscribeSelectedSessionMessageBestEffort(client, subscribedKey, subscribedAgentId);
|
||||
@@ -1254,7 +1208,7 @@ export async function patchSession(
|
||||
}
|
||||
const params: Record<string, unknown> = {
|
||||
key,
|
||||
...(isGlobalSessionKey(key) ? { agentId: resolveSelectedGlobalAgentId(state) } : {}),
|
||||
...(isUiGlobalSessionKey(key) ? { agentId: resolveSelectedGlobalAgentId(state) } : {}),
|
||||
};
|
||||
for (const field of [
|
||||
"label",
|
||||
@@ -1271,7 +1225,7 @@ export async function patchSession(
|
||||
await state.client.request("sessions.patch", params);
|
||||
await loadSessions(
|
||||
state,
|
||||
isGlobalSessionKey(key) ? { agentId: resolveSelectedGlobalAgentId(state) } : undefined,
|
||||
isUiGlobalSessionKey(key) ? { agentId: resolveSelectedGlobalAgentId(state) } : undefined,
|
||||
);
|
||||
} catch (err) {
|
||||
state.sessionsError = String(err);
|
||||
@@ -1329,7 +1283,7 @@ export async function deleteSessionsAndRefresh(
|
||||
try {
|
||||
await client.request("sessions.delete", {
|
||||
key,
|
||||
...(isGlobalSessionKey(key) ? { agentId: resolveSelectedGlobalAgentId(state) } : {}),
|
||||
...(isUiGlobalSessionKey(key) ? { agentId: resolveSelectedGlobalAgentId(state) } : {}),
|
||||
deleteTranscript: true,
|
||||
});
|
||||
deleted.push(key);
|
||||
@@ -1339,7 +1293,7 @@ export async function deleteSessionsAndRefresh(
|
||||
}
|
||||
});
|
||||
if (deleted.length > 0 && !refreshedDuringDelete) {
|
||||
const selectedGlobalDeleted = deleted.some((key) => isGlobalSessionKey(key));
|
||||
const selectedGlobalDeleted = deleted.some((key) => isUiGlobalSessionKey(key));
|
||||
await loadSessions(
|
||||
state,
|
||||
selectedGlobalDeleted ? { agentId: resolveSelectedGlobalAgentId(state) } : undefined,
|
||||
|
||||
@@ -12,6 +12,12 @@ export type ParsedAgentSessionKey = {
|
||||
export const DEFAULT_AGENT_ID = "main";
|
||||
export const DEFAULT_MAIN_KEY = "main";
|
||||
|
||||
export type UiSessionDefaultsHost = {
|
||||
assistantAgentId?: string | null;
|
||||
agentsList?: { defaultId?: string | null; mainKey?: string | null } | null;
|
||||
hello?: { snapshot?: unknown } | null;
|
||||
};
|
||||
|
||||
const VALID_ID_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
|
||||
const INVALID_CHARS_RE = /[^a-z0-9_-]+/g;
|
||||
const LEADING_DASH_RE = /^-+/;
|
||||
@@ -40,6 +46,88 @@ function normalizeMainKey(value: string | undefined | null): string {
|
||||
return normalizeOptionalLowercaseString(value) ?? DEFAULT_MAIN_KEY;
|
||||
}
|
||||
|
||||
function readSessionDefaults(
|
||||
host: Pick<UiSessionDefaultsHost, "hello">,
|
||||
): { defaultAgentId?: string | null; mainKey?: string | null } | undefined {
|
||||
const snapshot = host.hello?.snapshot;
|
||||
if (!snapshot || typeof snapshot !== "object" || !("sessionDefaults" in snapshot)) {
|
||||
return undefined;
|
||||
}
|
||||
const defaults = snapshot.sessionDefaults;
|
||||
return defaults && typeof defaults === "object"
|
||||
? (defaults as { defaultAgentId?: string | null; mainKey?: string | null })
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function resolveUiConfiguredMainKey(
|
||||
host: Pick<UiSessionDefaultsHost, "agentsList" | "hello">,
|
||||
): string {
|
||||
return normalizeMainKey(host.agentsList?.mainKey ?? readSessionDefaults(host)?.mainKey);
|
||||
}
|
||||
|
||||
export function resolveUiDefaultAgentId(
|
||||
host: Pick<UiSessionDefaultsHost, "agentsList" | "hello">,
|
||||
): string {
|
||||
return normalizeAgentId(
|
||||
host.agentsList?.defaultId ?? readSessionDefaults(host)?.defaultAgentId ?? DEFAULT_AGENT_ID,
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveUiKnownSelectedGlobalAgentId(
|
||||
host: Pick<UiSessionDefaultsHost, "assistantAgentId" | "agentsList" | "hello">,
|
||||
): string | undefined {
|
||||
const selectedAgentId =
|
||||
host.assistantAgentId ??
|
||||
host.agentsList?.defaultId ??
|
||||
readSessionDefaults(host)?.defaultAgentId;
|
||||
return selectedAgentId ? normalizeAgentId(selectedAgentId) : undefined;
|
||||
}
|
||||
|
||||
export function resolveUiSelectedGlobalAgentId(
|
||||
host: Pick<UiSessionDefaultsHost, "assistantAgentId" | "agentsList" | "hello">,
|
||||
): string {
|
||||
return resolveUiKnownSelectedGlobalAgentId(host) ?? DEFAULT_AGENT_ID;
|
||||
}
|
||||
|
||||
export function resolveUiGlobalAliasAgentId(
|
||||
host: Pick<UiSessionDefaultsHost, "agentsList" | "hello">,
|
||||
sessionKey: string | undefined | null,
|
||||
opts?: { rowKind?: string | null; requireGlobalRowForMainAlias?: boolean },
|
||||
): string | null {
|
||||
const parsed = parseAgentSessionKey(sessionKey);
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
const rest = normalizeLowercaseStringOrEmpty(parsed.rest);
|
||||
if (rest === "global") {
|
||||
return normalizeAgentId(parsed.agentId);
|
||||
}
|
||||
if (rest !== DEFAULT_MAIN_KEY && rest !== resolveUiConfiguredMainKey(host)) {
|
||||
return null;
|
||||
}
|
||||
if (opts?.requireGlobalRowForMainAlias && opts.rowKind !== "global") {
|
||||
return null;
|
||||
}
|
||||
return normalizeAgentId(parsed.agentId);
|
||||
}
|
||||
|
||||
export function isUiGlobalSessionKey(sessionKey: string | undefined | null): boolean {
|
||||
return normalizeLowercaseStringOrEmpty(sessionKey) === "global";
|
||||
}
|
||||
|
||||
export function uiSessionRowMatchesSelectedChat(
|
||||
host: Pick<UiSessionDefaultsHost, "agentsList" | "hello">,
|
||||
rowKey: string | undefined | null,
|
||||
selectedSessionKey: string | undefined | null,
|
||||
): boolean {
|
||||
if (areUiSessionKeysEquivalent(rowKey, selectedSessionKey)) {
|
||||
return true;
|
||||
}
|
||||
return Boolean(
|
||||
isUiGlobalSessionKey(rowKey) && resolveUiGlobalAliasAgentId(host, selectedSessionKey),
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizeAgentId(value: string | undefined | null): string {
|
||||
const trimmed = normalizeOptionalString(value) ?? "";
|
||||
if (!trimmed) {
|
||||
|
||||
Reference in New Issue
Block a user