Persist plugin install index in SQLite (#88794)

* refactor: persist plugin install index in sqlite

* fix: merge legacy plugin index records into sqlite

* test: update plugin index sqlite fixtures

* fix: migrate custom plugin install indexes

* test: update plugin index sentinel

* fix: exclude migrated plugin index archives

* fix: read post-upgrade plugin index from sqlite

* fix: migrate legacy plugin index before agent runs

* fix: respect disabled persisted plugin registry reads

* test: type plugin install record fixtures

* fix: simplify plugin index record reader type

* test: fix sqlite plugin index CI fallout

* test: mock provider normalization in agent command tests

# Conflicts:
#	src/commands/agent-command.test-mocks.ts

* build: remove unused ui three dependency
This commit is contained in:
Peter Steinberger
2026-05-31 20:51:33 -04:00
committed by GitHub
parent b475de834a
commit 5443baa852
57 changed files with 1711 additions and 629 deletions
+19
View File
@@ -168,6 +168,11 @@ describe("ensureConfigReady", () => {
commandPath: ["update", "status"],
expectedDoctorCalls: 0,
},
{
name: "skips doctor flow for agent without legacy state",
commandPath: ["agent"],
expectedDoctorCalls: 0,
},
{
name: "runs doctor flow for commands that may mutate state without legacy state",
commandPath: ["message"],
@@ -207,6 +212,20 @@ describe("ensureConfigReady", () => {
expect(loadAndMaybeMigrateDoctorConfigMock).toHaveBeenCalledOnce();
});
it("runs doctor flow before agent commands when the legacy plugin install index exists", async () => {
const root = useTempOpenClawHome();
writeStateMarker(root, "plugins/installs.json");
await runEnsureConfigReady(["agent"]);
expect(loadAndMaybeMigrateDoctorConfigMock).toHaveBeenCalledOnce();
expect(loadAndMaybeMigrateDoctorConfigMock).toHaveBeenCalledWith({
migrateState: true,
migrateLegacyConfig: false,
invalidConfigNote: false,
});
});
it.each([
["Discord model picker preferences", "discord/model-picker-preferences.json"],
["Feishu dedupe sidecar", "feishu/dedup/default.json"],
+6 -4
View File
@@ -110,16 +110,18 @@ function hasLegacyStateMigrationInputs(): boolean {
path.join(stateDir, "agents"),
path.join(stateDir, "flows", "registry.sqlite"),
path.join(stateDir, "plugin-state", "state.sqlite"),
path.join(stateDir, "plugins", "installs.json"),
path.join(stateDir, "sessions"),
path.join(stateDir, "tasks", "runs.sqlite"),
].some(fileOrDirExists) || hasBundledChannelLegacyStateMigrationInputs(stateDir, oauthDir)
);
}
function isReadOnlyStateMigrationCommand(commandPath: string[]): boolean {
function shouldRunStateMigrationOnlyWithLegacyInputs(commandPath: string[]): boolean {
const commandName = commandPath[0];
const subcommandName = commandPath[1];
return (
commandName === "agent" ||
commandName === "status" ||
(commandName === "tasks" &&
(subcommandName === undefined || ALLOWED_INVALID_TASK_SUBCOMMANDS.has(subcommandName)))
@@ -160,7 +162,7 @@ export async function ensureConfigReady(params: {
const commandPath = params.commandPath ?? [];
let preflightSnapshot: Awaited<ReturnType<typeof readConfigFileSnapshot>> | null = null;
const shouldConsiderStateMigration = shouldMigrateStateFromPath(commandPath);
const isReadOnlyMigrationCommand = isReadOnlyStateMigrationCommand(commandPath);
const requiresLegacyStateInput = shouldRunStateMigrationOnlyWithLegacyInputs(commandPath);
const runStateMigrationPreflight = async () => {
didRunDoctorConfigFlow = true;
const runDoctorConfigPreflight = async () =>
@@ -176,7 +178,7 @@ export async function ensureConfigReady(params: {
if (
!didRunDoctorConfigFlow &&
shouldConsiderStateMigration &&
(!isReadOnlyMigrationCommand || hasLegacyStateMigrationInputs())
(!requiresLegacyStateInput || hasLegacyStateMigrationInputs())
) {
preflightSnapshot = await runStateMigrationPreflight();
}
@@ -186,7 +188,7 @@ export async function ensureConfigReady(params: {
!preflightSnapshot &&
!didRunDoctorConfigFlow &&
shouldConsiderStateMigration &&
isReadOnlyMigrationCommand &&
requiresLegacyStateInput &&
snapshot.valid &&
snapshotHasConfiguredSessionStore(snapshot)
) {