Context
The operator reported that sync_all became extremely slow and appeared to fetch the full release-note history every run. After a machine restart the original logs were gone, but the remembered symptom was that cached releases were not identified and the log showed an undefined cached value.
Verify-before-assert evidence from this session confirms that symptom:
resources/content/.sync-metadata.json is warm and currently contains 1194 cached releases with releasesLastFetched: 2026-06-07T17:26:07.535Z.
- The latest cached entry is keyed by
12.1.0, but its persisted value only has publishedAt and contentHash; it does not have tagName.
ai/services/github-workflow/sync/ReleaseNotesSyncer.mjs builds the warm-cache array from Object.values(cachedReleases), then compares latestRelease.tagName === cachedLatest.tagName.
ai/services/github-workflow/sync/MetadataManager.mjs intentionally prunes release metadata to { publishedAt, contentHash }, so the warm-cache comparison sees cachedLatest.tagName === undefined for persisted metadata.
- Existing warm-cache coverage in
test/playwright/unit/ai/services/github-workflow/ReleaseNotesSyncer.spec.mjs uses fixtures where each cached release value still includes tagName, so it does not exercise the real persisted shape.
Live latest-open sweep: checked the latest 20 open GitHub issues on 2026-06-07 and found no equivalent ticket. Exact open searches for ReleaseNotesSyncer tagName undefined full release pagination and sync_all cached releases undefined returned no results. Knowledge Base and Memory Core searches for ReleaseNotesSyncer warm cache, sync_all slow release notes fetch every run, and release metadata warm cache regression found no prior session that already mapped this specific persisted-metadata bug.
The Problem
sync_all has a release-cache fast path intended to avoid fetching the full release history when the latest GitHub release matches the latest cached release. The persisted metadata shape prevents that fast path from recognizing a warm cache:
MetadataManager.save() prunes release objects to keep resources/content/.sync-metadata.json small.
- The release tag survives only as the map key, not as
value.tagName.
ReleaseNotesSyncer.fetchAndCacheReleases() discards the key by using Object.values().
- The latest-release comparison fails because the cached value's
tagName is undefined.
- The syncer falls back to full release pagination across roughly 1200 releases.
There is a second trap: if the implementation only restores tagName, the warm path can proceed into syncNotes(), where pruned cached releases still lack name and description. That must not regenerate corrupt markdown like # undefined or rewrite release notes from incomplete metadata.
The Architectural Reality
ai/services/github-workflow/sync/ReleaseNotesSyncer.mjs#fetchAndCacheReleases owns the release-history fetch and warm-cache fast path.
ai/services/github-workflow/sync/ReleaseNotesSyncer.mjs#syncNotes owns release-note markdown/index generation and currently expects full release bodies when iterating this.releases.
ai/services/github-workflow/sync/MetadataManager.mjs#save is the source of authority for the persisted sync metadata shape and intentionally prunes releases to publishedAt plus contentHash.
resources/content/.sync-metadata.json demonstrates the production shape: release tag in the object key, not in the release value.
test/playwright/unit/ai/services/github-workflow/ReleaseNotesSyncer.spec.mjs has the relevant warm-cache test but currently fixtures the pre-pruned shape.
The Fix
Preserve the pruned metadata contract, and make ReleaseNotesSyncer hydrate cached release identity from the release map key:
- In
fetchAndCacheReleases(metadata), build the cached release list from Object.entries(metadata.releases || {}) so each hydrated cached item gets tagName from the key plus publishedAt and contentHash from the value.
- Compare the latest remote release against that hydrated cached latest entry.
- Populate
sortedReleases from hydrated cached entries so downstream closed-item bucketing receives valid { tagName, publishedAt } pairs without full pagination.
- Ensure
syncNotes(metadata) does not regenerate or overwrite markdown for pruned cached release entries that lack enough body data. Unchanged cached entries should be skipped from content generation while keeping their index metadata valid.
- Update unit coverage to use the persisted/pruned metadata shape, not the pre-pruned in-memory shape.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
MetadataManager.save() release metadata |
ai/services/github-workflow/sync/MetadataManager.mjs |
Continue persisting release values as { publishedAt, contentHash }; do not bloat metadata with descriptions |
If pruning changes, this ticket must be re-scoped because metadata-size behavior changes |
Existing JSDoc plus test fixture comments |
Unit test verifies warm cache from pruned metadata |
ReleaseNotesSyncer.fetchAndCacheReleases(metadata) |
ai/services/github-workflow/sync/ReleaseNotesSyncer.mjs |
Hydrate tagName from cached release map keys and take the one-query warm path when latest release matches |
On mismatch or latest-query failure, retain existing full-history fetch |
Inline comments should explain key hydration |
Unit test asserts one latest-release query and no full pagination |
ReleaseNotesSyncer.sortedReleases |
ReleaseNotesSyncer cache contract consumed by issue/PR/discussion release bucketing |
Warm-cache path provides valid tagName and publishedAt for all cached releases |
Full fetch continues to populate from GitHub nodes |
Existing comments about full-history bucketing remain valid |
Unit test asserts no undefined tagName entries |
ReleaseNotesSyncer.syncNotes(metadata) |
ai/services/github-workflow/sync/ReleaseNotesSyncer.mjs |
Do not rewrite release-note markdown from pruned cached entries missing name or description; skip unchanged cached entries safely |
Full fetched release bodies still generate markdown as before |
Add a short comment at the guard if needed |
Unit test proves no corrupt # undefined write on pruned warm cache |
Decision Record Impact
none.
Acceptance Criteria
Out of Scope
- Lowering
maxReleases or reducing the full-history requirement. Full history is still needed for release bucketing when the cache is cold or stale.
- Persisting all release descriptions in
.sync-metadata.json.
- Reworking release-note archive bucketing.
- Creating the release-workflow or release-notes skill. That remains adjacent to #10321.
- Multi-body grid selection-model architecture.
Avoided Traps
- Do not solve this by persisting full release bodies in metadata; that trades the slow fetch for persistent metadata bloat.
- Do not lower the release cap as a performance workaround; that risks reintroducing bad closed-item bucketing for old issues and PRs.
- Do not trust the existing warm-cache test as sufficient; it uses a shape that
MetadataManager.save() does not persist.
- Do not only hydrate
tagName and ignore syncNotes(); that can turn a performance fix into release-note corruption.
Related
Origin Session ID
Origin Session ID: e8f07ef9-ef7e-4815-8ff4-7abe13720621
Handoff Retrieval Hints
query_raw_memories(query="sync_all ReleaseNotesSyncer cached releases undefined tagName full release pagination")
query_summaries(query="release metadata warm cache regression")
- Source anchors:
ai/services/github-workflow/sync/ReleaseNotesSyncer.mjs, ai/services/github-workflow/sync/MetadataManager.mjs, resources/content/.sync-metadata.json, test/playwright/unit/ai/services/github-workflow/ReleaseNotesSyncer.spec.mjs.
Context
The operator reported that
sync_allbecame extremely slow and appeared to fetch the full release-note history every run. After a machine restart the original logs were gone, but the remembered symptom was that cached releases were not identified and the log showed an undefined cached value.Verify-before-assert evidence from this session confirms that symptom:
resources/content/.sync-metadata.jsonis warm and currently contains 1194 cached releases withreleasesLastFetched: 2026-06-07T17:26:07.535Z.12.1.0, but its persisted value only haspublishedAtandcontentHash; it does not havetagName.ai/services/github-workflow/sync/ReleaseNotesSyncer.mjsbuilds the warm-cache array fromObject.values(cachedReleases), then compareslatestRelease.tagName === cachedLatest.tagName.ai/services/github-workflow/sync/MetadataManager.mjsintentionally prunes release metadata to{ publishedAt, contentHash }, so the warm-cache comparison seescachedLatest.tagName === undefinedfor persisted metadata.test/playwright/unit/ai/services/github-workflow/ReleaseNotesSyncer.spec.mjsuses fixtures where each cached release value still includestagName, so it does not exercise the real persisted shape.Live latest-open sweep: checked the latest 20 open GitHub issues on 2026-06-07 and found no equivalent ticket. Exact open searches for
ReleaseNotesSyncer tagName undefined full release paginationandsync_all cached releases undefinedreturned no results. Knowledge Base and Memory Core searches forReleaseNotesSyncer warm cache,sync_all slow release notes fetch every run, andrelease metadata warm cache regressionfound no prior session that already mapped this specific persisted-metadata bug.The Problem
sync_allhas a release-cache fast path intended to avoid fetching the full release history when the latest GitHub release matches the latest cached release. The persisted metadata shape prevents that fast path from recognizing a warm cache:MetadataManager.save()prunes release objects to keepresources/content/.sync-metadata.jsonsmall.value.tagName.ReleaseNotesSyncer.fetchAndCacheReleases()discards the key by usingObject.values().tagNameis undefined.There is a second trap: if the implementation only restores
tagName, the warm path can proceed intosyncNotes(), where pruned cached releases still lacknameanddescription. That must not regenerate corrupt markdown like# undefinedor rewrite release notes from incomplete metadata.The Architectural Reality
ai/services/github-workflow/sync/ReleaseNotesSyncer.mjs#fetchAndCacheReleasesowns the release-history fetch and warm-cache fast path.ai/services/github-workflow/sync/ReleaseNotesSyncer.mjs#syncNotesowns release-note markdown/index generation and currently expects full release bodies when iteratingthis.releases.ai/services/github-workflow/sync/MetadataManager.mjs#saveis the source of authority for the persisted sync metadata shape and intentionally prunes releases topublishedAtpluscontentHash.resources/content/.sync-metadata.jsondemonstrates the production shape: release tag in the object key, not in the release value.test/playwright/unit/ai/services/github-workflow/ReleaseNotesSyncer.spec.mjshas the relevant warm-cache test but currently fixtures the pre-pruned shape.The Fix
Preserve the pruned metadata contract, and make
ReleaseNotesSyncerhydrate cached release identity from the release map key:fetchAndCacheReleases(metadata), build the cached release list fromObject.entries(metadata.releases || {})so each hydrated cached item getstagNamefrom the key pluspublishedAtandcontentHashfrom the value.sortedReleasesfrom hydrated cached entries so downstream closed-item bucketing receives valid{ tagName, publishedAt }pairs without full pagination.syncNotes(metadata)does not regenerate or overwrite markdown for pruned cached release entries that lack enough body data. Unchanged cached entries should be skipped from content generation while keeping their index metadata valid.Contract Ledger Matrix
MetadataManager.save()release metadataai/services/github-workflow/sync/MetadataManager.mjs{ publishedAt, contentHash }; do not bloat metadata with descriptionsReleaseNotesSyncer.fetchAndCacheReleases(metadata)ai/services/github-workflow/sync/ReleaseNotesSyncer.mjstagNamefrom cached release map keys and take the one-query warm path when latest release matchesReleaseNotesSyncer.sortedReleasesReleaseNotesSyncercache contract consumed by issue/PR/discussion release bucketingtagNameandpublishedAtfor all cached releasestagNameentriesReleaseNotesSyncer.syncNotes(metadata)ai/services/github-workflow/sync/ReleaseNotesSyncer.mjsnameordescription; skip unchanged cached entries safely# undefinedwrite on pruned warm cacheDecision Record Impact
none.
Acceptance Criteria
publishedAt.FETCH_RELEASESpagination when unchanged.sortedReleasescontains validtagNameandpublishedAtentries on the warm path; no cached release hastagName: undefined.syncNotes()does not regenerate or overwrite release-note markdown from pruned cached release entries that lack full body fields; no# undefinedrelease note can be produced by the warm path.sync_allno-op validation shows releases are up to date and avoids the full release-history fetch.Out of Scope
maxReleasesor reducing the full-history requirement. Full history is still needed for release bucketing when the cache is cold or stale..sync-metadata.json.Avoided Traps
MetadataManager.save()does not persist.tagNameand ignoresyncNotes(); that can turn a performance fix into release-note corruption.Related
Origin Session ID
Origin Session ID: e8f07ef9-ef7e-4815-8ff4-7abe13720621
Handoff Retrieval Hints
query_raw_memories(query="sync_all ReleaseNotesSyncer cached releases undefined tagName full release pagination")query_summaries(query="release metadata warm cache regression")ai/services/github-workflow/sync/ReleaseNotesSyncer.mjs,ai/services/github-workflow/sync/MetadataManager.mjs,resources/content/.sync-metadata.json,test/playwright/unit/ai/services/github-workflow/ReleaseNotesSyncer.spec.mjs.