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:
Peter Steinberger
2026-05-31 21:18:41 -04:00
committed by GitHub
parent 1b10739d60
commit 6b940ed3ca
11 changed files with 301 additions and 212 deletions

View File

@@ -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>();

View File

@@ -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");

View File

@@ -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;

View File

@@ -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);

View File

@@ -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: [],

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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":

View File

@@ -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(

View File

@@ -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,

View File

@@ -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) {