Diagnostics Commands

Focus Deep-Dive

Health checks, status scanning, and automated repair flows

The diagnostics subsystem provides three complementary views into system health: the health command for real-time gateway probes, the status command for deep runtime and session scanning, and the doctor flow for interactive config repair. Together they form the primary observability surface for operators and the first line of automated self-healing.

📚 What is this & why does it matter?

Diagnostics commands are the nervous system of an OpenClaw deployment. When a channel stops responding, when sessions accumulate stale state, or when a gateway probe times out, these commands are the first tools an operator reaches for. They surface structured health data as both human-readable summaries and machine-parseable JSON, enabling both manual triage and automated monitoring pipelines.

💡 Real-world analogy

Think of a hospital triage system. The health command is the vital-signs monitor — quick pulse, temperature, blood pressure. The status command is the full diagnostic panel — blood work, imaging, detailed charts. The doctor command is the attending physician who reads the panels and prescribes treatment. Each level provides progressively deeper insight and intervention capability.

Contents

Overview

Primary role

Probe gateway health, scan runtime status, and interactively repair misconfigured deployments via structured diagnostic pipelines.

Main inputs

CLI flags (--json, --verbose, --deep, --usage, --timeout-ms), gateway RPC responses, and local config files.

Key decisions

Should we probe channels? Is the heartbeat stale? Are sessions orphaned? Does the config need migration? Should we offer an update first?

Outputs / side effects

HealthSummary or StatusReport objects rendered as tables or JSON. The doctor flow may mutate config files and restart services.

Three diagnostic tiers: health is fast and read-only (gateway round-trip). status adds local runtime and session scanning. doctor is the only tier that writes — it can repair config, update allowlists, and restart processes.

Core Types

The health subsystem is built around a hierarchy of summary types. HealthSummary is the top-level envelope returned by the gateway, containing per-channel, per-agent, and session data. Each nested type carries its own probe and heartbeat metadata.

// src/commands/health.ts — ChannelAccountHealthSummary
export type ChannelAccountHealthSummary = {
  accountId: string;
  configured?: boolean;
  linked?: boolean;
  authAgeMs?: number | null;
  probe?: unknown;
  lastProbeAt?: number | null;
  [key: string]: unknown;
};

// src/commands/health.ts — ChannelHealthSummary
export type ChannelHealthSummary = ChannelAccountHealthSummary & {
  accounts?: Record<string, ChannelAccountHealthSummary>;
};
// src/commands/health.ts — AgentHealthSummary & top-level HealthSummary
export type AgentHealthSummary = {
  agentId: string;
  name?: string;
  isDefault: boolean;
  heartbeat: AgentHeartbeatSummary;
  sessions: HealthSummary["sessions"];
};

export type HealthSummary = {
  ok: true;
  ts: number;
  durationMs: number;
  channels: Record<string, ChannelHealthSummary>;
  channelOrder: string[];
  channelLabels: Record<string, string>;
  heartbeatSeconds: number;
  defaultAgentId: string;
  agents: AgentHealthSummary[];
  sessions: {
    path: string;
    count: number;
    recent: Array<{
      key: string;
      updatedAt: number | null;
      age: number | null;
    }>;
  };
};
TypeScopeKey fields
ChannelAccountHealthSummarySingle account within a channelaccountId, configured, linked, authAgeMs, probe
ChannelHealthSummaryEntire channel (extends account)Inherits account fields plus accounts map for multi-account channels
AgentHealthSummarySingle agent instanceagentId, isDefault, heartbeat, sessions
HealthSummaryFull system snapshotok, ts, durationMs, channels, agents, sessions

Decision Points

DecisionWhat controls itWhy it matters
Probe depth--verbose flag sets { probe: true } in gateway paramsWithout verbose, channels report cached status only — probing triggers a live round-trip to each provider API
Timeout handlingopts.timeoutMs passed to callGatewayA slow channel probe must not block the entire health check; the timeout ensures bounded latency
JSON vs human outputopts.json boolean flagJSON mode emits raw HealthSummary for piping into monitoring tools; human mode renders colored tables
Deep status scanopts.deep flag on statusCommandDeep mode additionally scans local runtime files and session directories for orphaned state
Doctor auto-update offermaybeOfferUpdateBeforeDoctor guardRunning doctor on an outdated binary wastes time — the guard prompts for update first
Config migrationloadAndMaybeMigrateDoctorConfigOld config schemas are silently migrated before diagnostic checks run, preventing false positives

How It Works

The diagnostics subsystem has three independent but composable command paths: the health command for gateway-level probes, the status command for local runtime scanning, and the doctor flow for interactive repair. Each is designed to be invoked independently or chained in automated pipelines.

Health Command — Gateway Probing

The healthCommand function issues a single RPC call to the gateway's health method. When the --verbose flag is set, it passes { probe: true } to trigger live channel probes rather than returning cached status. The result is a complete HealthSummary snapshot.

// src/commands/health.ts — healthCommand
export async function healthCommand(
  opts: { json?: boolean; timeoutMs?: number; verbose?: boolean; config?: OpenClawConfig },
  runtime: RuntimeEnv,
) {
  const cfg = opts.config ?? runtime.config;
  const summary = await callGateway<HealthSummary>({
    method: "health",
    params: opts.verbose ? { probe: true } : undefined,
    timeoutMs: opts.timeoutMs,
    config: cfg,
  });

  if (opts.json) {
    runtime.stdout.write(JSON.stringify(summary, null, 2));
    return;
  }

  // Render human-readable channel table
  renderChannelTable(summary.channels, summary.channelOrder, summary.channelLabels, runtime);
  renderAgentTable(summary.agents, summary.defaultAgentId, runtime);
  renderSessionSummary(summary.sessions, runtime);
}

The companion getHealthSnapshot function provides a programmatic API for other subsystems (e.g. the doctor flow) that need a HealthSummary without CLI rendering:

// src/commands/health.ts — getHealthSnapshot
export async function getHealthSnapshot(
  params?: { timeoutMs?: number; probe?: boolean },
): Promise<HealthSummary> {
  return callGateway<HealthSummary>({
    method: "health",
    params: params?.probe ? { probe: true } : undefined,
    timeoutMs: params?.timeoutMs,
  });
}

Status Command — Runtime Scanning

The statusCommand extends health data with local runtime introspection. It scans session directories, checks process states, and optionally gathers usage statistics. The --deep flag activates the full scan pipeline including status.scan.runtime.ts and status.scan.fast-json.ts.

// src/commands/status.command.ts — statusCommand
export async function statusCommand(
  opts: {
    json?: boolean;
    deep?: boolean;
    usage?: boolean;
    timeoutMs?: number;
    verbose?: boolean;
    all?: boolean;
  },
  runtime: RuntimeEnv,
) {
  const health = await getHealthSnapshot({
    timeoutMs: opts.timeoutMs,
    probe: opts.verbose,
  });

  const scanResult = opts.deep
    ? await deepScan(runtime)
    : await fastScan(runtime);

  const summary = buildStatusSummary(health, scanResult, {
    includeUsage: opts.usage,
    includeAll: opts.all,
  });

  if (opts.json) {
    runtime.stdout.write(JSON.stringify(summary, null, 2));
    return;
  }

  renderStatusReport(summary, runtime);
}
Performance note: Deep scan reads every session file from disk via status.scan.fast-json.ts, which uses streaming JSON parsing to avoid loading entire session blobs into memory. On deployments with 10,000+ sessions, prefer --json output piped to jq for filtering.

Doctor Flow — Interactive Repair

The doctor flow is the most complex diagnostic path. It combines health probing, config validation, and interactive repair into a single guided workflow. The flow is orchestrated by createDoctorPrompter which presents issues and repair options through a terminal UI.

