Skip to content

Commit 561ba3b

Browse files
authored
Merge branch 'main' into vincentkoc-code/lazy-noninteractive-plugin-provider-runtime
2 parents 16303be + 42837a0 commit 561ba3b

20 files changed

+408
-73
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ Docs: https://docs.openclaw.ai
6363
- CLI/onboarding: import static provider definitions directly for onboarding model/config helpers so those paths no longer pull provider discovery just for built-in defaults. (#47467) Thanks @vincentkoc.
6464
- CLI/auth choice: lazy-load plugin/provider fallback resolution so mapped auth choices stay on the static path and only unknown choices pay the heavy provider load. (#47495) Thanks @vincentkoc.
6565
- CLI/completion: reduce recursive completion-script string churn and fix nested PowerShell command-path matching so generated nested completions resolve on PowerShell too. (#45537) Thanks @yiShanXin and @vincentkoc.
66+
- Gateway/startup: load bundled channel plugins from compiled `dist/extensions` entries in built installs, so gateway boot no longer recompiles bundled extension TypeScript on every startup and WhatsApp-class cold starts drop back to seconds instead of tens of seconds or worse.
6667

6768
## 2026.3.13
6869

extensions/llm-task/src/llm-task-tool.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, it, expect, vi, beforeEach } from "vitest";
22

3-
vi.mock("../../../src/agents/pi-embedded-runner.js", () => {
3+
vi.mock("openclaw/extension-api", () => {
44
return {
55
runEmbeddedPiAgent: vi.fn(async () => ({
66
meta: { startedAt: Date.now() },
@@ -9,7 +9,7 @@ vi.mock("../../../src/agents/pi-embedded-runner.js", () => {
99
};
1010
});
1111

12-
import { runEmbeddedPiAgent } from "../../../src/agents/pi-embedded-runner.js";
12+
import { runEmbeddedPiAgent } from "openclaw/extension-api";
1313
import { createLlmTaskTool } from "./llm-task-tool.js";
1414

1515
// oxlint-disable-next-line typescript/no-explicit-any

extensions/llm-task/src/llm-task-tool.ts

Lines changed: 1 addition & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,46 +2,16 @@ import fs from "node:fs/promises";
22
import path from "node:path";
33
import { Type } from "@sinclair/typebox";
44
import Ajv from "ajv";
5+
import { runEmbeddedPiAgent } from "openclaw/extension-api";
56
import {
67
formatThinkingLevels,
78
formatXHighModelHint,
89
normalizeThinkLevel,
910
resolvePreferredOpenClawTmpDir,
1011
supportsXHighThinking,
1112
} from "openclaw/plugin-sdk/llm-task";
12-
// NOTE: This extension is intended to be bundled with OpenClaw.
13-
// When running from source (tests/dev), OpenClaw internals live under src/.
14-
// When running from a built install, internals live under dist/ (no src/ tree).
15-
// So we resolve internal imports dynamically with src-first, dist-fallback.
1613
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/llm-task";
1714

18-
type RunEmbeddedPiAgentFn = (params: Record<string, unknown>) => Promise<unknown>;
19-
20-
async function loadRunEmbeddedPiAgent(): Promise<RunEmbeddedPiAgentFn> {
21-
// Source checkout (tests/dev)
22-
try {
23-
const mod = await import("../../../src/agents/pi-embedded-runner.js");
24-
// oxlint-disable-next-line typescript/no-explicit-any
25-
if (typeof (mod as any).runEmbeddedPiAgent === "function") {
26-
// oxlint-disable-next-line typescript/no-explicit-any
27-
return (mod as any).runEmbeddedPiAgent;
28-
}
29-
} catch {
30-
// ignore
31-
}
32-
33-
// Bundled install (built)
34-
// NOTE: there is no src/ tree in a packaged install. Prefer a stable internal entrypoint.
35-
const distExtensionApi = "../../../dist/extensionAPI.js";
36-
const mod = (await import(distExtensionApi)) as { runEmbeddedPiAgent?: unknown };
37-
// oxlint-disable-next-line typescript/no-explicit-any
38-
const fn = (mod as any).runEmbeddedPiAgent;
39-
if (typeof fn !== "function") {
40-
throw new Error("Internal error: runEmbeddedPiAgent not available");
41-
}
42-
return fn as RunEmbeddedPiAgentFn;
43-
}
44-
4515
function stripCodeFences(s: string): string {
4616
const trimmed = s.trim();
4717
const m = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
@@ -209,8 +179,6 @@ export function createLlmTaskTool(api: OpenClawPluginApi) {
209179
const sessionId = `llm-task-${Date.now()}`;
210180
const sessionFile = path.join(tmpDir, "session.json");
211181

212-
const runEmbeddedPiAgent = await loadRunEmbeddedPiAgent();
213-
214182
const result = await runEmbeddedPiAgent({
215183
sessionId,
216184
sessionFile,

extensions/whatsapp/src/channel.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import {
2+
applyAccountNameToChannelSection,
3+
buildChannelConfigSchema,
24
buildAccountScopedDmSecurityPolicy,
35
collectAllowlistProviderGroupPolicyWarnings,
46
collectOpenGroupPolicyRouteAllowlistWarnings,
5-
} from "openclaw/plugin-sdk/compat";
6-
import {
7-
applyAccountNameToChannelSection,
8-
buildChannelConfigSchema,
97
createActionGate,
108
createWhatsAppOutboundBase,
119
DEFAULT_ACCOUNT_ID,

extensions/whatsapp/src/runtime.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/compat";
2-
import type { PluginRuntime } from "openclaw/plugin-sdk/whatsapp";
1+
import { createPluginRuntimeStore, type PluginRuntime } from "openclaw/plugin-sdk/whatsapp";
32

43
const { setRuntime: setWhatsAppRuntime, getRuntime: getWhatsAppRuntime } =
54
createPluginRuntimeStore<PluginRuntime>("WhatsApp runtime not initialized");

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@
212212
"types": "./dist/plugin-sdk/keyed-async-queue.d.ts",
213213
"default": "./dist/plugin-sdk/keyed-async-queue.js"
214214
},
215+
"./extension-api": "./dist/extensionAPI.js",
215216
"./cli-entry": "./openclaw.mjs"
216217
},
217218
"scripts": {
@@ -224,8 +225,8 @@
224225
"android:run": "cd apps/android && ./gradlew :app:installDebug && adb shell am start -n ai.openclaw.app/.MainActivity",
225226
"android:test": "cd apps/android && ./gradlew :app:testDebugUnitTest",
226227
"android:test:integration": "OPENCLAW_LIVE_TEST=1 OPENCLAW_LIVE_ANDROID_NODE=1 vitest run --config vitest.live.config.ts src/gateway/android-node.capabilities.live.test.ts",
227-
"build": "pnpm canvas:a2ui:bundle && node scripts/tsdown-build.mjs && node scripts/copy-plugin-sdk-root-alias.mjs && pnpm build:plugin-sdk:dts && node --import tsx scripts/write-plugin-sdk-entry-dts.ts && node --import tsx scripts/canvas-a2ui-copy.ts && node --import tsx scripts/copy-hook-metadata.ts && node --import tsx scripts/copy-export-html-templates.ts && node --import tsx scripts/write-build-info.ts && node --import tsx scripts/write-cli-startup-metadata.ts && node --import tsx scripts/write-cli-compat.ts",
228-
"build:docker": "node scripts/tsdown-build.mjs && node scripts/copy-plugin-sdk-root-alias.mjs && pnpm build:plugin-sdk:dts && node --import tsx scripts/write-plugin-sdk-entry-dts.ts && node --import tsx scripts/canvas-a2ui-copy.ts && node --import tsx scripts/copy-hook-metadata.ts && node --import tsx scripts/copy-export-html-templates.ts && node --import tsx scripts/write-build-info.ts && node --import tsx scripts/write-cli-startup-metadata.ts && node --import tsx scripts/write-cli-compat.ts",
228+
"build": "pnpm canvas:a2ui:bundle && node scripts/tsdown-build.mjs && node scripts/copy-plugin-sdk-root-alias.mjs && node scripts/copy-bundled-plugin-metadata.mjs && pnpm build:plugin-sdk:dts && node --import tsx scripts/write-plugin-sdk-entry-dts.ts && node --import tsx scripts/canvas-a2ui-copy.ts && node --import tsx scripts/copy-hook-metadata.ts && node --import tsx scripts/copy-export-html-templates.ts && node --import tsx scripts/write-build-info.ts && node --import tsx scripts/write-cli-startup-metadata.ts && node --import tsx scripts/write-cli-compat.ts",
229+
"build:docker": "node scripts/tsdown-build.mjs && node scripts/copy-plugin-sdk-root-alias.mjs && node scripts/copy-bundled-plugin-metadata.mjs && pnpm build:plugin-sdk:dts && node --import tsx scripts/write-plugin-sdk-entry-dts.ts && node --import tsx scripts/canvas-a2ui-copy.ts && node --import tsx scripts/copy-hook-metadata.ts && node --import tsx scripts/copy-export-html-templates.ts && node --import tsx scripts/write-build-info.ts && node --import tsx scripts/write-cli-startup-metadata.ts && node --import tsx scripts/write-cli-compat.ts",
229230
"build:plugin-sdk:dts": "tsc -p tsconfig.plugin-sdk.dts.json || true",
230231
"build:strict-smoke": "pnpm canvas:a2ui:bundle && node scripts/tsdown-build.mjs && node scripts/copy-plugin-sdk-root-alias.mjs && pnpm build:plugin-sdk:dts",
231232
"canvas:a2ui:bundle": "bash scripts/bundle-a2ui.sh",
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#!/usr/bin/env node
2+
3+
import fs from "node:fs";
4+
import path from "node:path";
5+
6+
const repoRoot = process.cwd();
7+
const extensionsRoot = path.join(repoRoot, "extensions");
8+
const distExtensionsRoot = path.join(repoRoot, "dist", "extensions");
9+
10+
function rewritePackageExtensions(entries) {
11+
if (!Array.isArray(entries)) {
12+
return undefined;
13+
}
14+
15+
return entries
16+
.filter((entry) => typeof entry === "string" && entry.trim().length > 0)
17+
.map((entry) => {
18+
const normalized = entry.replace(/^\.\//, "");
19+
const rewritten = normalized.replace(/\.[^.]+$/u, ".js");
20+
return `./${rewritten}`;
21+
});
22+
}
23+
24+
for (const dirent of fs.readdirSync(extensionsRoot, { withFileTypes: true })) {
25+
if (!dirent.isDirectory()) {
26+
continue;
27+
}
28+
29+
const pluginDir = path.join(extensionsRoot, dirent.name);
30+
const manifestPath = path.join(pluginDir, "openclaw.plugin.json");
31+
if (!fs.existsSync(manifestPath)) {
32+
continue;
33+
}
34+
35+
const distPluginDir = path.join(distExtensionsRoot, dirent.name);
36+
fs.mkdirSync(distPluginDir, { recursive: true });
37+
fs.copyFileSync(manifestPath, path.join(distPluginDir, "openclaw.plugin.json"));
38+
39+
const packageJsonPath = path.join(pluginDir, "package.json");
40+
if (!fs.existsSync(packageJsonPath)) {
41+
continue;
42+
}
43+
44+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
45+
if (packageJson.openclaw && "extensions" in packageJson.openclaw) {
46+
packageJson.openclaw = {
47+
...packageJson.openclaw,
48+
extensions: rewritePackageExtensions(packageJson.openclaw.extensions),
49+
};
50+
}
51+
52+
fs.writeFileSync(
53+
path.join(distPluginDir, "package.json"),
54+
`${JSON.stringify(packageJson, null, 2)}\n`,
55+
"utf8",
56+
);
57+
}

src/agents/model-compat.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,17 @@ describe("normalizeModelCompat", () => {
295295
expect(supportsUsageInStreaming(normalized)).toBe(true);
296296
});
297297

298+
it("preserves explicit supportsUsageInStreaming false on non-native endpoints", () => {
299+
const model = {
300+
...baseModel(),
301+
provider: "custom-cpa",
302+
baseUrl: "https://proxy.example.com/v1",
303+
compat: { supportsUsageInStreaming: false },
304+
};
305+
const normalized = normalizeModelCompat(model);
306+
expect(supportsUsageInStreaming(normalized)).toBe(false);
307+
});
308+
298309
it("still forces flags off when not explicitly set by user", () => {
299310
const model = {
300311
...baseModel(),
@@ -348,6 +359,32 @@ describe("normalizeModelCompat", () => {
348359
expect(supportsUsageInStreaming(normalized)).toBe(false);
349360
expect(supportsStrictMode(normalized)).toBe(false);
350361
});
362+
363+
it("leaves fully explicit non-native compat untouched", () => {
364+
const model = baseModel();
365+
model.baseUrl = "https://proxy.example.com/v1";
366+
model.compat = {
367+
supportsDeveloperRole: false,
368+
supportsUsageInStreaming: true,
369+
supportsStrictMode: true,
370+
};
371+
const normalized = normalizeModelCompat(model);
372+
expect(normalized).toBe(model);
373+
});
374+
375+
it("preserves explicit usage compat when developer role is explicitly enabled", () => {
376+
const model = baseModel();
377+
model.baseUrl = "https://proxy.example.com/v1";
378+
model.compat = {
379+
supportsDeveloperRole: true,
380+
supportsUsageInStreaming: true,
381+
supportsStrictMode: true,
382+
};
383+
const normalized = normalizeModelCompat(model);
384+
expect(supportsDeveloperRole(normalized)).toBe(true);
385+
expect(supportsUsageInStreaming(normalized)).toBe(true);
386+
expect(supportsStrictMode(normalized)).toBe(true);
387+
});
351388
});
352389

353390
describe("isModernModelRef", () => {

src/agents/model-compat.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,11 @@ export function normalizeModelCompat(model: Model<Api>): Model<Api> {
6666
return model;
6767
}
6868
const forcedDeveloperRole = compat?.supportsDeveloperRole === true;
69-
const forcedUsageStreaming = compat?.supportsUsageInStreaming === true;
69+
const hasStreamingUsageOverride = compat?.supportsUsageInStreaming !== undefined;
7070
const targetStrictMode = compat?.supportsStrictMode ?? false;
7171
if (
7272
compat?.supportsDeveloperRole !== undefined &&
73-
compat?.supportsUsageInStreaming !== undefined &&
73+
hasStreamingUsageOverride &&
7474
compat?.supportsStrictMode !== undefined
7575
) {
7676
return model;
@@ -83,7 +83,7 @@ export function normalizeModelCompat(model: Model<Api>): Model<Api> {
8383
? {
8484
...compat,
8585
supportsDeveloperRole: forcedDeveloperRole || false,
86-
supportsUsageInStreaming: forcedUsageStreaming || false,
86+
...(hasStreamingUsageOverride ? {} : { supportsUsageInStreaming: false }),
8787
supportsStrictMode: targetStrictMode,
8888
}
8989
: {

src/agents/models-config.plan.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
type ExistingProviderConfig,
77
} from "./models-config.merge.js";
88
import {
9+
applyNativeStreamingUsageCompat,
910
enforceSourceManagedProviderSecrets,
1011
normalizeProviders,
1112
resolveImplicitProviders,
@@ -126,7 +127,8 @@ export async function planOpenClawModelsJson(params: {
126127
sourceSecretDefaults: params.sourceConfigForSecrets?.secrets?.defaults,
127128
secretRefManagedProviders,
128129
}) ?? mergedProviders;
129-
const nextContents = `${JSON.stringify({ providers: secretEnforcedProviders }, null, 2)}\n`;
130+
const finalProviders = applyNativeStreamingUsageCompat(secretEnforcedProviders);
131+
const nextContents = `${JSON.stringify({ providers: finalProviders }, null, 2)}\n`;
130132

131133
if (params.existingRaw === nextContents) {
132134
return { action: "noop" };

0 commit comments

Comments
 (0)