fix(gateway): release channel startup gate before handoff (#126555)

This commit is contained in:
Peter Steinberger
2026-08-19 23:29:01 -07:00
committed by GitHub
parent a947730c66
commit bf03acef39
2 changed files with 59 additions and 19 deletions
+43 -1
View File
@@ -1842,6 +1842,45 @@ describe("startGatewayPostAttachRuntime", () => {
);
});
it("releases startup account starts before awaiting channel handoff", async () => {
const events: string[] = [];
let releaseAccountStarts!: () => void;
const accountStartsReady = new Promise<void>((resolve) => {
releaseAccountStarts = resolve;
});
const startChannels = vi.fn(async () => {
events.push("channels-start");
await accountStartsReady;
events.push("channels-end");
});
const onChannelsStarted = vi.fn(() => {
events.push("channels-released");
releaseAccountStarts();
});
const sidecars = startGatewaySidecars({
cfg: { hooks: { internal: { enabled: false } } } as never,
pluginRegistry: createPostAttachParams().pluginRegistry,
defaultWorkspaceDir: "/tmp/openclaw-workspace",
deps: {} as never,
startChannels,
onChannelsStarted,
log: { warn: vi.fn() },
logHooks: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
logChannels: { info: vi.fn(), error: vi.fn() },
});
await waitForGatewayTestState(() => {
expect(onChannelsStarted).toHaveBeenCalledOnce();
});
expect(events.slice(0, 2)).toEqual(["channels-start", "channels-released"]);
await sidecars;
expect(events).toEqual(["channels-start", "channels-released", "channels-end"]);
expect(startChannels).toHaveBeenCalledOnce();
expect(onChannelsStarted).toHaveBeenCalledOnce();
});
it("starts and reports plugin services after channel startup completes", async () => {
await withEnvAsync(
{ OPENCLAW_SKIP_CHANNELS: undefined, OPENCLAW_SKIP_PROVIDERS: undefined },
@@ -1898,8 +1937,8 @@ describe("startGatewayPostAttachRuntime", () => {
});
expect(events).toEqual([
"channels-start",
"channels-end",
"channels-started",
"channels-end",
"plugin-services",
]);
expect(onPluginServices).toHaveBeenCalledTimes(1);
@@ -2112,6 +2151,7 @@ describe("startGatewayPostAttachRuntime", () => {
const trace = createStartupTraceRecorder();
const logChannels = { info: vi.fn(), error: vi.fn() };
const prewarmPrimaryModel = vi.fn(async () => {});
const onChannelsStarted = vi.fn();
await withEnvAsync(
{ OPENCLAW_SKIP_CHANNELS: "1", OPENCLAW_SKIP_PROVIDERS: undefined },
@@ -2134,6 +2174,7 @@ describe("startGatewayPostAttachRuntime", () => {
logChannels,
startupTrace: trace.startupTrace,
prewarmPrimaryModel,
onChannelsStarted,
});
},
);
@@ -2149,6 +2190,7 @@ describe("startGatewayPostAttachRuntime", () => {
expect(logChannels.info).toHaveBeenCalledWith(
"skipping channel start (OPENCLAW_SKIP_CHANNELS=1 or OPENCLAW_SKIP_PROVIDERS=1)",
);
expect(onChannelsStarted).toHaveBeenCalledOnce();
});
it("records prepared runtime build grouping in the startup trace", async () => {
+16 -18
View File
@@ -675,26 +675,24 @@ export async function startGatewaySidecars(params: {
await measureStartup(params.startupTrace, "sidecars.chat-metadata", async () => {
await params.refreshChatMetadata?.();
});
const shouldStartChannels = params.shouldStartChannels?.() !== false;
await measureStartup(params.startupTrace, "sidecars.channels", async () => {
if (skipChannels) {
await measureStartup(params.startupTrace, "sidecars.channel-skip", () =>
params.logChannels.info(
"skipping channel start (OPENCLAW_SKIP_CHANNELS=1 or OPENCLAW_SKIP_PROVIDERS=1)",
),
);
} else if (params.shouldStartChannels?.() !== false) {
try {
await measureStartup(params.startupTrace, "sidecars.channel-start", () =>
params.startChannels(),
);
} catch (err) {
params.logChannels.error(`channel startup failed: ${String(err)}`);
}
}
const channelStart = skipChannels
? measureStartup(params.startupTrace, "sidecars.channel-skip", () =>
params.logChannels.info(
"skipping channel start (OPENCLAW_SKIP_CHANNELS=1 or OPENCLAW_SKIP_PROVIDERS=1)",
),
)
: shouldStartChannels
? measureStartup(params.startupTrace, "sidecars.channel-start", params.startChannels).catch(
(err: unknown) => params.logChannels.error(`channel startup failed: ${String(err)}`),
)
: Promise.resolve();
// Account tasks can depend on this generation gate, so release it after
// their handoff is prepared but before waiting for that handoff to settle.
const accountStartGateRelease = shouldStartChannels ? params.onChannelsStarted?.() : undefined;
await Promise.all([accountStartGateRelease, channelStart]);
});
if (params.shouldStartChannels?.() !== false) {
await params.onChannelsStarted?.();
}
const shouldStartPluginServices = params.shouldStartPluginServices?.() !== false;
let pluginServicesOwner: PluginServicesHandle | null = null;