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:
Dallin Romney
2026-08-21 09:10:48 -07:00
committed by GitHub
parent 566b0e53d5
commit ef91c818c4
14 changed files with 96 additions and 31 deletions
+23 -17
View File
@@ -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");
+3 -1
View File
@@ -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,
+21 -3
View File
@@ -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<{
+6 -1
View File
@@ -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,
});
+3 -1
View File
@@ -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;
+4
View File
@@ -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";
+3
View File
@@ -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"],
+5
View File
@@ -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,
};
}