Skip to content

BLO-28863: add routine-period-aware retry clamp primitive - #1434

Merged
allyblockcast[bot] merged 3 commits into
masterfrom
blo-28863-routine-scoped-retry-clamp
Aug 25, 2026
Merged

BLO-28863: add routine-period-aware retry clamp primitive#1434
allyblockcast[bot] merged 3 commits into
masterfrom
blo-28863-routine-scoped-retry-clamp

Conversation

@allyblockcast

@allyblockcast allyblockcast Bot commented Aug 19, 2026

Copy link
Copy Markdown

Issue: https://paperclip.blockcast.net/BLO/issues/BLO-28863

Thinking Path

  • Paperclip is the open source app people use to manage AI agents for work
  • Agent work is dispatched by the heartbeat scheduler; when a run fails transiently, scheduleBoundedRetryForRun parks it with a scheduledRetryAt due time
  • Some of that work is owned by periodic routines that serve a bounded window — the 6-hourly agent-health sweep is the fleet's budget-alarm and stalled-issue detector
  • Retry backoff is period-unaware: measured 2026-08-19, 6 of 41 scheduled retries drew a backoff longer than the 6h period of the routine owning the issue, the worst being 23h59m43s on attempt 1
  • A retry that can only wake after its window closes cannot do the window's work, yet still consumes one of the agent's 3 concurrent run slots and one attempt — so it is worth less than no retry at all
  • This pull request adds resolveRoutineScopedRetry, a pure decision function that bounds a retry due time against the owning routine's period, against the tighter deadline of the specific window being served, and against a lead time sized from how late this fleet actually dispatches
  • The benefit is that a windowed routine stops silently losing windows to backoff it cannot survive; the wiring that calls this on the dispatch path is deliberately split out (see "What is deliberately NOT in this PR")

Linked Issues or Issue Description

Related PRs found by the dedup search below (none duplicate this change):

PR state relationship
#1441 OPEN Edits the same file (ccrotate-capacity-retry.ts) and its suite — capacity-park floor ceiling (BLO-28919). Adjacent, not overlapping: that PR touches the capacity floor, this one appends new period-aware code at end-of-file. Land order matters for conflicts; no logical dependency.
#1225 MERGED Introduced clampTransientRetryHorizon (BLO-23438) — the flat 24h ceiling this PR layers a tighter bound on top of, and does not replace.
#1324 MERGED Raised the clamped-floor attempt ceiling for transient_upstream (BLO-23525).

What Changed

Revision 2 (2026-08-22) applies the CTO ruling in comment 5377883876, which resolved the blocking clamp/abandon contradiction Ally raised. Details of the four changes are in the appendix; the summary:

  • Lead time — abandon is now reachable. The decision is
    target = min(failedAt + routinePeriodMs, windowClosesAt) - MIN_USEFUL_RETRY_MARGIN_MS,
    abandoning when target <= failedAt and clamping to target otherwise. The previous cut
    returned the deadline instant itself, which made abandon unreachable for every failure
    inside an open window — Ally's 360/360 reproduction.
  • MIN_USEFUL_RETRY_MARGIN_MS is derived, not picked. DISPATCH_LATENESS_P95_MS (71m) + MEDIAN_HEARTBEAT_RUN_DURATION_MS (7m41s) = 78m41s, both from the 2026-08-19 measurement,
    each exported and commented with its provenance so the total cannot be widened without
    re-deriving it. This follows the TRANSIENT_HORIZON_CLAMP_MIN_ATTEMPTS precedent in the
    same file, and a test asserts the identity.
  • Fails closed to abandon on non-finite input, covering routinePeriodMs (including
    NaN, Infinity, and non-positive), dueAt, failedAt, and windowClosesAt.
  • abandon reports rejectedDueAtIso, not clampedFromIso, so a uniform logger cannot
    record a clamp that never happened.
  • Suite rebuilt: 6 tests → 13, including the two Ally identified as vacuous or
    wrong-way-round, and the case she asked for that fails on the previous code.
  • Still additive only: resolveRoutineScopedRetry has no callers (verified by grep across
    the repo), and clampTransientRetryHorizon is untouched.

Verification

Run on head 8db2d565:

  • Target suites — vitest run src/__tests__/routine-scoped-retry.test.ts src/__tests__/ccrotate-capacity-retry.test.ts2 files passed, 25 tests passed (12 new + the 13 existing ccrotate-capacity-retry tests the acceptance criteria names, all still green).
  • Verifying signal fix(test): restore upstream agent-permissions expectations dropped during v513 merge #2, in full — adding heartbeat-retry-scheduling.test.ts and
    heartbeat-ccrotate-capacity-retry.test.ts4 files passed, 110 tests passed.
  • tsc --noEmit (server project, repo-pinned TypeScript) → exit 0, 0 errors.
  • This repo has no ESLint/Prettier config; typecheck and test are the gates.
  • Discriminating check, run explicitly against 1d491e35: the new test "abandons a failure
    late inside a still-open window"
    returns clamp (to 06:00:00.000Z) under the previous code,
    because marginMs = 06:00Z − 05:00Z = 60 min > 0 so the old guard never fired, and abandon
    under this one. That is the assertion Ally asked for, and it fails on the previous head.
  • Ally's reproduction re-run rather than inherited. Over a 60-minute sweep of an open window
    the previous code clamps 59/60 to exactly the close; the one abandon is the boundary case
    where failedAt coincides with the close. Ally's 360/360 figure was over a strictly-inside
    sweep and is correct as stated — the two agree. A test now pins the whole sweep to abandon.
  • The decision table was also computed independently of vitest before the suite was written, so
    the expected instants in the tests are not transcribed from the implementation's own output.
  • Honest scope statement, unchanged: the end-to-end assertion that a persisted
    scheduledRetryAt respects the period still cannot be made until the wiring lands. That is
    BLO-29052's verifying signal test(plugin-linear): requestId fixtures + getLinkByLinear mock-leak fix; scripts: ensure-build-deps freshness check #1, not this PR's.

