fix(plugins): align registry freshness inspection (#125048)

This commit is contained in:
Peter Steinberger
2026-08-16 22:29:55 -07:00
committed by GitHub
parent e3595c50ad
commit fecb223e5b
7 changed files with 296 additions and 242 deletions
@@ -14,11 +14,9 @@ import type { OpenClawConfig } from "../../../config/types.openclaw.js";
import { inspectPersistedInstalledPluginIndexInstallRecordsSync } from "../../../plugins/installed-plugin-index-record-state.js";
import { loadInstalledPluginIndexInstallRecords } from "../../../plugins/installed-plugin-index-records.js";
import {
inspectPersistedInstalledPluginIndex,
readPersistedInstalledPluginIndexSync,
resolveInstalledPluginIndexStorePath,
writePersistedInstalledPluginIndex,
type InstalledPluginIndexStoreInspection,
type InstalledPluginIndexStoreOptions,
} from "../../../plugins/installed-plugin-index-store.js";
import {
@@ -58,7 +56,6 @@ type PluginRegistryInstallMigrationResult =
status: "migrated";
migrated: true;
preflight: PluginRegistryInstallMigrationPreflight;
inspection: InstalledPluginIndexStoreInspection;
current: InstalledPluginIndex;
};
@@ -321,7 +318,6 @@ export async function migratePluginRegistryForInstall(
config,
installRecords,
};
const inspection = await inspectPersistedInstalledPluginIndex(migrationParams);
const candidateIndex = loadInstalledPluginIndex({
...migrationParams,
});
@@ -341,7 +337,6 @@ export async function migratePluginRegistryForInstall(
status: "migrated",
migrated: true,
preflight,
inspection,
current,
};
}
@@ -45,6 +45,8 @@ export function diffInstalledPluginIndexInvalidationReasons(
if (
previousPlugin.rootDir !== currentPlugin.rootDir ||
previousPlugin.manifestPath !== currentPlugin.manifestPath ||
previousPlugin.source !== currentPlugin.source ||
previousPlugin.setupSource !== currentPlugin.setupSource ||
resolveInstalledPluginIndexInstallOwner(previousPlugin) !==
resolveInstalledPluginIndexInstallOwner(currentPlugin) ||
isInstalledPluginIndexInstallOwnerAmbiguous(previousPlugin) !==
@@ -18,7 +18,6 @@ import {
writePersistedInstalledPluginIndexInstallRecordsWithLease,
} from "./installed-plugin-index-records.js";
import {
inspectPersistedInstalledPluginIndex,
readPersistedInstalledPluginIndex,
refreshPersistedInstalledPluginIndex,
resolveInstalledPluginIndexStorePath,
@@ -669,13 +668,12 @@ describe("installed plugin index persistence", () => {
};
await writePersistedInstalledPluginIndex(legacy, { stateDir });
const inspection = await inspectPersistedInstalledPluginIndex({
const inspection = loadPluginRegistrySnapshotWithMetadata({
stateDir,
candidates: [candidate],
env,
});
expect(inspection.state).toBe("stale");
expect(inspection.refreshReasons).toEqual(["migration"]);
expect(inspection.source).toBe("derived");
const refreshed = await refreshPersistedInstalledPluginIndex({
reason: "policy-changed",
@@ -735,85 +733,6 @@ describe("installed plugin index persistence", () => {
await expect(readPersistedInstalledPluginIndex({ stateDir })).resolves.toBeNull();
});
it("inspects missing, fresh, and stale persisted index state without loading runtime", async () => {
const stateDir = makeTempDir();
const pluginDir = path.join(stateDir, "plugins", "demo");
fs.mkdirSync(pluginDir, { recursive: true });
const candidate = createCandidate(pluginDir);
const env = {
OPENCLAW_BUNDLED_PLUGINS_DIR: undefined,
OPENCLAW_VERSION: "2026.4.25",
VITEST: "true",
};
const missingInspect = await inspectPersistedInstalledPluginIndex({
stateDir,
candidates: [candidate],
env,
});
expect(missingInspect.state).toBe("missing");
expect(missingInspect.refreshReasons).toEqual(["missing"]);
expect(missingInspect.persisted).toBeNull();
expectPluginIds(missingInspect.current, ["demo"]);
const current = await refreshPersistedInstalledPluginIndex({
reason: "manual",
stateDir,
candidates: [candidate],
env,
});
const freshInspect = await inspectPersistedInstalledPluginIndex({
stateDir,
candidates: [candidate],
env,
});
expect(freshInspect.state).toBe("fresh");
expect(freshInspect.refreshReasons).toEqual([]);
expect(freshInspect.persisted).toEqual(current);
expectPluginFields(freshInspect.current, "demo", { enabled: true });
const policyInspect = await inspectPersistedInstalledPluginIndex({
stateDir,
candidates: [candidate],
config: {
plugins: {
entries: {
demo: {
enabled: false,
},
},
},
},
env,
});
expect(policyInspect.state).toBe("stale");
expect(policyInspect.refreshReasons).toEqual(["policy-changed"]);
expect(policyInspect.persisted).toEqual(current);
expectPluginFields(policyInspect.current, "demo", { enabled: false });
fs.writeFileSync(
path.join(pluginDir, "openclaw.plugin.json"),
JSON.stringify({
id: "demo",
name: "Demo",
configSchema: { type: "object" },
providers: ["demo", "demo-next"],
}),
"utf8",
);
const staleManifestInspect = await inspectPersistedInstalledPluginIndex({
stateDir,
candidates: [candidate],
env,
});
expect(staleManifestInspect.state).toBe("stale");
expect(staleManifestInspect.refreshReasons).toEqual(["stale-manifest"]);
expect(staleManifestInspect.persisted).toEqual(current);
expectPluginIds(staleManifestInspect.current, ["demo"]);
});
it("refreshes and persists a rebuilt index without loading plugin runtime", async () => {
const stateDir = makeTempDir();
const pluginDir = path.join(stateDir, "plugins", "demo");
@@ -33,19 +33,15 @@ import {
type InstalledPluginIndexStoreOptions,
} from "./installed-plugin-index-store-path.js";
import {
diffInstalledPluginIndexInvalidationReasons,
extractPluginInstallRecordsFromInstalledPluginIndex,
hasInstalledPluginIndexWorkspaceScopeMismatch,
hasMissingConfigPathActivationMetadata,
INSTALLED_PLUGIN_INDEX_WARNING,
INSTALLED_PLUGIN_INDEX_VERSION,
INSTALLED_PLUGIN_INDEX_MIGRATION_VERSION,
loadInstalledPluginIndex,
resolveInstalledPluginIndexPolicyHash,
refreshInstalledPluginIndex,
type InstalledPluginIndex,
type InstalledPluginIndexRefreshReason,
type LoadInstalledPluginIndexParams,
type RefreshInstalledPluginIndexParams,
} from "./installed-plugin-index.js";
import { hasMissingInstalledPluginOwnerMetadata } from "./installed-plugin-package-ownership.js";
@@ -56,16 +52,6 @@ export {
type InstalledPluginIndexStoreOptions,
} from "./installed-plugin-index-store-path.js";
/** Freshness state for the persisted installed plugin index. */
type InstalledPluginIndexStoreState = "missing" | "fresh" | "stale";
export type InstalledPluginIndexStoreInspection = {
state: InstalledPluginIndexStoreState;
refreshReasons: readonly InstalledPluginIndexRefreshReason[];
persisted: InstalledPluginIndex | null;
current: InstalledPluginIndex;
};
export type InstalledPluginIndexWriteLease = {
assertOwnedInTransaction(database: DatabaseSync): void;
};
@@ -564,33 +550,6 @@ function refreshPersistedPolicyState(
};
}
export async function inspectPersistedInstalledPluginIndex(
params: LoadInstalledPluginIndexParams & InstalledPluginIndexStoreOptions = {},
): Promise<InstalledPluginIndexStoreInspection> {
const persisted = await readPersistedInstalledPluginIndex(params);
const current = loadInstalledPluginIndex({
...params,
installRecords:
params.installRecords ?? extractPluginInstallRecordsFromInstalledPluginIndex(persisted),
});
if (!persisted) {
return {
state: "missing",
refreshReasons: ["missing"],
persisted: null,
current,
};
}
const refreshReasons = diffInstalledPluginIndexInvalidationReasons(persisted, current);
return {
state: refreshReasons.length > 0 ? "stale" : "fresh",
refreshReasons,
persisted,
current,
};
}
export async function refreshPersistedInstalledPluginIndex(
params: RefreshInstalledPluginIndexParams & InstalledPluginIndexStoreOptions,
): Promise<InstalledPluginIndex> {
@@ -0,0 +1,235 @@
import fs from "node:fs";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js";
import type { PluginCandidate } from "./discovery.js";
import {
readPersistedInstalledPluginIndex,
refreshPersistedInstalledPluginIndex,
writePersistedInstalledPluginIndex,
} from "./installed-plugin-index-store.js";
import type { InstalledPluginIndex } from "./installed-plugin-index.js";
import { clearPluginMetadataLifecycleCaches } from "./plugin-metadata-lifecycle.js";
import {
inspectPluginRegistry,
loadPluginRegistrySnapshotWithMetadata,
refreshPluginRegistry,
} from "./plugin-registry.js";
import { cleanupTrackedTempDirs, makeTrackedTempDir } from "./test-helpers/fs-fixtures.js";
const tempDirs: string[] = [];
afterEach(() => {
closeOpenClawStateDatabaseForTest();
clearPluginMetadataLifecycleCaches();
cleanupTrackedTempDirs(tempDirs);
});
function makeTempDir(): string {
return makeTrackedTempDir("openclaw-plugin-registry-inspection", tempDirs);
}
function hermeticEnv(): NodeJS.ProcessEnv {
return {
OPENCLAW_BUNDLED_PLUGINS_DIR: undefined,
OPENCLAW_VERSION: "2026.4.25",
VITEST: "true",
};
}
function createCandidate(rootDir: string): PluginCandidate {
const source = path.join(rootDir, "index.ts");
fs.writeFileSync(source, "export default { register() {} };\n", "utf8");
fs.writeFileSync(
path.join(rootDir, "openclaw.plugin.json"),
JSON.stringify({ id: "demo", name: "Demo", configSchema: { type: "object" } }),
"utf8",
);
return { idHint: "demo", source, rootDir, origin: "global" };
}
function createEmptyIndex(stateDir: string): InstalledPluginIndex {
return {
version: 1,
hostContractVersion: "2026.4.25",
compatRegistryVersion: "compat-v1",
migrationVersion: 1,
policyHash: "policy-v1",
generatedAtMs: 1777118400000,
installRecords: {
missing: {
source: "npm",
spec: "missing-plugin@1.0.0",
installPath: path.join(stateDir, "plugins", "missing"),
},
},
plugins: [],
diagnostics: [],
};
}
describe("plugin registry inspection", () => {
it("derives without persisted install records when persisted reads are disabled", async () => {
const stateDir = makeTempDir();
const pluginDir = makeTempDir();
const candidate = createCandidate(pluginDir);
await writePersistedInstalledPluginIndex(createEmptyIndex(stateDir), { stateDir });
const result = loadPluginRegistrySnapshotWithMetadata({
stateDir,
candidates: [candidate],
env: hermeticEnv(),
preferPersisted: false,
});
expect(result.source).toBe("derived");
expect(result.snapshot.plugins.map((plugin) => plugin.pluginId)).toEqual(["demo"]);
expect(result.snapshot.installRecords).not.toHaveProperty("missing");
});
it("reports missing, fresh, policy, and manifest freshness from the snapshot selector", async () => {
const stateDir = makeTempDir();
const pluginDir = makeTempDir();
const candidate = createCandidate(pluginDir);
const env = hermeticEnv();
const config = {};
const missing = await inspectPluginRegistry({ stateDir, candidates: [candidate], config, env });
expect(missing.state).toBe("missing");
expect(missing.refreshReasons).toEqual(["missing"]);
await refreshPluginRegistry({
reason: "manual",
stateDir,
candidates: [candidate],
config,
env,
});
const fresh = await inspectPluginRegistry({ stateDir, candidates: [candidate], config, env });
expect(fresh.state).toBe("fresh");
expect(fresh.refreshReasons).toEqual([]);
const policy = await inspectPluginRegistry({
stateDir,
candidates: [candidate],
config: { plugins: { entries: { demo: { enabled: false } } } },
env,
});
expect(policy.state).toBe("stale");
expect(policy.refreshReasons).toEqual(["policy-changed"]);
fs.writeFileSync(
path.join(pluginDir, "openclaw.plugin.json"),
JSON.stringify({
id: "demo",
name: "Demo",
configSchema: { type: "object" },
providers: ["demo-next"],
}),
"utf8",
);
const manifest = await inspectPluginRegistry({
stateDir,
candidates: [candidate],
config,
env,
});
expect(manifest.state).toBe("stale");
expect(manifest.refreshReasons).toEqual(["stale-manifest"]);
});
it("agrees with snapshot selection when a packaged runtime entry changes", async () => {
const stateDir = makeTempDir();
const pluginDir = makeTempDir();
const sourceCandidate = createCandidate(pluginDir);
const env = hermeticEnv();
await refreshPluginRegistry({
reason: "manual",
stateDir,
candidates: [sourceCandidate],
env,
});
const builtSource = path.join(pluginDir, "index.js");
fs.writeFileSync(builtSource, "export default { register() {} };\n", "utf8");
fs.rmSync(sourceCandidate.source);
const builtCandidate = { ...sourceCandidate, source: builtSource };
const snapshot = loadPluginRegistrySnapshotWithMetadata({
stateDir,
candidates: [builtCandidate],
env,
});
const inspection = await inspectPluginRegistry({
stateDir,
candidates: [builtCandidate],
env,
});
expect(snapshot.source).toBe("derived");
expect(snapshot.diagnostics.map((diagnostic) => diagnostic.code)).toEqual([
"persisted-registry-stale-source",
]);
expect(inspection.state).toBe("stale");
expect(inspection.refreshReasons).toEqual(["source-changed"]);
expect(inspection.current.plugins[0]?.source).toBe(builtSource);
});
it("uses the configured system-agent workspace for the freshness verdict", async () => {
const stateDir = makeTempDir();
const workspaceDir = makeTempDir();
const pluginDir = path.join(workspaceDir, ".openclaw", "extensions", "demo");
fs.mkdirSync(pluginDir, { recursive: true });
createCandidate(pluginDir);
const env = { ...hermeticEnv(), OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1" };
const config = {
agents: {
ownership: "explicit" as const,
defaults: { systemAgent: { agentId: "main" } },
entries: { main: { workspace: workspaceDir } },
},
};
await refreshPersistedInstalledPluginIndex({
reason: "manual",
stateDir,
config,
env,
});
const listSelection = loadPluginRegistrySnapshotWithMetadata({
stateDir,
workspaceDir,
config,
env,
});
const inspection = await inspectPluginRegistry({ stateDir, config, env });
expect(listSelection.source).toBe("derived");
expect(listSelection.diagnostics.map((diagnostic) => diagnostic.code)).toEqual([
"persisted-registry-stale-source",
]);
expect(inspection.state).toBe("stale");
expect(inspection.refreshReasons).toEqual(["source-changed"]);
expect(inspection.current.workspaceDir).toBe(workspaceDir);
expect(inspection.current.plugins.map((plugin) => plugin.pluginId)).toEqual(["demo"]);
await refreshPluginRegistry({ reason: "manual", stateDir, config, env });
const repaired = await inspectPluginRegistry({ stateDir, config, env });
expect(repaired.state).toBe("fresh");
expect(repaired.refreshReasons).toEqual([]);
});
it("preserves install records when refreshing the persisted registry", async () => {
const stateDir = makeTempDir();
await writePersistedInstalledPluginIndex(createEmptyIndex(stateDir), { stateDir });
await refreshPluginRegistry({ reason: "manual", stateDir, candidates: [], env: hermeticEnv() });
const persisted = await readPersistedInstalledPluginIndex({ stateDir });
expect(persisted?.installRecords.missing).toMatchObject({
source: "npm",
spec: "missing-plugin@1.0.0",
installPath: path.join(stateDir, "plugins", "missing"),
});
expect(persisted?.plugins).toEqual([]);
});
});
+56 -17
View File
@@ -18,13 +18,12 @@ import { safeFileSignature, safeHashFile } from "./installed-plugin-index-hash.j
import { hasOptionalMissingPluginManifestFile } from "./installed-plugin-index-manifest.js";
import { loadInstalledPluginIndexInstallRecordsSync } from "./installed-plugin-index-record-reader.js";
import {
inspectPersistedInstalledPluginIndex,
readPersistedInstalledPluginIndexSync,
refreshPersistedInstalledPluginIndex,
type InstalledPluginIndexStoreInspection,
type InstalledPluginIndexStoreOptions,
} from "./installed-plugin-index-store.js";
import {
diffInstalledPluginIndexInvalidationReasons,
extractPluginInstallRecordsFromInstalledPluginIndex,
getInstalledPluginRecord,
hasInstalledPluginIndexWorkspaceScopeMismatch,
@@ -124,7 +123,6 @@ function resolvePluginRegistryContent(
export type PluginRegistrySnapshot = InstalledPluginIndex;
export type PluginRegistryRecord = InstalledPluginIndexRecord;
type PluginRegistryInspection = InstalledPluginIndexStoreInspection;
export type { PluginRegistrySnapshotSource } from "./plugin-registry-snapshot.types.js";
type PluginRegistrySnapshotDiagnosticCode =
| "persisted-registry-missing"
@@ -156,6 +154,26 @@ type GetPluginRecordParams = LoadPluginRegistryParams & {
pluginId: string;
};
function resolveControlPlaneRegistryParams<T extends LoadInstalledPluginIndexParams>(params: T): T {
if (!params.config) {
return params;
}
const workspace = resolvePluginControlPlaneWorkspace({
config: params.config,
env: params.env,
workspaceDir: params.workspaceDir,
});
const diagnostics = appendPluginControlPlaneWorkspaceDiagnostic(
params.diagnostics ?? [],
workspace,
);
return {
...params,
...(diagnostics.length > 0 ? { diagnostics } : {}),
...(workspace.workspaceDir !== undefined ? { workspaceDir: workspace.workspaceDir } : {}),
};
}
function canReuseCurrentPluginMetadataSnapshot(params: LoadPluginRegistryParams): boolean {
return (
params.allowCurrent !== false &&
@@ -619,10 +637,41 @@ export function isPluginEnabled(params: GetPluginRecordParams): boolean {
return isInstalledPluginEnabled(resolveSnapshot(params), params.pluginId, params.config);
}
export function inspectPluginRegistry(
export async function inspectPluginRegistry(
params: LoadInstalledPluginIndexParams & InstalledPluginIndexStoreOptions = {},
): Promise<PluginRegistryInspection> {
return inspectPersistedInstalledPluginIndex(params);
) {
const inspectionParams = resolveControlPlaneRegistryParams(params);
const persisted = readPersistedInstalledPluginIndexSync(inspectionParams);
// Inspection and runtime selection share one verdict so runtime cannot reject "fresh".
const result = loadPluginRegistrySnapshotWithMetadata({
...inspectionParams,
allowCurrent: false,
});
if (!persisted) {
return {
state: "missing" as const,
refreshReasons: ["missing"],
persisted: null,
current: result.snapshot,
};
}
const fresh = result.source === "persisted";
const refreshReasons = fresh
? []
: [...diffInstalledPluginIndexInvalidationReasons(persisted, result.snapshot)];
if (!fresh && refreshReasons.length === 0) {
refreshReasons.push(
result.diagnostics.some((diagnostic) => diagnostic.code === "persisted-registry-stale-policy")
? "policy-changed"
: "source-changed",
);
}
return {
state: fresh ? ("fresh" as const) : ("stale" as const),
refreshReasons,
persisted,
current: result.snapshot,
};
}
export function refreshPluginRegistry(
@@ -631,15 +680,5 @@ export function refreshPluginRegistry(
if (!params.config) {
return refreshPersistedInstalledPluginIndex(params);
}
const workspace = resolvePluginControlPlaneWorkspace({
config: params.config,
env: params.env,
workspaceDir: params.workspaceDir,
});
const refreshParams = {
...params,
diagnostics: appendPluginControlPlaneWorkspaceDiagnostic(params.diagnostics ?? [], workspace),
...(workspace.workspaceDir !== undefined ? { workspaceDir: workspace.workspaceDir } : {}),
};
return refreshPersistedInstalledPluginIndex(refreshParams);
return refreshPersistedInstalledPluginIndex(resolveControlPlaneRegistryParams(params));
}
+1 -96
View File
@@ -8,10 +8,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js";
import { recordPluginCandidateInstallOwner } from "./candidate-install-owner.js";
import type { PluginCandidate } from "./discovery.js";
import {
readPersistedInstalledPluginIndex,
writePersistedInstalledPluginIndex,
} from "./installed-plugin-index-store.js";
import { writePersistedInstalledPluginIndex } from "./installed-plugin-index-store.js";
import {
resolveInstalledPluginIndexPolicyHash,
type InstalledPluginIndex,
@@ -21,13 +18,11 @@ import { clearPluginMetadataLifecycleCaches } from "./plugin-metadata-lifecycle.
import {
createPluginRegistryIdNormalizer,
getPluginRecord,
inspectPluginRegistry,
isPluginEnabled,
listPluginContributionIds,
loadPluginRegistrySnapshot,
loadPluginRegistrySnapshotWithMetadata,
normalizePluginsConfigWithRegistry,
refreshPluginRegistry,
resolveManifestContractOwnerPluginId,
resolveManifestContractPluginIds,
resolvePluginContributionOwners,
@@ -981,94 +976,4 @@ describe("plugin registry facade", () => {
expect(first.snapshot.hostContractVersion).toBe("2026.4.25");
expect(second.snapshot.hostContractVersion).toBe("2026.4.26");
});
it("derives a fresh registry without persisted install records when caller disables persisted reads", async () => {
const stateDir = makeTempDir();
const rootDir = makeTempDir();
const candidate = createCandidate(rootDir);
await writePersistedInstalledPluginIndex(
createIndex("persisted", {
installRecords: {
persisted: {
source: "npm",
spec: "persisted-plugin@1.0.0",
installPath: path.join(stateDir, "plugins", "persisted"),
},
},
}),
{ stateDir },
);
const result = loadPluginRegistrySnapshotWithMetadata({
stateDir,
candidates: [candidate],
env: hermeticEnv(),
preferPersisted: false,
});
expect(result.source).toBe("derived");
expectSnapshotPluginIds(result.snapshot, ["demo"]);
expect(result.snapshot.installRecords).not.toHaveProperty("persisted");
});
it("exposes explicit persisted registry inspect and refresh operations", async () => {
const stateDir = makeTempDir();
const pluginDir = path.join(stateDir, "plugins", "demo");
fs.mkdirSync(pluginDir, { recursive: true });
const candidate = createCandidate(pluginDir);
const env = hermeticEnv();
const missingInspect = await inspectPluginRegistry({ stateDir, candidates: [candidate], env });
expect(missingInspect.state).toBe("missing");
expect(missingInspect.refreshReasons).toEqual(["missing"]);
expect(missingInspect.persisted).toBeNull();
expect(missingInspect.current.plugins.map((plugin) => plugin.pluginId)).toEqual(["demo"]);
await refreshPluginRegistry({
reason: "manual",
stateDir,
candidates: [candidate],
env,
});
const freshInspect = await inspectPluginRegistry({ stateDir, candidates: [candidate], env });
expect(freshInspect.state).toBe("fresh");
expect(freshInspect.refreshReasons).toEqual([]);
expect(freshInspect.persisted?.plugins.map((plugin) => plugin.pluginId)).toEqual(["demo"]);
});
it("preserves install records when refreshing the persisted registry", async () => {
const stateDir = makeTempDir();
await writePersistedInstalledPluginIndex(
createIndex("missing", {
installRecords: {
missing: {
source: "npm",
spec: "missing-plugin@1.0.0",
installPath: path.join(stateDir, "plugins", "missing"),
},
},
plugins: [],
}),
{ stateDir },
);
await refreshPluginRegistry({
reason: "manual",
stateDir,
candidates: [],
env: hermeticEnv(),
});
const persisted = await readPersistedInstalledPluginIndex({ stateDir });
if (!persisted) {
throw new Error("Expected persisted plugin index");
}
expectInstallRecord(persisted.installRecords, "missing", {
source: "npm",
spec: "missing-plugin@1.0.0",
installPath: path.join(stateDir, "plugins", "missing"),
});
expect(persisted.plugins).toEqual([]);
});
});