mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
refactor(agents): thread plugin metadata snapshots per turn (#112769)
* refactor(agents): thread plugin metadata snapshots per turn * fix(agents): validate threaded plugin metadata * fix(agents): preserve prepared metadata fallbacks * fix(commands): preserve snapshot auth refs typing * fix(commands): use indexed synthetic auth refs * test(auto-reply): isolate completed goal session store * test(agents): serialize embedded runner harness files * test(auto-reply): serialize reply runtime files * test(auto-reply): isolate goal context admission * test(auto-reply): allow persisted goal admission under CI load * test(agents): allow embedded harness warmup under CI load
This commit is contained in:
committed by
GitHub
parent
db5e59d15e
commit
e085b379f8
@@ -60,6 +60,11 @@ vi.mock("./agent-runtime-config.js", () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/plugin-metadata-snapshot.js", () => ({
|
||||
isPluginMetadataSnapshotCompatible: () => false,
|
||||
resolvePluginMetadataSnapshot: () => ({ plugins: [] }),
|
||||
}));
|
||||
|
||||
vi.mock("./agent-scope.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("./agent-scope.js")>("./agent-scope.js");
|
||||
return {
|
||||
@@ -79,10 +84,6 @@ vi.mock("./agent-scope.js", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../plugins/manifest-contract-eligibility.js", () => ({
|
||||
loadManifestMetadataSnapshot: () => ({ plugins: [] }),
|
||||
}));
|
||||
|
||||
vi.mock("./model-catalog.js", () => ({
|
||||
loadManifestModelCatalog: (params: LoadManifestModelCatalogParams) =>
|
||||
state.loadManifestModelCatalogMock(params),
|
||||
|
||||
@@ -282,13 +282,13 @@ vi.mock("./agent-runtime-config.js", () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../config/runtime-snapshot.js", () => ({
|
||||
setRuntimeConfigSnapshot: vi.fn(),
|
||||
vi.mock("../plugins/plugin-metadata-snapshot.js", () => ({
|
||||
isPluginMetadataSnapshotCompatible: () => false,
|
||||
resolvePluginMetadataSnapshot: () => ({ plugins: [] }),
|
||||
}));
|
||||
|
||||
// Model selection is mocked below, so plugin discovery cannot affect these assertions.
|
||||
vi.mock("../plugins/manifest-contract-eligibility.js", () => ({
|
||||
loadManifestMetadataSnapshot: () => ({ plugins: [] }),
|
||||
vi.mock("../config/runtime-snapshot.js", () => ({
|
||||
setRuntimeConfigSnapshot: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../config/sessions.js", () => ({
|
||||
|
||||
@@ -7,6 +7,8 @@ import { getRuntimeConfig, readConfigFileSnapshotForWrite } from "../config/io.j
|
||||
import { setRuntimeConfigSnapshot } from "../config/runtime-snapshot.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { isSecretRef } from "../config/types.secrets.js";
|
||||
import { resolvePluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.js";
|
||||
import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.types.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { discoverConfigSecretTargetsByIds } from "../secrets/target-registry.js";
|
||||
import { listAgentEntries } from "./agent-scope.js";
|
||||
@@ -22,6 +24,7 @@ export async function resolveAgentRuntimeConfig(
|
||||
loadedRaw: OpenClawConfig;
|
||||
sourceConfig: OpenClawConfig;
|
||||
cfg: OpenClawConfig;
|
||||
pluginMetadataSnapshot?: PluginMetadataSnapshot;
|
||||
}> {
|
||||
const loadedRaw = getRuntimeConfig();
|
||||
const includeChannelTargets = params?.runtimeTargetsChannelSecrets === true;
|
||||
@@ -31,15 +34,18 @@ export async function resolveAgentRuntimeConfig(
|
||||
includeChannelTargets,
|
||||
channel: channelSecretScope?.channel,
|
||||
});
|
||||
let pluginMetadataSnapshot: PluginMetadataSnapshot | undefined;
|
||||
const sourceConfig = await (async () => {
|
||||
try {
|
||||
const { snapshot } = await readConfigFileSnapshotForWrite();
|
||||
const { snapshot, writeOptions } = await readConfigFileSnapshotForWrite();
|
||||
if (snapshot.valid) {
|
||||
pluginMetadataSnapshot = writeOptions.basePluginMetadataSnapshot;
|
||||
return snapshot.resolved;
|
||||
}
|
||||
} catch {
|
||||
// Fall back to runtime-loaded config when source snapshot is unavailable.
|
||||
}
|
||||
pluginMetadataSnapshot = resolvePluginMetadataSnapshot({ config: loadedRaw });
|
||||
return loadedRaw;
|
||||
})();
|
||||
const cfg = hasRuntimeSecretRefs
|
||||
@@ -77,7 +83,12 @@ export async function resolveAgentRuntimeConfig(
|
||||
});
|
||||
secretsRuntime.activateSecretsRuntimeSnapshot(snapshot);
|
||||
}
|
||||
return { loadedRaw, sourceConfig, cfg };
|
||||
return {
|
||||
loadedRaw,
|
||||
sourceConfig,
|
||||
cfg,
|
||||
...(pluginMetadataSnapshot ? { pluginMetadataSnapshot } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function hasNestedSecretRef(value: unknown): boolean {
|
||||
|
||||
@@ -1131,6 +1131,7 @@ function createOpenClawCodingToolsInternal(options?: OpenClawCodingToolsOptions)
|
||||
auditLogLevel: options?.toolPolicyAuditLogLevel,
|
||||
declaredToolAllowlist: buildDeclaredToolAllowlistContext({
|
||||
config: options?.config,
|
||||
metadataSnapshot: options?.preparedModelRuntime?.metadataSnapshot,
|
||||
workspaceDir: workspaceRoot,
|
||||
toolDenylist: pluginToolDenylist,
|
||||
}),
|
||||
|
||||
@@ -10,7 +10,10 @@ import { resolveAgentExplicitRecipientSession } from "../../infra/outbound/agent
|
||||
import { buildOutboundSessionContext } from "../../infra/outbound/session-context.js";
|
||||
import { parseStrictNonNegativeInteger } from "../../infra/parse-finite-number.js";
|
||||
import { normalizePluginsConfig } from "../../plugins/config-state.js";
|
||||
import { loadManifestMetadataSnapshot } from "../../plugins/manifest-contract-eligibility.js";
|
||||
import {
|
||||
isPluginMetadataSnapshotCompatible,
|
||||
resolvePluginMetadataSnapshot,
|
||||
} from "../../plugins/plugin-metadata-snapshot.js";
|
||||
import {
|
||||
classifySessionKeyShape,
|
||||
isUnscopedSessionKeySentinel,
|
||||
@@ -133,7 +136,7 @@ export async function prepareAgentCommandExecution(opts: AgentCommandOpts, runti
|
||||
);
|
||||
}
|
||||
|
||||
const { cfg } = await resolveAgentRuntimeConfig(runtime, {
|
||||
const { cfg, pluginMetadataSnapshot } = await resolveAgentRuntimeConfig(runtime, {
|
||||
runtimeTargetsChannelSecrets: opts.deliver === true,
|
||||
runtimeChannelSecretScope:
|
||||
opts.deliver !== true && shouldResolveExplicitRecipientSession && recipientChannel
|
||||
@@ -291,7 +294,16 @@ export async function prepareAgentCommandExecution(opts: AgentCommandOpts, runti
|
||||
const agentDir = resolveAgentDir(cfg, sessionAgentId);
|
||||
const pluginsEnabled = normalizePluginsConfig(cfg.plugins).enabled;
|
||||
const manifestMetadataSnapshot = pluginsEnabled
|
||||
? loadManifestMetadataSnapshot({ config: cfg, workspaceDir, env: process.env })
|
||||
? pluginMetadataSnapshot &&
|
||||
pluginMetadataSnapshot.pluginIds === undefined &&
|
||||
isPluginMetadataSnapshotCompatible({
|
||||
snapshot: pluginMetadataSnapshot,
|
||||
config: cfg,
|
||||
env: process.env,
|
||||
workspaceDir,
|
||||
})
|
||||
? pluginMetadataSnapshot
|
||||
: resolvePluginMetadataSnapshot({ config: cfg, env: process.env, workspaceDir })
|
||||
: undefined;
|
||||
const modelManifestContext = {
|
||||
manifestPlugins: manifestMetadataSnapshot?.plugins ?? [],
|
||||
|
||||
@@ -743,6 +743,10 @@ export async function loadCompactHooksHarness(): Promise<{
|
||||
agentDir: input.agentDir,
|
||||
config: input.config,
|
||||
workspaceDir: input.workspaceDir,
|
||||
metadataSnapshot: {
|
||||
...emptyPluginMetadataSnapshot,
|
||||
workspaceDir: input.workspaceDir as string | undefined,
|
||||
},
|
||||
createStores: () => ({ authStorage: {}, modelRegistry: {} }),
|
||||
},
|
||||
release: vi.fn(),
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Applies final effective tool policy to embedded-agent runtime settings.
|
||||
*/
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import type { PluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.types.js";
|
||||
import { getPluginToolMeta } from "../../plugins/tools.js";
|
||||
import type { ResolvedConversationCapabilityProfile } from "../conversation-capability-profile.js";
|
||||
import { buildDeclaredToolAllowlistContext } from "../tool-policy-declared-context.js";
|
||||
@@ -30,6 +31,8 @@ type FinalEffectiveToolPolicyParams = {
|
||||
// metadata no longer survives core-tool wrapping/normalization.
|
||||
bundledTools: AnyAgentTool[];
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
metadataSnapshot?: PluginMetadataSnapshot;
|
||||
conversationCapabilityProfile: ResolvedConversationCapabilityProfile;
|
||||
warn: (message: string) => void;
|
||||
toolPolicyAuditLogLevel?: "info" | "debug";
|
||||
@@ -113,6 +116,8 @@ export function applyFinalEffectiveToolPolicy(
|
||||
onFilter: params.onFilter,
|
||||
declaredToolAllowlist: buildDeclaredToolAllowlistContext({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
metadataSnapshot: params.metadataSnapshot,
|
||||
toolDenylist: collectExplicitDenylist(pipelineSteps.map((step) => step.policy)),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -64,7 +64,7 @@ describe("runEmbeddedAgent incomplete-turn safety", () => {
|
||||
beforeAll(async () => {
|
||||
({ runEmbeddedAgent } = await loadRunOverflowCompactionHarness());
|
||||
await warmRunOverflowCompactionHarness(runEmbeddedAgent);
|
||||
});
|
||||
}, 300_000);
|
||||
|
||||
beforeEach(() => {
|
||||
resetRunOverflowCompactionHarnessMocks();
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
PluginHookBeforeAgentFinalizeEvent,
|
||||
PluginHookBeforeAgentFinalizeResult,
|
||||
} from "../../plugins/hook-types.js";
|
||||
import type { PluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.types.js";
|
||||
import type {
|
||||
PluginHookAgentContext,
|
||||
PluginHookBeforeAgentReplyResult,
|
||||
@@ -65,6 +66,45 @@ type MockResolvedModel = {
|
||||
reasoning?: boolean;
|
||||
};
|
||||
|
||||
const emptyPluginMetadataSnapshot: PluginMetadataSnapshot = {
|
||||
policyHash: "",
|
||||
index: {
|
||||
version: 1,
|
||||
hostContractVersion: "test",
|
||||
compatRegistryVersion: "test",
|
||||
migrationVersion: 1,
|
||||
policyHash: "",
|
||||
generatedAtMs: 1,
|
||||
installRecords: {},
|
||||
plugins: [],
|
||||
diagnostics: [],
|
||||
},
|
||||
registryDiagnostics: [],
|
||||
manifestRegistry: { plugins: [], diagnostics: [] },
|
||||
plugins: [],
|
||||
diagnostics: [],
|
||||
byPluginId: new Map(),
|
||||
normalizePluginId: (pluginId: string) => pluginId,
|
||||
owners: {
|
||||
channels: new Map(),
|
||||
channelConfigs: new Map(),
|
||||
providers: new Map(),
|
||||
modelCatalogProviders: new Map(),
|
||||
cliBackends: new Map(),
|
||||
setupProviders: new Map(),
|
||||
commandAliases: new Map(),
|
||||
contracts: new Map(),
|
||||
},
|
||||
metrics: {
|
||||
registrySnapshotMs: 0,
|
||||
manifestRegistryMs: 0,
|
||||
ownerMapsMs: 0,
|
||||
totalMs: 0,
|
||||
indexPluginCount: 0,
|
||||
manifestPluginCount: 0,
|
||||
},
|
||||
};
|
||||
|
||||
type MockAgentDiscoveryStores = {
|
||||
authStorage: {
|
||||
setRuntimeApiKey: ReturnType<typeof vi.fn>;
|
||||
@@ -887,6 +927,10 @@ export async function loadRunOverflowCompactionHarness(): Promise<{
|
||||
agentDir: input.agentDir,
|
||||
config: input.config,
|
||||
workspaceDir: input.workspaceDir,
|
||||
metadataSnapshot: {
|
||||
...emptyPluginMetadataSnapshot,
|
||||
workspaceDir: input.workspaceDir as string | undefined,
|
||||
},
|
||||
createStores: () => ({ authStorage: {}, modelRegistry: {} }),
|
||||
},
|
||||
release: vi.fn(),
|
||||
|
||||
@@ -80,6 +80,9 @@ describe("prepareEmbeddedAttemptBundleTools", () => {
|
||||
} as unknown as Parameters<typeof prepareEmbeddedAttemptBundleTools>[0];
|
||||
|
||||
await expect(prepareEmbeddedAttemptBundleTools(input)).rejects.toThrow("bundle policy failed");
|
||||
expect(mocks.applyFinalEffectiveToolPolicy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ workspaceDir: "/tmp/workspace" }),
|
||||
);
|
||||
expect(disposeMcp).toHaveBeenCalledOnce();
|
||||
expect(disposeLsp).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
@@ -138,6 +138,8 @@ export async function prepareEmbeddedAttemptBundleTools(params: {
|
||||
const filteredBundledTools = applyFinalEffectiveToolPolicy({
|
||||
bundledTools: [...allowedBundleMcpTools, ...allowedBundleLspTools],
|
||||
config: params.attempt.config,
|
||||
workspaceDir: params.effectiveWorkspace,
|
||||
metadataSnapshot: bundleMetadataSnapshot,
|
||||
conversationCapabilityProfile: runtimeCapabilityProfile,
|
||||
warn: (message) => log.warn(message),
|
||||
});
|
||||
@@ -150,6 +152,8 @@ export async function prepareEmbeddedAttemptBundleTools(params: {
|
||||
const allowedAppTools = applyFinalEffectiveToolPolicy({
|
||||
bundledTools: runtimeAllowedAppTools,
|
||||
config: params.attempt.config,
|
||||
workspaceDir: params.effectiveWorkspace,
|
||||
metadataSnapshot: bundleMetadataSnapshot,
|
||||
conversationCapabilityProfile: runtimeCapabilityProfile,
|
||||
warn: (message) => log.warn(message),
|
||||
});
|
||||
|
||||
@@ -127,6 +127,8 @@ export async function prepareAndDispatchEmbeddedRunAttempt(input: {
|
||||
harnessId: runtime.agentHarness.id,
|
||||
harnessRuntime: runtime.agentHarness.id,
|
||||
preparedAuthPlan: runtime.activePreparedAuthPlan,
|
||||
metadataSnapshot: runtime.pluginMetadataSnapshot,
|
||||
providerRuntimeHandle: runtime.providerRuntimeHandle,
|
||||
config: params.config,
|
||||
workspaceDir,
|
||||
agentDir,
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ProviderRuntimePluginHandle } from "../../../plugins/provider-hook-runtime.js";
|
||||
import type { EmbeddedRunAttemptParams } from "./types.js";
|
||||
|
||||
const resolveProviderRuntimePluginHandle = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("../../../plugins/provider-hook-runtime.js", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("../../../plugins/provider-hook-runtime.js")>()),
|
||||
resolveProviderRuntimePluginHandle,
|
||||
}));
|
||||
|
||||
import { prepareEmbeddedAttemptSetup } from "./attempt-setup.js";
|
||||
|
||||
describe("prepareEmbeddedAttemptSetup", () => {
|
||||
beforeEach(() => {
|
||||
resolveProviderRuntimePluginHandle.mockReset();
|
||||
});
|
||||
|
||||
it("reuses lifecycle metadata and the provider handle from the runtime plan", async () => {
|
||||
const metadataSnapshot = { plugins: [] } as never;
|
||||
const workspaceDir = path.join(os.tmpdir(), "openclaw-attempt-setup-prepared");
|
||||
const providerRuntimeHandle: ProviderRuntimePluginHandle & { prepared: true } = {
|
||||
provider: "openai",
|
||||
modelId: "gpt-5.4",
|
||||
prepared: true,
|
||||
workspaceDir,
|
||||
plugin: {} as never,
|
||||
};
|
||||
const setup = await prepareEmbeddedAttemptSetup({
|
||||
config: {},
|
||||
modelId: "gpt-5.4",
|
||||
provider: "openai",
|
||||
runId: "run-prepared",
|
||||
sessionId: "session-prepared",
|
||||
thinkLevel: "high",
|
||||
timeoutMs: 30_000,
|
||||
workspaceDir,
|
||||
preparedModelRuntime: { metadataSnapshot } as never,
|
||||
runtimePlan: { providerRuntimeHandle } as never,
|
||||
} as unknown as EmbeddedRunAttemptParams);
|
||||
|
||||
expect(setup.getCurrentAttemptPluginMetadataSnapshot()).toBe(metadataSnapshot);
|
||||
expect(setup.getProviderRuntimeHandle()).toBe(providerRuntimeHandle);
|
||||
expect(resolveProviderRuntimePluginHandle).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("resolves partial handles without trusting scoped metadata", async () => {
|
||||
const resolvedHandle: ProviderRuntimePluginHandle = {
|
||||
provider: "openai",
|
||||
modelId: "gpt-5.4",
|
||||
};
|
||||
resolveProviderRuntimePluginHandle.mockReturnValue(resolvedHandle);
|
||||
const setup = await prepareEmbeddedAttemptSetup({
|
||||
config: {},
|
||||
modelId: "gpt-5.4",
|
||||
provider: "openai",
|
||||
runId: "run-partial",
|
||||
sessionId: "session-partial",
|
||||
thinkLevel: "high",
|
||||
timeoutMs: 30_000,
|
||||
workspaceDir: path.join(os.tmpdir(), "openclaw-attempt-setup-partial"),
|
||||
preparedModelRuntime: {
|
||||
metadataSnapshot: { pluginIds: ["other"] },
|
||||
} as never,
|
||||
runtimePlan: { providerRuntimeHandle: { provider: "openai" } } as never,
|
||||
} as unknown as EmbeddedRunAttemptParams);
|
||||
|
||||
const preparedHandle = setup.getProviderRuntimeHandle();
|
||||
expect(preparedHandle).toMatchObject(resolvedHandle);
|
||||
expect(preparedHandle.modelId).toBe("gpt-5.4");
|
||||
expect(setup.getProviderRuntimeHandle()).toBe(preparedHandle);
|
||||
expect(resolveProviderRuntimePluginHandle).toHaveBeenCalledOnce();
|
||||
const call = resolveProviderRuntimePluginHandle.mock.calls[0]?.[0];
|
||||
expect(call).toMatchObject({ provider: "openai", modelId: "gpt-5.4" });
|
||||
expect(call).not.toHaveProperty("pluginMetadataSnapshot");
|
||||
});
|
||||
});
|
||||
@@ -2,8 +2,7 @@
|
||||
* Resolves workspace, sandbox, provider runtime, and phase reporting for an embedded attempt.
|
||||
*/
|
||||
import fs from "node:fs/promises";
|
||||
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
|
||||
import { getCurrentPluginMetadataSnapshot } from "../../../plugins/current-plugin-metadata-snapshot.js";
|
||||
import { isPluginMetadataSnapshotCompatible } from "../../../plugins/plugin-metadata-snapshot.js";
|
||||
import type { PluginMetadataSnapshot } from "../../../plugins/plugin-metadata-snapshot.types.js";
|
||||
import {
|
||||
resolveProviderRuntimePluginHandle,
|
||||
@@ -24,30 +23,10 @@ import {
|
||||
import { resolveAttemptFsWorkspaceOnly } from "./attempt.prompt-helpers.js";
|
||||
import type { EmbeddedRunAttemptParams } from "./types.js";
|
||||
|
||||
function pluginMetadataSnapshotCoversProvider(
|
||||
snapshot: PluginMetadataSnapshot | undefined,
|
||||
provider: string,
|
||||
): snapshot is PluginMetadataSnapshot {
|
||||
const normalizedProvider = normalizeProviderId(provider);
|
||||
if (!snapshot || !normalizedProvider) {
|
||||
return false;
|
||||
}
|
||||
return snapshot.manifestRegistry.plugins.some((plugin) => {
|
||||
const ownsProvider = plugin.providers.some(
|
||||
(providerId) => normalizeProviderId(providerId) === normalizedProvider,
|
||||
);
|
||||
if (ownsProvider) {
|
||||
return true;
|
||||
}
|
||||
const modelCatalogProviderIds = [
|
||||
...Object.keys(plugin.modelCatalog?.providers ?? {}),
|
||||
...Object.keys(plugin.modelCatalog?.aliases ?? {}),
|
||||
];
|
||||
return modelCatalogProviderIds.some(
|
||||
(providerId) => normalizeProviderId(providerId) === normalizedProvider,
|
||||
);
|
||||
});
|
||||
}
|
||||
type PreparedProviderRuntimePluginHandle = ProviderRuntimePluginHandle & {
|
||||
modelId: string;
|
||||
prepared: true;
|
||||
};
|
||||
|
||||
export async function prepareEmbeddedAttemptSetup(params: EmbeddedRunAttemptParams) {
|
||||
const resolvedWorkspace = resolveUserPath(params.workspaceDir);
|
||||
@@ -117,40 +96,50 @@ export async function prepareEmbeddedAttemptSetup(params: EmbeddedRunAttemptPara
|
||||
const effectiveCwd = sandbox?.enabled ? effectiveWorkspace : (requestedCwd ?? effectiveWorkspace);
|
||||
await fs.mkdir(effectiveWorkspace, { recursive: true });
|
||||
|
||||
let currentPluginMetadataSnapshotResolved = false;
|
||||
let currentPluginMetadataSnapshot: PluginMetadataSnapshot | undefined;
|
||||
const getCurrentAttemptPluginMetadataSnapshot = () => {
|
||||
if (!currentPluginMetadataSnapshotResolved) {
|
||||
currentPluginMetadataSnapshot = getCurrentPluginMetadataSnapshot({
|
||||
allowScopedSnapshot: true,
|
||||
config: params.config,
|
||||
env: process.env,
|
||||
workspaceDir: effectiveWorkspace,
|
||||
});
|
||||
currentPluginMetadataSnapshotResolved = true;
|
||||
}
|
||||
return currentPluginMetadataSnapshot;
|
||||
};
|
||||
let providerRuntimeHandle: ProviderRuntimePluginHandle | undefined;
|
||||
const getProviderRuntimeHandle = () => {
|
||||
if (providerRuntimeHandle?.plugin) {
|
||||
const getCurrentAttemptPluginMetadataSnapshot = (): PluginMetadataSnapshot | undefined =>
|
||||
params.preparedModelRuntime?.metadataSnapshot;
|
||||
let providerRuntimeHandle = params.runtimePlan?.providerRuntimeHandle as
|
||||
| PreparedProviderRuntimePluginHandle
|
||||
| undefined;
|
||||
const getProviderRuntimeHandle = (): PreparedProviderRuntimePluginHandle => {
|
||||
if (
|
||||
providerRuntimeHandle &&
|
||||
providerRuntimeHandle.prepared &&
|
||||
providerRuntimeHandle.provider === params.provider &&
|
||||
providerRuntimeHandle.modelId === params.modelId &&
|
||||
providerRuntimeHandle.workspaceDir === effectiveWorkspace
|
||||
) {
|
||||
return providerRuntimeHandle;
|
||||
}
|
||||
const pluginMetadataSnapshot = getCurrentAttemptPluginMetadataSnapshot();
|
||||
const resolvedHandle = resolveProviderRuntimePluginHandle({
|
||||
const compatibleMetadataSnapshot =
|
||||
pluginMetadataSnapshot &&
|
||||
pluginMetadataSnapshot.pluginIds === undefined &&
|
||||
isPluginMetadataSnapshotCompatible({
|
||||
snapshot: pluginMetadataSnapshot,
|
||||
config: params.config,
|
||||
env: process.env,
|
||||
workspaceDir: effectiveWorkspace,
|
||||
})
|
||||
? pluginMetadataSnapshot
|
||||
: undefined;
|
||||
providerRuntimeHandle = {
|
||||
...resolveProviderRuntimePluginHandle({
|
||||
provider: params.provider,
|
||||
modelId: params.modelId,
|
||||
config: params.config,
|
||||
workspaceDir: effectiveWorkspace,
|
||||
env: process.env,
|
||||
...(compatibleMetadataSnapshot
|
||||
? { pluginMetadataSnapshot: compatibleMetadataSnapshot }
|
||||
: {}),
|
||||
}),
|
||||
provider: params.provider,
|
||||
modelId: params.modelId,
|
||||
config: params.config,
|
||||
prepared: true,
|
||||
workspaceDir: effectiveWorkspace,
|
||||
env: process.env,
|
||||
...(pluginMetadataSnapshotCoversProvider(pluginMetadataSnapshot, params.provider)
|
||||
? { pluginMetadataSnapshot }
|
||||
: {}),
|
||||
});
|
||||
if (resolvedHandle.plugin) {
|
||||
providerRuntimeHandle = resolvedHandle;
|
||||
}
|
||||
return resolvedHandle;
|
||||
};
|
||||
return providerRuntimeHandle;
|
||||
};
|
||||
const { sessionAgentId } = resolveSessionAgentIds({
|
||||
sessionKey: params.sessionKey,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { ThinkLevel } from "../../../auto-reply/thinking.js";
|
||||
import { isPluginMetadataSnapshotCompatible } from "../../../plugins/plugin-metadata-snapshot.js";
|
||||
import { resolveProviderRuntimePluginHandle } from "../../../plugins/provider-hook-runtime.js";
|
||||
import type { AuthProfileStore } from "../../auth-profiles.js";
|
||||
import { isProfileInCooldown } from "../../auth-profiles.js";
|
||||
import type { ResolvedProviderAuth } from "../../model-auth.js";
|
||||
@@ -70,6 +72,7 @@ export async function prepareEmbeddedRunRuntime(input: {
|
||||
});
|
||||
provider = modelSetup.provider;
|
||||
modelId = modelSetup.modelId;
|
||||
const pluginMetadataSnapshot = input.preparedModelRuntime?.metadataSnapshot;
|
||||
const {
|
||||
requestedModelId,
|
||||
modelSelectionChangedByHook,
|
||||
@@ -462,6 +465,29 @@ export async function prepareEmbeddedRunRuntime(input: {
|
||||
}
|
||||
input.markStartupStage("auth");
|
||||
input.notifyExecutionPhase("auth", { provider, model: modelId });
|
||||
const compatibleMetadataSnapshot =
|
||||
pluginMetadataSnapshot &&
|
||||
pluginMetadataSnapshot.pluginIds === undefined &&
|
||||
isPluginMetadataSnapshotCompatible({
|
||||
snapshot: pluginMetadataSnapshot,
|
||||
config: params.config,
|
||||
env: process.env,
|
||||
workspaceDir: input.workspaceDir,
|
||||
})
|
||||
? pluginMetadataSnapshot
|
||||
: undefined;
|
||||
const providerRuntimeHandle = {
|
||||
...resolveProviderRuntimePluginHandle({
|
||||
provider,
|
||||
modelId,
|
||||
config: params.config,
|
||||
workspaceDir: input.workspaceDir,
|
||||
env: process.env,
|
||||
...(compatibleMetadataSnapshot ? { pluginMetadataSnapshot: compatibleMetadataSnapshot } : {}),
|
||||
}),
|
||||
modelId,
|
||||
prepared: true as const,
|
||||
};
|
||||
|
||||
return {
|
||||
provider,
|
||||
@@ -514,6 +540,8 @@ export async function prepareEmbeddedRunRuntime(input: {
|
||||
apiKeyInfo,
|
||||
lastProfileId,
|
||||
runtimeAuthState,
|
||||
pluginMetadataSnapshot,
|
||||
providerRuntimeHandle,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -98,15 +98,11 @@ function resolveManifestPluginsForModelIdNormalization(params: {
|
||||
if (currentManifestPlugins) {
|
||||
return currentManifestPlugins;
|
||||
}
|
||||
return loadManifestMetadataSnapshot({
|
||||
config: params.cfg,
|
||||
env: process.env,
|
||||
}).plugins;
|
||||
}
|
||||
return loadManifestMetadataSnapshot({
|
||||
config: params.cfg,
|
||||
workspaceDir,
|
||||
env: process.env,
|
||||
...(workspaceDir ? { workspaceDir } : {}),
|
||||
}).plugins;
|
||||
}
|
||||
|
||||
@@ -1356,17 +1352,15 @@ function resolveConfiguredModelManifestPlugins(params: {
|
||||
}
|
||||
const workspaceDir = params.workspaceDir ?? getActivePluginRegistryWorkspaceDirFromState();
|
||||
if (!workspaceDir) {
|
||||
return (
|
||||
getCurrentPluginMetadataSnapshot({
|
||||
config: params.cfg,
|
||||
env: process.env,
|
||||
})?.plugins ?? []
|
||||
);
|
||||
return getCurrentPluginMetadataSnapshot({
|
||||
config: params.cfg,
|
||||
env: process.env,
|
||||
})?.plugins;
|
||||
}
|
||||
return loadManifestMetadataSnapshot({
|
||||
config: params.cfg,
|
||||
workspaceDir,
|
||||
env: process.env,
|
||||
...(workspaceDir ? { workspaceDir } : {}),
|
||||
}).plugins;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { getCurrentPluginMetadataSnapshotState } from "../plugins/current-plugin
|
||||
import { buildManifestBuiltInModelSuppressionResolver } from "../plugins/manifest-model-suppression.js";
|
||||
import { resolvePluginControlPlaneFingerprint } from "../plugins/plugin-control-plane-context.js";
|
||||
import { registerPluginMetadataProcessMemoLifecycleClear } from "../plugins/plugin-metadata-lifecycle.js";
|
||||
import { resolvePluginMetadataSnapshotMemoEnvFingerprint } from "../plugins/plugin-metadata-snapshot.js";
|
||||
import { resolvePluginMetadataEnvFingerprint } from "../plugins/plugin-metadata-snapshot.js";
|
||||
|
||||
type ManifestSuppressionResolver = ReturnType<typeof buildManifestBuiltInModelSuppressionResolver>;
|
||||
|
||||
@@ -45,7 +45,7 @@ function resolveCachedManifestSuppressionResolver(params: {
|
||||
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
|
||||
});
|
||||
const cwd = process.cwd();
|
||||
const envFingerprint = resolvePluginMetadataSnapshotMemoEnvFingerprint(params.env);
|
||||
const envFingerprint = resolvePluginMetadataEnvFingerprint(params.env);
|
||||
const metadataSnapshot = getCurrentPluginMetadataSnapshotState().snapshot;
|
||||
if (
|
||||
cached !== undefined &&
|
||||
|
||||
@@ -320,6 +320,11 @@ vi.mock("../agents/provider-model-normalization.runtime.js", () => ({
|
||||
vi.mock("../plugins/current-plugin-metadata-snapshot.js", () => ({
|
||||
getCurrentPluginMetadataSnapshot: () => emptyPluginMetadataSnapshot,
|
||||
}));
|
||||
vi.mock("../plugins/plugin-metadata-snapshot.js", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("../plugins/plugin-metadata-snapshot.js")>()),
|
||||
isPluginMetadataSnapshotCompatible: () => true,
|
||||
resolvePluginMetadataSnapshot: () => emptyPluginMetadataSnapshot,
|
||||
}));
|
||||
vi.mock("../plugins/provider-thinking.js", () => ({
|
||||
resolveProviderBinaryThinking: () => undefined,
|
||||
resolveProviderDefaultThinkingLevel: () => undefined,
|
||||
|
||||
@@ -255,9 +255,6 @@ export function createOpenClawTools(
|
||||
const spawnWorkspaceDir = resolveWorkspaceRoot(
|
||||
options?.spawnWorkspaceDir ?? options?.workspaceDir ?? inferredWorkspaceDir,
|
||||
);
|
||||
const runtimeCwd = resolveWorkspaceRoot(
|
||||
options?.cwd ?? options?.workspaceDir ?? inferredWorkspaceDir,
|
||||
);
|
||||
options?.recordToolPrepStage?.("openclaw-tools:session-workspace");
|
||||
const deliveryContext = normalizeDeliveryContext({
|
||||
channel: options?.agentChannel,
|
||||
@@ -530,7 +527,7 @@ export function createOpenClawTools(
|
||||
? createTaskSuggestionTools({
|
||||
sessionKey: taskKey,
|
||||
agentId: sessionAgentId,
|
||||
cwd: runtimeCwd,
|
||||
cwd: resolveWorkspaceRoot(options?.cwd ?? options?.workspaceDir ?? inferredWorkspaceDir),
|
||||
})
|
||||
: []),
|
||||
...(messageTool && includeMessageTool ? [messageTool] : []),
|
||||
@@ -708,6 +705,7 @@ export function createOpenClawTools(
|
||||
sandboxed: options?.sandboxed,
|
||||
activeModelProvider: options?.modelProvider,
|
||||
activeModelId: options?.modelId,
|
||||
metadataSnapshot: options?.preparedModelRuntime?.metadataSnapshot,
|
||||
activeDeliveryContext: {
|
||||
channel: options?.agentChannel,
|
||||
to: options?.currentChannelId ?? options?.agentTo,
|
||||
|
||||
@@ -4,20 +4,14 @@ import { createParameterFreeTool } from "openclaw/plugin-sdk/agent-runtime-test-
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { resetConfigRuntimeState, setRuntimeConfigSnapshot } from "../../config/config.js";
|
||||
import {
|
||||
resolveProviderRuntimePluginHandle,
|
||||
prepareProviderExtraParams,
|
||||
resolveProviderFollowupFallbackRoute,
|
||||
resolveProviderRuntimePluginHandle,
|
||||
type ProviderRuntimePluginHandle,
|
||||
} from "../../plugins/provider-hook-runtime.js";
|
||||
import { buildAgentRuntimeDeliveryPlan, buildAgentRuntimePlan } from "./build.js";
|
||||
|
||||
const manifestMocks = vi.hoisted(() => ({
|
||||
loadManifestMetadataSnapshot: vi.fn(() => ({}) as never),
|
||||
}));
|
||||
|
||||
vi.mock("../../plugins/manifest-contract-eligibility.js", () => ({
|
||||
loadManifestMetadataSnapshot: manifestMocks.loadManifestMetadataSnapshot,
|
||||
}));
|
||||
const isPluginMetadataSnapshotCompatible = vi.hoisted(() => vi.fn(() => true));
|
||||
|
||||
vi.mock("../../plugins/provider-hook-runtime.js", () => ({
|
||||
clearProviderRuntimePluginCacheForTest: vi.fn(),
|
||||
@@ -37,6 +31,11 @@ vi.mock("../../plugins/provider-hook-runtime.js", () => ({
|
||||
wrapProviderStreamFn: vi.fn(() => undefined),
|
||||
}));
|
||||
|
||||
vi.mock("../../plugins/plugin-metadata-snapshot.js", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("../../plugins/plugin-metadata-snapshot.js")>()),
|
||||
isPluginMetadataSnapshotCompatible,
|
||||
}));
|
||||
|
||||
const gpt54Model = {
|
||||
id: "gpt-5.4",
|
||||
name: "GPT-5.4",
|
||||
@@ -90,8 +89,6 @@ function latestFollowupRouteCall(): {
|
||||
describe("AgentRuntimePlan", () => {
|
||||
afterEach(() => {
|
||||
resetConfigRuntimeState();
|
||||
manifestMocks.loadManifestMetadataSnapshot.mockClear();
|
||||
vi.mocked(resolveProviderRuntimePluginHandle).mockClear();
|
||||
});
|
||||
|
||||
it("defers default transport extra params until they are read", () => {
|
||||
@@ -292,8 +289,13 @@ describe("AgentRuntimePlan", () => {
|
||||
route: "dispatcher" as const,
|
||||
reason: "prepared-route",
|
||||
});
|
||||
const providerRuntimeHandle: ProviderRuntimePluginHandle = {
|
||||
const providerRuntimeHandle: ProviderRuntimePluginHandle & {
|
||||
modelId: string;
|
||||
prepared: true;
|
||||
} = {
|
||||
provider: "openai",
|
||||
modelId: "gpt-5.4",
|
||||
prepared: true,
|
||||
};
|
||||
|
||||
const plan = buildAgentRuntimePlan({
|
||||
@@ -323,27 +325,22 @@ describe("AgentRuntimePlan", () => {
|
||||
expect(followupCall.context?.dispatcherAvailable).toBe(true);
|
||||
});
|
||||
|
||||
it("resolves incomplete supplied provider handles before invoking runtime hooks", () => {
|
||||
const resolveProviderRuntimePluginHandleMock = vi.mocked(resolveProviderRuntimePluginHandle);
|
||||
it("reuses the provider handle prepared before plan construction", () => {
|
||||
const resolveProviderFollowupFallbackRouteMock = vi.mocked(
|
||||
resolveProviderFollowupFallbackRoute,
|
||||
);
|
||||
resolveProviderRuntimePluginHandleMock.mockClear();
|
||||
resolveProviderFollowupFallbackRouteMock.mockClear();
|
||||
|
||||
const suppliedHandle = {
|
||||
const suppliedHandle: ProviderRuntimePluginHandle & { modelId: string; prepared: true } = {
|
||||
provider: "openai",
|
||||
modelId: "gpt-5.4",
|
||||
prepared: true,
|
||||
config: { plugins: { allow: ["openai"] } },
|
||||
};
|
||||
const resolvedHandle: ProviderRuntimePluginHandle = {
|
||||
...suppliedHandle,
|
||||
workspaceDir: "/tmp/openclaw-runtime-plan",
|
||||
env: process.env,
|
||||
plugin: {} as never,
|
||||
};
|
||||
|
||||
resolveProviderRuntimePluginHandleMock.mockReturnValueOnce(resolvedHandle);
|
||||
|
||||
const plan = buildAgentRuntimePlan({
|
||||
provider: "openai",
|
||||
modelId: "gpt-5.4",
|
||||
@@ -352,7 +349,7 @@ describe("AgentRuntimePlan", () => {
|
||||
providerRuntimeHandle: suppliedHandle,
|
||||
});
|
||||
|
||||
expect(plan.providerRuntimeHandle).toBe(resolvedHandle);
|
||||
expect(plan.providerRuntimeHandle).toBe(suppliedHandle);
|
||||
|
||||
plan.delivery.resolveFollowupRoute({
|
||||
payload: { text: "hello" },
|
||||
@@ -360,39 +357,25 @@ describe("AgentRuntimePlan", () => {
|
||||
dispatcherAvailable: true,
|
||||
});
|
||||
|
||||
expect(resolveProviderRuntimePluginHandleMock).toHaveBeenCalledWith({
|
||||
provider: "openai",
|
||||
modelId: "gpt-5.4",
|
||||
config: suppliedHandle.config,
|
||||
workspaceDir: "/tmp/openclaw-runtime-plan",
|
||||
env: process.env,
|
||||
applyAutoEnable: undefined,
|
||||
bundledProviderVitestCompat: undefined,
|
||||
});
|
||||
const followupCall = latestFollowupRouteCall();
|
||||
expect(followupCall.runtimeHandle).toBe(resolvedHandle);
|
||||
expect(followupCall.runtimeHandle).toBe(suppliedHandle);
|
||||
});
|
||||
|
||||
it("resolves incomplete supplied delivery handles before follow-up routing", () => {
|
||||
const resolveProviderRuntimePluginHandleMock = vi.mocked(resolveProviderRuntimePluginHandle);
|
||||
it("reuses a delivery-only provider handle", () => {
|
||||
const resolveProviderFollowupFallbackRouteMock = vi.mocked(
|
||||
resolveProviderFollowupFallbackRoute,
|
||||
);
|
||||
resolveProviderRuntimePluginHandleMock.mockClear();
|
||||
resolveProviderFollowupFallbackRouteMock.mockClear();
|
||||
|
||||
const suppliedHandle = {
|
||||
provider: "openai",
|
||||
};
|
||||
const resolvedHandle: ProviderRuntimePluginHandle = {
|
||||
const suppliedHandle: ProviderRuntimePluginHandle & { modelId: string; prepared: true } = {
|
||||
provider: "openai",
|
||||
modelId: "gpt-5.4",
|
||||
prepared: true,
|
||||
workspaceDir: "/tmp/openclaw-runtime-plan",
|
||||
env: process.env,
|
||||
plugin: {} as never,
|
||||
};
|
||||
|
||||
resolveProviderRuntimePluginHandleMock.mockReturnValueOnce(resolvedHandle);
|
||||
|
||||
const delivery = buildAgentRuntimeDeliveryPlan({
|
||||
provider: "openai",
|
||||
modelId: "gpt-5.4",
|
||||
@@ -407,42 +390,42 @@ describe("AgentRuntimePlan", () => {
|
||||
dispatcherAvailable: true,
|
||||
});
|
||||
|
||||
expect(resolveProviderRuntimePluginHandleMock).toHaveBeenCalledWith({
|
||||
provider: "openai",
|
||||
modelId: "gpt-5.4",
|
||||
config: {},
|
||||
workspaceDir: "/tmp/openclaw-runtime-plan",
|
||||
env: process.env,
|
||||
applyAutoEnable: undefined,
|
||||
bundledProviderVitestCompat: undefined,
|
||||
});
|
||||
const followupCall = latestFollowupRouteCall();
|
||||
expect(followupCall.runtimeHandle).toBe(resolvedHandle);
|
||||
expect(followupCall.runtimeHandle).toBe(suppliedHandle);
|
||||
});
|
||||
|
||||
it("plans tool metadata against the runtime source snapshot lazily", () => {
|
||||
const sourceConfig = { channels: { telegram: { botToken: "token" } } };
|
||||
const runtimeConfig = {
|
||||
...sourceConfig,
|
||||
plugins: { allow: ["telegram"] },
|
||||
};
|
||||
setRuntimeConfigSnapshot(runtimeConfig, sourceConfig);
|
||||
|
||||
it("threads prepared tool metadata without discovery", () => {
|
||||
const metadataSnapshot = { plugins: [] };
|
||||
vi.mocked(resolveProviderRuntimePluginHandle).mockClear();
|
||||
const plan = buildAgentRuntimePlan({
|
||||
provider: "openai",
|
||||
modelId: "gpt-5.4",
|
||||
metadataSnapshot,
|
||||
});
|
||||
|
||||
expect(plan.tools.preparedPlanning?.metadataSnapshot).toBe(metadataSnapshot);
|
||||
expect(resolveProviderRuntimePluginHandle).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ pluginMetadataSnapshot: metadataSnapshot }),
|
||||
);
|
||||
});
|
||||
|
||||
it("validates threaded tool metadata against the source config projection", () => {
|
||||
const runtimeConfig = { plugins: { entries: { runtimeOnly: { enabled: true } } } };
|
||||
const sourceConfig = { plugins: { entries: {} } };
|
||||
const metadataSnapshot = { plugins: [] };
|
||||
setRuntimeConfigSnapshot(runtimeConfig, sourceConfig);
|
||||
isPluginMetadataSnapshotCompatible.mockClear();
|
||||
|
||||
buildAgentRuntimePlan({
|
||||
provider: "openai",
|
||||
modelId: "gpt-5.4",
|
||||
config: runtimeConfig,
|
||||
workspaceDir: "/tmp/openclaw-runtime-plan",
|
||||
metadataSnapshot,
|
||||
});
|
||||
|
||||
expect(manifestMocks.loadManifestMetadataSnapshot).not.toHaveBeenCalled();
|
||||
|
||||
plan.tools.preparedPlanning?.loadMetadataSnapshot?.();
|
||||
|
||||
expect(manifestMocks.loadManifestMetadataSnapshot).toHaveBeenCalledWith({
|
||||
config: sourceConfig,
|
||||
workspaceDir: "/tmp/openclaw-runtime-plan",
|
||||
env: process.env,
|
||||
});
|
||||
expect(isPluginMetadataSnapshotCompatible).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({ config: sourceConfig }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,7 +8,10 @@ import { isSilentReplyPayloadText, SILENT_REPLY_TOKEN } from "../../auto-reply/t
|
||||
import { projectConfigOntoRuntimeSourceSnapshot } from "../../config/config.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { hasReplyPayloadContent } from "../../interactive/payload.js";
|
||||
import { loadManifestMetadataSnapshot } from "../../plugins/manifest-contract-eligibility.js";
|
||||
import {
|
||||
isPluginMetadataSnapshotCompatible,
|
||||
resolvePluginMetadataSnapshot,
|
||||
} from "../../plugins/plugin-metadata-snapshot.js";
|
||||
import type { PluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.types.js";
|
||||
import {
|
||||
resolveProviderRuntimePluginHandle,
|
||||
@@ -54,40 +57,54 @@ function asProviderRuntimeModel(
|
||||
return value !== undefined ? (value as ProviderRuntimeModel) : undefined;
|
||||
}
|
||||
|
||||
function isProviderRuntimePluginHandle(
|
||||
value: BuildAgentRuntimePlanParams["providerRuntimeHandle"] | ProviderRuntimePluginHandle,
|
||||
): value is ProviderRuntimePluginHandle {
|
||||
return value !== undefined && "plugin" in value;
|
||||
type RuntimePlanMetadataParams = BuildAgentRuntimeDeliveryPlanParams & {
|
||||
metadataSnapshot?: BuildAgentRuntimePlanParams["metadataSnapshot"];
|
||||
};
|
||||
|
||||
function resolveCompatibleMetadataSnapshot(
|
||||
params: RuntimePlanMetadataParams,
|
||||
config: OpenClawConfig | undefined = asOpenClawConfig(params.config),
|
||||
): PluginMetadataSnapshot | undefined {
|
||||
const metadataSnapshot = params.metadataSnapshot as PluginMetadataSnapshot | undefined;
|
||||
return metadataSnapshot &&
|
||||
metadataSnapshot.pluginIds === undefined &&
|
||||
isPluginMetadataSnapshotCompatible({
|
||||
snapshot: metadataSnapshot,
|
||||
config,
|
||||
env: process.env,
|
||||
workspaceDir: params.workspaceDir,
|
||||
})
|
||||
? metadataSnapshot
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function resolveProviderRuntimeHandleForPlugins(params: {
|
||||
provider: string;
|
||||
modelId?: string;
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
runtimeHandle?: BuildAgentRuntimePlanParams["providerRuntimeHandle"];
|
||||
resolveWhenMissing?: boolean;
|
||||
}): ProviderRuntimePluginHandle | undefined {
|
||||
function resolvePreparedProviderRuntimeHandle(
|
||||
params: RuntimePlanMetadataParams,
|
||||
): ProviderRuntimePluginHandle & { modelId: string; prepared: true } {
|
||||
if (
|
||||
isProviderRuntimePluginHandle(params.runtimeHandle) &&
|
||||
(params.runtimeHandle.plugin ||
|
||||
!params.modelId ||
|
||||
params.runtimeHandle.modelId === params.modelId)
|
||||
params.providerRuntimeHandle?.prepared === true &&
|
||||
params.providerRuntimeHandle.provider === params.provider &&
|
||||
params.providerRuntimeHandle.modelId === params.modelId &&
|
||||
params.providerRuntimeHandle.workspaceDir === params.workspaceDir
|
||||
) {
|
||||
return params.runtimeHandle;
|
||||
return params.providerRuntimeHandle as ProviderRuntimePluginHandle & {
|
||||
modelId: string;
|
||||
prepared: true;
|
||||
};
|
||||
}
|
||||
if (!params.runtimeHandle && !params.resolveWhenMissing) {
|
||||
return undefined;
|
||||
}
|
||||
return resolveProviderRuntimePluginHandle({
|
||||
provider: params.runtimeHandle?.provider ?? params.provider,
|
||||
const compatibleMetadataSnapshot = resolveCompatibleMetadataSnapshot(params);
|
||||
return {
|
||||
...resolveProviderRuntimePluginHandle({
|
||||
provider: params.provider,
|
||||
modelId: params.modelId,
|
||||
config: asOpenClawConfig(params.config),
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: process.env,
|
||||
...(compatibleMetadataSnapshot ? { pluginMetadataSnapshot: compatibleMetadataSnapshot } : {}),
|
||||
}),
|
||||
modelId: params.modelId,
|
||||
config: asOpenClawConfig(params.runtimeHandle?.config) ?? params.config,
|
||||
workspaceDir: params.runtimeHandle?.workspaceDir ?? params.workspaceDir,
|
||||
env: params.runtimeHandle?.env ?? process.env,
|
||||
applyAutoEnable: params.runtimeHandle?.applyAutoEnable,
|
||||
bundledProviderVitestCompat: params.runtimeHandle?.bundledProviderVitestCompat,
|
||||
});
|
||||
prepared: true,
|
||||
};
|
||||
}
|
||||
|
||||
/** Build delivery-specific runtime decisions for one provider/model. */
|
||||
@@ -95,13 +112,7 @@ export function buildAgentRuntimeDeliveryPlan(
|
||||
params: BuildAgentRuntimeDeliveryPlanParams,
|
||||
): AgentRuntimeDeliveryPlan {
|
||||
const config = asOpenClawConfig(params.config);
|
||||
const providerRuntimeHandle = resolveProviderRuntimeHandleForPlugins({
|
||||
provider: params.provider,
|
||||
modelId: params.modelId,
|
||||
config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
runtimeHandle: params.providerRuntimeHandle,
|
||||
});
|
||||
const providerRuntimeHandle = resolvePreparedProviderRuntimeHandle(params);
|
||||
return {
|
||||
isSilentPayload(payload): boolean {
|
||||
return (
|
||||
@@ -146,25 +157,21 @@ export function buildAgentRuntimePlan(params: BuildAgentRuntimePlanParams): Agen
|
||||
const modelApi = params.modelApi ?? params.model?.api ?? undefined;
|
||||
const transport = params.resolvedTransport;
|
||||
const toolPlanningConfig = config ? projectConfigOntoRuntimeSourceSnapshot(config) : undefined;
|
||||
let toolPlanningMetadataSnapshot: PluginMetadataSnapshot | undefined;
|
||||
const loadToolPlanningMetadataSnapshot = () => {
|
||||
// Metadata is process-stable for one run; load lazily because many attempts
|
||||
// never need prepared tool planning.
|
||||
toolPlanningMetadataSnapshot ??= loadManifestMetadataSnapshot({
|
||||
config: toolPlanningConfig,
|
||||
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
|
||||
env: process.env,
|
||||
});
|
||||
return toolPlanningMetadataSnapshot;
|
||||
};
|
||||
const providerRuntimeHandleForPlugins = resolveProviderRuntimeHandleForPlugins({
|
||||
provider: params.provider,
|
||||
modelId: params.modelId,
|
||||
config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
runtimeHandle: params.providerRuntimeHandle,
|
||||
resolveWhenMissing: true,
|
||||
});
|
||||
const toolPlanningMetadataSnapshot = resolveCompatibleMetadataSnapshot(
|
||||
params,
|
||||
toolPlanningConfig,
|
||||
);
|
||||
const preparedPlanning = toolPlanningMetadataSnapshot
|
||||
? { metadataSnapshot: toolPlanningMetadataSnapshot }
|
||||
: {
|
||||
loadMetadataSnapshot: () =>
|
||||
resolvePluginMetadataSnapshot({
|
||||
config: toolPlanningConfig,
|
||||
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
|
||||
env: process.env,
|
||||
}),
|
||||
};
|
||||
const providerRuntimeHandleForPlugins = resolvePreparedProviderRuntimeHandle(params);
|
||||
const auth =
|
||||
params.preparedAuthPlan ??
|
||||
buildAgentRuntimeAuthPlan({
|
||||
@@ -294,9 +301,7 @@ export function buildAgentRuntimePlan(params: BuildAgentRuntimePlanParams): Agen
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
preparedPlanning: {
|
||||
loadMetadataSnapshot: loadToolPlanningMetadataSnapshot,
|
||||
},
|
||||
preparedPlanning,
|
||||
normalize<TSchemaType extends TSchema = TSchema, TResult = unknown>(
|
||||
tools: AgentTool<TSchemaType, TResult>[],
|
||||
overrides?: {
|
||||
|
||||
@@ -88,6 +88,7 @@ type AgentRuntimeTextTransforms = {
|
||||
/** Resolved provider runtime handle forwarded to plugin-owned hooks. */
|
||||
type AgentRuntimeProviderHandle = {
|
||||
provider: string;
|
||||
modelId?: string | null;
|
||||
config?: AgentRuntimeConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
@@ -95,6 +96,11 @@ type AgentRuntimeProviderHandle = {
|
||||
bundledProviderVitestCompat?: boolean;
|
||||
};
|
||||
|
||||
type PreparedAgentRuntimeProviderHandle = AgentRuntimeProviderHandle & {
|
||||
modelId: string | null;
|
||||
prepared: true;
|
||||
};
|
||||
|
||||
type AgentRuntimeInteractiveButtonStyle = "primary" | "secondary" | "success" | "danger";
|
||||
|
||||
type AgentRuntimeMessagePresentationAction =
|
||||
@@ -513,7 +519,7 @@ type AgentRuntimeTransportPlan = {
|
||||
/** Complete prepared runtime plan consumed by embedded-agent attempts. */
|
||||
export type AgentRuntimePlan = {
|
||||
resolvedRef: AgentRuntimeResolvedRef;
|
||||
providerRuntimeHandle?: AgentRuntimeProviderHandle;
|
||||
providerRuntimeHandle?: PreparedAgentRuntimeProviderHandle;
|
||||
auth: AgentRuntimeAuthPlan;
|
||||
prompt: AgentRuntimePromptPlan;
|
||||
tools: AgentRuntimeToolPlan;
|
||||
@@ -546,7 +552,7 @@ export type BuildAgentRuntimeDeliveryPlanParams = {
|
||||
agentDir?: string;
|
||||
provider: string;
|
||||
modelId: string;
|
||||
providerRuntimeHandle?: AgentRuntimeProviderHandle;
|
||||
providerRuntimeHandle?: PreparedAgentRuntimeProviderHandle;
|
||||
};
|
||||
|
||||
/** Inputs needed to build the full prepared runtime plan. */
|
||||
@@ -574,5 +580,8 @@ export type BuildAgentRuntimePlanParams = {
|
||||
thinkingLevel?: AgentRuntimeThinkLevel;
|
||||
extraParamsOverride?: Record<string, unknown>;
|
||||
resolvedTransport?: AgentRuntimeTransport;
|
||||
providerRuntimeHandle?: AgentRuntimeProviderHandle;
|
||||
/** Omit only when a standalone caller intentionally resolves provider hooks lazily. */
|
||||
providerRuntimeHandle?: PreparedAgentRuntimeProviderHandle;
|
||||
/** Lifecycle-owned plugin metadata prepared before the attempt starts. */
|
||||
metadataSnapshot?: AgentRuntimePreparedMetadataSnapshot;
|
||||
};
|
||||
|
||||
@@ -3,12 +3,12 @@ import { uniqueStrings } from "@openclaw/normalization-core/string-normalization
|
||||
import { normalizeConfiguredMcpServers } from "../config/mcp-config-normalize.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { normalizePluginsConfig } from "../plugins/config-state.js";
|
||||
import {
|
||||
isManifestPluginAvailableForControlPlane,
|
||||
loadManifestMetadataSnapshot,
|
||||
} from "../plugins/manifest-contract-eligibility.js";
|
||||
import { getCurrentPluginMetadataSnapshot } from "../plugins/current-plugin-metadata-snapshot.js";
|
||||
import { isManifestPluginAvailableForControlPlane } from "../plugins/manifest-contract-eligibility.js";
|
||||
import type { PluginManifestRecord } from "../plugins/manifest-registry.js";
|
||||
import { hasManifestToolAvailability } from "../plugins/manifest-tool-availability.js";
|
||||
import { isPluginMetadataSnapshotCompatible } from "../plugins/plugin-metadata-snapshot.js";
|
||||
import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.types.js";
|
||||
import { sanitizeServerName, TOOL_NAME_SEPARATOR } from "./agent-bundle-mcp-names.js";
|
||||
import { compileGlobPatterns, matchesAnyGlobPattern } from "./glob-pattern.js";
|
||||
import type { DeclaredToolAllowlistContext } from "./tool-policy.js";
|
||||
@@ -126,16 +126,33 @@ function collectDeclaredPluginContext(params: {
|
||||
workspaceDir?: string;
|
||||
toolDenylist?: string[];
|
||||
env?: NodeJS.ProcessEnv;
|
||||
metadataSnapshot?: PluginMetadataSnapshot;
|
||||
}): Pick<DeclaredToolAllowlistContext, "pluginIds" | "pluginToolNames"> {
|
||||
if (params.config?.plugins?.enabled === false) {
|
||||
return {};
|
||||
}
|
||||
const env = params.env ?? process.env;
|
||||
const snapshot = loadManifestMetadataSnapshot({
|
||||
config: params.config,
|
||||
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
|
||||
env,
|
||||
});
|
||||
const preparedSnapshot =
|
||||
params.metadataSnapshot &&
|
||||
params.metadataSnapshot.pluginIds === undefined &&
|
||||
isPluginMetadataSnapshotCompatible({
|
||||
snapshot: params.metadataSnapshot,
|
||||
config: params.config,
|
||||
env,
|
||||
workspaceDir: params.workspaceDir,
|
||||
})
|
||||
? params.metadataSnapshot
|
||||
: undefined;
|
||||
const snapshot =
|
||||
preparedSnapshot ??
|
||||
getCurrentPluginMetadataSnapshot({
|
||||
config: params.config,
|
||||
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
|
||||
env,
|
||||
});
|
||||
if (!snapshot) {
|
||||
return {};
|
||||
}
|
||||
const normalizedPlugins = normalizePluginsConfig(params.config?.plugins);
|
||||
const denylist = normalizeToolDenylist(params.toolDenylist);
|
||||
const pluginIds = new Set<string>();
|
||||
@@ -175,6 +192,7 @@ export function buildDeclaredToolAllowlistContext(params: {
|
||||
workspaceDir?: string;
|
||||
toolDenylist?: string[];
|
||||
env?: NodeJS.ProcessEnv;
|
||||
metadataSnapshot?: PluginMetadataSnapshot;
|
||||
}): DeclaredToolAllowlistContext | undefined {
|
||||
const mcpServerNames = uniqueStrings(
|
||||
collectConfiguredMcpServerNames({
|
||||
|
||||
@@ -30,10 +30,10 @@ import {
|
||||
type MediaUnderstandingProvider,
|
||||
} from "../../plugin-sdk/media-understanding.js";
|
||||
import { resolvePluginCapabilityProvider } from "../../plugins/capability-provider-runtime.js";
|
||||
import {
|
||||
isManifestPluginAvailableForControlPlane,
|
||||
loadManifestMetadataSnapshot,
|
||||
} from "../../plugins/manifest-contract-eligibility.js";
|
||||
import { getCurrentPluginMetadataSnapshot } from "../../plugins/current-plugin-metadata-snapshot.js";
|
||||
import { isManifestPluginAvailableForControlPlane } from "../../plugins/manifest-contract-eligibility.js";
|
||||
import { isPluginMetadataSnapshotCompatible } from "../../plugins/plugin-metadata-snapshot.js";
|
||||
import type { PluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.types.js";
|
||||
import type { ProviderRuntimeModel } from "../../plugins/provider-runtime-model.types.js";
|
||||
import { resolveUserPath } from "../../utils.js";
|
||||
import type { AuthProfileStore } from "../auth-profiles/types.js";
|
||||
@@ -490,6 +490,7 @@ function providerUsesRuntimeModelAugment(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
provider: string;
|
||||
workspaceDir?: string;
|
||||
metadataSnapshot?: PluginMetadataSnapshot;
|
||||
}): boolean {
|
||||
const provider = normalizeMediaProviderId(params.provider);
|
||||
if (!provider) {
|
||||
@@ -499,11 +500,27 @@ function providerUsesRuntimeModelAugment(params: {
|
||||
return true;
|
||||
}
|
||||
const config = params.cfg ?? {};
|
||||
const snapshot = loadManifestMetadataSnapshot({
|
||||
config,
|
||||
env: process.env,
|
||||
...(params.workspaceDir !== undefined ? { workspaceDir: params.workspaceDir } : {}),
|
||||
});
|
||||
const preparedSnapshot =
|
||||
params.metadataSnapshot &&
|
||||
params.metadataSnapshot.pluginIds === undefined &&
|
||||
isPluginMetadataSnapshotCompatible({
|
||||
snapshot: params.metadataSnapshot,
|
||||
config,
|
||||
env: process.env,
|
||||
workspaceDir: params.workspaceDir,
|
||||
})
|
||||
? params.metadataSnapshot
|
||||
: undefined;
|
||||
const snapshot =
|
||||
preparedSnapshot ??
|
||||
getCurrentPluginMetadataSnapshot({
|
||||
config,
|
||||
env: process.env,
|
||||
...(params.workspaceDir !== undefined ? { workspaceDir: params.workspaceDir } : {}),
|
||||
});
|
||||
if (!snapshot) {
|
||||
return false;
|
||||
}
|
||||
return snapshot.plugins.some((plugin) => {
|
||||
const ownsProvider =
|
||||
plugin.providers.some((candidate) => normalizeMediaProviderId(candidate) === provider) ||
|
||||
@@ -559,6 +576,7 @@ async function resolveCompressionModelPolicy(params: {
|
||||
model: string;
|
||||
agentDir?: string;
|
||||
workspaceDir?: string;
|
||||
metadataSnapshot?: PluginMetadataSnapshot;
|
||||
}): Promise<ImageCompressionModelPolicy> {
|
||||
const configuredStaticPolicy = await resolveCompressionModelPolicyWithHooks({
|
||||
...params,
|
||||
@@ -574,6 +592,7 @@ async function resolveCompressionModelPolicy(params: {
|
||||
cfg: params.cfg,
|
||||
provider: params.provider,
|
||||
workspaceDir: params.workspaceDir,
|
||||
metadataSnapshot: params.metadataSnapshot,
|
||||
})
|
||||
) {
|
||||
return staticPolicy;
|
||||
@@ -592,6 +611,7 @@ async function resolveImageCompressionPolicy(params: {
|
||||
imageCount: number;
|
||||
agentDir?: string;
|
||||
workspaceDir?: string;
|
||||
metadataSnapshot?: PluginMetadataSnapshot;
|
||||
}): Promise<ImageCompressionPolicy> {
|
||||
const modelCandidates = resolveCompressionModelCandidates(params);
|
||||
const quality = params.cfg?.agents?.defaults?.imageQuality;
|
||||
@@ -603,6 +623,7 @@ async function resolveImageCompressionPolicy(params: {
|
||||
model: candidate.model,
|
||||
agentDir: params.agentDir,
|
||||
workspaceDir: params.workspaceDir,
|
||||
metadataSnapshot: params.metadataSnapshot,
|
||||
});
|
||||
}),
|
||||
);
|
||||
@@ -974,6 +995,7 @@ export function createImageTool(options?: {
|
||||
imageCount: imageInputs.length,
|
||||
agentDir,
|
||||
workspaceDir: options?.workspaceDir,
|
||||
metadataSnapshot: options?.preparedModelRuntime?.metadataSnapshot,
|
||||
});
|
||||
imageRoute = { kind: "fallback", imageModelConfig, imageCompression };
|
||||
}
|
||||
|
||||
@@ -20,7 +20,11 @@ import {
|
||||
} from "../../config/sessions.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { triggerSessionPatchHook } from "../../gateway/session-patch-hooks.js";
|
||||
import { loadManifestMetadataSnapshot } from "../../plugins/manifest-contract-eligibility.js";
|
||||
import {
|
||||
isPluginMetadataSnapshotCompatible,
|
||||
resolvePluginMetadataSnapshot,
|
||||
} from "../../plugins/plugin-metadata-snapshot.js";
|
||||
import type { PluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.types.js";
|
||||
import {
|
||||
buildAgentMainSessionKey,
|
||||
parseAgentSessionKey,
|
||||
@@ -441,6 +445,7 @@ async function resolveModelOverride(params: {
|
||||
agentId: string;
|
||||
agentDir: string;
|
||||
workspaceDir: string;
|
||||
metadataSnapshot?: PluginMetadataSnapshot;
|
||||
}): Promise<
|
||||
| { kind: "reset" }
|
||||
| {
|
||||
@@ -475,13 +480,24 @@ async function resolveModelOverride(params: {
|
||||
? { workspaceDir: params.sessionEntry.spawnedWorkspaceDir }
|
||||
: {}),
|
||||
});
|
||||
const manifestMetadataSnapshot = loadManifestMetadataSnapshot({
|
||||
config: params.cfg,
|
||||
workspaceDir: params.sessionEntry?.spawnedWorkspaceDir,
|
||||
env: process.env,
|
||||
});
|
||||
const workspaceDir = params.sessionEntry?.spawnedWorkspaceDir ?? params.workspaceDir;
|
||||
const manifestMetadataSnapshot =
|
||||
params.metadataSnapshot &&
|
||||
params.metadataSnapshot.pluginIds === undefined &&
|
||||
isPluginMetadataSnapshotCompatible({
|
||||
snapshot: params.metadataSnapshot,
|
||||
config: params.cfg,
|
||||
env: process.env,
|
||||
workspaceDir,
|
||||
})
|
||||
? params.metadataSnapshot
|
||||
: resolvePluginMetadataSnapshot({
|
||||
config: params.cfg,
|
||||
...(workspaceDir ? { workspaceDir } : {}),
|
||||
env: process.env,
|
||||
});
|
||||
const modelManifestContext = {
|
||||
manifestPlugins: manifestMetadataSnapshot.plugins,
|
||||
manifestPlugins: manifestMetadataSnapshot?.plugins,
|
||||
};
|
||||
const policy = createModelVisibilityPolicy({
|
||||
cfg: params.cfg,
|
||||
@@ -532,6 +548,7 @@ export function createSessionStatusTool(opts?: {
|
||||
sandboxed?: boolean;
|
||||
activeModelProvider?: string;
|
||||
activeModelId?: string;
|
||||
metadataSnapshot?: PluginMetadataSnapshot;
|
||||
/** Active live-run route, kept separate from the persisted/origin delivery route. */
|
||||
activeDeliveryContext?: DeliveryContext;
|
||||
}): AnyAgentTool {
|
||||
@@ -845,6 +862,7 @@ export function createSessionStatusTool(opts?: {
|
||||
agentId,
|
||||
agentDir: selectedAgentDir,
|
||||
workspaceDir: selectedWorkspaceDir,
|
||||
metadataSnapshot: opts?.metadataSnapshot,
|
||||
});
|
||||
const modelSelection =
|
||||
selection.kind === "reset"
|
||||
|
||||
@@ -31,6 +31,7 @@ const resolveCommandSecretRefsViaGatewayMock = vi.fn();
|
||||
const resolveQueuedReplyExecutionConfigMock = vi.fn();
|
||||
const resolveProviderFollowupFallbackRouteMock = vi.fn();
|
||||
const resolveProviderThinkingProfileMock = vi.fn();
|
||||
const admitReplyTurnMock = vi.fn();
|
||||
let resolveQueuedReplyExecutionConfigActual:
|
||||
| (typeof import("./agent-runner-utils.js"))["resolveQueuedReplyExecutionConfig"]
|
||||
| undefined;
|
||||
@@ -428,6 +429,18 @@ async function loadFreshFollowupRunnerModuleForTest() {
|
||||
refreshQueuedFollowupSession: refreshQueuedFollowupSessionForFollowupTest,
|
||||
resolveQueueSettings: (): QueueSettings => ({ mode: "followup" }),
|
||||
}));
|
||||
vi.doMock("./reply-turn-admission.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("./reply-turn-admission.js")>(
|
||||
"./reply-turn-admission.js",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
admitReplyTurn: (...args: Parameters<typeof actual.admitReplyTurn>) =>
|
||||
admitReplyTurnMock.getMockImplementation()
|
||||
? admitReplyTurnMock(...args)
|
||||
: actual.admitReplyTurn(...args),
|
||||
};
|
||||
});
|
||||
vi.doMock("./session-run-accounting.js", () => ({
|
||||
persistRunSessionUsage: persistRunSessionUsageForFollowupTest,
|
||||
incrementRunCompactionCount: incrementRunCompactionCountForFollowupTest,
|
||||
@@ -620,6 +633,7 @@ beforeEach(() => {
|
||||
resolveProviderFollowupFallbackRouteMock.mockReturnValue(undefined);
|
||||
resolveProviderThinkingProfileMock.mockReset();
|
||||
resolveProviderThinkingProfileMock.mockReturnValue(undefined);
|
||||
admitReplyTurnMock.mockReset();
|
||||
const resolveQueuedReplyExecutionConfig = resolveQueuedReplyExecutionConfigActual;
|
||||
if (!resolveQueuedReplyExecutionConfig) {
|
||||
throw new Error("resolveQueuedReplyExecutionConfig mock not initialized");
|
||||
@@ -692,7 +706,10 @@ function createQueuedRun(
|
||||
describe("createFollowupRunner reply-lane admission", () => {
|
||||
it("drops stale active-goal context after the persisted goal completes", async () => {
|
||||
runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [], meta: {} });
|
||||
const storePath = "/tmp/openclaw-followup-completed-goal.json";
|
||||
const storePath = path.join(
|
||||
tmpdir(),
|
||||
`openclaw-followup-completed-goal-${crypto.randomUUID()}.json`,
|
||||
);
|
||||
const activeEntry: SessionEntry = {
|
||||
sessionId: "session-completed-goal",
|
||||
updatedAt: 1,
|
||||
@@ -714,6 +731,14 @@ describe("createFollowupRunner reply-lane admission", () => {
|
||||
goal: { ...activeEntry.goal!, status: "complete", updatedAt: 2 },
|
||||
};
|
||||
registerFollowupTestSessionStore(storePath, { main: completedEntry });
|
||||
admitReplyTurnMock.mockResolvedValueOnce({
|
||||
status: "admitted",
|
||||
operation: createReplyOperationForTest({
|
||||
sessionKey: "main",
|
||||
sessionId: completedEntry.sessionId,
|
||||
resetTriggered: false,
|
||||
}),
|
||||
});
|
||||
const runner = createFollowupRunner({
|
||||
typing: createMockTypingController(),
|
||||
typingMode: "instant",
|
||||
@@ -749,7 +774,7 @@ describe("createFollowupRunner reply-lane admission", () => {
|
||||
const context = requireRecord(call.currentInboundContext, "current inbound context");
|
||||
expect(context.text).toContain("Current message:\nmessage_id=next-turn");
|
||||
expect(context.text).not.toContain("Active goal:");
|
||||
});
|
||||
}, 300_000);
|
||||
|
||||
it("keeps the originating client caps on queued embedded runs", async () => {
|
||||
// Regression: the queued path built runEmbeddedAgent params inline and
|
||||
|
||||
@@ -116,6 +116,34 @@ describe("createModelListAuthIndex", () => {
|
||||
expect(index.evaluateModelAuth("disabled-provider").availability).toBeUndefined();
|
||||
});
|
||||
|
||||
it("uses enabled synthetic refs from prepared metadata without reloading the registry", () => {
|
||||
const metadataSnapshot = {
|
||||
registrySource: "persisted",
|
||||
registryDiagnostics: [],
|
||||
plugins: [],
|
||||
index: {
|
||||
plugins: [
|
||||
{ enabled: true, syntheticAuthRefs: ["codex"] },
|
||||
{ enabled: false, syntheticAuthRefs: ["disabled-provider"] },
|
||||
],
|
||||
},
|
||||
} as unknown as PluginMetadataSnapshot;
|
||||
const index = createModelListAuthIndex({
|
||||
cfg: {},
|
||||
authStore: emptyStore,
|
||||
env: {},
|
||||
metadataSnapshot,
|
||||
routeResolverFactory: dualRouteResolverFactory,
|
||||
});
|
||||
|
||||
expect(index.evaluateModelAuth("openai", { modelId: "gpt-5.5" })).toMatchObject({
|
||||
availability: undefined,
|
||||
evidence: "synthetic",
|
||||
});
|
||||
expect(index.evaluateModelAuth("disabled-provider").availability).toBeUndefined();
|
||||
expect(pluginRegistryMocks.loadPluginRegistrySnapshotWithMetadata).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each(["derived" as const, "persisted" as const])(
|
||||
"does not trust unusable synthetic refs from a %s snapshot",
|
||||
(source) => {
|
||||
@@ -140,10 +168,15 @@ describe("createModelListAuthIndex", () => {
|
||||
);
|
||||
|
||||
it("uses explicit synthetic refs without loading plugin metadata", () => {
|
||||
const metadataSnapshot = {
|
||||
registrySource: "persisted",
|
||||
plugins: [],
|
||||
} as unknown as PluginMetadataSnapshot;
|
||||
const index = createModelListAuthIndex({
|
||||
cfg: {},
|
||||
authStore: emptyStore,
|
||||
env: {},
|
||||
metadataSnapshot,
|
||||
syntheticAuthProviderRefs: ["codex"],
|
||||
routeResolverFactory: dualRouteResolverFactory,
|
||||
});
|
||||
|
||||
@@ -35,16 +35,27 @@ function listValidatedSyntheticAuthProviderRefs(params: {
|
||||
env: NodeJS.ProcessEnv;
|
||||
metadataSnapshot?: PluginMetadataSnapshot;
|
||||
}): readonly string[] {
|
||||
if (params.metadataSnapshot && (params.metadataSnapshot.registryDiagnostics?.length ?? 0) > 0) {
|
||||
return [];
|
||||
if (params.metadataSnapshot) {
|
||||
if (
|
||||
params.metadataSnapshot.registryDiagnostics.length > 0 ||
|
||||
(params.metadataSnapshot.registrySource !== "persisted" &&
|
||||
params.metadataSnapshot.registrySource !== "provided")
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
return params.metadataSnapshot.index.plugins
|
||||
.filter((plugin) => plugin.enabled)
|
||||
.flatMap((plugin) => plugin.syntheticAuthRefs ?? []);
|
||||
}
|
||||
const result = loadPluginRegistrySnapshotWithMetadata({
|
||||
config: params.cfg,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
index: params.metadataSnapshot?.index,
|
||||
});
|
||||
if (result.source !== "persisted" && result.source !== "provided") {
|
||||
if (
|
||||
result.diagnostics.length > 0 ||
|
||||
(result.source !== "persisted" && result.source !== "provided")
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
return result.snapshot.plugins
|
||||
|
||||
@@ -7,7 +7,6 @@ import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest
|
||||
import { startGatewayConfigReloader } from "../gateway/config-reload.js";
|
||||
import { executeSqliteQueryTakeFirstSync, getNodeSqliteKysely } from "../infra/kysely-sync.js";
|
||||
import type { PluginManifestRegistry } from "../plugins/manifest-registry.js";
|
||||
import { clearLoadPluginMetadataSnapshotMemo } from "../plugins/plugin-metadata-snapshot.js";
|
||||
import { readConfigMachineState } from "../state/config-machine-state.js";
|
||||
import type { DB as OpenClawStateKyselyDatabase } from "../state/openclaw-state-db.generated.js";
|
||||
import {
|
||||
@@ -128,7 +127,6 @@ describe("config io write", () => {
|
||||
|
||||
afterEach(() => {
|
||||
resetConfigRuntimeState();
|
||||
clearLoadPluginMetadataSnapshotMemo();
|
||||
mockMaintainConfigBackups.mockReset();
|
||||
mockMaintainConfigBackups.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
@@ -11,7 +11,8 @@ import { modelKey, parseModelRef, resolveDefaultModelForAgent } from "../agents/
|
||||
import { createModelVisibilityPolicy } from "../agents/model-visibility-policy.js";
|
||||
import { getRuntimeConfig } from "../config/io.js";
|
||||
import { resolveSessionEntryAccessTarget } from "../config/sessions/session-accessor.js";
|
||||
import { loadManifestMetadataSnapshot } from "../plugins/manifest-contract-eligibility.js";
|
||||
import { getCurrentPluginMetadataSnapshot } from "../plugins/current-plugin-metadata-snapshot.js";
|
||||
import { getActivePluginRegistryWorkspaceDirFromState } from "../plugins/runtime-state.js";
|
||||
import {
|
||||
buildAgentMainSessionKey,
|
||||
isAcpSessionKey,
|
||||
@@ -138,12 +139,14 @@ export async function resolveOpenAiCompatModelOverride(params: {
|
||||
const cfg = getRuntimeConfig();
|
||||
const defaultModelRef = resolveDefaultModelForAgent({ cfg, agentId: params.agentId });
|
||||
const defaultProvider = defaultModelRef.provider;
|
||||
const manifestMetadataSnapshot = loadManifestMetadataSnapshot({
|
||||
const workspaceDir = getActivePluginRegistryWorkspaceDirFromState();
|
||||
const manifestMetadataSnapshot = getCurrentPluginMetadataSnapshot({
|
||||
config: cfg,
|
||||
env: process.env,
|
||||
...(workspaceDir ? { workspaceDir } : {}),
|
||||
});
|
||||
const modelManifestContext = {
|
||||
manifestPlugins: manifestMetadataSnapshot.plugins,
|
||||
manifestPlugins: manifestMetadataSnapshot?.plugins,
|
||||
};
|
||||
const parsed = parseModelRef(raw, defaultProvider, {
|
||||
allowManifestNormalization: true,
|
||||
|
||||
@@ -8,7 +8,6 @@ import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resetLogger, setLoggerOverride } from "../logging/logger.js";
|
||||
import { loggingState } from "../logging/state.js";
|
||||
import type { PluginManifestRecord, PluginManifestRegistry } from "../plugins/manifest-registry.js";
|
||||
import { clearLoadPluginMetadataSnapshotMemo } from "../plugins/plugin-metadata-snapshot.js";
|
||||
import { withFetchPreconnect } from "../test-utils/fetch-mock.js";
|
||||
|
||||
const normalizeProviderModelIdWithRuntimeMock = vi.hoisted(() =>
|
||||
@@ -138,7 +137,6 @@ function requireAbortSignal(signal: RequestInit["signal"] | undefined): AbortSig
|
||||
describe("model-pricing-cache", () => {
|
||||
beforeEach(() => {
|
||||
clearGatewayModelPricingState();
|
||||
clearLoadPluginMetadataSnapshotMemo();
|
||||
pluginManifestRegistryMocks.manifestRegistry = undefined;
|
||||
pluginManifestRegistryMocks.loadPluginManifestRegistryForInstalledIndex.mockClear();
|
||||
pluginManifestRegistryMocks.listOpenClawPluginManifestMetadata.mockClear();
|
||||
@@ -147,7 +145,6 @@ describe("model-pricing-cache", () => {
|
||||
|
||||
afterEach(() => {
|
||||
clearGatewayModelPricingState();
|
||||
clearLoadPluginMetadataSnapshotMemo();
|
||||
loggingState.rawConsole = null;
|
||||
resetLogger();
|
||||
});
|
||||
|
||||
@@ -129,7 +129,6 @@ let prepareMediaCapabilityProviders: typeof import("./capability-provider-runtim
|
||||
let clearCurrentPluginMetadataSnapshot: typeof import("./current-plugin-metadata-snapshot.js").clearCurrentPluginMetadataSnapshot;
|
||||
let setCurrentPluginMetadataSnapshot: typeof import("./current-plugin-metadata-snapshot.js").setCurrentPluginMetadataSnapshot;
|
||||
let clearPluginMetadataLifecycleCaches: typeof import("./plugin-metadata-lifecycle.js").clearPluginMetadataLifecycleCaches;
|
||||
let clearLoadPluginMetadataSnapshotMemo: typeof import("./plugin-metadata-snapshot.js").clearLoadPluginMetadataSnapshotMemo;
|
||||
|
||||
function expectResolvedCapabilityProviderIds(providers: Array<{ id: string }>, expected: string[]) {
|
||||
expect(providers.map((provider) => provider.id)).toEqual(expected);
|
||||
@@ -292,11 +291,9 @@ describe("resolvePluginCapabilityProviders", () => {
|
||||
({ clearCurrentPluginMetadataSnapshot, setCurrentPluginMetadataSnapshot } =
|
||||
await import("./current-plugin-metadata-snapshot.js"));
|
||||
({ clearPluginMetadataLifecycleCaches } = await import("./plugin-metadata-lifecycle.js"));
|
||||
({ clearLoadPluginMetadataSnapshotMemo } = await import("./plugin-metadata-snapshot.js"));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
clearLoadPluginMetadataSnapshotMemo();
|
||||
clearCurrentPluginMetadataSnapshot();
|
||||
clearPluginMetadataLifecycleCaches();
|
||||
mocks.resolveRuntimePluginRegistry.mockReset();
|
||||
@@ -319,7 +316,6 @@ describe("resolvePluginCapabilityProviders", () => {
|
||||
|
||||
afterEach(() => {
|
||||
clearCurrentPluginMetadataSnapshot();
|
||||
clearLoadPluginMetadataSnapshotMemo();
|
||||
});
|
||||
|
||||
it("resolves bundled capability plugins from the current metadata snapshot", () => {
|
||||
|
||||
@@ -3,7 +3,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { resolveInstalledPluginIndexPolicyHash } from "./installed-plugin-index-policy.js";
|
||||
import type { PluginManifestRecord, PluginManifestRegistry } from "./manifest-registry.js";
|
||||
import { clearLoadPluginMetadataSnapshotMemo } from "./plugin-metadata-snapshot.js";
|
||||
import type { PluginRegistrySnapshot } from "./plugin-registry.js";
|
||||
|
||||
const listPotentialConfiguredChannelIds = vi.hoisted(() => vi.fn());
|
||||
@@ -160,7 +159,6 @@ async function expectStaleMetadataSnapshotRebuild(params: {
|
||||
|
||||
describe("loadPluginLookUpTable", () => {
|
||||
beforeEach(() => {
|
||||
clearLoadPluginMetadataSnapshotMemo();
|
||||
listPotentialConfiguredChannelIds
|
||||
.mockReset()
|
||||
.mockImplementation((config: OpenClawConfig) => Object.keys(config.channels ?? {}));
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,223 @@
|
||||
// Verifies lifecycle snapshot loading, ownership facts, and immutable boundaries.
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
clearCurrentPluginMetadataSnapshot,
|
||||
setCurrentPluginMetadataSnapshot,
|
||||
} from "./current-plugin-metadata-snapshot.js";
|
||||
import { resolveInstalledPluginIndexPolicyHash } from "./installed-plugin-index-policy.js";
|
||||
import type { InstalledPluginIndex } from "./installed-plugin-index.js";
|
||||
import type { PluginManifestRecord, PluginManifestRegistry } from "./manifest-registry.js";
|
||||
import {
|
||||
loadPluginMetadataSnapshot,
|
||||
resolvePluginMetadataSnapshot,
|
||||
} from "./plugin-metadata-snapshot.js";
|
||||
|
||||
const loadPluginRegistrySnapshotWithMetadata = vi.hoisted(() => vi.fn());
|
||||
const loadPluginManifestRegistryForInstalledIndex = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("./plugin-registry.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("./plugin-registry.js")>();
|
||||
return {
|
||||
...actual,
|
||||
loadPluginRegistrySnapshotWithMetadata: (params: unknown) =>
|
||||
loadPluginRegistrySnapshotWithMetadata(params),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("./manifest-registry-installed.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("./manifest-registry-installed.js")>();
|
||||
return {
|
||||
...actual,
|
||||
loadPluginManifestRegistryForInstalledIndex: (params: unknown) =>
|
||||
loadPluginManifestRegistryForInstalledIndex(params),
|
||||
};
|
||||
});
|
||||
|
||||
function makeIndex(pluginId = "demo"): InstalledPluginIndex {
|
||||
const rootDir = `/plugins/${pluginId}`;
|
||||
return {
|
||||
version: 1,
|
||||
hostContractVersion: "test",
|
||||
compatRegistryVersion: "test",
|
||||
migrationVersion: 1,
|
||||
policyHash: "test",
|
||||
generatedAtMs: 1,
|
||||
installRecords: {},
|
||||
diagnostics: [],
|
||||
plugins: [
|
||||
{
|
||||
pluginId,
|
||||
manifestPath: `${rootDir}/openclaw.plugin.json`,
|
||||
manifestHash: `${pluginId}-manifest`,
|
||||
rootDir,
|
||||
origin: "global",
|
||||
enabled: true,
|
||||
startup: {
|
||||
sidecar: false,
|
||||
memory: false,
|
||||
deferConfiguredChannelFullLoadUntilAfterListen: false,
|
||||
agentHarnesses: [],
|
||||
},
|
||||
compat: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function makeManifestRegistry(pluginId = "demo"): PluginManifestRegistry {
|
||||
const plugin: PluginManifestRecord = {
|
||||
id: pluginId,
|
||||
name: pluginId,
|
||||
channels: [],
|
||||
providers: [pluginId],
|
||||
cliBackends: [],
|
||||
skills: [],
|
||||
hooks: [],
|
||||
commandAliases: [{ name: `${pluginId}-command` }],
|
||||
rootDir: `/plugins/${pluginId}`,
|
||||
source: `/plugins/${pluginId}/index.js`,
|
||||
manifestPath: `/plugins/${pluginId}/openclaw.plugin.json`,
|
||||
origin: "global",
|
||||
};
|
||||
return { plugins: [plugin], diagnostics: [] };
|
||||
}
|
||||
|
||||
describe("plugin metadata snapshot", () => {
|
||||
beforeEach(() => {
|
||||
loadPluginRegistrySnapshotWithMetadata.mockReset();
|
||||
loadPluginManifestRegistryForInstalledIndex.mockReset();
|
||||
loadPluginManifestRegistryForInstalledIndex.mockReturnValue(makeManifestRegistry());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clearCurrentPluginMetadataSnapshot();
|
||||
});
|
||||
|
||||
it("keeps explicit control-plane loads fresh", () => {
|
||||
const index = makeIndex();
|
||||
loadPluginRegistrySnapshotWithMetadata.mockReturnValue({
|
||||
source: "provided",
|
||||
snapshot: index,
|
||||
diagnostics: [],
|
||||
});
|
||||
|
||||
const first = loadPluginMetadataSnapshot({ config: {}, env: {}, index });
|
||||
const second = loadPluginMetadataSnapshot({ config: {}, env: {}, index });
|
||||
|
||||
expect(second).not.toBe(first);
|
||||
expect(loadPluginRegistrySnapshotWithMetadata).toHaveBeenCalledTimes(2);
|
||||
expect(loadPluginManifestRegistryForInstalledIndex).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("reuses the lifecycle-owned current snapshot", () => {
|
||||
const config = {};
|
||||
const index = makeIndex();
|
||||
index.policyHash = resolveInstalledPluginIndexPolicyHash(config);
|
||||
loadPluginRegistrySnapshotWithMetadata.mockReturnValue({
|
||||
source: "provided",
|
||||
snapshot: index,
|
||||
diagnostics: [],
|
||||
});
|
||||
const snapshot = loadPluginMetadataSnapshot({ config, env: {}, index });
|
||||
setCurrentPluginMetadataSnapshot(snapshot, { config, env: {} });
|
||||
loadPluginRegistrySnapshotWithMetadata.mockClear();
|
||||
loadPluginManifestRegistryForInstalledIndex.mockClear();
|
||||
|
||||
expect(resolvePluginMetadataSnapshot({ config, env: {} })).toBe(snapshot);
|
||||
expect(loadPluginRegistrySnapshotWithMetadata).not.toHaveBeenCalled();
|
||||
expect(loadPluginManifestRegistryForInstalledIndex).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps scoped loads separate without an LRU", () => {
|
||||
const index = makeIndex();
|
||||
loadPluginRegistrySnapshotWithMetadata.mockReturnValue({
|
||||
source: "provided",
|
||||
snapshot: index,
|
||||
diagnostics: [],
|
||||
});
|
||||
|
||||
const scoped = loadPluginMetadataSnapshot({
|
||||
config: {},
|
||||
env: {},
|
||||
index,
|
||||
pluginIds: ["demo"],
|
||||
});
|
||||
const unscoped = loadPluginMetadataSnapshot({ config: {}, env: {}, index });
|
||||
|
||||
expect(scoped.pluginIds).toEqual(["demo"]);
|
||||
expect(unscoped.pluginIds).toBeUndefined();
|
||||
expect(loadPluginManifestRegistryForInstalledIndex.mock.calls[0]?.[0]).toMatchObject({
|
||||
pluginIds: ["demo"],
|
||||
});
|
||||
expect(loadPluginManifestRegistryForInstalledIndex.mock.calls[1]?.[0]).not.toHaveProperty(
|
||||
"pluginIds",
|
||||
);
|
||||
});
|
||||
|
||||
it("prepares provider endpoint and request facts", () => {
|
||||
const index = makeIndex();
|
||||
const registry = makeManifestRegistry();
|
||||
const plugin = registry.plugins[0];
|
||||
if (!plugin) {
|
||||
throw new Error("expected manifest plugin fixture");
|
||||
}
|
||||
plugin.providerEndpoints = [
|
||||
{
|
||||
endpointClass: "openai-public",
|
||||
hosts: [" API.EXAMPLE.COM "],
|
||||
baseUrls: ["https://api.example.com/v1/"],
|
||||
},
|
||||
];
|
||||
plugin.providerRequest = {
|
||||
providers: {
|
||||
demo: {
|
||||
family: " demo-family ",
|
||||
compatibilityFamily: " moonshot " as never,
|
||||
openAICompletions: { supportsStreamingUsage: true },
|
||||
},
|
||||
},
|
||||
};
|
||||
loadPluginRegistrySnapshotWithMetadata.mockReturnValue({
|
||||
source: "provided",
|
||||
snapshot: index,
|
||||
diagnostics: [],
|
||||
});
|
||||
loadPluginManifestRegistryForInstalledIndex.mockReturnValue(registry);
|
||||
|
||||
const snapshot = loadPluginMetadataSnapshot({ config: {}, env: {}, index });
|
||||
|
||||
expect(snapshot.owners.providerEndpoints).toContainEqual({
|
||||
endpointClass: "openai-public",
|
||||
hosts: ["api.example.com"],
|
||||
hostSuffixes: [],
|
||||
baseUrls: ["https://api.example.com/v1"],
|
||||
});
|
||||
expect(snapshot.owners.providerRequests?.get("demo")).toEqual({
|
||||
family: "demo-family",
|
||||
compatibilityFamily: "moonshot",
|
||||
openAICompletions: { supportsStreamingUsage: true },
|
||||
});
|
||||
});
|
||||
|
||||
it("freezes a cloned index instead of caller-owned records", () => {
|
||||
const index = makeIndex();
|
||||
loadPluginRegistrySnapshotWithMetadata.mockReturnValue({
|
||||
source: "provided",
|
||||
snapshot: index,
|
||||
diagnostics: [],
|
||||
});
|
||||
|
||||
const snapshot = loadPluginMetadataSnapshot({ config: {}, env: {}, index });
|
||||
const callerRecord = index.plugins[0];
|
||||
const snapshotRecord = snapshot.index.plugins[0];
|
||||
if (!callerRecord || !snapshotRecord) {
|
||||
throw new Error("expected metadata records");
|
||||
}
|
||||
|
||||
callerRecord.pluginId = "caller-mutated";
|
||||
expect(snapshotRecord.pluginId).toBe("demo");
|
||||
expect(() => {
|
||||
snapshotRecord.pluginId = "snapshot-mutated";
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
@@ -1,20 +1,12 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
|
||||
import { isRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { resolveIsNixMode } from "../config/paths.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import {
|
||||
getActiveDiagnosticsTimelineSpan,
|
||||
measureDiagnosticsTimelineSpanSync,
|
||||
} from "../infra/diagnostics-timeline.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import { resolveCompatibilityHostVersion } from "../version.js";
|
||||
import { getCurrentPluginMetadataSnapshot } from "./current-plugin-metadata-snapshot.js";
|
||||
import { resolveDefaultPluginNpmDir, resolvePluginNpmProjectsDir } from "./install-paths.js";
|
||||
import { hashJson } from "./installed-plugin-index-hash.js";
|
||||
import { resolveInstalledPluginIndexPolicyHash } from "./installed-plugin-index-policy.js";
|
||||
import { readPersistedInstalledPluginIndexSync } from "./installed-plugin-index-store.js";
|
||||
import type { InstalledPluginIndex } from "./installed-plugin-index.js";
|
||||
import {
|
||||
loadPluginManifestRegistryForInstalledIndex,
|
||||
@@ -22,7 +14,6 @@ import {
|
||||
} from "./manifest-registry-installed.js";
|
||||
import { loadPluginManifestRegistry, type PluginManifestRecord } from "./manifest-registry.js";
|
||||
import { resolvePluginControlPlaneFingerprint } from "./plugin-control-plane-context.js";
|
||||
import { registerPluginMetadataProcessMemoLifecycleClear } from "./plugin-metadata-lifecycle.js";
|
||||
import { buildPluginMetadataProviderFacts } from "./plugin-metadata-provider-facts.js";
|
||||
import type {
|
||||
LoadPluginMetadataSnapshotParams,
|
||||
@@ -31,37 +22,10 @@ import type {
|
||||
ResolvePluginMetadataSnapshotParams,
|
||||
} from "./plugin-metadata-snapshot.types.js";
|
||||
import { createPluginRegistryIdNormalizer } from "./plugin-registry-id-normalizer.js";
|
||||
import {
|
||||
loadPluginRegistrySnapshotWithMetadata,
|
||||
type PluginRegistrySnapshotSource,
|
||||
} from "./plugin-registry.js";
|
||||
import { loadPluginRegistrySnapshotWithMetadata } from "./plugin-registry.js";
|
||||
import { normalizePluginIdScope, serializePluginIdScope } from "./plugin-scope.js";
|
||||
import { fileFingerprint } from "./plugin-snapshot-fingerprint.js";
|
||||
|
||||
type PluginMetadataSnapshotMemo = {
|
||||
key: string;
|
||||
lookupContextHash: string;
|
||||
registryState?: PersistedRegistryMemoState;
|
||||
snapshot: PluginMetadataSnapshot;
|
||||
};
|
||||
|
||||
type PersistedRegistryMemoState = {
|
||||
contextHash: string;
|
||||
fastHash: string;
|
||||
fingerprint: unknown;
|
||||
};
|
||||
|
||||
const MAX_PLUGIN_METADATA_SNAPSHOT_MEMOS = 8;
|
||||
|
||||
let pluginMetadataSnapshotMemos: PluginMetadataSnapshotMemo[] = [];
|
||||
|
||||
export function clearLoadPluginMetadataSnapshotMemo(): void {
|
||||
pluginMetadataSnapshotMemos = [];
|
||||
}
|
||||
|
||||
registerPluginMetadataProcessMemoLifecycleClear(clearLoadPluginMetadataSnapshotMemo);
|
||||
|
||||
const MEMO_RELEVANT_ENV_KEYS = [
|
||||
const PLUGIN_METADATA_ENV_KEYS = [
|
||||
"APPDATA",
|
||||
"HOME",
|
||||
"OPENCLAW_BUNDLED_PLUGINS_DIR",
|
||||
@@ -86,47 +50,17 @@ export type {
|
||||
ResolvePluginMetadataSnapshotParams,
|
||||
} from "./plugin-metadata-snapshot.types.js";
|
||||
|
||||
function directoryChildPackageJsonFingerprint(directoryPath: string): unknown {
|
||||
let entries: fs.Dirent[];
|
||||
try {
|
||||
entries = fs.readdirSync(directoryPath, { withFileTypes: true });
|
||||
} catch {
|
||||
return [directoryPath, "missing"];
|
||||
}
|
||||
return [
|
||||
directoryPath,
|
||||
...entries
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.toSorted((a, b) => a.name.localeCompare(b.name))
|
||||
.map((entry) => fileFingerprint(path.join(directoryPath, entry.name, "package.json"))),
|
||||
];
|
||||
}
|
||||
|
||||
function stableMemoValue(value: unknown): unknown {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(stableMemoValue);
|
||||
}
|
||||
if (!isRecord(value)) {
|
||||
return value;
|
||||
}
|
||||
function pickPluginMetadataEnv(env: NodeJS.ProcessEnv): Record<string, string> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value)
|
||||
.toSorted(([left], [right]) => left.localeCompare(right))
|
||||
.map(([key, entry]) => [key, stableMemoValue(entry)]),
|
||||
);
|
||||
}
|
||||
|
||||
function pickMemoRelevantEnv(env: NodeJS.ProcessEnv): Record<string, string> {
|
||||
return Object.fromEntries(
|
||||
MEMO_RELEVANT_ENV_KEYS.flatMap((key) => {
|
||||
PLUGIN_METADATA_ENV_KEYS.flatMap((key) => {
|
||||
const value = env[key];
|
||||
return value === undefined ? [] : [[key, value]];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function resolvePluginMetadataSnapshotMemoEnvFingerprint(env: NodeJS.ProcessEnv): string {
|
||||
return hashJson(pickMemoRelevantEnv(env));
|
||||
export function resolvePluginMetadataEnvFingerprint(env: NodeJS.ProcessEnv): string {
|
||||
return hashJson(pickPluginMetadataEnv(env));
|
||||
}
|
||||
|
||||
function throwReadonlyPluginMetadataMutation(): never {
|
||||
@@ -174,195 +108,6 @@ function freezePluginMetadataSnapshot(snapshot: PluginMetadataSnapshot): PluginM
|
||||
return freezeSnapshotValue(snapshot);
|
||||
}
|
||||
|
||||
function resolvePersistedRegistryFastMemoFingerprint(params: {
|
||||
env: NodeJS.ProcessEnv;
|
||||
preferPersisted?: boolean;
|
||||
stateDir?: string;
|
||||
}): Record<string, unknown> {
|
||||
const disabled = params.preferPersisted === false;
|
||||
if (disabled) {
|
||||
return { disabled: true };
|
||||
}
|
||||
const npmRoot = params.stateDir
|
||||
? path.join(params.stateDir, "npm")
|
||||
: resolveDefaultPluginNpmDir(params.env);
|
||||
return {
|
||||
index: hashJson(
|
||||
stableMemoValue(
|
||||
readPersistedInstalledPluginIndexSync({
|
||||
env: params.env,
|
||||
...(params.stateDir ? { stateDir: params.stateDir } : {}),
|
||||
}),
|
||||
) ?? null,
|
||||
),
|
||||
npmPackageJson: fileFingerprint(path.join(npmRoot, "package.json")),
|
||||
npmProjectPackageJsons: directoryChildPackageJsonFingerprint(
|
||||
resolvePluginNpmProjectsDir(npmRoot),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function resolvePersistedRegistryMemoContextHash(params: {
|
||||
env: NodeJS.ProcessEnv;
|
||||
fastFingerprint: unknown;
|
||||
preferPersisted?: boolean;
|
||||
stateDir?: string;
|
||||
}): string {
|
||||
return hashJson({
|
||||
env: pickMemoRelevantEnv(params.env),
|
||||
fastFingerprint: params.fastFingerprint,
|
||||
preferPersisted: params.preferPersisted ?? null,
|
||||
stateDir: params.stateDir ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
function resolvePersistedRegistryMemoLookupContextHash(params: {
|
||||
env: NodeJS.ProcessEnv;
|
||||
preferPersisted?: boolean;
|
||||
stateDir?: string;
|
||||
}): string {
|
||||
return hashJson({
|
||||
env: pickMemoRelevantEnv(params.env),
|
||||
preferPersisted: params.preferPersisted ?? null,
|
||||
stateDir: params.stateDir ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
function resolvePersistedRegistryMemoState(params: {
|
||||
env: NodeJS.ProcessEnv;
|
||||
preferPersisted?: boolean;
|
||||
stateDir?: string;
|
||||
}): PersistedRegistryMemoState {
|
||||
const fastFingerprint = resolvePersistedRegistryFastMemoFingerprint(params);
|
||||
const fastHash = hashJson(fastFingerprint);
|
||||
const contextHash = resolvePersistedRegistryMemoContextHash({
|
||||
...params,
|
||||
fastFingerprint,
|
||||
});
|
||||
if (isRecord(fastFingerprint) && fastFingerprint.disabled === true) {
|
||||
return {
|
||||
contextHash,
|
||||
fastHash,
|
||||
fingerprint: fastFingerprint,
|
||||
};
|
||||
}
|
||||
const index = readPersistedInstalledPluginIndexSync({
|
||||
env: params.env,
|
||||
...(params.stateDir ? { stateDir: params.stateDir } : {}),
|
||||
});
|
||||
return {
|
||||
contextHash,
|
||||
fastHash,
|
||||
fingerprint: {
|
||||
...fastFingerprint,
|
||||
indexHash: hashJson(stableMemoValue(index) ?? null),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function resolvePersistedRegistryMemoStateForLookup(
|
||||
params: {
|
||||
env: NodeJS.ProcessEnv;
|
||||
preferPersisted?: boolean;
|
||||
stateDir?: string;
|
||||
},
|
||||
memos: readonly PluginMetadataSnapshotMemo[],
|
||||
): PersistedRegistryMemoState {
|
||||
const lookupContextHash = resolvePersistedRegistryMemoLookupContextHash(params);
|
||||
for (const memo of memos) {
|
||||
if (memo.lookupContextHash === lookupContextHash && memo.registryState) {
|
||||
// Gateway runtime metadata is process-stable. Installs/reloads clear the
|
||||
// memo lifecycle explicitly, so hot lookups can reuse the prepared
|
||||
// registry stamp instead of re-statting plugin roots on every turn.
|
||||
return memo.registryState;
|
||||
}
|
||||
}
|
||||
const fastFingerprint = resolvePersistedRegistryFastMemoFingerprint(params);
|
||||
const fastHash = hashJson(fastFingerprint);
|
||||
const contextHash = resolvePersistedRegistryMemoContextHash({
|
||||
...params,
|
||||
fastFingerprint,
|
||||
});
|
||||
for (const memo of memos) {
|
||||
const registryState = memo.registryState;
|
||||
if (
|
||||
registryState &&
|
||||
registryState.contextHash === contextHash &&
|
||||
registryState.fastHash === fastHash
|
||||
) {
|
||||
// Plugin files are immutable for a running gateway; plugin edits require
|
||||
// an explicit reload/restart, so hot lookups only validate the registry envelope.
|
||||
return registryState;
|
||||
}
|
||||
}
|
||||
return resolvePersistedRegistryMemoState(params);
|
||||
}
|
||||
|
||||
function resolveProvidedIndexMemoState(index: InstalledPluginIndex): PersistedRegistryMemoState {
|
||||
const fingerprint = {
|
||||
providedIndex: resolveInstalledManifestRegistryIndexFingerprint(index),
|
||||
};
|
||||
const fingerprintHash = hashJson(fingerprint);
|
||||
return {
|
||||
contextHash: fingerprintHash,
|
||||
fastHash: fingerprintHash,
|
||||
fingerprint,
|
||||
};
|
||||
}
|
||||
|
||||
function findPluginMetadataSnapshotMemo(key: string): PluginMetadataSnapshotMemo | undefined {
|
||||
const index = pluginMetadataSnapshotMemos.findIndex((memo) => memo.key === key);
|
||||
if (index === -1) {
|
||||
return undefined;
|
||||
}
|
||||
const [memo] = pluginMetadataSnapshotMemos.splice(index, 1);
|
||||
if (!memo) {
|
||||
return undefined;
|
||||
}
|
||||
pluginMetadataSnapshotMemos.unshift(memo);
|
||||
return memo;
|
||||
}
|
||||
|
||||
function rememberPluginMetadataSnapshotMemo(memo: PluginMetadataSnapshotMemo): void {
|
||||
pluginMetadataSnapshotMemos = [
|
||||
memo,
|
||||
...pluginMetadataSnapshotMemos.filter((existing) => existing.key !== memo.key),
|
||||
].slice(0, MAX_PLUGIN_METADATA_SNAPSHOT_MEMOS);
|
||||
}
|
||||
|
||||
function computePluginMetadataSnapshotMemoKey(params: {
|
||||
params: LoadPluginMetadataSnapshotParams;
|
||||
registryState: PersistedRegistryMemoState;
|
||||
}): string {
|
||||
const { params: snapshotParams, registryState } = params;
|
||||
const env = snapshotParams.env ?? process.env;
|
||||
const indexFingerprint = snapshotParams.index
|
||||
? resolveInstalledManifestRegistryIndexFingerprint(snapshotParams.index)
|
||||
: undefined;
|
||||
return hashJson({
|
||||
controlPlane: resolvePluginControlPlaneFingerprint({
|
||||
config: snapshotParams.config,
|
||||
env,
|
||||
workspaceDir: snapshotParams.workspaceDir,
|
||||
policyHash: resolveInstalledPluginIndexPolicyHash(snapshotParams.config),
|
||||
...(indexFingerprint ? { inventoryFingerprint: indexFingerprint } : {}),
|
||||
}),
|
||||
cwd: process.cwd(),
|
||||
env: pickMemoRelevantEnv(env),
|
||||
index: indexFingerprint ?? null,
|
||||
pathPolicy: {
|
||||
compatibilityHostVersion: resolveCompatibilityHostVersion(env),
|
||||
nixMode: resolveIsNixMode(env),
|
||||
},
|
||||
pluginIds: serializePluginIdScope(normalizePluginIdScope(snapshotParams.pluginIds)),
|
||||
pluginIdScopeKey: snapshotParams.pluginIdScope?.key ?? null,
|
||||
preferPersisted: snapshotParams.preferPersisted ?? null,
|
||||
registry: registryState.fingerprint,
|
||||
stateDir: snapshotParams.stateDir ? resolveUserPath(snapshotParams.stateDir, env) : null,
|
||||
workspaceDir: snapshotParams.workspaceDir ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
function resolvePluginMetadataControlPlaneFingerprint(
|
||||
params: Pick<LoadPluginMetadataSnapshotParams, "config" | "env" | "workspaceDir"> & {
|
||||
index?: InstalledPluginIndex;
|
||||
@@ -550,80 +295,25 @@ export function listPluginOriginsFromMetadataSnapshot(
|
||||
return new Map(snapshot.plugins.map((record) => [record.id, record.origin]));
|
||||
}
|
||||
|
||||
// Process-local memoization keeps the hot snapshot work cached while checking
|
||||
// the persisted metadata files that the installed-index loader consumes.
|
||||
export function loadPluginMetadataSnapshot(
|
||||
params: LoadPluginMetadataSnapshotParams,
|
||||
): PluginMetadataSnapshot {
|
||||
const activeTimelineSpan = getActiveDiagnosticsTimelineSpan();
|
||||
const env = params.env ?? process.env;
|
||||
const registryState = params.index
|
||||
? resolveProvidedIndexMemoState(params.index)
|
||||
: resolvePersistedRegistryMemoStateForLookup(
|
||||
{
|
||||
env,
|
||||
...(params.stateDir ? { stateDir: resolveUserPath(params.stateDir, env) } : {}),
|
||||
...(params.preferPersisted !== undefined
|
||||
? { preferPersisted: params.preferPersisted }
|
||||
: {}),
|
||||
return freezePluginMetadataSnapshot(
|
||||
measureDiagnosticsTimelineSpanSync(
|
||||
"plugins.metadata.scan",
|
||||
() => loadPluginMetadataSnapshotImpl(params),
|
||||
{
|
||||
phase: activeTimelineSpan?.phase ?? "startup",
|
||||
config: params.config,
|
||||
env: params.env,
|
||||
attributes: {
|
||||
hasWorkspaceDir: params.workspaceDir !== undefined,
|
||||
hasInstalledIndex: params.index !== undefined,
|
||||
},
|
||||
pluginMetadataSnapshotMemos,
|
||||
);
|
||||
const memoKey = computePluginMetadataSnapshotMemoKey({ params, registryState });
|
||||
const memo = findPluginMetadataSnapshotMemo(memoKey);
|
||||
if (memo?.key === memoKey) {
|
||||
return memo.snapshot;
|
||||
}
|
||||
|
||||
const result = measureDiagnosticsTimelineSpanSync(
|
||||
"plugins.metadata.scan",
|
||||
() => loadPluginMetadataSnapshotImpl(params),
|
||||
{
|
||||
phase: activeTimelineSpan?.phase ?? "startup",
|
||||
config: params.config,
|
||||
env: params.env,
|
||||
attributes: {
|
||||
hasWorkspaceDir: params.workspaceDir !== undefined,
|
||||
hasInstalledIndex: params.index !== undefined,
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
const snapshot = freezePluginMetadataSnapshot(result.snapshot);
|
||||
if (canMemoizePluginMetadataSnapshotResult(result)) {
|
||||
// Store under the exact key this call looked up by. Derived registries used
|
||||
// to re-key off the freshly built snapshot.index, so the store key never
|
||||
// matched the next lookup and every call re-ran the full manifest scan.
|
||||
rememberPluginMetadataSnapshotMemo({
|
||||
key: memoKey,
|
||||
lookupContextHash: resolvePersistedRegistryMemoLookupContextHash({
|
||||
env,
|
||||
...(params.stateDir ? { stateDir: resolveUserPath(params.stateDir, env) } : {}),
|
||||
...(params.preferPersisted !== undefined
|
||||
? { preferPersisted: params.preferPersisted }
|
||||
: {}),
|
||||
}),
|
||||
registryState,
|
||||
snapshot,
|
||||
});
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
function canMemoizePluginMetadataSnapshotResult(result: {
|
||||
registrySource: PluginRegistrySnapshotSource;
|
||||
snapshot: PluginMetadataSnapshot;
|
||||
}): boolean {
|
||||
const snapshot = result.snapshot;
|
||||
const hasCompleteSnapshotShape =
|
||||
Array.isArray(snapshot.plugins) &&
|
||||
Array.isArray(snapshot.diagnostics) &&
|
||||
Array.isArray(snapshot.registryDiagnostics) &&
|
||||
Array.isArray(snapshot.manifestRegistry.plugins) &&
|
||||
Array.isArray(snapshot.manifestRegistry.diagnostics) &&
|
||||
Array.isArray(snapshot.index.plugins) &&
|
||||
Array.isArray(snapshot.index.diagnostics);
|
||||
const hasPluginMetadata = snapshot.plugins.length > 0 || snapshot.index.plugins.length > 0;
|
||||
return hasCompleteSnapshotShape && hasPluginMetadata;
|
||||
}
|
||||
|
||||
export function resolvePluginMetadataSnapshot(
|
||||
@@ -668,10 +358,9 @@ export function resolvePluginMetadataSnapshot(
|
||||
return loadPluginMetadataSnapshot(params);
|
||||
}
|
||||
|
||||
function loadPluginMetadataSnapshotImpl(params: LoadPluginMetadataSnapshotParams): {
|
||||
snapshot: PluginMetadataSnapshot;
|
||||
registrySource: PluginRegistrySnapshotSource;
|
||||
} {
|
||||
function loadPluginMetadataSnapshotImpl(
|
||||
params: LoadPluginMetadataSnapshotParams,
|
||||
): PluginMetadataSnapshot {
|
||||
const totalStartedAt = performance.now();
|
||||
const registryStartedAt = performance.now();
|
||||
const registryResult = loadPluginRegistrySnapshotWithMetadata({
|
||||
@@ -716,36 +405,33 @@ function loadPluginMetadataSnapshotImpl(params: LoadPluginMetadataSnapshotParams
|
||||
const totalMs = performance.now() - totalStartedAt;
|
||||
|
||||
return {
|
||||
policyHash: index.policyHash,
|
||||
registrySource: registryResult.source,
|
||||
snapshot: {
|
||||
policyHash: index.policyHash,
|
||||
registrySource: registryResult.source,
|
||||
configFingerprint: resolvePluginMetadataControlPlaneFingerprint({
|
||||
config: params.config,
|
||||
env: params.env,
|
||||
index,
|
||||
policyHash: index.policyHash,
|
||||
workspaceDir: params.workspaceDir,
|
||||
}),
|
||||
...(pluginIds !== undefined ? { pluginIds } : {}),
|
||||
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
|
||||
configFingerprint: resolvePluginMetadataControlPlaneFingerprint({
|
||||
config: params.config,
|
||||
env: params.env,
|
||||
index,
|
||||
registryDiagnostics: registryResult.diagnostics,
|
||||
manifestRegistry,
|
||||
plugins: manifestRegistry.plugins,
|
||||
diagnostics: manifestRegistry.diagnostics,
|
||||
byPluginId,
|
||||
normalizePluginId,
|
||||
owners,
|
||||
metrics: {
|
||||
registrySnapshotMs,
|
||||
manifestRegistryMs,
|
||||
ownerMapsMs,
|
||||
totalMs,
|
||||
indexPluginCount: index.plugins.length,
|
||||
manifestPluginCount: manifestRegistry.plugins.length,
|
||||
},
|
||||
discovery: registryResult.discovery,
|
||||
policyHash: index.policyHash,
|
||||
workspaceDir: params.workspaceDir,
|
||||
}),
|
||||
...(pluginIds !== undefined ? { pluginIds } : {}),
|
||||
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
|
||||
index,
|
||||
registryDiagnostics: registryResult.diagnostics,
|
||||
manifestRegistry,
|
||||
plugins: manifestRegistry.plugins,
|
||||
diagnostics: manifestRegistry.diagnostics,
|
||||
byPluginId,
|
||||
normalizePluginId,
|
||||
owners,
|
||||
metrics: {
|
||||
registrySnapshotMs,
|
||||
manifestRegistryMs,
|
||||
ownerMapsMs,
|
||||
totalMs,
|
||||
indexPluginCount: index.plugins.length,
|
||||
manifestPluginCount: manifestRegistry.plugins.length,
|
||||
},
|
||||
discovery: registryResult.discovery,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,10 +9,7 @@ import { resolveInstalledPluginIndexPolicyHash } from "./installed-plugin-index-
|
||||
import type { InstalledPluginIndex } from "./installed-plugin-index.js";
|
||||
import type { PluginManifestRecord, PluginManifestRegistry } from "./manifest-registry.js";
|
||||
import { clearPluginMetadataLifecycleCaches } from "./plugin-metadata-lifecycle.js";
|
||||
import {
|
||||
clearLoadPluginMetadataSnapshotMemo,
|
||||
loadPluginMetadataSnapshot,
|
||||
} from "./plugin-metadata-snapshot.js";
|
||||
import { loadPluginMetadataSnapshot } from "./plugin-metadata-snapshot.js";
|
||||
import { resetPluginRuntimeStateForTest } from "./runtime.js";
|
||||
|
||||
// Mock the persisted-registry loaders so direct metadata loads are observable.
|
||||
@@ -124,7 +121,6 @@ describe("provider runtime consults the current plugin metadata snapshot", () =>
|
||||
beforeEach(() => {
|
||||
resetPluginRuntimeStateForTest();
|
||||
clearPluginMetadataLifecycleCaches();
|
||||
clearLoadPluginMetadataSnapshotMemo();
|
||||
clearCurrentPluginMetadataSnapshot();
|
||||
loadPluginRegistrySnapshotWithMetadata.mockReset();
|
||||
loadPluginManifestRegistryForInstalledIndex.mockReset();
|
||||
@@ -134,7 +130,6 @@ describe("provider runtime consults the current plugin metadata snapshot", () =>
|
||||
afterEach(() => {
|
||||
clearCurrentPluginMetadataSnapshot();
|
||||
clearPluginMetadataLifecycleCaches();
|
||||
clearLoadPluginMetadataSnapshotMemo();
|
||||
resetPluginRuntimeStateForTest();
|
||||
});
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import type { PluginManifestRecord, PluginManifestRegistry } from "./manifest-re
|
||||
import { createPluginCacheKey, PluginLruCache } from "./plugin-cache-primitives.js";
|
||||
import { resolvePluginControlPlaneFingerprint } from "./plugin-control-plane-context.js";
|
||||
import { registerPluginMetadataProcessMemoLifecycleClear } from "./plugin-metadata-lifecycle.js";
|
||||
import { resolvePluginMetadataSnapshotMemoEnvFingerprint } from "./plugin-metadata-snapshot.js";
|
||||
import { resolvePluginMetadataEnvFingerprint } from "./plugin-metadata-snapshot.js";
|
||||
import { getCachedPluginModuleLoader } from "./plugin-module-loader-cache.js";
|
||||
import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js";
|
||||
import type { PluginRuntime } from "./runtime/types.js";
|
||||
@@ -360,7 +360,7 @@ function resolveSetupRegistryCacheKey(params?: {
|
||||
env,
|
||||
workspaceDir: params?.workspaceDir,
|
||||
}),
|
||||
resolvePluginMetadataSnapshotMemoEnvFingerprint(env),
|
||||
resolvePluginMetadataEnvFingerprint(env),
|
||||
resolveCurrentSetupSnapshotCacheId(),
|
||||
process.cwd(),
|
||||
params?.pluginIds ? [...params.pluginIds].toSorted() : null,
|
||||
|
||||
@@ -15,7 +15,6 @@ let loadOpenClawPluginsMock: ReturnType<typeof vi.fn>;
|
||||
let setActivePluginRegistry: RuntimeModule["setActivePluginRegistry"];
|
||||
let resetPluginRuntimeStateForTest: RuntimeModule["resetPluginRuntimeStateForTest"];
|
||||
let resolvePluginWebFetchProviders: WebFetchProvidersRuntimeModule["resolvePluginWebFetchProviders"];
|
||||
let clearLoadPluginMetadataSnapshotMemo: typeof import("./plugin-metadata-snapshot.js").clearLoadPluginMetadataSnapshotMemo;
|
||||
|
||||
const DEFAULT_WORKSPACE = "/tmp/workspace";
|
||||
|
||||
@@ -121,12 +120,10 @@ describe("resolvePluginWebFetchProviders", () => {
|
||||
manifestRegistryModule = await import("./manifest-registry.js");
|
||||
webFetchProvidersSharedModule = await import("./web-fetch-providers.shared.js");
|
||||
({ resetPluginRuntimeStateForTest, setActivePluginRegistry } = await import("./runtime.js"));
|
||||
({ clearLoadPluginMetadataSnapshotMemo } = await import("./plugin-metadata-snapshot.js"));
|
||||
({ resolvePluginWebFetchProviders } = await import("./web-fetch-providers.runtime.js"));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
clearLoadPluginMetadataSnapshotMemo();
|
||||
vi.spyOn(manifestRegistryModule, "loadPluginManifestRegistry").mockReturnValue(
|
||||
createManifestRegistryFixture() as ManifestRegistryModule["loadPluginManifestRegistry"] extends (
|
||||
...args: unknown[]
|
||||
@@ -146,7 +143,6 @@ describe("resolvePluginWebFetchProviders", () => {
|
||||
|
||||
afterEach(() => {
|
||||
resetPluginRuntimeStateForTest();
|
||||
clearLoadPluginMetadataSnapshotMemo();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
|
||||
@@ -38,7 +38,6 @@ let loaderModule: typeof import("./loader.js");
|
||||
let pluginAutoEnableModule: PluginAutoEnableModule;
|
||||
let applyPluginAutoEnableSpy: ReturnType<typeof vi.fn>;
|
||||
let resetPluginRuntimeStateForTest: RuntimeModule["resetPluginRuntimeStateForTest"];
|
||||
let clearLoadPluginMetadataSnapshotMemo: typeof import("./plugin-metadata-snapshot.js").clearLoadPluginMetadataSnapshotMemo;
|
||||
|
||||
const DEFAULT_WEB_SEARCH_WORKSPACE = "/tmp/workspace";
|
||||
const EXPECTED_BUNDLED_RUNTIME_WEB_SEARCH_PROVIDER_KEYS = [
|
||||
@@ -325,12 +324,10 @@ describe("resolvePluginWebSearchProviders", () => {
|
||||
loaderModule = await import("./loader.js");
|
||||
pluginAutoEnableModule = await import("../config/plugin-auto-enable.js");
|
||||
({ resetPluginRuntimeStateForTest, setActivePluginRegistry } = await import("./runtime.js"));
|
||||
({ clearLoadPluginMetadataSnapshotMemo } = await import("./plugin-metadata-snapshot.js"));
|
||||
({ resolvePluginWebSearchProviders } = await import("./web-search-providers.runtime.js"));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
clearLoadPluginMetadataSnapshotMemo();
|
||||
applyPluginAutoEnableSpy?.mockRestore();
|
||||
applyPluginAutoEnableSpy = vi
|
||||
.spyOn(pluginAutoEnableModule, "applyPluginAutoEnable")
|
||||
@@ -359,7 +356,6 @@ describe("resolvePluginWebSearchProviders", () => {
|
||||
|
||||
afterEach(() => {
|
||||
resetPluginRuntimeStateForTest();
|
||||
clearLoadPluginMetadataSnapshotMemo();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ export function createAgentsEmbeddedVitestConfig(env?: Record<string, string | u
|
||||
return createScopedVitestConfig(agentsEmbeddedTestPatterns, {
|
||||
dir: "src/agents",
|
||||
env,
|
||||
fileParallelism: false,
|
||||
name: "agents-embedded-agent",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ export function createAutoReplyReplyVitestConfig(env?: Record<string, string | u
|
||||
return createScopedVitestConfig([...autoReplyReplySubtreeTestInclude], {
|
||||
dir: "src/auto-reply",
|
||||
env,
|
||||
fileParallelism: false,
|
||||
name: "auto-reply-reply",
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user