Skip to content

fix: missing subgraph contents lost when moved - #2703

Merged
camielvs merged 2 commits into
masterfrom
09-04-fix_subgraph_contents_lost_when_moved
Sep 4, 2026
Merged

fix: missing subgraph contents lost when moved#2703
camielvs merged 2 commits into
masterfrom
09-04-fix_subgraph_contents_lost_when_moved

Conversation

@camielvs

@camielvs camielvs commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Description

Two bug fixes, both cases where a pipeline quietly lost part of itself.

1. A subgraph inside a subgraph was emptied when you moved the task holding it

If a subgraph contained another subgraph, two everyday actions destroyed the inner one:

  • Unpacking the outer subgraph
  • Selecting the outer subgraph together with another task and grouping them into a new subgraph

Afterwards the inner subgraph task was still sitting on the canvas with its name and position, but it was hollow — double-clicking no longer opened it, and its ports were gone. Nothing warned you; you only found out when you went looking inside.

Both actions now bring the nested contents along with the task. Copy/paste already handled this correctly — these two paths simply weren't doing the same thing.

2. Connections to ports that don't exist were accepted

It was possible to end up with a connection that looked fine on the canvas but disappeared the moment the pipeline was saved, because it pointed at a port the component doesn't actually have, or ran the wrong way (out of a pipeline output, or into a pipeline input). Nothing flagged it, so the pipeline would happily submit and then behave as if the value had never been connected.

These are now caught in two places:

  • The pipeline validator reports them as errors, with a "Delete Connection" fix in the issues panel — so imported or hand-edited YAML gets checked too.
  • The AI assistant refuses to create one, and tells you which ports the task really has so it can correct itself.

Dragging on the canvas could never produce these, so in practice they came from the AI assistant or from imported YAML.

Related Issue and Pull requests

Follows up on the two review comments left on #2684 that were deferred out of that stack.

Type of Change

  • Bug fix
  • New feature
  • Improvement
  • Cleanup/Refactor
  • Breaking change
  • Documentation update

Checklist

  • I have tested this does not break current pipelines / runs functionality
  • I have tested the changes on staging

Test Instructions

Nested subgraphs (fix 1)

  1. Build a pipeline with a subgraph nested two deep: add two tasks, group them into a subgraph, then select that subgraph plus one more task and group again.
  2. Double-click down through both levels to confirm the inner subgraph opens and has its contents.
  3. Back at the top level, unpack the outer subgraph.
  4. The inner subgraph task should still open on double-click, with its tasks and ports intact. Before this change it opened empty.
  5. Undo, then try the other route: shift-select the outer subgraph plus another task and choose Create Subgraph. Look inside the new subgraph — the nested one should still have its contents.

Connections (fix 2)

  1. With the AI assistant on, ask it to connect two tasks using a port name that doesn't exist ("connect Train's foo output to Evaluate"). It should refuse and list the real port names rather than creating a connection that goes nowhere.
  2. Import a YAML file with a connection pointing at a port its component doesn't declare. The editor should now show an error for it with a Delete Connection action, instead of letting the pipeline submit.

Additional Comments

Regression tests were added for both fixes, and each new test was confirmed to fail without its fix. The full unit suite passes (2362 tests).

The new connection check is error-severity, which blocks submission, so all 16 shipped example and tour pipelines were checked against it — 95 connections, no new errors. Nothing that ships starts showing problems.

The check deliberately only looks at connections between tasks. Connections to a pipeline input or output are named after the input or output itself when saved, so renaming one leaves a harmless leftover name behind that would otherwise get flagged as broken.

@camielvs
camielvs requested a review from a team as a code owner September 4, 2026 17:46
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown

🎩 Preview

A preview build has been created at: 09-04-fix_subgraph_contents_lost_when_moved/8dda8ef

@camielvs camielvs changed the title 09 04 fix subgraph contents lost when moved fix: missing subgraph contents lost when moved Sep 4, 2026

@morgan-wowk morgan-wowk left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 Automated review

Reviewed both fixes against the head; the design is right and I traced the parts that could bite.

Fix 1 — nested contents preserved on move (verified correct). The switch from t.componentRef to t.resolvedComponentRef in snapshotTask/addInnerTasks, then routing through promoteInlineSubgraph, is exactly the right seam:

  • resolvedComponentRef returns this.componentRef verbatim for a non-subgraph and only serializes the live subgraphSpec for a subgraph — so ordinary library/container tasks are untouched (no accidental spec-inlining), and a subgraph task now carries the contents that previously lived only in the runtime subgraphSpec and never made it into componentRef.spec. That missing serialization is precisely why the inner graph came out hollow.
  • promoteInlineSubgraph gates on isGraphImplementation(ref.spec.implementation), so it promotes a real nested subgraph into a live subgraphSpec and leaves everything else as a plain ref. Both new tests assert the inner task, its position, and that the promoted task is a subgraph with componentRef.spec stripped — good.

Fix 2 — binding-endpoint validation (verified correct, incl. the risky edges). One shared findBindingEndpointProblems feeding both the validator and the CSOM bridge is the right call — the agent is refused on exactly the grounds the validator would report. The edges that could produce false errors all hold:

  • Silent while ports are unknown: non-subgraph resolvedComponentSpec is componentRef.spec, which is undefined until the component resolves, so the sourcePorts &&/targetPorts && guards short-circuit and an unresolved library task is never flagged. (Tested.)
  • Aggregator: createAggregatorInput yields a real InputSpec (agg_N) that lands in the spec's inputs, so legitimate aggregator connections resolve and pass; only the synthetic __add_aggregator_input__ handle — never a real port — is caught. (Tested.)
  • Direction checks fire only on genuinely reversed connections: source-is-output matches only when the source id is a pipeline output, target-is-input only when the target id is a pipeline input; the valid task→output and input→task shapes don't match either .find. And IS_ENABLED_PORT_NAME is whitelisted on targets.
  • No double-report with the orphaned-binding check: those require the entity id to be absent, while these require it to be present, so they're mutually exclusive per endpoint.

INVALID_BINDING_SOURCE/TARGET wire to a "Delete Connection" resolution, and the error-severity choice is justified (a bad endpoint serializes to nothing, so warning wouldn't prevent the silent data loss). Nice touch validating all 16 shipped pipelines against the new error before shipping it.

NIT (test gap, not a defect)

Fix 1's promote path round-trips through serializeComponentSpec(subgraphSpec)YamlDeserializer.deserialize, and both regression tests assert the inner tasks and a position survive — but not the inner bindings (a connection between two nested tasks). Since the bug was "contents silently lost," a nested task→task binding surviving the group/unpack is the other half of "contents," and it's the part that leans on the serializer round-trip rather than a direct field copy. Low risk (that serializer is heavily exercised elsewhere), but adding one binding to the nested fixture and asserting it's still there would fully close the regression.

Approving — both fixes are correct and the coverage is strong; the note above is a nice-to-have.

camielvs and others added 2 commits September 4, 2026 14:53
`promoteInlineSubgraph` moves an inline graph off `componentRef.spec` and
into `Task.subgraphSpec`, so a relocation that copies `componentRef` alone
carries a ref whose spec has already been stripped. Both `unpackSubgraph`
and `createSubgraph` did exactly that, silently flattening any subgraph
nested inside the task being moved.

Snapshot through `resolvedComponentRef` and re-promote, matching the
copy/paste path in `taskManifestBase`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A binding to a port the component does not declare, or one sourced from a
pipeline output or targeted at a pipeline input, serializes to nothing at
all — the argument is simply absent from the saved YAML. Nothing caught
either case, so `connect_nodes` accepted both and the pipeline looked
connected in the editor while silently dropping the value on save.

`findBindingEndpointProblems` is the single predicate for this, shared by
`validateSpec` (so every route is covered, not just the agent) and by the
AI bridge (so the agent is refused on the same grounds, with the available
port names in the refusal).

Port names are checked on task endpoints only: for graph inputs and
outputs the serializer reads the entity's own name and ignores the
binding's port name, so a rename leaves that field stale by design.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@camielvs
camielvs force-pushed the 09-04-fix_subgraph_contents_lost_when_moved branch from 0e05cdb to 8dda8ef Compare September 4, 2026 21:53
@camielvs
camielvs merged commit e6d82f2 into master Sep 4, 2026
15 of 16 checks passed
@camielvs
camielvs deleted the 09-04-fix_subgraph_contents_lost_when_moved branch September 4, 2026 22:02
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.

2 participants