STET

agent_patch

runs/2026-02-28__13-38-10__gpt-5-4/flux-pr-1232/flux-pr-1232.1-of-1.2026-02-28__13-38-10__gpt-5-4/agent-logs/agent.patch

15651 bytes

Back to adjudication
diff --git a/tmp/agent-patch-flux-pr-1232.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/postprocess/deduplicate_single_fetches.go b/app/v2/pkg/engine/postprocess/deduplicate_single_fetches.go
index 9fb8c1c..d0e4869 100644
--- a/tmp/agent-patch-flux-pr-1232.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/postprocess/deduplicate_single_fetches.go
+++ b/app/v2/pkg/engine/postprocess/deduplicate_single_fetches.go
@@ -14,16 +14,27 @@ func (d *deduplicateSingleFetches) ProcessFetchTree(root *resolve.FetchTreeNode)
 	if d.disable {
 		return
 	}
+	replacements := make(map[int]int)
 	for i := range root.ChildNodes {
 		for j := i + 1; j < len(root.ChildNodes); j++ {
 			if root.ChildNodes[i].Item.Equals(root.ChildNodes[j].Item) {
-				root.ChildNodes[i].Item.FetchPath = d.mergeFetchPath(root.ChildNodes[i].Item.FetchPath, root.ChildNodes[j].Item.FetchPath)
+				left := root.ChildNodes[i].Item
+				right := root.ChildNodes[j].Item
+				root.ChildNodes[i].Item.FetchPath = d.mergeFetchPath(left.FetchPath, right.FetchPath)
+				d.mergeFetches(left.Fetch, right.Fetch)
+				replacements[right.Fetch.Dependencies().FetchID] = left.Fetch.Dependencies().FetchID
 
 				root.ChildNodes = append(root.ChildNodes[:j], root.ChildNodes[j+1:]...)
 				j--
 			}
 		}
 	}
+	if len(replacements) == 0 {
+		return
+	}
+	for _, node := range root.ChildNodes {
+		d.updateNodeDependencies(node, replacements)
+	}
 }
 
 func (d *deduplicateSingleFetches) mergeFetchPath(left, right []resolve.FetchItemPathElement) []resolve.FetchItemPathElement {
@@ -44,3 +55,158 @@ func (d *deduplicateSingleFetches) mergeTypeNames(left []string, right []string)
 	slices.Sort(out)
 	return slices.Compact(out) // removes consecutive duplicates from the sorted slice
 }
