Skip to content

fix(web): dedupe phantom edges, for-each dive-in nav, PR #113 review nits (#145)#146

Merged
jrob5756 merged 4 commits intomicrosoft:mainfrom
PolyphonyRequiem:fix/dedupe-graph-edges
May 5, 2026
Merged

fix(web): dedupe phantom edges, for-each dive-in nav, PR #113 review nits (#145)#146
jrob5756 merged 4 commits intomicrosoft:mainfrom
PolyphonyRequiem:fix/dedupe-graph-edges

Conversation

@PolyphonyRequiem
Copy link
Copy Markdown
Member

@PolyphonyRequiem PolyphonyRequiem commented May 4, 2026

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: X followed by - to: ) emitted two RouteEdges with identical (source, target). graph-layout.ts rendered 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 implementation subworkflow → confirmed gone after the fix.

2. For-each iteration dive-in (this commit)

The detail panel for a for_each group 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>]) against subworkflowContexts and calls navigateIntoSubworkflow. Click + Enter/Space supported.

3. Tighten _is_proactor_shutdown_race#145 (I3)

Per Jason's review: the proactor race gate fell back to permissive True when 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_interrupt clears the interrupt event before raising (single-listener reset semantics, no race).
  • stores/workflow-store.ts: docstring on resolveSlotPath noting that newest-wins semantics make older for-each iterations unreachable by stale slot paths.

Validation

  • 137 web + engine tests pass
  • ruff check + ruff format --check clean
  • Frontend bundle rebuilt; index.html references new asset filenames

Refs

🤖 Co-authored with Copilot CLI

Daniel Green and others added 2 commits May 4, 2026 15:30
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-commenter
Copy link
Copy Markdown

codecov-commenter commented May 4, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (main@bf0e8e9). Learn more about missing BASE report.

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Collaborator

@jrob5756 jrob5756 left a comment

Choose a reason for hiding this comment

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

two small fixes

if tb is None:
return False
frames = tb_mod.extract_tb(tb)
return bool(frames) and "asyncio" in frames[-1].filename
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

_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:
        raise

Suggested 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}]` : ''}`;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

Daniel Green and others added 2 commits May 4, 2026 17:07
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>
@PolyphonyRequiem
Copy link
Copy Markdown
Member Author

Addressed both review comments in commit 9c0940f:

  1. _guarded_serve tightened - now calls _is_proactor_shutdown_race(ctx) with a synthesized context dict instead of the permissive should_exit-only check. Both exception-handler sites share the same strict asyncio-frame gate. Added test_guarded_serve_reraises_non_asyncio_assertion_during_shutdown to verify the tightened contract.

  2. Edge dedupe preserves label accuracy - changed seenPairs from a Set to a Map tracking both the when value and the edge index. When collapsing routes with different conditions to the same target, the label is cleared (undefined) so the UI doesn't misleadingly show only one condition.

All 48 web server tests pass.

Copy link
Copy Markdown
Collaborator

@jrob5756 jrob5756 left a comment

Choose a reason for hiding this comment

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

LGTM — both review findings addressed cleanly. Shared gate for the proactor race is the right call.

@jrob5756 jrob5756 merged commit 1c620f0 into microsoft:main May 5, 2026
7 checks passed
@PolyphonyRequiem PolyphonyRequiem deleted the fix/dedupe-graph-edges branch May 5, 2026 00:35
jrob5756 added a commit that referenced this pull request May 5, 2026
…, #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>
jrob5756 added a commit that referenced this pull request May 5, 2026
* 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>
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.

Follow-up from PR #113: tighten proactor AssertionError suppression + small comment/docstring nits

3 participants