feat(status): surface task run pressure (#57350)

* feat(status): surface task run pressure

* Update src/commands/tasks.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
Vincent Koc
2026-03-29 17:09:10 -07:00
committed by GitHub
parent 93dd25e6b2
commit e6445c22aa
18 changed files with 275 additions and 7 deletions

View File

@@ -10,7 +10,8 @@ import {
resolveTaskForLookupToken,
updateTaskRecordById,
} from "./task-registry.js";
import type { TaskRecord } from "./task-registry.types.js";
import { summarizeTaskRecords } from "./task-registry.summary.js";
import type { TaskRecord, TaskRegistrySummary } from "./task-registry.types.js";
const TASK_RECONCILE_GRACE_MS = 5 * 60_000;
const TASK_RETENTION_MS = 7 * 24 * 60 * 60_000;
@@ -124,6 +125,10 @@ export function reconcileInspectableTasks(): TaskRecord[] {
return listTaskRecords().map((task) => reconcileTaskRecordForOperatorInspection(task));
}
export function getInspectableTaskRegistrySummary(): TaskRegistrySummary {
return summarizeTaskRecords(reconcileInspectableTasks());
}
export function reconcileTaskLookupToken(token: string): TaskRecord | undefined {
ensureTaskRegistryReady();
const task = resolveTaskForLookupToken(token);

View File

@@ -0,0 +1,56 @@
import type {
TaskRecord,
TaskRegistrySummary,
TaskRuntimeCounts,
TaskStatusCounts,
} from "./task-registry.types.js";
function createEmptyTaskStatusCounts(): TaskStatusCounts {
return {
queued: 0,
running: 0,
succeeded: 0,
failed: 0,
timed_out: 0,
cancelled: 0,
lost: 0,
};
}
function createEmptyTaskRuntimeCounts(): TaskRuntimeCounts {
return {
subagent: 0,
acp: 0,
cli: 0,
cron: 0,
};
}
export function createEmptyTaskRegistrySummary(): TaskRegistrySummary {
return {
total: 0,
active: 0,
terminal: 0,
failures: 0,
byStatus: createEmptyTaskStatusCounts(),
byRuntime: createEmptyTaskRuntimeCounts(),
};
}
export function summarizeTaskRecords(records: Iterable<TaskRecord>): TaskRegistrySummary {
const summary = createEmptyTaskRegistrySummary();
for (const task of records) {
summary.total += 1;
summary.byStatus[task.status] += 1;
summary.byRuntime[task.runtime] += 1;
if (task.status === "queued" || task.status === "running") {
summary.active += 1;
} else {
summary.terminal += 1;
}
if (task.status === "failed" || task.status === "timed_out" || task.status === "lost") {
summary.failures += 1;
}
}
return summary;
}

View File

@@ -11,6 +11,7 @@ import {
createTaskRecord,
findTaskByRunId,
getTaskById,
getTaskRegistrySummary,
listTaskRecords,
maybeDeliverTaskStateChangeUpdate,
maybeDeliverTaskTerminalUpdate,
@@ -141,6 +142,60 @@ describe("task-registry", () => {
});
});
it("summarizes task pressure by status and runtime", async () => {
await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => {
process.env.OPENCLAW_STATE_DIR = root;
resetTaskRegistryForTests();
createTaskRecord({
runtime: "acp",
requesterSessionKey: "agent:main:main",
runId: "run-summary-acp",
task: "Investigate issue",
status: "queued",
deliveryStatus: "pending",
});
createTaskRecord({
runtime: "cron",
requesterSessionKey: "",
runId: "run-summary-cron",
task: "Daily digest",
status: "running",
deliveryStatus: "not_applicable",
});
createTaskRecord({
runtime: "subagent",
requesterSessionKey: "agent:main:main",
runId: "run-summary-subagent",
task: "Write patch",
status: "timed_out",
deliveryStatus: "session_queued",
});
expect(getTaskRegistrySummary()).toEqual({
total: 3,
active: 2,
terminal: 1,
failures: 1,
byStatus: {
queued: 1,
running: 1,
succeeded: 0,
failed: 0,
timed_out: 1,
cancelled: 0,
lost: 0,
},
byRuntime: {
subagent: 1,
acp: 1,
cli: 0,
cron: 1,
},
});
});
});
it("delivers ACP completion to the requester channel when a delivery origin exists", async () => {
await withTempDir({ prefix: "openclaw-task-registry-" }, async (root) => {
process.env.OPENCLAW_STATE_DIR = root;

View File

@@ -15,12 +15,14 @@ import {
resetTaskRegistryRuntimeForTests,
type TaskRegistryHookEvent,
} from "./task-registry.store.js";
import { summarizeTaskRecords } from "./task-registry.summary.js";
import type {
TaskDeliveryStatus,
TaskEventKind,
TaskEventRecord,
TaskNotifyPolicy,
TaskRecord,
TaskRegistrySummary,
TaskRegistrySnapshot,
TaskRuntime,
TaskStatus,
@@ -1049,6 +1051,11 @@ export function listTaskRecords(): TaskRecord[] {
.toSorted((a, b) => b.createdAt - a.createdAt);
}
export function getTaskRegistrySummary(): TaskRegistrySummary {
ensureTaskRegistryReady();
return summarizeTaskRecords(tasks.values());
}
export function getTaskRegistrySnapshot(): TaskRegistrySnapshot {
return {
tasks: listTaskRecords(),

View File

@@ -23,6 +23,18 @@ export type TaskNotifyPolicy = "done_only" | "state_changes" | "silent";
export type TaskTerminalOutcome = "succeeded" | "blocked";
export type TaskStatusCounts = Record<TaskStatus, number>;
export type TaskRuntimeCounts = Record<TaskRuntime, number>;
export type TaskRegistrySummary = {
total: number;
active: number;
terminal: number;
failures: number;
byStatus: TaskStatusCounts;
byRuntime: TaskRuntimeCounts;
};
export type TaskEventKind = TaskStatus | "progress";
export type TaskEventRecord = {