Skip to content

feat: Claude usage threshold gate#52

Merged
finedesignz merged 1 commit into
mainfrom
feat/claude-usage-thresholds
May 26, 2026
Merged

feat: Claude usage threshold gate#52
finedesignz merged 1 commit into
mainfrom
feat/claude-usage-thresholds

Conversation

@finedesignz
Copy link
Copy Markdown
Owner

Summary

Adds a per-user Anthropic usage threshold gate over the existing OAuth usage poller. When 5h or 7d utilization crosses a user-configured percentage, remo-code pauses NEW dispatches at all 4 entry points (scheduler, pickSessionTarget, error-capture, manual chat) until the window resets. In-flight runs are NOT killed.

Architecture: .planning/phases/claude-usage-thresholds/00-architecture-review.md

Key amendments from review (all applied):

  • DROP ccusage — uses existing /api/oauth/usage poller; ccusage deferred to future opt-in PR
  • Per-USER (not per-agent) — usage is subscription-scoped
  • Default NULL (gate OFF) — existing users not silently opted in
  • 4 dispatch sites gated, not 2
  • New scheduled_task_runs.status = 'skipped_quota'
  • Idempotent schema (ALTER … IF NOT EXISTS + DO $$ EXCEPTION wrapper for CHECK)

Schema

ALTER TABLE users ADD COLUMN IF NOT EXISTS claude_session_threshold_pct INTEGER;
ALTER TABLE users ADD COLUMN IF NOT EXISTS claude_week_threshold_pct INTEGER;
-- CHECK 1..100 OR NULL, scheduled_task_runs CHECK rebuilt with 'skipped_quota'

Refusal shape (HTTP 503 + WS send_refused)

{
  "error": "quota_threshold_reached",
  "reason": "session_threshold",
  "utilization_pct": 92,
  "threshold_pct": 90,
  "resets_at": "2026-05-26T05:00:00Z"
}

Tests

  • hub/test/threshold.test.ts — 10 cases (back-compat NULL, fail-open, session-over, week-over, both-over precedence, boundary >=, opus carve-out, single-cap modes)
  • Full suite: 256 pass / 0 fail / 56 skip (e2e DB-gated)
  • bun run build:web clean

Test plan

  • Coolify redeploy via UUID zewfc6g9dw3c4h88z2jd2o4g
  • DB migration auto-runs on hub boot (idempotent — safe on prod with existing data)
  • Settings → Profile → "Claude usage threshold gate" card renders, toggles + sliders work
  • PUT /api/account/claude-thresholds with {session_pct: 90, week_pct: null} persists
  • Once snapshot crosses threshold, manual chat send gets WS send_refused
  • Scheduled task fires while over → run row with status='skipped_quota'
  • In-flight session NOT terminated when threshold crossed mid-stream
  • Lowering threshold below current util immediately pauses (UI shows ⚠ warning before Save)

🤖 Generated with Claude Code

Adds a per-user threshold gate over the existing Anthropic OAuth usage poller
(agent/src/usage-poller.ts → hub/src/usage/store.ts) so remo-code pauses NEW
dispatches when 5-hour or 7-day utilization crosses a user-configured cap.

Architecture review:
  .planning/phases/claude-usage-thresholds/00-architecture-review.md

Schema (idempotent, back-compat):
- users.claude_session_threshold_pct INT NULL (1-100, NULL = OFF)
- users.claude_week_threshold_pct INT NULL    (1-100, NULL = OFF)
- scheduled_task_runs.status += 'skipped_quota' (CHECK constraint rebuilt)

Gate sites (all 4 dispatch entry points per review §3):
1. hub/src/scheduler/dispatcher.ts — fireTask() + queue-promotion path
2. hub/src/sessions/routing.ts — pickSessionTarget returns kind:'quota_blocked'
3. hub/src/error-capture/dispatcher.ts — dispatchPendingError head
4. hub/src/ws/client.ts — send_message handler (returns send_refused)

Refusal shape (uniform across HTTP + WS):
  { error: 'quota_threshold_reached', reason: 'session_threshold'|'week_threshold',
    utilization_pct, threshold_pct, resets_at }

Decision precedence: session_threshold wins ties (soonest reset). Opus carve-out
counts toward week_threshold. NULL thresholds OR null snapshot = fail-open.

API (plain Hono, JWT-authed):
- GET  /api/account/claude-thresholds → { session_pct, week_pct }
- PUT  /api/account/claude-thresholds   body strict-validated, both nullable
- GET  /api/account/usage → { usage, thresholds, paused, reason, resets_at }

UI: Settings → Profile → new ClaudeUsageCard. Two per-cap toggles + 50-95 sliders.
Inline 'Paused' banner when current snapshot trips a saved threshold. Suggested
default value is 90 but column stays NULL until the user clicks Save.

In-flight runs are NOT killed — only NEW dispatch is refused. Waiter promotion
in session-queue re-evaluates the gate and drops with skipped_quota if still
over.

Tests:
- hub/test/threshold.test.ts (10 cases): back-compat null, fail-open, session-over,
  week-over, both-over precedence, boundary >=, opus carve-out, single-cap modes.
- Full suite: 256 pass / 0 fail / 56 skip (e2e gated by REMO_E2E_DB_URL).

Out of scope (deferred per review §1/§4):
- ccusage fallback (the OAuth endpoint is authoritative; no new dep)
- Per-agent usage endpoint (usage is per-user / per-subscription)
- Token-counts hover (not in OAuth payload)
@finedesignz finedesignz merged commit 12b6369 into main May 26, 2026
1 check 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