Skip to content

fix(tasks): forkTaskJoin generates a JOIN with empty joinOn - #161

Open
samarth70 wants to merge 1 commit into
conductor-oss:mainfrom
samarth70:fix/fork-join-empty-joinon
Open

fix(tasks): forkTaskJoin generates a JOIN with empty joinOn#161
samarth70 wants to merge 1 commit into
conductor-oss:mainfrom
samarth70:fix/fork-join-empty-joinon

Conversation

@samarth70

Copy link
Copy Markdown

Fixes #135.

Problem

forkTaskJoin() builds its JOIN through generateJoinTask() without passing joinOn, so the generated task carries joinOn: []:

// src/sdk/builders/tasks/forkJoin.ts:19
generateJoinTask({ name: `${taskReferenceName}_join`, optional }),
//                 no joinOn -> generateJoinTask defaults it to []

The server evaluates a JOIN with joinOn.stream().allMatch(...), and allMatch() short-circuits to true on an empty stream. So the JOIN reaches COMPLETED the first time it is evaluated, without waiting for any fork branch. The branches keep running, but the workflow moves on past the join, which is the one thing a JOIN exists to prevent.

As reported in #135, against Conductor OSS 3.32.0-rc.9 with a 10s WAIT branch:

After 2 seconds:
  forkTaskJoin (joinOn=[])       -> join=COMPLETED    wait_branch=IN_PROGRESS   <- bug
  manual joinOn=['wait_branch']  -> join=IN_PROGRESS  wait_branch=IN_PROGRESS   <- correct

Fix

Derive joinOn from the fork branches that were just generated, taking the last task reference of each branch:

const fork = forkTask(taskReferenceName, forkTasks);
const joinOn = fork.forkTasks
  .map((branch) => branch[branch.length - 1]?.taskReferenceName)
  .filter((ref): ref is string => ref !== undefined);

Reading the branches back off the ForkJoinTaskDef rather than off the flat forkTasks argument means this stays correct if forkTask() later emits more than one branch (the multi-branch limitation tracked in #94) - no second fix needed there.

An empty fork task list still yields joinOn: [], since there is nothing to wait for.

Tests

factory.test.ts asserted joinOn: [] and therefore locked in the broken behaviour; it now expects the branch's last task reference. Two cases added: a multi-task branch, and an empty fork task list.

Verified both directions:

  • On unfixed source, Should return a tuple with both fork and join and Should join on the last task of each fork branch fail (Expected ["forkTaskJoin"], Received []).
  • With the fix, the factory suite is 25/25 green.
  • Full unit suite: npm run test:unit -> 1568 passed. The single failure in src/agents/__tests__/skill.test.ts (read_skill_file worker) reproduces on a clean checkout of main and is unrelated to this change.
  • npx eslint clean on both touched files.

Diff is +26/-5 across two files.

forkTaskJoin() built its JOIN via generateJoinTask() without passing
joinOn, so the task shipped with joinOn: []. The server evaluates the
join with joinOn.stream().allMatch(...), and allMatch() short-circuits
to true on an empty stream, so the JOIN reached COMPLETED on its first
evaluation without waiting for any fork branch. Downstream tasks then
ran while the branches were still in progress.

Derive joinOn from the generated fork branches, taking the last task
reference of each branch, so the JOIN blocks until every branch has
finished. Reading it back off the ForkJoinTaskDef keeps it correct if
forkTask() later emits more than one branch.

factory.test.ts asserted joinOn: [] and so locked in the broken
behaviour; it now expects the branch's last task reference. Added cases
for a multi-task branch and for an empty fork task list.

Fixes conductor-oss#135
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

forkTaskJoin() generates JOIN with empty joinOn — join completes immediately without waiting for branches

1 participant