Skip to content

Commit 843e3c7

Browse files
authored
improve(gateway): speed up pristine plugin-heavy startup [AI] (#106195)
* perf(gateway): skip absent core startup migrations * fix(gateway): revalidate recovered startup config
1 parent 938d410 commit 843e3c7

12 files changed

Lines changed: 289 additions & 18 deletions

src/cli/command-bootstrap.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,25 @@ describe("ensureCliCommandBootstrap", () => {
4444
});
4545
});
4646

47+
it("forwards prepared pristine migration facts to the config guard", async () => {
48+
const runtime = {} as never;
49+
50+
await ensureCliCommandBootstrap({
51+
runtime,
52+
commandPath: ["gateway"],
53+
loadPlugins: false,
54+
skipPristineCoreStateMigrations: true,
55+
skipPristineStartupStateMigrations: true,
56+
});
57+
58+
expect(ensureConfigReadyMock).toHaveBeenCalledWith({
59+
runtime,
60+
commandPath: ["gateway"],
61+
skipPristineCoreStateMigrations: true,
62+
skipPristineStartupStateMigrations: true,
63+
});
64+
});
65+
4766
it("skips config guard without skipping plugin loading", async () => {
4867
await ensureCliCommandBootstrap({
4968
runtime: {} as never,

src/cli/command-bootstrap.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export async function ensureCliCommandBootstrap(params: {
2222
beforeStateMigrations?: (snapshot?: ConfigFileSnapshot) => Promise<boolean>;
2323
loadPlugins?: boolean;
2424
pluginRegistry?: CliPluginRegistryPolicy;
25+
skipPristineCoreStateMigrations?: boolean;
2526
skipPristineStartupStateMigrations?: boolean;
2627
}) {
2728
if (!params.skipConfigGuard) {
@@ -37,6 +38,7 @@ export async function ensureCliCommandBootstrap(params: {
3738
...(params.skipPristineStartupStateMigrations
3839
? { skipPristineStartupStateMigrations: true }
3940
: {}),
41+
...(params.skipPristineCoreStateMigrations ? { skipPristineCoreStateMigrations: true } : {}),
4042
});
4143
}
4244
if (!params.loadPlugins) {

src/cli/command-execution-startup.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,8 @@ describe("command-execution-startup", () => {
217217
},
218218
allowInvalid: true,
219219
loadPlugins: true,
220+
skipPristineCoreStateMigrations: true,
221+
skipPristineStartupStateMigrations: true,
220222
});
221223

222224
expect(ensureCliCommandBootstrapMock).toHaveBeenLastCalledWith({
@@ -227,6 +229,8 @@ describe("command-execution-startup", () => {
227229
loadPlugins: true,
228230
pluginRegistry: { scope: "all" },
229231
skipConfigGuard: false,
232+
skipPristineCoreStateMigrations: true,
233+
skipPristineStartupStateMigrations: true,
230234
});
231235
});
232236
});

src/cli/command-execution-startup.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export async function ensureCliExecutionBootstrap(params: {
7171
beforeStateMigrations?: (snapshot?: ConfigFileSnapshot) => Promise<boolean>;
7272
loadPlugins?: boolean;
7373
skipConfigGuard?: boolean;
74+
skipPristineCoreStateMigrations?: boolean;
7475
skipPristineStartupStateMigrations?: boolean;
7576
}) {
7677
await ensureCliCommandBootstrap({
@@ -87,5 +88,6 @@ export async function ensureCliExecutionBootstrap(params: {
8788
...(params.skipPristineStartupStateMigrations
8889
? { skipPristineStartupStateMigrations: true }
8990
: {}),
91+
...(params.skipPristineCoreStateMigrations ? { skipPristineCoreStateMigrations: true } : {}),
9092
});
9193
}

src/cli/gateway-cli/pre-bootstrap.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ let appliedGatewayRunConfigEnvironment: GatewayRunEnvironmentSelection | undefin
2929
let lastGuardedGatewayRunSnapshot: ConfigFileSnapshot | undefined;
3030
let preparedGatewayRunBootstrapSnapshot: ConfigFileSnapshot | undefined;
3131
let preparedGatewayRunStateWasPristine = false;
32+
let preparedGatewayRunCoreStateWasPristine = false;
3233
let preparedGatewayRunReset: PreparedGatewayRunReset | undefined;
3334
let gatewayRunTargetSelectedByConfig = false;
3435

@@ -623,10 +624,11 @@ export async function selectGatewayRunEnvironment(params: GatewayRunGuardParams)
623624
export async function prepareGatewayRunBootstrap(params: GatewayRunGuardParams): Promise<boolean> {
624625
preparedGatewayRunReset = undefined;
625626
preparedGatewayRunStateWasPristine = false;
627+
preparedGatewayRunCoreStateWasPristine = false;
626628
const pristineSelectionSignature = resolveGatewayConfigSelectionSignature(process.env);
627-
const { canSkipPristineStartupStateMigrations } =
629+
const { planPristineStartupConfigMigrations, planPristineStartupStateMigrations } =
628630
await import("../../commands/doctor/shared/pristine-startup-state.js");
629-
const selectedStateWasPristine = canSkipPristineStartupStateMigrations(process.env);
631+
const pristineStatePlan = planPristineStartupStateMigrations(process.env);
630632
// Stop the early proxy before recovery can select another config/state target. Its lifecycle
631633
// restores the underlying env snapshot so the selected target's trusted dotenv can replace it.
632634
await getGatewayRunRuntimeHooks().releaseManagedProxy?.();
@@ -643,10 +645,23 @@ export async function prepareGatewayRunBootstrap(params: GatewayRunGuardParams):
643645
recoverSuspicious: true,
644646
restoreSuspicious: true,
645647
});
648+
// Recovery can replace config without changing its selected path. Revalidate the final authored
649+
// file while retaining the pre-guard physical-state fact, or stateful backup config could skip.
650+
const guardedConfigPlan = planPristineStartupConfigMigrations(
651+
guarded ? lastGuardedGatewayRunSnapshot?.parsed : undefined,
652+
process.env,
653+
);
646654
preparedGatewayRunStateWasPristine =
647655
guarded &&
648656
!params.opts.reset &&
649-
selectedStateWasPristine &&
657+
pristineStatePlan.skipAllStateMigrations &&
658+
guardedConfigPlan.skipAllStateMigrations &&
659+
resolveGatewayConfigSelectionSignature(process.env) === pristineSelectionSignature;
660+
preparedGatewayRunCoreStateWasPristine =
661+
guarded &&
662+
!params.opts.reset &&
663+
pristineStatePlan.skipCoreStateMigrations &&
664+
guardedConfigPlan.skipCoreStateMigrations &&
650665
resolveGatewayConfigSelectionSignature(process.env) === pristineSelectionSignature;
651666
await pinGatewayRunRuntimePaths();
652667
// Dev reset deletes the state directory before recreating config. Migrating first would
@@ -668,6 +683,11 @@ export function wasPreparedGatewayRunStatePristine(): boolean {
668683
return preparedGatewayRunStateWasPristine;
669684
}
670685

686+
/** Prepared fact keeps plugin-only configs out of unrelated core migration discovery. */
687+
export function wasPreparedGatewayRunCoreStatePristine(): boolean {
688+
return preparedGatewayRunCoreStateWasPristine;
689+
}
690+
671691
export async function recheckGatewayRunBootstrap(
672692
params: GatewayRunGuardParams & { snapshot?: ConfigFileSnapshot },
673693
): Promise<boolean> {

src/cli/gateway-cli/run.option-collisions.test.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ const configState = vi.hoisted(() => ({
6565
cfg: {} as Record<string, unknown>,
6666
snapshot: { config: {}, exists: false, sourceConfig: {}, valid: true } as Record<string, unknown>,
6767
}));
68+
const pristineStartupMigrationPlan = vi.hoisted(() => ({
69+
config: vi.fn(),
70+
state: vi.fn(),
71+
}));
6872
const readBestEffortConfig = vi.fn(async () => configState.cfg);
6973
type ConfigSnapshotReadOptionsStub = {
7074
isolateEnv?: boolean;
@@ -138,6 +142,13 @@ vi.mock("../../config/config.js", () => ({
138142
readConfigFileSnapshotWithPluginMetadata(options),
139143
}));
140144

145+
vi.mock("../../commands/doctor/shared/pristine-startup-state.js", () => ({
146+
planPristineStartupConfigMigrations: (config: unknown, env?: NodeJS.ProcessEnv) =>
147+
pristineStartupMigrationPlan.config(config, env),
148+
planPristineStartupStateMigrations: (env?: NodeJS.ProcessEnv) =>
149+
pristineStartupMigrationPlan.state(env),
150+
}));
151+
141152
vi.mock("../../config/paths.js", () => ({
142153
CONFIG_PATH: "/tmp/openclaw-test-missing-config.json",
143154
normalizeStateDirEnv: (env?: NodeJS.ProcessEnv) => normalizeStateDirEnv(env),
@@ -342,6 +353,16 @@ describe("gateway run option collisions", () => {
342353
resetRuntimeCapture();
343354
configState.cfg = {};
344355
configState.snapshot = { config: {}, exists: false, sourceConfig: {}, valid: true };
356+
pristineStartupMigrationPlan.config.mockReset();
357+
pristineStartupMigrationPlan.config.mockReturnValue({
358+
skipAllStateMigrations: false,
359+
skipCoreStateMigrations: false,
360+
});
361+
pristineStartupMigrationPlan.state.mockReset();
362+
pristineStartupMigrationPlan.state.mockReturnValue({
363+
skipAllStateMigrations: false,
364+
skipCoreStateMigrations: false,
365+
});
345366
netState.autoBindHost = "127.0.0.1";
346367
netState.container = false;
347368
readBestEffortConfig.mockClear();
@@ -425,6 +446,50 @@ describe("gateway run option collisions", () => {
425446
expect(callOrder).toEqual(["bootstrap", "normalize", "normalize", "start"]);
426447
});
427448

449+
it("drops the pristine core fact when guarded config becomes stateful", async () => {
450+
const initialConfig = {
451+
gateway: { mode: "local" },
452+
plugins: { load: { paths: ["/plugins/example"] } },
453+
};
454+
configState.snapshot = {
455+
config: initialConfig,
456+
exists: true,
457+
hash: "initial",
458+
parsed: initialConfig,
459+
path: "/tmp/openclaw.json",
460+
sourceConfig: initialConfig,
461+
valid: true,
462+
};
463+
pristineStartupMigrationPlan.state.mockReturnValue({
464+
skipAllStateMigrations: false,
465+
skipCoreStateMigrations: true,
466+
});
467+
const {
468+
prepareGatewayRunBootstrap,
469+
selectGatewayRunEnvironment,
470+
wasPreparedGatewayRunCoreStatePristine,
471+
} = await import("./pre-bootstrap.js");
472+
473+
expect(await selectGatewayRunEnvironment({ opts: {}, runtime: defaultRuntime })).toBe(true);
474+
const recoveredConfig = {
475+
gateway: { mode: "local" },
476+
session: { store: "/tmp/sessions.json" },
477+
};
478+
configState.snapshot = {
479+
config: recoveredConfig,
480+
exists: true,
481+
hash: "recovered",
482+
parsed: recoveredConfig,
483+
path: "/tmp/openclaw.json",
484+
sourceConfig: recoveredConfig,
485+
valid: true,
486+
};
487+
488+
expect(await prepareGatewayRunBootstrap({ opts: {}, runtime: defaultRuntime })).toBe(true);
489+
expect(wasPreparedGatewayRunCoreStatePristine()).toBe(false);
490+
expect(pristineStartupMigrationPlan.config).toHaveBeenCalledWith(recoveredConfig, process.env);
491+
});
492+
428493
it("refreshes the managed proxy from the final accepted config before gateway startup", async () => {
429494
const finalConfig = {
430495
gateway: { mode: "local" },

src/cli/program/config-guard.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ export async function ensureConfigReady(params: {
215215
suppressDoctorStdout?: boolean;
216216
allowInvalid?: boolean;
217217
beforeStateMigrations?: (snapshot?: ConfigFileSnapshot) => Promise<boolean>;
218+
skipPristineCoreStateMigrations?: boolean;
218219
skipPristineStartupStateMigrations?: boolean;
219220
}): Promise<void> {
220221
const commandPath = params.commandPath ?? [];
@@ -241,6 +242,9 @@ export async function ensureConfigReady(params: {
241242
...(params.skipPristineStartupStateMigrations
242243
? { skipPristineStartupStateMigrations: true }
243244
: {}),
245+
...(params.skipPristineCoreStateMigrations
246+
? { skipPristineCoreStateMigrations: true }
247+
: {}),
244248
});
245249
try {
246250
return !params.suppressDoctorStdout

src/cli/run-main.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,15 +183,18 @@ async function tryRunGatewayRunFastPath(
183183
const beforeRun = async (opts: { force?: boolean; reset?: boolean }) => {
184184
let beforeStateMigrations: ((snapshot?: ConfigFileSnapshot) => Promise<boolean>) | undefined;
185185
let skipPristineStartupStateMigrations = false;
186+
let skipPristineCoreStateMigrations = false;
186187
const shouldBootstrap = await startupTrace.measure("gateway-run-pre-bootstrap", async () => {
187188
const {
188189
prepareGatewayRunBootstrap,
189190
recheckGatewayRunBootstrap,
191+
wasPreparedGatewayRunCoreStatePristine,
190192
wasPreparedGatewayRunStatePristine,
191193
} = await import("./gateway-cli/pre-bootstrap.js");
192194
const prepared = await prepareGatewayRunBootstrap({ opts, runtime: defaultRuntime });
193195
if (prepared) {
194196
skipPristineStartupStateMigrations = wasPreparedGatewayRunStatePristine();
197+
skipPristineCoreStateMigrations = wasPreparedGatewayRunCoreStatePristine();
195198
beforeStateMigrations = (snapshot) =>
196199
recheckGatewayRunBootstrap({
197200
opts,
@@ -212,6 +215,7 @@ async function tryRunGatewayRunFastPath(
212215
loadPlugins: false,
213216
...(beforeStateMigrations ? { beforeStateMigrations } : {}),
214217
...(skipPristineStartupStateMigrations ? { skipPristineStartupStateMigrations: true } : {}),
218+
...(skipPristineCoreStateMigrations ? { skipPristineCoreStateMigrations: true } : {}),
215219
});
216220
const { reloadTrustedGatewayRunEnvironment } = await import("./gateway-cli/pre-bootstrap.js");
217221
await reloadTrustedGatewayRunEnvironment({ runtime: defaultRuntime });

src/commands/doctor-config-preflight.state-migration.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,12 @@ const runPostCorePluginConvergence = vi.hoisted(() =>
9494
const planStartupPluginConvergence = vi.hoisted(() =>
9595
vi.fn(async () => ({ required: true, installRecords: {} })),
9696
);
97+
const planPristineStartupStateMigrations = vi.hoisted(() =>
98+
vi.fn(() => ({
99+
skipAllStateMigrations: false,
100+
skipCoreStateMigrations: false,
101+
})),
102+
);
97103
const makeStartupConvergenceResult = vi.hoisted(
98104
() =>
99105
(overrides: Partial<StartupConvergenceResult> = {}): StartupConvergenceResult => ({
@@ -145,6 +151,10 @@ vi.mock("./doctor/shared/startup-plugin-convergence-plan.js", () => ({
145151
planStartupPluginConvergence,
146152
}));
147153

154+
vi.mock("./doctor/shared/pristine-startup-state.js", () => ({
155+
planPristineStartupStateMigrations,
156+
}));
157+
148158
vi.mock("../config/io.js", () => ({
149159
readConfigFileSnapshot,
150160
recoverConfigFromJsonRootSuffix: vi.fn(),
@@ -161,6 +171,10 @@ describe("runDoctorConfigPreflight state migration", () => {
161171
needsStartupMigrationCheckpoint.mockReturnValue(false);
162172
runPostCorePluginConvergence.mockResolvedValue(makeStartupConvergenceResult());
163173
planStartupPluginConvergence.mockResolvedValue({ required: true, installRecords: {} });
174+
planPristineStartupStateMigrations.mockReturnValue({
175+
skipAllStateMigrations: false,
176+
skipCoreStateMigrations: false,
177+
});
164178
autoMigrateLegacyStateDir.mockResolvedValue({
165179
migrated: false,
166180
skipped: false,
@@ -462,6 +476,43 @@ describe("runDoctorConfigPreflight state migration", () => {
462476
expect(recordSuccessfulStartupMigrations).toHaveBeenCalledOnce();
463477
});
464478

479+
it("runs only plugin-owned migrations for a pristine core state root", async () => {
480+
needsStartupMigrationCheckpoint.mockReturnValue(true);
481+
planPristineStartupStateMigrations.mockReturnValueOnce({
482+
skipAllStateMigrations: false,
483+
skipCoreStateMigrations: true,
484+
});
485+
486+
await runDoctorConfigPreflight({
487+
migrateLegacyConfig: false,
488+
invalidConfigNote: false,
489+
requireStartupMigrationCheckpoint: true,
490+
});
491+
492+
expect(autoMigrateLegacyStateDir).toHaveBeenCalledOnce();
493+
expect(repairLegacyCronStoreWithoutPrompt).not.toHaveBeenCalled();
494+
expect(autoMigrateLegacyState).not.toHaveBeenCalled();
495+
expect(autoMigrateLegacyTaskStateSidecars).not.toHaveBeenCalled();
496+
expect(autoMigrateLegacyPluginDoctorState).toHaveBeenCalledWith({
497+
config: { gateway: { mode: "local", port: 19091 } },
498+
env: process.env,
499+
});
500+
});
501+
502+
it("retains the prepared core-state fact after runtime files appear", async () => {
503+
needsStartupMigrationCheckpoint.mockReturnValue(true);
504+
505+
await runDoctorConfigPreflight({
506+
migrateLegacyConfig: false,
507+
invalidConfigNote: false,
508+
requireStartupMigrationCheckpoint: true,
509+
skipPristineCoreStateMigrations: true,
510+
});
511+
512+
expect(autoMigrateLegacyState).not.toHaveBeenCalled();
513+
expect(autoMigrateLegacyPluginDoctorState).toHaveBeenCalledOnce();
514+
});
515+
465516
it("blocks gateway readiness when startup migrations leave warnings", async () => {
466517
needsStartupMigrationCheckpoint.mockReturnValue(true);
467518
autoMigrateLegacyStateDir.mockResolvedValueOnce({

0 commit comments

Comments
 (0)