From f6a3ac7e58aef0ce3c30bac3fde8fe58813ee2f0 Mon Sep 17 00:00:00 2001
From: Alix-007
Date: Tue, 16 Jun 2026 00:09:41 +0800
Subject: [PATCH] fix(daemon): prefer stderr over stale stdout in gateway
restart diagnostics
Refs #93001.
---
src/daemon/diagnostics.test.ts | 17 +++++++++++++++++
src/daemon/diagnostics.ts | 5 ++++-
2 files changed, 21 insertions(+), 1 deletion(-)
diff --git a/src/daemon/diagnostics.test.ts b/src/daemon/diagnostics.test.ts
index 70da0f80b05d..b7421c2935c4 100644
--- a/src/daemon/diagnostics.test.ts
+++ b/src/daemon/diagnostics.test.ts
@@ -36,4 +36,21 @@ describe("readLastGatewayErrorLine", () => {
"gateway stdout current",
);
});
+
+ it("prefers the current stderr error over a stale stdout match on linux", async () => {
+ const stateDir = makeTempStateDir();
+ const homeDir = makeTempStateDir();
+ const env = { HOME: homeDir, OPENCLAW_STATE_DIR: stateDir };
+ const stateLogs = resolveGatewayLogPaths(env);
+ fs.mkdirSync(stateLogs.logDir, { recursive: true });
+ // stderr carries the real, current failure; stdout carries an older matching
+ // line. On non-darwin platforms stderr is the strongest failure signal, so
+ // it must win instead of the stale stdout match.
+ fs.writeFileSync(stateLogs.stderrPath, "failed to bind gateway socket EADDRINUSE\n", "utf8");
+ fs.writeFileSync(stateLogs.stdoutPath, "gateway start blocked: stale prior reason\n", "utf8");
+
+ await expect(readLastGatewayErrorLine(env, { platform: "linux" })).resolves.toBe(
+ "failed to bind gateway socket EADDRINUSE",
+ );
+ });
});
diff --git a/src/daemon/diagnostics.ts b/src/daemon/diagnostics.ts
index fc654a7f5c85..11bb0f64e0b8 100644
--- a/src/daemon/diagnostics.ts
+++ b/src/daemon/diagnostics.ts
@@ -40,7 +40,10 @@ export async function readLastGatewayErrorLine(
: resolveGatewayLogPaths(env);
const stderrRaw = readStderr ? await fs.readFile(stderrPath, "utf8").catch(() => "") : "";
const stdoutRaw = await fs.readFile(stdoutPath, "utf8").catch(() => "");
- const lines = [...stderrRaw.split(/\r?\n/), ...stdoutRaw.split(/\r?\n/)].map((line) =>
+ // stderr is the strongest failure signal on non-darwin platforms, so place it
+ // last and scan from the end: the most recent stderr error line then wins over
+ // any (possibly stale) stdout match, matching the stderr-first fallback below.
+ const lines = [...stdoutRaw.split(/\r?\n/), ...stderrRaw.split(/\r?\n/)].map((line) =>
line.trim(),
);
for (let i = lines.length - 1; i >= 0; i -= 1) {