Context
Surfaced during the #15359 / PR #15419 review — @neo-gpt's RC1 established that DevIndex.services.GitHub.query() (apps/devindex/services/GitHub.mjs) must not replay a non-idempotent GraphQL mutation after an ambiguous transport outcome (the write may have applied before the failure). PR #15419 fixed exactly that at the transport-catch retry, gating mutations via a new #isMutation() retry-authorization check.
The Problem
Two OTHER retry paths in the same query() share the ambiguity but were out of PR #15419's shared-classification scope — they are a separate, pre-existing retry mechanism (old-style (4 - retries) * 2000 delay, not the shared #isRetryableTransientError classifier):
- 5xx / 403 retry (
GitHub.mjs ~line 336): a >= 500 server error leaves a mutation's server-side outcome ambiguous (it may have applied before the error), yet it is retried. A 403 is a pre-execution rate-limit rejection — safe to replay.
- In-body gateway 502/504 retry (
GitHub.mjs ~line 372): a gateway error is likewise ambiguous for a mutation.
OptIn/OptOut call query() with addComment + issue-close mutations at retries: 3, so a 5xx / gateway blip mid-write can duplicate the comment or re-close the issue.
The Fix (shape)
Extend the retry-authorization gate (#isMutation(), shipped in PR #15419) to these two ambiguous paths: a mutation is not replayed on a >= 500 server error or an in-body 502/504; a 403 rate-limit and a 200-body rejection stay retryable. Alternatively, separate mutation execution from the read-retrying query() entirely so retry classification and retry authorization are distinct surfaces. The reviewer's own RETROSPECTIVE captures it: "a failure may be transient while replay is still unsafe."
Acceptance Criteria
Out of Scope
- The transport-catch path (fixed in PR #15419).
- REST-path retries (idempotent GETs).
Retrieval Hint: "GraphQL query 5xx gateway mutation replay idempotent retry authorization devindex GitHub"
Context
Surfaced during the #15359 / PR #15419 review — @neo-gpt's RC1 established that
DevIndex.services.GitHub.query()(apps/devindex/services/GitHub.mjs) must not replay a non-idempotent GraphQL mutation after an ambiguous transport outcome (the write may have applied before the failure). PR #15419 fixed exactly that at the transport-catch retry, gating mutations via a new#isMutation()retry-authorization check.The Problem
Two OTHER retry paths in the same
query()share the ambiguity but were out of PR #15419's shared-classification scope — they are a separate, pre-existing retry mechanism (old-style(4 - retries) * 2000delay, not the shared#isRetryableTransientErrorclassifier):GitHub.mjs~line 336): a>= 500server error leaves a mutation's server-side outcome ambiguous (it may have applied before the error), yet it is retried. A403is a pre-execution rate-limit rejection — safe to replay.GitHub.mjs~line 372): a gateway error is likewise ambiguous for a mutation.OptIn/OptOutcallquery()withaddComment+ issue-close mutations atretries: 3, so a 5xx / gateway blip mid-write can duplicate the comment or re-close the issue.The Fix (shape)
Extend the retry-authorization gate (
#isMutation(), shipped in PR #15419) to these two ambiguous paths: a mutation is not replayed on a>= 500server error or an in-body502/504; a403rate-limit and a 200-body rejection stay retryable. Alternatively, separate mutation execution from the read-retryingquery()entirely so retry classification and retry authorization are distinct surfaces. The reviewer's own RETROSPECTIVE captures it: "a failure may be transient while replay is still unsafe."Acceptance Criteria
>= 500server error (the:336path); a403rate-limit still retries.502/504gateway error (the:372path).query) still retries all of the above.Out of Scope
Retrieval Hint: "GraphQL query 5xx gateway mutation replay idempotent retry authorization devindex GitHub"