Skip to content

Design spec: parallel execution — fork/join (concurrent branches) (#88) - #94

Merged
EricWittmann merged 1 commit into
mainfrom
issues/gh-88
Sep 4, 2026
Merged

Design spec: parallel execution — fork/join (concurrent branches) (#88)#94
EricWittmann merged 1 commit into
mainfrom
issues/gh-88

Conversation

@EricWittmann

Copy link
Copy Markdown
Contributor

Summary

Design-only deliverable for the concurrent-branches epic (#88). Adds
docs/superpowers/specs/2026-09-04-parallel-execution-fork-join-design.md. No code changes.

The spec commits to the decisions made for this epic and works out the semantics they imply:

  • Reinterpret multiple edges (no new node types). A node forks when it has ≥2 outgoing edges that
    are all unconditional (no condition, no default); anything with a condition/default remains
    exclusive-choice; mixing the two is a new validation error. A join is the multi-incoming
    convergence node, paired to its fork by static analysis.
  • Wait-for-all (AND-join). The join fires once a token has arrived on every incoming edge;
    structured/balanced-parallelism validation guarantees this equals "all branches converged."
  • Fail-fast branch failure (cancels siblings), END terminates the whole workflow, WAITING
    when any branch is parked.
  • Flat, last-write-wins context retained; the concurrency hazard and disjoint-key guidance are
    documented rather than engineered away.

Key structural elements defined: an active-branch (token) set replacing the single currentNodeId
(kept as a derived back-compat accessor), a token-based advance() loop, branchId on
HistoryEntry, six new validation codes plus retiring UNCONDITIONAL_MULTIPLE_EDGES, multi-active
highlighting in the viewer/simulation, and a 5-phase delivery plan maintaining Java/TypeScript
parity.

Migration note

Definitions that today have ≥2 unconditional outgoing edges (previously the
UNCONDITIONAL_MULTIPLE_EDGES warning, "take one") will, after Phase 1, fork and run all branches.
This is the intended semantic upgrade; authors who meant exclusive choice must add conditions and/or a
default edge.

Scope

Design/spec only — no implementation. Once the design is approved, the companion phased plan and Phase 1
(Java engine + validator) follow.

Relates to #88 (design phase). Implementation tracked as follow-on phases.

Design-only deliverable for the concurrent-branches epic. Defines the
fork/join model (inferred from edge shape, no new node types), wait-for-all
AND-join semantics, fail-fast branch failure, flat last-write-wins context,
the active-branch (token) state model and token-based advance loop, new
validation rules for structured parallelism, viewer/simulation impact, and a
phased delivery plan maintaining Java/TypeScript parity.

@EricWittmann EricWittmann left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Review: Parallel Execution Fork/Join — Design Spec

This PR adds a single design document (docs/superpowers/specs/2026-09-04-parallel-execution-fork-join-design.md, 344 lines) defining fork/join semantics for concurrent branches. It is design-only with no code changes, so this review focuses on internal consistency, accuracy against the current codebase, and design risks worth capturing before implementation.

I verified the spec's factual claims about the existing code and they all hold:

  • WorkflowInstance is an immutable record with a single currentNodeId (engine/.../model/WorkflowInstance.java) and TS currentNodeId: string (ui/src/types/instance.ts).
  • advance() is a single-cursor while(true) loop guarded by MAX_TRANSITIONS, using hasEnteredCurrentNode / completeCurrentHistoryEntry and selectEdge exactly as described (WorkflowEngine.java).
  • UNCONDITIONAL_MULTIPLE_EDGES exists today as a warning (validateWorkflow.ts:250) — accurately characterized.
  • HistoryEntry has no branchId today (both languages) — accurate.

Overall this is a thorough, well-structured spec with clear decisions, an explicit non-goals list, a sensible phased plan, and a migration note. It is in good shape. The notes below are suggestions/nitpicks to strengthen it — none block merging a design doc.

Suggestions

  • currentNodeId as a "derived, back-compat accessor" on an immutable record needs a concrete plan. WorkflowInstance is a Java record, so currentNodeId() is an auto-generated component accessor and the field is also a Jackson-serialized wire component (@JsonFormat siblings, Map.copyOf/List.copyOf in build()). Making it "derived" (returns the sole active node or null) requires either removing the record component and adding a computed getter, or keeping a synchronized field — each with wire/serialization consequences. The spec says "wire parity" is intended; it would help to state explicitly whether currentNodeId remains a serialized field (populated for single-branch, null for multi-branch) or becomes computed-only, since that affects deserialization of persisted instances and any Jackson round-trips. Worth nailing down in Phase 1 rather than "an implementation detail."

  • Returning null from currentNodeId when >1 branch is active is an NPE hazard for existing consumers. The engine itself dereferences instance.currentNodeId() in many places (getHumanTaskInfo, getReceiveEventInfo, getWaitInfo, getActionInfo, matchesEvent, advance). The spec's back-compat argument ("only affects workflows that actually fork") is reasonable, but the transition of these call sites to the active-branch set is exactly the risky part and deserves a callout in the Phase 1 scope (the spec lists advance() but not these accessor/matchesEvent methods).

  • MAX_TRANSITIONS = 100 shared across all branches may be too tight under fan-out. The spec says the guard "counts total steps across all branches." With multiple concurrent branches each taking several steps, a legitimate parallel workflow will consume this shared budget much faster than a linear one, risking false "possible infinite loop" failures. Consider whether the bound should scale (e.g., per-branch, or MAX_TRANSITIONS * activeBranchCount) and note it in the design.

  • Resume API rename. The spec introduces completeNode(instance, nodeId, output) for resume-by-node, but the current public API is completeCurrentNode(workflow, instance, result). Whether this is an additive method, a rename with a deprecated shim, or a breaking change matters for consumers and should be stated (the spec's back-compat emphasis elsewhere makes the silence here notable).

  • Fail-fast + parked external work. Fail-fast "cancels siblings," but a sibling parked on a HUMAN_TASK/RECEIVE_EVENT represents work owned by an external system. The engine is stateless and cannot recall it. A one-line acknowledgment that cancellation is logical (marks branch cancelled) rather than a real-world recall would set correct expectations.

  • Edge priority semantics inside a fork. For an exclusive-choice node, edge priority (and the existing DUPLICATE_EDGE_PRIORITY warning) is meaningful; for a fork (all-unconditional) it is not. It would be worth stating that priority is ignored on fork edges and confirming DUPLICATE_EDGE_PRIORITY should not fire on a valid fork, alongside the other retired/adjusted rules.

Nitpicks

  • Line 209: typo — "namespate by branch purpose" should be "namespace by branch purpose."
  • Decisions table, "Graph modeling" row (line 66): the rationale reads "Reinterpret multiple edges (no new node types) | Minimal additions to NodeType and both node-component registries..." This is mildly self-contradictory — the chosen option requires no NodeType additions at all. Consider rewording to "No additions to NodeType..." to avoid confusion with the rejected FORK/JOIN alternative.
  • The spec references updating docs/user-guide/validation.md when retiring UNCONDITIONAL_MULTIPLE_EDGES (good — that file does document the rule set); Phase 5 already captures this.

Assessment

Good to merge as a design deliverable. The document is accurate, internally consistent, and appropriately scoped, with the semantic behavior change (fork-and-run-all) clearly flagged in the migration note. The suggestions above are refinements to fold into the Phase 1 plan — particularly the currentNodeId derivation/serialization strategy, the null-current call-site migration, and the MAX_TRANSITIONS budget under fan-out — rather than blockers for approving the design.

@EricWittmann
EricWittmann merged commit 4f36284 into main Sep 4, 2026
2 checks passed
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.

1 participant