mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 17:53:39 +00:00
fix(update): defer subagent recovery until activation (#162998)
This commit is contained in:
parent
26a3530288
commit
5cce0c99b0
6 changed files with 91 additions and 44 deletions
|
|
@ -199,8 +199,8 @@ classification. The live plugin files and host links stay unchanged. Channels,
|
|||
cron, automatic updates, and other side services are suppressed in this canary.
|
||||
The copied databases undergo the same schema checks and migrations without
|
||||
reviving the removed Tasks registry.
|
||||
The canary also defers session catalog hydration, worker recovery, and startup
|
||||
maintenance until activation, recording a warning. Required configuration,
|
||||
The canary also defers session catalog hydration, subagent and worker recovery,
|
||||
and startup maintenance until activation, recording a warning. Required configuration,
|
||||
database ownership, schema, and migration checks still run before readiness;
|
||||
plugin runtime loading remains part of validation. The serving Gateway prepares
|
||||
its session catalogs and maintenance normally after activation.
|
||||
|
|
|
|||
|
|
@ -490,8 +490,13 @@ export async function prepareGatewayServerBootstrap(input: {
|
|||
);
|
||||
}
|
||||
publishSystemEventStoreConfig(cfgAtStart);
|
||||
const pluginBootstrap = await startupTrace.measure("plugins.bootstrap", () =>
|
||||
prepareGatewayPluginBootstrap({
|
||||
const pluginBootstrap = await startupTrace.measure("plugins.bootstrap", async () => {
|
||||
if (!opts.updateCanary) {
|
||||
const { initSubagentRegistry } =
|
||||
await import("../agents/subagents/registry/subagent-registry.js");
|
||||
await initSubagentRegistry();
|
||||
}
|
||||
return prepareGatewayPluginBootstrap({
|
||||
cfgAtStart,
|
||||
activationSourceConfig: startupActivationSourceConfig,
|
||||
pluginMetadataSnapshot: startupConfigLoad.pluginMetadataSnapshot,
|
||||
|
|
@ -499,8 +504,8 @@ export async function prepareGatewayServerBootstrap(input: {
|
|||
minimalTestGateway,
|
||||
ambientEnvTriggers,
|
||||
log,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
const {
|
||||
gatewayPluginConfigAtStart,
|
||||
defaultWorkspaceDir,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@
|
|||
// ui-e2e suite that boots a minimal test gateway.
|
||||
import fs from "node:fs/promises";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
awaitGateBeforeSettlement,
|
||||
createDeferred,
|
||||
withinTest,
|
||||
} from "../../test/helpers/promise.js";
|
||||
import { resetConfigRuntimeState } from "../config/runtime-snapshot.js";
|
||||
import { readLoggingConfig } from "../logging/config.js";
|
||||
import { resetLogger } from "../logging/logger.js";
|
||||
|
|
@ -31,7 +36,7 @@ afterEach(() => {
|
|||
});
|
||||
|
||||
describe("gateway minimal boot smoke", () => {
|
||||
it("suppresses ambient channel triggers when the server option is omitted", async () => {
|
||||
it("suppresses ambient channel triggers and awaits subagent recovery", async ({ signal }) => {
|
||||
const port = await getFreePort();
|
||||
const state = await createOpenClawTestState({
|
||||
label: "gateway-bootstrap-ambient-default",
|
||||
|
|
@ -56,9 +61,29 @@ describe("gateway minimal boot smoke", () => {
|
|||
state.applyEnv();
|
||||
|
||||
try {
|
||||
const { prepareGatewayServerBootstrap } = await import("./server-startup-bootstrap.js");
|
||||
const [{ prepareGatewayServerBootstrap }, subagents, pluginContext] = await Promise.all([
|
||||
import("./server-startup-bootstrap.js"),
|
||||
import("../agents/subagents/registry/subagent-registry.js"),
|
||||
import("../plugins/runtime/load-context.js"),
|
||||
]);
|
||||
const enteredRecovery = createDeferred();
|
||||
const releaseRecovery = createDeferred();
|
||||
let recoveryComplete = false;
|
||||
const init = vi.spyOn(subagents, "initSubagentRegistry").mockImplementation(() => {
|
||||
enteredRecovery.resolve();
|
||||
return releaseRecovery.promise.then(() => {
|
||||
recoveryComplete = true;
|
||||
});
|
||||
});
|
||||
const publishPluginContext = pluginContext.setPluginRuntimeLoadContext;
|
||||
const publish = vi
|
||||
.spyOn(pluginContext, "setPluginRuntimeLoadContext")
|
||||
.mockImplementation((...args) => {
|
||||
expect(recoveryComplete).toBe(true);
|
||||
return publishPluginContext(...args);
|
||||
});
|
||||
const log = createSubsystemLogger("gateway/bootstrap-test");
|
||||
const bootstrap = await prepareGatewayServerBootstrap({
|
||||
const pendingBootstrap = prepareGatewayServerBootstrap({
|
||||
port,
|
||||
opts: {
|
||||
auth: { mode: "token", token },
|
||||
|
|
@ -72,13 +97,31 @@ describe("gateway minimal boot smoke", () => {
|
|||
await import("./server-worker-environment-startup.js"),
|
||||
formatRuntimeGatewayAuthTokenWarning: () => "unused",
|
||||
});
|
||||
|
||||
expect(bootstrap.ambientEnvTriggers).toBe("suppress");
|
||||
vi.stubEnv(
|
||||
"OPENCLAW_CONFIG_PATH",
|
||||
`/tmp/openclaw-bootstrap-missing-${process.pid}-${Date.now()}.json`,
|
||||
);
|
||||
expect(readLoggingConfig()).toMatchObject({ level: "debug" });
|
||||
try {
|
||||
await withinTest(
|
||||
awaitGateBeforeSettlement(
|
||||
enteredRecovery.promise,
|
||||
pendingBootstrap,
|
||||
"Gateway bootstrap did not prepare subagent recovery",
|
||||
),
|
||||
signal,
|
||||
);
|
||||
expect(publish).not.toHaveBeenCalled();
|
||||
releaseRecovery.resolve();
|
||||
const bootstrap = await withinTest(pendingBootstrap, signal);
|
||||
expect(publish).toHaveBeenCalled();
|
||||
expect(bootstrap.ambientEnvTriggers).toBe("suppress");
|
||||
vi.stubEnv(
|
||||
"OPENCLAW_CONFIG_PATH",
|
||||
`/tmp/openclaw-bootstrap-missing-${process.pid}-${Date.now()}.json`,
|
||||
);
|
||||
expect(readLoggingConfig()).toMatchObject({ level: "debug" });
|
||||
} finally {
|
||||
releaseRecovery.resolve();
|
||||
await pendingBootstrap.catch(() => {});
|
||||
init.mockRestore();
|
||||
publish.mockRestore();
|
||||
}
|
||||
} finally {
|
||||
await state.cleanup();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ const applyPluginAutoEnable = vi.hoisted(() =>
|
|||
autoEnabledReasons: {} as Record<string, string[]>,
|
||||
})),
|
||||
);
|
||||
const initSubagentRegistry = vi.hoisted(() => vi.fn());
|
||||
const getActivePluginRegistry = vi.hoisted(() => vi.fn<() => PluginRegistry | undefined>());
|
||||
const setActivePluginRegistry = vi.hoisted(() => vi.fn());
|
||||
const resolveProviderPolicySurfaceForOwner = vi.hoisted(() =>
|
||||
|
|
@ -141,10 +140,6 @@ vi.mock("../agents/workspace-state-dirs.js", () => ({
|
|||
assertConfiguredWorkspaceStateReady: () => {},
|
||||
}));
|
||||
|
||||
vi.mock("../agents/subagents/registry/subagent-registry.js", () => ({
|
||||
initSubagentRegistry: () => initSubagentRegistry(),
|
||||
}));
|
||||
|
||||
vi.mock("../channels/plugins/lifecycle-startup.js", () => ({
|
||||
runChannelPluginStartupMaintenance: (params: unknown) =>
|
||||
runChannelPluginStartupMaintenance(params),
|
||||
|
|
@ -316,7 +311,6 @@ describe("prepareGatewayPluginBootstrap startup plugins", () => {
|
|||
getActivePluginRegistry.mockReset();
|
||||
setActivePluginRegistry.mockClear();
|
||||
applyPluginAutoEnable.mockClear();
|
||||
initSubagentRegistry.mockClear();
|
||||
prepareGatewayPluginLoad.mockClear();
|
||||
listAmbientOnlyConfiguredChannelIds.mockClear().mockReturnValue([]);
|
||||
loadPluginLookUpTable.mockClear().mockReturnValue({
|
||||
|
|
@ -341,15 +335,6 @@ describe("prepareGatewayPluginBootstrap startup plugins", () => {
|
|||
expect(listLegacyPairingStoreFiles).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("hydrates the subagent registry before plugin bootstrap", async () => {
|
||||
await prepareBootstrapWithRuntimeConfig({});
|
||||
|
||||
expect(initSubagentRegistry).toHaveBeenCalledOnce();
|
||||
expect(initSubagentRegistry.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
loadPluginLookUpTable.mock.invocationCallOrder[0]!,
|
||||
);
|
||||
});
|
||||
|
||||
it("derives startup activation from source config instead of runtime plugin defaults", async () => {
|
||||
const sourceConfig = {
|
||||
channels: {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { tryResolveConfiguredAgentWorkspaceDir } from "../agents/agent-scope.js";
|
||||
import { initSubagentRegistry } from "../agents/subagents/registry/subagent-registry.js";
|
||||
import { resolveDefaultAgentWorkspaceDir } from "../agents/workspace-default.js";
|
||||
import type { AmbientEnvTriggerPolicy } from "../channels/config-presence.js";
|
||||
import { validateConfiguredBindings } from "../channels/plugins/configured-binding-registry.js";
|
||||
|
|
@ -136,7 +135,6 @@ export async function prepareGatewayPluginBootstrap(params: {
|
|||
ambientEnvTriggers?: AmbientEnvTriggerPolicy;
|
||||
}) {
|
||||
const activationSourceConfig = params.activationSourceConfig ?? params.cfgAtStart;
|
||||
await initSubagentRegistry();
|
||||
|
||||
// Activation uses the pre-runtime source so auto-enable policy cannot be skewed by
|
||||
// defaults injected while loading runtime config; runtime-only plugin config still merges in.
|
||||
|
|
|
|||
|
|
@ -64,17 +64,27 @@ beforeAll(async () => {
|
|||
},
|
||||
});
|
||||
|
||||
const [cliShim, githubCleanup, plugins, workers, projection, lifetime, discovery, skills] =
|
||||
await Promise.all([
|
||||
import("../infra/openclaw-cli-shim.js"),
|
||||
import("../agents/github-tool-profile-cleanup.js"),
|
||||
import("./server-startup-plugins.js"),
|
||||
import("./server-worker-environment-startup.js"),
|
||||
import("./session-row-projection.js"),
|
||||
import("./server-lifetime-sidecars.js"),
|
||||
import("./server-discovery-runtime.js"),
|
||||
import("../skills/runtime/remote.js"),
|
||||
]);
|
||||
const [
|
||||
cliShim,
|
||||
githubCleanup,
|
||||
plugins,
|
||||
workers,
|
||||
projection,
|
||||
lifetime,
|
||||
discovery,
|
||||
skills,
|
||||
subagents,
|
||||
] = await Promise.all([
|
||||
import("../infra/openclaw-cli-shim.js"),
|
||||
import("../agents/github-tool-profile-cleanup.js"),
|
||||
import("./server-startup-plugins.js"),
|
||||
import("./server-worker-environment-startup.js"),
|
||||
import("./session-row-projection.js"),
|
||||
import("./server-lifetime-sidecars.js"),
|
||||
import("./server-discovery-runtime.js"),
|
||||
import("../skills/runtime/remote.js"),
|
||||
import("../agents/subagents/registry/subagent-registry.js"),
|
||||
]);
|
||||
vi.spyOn(cliShim, "prepareGatewayAgentCliShim").mockImplementation(() =>
|
||||
rejectDeferredWork("agent CLI shim"),
|
||||
);
|
||||
|
|
@ -99,6 +109,12 @@ beforeAll(async () => {
|
|||
vi.spyOn(skills, "primeRemoteSkillsCache").mockImplementation(() =>
|
||||
rejectDeferredWork("remote skills warmup"),
|
||||
);
|
||||
vi.spyOn(subagents, "initSubagentRegistry").mockImplementation(() =>
|
||||
rejectDeferredWork("subagent recovery hydration"),
|
||||
);
|
||||
vi.spyOn(subagents, "activateSubagentRegistry").mockImplementation(() =>
|
||||
rejectDeferredWork("subagent recovery activation"),
|
||||
);
|
||||
const pluginLoad = vi.spyOn(plugins, "loadGatewayStartupPluginRuntime");
|
||||
await withAgentDatabaseStartupAdmission(async (admission) => {
|
||||
vi.spyOn(admission, "activate").mockImplementation(() =>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue