From 03ad3c06840822af7fcc9298c7e7964a4c8b5f24 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 4 May 2026 09:04:46 +0100 Subject: [PATCH] fix(gateway): log canvas host mount after bind --- CHANGELOG.md | 1 + src/gateway/server-runtime-state.test.ts | 42 +++++++++++++++++++++++- src/gateway/server-runtime-state.ts | 3 -- src/gateway/server.impl.ts | 6 ++++ 4 files changed, 48 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20ee0758b9b..9b595e180b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,7 @@ Docs: https://docs.openclaw.ai - Control UI/Talk: retry from a failed realtime Talk session on the next Talk click instead of requiring a separate stale-session stop click first. Thanks @vincentkoc. - Google Chat: create an isolated Google auth transport per auth client, so google-auth-library interceptor mutations do not accumulate across webhook verification and access-token clients. Thanks @vincentkoc. - Control UI/performance: cap long-task and long-animation-frame diagnostics in the shared event log, so slow-render telemetry does not evict gateway/plugin events from the Debug and Overview views. Thanks @vincentkoc. +- Gateway/startup: log the canvas host mount only after the HTTP server has bound, so startup logs no longer report the canvas host as mounted before it can serve requests. - Web fetch: late-bind `web_fetch` config and provider fallback metadata from the active runtime snapshot, matching `web_search` so long-lived tools do not use stale fetch provider settings. Thanks @vincentkoc. - Discord: clear stale startup probe bot/application status when the async bot probe throws, not just when it returns a degraded probe result. Thanks @vincentkoc. - Web search: scope explicit bundled `web_search` provider runtime loading through manifest ownership, so selecting DuckDuckGo/Gemini/etc. does not import unrelated bundled providers or log their optional dependency failures. Thanks @vincentkoc. diff --git a/src/gateway/server-runtime-state.test.ts b/src/gateway/server-runtime-state.test.ts index 3fb42b87b93..fe0f294d632 100644 --- a/src/gateway/server-runtime-state.test.ts +++ b/src/gateway/server-runtime-state.test.ts @@ -1,4 +1,7 @@ -import { afterEach, describe, expect, it } from "vitest"; +import { mkdtemp, rm } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { createEmptyPluginRegistry } from "../plugins/registry.js"; import { getActivePluginChannelRegistry, @@ -26,10 +29,13 @@ function createRegistryWithRoute(path: string) { } describe("createGatewayRuntimeState", () => { + const tempDirs: string[] = []; + afterEach(() => { releasePinnedPluginHttpRouteRegistry(); releasePinnedPluginChannelRegistry(); resetPluginRuntimeStateForTest(); + return Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); }); it("releases post-bootstrap repinned plugin registries on cleanup", async () => { @@ -70,4 +76,38 @@ describe("createGatewayRuntimeState", () => { expect(resolveActivePluginHttpRouteRegistry(fallbackRegistry)).toBe(startupRegistry); expect(getActivePluginChannelRegistry()).toBe(startupRegistry); }); + + it("creates the canvas host without logging it before HTTP bind", async () => { + const root = await mkdtemp(path.join(os.tmpdir(), "openclaw-canvas-runtime-")); + tempDirs.push(root); + const registry = createEmptyPluginRegistry(); + const logCanvas = { info: vi.fn(), warn: vi.fn() }; + + const runtimeState = await createGatewayRuntimeState({ + cfg: { canvasHost: { root, liveReload: false } }, + bindHost: "127.0.0.1", + port: 18789, + controlUiEnabled: false, + controlUiBasePath: "/", + openAiChatCompletionsEnabled: false, + openResponsesEnabled: false, + resolvedAuth: {} as never, + getResolvedAuth: () => ({}) as never, + hooksConfig: () => null, + getHookClientIpConfig: () => ({}) as never, + pluginRegistry: registry, + deps: {} as never, + canvasRuntime: { log: () => {} } as never, + canvasHostEnabled: true, + allowCanvasHostInTests: true, + logCanvas, + log: { info: () => {}, warn: () => {} }, + logHooks: { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} } as never, + logPlugins: { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} } as never, + }); + + expect(runtimeState.canvasHost?.rootDir).toBe(root); + expect(logCanvas.info).not.toHaveBeenCalled(); + await runtimeState.canvasHost?.close(); + }); }); diff --git a/src/gateway/server-runtime-state.ts b/src/gateway/server-runtime-state.ts index d1d9b05bf61..0b1b624c137 100644 --- a/src/gateway/server-runtime-state.ts +++ b/src/gateway/server-runtime-state.ts @@ -130,9 +130,6 @@ export async function createGatewayRuntimeState(params: { }); if (handler.rootDir) { canvasHost = handler; - params.logCanvas.info( - `canvas host mounted at http://${params.bindHost}:${params.port}${CANVAS_HOST_PATH}/ (root ${handler.rootDir})`, - ); } } catch (err) { params.logCanvas.warn(`canvas host failed to start: ${String(err)}`); diff --git a/src/gateway/server.impl.ts b/src/gateway/server.impl.ts index d4ecdad7128..23972f23e18 100644 --- a/src/gateway/server.impl.ts +++ b/src/gateway/server.impl.ts @@ -1,6 +1,7 @@ import { monitorEventLoopDelay, performance } from "node:perf_hooks"; import { getActiveEmbeddedRunCount } from "../agents/pi-embedded-runner/run-state.js"; import { getTotalPendingReplies } from "../auto-reply/reply/dispatcher-registry.js"; +import { CANVAS_HOST_PATH } from "../canvas-host/a2ui-shared.js"; import type { CanvasHostServer } from "../canvas-host/server.js"; import type { ChannelRuntimeSurface } from "../channels/plugins/channel-runtime-surface.types.js"; import { @@ -1354,6 +1355,11 @@ export async function startGatewayServer( context: gatewayRequestContext, }); await startListening(); + if (canvasHost?.rootDir) { + logCanvas.info( + `canvas host mounted at http://${bindHost}:${port}${CANVAS_HOST_PATH}/ (root ${canvasHost.rootDir})`, + ); + } startupTrace.mark("http.bound"); const sessionDeliveryRecoveryMaxEnqueuedAt = Date.now(); let postAttachRuntimeReturned = false;