Repository navigation
fix(ladder): tranche status is never updated, so in-flight netting grows and planning silently stops #118
Description
Reviewed commit: be11bdcb5. Note: origin/main moved to 3e9660d06 during the review; re-verify against current main before changing code, since a finding may have been fixed or moved.
Where
internal/config/store_postgres_ladder.go:378-390-GetInFlightLadderCommitUSDHr,WHERE config_id = $1 AND status = $2withladder.TrancheStatusScheduled- Rows written at
internal/config/store_postgres_ladder.go:296-313 - Status assigned by the planner at
pkg/ladder/ladder.go:308(Status: TrancheStatusScheduledfor every tranche) - Consumer:
internal/server/handler_ladder.go:323 - Executor absence documented at
internal/server/handler.go:66
What
GetInFlightLadderCommitUSDHr sums amount_usd_hr over tranches in status scheduled, and the caller subtracts that from the allocation gap.
Every tranche is inserted as scheduled, and there is no UPDATE of ladder_tranches.status anywhere in the repository. git grep "UPDATE ladder_tranches" over origin/main returns nothing, and the tranche-firing executor does not exist yet. internal/server/handler.go:66 says so explicitly:
persists the ladder_run and ladder_tranches rows ... and stops. No purchases ... Execution arrives in a later PR
So scheduled is a terminal state in practice. The sum is cumulative over the config's entire history rather than a measure of what is actually in flight.
Failure scenario
A ladder config with cadence = weekly and a $2/hr gap plans 4 tranches of $0.50/hr in week 1.
Week 2's run calls GetInFlightLadderCommitUSDHr, which returns $2.00 (week 1's tranches, still scheduled, none of which ever fired), and subtracts it. The recomputed gap is about $0, so the run plans nothing. Week 3 and every week after behave the same.
The ladder silently stops producing plans after its first run and reports no error. The failure is indistinguishable from "the target coverage is already met", which is the one state an operator would never investigate.
Once the execution phase lands, the same sum will also be double-counted against the provider-reported ExistingUSDPerHour that the docstring says already contains fired and completed tranches, so the netting is wrong in both directions.
Fix direction
Either restrict the sum to un-fired tranches with a not-yet-elapsed scheduled_date (plus a per-run recency window), or land the status lifecycle transition in the same change that introduced the netting so scheduled stops being terminal. The second is the real fix; the first is a stopgap that keeps planning alive until the executor exists.
Related
- LeanerCloud/cloud-commitments-cli#1398 (ladder: buy-now ramp steps escape netting) covers the opposite direction on the same query: amounts that should be counted but are not, because
AfterDays == 0steps never become tranche rows. It also raises the read/append serialization gap. Neither item covers this one: thatschedulednever transitions, so everything ever planned stays counted forever. Both need fixing before the netting is correct, and fixing only LeanerCloud/cloud-commitments-cli#1398 leaves this defect intact. - fix(ladder): net in-flight tranches and use append-only persistence to converge to target coverage cloud-commitments-cli#1397 introduced the L5 tranche-netting convergence this query belongs to.
Activity
fix(ladder): stop netting elapsed scheduled tranches from the gap (#535)
cristim commentedon Oct 8, 2026
From PR 670 (#544): overdue scheduled ladder tranches are now canceled only when a new run is persisted, so they can still accumulate while a config is paused or the cadence gate skips runs. When the executor lands it must fire only rows within a grace window (scheduled_date > now - grace) or run the same sweep before claiming, and must set status and execution_id consistently.
feat(ladder): tranche lifecycle store API and grace-bound netting (#824)
fix(ladder): persist immediate ramp steps as tranches and keep the pl…
fix(config): clear no_submission unless the written status is failed (#…
feat(purchase): persist no_submission proof on failed executions (#118 …
feat(ladder): reconcile fired tranches from execution outcomes (#118 …
5 remaining items
feat(purchase): recheck ladder switches before any provider call (#118 …
feat(ladder): stamp ladder_tranches.settled_at when a tranche complet…
test(ladder): fix misspell lint and refuse an unknown tranche status
feat(ladder): operator action to fail an unproven fired tranche
test(ladder): remove the data race in the resolve race test
fix(ladder): refuse to execute an execution whose tranche is already …
feat(ladder): operator action to fail an unproven fired tranche (#854)
feat(ladder): prefetch commitment visibility for the tranche reconcil…
cristim commentedon Oct 17, 2026
Owner decision (2026-10-17): this issue moves from the ladder sessions to the platform implementation driver. #118 is taken first as a concrete fix; #70 is planned as an epic and split into slices of at most 400 lines.
cristim commentedon Oct 17, 2026
Claiming: working on this [claude-cc-impl]
cristim commentedon Oct 17, 2026
Checked against current main (d47d516): already fixed by the #118 lifecycle PRs, so no further code change is needed.
inFlightLadderRows(internal/config/store_postgres_ladder_lock.go~:184) counts fired rows plus scheduled rows whosescheduled_dateis afterasOf - LadderFireGrace. The persisted-run sweep (store_postgres_ladder.go~:326-352) cancels the exact complement as 'expired', soscheduledis no longer effectively terminal and week 2 plans the remaining gap.store_postgres_ladder_lifecycle.go).Existing tests cover the failure scenario:
TestLadderB1_NextRunNetsImmediateRowWithinGrace,TestLadderB1_DailyRerunNeverExceedsTarget, the counted-XOR-cancelled grace boundary property test, and the 12-week sweep test.What remains belongs to #70. No tranche executor calls
ClaimDueLadderTranchesyet, and nothing schedulesRunLadderReconcile. #946 (SP ExistingUSDPerHour in commitment dollars vs on-demand-equivalent) is the gating item before the executor can be enabled. A maintainer can close this. [claude-cc-impl]cristim commentedon Oct 17, 2026
Released: already fixed on main (see above) [claude-cc-impl]