Skip to content

Fix disjointed dashboard layout for workflows with human_gate options and loop-backs - #153

Merged
Jason Robert (jrob5756) merged 2 commits into
mainfrom
fix/dashboard-layout-loopbacks
May 6, 2026
Merged

Fix disjointed dashboard layout for workflows with human_gate options and loop-backs#153
Jason Robert (jrob5756) merged 2 commits into
mainfrom
fix/dashboard-layout-loopbacks

Conversation

@jrob5756

Copy link
Copy Markdown
Collaborator

Summary

Workflows with human_gate options and/or multiple loop-back routes (e.g. workflows/sdd/plan-v3.yaml) rendered as visually disconnected columns in the dashboard, with long diagonal edges and downstream nodes placed above their predecessors. This PR addresses both root causes.

Fixes

1. Engine: emit human_gate option routes in workflow_started

workflow_started only included routes from agent.routes, parallel.routes, and for_each.routes. Edges declared via human_gateoptions[].route were never emitted, so any subgraph reachable only through a gate appeared genuinely disconnected in the dashboard. This was the root cause of the visible disconnect — design_approval_gate had no outgoing edges at all.

Added a list comprehension that emits one route per human_gate option, with when set to selection == '<value>' to mirror what the engine emits for execution-plan steps elsewhere in the file.

2. Frontend: pre-classify back-edges before passing to Dagre

When a workflow has multiple loop-backs (e.g. revision loops), Dagre's greedy acyclicer reverses arbitrary edges to break cycles, which scrambles rank assignment. Downstream nodes end up above their predecessors and the layout looks like two disconnected columns even when all edges are present.

Added findBackEdges() in graph-layout.ts that runs a DFS from \$start to identify loop-back edges (edges whose target is an ancestor of the source in the DFS tree). Those edges are passed to Dagre in reversed direction so it ranks the underlying DAG correctly. The visible edges in React Flow keep their original direction.

This is more accurate than Dagre's greedy heuristic because it knows about the entry point. Traversal order is stable (sorted target IDs) so layout is deterministic across renders.

Files changed

  • `src/conductor/engine/workflow.py` — emit human_gate option edges
  • `src/conductor/web/frontend/src/components/graph/graph-layout.ts` — back-edge classification + reversed Dagre input
  • `src/conductor/web/static/{index.html,assets/}` — rebuilt frontend assets

Verification

  • `npm run build` in `src/conductor/web/frontend` ✓
  • `make lint` ✓ (ruff check + format)
  • `uv run pytest tests/test_engine tests/test_integration tests/test_gates tests/test_web` — 773 passed, 9 skipped
  • Standalone Dagre simulation of the plan-v3 graph confirmed planner, plan_reviewer, and consistency_checker now rank in correct top-to-bottom order under design_approval_gate

Out of scope

  • Switching layout engine to ELK (considered, deferred — Dagre tuning fully addresses the reported issue)
  • Restyling back-edges visually (could be a follow-up if loop-back edges should be marked differently)
  • Backend route/event schema changes

Jason Robert (jrob5756)and others added 2 commits May 5, 2026 16:01
… and loop-backs
The dashboard rendered workflows like sdd/plan-v3 as visually disconnected
columns with long diagonal edges. Two independent issues were responsible:
1. Engine: workflow_started omitted human_gate option routes
The 'routes' field of the workflow_started event was built only from
agent.routes, parallel.routes, and for_each.routes. Edges declared via
human_gate options[].route were never emitted, so any subgraph reachable
only through a gate appeared genuinely disconnected in the dashboard.
Fixed by adding a list comprehension that emits one route per human_gate
option.
2. Frontend: Dagre arbitrarily reversed edges when breaking cycles
When workflows have multiple loop-back routes (e.g. revision loops),
Dagre's greedy acyclicer reversed arbitrary edges to break cycles,
scrambling the rank assignment so downstream nodes ended up above their
predecessors. Added findBackEdges() that DFS's from $start to identify
loop-back edges, then passes them to Dagre in reversed direction so
it ranks the underlying DAG correctly. The visible edges keep their
original direction.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- workflow.py: drop 'if o.route' filter; GateOption.route is required str
- graph-layout.ts: pass '$start' directly (findBackEdges handles missing)
- graph-layout.ts: drop redundant pre-filter on remainingSources
- graph-layout.ts: tighten docstring on traversal order
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jrob5756
Jason Robert (jrob5756)force-pushed the fix/dashboard-layout-loopbacks branch from 4f60d5d to 42c8967CompareMay 5, 2026 23:28

@PolyphonyRequiemPolyphonyRequiem left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Good catch on the dual root cause — separating the engine emission gap (gate routes literally absent from workflow_started) from Dagre's greedy-acyclicer mishandling multiple loop-backs is the right framing, and both fixes are minimal.

I had the thought to add frontend tests for findBackEdges but wanted to consult on the approach first — the frontend has no test runner configured today (no Vitest/Jest in devDependencies, no test script, no existing test files), so doing it here means standing up the harness as well. Happy to defer to a follow-up; just flagging since the function has enough branching (DFS stack tracking, cycle vs cross-edge, unreachable subgraph fallback) that it'd benefit from coverage once a runner exists.

{
"from": a.name,
"to": o.route,
"when": f"selection == '{o.value}'",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggestion: add a regression test alongside test_workflow_started_includes_routes (tests/test_engine/test_event_emission.py:163) that asserts a human_gate's option routes appear in event.data["routes"] with when == "selection == '<value>'". Cheap insurance that the silent regression motivating this PR can't sneak back in.

* target-ID order, and unreachable subgraphs are entered in sorted source-ID
* order, so layout is stable across renders.
*/
function findBackEdges(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Question: findBackEdges uses recursive DFS via a closure — fine for normal graphs, but does it hold up for workflows with sub-workflows / type: workflow agents? My read is that each sub-workflow renders its own isolated graph in the dashboard so recursion depth is bounded by the longest path within a single workflow (not by MAX_SUBWORKFLOW_DEPTH nesting), but wanted to confirm you've thought about it — happy to be wrong.

@jrob5756
Jason Robert (jrob5756) merged commit d3b94a1 into mainMay 6, 2026
7 checks passed
@jrob5756
Jason Robert (jrob5756) deleted the fix/dashboard-layout-loopbacks branch May 6, 2026 01:10
Jason Robert (jrob5756) added a commit that referenced this pull request May 6, 2026
* chore: release 0.1.12
Bumps version to 0.1.12 and updates CHANGELOG with the four PRs
merged since v0.1.11:
- #149: Windows install diagnostics
- #151: Tag-based registry versioning with # ref syntax
- #152: Unified reasoning.effort configuration
- #153: Dashboard layout fix for human_gate options + loop-backs
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore: add #155 (Windows update reliability) to 0.1.12 changelog
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto 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.

2 participants

@jrob5756@PolyphonyRequiem