From 8585e7e04eef67c43e49a3db290d6d8e4f709ae3 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 26 Aug 2026 12:55:48 -0700 Subject: [PATCH] fix(daemon): recovery hints target the wrong profile or container (#130216) * fix(daemon): preserve profile context in systemd recovery hints * fix(cli): remove obsolete daemon container helper export --- src/cli/daemon-cli/shared.test.ts | 14 ------------- src/cli/daemon-cli/shared.ts | 1 - src/cli/daemon-cli/status.print.test.ts | 3 +-- src/cli/daemon-cli/status.print.ts | 4 +--- src/commands/doctor-format.test.ts | 28 +++++++++++++++++++++++++ src/commands/doctor-format.ts | 4 +--- src/daemon/systemd-hints.test.ts | 5 +++-- src/daemon/systemd-hints.ts | 7 ++++--- 8 files changed, 38 insertions(+), 28 deletions(-) diff --git a/src/cli/daemon-cli/shared.test.ts b/src/cli/daemon-cli/shared.test.ts index 0d76f8bece18..7bdc3e99423e 100644 --- a/src/cli/daemon-cli/shared.test.ts +++ b/src/cli/daemon-cli/shared.test.ts @@ -6,7 +6,6 @@ import { parsePortFromArgs, renderRuntimeHints, renderGatewayServiceStartHints, - resolveDaemonContainerContext, resolveRuntimeStatusColor, } from "./shared.js"; @@ -61,19 +60,6 @@ describe("renderGatewayServiceStartHints", () => { ).toContain("logged-in macOS GUI session"); }); - it("resolves daemon container context from either env key", () => { - expect( - resolveDaemonContainerContext({ - OPENCLAW_CONTAINER: "openclaw-demo-container", - } as NodeJS.ProcessEnv), - ).toBe("openclaw-demo-container"); - expect( - resolveDaemonContainerContext({ - OPENCLAW_CONTAINER_HINT: "openclaw-demo-container", - } as NodeJS.ProcessEnv), - ).toBe("openclaw-demo-container"); - }); - it("prepends a single container restart hint when OPENCLAW_CONTAINER is set", () => { expect( renderGatewayServiceStartHints({ diff --git a/src/cli/daemon-cli/shared.ts b/src/cli/daemon-cli/shared.ts index 1c0deeed446a..739c61032358 100644 --- a/src/cli/daemon-cli/shared.ts +++ b/src/cli/daemon-cli/shared.ts @@ -19,7 +19,6 @@ import { createDaemonActionContext } from "./response.js"; export { formatRuntimeStatus }; export { parsePort }; -export { resolveDaemonContainerContext }; /** Create install action context with JSON flag normalization. */ export function createDaemonInstallActionContext(jsonFlag: unknown) { diff --git a/src/cli/daemon-cli/status.print.test.ts b/src/cli/daemon-cli/status.print.test.ts index c53fbbcdcad8..e8e8fd4aa138 100644 --- a/src/cli/daemon-cli/status.print.test.ts +++ b/src/cli/daemon-cli/status.print.test.ts @@ -107,7 +107,6 @@ vi.mock("./shared.js", () => ({ filterDaemonEnv: () => ({}), formatRuntimeStatus: () => "running (pid 8000)", resolveRuntimeStatusColor: () => "", - resolveDaemonContainerContext: () => null, renderRuntimeHints: () => [], safeDaemonEnv: () => [], })); @@ -475,7 +474,7 @@ describe("printDaemonStatus", () => { expect(renderSystemdUnavailableHintsMock).toHaveBeenCalledWith({ wsl: true, kind: "generic_unavailable", - container: false, + env: { WSL_DISTRO_NAME: "Ubuntu" }, }); expectMockLineContains(runtime.log, "Service: systemd (unknown)"); expect(runtime.log.mock.calls.flat().join("\n")).not.toContain("Service: systemd (not loaded)"); diff --git a/src/cli/daemon-cli/status.print.ts b/src/cli/daemon-cli/status.print.ts index 349b88afe6e9..7cbb614d022a 100644 --- a/src/cli/daemon-cli/status.print.ts +++ b/src/cli/daemon-cli/status.print.ts @@ -29,7 +29,6 @@ import { createCliStatusTextStyles, filterDaemonEnv, formatRuntimeStatus, - resolveDaemonContainerContext, resolveRuntimeStatusColor, renderRuntimeHints, safeDaemonEnv, @@ -407,12 +406,11 @@ export function printDaemonStatus(status: DaemonStatus, opts: { json: boolean; d isSystemdUnavailableDetail(systemdUnavailableDetail); if (systemdUnavailable) { const serviceEnv = service.command?.environment ?? process.env; - const container = Boolean(resolveDaemonContainerContext(serviceEnv)); defaultRuntime.error(errorText("systemd user services unavailable.")); for (const hint of renderSystemdUnavailableHints({ wsl: isWSLEnv(serviceEnv), kind: classifySystemdUnavailableDetail(systemdUnavailableDetail), - container, + env: serviceEnv, })) { defaultRuntime.error(errorText(hint)); } diff --git a/src/commands/doctor-format.test.ts b/src/commands/doctor-format.test.ts index befd596b1bc7..cd220972fb2d 100644 --- a/src/commands/doctor-format.test.ts +++ b/src/commands/doctor-format.test.ts @@ -58,6 +58,34 @@ describe("buildGatewayRuntimeHints", () => { expect(hints.join("\n")).not.toContain("systemd user services are unavailable"); }); + it.each([ + { + env: { OPENCLAW_PROFILE: "blue" }, + command: "openclaw --profile blue gateway", + }, + { + env: { OPENCLAW_CONTAINER_HINT: "sandbox" }, + command: "openclaw --container sandbox gateway", + }, + { + env: { OPENCLAW_PROFILE: "blue", OPENCLAW_CONTAINER_HINT: "sandbox" }, + command: "openclaw --container sandbox gateway", + }, + ])("preserves the active target in systemd recovery commands: $command", ({ env, command }) => { + const hints = buildGatewayRuntimeHints( + { + status: "unknown", + detail: "systemctl --user unavailable: Failed to connect to bus", + }, + { platform: "linux", env }, + ); + + expect(hints.some((hint) => hint.includes(command))).toBe(true); + expect(hints.some((hint) => hint.includes("headless server"))).toBe( + !env.OPENCLAW_CONTAINER_HINT, + ); + }); + it("guides recovery when systemd hit its restart start limit (crash loop)", () => { // Real give-up shape: process kept failing (Result=exit-code) until NRestarts // reached StartLimitBurst and systemd stopped restarting. diff --git a/src/commands/doctor-format.ts b/src/commands/doctor-format.ts index aef761bc1a06..da462b332c63 100644 --- a/src/commands/doctor-format.ts +++ b/src/commands/doctor-format.ts @@ -5,7 +5,6 @@ import { resolveGatewaySystemdServiceName, resolveGatewayWindowsTaskName, } from "../daemon/constants.js"; -import { resolveDaemonContainerContext } from "../daemon/container-context.js"; import { formatRuntimeStatus } from "../daemon/runtime-format.js"; import { buildPlatformRuntimeLogHints } from "../daemon/runtime-hints.js"; import { @@ -45,7 +44,6 @@ export function buildGatewayRuntimeHints( } const platform = options.platform ?? process.platform; const env = options.env ?? process.env; - const container = Boolean(resolveDaemonContainerContext(env)); const fileLog = (() => { try { return getResolvedLoggerSettings().file; @@ -58,7 +56,7 @@ export function buildGatewayRuntimeHints( ...renderSystemdUnavailableHints({ wsl: isWSLEnv(env), kind: classifySystemdUnavailableDetail(runtime.detail), - container, + env, }), ); if (fileLog) { diff --git a/src/daemon/systemd-hints.test.ts b/src/daemon/systemd-hints.test.ts index 40cfc49e96a0..69abaf16c0e3 100644 --- a/src/daemon/systemd-hints.test.ts +++ b/src/daemon/systemd-hints.test.ts @@ -49,14 +49,15 @@ describe("renderSystemdUnavailableHints", () => { }); it("skips headless recovery hints when container context is known", () => { + const env = { OPENCLAW_CONTAINER_HINT: "sandbox" }; expect( renderSystemdUnavailableHints({ kind: "user_bus_unavailable", - container: true, + env, }), ).toEqual([ "systemd user services are unavailable; install/enable systemd or run the gateway under your supervisor.", - `If you're in a container, run the gateway in the foreground instead of \`${formatCliCommand("openclaw gateway")}\`.`, + `If you're in a container, run the gateway in the foreground instead of \`${formatCliCommand("openclaw gateway", env)}\`.`, ]); }); }); diff --git a/src/daemon/systemd-hints.ts b/src/daemon/systemd-hints.ts index 23e33401135d..bad7c30ec0f4 100644 --- a/src/daemon/systemd-hints.ts +++ b/src/daemon/systemd-hints.ts @@ -1,5 +1,6 @@ /** Renders Linux systemd availability hints for gateway service commands. */ import { formatCliCommand } from "../cli/command-format.js"; +import { resolveDaemonContainerContext } from "./container-context.js"; import { classifySystemdUnavailableDetail, type SystemdUnavailableKind, @@ -8,7 +9,7 @@ import { type SystemdUnavailableHintOptions = { wsl?: boolean; kind?: SystemdUnavailableKind | null; - container?: boolean; + env?: Record; }; /** Detects details that should get systemd availability repair hints. */ @@ -36,9 +37,9 @@ export function renderSystemdUnavailableHints( } return [ "systemd user services are unavailable; install/enable systemd or run the gateway under your supervisor.", - ...(options.container || options.kind !== "user_bus_unavailable" + ...(resolveDaemonContainerContext(options.env) || options.kind !== "user_bus_unavailable" ? [] : renderSystemdHeadlessServerHints()), - `If you're in a container, run the gateway in the foreground instead of \`${formatCliCommand("openclaw gateway")}\`.`, + `If you're in a container, run the gateway in the foreground instead of \`${formatCliCommand("openclaw gateway", options.env)}\`.`, ]; }