agent_patch
runs/2026-02-28__13-38-10__gpt-5-3-codex/flux-pr-1351/flux-pr-1351.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/agent-logs/agent.patch
14119 bytes
diff --git a/tmp/agent-patch-flux-pr-1351.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/resolve/authorization_test.go b/app/v2/pkg/engine/resolve/authorization_test.go
index 263724a..fe1c44b 100644
--- a/tmp/agent-patch-flux-pr-1351.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/resolve/authorization_test.go
+++ b/app/v2/pkg/engine/resolve/authorization_test.go
@@ -212,8 +212,11 @@ func TestAuthorization(t *testing.T) {
assert.Equal(t, int64(1), authorizer.(*testAuthorizer).preFetchCalls.Load())
assert.Equal(t, int64(0), authorizer.(*testAuthorizer).objectFieldCalls.Load())
+ groupedErrors := resolveCtx.SubgraphErrorsBySubgraph()
+ require.NotNil(t, groupedErrors)
+
var subgraphError *SubgraphError
- require.ErrorAs(t, resolveCtx.subgraphErrors, &subgraphError)
+ require.ErrorAs(t, groupedErrors["users"], &subgraphError)
require.Equal(t, "users", subgraphError.DataSourceInfo.Name)
require.Equal(t, "query", subgraphError.Path)
require.Equal(t, "Not allowed to fetch from users Subgraph", subgraphError.Reason)
@@ -243,8 +246,11 @@ func TestAuthorization(t *testing.T) {
assert.Equal(t, int64(1), authorizer.(*testAuthorizer).preFetchCalls.Load())
assert.Equal(t, int64(0), authorizer.(*testAuthorizer).objectFieldCalls.Load())
+ groupedErrors := resolveCtx.SubgraphErrorsBySubgraph()
+ require.NotNil(t, groupedErrors)
+
var subgraphError *SubgraphError
- require.ErrorAs(t, resolveCtx.subgraphErrors, &subgraphError)
+ require.ErrorAs(t, groupedErrors["users"], &subgraphError)
require.Equal(t, "users", subgraphError.DataSourceInfo.Name)
require.Equal(t, "query", subgraphError.Path)
require.Equal(t, "", subgraphError.Reason)
diff --git a/tmp/agent-patch-flux-pr-1351.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/resolve/context.go b/app/v2/pkg/engine/resolve/context.go
index 65d2d6b..3c58d09 100644
--- a/tmp/agent-patch-flux-pr-1351.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/resolve/context.go
+++ b/app/v2/pkg/engine/resolve/context.go
@@ -6,6 +6,8 @@ import (
"errors"
"io"
"net/http"
+ "slices"
+ "sync"
"time"
"github.com/wundergraph/astjson"
@@ -31,7 +33,9 @@ type Context struct {
rateLimiter RateLimiter
fieldRenderer FieldValueRenderer
- subgraphErrors error
+ subgraphErrors error
+ subgraphErrorsMu sync.Mutex
+ subgraphErrorsBySubgraph map[string][]error
}
type ExecutionOptions struct {
@@ -138,11 +142,112 @@ func (c *Context) SetRateLimiter(limiter RateLimiter) {
}
func (c *Context) SubgraphErrors() error {
+ c.subgraphErrorsMu.Lock()
+ defer c.subgraphErrorsMu.Unlock()
return c.subgraphErrors
}
-func (c *Context) appendSubgraphErrors(errs ...error) {
- c.subgraphErrors = errors.Join(c.subgraphErrors, errors.Join(errs...))
+func (c *Context) SubgraphErrorsBySubgraph() map[string]error {
+ c.subgraphErrorsMu.Lock()
+ defer c.subgraphErrorsMu.Unlock()
+
+ if len(c.subgraphErrorsBySubgraph) == 0 {
+ return nil
+ }
+
+ out := make(map[string]error, len(c.subgraphErrorsBySubgraph))
+ for subgraph, errs := range c.subgraphErrorsBySubgraph {
+ out[subgraph] = joinErrorsDeterministically(errs)
+ }
+ return out
+}
+
+func (c *Context) appendSubgraphErrors(ds DataSourceInfo, errs ...error) {
+ if len(errs) == 0 {
+ return
+ }
+
+ c.subgraphErrorsMu.Lock()
+ defer c.subgraphErrorsMu.Unlock()
+
+ filteredErrs := make([]error, 0, len(errs))
+ for _, err := range errs {
+ if err != nil {
+ filteredErrs = append(filteredErrs, err)
+ }
+ }
+ if len(filteredErrs) == 0 {
+ return
+ }
+
+ if c.subgraphErrorsBySubgraph == nil {
+ c.subgraphErrorsBySubgraph = make(map[string][]error)
+ }
+
+ subgraphKey := ds.Name
+ if subgraphKey == "" {
+ subgraphKey = ds.ID
+ }
+
+ c.subgraphErrorsBySubgraph[subgraphKey] = append(c.subgraphErrorsBySubgraph[subgraphKey], filteredErrs...)
+ c.subgraphErrors = joinSubgraphErrorsDeterministically(c.subgraphErrorsBySubgraph)
+}
+
+func joinSubgraphErrorsDeterministically(subgraphErrors map[string][]error) error {
+ if len(subgraphErrors) == 0 {
+ return nil
+ }
+
+ keys := make([]string, 0, len(subgraphErrors))
+ for key := range subgraphErrors {
+ keys = append(keys, key)
+ }
+ slices.Sort(keys)
+
+ joined := make([]error, 0, len(keys))
+ for _, key := range keys {
+ if err := joinErrorsDeterministically(subgraphErrors[key]); err != nil {
+ joined = append(joined, err)
+ }
+ }
+
+ return errors.Join(joined...)
+}
+
+func joinErrorsDeterministically(errs []error) error {
+ if len(errs) == 0 {
+ return nil
+ }
+
+ filteredErrs := make([]error, 0, len(errs))
+ for _, err := range errs {
+ if err != nil {
+ filteredErrs = append(filteredErrs, err)
+ }
+ }
+
+ if len(filteredErrs) == 0 {
+ return nil
+ }
+
+ slices.SortStableFunc(filteredErrs, func(a, b error) int {
+ aKey := errorSortKey(a)
+ bKey := errorSortKey(b)
+ switch {
+ case aKey < bKey:
+ return -1
+ case aKey > bKey:
+ return 1
+ default:
+ return 0
+ }
+ })
+
+ return errors.Join(filteredErrs...)
+}
+
+func errorSortKey(err error) string {
+ return err.Error()
}
type Request struct {
@@ -175,6 +280,9 @@ func (c *Context) WithContext(ctx context.Context) *Context {
func (c *Context) clone(ctx context.Context) *Context {
cpy := *c
cpy.ctx = ctx
+ cpy.subgraphErrors = nil
+ cpy.subgraphErrorsMu = sync.Mutex{}
+ cpy.subgraphErrorsBySubgraph = nil
if c.Variables != nil {
variablesData := c.Variables.MarshalTo(nil)
cpy.Variables = astjson.MustParseBytes(variablesData)
@@ -203,6 +311,8 @@ func (c *Context) Free() {
c.TracingOptions.DisableAll()
c.Extensions = nil
c.subgraphErrors = nil
+ c.subgraphErrorsMu = sync.Mutex{}
+ c.subgraphErrorsBySubgraph = nil
c.authorizer = nil
c.LoaderHooks = nil
}
diff --git a/app/v2/pkg/engine/resolve/context_subgraph_errors_test.go b/app/v2/pkg/engine/resolve/context_subgraph_errors_test.go
new file mode 100644
index 0000000..05799cf
--- /dev/null
+++ b/app/v2/pkg/engine/resolve/context_subgraph_errors_test.go
@@ -0,0 +1,39 @@
+package resolve
+
+import (
+ "context"
+ "errors"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestContext_SubgraphErrorsAreGroupedAndDeterministic(t *testing.T) {
+ subgraphA := DataSourceInfo{Name: "accounts"}
+ subgraphB := DataSourceInfo{Name: "products"}
+
+ ctxA := NewContext(context.Background())
+ ctxA.appendSubgraphErrors(subgraphB, errors.New("b-2"), errors.New("b-1"))
+ ctxA.appendSubgraphErrors(subgraphA, errors.New("a-2"), errors.New("a-1"))
+
+ ctxB := NewContext(context.Background())
+ ctxB.appendSubgraphErrors(subgraphA, errors.New("a-1"), errors.New("a-2"))
+ ctxB.appendSubgraphErrors(subgraphB, errors.New("b-1"), errors.New("b-2"))
+
+ require.EqualError(t, ctxA.SubgraphErrors(), "a-1\na-2\nb-1\nb-2")
+ require.EqualError(t, ctxB.SubgraphErrors(), "a-1\na-2\nb-1\nb-2")
+
+ grouped := ctxA.SubgraphErrorsBySubgraph()
+ require.Len(t, grouped, 2)
+ require.EqualError(t, grouped["accounts"], "a-1\na-2")
+ require.EqualError(t, grouped["products"], "b-1\nb-2")
+}
+
+func TestContext_SubgraphErrorsUseDatasourceIDWhenNameIsMissing(t *testing.T) {
+ ctx := NewContext(context.Background())
+ ctx.appendSubgraphErrors(DataSourceInfo{ID: "id-only-subgraph"}, errors.New("boom"))
+
+ grouped := ctx.SubgraphErrorsBySubgraph()
+ require.Len(t, grouped, 1)
+ require.EqualError(t, grouped["id-only-subgraph"], "boom")
+}
diff --git a/tmp/agent-patch-flux-pr-1351.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/resolve/loader.go b/app/v2/pkg/engine/resolve/loader.go
index 7a14d61..abde1f4 100644
--- a/tmp/agent-patch-flux-pr-1351.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/resolve/loader.go
+++ b/app/v2/pkg/engine/resolve/loader.go
@@ -243,7 +243,7 @@ func (l *Loader) resolveParallel(nodes []*FetchTreeNode) error {
if l.ctx.LoaderHooks != nil && results[i].nestedMergeItems[j].loaderHookContext != nil {
l.ctx.LoaderHooks.OnFinished(results[i].nestedMergeItems[j].loaderHookContext,
results[i].nestedMergeItems[j].ds,
- newResponseInfo(results[i].nestedMergeItems[j], l.ctx.subgraphErrors))
+ newResponseInfo(results[i].nestedMergeItems[j], l.ctx.SubgraphErrors()))
}
if err != nil {
return errors.WithStack(err)
@@ -252,7 +252,7 @@ func (l *Loader) resolveParallel(nodes []*FetchTreeNode) error {
} else {
err = l.mergeResult(nodes[i].Item, results[i], itemsItems[i])
if l.ctx.LoaderHooks != nil {
- l.ctx.LoaderHooks.OnFinished(results[i].loaderHookContext, results[i].ds, newResponseInfo(results[i], l.ctx.subgraphErrors))
+ l.ctx.LoaderHooks.OnFinished(results[i].loaderHookContext, results[i].ds, newResponseInfo(results[i], l.ctx.SubgraphErrors()))
}
if err != nil {
return errors.WithStack(err)
@@ -289,7 +289,7 @@ func (l *Loader) resolveSingle(item *FetchItem) error {
}
err = l.mergeResult(item, res, items)
if l.ctx.LoaderHooks != nil {
- l.ctx.LoaderHooks.OnFinished(res.loaderHookContext, res.ds, newResponseInfo(res, l.ctx.subgraphErrors))
+ l.ctx.LoaderHooks.OnFinished(res.loaderHookContext, res.ds, newResponseInfo(res, l.ctx.SubgraphErrors()))
}
return err
@@ -303,7 +303,7 @@ func (l *Loader) resolveSingle(item *FetchItem) error {
}
err = l.mergeResult(item, res, items)
if l.ctx.LoaderHooks != nil {
- l.ctx.LoaderHooks.OnFinished(res.loaderHookContext, res.ds, newResponseInfo(res, l.ctx.subgraphErrors))
+ l.ctx.LoaderHooks.OnFinished(res.loaderHookContext, res.ds, newResponseInfo(res, l.ctx.SubgraphErrors()))
}
return err
case *EntityFetch:
@@ -316,7 +316,7 @@ func (l *Loader) resolveSingle(item *FetchItem) error {
}
err = l.mergeResult(item, res, items)
if l.ctx.LoaderHooks != nil {
- l.ctx.LoaderHooks.OnFinished(res.loaderHookContext, res.ds, newResponseInfo(res, l.ctx.subgraphErrors))
+ l.ctx.LoaderHooks.OnFinished(res.loaderHookContext, res.ds, newResponseInfo(res, l.ctx.SubgraphErrors()))
}
return err
case *ParallelListItemFetch:
@@ -349,7 +349,7 @@ func (l *Loader) resolveSingle(item *FetchItem) error {
for i := range results {
err = l.mergeResult(item, results[i], items[i:i+1])
if l.ctx.LoaderHooks != nil {
- l.ctx.LoaderHooks.OnFinished(results[i].loaderHookContext, results[i].ds, newResponseInfo(results[i], l.ctx.subgraphErrors))
+ l.ctx.LoaderHooks.OnFinished(results[i].loaderHookContext, results[i].ds, newResponseInfo(results[i], l.ctx.SubgraphErrors()))
}
if err != nil {
return errors.WithStack(err)
@@ -740,7 +740,7 @@ func (l *Loader) appendSubgraphError(res *result, fetchItem *FetchItem, value *a
subgraphError.AppendDownstreamError(&gErr)
}
- l.ctx.appendSubgraphErrors(res.err, subgraphError)
+ l.ctx.appendSubgraphErrors(res.ds, res.err, subgraphError)
return nil
}
@@ -1088,7 +1088,7 @@ func (l *Loader) renderErrorsFailedDeps(fetchItem *FetchItem, res *result) error
}
func (l *Loader) renderErrorsFailedToFetch(fetchItem *FetchItem, res *result, reason string) error {
- l.ctx.appendSubgraphErrors(res.err, NewSubgraphError(res.ds, fetchItem.ResponsePath, reason, res.statusCode))
+ l.ctx.appendSubgraphErrors(res.ds, res.err, NewSubgraphError(res.ds, fetchItem.ResponsePath, reason, res.statusCode))
errorObject, err := astjson.ParseWithoutCache(l.renderSubgraphBaseError(res.ds, fetchItem.ResponsePath, reason))
if err != nil {
return err
@@ -1104,7 +1104,7 @@ func (l *Loader) renderErrorsStatusFallback(fetchItem *FetchItem, res *result, s
reason += fmt.Sprintf(": %s", statusText)
}
- l.ctx.appendSubgraphErrors(res.err, NewSubgraphError(res.ds, fetchItem.ResponsePath, reason, res.statusCode))
+ l.ctx.appendSubgraphErrors(res.ds, res.err, NewSubgraphError(res.ds, fetchItem.ResponsePath, reason, res.statusCode))
errorObject, err := astjson.ParseWithoutCache(fmt.Sprintf(`{"message":"%s"}`, reason))
if err != nil {
@@ -1133,7 +1133,7 @@ func (l *Loader) renderSubgraphBaseError(ds DataSourceInfo, path, reason string)
func (l *Loader) renderAuthorizationRejectedErrors(fetchItem *FetchItem, res *result) error {
for i := range res.authorizationRejectedReasons {
- l.ctx.appendSubgraphErrors(res.err, NewSubgraphError(res.ds, fetchItem.ResponsePath, res.authorizationRejectedReasons[i], res.statusCode))
+ l.ctx.appendSubgraphErrors(res.ds, res.err, NewSubgraphError(res.ds, fetchItem.ResponsePath, res.authorizationRejectedReasons[i], res.statusCode))
}
pathPart := l.renderAtPathErrorPart(fetchItem.ResponsePath)
extensionErrorCode := fmt.Sprintf(`"extensions":{"code":"%s"}`, errorcodes.UnauthorizedFieldOrType)
@@ -1174,7 +1174,7 @@ func (l *Loader) renderAuthorizationRejectedErrors(fetchItem *FetchItem, res *re
}
func (l *Loader) renderRateLimitRejectedErrors(fetchItem *FetchItem, res *result) error {
- l.ctx.appendSubgraphErrors(res.err, NewRateLimitError(res.ds.Name, fetchItem.ResponsePath, res.rateLimitRejectedReason))
+ l.ctx.appendSubgraphErrors(res.ds, res.err, NewRateLimitError(res.ds.Name, fetchItem.ResponsePath, res.rateLimitRejectedReason))
pathPart := l.renderAtPathErrorPart(fetchItem.ResponsePath)
var (
err error
diff --git a/tmp/agent-patch-flux-pr-1351.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/resolve/resolvable.go b/app/v2/pkg/engine/resolve/resolvable.go
index 5219c91..13df1db 100644
--- a/tmp/agent-patch-flux-pr-1351.1-of-1.2026-02-28__13-38-10__gpt-5-3-codex/app/v2/pkg/engine/resolve/resolvable.go
+++ b/app/v2/pkg/engine/resolve/resolvable.go
@@ -758,7 +758,7 @@ func (r *Resolvable) addRejectFieldError(reason string, ds DataSourceInfo, field
} else {
errorMessage = fmt.Sprintf("Unauthorized to load field '%s', Reason: %s.", fieldPath, reason)
}
- r.ctx.appendSubgraphErrors(errors.New(errorMessage),
+ r.ctx.appendSubgraphErrors(ds, errors.New(errorMessage),
NewSubgraphError(ds, fieldPath, reason, 0))
fastjsonext.AppendErrorWithExtensionsCodeToArray(r.astjsonArena, r.errors, errorMessage, errorcodes.UnauthorizedFieldOrType, r.path)
r.popNodePathElement(nodePath)