mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:10:43 +00:00
fix(cli): speed up gateway status config reads
This commit is contained in:
@@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Exec/node: synthesize a local approval plan when a paired node advertises `system.run` without `system.run.prepare`, unblocking approval-required `host=node` exec on current macOS companion nodes while preserving remote prepare for node hosts that support it. Fixes #37591 and duplicate #66839; carries forward #69725. Thanks @soloclz.
|
||||
- Memory/QMD: prefer QMD's `--mask` collection pattern flag so root memory indexing stays scoped to `MEMORY.md` instead of widening to every markdown file in the workspace. Thanks @codex.
|
||||
- Gateway/memory: defer QMD startup for implicit non-default agents and scope memory runtime loading to the selected memory slot so Gateway boot and first memory recall avoid broad plugin runtime fanout. Thanks @vincentkoc.
|
||||
- CLI/Gateway: use a parse-only config snapshot for plain `gateway status` reads and reuse same-path service config context so status no longer spends tens of seconds in full config validation before printing. Thanks @vincentkoc.
|
||||
- Lobster/Gateway: memoize repeated Ajv schema compilation before loading the embedded Lobster runtime so scheduled workflows and `llm.invoke` loops stop growing gateway heap on content-identical schemas. Fixes #71148. Thanks @cmi525, @vsolaz, and @vincentkoc.
|
||||
- Codex harness: normalize cached input tokens before session/context accounting so prompt cache reads are not double-counted in `/status`, `session_status`, or persisted `sessionEntry.totalTokens`. Fixes #69298. Thanks @richardmqq.
|
||||
- Hooks/session-memory: use the host local timezone for memory filenames, fallback timestamp slugs, and markdown headers instead of UTC dates. Fixes #46703. (#46721) Thanks @Astro-Han.
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createMockGatewayService } from "../../daemon/service.test-helpers.js";
|
||||
import { captureEnv } from "../../test-utils/env.js";
|
||||
@@ -340,6 +343,52 @@ describe("gatherDaemonStatus", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the fast config path for plain same-file status reads", async () => {
|
||||
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-status-config-"));
|
||||
const configPath = path.join(tmp, "openclaw.json");
|
||||
await fs.writeFile(
|
||||
configPath,
|
||||
JSON.stringify({
|
||||
gateway: {
|
||||
bind: "custom",
|
||||
customBindHost: "10.0.0.5",
|
||||
controlUi: { enabled: true },
|
||||
},
|
||||
}),
|
||||
);
|
||||
process.env.OPENCLAW_STATE_DIR = tmp;
|
||||
process.env.OPENCLAW_CONFIG_PATH = configPath;
|
||||
serviceReadCommand.mockResolvedValueOnce({
|
||||
programArguments: ["/bin/node", "cli", "gateway", "--port", "19001"],
|
||||
environment: {
|
||||
OPENCLAW_STATE_DIR: tmp,
|
||||
OPENCLAW_CONFIG_PATH: configPath,
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const status = await gatherDaemonStatus({
|
||||
rpc: {},
|
||||
probe: false,
|
||||
deep: false,
|
||||
});
|
||||
|
||||
expect(readConfigFileSnapshotCalls).not.toHaveBeenCalled();
|
||||
expect(loadConfigCalls).not.toHaveBeenCalled();
|
||||
expect(status.config?.cli).toMatchObject({
|
||||
path: configPath,
|
||||
exists: true,
|
||||
valid: true,
|
||||
controlUi: { enabled: true },
|
||||
});
|
||||
expect(status.config?.daemon).toBe(status.config?.cli);
|
||||
expect(status.gateway?.bindMode).toBe("custom");
|
||||
expect(status.gateway?.customBindHost).toBe("10.0.0.5");
|
||||
} finally {
|
||||
await fs.rm(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("resolves daemon gateway auth password SecretRef values before probing", async () => {
|
||||
daemonLoadedConfig = {
|
||||
gateway: {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import fs from "node:fs/promises";
|
||||
import JSON5 from "json5";
|
||||
import {
|
||||
createConfigIO,
|
||||
resolveConfigPath,
|
||||
@@ -66,6 +68,12 @@ type DaemonConfigContext = {
|
||||
configMismatch: boolean;
|
||||
};
|
||||
|
||||
type StatusConfigRead = {
|
||||
summary: ConfigSummary;
|
||||
cfg: OpenClawConfig;
|
||||
mode: "fast" | "full";
|
||||
};
|
||||
|
||||
type ResolvedGatewayStatus = {
|
||||
gateway: GatewayStatusSummary;
|
||||
daemonPort: number;
|
||||
@@ -119,6 +127,104 @@ function resolveSnapshotRuntimeConfig(snapshot: ConfigFileSnapshot | null): Open
|
||||
return snapshot.runtimeConfig;
|
||||
}
|
||||
|
||||
function coerceStatusConfig(value: unknown): OpenClawConfig {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return {};
|
||||
}
|
||||
return value as OpenClawConfig;
|
||||
}
|
||||
|
||||
function hasOwnKey(value: unknown, key: string): boolean {
|
||||
return Boolean(
|
||||
value &&
|
||||
typeof value === "object" &&
|
||||
!Array.isArray(value) &&
|
||||
Object.prototype.hasOwnProperty.call(value, key),
|
||||
);
|
||||
}
|
||||
|
||||
function needsFullStatusConfigRead(raw: string, parsed: unknown): boolean {
|
||||
return raw.includes("$include") || raw.includes("${") || hasOwnKey(parsed, "env");
|
||||
}
|
||||
|
||||
async function readFastStatusConfig(configPath: string): Promise<StatusConfigRead | null> {
|
||||
let raw: string;
|
||||
try {
|
||||
raw = await fs.readFile(configPath, "utf8");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON5.parse(raw);
|
||||
} catch (err) {
|
||||
return {
|
||||
summary: {
|
||||
path: configPath,
|
||||
exists: true,
|
||||
valid: false,
|
||||
issues: [{ path: "", message: `JSON5 parse failed: ${String(err)}` }],
|
||||
},
|
||||
cfg: {},
|
||||
mode: "fast",
|
||||
};
|
||||
}
|
||||
|
||||
if (needsFullStatusConfigRead(raw, parsed)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cfg = coerceStatusConfig(parsed);
|
||||
return {
|
||||
summary: {
|
||||
path: configPath,
|
||||
exists: true,
|
||||
valid: true,
|
||||
controlUi: cfg.gateway?.controlUi,
|
||||
},
|
||||
cfg,
|
||||
mode: "fast",
|
||||
};
|
||||
}
|
||||
|
||||
async function readFullStatusConfig(params: {
|
||||
env: NodeJS.ProcessEnv;
|
||||
configPath: string;
|
||||
}): Promise<StatusConfigRead> {
|
||||
const io = createConfigIO({
|
||||
env: params.env,
|
||||
configPath: params.configPath,
|
||||
pluginValidation: "skip",
|
||||
});
|
||||
const snapshot = await io.readConfigFileSnapshot().catch(() => null);
|
||||
const cfg = resolveSnapshotRuntimeConfig(snapshot) ?? io.loadConfig();
|
||||
return {
|
||||
summary: {
|
||||
path: snapshot?.path ?? params.configPath,
|
||||
exists: snapshot?.exists ?? false,
|
||||
valid: snapshot?.valid ?? true,
|
||||
...(snapshot?.issues?.length ? { issues: snapshot.issues } : {}),
|
||||
controlUi: cfg.gateway?.controlUi,
|
||||
},
|
||||
cfg,
|
||||
mode: "full",
|
||||
};
|
||||
}
|
||||
|
||||
async function readStatusConfig(params: {
|
||||
env: NodeJS.ProcessEnv;
|
||||
configPath: string;
|
||||
}): Promise<StatusConfigRead> {
|
||||
return (
|
||||
(await readFastStatusConfig(params.configPath)) ??
|
||||
(await readFullStatusConfig({
|
||||
env: params.env,
|
||||
configPath: params.configPath,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
function appendProbeNote(
|
||||
existing: string | undefined,
|
||||
extra: string | undefined,
|
||||
@@ -207,57 +313,27 @@ async function loadDaemonConfigContext(
|
||||
mergedDaemonEnv as NodeJS.ProcessEnv,
|
||||
resolveStateDir(mergedDaemonEnv as NodeJS.ProcessEnv),
|
||||
);
|
||||
|
||||
const cliIO = createConfigIO({
|
||||
const sameConfigPath = cliConfigPath === daemonConfigPath;
|
||||
const cliConfigRead = await readStatusConfig({
|
||||
env: process.env,
|
||||
configPath: cliConfigPath,
|
||||
pluginValidation: "skip",
|
||||
});
|
||||
const sharesDaemonConfigContext = !serviceEnv && cliConfigPath === daemonConfigPath;
|
||||
const daemonIO = sharesDaemonConfigContext
|
||||
? cliIO
|
||||
: createConfigIO({
|
||||
env: mergedDaemonEnv,
|
||||
const sharesDaemonConfigContext =
|
||||
sameConfigPath && (cliConfigRead.mode === "fast" || !serviceEnv);
|
||||
const daemonConfigRead = sharesDaemonConfigContext
|
||||
? cliConfigRead
|
||||
: await readStatusConfig({
|
||||
env: mergedDaemonEnv as NodeJS.ProcessEnv,
|
||||
configPath: daemonConfigPath,
|
||||
pluginValidation: "skip",
|
||||
});
|
||||
|
||||
const cliSnapshotPromise = cliIO.readConfigFileSnapshot().catch(() => null);
|
||||
const daemonSnapshotPromise = sharesDaemonConfigContext
|
||||
? cliSnapshotPromise
|
||||
: daemonIO.readConfigFileSnapshot().catch(() => null);
|
||||
const [cliSnapshot, daemonSnapshot] = await Promise.all([
|
||||
cliSnapshotPromise,
|
||||
daemonSnapshotPromise,
|
||||
]);
|
||||
const cliCfg = resolveSnapshotRuntimeConfig(cliSnapshot) ?? cliIO.loadConfig();
|
||||
const daemonCfg =
|
||||
sharesDaemonConfigContext && cliSnapshot === daemonSnapshot
|
||||
? cliCfg
|
||||
: (resolveSnapshotRuntimeConfig(daemonSnapshot) ?? daemonIO.loadConfig());
|
||||
|
||||
const cliConfigSummary: ConfigSummary = {
|
||||
path: cliSnapshot?.path ?? cliConfigPath,
|
||||
exists: cliSnapshot?.exists ?? false,
|
||||
valid: cliSnapshot?.valid ?? true,
|
||||
...(cliSnapshot?.issues?.length ? { issues: cliSnapshot.issues } : {}),
|
||||
controlUi: cliCfg.gateway?.controlUi,
|
||||
};
|
||||
const daemonConfigSummary: ConfigSummary = {
|
||||
path: daemonSnapshot?.path ?? daemonConfigPath,
|
||||
exists: daemonSnapshot?.exists ?? false,
|
||||
valid: daemonSnapshot?.valid ?? true,
|
||||
...(daemonSnapshot?.issues?.length ? { issues: daemonSnapshot.issues } : {}),
|
||||
controlUi: daemonCfg.gateway?.controlUi,
|
||||
};
|
||||
|
||||
return {
|
||||
mergedDaemonEnv,
|
||||
cliCfg,
|
||||
daemonCfg,
|
||||
cliConfigSummary,
|
||||
daemonConfigSummary,
|
||||
configMismatch: cliConfigSummary.path !== daemonConfigSummary.path,
|
||||
cliCfg: cliConfigRead.cfg,
|
||||
daemonCfg: daemonConfigRead.cfg,
|
||||
cliConfigSummary: cliConfigRead.summary,
|
||||
daemonConfigSummary: daemonConfigRead.summary,
|
||||
configMismatch: cliConfigRead.summary.path !== daemonConfigRead.summary.path,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user