惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

量子位
宝玉的分享
宝玉的分享
Apple Machine Learning Research
Apple Machine Learning Research
阮一峰的网络日志
阮一峰的网络日志
博客园 - 司徒正美
酷 壳 – CoolShell
酷 壳 – CoolShell
IT之家
IT之家
H
Help Net Security
雷峰网
雷峰网
M
MIT News - Artificial intelligence
Microsoft Security Blog
Microsoft Security Blog
美团技术团队
GbyAI
GbyAI
博客园_首页
博客园 - 三生石上(FineUI控件)
T
Tailwind CSS Blog
I
InfoQ
小众软件
小众软件
Google DeepMind News
Google DeepMind News
D
Docker
Engineering at Meta
Engineering at Meta
WordPress大学
WordPress大学
博客园 - Franky
Hugging Face - Blog
Hugging Face - Blog

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
fix(gateway): bound supervised lock recovery · openclaw/o...
steipete · 2026-04-28 · via Recent Commits to openclaw:main

@@ -1,4 +1,5 @@

11

import fs from "node:fs";

2+

import { request } from "node:http";

23

import path from "node:path";

34

import type { Command } from "commander";

45

import { readSecretFromFile } from "../../acp/secret-file.js";

@@ -111,8 +112,11 @@ const GATEWAY_RUN_BOOLEAN_KEYS = [

111112

] as const;

112113113114

const SUPERVISED_GATEWAY_LOCK_RETRY_MS = 5000;

115+

const SUPERVISED_GATEWAY_LOCK_RETRY_TIMEOUT_MS = 30_000;

116+

const SUPERVISED_GATEWAY_HEALTH_PROBE_TIMEOUT_MS = 1000;

114117115118

type Awaitable<T> = T | Promise<T>;

119+

type GatewayRunLogger = Pick<ReturnType<typeof createSubsystemLogger>, "info" | "warn">;

116120117121

