import { listTasksForFlowId } from "./runtime-internal.js" ;
import { getTaskFlowRegistryRestoreFailure, listTaskFlowRecords } from "./task-flow-registry.js" ;
import type { TaskFlowRecord } from "./task-flow-registry.types.js" ;
import type { TaskRecord } from "./task-registry.types.js" ;
export type TaskFlowAuditSeverity = "warn" | "error" ;
export type TaskFlowAuditCode =
| "restore_failed"
| "stale_running"
| "stale_waiting"
| "stale_blocked"
| "cancel_stuck"
| "missing_linked_tasks"
| "blocked_task_missing"
| "inconsistent_timestamps" ;
export type TaskFlowAuditFinding = {
severity: TaskFlowAuditSeverity;
code: TaskFlowAuditCode;
detail: string;
ageMs?: number;
flow?: TaskFlowRecord;
};
export type TaskFlowAuditSummary = {
total: number;
warnings: number;
errors: number;
byCode: Record<TaskFlowAuditCode, number>;
};
export type TaskFlowAuditOptions = {
now?: number;
flows?: TaskFlowRecord[];
staleRunningMs?: number;
staleWaitingMs?: number;
staleBlockedMs?: number;
cancelStuckMs?: number;
};
const DEFAULT_STALE_RUNNING_MS = 30 * 60 _000 ;
const DEFAULT_STALE_WAITING_MS = 30 * 60 _000 ;
const DEFAULT_STALE_BLOCKED_MS = 30 * 60 _000 ;
const DEFAULT_CANCEL_STUCK_MS = 5 * 60 _000 ;
function createFinding(params: {
severity: TaskFlowAuditSeverity;
code: TaskFlowAuditCode;
detail: string;
ageMs?: number;
flow?: TaskFlowRecord;
}): TaskFlowAuditFinding {
return {
severity: params.severity,
code: params.code,
detail: params.detail,
...(typeof params.ageMs === "number" ? { ageMs: params.ageMs } : {}),
...(params.flow ? { flow: params.flow } : {}),
};
}
function severityRank(severity: TaskFlowAuditSeverity): number {
return severity === "error" ? 0 : 1 ;
}
function compareFindings(left: TaskFlowAuditFinding, right: TaskFlowAuditFinding): number {
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.flow?.createdAt ?? 0 ) - (right.flow?.createdAt ?? 0 );
}
function getReferenceAt(flow: TaskFlowRecord): number {
return flow.updatedAt ?? flow.createdAt;
}
function getLinkedTasks(flowId: string): TaskRecord[] {
return listTasksForFlowId(flowId);
}
function hasBlockingMetadata(flow: TaskFlowRecord): boolean {
return Boolean (
flow.blockedTaskId?.trim() || flow.blockedSummary?.trim() || flow.waitJson != null ,
);
}
function findTimestampInconsistency(flow: TaskFlowRecord): TaskFlowAuditFinding | null {
if (flow.updatedAt < flow.createdAt) {
return createFinding({
severity: "warn" ,
code: "inconsistent_timestamps" ,
flow,
detail: "updatedAt is earlier than createdAt" ,
});
}
if (flow.endedAt && flow.endedAt < flow.createdAt) {
return createFinding({
severity: "warn" ,
code: "inconsistent_timestamps" ,
flow,
detail: "endedAt is earlier than createdAt" ,
});
}
if (flow.endedAt && flow.endedAt < flow.updatedAt) {
return createFinding({
severity: "warn" ,
code: "inconsistent_timestamps" ,
flow,
detail: "endedAt is earlier than updatedAt" ,
});
}
return null ;
}
export function createEmptyTaskFlowAuditSummary(): TaskFlowAuditSummary {
return {
total: 0 ,
warnings: 0 ,
errors: 0 ,
byCode: {
restore_failed: 0 ,
stale_running: 0 ,
stale_waiting: 0 ,
stale_blocked: 0 ,
cancel_stuck: 0 ,
missing_linked_tasks: 0 ,
blocked_task_missing: 0 ,
inconsistent_timestamps: 0 ,
},
};
}
export function listTaskFlowAuditFindings(
options: TaskFlowAuditOptions = {},
): TaskFlowAuditFinding[] {
const flows = options.flows ?? listTaskFlowRecords();
const now = options.now ?? Date.now();
const staleRunningMs = options.staleRunningMs ?? DEFAULT_STALE_RUNNING_MS;
const staleWaitingMs = options.staleWaitingMs ?? DEFAULT_STALE_WAITING_MS;
const staleBlockedMs = options.staleBlockedMs ?? DEFAULT_STALE_BLOCKED_MS;
const cancelStuckMs = options.cancelStuckMs ?? DEFAULT_CANCEL_STUCK_MS;
const findings: TaskFlowAuditFinding[] = [];
const restoreFailure = getTaskFlowRegistryRestoreFailure();
if (restoreFailure) {
findings.push(
createFinding({
severity: "error" ,
code: "restore_failed" ,
detail: `task-flow registry restore failed: ${restoreFailure}`,
}),
);
}
for (const flow of flows) {
const referenceAt = getReferenceAt(flow);
const ageMs = Math.max(0 , now - referenceAt);
const linkedTasks = getLinkedTasks(flow.flowId);
const activeTasks = linkedTasks.filter(
(task) => task.status === "queued" || task.status === "running" ,
);
if (flow.status === "running" && ageMs >= staleRunningMs) {
findings.push(
createFinding({
severity: "error" ,
code: "stale_running" ,
flow,
ageMs,
detail: "running TaskFlow has not advanced recently" ,
}),
);
}
if (flow.status === "waiting" && ageMs >= staleWaitingMs) {
findings.push(
createFinding({
severity: "warn" ,
code: "stale_waiting" ,
flow,
ageMs,
detail: "waiting TaskFlow has not advanced recently" ,
}),
);
}
if (flow.status === "blocked" && ageMs >= staleBlockedMs) {
findings.push(
createFinding({
severity: "warn" ,
code: "stale_blocked" ,
flow,
ageMs,
detail: "blocked TaskFlow has not advanced recently" ,
}),
);
}
if (
flow.cancelRequestedAt != null &&
flow.status !== "cancelled" &&
flow.status !== "failed" &&
flow.status !== "succeeded" &&
flow.status !== "lost" &&
activeTasks.length === 0 &&
now - flow.cancelRequestedAt >= cancelStuckMs
) {
findings.push(
createFinding({
severity: "warn" ,
code: "cancel_stuck" ,
flow,
ageMs: Math.max(0 , now - flow.cancelRequestedAt),
detail: "cancel-requested TaskFlow has no active child tasks but is still nonterminal" ,
}),
);
}
if (
flow.syncMode === "managed" &&
(flow.status === "running" || flow.status === "waiting" || flow.status === "blocked" ) &&
ageMs >=
(flow.status === "running"
? staleRunningMs
: flow.status === "waiting"
? staleWaitingMs
: staleBlockedMs) &&
linkedTasks.length === 0 &&
!hasBlockingMetadata(flow)
) {
findings.push(
createFinding({
severity: flow.status === "running" ? "error" : "warn" ,
code: "missing_linked_tasks" ,
flow,
ageMs,
detail: "managed TaskFlow has no linked tasks or wait state" ,
}),
);
}
if (flow.blockedTaskId?.trim()) {
const blockedTaskId = flow.blockedTaskId.trim();
if (!linkedTasks.some((task) => task.taskId === blockedTaskId)) {
findings.push(
createFinding({
severity: "warn" ,
code: "blocked_task_missing" ,
flow,
ageMs,
detail: `blocked TaskFlow points at missing task ${blockedTaskId}`,
}),
);
}
}
const inconsistency = findTimestampInconsistency(flow);
if (inconsistency) {
findings.push(inconsistency);
}
}
return findings.toSorted(compareFindings);
}
export function summarizeTaskFlowAuditFindings(
findings: Iterable<TaskFlowAuditFinding>,
): TaskFlowAuditSummary {
const summary = createEmptyTaskFlowAuditSummary();
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;
}
Messung V0.5 in Prozent C=99 H=98 G=98
¤ Dauer der Verarbeitung: 0.8 Sekunden
(vorverarbeitet am 2026-06-10)
¤
*© Formatika GbR, Deutschland