Skip to content

Commit 86fc9e3

Browse files
committed
perf: trim gateway startup plugin imports
1 parent 3dcff3b commit 86fc9e3

4 files changed

Lines changed: 102 additions & 61 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Docs: https://docs.openclaw.ai
1111

1212
### Changes
1313

14-
- Gateway/startup: keep model-catalog test helpers and run-session lookup code out of the hot `server.impl` import graph, reducing default gateway benchmark readiness latency.
14+
- Gateway/startup: keep model-catalog test helpers, run-session lookup code, QR pairing helpers, and TypeBox memory-tool schema construction out of hot startup import paths, reducing default gateway benchmark plugin-load and memory pressure.
1515
- Channels/streaming: add unified `streaming.mode: "progress"` drafts with auto single-word status labels and shared progress configuration across Discord, Telegram, Matrix, Slack, and Microsoft Teams.
1616
- Slack/streaming: add `streaming.progress.render: "rich"` for Block Kit progress drafts backed by structured progress line data.
1717
- Slack/streaming: keep the newest rich progress lines when Block Kit limits trim long progress drafts. Thanks @vincentkoc.

extensions/device-pair/index.ts

Lines changed: 67 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,41 @@
11
import { rm } from "node:fs/promises";
22
import os from "node:os";
33
import path from "node:path";
4+
import { definePluginEntry, type OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
45
import {
56
normalizeLowercaseStringOrEmpty,
67
normalizeOptionalString,
78
} from "openclaw/plugin-sdk/text-runtime";
8-
import {
9-
clearDeviceBootstrapTokens,
10-
definePluginEntry,
11-
issueDeviceBootstrapToken,
12-
listDevicePairing,
13-
PAIRING_SETUP_BOOTSTRAP_PROFILE,
14-
renderQrPngDataUrl,
15-
writeQrPngTempFile,
16-
revokeDeviceBootstrapToken,
17-
resolveGatewayBindUrl,
18-
resolveGatewayPort,
19-
resolvePreferredOpenClawTmpDir,
20-
runPluginCommandWithTimeout,
21-
resolveTailnetHostWithRunner,
22-
type OpenClawPluginApi,
23-
} from "./api.js";
24-
import {
25-
armPairNotifyOnce,
26-
formatPendingRequests,
27-
handleNotifyCommand,
28-
registerPairingNotifierService,
29-
} from "./notify.js";
30-
import {
31-
approvePendingPairingRequest,
32-
selectPendingApprovalRequest,
33-
} from "./pair-command-approve.js";
34-
import {
35-
buildMissingPairingScopeReply,
36-
resolvePairingCommandAuthState,
37-
} from "./pair-command-auth.js";
9+
10+
type DevicePairApiModule = typeof import("./api.js");
11+
type NotifyModule = typeof import("./notify.js");
12+
type PairCommandApproveModule = typeof import("./pair-command-approve.js");
13+
type PairCommandAuthModule = typeof import("./pair-command-auth.js");
14+
15+
let devicePairApiModulePromise: Promise<DevicePairApiModule> | undefined;
16+
let notifyModulePromise: Promise<NotifyModule> | undefined;
17+
let pairCommandApproveModulePromise: Promise<PairCommandApproveModule> | undefined;
18+
let pairCommandAuthModulePromise: Promise<PairCommandAuthModule> | undefined;
19+
20+
function loadDevicePairApiModule(): Promise<DevicePairApiModule> {
21+
devicePairApiModulePromise ??= import("./api.js");
22+
return devicePairApiModulePromise;
23+
}
24+
25+
function loadNotifyModule(): Promise<NotifyModule> {
26+
notifyModulePromise ??= import("./notify.js");
27+
return notifyModulePromise;
28+
}
29+
30+
function loadPairCommandApproveModule(): Promise<PairCommandApproveModule> {
31+
pairCommandApproveModulePromise ??= import("./pair-command-approve.js");
32+
return pairCommandApproveModulePromise;
33+
}
34+
35+
function loadPairCommandAuthModule(): Promise<PairCommandAuthModule> {
36+
pairCommandAuthModulePromise ??= import("./pair-command-auth.js");
37+
return pairCommandAuthModulePromise;
38+
}
3839

3940
function formatDurationMinutes(expiresAtMs: number): string {
4041
const msRemaining = Math.max(0, expiresAtMs - Date.now());
@@ -254,6 +255,8 @@ function pickTailnetIPv4(): string | null {
254255
}
255256

256257
async function resolveTailnetHost(): Promise<string | null> {
258+
const { resolveTailnetHostWithRunner, runPluginCommandWithTimeout } =
259+
await loadDevicePairApiModule();
257260
return await resolveTailnetHostWithRunner((argv, opts) =>
258261
runPluginCommandWithTimeout({
259262
argv,
@@ -307,6 +310,7 @@ function resolveRequiredAuthLabel(
307310
}
308311

309312
async function resolveGatewayUrl(api: OpenClawPluginApi): Promise<ResolveUrlResult> {
313+
const { resolveGatewayBindUrl, resolveGatewayPort } = await loadDevicePairApiModule();
310314
const cfg = api.config;
311315
const pluginCfg = (api.pluginConfig ?? {}) as DevicePairPluginConfig;
312316
const scheme = resolveScheme(cfg);
@@ -511,6 +515,8 @@ function issuesPairSetupCode(action: string): boolean {
511515
}
512516

513517
async function issueSetupPayload(url: string): Promise<SetupPayload> {
518+
const { issueDeviceBootstrapToken, PAIRING_SETUP_BOOTSTRAP_PROFILE } =
519+
await loadDevicePairApiModule();
514520
const issuedBootstrap = await issueDeviceBootstrapToken({
515521
profile: PAIRING_SETUP_BOOTSTRAP_PROFILE,
516522
});
@@ -558,7 +564,19 @@ export default definePluginEntry({
558564
name: "Device Pair",
559565
description: "QR/bootstrap pairing helpers for OpenClaw devices",
560566
register(api: OpenClawPluginApi) {
561-
registerPairingNotifierService(api);
567+
let notifierService: ReturnType<NotifyModule["createPairingNotifierService"]> | undefined;
568+
api.registerService({
569+
id: "device-pair-notifier",
570+
start: async (ctx) => {
571+
const { createPairingNotifierService } = await loadNotifyModule();
572+
notifierService = createPairingNotifierService(api);
573+
await notifierService.start(ctx);
574+
},
575+
stop: async (ctx) => {
576+
await notifierService?.stop?.(ctx);
577+
notifierService = undefined;
578+
},
579+
});
562580

563581
api.registerCommand({
564582
name: "pair",
@@ -571,6 +589,8 @@ export default definePluginEntry({
571589
const gatewayClientScopes = Array.isArray(ctx.gatewayClientScopes)
572590
? ctx.gatewayClientScopes
573591
: undefined;
592+
const { buildMissingPairingScopeReply, resolvePairingCommandAuthState } =
593+
await loadPairCommandAuthModule();
574594
const authState = resolvePairingCommandAuthState({
575595
channel: ctx.channel,
576596
gatewayClientScopes,
@@ -582,12 +602,17 @@ export default definePluginEntry({
582602
);
583603

584604
if (action === "status" || action === "pending") {
605+
const [{ listDevicePairing }, { formatPendingRequests }] = await Promise.all([
606+
loadDevicePairApiModule(),
607+
loadNotifyModule(),
608+
]);
585609
const list = await listDevicePairing();
586610
return { text: formatPendingRequests(list.pending) };
587611
}
588612

589613
if (action === "notify") {
590614
const notifyAction = normalizeLowercaseStringOrEmpty(tokens[1]) || "status";
615+
const { handleNotifyCommand } = await loadNotifyModule();
591616
return await handleNotifyCommand({
592617
api,
593618
ctx,
@@ -599,6 +624,10 @@ export default definePluginEntry({
599624
if (authState.isMissingInternalPairingPrivilege) {
600625
return buildMissingPairingScopeReply();
601626
}
627+
const [
628+
{ listDevicePairing },
629+
{ approvePendingPairingRequest, selectPendingApprovalRequest },
630+
] = await Promise.all([loadDevicePairApiModule(), loadPairCommandApproveModule()]);
602631
const list = await listDevicePairing();
603632
const selected = selectPendingApprovalRequest({
604633
pending: list.pending,
@@ -621,6 +650,7 @@ export default definePluginEntry({
621650
if (authState.isMissingInternalPairingPrivilege) {
622651
return buildMissingPairingScopeReply();
623652
}
653+
const { clearDeviceBootstrapTokens } = await loadDevicePairApiModule();
624654
const cleared = await clearDeviceBootstrapTokens();
625655
return {
626656
text:
@@ -651,6 +681,7 @@ export default definePluginEntry({
651681

652682
if (channel === "telegram" && target) {
653683
try {
684+
const { armPairNotifyOnce } = await loadNotifyModule();
654685
autoNotifyArmed = await armPairNotifyOnce({ api, ctx });
655686
} catch (err) {
656687
api.logger.warn?.(
@@ -672,6 +703,8 @@ export default definePluginEntry({
672703
if (target && canSendQrPngToChannel(channel)) {
673704
let qrFilePath: string | undefined;
674705
try {
706+
const { resolvePreferredOpenClawTmpDir, writeQrPngTempFile } =
707+
await loadDevicePairApiModule();
675708
qrFilePath = (
676709
await writeQrPngTempFile(setupCode, {
677710
tmpRoot: resolvePreferredOpenClawTmpDir(),
@@ -697,6 +730,7 @@ export default definePluginEntry({
697730
};
698731
}
699732
} catch (err) {
733+
const { revokeDeviceBootstrapToken } = await loadDevicePairApiModule();
700734
api.logger.warn?.(
701735
`device-pair: QR image send failed channel=${channel}, falling back (${(err as Error)?.message ?? err})`,
702736
);
@@ -716,8 +750,10 @@ export default definePluginEntry({
716750
if (channel === "webchat") {
717751
let qrDataUrl: string;
718752
try {
753+
const { renderQrPngDataUrl } = await loadDevicePairApiModule();
719754
qrDataUrl = await renderQrPngDataUrl(setupCode);
720755
} catch (err) {
756+
const { revokeDeviceBootstrapToken } = await loadDevicePairApiModule();
721757
api.logger.warn?.(
722758
`device-pair: webchat QR render failed, falling back (${(err as Error)?.message ?? err})`,
723759
);

extensions/device-pair/notify.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { promises as fs } from "node:fs";
22
import path from "node:path";
3+
import type { OpenClawPluginService } from "openclaw/plugin-sdk/core";
4+
import { listDevicePairing } from "openclaw/plugin-sdk/device-bootstrap";
35
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
6+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
47
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
5-
import type { OpenClawPluginApi } from "./api.js";
6-
import { listDevicePairing } from "./api.js";
78

89
const NOTIFY_STATE_FILE = "device-pair-notify.json";
910
const NOTIFY_POLL_INTERVAL_MS = 10_000;
@@ -488,10 +489,10 @@ export async function handleNotifyCommand(params: {
488489
return { text: "Usage: /pair notify on|off|once|status" };
489490
}
490491

491-
export function registerPairingNotifierService(api: OpenClawPluginApi): void {
492+
export function createPairingNotifierService(api: OpenClawPluginApi): OpenClawPluginService {
492493
let notifyInterval: ReturnType<typeof setInterval> | null = null;
493494

494-
api.registerService({
495+
return {
495496
id: "device-pair-notifier",
496497
start: async (ctx) => {
497498
const statePath = resolveNotifyStatePath(ctx.stateDir);
@@ -502,7 +503,6 @@ export function registerPairingNotifierService(api: OpenClawPluginApi): void {
502503
await tick().catch((err) => {
503504
api.logger.warn(`device-pair: initial notify poll failed: ${formatErrorMessage(err)}`);
504505
});
505-
506506
notifyInterval = setInterval(() => {
507507
tick().catch((err) => {
508508
api.logger.warn(`device-pair: notify poll failed: ${formatErrorMessage(err)}`);
@@ -516,5 +516,9 @@ export function registerPairingNotifierService(api: OpenClawPluginApi): void {
516516
notifyInterval = null;
517517
}
518518
},
519-
});
519+
};
520+
}
521+
522+
export function registerPairingNotifierService(api: OpenClawPluginApi): void {
523+
api.registerService(createPairingNotifierService(api));
520524
}

extensions/memory-core/index.ts

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
type AnyAgentTool,
1212
type OpenClawPluginToolContext,
1313
} from "openclaw/plugin-sdk/plugin-entry";
14-
import { Type } from "typebox";
14+
import type { TSchema } from "typebox";
1515
import { registerShortTermPromotionDreaming } from "./src/dreaming.js";
1616
import { buildMemoryFlushPlan } from "./src/flush-plan.js";
1717
import { registerBuiltInMemoryEmbeddingProviders } from "./src/memory/provider-adapters.js";
@@ -58,28 +58,29 @@ function hasMemoryToolContext(options: MemoryToolOptions): boolean {
5858
return Boolean(resolveMemorySearchConfig(cfg, agentId));
5959
}
6060

61-
const MemorySearchSchema = Type.Object({
62-
query: Type.String(),
63-
maxResults: Type.Optional(Type.Number()),
64-
minScore: Type.Optional(Type.Number()),
65-
corpus: Type.Optional(
66-
Type.Union([
67-
Type.Literal("memory"),
68-
Type.Literal("wiki"),
69-
Type.Literal("all"),
70-
Type.Literal("sessions"),
71-
]),
72-
),
73-
});
74-
75-
const MemoryGetSchema = Type.Object({
76-
path: Type.String(),
77-
from: Type.Optional(Type.Number()),
78-
lines: Type.Optional(Type.Number()),
79-
corpus: Type.Optional(
80-
Type.Union([Type.Literal("memory"), Type.Literal("wiki"), Type.Literal("all")]),
81-
),
82-
});
61+
const MemorySearchSchema = {
62+
type: "object",
63+
properties: {
64+
query: { type: "string" },
65+
maxResults: { type: "number" },
66+
minScore: { type: "number" },
67+
corpus: { type: "string", enum: ["memory", "wiki", "all", "sessions"] },
68+
},
69+
required: ["query"],
70+
additionalProperties: false,
71+
} as const satisfies TSchema;
72+
73+
const MemoryGetSchema = {
74+
type: "object",
75+
properties: {
76+
path: { type: "string" },
77+
from: { type: "number" },
78+
lines: { type: "number" },
79+
corpus: { type: "string", enum: ["memory", "wiki", "all"] },
80+
},
81+
required: ["path"],
82+
additionalProperties: false,
83+
} as const satisfies TSchema;
8384

8485
function createLazyMemoryTool(params: {
8586
options: MemoryToolOptions;

0 commit comments

Comments
 (0)