File size: 3,223 Bytes
f778c12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import { formatErrorMessage } from "../../infra/errors.js";
import { findStartupMaintenanceRequiredError } from "../../infra/startup-maintenance-required.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
import { defaultRuntime } from "../../runtime.js";
import { OpenClawDatabaseSchemaPreflightError } from "../../state/openclaw-database-preflight.messages.js";
import { formatCliCommand } from "../command-format.js";

const gatewayLog = createSubsystemLogger("gateway");

export function resolveGatewayStartupMaintenanceReason(error: unknown) {
  return findStartupMaintenanceRequiredError(error)?.reason;
}

export async function handleGatewayStartupMaintenance(error: unknown): Promise<boolean> {
  const maintenance = findStartupMaintenanceRequiredError(error);
  if (!maintenance) {
    return false;
  }
  const reason = maintenance.reason;
  let refusal = maintenance;
  if (
    maintenance.kind === "newer-schema" &&
    !(maintenance instanceof OpenClawDatabaseSchemaPreflightError)
  ) {
    // Config reads can refuse shared state before bootstrap reaches schema preflight.
    try {
      const { preflightOpenClawDatabaseSchemas } =
        await import("../../state/openclaw-database-preflight.js");
      const schemas = await preflightOpenClawDatabaseSchemas({ env: process.env });
      if (schemas.incompatible.length > 0) {
        refusal = new OpenClawDatabaseSchemaPreflightError(schemas.incompatible);
      }
    } catch {
      // Diagnostic inspection must not prevent parking and exit for the original refusal.
    }
  }
  const stop = `Stop the service with ${formatCliCommand("openclaw gateway stop")} (or its service owner), then`;
  const guidance =
    reason === "a newer OpenClaw build"
      ? `${stop} restore your pre-update backup created with ${formatCliCommand("openclaw backup create")}, then start it again with ${formatCliCommand("openclaw gateway start")}. See https://docs.openclaw.ai/install/updating#rollback.`
      : `${stop} run ${formatCliCommand("openclaw doctor --fix")}, then start it again with ${formatCliCommand("openclaw gateway start")}.`;
  let parked = false;
  try {
    // launchd ignores exit 78 under KeepAlive. Park without opening the database,
    // which may also be unavailable to the persisted crash-loop counter.
    const { parkCurrentLaunchAgentForMaintenance } = await import("../../daemon/launchd.js");
    parked = await parkCurrentLaunchAgentForMaintenance();
  } catch (parkError) {
    gatewayLog.error(`failed to park the managed LaunchAgent: ${formatErrorMessage(parkError)}`);
  }
  if (refusal instanceof OpenClawDatabaseSchemaPreflightError) {
    gatewayLog.error(
      `${formatErrorMessage(refusal)}${parked ? " Parked the managed LaunchAgent." : ""}`,
    );
    defaultRuntime.error(`Gateway failed to start: ${formatErrorMessage(refusal)}`);
  } else {
    gatewayLog.error(
      `gateway requires ${reason}${parked ? "; parked the managed LaunchAgent" : ""}. ${guidance}`,
    );
    defaultRuntime.error(`Gateway failed to start: ${formatErrorMessage(error)}. ${guidance}`);
  }
  // systemd's RestartPreventExitStatus already treats EX_CONFIG as terminal.
  defaultRuntime.exit(78);
  return true;
}