fix(gateway): add lightweight row path for sessions.list to reduce event-loop blocking

sessions.list calls buildGatewaySessionRow for every visible session,
running transcript usage fallback, display model inference, cost/context
recomputation, thinking level enumeration, agent runtime metadata, and
plugin extension projection per row. On installs with 30-50+ sessions
this blocks the event loop for 20-80+ seconds, starving Discord
heartbeats and Control UI RPCs.

Add skipTranscriptUsageFallback and lightweightListRow flags to
buildGatewaySessionRow. In lightweight mode, skip transcript usage
fallback, display model inference, cost/context recomputation, thinking
level options, agent runtime metadata, and plugin extension projection.
Use persisted entry fields directly for cost, tokens, and model identity.

listSessionsFromStoreAsync now passes both flags for bulk list rows.
Detail endpoints and single-row loads are unaffected.

Observed improvement on a production install (33 sessions):
sessions.list row construction dropped from ~82s to ~6s.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Marvinthebored
2026-05-03 11:09:43 +08:00
parent a58624293b
commit 803879ec54

View File

@@ -1430,8 +1430,12 @@ export function buildGatewaySessionRow(params: {
transcriptUsageMaxBytes?: number;
storeChildSessionsByKey?: Map<string, string[]>;
rowContext?: SessionListRowContext;
skipTranscriptUsageFallback?: boolean;
lightweightListRow?: boolean;
}): GatewaySessionRow {
const { cfg, storePath, store, key, entry } = params;
const lightweight = params.lightweightListRow === true;
const skipTranscriptUsage = params.skipTranscriptUsageFallback === true;
const now = params.now ?? Date.now();
const updatedAt = entry?.updatedAt ?? null;
const parsed = parseGroupKey(key);
@@ -1533,7 +1537,8 @@ export function buildGatewaySessionRow(params: {
entry,
}) === undefined;
const transcriptUsage =
needsTranscriptTotalTokens || needsTranscriptContextTokens || needsTranscriptEstimatedCostUsd
!skipTranscriptUsage &&
(needsTranscriptTotalTokens || needsTranscriptContextTokens || needsTranscriptEstimatedCostUsd)
? resolveTranscriptUsageFallback({
cfg,
key,
@@ -1577,36 +1582,39 @@ export function buildGatewaySessionRow(params: {
const latestCompactionCheckpoint = buildCompactionCheckpointPreview(
resolveLatestCompactionCheckpoint(entry),
);
const agentRuntime = resolveAgentRuntimeMetadata(cfg, sessionAgentId);
const agentRuntime = lightweight ? undefined : resolveAgentRuntimeMetadata(cfg, sessionAgentId);
const selectedOrRuntimeModelProvider = selectedModel?.provider ?? modelProvider;
const selectedOrRuntimeModel = selectedModel?.model ?? model;
const rowModelIdentity = resolveSessionDisplayModelIdentityRef({
cfg,
agentId: sessionAgentId,
provider: selectedOrRuntimeModelProvider,
model: selectedOrRuntimeModel,
});
const rowModelIdentity = lightweight
? { provider: selectedOrRuntimeModelProvider, model: selectedOrRuntimeModel }
: resolveSessionDisplayModelIdentityRef({
cfg,
agentId: sessionAgentId,
provider: selectedOrRuntimeModelProvider,
model: selectedOrRuntimeModel,
});
const rowModelProvider = rowModelIdentity.provider;
const rowModel = rowModelIdentity.model;
const estimatedCostUsd =
resolveEstimatedSessionCostUsd({
cfg,
provider: rowModelProvider,
model: rowModel,
entry,
}) ?? resolveNonNegativeNumber(transcriptUsage?.estimatedCostUsd);
const contextTokens =
resolvePositiveNumber(entry?.contextTokens) ??
resolvePositiveNumber(transcriptUsage?.contextTokens) ??
resolvePositiveNumber(
resolveContextTokensForModel({
const estimatedCostUsd = lightweight
? resolveNonNegativeNumber(entry?.estimatedCostUsd)
: resolveEstimatedSessionCostUsd({
cfg,
provider: rowModelProvider,
model: rowModel,
// Gateway/session listing is read-only; don't start async model discovery.
allowAsyncLoad: false,
}),
);
entry,
}) ?? resolveNonNegativeNumber(transcriptUsage?.estimatedCostUsd);
const contextTokens = lightweight
? resolvePositiveNumber(entry?.contextTokens)
: resolvePositiveNumber(entry?.contextTokens) ??
resolvePositiveNumber(transcriptUsage?.contextTokens) ??
resolvePositiveNumber(
resolveContextTokensForModel({
cfg,
provider: rowModelProvider,
model: rowModel,
allowAsyncLoad: false,
}),
);
let derivedTitle: string | undefined;
let lastMessagePreview: string | undefined;
@@ -1627,14 +1635,11 @@ export function buildGatewaySessionRow(params: {
const thinkingProvider = rowModelProvider ?? DEFAULT_PROVIDER;
const thinkingModel = rowModel ?? DEFAULT_MODEL;
const thinkingLevels = listThinkingLevelOptions(
thinkingProvider,
thinkingModel,
params.modelCatalog,
);
const pluginExtensions = entry
? projectPluginSessionExtensionsSync({ sessionKey: key, entry })
: [];
const thinkingLevels = lightweight
? []
: listThinkingLevelOptions(thinkingProvider, thinkingModel, params.modelCatalog);
const pluginExtensions =
!lightweight && entry ? projectPluginSessionExtensionsSync({ sessionKey: key, entry }) : [];
return {
key,
@@ -1662,13 +1667,15 @@ export function buildGatewaySessionRow(params: {
thinkingLevel: entry?.thinkingLevel,
thinkingLevels,
thinkingOptions: thinkingLevels.map((level) => level.label),
thinkingDefault: resolveGatewaySessionThinkingDefault({
cfg,
provider: thinkingProvider,
model: thinkingModel,
agentId: sessionAgentId,
modelCatalog: params.modelCatalog,
}),
thinkingDefault: lightweight
? entry?.thinkingLevel
: resolveGatewaySessionThinkingDefault({
cfg,
provider: thinkingProvider,
model: thinkingModel,
agentId: sessionAgentId,
modelCatalog: params.modelCatalog,
}),
fastMode: entry?.fastMode,
verboseLevel: entry?.verboseLevel,
traceLevel: entry?.traceLevel,
@@ -2016,6 +2023,8 @@ export async function listSessionsFromStoreAsync(params: {
transcriptUsageMaxBytes: sessionListTranscriptUsageMaxBytes,
storeChildSessionsByKey: getRowContext().storeChildSessionsByKey,
rowContext: getRowContext(),
skipTranscriptUsageFallback: true,
lightweightListRow: true,
});
if (
entry?.sessionId &&