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
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.
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.
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
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")
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:
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-progressholds 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=0versuserrors=N. Nothing new needs measuring.Cost at the observed rate — 55 embeddings per slice against 5,916 pending units:
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 dueand thensuppressed 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
2^consecutiveFailures × cadence, capped — the streak feeds the multiplier directly, so a stale streak is not cosmetic.partial-progressis a distinct completion status fromfailedand from a clean finish; it already carries the per-sliceerrorscount that would discriminate.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
Key the streak decision on the slice's own error count:
errors=0on a partial slice decays the streak (or at minimum does not hold it);errors>0holds it as today.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.
effectiveCadenceismin(2^streak × (base + jitter), backoffCapMs), and on the shipped leaves (backoffCapMs2h,intervals.tenantRepoSyncMs30min,jitterRatio0.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
lastSourceErrorCodeand 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.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
partial-progressyieldedcheck (TenantRepoSyncService.mjs:470-504);errors>0takes the failure path and holds as todayerrors=0besidestreak held at 42repo stays duethensuppressed by backoffDecision Record impact
none.Acceptance Criteria
partial-progressslice witherrors=0does not increase the streak, and clears it. (Was "decays it" — superseded per Fix step 2 above, before implementation.)partial-progressslice witherrors>0holds the streak exactly as today.errors=0partial slices must show a non-decreasing streak onmainand a decaying one after the change. A fixture witherrors>0behaves 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
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.partial-progressas 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=alltitle sweep onstreak/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")