Data Sync is all-or-nothing: one facet''s failure discards every facet that already succeeded, so no run has made any progress since the corpus outgrew one query
[x] 16001 Discussion delta cutoff never engages — `updatedAt` is never persisted, so every run re-pages the entire history
closedAt
Jul 28, 2026, 8:45 PM
Data Sync is all-or-nothing: one facet's failure discards every facet that already succeeded, so no run has made any progress since the corpus outgrew one query
Operator-designated PRIO 0, and stated as the requirement this sync has to satisfy:
"if one sync can no longer do ALL tickets, pulls, discussions, do as much as possible and sync that. next run catches up more. next run catches up more, until we got it all. if a sync BREAKS, we get nothing. and every next run fails."
That is a precise description of current behaviour, not a hypothetical. #16001 (discussion query cost) and #15993 / PR #15999 (label-stage credential) are symptoms whose blast radius is set by this defect. Fixing them individually leaves the next single failure just as total.
The Problem
SyncService.emitGeneratedContentAndDerive is a strict sequential await chain with no error isolation between facets, and — per its own JSDoc step list — metadata is saved at step 11, after every facet has run.
step
work
outcome when step 6 throws
1
fetch + cache releases
done, then discarded
2 / 2b
reconcile closed issue + pull locations
done, then discarded
3
push local changes
done, then discarded
4
pull remote issues/tickets
done, then discarded
5
release notes
done, then discarded
6
discussions
throws
7 – 7c
pull requests, duplicate repair, index realign, integrity
never execute
11
save metadata to disk
never executes
Two independent losses compound:
No facet isolation. A discussions failure at step 6 means PullRequestSyncer.syncPullRequests (step 7) is never called. This is the mechanical reason pulls/ and discussions/ are both stale — one starves the other, and the two "unowned facets" blocking #15972 are one defect, not two.
No incremental persistence. Metadata is written once, at the end. Steps 1–5 genuinely succeeded and their high-water marks are thrown away with the exception.
And within a facet it is the same shape.DiscussionSyncer.syncDiscussions accumulates every page into an in-memory allDiscussions array (:401-441) and performs no writes inside the loop — all file and metadata work happens after it. So a failure on page N discards pages 1…N-1 as well.
Why this produces permanent zero progress rather than degraded progress
The current step-6 failure is deterministic: limit: 50 exceeds GitHub's per-query cost ceiling on every attempt (#16001, measured — 30 succeeds, 32 fails). A deterministic failure plus all-or-nothing persistence means:
every run does the same work,
fails at the same point,
writes nothing,
and the next run starts from the identical state.
No run has advanced the corpus since the discussion corpus crossed that ceiling. Not "slowly falling behind" — zero progress, indefinitely, while every individual facet's own code is working correctly.
The Architectural Reality — the tension this must resolve, not ignore
Step 7c is a deliberate all-or-nothing, and its comment argues the case well:
"Committing a corpus we have already measured as broken is worse than failing the run: the run can be retried, but a generated commit is what every consumer then reads as truth."
That reasoning is sound and must survive. It is not in conflict with the operator's requirement, because the two are about different units:
7c refuses to publish a facet known to be internally broken. Correct.
The operator requires that a facet which completed cleanly not be rolled back by a different facet's failure.
The resolution is facet-level atomicity: each facet advances its own high-water mark if and only if that facet completed consistently, and no facet's failure reverts another's success. "Partial progress" means fewer facets advanced this run, never a half-written facet published.
The Fix
Isolate each facet. Run facets so one throwing does not skip the others. Collect per-facet outcomes; the run's exit status reflects the aggregate, so a failure is still loud and still fails CI.
Persist per facet. Save metadata after each facet that completed cleanly, rather than once at the end. A facet that threw leaves its own high-water mark untouched.
Persist within a facet, per page. Write and advance the cursor per batch instead of accumulating the whole corpus in memory, so page N failing keeps pages 1…N-1.
Preserve 7c. Integrity verdicts still abort their own facet's publication. A facet measured broken does not advance.
Report the shortfall explicitly. A run that advanced 4 of 6 facets must say which two did not and why — a partial run that looks like a clean one is the silent-channel failure, and this ticket must not trade total loss for invisible loss.
Acceptance Criteria
AC1 — Facet isolation:(delivered — #16010 / PR #16011, 90403a8c13) an injected failure in the discussions facet leaves PullRequestSyncer.syncPullRequestscalled and its results persisted. Unit witness; RED against current SyncService ordering.
AC2 — Per-facet persistence:(delivered — #16010 / PR #16011) after that injected failure, on-disk metadata reflects the facets that succeeded, and the failed facet's high-water mark is unchanged. Assert both halves — advancement of the good, non-advancement of the bad.
AC3 — Within-facet resume: a discussions sync failing on page 3 persists pages 1–2 and advances the cursor such that the next run starts at page 3. Assert no gap and no duplicate.
AC4 — Convergence, which is the operator's actual requirement: across N successive runs against a corpus too large for one pass, coverage is monotonically non-decreasing and reaches complete. A test that runs the sync repeatedly against a fixture and asserts it converges — not merely that one run does not crash.
AC5 — The run still fails loudly:(delivered — #16010 / PR #16011) a partial run exits non-zero and names each facet that did not advance, with its reason. A partial success must never be reported as a success.
AC6 — 7c preserved:(delivered — #16010 / PR #16011) a facet failing its integrity verdict does not advance its high-water mark and does not publish, while other clean facets still do. Unit witness distinguishing "this facet is broken" from "another facet is broken".
[~] AC7 — Live evidence: RETIRED as unsatisfiable, 2026-07-26. As written this AC required a local syncGithubWorkflow advancing a starved facet "with #16001 still unfixed" — proving isolation independent of the delta-cutoff symptom. #16001 merged (b67e208115, 2026-07-26T22:27:49Z), so that precondition is permanently unreachable and no future evidence can satisfy the clause. Retiring rather than quietly reinterpreting it: an AC nobody can satisfy either blocks closure forever or licenses hand-waving, and restating it post-hoc to fit whatever evidence exists is the same defect wearing the opposite hat. What it was protecting is already covered: AC1/AC2/AC6 are unit witnesses that are mutation-discriminating against the isolation logic itself, with no dependence on the #16001 symptom (verified RED by reinstating each removed guard). The confounder AC7 existed to exclude was excluded by construction, not by timing.
Out of Scope
#16001 — the discussion query cost ceiling. The trigger, not this defect. Fixing it first would hide this one by removing today's only failure.
Ruleset 19087298 — the CI-side GH013 root cause of #15972; operator-held repo configuration.
Changing what the corpus stores, or any facet's fetch semantics.
Parallelising facets. Isolation is not concurrency, and mixing them would make the failure attribution harder.
Avoided Traps
Fixing the symptom and declaring victory.#16001 would make today's run pass and leave the next single failure just as total. This ticket exists to make the blast radius small, and it must be verified while a symptom is still live (AC7).
Reading step 7c as an obstacle. Its argument is correct; the fix keeps it and narrows its unit to the facet. Overturning it would trade a total-loss failure for a publish-broken-truth failure, which is worse.
Trading total loss for silent partial loss. A partial run that exits 0 and looks clean is a regression in a different direction — AC5 is not optional.
"Just wrap it in try/catch." Catching without per-facet persistence still discards steps 1–5, because the metadata write is at the end. Isolation and persistence are two changes, not one.
Assuming the current failure is transient. It is deterministic at limit: 50; a retry loop spends the budget repeatedly and converges on nothing.
Related
#15972 (the alarm; ruleset root cause + serial-cause analysis) · #16001 (discussion query cost — the current trigger) · #15993 / PR #15999 (label-stage credential) · #15977 (corpus generator scheduled / publisher unowned — the ownership half of the same starvation) · #15751 (blocked by this outage).
Live latest-open sweep at 2026-07-26T16:11Z across incremental resume sync, facet isolation, partial progress, resumable, emitGeneratedContentAndDerive, all-or-nothing: adjacent set is #16001, #15977, #15693, #15490; none covers facet isolation or incremental persistence in the sync chain. No duplicate.
Authored by Ada (@neo-opus-ada, Claude Opus 5, Claude Code).
tobiu referenced in commit 90403a8 - "fix(ai): sync facets persist independently instead of all-or-nothing (#16011) on Jul 26, 2026, 11:14 PM
tobiu unassigned from @neo-opus-ada on Jul 27, 2026, 12:24 AM
tobiu referenced in commit c45d5db - "Assert one artifact per logical name where the corpus is committed (#16067) on Jul 28, 2026, 12:54 AM
tobiu unassigned from @neo-opus-ada on Jul 28, 2026, 11:24 AM
Context
Operator-designated PRIO 0, and stated as the requirement this sync has to satisfy:
That is a precise description of current behaviour, not a hypothetical.
#16001(discussion query cost) and#15993/ PR#15999(label-stage credential) are symptoms whose blast radius is set by this defect. Fixing them individually leaves the next single failure just as total.The Problem
SyncService.emitGeneratedContentAndDeriveis a strict sequentialawaitchain with no error isolation between facets, and — per its own JSDoc step list — metadata is saved at step 11, after every facet has run.Two independent losses compound:
PullRequestSyncer.syncPullRequests(step 7) is never called. This is the mechanical reasonpulls/anddiscussions/are both stale — one starves the other, and the two "unowned facets" blocking#15972are one defect, not two.And within a facet it is the same shape.
DiscussionSyncer.syncDiscussionsaccumulates every page into an in-memoryallDiscussionsarray (:401-441) and performs no writes inside the loop — all file and metadata work happens after it. So a failure on page N discards pages 1…N-1 as well.Why this produces permanent zero progress rather than degraded progress
The current step-6 failure is deterministic:
limit: 50exceeds GitHub's per-query cost ceiling on every attempt (#16001, measured — 30 succeeds, 32 fails). A deterministic failure plus all-or-nothing persistence means:No run has advanced the corpus since the discussion corpus crossed that ceiling. Not "slowly falling behind" — zero progress, indefinitely, while every individual facet's own code is working correctly.
The Architectural Reality — the tension this must resolve, not ignore
Step 7c is a deliberate all-or-nothing, and its comment argues the case well:
That reasoning is sound and must survive. It is not in conflict with the operator's requirement, because the two are about different units:
The resolution is facet-level atomicity: each facet advances its own high-water mark if and only if that facet completed consistently, and no facet's failure reverts another's success. "Partial progress" means fewer facets advanced this run, never a half-written facet published.
The Fix
Acceptance Criteria
90403a8c13) an injected failure in the discussions facet leavesPullRequestSyncer.syncPullRequestscalled and its results persisted. Unit witness; RED against currentSyncServiceordering.syncGithubWorkflowadvancing a starved facet "with#16001still unfixed" — proving isolation independent of the delta-cutoff symptom.#16001merged (b67e208115, 2026-07-26T22:27:49Z), so that precondition is permanently unreachable and no future evidence can satisfy the clause. Retiring rather than quietly reinterpreting it: an AC nobody can satisfy either blocks closure forever or licenses hand-waving, and restating it post-hoc to fit whatever evidence exists is the same defect wearing the opposite hat. What it was protecting is already covered: AC1/AC2/AC6 are unit witnesses that are mutation-discriminating against the isolation logic itself, with no dependence on the#16001symptom (verified RED by reinstating each removed guard). The confounder AC7 existed to exclude was excluded by construction, not by timing.Out of Scope
#16001— the discussion query cost ceiling. The trigger, not this defect. Fixing it first would hide this one by removing today's only failure.#15993/ PR#15999— label-stage credential. Independent.19087298— the CI-sideGH013root cause of#15972; operator-held repo configuration.Avoided Traps
#16001would make today's run pass and leave the next single failure just as total. This ticket exists to make the blast radius small, and it must be verified while a symptom is still live (AC7).limit: 50; a retry loop spends the budget repeatedly and converges on nothing.Related
#15972(the alarm; ruleset root cause + serial-cause analysis) ·#16001(discussion query cost — the current trigger) ·#15993/ PR#15999(label-stage credential) ·#15977(corpus generator scheduled / publisher unowned — the ownership half of the same starvation) ·#15751(blocked by this outage).Live latest-open sweep at 2026-07-26T16:11Z across
incremental resume sync,facet isolation,partial progress,resumable,emitGeneratedContentAndDerive,all-or-nothing: adjacent set is#16001,#15977,#15693,#15490; none covers facet isolation or incremental persistence in the sync chain. No duplicate.Authored by Ada (@neo-opus-ada, Claude Opus 5, Claude Code).