fix(status): keep startup paths free of plugin warmup
This commit is contained in:
parent
8e132aed6e
commit
0f69b5c11a
@ -82,6 +82,25 @@ describe("lookupContextTokens", () => {
|
||||
expect(lookupContextTokens("openrouter/claude-sonnet")).toBe(321_000);
|
||||
});
|
||||
|
||||
it("can skip async warmup for read-only callers", async () => {
|
||||
const { ensureOpenClawModelsJson } = mockContextModuleDeps(() => ({
|
||||
models: {
|
||||
providers: {
|
||||
openrouter: {
|
||||
models: [{ id: "openrouter/claude-sonnet", contextWindow: 321_000 }],
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const { lookupContextTokens } = await import("./context.js");
|
||||
expect(
|
||||
lookupContextTokens("openrouter/claude-sonnet", { allowAsyncLoad: false }),
|
||||
).toBeUndefined();
|
||||
await flushAsyncWarmup();
|
||||
expect(ensureOpenClawModelsJson).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("only warms eagerly for real openclaw startup commands that need model metadata", async () => {
|
||||
const argvSnapshot = process.argv;
|
||||
try {
|
||||
|
||||
@ -225,12 +225,17 @@ function ensureContextWindowCacheLoaded(): Promise<void> {
|
||||
return loadPromise;
|
||||
}
|
||||
|
||||
export function lookupContextTokens(modelId?: string): number | undefined {
|
||||
export function lookupContextTokens(
|
||||
modelId?: string,
|
||||
options?: { allowAsyncLoad?: boolean },
|
||||
): number | undefined {
|
||||
if (!modelId) {
|
||||
return undefined;
|
||||
}
|
||||
// Best-effort: kick off loading on demand, but don't block lookups.
|
||||
void ensureContextWindowCacheLoaded();
|
||||
if (options?.allowAsyncLoad !== false) {
|
||||
void ensureContextWindowCacheLoaded();
|
||||
}
|
||||
return MODEL_CACHE.get(modelId);
|
||||
}
|
||||
|
||||
@ -354,6 +359,7 @@ export function resolveContextTokensForModel(params: {
|
||||
model?: string;
|
||||
contextTokensOverride?: number;
|
||||
fallbackContextTokens?: number;
|
||||
allowAsyncLoad?: boolean;
|
||||
}): number | undefined {
|
||||
if (typeof params.contextTokensOverride === "number" && params.contextTokensOverride > 0) {
|
||||
return params.contextTokensOverride;
|
||||
@ -402,6 +408,7 @@ export function resolveContextTokensForModel(params: {
|
||||
if (params.provider && ref && !ref.model.includes("/")) {
|
||||
const qualifiedResult = lookupContextTokens(
|
||||
`${normalizeProviderId(ref.provider)}/${ref.model}`,
|
||||
{ allowAsyncLoad: params.allowAsyncLoad },
|
||||
);
|
||||
if (qualifiedResult !== undefined) {
|
||||
return qualifiedResult;
|
||||
@ -410,7 +417,9 @@ export function resolveContextTokensForModel(params: {
|
||||
|
||||
// Bare key fallback. For model-only calls with slash-containing IDs
|
||||
// (e.g. "google/gemini-2.5-pro") this IS the raw discovery cache key.
|
||||
const bareResult = lookupContextTokens(params.model);
|
||||
const bareResult = lookupContextTokens(params.model, {
|
||||
allowAsyncLoad: params.allowAsyncLoad,
|
||||
});
|
||||
if (bareResult !== undefined) {
|
||||
return bareResult;
|
||||
}
|
||||
@ -421,6 +430,7 @@ export function resolveContextTokensForModel(params: {
|
||||
if (!params.provider && ref && !ref.model.includes("/")) {
|
||||
const qualifiedResult = lookupContextTokens(
|
||||
`${normalizeProviderId(ref.provider)}/${ref.model}`,
|
||||
{ allowAsyncLoad: params.allowAsyncLoad },
|
||||
);
|
||||
if (qualifiedResult !== undefined) {
|
||||
return qualifiedResult;
|
||||
|
||||
@ -5,7 +5,6 @@ import { hasPotentialConfiguredChannels } from "../channels/config-presence.js";
|
||||
import { resolveConfigPath, resolveStateDir } from "../config/paths.js";
|
||||
import type { OpenClawConfig } from "../config/types.js";
|
||||
import { resolveOsSummary } from "../infra/os-summary.js";
|
||||
import { buildPluginCompatibilityNotices } from "../plugins/status.js";
|
||||
import { runExec } from "../process/exec.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { getAgentLocalStatuses } from "./status.agent-local.js";
|
||||
@ -23,6 +22,7 @@ import { getStatusSummary } from "./status.summary.js";
|
||||
import { getUpdateCheckResult } from "./status.update.js";
|
||||
|
||||
let pluginRegistryModulePromise: Promise<typeof import("../cli/plugin-registry.js")> | undefined;
|
||||
let pluginStatusModulePromise: Promise<typeof import("../plugins/status.js")> | undefined;
|
||||
let configIoModulePromise: Promise<typeof import("../config/io.js")> | undefined;
|
||||
let commandSecretTargetsModulePromise:
|
||||
| Promise<typeof import("../cli/command-secret-targets.js")>
|
||||
@ -40,6 +40,11 @@ function loadPluginRegistryModule() {
|
||||
return pluginRegistryModulePromise;
|
||||
}
|
||||
|
||||
function loadPluginStatusModule() {
|
||||
pluginStatusModulePromise ??= import("../plugins/status.js");
|
||||
return pluginStatusModulePromise;
|
||||
}
|
||||
|
||||
function loadConfigIoModule() {
|
||||
configIoModulePromise ??= import("../config/io.js");
|
||||
return configIoModulePromise;
|
||||
@ -194,7 +199,12 @@ export async function scanStatusJsonFast(
|
||||
const memoryPlugin = resolveMemoryPluginStatus(cfg);
|
||||
const memory = await resolveMemoryStatusSnapshot({ cfg, agentStatus, memoryPlugin });
|
||||
const pluginCompatibility = shouldCollectPluginCompatibility(cfg)
|
||||
? buildPluginCompatibilityNotices({ config: cfg })
|
||||
? await loadPluginStatusModule().then(({ buildPluginCompatibilityNotices }) =>
|
||||
// Keep plugin status loading off the empty-config `status --json` fast path.
|
||||
// The plugin status module pulls in the full loader graph and materially bloats
|
||||
// startup RSS even when plugin compatibility is never consulted.
|
||||
buildPluginCompatibilityNotices({ config: cfg }),
|
||||
)
|
||||
: [];
|
||||
|
||||
return {
|
||||
|
||||
@ -78,6 +78,7 @@ vi.mock("./status.link-channel.js", () => ({
|
||||
}));
|
||||
|
||||
const { hasPotentialConfiguredChannels } = await import("../channels/config-presence.js");
|
||||
const { resolveContextTokensForModel } = await import("../agents/context.js");
|
||||
const { buildChannelSummary } = await import("../infra/channel-summary.js");
|
||||
const { resolveLinkChannelContext } = await import("./status.link-channel.js");
|
||||
const { getStatusSummary } = await import("./status.summary.js");
|
||||
@ -105,4 +106,12 @@ describe("getStatusSummary", () => {
|
||||
expect(buildChannelSummary).not.toHaveBeenCalled();
|
||||
expect(resolveLinkChannelContext).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not trigger async context warmup while building status summaries", async () => {
|
||||
await getStatusSummary();
|
||||
|
||||
expect(vi.mocked(resolveContextTokensForModel)).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ allowAsyncLoad: false }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -219,6 +219,9 @@ export async function getStatusSummary(
|
||||
model: configModel,
|
||||
contextTokensOverride: cfg.agents?.defaults?.contextTokens,
|
||||
fallbackContextTokens: DEFAULT_CONTEXT_TOKENS,
|
||||
// Keep `status`/`status --json` startup read-only. These summary lookups
|
||||
// should not kick off background provider discovery or plugin scans.
|
||||
allowAsyncLoad: false,
|
||||
}) ?? DEFAULT_CONTEXT_TOKENS;
|
||||
|
||||
const now = Date.now();
|
||||
@ -250,6 +253,7 @@ export async function getStatusSummary(
|
||||
model,
|
||||
contextTokensOverride: entry?.contextTokens,
|
||||
fallbackContextTokens: configContextTokens ?? undefined,
|
||||
allowAsyncLoad: false,
|
||||
}) ?? null;
|
||||
const total = resolveFreshSessionTotalTokens(entry);
|
||||
const totalTokensFresh =
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user