mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
fix(agents): preserve built plugin artifacts in prepared runtimes (#125957)
* fix(agents): preserve built plugin artifacts in prepared runtimes * test(agents): update prepared runtime loader contract * fix(agents): preserve built preference for deferred plugins * fix(plugins): carry artifact preference through load context * fix(plugins): keep artifact context optional * test(plugins): prove prepared built artifact projection * fix(agents): scope built artifacts to gateway runtimes * test(agents): preserve standalone loader call shape * fix(agents): retain prepared artifact choice on reuse * test(plugins): update prepared load context expectation Amp-Thread-ID: https://ampcode.com/threads/T-01a023b8-1041-7686-975f-c9f3ef91755d --------- Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
@@ -15,11 +15,13 @@ const smokeEntryPath = path.join(repoRoot, "dist", "plugins", "build-smoke-entry
|
||||
assert.ok(fs.existsSync(smokeEntryPath), `missing build output: ${smokeEntryPath}`);
|
||||
|
||||
const {
|
||||
buildPluginRuntimeLoadOptions,
|
||||
clearPluginCommands,
|
||||
getPluginCommandSpecs,
|
||||
getPluginModuleLoaderStats,
|
||||
loadOpenClawPlugins,
|
||||
matchPluginCommand,
|
||||
resolvePluginRuntimeLoadContext,
|
||||
} = await import(pathToFileURL(smokeEntryPath).href);
|
||||
|
||||
assert.equal(typeof loadOpenClawPlugins, "function", "built loader export missing");
|
||||
@@ -126,24 +128,28 @@ assert.equal(
|
||||
clearPluginCommands();
|
||||
|
||||
const smsStatsBefore = getPluginModuleLoaderStats();
|
||||
const smsRegistry = loadOpenClawPlugins({
|
||||
cache: false,
|
||||
preferBuiltPluginArtifacts: true,
|
||||
workspaceDir: tempRoot,
|
||||
env: {
|
||||
...process.env,
|
||||
OPENCLAW_BUNDLED_PLUGINS_DIR: path.join(repoRoot, "dist-runtime", "extensions"),
|
||||
},
|
||||
config: {
|
||||
plugins: {
|
||||
enabled: true,
|
||||
allow: ["sms"],
|
||||
entries: {
|
||||
sms: { enabled: true },
|
||||
// Prepared runtimes carry this context into late, plugin-scoped loads. Prove that the load-options
|
||||
// projection retains the built-artifact choice instead of reopening source transformation.
|
||||
const smsRegistry = loadOpenClawPlugins(
|
||||
buildPluginRuntimeLoadOptions(
|
||||
resolvePluginRuntimeLoadContext({
|
||||
config: {
|
||||
plugins: {
|
||||
enabled: true,
|
||||
allow: ["sms"],
|
||||
entries: { sms: { enabled: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
env: {
|
||||
...process.env,
|
||||
OPENCLAW_BUNDLED_PLUGINS_DIR: path.join(repoRoot, "extensions"),
|
||||
},
|
||||
preferBuiltPluginArtifacts: true,
|
||||
workspaceDir: tempRoot,
|
||||
}),
|
||||
{ cache: false, onlyPluginIds: ["sms"] },
|
||||
),
|
||||
);
|
||||
const smsRecord = smsRegistry.plugins.find((entry: { id: string }) => entry.id === "sms");
|
||||
assert.ok(smsRecord, "SMS plugin missing from registry");
|
||||
assert.equal(smsRecord.status, "loaded", smsRecord.error ?? "SMS plugin failed to load");
|
||||
|
||||
@@ -342,10 +342,12 @@ async function buildSnapshotBatch(
|
||||
const prepareInboundPluginRegistry = groupInputs.some((input) =>
|
||||
inboundPluginRegistryInputs.has(input),
|
||||
);
|
||||
const preferBuiltPluginArtifacts =
|
||||
pluginGeneration?.preferBuiltPluginArtifacts ?? prepareInboundPluginRegistry;
|
||||
const prepared = await prepareWorkspaceBuildGroup(
|
||||
groupInputs,
|
||||
catalogMode,
|
||||
{},
|
||||
{ preferBuiltPluginArtifacts },
|
||||
prepareInboundPluginRegistry ? loadInboundPluginRegistry : undefined,
|
||||
pluginGeneration,
|
||||
pluginMetadataSnapshot,
|
||||
|
||||
@@ -140,7 +140,10 @@ function prepareAgentFacts(
|
||||
export async function prepareWorkspaceBuildGroup(
|
||||
inputs: readonly PreparedModelRuntimeInput[],
|
||||
catalogMode: PreparedModelRuntimeCatalogMode,
|
||||
options: { providerDiscoveryProviderIds?: readonly string[] } = {},
|
||||
options: {
|
||||
providerDiscoveryProviderIds?: readonly string[];
|
||||
preferBuiltPluginArtifacts?: boolean;
|
||||
} = {},
|
||||
loadInboundPluginRegistry?: PreparedInboundRegistryLoader,
|
||||
reusablePluginGeneration?: PreparedModelRuntimePluginGeneration,
|
||||
preparedPluginMetadataSnapshot?: PreparedModelRuntimePluginGeneration["pluginMetadataSnapshot"],
|
||||
@@ -176,9 +179,23 @@ export async function prepareWorkspaceBuildGroup(
|
||||
inboundPluginRegistry: reusablePluginGeneration.inboundPluginRegistry,
|
||||
runtimePluginRegistry: reusablePluginGeneration.pluginRegistry,
|
||||
}
|
||||
: prepareWorkspacePluginRegistries(input, pluginMetadataSnapshot, loadInboundPluginRegistry);
|
||||
: prepareWorkspacePluginRegistries(
|
||||
input,
|
||||
pluginMetadataSnapshot,
|
||||
loadInboundPluginRegistry,
|
||||
options.preferBuiltPluginArtifacts === true,
|
||||
);
|
||||
const runtimePluginMs = reusablePluginGeneration ? 0 : performance.now() - runtimePluginStartedAt;
|
||||
prepareOwnedPluginLoadContext(input, env, runtimePluginRegistry, pluginMetadataSnapshot);
|
||||
const preferBuiltPluginArtifacts =
|
||||
reusablePluginGeneration?.preferBuiltPluginArtifacts ??
|
||||
options.preferBuiltPluginArtifacts === true;
|
||||
prepareOwnedPluginLoadContext(
|
||||
input,
|
||||
env,
|
||||
runtimePluginRegistry,
|
||||
pluginMetadataSnapshot,
|
||||
preferBuiltPluginArtifacts,
|
||||
);
|
||||
const prepare = async () => {
|
||||
const matchesStaticModelId = createStaticModelIdMatcher({
|
||||
manifestPlugins: pluginMetadataSnapshot.plugins,
|
||||
@@ -386,6 +403,7 @@ export async function prepareWorkspaceBuildGroup(
|
||||
pluginMetadataSnapshot,
|
||||
preparedStaticProviderCatalog,
|
||||
providerStaticModels,
|
||||
preferBuiltPluginArtifacts,
|
||||
reusablePluginGeneration,
|
||||
runtimePluginRegistry,
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
registerPreparedModelRuntimePublicationListener,
|
||||
refreshPreparedModelRuntimeSnapshots,
|
||||
} from "./prepared-model-runtime.js";
|
||||
import { getPreparedPluginRuntimeLoadContext } from "./prepared-model-runtime.plugin-context.js";
|
||||
|
||||
const mocks = getPreparedModelRuntimeMocks();
|
||||
|
||||
@@ -195,6 +196,9 @@ describe("prepared reply dispatch runtime", () => {
|
||||
pluginGeneration: configuredRuntimeBefore.pluginGeneration,
|
||||
});
|
||||
const dynamicSelectedBefore = dynamicLease.snapshot.pluginRegistry;
|
||||
expect(getPreparedPluginRuntimeLoadContext(dynamicSelectedBefore)).toMatchObject({
|
||||
preferBuiltPluginArtifacts: true,
|
||||
});
|
||||
dynamicLease.release();
|
||||
expect(mocks.loadAgentRuntimePluginRegistryHandle).toHaveBeenCalledTimes(2);
|
||||
expect(dynamicPreparationRegistries.every(Boolean)).toBe(true);
|
||||
|
||||
@@ -51,6 +51,7 @@ export function createPreparedInboundRegistryLoader(): PreparedInboundRegistryLo
|
||||
...(input.workspaceDir ? { workspaceDir: input.workspaceDir } : {}),
|
||||
...(input.allowGatewaySubagentBinding ? { allowGatewaySubagentBinding: true } : {}),
|
||||
metadataSnapshot,
|
||||
preferBuiltPluginArtifacts: true,
|
||||
});
|
||||
registries.set(key, registry);
|
||||
return registry;
|
||||
@@ -62,6 +63,7 @@ export function prepareWorkspacePluginRegistries(
|
||||
input: PreparedModelRuntimeInput,
|
||||
metadataSnapshot: PluginMetadataSnapshot,
|
||||
loadInboundRegistry?: PreparedInboundRegistryLoader,
|
||||
preferBuiltPluginArtifacts = false,
|
||||
): {
|
||||
runtimePluginRegistry?: PluginRegistry;
|
||||
inboundPluginRegistry?: PluginRegistry;
|
||||
@@ -83,6 +85,7 @@ export function prepareWorkspacePluginRegistries(
|
||||
...(input.workspaceDir ? { workspaceDir: input.workspaceDir } : {}),
|
||||
...(input.allowGatewaySubagentBinding ? { allowGatewaySubagentBinding: true } : {}),
|
||||
metadataSnapshot,
|
||||
...(preferBuiltPluginArtifacts ? { preferBuiltPluginArtifacts: true } : {}),
|
||||
selections: input.runtimePluginSelections,
|
||||
})
|
||||
: inboundPluginRegistry;
|
||||
|
||||
@@ -42,12 +42,13 @@ describe("prepared model runtime plugin metadata ownership", () => {
|
||||
try {
|
||||
for (const input of inputs) {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
expect(prepareOwnedPluginLoadContext(input, process.env, registry, gatewaySnapshot)).toBe(
|
||||
gatewaySnapshot,
|
||||
);
|
||||
expect(getPreparedPluginRuntimeLoadContext(registry)?.metadataSnapshot).toBe(
|
||||
gatewaySnapshot,
|
||||
);
|
||||
expect(
|
||||
prepareOwnedPluginLoadContext(input, process.env, registry, gatewaySnapshot, true),
|
||||
).toBe(gatewaySnapshot);
|
||||
expect(getPreparedPluginRuntimeLoadContext(registry)).toMatchObject({
|
||||
metadataSnapshot: gatewaySnapshot,
|
||||
preferBuiltPluginArtifacts: true,
|
||||
});
|
||||
expect(
|
||||
withPreparedPluginGenerationScope({ input, pluginGeneration }, (snapshot) => snapshot),
|
||||
).toBe(gatewaySnapshot);
|
||||
@@ -71,6 +72,7 @@ describe("prepared model runtime plugin metadata ownership", () => {
|
||||
const resolveMetadata = vi
|
||||
.spyOn(pluginMetadata, "loadPluginMetadataSnapshot")
|
||||
.mockReturnValue(directSnapshot);
|
||||
const registry = createEmptyPluginRegistry();
|
||||
|
||||
try {
|
||||
expect(
|
||||
@@ -81,9 +83,13 @@ describe("prepared model runtime plugin metadata ownership", () => {
|
||||
workspaceDir,
|
||||
},
|
||||
process.env,
|
||||
undefined,
|
||||
registry,
|
||||
),
|
||||
).toBe(directSnapshot);
|
||||
expect(getPreparedPluginRuntimeLoadContext(registry)).toMatchObject({
|
||||
metadataSnapshot: directSnapshot,
|
||||
preferBuiltPluginArtifacts: false,
|
||||
});
|
||||
expect(resolveMetadata).toHaveBeenCalledWith({
|
||||
config,
|
||||
env: process.env,
|
||||
|
||||
@@ -29,6 +29,7 @@ function preparePluginLoadContext(
|
||||
env: NodeJS.ProcessEnv,
|
||||
registry: PluginRegistry | undefined,
|
||||
metadataSnapshot: PluginMetadataSnapshot,
|
||||
preferBuiltPluginArtifacts: boolean,
|
||||
): PluginRuntimeLoadContext & { metadataSnapshot: PluginMetadataSnapshot } {
|
||||
const { config } = input;
|
||||
const workspaceDir = metadataSnapshot.workspaceDir ?? input.workspaceDir;
|
||||
@@ -44,6 +45,7 @@ function preparePluginLoadContext(
|
||||
workspaceDir,
|
||||
metadataSnapshot: preparedMetadataSnapshot,
|
||||
manifestRegistry: metadataSnapshot.manifestRegistry,
|
||||
preferBuiltPluginArtifacts,
|
||||
}),
|
||||
metadataSnapshot,
|
||||
installRecords: extractPluginInstallRecordsFromInstalledPluginIndex(metadataSnapshot.index),
|
||||
@@ -61,9 +63,10 @@ export function prepareOwnedPluginLoadContext(
|
||||
env: NodeJS.ProcessEnv,
|
||||
registry: PluginRegistry | undefined,
|
||||
preparedMetadataSnapshot?: PluginMetadataSnapshot,
|
||||
preferBuiltPluginArtifacts = false,
|
||||
): PluginMetadataSnapshot {
|
||||
const metadataSnapshot = preparedMetadataSnapshot ?? resolveColdMetadataSnapshot(input, env);
|
||||
preparePluginLoadContext(input, env, registry, metadataSnapshot);
|
||||
preparePluginLoadContext(input, env, registry, metadataSnapshot, preferBuiltPluginArtifacts);
|
||||
return metadataSnapshot;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ export function createPreparedPluginGeneration(params: {
|
||||
pluginMetadataSnapshot: PreparedModelRuntimePluginGeneration["pluginMetadataSnapshot"];
|
||||
preparedStaticProviderCatalog: PreparedModelRuntimePluginGeneration["preparedStaticProviderCatalog"];
|
||||
providerStaticModels: PreparedModelRuntimePluginGeneration["providerStaticModels"];
|
||||
preferBuiltPluginArtifacts?: boolean;
|
||||
reusablePluginGeneration?: PreparedModelRuntimePluginGeneration;
|
||||
runtimePluginRegistry: PreparedModelRuntimePluginGeneration["pluginRegistry"];
|
||||
}): PreparedModelRuntimePluginGeneration {
|
||||
@@ -35,6 +36,7 @@ export function createPreparedPluginGeneration(params: {
|
||||
...(params.inboundPluginRegistry
|
||||
? { inboundPluginRegistry: params.inboundPluginRegistry }
|
||||
: {}),
|
||||
...(params.preferBuiltPluginArtifacts ? { preferBuiltPluginArtifacts: true } : {}),
|
||||
...(params.mediaCapabilityProviders
|
||||
? { mediaCapabilityProviders: params.mediaCapabilityProviders }
|
||||
: {}),
|
||||
|
||||
@@ -26,6 +26,8 @@ export type PreparedModelRuntimePluginGeneration = Readonly<{
|
||||
configuredCatalogEntries: readonly ModelCatalogEntry[];
|
||||
pluginRegistry?: PluginRegistry;
|
||||
inboundPluginRegistry?: PluginRegistry;
|
||||
/** Immutable artifact choice for every registry reuse in this generation. */
|
||||
preferBuiltPluginArtifacts?: boolean;
|
||||
}>;
|
||||
|
||||
export type PreparedModelRuntimeSnapshot = Readonly<{
|
||||
|
||||
@@ -162,7 +162,7 @@ describe("agent runtime plugin registries", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves the gateway startup scope and ordering", () => {
|
||||
it("keeps an explicit metadata generation source-default without Gateway selection", () => {
|
||||
const config = {} as never;
|
||||
const metadataSnapshot = createMetadataSnapshot();
|
||||
|
||||
@@ -184,6 +184,9 @@ describe("agent runtime plugin registries", () => {
|
||||
channelPluginLoadIntent: "full",
|
||||
}),
|
||||
);
|
||||
expect(hoisted.loadPluginRegistryHandle).toHaveBeenCalledWith(
|
||||
expect.not.objectContaining({ preferBuiltPluginArtifacts: true }),
|
||||
);
|
||||
});
|
||||
|
||||
it("inherits the current request registry before process-wide startup metadata", () => {
|
||||
@@ -242,6 +245,7 @@ describe("agent runtime plugin registries", () => {
|
||||
env,
|
||||
workspaceDir: "/tmp/agent-workspace",
|
||||
metadataSnapshot: snapshot as never,
|
||||
preferBuiltPluginArtifacts: true,
|
||||
});
|
||||
|
||||
expect(hoisted.resolveAgentRuntimePluginLoadPlan).toHaveBeenCalledWith({
|
||||
@@ -261,6 +265,7 @@ describe("agent runtime plugin registries", () => {
|
||||
installRecords: {},
|
||||
manifestRegistry: snapshot.manifestRegistry,
|
||||
onlyPluginIds: ["codex", "memory-core"],
|
||||
preferBuiltPluginArtifacts: true,
|
||||
runtimeOptions: undefined,
|
||||
workspaceDir: snapshot.workspaceDir,
|
||||
});
|
||||
|
||||
@@ -28,7 +28,8 @@ type AgentRuntimePluginRegistryParams = {
|
||||
/** Explicit base scope for hosts without a Gateway startup registry. */
|
||||
basePluginIds?: readonly string[];
|
||||
selections?: readonly AgentHarnessPluginSelection[];
|
||||
/** Lifecycle-selected metadata. Omission selects one standalone cold generation. */
|
||||
/** Lifecycle-owned selection; standalone/direct generations stay source-default. */
|
||||
preferBuiltPluginArtifacts?: boolean;
|
||||
metadataSnapshot?: PluginMetadataSnapshot;
|
||||
};
|
||||
|
||||
@@ -63,6 +64,7 @@ function resolveAgentRuntimePluginRegistryLoad(params: AgentRuntimePluginRegistr
|
||||
...(metadataSnapshot.discovery ? { discovery: metadataSnapshot.discovery } : {}),
|
||||
installRecords: extractPluginInstallRecordsFromInstalledPluginIndex(metadataSnapshot.index),
|
||||
manifestRegistry: metadataSnapshot.manifestRegistry,
|
||||
...(params.preferBuiltPluginArtifacts ? { preferBuiltPluginArtifacts: true } : {}),
|
||||
...(workspaceDir ? { workspaceDir } : {}),
|
||||
};
|
||||
const requestPluginRegistry = getPluginRuntimeGatewayRequestScope()?.pluginRegistry;
|
||||
|
||||
@@ -3,3 +3,7 @@ export { clearPluginCommands, executePluginCommand, matchPluginCommand } from ".
|
||||
export { getPluginCommandSpecs } from "./command-specs.js";
|
||||
export { loadOpenClawPlugins, loadPluginRegistryHandle } from "./loader.js";
|
||||
export { getPluginModuleLoaderStats } from "./plugin-module-loader-cache.js";
|
||||
export {
|
||||
buildPluginRuntimeLoadOptions,
|
||||
resolvePluginRuntimeLoadContext,
|
||||
} from "./runtime/load-context.js";
|
||||
|
||||
@@ -136,6 +136,7 @@ describe("resolvePluginRuntimeLoadContext", () => {
|
||||
manifestRegistry,
|
||||
metadataSnapshot,
|
||||
installRecords: {},
|
||||
preferBuiltPluginArtifacts: false,
|
||||
});
|
||||
expect(loadPluginMetadataSnapshotMock).toHaveBeenCalledWith({
|
||||
allowWorkspaceScopedCurrent: true,
|
||||
@@ -314,6 +315,7 @@ describe("resolvePluginRuntimeLoadContext", () => {
|
||||
const context = resolvePluginRuntimeLoadContext({
|
||||
config: { plugins: {} },
|
||||
env: { HOME: "/tmp/openclaw-home" } as NodeJS.ProcessEnv,
|
||||
preferBuiltPluginArtifacts: true,
|
||||
workspaceDir: "/explicit-workspace",
|
||||
});
|
||||
|
||||
@@ -332,6 +334,7 @@ describe("resolvePluginRuntimeLoadContext", () => {
|
||||
logger: context.logger,
|
||||
manifestRegistry,
|
||||
installRecords: {},
|
||||
preferBuiltPluginArtifacts: true,
|
||||
cache: false,
|
||||
activate: false,
|
||||
onlyPluginIds: ["demo"],
|
||||
|
||||
@@ -137,6 +137,7 @@ export type PluginRuntimeLoadContext = {
|
||||
manifestRegistry?: PluginManifestRegistry;
|
||||
metadataSnapshot?: PluginMetadataSnapshot;
|
||||
installRecords?: Record<string, PluginInstallRecord>;
|
||||
preferBuiltPluginArtifacts?: boolean;
|
||||
};
|
||||
|
||||
/** Runtime load option values that can be passed directly to plugin loading. */
|
||||
@@ -150,6 +151,7 @@ type PluginRuntimeResolvedLoadValues = Pick<
|
||||
| "logger"
|
||||
| "manifestRegistry"
|
||||
| "installRecords"
|
||||
| "preferBuiltPluginArtifacts"
|
||||
>;
|
||||
|
||||
/** Options accepted while resolving plugin runtime load context. */
|
||||
@@ -162,6 +164,7 @@ type PluginRuntimeLoadContextOptions = {
|
||||
logger?: PluginLogger;
|
||||
manifestRegistry?: PluginManifestRegistry;
|
||||
metadataSnapshot?: PluginMetadataSnapshot;
|
||||
preferBuiltPluginArtifacts?: boolean;
|
||||
};
|
||||
|
||||
/** Creates the default plugin runtime loader logger. */
|
||||
@@ -262,6 +265,7 @@ export function resolvePluginRuntimeLoadContext(
|
||||
...(finalManifestRegistry ? { manifestRegistry: finalManifestRegistry } : {}),
|
||||
...(metadataSnapshot ? { metadataSnapshot } : {}),
|
||||
installRecords,
|
||||
preferBuiltPluginArtifacts: options?.preferBuiltPluginArtifacts === true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -287,6 +291,7 @@ export function buildPluginRuntimeLoadOptionsFromValues(
|
||||
logger: values.logger,
|
||||
manifestRegistry: values.manifestRegistry,
|
||||
installRecords: values.installRecords,
|
||||
preferBuiltPluginArtifacts: values.preferBuiltPluginArtifacts,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user