LearnNewsExamplesServices
Frontmatter
id16890
titleA capped tenant-sync cadence is indistinguishable from a configured one
stateClosed
labels
bugaiagent-os
assigneesneo-opus-vega
createdAtAug 10, 2026, 3:39 PM
updatedAtAug 10, 2026, 8:47 PM
githubUrlhttps://github.com/neomjs/neo/issues/16890
authorneo-opus-vega
commentsCount0
parentIssue16566
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 10, 2026, 8:47 PM

A capped tenant-sync cadence is indistinguishable from a configured one

Closed Backlog/active-chunk-15 bugaiagent-os
neo-opus-vega
neo-opus-vega commented on Aug 10, 2026, 3:39 PM

Leaf of #16566, split so the epic's remaining criteria are not closed over by one PR (the #16776 pattern). Delivered by PR #16891.

Context

isRepoDue computes backoffCapped (ai/daemons/orchestrator/scheduling/tenantRepoSync.mjs:183) and returns it. The reported per-repo state drops it, so the only cadence number an operator can read is effectiveCadenceMs.

The Problem

effectiveCadenceMs: 7200000 has two meanings and no way to tell them apart:

  1. a repo configured with a 2h cadence — nothing wrong;
  2. a repo whose failure streak has driven the uncapped cadence far past the cap, so the cap is all that is left of it — badly wrong, and at consecutiveFailures: 12 the multiplier is 4096.

Those need opposite responses. #16566's parent context has the live shape: repos sitting at consecutiveFailures: 12 with the cadence pinned at the 2h cap, corpus frozen, while the reported state read like a deliberate configuration.

The Architectural Reality

Verified against origin/dev at 7ef07a7ee3:

  • isRepoDue returns {due, dueReason, recoveryBypass, effectiveCadenceMs, jitterMs, backoffMultiplier, backoffCapped, lastRunAttemptAt}.
  • There are TWO independent producers of a per-repo cadence row, and the operator-facing one is the bridge. DeploymentStateBridgeService.summarizeTenantRepoState (:2042) builds its rows from the persisted revisions plus isRepoDue, so it never touches the record runTask assembles. It publishes effectiveCadenceMs, jitterMs and backoffMultiplier — the row an operator and inspect_deployment actually read.
  • Seven repoStates.push sites inside TenantRepoSyncService, of which only the not-due/backoff-suppressed one publishes a cadence; the other six (revalidation-deferred, recovery-receipt-deferred, deferred, active, aborted-lease-lost, failedRepoState) carry a status and no numbers.
  • The "cadence-assembling path" the criterion warned about turned out to be the log line, which already prints backoffX=${dueState.backoffMultiplier} while both structured records carried nothing. So a human tailing logs could tell; a consumer reading either row could not.
  • backoffMultiplier beside the cadence is what makes this look already-solved and does not solve it: a multiplier of 4096 with a cadence at the cap and a multiplier of 1 with a cadence below it are the two readings, and distinguishing them requires the reader to recompute the uncapped product. An adjacent field that almost answers the question is worse than none, because it stops the search.

Corrected 2026-08-10 after @neo-gpt's RC on PR #16891. This section originally said "all six push sites traced" and treated the internal task record as the surface. Both were wrong: the census was seven, and the bridge is a second producer I never looked for because I enumerated producers inside one file instead of asking who projects the state outward.

The Fix

Publish backoffCapped on both cadence-publishing rows — the bridge's operator-facing repos[] row first, the internal task record second.

Disabled repos report null, not false. The bridge synthesizes a dueState for a disabled repo with every cadence field nulled; a repo with no cadence has no answer to "is the cap binding?", and false would read as an observation that it is not.

The magnitude the cap hides is deliberately NOT republished. consecutiveFailures is already on the record and the multiplier is 2^failures, so a consumer can derive it and falsify the arithmetic rather than inherit a number it cannot check — the same reasoning processHeapObservation uses for carrying raw spaces beside their sums. An earlier revision published uncappedCadenceMs and extended isRepoDue to return it; both were dropped as accretion once the derivation was obvious.

Acceptance Criteria

  • The OPERATOR-facing tenantRepoSync.repos[] row carries backoffCapped, and the test drives the real collectTenantRepoSyncSnapshot — not the internal task record, which is a different producer and would leave the ticket's own surface unrepaired.
  • The not-due/backoff-suppressed task record carries it too, as the secondary consumer.
  • A capped repo reports backoffCapped: true with effectiveCadenceMs equal to the cap, proven by a spec that fails against the current tree.
  • Negative control: an uncapped repo reports backoffCapped: false and a cadence below the cap — without it, hard-coding true passes.
  • No-answer control: a disabled repo reports null, matching its nulled cadence fields rather than asserting the cap is not binding.
  • Every arm mutation-convicted against the unpatched source, through the production consumer rather than terminating one layer short of it.
  • All seven push sites and both producers are checked and stated rather than assumed irrelevant.
  • schemaVersion disposition stated explicitly, since the row is a consumed contract.

Out of Scope

  • Publishing or recomputing the uncapped cadence, per above.
  • The remaining #16566 criteria: scheduling fairness, the failed-never-uninitialized reporting half, and the plane-named L3 proof.

Related

Leaf of #16566 · sibling of #16577 (same reporting surface, different defect).

Live latest-open sweep: latest 8 open issues created-descending at 2026-08-10T13:39:08Z; A2A claim sweep over the latest messages, all read-states, same minute. No equivalent and no competing claim.

Origin Session ID: 4131135d-1b20-487f-9d23-d7213914246b

tobiu referenced in commit 7be8583 - "fix(ai): a capped cadence says so, instead of reading as a misconfiguration (#16890) (#16891) on Aug 10, 2026, 8:47 PM
tobiu closed this issue on Aug 10, 2026, 8:47 PM