mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-26 08:31:55 +00:00
fix: keep status --json stdout clean (#52449) (thanks @cgdusek)
This commit is contained in:
@@ -115,6 +115,7 @@ Docs: https://docs.openclaw.ai
|
||||
- CLI/configure: clarify fresh-setup memory-search warnings so they say semantic recall needs at least one embedding provider, and scope the initial model allowlist picker to the provider selected in configure. Thanks @vincentkoc.
|
||||
- CLI/auth choice: lazy-load plugin/provider fallback resolution so mapped auth choices stay on the static path and only unknown choices pay the heavy provider load. (#47495) Thanks @vincentkoc.
|
||||
- CLI: avoid loading provider discovery during startup model normalization. (#46522) Thanks @ItsAditya-xyz and @vincentkoc.
|
||||
- CLI/status: keep `status --json` stdout clean by skipping plugin compatibility scans that were not rendered in the JSON payload. (#52449) Thanks @cgdusek.
|
||||
- Agents/Telegram: avoid rebuilding the full model catalog on ordinary inbound replies so Telegram message handling no longer pays multi-second core startup latency before reply generation. Thanks @vincentkoc.
|
||||
- Gateway/Discord startup: load only configured channel plugins during gateway boot, and lazy-load Discord provider/session runtime setup so startup stops importing unrelated providers and trims cold-start delay. Thanks @vincentkoc.
|
||||
- Security/exec: harden macOS allowlist resolution against wrapper and `env` spoofing, require fresh approval for inline interpreter eval with `tools.exec.strictInlineEval`, wrap Discord guild message bodies as untrusted external content, and add audit findings for risky exec approval and open-channel combinations.
|
||||
|
||||
@@ -352,8 +352,8 @@ describe("registerPreActionHooks", () => {
|
||||
});
|
||||
|
||||
await runPreAction({
|
||||
parseArgv: ["agents"],
|
||||
processArgv: ["node", "openclaw", "agents", "--json"],
|
||||
parseArgv: ["agents", "list"],
|
||||
processArgv: ["node", "openclaw", "agents", "list", "--json"],
|
||||
});
|
||||
|
||||
expect(ensurePluginRegistryLoadedMock).toHaveBeenCalled();
|
||||
@@ -369,8 +369,8 @@ describe("registerPreActionHooks", () => {
|
||||
});
|
||||
|
||||
await runPreAction({
|
||||
parseArgv: ["agents"],
|
||||
processArgv: ["node", "openclaw", "agents"],
|
||||
parseArgv: ["agents", "list"],
|
||||
processArgv: ["node", "openclaw", "agents", "list"],
|
||||
});
|
||||
|
||||
expect(ensurePluginRegistryLoadedMock).toHaveBeenCalled();
|
||||
|
||||
@@ -139,7 +139,6 @@ export function registerPreActionHooks(program: Command, programVersion: string)
|
||||
commandPath,
|
||||
...(jsonOutputMode ? { suppressDoctorStdout: true } : {}),
|
||||
});
|
||||
<<<<<<< HEAD
|
||||
// Load plugins for commands that need channel access.
|
||||
// When --json output is active, temporarily route logs to stderr so plugin
|
||||
// registration messages don't corrupt the JSON payload on stdout.
|
||||
|
||||
@@ -195,6 +195,14 @@ describe("scanStatusJsonFast", () => {
|
||||
expect(loggingState.forceConsoleToStderr).toBe(false);
|
||||
});
|
||||
|
||||
it("skips plugin compatibility loading even when configured channels are present", async () => {
|
||||
mocks.hasPotentialConfiguredChannels.mockReturnValue(true);
|
||||
|
||||
await scanStatusJsonFast({}, {} as never);
|
||||
|
||||
expect(mocks.buildPluginCompatibilityNotices).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips memory inspection for the lean status --json fast path", async () => {
|
||||
const result = await scanStatusJsonFast({}, {} as never);
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ 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")>
|
||||
@@ -41,11 +40,6 @@ function loadPluginRegistryModule() {
|
||||
return pluginRegistryModulePromise;
|
||||
}
|
||||
|
||||
function loadPluginStatusModule() {
|
||||
pluginStatusModulePromise ??= import("../plugins/status.js");
|
||||
return pluginStatusModulePromise;
|
||||
}
|
||||
|
||||
function loadConfigIoModule() {
|
||||
configIoModulePromise ??= import("../config/io.js");
|
||||
return configIoModulePromise;
|
||||
@@ -91,13 +85,6 @@ function buildColdStartUpdateResult(): Awaited<ReturnType<typeof getUpdateCheckR
|
||||
};
|
||||
}
|
||||
|
||||
function shouldCollectPluginCompatibility(cfg: OpenClawConfig): boolean {
|
||||
if (hasPotentialConfiguredChannels(cfg)) {
|
||||
return true;
|
||||
}
|
||||
return existsSync(resolveConfigPath(process.env));
|
||||
}
|
||||
|
||||
function resolveDefaultMemoryStorePath(agentId: string): string {
|
||||
return path.join(resolveStateDir(process.env, os.homedir), "memory", `${agentId}.sqlite`);
|
||||
}
|
||||
@@ -233,14 +220,9 @@ export async function scanStatusJsonFast(
|
||||
const memory = opts.all
|
||||
? await resolveMemoryStatusSnapshot({ cfg, agentStatus, memoryPlugin })
|
||||
: null;
|
||||
const pluginCompatibility = shouldCollectPluginCompatibility(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 }),
|
||||
)
|
||||
: [];
|
||||
// `status --json` does not serialize plugin compatibility notices, so keep the
|
||||
// fast path off the full plugin status graph after the initial scoped preload.
|
||||
const pluginCompatibility: StatusScanResult["pluginCompatibility"] = [];
|
||||
|
||||
return {
|
||||
cfg,
|
||||
|
||||
@@ -284,6 +284,59 @@ describe("scanStatus", () => {
|
||||
expect(mocks.buildPluginCompatibilityNotices).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips plugin compatibility loading for status --json even with configured channels", async () => {
|
||||
mocks.hasPotentialConfiguredChannels.mockReturnValue(true);
|
||||
mocks.readBestEffortConfig.mockResolvedValue({
|
||||
session: {},
|
||||
gateway: {},
|
||||
channels: { discord: {} },
|
||||
});
|
||||
mocks.resolveCommandSecretRefsViaGateway.mockResolvedValue({
|
||||
resolvedConfig: {
|
||||
session: {},
|
||||
gateway: {},
|
||||
channels: { discord: {} },
|
||||
},
|
||||
diagnostics: [],
|
||||
});
|
||||
mocks.getUpdateCheckResult.mockResolvedValue({
|
||||
installKind: "git",
|
||||
git: null,
|
||||
registry: null,
|
||||
});
|
||||
mocks.getAgentLocalStatuses.mockResolvedValue({
|
||||
defaultId: "main",
|
||||
agents: [],
|
||||
});
|
||||
mocks.getStatusSummary.mockResolvedValue({
|
||||
linkChannel: undefined,
|
||||
sessions: { count: 0, paths: [], defaults: {}, recent: [] },
|
||||
});
|
||||
mocks.buildGatewayConnectionDetails.mockReturnValue({
|
||||
url: "ws://127.0.0.1:18789",
|
||||
urlSource: "default",
|
||||
});
|
||||
mocks.resolveGatewayProbeAuthResolution.mockResolvedValue({
|
||||
auth: {},
|
||||
warning: undefined,
|
||||
});
|
||||
mocks.probeGateway.mockResolvedValue({
|
||||
ok: false,
|
||||
url: "ws://127.0.0.1:18789",
|
||||
connectLatencyMs: null,
|
||||
error: "timeout",
|
||||
close: null,
|
||||
health: null,
|
||||
status: null,
|
||||
presence: null,
|
||||
configSnapshot: null,
|
||||
});
|
||||
|
||||
await scanStatus({ json: true }, {} as never);
|
||||
|
||||
expect(mocks.buildPluginCompatibilityNotices).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips gateway and update probes on cold-start status paths", async () => {
|
||||
mocks.readBestEffortConfig.mockResolvedValue({
|
||||
session: {},
|
||||
|
||||
@@ -69,13 +69,6 @@ function unwrapDeferredResult<T>(result: DeferredResult<T>): T {
|
||||
return result.value;
|
||||
}
|
||||
|
||||
function shouldCollectPluginCompatibility(cfg: OpenClawConfig): boolean {
|
||||
if (hasPotentialConfiguredChannels(cfg)) {
|
||||
return true;
|
||||
}
|
||||
return existsSync(resolveConfigPath(process.env));
|
||||
}
|
||||
|
||||
function isMissingConfigColdStart(): boolean {
|
||||
return !existsSync(resolveConfigPath(process.env));
|
||||
}
|
||||
@@ -237,9 +230,9 @@ async function scanStatusJsonFast(opts: {
|
||||
const memoryPlugin = resolveMemoryPluginStatus(cfg);
|
||||
const memoryPromise = resolveMemoryStatusSnapshot({ cfg, agentStatus, memoryPlugin });
|
||||
const memory = await memoryPromise;
|
||||
const pluginCompatibility = shouldCollectPluginCompatibility(cfg)
|
||||
? buildPluginCompatibilityNotices({ config: cfg })
|
||||
: [];
|
||||
// `status --json` never renders plugin compatibility notices, so skip the
|
||||
// full compatibility scan and avoid a second plugin load on the JSON path.
|
||||
const pluginCompatibility: StatusScanResult["pluginCompatibility"] = [];
|
||||
|
||||
return {
|
||||
cfg,
|
||||
|
||||
Reference in New Issue
Block a user