From 6c63c57fe263a2df231a5d0242d49bb2bfc3f302 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 1 Aug 2026 17:22:49 -0700 Subject: [PATCH] perf(sessions): reuse plugin metadata during listing (#117707) --- src/agents/model-selection-cli.ts | 18 +++- src/agents/model-selection.ts | 6 +- src/commands/sessions-display-model.ts | 15 ++- src/commands/sessions.plugin-metadata.test.ts | 91 +++++++++++++++++++ src/commands/sessions.ts | 45 ++++----- src/plugins/setup-registry.runtime.ts | 7 ++ src/status/agent-runtime-label.ts | 5 +- 7 files changed, 151 insertions(+), 36 deletions(-) create mode 100644 src/commands/sessions.plugin-metadata.test.ts diff --git a/src/agents/model-selection-cli.ts b/src/agents/model-selection-cli.ts index d80fe3b8e5d5..6358cd1978b2 100644 --- a/src/agents/model-selection-cli.ts +++ b/src/agents/model-selection-cli.ts @@ -3,9 +3,25 @@ */ import type { OpenClawConfig } from "../config/types.openclaw.js"; import { resolveRuntimeCliBackends } from "../plugins/cli-backends.runtime.js"; -import { resolvePluginSetupCliBackendDescriptor } from "../plugins/setup-registry.runtime.js"; +import { + resolvePluginSetupCliBackendDescriptor, + resolvePluginSetupCliBackendIds, +} from "../plugins/setup-registry.runtime.js"; import { normalizeProviderId } from "./model-ref-shared.js"; +export type CliProviderClassifier = (provider: string) => boolean; + +/** Prepare one CLI-provider lookup for request paths that classify multiple models. */ +export function prepareCliProviderClassifier(cfg?: OpenClawConfig): CliProviderClassifier { + const providers = new Set( + [ + ...resolveRuntimeCliBackends().map((backend) => backend.id), + ...resolvePluginSetupCliBackendIds({ config: cfg }), + ].map(normalizeProviderId), + ); + return (provider) => providers.has(normalizeProviderId(provider)); +} + /** Return true when a provider id resolves to a configured or plugin CLI backend. */ export function isCliProvider(provider: string, cfg?: OpenClawConfig): boolean { const normalized = normalizeProviderId(provider); diff --git a/src/agents/model-selection.ts b/src/agents/model-selection.ts index c5c7d55e4d4f..721a48c2c0a1 100644 --- a/src/agents/model-selection.ts +++ b/src/agents/model-selection.ts @@ -79,7 +79,11 @@ export { resolveModelAliasFromPair, resolveModelRefFromString, }; -export { isCliProvider } from "./model-selection-cli.js"; +export { + isCliProvider, + prepareCliProviderClassifier, + type CliProviderClassifier, +} from "./model-selection-cli.js"; // Cron imports this narrow owner directly; the public facade must not fork its policy. export { getModelRefStatus } from "./model-selection-resolve.js"; diff --git a/src/commands/sessions-display-model.ts b/src/commands/sessions-display-model.ts index 9dcdb53d2c79..7c832752ecdf 100644 --- a/src/commands/sessions-display-model.ts +++ b/src/commands/sessions-display-model.ts @@ -9,6 +9,7 @@ import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js"; import { inferUniqueProviderFromConfiguredModels, isCliProvider, + type CliProviderClassifier, } from "../agents/model-selection.js"; import { resolveAgentModelPrimaryValue } from "../config/model-input.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; @@ -95,15 +96,16 @@ function normalizeCliRuntimeDisplayRef( cfg: OpenClawConfig, ref: SessionDisplayModelRef, defaultRef: SessionDisplayModelRef, + classifyCliProvider: CliProviderClassifier, ): SessionDisplayModelRef { - if (!isCliProvider(ref.provider, cfg)) { + if (!classifyCliProvider(ref.provider)) { return ref; } if (ref.model.includes("/")) { // CLI runtimes can store the real provider/model inside the model field; // prefer that embedded provider when it is not another CLI runtime alias. const parsed = parseModelRef(ref.model, defaultRef.provider); - if (!isCliProvider(parsed.provider, cfg)) { + if (!classifyCliProvider(parsed.provider)) { return parsed; } } @@ -111,13 +113,13 @@ function normalizeCliRuntimeDisplayRef( cfg, model: ref.model, }); - if (inferredProvider && !isCliProvider(inferredProvider, cfg)) { + if (inferredProvider && !classifyCliProvider(inferredProvider)) { return { provider: inferredProvider, model: ref.model }; } // If the CLI runtime model cannot be mapped to a concrete provider, fall // back to the configured default provider so rows stay comparable. const parsed = parseModelRef(ref.model, defaultRef.provider); - if (!isCliProvider(parsed.provider, cfg)) { + if (!classifyCliProvider(parsed.provider)) { return parsed; } return { @@ -130,14 +132,16 @@ function normalizeCliRuntimeDisplayRef( export function resolveSessionDisplayModel( cfg: OpenClawConfig, row: SessionDisplayModelRow, + classifyCliProvider?: CliProviderClassifier, ): string { - return resolveSessionDisplayModelRef(cfg, row).model; + return resolveSessionDisplayModelRef(cfg, row, classifyCliProvider).model; } /** Resolves provider/model display metadata for a session row. */ export function resolveSessionDisplayModelRef( cfg: OpenClawConfig, row: SessionDisplayModelRow, + classifyCliProvider: CliProviderClassifier = (provider) => isCliProvider(provider, cfg), ): SessionDisplayModelRef { const agentId = row.key.startsWith("agent:") ? row.key.split(":")[1] : undefined; const defaultRef = resolveDefaultModelRef(cfg, agentId); @@ -157,6 +161,7 @@ export function resolveSessionDisplayModelRef( cfg, parseModelRef(row.model, row.modelProvider ?? defaultRef.provider), defaultRef, + classifyCliProvider, ); } return defaultRef; diff --git a/src/commands/sessions.plugin-metadata.test.ts b/src/commands/sessions.plugin-metadata.test.ts new file mode 100644 index 000000000000..1df04913de37 --- /dev/null +++ b/src/commands/sessions.plugin-metadata.test.ts @@ -0,0 +1,91 @@ +// Session listing prepares plugin-backed CLI provider metadata once for every row. +import { afterEach, describe, expect, it, vi } from "vitest"; +import type { SessionEntry } from "../config/sessions/types.js"; +import { + mockSessionsConfig, + resetMockSessionsConfig, + runSessionsJson, + setMockSessionsConfig, + writeStore, +} from "./sessions.test-helpers.js"; + +const resolvePluginMetadataSnapshotMock = vi.hoisted(() => vi.fn()); + +vi.mock("../plugins/plugin-metadata-snapshot.js", async (importOriginal) => ({ + ...(await importOriginal()), + resolvePluginMetadataSnapshot: resolvePluginMetadataSnapshotMock, +})); + +mockSessionsConfig(); + +const { sessionsCommand } = await import("./sessions.js"); + +afterEach(() => { + resetMockSessionsConfig(); + resolvePluginMetadataSnapshotMock.mockReset(); +}); + +describe("sessions plugin metadata preparation", () => { + it("loads one plugin metadata snapshot before enriching every stored row", async () => { + const config = { + agents: { + defaults: { + model: { primary: "openai/gpt-5.5" }, + models: { "openai/gpt-5.5": {} }, + contextTokens: 128_000, + }, + }, + }; + setMockSessionsConfig(() => config); + resolvePluginMetadataSnapshotMock.mockReturnValue({ + configFingerprint: "sessions-plugin-metadata-test", + index: { + plugins: [ + { + pluginId: "fixture-plugin", + origin: "global", + enabled: true, + }, + ], + }, + plugins: [ + { + id: "fixture-plugin", + origin: "global", + cliBackends: ["fixture-cli"], + }, + ], + }); + + const entries = Object.fromEntries( + Array.from({ length: 12 }, (_, index) => [ + `agent:main:fixture-${index}`, + { + sessionId: `fixture-session-${index}`, + updatedAt: Date.now() - index * 1_000, + modelProvider: "fixture-cli", + model: "openai/gpt-5.5", + } satisfies SessionEntry, + ]), + ); + const store = await writeStore(entries, "sessions-plugin-metadata"); + + const payload = await runSessionsJson<{ + count: number; + totalCount: number; + sessions: Array<{ model: string; modelProvider: string }>; + }>(sessionsCommand, store, { limit: 1 }); + + expect(payload).toMatchObject({ + count: 1, + totalCount: 12, + sessions: [{ model: "gpt-5.5", modelProvider: "openai" }], + }); + expect(payload.sessions[0]).not.toHaveProperty("displayModelRef"); + expect(resolvePluginMetadataSnapshotMock).toHaveBeenCalledTimes(1); + expect(resolvePluginMetadataSnapshotMock).toHaveBeenCalledWith({ + config, + env: process.env, + }); + }); +}); diff --git a/src/commands/sessions.ts b/src/commands/sessions.ts index eddaa4a42bca..5ed298f7bad1 100644 --- a/src/commands/sessions.ts +++ b/src/commands/sessions.ts @@ -12,6 +12,10 @@ import { isRich, theme } from "../../packages/terminal-core/src/theme.js"; import { readAcpSessionMetaForEntry } from "../acp/runtime/session-meta.js"; import { resolveModelAgentRuntimeMetadata } from "../agents/agent-runtime-metadata.js"; import { DEFAULT_CONTEXT_TOKENS } from "../agents/defaults.js"; +import { + prepareCliProviderClassifier, + type CliProviderClassifier, +} from "../agents/model-selection.js"; import { resolveRuntimePolicySessionKey } from "../auto-reply/reply/runtime-policy-session-key.js"; import { normalizeChatType } from "../channels/chat-type.js"; import { getRuntimeConfig } from "../config/config.js"; @@ -55,6 +59,8 @@ type SessionRow = SessionDisplayRow & { kind: SessionKind; agentRuntime: ReturnType; runtimeLabel: string; + /** Carry the prepared identity into JSON/table emission without re-resolving plugin metadata. */ + displayModelRef: { provider: string; model: string }; /** * True only when the session has persisted ACP runtime metadata. Key-shape * alone is not sufficient because ACP bridge sessions (translator.ts) may @@ -215,9 +221,7 @@ function resolveSessionRuntimeLabel(params: { entry: SessionEntry; agentRuntime: ReturnType; modelProvider: string; - model: string; - agentId: string; - sessionKey: string; + classifyCliProvider: CliProviderClassifier; }): string { const id = normalizeOptionalLowercaseString(params.agentRuntime.id); const resolvedHarness = id && id !== "openclaw" && id !== "auto" ? id : undefined; @@ -226,6 +230,7 @@ function resolveSessionRuntimeLabel(params: { sessionEntry: params.entry, resolvedHarness, fallbackProvider: params.modelProvider, + classifyCliProvider: params.classifyCliProvider, }); } @@ -240,8 +245,9 @@ function resolveSessionStoreDisplayPath(target: { agentId: string; storePath: st }).path; } -function toJsonSessionRow(row: SessionRow): Omit { - const { runtimeLabel, ...jsonRow } = row; +function toJsonSessionRow(row: SessionRow): Omit { + const { displayModelRef, runtimeLabel, ...jsonRow } = row; + void displayModelRef; void runtimeLabel; return jsonRow; } @@ -365,6 +371,8 @@ export async function sessionsCommand( return; } + const classifyCliProvider = prepareCliProviderClassifier(cfg); + const allRows = targets.flatMap((target) => { return listSessionEntriesReadOnly({ agentId: target.agentId, storePath: target.storePath }) .filter(({ entry }) => { @@ -390,7 +398,7 @@ export async function sessionsCommand( // ACP rows need stored-key metadata before model/runtime resolution so // bridge sessions and true ACP runtime sessions display differently. const modelRef = applyAcpModelOverlayIfNeeded( - resolveSessionDisplayModelRef(cfg, row), + resolveSessionDisplayModelRef(cfg, row, classifyCliProvider), acpSessionKey, acpRuntime, ); @@ -408,6 +416,7 @@ export async function sessionsCommand( agentId, acpRuntime, agentRuntime, + displayModelRef: modelRef, kind: classifySessionKind(row.key, entry), runtimePolicySessionKey: resolveDisplayRuntimePolicySessionKey({ cfg, @@ -419,9 +428,7 @@ export async function sessionsCommand( entry, agentRuntime, modelProvider: modelRef.provider, - model: modelRef.model, - agentId, - sessionKey: row.key, + classifyCliProvider, }), }); }); @@ -450,15 +457,7 @@ export async function sessionsCommand( sessions: await Promise.all( rows.map(async (row) => { const r = toJsonSessionRow(row); - const modelRef = applyAcpModelOverlayIfNeeded( - resolveSessionDisplayModelRef(cfg, r), - resolveStoredSessionKeyForAgentStore({ - cfg, - agentId: row.agentId, - sessionKey: r.key, - }), - row.acpRuntime, - ); + const modelRef = row.displayModelRef; return { ...r, totalTokens: resolveSessionTotalTokens(r) ?? null, @@ -520,15 +519,7 @@ export async function sessionsCommand( runtime.log(rich ? theme.heading(header) : header); for (const row of rows) { - const model = applyAcpModelOverlayIfNeeded( - resolveSessionDisplayModelRef(cfg, row), - resolveStoredSessionKeyForAgentStore({ - cfg, - agentId: row.agentId, - sessionKey: row.key, - }), - row.acpRuntime, - ).model; + const model = row.displayModelRef.model; const contextTokens = row.contextTokens ?? configuredContextTokens ?? diff --git a/src/plugins/setup-registry.runtime.ts b/src/plugins/setup-registry.runtime.ts index 6bc679a9d8b4..0847c26eeb45 100644 --- a/src/plugins/setup-registry.runtime.ts +++ b/src/plugins/setup-registry.runtime.ts @@ -90,3 +90,10 @@ export function resolvePluginSetupCliBackendDescriptor( (entry) => normalizeProviderId(entry.backend.id) === normalized, ); } + +/** Resolve enabled setup CLI backend ids from one metadata snapshot. */ +export function resolvePluginSetupCliBackendIds( + params: Omit = {}, +): string[] { + return resolveSetupCliBackendDescriptors(params).map((entry) => entry.backend.id); +} diff --git a/src/status/agent-runtime-label.ts b/src/status/agent-runtime-label.ts index 7096cdc19f5c..c6255895673b 100644 --- a/src/status/agent-runtime-label.ts +++ b/src/status/agent-runtime-label.ts @@ -5,7 +5,7 @@ import { normalizeOptionalString, } from "@openclaw/normalization-core/string-coerce"; import { sanitizeTerminalText } from "../../packages/terminal-core/src/safe-text.js"; -import { isCliProvider } from "../agents/model-selection.js"; +import { isCliProvider, type CliProviderClassifier } from "../agents/model-selection.js"; import type { SessionEntry } from "../config/sessions/types.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; @@ -27,6 +27,7 @@ export function resolveAgentRuntimeLabel(args: { >; resolvedHarness?: string; fallbackProvider?: string; + classifyCliProvider?: CliProviderClassifier; }): string { const acpAgentRaw = normalizeOptionalString(args.sessionEntry?.acp?.agent); const acpAgent = acpAgentRaw ? sanitizeTerminalText(acpAgentRaw) : undefined; @@ -49,7 +50,7 @@ export function resolveAgentRuntimeLabel(args: { normalizeOptionalString(args.sessionEntry?.providerOverride) ?? normalizeOptionalString(args.fallbackProvider); const provider = providerRaw ? sanitizeTerminalText(providerRaw) : undefined; - if (provider && isCliProvider(provider, args.config)) { + if (provider && (args.classifyCliProvider?.(provider) ?? isCliProvider(provider, args.config))) { return ( AGENT_RUNTIME_LABELS[normalizeOptionalLowercaseString(providerRaw) ?? ""] ?? `${provider} (cli)`