Skip to content

fix: scope a workspace credits blocker to the seats it actually blocks - #37

Merged
george-elphick-talieisin merged 2 commits into
mainfrom
fix/credits-cooldown-respects-seat-quota
Sep 20, 2026
Merged

george-elphick-talieisin merged 2 commits into
mainfrom
fix/credits-cooldown-respects-seat-quota

Conversation

@george-elphick-talieisin

@george-elphick-talieisin george-elphick-talieisin commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • A credits blocker on one seat cooled every seat sharing its account_id, regardless of their own usage. On a live two-seat workspace that took the only usable seat out of rotation: backup1 was at 100% on its 5h window and flagged workspace_member_credits_depleted; main was at 5h 46% / wk 7% with no flag of its own, and was cooled anyway.
  • The code already knew it was wrong — it logged seat 'main' is cooling until Sun 22:30 (credits) but reports 5h 46% wk 7%; clear with ... --clear-cooldown main — and the clear it advised does not stick, because seat status re-reconciles in the same command.
  • It compounded: credits carries no resets_at, so the deadline was a flat default_cooldown_seconds rather than a real reset; is_lifted_by_reset is false for it, so no free reset was ever offered; and the blocked-run probe re-applied it. A seat with quota to spare stayed blocked in rolling one-hour chunks for as long as its sibling was out.
  • Credits only matter to a seat that has used up its included quota. New usage::blocker_targets narrows a credits blocker to the members with no headroom left. A member with no reading is still cooled (no evidence either way; the next refresh corrects it), and spend_control still cools every member.
  • Applied at both cooling sites: reconcile_snapshots for snapshot evidence, and runner::affected_seats for a live run failure — where the seat that actually failed is always cooled, whatever its possibly stale reading says.

Not changed, deliberately: the flag taxonomy in reason_for_reached_type. workspace_member_usage_limit_reached is already treated as workspace-wide, so the workspace_member_ prefix does not mean per-member here; scoping from each seat's own quota evidence is the better mechanism and makes the distinction moot.

Test plan

  • cargo test — 155 lib + 93 integration tests pass
  • credits_blocker_spares_a_sibling_that_still_has_included_quota replays the real-world numbers (46%/7% vs 100%/31% + the flag) and asserts the in-quota seat stays usable
  • credits_blocker_cools_a_sibling_with_no_reading_of_its_own pins the conservative case, and that a later healthy reading does not re-cool it
  • spend_control_still_cools_every_member_whatever_their_headroom
  • reconcile_blocker_dominates_credits_in_either_order updated: its healthy-with-credits seat was at 50%, so under the fix it is correctly no longer cooled; moved to 100% so the order-independence property still holds on a seat genuinely out of quota
  • A/B replay of the real recorded state.json and seats.toml through reconcile_snapshots: pre-fix cools main and backup1; post-fix cools only backup1
  • Codex code review r1 — VERDICT: PASS, no findings
  • Copilot review — 3 findings, all accepted and fixed in 129785a; both new regression tests A/B'd against the pre-fix code (the runner-path one returns 75 instead of 0 without the fix)
  • Confirmed in the live CLI: a full codex-clean seat status with both seats fetched leaves main uncooled
2026-09-20T20:41:08  status  credits is workspace-wide; cooling main, backup1 until Sun 22:41   <- before
2026-09-20T21:45:53  status  credits is workspace-wide; cooling backup1 until Sun 23:45         <- after

main reports ready (active) at 5h 61% / wk 10%; backup1 cools on credits, as it should. The "is cooling ... but reports ..." advice line no longer appears.

Known adjacent issue, not fixed here

backup1 is denied a free reset that would genuinely unblock it: its credits reason fails is_lifted_by_reset (src/seat.rs:209) even though it is at 100% on a window a "Weekly + 5 hr" grant would clear. That is a separate defect in verdict's flag-first precedence.

A "credits depleted" flag on one seat cooled every seat sharing its
account_id, whatever their own usage looked like. On a live two-seat
workspace that took the only usable seat out of rotation: backup1 was at
100% on its 5h window and flagged workspace_member_credits_depleted,
while main sat at 5h 46% / wk 7% with no flag of its own. main was cooled
anyway, and reconcile_snapshots said so in as many words:

  credits is workspace-wide; cooling main, backup1 until Sun 22:30
  seat 'main' is cooling until Sun 22:30 (credits) but reports 5h 46% wk
  7%; clear with codex-clean seat status --clear-cooldown main

That second notice is the whole bug in one line: the code knew the seat
was fine and cooled it regardless, then advised a manual clear that does
not stick, because seat status re-reconciles in the same command.

It compounds: credits carries no resets_at, so the deadline is a flat
default_cooldown_seconds rather than a real reset time; is_lifted_by_reset
is false for credits, so a free reset is never offered for it; and the
blocked-run probe refetches both seats and re-applies it. A seat with
quota to spare stayed blocked in rolling one-hour chunks for as long as
its sibling was out.

Credits only matter to a seat that has used up its included quota. A seat
inside its own windows runs for free and was never going to spend credits.
blocker_targets narrows a credits blocker to the members with no headroom
left; a member with no reading at all is still cooled, since there is no
evidence either way and the next refresh corrects it. spend_control is an
admin-set hard stop and still cools every member.

Applied at both cooling sites: reconcile_snapshots for snapshot evidence,
and runner::affected_seats for a live run failure, where the seat that
actually failed is always cooled whatever its stale reading says.

Verified by replaying the real recorded state through reconcile_snapshots:
before, main and backup1 both cool; after, only backup1 does. Codex code
review r1: PASS, no findings.
Copilot AI lite review requested due to automatic review settings September 20, 2026 21:05

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Clear stale credits cooldowns and add live-run coverage before approval.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 2 Medium severity · 1 Low severity

Open (3)
What changed in this PR

Scopes workspace credits cooldowns to seats lacking quota headroom while preserving workspace-wide spend_control behavior.

Changes:

  • Adds per-seat blocker targeting.
  • Applies targeting to reconciliation and live run failures.
  • Adds credits and spend-control regression tests.
File Review
src/​usage.rs Implements scoped blocker targeting; fresh healthy readings do not clear previously propagated credits cooldowns.
src/​runner.rs Applies scoped targets to live failures; add run-path coverage and update the contradictory helper comment.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/runner.rs
Comment thread src/usage.rs
Comment thread src/runner.rs Outdated
…uota

Copilot review on PR #37, all three points accepted.

- A seat cooled conservatively while its reading was unknown stayed cooled
  for the full default cooldown even after a refresh proved it had headroom:
  the fresh reading removed it from the blocker's targets, but cool_seats
  only ever extends, and the credits-available branch needs has_credits, so
  nothing lifted it. The blocked-run probe then reached the same dead end on
  every pass. A fresh Healthy reading now clears a credits cooldown, which is
  never that seat's own block. Healthy is the whole condition: an OnCredits
  seat has no included quota left and keeps it, and rate_limit, model_limit
  and spend_control are left alone.

  The gap was visible in the test, which cleared the cooldown by hand before
  asserting. It no longer does, which is what makes it a regression test.

- The runner half of the change had no coverage: the existing credits case
  has no cached readings, so every sibling was a target there either way,
  and the new usage tests only reach reconcile_snapshots. Added a run-path
  test where main fails for credits and backup1 holds a cached healthy
  reading. Against the parent commit it returns 75 with a usable seat idle;
  it now returns 0, having fallen through to backup1.

- affected_seats still documented cooling every same-workspace seat, which
  its own body had stopped doing.
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.

2 participants