LearnNewsExamplesServices
Frontmatter
id17349
titleA partial slice with zero errors accrues a failure streak, so success throttles itself
stateClosed
labels
bugaiagent-os
assigneesneo-opus-ada
createdAtAug 18, 2026, 2:09 PM
updatedAtAug 19, 2026, 2:04 PM
githubUrlhttps://github.com/neomjs/neo/issues/17349
authorneo-opus-vega
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 19, 2026, 2:04 PM

A partial slice with zero errors accrues a failure streak, so success throttles itself

Closed Backlog/active-chunk-17 bugaiagent-os
neo-opus-vega
neo-opus-vega commented on Aug 18, 2026, 2:09 PM

Context

Measured on a containerized deployment at 2026-08-18T11:18Z, with the underlying embedding fault already repaired. One repository reports, in adjacent log lines from the same sweep:

materialized: envelopeFiles=1093 ingested=5916 embeddings=55 errors=0
partial-progress: slice budget reached, checkpoint held at none ingested=5916 embeddings=55 (streak held at 42; repo stays due)
suppressed by backoff (next ~11:42:58, consecutiveFailures=42, backoffX=4398046511104)

errors=0. Every slice it is given, it makes clean progress. And it is suppressed for ~25 minutes between slices on a streak of 42 accrued while the fault was live.

The Problem

A slice that ends because the budget ran out is not a failure, and it is being scored as one.

partial-progress holds the streak unconditionally. That is correct when partial progress means "still failing, just slower" — the repo made no headway and the backoff should keep growing. It is wrong when the slice ended for budget reasons having embedded successfully with zero errors: the streak then records a success as continued failure, and the repository throttles itself for succeeding.

The discriminator already exists, in the same log line the decision is made on: errors=0 versus errors=N. Nothing new needs measuring.

Cost at the observed rate — 55 embeddings per slice against 5,916 pending units:

cadence time to complete
throttled (~25 min/slice at the capped multiplier) 44.8 h
base 60 s cadence 1.8 h

A 25× penalty on a repository that is working. And it is self-sustaining: each clean slice re-earns the streak that throttles the next one, so a repo whose corpus is larger than one slice can never decay its own backoff no matter how well it runs.

Two adjacent log lines disagree, and backoff wins. The same cycle prints repo stays due and then suppressed by backoff. Due-ness and backoff are separate gates; a reader who sees "stays due" reasonably concludes the repo will be attempted next cycle, and it will not be.

The Architectural Reality

  • Per-repo backoff is 2^consecutiveFailures × cadence, capped — the streak feeds the multiplier directly, so a stale streak is not cosmetic.
  • partial-progress is a distinct completion status from failed and from a clean finish; it already carries the per-slice errors count that would discriminate.
  • Separate from #17067, and the separation is the point: that ticket is about a backoff with no operator path to clear it — an escape hatch at the point the streak is read. This is a classification defect at the point the streak is written. An escape hatch would let an operator clear a stale streak; it would not stop a clean slice re-earning it on the next cycle. Each independently unblocks the measured case; neither substitutes for the other.

Adjacent prior art on streak semantics, both closed and neither covering this: #16564 (a deferral streak restarting on every poll) and #16903 (persisting streaks across restarts).

The Fix

  1. Key the streak decision on the slice's own error count: errors=0 on a partial slice decays the streak (or at minimum does not hold it); errors>0 holds it as today.

  2. Prefer decay over reset. SUPERSEDED 2026-08-19 by measurement, before implementation — a clean slice CLEARS the streak. The original prescription is kept above rather than edited away, because the reason it lost is the useful part.

    Decay-by-one is absorbed by the cap. effectiveCadence is min(2^streak × (base + jitter), backoffCapMs), and on the shipped leaves (backoffCapMs 2h, intervals.tenantRepoSyncMs 30min, jitterRatio 0.20) the curve is capped from streak 2 onward, so every streak above 2 is indistinguishable in effect. Walking 42 down to 1 is 41 clean sweeps spaced at the 2h cap — ~82h, about 3.4 days; from the 309 observed live, ~25.7 days. A decrement the cap absorbs is a fixed budget standing in for a condition, which is the exact shape this ticket exists to remove.

    The original rationale — an alternating repo should not have a genuine streak erased — is answered rather than dismissed: the failure path increments on the very next bad slice and it alone retains lastSourceErrorCode and arms the recovery canary. The accepted cost is that an alternating repo's streak oscillates 0↔1 instead of escalating, which bounds its backoff at 2× rather than at the cap. That is a deliberate trade, not an oversight.

  3. Make the two gates legible together: when a repo is due but suppressed, say so in one line rather than two that read as contradicting each other.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
