fix(loops): persist cost on stop; reset budget baseline on restart #172

Merged
dries merged 1 commit from fix/loop-restart-budget into main 2026-06-28 23:01:26 +02:00
Owner

Summary

Fixes two loop budget-accounting bugs, surfaced by loop-end messages like:

Loop "Check open PRs are mergeable" ended: reached cost budget ($5.00) (after 0 iterations, $6.76).

Bug A — terminal stop didn't persist the cost that triggered it

The stop path wrote only state + last_summary (via SetLoopState), never the refreshed cost_usd/iteration. The stored row showed cost 0 next to a "$5 budget reached" summary, so the UI contradicted itself.

Fix: persist the loop (UpdateLoop) before SetLoopState in the stop branch, so the row matches the final summary.

Bug B — Restart instantly re-terminated ("0 iterations, $6.76")

Restart zeroed the loop's cost_usd/iteration, but usage aggregation re-summed the previous run's child_sessions (still linked by loop_id) on the next tick. Cost jumped back over the cap and the loop re-terminated immediately — a restarted loop could never run.

Fix: add loops.usage_baseline_at (migration v18). Only child sessions created at/after the baseline count toward the budget. Restart sets it to now, so a restarted loop runs against a fresh budget while history is preserved. Baseline 0 (all existing loops) counts everything — behavior unchanged.

Tests

  • TestEvaluateOne_StopPersistsCostThatTriggeredIt — stop persists the cost (row matches summary).
  • TestRestart_ResetsBudgetBaselineSoOldSpendDoesNotReTrip — restart succeeds and old spend is excluded.
  • TestRefreshUsage_ExcludesSessionsBeforeBaseline — baseline filtering.

go test ./..., go vet ./... pass.

Migration

v18: ALTER TABLE loops ADD COLUMN usage_baseline_at INTEGER NOT NULL DEFAULT 0 (additive, backward-compatible).

## Summary Fixes two loop budget-accounting bugs, surfaced by loop-end messages like: > Loop "Check open PRs are mergeable" ended: reached cost budget ($5.00) (after 0 iterations, $6.76). ### Bug A — terminal stop didn't persist the cost that triggered it The stop path wrote only `state` + `last_summary` (via `SetLoopState`), never the refreshed `cost_usd`/`iteration`. The stored row showed **cost 0** next to a "$5 budget reached" summary, so the UI contradicted itself. **Fix:** persist the loop (`UpdateLoop`) before `SetLoopState` in the stop branch, so the row matches the final summary. ### Bug B — Restart instantly re-terminated ("0 iterations, $6.76") `Restart` zeroed the loop's `cost_usd`/`iteration`, but usage aggregation re-summed the **previous run's** `child_sessions` (still linked by `loop_id`) on the next tick. Cost jumped back over the cap and the loop re-terminated immediately — a restarted loop could never run. **Fix:** add `loops.usage_baseline_at` (migration **v18**). Only child sessions created at/after the baseline count toward the budget. `Restart` sets it to `now`, so a restarted loop runs against a **fresh budget** while history is preserved. Baseline `0` (all existing loops) counts everything — behavior unchanged. ## Tests - `TestEvaluateOne_StopPersistsCostThatTriggeredIt` — stop persists the cost (row matches summary). - `TestRestart_ResetsBudgetBaselineSoOldSpendDoesNotReTrip` — restart succeeds and old spend is excluded. - `TestRefreshUsage_ExcludesSessionsBeforeBaseline` — baseline filtering. `go test ./...`, `go vet ./...` pass. ## Migration v18: `ALTER TABLE loops ADD COLUMN usage_baseline_at INTEGER NOT NULL DEFAULT 0` (additive, backward-compatible).
fix(loops): persist cost on stop; reset budget baseline on restart
All checks were successful
CI / Frontend (pull_request) Successful in 4m12s
CI / Backend (pull_request) Successful in 4m25s
CI / Playwright E2E (pull_request) Successful in 7m49s
CI / Build Desktop (macOS arm64) (pull_request) Successful in 3m52s
CI / Build (pull_request) Successful in 4m29s
CI / Semantic Tag (pull_request) Has been skipped
CI / Coverage Ratchet (pull_request) Successful in 6m59s
e11f87f9be
Two budget-accounting bugs surfaced by loop end messages like
"reached cost budget ($5.00) (after 0 iterations, $6.76)".

Bug A: the terminal stop path wrote only state + summary, never the
refreshed cost/iteration that triggered the stop. The row showed
cost 0 next to a "$5 budget reached" summary. Now the stop path
persists the loop (UpdateLoop) before SetLoopState so the stored
row matches the final summary.

Bug B: Restart zeroed the loop's cost/iteration, but usage
re-aggregated the previous run's child_sessions (still linked by
loop_id) on the next tick, so the loop instantly re-terminated with
"0 iterations, $6.76" and could never run again. Add
loops.usage_baseline_at (migration v18): only child sessions created
at/after it count toward the budget. Restart sets it to now, so a
restarted loop runs against a fresh budget while history is preserved.
0 (existing loops) counts everything, unchanged.

Coverage ratchet: pass

Suite Baseline This PR Δ
go 69.40% 69.40% +0.00
frontend 56.96% 56.96% +0.00

Tolerance: -0.1%. Baseline stored on gh-pages.

<!-- coverage-ratchet --> ### Coverage ratchet: ✅ pass | Suite | Baseline | This PR | Δ | | |---|---|---|---|---| | go | 69.40% | 69.40% | +0.00 | ✅ | | frontend | 56.96% | 56.96% | +0.00 | ✅ | _Tolerance: -0.1%. Baseline stored on `gh-pages`._
dries merged commit 8bcc2fefdc into main 2026-06-28 23:01:26 +02:00
Sign in to join this conversation.
No description provided.