Skip to content

Commit 0d61844

Browse files
committed
perf: cache plugin package realpaths
1 parent f0bfb3f commit 0d61844

3 files changed

Lines changed: 51 additions & 20 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai
1717

1818
- Gateway: avoid sending duplicate tool-event frames to Control UI connections that are subscribed by both run and session.
1919
- Discord/OpenAI voice: accept longer leading wake-name mistranscripts such as "Open Club" for OpenClaw.
20+
- Gateway/plugins: reuse plugin package realpath checks while building installed plugin indexes so startup avoids repeated filesystem resolution work.
2021
- Discord/OpenAI voice: accept leading fuzzy wake-name transcripts such as "Monty" or "Moti" for a Molty agent while keeping ambient speech gated.
2122
- Media understanding: convert HEIC and HEIF images to JPEG before image description providers run so iPhone photos work in direct and configured image-description flows. (#86037)
2223
- Agents: release embedded-attempt session locks from outer teardown so post-prompt exceptions cannot wedge later requests behind `SessionWriteLockTimeoutError`. Fixes #86014. Thanks @openperf.

src/plugins/installed-plugin-index-record-builder.ts

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import fs from "node:fs";
21
import path from "node:path";
32
import type { OpenClawConfig } from "../config/types.js";
43
import type { PluginCompatCode } from "./compat/registry.js";
@@ -18,7 +17,7 @@ import type {
1817
import type { PluginManifestRecord, PluginManifestRegistry } from "./manifest-registry.js";
1918
import type { PluginDiagnostic } from "./manifest-types.js";
2019
import type { PluginPackageChannel } from "./manifest.js";
21-
import { isPathInsideWithRealpath, safeRealpathSync } from "./path-safety.js";
20+
import { isPathInside, safeRealpathSync } from "./path-safety.js";
2221
import { hasKind } from "./slots.js";
2322

2423
function sortUnique(values: readonly string[] | undefined): readonly string[] {
@@ -77,20 +76,30 @@ export function collectPluginManifestCompatCodes(
7776
return sortUnique(codes) as readonly PluginCompatCode[];
7877
}
7978

80-
function resolvePackageJsonPath(candidate: PluginCandidate | undefined): string | undefined {
79+
function resolvePackageJsonPath(
80+
candidate: PluginCandidate | undefined,
81+
realpathCache: Map<string, string>,
82+
): string | undefined {
8183
if (!candidate?.packageDir) {
8284
return undefined;
8385
}
84-
const packageDir = safeRealpathSync(candidate.packageDir) ?? path.resolve(candidate.packageDir);
86+
const packageDir =
87+
safeRealpathSync(candidate.packageDir, realpathCache) ?? path.resolve(candidate.packageDir);
8588
const packageJsonPath = path.join(packageDir, "package.json");
86-
const rootDir = safeRealpathSync(candidate.rootDir) ?? path.resolve(candidate.rootDir);
87-
return fs.existsSync(packageJsonPath) && isPathInsideWithRealpath(rootDir, packageJsonPath)
89+
const rootDir =
90+
safeRealpathSync(candidate.rootDir, realpathCache) ?? path.resolve(candidate.rootDir);
91+
const packageJsonRealPath = safeRealpathSync(packageJsonPath, realpathCache);
92+
return packageJsonRealPath && isPathInside(rootDir, packageJsonRealPath)
8893
? packageJsonPath
8994
: undefined;
9095
}
9196

92-
function resolvePackageJsonRelativePath(rootDir: string, packageJsonPath: string): string {
93-
const resolvedRootDir = safeRealpathSync(rootDir) ?? path.resolve(rootDir);
97+
function resolvePackageJsonRelativePath(
98+
rootDir: string,
99+
packageJsonPath: string,
100+
realpathCache: Map<string, string>,
101+
): string {
102+
const resolvedRootDir = safeRealpathSync(rootDir, realpathCache) ?? path.resolve(rootDir);
94103
const relativePath = path.relative(resolvedRootDir, packageJsonPath) || "package.json";
95104
return relativePath.split(path.sep).join("/");
96105
}
@@ -100,6 +109,7 @@ function resolvePackageJsonRecord(params: {
100109
packageJsonPath: string | undefined;
101110
diagnostics: PluginDiagnostic[];
102111
pluginId: string;
112+
realpathCache: Map<string, string>;
103113
}): InstalledPluginIndexRecord["packageJson"] | undefined {
104114
if (!params.candidate?.packageDir || !params.packageJsonPath) {
105115
return undefined;
@@ -115,7 +125,11 @@ function resolvePackageJsonRecord(params: {
115125
}
116126
const fileSignature = safeFileSignature(params.packageJsonPath);
117127
return {
118-
path: resolvePackageJsonRelativePath(params.candidate.rootDir, params.packageJsonPath),
128+
path: resolvePackageJsonRelativePath(
129+
params.candidate.rootDir,
130+
params.packageJsonPath,
131+
params.realpathCache,
132+
),
119133
hash,
120134
...(fileSignature ? { fileSignature } : {}),
121135
};
@@ -207,9 +221,10 @@ export function buildInstalledPluginIndexRecords(params: {
207221
}): InstalledPluginIndexRecord[] {
208222
const candidateByRootDir = buildCandidateLookup(params.candidates);
209223
const normalizedConfig = normalizePluginsConfig(params.config?.plugins);
224+
const realpathCache = new Map<string, string>();
210225
return params.registry.plugins.map((record): InstalledPluginIndexRecord => {
211226
const candidate = candidateByRootDir.get(record.rootDir);
212-
const packageJsonPath = resolvePackageJsonPath(candidate);
227+
const packageJsonPath = resolvePackageJsonPath(candidate, realpathCache);
213228
const installRecord = params.installRecords[record.id];
214229
const packageInstall = describePackageInstallSource(candidate);
215230
const packageChannel = normalizePackageChannel(
@@ -224,6 +239,7 @@ export function buildInstalledPluginIndexRecords(params: {
224239
packageJsonPath,
225240
diagnostics: params.diagnostics,
226241
pluginId: record.id,
242+
realpathCache,
227243
});
228244
const enabled = resolveEffectiveEnableState({
229245
id: record.id,

src/plugins/manifest-registry-installed.ts

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
type PackageManifest,
1818
type PluginPackageChannel,
1919
} from "./manifest.js";
20-
import { isPathInsideWithRealpath, safeRealpathSync } from "./path-safety.js";
20+
import { isPathInside, safeRealpathSync } from "./path-safety.js";
2121
import { tracePluginLifecyclePhase } from "./plugin-lifecycle-trace.js";
2222
import {
2323
normalizePluginDependencySpecs,
@@ -33,18 +33,22 @@ function isRelativePathInsideOrEqual(relativePath: string): boolean {
3333
);
3434
}
3535

36-
function resolvePackageJsonPath(record: InstalledPluginIndexRecord): string | undefined {
36+
function resolvePackageJsonPath(
37+
record: InstalledPluginIndexRecord,
38+
realpathCache: Map<string, string>,
39+
): string | undefined {
3740
if (!record.packageJson?.path) {
3841
return undefined;
3942
}
4043
const rootDir = resolveInstalledPluginRootDir(record);
41-
const realRootDir = safeRealpathSync(rootDir) ?? path.resolve(rootDir);
44+
const realRootDir = safeRealpathSync(rootDir, realpathCache) ?? path.resolve(rootDir);
4245
const packageJsonPath = path.resolve(realRootDir, record.packageJson.path);
4346
const relative = path.relative(realRootDir, packageJsonPath);
4447
if (!isRelativePathInsideOrEqual(relative)) {
4548
return undefined;
4649
}
47-
if (!isPathInsideWithRealpath(realRootDir, packageJsonPath)) {
50+
const packageJsonRealPath = safeRealpathSync(packageJsonPath, realpathCache);
51+
if (!packageJsonRealPath || !isPathInside(realRootDir, packageJsonRealPath)) {
4852
return undefined;
4953
}
5054
return packageJsonPath;
@@ -63,6 +67,7 @@ function safeFileSignature(filePath: string | undefined): string | undefined {
6367
}
6468

6569
function buildInstalledManifestRegistryIndexKey(index: InstalledPluginIndex) {
70+
const realpathCache = new Map<string, string>();
6671
return {
6772
version: index.version,
6873
hostContractVersion: index.hostContractVersion,
@@ -72,7 +77,7 @@ function buildInstalledManifestRegistryIndexKey(index: InstalledPluginIndex) {
7277
installRecords: index.installRecords,
7378
diagnostics: index.diagnostics,
7479
plugins: index.plugins.map((record) => {
75-
const packageJsonPath = resolvePackageJsonPath(record);
80+
const packageJsonPath = resolvePackageJsonPath(record, realpathCache);
7681
return {
7782
pluginId: record.pluginId,
7883
packageName: record.packageName,
@@ -359,7 +364,10 @@ function normalizePersistedPackageChannel(value: unknown): PluginPackageChannel
359364
return channel;
360365
}
361366

362-
function resolveInstalledPackageMetadata(record: InstalledPluginIndexRecord): {
367+
function resolveInstalledPackageMetadata(
368+
record: InstalledPluginIndexRecord,
369+
realpathCache: Map<string, string>,
370+
): {
363371
packageManifest?: OpenClawPackageManifest;
364372
packageDependencies?: PluginDependencySpecMap;
365373
packageOptionalDependencies?: PluginDependencySpecMap;
@@ -370,7 +378,9 @@ function resolveInstalledPackageMetadata(record: InstalledPluginIndexRecord): {
370378
channel: recordPackageChannel,
371379
}
372380
: undefined;
373-
const packageJsonPath = record.packageJson?.path ? resolvePackageJsonPath(record) : undefined;
381+
const packageJsonPath = record.packageJson?.path
382+
? resolvePackageJsonPath(record, realpathCache)
383+
: undefined;
374384
if (!packageJsonPath) {
375385
return fallbackPackageManifest ? { packageManifest: fallbackPackageManifest } : {};
376386
}
@@ -409,9 +419,12 @@ function resolveInstalledPackageMetadata(record: InstalledPluginIndexRecord): {
409419
return fallbackPackageManifest ? { packageManifest: fallbackPackageManifest } : {};
410420
}
411421

412-
function toPluginCandidate(record: InstalledPluginIndexRecord): PluginCandidate {
422+
function toPluginCandidate(
423+
record: InstalledPluginIndexRecord,
424+
realpathCache: Map<string, string>,
425+
): PluginCandidate {
413426
const rootDir = resolveInstalledPluginRootDir(record);
414-
const packageMetadata = resolveInstalledPackageMetadata(record);
427+
const packageMetadata = resolveInstalledPackageMetadata(record, realpathCache);
415428
return {
416429
idHint: record.pluginId,
417430
source: record.source ?? resolveFallbackPluginSource(record),
@@ -452,6 +465,7 @@ export function loadPluginManifestRegistryForInstalledIndex(params: {
452465
}
453466
const env = params.env ?? process.env;
454467
const pluginIdSet = params.pluginIds?.length ? new Set(params.pluginIds) : null;
468+
const realpathCache = new Map<string, string>();
455469
const diagnostics = pluginIdSet
456470
? params.index.diagnostics.filter((diagnostic) => {
457471
const pluginId = diagnostic.pluginId;
@@ -461,7 +475,7 @@ export function loadPluginManifestRegistryForInstalledIndex(params: {
461475
const candidates = params.index.plugins
462476
.filter((plugin) => params.includeDisabled || plugin.enabled)
463477
.filter((plugin) => !pluginIdSet || pluginIdSet.has(plugin.pluginId))
464-
.map(toPluginCandidate);
478+
.map((plugin) => toPluginCandidate(plugin, realpathCache));
465479
return loadPluginManifestRegistry({
466480
config: params.config,
467481
workspaceDir: params.workspaceDir,

0 commit comments

Comments
 (0)