mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
fix(perf): reuse prepared plugin generations
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 } : {}),
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
Reference in New Issue
Block a user