fix: add gateway connection debug output

This commit is contained in:
Peter Steinberger
2026-01-08 02:51:28 +01:00
parent 1ebde4dc24
commit 6aa6c837e7
8 changed files with 133 additions and 44 deletions

View File

@@ -653,6 +653,7 @@ Examples:
)
.option("--timeout <ms>", "Probe timeout in milliseconds", "10000")
.option("--verbose", "Verbose logging", false)
.option("--debug", "Alias for --verbose", false)
.addHelpText(
"after",
`
@@ -664,7 +665,8 @@ Examples:
clawdbot status --deep --timeout 5000 # tighten probe timeout`,
)
.action(async (opts) => {
setVerbose(Boolean(opts.verbose));
const verbose = Boolean(opts.verbose || opts.debug);
setVerbose(verbose);
const timeout = opts.timeout
? Number.parseInt(String(opts.timeout), 10)
: undefined;
@@ -682,6 +684,7 @@ Examples:
deep: Boolean(opts.deep),
usage: Boolean(opts.usage),
timeoutMs: timeout,
verbose,
},
defaultRuntime,
);
@@ -697,8 +700,10 @@ Examples:
.option("--json", "Output JSON instead of text", false)
.option("--timeout <ms>", "Connection timeout in milliseconds", "10000")
.option("--verbose", "Verbose logging", false)
.option("--debug", "Alias for --verbose", false)
.action(async (opts) => {
setVerbose(Boolean(opts.verbose));
const verbose = Boolean(opts.verbose || opts.debug);
setVerbose(verbose);
const timeout = opts.timeout
? Number.parseInt(String(opts.timeout), 10)
: undefined;
@@ -714,6 +719,7 @@ Examples:
{
json: Boolean(opts.json),
timeoutMs: timeout,
verbose,
},
defaultRuntime,
);

View File

@@ -10,6 +10,7 @@ import {
} from "../config/config.js";
import { GATEWAY_LAUNCH_AGENT_LABEL } from "../daemon/constants.js";
import { resolveGatewayService } from "../daemon/service.js";
import { buildGatewayConnectionDetails } from "../gateway/call.js";
import { formatPortDiagnostics, inspectPortUsage } from "../infra/ports.js";
import type { RuntimeEnv } from "../runtime.js";
import { defaultRuntime } from "../runtime.js";
@@ -111,6 +112,10 @@ export async function doctorCommand(
}
cfg = await maybeRepairAnthropicOAuthProfileId(cfg, prompter);
const gatewayDetails = buildGatewayConnectionDetails({ config: cfg });
if (gatewayDetails.remoteFallbackNote) {
note(gatewayDetails.remoteFallbackNote, "Gateway");
}
const legacyState = await detectLegacyStateMigrations({ cfg });
if (legacyState.preview.length > 0) {
@@ -204,6 +209,7 @@ export async function doctorCommand(
const message = String(err);
if (message.includes("gateway closed")) {
note("Gateway not running.", "Gateway");
note(gatewayDetails.message, "Gateway connection");
} else {
runtime.error(`Health check failed: ${message}`);
}
@@ -255,6 +261,7 @@ export async function doctorCommand(
const message = String(err);
if (message.includes("gateway closed")) {
note("Gateway not running.", "Gateway");
note(gatewayDetails.message, "Gateway connection");
} else {
runtime.error(`Health check failed: ${message}`);
}

View File

@@ -1,7 +1,7 @@
import { loadConfig } from "../config/config.js";
import { loadSessionStore, resolveStorePath } from "../config/sessions.js";
import { type DiscordProbe, probeDiscord } from "../discord/probe.js";
import { callGateway } from "../gateway/call.js";
import { buildGatewayConnectionDetails, callGateway } from "../gateway/call.js";
import { info } from "../globals.js";
import type { RuntimeEnv } from "../runtime.js";
import { probeTelegram, type TelegramProbe } from "../telegram/probe.js";
@@ -110,7 +110,7 @@ export async function getHealthSnapshot(
}
export async function healthCommand(
opts: { json?: boolean; timeoutMs?: number },
opts: { json?: boolean; timeoutMs?: number; verbose?: boolean },
runtime: RuntimeEnv,
) {
// Always query the running gateway; do not open a direct Baileys socket here.
@@ -124,6 +124,13 @@ export async function healthCommand(
if (opts.json) {
runtime.log(JSON.stringify(summary, null, 2));
} else {
if (opts.verbose) {
const details = buildGatewayConnectionDetails();
runtime.log(info("Gateway connection:"));
for (const line of details.message.split("\n")) {
runtime.log(` ${line}`);
}
}
runtime.log(
summary.web.linked
? `Web: linked (auth age ${summary.web.authAgeMs ? `${Math.round(summary.web.authAgeMs / 60000)}m` : "unknown"})`

View File

@@ -11,7 +11,7 @@ import {
resolveStorePath,
type SessionEntry,
} from "../config/sessions.js";
import { callGateway } from "../gateway/call.js";
import { buildGatewayConnectionDetails, callGateway } from "../gateway/call.js";
import { info } from "../globals.js";
import { buildProviderSummary } from "../infra/provider-summary.js";
import {
@@ -222,7 +222,13 @@ const buildFlags = (entry: SessionEntry): string[] => {
};
export async function statusCommand(
opts: { json?: boolean; deep?: boolean; usage?: boolean; timeoutMs?: number },
opts: {
json?: boolean;
deep?: boolean;
usage?: boolean;
timeoutMs?: number;
verbose?: boolean;
},
runtime: RuntimeEnv,
) {
const summary = await getStatusSummary();
@@ -247,6 +253,14 @@ export async function statusCommand(
return;
}
if (opts.verbose) {
const details = buildGatewayConnectionDetails();
runtime.log(info("Gateway connection:"));
for (const line of details.message.split("\n")) {
runtime.log(` ${line}`);
}
}
runtime.log(
`Web session: ${summary.web.linked ? "linked" : "not linked"}${summary.web.linked ? ` (last refreshed ${formatAge(summary.web.authAgeMs)})` : ""}`,
);

View File

@@ -28,6 +28,11 @@ vi.mock("../infra/tailnet.js", () => ({
}));
vi.mock("./client.js", () => ({
describeGatewayCloseCode: (code: number) => {
if (code === 1000) return "normal closure";
if (code === 1006) return "abnormal closure (no close frame)";
return undefined;
},
GatewayClient: class {
constructor(opts: {
url?: string;

View File

@@ -1,7 +1,11 @@
import { randomUUID } from "node:crypto";
import { loadConfig, resolveGatewayPort } from "../config/config.js";
import {
type ClawdbotConfig,
loadConfig,
resolveGatewayPort,
} from "../config/config.js";
import { pickPrimaryTailnetIPv4 } from "../infra/tailnet.js";
import { GatewayClient } from "./client.js";
import { describeGatewayCloseCode, GatewayClient } from "./client.js";
import { PROTOCOL_VERSION } from "./protocol/index.js";
export type CallGatewayOptions = {
@@ -21,14 +25,26 @@ export type CallGatewayOptions = {
maxProtocol?: number;
};
export async function callGateway<T = unknown>(
opts: CallGatewayOptions,
): Promise<T> {
const timeoutMs = opts.timeoutMs ?? 10_000;
const config = loadConfig();
export type GatewayConnectionDetails = {
url: string;
urlSource: string;
bindMode: string;
preferTailnet: boolean;
tailnetIPv4?: string;
isRemoteMode: boolean;
remoteUrl?: string;
urlOverride?: string;
localUrl: string;
remoteFallbackNote?: string;
message: string;
};
export function buildGatewayConnectionDetails(
opts: { url?: string; config?: ClawdbotConfig } = {},
): GatewayConnectionDetails {
const config = opts.config ?? loadConfig();
const isRemoteMode = config.gateway?.mode === "remote";
const remote = isRemoteMode ? config.gateway?.remote : undefined;
const authToken = config.gateway?.auth?.token;
const localPort = resolveGatewayPort(config);
const tailnetIPv4 = pickPrimaryTailnetIPv4();
const bindMode = config.gateway?.bind ?? "loopback";
@@ -47,6 +63,56 @@ export async function callGateway<T = unknown>(
? remote.url.trim()
: undefined;
const url = urlOverride || remoteUrl || localUrl;
const urlSource = urlOverride
? "cli --url"
: remoteUrl
? "config gateway.remote.url"
: preferTailnet && tailnetIPv4
? `local tailnet ${tailnetIPv4}`
: "local loopback";
const remoteFallbackNote =
isRemoteMode && !urlOverride && !remoteUrl
? "gateway.mode=remote but gateway.remote.url is missing; using local URL."
: undefined;
const bindDetail =
!urlOverride && !remoteUrl ? `Bind: ${bindMode}` : undefined;
const message = [
`Gateway target: ${url}`,
`Source: ${urlSource}`,
bindDetail,
remoteFallbackNote ? `Note: ${remoteFallbackNote}` : undefined,
]
.filter(Boolean)
.join("\n");
return {
url,
urlSource,
bindMode,
preferTailnet,
tailnetIPv4,
isRemoteMode,
remoteUrl,
urlOverride,
localUrl,
remoteFallbackNote,
message,
};
}
export async function callGateway<T = unknown>(
opts: CallGatewayOptions,
): Promise<T> {
const timeoutMs = opts.timeoutMs ?? 10_000;
const config = loadConfig();
const details = buildGatewayConnectionDetails({
url: opts.url,
config,
});
const isRemoteMode = details.isRemoteMode;
const remote = isRemoteMode ? config.gateway?.remote : undefined;
const authToken = config.gateway?.auth?.token;
const url = details.url;
const token =
(typeof opts.token === "string" && opts.token.trim().length > 0
? opts.token.trim()
@@ -67,38 +133,11 @@ export async function callGateway<T = unknown>(
(typeof remote?.password === "string" && remote.password.trim().length > 0
? remote.password.trim()
: undefined);
const urlSource = urlOverride
? "cli --url"
: remoteUrl
? "config gateway.remote.url"
: preferTailnet && tailnetIPv4
? `local tailnet ${tailnetIPv4}`
: "local loopback";
const remoteFallbackNote =
isRemoteMode && !urlOverride && !remoteUrl
? "Note: gateway.mode=remote but gateway.remote.url is missing; using local URL."
: undefined;
const bindDetail =
!urlOverride && !remoteUrl
? `Bind: ${bindMode}`
: undefined;
const connectionDetails = [
`Gateway target: ${url}`,
`Source: ${urlSource}`,
bindDetail,
remoteFallbackNote,
]
.filter(Boolean)
.join("\n");
const connectionDetails = details.message;
const formatCloseError = (code: number, reason: string) => {
const reasonText = reason?.trim() || "no close reason";
const hint =
code === 1006
? "abnormal closure (no close frame)"
: code === 1000
? "normal closure"
: "";
const hint = describeGatewayCloseCode(code) ?? "";
const suffix = hint ? ` ${hint}` : "";
return `gateway closed (${code}${suffix}): ${reasonText}\n${connectionDetails}`;
};

View File

@@ -36,6 +36,17 @@ export type GatewayClientOptions = {
onGap?: (info: { expected: number; received: number }) => void;
};
export const GATEWAY_CLOSE_CODE_HINTS: Readonly<Record<number, string>> = {
1000: "normal closure",
1006: "abnormal closure (no close frame)",
1008: "policy violation",
1012: "service restart",
};
export function describeGatewayCloseCode(code: number): string | undefined {
return GATEWAY_CLOSE_CODE_HINTS[code];
}
export class GatewayClient {
private ws: WebSocket | null = null;
private opts: GatewayClientOptions;