Files
openclaw/src/tasks/task-registry.audit.ts
2026-04-09 03:56:22 +01:00

188 lines
5.1 KiB
TypeScript

import {
createEmptyTaskAuditSummary,
type TaskAuditCode,
type TaskAuditFinding,
type TaskAuditSeverity,
type TaskAuditSummary,
} from "./task-registry.audit.shared.js";
import type { TaskRecord } from "./task-registry.types.js";
export type TaskAuditOptions = {
now?: number;
tasks?: TaskRecord[];
staleQueuedMs?: number;
staleRunningMs?: number;
};
const DEFAULT_STALE_QUEUED_MS = 10 * 60_000;
const DEFAULT_STALE_RUNNING_MS = 30 * 60_000;
export { createEmptyTaskAuditSummary };
export type { TaskAuditCode, TaskAuditFinding, TaskAuditSeverity, TaskAuditSummary };
let taskAuditTaskProvider: () => TaskRecord[] = () => [];
export function configureTaskAuditTaskProvider(provider: () => TaskRecord[]): void {
taskAuditTaskProvider = provider;
}
function createFinding(params: {
severity: TaskAuditSeverity;
code: TaskAuditCode;
task: TaskRecord;
detail: string;
ageMs?: number;
}): TaskAuditFinding {
return {
severity: params.severity,
code: params.code,
task: params.task,
detail: params.detail,
...(typeof params.ageMs === "number" ? { ageMs: params.ageMs } : {}),
};
}
function taskReferenceAt(task: TaskRecord): number {
return task.lastEventAt ?? task.startedAt ?? task.createdAt;
}
function findTimestampInconsistency(task: TaskRecord): TaskAuditFinding | null {
if (task.startedAt && task.startedAt < task.createdAt) {
return createFinding({
severity: "warn",
code: "inconsistent_timestamps",
task,
detail: "startedAt is earlier than createdAt",
});
}
if (task.endedAt && task.startedAt && task.endedAt < task.startedAt) {
return createFinding({
severity: "warn",
code: "inconsistent_timestamps",
task,
detail: "endedAt is earlier than startedAt",
});
}
if ((task.status === "queued" || task.status === "running") && task.endedAt) {
return createFinding({
severity: "warn",
code: "inconsistent_timestamps",
task,
detail: `${task.status} task should not already have endedAt`,
});
}
return null;
}
function compareFindings(left: TaskAuditFinding, right: TaskAuditFinding): number {
const severityRank = (severity: TaskAuditSeverity) => (severity === "error" ? 0 : 1);
const severityDiff = severityRank(left.severity) - severityRank(right.severity);
if (severityDiff !== 0) {
return severityDiff;
}
const leftAge = left.ageMs ?? -1;
const rightAge = right.ageMs ?? -1;
if (leftAge !== rightAge) {
return rightAge - leftAge;
}
return left.task.createdAt - right.task.createdAt;
}
export function listTaskAuditFindings(options: TaskAuditOptions = {}): TaskAuditFinding[] {
const tasks = options.tasks ?? taskAuditTaskProvider();
const now = options.now ?? Date.now();
const staleQueuedMs = options.staleQueuedMs ?? DEFAULT_STALE_QUEUED_MS;
const staleRunningMs = options.staleRunningMs ?? DEFAULT_STALE_RUNNING_MS;
const findings: TaskAuditFinding[] = [];
for (const task of tasks) {
const referenceAt = taskReferenceAt(task);
const ageMs = Math.max(0, now - referenceAt);
if (task.status === "queued" && ageMs >= staleQueuedMs) {
findings.push(
createFinding({
severity: "warn",
code: "stale_queued",
task,
ageMs,
detail: "queued task has not advanced recently",
}),
);
}
if (task.status === "running" && ageMs >= staleRunningMs) {
findings.push(
createFinding({
severity: "error",
code: "stale_running",
task,
ageMs,
detail: "running task appears stuck",
}),
);
}
if (task.status === "lost") {
findings.push(
createFinding({
severity: "error",
code: "lost",
task,
ageMs,
detail: task.error?.trim() || "task lost its backing session",
}),
);
}
if (task.deliveryStatus === "failed" && task.notifyPolicy !== "silent") {
findings.push(
createFinding({
severity: "warn",
code: "delivery_failed",
task,
ageMs,
detail: "terminal update delivery failed",
}),
);
}
if (
task.status !== "lost" &&
task.status !== "queued" &&
task.status !== "running" &&
typeof task.cleanupAfter !== "number"
) {
findings.push(
createFinding({
severity: "warn",
code: "missing_cleanup",
task,
ageMs,
detail: "terminal task is missing cleanupAfter",
}),
);
}
const inconsistency = findTimestampInconsistency(task);
if (inconsistency) {
findings.push(inconsistency);
}
}
return findings.toSorted(compareFindings);
}
export function summarizeTaskAuditFindings(findings: Iterable<TaskAuditFinding>): TaskAuditSummary {
const summary = createEmptyTaskAuditSummary();
for (const finding of findings) {
summary.total += 1;
summary.byCode[finding.code] += 1;
if (finding.severity === "error") {
summary.errors += 1;
} else {
summary.warnings += 1;
}
}
return summary;
}