/**

118122

* EX_CONFIG (78) from sysexits.h — used for configuration errors so systemd

@@ -356,6 +360,107 @@ function isHealthyGatewayLockError(err: unknown): boolean {

356360

);

357361

}

358362363+

function normalizeGatewayHealthProbeHost(host: string): string {

364+

if (host === "0.0.0.0" || host === "::") {

365+

return "127.0.0.1";

366+

}

367+

return host;

368+

}

369+370+

async function probeGatewayHealthz(params: {

371+

host: string;

372+

port: number;

373+

timeoutMs?: number;

374+

}): Promise<boolean> {

375+

const timeoutMs = params.timeoutMs ?? SUPERVISED_GATEWAY_HEALTH_PROBE_TIMEOUT_MS;

376+

return await new Promise<boolean>((resolve) => {

377+

const req = request(

378+

{

379+

hostname: normalizeGatewayHealthProbeHost(params.host),

380+

port: params.port,

381+

path: "/healthz",

382+

method: "GET",

383+

timeout: timeoutMs,

384+

},

385+

(res) => {

386+

res.resume();

387+

resolve(typeof res.statusCode === "number" && res.statusCode < 500);

388+

},

389+

);

390+

req.once("timeout", () => {

391+

req.destroy();

392+

resolve(false);

393+

});

394+

req.once("error", () => {

395+

resolve(false);

396+

});

397+

req.end();

398+

});

399+

}

400+401+

async function runGatewayLoopWithSupervisedLockRecovery(params: {

402+

startLoop: () => Promise<void>;

403+

supervisor: ReturnType<typeof detectRespawnSupervisor>;

404+

port: number;

405+

healthHost: string;

406+

log: GatewayRunLogger;

407+

now?: () => number;

408+

sleep?: (ms: number) => Promise<void>;

409+

probeHealth?: (params: { host: string; port: number }) => Promise<boolean>;

410+

retryMs?: number;

411+

timeoutMs?: number;

412+

}) {

413+

const supervisor = params.supervisor;

414+

if (!supervisor) {

415+

await params.startLoop();

416+

return;

417+

}

418+419+

const now = params.now ?? Date.now;

420+

const sleep =

421+

params.sleep ?? (async (ms: number) => await new Promise((resolve) => setTimeout(resolve, ms)));

422+

const probeHealth = params.probeHealth ?? ((probeParams) => probeGatewayHealthz(probeParams));

423+

const retryMs = params.retryMs ?? SUPERVISED_GATEWAY_LOCK_RETRY_MS;

424+

const timeoutMs = params.timeoutMs ?? SUPERVISED_GATEWAY_LOCK_RETRY_TIMEOUT_MS;

425+

const startedAt = now();

426+427+

for (;;) {

428+

try {

429+

await params.startLoop();

430+

return;

431+

} catch (err) {

432+

const isGatewayAlreadyRunning =

433+

err instanceof GatewayLockError &&

434+

typeof err.message === "string" &&

435+

err.message.includes("gateway already running");

436+

if (!isGatewayAlreadyRunning) {

437+

throw err;

438+

}

439+440+

if (await probeHealth({ host: params.healthHost, port: params.port })) {

441+

params.log.info(

442+

`gateway already running under ${supervisor}; existing gateway is healthy, leaving it in control`,

443+

);

444+

return;

445+

}

446+447+

const elapsedMs = now() - startedAt;

448+

if (elapsedMs >= timeoutMs) {

449+

throw new GatewayLockError(

450+

`gateway already running under ${supervisor}; existing gateway did not become healthy after ${timeoutMs}ms`,

451+

err,

452+

);

453+

}

454+455+

const waitMs = Math.min(retryMs, Math.max(0, timeoutMs - elapsedMs));

456+

params.log.warn(

457+

`gateway already running under ${supervisor}; waiting ${waitMs}ms before retrying startup`,

458+

);

459+

await sleep(waitMs);

460+

}

461+

}

462+

}

463+359464

function maybeWriteGatewayStartupFailureBundle(err: unknown): void {

360465

const result = writeDiagnosticStabilityBundleForFailureSync("gateway.startup_failed", err);

361466

if ("message" in result) {

@@ -680,11 +785,12 @@ async function runGatewayCommand(opts: GatewayRunOpts) {

680785681786

gatewayLog.info("starting...");

682787

startupTrace.mark("cli.gateway-loop");

788+

const healthHost = await resolveGatewayBindHost(bind, cfg.gateway?.customBindHost);

683789

const startLoop = async () =>

684790

await runGatewayLoop({

685791

runtime: defaultRuntime,

686792

lockPort: port,

687-

healthHost: await resolveGatewayBindHost(bind, cfg.gateway?.customBindHost),

793+

healthHost,

688794

start: async ({ startupStartedAt } = {}) =>

689795

await startGatewayServer(port, {

690796

bind,

@@ -695,25 +801,13 @@ async function runGatewayCommand(opts: GatewayRunOpts) {

695801

});

696802697803

try {

698-

const supervisor = detectRespawnSupervisor(process.env);

699-

while (true) {

700-

try {

701-

await startLoop();

702-

break;

703-

} catch (err) {

704-

const isGatewayAlreadyRunning =

705-

err instanceof GatewayLockError &&

706-

typeof err.message === "string" &&

707-

err.message.includes("gateway already running");

708-

if (!supervisor || !isGatewayAlreadyRunning) {

709-

throw err;

710-

}

711-

gatewayLog.warn(

712-

`gateway already running under ${supervisor}; waiting ${SUPERVISED_GATEWAY_LOCK_RETRY_MS}ms before retrying startup`,

713-

);

714-

await new Promise((resolve) => setTimeout(resolve, SUPERVISED_GATEWAY_LOCK_RETRY_MS));

715-

}

716-

}

804+

await runGatewayLoopWithSupervisedLockRecovery({

805+

startLoop,

806+

supervisor: detectRespawnSupervisor(process.env),

807+

port,

808+

healthHost,

809+

log: gatewayLog,

810+

});

717811

} catch (err) {

718812

if (isGatewayLockError(err)) {

719813

const errMessage = formatErrorMessage(err);

@@ -740,6 +834,11 @@ async function runGatewayCommand(opts: GatewayRunOpts) {

740834

}

741835

}

742836837+

export const __testing = {

838+

normalizeGatewayHealthProbeHost,

839+

runGatewayLoopWithSupervisedLockRecovery,

840+

};

841+743842

export function addGatewayRunCommand(cmd: Command): Command {

744843

return cmd

745844

.option("--port <port>", "Port for the gateway WebSocket")