Files
openclaw/src/tasks/detached-task-runtime-contract.ts
Masato Hoshino 153ee2abba fix(subagents): killed subagent runs stay running in the task list (#99806)
* fix(subagents): reconcile killed task outcomes

Co-authored-by: masatohoshino <g515hoshino@gmail.com>

* fix(subagents): retain reconciliation marker narrowing

* fix(subagents): honor generations in latest-run views

* test(subagents): align reconciliation fixtures

* fix(subagents): keep steer recovery best effort

* style(tasks): use type-only runtime contract import

* refactor(subagents): keep generation ordering leaf-only

* fix(subagents): sanitize persisted task owner ids

* fix(subagents): preserve failed replay lifecycle reason

* fix(agents): clear killed lifecycle timeout grace

* fix(tasks): preserve canonical subagent outcomes

* fix(agents): continue requester-wide cancellation

* fix(agents): fence yield revival races

* test(agents): mock detached task lookup

* fix: fence subagent cleanup deletion

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-07-05 02:17:10 -07:00

187 lines
5.3 KiB
TypeScript

// Defines the detached task runtime contract and spawn options.
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type {
TaskDeliveryState,
TaskDeliveryStatus,
TaskNotifyPolicy,
TaskRecord,
TaskRuntime,
TaskScopeKind,
TaskStatus,
TaskTerminalOutcome,
} from "./task-registry.types.js";
// A killed subagent can still report a completion that raced the kill marker.
// Task cancellation replaces this marker once the operator request is accepted.
export const SUBAGENT_KILL_TASK_ERROR = "Subagent run killed.";
export type DetachedTaskCreateParams = {
runtime: TaskRuntime;
taskKind?: string;
sourceId?: string;
requesterSessionKey?: string;
ownerKey?: string;
scopeKind?: TaskScopeKind;
requesterOrigin?: TaskDeliveryState["requesterOrigin"];
parentFlowId?: string;
childSessionKey?: string;
parentTaskId?: string;
agentId?: string;
requesterAgentId?: string;
runId?: string;
label?: string;
task: string;
preferMetadata?: boolean;
notifyPolicy?: TaskNotifyPolicy;
deliveryStatus?: TaskDeliveryStatus;
};
export type DetachedRunningTaskCreateParams = DetachedTaskCreateParams & {
startedAt?: number;
lastEventAt?: number;
progressSummary?: string | null;
};
export type DetachedTaskStartParams = {
runId: string;
runtime?: TaskRuntime;
sessionKey?: string;
startedAt?: number;
lastEventAt?: number;
progressSummary?: string | null;
eventSummary?: string | null;
};
export type DetachedTaskProgressParams = {
runId: string;
runtime?: TaskRuntime;
sessionKey?: string;
lastEventAt?: number;
progressSummary?: string | null;
eventSummary?: string | null;
};
export type DetachedTaskCompleteParams = {
runId: string;
runtime?: TaskRuntime;
sessionKey?: string;
endedAt: number;
lastEventAt?: number;
progressSummary?: string | null;
terminalSummary?: string | null;
terminalOutcome?: TaskTerminalOutcome | null;
suppressDelivery?: boolean;
};
export type DetachedTaskFailParams = {
runId: string;
runtime?: TaskRuntime;
sessionKey?: string;
status?: Extract<TaskStatus, "failed" | "timed_out" | "cancelled">;
endedAt: number;
lastEventAt?: number;
error?: string;
progressSummary?: string | null;
terminalSummary?: string | null;
suppressDelivery?: boolean;
};
export type DetachedTaskFinalizeParams = {
runId: string;
runtime?: TaskRuntime;
sessionKey?: string;
status: Extract<TaskStatus, "succeeded" | "failed" | "timed_out" | "cancelled">;
endedAt: number;
lastEventAt?: number;
error?: string;
progressSummary?: string | null;
terminalSummary?: string | null;
terminalOutcome?: TaskTerminalOutcome | null;
suppressDelivery?: boolean;
};
export type DetachedTaskTerminalState = Omit<
DetachedTaskFinalizeParams,
"runId" | "runtime" | "sessionKey"
>;
export type DetachedTaskDeliveryStatusParams = {
runId: string;
runtime?: TaskRuntime;
sessionKey?: string;
deliveryStatus: TaskDeliveryStatus;
error?: string;
};
export type DetachedTaskCancelParams = {
cfg: OpenClawConfig;
taskId: string;
reason?: string;
};
export type DetachedTaskCancelResult = {
found: boolean;
cancelled: boolean;
reason?: string;
task?: TaskRecord;
};
export type DetachedTaskRecoveryAttemptParams = {
taskId: string;
runtime: TaskRuntime;
task: TaskRecord;
now: number;
};
export type DetachedTaskRecoveryAttemptResult = {
recovered: boolean;
};
export type DetachedTaskFindParams = {
runId: string;
runtime: TaskRuntime;
sessionKey: string;
createdAtOrAfter: number;
createdBefore?: number;
allowSessionFallback?: boolean;
};
export type DetachedTaskFindResult =
| { lookup: "available"; task?: TaskRecord }
| { lookup: "unavailable"; task?: undefined };
export type DetachedTaskLifecycleRuntime = {
createQueuedTaskRun: (params: DetachedTaskCreateParams) => TaskRecord | null;
createRunningTaskRun: (params: DetachedRunningTaskCreateParams) => TaskRecord | null;
startTaskRunByRunId: (params: DetachedTaskStartParams) => TaskRecord[];
recordTaskRunProgressByRunId: (params: DetachedTaskProgressParams) => TaskRecord[];
finalizeTaskRunByRunId?: (params: DetachedTaskFinalizeParams) => TaskRecord[];
completeTaskRunByRunId: (params: DetachedTaskCompleteParams) => TaskRecord[];
failTaskRunByRunId: (params: DetachedTaskFailParams) => TaskRecord[];
setDetachedTaskDeliveryStatusByRunId: (params: DetachedTaskDeliveryStatusParams) => TaskRecord[];
/**
* Resolve the task owned by one run generation. Custom runtimes should
* implement this when their records are not mirrored into core task state.
*/
findTaskRun?: (params: DetachedTaskFindParams) => TaskRecord | undefined;
/**
* Return `found: false` when this runtime does not own the task so core can
* fall back to the legacy detached-task cancel path.
*/
cancelDetachedTaskRunById: (
params: DetachedTaskCancelParams,
) => Promise<DetachedTaskCancelResult>;
/**
* Give a registered detached runtime one last chance to recover a stale task
* before core marks it lost during maintenance.
*/
tryRecoverTaskBeforeMarkLost?: (
params: DetachedTaskRecoveryAttemptParams,
) => DetachedTaskRecoveryAttemptResult | Promise<DetachedTaskRecoveryAttemptResult>;
};
export type DetachedTaskLifecycleRuntimeRegistration = {
pluginId: string;
runtime: DetachedTaskLifecycleRuntime;
};