Skip to content

Commit edbd833

Browse files
committed
perf(gateway): reduce startup filesystem probes
1 parent fcb9c46 commit edbd833

4 files changed

Lines changed: 98 additions & 11 deletions

File tree

CHANGELOG.md

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

99
- Gateway/perf: lazy-load startup-idle plugin work, core gateway method handlers, and the embedded ACPX runtime so Gateway health and ready signals no longer wait on unused handler trees or ACPX probes.
10+
- Gateway/perf: cache plugin SDK public-surface alias maps and skip irrelevant macOS Linuxbrew PATH probes so Gateway startup avoids repeated filesystem walks and slow missing-directory stats.
1011
- Meeting Notes: add a source-only external meeting-notes plugin and SDK source-provider contract outside the core npm package, with auto-start capture config, manual transcript imports, read-only `openclaw meeting-notes` CLI access, and Discord voice as the first live source.
1112
- Docs/channels/config: add Signal `configPath`, Telegram wildcard topic defaults, local-time backup archive names, Termux home fallback, include-path validation, secret-scanner-safe placeholder guidance, Gemini CLI/Antigravity media guidance, and macOS VM auto-login guidance. Thanks @NorseGaud, @yudistiraashadi, @huangqian8, @VibhorGautam, @maweibin, @tianxingleo, @IgnacioPro, and @xzcxzcyy-claw.
1213
- Docs: clarify model-usage portability, Codex migration prerequisites, status bootstrap wording, thread-bound subagent limits, hook ownership, and config-preserving safety guidance. Thanks @aniruddhaadak80, @leno23, @TomDjerry, @matthewxmurphy, @vincentkoc, and @stablegenius49.

src/infra/path-env.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,4 +366,42 @@ describe("ensureOpenClawCliOnPath", () => {
366366
expect(updated).not.toContain(maliciousBin);
367367
expect(updated).not.toContain(maliciousSbin);
368368
});
369+
370+
it("does not probe Linuxbrew fallbacks on macOS unless already inherited", () => {
371+
const { tmp, appCli } = setupAppCliRoot("case-no-darwin-linuxbrew");
372+
const homeLinuxbrewBin = path.join(tmp, ".linuxbrew", "bin");
373+
const globalLinuxbrewBin = "/home/linuxbrew/.linuxbrew/bin";
374+
setDir(path.join(tmp, ".linuxbrew"));
375+
setDir(homeLinuxbrewBin);
376+
setDir("/home");
377+
setDir("/home/linuxbrew");
378+
setDir("/home/linuxbrew/.linuxbrew");
379+
setDir(globalLinuxbrewBin);
380+
resetBootstrapEnv("/usr/bin:/bin");
381+
382+
const updated = bootstrapPath({
383+
execPath: appCli,
384+
cwd: tmp,
385+
homeDir: tmp,
386+
platform: "darwin",
387+
});
388+
389+
expect(updated).not.toContain(homeLinuxbrewBin);
390+
expect(updated).not.toContain(globalLinuxbrewBin);
391+
});
392+
393+
it("keeps inherited Linuxbrew path entries on macOS", () => {
394+
const { tmp, appCli } = setupAppCliRoot("case-keep-darwin-linuxbrew");
395+
const globalLinuxbrewBin = "/home/linuxbrew/.linuxbrew/bin";
396+
resetBootstrapEnv(`${globalLinuxbrewBin}:/usr/bin:/bin`);
397+
398+
const updated = bootstrapPath({
399+
execPath: appCli,
400+
cwd: tmp,
401+
homeDir: tmp,
402+
platform: "darwin",
403+
});
404+
405+
expect(updated).toContain(globalLinuxbrewBin);
406+
});
369407
});

src/infra/path-env.ts

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,37 @@ function isDirectory(dirPath: string): boolean {
3030
}
3131
}
3232

