Skip to content

Commit 36bca54

Browse files
committed
fix(plugins): keep derived metadata snapshots fresh
1 parent b8e9ab9 commit 36bca54

8 files changed

Lines changed: 61 additions & 21 deletions

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -959,7 +959,7 @@ Docs: https://docs.openclaw.ai
959959
- CLI/plugins: route lazy plugin command-registration chatter to stderr only during JSON-output command registration, keeping plugin-backed `--json` stdout parseable without changing parse-only or pass-through `--json` behavior. Fixes #81535. (#81536) Thanks @ScientificProgrammer and @vincentkoc.
960960
- Plugins: treat git plugin install refs as refs instead of checkout flags, so option-like selectors fail checkout instead of silently installing the default branch. Fixes #79898. (#79901) Thanks @afurm and @vincentkoc.
961961
- Doctor/memory: stop warning that no memory plugin is active when an enabled alternate memory plugin explicitly owns the memory slot, while preserving the warning for missing or disabled slot entries. Fixes #78540. (#78557) Thanks @carladams1299-lab and @vincentkoc.
962-
- Plugins: keep process-local plugin metadata snapshot memo freshness tied to the cached registry snapshot so policy-stale derived plugin metadata edits invalidate the memo instead of returning stale owners or command aliases. (#81064) Thanks @Kaspre.
962+
- Plugins: keep derived plugin metadata snapshots uncached when the persisted registry is missing, disabled, or stale, so newly added plugins are discovered without restarting. (#81064) Thanks @Kaspre.
963963
- Plugins: discover provider plugins from `setup.providers[].envVars` credentials during provider discovery while keeping the deprecated `providerAuthEnvVars` fallback. (#81542) Thanks @JARVIS-Glasses.
964964
- Docs/Codex harness: clarify that per-agent `CODEX_HOME` isolates `~/.codex` while inherited `HOME` intentionally keeps `.agents` discovery and subprocess user-home state available.
965965
- CLI/plugins: keep bare plugin and parent-command help on the lightweight path, avoiding plugin registry discovery before rendering help.

src/plugins/current-plugin-metadata-snapshot.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@ import type { PluginMetadataSnapshot } from "./plugin-metadata-snapshot.js";
1616
function createSnapshot(
1717
params: {
1818
config?: Parameters<typeof resolveInstalledPluginIndexPolicyHash>[0];
19+
registrySource?: PluginMetadataSnapshot["registrySource"];
1920
workspaceDir?: string;
2021
} = {},
2122
): PluginMetadataSnapshot {
2223
return {
2324
policyHash: resolveInstalledPluginIndexPolicyHash(params.config),
25+
...(params.registrySource ? { registrySource: params.registrySource } : {}),
2426
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
2527
index: {
2628
version: 1,
@@ -216,6 +218,14 @@ describe("current plugin metadata snapshot", () => {
216218
expect(getCurrentPluginMetadataSnapshot()).toBeUndefined();
217219
});
218220

221+
it("does not keep derived registry snapshots as the current snapshot", () => {
222+
const persisted = createSnapshot({ registrySource: "persisted" });
223+
setCurrentPluginMetadataSnapshot(persisted);
224+
setCurrentPluginMetadataSnapshot(createSnapshot({ registrySource: "derived" }));
225+
226+
expect(getCurrentPluginMetadataSnapshot()).toBeUndefined();
227+
});
228+
219229
it("restores a captured current snapshot state", () => {
220230
const firstConfig = { plugins: { allow: ["first"] } };
221231
const secondConfig = { plugins: { allow: ["second"] } };

src/plugins/current-plugin-metadata-snapshot.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ export function resolvePluginMetadataControlPlaneFingerprint(
2323
});
2424
}
2525

26+
export function isReusableCurrentPluginMetadataSnapshot(
27+
snapshot: PluginMetadataSnapshot,
28+
): boolean {
29+
return snapshot.registrySource !== "derived";
30+
}
31+
2632
// Single-slot Gateway-owned handoff. Replace or clear it at lifecycle boundaries;
2733
// never accumulate historical metadata snapshots here.
2834
export function setCurrentPluginMetadataSnapshot(
@@ -34,6 +40,10 @@ export function setCurrentPluginMetadataSnapshot(
3440
workspaceDir?: string;
3541
} = {},
3642
): void {
43+
if (snapshot && !isReusableCurrentPluginMetadataSnapshot(snapshot)) {
44+
clearCurrentPluginMetadataSnapshotState();
45+
return;
46+
}
3747
const compatiblePolicyHashes = snapshot
3848
? options.compatibleConfigs?.map((config) => resolveInstalledPluginIndexPolicyHash(config))
3949
: undefined;

src/plugins/plugin-metadata-snapshot.memo.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ describe("loadPluginMetadataSnapshot process memo", () => {
254254
expect(second.byPluginId.get("demo")).toBe(second.plugins[0]);
255255
});
256256

257-
it("memoizes policy-stale derived snapshots used by validation callers", () => {
257+
it("does not memoize policy-stale derived snapshots", () => {
258258
const stateDir = tempStateDir();
259259
touchPersistedIndex(stateDir);
260260
loadPluginRegistrySnapshotWithMetadata.mockReturnValue({
@@ -272,7 +272,7 @@ describe("loadPluginMetadataSnapshot process memo", () => {
272272
loadPluginMetadataSnapshot({ config: {}, env: {}, stateDir });
273273
loadPluginMetadataSnapshot({ config: {}, env: {}, stateDir });
274274

275-
expect(loadPluginRegistrySnapshotWithMetadata).toHaveBeenCalledOnce();
275+
expect(loadPluginRegistrySnapshotWithMetadata).toHaveBeenCalledTimes(2);
276276
});
277277

278278
it("refreshes policy-stale derived snapshots when derived plugin files change", () => {

src/plugins/plugin-metadata-snapshot.ts

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -768,18 +768,7 @@ function canMemoizePluginMetadataSnapshotResult(result: {
768768
registrySource: PluginRegistrySnapshotSource;
769769
snapshot: PluginMetadataSnapshot;
770770
}): boolean {
771-
if (result.snapshot.index.plugins.length === 0) {
772-
return false;
773-
}
774-
if (result.registrySource !== "derived") {
775-
return true;
776-
}
777-
return (
778-
result.snapshot.registryDiagnostics.length > 0 &&
779-
result.snapshot.registryDiagnostics.every(
780-
(diagnostic) => diagnostic.code === "persisted-registry-stale-policy",
781-
)
782-
);
771+
return result.registrySource !== "derived" && result.snapshot.index.plugins.length > 0;
783772
}
784773

785774
function loadPluginMetadataSnapshotImpl(params: LoadPluginMetadataSnapshotParams): {
@@ -831,6 +820,7 @@ function loadPluginMetadataSnapshotImpl(params: LoadPluginMetadataSnapshotParams
831820
registrySource: registryResult.source,
832821
snapshot: {
833822
policyHash: index.policyHash,
823+
registrySource: registryResult.source,
834824
configFingerprint: resolvePluginMetadataControlPlaneFingerprint({
835825
config: params.config,
836826
env: params.env,

src/plugins/plugin-metadata-snapshot.types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { OpenClawConfig } from "../config/types.openclaw.js";
22
import type { InstalledPluginIndex } from "./installed-plugin-index.js";
33
import type { PluginManifestRecord, PluginManifestRegistry } from "./manifest-registry.js";
44
import type { PluginDiagnostic } from "./manifest-types.js";
5+
import type { PluginRegistrySnapshotSource } from "./plugin-registry-snapshot.js";
56

67
export type PluginMetadataSnapshotOwnerMaps = {
78
channels: ReadonlyMap<string, readonly string[]>;
@@ -36,6 +37,7 @@ export type PluginMetadataSnapshotRegistryDiagnostic = {
3637
export type PluginMetadataSnapshot = {
3738
policyHash: string;
3839
configFingerprint?: string;
40+
registrySource?: PluginRegistrySnapshotSource;
3941
workspaceDir?: string;
4042
index: InstalledPluginIndex;
4143
registryDiagnostics: readonly PluginMetadataSnapshotRegistryDiagnostic[];

src/plugins/runtime/load-context.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const metadataSnapshot = {
2222
const loadPluginMetadataSnapshotMock = vi.fn(() => metadataSnapshot);
2323
const getCurrentPluginMetadataSnapshotMock = vi.fn(() => undefined);
2424
const setCurrentPluginMetadataSnapshotMock = vi.fn();
25+
const clearCurrentPluginMetadataSnapshotMock = vi.fn();
2526

2627
let resolvePluginRuntimeLoadContext: typeof import("./load-context.js").resolvePluginRuntimeLoadContext;
2728
let buildPluginRuntimeLoadOptions: typeof import("./load-context.js").buildPluginRuntimeLoadOptions;
@@ -47,7 +48,11 @@ vi.mock("../plugin-metadata-snapshot.js", () => ({
4748
}));
4849

4950
vi.mock("../current-plugin-metadata-snapshot.js", () => ({
51+
clearCurrentPluginMetadataSnapshot: clearCurrentPluginMetadataSnapshotMock,
5052
getCurrentPluginMetadataSnapshot: getCurrentPluginMetadataSnapshotMock,
53+
isReusableCurrentPluginMetadataSnapshot: (
54+
snapshot: typeof metadataSnapshot & { registrySource?: "derived" },
55+
) => snapshot.registrySource !== "derived",
5156
setCurrentPluginMetadataSnapshot: setCurrentPluginMetadataSnapshotMock,
5257
}));
5358

@@ -65,6 +70,7 @@ describe("resolvePluginRuntimeLoadContext", () => {
6570
loadPluginMetadataSnapshotMock.mockClear();
6671
getCurrentPluginMetadataSnapshotMock.mockClear();
6772
setCurrentPluginMetadataSnapshotMock.mockClear();
73+
clearCurrentPluginMetadataSnapshotMock.mockClear();
6874
resolveAgentWorkspaceDirMock.mockClear();
6975
resolveDefaultAgentIdMock.mockClear();
7076

@@ -134,6 +140,22 @@ describe("resolvePluginRuntimeLoadContext", () => {
134140
expect(resolveAgentWorkspaceDirMock).toHaveBeenCalledWith(resolvedConfig, "default");
135141
});
136142

143+
it("does not store derived metadata as the reusable runtime snapshot", () => {
144+
const derivedSnapshot = { ...metadataSnapshot } as typeof metadataSnapshot & {
145+
registrySource: "derived";
146+
};
147+
derivedSnapshot.registrySource = "derived";
148+
loadPluginMetadataSnapshotMock.mockReturnValueOnce(derivedSnapshot);
149+
150+
resolvePluginRuntimeLoadContext({
151+
config: { plugins: {} },
152+
env: { HOME: "/tmp/openclaw-home" } as NodeJS.ProcessEnv,
153+
});
154+
155+
expect(setCurrentPluginMetadataSnapshotMock).not.toHaveBeenCalled();
156+
expect(clearCurrentPluginMetadataSnapshotMock).toHaveBeenCalledOnce();
157+
});
158+
137159
it("uses the source runtime snapshot for plugin activation source config", () => {
138160
const runtimeConfig = { plugins: {} };
139161
const sourceConfig = {

src/plugins/runtime/load-context.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import type { PluginInstallRecord } from "../../config/types.plugins.js";
66
import { createSubsystemLogger } from "../../logging.js";
77
import { resolvePluginActivationSourceConfig } from "../activation-source-config.js";
88
import {
9+
clearCurrentPluginMetadataSnapshot,
910
getCurrentPluginMetadataSnapshot,
11+
isReusableCurrentPluginMetadataSnapshot,
1012
setCurrentPluginMetadataSnapshot,
1113
} from "../current-plugin-metadata-snapshot.js";
1214
import { extractPluginInstallRecordsFromInstalledPluginIndex } from "../installed-plugin-index-install-records.js";
@@ -95,12 +97,16 @@ export function resolvePluginRuntimeLoadContext(
9597
const workspaceDir =
9698
options?.workspaceDir ?? resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config));
9799
if (metadataSnapshot) {
98-
setCurrentPluginMetadataSnapshot(metadataSnapshot, {
99-
config: rawConfig,
100-
compatibleConfigs: [config, activationSourceConfig],
101-
env,
102-
workspaceDir,
103-
});
100+
if (isReusableCurrentPluginMetadataSnapshot(metadataSnapshot)) {
101+
setCurrentPluginMetadataSnapshot(metadataSnapshot, {
102+
config: rawConfig,
103+
compatibleConfigs: [config, activationSourceConfig],
104+
env,
105+
workspaceDir,
106+
});
107+
} else {
108+
clearCurrentPluginMetadataSnapshot();
109+
}
104110
}
105111
return {
106112
rawConfig,

0 commit comments

Comments
 (0)