perf(migrations): trim legacy migration and bind cold paths

This commit is contained in:
Vincent Koc
2026-04-15 00:38:38 +01:00
parent a2888f8f7d
commit 97ee0c6fd3
10 changed files with 110 additions and 14 deletions

View File

@@ -29,6 +29,10 @@ type BundledChannelSetupEntryRuntimeContract = {
kind: "bundled-channel-setup-entry";
loadSetupPlugin: () => ChannelPlugin;
loadSetupSecrets?: () => ChannelPlugin["secrets"] | undefined;
features?: {
legacyStateMigrations?: boolean;
legacySessionSurfaces?: boolean;
};
};
type GeneratedBundledChannelEntry = {
@@ -88,6 +92,13 @@ function resolveChannelSetupModuleEntry(
return record as BundledChannelSetupEntryRuntimeContract;
}
function hasSetupEntryFeature(
entry: BundledChannelSetupEntryRuntimeContract | undefined,
feature: keyof NonNullable<BundledChannelSetupEntryRuntimeContract["features"]>,
): boolean {
return entry?.features?.[feature] === true;
}
function resolveBundledChannelBoundaryRoot(params: {
metadata: BundledChannelPluginMetadata;
modulePath: string;
@@ -263,6 +274,19 @@ export function listBundledChannelSetupPlugins(): readonly ChannelPlugin[] {
});
}
export function listBundledChannelSetupPluginsByFeature(
feature: keyof NonNullable<BundledChannelSetupEntryRuntimeContract["features"]>,
): readonly ChannelPlugin[] {
return listBundledChannelPluginIds().flatMap((id) => {
const setupEntry = getLazyGeneratedBundledChannelEntry(id, { includeSetup: true })?.setupEntry;
if (!hasSetupEntryFeature(setupEntry, feature)) {
return [];
}
const plugin = getBundledChannelSetupPlugin(id);
return plugin ? [plugin] : [];
});
}
export function getBundledChannelPlugin(id: ChannelId): ChannelPlugin | undefined {
const cached = lazyPluginsById.get(id);
if (cached) {

View File

@@ -5,7 +5,7 @@ import {
createTestRegistry,
} from "../test-utils/channel-plugins.js";
import {
loadFreshAgentsCommandModuleForTest,
loadFreshAgentsBindCommandModuleForTest,
readConfigFileSnapshotMock,
resetAgentsBindTestHarness,
runtime,
@@ -25,11 +25,11 @@ const matrixBindingPlugin = createBindingResolverTestPlugin({
},
});
let agentsBindCommand: typeof import("./agents.js").agentsBindCommand;
let agentsBindCommand: typeof import("./agents.commands.bind.js").agentsBindCommand;
describe("agents bind matrix integration", () => {
beforeEach(async () => {
({ agentsBindCommand } = await loadFreshAgentsCommandModuleForTest());
({ agentsBindCommand } = await loadFreshAgentsBindCommandModuleForTest());
resetAgentsBindTestHarness();
setActivePluginRegistry(

View File

@@ -39,12 +39,18 @@ vi.mock("../config/config.js", async () => {
export const runtime = createTestRuntime();
let agentsCommandModulePromise: Promise<typeof import("./agents.js")> | undefined;
let agentsBindCommandModulePromise: Promise<typeof import("./agents.commands.bind.js")> | undefined;
export async function loadFreshAgentsCommandModuleForTest() {
agentsCommandModulePromise ??= import("./agents.js");
return await agentsCommandModulePromise;
}
export async function loadFreshAgentsBindCommandModuleForTest() {
agentsBindCommandModulePromise ??= import("./agents.commands.bind.js");
return await agentsBindCommandModulePromise;
}
export function resetAgentsBindTestHarness(): void {
readConfigFileSnapshotMock.mockClear();
writeConfigFileMock.mockClear();

View File

@@ -1,8 +1,12 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import {
resetSessionStoreLockRuntimeForTests,
setSessionWriteLockAcquirerForTests,
} from "../config/sessions/store.js";
import {
autoMigrateLegacyStateDir,
autoMigrateLegacyState,
@@ -14,6 +18,30 @@ import {
let tempRoot: string | null = null;
vi.mock("../infra/json-files.js", async () => {
const actual =
await vi.importActual<typeof import("../infra/json-files.js")>("../infra/json-files.js");
return {
...actual,
writeTextAtomic: async (
filePath: string,
content: string,
options?: { mode?: number; ensureDirMode?: number; appendTrailingNewline?: boolean },
) => {
const payload =
options?.appendTrailingNewline && !content.endsWith("\n") ? `${content}\n` : content;
await fs.promises.mkdir(path.dirname(filePath), {
recursive: true,
...(typeof options?.ensureDirMode === "number" ? { mode: options.ensureDirMode } : {}),
});
await fs.promises.writeFile(filePath, payload, {
encoding: "utf8",
mode: options?.mode ?? 0o600,
});
},
};
});
async function makeTempRoot() {
const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), "openclaw-doctor-"));
tempRoot = root;
@@ -52,9 +80,16 @@ async function runTelegramAllowFromMigration(params: { root: string; cfg: OpenCl
return { oauthDir, detected, result };
}
beforeEach(() => {
setSessionWriteLockAcquirerForTests(async () => ({
release: async () => undefined,
}));
});
afterEach(async () => {
resetAutoMigrateLegacyStateForTest();
resetAutoMigrateLegacyStateDirForTest();
resetSessionStoreLockRuntimeForTests();
if (!tempRoot) {
return;
}

View File

@@ -2,8 +2,7 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { resolveDefaultAgentId } from "../agents/agent-scope.js";
import { iterateBootstrapChannelPlugins } from "../channels/plugins/bootstrap-registry.js";
import { listBundledChannelPlugins } from "../channels/plugins/bundled.js";
import { listBundledChannelSetupPluginsByFeature } from "../channels/plugins/bundled.js";
import type { ChannelLegacyStateMigrationPlan } from "../channels/plugins/types.core.js";
import {
resolveLegacyStateDirs,
@@ -73,6 +72,7 @@ type MigrationLogger = {
let autoMigrateChecked = false;
let autoMigrateStateDirChecked = false;
let cachedLegacySessionSurfaces: LegacySessionSurface[] | null = null;
type LegacySessionSurface = {
isLegacyGroupSessionKey?: (key: string) => boolean;
@@ -83,14 +83,16 @@ type LegacySessionSurface = {
};
function getLegacySessionSurfaces(): LegacySessionSurface[] {
const surfaces: LegacySessionSurface[] = [];
for (const plugin of iterateBootstrapChannelPlugins()) {
// Legacy migrations run on cold doctor/startup paths. Prefer the narrower
// setup plugin surface here so session-key cleanup does not materialize full
// bundled channel runtimes.
cachedLegacySessionSurfaces ??= listBundledChannelSetupPluginsByFeature(
"legacySessionSurfaces",
).flatMap((plugin) => {
const surface = plugin.messaging;
if (surface && typeof surface === "object") {
surfaces.push(surface);
}
}
return surfaces;
return surface && typeof surface === "object" ? [surface] : [];
});
return cachedLegacySessionSurfaces;
}
function isSurfaceGroupKey(key: string): boolean {
@@ -419,6 +421,7 @@ function removeDirIfEmpty(dir: string) {
export function resetAutoMigrateLegacyStateForTest() {
autoMigrateChecked = false;
cachedLegacySessionSurfaces = null;
}
export function resetAutoMigrateLegacyAgentDirForTest() {
@@ -667,7 +670,9 @@ async function collectChannelLegacyStateMigrationPlans(params: {
oauthDir: string;
}): Promise<ChannelLegacyStateMigrationPlan[]> {
const plans: ChannelLegacyStateMigrationPlan[] = [];
for (const plugin of listBundledChannelPlugins()) {
// Legacy state detection belongs on the lightweight setup surface so doctor
// does not cold-load unrelated runtime channel code.
for (const plugin of listBundledChannelSetupPluginsByFeature("legacyStateMigrations")) {
const detected = await plugin.lifecycle?.detectLegacyStateMigrations?.({
cfg: params.cfg,
env: params.env,

View File

@@ -44,6 +44,12 @@ type DefineBundledChannelSetupEntryOptions = {
importMetaUrl: string;
plugin: BundledEntryModuleRef;
secrets?: BundledEntryModuleRef;
features?: BundledChannelSetupEntryFeatures;
};
export type BundledChannelSetupEntryFeatures = {
legacyStateMigrations?: boolean;
legacySessionSurfaces?: boolean;
};
export type BundledChannelEntryContract<TPlugin = ChannelPlugin> = {
@@ -62,6 +68,7 @@ export type BundledChannelSetupEntryContract<TPlugin = ChannelPlugin> = {
kind: "bundled-channel-setup-entry";
loadSetupPlugin: () => TPlugin;
loadSetupSecrets?: () => ChannelPlugin["secrets"] | undefined;
features?: BundledChannelSetupEntryFeatures;
};
const nodeRequire = createRequire(import.meta.url);
@@ -373,6 +380,7 @@ export function defineBundledChannelSetupEntry<TPlugin = ChannelPlugin>({
importMetaUrl,
plugin,
secrets,
features,
}: DefineBundledChannelSetupEntryOptions): BundledChannelSetupEntryContract<TPlugin> {
return {
kind: "bundled-channel-setup-entry",
@@ -386,5 +394,6 @@ export function defineBundledChannelSetupEntry<TPlugin = ChannelPlugin>({
),
}
: {}),
...(features ? { features } : {}),
};
}