+
+func (d *deduplicateSingleFetches) mergeFetches(left, right resolve.Fetch) {
+	switch l := left.(type) {
+	case *resolve.SingleFetch:
+		r, ok := right.(*resolve.SingleFetch)
+		if !ok {
+			return
+		}
+		l.FetchDependencies.DependsOnFetchIDs = d.mergeIntSlice(l.FetchDependencies.DependsOnFetchIDs, r.FetchDependencies.DependsOnFetchIDs)
+		l.FetchConfiguration.CoordinateDependencies = d.mergeCoordinateDependencies(l.FetchConfiguration.CoordinateDependencies, r.FetchConfiguration.CoordinateDependencies)
+	case *resolve.EntityFetch:
+		r, ok := right.(*resolve.EntityFetch)
+		if !ok {
+			return
+		}
+		l.FetchDependencies.DependsOnFetchIDs = d.mergeIntSlice(l.FetchDependencies.DependsOnFetchIDs, r.FetchDependencies.DependsOnFetchIDs)
+		l.CoordinateDependencies = d.mergeCoordinateDependencies(l.CoordinateDependencies, r.CoordinateDependencies)
+	case *resolve.BatchEntityFetch:
+		r, ok := right.(*resolve.BatchEntityFetch)
+		if !ok {
+			return
+		}
+		l.FetchDependencies.DependsOnFetchIDs = d.mergeIntSlice(l.FetchDependencies.DependsOnFetchIDs, r.FetchDependencies.DependsOnFetchIDs)
+		l.CoordinateDependencies = d.mergeCoordinateDependencies(l.CoordinateDependencies, r.CoordinateDependencies)
+	case *resolve.ParallelListItemFetch:
+		r, ok := right.(*resolve.ParallelListItemFetch)
+		if !ok {
+			return
+		}
+		d.mergeFetches(l.Fetch, r.Fetch)
+	}
+}
+
+func (d *deduplicateSingleFetches) updateNodeDependencies(node *resolve.FetchTreeNode, replacements map[int]int) {
+	if node == nil || node.Item == nil {
+		return
+	}
+	d.updateFetchDependencies(node.Item.Fetch, replacements)
+	for _, child := range node.ChildNodes {
+		d.updateNodeDependencies(child, replacements)
+	}
+	if node.Trigger != nil {
+		d.updateNodeDependencies(node.Trigger, replacements)
+	}
+}
+
+func (d *deduplicateSingleFetches) updateFetchDependencies(fetch resolve.Fetch, replacements map[int]int) {
+	switch f := fetch.(type) {
+	case *resolve.SingleFetch:
+		f.FetchDependencies.DependsOnFetchIDs = d.remapIntSlice(f.FetchDependencies.DependsOnFetchIDs, replacements)
+		f.FetchConfiguration.CoordinateDependencies = d.remapCoordinateDependencies(f.FetchConfiguration.CoordinateDependencies, replacements)
+	case *resolve.EntityFetch:
+		f.FetchDependencies.DependsOnFetchIDs = d.remapIntSlice(f.FetchDependencies.DependsOnFetchIDs, replacements)
+		f.CoordinateDependencies = d.remapCoordinateDependencies(f.CoordinateDependencies, replacements)
+	case *resolve.BatchEntityFetch:
+		f.FetchDependencies.DependsOnFetchIDs = d.remapIntSlice(f.FetchDependencies.DependsOnFetchIDs, replacements)
+		f.CoordinateDependencies = d.remapCoordinateDependencies(f.CoordinateDependencies, replacements)
+	case *resolve.ParallelListItemFetch:
+		d.updateFetchDependencies(f.Fetch, replacements)
+	}
+}
+
+func (d *deduplicateSingleFetches) remapIntSlice(ids []int, replacements map[int]int) []int {
+	if len(ids) == 0 {
+		return nil
+	}
+	out := make([]int, 0, len(ids))
+	for _, id := range ids {
+		out = append(out, d.resolveFetchID(id, replacements))
+	}
+	return d.compactInts(out)
+}
+
+func (d *deduplicateSingleFetches) mergeIntSlice(left, right []int) []int {
+	if len(left) == 0 && len(right) == 0 {
+		return nil
+	}
+	out := append(slices.Clone(left), right...)
+	return d.compactInts(out)
+}
+
+func (d *deduplicateSingleFetches) compactInts(ids []int) []int {
+	if len(ids) == 0 {
+		return nil
+	}
+	out := make([]int, 0, len(ids))
+	seen := make(map[int]struct{}, len(ids))
+	for _, id := range ids {
+		if _, ok := seen[id]; ok {
+			continue
+		}
+		seen[id] = struct{}{}
+		out = append(out, id)
+	}
+	return out
+}
+
+func (d *deduplicateSingleFetches) mergeCoordinateDependencies(left, right []resolve.FetchDependency) []resolve.FetchDependency {
+	if len(left) == 0 && len(right) == 0 {
+		return nil
+	}
+	out := slices.Clone(left)
+	for _, dep := range right {
+		if !slices.ContainsFunc(out, func(existing resolve.FetchDependency) bool {
+			return d.fetchDependencyEquals(existing, dep)
+		}) {
+			out = append(out, dep)
+		}
+	}
+	return out
+}
+
+func (d *deduplicateSingleFetches) remapCoordinateDependencies(deps []resolve.FetchDependency, replacements map[int]int) []resolve.FetchDependency {
+	if len(deps) == 0 {
+		return nil
+	}
+	out := make([]resolve.FetchDependency, 0, len(deps))
+	for _, dep := range deps {
+		updated := dep
+		updated.DependsOn = make([]resolve.FetchDependencyOrigin, 0, len(dep.DependsOn))
+		for _, origin := range dep.DependsOn {
+			origin.FetchID = d.resolveFetchID(origin.FetchID, replacements)
+			if slices.ContainsFunc(updated.DependsOn, func(existing resolve.FetchDependencyOrigin) bool {
+				return existing == origin
+			}) {
+				continue
+			}
+			updated.DependsOn = append(updated.DependsOn, origin)
+		}
+		if slices.ContainsFunc(out, func(existing resolve.FetchDependency) bool {
+			return d.fetchDependencyEquals(existing, updated)
+		}) {
+			continue
+		}
+		out = append(out, updated)
+	}
+	return out
+}
+
+func (d *deduplicateSingleFetches) fetchDependencyEquals(left, right resolve.FetchDependency) bool {
+	if left.Coordinate != right.Coordinate || left.IsUserRequested != right.IsUserRequested {
+		return false
+	}
+	return slices.Equal(left.DependsOn, right.DependsOn)
+}
+
+func (d *deduplicateSingleFetches) resolveFetchID(id int, replacements map[int]int) int {
+	for {
+		replacement, ok := replacements[id]
+		if !ok || replacement == id {
+			return id
+		}
+		id = replacement
+	}
+}
diff --git a/tmp/agent-patch-flux-pr-1232.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/postprocess/deduplicate_single_fetches_test.go b/app/v2/pkg/engine/postprocess/deduplicate_single_fetches_test.go
index 88cf2be..0d8fdba 100644
--- a/tmp/agent-patch-flux-pr-1232.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/postprocess/deduplicate_single_fetches_test.go
+++ b/app/v2/pkg/engine/postprocess/deduplicate_single_fetches_test.go
@@ -115,4 +115,144 @@ func TestDeduplicateSingleFetches_ProcessFetchTree(t *testing.T) {
 
 		assert.Equal(t, output, input)
 	})
