fix(perf): reuse prepared plugin generations

This commit is contained in:
Jesse Merhi
2026-08-13 02:13:24 +10:00
committed by Jesse Merhi
parent b2e023adc6
commit b9f2eba177
13 changed files with 125 additions and 19 deletions
@@ -653,6 +653,7 @@ export async function loadCompactHooksHarness(): Promise<{
getCurrentPluginMetadataSnapshot: () => emptyPluginMetadataSnapshot,
resolvePluginMetadataControlPlaneFingerprint: vi.fn(() => "test-plugin-fingerprint"),
setCurrentPluginMetadataSnapshot: vi.fn(),
withPluginMetadataSnapshotScope: (_snapshot: unknown, run: () => unknown) => run(),
}));
vi.doMock("../../plugins/command-registry-state.js", () => {
@@ -19,7 +19,7 @@ import { formatErrorMessage } from "../../infra/errors.js";
import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
import type { ProviderRuntimeModel } from "../../plugins/provider-runtime-model.types.js";
import { requireActivePluginRegistry } from "../../plugins/runtime.js";
import { withPluginRuntimeRegistryScope } from "../../plugins/runtime/gateway-request-scope.js";
import { withPluginRuntimeGenerationScope } from "../../plugins/runtime/gateway-request-scope.js";
import { enqueueCommandInLane } from "../../process/command-queue.js";
import { resolveUserPath } from "../../utils.js";
import { normalizeOptionalAgentRuntimeId } from "../agent-runtime-id.js";
@@ -380,7 +380,7 @@ async function compactEmbeddedAgentSessionImpl(
}
};
try {
return await withPluginRuntimeRegistryScope(lease.snapshot.pluginRegistry, run);
return await withPluginRuntimeGenerationScope(lease.snapshot, run);
} finally {
lease.release();
}
+2 -5
View File
@@ -3,7 +3,7 @@
*/
import { resolveAgentModelFallbackValues } from "../../config/model-input.js";
import { formatErrorMessage } from "../../infra/errors.js";
import { withPluginRuntimeRegistryScope } from "../../plugins/runtime/gateway-request-scope.js";
import { withPluginRuntimeGenerationScope } from "../../plugins/runtime/gateway-request-scope.js";
import { resolveUserPath } from "../../utils.js";
import { normalizeOptionalAgentRuntimeId } from "../agent-runtime-id.js";
import {
@@ -336,10 +336,7 @@ export async function compactEmbeddedAgentSessionDirect(
});
return fallbackResult.result;
};
return await withPluginRuntimeRegistryScope(
preparedModelRuntime.pluginRegistry,
compactPrepared,
);
return await withPluginRuntimeGenerationScope(preparedModelRuntime, compactPrepared);
} catch (err) {
return fallbackFailureToCompactionResult(err);
} finally {
@@ -15,12 +15,13 @@ import {
buildHandledBeforeAgentReplyPayloads,
runBeforeAgentReplyForTurn,
} from "../../plugins/before-agent-reply.js";
import { getCurrentPluginMetadataSnapshot } from "../../plugins/current-plugin-metadata-snapshot.js";
import {
buildAgentHookContextChannelFields,
buildAgentHookContextIdentityFields,
} from "../../plugins/hook-agent-context.js";
import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
import { withPluginRuntimeRegistryScope } from "../../plugins/runtime/gateway-request-scope.js";
import { withPluginRuntimeGenerationScope } from "../../plugins/runtime/gateway-request-scope.js";
import { resolveUserPath } from "../../utils.js";
import { isMarkdownCapableMessageChannel } from "../../utils/message-channel.js";
import {
@@ -226,8 +227,15 @@ async function runEmbeddedAgentInternal(
agentId: requestedWorkspaceResolution.agentId,
sessionKey: params.sessionKey,
});
const currentPluginMetadataSnapshot = getCurrentPluginMetadataSnapshot({
config,
workspaceDir: requestedWorkspaceResolution.workspaceDir,
env: process.env,
allowWorkspaceScopedSnapshot: true,
});
const runtimePluginSelections = resolveModelCandidateChain({
cfg: config,
manifestPlugins: currentPluginMetadataSnapshot?.plugins,
provider: requestedRuntimeSelection.provider,
model: requestedRuntimeSelection.modelId,
requestedRouteResolution: "resolved",
@@ -424,10 +432,7 @@ async function runEmbeddedAgentInternal(
preparedModelRuntime,
});
};
return await withPluginRuntimeRegistryScope(
preparedModelRuntime.pluginRegistry,
runPrepared,
);
return await withPluginRuntimeGenerationScope(preparedModelRuntime, runPrepared);
} finally {
preparedModelRuntimeLease.release();
}
+4
View File
@@ -13,6 +13,7 @@ import {
type PreparedAgentRunAdmission,
} from "./admitted-run-context.js";
import type { AgentHarness } from "./harness/types.js";
import { createEmptyPluginMetadataSnapshot } from "./test-helpers/embedded-agent-runner-e2e-mocks.js";
type IsolatedCliRunParams = {
preparedRunAdmission: PreparedAgentRunAdmission;
@@ -145,7 +146,10 @@ beforeEach(() => {
vi.clearAllMocks();
mocks.acquireAgentRunPreparedModelRuntime.mockResolvedValue({
snapshot: {
config: {},
metadataSnapshot: createEmptyPluginMetadataSnapshot("/tmp/workspace"),
pluginRegistry: createEmptyPluginRegistry(),
workspaceDir: "/tmp/workspace",
createStores: () => ({ modelRegistry: {} }),
},
release: vi.fn(),
+2 -2
View File
@@ -13,7 +13,7 @@ import type { OpenClawConfig } from "../config/types.openclaw.js";
import { withTempWorkspace } from "../infra/private-temp-workspace.js";
import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js";
import type { AssistantMessage } from "../llm/types.js";
import { withPluginRuntimeRegistryScope } from "../plugins/runtime/gateway-request-scope.js";
import { withPluginRuntimeGenerationScope } from "../plugins/runtime/gateway-request-scope.js";
import { prepareSystemAgentRunAdmission } from "./admitted-run-context.js";
import { resolveAgentDir, resolveAgentWorkspaceDir, resolveDefaultAgentId } from "./agent-scope.js";
import { resolveCliBackendConfig, resolveCliRuntimeCanonicalProvider } from "./cli-backends.js";
@@ -682,7 +682,7 @@ export async function runIsolatedCompletion(
usage: result.assistant.usage,
};
};
return await withPluginRuntimeRegistryScope(pluginRegistry, run);
return await withPluginRuntimeGenerationScope(lease.snapshot, run);
} finally {
lease.release();
}
@@ -29,7 +29,7 @@ type EmbeddedRunnerBackoffMockOptions = {
sleepWithAbort: (ms: number, abortSignal?: AbortSignal) => unknown;
};
function createEmptyPluginMetadataSnapshot(workspaceDir?: string): PluginMetadataSnapshot {
export function createEmptyPluginMetadataSnapshot(workspaceDir?: string): PluginMetadataSnapshot {
return {
policyHash: "",
...(workspaceDir !== undefined ? { workspaceDir } : {}),
+5 -1
View File
@@ -363,6 +363,10 @@ export function buildGatewaySessionRow(params: {
const thinkingProvider = rowModelProvider ?? DEFAULT_PROVIDER;
const thinkingModel = rowModel ?? DEFAULT_MODEL;
// Event/list rows must not rediscover plugin-backed configured catalog metadata.
// An owner-provided catalog remains authoritative; otherwise the lightweight
// projection uses built-in model-family policy only.
const thinkingModelCatalog = params.modelCatalog ?? (lightweight ? [] : undefined);
const thinkingProjection = resolveGatewaySessionThinkingProjectionInternal({
cfg,
agentId: sessionAgentId,
@@ -370,7 +374,7 @@ export function buildGatewaySessionRow(params: {
model: thinkingModel,
sessionKey: acpSessionKey,
entry,
modelCatalog: params.modelCatalog,
modelCatalog: thinkingModelCatalog,
rowContext,
});
const fastModeState = resolveFastModeState({
+12 -2
View File
@@ -153,9 +153,10 @@ type LoadGatewaySessionRowOptions = {
transcriptUsageMaxBytes?: number;
};
export function loadGatewaySessionLifecycleSnapshot(
function loadGatewaySessionSnapshot(
sessionKey: string,
options?: LoadGatewaySessionRowOptions,
lightweight = false,
): { lifecycleRunId?: string; row: GatewaySessionRow | null } {
const now = options?.now ?? Date.now();
const { cfg, storePath, store, entry, canonicalKey } = loadGatewaySessionEntryReadOnly(
@@ -189,16 +190,25 @@ export function loadGatewaySessionLifecycleSnapshot(
includeLastMessage: options?.includeLastMessage,
transcriptUsageMaxBytes: options?.transcriptUsageMaxBytes,
storeChildSessionsByKey,
skipTranscriptUsageFallback: lightweight,
lightweightListRow: lightweight,
...(options?.agentId ? { agentId: options.agentId } : {}),
}),
};
}
export function loadGatewaySessionLifecycleSnapshot(
sessionKey: string,
options?: LoadGatewaySessionRowOptions,
): { lifecycleRunId?: string; row: GatewaySessionRow | null } {
return loadGatewaySessionSnapshot(sessionKey, options, true);
}
export function loadGatewaySessionRow(
sessionKey: string,
options?: LoadGatewaySessionRowOptions,
): GatewaySessionRow | null {
return loadGatewaySessionLifecycleSnapshot(sessionKey, options).row;
return loadGatewaySessionSnapshot(sessionKey, options).row;
}
export function buildGatewaySessionInfo(params: {
@@ -3,7 +3,9 @@
*/
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import type { SessionEntry } from "../config/sessions.js";
import { resolveSessionStorePathCore, type SessionEntry } from "../config/sessions.js";
import { replaceSessionEntry } from "../config/sessions/session-accessor.js";
import { withStateDirEnv } from "../test-helpers/state-dir-env.js";
const normalizeProviderModelIdWithPluginMock = vi.fn();
const emptyPluginMetadataSnapshot = vi.hoisted(() => ({
@@ -86,4 +88,38 @@ describe("gateway session list plugin runtime normalization", () => {
expect(row.model).toBe("custom-modern-model");
expect(normalizeProviderModelIdWithPluginMock).toHaveBeenCalled();
});
it("keeps lifecycle event rows lightweight without changing explicit detail rows", async () => {
await withStateDirEnv("openclaw-lifecycle-row-plugin-runtime-", async () => {
normalizeProviderModelIdWithPluginMock.mockImplementation(
({ provider, context }: { provider?: string; context?: { modelId?: string } }) =>
provider === "custom-provider" && context?.modelId === "custom-legacy-model"
? "custom-modern-model"
: undefined,
);
const cfg = {
agents: {
defaults: { model: { primary: "custom-provider/custom-legacy-model" } },
},
} as OpenClawConfig;
const configRuntime = await import("../config/config.js");
configRuntime.resetConfigRuntimeState();
configRuntime.setRuntimeConfigSnapshot(cfg, cfg);
const sessionKey = "agent:main:lifecycle-plugin-runtime";
const storePath = resolveSessionStorePathCore(cfg.session?.store, { agentId: "main" });
await replaceSessionEntry({ sessionKey, storePath }, {
sessionId: "lifecycle-plugin-runtime",
updatedAt: 1,
} satisfies SessionEntry);
const lifecycle = sessionUtils.loadGatewaySessionLifecycleSnapshot(sessionKey);
expect(lifecycle.row?.model).toBe("custom-legacy-model");
expect(normalizeProviderModelIdWithPluginMock).not.toHaveBeenCalled();
expect(sessionUtils.loadGatewaySessionRow(sessionKey)?.model).toBe("custom-modern-model");
expect(normalizeProviderModelIdWithPluginMock).toHaveBeenCalled();
configRuntime.resetConfigRuntimeState();
});
});
});
@@ -16,6 +16,11 @@ import { writePersistedInstalledPluginIndexSync } from "./installed-plugin-index
import type { PluginManifestRecord } from "./manifest-registry.js";
import { clearPluginMetadataLifecycleCaches } from "./plugin-metadata-lifecycle.js";
import type { PluginMetadataSnapshot } from "./plugin-metadata-snapshot.js";
import { createEmptyPluginRegistry } from "./registry-empty.js";
import {
getPluginRuntimeGatewayRequestScope,
withPluginRuntimeGenerationScope,
} from "./runtime/gateway-request-scope.js";
function createSnapshot(
params: {
@@ -160,6 +165,26 @@ describe("current plugin metadata snapshot", () => {
).toBe(globalSnapshot);
});
it("carries prepared metadata and registry as one runtime generation", async () => {
const config = { plugins: { allow: ["scoped"] } };
const workspaceDir = "/workspace/scoped";
const metadataSnapshot = createSnapshot({ config, workspaceDir });
const pluginRegistry = createEmptyPluginRegistry();
setCurrentPluginMetadataSnapshot(undefined);
await withPluginRuntimeGenerationScope(
{ config, metadataSnapshot, pluginRegistry, workspaceDir },
async () => {
await Promise.resolve();
expect(getCurrentPluginMetadataSnapshot({ config, workspaceDir })).toBe(metadataSnapshot);
expect(getPluginRuntimeGatewayRequestScope()?.pluginRegistry).toBe(pluginRegistry);
},
);
expect(getCurrentPluginMetadataSnapshot({ config, workspaceDir })).toBeUndefined();
expect(getPluginRuntimeGatewayRequestScope()).toBeUndefined();
});
it("lets configless nested readers inherit explicit owner discovery context", () => {
const config = {
plugins: {
@@ -1,10 +1,13 @@
// Gateway request scope tracks request-local plugin runtime context across async work.
import { AsyncLocalStorage } from "node:async_hooks";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import type {
GatewayRequestContext,
GatewayRequestOptions,
} from "../../gateway/server-methods/types.js";
import { resolveGlobalSingleton } from "../../shared/global-singleton.js";
import { withPluginMetadataSnapshotScope } from "../current-plugin-metadata-snapshot.js";
import type { PluginMetadataSnapshot } from "../plugin-metadata-snapshot.types.js";
import type { PluginOrigin } from "../plugin-origin.types.js";
import type { PluginRegistry } from "../registry-types.js";
@@ -63,6 +66,26 @@ export function withPluginRuntimeRegistryScope<T>(
);
}
/** Carries one prepared plugin generation through all nested runtime lookups. */
export function withPluginRuntimeGenerationScope<T>(
generation: {
config: OpenClawConfig;
metadataSnapshot: PluginMetadataSnapshot;
pluginRegistry?: PluginRegistry;
workspaceDir?: string;
},
run: () => T,
): T {
return withPluginMetadataSnapshotScope(
generation.metadataSnapshot,
() => withPluginRuntimeRegistryScope(generation.pluginRegistry, run),
{
config: generation.config,
...(generation.workspaceDir ? { workspaceDir: generation.workspaceDir } : {}),
},
);
}
/**
* Runs work under the current gateway request scope while attaching plugin identity.
*/
@@ -128,6 +128,7 @@ describe("gateway concurrency benchmark script", () => {
const sample = {
controlUi: [],
durationMs: 10,
pluginMetadataScans: { count: 0, durationMs: null, totalDurationMs: 0 },
probeWarmup: { durationMs: 2, samples: [] },
readyz: [],
sessionsList: [],