Skip to content

TaskFlow: add managed child task execution#59610

Merged
mbelinky merged 5 commits into
mainfrom
workspace/clawflow-run-task-in-flow
Apr 2, 2026
Merged

TaskFlow: add managed child task execution#59610
mbelinky merged 5 commits into
mainfrom
workspace/clawflow-run-task-in-flow

Conversation

@mbelinky

@mbelinky mbelinky commented Apr 2, 2026

Copy link
Copy Markdown
Contributor

Summary

  • let managed TaskFlows create child tasks under themselves
  • make cancellation sticky so orchestrators stop scheduling immediately
  • keep tasks as the units of work while TaskFlow manages the parent-job lifecycle above them

Supersedes

What This PR Does

This is the first execution layer on top of the restored TaskFlow substrate.

Before this PR, TaskFlow could exist as durable state, but a managed TaskFlow could not yet launch child tasks under itself in a first-class way.
After this PR, a managed TaskFlow can spawn child tasks, and cancellation behaves like a real parent-job cancel: mark stop intent first, then shut down active children, then settle the TaskFlow once those children finish.

This PR adds:

  • runTaskInFlow(...) / owner-scoped spawning for managed TaskFlows
  • sticky cancelRequestedAt semantics before child shutdown
  • finalization to cancelled once the last active child settles
  • denial of new child-task creation after cancel has been requested or the TaskFlow is already terminal

What This PR Does Not Do

  • no DAG scheduling
  • no branching or loop semantics
  • no plugin DSL
  • no public SDK seam yet

Tasks vs TaskFlow

  • Tasks still do the work
  • TaskFlow now actually manages multi-task work by spawning child tasks under one parent job

Validation

  • mb-server: pnpm build
  • mb-server: focused Vitest slice across flow/task/doctor surfaces

@openclaw-barnacle openclaw-barnacle Bot added commands Command implementations size: L maintainer Maintainer-authored PR labels Apr 2, 2026
@greptile-apps

greptile-apps Bot commented Apr 2, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds managed child task spawning to the TaskFlow substrate: runTaskInFlow / runTaskInFlowForOwner let a managed TaskFlow spawn child tasks under itself, cancelRequestedAt is now stamped on a flow before children are shut down (sticky cancel intent), and syncManagedFlowCancellationFromTask finalises the flow to cancelled once its last active child settles — enabling external orchestrators to cancel immediately and let the parent job settle asynchronously.

Key changes and observations:

  • cancelFlowById refactor – stamps cancelRequestedAt first, attempts child shutdown, and correctly handles the case where syncManagedFlowCancellationFromTask finalises the flow synchronously inside the cancellation loop (via the post-loop terminal status re-check).
  • assertParentFlowLinkAllowed upgrade – now throws typed ParentFlowLinkError with discriminated code values, making error handling in mapRunTaskInFlowCreateError exhaustive for the cases reachable from runTaskInFlow.
  • Helper duplicationisActiveTaskStatus and isTerminalFlowStatus are defined verbatim in both task-executor.ts and task-registry.ts; extracting them to a shared location would remove the drift risk.
  • mapRunTaskInFlowCreateError coverageowner_key_mismatch and scope_kind_not_session error codes are unreachable from the current call site but have no branch and silently re-throw; a brief comment would make the intentional exclusion explicit for future maintainers.
  • Test coverage is solid: happy-path spawning, pre-cancel rejection, sticky cancel intent with deferred finalisation, and cross-owner denial are all exercised.

Confidence Score: 4/5

  • Safe to merge; no logic bugs identified, all critical paths are tested, and the two flagged items are minor code quality concerns.
  • The cancellation flow, sticky cancel intent, and deferred finalisation logic are all correct and well-tested. The one point deducted is for the duplicated private helper functions (isActiveTaskStatus / isTerminalFlowStatus) across two modules, which creates a maintenance drift risk, and the implicit exhaustiveness gap in mapRunTaskInFlowCreateError for currently-unreachable error codes.
  • src/tasks/task-executor.ts — duplicated helpers and the mapRunTaskInFlowCreateError coverage note.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/tasks/task-executor.ts
Line: 389-397

Comment:
**Duplicated helpers also present in `task-registry.ts`**

`isActiveTaskStatus` (lines 389–391) and `isTerminalFlowStatus` (lines 393–397) are defined verbatim in both `task-executor.ts` and `task-registry.ts` (lines ~83 and ~100 respectively). If the definition of "active" or "terminal" ever needs to change, both copies must be updated in sync.

