diff --git a/CHANGELOG.md b/CHANGELOG.md index 4957e6a217d..d4beb5915c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -315,6 +315,7 @@ Docs: https://docs.openclaw.ai - Auth/device pairing: bound bootstrap handoff token issuance, redemption, and approved pairing baselines to the documented per-role scope allowlist, so bootstrap approvals cannot persistently grant `operator.admin`, `operator.pairing`, or `node.exec` scopes. Thanks @eleqtrizit. - Providers/GitHub Copilot: support the GUI/RPC wizard device-code auth flow so onboarding from non-TTY clients (gateway RPC bridge, GUI wizards) completes instead of returning empty profiles. Dangerous-state handling now distinguishes `access_denied` and `expired_token` from transport errors. (#73290) Thanks @indierawk2k2. - Installer/Linux: warn before switching an unwritable npm global prefix to `~/.npm-global`, then tell users to run future global updates with `npm i -g openclaw@latest` without `sudo` so npm keeps using the redirected user prefix. Fixes #44365; carries forward #50479. Thanks @Sayeem3051. +- Gateway/plugins: enable the native `require()` fast path on Windows for bundled plugin modules so plugin loading uses `require()` instead of Jiti's transform pipeline, reducing startup from ~39s to ~2s on typical 6-plugin setups. Fixes #68656. (#74173) Thanks @galiniliev. ## 2026.4.27 diff --git a/src/plugins/doctor-contract-registry.ts b/src/plugins/doctor-contract-registry.ts index 96c68b2fc8e..0b7af25cf36 100644 --- a/src/plugins/doctor-contract-registry.ts +++ b/src/plugins/doctor-contract-registry.ts @@ -48,7 +48,7 @@ function getJiti(modulePath: string) { } function loadPluginDoctorContractModule(modulePath: string): PluginDoctorContractModule { - const nativeModule = tryNativeRequireJavaScriptModule(modulePath); + const nativeModule = tryNativeRequireJavaScriptModule(modulePath, { allowWindows: true }); if (nativeModule.ok) { return nativeModule.moduleExport as PluginDoctorContractModule; } diff --git a/src/plugins/jiti-loader-cache.test.ts b/src/plugins/jiti-loader-cache.test.ts index d327d5fd56e..a1fa92e9111 100644 --- a/src/plugins/jiti-loader-cache.test.ts +++ b/src/plugins/jiti-loader-cache.test.ts @@ -208,13 +208,14 @@ describe("getCachedPluginJitiLoader", () => { const jitiLoader = vi.fn(); const createJiti = vi.fn(() => jitiLoader); vi.doMock("jiti", () => ({ createJiti })); + const nativeStub = vi.fn((target: string) => ({ + ok: true as const, + moduleExport: { loadedFrom: target }, + })); vi.doMock("./native-module-require.js", () => ({ isJavaScriptModulePath: (p: string) => p.endsWith(".js") || p.endsWith(".mjs") || p.endsWith(".cjs"), - tryNativeRequireJavaScriptModule: (target: string) => ({ - ok: true, - moduleExport: { loadedFrom: target }, - }), + tryNativeRequireJavaScriptModule: nativeStub, })); const { getCachedPluginJitiLoader } = await importFreshModule< typeof import("./jiti-loader-cache.js") @@ -233,6 +234,10 @@ describe("getCachedPluginJitiLoader", () => { // jiti is created eagerly, but its loader must NOT be invoked for .js // targets that `tryNativeRequireJavaScriptModule` resolves. expect(jitiLoader).not.toHaveBeenCalled(); + // allowWindows must be passed so the native fast path works on Windows too. + expect(nativeStub).toHaveBeenCalledWith("/repo/dist/extensions/demo/api.js", { + allowWindows: true, + }); }); it("falls back to jiti when the native-require helper declines", async () => { diff --git a/src/plugins/jiti-loader-cache.ts b/src/plugins/jiti-loader-cache.ts index d2fc3d6dbfd..90dc20b7498 100644 --- a/src/plugins/jiti-loader-cache.ts +++ b/src/plugins/jiti-loader-cache.ts @@ -105,7 +105,7 @@ export function getCachedPluginJitiLoader(params: { // async-module fallbacks `tryNativeRequireJavaScriptModule` declines to // handle. const loader = ((target: string, ...rest: unknown[]) => { - const native = tryNativeRequireJavaScriptModule(target); + const native = tryNativeRequireJavaScriptModule(target, { allowWindows: true }); if (native.ok) { return native.moduleExport; } diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index 4b1282d8f6a..9c8f6c57c6b 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -1316,6 +1316,8 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi let selectedMemoryPluginId: string | null = null; let memorySlotMatched = false; const dreamingEngineId = resolveDreamingSidecarEngineId({ cfg, memorySlot }); + const pluginLoadStartMs = performance.now(); + let pluginLoadAttemptCount = 0; for (const candidate of orderedCandidates) { const manifestRecord = manifestByRoot.get(candidate.rootDir); @@ -1702,6 +1704,8 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi // Track the plugin as imported once module evaluation begins. Top-level // code may have already executed even if evaluation later throws. recordImportedPluginId(record.id); + pluginLoadAttemptCount++; + logger.debug?.(`[plugins] loading ${record.id} from ${safeSource}`); mod = withProfile( { pluginId: record.id, source: safeSource }, registrationMode, @@ -2065,6 +2069,13 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi } } + const pluginLoadElapsedMs = performance.now() - pluginLoadStartMs; + if (pluginLoadAttemptCount > 0) { + logger.debug?.( + `[plugins] loaded ${registry.plugins.length} plugin(s) (${pluginLoadAttemptCount} attempted) in ${pluginLoadElapsedMs.toFixed(1)}ms`, + ); + } + // Scoped snapshot loads may intentionally omit the configured memory plugin, so only // emit the missing-memory diagnostic for full registry loads. if (!onlyPluginIdSet && typeof memorySlot === "string" && !memorySlotMatched) { diff --git a/src/plugins/public-surface-loader.test.ts b/src/plugins/public-surface-loader.test.ts index 4e9263ae94f..135fd374a0b 100644 --- a/src/plugins/public-surface-loader.test.ts +++ b/src/plugins/public-surface-loader.test.ts @@ -111,7 +111,7 @@ afterEach(() => { }); describe("bundled plugin public surface loader", () => { - it("uses transpiled Jiti import for Windows dist public artifact loads", async () => { + it("uses native Jiti import for Windows dist public artifact loads", async () => { const createJiti = vi.fn(() => vi.fn(() => ({ marker: "windows-dist-ok" }))); vi.doMock("jiti", () => ({ createJiti, @@ -140,7 +140,7 @@ describe("bundled plugin public surface loader", () => { expect(createJiti).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ - tryNative: false, + tryNative: true, }), ); } finally { diff --git a/src/plugins/sdk-alias.test.ts b/src/plugins/sdk-alias.test.ts index e115dbe9cd5..c72ab8f19af 100644 --- a/src/plugins/sdk-alias.test.ts +++ b/src/plugins/sdk-alias.test.ts @@ -952,7 +952,7 @@ describe("plugin sdk alias helpers", () => { } }); - it("disables native Jiti loads on Windows for built JavaScript entries", () => { + it("enables native Jiti loads on Windows for built JavaScript entries", () => { const originalPlatform = process.platform; Object.defineProperty(process, "platform", { configurable: true, @@ -960,9 +960,9 @@ describe("plugin sdk alias helpers", () => { }); try { - expect(shouldPreferNativeJiti("/repo/dist/plugins/runtime/index.js")).toBe(false); + expect(shouldPreferNativeJiti("/repo/dist/plugins/runtime/index.js")).toBe(true); expect(shouldPreferNativeJiti(`/repo/${bundledDistPluginFile("browser", "index.js")}`)).toBe( - false, + true, ); } finally { Object.defineProperty(process, "platform", { @@ -972,7 +972,7 @@ describe("plugin sdk alias helpers", () => { } }); - it("keeps plugin loader dist shortcuts on transpiled Jiti on Windows", () => { + it("keeps plugin loader dist shortcuts on native Jiti on Windows for JS entries", () => { const originalPlatform = process.platform; Object.defineProperty(process, "platform", { configurable: true, @@ -984,7 +984,7 @@ describe("plugin sdk alias helpers", () => { resolvePluginLoaderJitiTryNative(`/repo/${bundledDistPluginFile("browser", "index.js")}`, { preferBuiltDist: true, }), - ).toBe(false); + ).toBe(true); expect( resolvePluginLoaderJitiTryNative(`/repo/${bundledDistPluginFile("browser", "helper.ts")}`, { preferBuiltDist: true, diff --git a/src/plugins/sdk-alias.ts b/src/plugins/sdk-alias.ts index 212537d55f1..26b5aba0d06 100644 --- a/src/plugins/sdk-alias.ts +++ b/src/plugins/sdk-alias.ts @@ -695,7 +695,7 @@ export function buildPluginLoaderJitiOptions(aliasMap: Record) { function supportsNativeJitiRuntime(): boolean { const versions = process.versions as { bun?: string }; - return typeof versions.bun !== "string" && process.platform !== "win32"; + return typeof versions.bun !== "string"; } function isBundledPluginDistModulePath(modulePath: string): boolean { diff --git a/src/test-utils/jiti-runtime.ts b/src/test-utils/jiti-runtime.ts index f6051312b63..c11ad43be66 100644 --- a/src/test-utils/jiti-runtime.ts +++ b/src/test-utils/jiti-runtime.ts @@ -1,5 +1,3 @@ export function shouldExpectNativeJitiForJavaScriptTestRuntime(): boolean { - return ( - typeof (process.versions as { bun?: string }).bun !== "string" && process.platform !== "win32" - ); + return typeof (process.versions as { bun?: string }).bun !== "string"; }