+
+	t.Run("rewrites stale dependencies when merging duplicates", func(t *testing.T) {
+		input := &resolve.FetchTreeNode{
+			ChildNodes: []*resolve.FetchTreeNode{
+				{
+					Kind: resolve.FetchTreeNodeKindSingle,
+					Item: &resolve.FetchItem{
+						FetchPath: []resolve.FetchItemPathElement{{Kind: resolve.FetchItemPathElementKindObject, Path: []string{"a"}}},
+						Fetch: &resolve.SingleFetch{
+							FetchConfiguration: resolve.FetchConfiguration{
+								Input: "a",
+								CoordinateDependencies: []resolve.FetchDependency{
+									{
+										Coordinate:      resolve.GraphCoordinate{TypeName: "Query", FieldName: "a"},
+										IsUserRequested: true,
+										DependsOn: []resolve.FetchDependencyOrigin{
+											{FetchID: 10, Subgraph: "products", Coordinate: resolve.GraphCoordinate{TypeName: "Product", FieldName: "id"}, IsKey: true},
+										},
+									},
+								},
+							},
+							FetchDependencies: resolve.FetchDependencies{FetchID: 1, DependsOnFetchIDs: []int{10}},
+						},
+					},
+				},
+				{
+					Kind: resolve.FetchTreeNodeKindSingle,
+					Item: &resolve.FetchItem{
+						FetchPath: []resolve.FetchItemPathElement{{Kind: resolve.FetchItemPathElementKindObject, Path: []string{"a"}}},
+						Fetch: &resolve.SingleFetch{
+							FetchConfiguration: resolve.FetchConfiguration{
+								Input: "a",
+								CoordinateDependencies: []resolve.FetchDependency{
+									{
+										Coordinate:      resolve.GraphCoordinate{TypeName: "Query", FieldName: "a"},
+										IsUserRequested: true,
+										DependsOn: []resolve.FetchDependencyOrigin{
+											{FetchID: 11, Subgraph: "reviews", Coordinate: resolve.GraphCoordinate{TypeName: "Review", FieldName: "id"}, IsKey: true},
+										},
+									},
+								},
+							},
+							FetchDependencies: resolve.FetchDependencies{FetchID: 2, DependsOnFetchIDs: []int{11}},
+						},
+					},
+				},
+				{
+					Kind: resolve.FetchTreeNodeKindSingle,
+					Item: &resolve.FetchItem{
+						Fetch: &resolve.SingleFetch{
+							FetchConfiguration: resolve.FetchConfiguration{
+								Input: "b",
+								CoordinateDependencies: []resolve.FetchDependency{
+									{
+										Coordinate:      resolve.GraphCoordinate{TypeName: "Query", FieldName: "b"},
+										IsUserRequested: true,
+										DependsOn: []resolve.FetchDependencyOrigin{
+											{FetchID: 2, Subgraph: "products", Coordinate: resolve.GraphCoordinate{TypeName: "Query", FieldName: "a"}},
+										},
+									},
+								},
+							},
+							FetchDependencies: resolve.FetchDependencies{FetchID: 3, DependsOnFetchIDs: []int{2}},
+						},
+					},
+				},
+			},
+		}
+
+		dedup := &deduplicateSingleFetches{}
+		dedup.ProcessFetchTree(input)
+
+		assert.Len(t, input.ChildNodes, 2)
+
+		mergedFetch, ok := input.ChildNodes[0].Item.Fetch.(*resolve.SingleFetch)
+		assert.True(t, ok)
+		assert.ElementsMatch(t, []int{10, 11}, mergedFetch.FetchDependencies.DependsOnFetchIDs)
+		assert.Len(t, mergedFetch.FetchConfiguration.CoordinateDependencies, 2)
+
+		dependentFetch, ok := input.ChildNodes[1].Item.Fetch.(*resolve.SingleFetch)
+		assert.True(t, ok)
+		assert.Equal(t, []int{1}, dependentFetch.FetchDependencies.DependsOnFetchIDs)
+		assert.Equal(t, 1, dependentFetch.FetchConfiguration.CoordinateDependencies[0].DependsOn[0].FetchID)
+	})
+
+	t.Run("ignores coordinate dependency differences for single fetch deduplication", func(t *testing.T) {
+		input := &resolve.FetchTreeNode{
+			ChildNodes: []*resolve.FetchTreeNode{
+				{
+					Kind: resolve.FetchTreeNodeKindSingle,
+					Item: &resolve.FetchItem{
+						FetchPath: []resolve.FetchItemPathElement{{Kind: resolve.FetchItemPathElementKindObject, Path: []string{"a"}}},
+						Fetch: &resolve.SingleFetch{
+							FetchConfiguration: resolve.FetchConfiguration{
+								Input: "a",
+								CoordinateDependencies: []resolve.FetchDependency{
+									{
+										Coordinate:      resolve.GraphCoordinate{TypeName: "Query", FieldName: "a"},
+										IsUserRequested: true,
+										DependsOn: []resolve.FetchDependencyOrigin{
+											{FetchID: 1, Subgraph: "products", Coordinate: resolve.GraphCoordinate{TypeName: "Product", FieldName: "id"}},
+										},
+									},
+								},
+							},
+							FetchDependencies: resolve.FetchDependencies{FetchID: 1},
+						},
+					},
+				},
+				{
+					Kind: resolve.FetchTreeNodeKindSingle,
+					Item: &resolve.FetchItem{
+						FetchPath: []resolve.FetchItemPathElement{{Kind: resolve.FetchItemPathElementKindObject, Path: []string{"a"}}},
+						Fetch: &resolve.SingleFetch{
+							FetchConfiguration: resolve.FetchConfiguration{
+								Input: "a",
+								CoordinateDependencies: []resolve.FetchDependency{
+									{
+										Coordinate:      resolve.GraphCoordinate{TypeName: "Query", FieldName: "a"},
+										IsUserRequested: true,
+										DependsOn: []resolve.FetchDependencyOrigin{
+											{FetchID: 2, Subgraph: "reviews", Coordinate: resolve.GraphCoordinate{TypeName: "Review", FieldName: "id"}},
+										},
+									},
+								},
+							},
+							FetchDependencies: resolve.FetchDependencies{FetchID: 2},
+						},
+					},
+				},
+			},
+		}
+
+		dedup := &deduplicateSingleFetches{}
+		dedup.ProcessFetchTree(input)
+
+		assert.Len(t, input.ChildNodes, 1)
+		mergedFetch := input.ChildNodes[0].Item.Fetch.(*resolve.SingleFetch)
+		assert.Len(t, mergedFetch.FetchConfiguration.CoordinateDependencies, 2)
+	})
 }
