Context
The scheduled Data Sync Pipeline on dev is currently failing before it can publish the refreshed DevIndex artifacts. Run 29541194846 aborted in Cleanup.reconcileRichUsersWithTracker() when getLoginByDatabaseId(5704244) reached GitHub REST and received 503 Service Unavailable.
The account lookup is valid: a fresh gh api user/5704244 resolves dregitsky. An earlier run, 29534893482, logged 267 REST 503 responses without retry before later failing on an unrelated generated-data rebase conflict. This is transient transport failure, not a deleted account.
Live latest-open sweep: checked the latest 20 open issues at 2026-07-17T00:11:17.148Z; no equivalent ticket found. The contemporaneous A2A claim sweep also found no competing DevIndex/GitHub REST retry lane.
The Problem
DevIndex.services.GitHub.rest() performs one fetch and throws every non-success response immediately. A single transient GitHub edge or proxy failure can therefore abort the complete Data Sync job, even when the same request would succeed moments later.
The caller must continue to fail closed after retry exhaustion: converting an unresolved identity lookup into null inside Cleanup would blur transient transport failure into account deletion and risk destructive reconciliation. The missing resilience belongs in the transport.
The DevIndex GitHub API guide currently describes retry behavior generically, although the implementation only retries GraphQL requests. It also uses a stale GraphQL database-ID example even though database-ID resolution is REST-owned.
The Architectural Reality
apps/devindex/services/GitHub.mjs:258-286 owns the authenticated REST transport and is the narrowest correct retry boundary.
apps/devindex/services/Cleanup.mjs:217 intentionally propagates unresolved identity failures; that identity-safety contract must remain unchanged.
ai/services/github-workflow/GraphqlService.mjs already provides the proven local policy shape: bounded retries for 429/502/503/504 and recognized network failures, Retry-After support, capped exponential backoff, and jitter.
test/playwright/unit/app/devindex/GitHubService.spec.mjs:57-63 proves exhausted transient failures propagate through getLoginByDatabaseId(), but does not exercise transport recovery.
- Retry policy values belong in documented, overrideable Neo
static config fields. They must not become uppercase hidden magic numbers.
The Fix
Extend DevIndex.services.GitHub.rest() with a bounded, config-driven transient retry policy:
- expose the attempt budget, base/max delay, jitter ratio, and retryable status set through documented
static config fields;
- retry
429, 502, 503, 504, and recognized fetch/network failures;
- honor both numeric and HTTP-date
Retry-After values, otherwise use capped exponential backoff with jitter;
- retain immediate failure for non-transient responses and retain failure after the retry budget is exhausted;
- add transport-level unit witnesses with zero-delay config overrides; and
- correct the DevIndex GitHub API guide so its REST and GraphQL claims match production.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
DevIndex.services.GitHub.rest() |
Current service transport plus GraphqlService.rest() precedent |
Retry only bounded transient HTTP/network failures |
Exhaustion rethrows; non-transient responses fail immediately |
GitHubAPI.md REST/retry sections |
503-to-success, network-error-to-success, exhaustion, and 4xx witnesses |
| DevIndex REST retry config |
Neo static config convention |
Operator-overridable attempt, delay, jitter, and status policy |
Safe documented defaults aligned with the proven sibling |
Service JSDoc plus guide |
Unit tests set delay/jitter to zero without patching module constants |
Cleanup.reconcileRichUsersWithTracker() |
Existing identity-safety behavior |
No behavioral change |
Transport exhaustion still aborts reconciliation |
No cleanup-doc change |
Existing propagation witness remains green |
Decision Record impact
None. This is a local transport-resilience repair aligned with existing service precedent; it does not amend an ADR or change an external API contract.
Acceptance Criteria
Out of Scope
- Changing
Cleanup fallback or identity semantics
- Adding workflow-level reruns
- Redesigning the GraphQL retry path
- Repairing generated-data rebase conflicts
- Changing quota-exhaustion policy
- Treating external GitHub availability as pre-merge proof
Avoided Traps
- Swallowing the 503 in
Cleanup: would misclassify an unknown identity as a deleted account.
- Retrying the whole workflow: repeats expensive completed work and leaves every REST caller brittle.
- Hidden module constants: prevents operator/hardware-specific overrides and violates Neo config conventions.
- Unbounded retries: converts an upstream outage into a wedged pipeline.
Related
Origin Session ID: b681a37a-4353-4ed0-bbf1-b46e6f2501c7
Retrieval Hint: DevIndex GitHub REST 503 retry Data Sync transport
Retrieval Hint: apps/devindex/services/GitHub.mjs rest Retry-After
Context
The scheduled Data Sync Pipeline on
devis currently failing before it can publish the refreshed DevIndex artifacts. Run 29541194846 aborted inCleanup.reconcileRichUsersWithTracker()whengetLoginByDatabaseId(5704244)reached GitHub REST and received503 Service Unavailable.The account lookup is valid: a fresh
gh api user/5704244resolvesdregitsky. An earlier run, 29534893482, logged 267 REST 503 responses without retry before later failing on an unrelated generated-data rebase conflict. This is transient transport failure, not a deleted account.Live latest-open sweep: checked the latest 20 open issues at 2026-07-17T00:11:17.148Z; no equivalent ticket found. The contemporaneous A2A claim sweep also found no competing DevIndex/GitHub REST retry lane.
The Problem
DevIndex.services.GitHub.rest()performs one fetch and throws every non-success response immediately. A single transient GitHub edge or proxy failure can therefore abort the complete Data Sync job, even when the same request would succeed moments later.The caller must continue to fail closed after retry exhaustion: converting an unresolved identity lookup into
nullinsideCleanupwould blur transient transport failure into account deletion and risk destructive reconciliation. The missing resilience belongs in the transport.The DevIndex GitHub API guide currently describes retry behavior generically, although the implementation only retries GraphQL requests. It also uses a stale GraphQL database-ID example even though database-ID resolution is REST-owned.
The Architectural Reality
apps/devindex/services/GitHub.mjs:258-286owns the authenticated REST transport and is the narrowest correct retry boundary.apps/devindex/services/Cleanup.mjs:217intentionally propagates unresolved identity failures; that identity-safety contract must remain unchanged.ai/services/github-workflow/GraphqlService.mjsalready provides the proven local policy shape: bounded retries for429/502/503/504and recognized network failures,Retry-Aftersupport, capped exponential backoff, and jitter.test/playwright/unit/app/devindex/GitHubService.spec.mjs:57-63proves exhausted transient failures propagate throughgetLoginByDatabaseId(), but does not exercise transport recovery.static configfields. They must not become uppercase hidden magic numbers.The Fix
Extend
DevIndex.services.GitHub.rest()with a bounded, config-driven transient retry policy:static configfields;429,502,503,504, and recognized fetch/network failures;Retry-Aftervalues, otherwise use capped exponential backoff with jitter;Contract Ledger Matrix
DevIndex.services.GitHub.rest()GraphqlService.rest()precedentGitHubAPI.mdREST/retry sectionsstatic configconventionCleanup.reconcileRichUsersWithTracker()Decision Record impact
None. This is a local transport-resilience repair aligned with existing service precedent; it does not amend an ADR or change an external API contract.
Acceptance Criteria
GitHub.rest()retries configured transient statuses including429,502,503, and504.Retry-Aftervalues are honored; absent headers use capped exponential backoff with jitter.404and other non-transient 4xx responses remain fail-fast.Cleanupidentity safety.learn/guides/devindex/data-factory/GitHubAPI.mdaccurately distinguishes GraphQL and REST retry behavior and replaces the stale database-ID example.Out of Scope
Cleanupfallback or identity semanticsAvoided Traps
Cleanup: would misclassify an unknown identity as a deleted account.Related
Origin Session ID: b681a37a-4353-4ed0-bbf1-b46e6f2501c7
Retrieval Hint:
DevIndex GitHub REST 503 retry Data Sync transportRetrieval Hint:apps/devindex/services/GitHub.mjs rest Retry-After