mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
37b4fc8621
* refactor(infra): extract shared git exec and verified snapshot-copy helpers Moves the worktrees git wrapper to src/infra/git-exec.ts (with optional maxOutputBytes for large buffered reads) and the online-backup/sanitize/ VACUUM/verify snapshot step into src/snapshot/openclaw-snapshot-copy.ts so snapshot backends share one hardened copy path. Behavior-identical moves; all importers updated. * feat(snapshot): git-backed versioned SQLite snapshot engine Deterministic per-table JSONL dumps (PK-ordered, lossless bigint/blob encoding), verbatim DDL preservation, virtual/shadow-table skipping with FTS rebuild on restore, secret-table redaction policy, manifest with per-table row counts and content hashes, and restore verification by re-serialization. Unchanged data produces no commit. * feat(backup): recorded runs, freshness surfacing, and scheduled git backups Every backup attempt is recorded in the previously writer-less backup_runs table (bounded to 200 rows). openclaw status gains a Backups overview row and JSON payload; doctor prints an informational hint when no successful backup is recorded or the newest is stale. New commands: backup git init/create/log/verify/restore and backup enable/disable, which provision one idempotent gateway cron job running scheduled git backups. * fix(state): stop bumping schema_meta.updated_at on unchanged opens updated_at now records when schema metadata actually changed instead of when the database was last opened; unconditional bumps dirtied the row on every open and defeated no-change backup detection. * docs: document versioned git backups, scheduling, and backup freshness * fix(backup): satisfy CI ownership checks * fix(backup): complete CI contract coverage * fix(backup): complete credential table redaction * fix(backup): isolate git repository ownership * fix(backup): persist push degradation * fix(backup): atomically converge schedules * fix(status): isolate backup freshness environment * fix(status): carry scan environment to freshness reads * fix(backup): harden Git repository ownership * docs(backup): document Git repository safety * fix(backup): non-creating outcome log and origin preflight for pushed schedules Recording a backup outcome never bootstraps an absent state database (a failed backup on a fresh host would otherwise create a blank DB that a retry treats as real input), and backup enable --push now requires the repository to have an origin remote, pointing at backup git init --remote instead of scheduling permanently degraded pushes. * refactor(worktrees): use shared git exec helpers * refactor(worktrees): remove unused git buffer wrapper * refactor(worktrees): consume buffered git helper * feat(backup): redact pushed schedules by default Unattended recurring pushes retain credential-bearing tables durably in remote Git history, so backup enable --push now defaults to --exclude-secrets; --include-secrets is the explicit full-fidelity override (still warned). Local non-push schedules keep full fidelity for complete restores. * fix(backup): redact audit HMAC and OAuth pending state; tolerate absent backup_runs Adds audit_identity_keys (audit HMAC key) and mcp_oauth_pending_authorizations (live OAuth callback state) to the redaction inventory, and makes read-only backup freshness treat a same-version database without the additive backup_runs table as no recorded backups instead of failing before a writable open converges the schema. * fix(backup): restrict schedules to local gateways * fix(snapshot): harden Git restore and redaction * fix(backup): block pushes of adopted history * fix(backup): contain commits and pairing secrets
233 lines
9.1 KiB
TypeScript
233 lines
9.1 KiB
TypeScript
// Builds the data model for the standard `openclaw status` text report.
|
|
// It converts scan/runtime state into table rows and section lines before rendering.
|
|
|
|
import { timestampMsToIsoString } from "@openclaw/normalization-core/number-coercion";
|
|
import type { ConnectPairingRequiredReason } from "../../packages/gateway-protocol/src/connect-error-details.js";
|
|
import type { RenderTableOptions, TableColumn } from "../../packages/terminal-core/src/table.js";
|
|
import type { HeartbeatEventPayload } from "../infra/heartbeat-events.js";
|
|
import type { resolveOsSummary } from "../infra/os-summary.js";
|
|
import type { PluginCompatibilityNotice } from "../plugins/status.js";
|
|
import type { SecurityAuditReport } from "../security/audit.js";
|
|
import type { SessionStatus, StatusSummary } from "../status/types.js";
|
|
import type { HealthSummary } from "./health.js";
|
|
import {
|
|
buildStatusChannelsTableRows,
|
|
statusChannelsTableColumns,
|
|
} from "./status-all/channels-table.js";
|
|
import { buildStatusCommandOverviewRows } from "./status-overview-rows.ts";
|
|
import type { StatusOverviewSurface } from "./status-overview-surface.ts";
|
|
import type { AgentLocalStatus } from "./status.agent-local.js";
|
|
import {
|
|
buildStatusFooterLines,
|
|
buildStatusHealthRows,
|
|
buildStatusModelSelectionLines,
|
|
buildStatusPairingRecoveryLines,
|
|
buildStatusPluginCompatibilityLines,
|
|
buildStatusSecurityAuditLines,
|
|
buildStatusSessionsRows,
|
|
buildStatusSystemEventsRows,
|
|
buildStatusSystemEventsTrailer,
|
|
statusHealthColumns,
|
|
type StatusMemoryStateResolvers,
|
|
} from "./status.command-sections.js";
|
|
import type { MemoryPluginStatus, MemoryStatusSnapshot } from "./status.scan.shared.js";
|
|
|
|
/** Builds all table rows, section lines, and footer data needed by the status report renderer. */
|
|
export async function buildStatusCommandReportData(
|
|
params: {
|
|
env: NodeJS.ProcessEnv;
|
|
opts: {
|
|
deep?: boolean;
|
|
verbose?: boolean;
|
|
};
|
|
surface: StatusOverviewSurface;
|
|
osSummary: ReturnType<typeof resolveOsSummary>;
|
|
summary: StatusSummary;
|
|
securityAudit?: SecurityAuditReport;
|
|
health?: HealthSummary;
|
|
usageLines?: string[];
|
|
lastHeartbeat: HeartbeatEventPayload | null;
|
|
agentStatus: {
|
|
defaultId?: string | null;
|
|
bootstrapPendingCount: number;
|
|
totalSessions: number;
|
|
agents: AgentLocalStatus[];
|
|
};
|
|
channels: {
|
|
rows: Array<{
|
|
id: string;
|
|
label: string;
|
|
enabled: boolean;
|
|
state: "ok" | "warn" | "off" | "setup";
|
|
detail: string;
|
|
}>;
|
|
};
|
|
channelIssues: Array<{
|
|
channel: string;
|
|
message: string;
|
|
}>;
|
|
memory: MemoryStatusSnapshot | null;
|
|
memoryPlugin: MemoryPluginStatus;
|
|
pluginCompatibility: PluginCompatibilityNotice[];
|
|
pairingRecovery: {
|
|
requestId: string | null;
|
|
reason: ConnectPairingRequiredReason | null;
|
|
remediationHint: string | null;
|
|
} | null;
|
|
tableWidth: number;
|
|
ok: (value: string) => string;
|
|
warn: (value: string) => string;
|
|
muted: (value: string) => string;
|
|
shortenText: (value: string, maxLen: number) => string;
|
|
formatCliCommand: (value: string) => string;
|
|
formatTimeAgo: (ageMs: number) => string;
|
|
formatKTokens: (value: number) => string;
|
|
formatTokensCompact: (value: SessionStatus) => string;
|
|
formatPromptCacheCompact: (value: SessionStatus) => string | null;
|
|
formatHealthChannelLines: (summary: HealthSummary, opts: { accountMode: "all" }) => string[];
|
|
formatPluginCompatibilityNotice: (notice: PluginCompatibilityNotice) => string;
|
|
formatUpdateAvailableHint: (update: StatusOverviewSurface["update"]) => string | null;
|
|
accentDim: (value: string) => string;
|
|
updateValue?: string;
|
|
updateRestartValue?: string | null;
|
|
theme: {
|
|
heading: (value: string) => string;
|
|
muted: (value: string) => string;
|
|
warn: (value: string) => string;
|
|
error: (value: string) => string;
|
|
};
|
|
renderTable: (input: RenderTableOptions) => string;
|
|
} & StatusMemoryStateResolvers,
|
|
) {
|
|
const overviewRows = buildStatusCommandOverviewRows({
|
|
env: params.env,
|
|
opts: params.opts,
|
|
surface: params.surface,
|
|
osLabel: params.osSummary.label,
|
|
summary: params.summary,
|
|
health: params.health,
|
|
lastHeartbeat: params.lastHeartbeat,
|
|
agentStatus: params.agentStatus,
|
|
memory: params.memory,
|
|
memoryPlugin: params.memoryPlugin,
|
|
pluginCompatibility: params.pluginCompatibility,
|
|
ok: params.ok,
|
|
warn: params.warn,
|
|
muted: params.muted,
|
|
formatTimeAgo: params.formatTimeAgo,
|
|
formatKTokens: params.formatKTokens,
|
|
resolveMemoryVectorState: params.resolveMemoryVectorState,
|
|
resolveMemoryFtsState: params.resolveMemoryFtsState,
|
|
resolveMemoryCacheSummary: params.resolveMemoryCacheSummary,
|
|
updateValue: params.updateValue,
|
|
updateRestartValue: params.updateRestartValue,
|
|
});
|
|
|
|
const sessionsColumns = [
|
|
{ key: "Key", header: "Key", minWidth: 20, flex: true },
|
|
{ key: "Kind", header: "Kind", minWidth: 6 },
|
|
{ key: "Age", header: "Age", minWidth: 9 },
|
|
{ key: "Model", header: "Model", minWidth: 14 },
|
|
{ key: "Runtime", header: "Runtime", minWidth: 14 },
|
|
{ key: "Tokens", header: "Tokens", minWidth: 16 },
|
|
// Verbose mode exposes prompt-cache details because it can widen rows substantially.
|
|
...(params.opts.verbose ? [{ key: "Cache", header: "Cache", minWidth: 16, flex: true }] : []),
|
|
] satisfies TableColumn[];
|
|
const securityAuditLines = params.securityAudit
|
|
? buildStatusSecurityAuditLines({
|
|
securityAudit: params.securityAudit,
|
|
theme: params.theme,
|
|
shortenText: params.shortenText,
|
|
formatCliCommand: params.formatCliCommand,
|
|
})
|
|
: [
|
|
params.theme.muted(
|
|
`Skipped in fast status. Full report: ${params.formatCliCommand("openclaw security audit")}`,
|
|
),
|
|
params.theme.muted(`Deep probe: ${params.formatCliCommand("openclaw status --deep")}`),
|
|
];
|
|
const retainedLost = params.summary.taskAuditRetainedLost;
|
|
// Lost task retention is operational noise unless the user requested deep/verbose status.
|
|
const retainedLostLine =
|
|
(params.opts.deep || params.opts.verbose) && retainedLost && retainedLost.count > 0
|
|
? params.theme.muted(
|
|
`${retainedLost.count} lost task${retainedLost.count === 1 ? "" : "s"} retained until ${timestampMsToIsoString(retainedLost.nextCleanupAfter) ?? "cleanupAfter"}`,
|
|
)
|
|
: null;
|
|
|
|
return {
|
|
heading: params.theme.heading,
|
|
muted: params.theme.muted,
|
|
renderTable: params.renderTable,
|
|
width: params.tableWidth,
|
|
overviewRows,
|
|
showTaskMaintenanceHint: params.summary.taskAudit.errors > 0,
|
|
taskMaintenanceHint: `Task maintenance: ${params.formatCliCommand("openclaw tasks maintenance --apply")}`,
|
|
retainedLostTaskLine: retainedLostLine,
|
|
pluginCompatibilityLines: buildStatusPluginCompatibilityLines({
|
|
notices: params.pluginCompatibility,
|
|
formatNotice: params.formatPluginCompatibilityNotice,
|
|
warn: params.theme.warn,
|
|
muted: params.theme.muted,
|
|
}),
|
|
pairingRecoveryLines: buildStatusPairingRecoveryLines({
|
|
pairingRecovery: params.pairingRecovery,
|
|
warn: params.theme.warn,
|
|
muted: params.theme.muted,
|
|
formatCliCommand: params.formatCliCommand,
|
|
}),
|
|
modelSelectionLines: buildStatusModelSelectionLines({
|
|
recent: params.summary.sessions.recent,
|
|
shortenText: params.shortenText,
|
|
warn: params.theme.warn,
|
|
muted: params.theme.muted,
|
|
}),
|
|
securityAuditLines,
|
|
channelsColumns: statusChannelsTableColumns,
|
|
channelsRows: buildStatusChannelsTableRows({
|
|
rows: params.channels.rows,
|
|
channelIssues: params.channelIssues,
|
|
ok: params.ok,
|
|
warn: params.warn,
|
|
muted: params.muted,
|
|
accentDim: params.accentDim,
|
|
formatIssueMessage: (message) => params.shortenText(message, 84),
|
|
}),
|
|
sessionsColumns,
|
|
sessionsRows: buildStatusSessionsRows({
|
|
recent: params.summary.sessions.recent,
|
|
verbose: params.opts.verbose,
|
|
shortenText: params.shortenText,
|
|
formatTimeAgo: params.formatTimeAgo,
|
|
formatTokensCompact: params.formatTokensCompact,
|
|
formatPromptCacheCompact: params.formatPromptCacheCompact,
|
|
muted: params.muted,
|
|
}),
|
|
systemEventsRows: buildStatusSystemEventsRows({
|
|
queuedSystemEvents: params.summary.queuedSystemEvents,
|
|
}),
|
|
systemEventsTrailer: buildStatusSystemEventsTrailer({
|
|
queuedSystemEvents: params.summary.queuedSystemEvents,
|
|
muted: params.muted,
|
|
}),
|
|
healthColumns: params.health ? statusHealthColumns : undefined,
|
|
healthRows: params.health
|
|
? buildStatusHealthRows({
|
|
health: params.health,
|
|
formatHealthChannelLines: params.formatHealthChannelLines,
|
|
ok: params.ok,
|
|
warn: params.warn,
|
|
muted: params.muted,
|
|
})
|
|
: undefined,
|
|
usageLines: params.usageLines,
|
|
footerLines: buildStatusFooterLines({
|
|
updateHint: params.formatUpdateAvailableHint(params.surface.update),
|
|
warn: params.theme.warn,
|
|
formatCliCommand: params.formatCliCommand,
|
|
nodeOnlyGateway: params.surface.nodeOnlyGateway,
|
|
gatewayReachable: params.surface.gatewayReachable,
|
|
}),
|
|
};
|
|
}
|