Skip to content

Commit c52fac8

Browse files
authored
feat(tasks): add status health and maintenance command (#57423)
* feat(tasks): add status health and maintenance command * fix(tasks): address status and maintenance review feedback
1 parent d28349c commit c52fac8

14 files changed

Lines changed: 476 additions & 5 deletions

src/cli/program/register.status-health-sessions.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const sessionsCommand = vi.fn();
88
const sessionsCleanupCommand = vi.fn();
99
const tasksListCommand = vi.fn();
1010
const tasksAuditCommand = vi.fn();
11+
const tasksMaintenanceCommand = vi.fn();
1112
const tasksShowCommand = vi.fn();
1213
const tasksNotifyCommand = vi.fn();
1314
const tasksCancelCommand = vi.fn();
@@ -34,6 +35,7 @@ vi.mock("../../commands/sessions-cleanup.js", () => ({
3435
vi.mock("../../commands/tasks.js", () => ({
3536
tasksListCommand,
3637
tasksAuditCommand,
38+
tasksMaintenanceCommand,
3739
tasksShowCommand,
3840
tasksNotifyCommand,
3941
tasksCancelCommand,
@@ -70,6 +72,7 @@ describe("registerStatusHealthSessionsCommands", () => {
7072
sessionsCleanupCommand.mockResolvedValue(undefined);
7173
tasksListCommand.mockResolvedValue(undefined);
7274
tasksAuditCommand.mockResolvedValue(undefined);
75+
tasksMaintenanceCommand.mockResolvedValue(undefined);
7376
tasksShowCommand.mockResolvedValue(undefined);
7477
tasksNotifyCommand.mockResolvedValue(undefined);
7578
tasksCancelCommand.mockResolvedValue(undefined);
@@ -245,6 +248,18 @@ describe("registerStatusHealthSessionsCommands", () => {
245248
);
246249
});
247250

251+
it("runs tasks maintenance subcommand with apply forwarding", async () => {
252+
await runCli(["tasks", "--json", "maintenance", "--apply"]);
253+
254+
expect(tasksMaintenanceCommand).toHaveBeenCalledWith(
255+
expect.objectContaining({
256+
json: true,
257+
apply: true,
258+
}),
259+
runtime,
260+
);
261+
});
262+
248263
it("runs tasks audit subcommand with filters", async () => {
249264
await runCli([
250265
"tasks",

src/cli/program/register.status-health-sessions.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
tasksAuditCommand,
88
tasksCancelCommand,
99
tasksListCommand,
10+
tasksMaintenanceCommand,
1011
tasksNotifyCommand,
1112
tasksShowCommand,
1213
} from "../../commands/tasks.js";
@@ -305,6 +306,24 @@ export function registerStatusHealthSessionsCommands(program: Command) {
305306
});
306307
});
307308

309+
tasksCmd
310+
.command("maintenance")
311+
.description("Preview or apply task ledger maintenance")
312+
.option("--json", "Output as JSON", false)
313+
.option("--apply", "Apply reconciliation, cleanup stamping, and pruning", false)
314+
.action(async (opts, command) => {
315+
const parentOpts = command.parent?.opts() as { json?: boolean } | undefined;
316+
await runCommandWithRuntime(defaultRuntime, async () => {
317+
await tasksMaintenanceCommand(
318+
{
319+
json: Boolean(opts.json || parentOpts?.json),
320+
apply: Boolean(opts.apply),
321+
},
322+
defaultRuntime,
323+
);
324+
});
325+
});
326+
308327
tasksCmd
309328
.command("show")
310329
.description("Show one background task by task id, run id, or session key")

src/commands/status.command.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,13 @@ export async function statusCommand(
394394
summary.tasks.failures > 0
395395
? warn(`${summary.tasks.failures} issue${summary.tasks.failures === 1 ? "" : "s"}`)
396396
: muted("no issues"),
397+
summary.taskAudit.errors > 0
398+
? warn(
399+
`audit ${summary.taskAudit.errors} error${summary.taskAudit.errors === 1 ? "" : "s"} · ${summary.taskAudit.warnings} warn`,
400+
)
401+
: summary.taskAudit.warnings > 0
402+
? muted(`audit ${summary.taskAudit.warnings} warn`)
403+
: muted("audit clean"),
397404
`${summary.tasks.total} tracked`,
398405
].join(" · ")
399406
: muted("none");

src/commands/status.scan.json-core.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { UpdateCheckResult } from "../infra/update-check.js";
33
import { loggingState } from "../logging/state.js";
44
import { runExec } from "../process/exec.js";
55
import type { RuntimeEnv } from "../runtime.js";
6+
import { createEmptyTaskAuditSummary } from "../tasks/task-registry.audit.js";
67
import { createEmptyTaskRegistrySummary } from "../tasks/task-registry.summary.js";
78
import type { getAgentLocalStatuses as getAgentLocalStatusesFn } from "./status.agent-local.js";
89
import type { StatusScanResult } from "./status.scan.js";
@@ -74,6 +75,7 @@ function buildColdStartStatusSummary(): Awaited<ReturnType<typeof getStatusSumma
7475
channelSummary: [],
7576
queuedSystemEvents: [],
7677
tasks: createEmptyTaskRegistrySummary(),
78+
taskAudit: createEmptyTaskAuditSummary(),
7779
sessions: {
7880
paths: [],
7981
count: 0,

src/commands/status.scan.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
import { runExec } from "../process/exec.js";
1818
import type { RuntimeEnv } from "../runtime.js";
1919
import { createLazyRuntimeSurface } from "../shared/lazy-runtime.js";
20+
import { createEmptyTaskAuditSummary } from "../tasks/task-registry.audit.js";
2021
import { createEmptyTaskRegistrySummary } from "../tasks/task-registry.summary.js";
2122
import type { buildChannelsTable as buildChannelsTableFn } from "./status-all/channels.js";
2223
import type { getAgentLocalStatuses as getAgentLocalStatusesFn } from "./status.agent-local.js";
@@ -174,6 +175,7 @@ function buildColdStartStatusSummary(): Awaited<ReturnType<typeof getStatusSumma
174175
channelSummary: [],
175176
queuedSystemEvents: [],
176177
tasks: createEmptyTaskRegistrySummary(),
178+
taskAudit: createEmptyTaskAuditSummary(),
177179
sessions: {
178180
paths: [],
179181
count: 0,

src/commands/status.summary.redaction.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,19 @@ describe("redactSensitiveStatusSummary", () => {
5050
cron: 1,
5151
},
5252
},
53+
taskAudit: {
54+
total: 1,
55+
warnings: 1,
56+
errors: 0,
57+
byCode: {
58+
stale_queued: 0,
59+
stale_running: 0,
60+
lost: 0,
61+
delivery_failed: 1,
62+
missing_cleanup: 0,
63+
inconsistent_timestamps: 0,
64+
},
65+
},
5366
sessions: {
5467
paths: ["/tmp/openclaw/sessions.json"],
5568
count: 1,
@@ -76,5 +89,6 @@ describe("redactSensitiveStatusSummary", () => {
7689
expect(redacted.heartbeat).toEqual(input.heartbeat);
7790
expect(redacted.channelSummary).toEqual(input.channelSummary);
7891
expect(redacted.tasks).toEqual(input.tasks);
92+
expect(redacted.taskAudit).toEqual(input.taskAudit);
7993
});
8094
});

src/commands/status.summary.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,19 @@ vi.mock("../tasks/task-registry.maintenance.js", () => ({
8181
cron: 0,
8282
},
8383
})),
84+
getInspectableTaskAuditSummary: vi.fn(() => ({
85+
total: 1,
86+
warnings: 1,
87+
errors: 0,
88+
byCode: {
89+
stale_queued: 0,
90+
stale_running: 0,
91+
lost: 0,
92+
delivery_failed: 1,
93+
missing_cleanup: 0,
94+
inconsistent_timestamps: 0,
95+
},
96+
})),
8497
}));
8598

8699
vi.mock("../routing/session-key.js", () => ({
@@ -121,6 +134,7 @@ describe("getStatusSummary", () => {
121134
expect(summary.heartbeat.defaultAgentId).toBe("main");
122135
expect(summary.channelSummary).toEqual(["ok"]);
123136
expect(summary.tasks.active).toBe(0);
137+
expect(summary.taskAudit.warnings).toBe(1);
124138
});
125139

126140
it("skips channel summary imports when no channels are configured", async () => {

src/commands/status.summary.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,9 @@ export async function getStatusSummary(
144144
: [];
145145
const mainSessionKey = resolveMainSessionKey(cfg);
146146
const queuedSystemEvents = peekSystemEvents(mainSessionKey);
147-
const tasks = (await loadTaskRegistryMaintenanceModule()).getInspectableTaskRegistrySummary();
147+
const taskMaintenanceModule = await loadTaskRegistryMaintenanceModule();
148+
const tasks = taskMaintenanceModule.getInspectableTaskRegistrySummary();
149+
const taskAudit = taskMaintenanceModule.getInspectableTaskAuditSummary();
148150

149151
const resolved = resolveConfiguredStatusModelRef({
150152
cfg,
@@ -273,6 +275,7 @@ export async function getStatusSummary(
273275
channelSummary,
274276
queuedSystemEvents,
275277
tasks,
278+
taskAudit,
276279
sessions: {
277280
paths: Array.from(paths),
278281
count: totalSessions,

src/commands/status.types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { ChannelId } from "../channels/plugins/types.js";
2+
import type { TaskAuditSummary } from "../tasks/task-registry.audit.js";
23
import type { TaskRegistrySummary } from "../tasks/task-registry.types.js";
34

45
export type SessionStatus = {
@@ -50,6 +51,7 @@ export type StatusSummary = {
5051
channelSummary: string[];
5152
queuedSystemEvents: string[];
5253
tasks: TaskRegistrySummary;
54+
taskAudit: TaskAuditSummary;
5355
sessions: {
5456
paths: string[];
5557
count: number;

0 commit comments

Comments
 (0)