mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(cli): reduce plugin listing startup memory (#103132)
This commit is contained in:
committed by
GitHub
parent
ecad886cd1
commit
a1ce77d2a9
@@ -25,6 +25,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- **CLI plugin listing:** skip state-migration runtime loading when no legacy inputs exist, reducing packaged cold-start memory while preserving migrations for legacy plugin indexes and configured session stores.
|
||||
- **Unicode-safe bounded text:** preserve complete UTF-16 surrogate pairs when shortening previews, prompts, diagnostics, labels, session keys, link metadata, and identity values across Control UI, CLI, Gateway, plugins, QA, memory, and Android surfaces. (#102625, #102626, #102627, #102656, #102816, #102823, #102833, #102877, #102949, #102963, #102969, #102988, #103010, #103034) Thanks @zhangguiping-xydt, @wings1029, @wangyan2026, @Pandah97, @MoerAI, @SunnyShu0925, @zhangqueping, @zw-xysk, @cxbAsDev, @lzyyzznl, @coder-master-0915, @LeonidasLux, @mushuiyu886, @ly85206559, and @Simon-XYDT.
|
||||
- **CLI model tables:** sanitize, truncate, and pad model-list cells by rendered terminal width so emoji, CJK, and other wide graphemes keep columns aligned. (#102819) Thanks @Kevin23-design and @vincentkoc.
|
||||
- **Skills prompt compaction:** preserve every included skill identity before using the remaining prompt budget for shortened, UTF-16-safe descriptions, retaining trigger guidance without exceeding the hard limit. (#88426) Thanks @abel-zer0.
|
||||
|
||||
@@ -95,6 +95,9 @@ function resolveDefaultLimitsMb(platform = process.platform) {
|
||||
// higher RSS for the same launcher path, so keep it supported without hiding
|
||||
// Linux help-path regressions.
|
||||
help: platform === "darwin" ? 300 : 100,
|
||||
// Plugin discovery is heavier than help, but must stay below the doctor/channel
|
||||
// runtime graph that an empty metadata-only invocation must not import.
|
||||
pluginsList: platform === "darwin" ? 500 : 350,
|
||||
statusJson: 400,
|
||||
gatewayStatus: 500,
|
||||
};
|
||||
@@ -109,6 +112,15 @@ const cases = [
|
||||
args: ["openclaw.mjs", "--help"],
|
||||
limitMb: readPositiveNumberEnv("OPENCLAW_STARTUP_MEMORY_HELP_MB", DEFAULT_LIMITS_MB.help),
|
||||
},
|
||||
{
|
||||
id: "pluginsList",
|
||||
label: "plugins list --json",
|
||||
args: ["openclaw.mjs", "plugins", "list", "--json"],
|
||||
limitMb: readPositiveNumberEnv(
|
||||
"OPENCLAW_STARTUP_MEMORY_PLUGINS_LIST_MB",
|
||||
DEFAULT_LIMITS_MB.pluginsList,
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "statusJson",
|
||||
label: "status --json",
|
||||
|
||||
@@ -163,6 +163,11 @@ describe("ensureConfigReady", () => {
|
||||
commandPath: ["agent"],
|
||||
expectedDoctorCalls: 0,
|
||||
},
|
||||
{
|
||||
name: "skips doctor flow for plugin listing without legacy state",
|
||||
commandPath: ["plugins", "list"],
|
||||
expectedDoctorCalls: 0,
|
||||
},
|
||||
{
|
||||
name: "runs doctor flow for commands that may mutate state without legacy state",
|
||||
commandPath: ["message"],
|
||||
@@ -250,6 +255,43 @@ describe("ensureConfigReady", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves plugin listing migrations when the legacy plugin install index exists", async () => {
|
||||
const root = useTempOpenClawHome();
|
||||
writeStateMarker(root, "plugins/installs.json");
|
||||
const migratedSnapshot = {
|
||||
...makeSnapshot(),
|
||||
config: { plugins: { entries: { legacy: { enabled: true } } } },
|
||||
runtimeConfig: { plugins: { entries: { legacy: { enabled: true } } } },
|
||||
sourceConfig: { plugins: { entries: { legacy: { enabled: true } } } },
|
||||
};
|
||||
loadAndMaybeMigrateDoctorConfigMock.mockResolvedValue({
|
||||
snapshot: migratedSnapshot,
|
||||
baseConfig: {},
|
||||
});
|
||||
|
||||
await runEnsureConfigReady(["plugins", "list"]);
|
||||
|
||||
expect(loadAndMaybeMigrateDoctorConfigMock).toHaveBeenCalledOnce();
|
||||
expect(loadAndMaybeMigrateDoctorConfigMock).toHaveBeenCalledWith({
|
||||
migrateState: true,
|
||||
migrateLegacyConfig: false,
|
||||
invalidConfigNote: false,
|
||||
});
|
||||
expect(setRuntimeConfigSnapshotMock).toHaveBeenCalledWith(
|
||||
migratedSnapshot.runtimeConfig,
|
||||
migratedSnapshot.sourceConfig,
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves plugin listing migrations when the shared state database exists", async () => {
|
||||
const root = useTempOpenClawHome();
|
||||
writeStateMarker(root, "state/openclaw.sqlite");
|
||||
|
||||
await runEnsureConfigReady(["plugins", "list"]);
|
||||
|
||||
expect(loadAndMaybeMigrateDoctorConfigMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("runs doctor flow before agent commands when default exec approvals must move to a custom state dir", async () => {
|
||||
const root = useTempOpenClawHome();
|
||||
const stateDir = path.join(root, "custom-state");
|
||||
@@ -301,7 +343,10 @@ describe("ensureConfigReady", () => {
|
||||
expect(loadAndMaybeMigrateDoctorConfigMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("runs doctor flow for read-only commands with configured custom session stores", async () => {
|
||||
it.each([
|
||||
{ name: "status", commandPath: ["status"] },
|
||||
{ name: "plugin listing", commandPath: ["plugins", "list"] },
|
||||
])("runs doctor flow for $name with configured custom session stores", async ({ commandPath }) => {
|
||||
const root = useTempOpenClawHome();
|
||||
const customStore = path.join(root, "sessions", "sessions.json");
|
||||
const snapshot = {
|
||||
@@ -315,7 +360,7 @@ describe("ensureConfigReady", () => {
|
||||
baseConfig: {},
|
||||
});
|
||||
|
||||
await runEnsureConfigReady(["status"]);
|
||||
await runEnsureConfigReady(commandPath);
|
||||
|
||||
expect(loadAndMaybeMigrateDoctorConfigMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
@@ -337,6 +382,24 @@ describe("ensureConfigReady", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("pins plugin listing config without loading state migration runtime", async () => {
|
||||
const snapshot = {
|
||||
...makeSnapshot(),
|
||||
config: { plugins: { entries: { alpha: { enabled: true } } } },
|
||||
runtimeConfig: { plugins: { entries: { alpha: { enabled: true } } } },
|
||||
sourceConfig: { plugins: { entries: { alpha: { enabled: true } } } },
|
||||
};
|
||||
readConfigFileSnapshotMock.mockResolvedValue(snapshot);
|
||||
|
||||
await runEnsureConfigReady(["plugins", "list"]);
|
||||
|
||||
expect(loadAndMaybeMigrateDoctorConfigMock).not.toHaveBeenCalled();
|
||||
expect(setRuntimeConfigSnapshotMock).toHaveBeenCalledWith(
|
||||
snapshot.runtimeConfig,
|
||||
snapshot.sourceConfig,
|
||||
);
|
||||
});
|
||||
|
||||
it("retries the cached config snapshot after a read rejection", async () => {
|
||||
const originalVitest = process.env.VITEST;
|
||||
process.env.VITEST = "false";
|
||||
|
||||
@@ -142,6 +142,7 @@ function hasLegacyStateMigrationInputs(): boolean {
|
||||
path.join(stateDir, "agents"),
|
||||
path.join(stateDir, "plugins", "installs.json"),
|
||||
path.join(stateDir, "sessions"),
|
||||
path.join(stateDir, "state", "openclaw.sqlite"),
|
||||
].some(fileOrDirExists) ||
|
||||
sqliteSidecarPaths.some(
|
||||
(sourcePath) => fileOrDirExists(sourcePath) || hasPendingSqliteSidecarArchive(sourcePath),
|
||||
@@ -154,9 +155,12 @@ function hasLegacyStateMigrationInputs(): boolean {
|
||||
function shouldRunStateMigrationOnlyWithLegacyInputs(commandPath: string[]): boolean {
|
||||
const commandName = commandPath[0];
|
||||
const subcommandName = commandPath[1];
|
||||
// Metadata-only plugin listing still migrates known legacy inputs, but an empty
|
||||
// state must not cold-load doctor and bundled channel runtime graphs.
|
||||
return (
|
||||
commandName === "agent" ||
|
||||
commandName === "status" ||
|
||||
(commandName === "plugins" && subcommandName === "list") ||
|
||||
(commandName === "tasks" &&
|
||||
(subcommandName === undefined || ALLOWED_INVALID_TASK_SUBCOMMANDS.has(subcommandName)))
|
||||
);
|
||||
|
||||
@@ -52,6 +52,17 @@ describe("check-cli-startup-memory", () => {
|
||||
expect(testing.resolveDefaultLimitsMb("darwin").help).toBeGreaterThan(100);
|
||||
});
|
||||
|
||||
it("guards packaged plugin listing startup memory", () => {
|
||||
expect(testing.resolveDefaultLimitsMb("linux").pluginsList).toBe(350);
|
||||
expect(testing.resolveDefaultLimitsMb("darwin").pluginsList).toBeGreaterThan(350);
|
||||
expect(testing.cases).toContainEqual(
|
||||
expect.objectContaining({
|
||||
id: "pluginsList",
|
||||
args: ["openclaw.mjs", "plugins", "list", "--json"],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps invalid startup memory env values from bypassing budgets", () => {
|
||||
expect(() =>
|
||||
testing.readPositiveNumberEnv("OPENCLAW_STARTUP_MEMORY_HELP_MB", 100, {
|
||||
@@ -188,8 +199,8 @@ describe("check-cli-startup-memory", () => {
|
||||
},
|
||||
),
|
||||
).toThrow("--help timed out after 1234ms");
|
||||
expect(seenTimeouts).toEqual([1234, 1234, 1234]);
|
||||
expect(seenKillSignals).toEqual(["SIGKILL", "SIGKILL", "SIGKILL"]);
|
||||
expect(seenTimeouts).toEqual(testing.cases.map(() => 1234));
|
||||
expect(seenKillSignals).toEqual(testing.cases.map(() => "SIGKILL"));
|
||||
});
|
||||
|
||||
it("rejects zero RSS markers instead of passing empty resource evidence", () => {
|
||||
|
||||
Reference in New Issue
Block a user