Skip to content

Commit 6b940ed

Browse files
authored
perf: streamline chat startup metadata (#88825)
* perf: streamline chat startup metadata * fix: defer global queued agent selection * style: format gateway startup refresh
1 parent 1b10739 commit 6b940ed

11 files changed

Lines changed: 301 additions & 212 deletions

src/gateway/server.chat.gateway-server-chat-b.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,81 @@ describe("gateway server chat", () => {
288288
}
289289
});
290290

291+
test("chat.history exposes persisted and synthetic session metadata for startup hydration", async () => {
292+
await withGatewayChatHarness(async ({ ws, createSessionDir }) => {
293+
await connectOk(ws);
294+
const sessionDir = await createSessionDir();
295+
const updatedAt = Date.now();
296+
await writeSessionStore({
297+
entries: {
298+
main: {
299+
sessionId: "sess-main",
300+
updatedAt,
301+
modelProvider: "openai",
302+
model: "gpt-5",
303+
contextTokens: 128_000,
304+
},
305+
},
306+
});
307+
await writeMainSessionTranscript(sessionDir, [
308+
JSON.stringify({
309+
message: {
310+
role: "user",
311+
content: [{ type: "text", text: "persisted metadata" }],
312+
timestamp: updatedAt,
313+
},
314+
}),
315+
]);
316+
317+
const persisted = await rpcReq<{
318+
defaults?: { modelProvider?: string | null; model?: string | null };
319+
sessionInfo?: {
320+
key?: string;
321+
sessionId?: string;
322+
updatedAt?: number | null;
323+
modelProvider?: string | null;
324+
model?: string | null;
325+
contextTokens?: number | null;
326+
};
327+
}>(ws, "chat.history", { sessionKey: "main" });
328+
329+
expect(persisted.ok).toBe(true);
330+
expect(persisted.payload?.defaults?.modelProvider).toBeTruthy();
331+
expect(persisted.payload?.defaults?.model).toBeTruthy();
332+
expect(persisted.payload?.sessionInfo).toMatchObject({
333+
key: "agent:main:main",
334+
sessionId: "sess-main",
335+
updatedAt,
336+
modelProvider: "openai",
337+
model: "gpt-5",
338+
contextTokens: 128_000,
339+
});
340+
341+
await writeSessionStore({ entries: {} });
342+
const synthetic = await rpcReq<{
343+
defaults?: { modelProvider?: string | null; model?: string | null };
344+
sessionInfo?: {
345+
key?: string;
346+
sessionId?: string;
347+
updatedAt?: number | null;
348+
modelProvider?: string | null;
349+
model?: string | null;
350+
contextTokens?: number | null;
351+
};
352+
}>(ws, "chat.history", { sessionKey: "main" });
353+
354+
expect(synthetic.ok).toBe(true);
355+
expect(synthetic.payload?.defaults?.modelProvider).toBeTruthy();
356+
expect(synthetic.payload?.defaults?.model).toBeTruthy();
357+
expect(synthetic.payload?.sessionInfo?.key).toBe("agent:main:main");
358+
expect(synthetic.payload?.sessionInfo?.sessionId).toBeUndefined();
359+
expect(synthetic.payload?.sessionInfo?.updatedAt).toBeNull();
360+
expect(synthetic.payload?.sessionInfo?.modelProvider).toBeTruthy();
361+
expect(synthetic.payload?.sessionInfo?.model).toBeTruthy();
362+
expect(synthetic.payload?.sessionInfo?.contextTokens).toEqual(expect.any(Number));
363+
});
364+
});
365+
291366
test("chat.send returns in_flight when duplicate attachment send wins parsing race", async () => {
292367
const sessionDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-"));
293368
const dispatchRelease = createDeferred<void>();

src/routing/session-key.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
resolveThreadParentSessionKey,
88
} from "../sessions/session-key-utils.js";
99
import {
10+
agentSessionKeysMatchByRequestKey,
1011
buildAgentPeerSessionKey,
1112
buildGroupHistoryKey,
1213
classifySessionKeyShape,
@@ -76,6 +77,14 @@ describe("isUnscopedSessionKeySentinel", () => {
7677
});
7778
});
7879

