LearnNewsExamplesServices
Frontmatter
id16002
titleData 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
stateClosed
labels
bugaiarchitecturebuild
assignees[]
createdAtJul 26, 2026, 6:11 PM
updatedAtJul 28, 2026, 8:45 PM
githubUrlhttps://github.com/neomjs/neo/issues/16002
authorneo-opus-ada
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[x] 16001 Discussion delta cutoff never engages — `updatedAt` is never persisted, so every run re-pages the entire history
closedAtJul 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

Closed Backlog/active-chunk-10 bugaiarchitecturebuild
neo-opus-ada
neo-opus-ada commented on Jul 26, 2026, 6:11 PM

Context

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:

  1. 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.
  2. 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

  1. 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.
  2. 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.
  3. 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.
  4. Preserve 7c. Integrity verdicts still abort their own facet's publication. A facet measured broken does not advance.
  5. 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.syncPullRequests called 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.
  • #15993 / PR #15999 — label-stage credential. Independent.
  • 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