From 6aa6c837e77a41af24a612ac5e0ee0e47f59fe5b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 8 Jan 2026 02:51:28 +0100 Subject: [PATCH] fix: add gateway connection debug output --- CHANGELOG.md | 2 +- src/cli/program.ts | 10 +++- src/commands/doctor.ts | 7 +++ src/commands/health.ts | 11 +++- src/commands/status.ts | 18 ++++++- src/gateway/call.test.ts | 5 ++ src/gateway/call.ts | 113 ++++++++++++++++++++++++++------------- src/gateway/client.ts | 11 ++++ 8 files changed, 133 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d0d9b828..9193d8853 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ ### Fixes - macOS: harden Voice Wake tester/runtime (pause trigger, mic persistence, local-only tester) and keep transcript logs private. Thanks @xadenryan for PR #438. - Doctor/Daemon: surface gateway runtime state + port collision diagnostics; warn on legacy workspace dirs. -- Gateway/CLI: include gateway target/source details in close/timeout errors. +- Gateway/CLI: include gateway target/source details in close/timeout errors and verbose health/status output. - Discord: format slow listener logs in seconds to match shared duration style. - CLI: show colored table output for `clawdbot cron list` (JSON behind `--json`). - CLI: add cron `create`/`remove`/`delete` aliases for job management. diff --git a/src/cli/program.ts b/src/cli/program.ts index d952b907e..4460245ea 100644 --- a/src/cli/program.ts +++ b/src/cli/program.ts @@ -653,6 +653,7 @@ Examples: ) .option("--timeout ", "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 ", "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, ); diff --git a/src/commands/doctor.ts b/src/commands/doctor.ts index cb2297621..143a422ac 100644 --- a/src/commands/doctor.ts +++ b/src/commands/doctor.ts @@ -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}`); } diff --git a/src/commands/health.ts b/src/commands/health.ts index 03bbbf062..382382997 100644 --- a/src/commands/health.ts +++ b/src/commands/health.ts @@ -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"})` diff --git a/src/commands/status.ts b/src/commands/status.ts index b1e7eadc0..708f0b4d8 100644 --- a/src/commands/status.ts +++ b/src/commands/status.ts @@ -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)})` : ""}`, ); diff --git a/src/gateway/call.test.ts b/src/gateway/call.test.ts index 822ad2689..15127d3e0 100644 --- a/src/gateway/call.test.ts +++ b/src/gateway/call.test.ts @@ -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; diff --git a/src/gateway/call.ts b/src/gateway/call.ts index 5e97d9912..dc01af425 100644 --- a/src/gateway/call.ts +++ b/src/gateway/call.ts @@ -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( - opts: CallGatewayOptions, -): Promise { - 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( ? 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( + opts: CallGatewayOptions, +): Promise { + 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( (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}`; }; diff --git a/src/gateway/client.ts b/src/gateway/client.ts index 24e138d03..b0c06b293 100644 --- a/src/gateway/client.ts +++ b/src/gateway/client.ts @@ -36,6 +36,17 @@ export type GatewayClientOptions = { onGap?: (info: { expected: number; received: number }) => void; }; +export const GATEWAY_CLOSE_CODE_HINTS: Readonly> = { + 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;