Context
Three PRs in the sync architecture each solve the same underlying problem — GitHub's issue.updatedAt does not bump for certain state transitions — with bespoke, one-off mechanisms:
- #7948 (Nov 2025) —
SubIssueAddedEvent, BlockedByAddedEvent, etc. relationship-event timeline scan inside pullFromGitHub.
- #10090 / PR #10091 (Apr 2026) —
timelineItems truncation; fixed by GraphQL pagination via pageInfo.hasNextPage inside #exhaustTimelineItems.
- #10092 / PR #10093 (Apr 2026) — comment deletions; fixed by a totals-sentinel batch query inside
detectStaleCommentsCounts.
Each has: (a) a detection primitive living in IssueSyncer as a bespoke method, (b) a direct call from runFullSync, (c) and the last two converged on a shared recovery primitive (refetchIssuesByNumber). A fourth class WILL emerge — milestone metadata, reaction counts, review-thread state on PRs are all candidate blind spots. Each future fix will add another bespoke branch to runFullSync unless this pattern is formalized.
The Ask
Introduce a pluggable InvalidationDetector interface. Each detector:
- Implements
detect(metadata): Promise<{stale: number[], seeded: number, errors: Array}>
- Is registered in a central array consumed by
runFullSync after the delta pull
- Feeds its
stale set into the shared refetchIssuesByNumber recovery primitive
runFullSync becomes:
const detectors = [RelationshipDetector, CommentTotalsDetector, ];
const allStale = new Set();
for (const detector of detectors) {
const result = await detector.detect(newMetadata);
result.stale.forEach(n => allStale.add(n));
mergeStats(detectorStats, result);
}
if (allStale.size > 0) await IssueSyncer.refetchIssuesByNumber([...allStale], newMetadata);Acceptance Criteria
Avoided Trap
Making TimelineTruncationDetector a first-class detector would be wrong. It's intrinsic to the fetch path (the pagination happens when we DO fetch, not as a sweep). Keep it inside #exhaustTimelineItems; the detector interface is for sweep-style deferred detection only.
Origin Session ID
d9eb5e76-5430-45f7-b3ea-8600664d28f9
Related: #10090 ( timelineItems pagination), #10092 (comment-totals sentinel), #7948 (relationship events).
Context
Three PRs in the sync architecture each solve the same underlying problem — GitHub's
issue.updatedAtdoes not bump for certain state transitions — with bespoke, one-off mechanisms:SubIssueAddedEvent,BlockedByAddedEvent, etc. relationship-event timeline scan insidepullFromGitHub.timelineItemstruncation; fixed by GraphQL pagination viapageInfo.hasNextPageinside#exhaustTimelineItems.detectStaleCommentsCounts.Each has: (a) a detection primitive living in
IssueSynceras a bespoke method, (b) a direct call fromrunFullSync, (c) and the last two converged on a shared recovery primitive (refetchIssuesByNumber). A fourth class WILL emerge — milestone metadata, reaction counts, review-thread state on PRs are all candidate blind spots. Each future fix will add another bespoke branch torunFullSyncunless this pattern is formalized.The Ask
Introduce a pluggable
InvalidationDetectorinterface. Each detector:detect(metadata): Promise<{stale: number[], seeded: number, errors: Array}>runFullSyncafter the delta pullstaleset into the sharedrefetchIssuesByNumberrecovery primitiverunFullSyncbecomes:const detectors = [RelationshipDetector, CommentTotalsDetector, /* future */]; const allStale = new Set(); for (const detector of detectors) { const result = await detector.detect(newMetadata); result.stale.forEach(n => allStale.add(n)); mergeStats(detectorStats, result); } if (allStale.size > 0) await IssueSyncer.refetchIssuesByNumber([...allStale], newMetadata);Acceptance Criteria
ai/mcp/server/github-workflow/services/sync/detectors/Base.mjs— abstract class withdetect(metadata)contractRelationshipDetector,CommentTotalsDetector(timelineItems pagination stays inside#exhaustTimelineItemssince it's intrinsic to the fetch path — not a sweep)runFullSynciterates a registered detector list instead of explicitly calling eachAvoided Trap
Making
TimelineTruncationDetectora first-class detector would be wrong. It's intrinsic to the fetch path (the pagination happens when we DO fetch, not as a sweep). Keep it inside#exhaustTimelineItems; the detector interface is for sweep-style deferred detection only.Origin Session ID
d9eb5e76-5430-45f7-b3ea-8600664d28f9Related: #10090 (
timelineItemspagination), #10092 (comment-totals sentinel), #7948 (relationship events).