ClawFlow: add linear flow control surface (#58227)

* ClawFlow: add linear flow control surface

* Flows: clear blocked metadata on resume
This commit is contained in:
Mariano
2026-03-31 10:08:50 +02:00
committed by GitHub
parent ab4ddff7f1
commit f86e5c0a08
21 changed files with 1108 additions and 8 deletions

View File

@@ -1,22 +1,32 @@
import type { OpenClawConfig } from "../config/config.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { createFlowForTask, deleteFlowRecordById, getFlowById } from "./flow-registry.js";
import {
createFlowForTask,
createFlowRecord,
deleteFlowRecordById,
getFlowById,
updateFlowRecordById,
} from "./flow-registry.js";
import type { FlowRecord } from "./flow-registry.types.js";
import {
cancelTaskById,
createTaskRecord,
findLatestTaskForFlowId,
linkTaskToFlowById,
listTasksForFlowId,
markTaskLostById,
markTaskRunningByRunId,
markTaskTerminalByRunId,
recordTaskProgressByRunId,
setTaskRunDeliveryStatusByRunId,
} from "./task-registry.js";
import { summarizeTaskRecords } from "./task-registry.summary.js";
import type {
TaskDeliveryState,
TaskDeliveryStatus,
TaskNotifyPolicy,
TaskRecord,
TaskRegistrySummary,
TaskRuntime,
TaskStatus,
TaskTerminalOutcome,
@@ -95,6 +105,32 @@ export function createQueuedTaskRun(params: {
});
}
export function createLinearFlow(params: {
ownerSessionKey: string;
requesterOrigin?: TaskDeliveryState["requesterOrigin"];
goal: string;
notifyPolicy?: TaskNotifyPolicy;
currentStep?: string;
createdAt?: number;
updatedAt?: number;
}): FlowRecord {
return createFlowRecord({
shape: "linear",
ownerSessionKey: params.ownerSessionKey,
requesterOrigin: params.requesterOrigin,
goal: params.goal,
notifyPolicy: params.notifyPolicy,
currentStep: params.currentStep,
status: "queued",
createdAt: params.createdAt,
updatedAt: params.updatedAt,
});
}
export function getFlowTaskSummary(flowId: string): TaskRegistrySummary {
return summarizeTaskRecords(listTasksForFlowId(flowId));
}
type RetryBlockedFlowResult = {
found: boolean;
retried: boolean;
@@ -230,6 +266,79 @@ export function retryBlockedFlowAsRunningTaskRun(
});
}
type CancelFlowResult = {
found: boolean;
cancelled: boolean;
reason?: string;
flow?: FlowRecord;
tasks?: TaskRecord[];
};
function isActiveTaskStatus(status: TaskStatus): boolean {
return status === "queued" || status === "running";
}
function isTerminalFlowStatus(status: FlowRecord["status"]): boolean {
return (
status === "succeeded" || status === "failed" || status === "cancelled" || status === "lost"
);
}
export async function cancelFlowById(params: {
cfg: OpenClawConfig;
flowId: string;
}): Promise<CancelFlowResult> {
const flow = getFlowById(params.flowId);
if (!flow) {
return {
found: false,
cancelled: false,
reason: "Flow not found.",
};
}
const linkedTasks = listTasksForFlowId(flow.flowId);
const activeTasks = linkedTasks.filter((task) => isActiveTaskStatus(task.status));
for (const task of activeTasks) {
await cancelTaskById({
cfg: params.cfg,
taskId: task.taskId,
});
}
const refreshedTasks = listTasksForFlowId(flow.flowId);
const remainingActive = refreshedTasks.filter((task) => isActiveTaskStatus(task.status));
if (remainingActive.length > 0) {
return {
found: true,
cancelled: false,
reason: "One or more child tasks are still active.",
flow: getFlowById(flow.flowId),
tasks: refreshedTasks,
};
}
if (isTerminalFlowStatus(flow.status)) {
return {
found: true,
cancelled: false,
reason: `Flow is already ${flow.status}.`,
flow,
tasks: refreshedTasks,
};
}
const updatedFlow = updateFlowRecordById(flow.flowId, {
status: "cancelled",
blockedTaskId: null,
blockedSummary: null,
endedAt: Date.now(),
updatedAt: Date.now(),
});
return {
found: true,
cancelled: true,
flow: updatedFlow ?? getFlowById(flow.flowId),
tasks: refreshedTasks,
};
}
export function createRunningTaskRun(params: {
runtime: TaskRuntime;
sourceId?: string;