Skip to content

Commit 2682c02

Browse files
authored
perf: hydrate chat history session metadata
Use chat.history metadata to hydrate TUI and web startup state without the extra sessions.list refresh, with guards for aliases, stale active rows, blank-session defaults, and lightweight TUI usage metadata.
1 parent 5968397 commit 2682c02

20 files changed

Lines changed: 1831 additions & 245 deletions

src/gateway/server-methods/chat.ts

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -133,11 +133,12 @@ import { resolveSessionHistoryTailReadOptions } from "../session-history-state.j
133133
import { readSessionTranscriptIndex } from "../session-transcript-index.fs.js";
134134
import {
135135
capArrayByJsonBytes,
136+
buildGatewaySessionInfo,
137+
getSessionDefaults,
136138
loadSessionEntry,
137139
readSessionMessageByIdAsync,
138140
readSessionMessagesAsync,
139141
resolveGatewayModelSupportsImages,
140-
resolveGatewaySessionThinkingDefault,
141142
resolveDeletedAgentIdFromSessionKey,
142143
readRecentSessionMessagesAsync,
143144
resolveSessionModelRef,
@@ -156,6 +157,8 @@ import {
156157
buildWebchatAssistantMessageFromReplyPayloads,
157158
buildWebchatAudioContentBlocksFromReplyPayloads,
158159
} from "./chat-webchat-media.js";
160+
import { hasTrackedActiveSessionRun } from "./session-active-runs.js";
161+
import { loadOptionalSessionMetadataModelCatalog } from "./session-model-catalog.js";
159162
import type {
160163
GatewayRequestContext,
161164
GatewayRequestHandlerOptions,
@@ -2418,7 +2421,10 @@ export const chatHandlers: GatewayRequestHandlers = {
24182421
agentId: agentIdOverride,
24192422
});
24202423
const sessionLoadOptions = requestedAgentId ? { agentId: requestedAgentId } : undefined;
2421-
const { cfg, storePath, entry } = loadSessionEntry(sessionKey, sessionLoadOptions);
2424+
const { cfg, storePath, store, entry, canonicalKey } = loadSessionEntry(
2425+
sessionKey,
2426+
sessionLoadOptions,
2427+
);
24222428
const selectedAgent = validateChatSelectedAgent({
24232429
cfg,
24242430
requestedSessionKey: sessionKey,
@@ -2508,20 +2514,42 @@ export const chatHandlers: GatewayRequestHandlers = {
25082514
`chat.history omitted oversized payloads placeholders=${placeholderCount} total=${chatHistoryPlaceholderEmitCount}`,
25092515
);
25102516
}
2511-
let thinkingLevel = entry?.thinkingLevel;
2512-
if (!thinkingLevel) {
2513-
thinkingLevel = resolveGatewaySessionThinkingDefault({
2514-
cfg,
2515-
agentId: sessionAgentId,
2516-
provider: resolvedSessionModel.provider,
2517-
model: resolvedSessionModel.model,
2518-
});
2519-
}
2517+
const modelCatalog = await measureDiagnosticsTimelineSpan(
2518+
"gateway.chat.history.model_catalog",
2519+
() => loadOptionalSessionMetadataModelCatalog(context, "chat.history"),
2520+
{
2521+
config: cfg,
2522+
phase: "chat.history",
2523+
},
2524+
);
2525+
const sessionInfo = buildGatewaySessionInfo({
2526+
cfg,
2527+
storePath,
2528+
store,
2529+
key: canonicalKey,
2530+
entry,
2531+
agentId: selectedAgent.agentId,
2532+
modelCatalog,
2533+
});
2534+
sessionInfo.hasActiveRun = hasTrackedActiveSessionRun({
2535+
context,
2536+
requestedKey: sessionKey,
2537+
canonicalKey,
2538+
...(canonicalKey === "global" && selectedAgent.agentId
2539+
? { agentId: selectedAgent.agentId }
2540+
: {}),
2541+
defaultAgentId: resolveDefaultAgentId(cfg),
2542+
});
2543+
const defaults = getSessionDefaults(cfg, modelCatalog, { allowPluginNormalization: false });
2544+
const thinkingLevel = sessionInfo.thinkingLevel ?? sessionInfo.thinkingDefault;
25202545
const verboseLevel = entry?.verboseLevel ?? cfg.agents?.defaults?.verboseDefault;
2546+
sessionInfo.verboseLevel = verboseLevel;
25212547
respond(true, {
25222548
sessionKey,
25232549
sessionId,
25242550
messages: bounded.messages,
2551+
defaults,
2552+
sessionInfo,
25252553
thinkingLevel,
25262554
fastMode: entry?.fastMode,
25272555
verboseLevel,
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { normalizeAgentId } from "../../routing/session-key.js";
2+
import type { GatewayRequestContext } from "./types.js";
3+
4+
type TrackedActiveSessionRun = {
5+
sessionKey: string;
6+
agentId?: string;
7+
};
8+
9+
function collectTrackedActiveSessionRuns(
10+
context: Partial<Pick<GatewayRequestContext, "chatAbortControllers">>,
11+
): TrackedActiveSessionRun[] {
12+
const runs: TrackedActiveSessionRun[] = [];
13+
if (!(context.chatAbortControllers instanceof Map)) {
14+
return runs;
15+
}
16+
for (const active of context.chatAbortControllers.values()) {
17+
if (
18+
active.projectSessionActive !== false &&
19+
typeof active.sessionKey === "string" &&
20+
active.sessionKey.trim()
21+
) {
22+
runs.push({
23+
sessionKey: active.sessionKey,
24+
agentId: typeof active.agentId === "string" ? normalizeAgentId(active.agentId) : undefined,
25+
});
26+
}
27+
}
28+
return runs;
29+
}
30+
31+
function isTrackedActiveSessionRunForKey(
32+
active: TrackedActiveSessionRun,
33+
key: string,
34+
agentId?: string,
35+
defaultAgentId?: string,
36+
): boolean {
37+
if (active.sessionKey !== key) {
38+
return false;
39+
}
40+
if (key !== "global" || agentId === undefined) {
41+
return true;
42+
}
43+
const activeAgentId = active.agentId ?? defaultAgentId;
44+
return activeAgentId ? normalizeAgentId(activeAgentId) === normalizeAgentId(agentId) : false;
45+
}
46+
47+
export function hasTrackedActiveSessionRun(params: {
48+
context: Partial<Pick<GatewayRequestContext, "chatAbortControllers">>;
49+
requestedKey: string;
50+
canonicalKey: string;
51+
agentId?: string;
52+
defaultAgentId?: string;
53+
}): boolean {
54+
const activeRuns = collectTrackedActiveSessionRuns(params.context);
55+
return activeRuns.some(
56+
(active) =>
57+
isTrackedActiveSessionRunForKey(
58+
active,
59+
params.canonicalKey,
60+
params.agentId,
61+
params.defaultAgentId,
62+
) ||
63+
isTrackedActiveSessionRunForKey(
64+
active,
65+
params.requestedKey,
66+
params.agentId,
67+
params.defaultAgentId,
68+
),
69+
);
70+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type { ModelCatalogEntry } from "../../agents/model-catalog.js";
2+
import type { GatewayRequestContext } from "./types.js";
3+
4+
const SESSION_METADATA_MODEL_CATALOG_TIMEOUT_MS = 750;
5+
6+
let loggedSlowSessionMetadataCatalog = false;
7+
8+
export async function loadOptionalSessionMetadataModelCatalog(
9+
context: GatewayRequestContext,
10+
surface: string,
11+
): Promise<ModelCatalogEntry[] | undefined> {
12+
let timeout: NodeJS.Timeout | undefined;
13+
const timedOut = Symbol("session-metadata-model-catalog-timeout");
14+
const timeoutPromise = new Promise<typeof timedOut>((resolve) => {
15+
timeout = setTimeout(() => resolve(timedOut), SESSION_METADATA_MODEL_CATALOG_TIMEOUT_MS);
16+
timeout.unref?.();
17+
});
18+
try {
19+
const result = await Promise.race([
20+
context.loadGatewayModelCatalog().catch(() => undefined),
21+
timeoutPromise,
22+
]);
23+
if (result === timedOut) {
24+
if (!loggedSlowSessionMetadataCatalog) {
25+
loggedSlowSessionMetadataCatalog = true;
26+
context.logGateway.debug(
27+
`${surface} continuing without model catalog after ${SESSION_METADATA_MODEL_CATALOG_TIMEOUT_MS}ms`,
28+
);
29+
}
30+
return undefined;
31+
}
32+
return Array.isArray(result) ? result : undefined;
33+
} finally {
34+
if (timeout) {
35+
clearTimeout(timeout);
36+
}
37+
}
38+
}

src/gateway/server-methods/sessions.ts

Lines changed: 10 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ import { applySessionsPatchToStore } from "../sessions-patch.js";
120120
import { resolveSessionKeyFromResolveParams } from "../sessions-resolve.js";
121121
import { setGatewayDedupeEntry } from "./agent-wait-dedupe.js";
122122
import { chatHandlers } from "./chat.js";
123+
import { hasTrackedActiveSessionRun } from "./session-active-runs.js";
124+
import { loadOptionalSessionMetadataModelCatalog } from "./session-model-catalog.js";
123125
import type {
124126
GatewayClient,
125127
GatewayRequestContext,
@@ -197,46 +199,12 @@ function inheritSessionRuntimeSelection(
197199
type SessionsRuntimeModule = typeof import("./sessions.runtime.js");
198200

199201
let sessionsRuntimeModulePromise: Promise<SessionsRuntimeModule> | undefined;
200-
let loggedSlowSessionsListCatalog = false;
201-
202-
const SESSIONS_LIST_MODEL_CATALOG_TIMEOUT_MS = 750;
203202

204203
function loadSessionsRuntimeModule(): Promise<SessionsRuntimeModule> {
205204
sessionsRuntimeModulePromise ??= import("./sessions.runtime.js");
206205
return sessionsRuntimeModulePromise;
207206
}
208207

209-
async function loadOptionalSessionsListModelCatalog(
210-
context: GatewayRequestContext,
211-
): Promise<Awaited<ReturnType<GatewayRequestContext["loadGatewayModelCatalog"]>> | undefined> {
212-
let timeout: NodeJS.Timeout | undefined;
213-
const timedOut = Symbol("sessions-list-model-catalog-timeout");
214-
const timeoutPromise = new Promise<typeof timedOut>((resolve) => {
215-
timeout = setTimeout(() => resolve(timedOut), SESSIONS_LIST_MODEL_CATALOG_TIMEOUT_MS);
216-
timeout.unref?.();
217-
});
218-
try {
219-
const result = await Promise.race([
220-
context.loadGatewayModelCatalog().catch(() => undefined),
221-
timeoutPromise,
222-
]);
223-
if (result === timedOut) {
224-
if (!loggedSlowSessionsListCatalog) {
225-
loggedSlowSessionsListCatalog = true;
226-
context.logGateway.debug(
227-
`sessions.list continuing without model catalog after ${SESSIONS_LIST_MODEL_CATALOG_TIMEOUT_MS}ms`,
228-
);
229-
}
230-
return undefined;
231-
}
232-
return Array.isArray(result) ? result : undefined;
233-
} finally {
234-
if (timeout) {
235-
clearTimeout(timeout);
236-
}
237-
}
238-
}
239-
240208
function requireSessionKey(key: unknown, respond: RespondFn): string | null {
241209
const raw =
242210
typeof key === "string"
@@ -723,74 +691,6 @@ function resolveScopedAbortKey(params: {
723691
});
724692
}
725693

726-
type TrackedActiveSessionRun = {
727-
sessionKey: string;
728-
agentId?: string;
729-
};
730-
731-
function collectTrackedActiveSessionRuns(
732-
context: Partial<Pick<GatewayRequestContext, "chatAbortControllers">>,
733-
): TrackedActiveSessionRun[] {
734-
const runs: TrackedActiveSessionRun[] = [];
735-
if (!(context.chatAbortControllers instanceof Map)) {
736-
return runs;
737-
}
738-
for (const active of context.chatAbortControllers.values()) {
739-
if (
740-
active.projectSessionActive !== false &&
741-
typeof active.sessionKey === "string" &&
742-
active.sessionKey.trim()
743-
) {
744-
runs.push({
745-
sessionKey: active.sessionKey,
746-
agentId: typeof active.agentId === "string" ? normalizeAgentId(active.agentId) : undefined,
747-
});
748-
}
749-
}
750-
return runs;
751-
}
752-
753-
function isTrackedActiveSessionRunForKey(
754-
active: TrackedActiveSessionRun,
755-
key: string,
756-
agentId?: string,
757-
defaultAgentId?: string,
758-
): boolean {
759-
if (active.sessionKey !== key) {
760-
return false;
761-
}
762-
if (key !== "global" || agentId === undefined) {
763-
return true;
764-
}
765-
const activeAgentId = active.agentId ?? defaultAgentId;
766-
return activeAgentId ? normalizeAgentId(activeAgentId) === normalizeAgentId(agentId) : false;
767-
}
768-
769-
function hasTrackedActiveSessionRun(params: {
770-
context: Partial<Pick<GatewayRequestContext, "chatAbortControllers">>;
771-
requestedKey: string;
772-
canonicalKey: string;
773-
agentId?: string;
774-
defaultAgentId?: string;
775-
}): boolean {
776-
const activeRuns = collectTrackedActiveSessionRuns(params.context);
777-
return activeRuns.some(
778-
(active) =>
779-
isTrackedActiveSessionRunForKey(
780-
active,
781-
params.canonicalKey,
782-
params.agentId,
783-
params.defaultAgentId,
784-
) ||
785-
isTrackedActiveSessionRunForKey(
786-
active,
787-
params.requestedKey,
788-
params.agentId,
789-
params.defaultAgentId,
790-
),
791-
);
792-
}
793-
794694
function resolveSessionMessageSubscriptionKey(params: {
795695
canonicalKey: string;
796696
agentId?: string;
@@ -1142,7 +1042,7 @@ export const sessionsHandlers: GatewayRequestHandlers = {
11421042
: store;
11431043
const modelCatalog = await measureDiagnosticsTimelineSpan(
11441044
"gateway.sessions.list.model_catalog",
1145-
() => loadOptionalSessionsListModelCatalog(context),
1045+
() => loadOptionalSessionMetadataModelCatalog(context, "sessions.list"),
11461046
{
11471047
config: cfg,
11481048
phase: "sessions.list",
@@ -1169,17 +1069,15 @@ export const sessionsHandlers: GatewayRequestHandlers = {
11691069
const sessions = measureDiagnosticsTimelineSpanSync(
11701070
"gateway.sessions.list.active_run_flags",
11711071
() => {
1172-
const activeRuns = collectTrackedActiveSessionRuns(context);
11731072
return result.sessions.map((session) =>
11741073
Object.assign({}, session, {
1175-
hasActiveRun: activeRuns.some((active) =>
1176-
isTrackedActiveSessionRunForKey(
1177-
active,
1178-
session.key,
1179-
session.key === "global" ? p.agentId : undefined,
1180-
resolveDefaultAgentId(cfg),
1181-
),
1182-
),
1074+
hasActiveRun: hasTrackedActiveSessionRun({
1075+
context,
1076+
requestedKey: session.key,
1077+
canonicalKey: session.key,
1078+
...(session.key === "global" && p.agentId ? { agentId: p.agentId } : {}),
1079+
defaultAgentId: resolveDefaultAgentId(cfg),
1080+
}),
11831081
}),
11841082
);
11851083
},

0 commit comments

Comments
 (0)