33+
function splitPathParts(pathEnv: string): Set<string> {
34+
return new Set(
35+
pathEnv
36+
.split(path.delimiter)
37+
.map((part) => part.trim())
38+
.filter(Boolean),
39+
);
40+
}
41+
42+
function isKnownPathDir(existingPathParts: ReadonlySet<string>, dirPath: string): boolean {
43+
return existingPathParts.has(dirPath) || isDirectory(dirPath);
44+
}
45+
46+
function isLinuxbrewPath(dirPath: string): boolean {
47+
return dirPath.split(path.sep).includes(".linuxbrew");
48+
}
49+
50+
function resolvePathBootstrapBrewDirs(params: {
51+
homeDir: string;
52+
platform: NodeJS.Platform;
53+
existingPathParts: ReadonlySet<string>;
54+
}): string[] {
55+
const candidates = resolveBrewPathDirs({ homeDir: params.homeDir });
56+
if (params.platform !== "darwin") {
57+
return candidates;
58+
}
59+
return candidates.filter(
60+
(candidate) => !isLinuxbrewPath(candidate) || params.existingPathParts.has(candidate),
61+
);
62+
}
63+
3364
function mergePath(params: { existing: string; prepend?: string[]; append?: string[] }): string {
3465
const partsExisting = params.existing
3566
.split(path.delimiter)
@@ -49,7 +80,10 @@ function mergePath(params: { existing: string; prepend?: string[]; append?: stri
4980
return merged.join(path.delimiter);
5081
}
5182

52-
function candidateBinDirs(opts: EnsureOpenClawPathOpts): { prepend: string[]; append: string[] } {
83+
function candidateBinDirs(
84+
opts: EnsureOpenClawPathOpts,
85+
existingPathParts: ReadonlySet<string>,
86+
): { prepend: string[]; append: string[] } {
5387
const execPath = opts.execPath ?? process.execPath;
5488
const cwd = opts.cwd ?? process.cwd();
5589
const homeDir = opts.homeDir ?? os.homedir();
@@ -100,10 +134,10 @@ function candidateBinDirs(opts: EnsureOpenClawPathOpts): { prepend: string[]; ap
100134
// shadow trusted OS binaries.
101135
// This includes Brew/Homebrew dirs, which are useful for finding `openclaw`
102136
// in launchd/minimal environments but must not be treated as trusted.
103-
append.push(...resolveBrewPathDirs({ homeDir }));
137+
append.push(...resolvePathBootstrapBrewDirs({ homeDir, platform, existingPathParts }));
104138
const miseDataDir = process.env.MISE_DATA_DIR ?? path.join(homeDir, ".local", "share", "mise");
105139
const miseShims = path.join(miseDataDir, "shims");
106-
if (isDirectory(miseShims)) {
140+
if (isKnownPathDir(existingPathParts, miseShims)) {
107141
append.push(miseShims);
108142
}
109143
if (platform === "darwin") {
@@ -117,7 +151,10 @@ function candidateBinDirs(opts: EnsureOpenClawPathOpts): { prepend: string[]; ap
117151
append.push(path.join(homeDir, ".bun", "bin"));
118152
append.push(path.join(homeDir, ".yarn", "bin"));
119153

120-
return { prepend: prepend.filter(isDirectory), append: append.filter(isDirectory) };
154+
return {
155+
prepend: prepend.filter((candidate) => isKnownPathDir(existingPathParts, candidate)),
156+
append: append.filter((candidate) => isKnownPathDir(existingPathParts, candidate)),
157+
};
121158
}
122159

123160
/**
@@ -131,7 +168,8 @@ export function ensureOpenClawCliOnPath(opts: EnsureOpenClawPathOpts = {}) {
131168
process.env.OPENCLAW_PATH_BOOTSTRAPPED = "1";
132169

133170
const existing = opts.pathEnv ?? process.env.PATH ?? "";
134-
const { prepend, append } = candidateBinDirs(opts);
171+
const existingPathParts = splitPathParts(existing);
172+
const { prepend, append } = candidateBinDirs(opts, existingPathParts);
135173
if (prepend.length === 0 && append.length === 0) {
136174
return;
137175
}

src/plugins/sdk-alias.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,9 @@ const cachedPluginSdkExportedSubpaths = new PluginLruCache<string[]>(
264264
const cachedPluginSdkScopedAliasMaps = new PluginLruCache<Record<string, string>>(
265265
MAX_PLUGIN_LOADER_ALIAS_CACHE_ENTRIES,
266266
);
267+
const cachedBundledPluginPublicSurfaceAliasMaps = new PluginLruCache<Record<string, string>>(
268+
MAX_PLUGIN_LOADER_ALIAS_CACHE_ENTRIES,
269+
);
267270
const PLUGIN_SDK_PACKAGE_NAMES = ["openclaw/plugin-sdk", "@openclaw/plugin-sdk"] as const;
268271
const OFFICIAL_CODEX_PLUGIN_PACKAGE_NAME = "@openclaw/codex";
269272
const CODEX_NATIVE_TASK_RUNTIME_PLUGIN_SDK_SUBPATH = "codex-native-task-runtime";
@@ -417,19 +420,25 @@ function resolveBundledPluginPackagePublicSurfaceAliasMap(params: {
417420
if (!packageRoot) {
418421
return {};
419422
}
423+
const orderedKinds = resolvePluginSdkAliasCandidateOrder({
424+
modulePath: params.modulePath,
425+
isProduction: process.env.NODE_ENV === "production",
426+
pluginSdkResolution: params.pluginSdkResolution,
427+
});
428+
const includePrivateQa = shouldIncludePrivateLocalOnlyPluginSdkSubpaths();
429+
const cacheKey = `${packageRoot}::${orderedKinds.join(",")}::privateQa=${includePrivateQa ? "1" : "0"}`;
430+
const cached = cachedBundledPluginPublicSurfaceAliasMaps.get(cacheKey);
431+
if (cached) {
432+
return cached;
433+
}
420434
const extensionsRoot = path.join(packageRoot, "extensions");
421435
let extensionDirs: fs.Dirent[];
422436
try {
423437
extensionDirs = fs.readdirSync(extensionsRoot, { withFileTypes: true });
424438
} catch {
439+
cachedBundledPluginPublicSurfaceAliasMaps.set(cacheKey, {});
425440
return {};
426441
}
427-
const orderedKinds = resolvePluginSdkAliasCandidateOrder({
428-
modulePath: params.modulePath,
429-
isProduction: process.env.NODE_ENV === "production",
430-
pluginSdkResolution: params.pluginSdkResolution,
431-
});
432-
const includePrivateQa = shouldIncludePrivateLocalOnlyPluginSdkSubpaths();
433442
const aliasMap: Record<string, string> = {};
434443
for (const entry of extensionDirs) {
435444
if (!entry.isDirectory()) {
@@ -458,6 +467,7 @@ function resolveBundledPluginPackagePublicSurfaceAliasMap(params: {
458467
aliasMap[`${packageName}/${basename}.js`] = normalizeJitiAliasTargetPath(target);
459468
}
460469
}
470+
cachedBundledPluginPublicSurfaceAliasMaps.set(cacheKey, aliasMap);
461471
return aliasMap;
462472
}
463473

0 commit comments

Comments
 (0)