feat(budget): multi-period per-workspace LLM budget (hourly/daily/weekly/monthly) #2009

Merged
hongming merged 1 commits from feat/mc-multiperiod-workspace-budget into main 2026-05-29 11:45:32 +00:00
Member

What

Extends the per-workspace LLM budget from a single monthly ceiling to four independent rolling windows — hourly / daily / weekly / monthly (#49). Gives the canvas Budget tab a real lever against runaway LLM spend (origin: the reno-stars opus drain).

SSOT design

  • budget_periods.go — single source of truth: the period set + rolling windows, one FILTERed per-period spend query over the ledger, and the pure parse/encode/exceededPeriods logic. Adding a period = one line.
  • Migrationworkspaces.budget_limits jsonb (canonical config, backfilled from the legacy monthly budget_limit) + workspace_spend_events ledger.
  • Heartbeat (registry.go) — derives the spend increment from the agentʼs existing cumulative report (delta vs prev; reset-aware) → ledger row. Server owns windowing; no runtime change.
  • GET/PATCH (budget.go) — per-period limit/spend/remaining; accepts new {budget_limits:{…}} and legacy {budget_limit} (→ monthly); legacy fields still emitted + budget_limit kept synced (rollout back-compat). 0 = block-all (preserved); null/absent = no limit.
  • Enforcement (a2a_proxy.go) — 402 if any configured periodʼs rolling-window spend ≥ its limit; fail-open on DB error.
  • Canvas BudgetSection — four period rows (USD limit input + spend/limit + bar).

Tests

Pure SSOT (parse/encode/exceededPeriods); GET/PATCH + multi-period + A2A enforcement (sqlmock migrated to the new two-query flow; shared expectBudgetCheck helpers updated); canvas behavioral + per-period progress/aria. go build + vet + full handlers suite + migrations + canvas vitest all green locally.

Note: the duplicate components/__tests__/BudgetSection.test.tsx (old single-limit UI) was repurposed to a focused per-period progress/aria suite — behavioral coverage now lives in tabs/__tests__/BudgetSection.test.tsx (one component, no parallel identical suites).

Refs #49.

🤖 Generated with Claude Code

## What Extends the per-workspace LLM budget from a single **monthly** ceiling to **four independent rolling windows — hourly / daily / weekly / monthly** (#49). Gives the canvas Budget tab a real lever against runaway LLM spend (origin: the reno-stars opus drain). ## SSOT design - **`budget_periods.go`** — single source of truth: the period set + rolling windows, one FILTERed per-period spend query over the ledger, and the **pure** `parse`/`encode`/`exceededPeriods` logic. Adding a period = one line. - **Migration** — `workspaces.budget_limits` jsonb (canonical config, backfilled from the legacy monthly `budget_limit`) + `workspace_spend_events` ledger. - **Heartbeat** (`registry.go`) — derives the spend **increment** from the agentʼs existing cumulative report (delta vs prev; reset-aware) → ledger row. **Server owns windowing; no runtime change.** - **GET/PATCH** (`budget.go`) — per-period limit/spend/remaining; accepts new `{budget_limits:{…}}` **and** legacy `{budget_limit}` (→ monthly); legacy fields still emitted + `budget_limit` kept synced (rollout back-compat). **`0` = block-all** (preserved); null/absent = no limit. - **Enforcement** (`a2a_proxy.go`) — `402` if **any** configured periodʼs rolling-window spend ≥ its limit; fail-open on DB error. - **Canvas** `BudgetSection` — four period rows (USD limit input + spend/limit + bar). ## Tests Pure SSOT (parse/encode/exceededPeriods); GET/PATCH + multi-period + A2A enforcement (sqlmock migrated to the new two-query flow; shared `expectBudgetCheck` helpers updated); canvas behavioral + per-period progress/aria. **`go build` + `vet` + full handlers suite + migrations + canvas vitest all green** locally. **Note:** the duplicate `components/__tests__/BudgetSection.test.tsx` (old single-limit UI) was **repurposed** to a focused per-period progress/aria suite — behavioral coverage now lives in `tabs/__tests__/BudgetSection.test.tsx` (one component, no parallel identical suites). Refs #49. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fullstack-engineer added 1 commit 2026-05-29 11:12:02 +00:00
feat(budget): multi-period per-workspace LLM budget (hourly/daily/weekly/monthly)
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 4s
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Successful in 9s
CI / Detect changes (pull_request) Successful in 8s
CI / Python Lint & Test (pull_request) Successful in 3s
Check migration collisions / Migration version collision check (pull_request) Successful in 16s
E2E API Smoke Test / detect-changes (pull_request) Successful in 5s
E2E Chat / detect-changes (pull_request) Successful in 7s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (pull_request) Has been skipped
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 8s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (local) (pull_request) Successful in 1m10s
E2E Staging SaaS (full lifecycle) / pr-validate (pull_request) Successful in 59s
E2E Staging External Runtime / E2E Staging External Runtime (pull_request) Successful in 5m15s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 3s
Harness Replays / detect-changes (pull_request) Successful in 4s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 4s
Lint no tenant GITEA or GITHUB token write / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 3s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m30s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 5s
verify-providers-gen / Regenerate providers artifact and fail on drift (pull_request) Successful in 58s
gate-check-v3 / gate-check (pull_request) Successful in 6s
sop-checklist / na-declarations (pull_request) N/A: (none)
sop-checklist / all-items-acked (pull_request) Successful in 3s
sop-checklist / review-refire (pull_request) Has been skipped
sop-tier-check / tier-check (pull_request) Successful in 6s
CI / Platform (Go) (pull_request) Successful in 5m4s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 2s
CI / Canvas (Next.js) (pull_request) Successful in 5m6s
E2E Chat / E2E Chat (pull_request) Successful in 7s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 3s
CI / all-required (pull_request) Successful in 19m21s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 1m28s
Harness Replays / Harness Replays (pull_request) Successful in 4s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 1m24s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
qa-review / approved (pull_request) Refired via /qa-recheck by unknown
security-review / approved (pull_request) Refired via /security-recheck by unknown
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (pull_request) Failing after 30m49s
audit-force-merge / audit (pull_request) Successful in 4s
cf7b587f16
Extends the single monthly per-workspace budget to four independent ROLLING
windows so a workspace can be capped per hour/day/week/month (#49 — gives the
canvas Budget tab a real lever against runaway LLM spend, e.g. the reno-stars
opus drain). SSOT design:

- budget_periods.go = single source of truth: the period set + rolling windows,
  one FILTERed per-period spend query over the ledger, and the PURE
  parse/encode/exceededPeriods logic. Add a period = one line here.
- migration: workspaces.budget_limits jsonb (canonical config, backfilled from
  the legacy monthly budget_limit) + workspace_spend_events ledger.
- heartbeat (registry.go): derive the spend INCREMENT from the agent's existing
  cumulative report (delta vs prev; reset-aware) → ledger row. Server owns
  windowing; NO runtime change.
- budget.go GET/PATCH: per-period limit/spend/remaining; accepts the new
  {budget_limits:{...}} shape AND the legacy {budget_limit} (→ monthly); legacy
  response fields still emitted + budget_limit kept synced (rollout back-compat).
  A limit of 0 = block-all (preserved); null/absent = no limit.
- a2a_proxy.go checkWorkspaceBudget: 402 if ANY configured period's rolling
  window spend >= its limit; fail-open on DB error.
- canvas BudgetSection: four period rows (USD limit input + spend/limit + bar).

Tests: pure SSOT (parse/encode/exceededPeriods); GET/PATCH + multi-period +
A2A enforcement (sqlmock, migrated to the new two-query flow); shared
expectBudgetCheck helpers updated; canvas behavioral + per-period progress/aria.
go build + vet + full handlers suite + migrations + canvas vitest all green.

NOTE: the duplicate components/__tests__/BudgetSection.test.tsx (old single-limit
UI) was repurposed to a focused per-period progress/aria suite — behavioral
coverage now lives in tabs/__tests__/BudgetSection.test.tsx (one component, no
parallel identical suites).

Refs #49.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
core-qa approved these changes 2026-05-29 11:42:20 +00:00
core-qa left a comment
Member

qa-review APPROVE. SSOT design is sound: budget_periods.go is the single source for periods/windows + pure parse/encode/exceededPeriods (table-tested incl. 0=block-all, null-clears, negative/unknown dropped). Spend is a server-owned rolling-window ledger derived from the existing heartbeat delta — no runtime change, reset-aware. GET/PATCH cover multi-period + legacy back-compat; A2A enforcement blocks on ANY period over (fail-open on DB error). Test migration is thorough: pure + sqlmock GET/PATCH/multi-period/enforcement (two-query flow), shared expectBudgetCheck helpers updated, canvas behavioral + per-period progress/aria. go build/vet/full handlers suite/migrations + canvas vitest all green. Coverage matches the surface.

qa-review APPROVE. SSOT design is sound: budget_periods.go is the single source for periods/windows + pure parse/encode/exceededPeriods (table-tested incl. 0=block-all, null-clears, negative/unknown dropped). Spend is a server-owned rolling-window ledger derived from the existing heartbeat delta — no runtime change, reset-aware. GET/PATCH cover multi-period + legacy back-compat; A2A enforcement blocks on ANY period over (fail-open on DB error). Test migration is thorough: pure + sqlmock GET/PATCH/multi-period/enforcement (two-query flow), shared expectBudgetCheck helpers updated, canvas behavioral + per-period progress/aria. go build/vet/full handlers suite/migrations + canvas vitest all green. Coverage matches the surface.
core-security approved these changes 2026-05-29 11:42:20 +00:00
core-security left a comment
Member

security-review APPROVE. No credential/secret surface in the diff. Input validation: PATCH rejects negative limits + unknown periods (400); agent-reported spend is clamped upstream (#615 maxMonthlySpend) and the ledger delta is non-negative (reset-aware). Enforcement fail-OPEN on DB error is the existing, intentional contract (a budget-check outage must not block legitimate traffic) — acceptable since the cap is a cost guardrail, not an authz boundary. 0=block-all preserved. jsonb config parsed safely (tolerates NULL/malformed). No SSRF/path/token surface touched. LGTM.

security-review APPROVE. No credential/secret surface in the diff. Input validation: PATCH rejects negative limits + unknown periods (400); agent-reported spend is clamped upstream (#615 maxMonthlySpend) and the ledger delta is non-negative (reset-aware). Enforcement fail-OPEN on DB error is the existing, intentional contract (a budget-check outage must not block legitimate traffic) — acceptable since the cap is a cost guardrail, not an authz boundary. 0=block-all preserved. jsonb config parsed safely (tolerates NULL/malformed). No SSRF/path/token surface touched. LGTM.
Member

/qa-recheck

/qa-recheck
Member

/security-recheck

/security-recheck
hongming merged commit 64b7ecfb70 into main 2026-05-29 11:45:32 +00:00
Sign in to join this conversation.
4 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: molecule-ai/molecule-core#2009