LearnNewsExamplesServices
Frontmatter
id12186
titleEpic: gh-workflow syncer — sync-scope correctness + rate-limit/efficiency hardening
stateClosed
labels
epicaiarchitectureperformance
assignees[]
createdAtMay 30, 2026, 2:36 AM
updatedAtJun 6, 2026, 9:42 AM
githubUrlhttps://github.com/neomjs/neo/issues/12186
authorneo-opus-ada
commentsCount5
parentIssuenull
subIssues
12188 gh-workflow syncer: fetch full release history so closed-issue bucketing isn''t floored into v8.1.0
12184 gh-workflow syncer archives non-semver milestones as version folders
12190 gh-workflow syncer: PR + Discussion delta-fetch + Discussion query cost + shared rate-limit governance
12191 gh-workflow syncer: needsUpdate reads wrong field (always re-renders) + O(N²) #planBuckets in reconcile loop
12194 gh-workflow syncer: one-time re-bucket migration to clear the existing v8.1.0 pile-up (sealed-chunk-pinned)
subIssuesCompleted5
subIssuesTotal5
blockedBy[]
blocking[]
closedAtJun 6, 2026, 9:42 AM

Epic: gh-workflow syncer — sync-scope correctness + rate-limit/efficiency hardening

Closed v13.0.0/archive-v13-0-0-chunk-14 epicaiarchitectureperformance
neo-opus-ada
neo-opus-ada commented on May 30, 2026, 2:36 AM

Context

8-agent challenge+verify investigation (workflow run wgr9t499x, 2026-05-30) of the gh-workflow syncer, triggered by: the portal tickets view dumps thousands of old tickets into v8.1.0, and rising GitHub rate-limit hits. Operator hypothesis came back 3/4 confirmed, with the bucketing cause corrected, the rate-limit mechanism corrected, plus 2 bonus efficiency bugs.

Verdict (verified, high confidence — every load-bearing line re-read + disk re-counted)