flowchart TD
  START["doctor command invoked"]
  UPDATE{"maybeOfferUpdateBeforeDoctor()"}
  MIGRATE["loadAndMaybeMigrateDoctorConfig()"]
  HEALTH["getHealthSnapshot(probe: true)"]
  INSTALL["noteSourceInstallIssues()"]
  STARTUP["noteStartupOptimizationHints()"]
  ALLOWLIST["Check allowlist policies"]
  ISSUES{"Issues found?"}
  PROMPT["createDoctorPrompter — display issues"]
  REPAIR["Apply selected repairs"]
  VERIFY["Re-run health check"]
  DONE["Report summary"]

  START --> UPDATE
  UPDATE -->|"Outdated"| DONE
  UPDATE -->|"Current"| MIGRATE
  MIGRATE --> HEALTH
  HEALTH --> INSTALL
  INSTALL --> STARTUP
  STARTUP --> ALLOWLIST
  ALLOWLIST --> ISSUES
  ISSUES -->|"No"| DONE
  ISSUES -->|"Yes"| PROMPT
  PROMPT --> REPAIR
  REPAIR --> VERIFY
  VERIFY --> DONE

The doctor flow runs six sequential diagnostic stages. First, it checks whether the binary itself is outdated. Then it migrates any legacy config schemas. The health snapshot is taken with full probing enabled. Installation issues (missing binaries, incorrect permissions) and startup optimization hints (unnecessary env vars, slow plugin loads) are noted. Finally, allowlist policies are validated against the current channel configuration.

Idempotent repairs: Every repair action in the doctor flow is idempotent. Running doctor twice with the same config produces the same result. Repairs are applied through atomic file writes with rollback on failure.

Code Walkthrough

Gateway health RPC

The health command calls callGateway with method "health". The gateway aggregates channel probe results, agent heartbeats, and session counts into a single HealthSummary response. The timeoutMs parameter bounds the total round-trip including all channel probes.

Channel iteration

The summary's channelOrder array determines rendering order. Each channel's ChannelHealthSummary may contain nested accounts for multi-account providers (e.g. multiple Telegram bot tokens). The configured and linked booleans distinguish between channels that are defined in config versus channels that have active authenticated connections.

Agent heartbeat evaluation

Each AgentHealthSummary contains a heartbeat field with the last heartbeat timestamp. The health renderer compares this against heartbeatSeconds from the top-level summary to determine if an agent is stale. Agents whose last heartbeat exceeds 2 × heartbeatSeconds are flagged as unhealthy.

Session age analysis

The sessions.recent array contains the most recently updated sessions with their age in milliseconds. The status command uses this to identify orphaned sessions — those with age exceeding the configured session TTL. The count field gives the total number of active sessions without loading them all.

Doctor repair dispatch

When the doctor flow identifies issues, createDoctorPrompter presents them as a numbered list. Each issue has an associated repair function. Repairs are executed in dependency order: config repairs before service restarts, allowlist repairs before channel re-probes. The maybeRepairUiProtocolFreshness function handles the special case of stale UI protocol versions.

Doctor Command Repair Sequence

The following diagram shows the interaction between the doctor command, the health gateway, and the local repair subsystem during a typical repair session:

sequenceDiagram
    participant Operator
    participant Doctor as doctor flow
    participant Config as config loader
    participant Gateway as health gateway
    participant Repair as repair subsystem
    participant FS as file system

    Operator->>Doctor: openclaw doctor
    Doctor->>Config: loadAndMaybeMigrateDoctorConfig()
    Config->>FS: read config files
    FS-->>Config: raw config
    Config-->>Doctor: migrated config

    Doctor->>Gateway: getHealthSnapshot(probe: true)
    Gateway-->>Doctor: HealthSummary

    Doctor->>Doctor: noteSourceInstallIssues()
    Doctor->>Doctor: noteStartupOptimizationHints()
    Doctor->>Doctor: check allowlist policies

    Doctor-->>Operator: display issues list

    Operator->>Doctor: select repairs
    Doctor->>Repair: apply selected repairs
    Repair->>FS: atomic config writes
    FS-->>Repair: write confirmation
    Repair-->>Doctor: repair results

    Doctor->>Gateway: getHealthSnapshot(probe: true)
    Gateway-->>Doctor: updated HealthSummary
    Doctor-->>Operator: final report

Edge Cases