These should either live in a shared utility (e.g., `task-status-helpers.ts`) and imported into both modules, or one module should import them from the other. As-is, a change to one copy won't automatically propagate.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/tasks/task-executor.ts
Line: 452-484

Comment:
**`owner_key_mismatch` and `scope_kind_not_session` errors are silently re-thrown**

`mapRunTaskInFlowCreateError` handles `cancel_requested`, `terminal`, and `parent_flow_not_found` codes, but falls through to `throw params.error` for `owner_key_mismatch` and `scope_kind_not_session`.

In the current call site (`runTaskInFlow`) these codes are unreachable because `ownerKey` is always set from `flow.ownerKey` and `scopeKind` is hardcoded to `"session"`. However, if a future call site ever creates a task with different params and routes through this helper, the raw `ParentFlowLinkError` would escape as an unhandled exception instead of returning a structured `RunTaskInFlowResult`.

Adding explicit branches (or at minimum a developer comment marking them as intentionally unreachable) would make the exhaustiveness contract clear to future maintainers:

```typescript
// scope_kind_not_session and owner_key_mismatch are unreachable from runTaskInFlow
// because scopeKind is always "session" and ownerKey is always flow.ownerKey.
// Re-throw as a programmer error if somehow reached.
throw params.error;
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "Changelog: update TaskFlow PR reference" | Re-trigger Greptile

Comment on lines 389 to 397
@@ -386,6 +396,237 @@ function isTerminalFlowStatus(status: FlowRecord["status"]): boolean {
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Duplicated helpers also present in task-registry.ts

isActiveTaskStatus (lines 389–391) and isTerminalFlowStatus (lines 393–397) are defined verbatim in both task-executor.ts and task-registry.ts (lines ~83 and ~100 respectively). If the definition of "active" or "terminal" ever needs to change, both copies must be updated in sync.

These should either live in a shared utility (e.g., task-status-helpers.ts) and imported into both modules, or one module should import them from the other. As-is, a change to one copy won't automatically propagate.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/tasks/task-executor.ts
Line: 389-397

Comment:
**Duplicated helpers also present in `task-registry.ts`**

`isActiveTaskStatus` (lines 389–391) and `isTerminalFlowStatus` (lines 393–397) are defined verbatim in both `task-executor.ts` and `task-registry.ts` (lines ~83 and ~100 respectively). If the definition of "active" or "terminal" ever needs to change, both copies must be updated in sync.

These should either live in a shared utility (e.g., `task-status-helpers.ts`) and imported into both modules, or one module should import them from the other. As-is, a change to one copy won't automatically propagate.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +452 to +484
function mapRunTaskInFlowCreateError(params: {
error: unknown;
flowId: string;
}): RunTaskInFlowResult {
const flow = getFlowById(params.flowId);
if (isParentFlowLinkError(params.error)) {
if (params.error.code === "cancel_requested") {
return {
found: true,
created: false,
reason: "Flow cancellation has already been requested.",
...(flow ? { flow } : {}),
};
}
if (params.error.code === "terminal") {
const terminalStatus = flow?.status ?? params.error.details?.status ?? "terminal";
return {
found: true,
created: false,
reason: `Flow is already ${terminalStatus}.`,
...(flow ? { flow } : {}),
};
}
if (params.error.code === "parent_flow_not_found") {
return {
found: false,
created: false,
reason: "Flow not found.",
};
}
}
throw params.error;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 owner_key_mismatch and scope_kind_not_session errors are silently re-thrown

mapRunTaskInFlowCreateError handles cancel_requested, terminal, and parent_flow_not_found codes, but falls through to throw params.error for owner_key_mismatch and scope_kind_not_session.

In the current call site (runTaskInFlow) these codes are unreachable because ownerKey is always set from flow.ownerKey and scopeKind is hardcoded to "session". However, if a future call site ever creates a task with different params and routes through this helper, the raw ParentFlowLinkError would escape as an unhandled exception instead of returning a structured RunTaskInFlowResult.

Adding explicit branches (or at minimum a developer comment marking them as intentionally unreachable) would make the exhaustiveness contract clear to future maintainers:

// scope_kind_not_session and owner_key_mismatch are unreachable from runTaskInFlow
// because scopeKind is always "session" and ownerKey is always flow.ownerKey.
// Re-throw as a programmer error if somehow reached.
throw params.error;
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/tasks/task-executor.ts
Line: 452-484

Comment:
**`owner_key_mismatch` and `scope_kind_not_session` errors are silently re-thrown**

`mapRunTaskInFlowCreateError` handles `cancel_requested`, `terminal`, and `parent_flow_not_found` codes, but falls through to `throw params.error` for `owner_key_mismatch` and `scope_kind_not_session`.

In the current call site (`runTaskInFlow`) these codes are unreachable because `ownerKey` is always set from `flow.ownerKey` and `scopeKind` is hardcoded to `"session"`. However, if a future call site ever creates a task with different params and routes through this helper, the raw `ParentFlowLinkError` would escape as an unhandled exception instead of returning a structured `RunTaskInFlowResult`.

Adding explicit branches (or at minimum a developer comment marking them as intentionally unreachable) would make the exhaustiveness contract clear to future maintainers:

```typescript
// scope_kind_not_session and owner_key_mismatch are unreachable from runTaskInFlow
// because scopeKind is always "session" and ownerKey is always flow.ownerKey.
// Re-throw as a programmer error if somehow reached.
throw params.error;
```

How can I resolve this? If you propose a fix, please make it concise.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9f2abfc424

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +642 to +646
if (isTerminalFlowStatus(flow.status)) {
return {
found: true,
cancelled: false,
reason: `Flow is already ${flow.status}.`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Cancel active child tasks even when flow is terminal

This early terminal-status return skips the child-cancellation loop entirely, so cancelFlowById no longer cleans up queued/running child tasks when flow/task state is inconsistent (for example after partial restore or prior failed updates). Before this change, cancellation still attempted cancelTaskById first, which gave operators a recovery path; now the call exits with "Flow is already ..." and leaves active children running.

Useful? React with 👍 / 👎.

Comment on lines +652 to +656
if ("reason" in cancelRequestedFlow) {
return {
found: true,
cancelled: false,
reason: cancelRequestedFlow.reason,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retry cancel-intent write on flow revision conflicts

If requestFlowCancel returns a revision conflict, this branch returns immediately and never reaches the loop that cancels active child tasks. That means a concurrent metadata/state update on the flow can turn a cancel request into a no-op until someone retries manually, even though children are still running. The cancel path should reconcile with the latest revision (or retry) instead of aborting before task cancellation.

Useful? React with 👍 / 👎.

@mbelinky mbelinky force-pushed the workspace/clawflow-run-task-in-flow branch from 15d453b to 498779a Compare April 2, 2026 10:40
@mbelinky mbelinky force-pushed the workspace/clawflow-run-task-in-flow branch from 498779a to e6cdde6 Compare April 2, 2026 10:43
@mbelinky mbelinky merged commit 8bdca23 into main Apr 2, 2026
22 of 26 checks passed
@mbelinky mbelinky deleted the workspace/clawflow-run-task-in-flow branch April 2, 2026 10:45
@mbelinky

mbelinky commented Apr 2, 2026

Copy link
Copy Markdown
Contributor Author

Merged via squash.

Thanks @mbelinky!

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e6cdde6c21

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines 673 to +676
found: true,
cancelled: false,
reason: "One or more child tasks are still active.",
flow: getFlowById(flow.flowId),
flow: getFlowById(flow.flowId) ?? cancelRequestedFlow,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat accepted flow-cancel requests as successful

This branch reports cancelled: false even after cancelRequestedAt has already been persisted, so callers cannot distinguish “cancel request accepted, waiting for child shutdown” from a true failure. In this repo, flowsCancelCommand exits with an error whenever cancelled is false, which means operators see a failed cancel even though scheduling is already blocked and the flow is now in sticky-cancel mode.

Useful? React with 👍 / 👎.

ngutman pushed a commit that referenced this pull request Apr 3, 2026
Merged via squash.

Prepared head SHA: e6cdde6
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
steipete pushed a commit to duncanita/openclaw that referenced this pull request Apr 4, 2026
Merged via squash.

Prepared head SHA: e6cdde6
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
lovewanwan pushed a commit to lovewanwan/openclaw that referenced this pull request Apr 28, 2026
Merged via squash.

Prepared head SHA: e6cdde6
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
Merged via squash.

Prepared head SHA: e6cdde6
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
Merged via squash.

Prepared head SHA: e6cdde6
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 24, 2026
Merged via squash.

Prepared head SHA: e6cdde6
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

commands Command implementations maintainer Maintainer-authored PR size: L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant