From 803879ec54ac674f78230b50cc780d5300b8e78e Mon Sep 17 00:00:00 2001 From: Marvinthebored Date: Sun, 3 May 2026 11:09:43 +0800 Subject: [PATCH] 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 --- src/gateway/session-utils.ts | 87 ++++++++++++++++++++---------------- 1 file changed, 48 insertions(+), 39 deletions(-) diff --git a/src/gateway/session-utils.ts b/src/gateway/session-utils.ts index 519682cd4e1..fa3e7adc439 100644 --- a/src/gateway/session-utils.ts +++ b/src/gateway/session-utils.ts @@ -1430,8 +1430,12 @@ export function buildGatewaySessionRow(params: { transcriptUsageMaxBytes?: number; storeChildSessionsByKey?: Map; 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 &&