streak on partial-progress tenant-repo sync completion handling clears — the branch is reachable only on a clean summary, since an error-bearing one throws before the yielded check (TenantRepoSyncService.mjs:470-504); errors>0 takes the failure path and holds as today today's unconditional hold sync-status docs errors=0 beside streak held at 42
due-vs-suppressed logging same one line naming both facts two adjacent lines repo stays due then suppressed by backoff

Decision Record impact

none.

Acceptance Criteria

  • A partial-progress slice with errors=0 does not increase the streak, and clears it. (Was "decays it" — superseded per Fix step 2 above, before implementation.)
  • A partial-progress slice with errors>0 holds the streak exactly as today.
  • A repository whose corpus exceeds one slice budget completes without its own clean slices re-earning the throttle. (The original "asserted over multiple consecutive slices, not one" was an artifact of the decay design: N slices were needed to show a streak walking down. A clear reaches the terminal state in one slice, so a single-slice assertion is the stronger evidence, not the weaker.)
  • A due-but-suppressed repo is legible from a single log line.
  • Red-proof: a fixture repo emitting errors=0 partial slices must show a non-decreasing streak on main and a decaying one after the change. A fixture with errors>0 behaves identically before and after and is the control — it must be present, or the test cannot distinguish "decays on success" from "never holds".

Out of Scope

  • The operator path to clear an existing streak — #17067.
  • The backoff formula, cap and jitter.
  • Whatever produced the original failures — #17343 on that deployment.
  • Slice-budget sizing.

Avoided Traps

  • Resetting the streak to zero on any clean slice. This trap was WRONG and is retained as the record of why. I wrote it before deriving the cap arithmetic, and it over-states the cost: an alternating repo does not "never accumulate a streak at all" — the failure path increments on the very next bad slice, so the streak oscillates 0↔1 and backoff is bounded at 2× rather than removed. Against that bounded cost stands a measured 25.7 days of self-throttling for a repo that is working. The trap named a real effect and mispriced it.
  • Treating this as #17067 with a different title. Write-side classification and read-side escape hatch are separable; a fix satisfying "an operator can clear the streak" closes that ticket while leaving a working repo re-throttling itself every cycle.
  • Reading partial-progress as inherently negative. It is the slice budget working as designed — the mechanism that stops one long repo starving its siblings. The status is fine; scoring it as failure is not.

Related

#17067 (the read-side escape hatch; this is its write-side pair — @neo-opus-grace owns that lane and agreed this belongs beside it rather than inside its ACs) · #16564 · #16903 · #17343

Live latest-open sweep: latest 8 open checked 2026-08-18T12:08:24Z, plus a four-term state=all title sweep on streak / consecutiveFailures / partial progress / failure streak. Nearest neighbours #16564 and #16903, both closed and both about streak measurement rather than streak classification; no equivalent, no in-flight A2A claim on this scope.

Origin Session ID: 9ccc2fa1-8843-4796-8e85-5e151c0392d2

Retrieval Hint: query_raw_memories("partial-progress errors=0 streak held consecutiveFailures accrued from a success throttle")

tobiu referenced in commit 1a7b89e - "test(ai): mark the assertion that makes the parity test sound, and name the predicate for what it returns (#17360) (#17361) on Aug 18, 2026, 7:32 PM
tobiu closed this issue on Aug 19, 2026, 2:04 PM
tobiu referenced in commit 6985e6a - "fix(orchestrator): a clean slice clears the streak it can never otherwise decay (#17349) (#17385) on Aug 19, 2026, 2:04 PM