docs: document status summary helpers

This commit is contained in:
Peter Steinberger
2026-06-04 13:15:35 -04:00
parent cb4f6af504
commit 726bc2b6c7
7 changed files with 56 additions and 8 deletions
+7
View File
@@ -1,3 +1,6 @@
// Top-level status scan entrypoint.
// Chooses fast JSON policy or full human scan and returns one normalized scan result.
import { withProgress } from "../cli/progress.js";
import { hasConfiguredChannelsForReadOnlyScope } from "../plugins/channel-plugin-ids.js";
import { buildPluginCompatibilitySnapshotNotices } from "../plugins/status.js";
@@ -8,6 +11,7 @@ import { collectStatusScanOverview } from "./status.scan-overview.ts";
import type { StatusScanResult } from "./status.scan-result.ts";
import { scanStatusJsonWithPolicy } from "./status.scan.fast-json.js";
/** Runs the status scan for text or JSON command modes. */
export async function scanStatus(
opts: {
json?: boolean;
@@ -18,6 +22,7 @@ export async function scanStatus(
_runtime: RuntimeEnv,
): Promise<StatusScanResult> {
if (opts.json) {
// JSON mode uses a policy wrapper so tests and `status-json` can tune fast-path behavior.
return await scanStatusJsonWithPolicy(
{
timeoutMs: opts.timeoutMs,
@@ -54,6 +59,7 @@ export async function scanStatus(
showSecrets: process.env.OPENCLAW_SHOW_SECRETS?.trim() !== "0",
includeLiveChannelStatus: isFullScan,
includeChannelSetupRuntimeFallback: isFullScan,
// Fast status avoids local secret resolution and relies on config/runtime hints.
channelCredentialResolutionSkipped: !isFullScan,
includeChannelSecretTargets: isFullScan ? undefined : false,
fetchGitUpdate: isFullScan,
@@ -80,6 +86,7 @@ export async function scanStatus(
const result = await executeStatusScanFromOverview({
overview,
resolveMemory: async ({ cfg, agentStatus, memoryPlugin }) =>
// Memory plugin probing can touch disk/plugin state; reserve it for full scans.
opts.all
? await resolveStatusMemoryStatusSnapshot({
cfg,
+6
View File
@@ -1,3 +1,6 @@
// Reads service manager state for status reports.
// Converts gateway/node launchd/systemd state into a compact summary shape.
import {
summarizeGatewayServiceLayout,
type GatewayServiceLayoutSummary,
@@ -16,6 +19,7 @@ export type ServiceStatusSummary = {
layout?: GatewayServiceLayoutSummary;
};
/** Reads a daemon service summary, falling back to unknown when service inspection fails. */
export async function readServiceStatusSummary(
service: GatewayService,
fallbackLabel: string,
@@ -24,6 +28,7 @@ export async function readServiceStatusSummary(
const state = await readGatewayServiceState(service, { env: process.env });
const layout = await summarizeGatewayServiceLayout(state.command);
const managedByOpenClaw = state.installed;
// A running unmanaged process still counts as installed for status display.
const externallyManaged = !managedByOpenClaw && state.running;
const installed = managedByOpenClaw || externallyManaged;
const loadedText = externallyManaged
@@ -42,6 +47,7 @@ export async function readServiceStatusSummary(
...(layout ? { layout } : {}),
};
} catch {
// Status output should survive service-manager errors and show an unknown row.
return {
label: fallbackLabel,
installed: null,
+8
View File
@@ -1,3 +1,6 @@
// Runtime helpers for building status summaries.
// Kept behind a lazy surface because status summary imports model/session/runtime metadata helpers.
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
import {
normalizeLowercaseStringOrEmpty,
@@ -27,6 +30,7 @@ function resolveStatusModelRefFromRaw(params: {
}
const configuredModels = params.cfg.agents?.defaults?.models ?? {};
if (!trimmed.includes("/")) {
// Bare model names may be aliases from agents.defaults.models before falling back to default provider.
const aliasKey = normalizeLowercaseStringOrEmpty(trimmed);
for (const [modelKey, entry] of Object.entries(configuredModels)) {
const aliasValue = (entry as { alias?: unknown } | undefined)?.alias;
@@ -60,6 +64,7 @@ function resolveConfiguredStatusModelRef(params: {
)
: undefined;
if (agentRawModel) {
// Agent-specific primary model wins over global defaults for session status rows.
const parsed = resolveStatusModelRefFromRaw({
cfg: params.cfg,
rawModel: agentRawModel,
@@ -141,6 +146,7 @@ function resolveSessionModelRef(
agentId,
});
return (
// Persisted selected model or overrides describe the active session, not just current config.
resolvePersistedSelectedModelRef({
defaultProvider: resolved.provider || DEFAULT_PROVIDER,
runtimeProvider: entry?.modelProvider,
@@ -178,6 +184,7 @@ function resolveSessionRuntimeLabel(params: {
acpBackend: acpMeta?.backend,
});
const id = normalizeOptionalLowercaseString(runtime.id);
// OpenClaw/auto are generic labels; concrete harness ids give better operator signal.
const resolvedHarness = id && id !== "openclaw" && id !== "auto" ? id : undefined;
return resolveAgentRuntimeLabel({
config: params.cfg,
@@ -196,6 +203,7 @@ function resolveContextTokensForModel(params: {
allowAsyncLoad?: boolean;
}): number | undefined {
void params.allowAsyncLoad;
// Status summaries are synchronous/read-only; caller passes allowAsyncLoad for interface parity only.
if (typeof params.contextTokensOverride === "number" && params.contextTokensOverride > 0) {
return params.contextTokensOverride;
}
+19 -8
View File
@@ -1,3 +1,6 @@
// Builds the status summary used by human and JSON status output.
// It aggregates sessions, tasks, heartbeat, channel summary, and model/runtime metadata.
import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
import { areRuntimeModelRefsEquivalent } from "../agents/model-runtime-aliases.js";
import { getRuntimeConfig } from "../config/config.js";
@@ -96,6 +99,7 @@ function discountRetainedLostTaskFailures(
tasks: StatusSummary["tasks"],
retainedLostCount: number,
): StatusSummary["tasks"] {
// Retained lost tasks are reported separately; avoid double-counting them as active failures.
if (retainedLostCount <= 0 || tasks.failures <= 0) {
return tasks;
}
@@ -129,16 +133,20 @@ function compareSessionCandidatesByUpdatedAt(left: SessionCandidate, right: Sess
}
function listSessionCandidates(store: Record<string, SessionEntry | undefined>) {
return Object.entries(store)
.filter(([key]) => key !== "global" && key !== "unknown")
.map(([key, entry]) => ({
key,
entry,
updatedAt: entry?.updatedAt ?? null,
}))
.toSorted(compareSessionCandidatesByUpdatedAt);
return (
Object.entries(store)
// Compatibility aggregate buckets are not real user sessions.
.filter(([key]) => key !== "global" && key !== "unknown")
.map(([key, entry]) => ({
key,
entry,
updatedAt: entry?.updatedAt ?? null,
}))
.toSorted(compareSessionCandidatesByUpdatedAt)
);
}
/** Removes session paths and recent session details from a status summary. */
export function redactSensitiveStatusSummary(summary: StatusSummary): StatusSummary {
return {
...summary,
@@ -159,6 +167,7 @@ export function redactSensitiveStatusSummary(summary: StatusSummary): StatusSumm
};
}
/** Builds the aggregate status summary for agents, sessions, tasks, heartbeat, and channels. */
export async function getStatusSummary(
options: {
includeSensitive?: boolean;
@@ -212,6 +221,7 @@ export async function getStatusSummary(
const mainSessionKey = resolveMainSessionKey(cfg);
const queuedSystemEvents = peekSystemEvents(mainSessionKey);
const taskMaintenanceModule = await loadTaskRegistryMaintenanceModule();
// Configure maintenance store before reading task summaries so cron-backed tasks are in scope.
taskMaintenanceModule.configureTaskRegistryMaintenance({
cronStorePath: resolveCronJobsStorePath(cfg.cron?.store),
});
@@ -285,6 +295,7 @@ export async function getStatusSummary(
selectedModelLabel !== configuredSessionModelLabel &&
!areRuntimeModelRefsEquivalent(selectedModelLabel, configuredSessionModelLabel) &&
hasUserPinnedModelSelection(entry);
// Session rows show the live selected model but warn only for user-pinned differences.
const contextTokens =
resolveContextTokensForModel({
cfg,
+3
View File
@@ -1,3 +1,6 @@
// Public status command barrel.
// Exposes the command, summary builder, and summary types without importing implementation details.
export { statusCommand } from "./status.command.js";
export { getStatusSummary } from "./status.summary.js";
export type { SessionStatus, StatusSummary } from "./status.types.js";
+5
View File
@@ -1,3 +1,6 @@
// Shared status output types.
// These shapes are consumed by scan, summary, text report, and JSON status builders.
import type { ChannelId } from "../channels/plugins/types.public.js";
import type { SessionKind } from "../sessions/classify-session-kind.js";
import type {
@@ -38,6 +41,7 @@ export type SessionStatus = {
flags: string[];
};
/** Heartbeat schedule state for one agent. */
export type HeartbeatStatus = {
agentId: string;
enabled: boolean;
@@ -45,6 +49,7 @@ export type HeartbeatStatus = {
everyMs: number | null;
};
/** Aggregate status summary before text or JSON formatting. */
export type StatusSummary = {
runtimeVersion?: string | null;
eventLoop?: import("../gateway/server/event-loop-health.js").GatewayEventLoopHealth;
+8
View File
@@ -1,3 +1,6 @@
// Update status helpers for `openclaw status`.
// Wraps registry/git update checks and formats compact update rows/hints.
import { formatCliCommand } from "../cli/command-format.js";
import { resolveOpenClawPackageRoot } from "../infra/openclaw-root.js";
import { normalizeUpdateChannel, resolveRegistryUpdateChannel } from "../infra/update-channels.js";
@@ -8,6 +11,7 @@ import {
} from "../infra/update-check.js";
import { VERSION } from "../version.js";
/** Runs the update check using the configured update channel and current install root. */
export async function getUpdateCheckResult(params: {
timeoutMs: number;
fetchGit: boolean;
@@ -40,6 +44,7 @@ export type UpdateAvailability = {
gitBehind: number | null;
};
/** Determines whether git and/or registry data indicate an available update. */
export function resolveUpdateAvailability(update: UpdateCheckResult): UpdateAvailability {
const latestVersion = update.registry?.latestVersion ?? null;
const registryCmp = latestVersion ? compareSemverStrings(VERSION, latestVersion) : null;
@@ -59,6 +64,7 @@ export function resolveUpdateAvailability(update: UpdateCheckResult): UpdateAvai
};
}
/** Formats the actionable update hint shown in status footers. */
export function formatUpdateAvailableHint(update: UpdateCheckResult): string | null {
const availability = resolveUpdateAvailability(update);
if (!availability.available) {
@@ -76,6 +82,7 @@ export function formatUpdateAvailableHint(update: UpdateCheckResult): string | n
return `Update available${suffix}. Run: ${formatCliCommand("openclaw update")}`;
}
/** Formats a compact one-line update summary for overview rows. */
export function formatUpdateOneLiner(update: UpdateCheckResult): string {
const parts: string[] = [];
@@ -90,6 +97,7 @@ export function formatUpdateOneLiner(update: UpdateCheckResult): string {
if (update.installKind !== "git") {
parts.push("up to date");
}
// Git installs still show registry latest, but git ahead/behind remains the primary state.
parts.push(`${registryLabel} ${update.registry.latestVersion}`);
} else if (cmp != null && cmp < 0) {
parts.push(