fix(cli): reduce plugin listing startup memory (#103132)

This commit is contained in:
Peter Steinberger
2026-07-09 23:28:11 +01:00
committed by GitHub
parent ecad886cd1
commit a1ce77d2a9
5 changed files with 95 additions and 4 deletions

View File

@@ -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";

View File

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