Pile-up confirmed + quantified: resources/content/archive/issues/v8.1.0/ = 4,133 of 7,263 archived issues (56.9%, 10× the next bucket, contiguous #1–#6198) — a catch-all dump, not a real per-release distribution.

Root cause (corrected: the release floor, not "all tickets"): IssueSyncer.mjs:367-369 buckets a closed issue via (ReleaseNotesSyncer.sortedReleases||[]).find(r => new Date(r.publishedAt) > closed). sortedReleases is filtered to publishedAt >= syncStartDate (ReleaseNotesSyncer.mjs:156, value 2025-01-01) and sorted ascending (:157), so .find() returns the oldest in-window release (v8.1.0) for every pre-2025 closed issue. The all-tickets change (IssueSyncer.mjs:532 since: lastSync||'2017-01-01', commit ecadd27b4/#11451) only amplified the victim count.

Deeper bug — hidden-default fallback: that .find() silently defaults to the oldest fetched release when nothing post-dates closedAt — a confidently-wrong bucket (violates the "no hidden default fallbacks" contract). Fetching all releases alone only slides the wrong-bucket boundary; a fail-loud guard is required.

Rate-limit (corrected: not item count): GraphQL bills by nested-connection multipliers. Drivers: (A) missing deltaPullRequestSyncer + DiscussionSyncer full-scan the whole corpus every run (no since; PR ≈ 106 sequential pages = dominant load on the secondary req/min limit; pullRequestQueries.mjs:77 documents a $since that was never wired up); (B) expensive shape — Discussion comments(50)×replies(20) = 26 pts/page. Issues are correctly delta-gated (6/page). Rate-limit accounting exists only in the issue path (IssueSyncer.mjs:548-553).

Bonus efficiency bugs: (1) O(N²) reconcile — #planBuckets(metadata) called inside the all-issues loop (IssueSyncer.mjs:990/1007), loop-invariant, ~77M touches/run (worsened by #11405); (2) latent field bug — IssueSyncer.mjs:624 reads oldIssue.updated but the persisted field is updatedAt (MetadataManager.mjs:83) → always-true needsUpdate → every delta issue re-rendered.

The Fix (prioritized subs)

  • P0 — Fetch full release history for the archiving feed (ReleaseNotesSyncer.fetchAndCacheReleases:117-157): bypass the syncStartDate early-exit+filter so sortedReleases is complete. Mirror the precedent at IssueSyncer.mjs:528-532 (clean-slate issue sync overrides syncStartDate with 2017-01-01). Cost trivial (~24 GraphQL pages @ ~1pt for ~1194 releases). One-time re-sort of release-notes chunks = regeneratable-cache (ADR §1.3/§3.6), not a sealed-chunk violation (release-notes have no archive tier, §2.1).
  • P0 — Fail-loud guard on the closedAt→release .find() (IssueSyncer.mjs:367-375 + dups PullRequestSyncer.mjs:117, DiscussionSyncer.mjs:112): no post-closedAt release / incomplete list → leave active or loud WARN, never default to oldest.
  • P0 — Milestone semver-guard (#12184) — folds in; necessary-but-insufficient alone.
  • P1 — PR $since delta (pullRequestQueries.mjs:82-90 + PullRequestSyncer ~:205): wire the documented-but-missing $since/filterBy:{since} (mirror issueQueries.mjs:54). ~106 pages → ~1-2. Largest rate-limit win; independent of the bucketing work.
  • P1 — Discussion query cost (discussionQueries.mjs): split replies(first:20) into a continuation query (like issue timeline) or add a since gate. 26 → ~1 pt/page.
  • P2 — Efficiency: hoist #planBuckets(metadata) out of the reconcile loop (IssueSyncer.mjs:990); fix oldIssue.updatedupdatedAt (:624).
  • P3 — Rate-limit governance: lift rateLimit{cost remaining resetAt} + the <500-remaining guard into a shared GraphqlService wrapper so PR + Discussion are governed too.

Decision Record impact

aligned-with ADR 0004 — the archive is a regeneratable cache; a re-sync regenerates correct buckets. The P0 release-fetch re-sort is the §1.3/§3.6 regeneratable-cache model.

Acceptance Criteria (epic-level)

  • After the P0 trio + re-sync: archive/issues/ has no v8.1.0 catch-all dump (issues distribute to true historical releases) and no non-semver garbage version folders.
  • No hidden-default bucketing (fail-loud verified).
  • PR + Discussion syncs delta-gated; a typical incremental sync drops from ~106+ sequential pages to a handful.
  • Reconcile is not O(N²); unchanged delta issues are not re-rendered.

Related

  • #12184 (milestone semver-guard — sub; necessary-but-insufficient).
  • #11365 (milestone-aware routing — adjacent).
  • Authority: learn/agentos/decisions/0004-github-content-architecture.md.

Empirical anchors

  • Pile-up: v8.1.0 = 4133/7263 (56.9%); next buckets v12.0.0=413, v11.0.0=388. 569 of 5000 most-recent closed issues mis-bucketed to v8.1.0 (only 1/5000 uses a milestone).
  • Floor: syncStartDate='2025-01-01' (config.template.mjs:155); 166 releases fetched, ~1028 dropped (oldest v1.0.3-beta 2019-11-24); ~1194 total.
  • All-tickets unbounding: ecadd27b4 (#11451, ADR-0004 clean-slate, 2026-05-16).
  • Rate per-page: Discussion 26 / Issue 6 / PR 1; ~106 sequential PR pages/run.

Retrieval Hint: query_raw_memories("gh-syncer release floor v8.1.0 pileup closedAt fallback hidden-default rate-limit"); grep sortedReleases.find + syncStartDate in ai/services/github-workflow/sync/. Investigation: 8-agent workflow run wgr9t499x (2026-05-30).