ScenarioBehaviorRationale
Gateway unreachablehealthCommand throws with a timeout error after timeoutMs elapsesBounded failure prevents indefinite hangs in monitoring scripts
Partial channel failureSummary returns with per-channel error fields; overall ok remains trueOne broken channel should not mask the health of other channels
Stale heartbeatAgent flagged as unhealthy when lastHeartbeat > 2 × heartbeatSecondsThe 2× multiplier provides a grace period for transient delays
Empty session directorysessions.count is 0 and recent is an empty arrayFresh deployments have no sessions; this is a valid healthy state
Config migration failureDoctor aborts with a migration error before any repairs are attemptedRunning repairs against an un-migrated config could corrupt state
Concurrent doctor runsFile-level locks prevent two doctor instances from writing config simultaneouslyAtomic writes with lock files ensure config consistency
Null authAgeMsChannel reported as configured but auth age unknown; rendered as "—"Some channels (e.g. webhook-based) have no auth token with a measurable age
probe field is unknownType-safe: callers must narrow before accessing probe-specific fieldsDifferent channel providers return different probe shapes; the index signature allows extension

Configuration

SettingLocationDefaultDescription
--jsonCLI flag on health and statusfalseEmit raw JSON instead of human-readable tables
--verboseCLI flag on health and statusfalseEnable live channel probing (slower but more accurate)
--deepCLI flag on statusfalseScan local runtime files and session directories
--usageCLI flag on statusfalseInclude token usage statistics in the report
--allCLI flag on statusfalseShow all agents and channels, including unconfigured ones
--timeout-msCLI flag on health and status10000Maximum milliseconds to wait for gateway response
heartbeatSecondsGateway config30Expected interval between agent heartbeats; staleness threshold is 2×

Doctor Shared Modules

The src/commands/doctor/shared/ directory contains 25+ utility modules that the doctor flow delegates to for specific repair tasks. These are intentionally decoupled so they can be unit-tested in isolation.

ModuleResponsibility
allowlist-policy-repair.tsDetects and repairs mismatched allowlist policies against current channel configuration
exec-safe-bins.tsValidates that all referenced binaries in exec commands exist and have correct permissions
mutable-allowlist.tsProvides a mutable in-memory allowlist that the repair flow can modify before persisting
empty-allowlist-policy.tsGenerates a clean empty allowlist policy as a baseline when no policy file exists
Gateway status subsystem: The src/commands/gateway-status/ directory contains four modules — discovery.ts (gateway endpoint resolution), helpers.ts (response normalization), output.ts (rendering), and probe-run.ts (individual probe execution) — that handle the low-level gateway communication for both health and status commands.
Admin Operations

Administrative commands that modify system state — complementary to the read-heavy diagnostics commands.

Permission Gating

Authorization layer that controls who can run diagnostic commands, especially the write-capable doctor flow.

Command Parsing

How /health, /status, and /doctor are parsed from raw input before reaching the diagnostic handlers.

Source Files

Key files involved in diagnostics commands
src/
├── commands/
│   ├── health.ts                        ← healthCommand(), getHealthSnapshot(), HealthSummary types
│   ├── status.command.ts                ← statusCommand() entry point
│   ├── status.scan.ts                   ← Core scan orchestration
│   ├── status.scan.fast-json.ts         ← Streaming JSON session parser
│   ├── status.scan.runtime.ts           ← Local runtime file scanner
│   ├── status.summary.ts               ← buildStatusSummary() aggregation
│   ├── doctor/
│   │   └── shared/
│   │       ├── allowlist-policy-repair.ts
│   │       ├── exec-safe-bins.ts
│   │       ├── mutable-allowlist.ts
│   │       └── empty-allowlist-policy.ts  (25 files total)
│   └── gateway-status/
│       ├── discovery.ts                 ← Gateway endpoint resolution
│       ├── helpers.ts                   ← Response normalization
│       ├── output.ts                    ← Status rendering
│       └── probe-run.ts                ← Individual probe execution
└── flows/
    └── doctor-health.ts                 ← Doctor flow orchestration