TaskFlow: add managed child task execution#58939
Conversation
Greptile SummaryThis PR adds managed-flow child task spawning ( Key changes:
Confidence Score: 5/5Safe to merge — logic is correct, all new behaviour is covered by targeted tests, and the only finding is a minor code-duplication style note. No P0/P1 issues found. The FlowRecord/FlowUpdateFailure discriminant is safe (FlowRecord has no reason field). The two-phase cancel correctly prevents new task spawns after cancel intent is recorded. The retry loop in syncManagedFlowCancellationFromTask is sufficient for Node.js's single-threaded event loop. The only finding is duplicated isActiveTaskStatus/isTerminalFlowStatus helpers across two modules (P2). No files require special attention. Prompt To Fix All With AIThis is a comment left during a code review.
Path: src/tasks/task-executor.ts
Line: 387-396
Comment:
**Duplicated helpers already defined in `task-registry.ts`**
`isActiveTaskStatus` (line 387) and `isTerminalFlowStatus` (line 391) are identical to the helpers added to `src/tasks/task-registry.ts` (lines 71–79). If the import boundaries allow it, these could be extracted into a small shared internal utility (e.g., `task-status-utils.ts`) so the definitions stay in one place. If the circular-import risk makes that awkward, a brief inline comment explaining the intentional duplication would help future readers.
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "Tasks/flows: add managed flow task execu..." | Re-trigger Greptile |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 644628d2bf
ℹ️ 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".
| if (isTerminalFlowStatus(flow.status)) { | ||
| return { | ||
| found: true, | ||
| cancelled: false, | ||
| reason: `Flow is already ${flow.status}.`, |
There was a problem hiding this comment.
Cancel active child tasks before terminal-flow early return
This early return prevents cancelFlowById from even attempting cancelTaskById when the flow is already terminal. For managed flows, status is not mirrored from task state, so a flow can be marked failed/cancelled while child tasks are still queued/running; in that case this change leaves those child sessions running indefinitely instead of shutting them down. Previously the function canceled active children first and only then reported terminal flow status.
Useful? React with 👍 / 👎.
| @@ -386,6 +395,145 @@ function isTerminalFlowStatus(status: FlowRecord["status"]): boolean { | |||
| ); | |||
| } | |||
There was a problem hiding this comment.
Duplicated helpers already defined in
task-registry.ts
isActiveTaskStatus (line 387) and isTerminalFlowStatus (line 391) are identical to the helpers added to src/tasks/task-registry.ts (lines 71–79). If the import boundaries allow it, these could be extracted into a small shared internal utility (e.g., task-status-utils.ts) so the definitions stay in one place. If the circular-import risk makes that awkward, a brief inline comment explaining the intentional duplication would help future readers.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/tasks/task-executor.ts
Line: 387-396
Comment:
**Duplicated helpers already defined in `task-registry.ts`**
`isActiveTaskStatus` (line 387) and `isTerminalFlowStatus` (line 391) are identical to the helpers added to `src/tasks/task-registry.ts` (lines 71–79). If the import boundaries allow it, these could be extracted into a small shared internal utility (e.g., `task-status-utils.ts`) so the definitions stay in one place. If the circular-import risk makes that awkward, a brief inline comment explaining the intentional duplication would help future readers.
How can I resolve this? If you propose a fix, please make it concise.
🔒 Aisle Security AnalysisWe found 5 potential security issue(s) in this PR:
1. 🟠 Broken access control in `flows` CLI: ownerKey lookup enables cross-owner flow disclosure and cancellation
DescriptionThe
If the CLI is usable by untrusted users in a shared/multi-tenant environment (e.g., multiple agents/owners on the same deployment, remote execution wrappers, exposed admin endpoints), a caller can:
Vulnerable code: const flow = resolveFlowForLookupToken(opts.lookup);
...
const result = await cancelFlowById({ cfg: loadConfig(), flowId: flow.flowId });RecommendationEnforce owner scoping (or explicit admin authorization) for all flow inspection and mutation operations. Options:
import { resolveFlowForLookupTokenForOwner } from "../tasks/flow-owner-access.js";
import { cancelFlowByIdForOwner } from "../tasks/task-executor.js";
const flow = resolveFlowForLookupTokenForOwner({
token: opts.lookup,
callerOwnerKey,
});
if (!flow) { /* not found */ }
await cancelFlowByIdForOwner({ cfg: loadConfig(), flowId: flow.flowId, callerOwnerKey });
Also consider filtering 2. 🟠 Symlink-following chmod/file clobbering via OPENCLAW_STATE_DIR in SQLite flow registry store
DescriptionThe SQLite-backed flow registry store derives its database path from
If an attacker can influence
Vulnerable code: mkdirSync(dir, { recursive: true, mode: FLOW_REGISTRY_DIR_MODE });
chmodSync(dir, FLOW_REGISTRY_DIR_MODE);
...
if (existsSync(candidate)) {
chmodSync(candidate, FLOW_REGISTRY_FILE_MODE);
}RecommendationHarden filesystem handling for the state directory and SQLite files:
Example approach (POSIX-oriented): import { mkdirSync, lstatSync, openSync, fchmodSync, closeSync } from "node:fs";
import { constants } from "node:fs";
function safeChmodFileNoSymlink(p: string, mode: number) {
const st = lstatSync(p);
if (st.isSymbolicLink()) throw new Error(`Refusing to chmod symlink: ${p}`);
const fd = openSync(p, constants.O_RDWR | constants.O_NOFOLLOW);
try { fchmodSync(fd, mode); } finally { closeSync(fd); }
}Also ensure the flow registry directory is created with correct ownership and permissions and is not a symlink before use. 3. 🟡 Orphaned flow record on exception in ensureSingleTaskFlow() (potential storage/memory DoS)
Description
Impact:
Vulnerable code: const flow = createFlowForTask({ task: params.task, requesterOrigin: params.requesterOrigin });
const linked = linkTaskToFlowById({ taskId: params.task.taskId, flowId: flow.flowId });
...
} catch (error) {
log.warn("Failed to create one-task flow for detached run", { ... });
return params.task; // flow not deleted here
}RecommendationTrack the created flow ID and ensure it is deleted on any failure after creation. One approach is to use a let flowId: string | undefined;
try {
const flow = createFlowForTask({ task: params.task, requesterOrigin: params.requesterOrigin });
flowId = flow.flowId;
const linked = linkTaskToFlowById({ taskId: params.task.taskId, flowId });
if (!linked || linked.parentFlowId !== flowId) {
deleteFlowRecordById(flowId);
return linked ?? params.task;
}
return linked;
} catch (e) {
if (flowId) {
deleteFlowRecordById(flowId);
}
throw e; // or return params.task, but after cleanup
}If the intention is to swallow errors and continue, still perform the cleanup before returning. 4. 🟡 CLI flows commands leak potentially sensitive delivery/session metadata in JSON output
DescriptionThe new Because these records include fields like:
…the JSON output can disclose PII (chat/account identifiers), internal routing/session keys, and operational metadata into terminals, CI logs, or shared artifacts if users run these commands in automated environments. Vulnerable code (JSON output includes raw records): flows: flows.map((flow) => ({
...flow,
tasks: listTasksForFlowId(flow.flowId),
taskSummary: getFlowTaskSummary(flow.flowId),
})),and: JSON.stringify({
...flow,
tasks,
taskSummary,
})While this is a local CLI, the disclosure is still impactful because JSON output is commonly redirected/archived, and these fields are not obviously safe for broad exposure. RecommendationRedact or minimize sensitive fields by default, and require an explicit opt-in for verbose/raw dumping. Suggested approach:
Example: function redactDeliveryContext(ctx?: DeliveryContext) {
if (!ctx) return undefined;
return {
channel: ctx.channel,
// Avoid emitting direct message targets/account ids by default
to: ctx.to ? "[redacted]" : undefined,
accountId: ctx.accountId ? "[redacted]" : undefined,
threadId: ctx.threadId != null ? "[redacted]" : undefined,
};
}
function toSafeFlow(flow: FlowRecord) {
return {
flowId: flow.flowId,
syncMode: flow.syncMode,
status: flow.status,
revision: flow.revision,
goal: flow.goal,
controllerId: flow.controllerId,
requesterOrigin: redactDeliveryContext(flow.requesterOrigin),
// omit ownerKey/stateJson/waitJson unless verbose
};
}
// in --json path:
flows: flows.map((flow) => ({
...toSafeFlow(flow),
taskSummary: getFlowTaskSummary(flow.flowId),
})),Document the security/privacy implications in CLI help text for the verbose option. 5. 🟡 Persistent DoS via NULL/invalid owner_key in flow registry SQLite migration/restore
DescriptionThe flow registry restore path can be crashed (and repeatedly re-crashed on subsequent CLI runs) by a corrupted or partially-migrated SQLite registry where
Vulnerable code (migration/backfill leaves nullable owner_key): if (!hasFlowRunsColumn(db, "owner_key") && hasFlowRunsColumn(db, "owner_session_key")) {
db.exec(`ALTER TABLE flow_runs ADD COLUMN owner_key TEXT;`);
db.exec(`
UPDATE flow_runs
SET owner_key = owner_session_key
WHERE owner_key IS NULL
`);
}And restore-time hard failure: ownerKey: assertFlowOwnerKey(record.ownerKey),RecommendationHarden both the migration and restore paths so a corrupted registry cannot brick the CLI.
Example approach (repair on load): // after ALTER TABLE
db.exec(`UPDATE flow_runs SET owner_key = '' WHERE owner_key IS NULL;`);
// OR delete invalid rows
db.exec(`DELETE FROM flow_runs WHERE owner_key IS NULL OR trim(owner_key) = ''`);Example approach (defensive restore): for (const [flowId, flow] of restored.flows) {
try {
flows.set(flowId, normalizeRestoredFlowRecord(flow));
} catch {
// skip or repair
}
}Analyzed PR: #58939 at commit Last updated on: 2026-04-02T10:04:24Z Latest run failed. Keeping previous successful results. Trace ID: Last updated on: 2026-04-02T10:44:44Z Latest run failed. Keeping previous successful results. Trace ID: Last updated on: 2026-04-02T11:11:31Z |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0e82d237a8
ℹ️ 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".
| if (isTerminalFlowStatus(flow.status)) { | ||
| return { | ||
| found: true, | ||
| cancelled: false, | ||
| reason: `Flow is already ${flow.status}.`, |
There was a problem hiding this comment.
Cancel active child tasks before terminal-flow short-circuit
Fresh evidence in this commit: cancelFlowById now returns immediately when the flow status is terminal, before the active-child cancellation loop runs. In managed flows, flow status can become failed/cancelled independently of child task state, so this early return can leave queued/running children alive and their sessions never canceled, which breaks the intended sticky-cancel shutdown behavior.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 27e6baa3dc
ℹ️ 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".
| if ("reason" in cancelRequestedFlow) { | ||
| return { | ||
| found: true, |
There was a problem hiding this comment.
Retry cancel-intent conflicts before aborting cancellation
If requestFlowCancel hits a revision conflict (for example, the controller updates flow state between the initial getFlowById and this call), this branch returns immediately and never reaches the child-cancellation loop below. In that case cancelFlowById reports failure without even attempting cancelTaskById, so active child sessions keep running until someone retries manually. This regression is introduced by making cancel-intent CAS a hard precondition instead of retrying with result.current/fresh flow data.
Useful? React with 👍 / 👎.
c0ede2a to
7c6f535
Compare
27e6baa to
f26951c
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f26951c10a
ℹ️ 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".
| - Channels/session routing: move provider-specific session conversation grammar into plugin-owned session-key surfaces, preserving Telegram topic routing and Feishu scoped inheritance across bootstrap, model override, restart, and tool-policy paths. | ||
| - WhatsApp/reactions: add `reactionLevel` guidance for agent reactions. Thanks @mcaxtr. | ||
| - Feishu/comments: add a dedicated Drive comment-event flow with comment-thread context resolution, in-thread replies, and `feishu_drive` comment actions for document collaboration workflows. (#58497) thanks @wittam-01. | ||
| <<<<<<< HEAD |
There was a problem hiding this comment.
Remove unresolved merge marker from changelog
The committed <<<<<<< HEAD line is an unresolved merge-conflict marker in the Unreleased section, so release notes now contain conflict syntax instead of valid content. This can break changelog tooling/parsing and should be removed before landing.
Useful? React with 👍 / 👎.
7c6f535 to
9c946d5
Compare
f26951c to
411d718
Compare
411d718 to
08ed516
Compare
08ed516 to
af82b86
Compare
af82b86 to
c473c0c
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c473c0cf8d
ℹ️ 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".
| if (flow.cancelRequestedAt != null) { | ||
| throw new ParentFlowLinkError( | ||
| "cancel_requested", | ||
| "Parent flow cancellation has already been requested.", | ||
| { flowId, status: flow.status }, |
There was a problem hiding this comment.
Preserve idempotent child-task replays after cancel intent
This new cancel_requested rejection runs inside assertParentFlowLinkAllowed, which createTaskRecord calls before it attempts run-id deduplication (findExistingTaskForCreate). That means a replay of an already-created child run (same runId/scope) will now throw once cancelRequestedAt is set, instead of returning the existing task record. In at-least-once orchestrators, this breaks idempotent recovery and can make callers treat an existing child as missing right after cancellation intent is recorded.
Useful? React with 👍 / 👎.
1e51229 to
c990938
Compare
Summary
Stack
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 TaskFlowscancelRequestedAtsemantics before child shutdowncancelledonce the last active child settlesWhat This PR Does Not Do
Tasks vs TaskFlow
Validation
mb-server:pnpm buildmb-server: focused Vitest slice across flow/task/doctor surfaces