diff --git a/tmp/agent-patch-flux-pr-1232.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/resolve/fetch.go b/app/v2/pkg/engine/resolve/fetch.go
index 9979256..b352e4b 100644
--- a/tmp/agent-patch-flux-pr-1232.1-of-1.2026-02-28__13-38-10__gpt-5-4/app/v2/pkg/engine/resolve/fetch.go
+++ b/app/v2/pkg/engine/resolve/fetch.go
@@ -68,7 +68,7 @@ func (f *FetchItem) Equals(other *FetchItem) bool {
 	if !ok {
 		return false
 	}
-	return l.FetchConfiguration.Equals(&r.FetchConfiguration)
+	return l.FetchConfiguration.EqualsForSingleFetch(&r.FetchConfiguration)
 }
 
 type FetchItemPathElement struct {
@@ -293,6 +293,34 @@ type FetchConfiguration struct {
 }
 
 func (fc *FetchConfiguration) Equals(other *FetchConfiguration) bool {
+	if !fc.equalsSharedFields(other) {
+		return false
+	}
+	if !slices.EqualFunc(fc.CoordinateDependencies, other.CoordinateDependencies, func(a, b FetchDependency) bool {
+		if a.Coordinate != b.Coordinate {
+			return false
+		}
+		if a.IsUserRequested != b.IsUserRequested {
+			return false
+		}
+		return slices.EqualFunc(a.DependsOn, b.DependsOn, func(x, y FetchDependencyOrigin) bool {
+			return x.FetchID == y.FetchID &&
+				x.Subgraph == y.Subgraph &&
+				x.Coordinate == y.Coordinate &&
+				x.IsKey == y.IsKey &&
+				x.IsRequires == y.IsRequires
+		})
+	}) {
+		return false
+	}
+	return true
+}
+
+func (fc *FetchConfiguration) EqualsForSingleFetch(other *FetchConfiguration) bool {
+	return fc.equalsSharedFields(other)
+}
+
+func (fc *FetchConfiguration) equalsSharedFields(other *FetchConfiguration) bool {
 	if fc.Input != other.Input {
 		return false
 	}
@@ -319,23 +347,6 @@ func (fc *FetchConfiguration) Equals(other *FetchConfiguration) bool {
 	if fc.SetTemplateOutputToNullOnVariableNull != other.SetTemplateOutputToNullOnVariableNull {
 		return false
 	}
-	if !slices.EqualFunc(fc.CoordinateDependencies, other.CoordinateDependencies, func(a, b FetchDependency) bool {
-		if a.Coordinate != b.Coordinate {
-			return false
-		}
-		if a.IsUserRequested != b.IsUserRequested {
-			return false
-		}
-		return slices.EqualFunc(a.DependsOn, b.DependsOn, func(x, y FetchDependencyOrigin) bool {
-			return x.FetchID == y.FetchID &&
-				x.Subgraph == y.Subgraph &&
-				x.Coordinate == y.Coordinate &&
-				x.IsKey == y.IsKey &&
-				x.IsRequires == y.IsRequires
-		})
-	}) {
-		return false
-	}
 	return true
 }