Skip to content

fix(ladder): tranche status is never updated, so in-flight netting grows and planning silently stops #118

Open

Description

@cristim

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 = $2 with ladder.TrancheStatusScheduled
  • Rows written at internal/config/store_postgres_ladder.go:296-313
  • Status assigned by the planner at pkg/ladder/ladder.go:308 (Status: TrancheStatusScheduled for 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 == 0 steps never become tranche rows. It also raises the read/append serialization gap. Neither item covers this one: that scheduled never 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

  1. cristim commented on Oct 8, 2026

    @cristim
    MemberAuthor

    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.

  2. 5 remaining items

  3. cristim commented on Oct 17, 2026

    @cristim
    MemberAuthor

    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.

  4. cristim commented on Oct 17, 2026

    @cristim
    MemberAuthor

    Claiming: working on this [claude-cc-impl]

  5. self-assigned this
    on Oct 17, 2026
  6. cristim commented on Oct 17, 2026

    @cristim
    MemberAuthor

    Checked against current main (d47d516): already fixed by the #118 lifecycle PRs, so no further code change is needed.

    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 ClaimDueLadderTranches yet, and nothing schedules RunLadderReconcile. #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]

  7. removed their assignment
    on Oct 17, 2026
  8. cristim commented on Oct 17, 2026

    @cristim
    MemberAuthor

    Released: already fixed on main (see above) [claude-cc-impl]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions