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 9606bb27d5.
- Required merge gates passed before the squash merge.

Prepared head SHA: 9606bb27d5
Review: https://github.com/openclaw/openclaw/pull/76312#issuecomment-4364958708

Co-authored-by: FullerStackDev <263060202+fuller-stack-dev@users.noreply.github.com>
This commit is contained in:
Jason
2026-05-02 19:26:00 -06:00
committed by GitHub
parent 13dc14d43e
commit 53bd718a1a
15 changed files with 170 additions and 22 deletions

View File

@@ -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.

View File

@@ -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) ?? [];

View File

@@ -100,6 +100,12 @@ function resolveRuntimeDiscordMessageActions() {
}
const discordMessageActions = {
resolveExecutionMode: (
ctx: Parameters<NonNullable<ChannelMessageActionAdapter["resolveExecutionMode"]>>[0],
) =>
resolveRuntimeDiscordMessageActions()?.resolveExecutionMode?.(ctx) ??
discordMessageActionsImpl.resolveExecutionMode?.(ctx) ??
"local",
describeMessageTool: (
ctx: Parameters<NonNullable<ChannelMessageActionAdapter["describeMessageTool"]>>[0],
): ChannelMessageToolDiscovery | null =>

View File

@@ -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({

View File

@@ -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) ??

View File

@@ -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,

View File

@@ -156,6 +156,7 @@ const SKIP_EAGER_WARMUP_PRIMARY_COMMANDS = new Set([
"hooks",
"logs",
"memory",
"message",
"models",
"pairing",
"plugins",

View File

@@ -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", () => {

View File

@@ -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<PluginManifestRegistry, "plugins">;
};
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
);
}

View File

@@ -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,
}),
);
}

View File

@@ -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<PluginManifestRegistry, "plugins"> } = {},
): 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);

View File

@@ -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<PluginManifestRegistry, "plugins">;
}): 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<PluginManifestRegistry, "plugins">;
}): OpenClawConfig {
return (
resolveBundledProviderPolicySurface(params.provider)?.applyConfigDefaults?.({
resolveBundledProviderPolicySurface(params.provider, {
manifestRegistry: params.manifestRegistry,
})?.applyConfigDefaults?.({
provider: params.provider,
config: params.config,
env: params.env,

View File

@@ -664,6 +664,7 @@ export function validateConfigObjectRaw(
export function validateConfigObject(
raw: unknown,
opts?: {
manifestRegistry?: Pick<PluginMetadataSnapshot, "manifestRegistry">["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<string> | null = null;
let compatPluginIdsResolved = false;

View File

@@ -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<typeof import("./manifest-registry.js")>();
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,

View File

@@ -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<PluginManifestRegistry, "plugins"> } = {},
): 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<PluginManifestRegistry, "plugins"> } = {},
): 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,
)
);
}