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
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
}