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 delta — PullRequestSyncer + 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)
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)
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).
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-369buckets a closed issue via(ReleaseNotesSyncer.sortedReleases||[]).find(r => new Date(r.publishedAt) > closed).sortedReleasesis filtered topublishedAt >= syncStartDate(ReleaseNotesSyncer.mjs:156, value2025-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:532since: lastSync||'2017-01-01', commitecadd27b4/#11451) only amplified the victim count.Deeper bug — hidden-default fallback: that
.find()silently defaults to the oldest fetched release when nothing post-datesclosedAt— 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 delta —
PullRequestSyncer+DiscussionSyncerfull-scan the whole corpus every run (nosince; PR ≈ 106 sequential pages = dominant load on the secondary req/min limit;pullRequestQueries.mjs:77documents a$sincethat was never wired up); (B) expensive shape — Discussioncomments(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:624readsoldIssue.updatedbut the persisted field isupdatedAt(MetadataManager.mjs:83) → always-trueneedsUpdate→ every delta issue re-rendered.The Fix (prioritized subs)
ReleaseNotesSyncer.fetchAndCacheReleases:117-157): bypass thesyncStartDateearly-exit+filter sosortedReleasesis complete. Mirror the precedent atIssueSyncer.mjs:528-532(clean-slate issue sync overridessyncStartDatewith2017-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).closedAt→release.find()(IssueSyncer.mjs:367-375+ dupsPullRequestSyncer.mjs:117,DiscussionSyncer.mjs:112): no post-closedAtrelease / incomplete list → leave active or loud WARN, never default to oldest.$sincedelta (pullRequestQueries.mjs:82-90+PullRequestSyncer~:205): wire the documented-but-missing$since/filterBy:{since}(mirrorissueQueries.mjs:54). ~106 pages → ~1-2. Largest rate-limit win; independent of the bucketing work.discussionQueries.mjs): splitreplies(first:20)into a continuation query (like issue timeline) or add asincegate. 26 → ~1 pt/page.#planBuckets(metadata)out of the reconcile loop (IssueSyncer.mjs:990); fixoldIssue.updated→updatedAt(:624).rateLimit{cost remaining resetAt}+ the <500-remaining guard into a sharedGraphqlServicewrapper 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)
archive/issues/has no v8.1.0 catch-all dump (issues distribute to true historical releases) and no non-semver garbage version folders.Related
learn/agentos/decisions/0004-github-content-architecture.md.Empirical anchors
syncStartDate='2025-01-01'(config.template.mjs:155); 166 releases fetched, ~1028 dropped (oldest v1.0.3-beta 2019-11-24); ~1194 total.ecadd27b4(#11451, ADR-0004 clean-slate, 2026-05-16).Retrieval Hint:
query_raw_memories("gh-syncer release floor v8.1.0 pileup closedAt fallback hidden-default rate-limit"); grepsortedReleases.find+syncStartDateinai/services/github-workflow/sync/. Investigation: 8-agent workflow runwgr9t499x(2026-05-30).