From 53bd718a1ac5b019b35d2f7336152fe27e572b6d Mon Sep 17 00:00:00 2001 From: Jason <263060202+fuller-stack-dev@users.noreply.github.com> Date: Sat, 2 May 2026 19:26:00 -0600 Subject: [PATCH] fix(cli): avoid model warmup for message actions (#76312) Summary: - The PR skips eager context-window warmup for `openclaw message`, forwards Discord/Telegram execution-mode de ... reuses caller-owned manifest metadata during config materialization, and adds tests plus a changelog entry. - Reproducibility: yes. Source inspection on current main shows `openclaw message` is still eligible for eager context-window warmup and the Discord/Telegram wrappers still drop the gateway execution-mode declarations. Automerge notes: - PR branch already contained follow-up commit before automerge: fix: type message action routing fallbacks - PR branch already contained follow-up commit before automerge: docs: credit message latency fix contributor Validation: - ClawSweeper review passed for head 9606bb27d54ac12369d6adf32268c3a7a601bb2e. - Required merge gates passed before the squash merge. Prepared head SHA: 9606bb27d54ac12369d6adf32268c3a7a601bb2e Review: https://github.com/openclaw/openclaw/pull/76312#issuecomment-4364958708 Co-authored-by: FullerStackDev <263060202+fuller-stack-dev@users.noreply.github.com> --- CHANGELOG.md | 1 + extensions/discord/src/channel.test.ts | 12 +++++ extensions/discord/src/channel.ts | 6 +++ .../telegram/src/channel.gateway.test.ts | 9 ++++ extensions/telegram/src/channel.ts | 4 ++ src/agents/context.lookup.test.ts | 1 + src/agents/context.ts | 1 + src/config/defaults.test.ts | 6 ++- src/config/defaults.ts | 16 +++++- src/config/io.ts | 26 +++++++-- src/config/materialize.ts | 6 ++- src/config/provider-policy.ts | 11 +++- src/config/validation.ts | 27 +++++++--- src/plugins/provider-public-artifacts.test.ts | 54 +++++++++++++++++++ src/plugins/provider-public-artifacts.ts | 12 +++-- 15 files changed, 170 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d12a3a31cec..12ce64e07c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -111,6 +111,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- CLI/message: skip eager model context warmup and preserve channel-declared gateway execution for Discord and Telegram message actions, avoiding Codex app-server/model discovery during simple send/read commands. Thanks @fuller-stack-dev. - Codex/app-server: resolve managed binaries from bundled `dist` chunks and from the `@openai/codex` package bin when installs do not provide a nearby `.bin/codex` shim, avoiding false missing-binary startup failures. - Plugins/ClawHub: use the ClawHub artifact resolver response as the install decision before downloading, keeping legacy ZIP fallback and future ClawPack npm-pack installs on the same explicit resolver path. Thanks @vincentkoc. - Plugins/ClawHub: keep bare plugin package specs on npm for the launch cutover and reserve ClawHub resolution for explicit `clawhub:` specs until ClawHub pack readiness is deployed. Thanks @vincentkoc. diff --git a/extensions/discord/src/channel.test.ts b/extensions/discord/src/channel.test.ts index 696dbeb506d..ffddbb5dfd6 100644 --- a/extensions/discord/src/channel.test.ts +++ b/extensions/discord/src/channel.test.ts @@ -119,6 +119,18 @@ describe("discordPlugin outbound", () => { expect(discordPlugin.outbound?.preferFinalAssistantVisibleText).toBe(true); }); + it("routes read and search actions through the gateway", () => { + expect(discordPlugin.actions?.resolveExecutionMode?.({ action: "read" as never })).toBe( + "gateway", + ); + expect(discordPlugin.actions?.resolveExecutionMode?.({ action: "search" as never })).toBe( + "gateway", + ); + expect(discordPlugin.actions?.resolveExecutionMode?.({ action: "send" as never })).toBe( + "local", + ); + }); + it("adds Discord mention formatting to agent prompt hints", () => { const hints = discordPlugin.agentPrompt?.messageToolHints?.({} as never) ?? []; diff --git a/extensions/discord/src/channel.ts b/extensions/discord/src/channel.ts index 2738e47d586..c42c592a30d 100644 --- a/extensions/discord/src/channel.ts +++ b/extensions/discord/src/channel.ts @@ -100,6 +100,12 @@ function resolveRuntimeDiscordMessageActions() { } const discordMessageActions = { + resolveExecutionMode: ( + ctx: Parameters>[0], + ) => + resolveRuntimeDiscordMessageActions()?.resolveExecutionMode?.(ctx) ?? + discordMessageActionsImpl.resolveExecutionMode?.(ctx) ?? + "local", describeMessageTool: ( ctx: Parameters>[0], ): ChannelMessageToolDiscovery | null => diff --git a/extensions/telegram/src/channel.gateway.test.ts b/extensions/telegram/src/channel.gateway.test.ts index b5d3605d286..bc64c63005a 100644 --- a/extensions/telegram/src/channel.gateway.test.ts +++ b/extensions/telegram/src/channel.gateway.test.ts @@ -81,6 +81,15 @@ afterEach(() => { }); describe("telegramPlugin gateway startup", () => { + it("routes message actions through the gateway", () => { + expect(telegramPlugin.actions?.resolveExecutionMode?.({ action: "send" as never })).toBe( + "gateway", + ); + expect(telegramPlugin.actions?.resolveExecutionMode?.({ action: "read" as never })).toBe( + "gateway", + ); + }); + it("stops before monitor startup when getMe rejects the token", async () => { installTelegramRuntime(); probeTelegram.mockResolvedValue({ diff --git a/extensions/telegram/src/channel.ts b/extensions/telegram/src/channel.ts index 7f2ab95bb02..3545f01f533 100644 --- a/extensions/telegram/src/channel.ts +++ b/extensions/telegram/src/channel.ts @@ -220,6 +220,10 @@ async function sendTelegramOutbound(params: { } const telegramMessageActions: ChannelMessageActionAdapter = { + resolveExecutionMode: (ctx) => + getOptionalTelegramRuntime()?.channel?.telegram?.messageActions?.resolveExecutionMode?.(ctx) ?? + telegramMessageActionsImpl.resolveExecutionMode?.(ctx) ?? + "gateway", describeMessageTool: (ctx) => getOptionalTelegramRuntime()?.channel?.telegram?.messageActions?.describeMessageTool?.(ctx) ?? telegramMessageActionsImpl.describeMessageTool?.(ctx) ?? diff --git a/src/agents/context.lookup.test.ts b/src/agents/context.lookup.test.ts index 10b8b486f20..01116cde3fd 100644 --- a/src/agents/context.lookup.test.ts +++ b/src/agents/context.lookup.test.ts @@ -230,6 +230,7 @@ describe("lookupContextTokens", () => { expect( shouldEagerWarmContextWindowCache(["node", "openclaw", "memory", "search", "--json"]), ).toBe(false); + expect(shouldEagerWarmContextWindowCache(["node", "openclaw", "message", "read"])).toBe(false); expect(shouldEagerWarmContextWindowCache(["node", "openclaw", "status", "--json"])).toBe(false); expect(shouldEagerWarmContextWindowCache(["node", "openclaw", "sessions", "--json"])).toBe( false, diff --git a/src/agents/context.ts b/src/agents/context.ts index 25cbfe2c061..e0dbf644b29 100644 --- a/src/agents/context.ts +++ b/src/agents/context.ts @@ -156,6 +156,7 @@ const SKIP_EAGER_WARMUP_PRIMARY_COMMANDS = new Set([ "hooks", "logs", "memory", + "message", "models", "pairing", "plugins", diff --git a/src/config/defaults.test.ts b/src/config/defaults.test.ts index 83587407cd7..0fc1954449e 100644 --- a/src/config/defaults.test.ts +++ b/src/config/defaults.test.ts @@ -77,8 +77,12 @@ describe("config defaults", () => { }; mocks.applyProviderConfigDefaultsForConfig.mockReturnValue(nextCfg); - expect(applyContextPruningDefaults(cfg as never)).toBe(nextCfg); + const manifestRegistry = { plugins: [] }; + expect(applyContextPruningDefaults(cfg as never, { manifestRegistry })).toBe(nextCfg); expect(mocks.applyProviderConfigDefaultsForConfig).toHaveBeenCalledTimes(1); + expect(mocks.applyProviderConfigDefaultsForConfig).toHaveBeenCalledWith( + expect.objectContaining({ manifestRegistry }), + ); }); it("defaults ackReactionScope without deriving other message fields", () => { diff --git a/src/config/defaults.ts b/src/config/defaults.ts index 968cbbbe96c..67d4a6bb4d2 100644 --- a/src/config/defaults.ts +++ b/src/config/defaults.ts @@ -1,5 +1,6 @@ import { DEFAULT_CONTEXT_TOKENS } from "../agents/defaults.js"; import { normalizeProviderId } from "../agents/provider-id.js"; +import type { PluginManifestRegistry } from "../plugins/manifest-registry.js"; import { DEFAULT_AGENT_MAX_CONCURRENT, DEFAULT_SUBAGENT_MAX_CONCURRENT } from "./agent-limits.js"; import { applyProviderConfigDefaultsForConfig, @@ -10,6 +11,9 @@ import type { ModelDefinitionConfig } from "./types.models.js"; import type { OpenClawConfig } from "./types.openclaw.js"; type WarnState = { warned: boolean }; +type ProviderPolicyDefaultsOptions = { + manifestRegistry?: Pick; +}; let defaultWarnState: WarnState = { warned: false }; @@ -134,7 +138,10 @@ export function applyTalkConfigNormalization(config: OpenClawConfig): OpenClawCo return normalizeTalkConfig(config); } -export function applyModelDefaults(cfg: OpenClawConfig): OpenClawConfig { +export function applyModelDefaults( + cfg: OpenClawConfig, + options: ProviderPolicyDefaultsOptions = {}, +): OpenClawConfig { let mutated = false; let nextCfg = cfg; @@ -145,6 +152,7 @@ export function applyModelDefaults(cfg: OpenClawConfig): OpenClawConfig { const normalizedProvider = normalizeProviderConfigForConfigDefaults({ provider: providerId, providerConfig: provider, + manifestRegistry: options.manifestRegistry, }); const models = normalizedProvider.models; if (!Array.isArray(models) || models.length === 0) { @@ -365,7 +373,10 @@ function hasAnthropicDefaultSignal(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): }); } -export function applyContextPruningDefaults(cfg: OpenClawConfig): OpenClawConfig { +export function applyContextPruningDefaults( + cfg: OpenClawConfig, + options: ProviderPolicyDefaultsOptions = {}, +): OpenClawConfig { if (!cfg.agents?.defaults) { return cfg; } @@ -377,6 +388,7 @@ export function applyContextPruningDefaults(cfg: OpenClawConfig): OpenClawConfig provider: "anthropic", config: cfg, env: process.env, + manifestRegistry: options.manifestRegistry, }) ?? cfg ); } diff --git a/src/config/io.ts b/src/config/io.ts index 0e9e7372e39..494e806cf2f 100644 --- a/src/config/io.ts +++ b/src/config/io.ts @@ -1605,9 +1605,23 @@ export function createConfigIO( if (preValidationDuplicates.length > 0) { throw new DuplicateAgentDirError(preValidationDuplicates); } + let pluginMetadataSnapshot: PluginMetadataSnapshot | undefined; + const loadValidationPluginMetadataSnapshot = (config: OpenClawConfig) => { + if (pluginMetadataSnapshot) { + return pluginMetadataSnapshot; + } + const defaultAgentId = resolveDefaultAgentId(config); + pluginMetadataSnapshot = loadPluginMetadataSnapshot({ + config, + workspaceDir: resolveAgentWorkspaceDir(config, defaultAgentId), + env: deps.env, + }); + return pluginMetadataSnapshot; + }; const validated = validateConfigObjectWithPlugins(effectiveConfigRaw, { env: deps.env, pluginValidation: overrides.pluginValidation, + loadPluginMetadataSnapshot: loadValidationPluginMetadataSnapshot, sourceRaw: effectiveParsed, }); if (!validated.ok) { @@ -1643,7 +1657,9 @@ export function createConfigIO( deps.logger.warn(`Config warnings:\n${details}`); } warnIfConfigFromFuture(validated.config, deps.logger); - const cfg = materializeRuntimeConfig(validated.config, "load"); + const cfg = materializeRuntimeConfig(validated.config, "load", { + manifestRegistry: pluginMetadataSnapshot?.manifestRegistry, + }); observeLoadConfigSnapshot({ ...createConfigFileSnapshot({ path: configPath, @@ -1855,7 +1871,9 @@ export function createConfigIO( warnIfConfigFromFuture(validated.config, deps.logger); const snapshotConfig = await deps.measure("config.snapshot.read.materialize", () => - materializeRuntimeConfig(validated.config, "snapshot"), + materializeRuntimeConfig(validated.config, "snapshot", { + manifestRegistry: pluginMetadataSnapshot?.manifestRegistry, + }), ); return await deps.measure("config.snapshot.read.observe", () => finalizeReadConfigSnapshotInternalResult(deps, { @@ -1980,7 +1998,9 @@ export function createConfigIO( return result.snapshot.config; } return finalizeLoadedRuntimeConfig( - materializeRuntimeConfig(result.snapshot.sourceConfig, "load"), + materializeRuntimeConfig(result.snapshot.sourceConfig, "load", { + manifestRegistry: result.pluginMetadataSnapshot?.manifestRegistry, + }), ); } diff --git a/src/config/materialize.ts b/src/config/materialize.ts index 47ad6c14758..17f123438a0 100644 --- a/src/config/materialize.ts +++ b/src/config/materialize.ts @@ -1,3 +1,4 @@ +import type { PluginManifestRegistry } from "../plugins/manifest-registry.js"; import { applyCompactionDefaults, applyContextPruningDefaults, @@ -53,6 +54,7 @@ export function asRuntimeConfig(config: OpenClawConfig): RuntimeConfig { export function materializeRuntimeConfig( config: OpenClawConfig, mode: ConfigMaterializationMode, + options: { manifestRegistry?: Pick } = {}, ): RuntimeConfig { const profile = MATERIALIZATION_PROFILES[mode]; let next = applyMessageDefaults(config); @@ -62,12 +64,12 @@ export function materializeRuntimeConfig( next = applySessionDefaults(next); next = applyAgentDefaults(next); if (profile.includeContextPruningDefaults) { - next = applyContextPruningDefaults(next); + next = applyContextPruningDefaults(next, { manifestRegistry: options.manifestRegistry }); } if (profile.includeCompactionDefaults) { next = applyCompactionDefaults(next); } - next = applyModelDefaults(next); + next = applyModelDefaults(next, { manifestRegistry: options.manifestRegistry }); next = applyTalkConfigNormalization(next); if (profile.normalizePaths) { normalizeConfigPaths(next); diff --git a/src/config/provider-policy.ts b/src/config/provider-policy.ts index 1ff87e00381..90874505d74 100644 --- a/src/config/provider-policy.ts +++ b/src/config/provider-policy.ts @@ -1,11 +1,15 @@ +import type { PluginManifestRegistry } from "../plugins/manifest-registry.js"; import { resolveBundledProviderPolicySurface } from "../plugins/provider-public-artifacts.js"; import type { ModelProviderConfig, OpenClawConfig } from "./types.js"; export function normalizeProviderConfigForConfigDefaults(params: { provider: string; providerConfig: ModelProviderConfig; + manifestRegistry?: Pick; }): ModelProviderConfig { - const normalized = resolveBundledProviderPolicySurface(params.provider)?.normalizeConfig?.({ + const normalized = resolveBundledProviderPolicySurface(params.provider, { + manifestRegistry: params.manifestRegistry, + })?.normalizeConfig?.({ provider: params.provider, providerConfig: params.providerConfig, }); @@ -16,9 +20,12 @@ export function applyProviderConfigDefaultsForConfig(params: { provider: string; config: OpenClawConfig; env: NodeJS.ProcessEnv; + manifestRegistry?: Pick; }): OpenClawConfig { return ( - resolveBundledProviderPolicySurface(params.provider)?.applyConfigDefaults?.({ + resolveBundledProviderPolicySurface(params.provider, { + manifestRegistry: params.manifestRegistry, + })?.applyConfigDefaults?.({ provider: params.provider, config: params.config, env: params.env, diff --git a/src/config/validation.ts b/src/config/validation.ts index 300d56c5977..fcaa78ba2f5 100644 --- a/src/config/validation.ts +++ b/src/config/validation.ts @@ -664,6 +664,7 @@ export function validateConfigObjectRaw( export function validateConfigObject( raw: unknown, opts?: { + manifestRegistry?: Pick["manifestRegistry"]; sourceRaw?: unknown; }, ): { ok: true; config: OpenClawConfig } | { ok: false; issues: ConfigValidationIssue[] } { @@ -673,7 +674,9 @@ export function validateConfigObject( } return { ok: true, - config: materializeRuntimeConfig(result.config, "snapshot"), + config: materializeRuntimeConfig(result.config, "snapshot", { + manifestRegistry: opts?.manifestRegistry, + }), }; } @@ -731,14 +734,25 @@ function validateConfigObjectWithPluginsBase( raw: unknown, opts: ValidateConfigWithPluginsParams & { applyDefaults: boolean }, ): ValidateConfigWithPluginsResult { - const base = opts.applyDefaults - ? validateConfigObject(raw, { sourceRaw: opts.sourceRaw }) - : validateConfigObjectRaw(raw, { sourceRaw: opts.sourceRaw }); + const base = validateConfigObjectRaw(raw, { sourceRaw: opts.sourceRaw }); if (!base.ok) { return { ok: false, issues: base.issues, warnings: [] }; } - const config = base.config; + let registryInfo: RegistryInfo | null = opts.pluginMetadataSnapshot + ? { registry: opts.pluginMetadataSnapshot.manifestRegistry } + : null; + if (opts.applyDefaults && !registryInfo && opts.pluginValidation !== "skip") { + const pluginMetadataSnapshot = opts.loadPluginMetadataSnapshot?.(base.config); + if (pluginMetadataSnapshot) { + registryInfo = { registry: pluginMetadataSnapshot.manifestRegistry }; + } + } + const config = opts.applyDefaults + ? materializeRuntimeConfig(base.config, "snapshot", { + manifestRegistry: registryInfo?.registry, + }) + : base.config; if (opts.pluginValidation === "skip") { return { ok: true, @@ -773,9 +787,6 @@ function validateConfigObjectWithPluginsBase( >; }; - let registryInfo: RegistryInfo | null = opts.pluginMetadataSnapshot - ? { registry: opts.pluginMetadataSnapshot.manifestRegistry } - : null; let compatConfig: OpenClawConfig | null | undefined; let compatPluginIds: ReadonlySet | null = null; let compatPluginIdsResolved = false; diff --git a/src/plugins/provider-public-artifacts.test.ts b/src/plugins/provider-public-artifacts.test.ts index 8779f14daaf..d51fc80764c 100644 --- a/src/plugins/provider-public-artifacts.test.ts +++ b/src/plugins/provider-public-artifacts.test.ts @@ -183,6 +183,60 @@ describe("provider public artifacts", () => { } }); + it("uses caller-provided manifest metadata for provider policy aliases", async () => { + const loadPluginManifestRegistry = vi.fn(() => { + throw new Error("unexpected manifest registry scan"); + }); + const loadBundledPluginPublicArtifactModuleSync = vi.fn(({ dirName }: { dirName: string }) => { + if (dirName !== "owner") { + throw new Error(`Unable to resolve bundled plugin public surface ${dirName}`); + } + return { + resolveThinkingProfile: () => ({ levels: [{ id: dirName }] }), + }; + }); + + vi.doMock("./manifest-registry.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + loadPluginManifestRegistry, + }; + }); + vi.doMock("./public-surface-loader.js", () => ({ + loadBundledPluginPublicArtifactModuleSync, + })); + vi.resetModules(); + + const { resolveBundledProviderPolicySurface: resolvePolicySurface } = await importFreshModule< + typeof import("./provider-public-artifacts.js") + >(import.meta.url, "./provider-public-artifacts.js?scope=provider-alias-manifest"); + + const surface = resolvePolicySurface("alias", { + manifestRegistry: { + plugins: [ + { + id: "owner", + channels: [], + cliBackends: [], + hooks: [], + origin: "bundled", + manifestPath: "/tmp/owner/openclaw.plugin.json", + providers: ["alias"], + rootDir: "/tmp/owner", + skills: [], + source: "/tmp/owner/index.js", + }, + ], + }, + }); + + expect(surface?.resolveThinkingProfile?.({ provider: "alias", modelId: "demo" })).toEqual({ + levels: [{ id: "owner" }], + }); + expect(loadPluginManifestRegistry).not.toHaveBeenCalled(); + }); + it("loads provider policy surfaces without package-manager repair", async () => { const loadBundledPluginPublicArtifactModuleSync = vi.fn(() => ({ normalizeConfig: (ctx: { providerConfig: ModelProviderConfig }) => ctx.providerConfig, diff --git a/src/plugins/provider-public-artifacts.ts b/src/plugins/provider-public-artifacts.ts index 1b7c9998451..b0889a29b6c 100644 --- a/src/plugins/provider-public-artifacts.ts +++ b/src/plugins/provider-public-artifacts.ts @@ -2,7 +2,7 @@ import { normalizeProviderId } from "../agents/provider-id.js"; import type { ModelProviderConfig } from "../config/types.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { resolveBundledPluginsDir } from "./bundled-dir.js"; -import { loadPluginManifestRegistry } from "./manifest-registry.js"; +import { loadPluginManifestRegistry, type PluginManifestRegistry } from "./manifest-registry.js"; import type { ProviderApplyConfigDefaultsContext, ProviderNormalizeConfigContext, @@ -63,7 +63,10 @@ function tryLoadBundledProviderPolicySurface( return null; } -function resolveBundledProviderPolicyPluginId(providerId: string): string | null { +function resolveBundledProviderPolicyPluginId( + providerId: string, + options: { manifestRegistry?: Pick } = {}, +): string | null { const normalizedProviderId = normalizeProviderId(providerId); if (!normalizedProviderId) { return null; @@ -73,7 +76,7 @@ function resolveBundledProviderPolicyPluginId(providerId: string): string | null return null; } - const registry = loadPluginManifestRegistry(); + const registry = options.manifestRegistry ?? loadPluginManifestRegistry(); for (const plugin of registry.plugins.toSorted((left, right) => left.id.localeCompare(right.id), )) { @@ -93,6 +96,7 @@ function resolveBundledProviderPolicyPluginId(providerId: string): string | null export function resolveBundledProviderPolicySurface( providerId: string, + options: { manifestRegistry?: Pick } = {}, ): BundledProviderPolicySurface | null { const normalizedProviderId = normalizeProviderId(providerId); if (!normalizedProviderId) { @@ -101,7 +105,7 @@ export function resolveBundledProviderPolicySurface( return ( tryLoadBundledProviderPolicySurface(normalizedProviderId) ?? tryLoadBundledProviderPolicySurface( - resolveBundledProviderPolicyPluginId(normalizedProviderId) ?? normalizedProviderId, + resolveBundledProviderPolicyPluginId(normalizedProviderId, options) ?? normalizedProviderId, ) ); }