diff options
| author | Claude <noreply@anthropic.com> | 2026-05-26 20:31:24 +0000 |
|---|---|---|
| committer | Claude <noreply@anthropic.com> | 2026-05-26 20:31:24 +0000 |
| commit | 32715355fe2eed321df4f7083dfe580d35f8a62a (patch) | |
| tree | 76c3fd6a45646a2f6bdca95e46f66a00d6f08e59 /internal/cli | |
| parent | 39f74dbbc7adca0e2058112408bb99694deefcaf (diff) | |
feat(executor): budget-gate the dispatcher — reroute to local or block (Phase 6)
The pool now consults a BudgetGate before taking an agent slot. If running on
the chosen paid provider could breach its rolling window (estimated by the
task's max_budget_usd), it reroutes to the free local runner when one is
registered; otherwise it stops before running and marks the task
BUDGET_EXCEEDED. serve.go builds a budget.Accountant from [budget] config and
wires it into the pool (only when limits are configured).
Allows the QUEUED → BUDGET_EXCEEDED transition, since the gate blocks a queued
task before it ever reaches RUNNING. Covered by pool tests for both the block
and reroute paths against a fake gate.
https://claude.ai/code/session_01SESwn7kQ7oP62trWw6pc39
Diffstat (limited to 'internal/cli')
| -rw-r--r-- | internal/cli/serve.go | 18 |
1 files changed, 18 insertions, 0 deletions
diff --git a/internal/cli/serve.go b/internal/cli/serve.go index e545763..55fdaf5 100644 --- a/internal/cli/serve.go +++ b/internal/cli/serve.go @@ -11,6 +11,7 @@ import ( "time" "github.com/thepeterstone/claudomator/internal/api" + "github.com/thepeterstone/claudomator/internal/budget" "github.com/thepeterstone/claudomator/internal/executor" "github.com/thepeterstone/claudomator/internal/notify" "github.com/thepeterstone/claudomator/internal/storage" @@ -144,6 +145,23 @@ func serve(addr string) error { pool.LLM = localClient } + // Budget accountant: gate paid providers against rolling per-provider caps. + // With no limits configured this is created but allows everything. + var accountant *budget.Accountant + if len(cfg.Budget.Provider5hUSD) > 0 { + window := budget.DefaultWindow + if cfg.Budget.Window != "" { + if d, derr := time.ParseDuration(cfg.Budget.Window); derr == nil { + window = d + } else { + logger.Warn("invalid budget.window; using default", "value", cfg.Budget.Window, "default", window) + } + } + accountant = budget.New(store, budget.Limits{Window: window, PerProvider: cfg.Budget.Provider5hUSD}) + pool.Budget = accountant + logger.Info("budget gating enabled", "window", window, "limits", cfg.Budget.Provider5hUSD) + } + if err := store.SeedProjects(); err != nil { logger.Error("failed to seed projects", "error", err) } |
