fix(web): dedupe phantom edges, for-each dive-in nav, PR #113 review nits (#145)#146
Conversation
When user YAML defines a conditional route plus a catch-all to the same target (a common pattern, e.g. 'when: ready==false' then bare 'to: gate' fallback), the engine evaluates them in order with first-match-wins semantics — they represent ONE visual transition, not parallel edges. Previously graph-layout.ts emitted an Edge per route entry, so duplicate (from, to) pairs produced overlapping/diverging edges that dagre laid out as phantom strands stretching off-canvas. Visible at the impl subworkflow view for routes like preflight_lite -> preflight_lite_gate (when + bare) and load_work_tree -> \ (when + bare). Dedupe by (from, to) — first route wins, matching engine semantics. Highlighted-edge state is already keyed by (from, to) so this is consistent with the rest of the rendering pipeline. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Bundles four follow-ups for PR microsoft#113: * feat(web): "Dive In" affordance on for-each iteration rows so users can navigate into a specific iteration's subworkflow context. The detail panel for a for_each group now exposes a Layers button per iteration that resolves the slot key (`<group>[<key>]`) against subworkflowContexts and calls navigateIntoSubworkflow. Resolves the reported case where pg_execution_group[0] could not be linked into. * fix(web/server): tighten _is_proactor_shutdown_race to require the deepest traceback frame to originate from asyncio internals (microsoft#145 I3). Previously the gate fell back to permissive True when the traceback was missing or empty, which could swallow unrelated AssertionErrors raised by user/workflow code during shutdown. * docs(engine): explain why _check_interrupt clears the interrupt event before raising — single-listener reset semantics, no race, callers rely on the post-raise event being clear (microsoft#145 S2). * docs(web): note on resolveSlotPath that newest-wins makes older for-each iterations unreachable by stale slot paths (microsoft#145 S3). Tests: - Updated TestProactorShutdownRace to assert the tightened contract: asyncio-framed traceback -> True, non-asyncio frame -> False, no traceback -> False. All 11 cases pass. - 137 web/engine tests pass; ruff check + format clean. Refs: microsoft#113, microsoft#145 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #146 +/- ##
=======================================
Coverage ? 85.33%
=======================================
Files ? 56
Lines ? 8109
Branches ? 0
=======================================
Hits ? 6920
Misses ? 1189
Partials ? 0 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
| if tb is None: | ||
| return False | ||
| frames = tb_mod.extract_tb(tb) | ||
| return bool(frames) and "asyncio" in frames[-1].filename |
There was a problem hiding this comment.
_guarded_serve retains the same permissive AssertionError handling that was just tightened here.
Issue #145 calls out both silent-swallow sites — _loop_exception_handler and _guarded_serve (lines 443-458 below). The wrapper still catches any AssertionError whenever should_exit is set, with no traceback inspection:
except AssertionError:
if self._server is not None and getattr(self._server, "should_exit", False):
logger.debug(...) # Suppressed silently
else:
raiseSuggested options:
- Apply the same asyncio-frame check inline in the
except, or - Refactor: synthesize a context dict from the exception and call
self._is_proactor_shutdown_race(ctx)so both sites share one gate.
| const pairKey = `${from}->${to}`; | ||
| if (seenPairs.has(pairKey)) continue; | ||
| seenPairs.add(pairKey); | ||
| const edgeId = `${pairKey}${r.when ? `[${r.when}]` : ''}`; |
There was a problem hiding this comment.
Nit/Suggestion — Edge dedupe is lossy on when metadata.
The surviving edge keeps data: { when: r.when } from the first route only. For the documented repro (conditional + bare catch-all) that's fine — the second route has no when. But for two-conditional routes to the same target (e.g. when: success then when: error), the visual edge will display only [success] even though the actual graph means "go to $end on either condition."
Consider clearing when (or marking it '*' / 'multiple') when collapsing routes with non-equivalent conditions, e.g.:
const existing = seenPairs.get(pairKey);
if (existing && existing !== r.when) {
// Multiple distinct conditions collapse — drop the label.
flowEdges[existing.idx].data = { when: undefined };
continue;
}Minor UX nit, not blocking.
1. _guarded_serve now reuses _is_proactor_shutdown_race gate instead of permissive should_exit-only check. Both exception handler sites now share the same strict contract: suppress only when the deepest traceback frame originates from asyncio internals. 2. Edge dedupe clears the 'when' label when collapsing routes with different conditions to the same target, avoiding a misleading single-condition label on a multi-condition edge. 3. Added test_guarded_serve_reraises_non_asyncio_assertion_during_shutdown to verify the tightened contract. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Merged upstream/main into fix/dedupe-graph-edges. Conflicts were in src/conductor/web/static/ (competing built-asset hashes). Resolved by rebuilding the frontend to produce a single bundle combining both the PR's dedupe/for-each features and upstream's dialog-mode changes. Fixed TypeScript errors from the merge (unused imports, type narrowing, missing JSX namespace). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Addressed both review comments in commit 9c0940f:
All 48 web server tests pass. |
jrob5756
left a comment
There was a problem hiding this comment.
LGTM — both review findings addressed cleanly. Shared gate for the proactor race is the right call.
…, #121-#123, #125, #129, #130, #131, #139, #141-#144, #146) CHANGELOG: add 6 newer PRs (#119, #121, #122, #123, #125, #113, #130, #131, #141, #146) to [Unreleased] alongside the previously documented batch. docs/workflow-syntax.md: - Add metadata + instructions fields to the workflow configuration block. - Add input_mapping and max_depth to Sub-Workflow Steps; correct stale claims that circular references are rejected and that workflow steps cannot be used in for_each groups. - Add 'Sub-workflows in for_each groups' subsection with example. - Add JSON stdout auto-parsing note + example to Script Steps output section. - Add type-appropriate zero values table to Workflow Inputs. - Add 'Workflow Metadata Variables' subsection covering workflow.dir, workflow.file, workflow.name. - Update on_start hook context list to include the new workflow.dir/file vars. docs/cli-reference.md: - Document --metadata/-m, --workspace-instructions, and --instructions flags on conductor run. - Add 'Metadata and Instructions' examples block. - Update conductor validate to describe the new template-reference error/warning checks added in #125. docs/providers/claude.md, docs/providers/comparison.md: - Replace stale 'All models support a 200K token context window' / '200K (all models)' claims with notes that the dashboard now sources context_window_max from each provider's SDK at runtime (#144). README.md: - Refresh the Features list to mention sub-workflow composition, dialog mode, workspace instructions, breadcrumb navigation, and the enhanced validate behavior. - Add --metadata, --workspace-instructions, --instructions to the conductor run options table. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs: changelog + doc updates for unreleased PRs (#100, #109-#111, #119, #121-#123, #125, #129, #130, #131, #139, #141-#144, #146) CHANGELOG: add 6 newer PRs (#119, #121, #122, #123, #125, #113, #130, #131, #141, #146) to [Unreleased] alongside the previously documented batch. docs/workflow-syntax.md: - Add metadata + instructions fields to the workflow configuration block. - Add input_mapping and max_depth to Sub-Workflow Steps; correct stale claims that circular references are rejected and that workflow steps cannot be used in for_each groups. - Add 'Sub-workflows in for_each groups' subsection with example. - Add JSON stdout auto-parsing note + example to Script Steps output section. - Add type-appropriate zero values table to Workflow Inputs. - Add 'Workflow Metadata Variables' subsection covering workflow.dir, workflow.file, workflow.name. - Update on_start hook context list to include the new workflow.dir/file vars. docs/cli-reference.md: - Document --metadata/-m, --workspace-instructions, and --instructions flags on conductor run. - Add 'Metadata and Instructions' examples block. - Update conductor validate to describe the new template-reference error/warning checks added in #125. docs/providers/claude.md, docs/providers/comparison.md: - Replace stale 'All models support a 200K token context window' / '200K (all models)' claims with notes that the dashboard now sources context_window_max from each provider's SDK at runtime (#144). README.md: - Refresh the Features list to mention sub-workflow composition, dialog mode, workspace instructions, breadcrumb navigation, and the enhanced validate behavior. - Add --metadata, --workspace-instructions, --instructions to the conductor run options table. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: bump version to 0.1.11 and changelog #148 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Follow-ups for #113 — bundles four fixes surfaced after merge.
1. Phantom edges in subworkflow graphs (commit fcab784)
Workflows that use a conditional + bare catch-all route to the same target (e.g.
- to: when: Xfollowed by- to:) emitted two RouteEdges with identical (source, target).graph-layout.tsrendered each as a separate edge, which dagre then laid out as overlapping/diverging strands — hence the visual "phantom" edges. Fix: dedupe by(from, to), first route wins (matches engine first-match-wins).Repro: open dashboard at root, click into
implementationsubworkflow → confirmed gone after the fix.2. For-each iteration dive-in (this commit)
The detail panel for a
for_eachgroup listed iterations but offered no way to navigate into a specific iteration's subworkflow context.pg_execution_group[0]could not be linked into. Added a Layers icon button per iteration row that resolves the slot key (<group>[<key>]) againstsubworkflowContextsand callsnavigateIntoSubworkflow. Click + Enter/Space supported.3. Tighten
_is_proactor_shutdown_race— #145 (I3)Per Jason's review: the proactor race gate fell back to permissive
Truewhen no traceback was attached, which could swallow unrelated AssertionErrors raised by user/workflow code during shutdown. Now requires the deepest traceback frame to originate from asyncio internals. Tests updated to assert the tightened contract (asyncio frame → True; non-asyncio frame → False; missing traceback → False).4. Doc nits — #145 (S2, S3)
engine/workflow.py: comment on why_check_interruptclears the interrupt event before raising (single-listener reset semantics, no race).stores/workflow-store.ts: docstring onresolveSlotPathnoting that newest-wins semantics make older for-each iterations unreachable by stale slot paths.Validation
ruff check+ruff format --checkcleanindex.htmlreferences new asset filenamesRefs
🤖 Co-authored with Copilot CLI