80+
describe("agentSessionKeysMatchByRequestKey", () => {
81+
it("matches canonical agent keys against their request-key aliases", () => {
82+
expect(agentSessionKeysMatchByRequestKey("agent:main:main", "main")).toBe(true);
83+
expect(agentSessionKeysMatchByRequestKey("agent:ops:incident-42", "incident-42")).toBe(true);
84+
expect(agentSessionKeysMatchByRequestKey("agent:ops:incident-42", "main")).toBe(false);
85+
});
86+
});
87+
7988
describe("session key backward compatibility", () => {
8089
function expectBackwardCompatibleDirectSessionKey(key: string) {
8190
expect(classifySessionKeyShape(key)).toBe("agent");

src/routing/session-key.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,20 @@ export function toAgentRequestSessionKey(storeKey: string | undefined | null): s
9292
return parseAgentSessionKey(raw)?.rest ?? raw;
9393
}
9494

95+
export function agentSessionKeysMatchByRequestKey(
96+
left: string | undefined | null,
97+
right: string | undefined | null,
98+
): boolean {
99+
const leftRaw = (left ?? "").trim();
100+
const rightRaw = (right ?? "").trim();
101+
if (!leftRaw || !rightRaw) {
102+
return false;
103+
}
104+
return (
105+
leftRaw === rightRaw || toAgentRequestSessionKey(leftRaw) === toAgentRequestSessionKey(rightRaw)
106+
);
107+
}
108+
95109
export function toAgentStoreSessionKey(params: {
96110
agentId: string;
97111
requestKey: string | undefined | null;

src/tui/tui-session-actions.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { normalizeOptionalString } from "@openclaw/normalization-core/string-coe
33
import type { SessionsPatchResult } from "../../packages/gateway-protocol/src/index.js";
44
import { resolveSessionInfoModelSelection } from "../agents/model-selection-display.js";
55
import {
6+
agentSessionKeysMatchByRequestKey,
67
normalizeAgentId,
78
normalizeMainKey,
89
parseAgentSessionKey,
@@ -312,15 +313,8 @@ export function createSessionActions(context: SessionActionContext) {
312313
includeUnknown: state.currentSessionKey === "unknown",
313314
agentId: listAgentId,
314315
});
315-
const normalizeMatchKey = (key: string) => parseAgentSessionKey(key)?.rest ?? key;
316-
const currentMatchKey = normalizeMatchKey(state.currentSessionKey);
317316
const entry = result.sessions.find((row) => {
318-
// Exact match
319-
if (row.key === state.currentSessionKey) {
320-
return true;
321-
}
322-
// Also match canonical keys like "agent:default:main" against "main"
323-
return normalizeMatchKey(row.key) === currentMatchKey;
317+
return agentSessionKeysMatchByRequestKey(row.key, state.currentSessionKey);
324318
});
325319
if (entry?.key && entry.key !== state.currentSessionKey) {
326320
updateAgentFromSessionKey(entry.key);

ui/src/ui/app-chat.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1847,6 +1847,43 @@ describe("handleSendChat", () => {
18471847
expect(host.chatStream).toBeNull();
18481848
});
18491849

1850+
it("defers queued global send agent selection until defaults are known", async () => {
1851+
const request = vi.fn((method: string) => {
1852+
if (method === "chat.send") {
1853+
return Promise.resolve({ runId: "run-work", status: "started" });
1854+
}
1855+
throw new Error(`Unexpected request: ${method}`);
1856+
});
1857+
const host = makeHost({
1858+
client: null,
1859+
connected: false,
1860+
sessionKey: "global",
1861+
chatMessage: "send to default later",
1862+
});
1863+
1864+
await handleSendChat(host);
1865+
1866+
expect(host.chatQueue[0]).toMatchObject({
1867+
text: "send to default later",
1868+
sessionKey: "global",
1869+
sendState: "waiting-reconnect",
1870+
});
1871+
expect(host.chatQueue[0]?.agentId).toBeUndefined();
1872+
1873+
host.agentsList = { defaultId: "work" };
1874+
host.client = { request } as unknown as ChatHost["client"];
1875+
host.connected = true;
1876+
await retryReconnectableQueuedChatSends(host);
1877+
1878+
const payload = findRequestPayload(
1879+
request as unknown as MockCallSource,
1880+
"chat.send",
1881+
"queued global send payload",
1882+
);
1883+
expect(payload.sessionKey).toBe("global");
1884+
expect(payload.agentId).toBe("work");
1885+
});
1886+
18501887
it("marks saved session queued sends waiting after a disconnect", () => {
18511888
const host = makeHost({
18521889
chatQueue: [],

ui/src/ui/app-chat.ts

Lines changed: 24 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,13 @@ import { normalizeBasePath } from "./navigation.ts";
4545
import {
4646
areUiSessionKeysEquivalent,
4747
DEFAULT_AGENT_ID,
48+
isUiGlobalSessionKey,
4849
normalizeAgentId,
4950
parseAgentSessionKey,
51+
resolveUiDefaultAgentId,
52+
resolveUiGlobalAliasAgentId,
53+
resolveUiKnownSelectedGlobalAgentId,
54+
resolveUiSelectedGlobalAgentId,
5055
} from "./session-key.ts";
5156
import { isSessionRunActive } from "./session-run-state.ts";
5257
import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "./string-coerce.ts";
@@ -219,61 +224,17 @@ function isBtwCommand(text: string) {
219224
return /^\/(?:btw|side)(?::|\s|$)/i.test(text.trim());
220225
}
221226

222-
function isGlobalSessionKey(sessionKey: string | undefined | null): boolean {
223-
return normalizeLowercaseStringOrEmpty(sessionKey) === "global";
224-
}
225-
226227
function readHelloDefaultAgentId(host: Pick<ChatHost, "hello">): string | undefined {
227228
const snapshot = host.hello?.snapshot as
228229
| { sessionDefaults?: SessionDefaultsSnapshot }
229230
| undefined;
230231
return snapshot?.sessionDefaults?.defaultAgentId?.trim() || undefined;
231232
}
232233

233-
function readHelloMainKey(host: Pick<ChatHost, "hello">): string | undefined {
234-
const snapshot = host.hello?.snapshot as
235-
| { sessionDefaults?: SessionDefaultsSnapshot }
236-
| undefined;
237-
return snapshot?.sessionDefaults?.mainKey?.trim() || undefined;
238-
}
239-
240-
function resolveGlobalAliasAgentId(
241-
host: Pick<ChatHost, "agentsList" | "hello">,
242-
sessionKey: string | undefined | null,
243-
): string | undefined {
244-
const parsed = parseAgentSessionKey(sessionKey);
245-
if (!parsed) {
246-
return undefined;
247-
}
248-
const rest = normalizeLowercaseStringOrEmpty(parsed.rest);
249-
const configuredMainKey = normalizeLowercaseStringOrEmpty(
250-
host.agentsList?.mainKey ?? readHelloMainKey(host) ?? "main",
251-
);
252-
return rest === "global" || rest === "main" || rest === configuredMainKey
253-
? normalizeAgentId(parsed.agentId)
254-
: undefined;
255-
}
256-
257-
function resolveSelectedGlobalAgentId(
258-
host: Pick<ChatHost, "assistantAgentId" | "agentsList" | "hello">,
259-
): string | undefined {
260-
const agentId =
261-
host.assistantAgentId?.trim() ||
262-
host.agentsList?.defaultId?.trim() ||
263-
readHelloDefaultAgentId(host);
264-
return agentId ? normalizeAgentId(agentId) : undefined;
265-
}
266-
267-
function resolveDefaultAgentIdForList(host: Pick<ChatHost, "agentsList" | "hello">): string {
268-
return normalizeAgentId(
269-
host.agentsList?.defaultId ?? readHelloDefaultAgentId(host) ?? DEFAULT_AGENT_ID,
270-
);
271-
}
272-
273234
function scopedAgentIdForSession(host: ChatHost, sessionKey: string | undefined | null) {
274-
return isGlobalSessionKey(sessionKey)
275-
? resolveSelectedGlobalAgentId(host)
276-
: resolveGlobalAliasAgentId(host, sessionKey);
235+
return isUiGlobalSessionKey(sessionKey)
236+
? resolveUiKnownSelectedGlobalAgentId(host)
237+
: (resolveUiGlobalAliasAgentId(host, sessionKey) ?? undefined);
277238
}
278239

279240
function visibleSessionMatches(
@@ -282,19 +243,19 @@ function visibleSessionMatches(
282243
agentId: string | undefined,
283244
): boolean {
284245
if (host.sessionKey !== sessionKey) {
285-
const hostAliasAgentId = resolveGlobalAliasAgentId(host, host.sessionKey);
286-
if (!hostAliasAgentId || !isGlobalSessionKey(sessionKey)) {
246+
const hostAliasAgentId = resolveUiGlobalAliasAgentId(host, host.sessionKey);
247+
if (!hostAliasAgentId || !isUiGlobalSessionKey(sessionKey)) {
287248
return false;
288249
}
289250
const expectedAgentId = agentId ?? host.agentsList?.defaultId ?? readHelloDefaultAgentId(host);
290251
return expectedAgentId
291252
? hostAliasAgentId === normalizeAgentId(expectedAgentId)
292253
: hostAliasAgentId === normalizeAgentId("main");
293254
}
294-
if (!isGlobalSessionKey(sessionKey)) {
255+
if (!isUiGlobalSessionKey(sessionKey)) {
295256
return true;
296257
}
297-
const selectedAgentId = resolveSelectedGlobalAgentId(host);
258+
const selectedAgentId = resolveUiKnownSelectedGlobalAgentId(host);
298259
const expectedAgentId = agentId ?? host.agentsList?.defaultId ?? readHelloDefaultAgentId(host);
299260
return expectedAgentId
300261
? selectedAgentId === normalizeAgentId(expectedAgentId)
@@ -305,9 +266,9 @@ export function scopedAgentParamsForSession(
305266
host: Pick<ChatHost, "assistantAgentId" | "agentsList" | "hello">,
306267
sessionKey: string,
307268
) {
308-
const agentId = isGlobalSessionKey(sessionKey)
309-
? resolveSelectedGlobalAgentId(host)
310-
: resolveGlobalAliasAgentId(host, sessionKey);
269+
const agentId = isUiGlobalSessionKey(sessionKey)
270+
? resolveUiKnownSelectedGlobalAgentId(host)
271+
: resolveUiGlobalAliasAgentId(host, sessionKey);
311272
return agentId ? { agentId } : {};
312273
}
313274

@@ -320,10 +281,10 @@ export function scopedAgentListParamsForSession(
320281
const agentId =
321282
parsed?.agentId ??
322283
(normalizedSessionKey === "global"
323-
? resolveSelectedGlobalAgentId(host)
284+
? resolveUiKnownSelectedGlobalAgentId(host)
324285
: normalizedSessionKey === "unknown"
325286
? undefined
326-
: resolveDefaultAgentIdForList(host));
287+
: resolveUiDefaultAgentId(host));
327288
return agentId ? { agentId: normalizeAgentId(agentId) } : {};
328289
}
329290

@@ -986,8 +947,8 @@ function isHistorySessionInfoForRequestedSession(
986947
}
987948
return Boolean(
988949
historySessionKey &&
989-
isGlobalSessionKey(historySessionKey) &&
990-
resolveGlobalAliasAgentId(host, requestedSessionKey),
950+
isUiGlobalSessionKey(historySessionKey) &&
951+
resolveUiGlobalAliasAgentId(host, requestedSessionKey),
991952
);
992953
}
993954

@@ -998,16 +959,16 @@ function findSelectedSessionRow(
998959
historySessionKey: string | undefined,
999960
): GatewaySessionRow | undefined {
1000961
const requestedGlobalAgentId =
1001-
historySessionKey && isGlobalSessionKey(historySessionKey)
1002-
? resolveGlobalAliasAgentId(host, sessionKey)
962+
historySessionKey && isUiGlobalSessionKey(historySessionKey)
963+
? resolveUiGlobalAliasAgentId(host, sessionKey)
1003964
: undefined;
1004965
return sessionsResult?.sessions.find((session) => {
1005966
if (areUiSessionKeysEquivalent(session.key, sessionKey)) {
1006967
return true;
1007968
}
1008969
return (
1009970
requestedGlobalAgentId != null &&
1010-
resolveGlobalAliasAgentId(host, session.key) === requestedGlobalAgentId
971+
resolveUiGlobalAliasAgentId(host, session.key) === requestedGlobalAgentId
1011972
);
1012973
});
1013974
}
@@ -1558,8 +1519,8 @@ function resolveAgentIdForSession(host: ChatHost): string | null {
15581519
if (parsed?.agentId) {
15591520
return parsed.agentId;
15601521
}
1561-
if (isGlobalSessionKey(host.sessionKey)) {
1562-
return resolveSelectedGlobalAgentId(host) || DEFAULT_AGENT_ID;
1522+
if (isUiGlobalSessionKey(host.sessionKey)) {
1523+
return resolveUiSelectedGlobalAgentId(host) || DEFAULT_AGENT_ID;
15631524
}
15641525
return readHelloDefaultAgentId(host) || DEFAULT_AGENT_ID;
15651526
}

0 commit comments

Comments
 (0)