Risks

  • Low risk as merged, by construction: the function has no callers, so this PR cannot change
    fleet dispatch behaviour. The behavioural risk lives entirely in the wiring PR (BLO-29052),
    which touches retry scheduling for all 13 agents and is explicitly gated on CTO review.
  • The clamp branch is mostly an abandon-machine until defect 1 lands, and that is intended.
    At a 78m41s lead, a 6h routine failing in the last ~79 min of a window always abandons. The
    next scheduled fire owns that work. This is still strictly better than today, where the same
    retry is scheduled, strands, and burns a run slot and an attempt on the way — but it is the
    reason BLO-29052's verifying signal v513 test-fallout cleanup batch 2: codex-local SSH dispatch + company-portability mock/expectations #3 was amended to require the abandon count and the
    retry-creation rate alongside the backoff p95. A falling p95 alone is equally consistent with
    the fix working and with retries being silently dropped.
  • A clamp can land before a provider-advertised floor. dueAt is often
    penstockAdvertisedResumeAt, and clamping earlier means the re-probe may find capacity still
    out. This is the same trade clampTransientRetryHorizon already makes and documents ("clamping
    only shortens a wait … the worst case is an early re-probe that defers again"), and it
    converges rather than looping: each re-probe re-enters the decision against a window with less
    margin left, so the sequence terminates in abandon rather than in a strand. Documented at the
    function.
  • The margin is sized from a censored measurement and will need re-tuning. See the appendix —
    the two available statistics disagree by 29 minutes and the conservative one was chosen
    deliberately. Both inputs are exported so the next reader can re-measure rather than guess.
  • Merge-order conflict risk with fix(heartbeat): give the capacity retry floor one ceiling, not two (BLO-28919) #1441, which edits the same file. Both are localized; this
    change appends at end-of-file while fix(heartbeat): give the capacity retry floor one ceiling, not two (BLO-28919) #1441 edits the capacity floor above it.

Model Used

Claude Opus 5 (claude-opus-5[1m], 1M context), extended thinking, with tool use and code execution, via the Paperclip claude_k8s adapter.

Checklist

  • I have included a thinking path that traces from project context to this change
  • I have specified the model used (with version and capability details)
  • I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work
  • I searched the GitHub PR list (open and closed) for similar or duplicate PRs and linked the related ones above
  • I have either (a) linked existing issues with Fixes: # / Closes # / Refs # OR (b) described the issue in-PR following the relevant issue template
  • I have run tests locally and they pass
  • I have added or updated tests where applicable
  • If this change affects the UI, I have included before/after screenshots — n/a, server-only
  • I have considered and documented any risks above

Appendix — how MIN_USEFUL_RETRY_MARGIN_MS was sized

The ruling asked for the lead to be derived from the live dispatch-lateness p95 rather than
hand-picked, and cited "today's p95 (>70 min) … roughly 75–90 min". Reconciling that against the
BLO-28863 findings document turned up two different statistics that disagree by 29 minutes,
so which one is used matters and is recorded in the code:

statistic value what it measures
p95 of startedAt − scheduledRetryAt, rows with both (n=197) 42m04s (max 2h39m32s) realized lateness of retries that did dispatch
overdueMs of still-parked overdue rows (40/40 overdue, retryInMs: 0) 55–71 min lateness-so-far of retries that had not dispatched

The first is survivor-biased downward: it can only measure retries that eventually
dispatched, and it excludes every row still stuck in the queue precisely because those are the
latest. The second is right-censored — those rows had already waited 55–71 min and had not
started, so their eventual lateness is a lower bound, not a value. Sizing the margin from the
survivor p95 (42m) would under-size it against the exact failure mode this change exists to
prevent, so the top of the censored band is used.

A retry also has to finish the window's work, not merely start, so the margin carries one
median run on top of the lateness it must survive:

MIN_USEFUL_RETRY_MARGIN_MS = DISPATCH_LATENESS_P95_MS (71m) + MEDIAN_HEARTBEAT_RUN_DURATION_MS (7m41s)
                           = 78m41s

Consistency check on the formula, not just the number: substituting defect 1's acceptance
criterion (p95 ≤ 5 min) yields 5m + 7m41s = 12m40s — inside the 10–15 min the ruling
independently predicted for the post-fix value. The formula reproduces a figure it was not fitted
to, which is the reason to prefer it over picking 80 min directly.

Appendix — the four ruled changes, and one further finding

  1. Lead time. Implemented as ruled. honour now requires dueAt <= target rather than
    dueAt <= deadline, which is what subsumes non-blocking finding v513 test-fallout cleanup batch 2: codex-local SSH dispatch + company-portability mock/expectations #3: a dueAt landing exactly
    on windowClosesAt no longer slips through honour with clampedFromIso: null, so the 0 ms
    strand leaves a trace instead of vanishing. Test: "leaves a trace when the requested due time
    is exactly the window close"
    .
  2. Non-finite guard. Fails closed to abandon with a distinct reason. One thing worth
    flagging for the reviewer: the guard cannot report the rejected instant unconditionally,
    because new Date(NaN).toISOString() throws RangeError — so an unguarded "just log what we
    rejected" abandon path would have converted a bad input into a thrown exception on the
    dispatch path. rejectedDueAtIso is therefore string | null, and a test pins the null
    case.
  3. Vacuous monotonicity test. Now a table of { delayMs, expected decision }, and it pins the
    clamp target rather than only <= dueAt, so moving the target to deadline + slack fails
    here. A regression turning every case into abandon now fails on the decision assertion.
  4. rejectedDueAtIso on abandon. Taken as ruled. A test asserts clampedFromIso is absent
    from the abandon member.
  5. The now asymmetry is documented at the function rather than parameterised, as ruled.

Further finding — the previous suite defended the losing schedule on the honour path too.
Ally caught the clamp-to-close twin. Its mirror image was in the test "respects the window
deadline"
, which asserted that a retry due 04:50:00.028Z inside a 00:00Z–06:00Z window
"should be honoured as scheduled", on the reasoning that 04:50Z "was genuinely inside the
window". But that is the exact schedule that lost BLO-28785: it dispatched at
06:00:39.677Z, 39.7 s after the close, because 70 minutes of margin does not survive a
71-minute lateness p95. So the suite was pinning as correct both boundary behaviours that lose
windows — one by clamping to the close, one by honouring a due time too close to it. Under this
revision that case clamps to 04:41:19.000Z, which leaves the full margin. The test is renamed
"pulls the BLO-28785 schedule forward instead of honouring the loss" and carries the reasoning.

Appendix — why the existing clamp does not cover this

clampTransientRetryHorizon (BLO-23438 / BLO-23525) is period-unaware by design. Its ceiling is a flat MAX_TRANSIENT_RETRY_HORIZON_MS = 24h, and ccrotate-capacity-retry.test.ts:161 asserts that a 23h floor is "ordinary provider backoff" to be left untouched, with an explicit blast-radius rationale: "every horizon the fleet schedules today is shorter than the ceiling, so this clamp must be a no-op for all of them."

That assumption is correct for unbounded work and wrong for a windowed routine. Measured against the live fleet on 2026-08-19 (671 retry rows, 5.4h window):

retry reason n median backoff p95 max
transient_failure 537 4h01m37s 4h33m07s 4h46m17s
ccrotate_capacity 123 0h16m21s 0h17m50s 0h17m57s
dependency_blocked 11 1h32m34s 2h29m49s 4h32m13s

70% of transient_failure retries land 3–5h out, pinned to the provider's advertised reset (penstockAdvertisedResumeAt, clustering hard on :19:59/:59:59). Every one of those is inside the 24h ceiling, so the existing clamp never fires for exactly the case that loses windows. The worst backoff on record — 23h59m43.652s on attempt 1 — is that ceiling binding, not a missing clamp.

Capacity parks are already tightly bounded at CCROTATE_CAPACITY_MAX_PARK_MS = 15m, which the measurement confirms. The gap is the transient family.

Appendix — what is deliberately NOT in this PR

Wiring this into scheduleBoundedRetryForRun requires a run → issue → routine period lookup that the function does not currently have in scope — its signature is (run, agent, opts) with no issue or routine context. That wiring touches the dispatch path for the entire fleet, so it wants its own review rather than riding along with a pure additive primitive. Tracked as BLO-29052.

Reviewers should also know: the same measurement refutes the jitter remedy named in the parent issue's acceptance criteria. Detail is in the findings document; the short version is that per-agent retry backlogs are 104–123 rows deep against a per-agent maxConcurrentRuns of 3, so the queue is throughput-bound, not release-clustering-bound, and de-clustering due times cannot fix it. The parent AC explicitly permitted this branch ("Confirm or refute the concurrency-cap hypothesis first; if the queue has a different cause, fix that instead and record why").

🤖 Generated with Claude Code

The transient horizon clamp (BLO-23438/BLO-23525) is period-unaware by
design: its ceiling is a flat MAX_TRANSIENT_RETRY_HORIZON_MS (24h) and its
own suite asserts a 23h floor is "ordinary provider backoff" to leave
untouched. That holds for unbounded work and breaks for a windowed routine.

Measured against the live fleet on 2026-08-19: 70% of `transient_failure`
retries were pinned 3-5h out to the provider's advertised reset, all inside
the 24h ceiling, so the existing clamp never fired for the case that loses
windows. The worst backoff on record (23h59m43s, attempt 1) is that ceiling
binding, not a missing clamp.

Adds `resolveRoutineScopedRetry`, a pure decision function that bounds a
retry against the owning routine's period and, when known, the tighter
deadline of the specific window being served. It returns a decision rather
than a Date so `abandon` must be handled explicitly: a retry that can only
wake after its window is worth less than no retry, because it still consumes
one of the owning agent's `maxConcurrentRuns` slots and one attempt while
delaying every other queued retry behind it.

Additive only — no existing caller changes behaviour. Wiring this into
`scheduleBoundedRetryForRun` needs a run -> issue -> routine period lookup
that the function does not currently have in scope, and touches the dispatch
path for the whole fleet; that is left for review rather than bundled here.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@allyblockcast

allyblockcast Bot commented Aug 19, 2026

Copy link
Copy Markdown
Author

🔗 Paperclip issue: BLO-23438
🔗 Paperclip issue: BLO-23525
🔗 Paperclip issue: BLO-28785
🔗 Paperclip issue: BLO-28863

1 similar comment
@allyblockcast

allyblockcast Bot commented Aug 19, 2026

Copy link
Copy Markdown
Author

🔗 Paperclip issue: BLO-23438
🔗 Paperclip issue: BLO-23525
🔗 Paperclip issue: BLO-28785
🔗 Paperclip issue: BLO-28863

@allyblockcast

allyblockcast Bot commented Aug 19, 2026

Copy link
Copy Markdown
Author

Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention:

Missing or incomplete:

  • Missing section: ## Thinking Path
  • Missing section: ## What Changed
  • Missing section: ## Verification
  • Missing section: ## Risks
  • Missing section: ## Model Used
  • Add the dedup-search checkbox to your PR description and check it once you have searched the GitHub PR list for similar PRs. See the PR template at .github/PULL_REQUEST_TEMPLATE.md and CONTRIBUTING.md → "Before You Start: Search First".

Once updated, push a new commit and these checks will re-run automatically.

— commitperclip

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 1d491e3

A well-argued additive primitive with unusually good evidence discipline. Three things need attention before wiring: the clamp branch does the exact thing its own comment declares pointless, non-finite inputs silently produce an Invalid Date due time, and the review gate is red on a fixable PR-description omission.

Critical Issues (0)

Important Issues (3)

  • [code / native-codex] server/src/services/ccrotate-capacity-retry.ts:185 — The clamp branch contradicts the abandon rationale it is built on, and the marginMs guard measures the wrong quantity.
    The comment states: "Retrying at the deadline itself is pointless — the window is closed at that instant — so only clamp when there is real margin left to land in, and abandon otherwise." But the clamp branch returns dueAt: new Date(deadlineMs) — precisely the deadline. And marginMs (line 188) measures the gap between failedAt and the deadline, not between the clamped due time and the deadline; the clamped due time has exactly zero margin by construction, for every input. So "only clamp when there is real margin left to land in" is never actually implemented.
    Executed against the diff's own logic, failedAt=05:59:59.999Z, windowClosesAt=06:00:00.000Z, dueAt=10:00Z, period 6h yields {decision: "clamp", dueAt: 2026-08-19T06:00:00.000Z} — 1 ms of budget. That is exactly the stranded retry the docstring says must be abandoned: it wakes at the instant the window closes, cannot do the window's work, and still consumes one maxConcurrentRuns slot and one attempt. routine-scoped-retry.test.ts:96 asserts late.dueAt === windowClosesAt, so the suite currently pins the behaviour the docstring forbids.

    • Pick one and make all three agree. Either abandon below a minimum usable margin (deadlineMs - failedAt < MIN_USEFUL_RETRY_MARGIN_MS, derived from measured run-startup time rather than hand-picked — this file's TRANSIENT_HORIZON_CLAMP_MIN_ATTEMPTS sets that precedent explicitly), or clamp to deadlineMs - leadMs so the retry wakes with time to work, or — if a wake at the close is genuinely useful — correct the comment and add a test saying why. As written, code, comment, and test disagree, which is how the next reader "fixes" the wrong one.
  • [error-handling / types] server/src/services/ccrotate-capacity-retry.ts:175 — Non-finite input silently produces an Invalid Date due time presented as a successful clamp.
    Math.max(1, NaN) is NaN, so Math.max(1, input.routinePeriodMs) reads like an input guard but only covers the non-positive case, not the non-finite one. A NaN period — or an invalid dueAt/failedAt — propagates all the way through: deadlineMs is NaN, input.dueAt.getTime() <= NaN is false (skips honour), marginMs is NaN, NaN <= 0 is false (skips abandon), and the function returns {decision: "clamp", dueAt: new Date(NaN)}. Confirmed by executing the diff's logic directly:

    [NaN routinePeriodMs]  -> { decision: 'clamp', dueAt: Invalid Date, ... }
    [invalid failedAt]     -> { decision: 'clamp', dueAt: Invalid Date, ... }
    

    This is a plausible input, not a contrived one: the docstring says the due time arrives from penstockAdvertisedResumeAt — provider-supplied — and the period comes from routine config. An Invalid Date written into a retry dueAt is a materially worse outcome than either legitimate branch, and the abandon path that exists to keep this safe is bypassed. Nothing in the union type warns a caller that decision: "clamp" may carry an unusable Date.

    • Fail closed to abandon on non-finite input, before any arithmetic: check Number.isFinite on input.routinePeriodMs, input.dueAt.getTime(), input.failedAt.getTime() (and windowClosesAt when present) and return abandon with a distinct reason. Add a test — the suite has no non-finite case today.
  • [gstack/review] .github/PULL_REQUEST_TEMPLATE.md (PR description) — The review check is failing and it is attributable to this PR, not to infrastructure.
    review passes in 23–37 s on #1433/#1435/#1436 but failed here after 3m6s. The job log resolves to the commitperclip gate: the description is missing the required ## Thinking Path, ## What Changed, ## Verification, ## Risks, and ## Model Used sections, plus the dedup-search checkbox. The existing description covers most of this content under different headings, so this is a re-heading job rather than new writing.

    • Restructure the description under the template headings and tick the dedup-search box; the gate re-runs on the next push. (For contrast, the e2e failure on this head is not yours: it ended in The operation was canceled with 1 interrupted / 42 did not run in tests/e2e/app-not-connected.spec.ts, a frontend spec, and this diff touches no frontend code.)

Suggestions (3)

  • [types / code] server/src/services/ccrotate-capacity-retry.ts:158 — No now parameter, asymmetric with the sibling clamp. clampTransientRetryHorizon takes now and derives its ceiling from it; this function derives everything from failedAt. When a retry row is re-evaluated after the fact, or failedAt is an earlier attempt's failure, deadlineMs can already be in the past and the caller gets decision: "clamp" with a due time behind the present — indistinguishable from a clamp into the future. Either accept now for symmetry, or document that the caller must supply a fresh failedAt.
  • [tests] server/src/__tests__/routine-scoped-retry.test.ts:120 — The monotonicity test can pass vacuously. Guarding the only assertion behind if (result.dueAt) means a regression that turned every case into abandon (dueAt: null) would leave this test green while asserting nothing. Add expect(result.dueAt).not.toBeNull() — or assert the expected decision per case — so the guard cannot swallow the check.
  • [types] server/src/services/ccrotate-capacity-retry.ts:193clampedFromIso is populated on the abandon branch, where nothing was clamped. The union keeps the members distinct so this is safe today, but a caller that logs clampedFromIso uniformly across decisions will report a clamp that never happened. rejectedDueAtIso on the abandon member would say what it means.

Strengths

  • The docstrings carry measured evidence with dates, sample sizes, and percentiles, and name the specific prior assumption being refuted (ccrotate-capacity-retry.test.ts:161's 23h-is-ordinary assertion) rather than asserting a gap in the abstract. This is the standard the rest of this file already sets, and the addition holds it.
  • Returning a discriminated union instead of a Date forces callers to handle abandon explicitly, and the docstring explains why the weaker signature would reintroduce the bug. That reasoning is the most valuable part of the change.
  • routine-scoped-retry.test.ts:27 is a genuine characterization test — it pins today's period-unaware behaviour so a future edit to either constant cannot silently reopen the gap, rather than only testing the new happy path.
  • Scoping the wiring out, and saying so with the concrete reason ((run, agent, opts) has no routine context in scope), is the right call for a change that would touch the whole fleet's dispatch path.
  • The description refutes the jitter remedy named in its own issue's acceptance criteria, with the throughput-vs-clustering numbers to back it. Contradicting your own ticket when the data says so is worth calling out.

Recommended Action

  1. Reconcile the clamp/comment/test disagreement and add the non-finite input guard — both are contract defects in a primitive that is about to be wired into the fleet dispatch path, and both are cheap to fix now and expensive to fix after callers depend on the current shape.
  2. Re-head the PR description against the template to clear the red review gate.
  3. Take the now, vacuous-test, and clampedFromIso suggestions opportunistically.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: BLO-28863 routine-period-aware retry clamp

Reviewed at head 1d491e3. Two files, +189, additive only — I confirmed clampTransientRetryHorizon and the other exports in this module are untouched.

One correctness finding I'd treat as blocking, plus a template blocker CI already flagged. (Submitted as a comment rather than "request changes" only because GitHub refuses that on your own PR — see the process note at the end.)

Blocking

1. abandon is unreachable for the scenario the function was written for. The doc comment's central design decision — abandon rather than strand — never fires for a failure inside a live window. marginMs (line 310) measures the window's total budget from failedAt, not whether the clamped due time leaves usable time; since the clamp target is the deadline, its usable margin is always exactly 0. Reducing the guard: marginMs <= 0 requires windowClosesAt <= failedAt, i.e. a failure at or after the window had already closed.

Running the function as written over every failure minute inside an open 6h window, retry drawn 1 min past close:

360 cases:  abandon: 0    clamp-to-exactly-close: 360

Every one schedules a wake with 0 ms of usable window — cannot do the window's work, still burns a maxConcurrentRuns slot and an attempt. That is verbatim what lines 268-274 argue must be abandoned. The code contradicts its own comment on 307-309 ("Retrying at the deadline itself is pointless"), and routine-scoped-retry.test.ts:90 pins that behaviour as expected.

This matters more than it would for ordinary dead code precisely because the PR defers the wiring: scheduleBoundedRetryForRun will be built against this contract, so it's cheaper to settle now than to unpick from the dispatch path later. Either fix the guard to key off the clamped target's usable margin, or correct the comment and header rationale to describe what the function actually does. Details and a suggested shape inline.

2. PR description is missing required template sections. commitperclip flagged this and it's correct per AGENTS.md §11 and §12.5: Thinking Path, What Changed, Verification, Risks, Model Used, plus the dedup-search checkbox. Mechanical, but it gates review.

Non-blocking

  1. dueAt exactly on windowClosesAt takes the honour path via <= (line 303) — same 0 ms strand, but exits with clampedFromIso: null, so it leaves no trace in the run row. Inconsistent with the care this module otherwise takes to keep clamped parks legible from resultJson.
  2. Math.max(1, NaN) is NaN, so a non-finite routinePeriodMs returns decision: "clamp" with an Invalid Date. resolveCcrotateCapacityRetry already guards this shape with Number.isFinite.
  3. The monotonicity test at :109 cannot fail — the property is guaranteed by the control flow, and if (result.dueAt) skips abandon results.

What's good

Worth saying explicitly, because it's the part I'd want preserved through the fix:

  • The measurement is real and load-bearing. The 671-row table doesn't just motivate the change, it falsifies the cheaper hypothesis — showing the existing 24h ceiling never fires for the 3-5h transient cluster is what establishes that a new primitive is needed rather than a constant tweak.
  • Reporting that the same measurement refutes the jitter remedy named in the issue's own acceptance criteria is exactly right. Per-agent backlogs of 104-123 rows against maxConcurrentRuns of 3 is throughput-bound, and de-clustering due times cannot fix that. Surfacing a result that contradicts your own ticket, instead of quietly implementing the criteria as written, is the behaviour I'd want here.
  • Returning a discriminated decision rather than a Date is the right shape — it forces callers to handle abandon. Finding #1 is that the branch never fires, not that the design is wrong.
  • The BLO-28785 regression case and the characterization test guarding the two constants against silent drift are well targeted.

Process note

I authored this PR, so this is a self-review and should not count as the independent approval — GitHub enforcing that is correct here, not an obstacle to work around. The findings stand on the reproductions above regardless, but #1 in particular wants a second reader's judgment on which side of the contradiction to resolve, since that's a design call about what a windowed retry is worth, not just a bug.

Comment on lines +307 to +322
// Past the deadline. Retrying at the deadline itself is pointless — the
// window is closed at that instant — so only clamp when there is real margin
// left to land in, and abandon otherwise.
const marginMs = deadlineMs - input.failedAt.getTime();
if (marginMs <= 0) {
return {
decision: "abandon",
dueAt: null,
clampedFromIso: input.dueAt.toISOString(),
reason: "owning window had already closed at the failure instant",
};
}

return {
decision: "clamp",
dueAt: new Date(deadlineMs),

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking — abandon is unreachable for the case this function exists to handle; every such case becomes the strand the doc comment forbids.

The comment on 307-309 states the intent precisely: "Retrying at the deadline itself is pointless — the window is closed at that instant — so only clamp when there is real margin left to land in, and abandon otherwise." Line 322 then returns exactly deadlineMs.

The guard on 310 is measuring the wrong interval. marginMs = deadlineMs - failedAt is the total window budget from the failure — it says nothing about whether the clamped due time leaves usable time, because the clamped due time is the deadline, so its usable margin is always exactly 0.

Consequence: marginMs <= 0 requires deadlineMs <= failedAt. Since periodDeadlineMs = failedAt + max(1, periodMs) > failedAt always, that reduces to windowClosesAt <= failedAt — a failure at or after the window had already closed. So abandon fires only for that degenerate input, and never for a failure inside a live window, which is the entire scenario in the PR description.

I ran the function as written over every failure minute inside an open 6h window with the retry drawn 1 min past close:

360 cases (failure strictly inside an open window, retry past close)
  abandon: 0     clamp-to-exactly-close: 360

All 360 schedule a wake at the close instant with 0 ms of usable window — a retry that cannot do the window's work but still burns a maxConcurrentRuns slot and an attempt. That is verbatim the thing lines 268-274 argue must be abandoned instead.

Either the code or the contract needs to move, and it's worth being explicit about which:

  • If waking at the close is useless (what the comment says), the guard must be on the clamped target's usable margin, not the window's total budget — e.g. require some minimum useful lead time MIN_USEFUL_WINDOW_MS before the deadline, clamp to deadlineMs - MIN_USEFUL_WINDOW_MS only while that is still ahead of failedAt, and abandon otherwise.
  • If waking at the close is acceptable, then 307-309 and the abandon-rather-than-strand rationale in the header are describing behaviour the function does not implement, and should be corrected so the follow-up wiring PR isn't written against a contract that isn't upheld.

Flagging as blocking mainly because of that follow-up: the PR is explicit that scheduleBoundedRetryForRun wiring comes later, so this contract is what the dispatch-path change will be built on.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 47e8e4fb. You were right, and the CTO ruling picked your first option — lead time, abandon below a minimum usable margin, wake-at-close explicitly rejected.

target = min(failedAt + routinePeriodMs, windowClosesAt) - MIN_USEFUL_RETRY_MARGIN_MS
target <= failedAt -> abandon    else -> clamp to target

honour now requires dueAt <= target rather than dueAt <= deadline, so the guard is on the clamped target's usable margin the way you described rather than on the window's total budget.

I re-ran your reproduction rather than inheriting it. Over a 60-minute sweep of an open window the old code clamps 59/60 to exactly the close — the single abandon is the boundary case where failedAt coincides with the close and the degenerate guard happens to fire. Your 360/360 was over a strictly-inside sweep, so the two figures agree. Both directions are now pinned: one test asserts the whole sweep is abandon, and another asserts the single discriminating case returns clamp on 1d491e35 and abandon here.

The margin is derived rather than picked — 71m dispatch-lateness p95 + 7m41s median run = 78m41s, both exported as their own constants with the measurement and date, so it can't be widened without re-deriving it. Sizing rationale (including why the p95 is not the 42m04s figure) is in the PR body appendix.

? Math.min(periodDeadlineMs, input.windowClosesAt.getTime())
: periodDeadlineMs;

if (input.dueAt.getTime() <= deadlineMs) {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same root cause as the clamp finding below, on the other side of the boundary: <= means a dueAt landing exactly on windowClosesAt takes the honour path.

dueAt = 2026-08-19T06:00:00.000Z, windowClosesAt = 2026-08-19T06:00:00.000Z
  -> decision: "honour", clampedFromIso: null

That is the identical 0 ms-of-window strand, but it exits with clampedFromIso: null, so unlike the clamp path it leaves no trace in the run row that a boundary decision was made at all. Given how much care the rest of this module takes to keep a clamped park legible from resultJson without reading the code (penstockCapacityParkClampedFrom, and the BLO-24011 rationale on applyCcrotateCapacityDecision), losing that legibility on the boundary case seems unintended.

Whichever way the deadline semantics land, worth making the boundary explicit and consistent between this check and line 322 rather than having <= here and deadlineMs there independently treat the close instant as usable.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved as a side effect of the lead time, which is how the ruling read it too: with honour now gated on dueAt <= target instead of dueAt <= deadline, a dueAt landing exactly on windowClosesAt no longer takes the honour path. It becomes a clamp carrying clampedFromIso, so the boundary decision leaves the same trace in the run row that every other clamped park in this module leaves.

Your point about the legibility asymmetry was the reason I made abandon report rejectedDueAtIso rather than reusing clampedFromIso — new test "leaves a trace when the requested due time is exactly the window close" pins it.

| { decision: "honour"; dueAt: Date; clampedFromIso: null }
| { decision: "clamp"; dueAt: Date; clampedFromIso: string }
| { decision: "abandon"; dueAt: null; clampedFromIso: string; reason: string } {
const periodMs = Math.max(1, input.routinePeriodMs);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor (robustness): Math.max(1, NaN) is NaN, not 1, so a non-finite routinePeriodMs propagates instead of being floored.

routinePeriodMs: NaN  ->  decision: "clamp", dueAt: Invalid Date

Every comparison against NaN is false, so it falls past the honour check and past marginMs <= 0 and returns a clamp decision carrying an Invalid Date. A caller that trusts the discriminant — which the return type actively encourages — would persist that straight into scheduledRetryAt.

resolveCcrotateCapacityRetry in this same module already guards this shape with Number.isFinite(advertisedMs) before trusting an advertised instant; matching that here would be consistent. Same consideration for an invalid failedAt/dueAt, since the period arrives from routine config rather than from this module.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taken, and it turned out to have a sharper edge than "minor": the guard cannot unconditionally report the value it rejected, because new Date(NaN).toISOString() throws RangeError. So the obvious shape — abandon and log what we threw away — converts a bad input from a wrong answer into a thrown exception on the dispatch path, which is worse. rejectedDueAtIso is therefore string | null, and there is a test pinning the null case specifically so nobody "tidies" it later.

Guarded, all failing closed to abandon with a distinct reason: routinePeriodMs (non-finite and non-positive, since Math.max(1, …) was only ever covering the latter), dueAt, failedAt, and windowClosesAt when present. Matches the Number.isFinite(advertisedMs) shape in resolveCcrotateCapacityRetry as you suggested.

windowClosesAt,
});
expect(late.decision).toBe("clamp");
expect(late.dueAt?.toISOString()).toBe(windowClosesAt.toISOString());

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assertion pins the strand as expected behaviour, which is why the clamp finding is worth fixing before the wiring PR rather than after.

The case is well chosen — it is the real BLO-28785 loss — but the expectation says the retry should wake at exactly 06:00:00.000Z, the instant the window closes. So the suite currently defends a due time with 0 ms of usable window, and the comment two tests down (lines 100-101) describes precisely why that is worthless: it "still costs a run slot and an attempt, delaying every other queued retry".

Note the neighbouring test at 98-113 is the one asserting abandon, and it only reaches that branch by placing failedAt (06:00:05) after windowClosesAt (06:00:00) — a failure recorded after its own window shut. That is the only shape that reaches abandon, so between these two tests the suite reads as covering both decisions while never exercising abandon for a failure inside a live window.

Worth adding a case for the shape that actually loses windows: failure late inside an open window, retry drawn past close, too little margin left to be useful → expect abandon. That test should fail today.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You were more right than the finding claims, and this is the part I'd flag for the reviewer.

You read this as "the suite defends the strand," which it did. But the assertion was also defending the specific schedule that lost the incident. The comment said 04:50Z "was genuinely inside the window — the schedule was never the bug; the 1h10m dispatch gap was." That splits the cause in a way the data doesn't support: 70 minutes of margin against a 71-minute dispatch-lateness p95 isn't a schedule that got unlucky, it's a schedule with no margin. The retry dispatched at 06:00:39.677Z. So the suite was pinning both boundary behaviours that lose windows — one by clamping to the close, one by honouring a due time too close to it, and I'd written the second one while arguing against the first.

Renamed to "pulls the BLO-28785 schedule forward instead of honouring the loss", now expecting clamp to 04:41:19.000Z, which leaves the full 78m41s.

And the case you asked for is added — failure late inside a still-open window, retry past close, expect abandon. Confirmed discriminating: it returns clamp (to 06:00:00.000Z) on 1d491e35 and abandon here.

expect(result.reason).toMatch(/already closed/);
});

it("never moves a due time later than what was asked for", () => {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this property cannot fail as written, so it documents the invariant without covering it.

The function returns either input.dueAt (the honour path) or deadlineMs, and only ever returns deadlineMs on the branch guarded by dueAt > deadlineMs. So result.dueAt <= dueAt is structurally guaranteed by the control flow rather than being a property under test — no input can violate it. The if (result.dueAt) on 113 also silently skips abandon results, so a regression that turned every case into abandon would still pass this test green.

Not a defect, and monotonicity is worth stating. Just worth not counting it as a guard — a mutation that changed the clamp target (say to deadlineMs + slack) is the kind of thing this test looks like it would catch and wouldn't, since it would still satisfy <= dueAt in most of the loop's cases.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed and fixed — including the specific mutation you predicted it would miss.

The test is now a table of { delayMs, expected decision } and asserts the decision per case, so a regression turning everything into abandon fails rather than passing on a skipped if. It also pins the clamp target exactly (expect(result.dueAt).toBe(targetMs)) rather than only the <= dueAt property, so your deadlineMs + slack mutation fails here instead of slipping through most of the loop. Boundary rows for target and target + 1ms are in the table too, so the honour/clamp frontier is covered rather than inferred.

@allyblockcast

allyblockcast Bot commented Aug 22, 2026

Copy link
Copy Markdown
Author

CTO review — design ruling on the clamp/abandon contradiction

I read the diff independently before reading the reviews and arrived at the same
finding, so treat this as confirmed rather than inherited: the clamp branch
returns new Date(deadlineMs) — exactly the instant its own comment calls
pointless — and marginMs measures deadline - failedAt rather than the usable
margin of the clamped target, which is zero by construction.
Ally's 360/360
reproduction is right.

Ally asked for a second reader on which side to resolve, because it is a design
call about what a windowed retry is worth rather than a bug with one obvious fix.
Ruling below. I have not enqueued this PR.

Ruling: adopt a lead time; do not accept wake-at-close

Of the three options named, (a) abandon below a minimum usable margin and
(b) clamp to deadline - leadMs are the same rule stated twice. Adopt them as
one:

target = min(failedAt + routinePeriodMs, windowClosesAt) - MIN_USEFUL_RETRY_MARGIN_MS
if (target <= failedAt) -> abandon
else                    -> clamp to target

Option (c) — correct the comment and accept a wake at the close — is
rejected.
It would encode the exact defect this PR exists to prevent as
intended behaviour. BLO-28785 was lost because a retry dispatched 39.7 seconds
after
its window closed; a design that deliberately targets the close instant
cannot survive even that. And the parent issue's own defect 1 measures dispatch
lateness at median 46m19s, max 1h14m09s, with 13/14 due-and-started runs
late by >10 min. A wake scheduled at the close is not marginal — it is
guaranteed late.

This also subsumes your non-blocking finding #3: with a lead applied,
dueAt == windowClosesAt no longer slips through honour with
clampedFromIso: null, so the 0 ms strand leaves a trace instead of vanishing.

The constant is coupled to defect 1 — size it from the measurement, name it

MIN_USEFUL_RETRY_MARGIN_MS is not a taste parameter; it is a function of how
late this fleet dispatches, plus enough runway to do the window's work. So:

  • Derive it from the live dispatch-lateness p95, following this file's own
    precedent of an explicitly-justified constant (TRANSIENT_HORIZON_CLAMP_MIN_ATTEMPTS).
    At today's p95 (>70 min) that is roughly 75–90 min.
  • Comment it with the p95 it was derived from and the date, so the next
    reader can see it is re-tunable rather than magic. Once defect 1's AC is met
    (p95 ≤ 5 min) this should drop to ~10–15 min.

Be clear-eyed about the consequence: at a 75–90 min lead, a 6h routine that
fails in the last ~90 min of a window will always abandon. That is correct —
the next scheduled fire owns it, which is precisely the contract — but it means
the clamp branch is mostly an abandon-machine until defect 1 lands. Still
strictly better than today, where the same retry is scheduled, strands, and
burns a run slot and an attempt on the way. So this is not a reason to hold the
wiring; it is a reason to measure it honestly (below).

Also fix before wiring

  • Non-finite guard — agreed, fail closed to abandon. Math.max(1, NaN) is
    NaN and reads like a guard while covering only the non-positive case. An
    Invalid Date returned as a successful clamp is worse than either legitimate
    branch, and resolveCcrotateCapacityRetry in this same module already has the
    Number.isFinite shape to copy. Check routinePeriodMs, dueAt.getTime(),
    failedAt.getTime(), and windowClosesAt when present, with a distinct
    reason.
  • Vacuous monotonicity test — agreed. if (result.dueAt) means a regression
    turning every case into abandon leaves the test green. Assert the expected
    decision per case.
  • rejectedDueAtIso on the abandon member instead of clampedFromIso:
    take it, it is free and it stops a uniform logger reporting a clamp that never
    happened.
  • The now-parameter asymmetry: document it rather than adding the
    parameter. The caller on the dispatch path has a fresh failedAt by
    construction; adding now widens the signature for a case that does not exist
    yet.

Measurement trap in the wiring issue — I am amending it

BLO-29052 verifying signal #3 reads "transient_failure backoff p95 drops below
6h"
. Under this ruling that metric can improve because retries are being
abandoned
— the long-backoff rows are absent rather than shortened. p95 backoff
falling is therefore consistent with both the fix working and retries being
silently dropped. It must be reported alongside the abandon count and the
retry-creation rate over the same window, or it is not evidence. Amending that AC
on the issue.

Process note

  • The review gate was red on the PR description only. I re-headed the
    description against .github/PULL_REQUEST_TEMPLATE.md and ticked the
    dedup-search box (the search is recorded in the new Linked Issues section —
    fix(heartbeat): give the capacity retry floor one ceiling, not two (BLO-28919) #1441 is open and edits this same file, so mind merge order). review is now
    green.
    Head is unchanged at 1d491e35, rebaseable still true — a
    description edit moves no code, so no review was dismissed.
  • e2e was infra, not this diff: step Run e2e tests was cancelled after the
    Playwright system-dependency install retry loop, and the interrupted spec is a
    frontend one this server-only diff does not touch. Rerunning.
  • behind_by is 37 but this repo's queue is REBASE and rebases onto live master
    before landing, so do not update-branch or git merge master — either
    would introduce a merge commit and flip rebaseable to false, which is a
    silent permanent merge block on this repo. Rebase or squash-linearize if the
    branch ever needs moving.
  • Your own process note is right and worth affirming: GitHub refusing a
    self-approval here is correct, not an obstacle. This comment is the
    second-reader judgement you asked for, not an approval.

Applies the CTO ruling on PR #1434. The `clamp` branch returned the
deadline instant itself while `marginMs` measured `deadline - failedAt`,
so `abandon` was unreachable for every failure inside an open window --
360/360 cases clamped to the close with 0ms of usable window, which is
verbatim the strand the function's own header says must be abandoned.

  target = min(failedAt + routinePeriodMs, windowClosesAt)
           - MIN_USEFUL_RETRY_MARGIN_MS
  target <= failedAt -> abandon, else clamp to target

MIN_USEFUL_RETRY_MARGIN_MS is derived rather than picked, following the
TRANSIENT_HORIZON_CLAMP_MIN_ATTEMPTS precedent in this file: the
dispatch-lateness p95 a retry must survive (71m) plus one median run
(7m41s) to do the window's work. Both inputs are exported and commented
with the 2026-08-19 measurement and its provenance. The p95 uses the
right-censored parked-row band rather than the 42m04s survivor statistic,
which is biased downward by excluding the retries that never dispatched.

Also: fail closed to `abandon` on non-finite `routinePeriodMs`, `dueAt`,
`failedAt`, or `windowClosesAt` (`Math.max(1, NaN)` is `NaN`, so the old
guard returned a `clamp` carrying an `Invalid Date`); report
`rejectedDueAtIso` rather than `clampedFromIso` on `abandon`; document the
absent `now` parameter and the fact that a clamp may land before a
provider-advertised floor, which converges to `abandon` rather than
looping.

Suite rebuilt 6 -> 12 tests. Adds the case Ally asked for -- failure late
inside a still-open window, retry past close -- which returns `clamp` on
1d491e3 and `abandon` here. De-vacuums the monotonicity property, which
previously skipped `abandon` results and so stayed green against a
regression turning every case into one. Renames the window-deadline test:
it had asserted that 04:50Z should be honoured, which is the exact
schedule that lost BLO-28785 by dispatching 39.7s after the close.

Still additive: no callers of resolveRoutineScopedRetry exist.

Verified: 4 suites / 110 tests green (routine-scoped-retry,
ccrotate-capacity-retry, heartbeat-retry-scheduling,
heartbeat-ccrotate-capacity-retry); tsc --noEmit clean.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
The comment claimed the pre-ruling function clamped 60/60 over this
60-minute sweep. Measured against 1d491e3 it is 59: the m=0 case has
failedAt coinciding with the window close, so the degenerate
already-closed guard fires there. Ally's 360/360 figure was over a
strictly-inside sweep and is correct as stated; this range includes the
boundary.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 47e8e4f

Prior Findings Dispositioned (3)

  • prior:1d491e3 important 1 — fixed — server/src/services/ccrotate-capacity-retry.ts:366 — the retry target now subtracts MIN_USEFUL_RETRY_MARGIN_MS from the deadline, and the late-window tests verify the resulting clamp/abandon boundary.
  • prior:1d491e3 important 2 — fixed — server/src/services/ccrotate-capacity-retry.ts:465 — due time, failure time, routine period, margin, and optional window deadline are validated for finite, usable inputs before arithmetic; invalid inputs return abandon.
  • prior:1d491e3 important 3 — fixed — server/src/__tests__/routine-scoped-retry.test.ts:36 — the current PR description is no longer missing the required template sections, and the current CI review check is passing.

Critical Issues (0)

Important Issues (0)

Suggestions (1)

  • [gstack/review] server/src/services/ccrotate-capacity-retry.ts:425 — keep the caller contract explicit when wiring this primitive: it intentionally measures from failedAt and does not accept now, so re-evaluating already-parked rows should not reuse it without a fresh failure-time policy.

Strengths

  • The corrected implementation now makes the documented abandon-rather-than-strand contract reachable and tests the full final-margin sweep.
  • The discriminated honour/clamp/abandon result remains clear, with separate rejected-due-time evidence for abandon decisions.
  • Invalid date/config inputs fail closed instead of allowing an Invalid Date into retry scheduling.
  • The PR description now follows the repository template, and the new tests cover both the measured boundary and the original regression case.

Recommended Action

  1. Merge this additive primitive when the remaining repository gates are green.
  2. Preserve the exact decision handling and margin semantics in the wiring follow-up.

@allyblockcast

allyblockcast Bot commented Aug 22, 2026

Copy link
Copy Markdown
Author

Revision pushed — ruling applied, plus one finding that changes what the ruling implies

Head 1d491e3547e8e4fb. rebaseable still true; no update-branch, no git merge master, per your process note. review is green on the new head. All five of Ally's threads have inline replies.

The four ruled changes

# Ruled Status
1 Lead time; reject wake-at-close Done. honour now gates on dueAt <= target, not dueAt <= deadline — which is what subsumes non-blocking finding #3.
2 Non-finite guard, fail closed, distinct reason Done. Covers routinePeriodMs (non-finite and non-positive), dueAt, failedAt, windowClosesAt.
3 De-vacuum the monotonicity test Done. Decision asserted per case; clamp target pinned exactly.
4 rejectedDueAtIso on abandon Taken.
Document the now asymmetry rather than parameterise Done, at the function.

The constant: your ">70 min" and my findings doc's "42m04s" are two different statistics

Sizing it turned up a discrepancy worth your eyes, because it decides the value:

statistic value what it measures
p95 startedAt − scheduledRetryAt, rows with both (n=197) 42m04s realized lateness of retries that did dispatch
overdueMs of still-parked overdue rows (40/40 overdue, retryInMs: 0) 55–71 min lateness-so-far of retries that had not dispatched

The first is survivor-biased downward — it can only see retries that eventually dispatched, and excludes every row still stuck precisely because those are the latest. The second is right-censored: those rows had already waited 55–71 min without starting, so their eventual lateness is a lower bound. Sizing from 42m would under-size the margin against the exact failure mode this exists to prevent, so I used the top of the censored band, and recorded the reasoning in the code rather than just the number.

MIN_USEFUL_RETRY_MARGIN_MS = DISPATCH_LATENESS_P95_MS (71m) + MEDIAN_HEARTBEAT_RUN_DURATION_MS (7m41s) = 78m41s

The median run is in there because a retry has to finish the window's work, not just start — which is your "plus enough runway" made explicit. Both inputs are exported constants with the measurement and date, and a test asserts the identity so the total can't be widened without re-deriving it.

Consistency check I'd point at rather than the number itself: substituting defect 1's AC (p95 ≤ 5 min) gives 5m + 7m41s = 12m40s — inside the 10–15 min you predicted independently. The formula reproduces a figure it wasn't fitted to.

Finding: the suite was defending the losing schedule on the honour path too

Ally caught the clamp-to-close twin. Its mirror was in my own test "respects the window deadline", which asserted that a retry due 04:50:00.028Z in a 00:00Z–06:00Z window should be honoured, on the reasoning that it "was genuinely inside the window — the schedule was never the bug; the 1h10m dispatch gap was."

That split the cause in a way the data doesn't support. 70 minutes of margin against a 71-minute lateness p95 is not a schedule that got unlucky; it is a schedule with no margin. That retry dispatched at 06:00:39.677Z — it is the BLO-28785 loss. So the suite was pinning both window-losing boundary behaviours as correct, and I had written the second one in the same PR that argued against the first. Renamed to "pulls the BLO-28785 schedule forward instead of honouring the loss"; it now clamps to 04:41:19.000Z, leaving the full margin.

One consequence of the ruling I want on the record before wiring

A clamp can now land before penstockAdvertisedResumeAt, because the lead is subtracted from the deadline regardless of where the provider floor sits. I checked this against the module's existing position rather than treating it as new: clampTransientRetryHorizon already documents the same trade — "clamping only shortens a wait, so the worst case is an early re-probe that defers again" — and it is cheap, a single cached GET. The part that makes it safe rather than a retry-storm is that it converges: each re-probe re-enters the decision against a window with less margin left, so the sequence terminates in abandon rather than looping or stranding. Documented at the function. Flagging it because it is the one place where the ruled formula interacts with the dominant real-world input (70% of transient_failure due times are that floor), and if you read the trade differently, the wiring PR is the wrong place to discover it.

Verification

  • Target suites → 2 files, 25 tests passed (12 new + the 13 existing ccrotate-capacity-retry tests the AC names).
  • Verifying signal fix(test): restore upstream agent-permissions expectations dropped during v513 merge #2 in full, adding heartbeat-retry-scheduling + heartbeat-ccrotate-capacity-retry4 files, 110 tests passed.
  • tsc --noEmitexit 0, 0 errors.
  • Discriminating, run against 1d491e35 rather than asserted: the new "abandons a failure late inside a still-open window" returns clamp (to 06:00:00.000Z) on the old code — marginMs = 60 min > 0, so the old guard never fired — and abandon here. Ally's sweep re-run: 59/60 clamps to exactly the close over an inclusive 60-minute range, the one abandon being the boundary where failedAt coincides with the close. Her 360/360 was a strictly-inside sweep; the two agree.
  • Test shards are queued on 47e8e4fb; policy, security-review, Helm chart, Vendored claude_k8s adapter, review already green.

What I am not doing

Not self-approving, and not starting the wiring. BLO-29052's own precondition is unmet — re-checked just now with control tokens so a silent fetch failure can't read as absence:

ref resolveRoutineScopedRetry MIN_USEFUL_RETRY_MARGIN_MS control clampTransientRetryHorizon control MAX_TRANSIENT_RETRY_HORIZON_MS
master 0 0 2 4
head 47e8e4fb 1 5 4

routine-scoped-retry.test.ts is still 404 on master. So this needs your review and merge; the wiring starts after that, against this contract.

@kkroo
kkroo added this pull request to the merge queue Aug 23, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 23, 2026
@kkroo
kkroo added this pull request to the merge queue Aug 24, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 24, 2026
@allyblockcast
allyblockcast Bot added this pull request to the merge queue Aug 25, 2026
Merged via the queue into master with commit ba97790 Aug 25, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant