Probe gateway health, scan runtime status, and interactively repair misconfigured deployments via structured diagnostic pipelines.
Diagnostics Commands
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
CLI flags (--json, --verbose, --deep, --usage, --timeout-ms), gateway RPC responses, and local config files.
Should we probe channels? Is the heartbeat stale? Are sessions orphaned? Does the config need migration? Should we offer an update first?
HealthSummary or StatusReport objects rendered as tables or JSON. The doctor flow may mutate config files and restart services.
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;
}>;
};
};
| Type | Scope | Key fields |
|---|---|---|
ChannelAccountHealthSummary | Single account within a channel | accountId, configured, linked, authAgeMs, probe |
ChannelHealthSummary | Entire channel (extends account) | Inherits account fields plus accounts map for multi-account channels |
AgentHealthSummary | Single agent instance | agentId, isDefault, heartbeat, sessions |
HealthSummary | Full system snapshot | ok, ts, durationMs, channels, agents, sessions |
Decision Points
| Decision | What controls it | Why it matters |
|---|---|---|
| Probe depth | --verbose flag sets { probe: true } in gateway params | Without verbose, channels report cached status only — probing triggers a live round-trip to each provider API |
| Timeout handling | opts.timeoutMs passed to callGateway | A slow channel probe must not block the entire health check; the timeout ensures bounded latency |
| JSON vs human output | opts.json boolean flag | JSON mode emits raw HealthSummary for piping into monitoring tools; human mode renders colored tables |
| Deep status scan | opts.deep flag on statusCommand | Deep mode additionally scans local runtime files and session directories for orphaned state |
| Doctor auto-update offer | maybeOfferUpdateBeforeDoctor guard | Running doctor on an outdated binary wastes time — the guard prompts for update first |
| Config migration | loadAndMaybeMigrateDoctorConfig | Old 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);
}
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.
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
| Scenario | Behavior | Rationale |
|---|---|---|
| Gateway unreachable | healthCommand throws with a timeout error after timeoutMs elapses | Bounded failure prevents indefinite hangs in monitoring scripts |
| Partial channel failure | Summary returns with per-channel error fields; overall ok remains true | One broken channel should not mask the health of other channels |
| Stale heartbeat | Agent flagged as unhealthy when lastHeartbeat > 2 × heartbeatSeconds | The 2× multiplier provides a grace period for transient delays |
| Empty session directory | sessions.count is 0 and recent is an empty array | Fresh deployments have no sessions; this is a valid healthy state |
| Config migration failure | Doctor aborts with a migration error before any repairs are attempted | Running repairs against an un-migrated config could corrupt state |
| Concurrent doctor runs | File-level locks prevent two doctor instances from writing config simultaneously | Atomic writes with lock files ensure config consistency |
Null authAgeMs | Channel 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 unknown | Type-safe: callers must narrow before accessing probe-specific fields | Different channel providers return different probe shapes; the index signature allows extension |
Configuration
| Setting | Location | Default | Description |
|---|---|---|---|
--json | CLI flag on health and status | false | Emit raw JSON instead of human-readable tables |
--verbose | CLI flag on health and status | false | Enable live channel probing (slower but more accurate) |
--deep | CLI flag on status | false | Scan local runtime files and session directories |
--usage | CLI flag on status | false | Include token usage statistics in the report |
--all | CLI flag on status | false | Show all agents and channels, including unconfigured ones |
--timeout-ms | CLI flag on health and status | 10000 | Maximum milliseconds to wait for gateway response |
heartbeatSeconds | Gateway config | 30 | Expected 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.
| Module | Responsibility |
|---|---|
allowlist-policy-repair.ts | Detects and repairs mismatched allowlist policies against current channel configuration |
exec-safe-bins.ts | Validates that all referenced binaries in exec commands exist and have correct permissions |
mutable-allowlist.ts | Provides a mutable in-memory allowlist that the repair flow can modify before persisting |
empty-allowlist-policy.ts | Generates a clean empty allowlist policy as a baseline when no policy file exists |
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.
Related Topics
Administrative commands that modify system state — complementary to the read-heavy diagnostics commands.
Authorization layer that controls who can run diagnostic commands, especially the write-capable doctor flow.
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