Skip to content

Commit 12f8227

Browse files
committed
perf: cache stable gateway metadata
1 parent fc3c979 commit 12f8227

10 files changed

Lines changed: 400 additions & 56 deletions

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: reuse process-stable channel catalog reads, avoid repeated bundled-channel boundary checks, and rotate gateway watch CPU profiles so benchmark runs do not accumulate unbounded artifacts.
10+
- Gateway/perf: cache stable install-record, channel-catalog, bundled-channel, and Telegram session-store metadata during process-local hot paths to reduce repeated JSON and manifest reads.
1011
- Gateway/perf: reuse immutable plugin metadata snapshots across startup, config, model, channel, setup, and secret metadata readers so hot paths avoid repeated plugin file stats and manifest registry reloads.
1112
- Talk/realtime: let WebUI and Discord voice callers ask for active OpenClaw run status, cancel, steer, or queue follow-up work while a consult is still running. (#84231) Thanks @Solvely-Colin.
1213
- 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.

extensions/telegram/src/bot-message-dispatch.ts

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -235,23 +235,48 @@ type DispatchTelegramMessageParams = {
235235
type TelegramReasoningLevel = "off" | "on" | "stream";
236236

237237
type TelegramTranscriptMirrorPayload = { text?: string; mediaUrls?: string[] };
238+
type TelegramSessionStore = ReturnType<typeof loadSessionStore>;
239+
type FreshTelegramSessionStoreLoader = ((agentId: string) => {
240+
storePath: string;
241+
store: TelegramSessionStore;
242+
}) & {
243+
clear: () => void;
244+
};
245+
246+
function createFreshTelegramSessionStoreLoader(params: {
247+
cfg: OpenClawConfig;
248+
telegramDeps: TelegramBotDeps;
249+
}): FreshTelegramSessionStoreLoader {
250+
const storesByPath = new Map<string, TelegramSessionStore>();
251+
const load = ((agentId: string) => {
252+
const storePath = params.telegramDeps.resolveStorePath(params.cfg.session?.store, { agentId });
253+
const cachedStore = storesByPath.get(storePath);
254+
if (cachedStore) {
255+
return { storePath, store: cachedStore };
256+
}
257+
const store = (params.telegramDeps.loadSessionStore ?? loadSessionStore)(storePath, {
258+
skipCache: true,
259+
});
260+
storesByPath.set(storePath, store);
261+
return { storePath, store };
262+
}) as FreshTelegramSessionStoreLoader;
263+
load.clear = () => storesByPath.clear();
264+
return load;
265+
}
238266

239267
function resolveTelegramReasoningLevel(params: {
240268
cfg: OpenClawConfig;
241269
sessionKey?: string;
242270
agentId: string;
243-
telegramDeps: TelegramBotDeps;
271+
loadFreshSessionStore: FreshTelegramSessionStoreLoader;
244272
}): TelegramReasoningLevel {
245-
const { cfg, sessionKey, agentId, telegramDeps } = params;
273+
const { cfg, sessionKey, agentId } = params;
246274
const configDefault = resolveTelegramConfigReasoningDefault(cfg, agentId);
247275
if (!sessionKey) {
248276
return configDefault;
249277
}
250278
try {
251-
const storePath = telegramDeps.resolveStorePath(cfg.session?.store, { agentId });
252-
const store = (telegramDeps.loadSessionStore ?? loadSessionStore)(storePath, {
253-
skipCache: true,
254-
});
279+
const { store } = params.loadFreshSessionStore(agentId);
255280
const entry = resolveSessionStoreEntry({ store, sessionKey }).existing;
256281
const level = entry?.reasoningLevel;
257282
if (level === "on" || level === "stream" || level === "off") {
@@ -285,19 +310,14 @@ async function mirrorTelegramAssistantReplyToTranscript(params: {
285310
cfg: OpenClawConfig;
286311
route: TelegramMessageContext["route"];
287312
sessionKey: string;
288-
telegramDeps: TelegramBotDeps;
313+
loadFreshSessionStore: FreshTelegramSessionStoreLoader;
289314
payload: TelegramTranscriptMirrorPayload;
290315
}) {
291316
const text = resolveTelegramMirroredTranscriptText(params.payload);
292317
if (!text) {
293318
return;
294319
}
295-
const storePath = params.telegramDeps.resolveStorePath(params.cfg.session?.store, {
296-
agentId: params.route.agentId,
297-
});
298-
const store = (params.telegramDeps.loadSessionStore ?? loadSessionStore)(storePath, {
299-
skipCache: true,
300-
});
320+
const { storePath, store } = params.loadFreshSessionStore(params.route.agentId);
301321
const sessionEntry = resolveSessionStoreEntry({
302322
store,
303323
sessionKey: params.sessionKey,
@@ -384,6 +404,7 @@ export const dispatchTelegramMessage = async ({
384404
const dispatchStartedAt = Date.now();
385405
const telegramDeps =
386406
injectedTelegramDeps ?? (await import("./bot-deps.js")).defaultTelegramBotDeps;
407+
const loadFreshSessionStore = createFreshTelegramSessionStoreLoader({ cfg, telegramDeps });
387408
const {
388409
ctxPayload,
389410
msg,
@@ -499,7 +520,7 @@ export const dispatchTelegramMessage = async ({
499520
cfg,
500521
sessionKey: ctxPayload.SessionKey,
501522
agentId: route.agentId,
502-
telegramDeps,
523+
loadFreshSessionStore,
503524
});
504525
const forceBlockStreamingForReasoning = resolvedReasoningLevel === "on";
505526
const streamReasoningDraft = resolvedReasoningLevel === "stream";
@@ -960,12 +981,7 @@ export const dispatchTelegramMessage = async ({
960981
return undefined;
961982
}
962983
try {
963-
const storePath = telegramDeps.resolveStorePath(cfg.session?.store, {
964-
agentId: route.agentId,
965-
});
966-
const store = (telegramDeps.loadSessionStore ?? loadSessionStore)(storePath, {
967-
skipCache: true,
968-
});
984+
const { storePath, store } = loadFreshSessionStore(route.agentId);
969985
const sessionEntry = resolveSessionStoreEntry({
970986
store,
971987
sessionKey,
@@ -1020,7 +1036,7 @@ export const dispatchTelegramMessage = async ({
10201036
cfg,
10211037
route,
10221038
sessionKey,
1023-
telegramDeps,
1039+
loadFreshSessionStore,
10241040
payload,
10251041
});
10261042
}
@@ -1285,12 +1301,7 @@ export const dispatchTelegramMessage = async ({
12851301

12861302
if (isDmTopic) {
12871303
try {
1288-
const storePath = telegramDeps.resolveStorePath(cfg.session?.store, {
1289-
agentId: route.agentId,
1290-
});
1291-
const store = (telegramDeps.loadSessionStore ?? loadSessionStore)(storePath, {
1292-
skipCache: true,
1293-
});
1304+
const { store } = loadFreshSessionStore(route.agentId);
12941305
const sessionKey = ctxPayload.SessionKey;
12951306
if (sessionKey) {
12961307
const entry = resolveSessionStoreEntry({ store, sessionKey }).existing;
@@ -1302,6 +1313,7 @@ export const dispatchTelegramMessage = async ({
13021313
logVerbose(`auto-topic-label: session store error: ${formatErrorMessage(err)}`);
13031314
}
13041315
}
1316+
loadFreshSessionStore.clear();
13051317

13061318
if (statusReactionController && !isRoomEvent) {
13071319
void statusReactionController.setThinking();

src/channels/plugins/bundled.ts

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ type BundledChannelLoadContext = {
9090
ChannelId,
9191
NonNullable<ChannelPlugin["config"]["inspectAccount"]> | null
9292
>;
93+
metadataById: Map<ChannelId, BundledChannelPluginMetadata | null>;
94+
metadataLoaded: boolean;
9395
};
9496

9597
const log = createSubsystemLogger("channels");
@@ -373,6 +375,8 @@ function createBundledChannelLoadContext(): BundledChannelLoadContext {
373375
lazySecretsById: new Map(),
374376
lazySetupSecretsById: new Map(),
375377
lazyAccountInspectorsById: new Map(),
378+
metadataById: new Map(),
379+
metadataLoaded: false,
376380
};
377381
}
378382

@@ -502,19 +506,39 @@ export function hasBundledChannelPackageSetupFeature(
502506
id: ChannelId,
503507
feature: BundledChannelPackageSetupFeature,
504508
): boolean {
505-
const rootScope = resolveBundledChannelRootScope();
509+
const { rootScope, loadContext } = resolveActiveBundledChannelLoadScope();
506510
return (
507-
resolveBundledChannelMetadata(id, rootScope)?.packageManifest?.setupFeatures?.[feature] === true
511+
resolveBundledChannelMetadata(id, rootScope, loadContext)?.packageManifest?.setupFeatures?.[
512+
feature
513+
] === true
508514
);
509515
}
510516

511517
function resolveBundledChannelMetadata(
512518
id: ChannelId,
513519
rootScope: BundledChannelRootScope,
520+
loadContext: BundledChannelLoadContext,
514521
): BundledChannelPluginMetadata | undefined {
515-
return listBundledChannelMetadata(rootScope).find(
516-
(metadata) => metadata.manifest.id === id || metadata.manifest.channels?.includes(id),
517-
);
522+
if (loadContext.metadataById.has(id)) {
523+
return loadContext.metadataById.get(id) ?? undefined;
524+
}
525+
if (loadContext.metadataLoaded) {
526+
loadContext.metadataById.set(id, null);
527+
return undefined;
528+
}
529+
for (const metadata of listBundledChannelMetadata(rootScope)) {
530+
const ids = new Set<ChannelId>([metadata.manifest.id, ...(metadata.manifest.channels ?? [])]);
531+
for (const metadataId of ids) {
532+
loadContext.metadataById.set(metadataId, metadata);
533+
}
534+
}
535+
loadContext.metadataLoaded = true;
536+
const metadata = loadContext.metadataById.get(id);
537+
if (metadata) {
538+
return metadata;
539+
}
540+
loadContext.metadataById.set(id, null);
541+
return undefined;
518542
}
519543

520544
function getLazyGeneratedBundledChannelEntryForRoot(
@@ -529,7 +553,7 @@ function getLazyGeneratedBundledChannelEntryForRoot(
529553
if (previous === null) {
530554
return null;
531555
}
532-
const metadata = resolveBundledChannelMetadata(id, rootScope);
556+
const metadata = resolveBundledChannelMetadata(id, rootScope, loadContext);
533557
if (!metadata) {
534558
loadContext.lazyEntriesById.set(id, null);
535559
return null;
@@ -577,7 +601,7 @@ function getLazyGeneratedBundledChannelSetupEntryForRoot(
577601
if (loadContext.lazySetupEntriesById.has(id)) {
578602
return loadContext.lazySetupEntriesById.get(id) ?? null;
579603
}
580-
const metadata = resolveBundledChannelMetadata(id, rootScope);
604+
const metadata = resolveBundledChannelMetadata(id, rootScope, loadContext);
581605
if (!metadata) {
582606
loadContext.lazySetupEntriesById.set(id, null);
583607
return null;
@@ -615,7 +639,7 @@ function getBundledChannelPluginForRoot(
615639
}
616640
loadContext.pluginLoadInProgressIds.add(id);
617641
try {
618-
const metadata = resolveBundledChannelMetadata(id, rootScope);
642+
const metadata = resolveBundledChannelMetadata(id, rootScope, loadContext);
619643
const plugin = entry.loadChannelPlugin() as ChannelPlugin | undefined;
620644
if (!plugin) {
621645
loadContext.lazyPluginsById.set(id, null);

src/plugins/channel-catalog-registry.test.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,30 @@ function firstDiscoverOptions(discoverSpy: ReturnType<typeof vi.fn>): Record<str
6565
return options as Record<string, unknown>;
6666
}
6767

68+
function createChannelCandidate(params: {
69+
idHint?: string;
70+
pluginId?: string;
71+
bundledPluginId?: string;
72+
origin?: PluginCandidate["origin"];
73+
}): PluginCandidate {
74+
return {
75+
idHint: params.idHint ?? "hint-plugin",
76+
source: "/tmp/openclaw-test-plugin/index.js",
77+
rootDir: "/tmp/openclaw-test-plugin",
78+
origin: params.origin ?? "global",
79+
packageName: "@vendor/openclaw-test-plugin",
80+
packageManifest: {
81+
...(params.pluginId ? { plugin: { id: params.pluginId } } : {}),
82+
channel: {
83+
id: "test-channel",
84+
name: "Test Channel",
85+
description: "Test channel",
86+
},
87+
},
88+
...(params.bundledPluginId ? { bundledManifestId: params.bundledPluginId } : {}),
89+
} as PluginCandidate;
90+
}
91+
6892
describe("listChannelCatalogEntries", () => {
6993
it("forwards lazily loaded install records to discovery when origin is unspecified", async () => {
7094
const { module, discoverSpy, loadRecordsSpy } = await loadWithMocks({});
@@ -134,4 +158,53 @@ describe("listChannelCatalogEntries", () => {
134158
expect(discoverSpy).toHaveBeenCalledTimes(1);
135159
expect(firstDiscoverOptions(discoverSpy)).not.toHaveProperty("installRecords");
136160
});
161+
162+
it("uses discovered package metadata for channel plugin ids", async () => {
163+
const { module, loadRecordsSpy } = await loadWithMocks({});
164+
165+
expect(
166+
module.listChannelCatalogEntries({
167+
installRecords: {},
168+
discovery: {
169+
candidates: [createChannelCandidate({ pluginId: "package-plugin" })],
170+
diagnostics: [],
171+
},
172+
}),
173+
).toStrictEqual([
174+
{
175+
pluginId: "package-plugin",
176+
origin: "global",
177+
packageName: "@vendor/openclaw-test-plugin",
178+
workspaceDir: undefined,
179+
rootDir: "/tmp/openclaw-test-plugin",
180+
channel: {
181+
id: "test-channel",
182+
name: "Test Channel",
183+
description: "Test channel",
184+
},
185+
},
186+
]);
187+
expect(loadRecordsSpy).not.toHaveBeenCalled();
188+
});
189+
190+
it("prefers bundled manifest ids over package id hints", async () => {
191+
const { module } = await loadWithMocks({});
192+
193+
expect(
194+
module.listChannelCatalogEntries({
195+
installRecords: {},
196+
discovery: {
197+
candidates: [
198+
createChannelCandidate({
199+
idHint: "hint-plugin",
200+
pluginId: "package-plugin",
201+
bundledPluginId: "bundled-plugin",
202+
origin: "bundled",
203+
}),
204+
],
205+
diagnostics: [],
206+
},
207+
})[0]?.pluginId,
208+
).toBe("bundled-plugin");
209+
});
137210
});

src/plugins/channel-catalog-registry.ts

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
import type { PluginInstallRecord } from "../config/types.plugins.js";
22
import { discoverOpenClawPlugins, type PluginDiscoveryResult } from "./discovery.js";
3-
import { shouldRejectHardlinkedPluginFiles } from "./hardlink-policy.js";
43
import { loadInstalledPluginIndexInstallRecordsSync } from "./installed-plugin-index-record-reader.js";
5-
import {
6-
loadPluginManifest,
7-
type PluginPackageChannel,
8-
type PluginPackageInstall,
9-
} from "./manifest.js";
4+
import type { PluginPackageChannel, PluginPackageInstall } from "./manifest.js";
105
import type { PluginOrigin } from "./plugin-origin.types.js";
116

127
export type PluginChannelCatalogEntry = {
@@ -50,20 +45,13 @@ export function listChannelCatalogEntries(
5045
if (!channel?.id) {
5146
return [];
5247
}
53-
const manifest = loadPluginManifest(
54-
candidate.rootDir,
55-
shouldRejectHardlinkedPluginFiles({
56-
origin: candidate.origin,
57-
rootDir: candidate.rootDir,
58-
env: params.env,
59-
}),
60-
);
61-
if (!manifest.ok) {
48+
const pluginId = resolveChannelCatalogPluginId(candidate);
49+
if (!pluginId) {
6250
return [];
6351
}
6452
return [
6553
{
66-
pluginId: manifest.manifest.id,
54+
pluginId,
6755
origin: candidate.origin,
6856
packageName: candidate.packageName,
6957
workspaceDir: candidate.workspaceDir,
@@ -77,6 +65,21 @@ export function listChannelCatalogEntries(
7765
});
7866
}
7967

68+
function resolveOptionalString(value: unknown): string | undefined {
69+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
70+
}
71+
72+
function resolveChannelCatalogPluginId(
73+
candidate: PluginDiscoveryResult["candidates"][number],
74+
): string | undefined {
75+
return (
76+
resolveOptionalString(candidate.bundledManifest?.id) ??
77+
resolveOptionalString(candidate.bundledManifestId) ??
78+
resolveOptionalString(candidate.packageManifest?.plugin?.id) ??
79+
resolveOptionalString(candidate.idHint)
80+
);
81+
}
82+
8083
function resolveInstallRecords(params: {
8184
origin?: PluginOrigin;
8285
env?: NodeJS.ProcessEnv;

0 commit comments

Comments
 (0)