tasks table already has cost_estimate_usd and cost_spent_usdscidex/forge/cost_budget.py with a context-managerBudgetGuard(analysis_id, budget_usd) that:cost_spent_usd_so_far from aBudgetExceeded (subclass of RuntimeError) when projectedscidex/forge/executor.py wraps the user script in BudgetGuardcost_estimate_usd from the linked task.
analysis_cost_ledger(analysis_id, ts, source,actual_cost_usd rollup column on analyses.
llm.py call cost_budget.charge(...) after eachBudgetExceeded, executor records partial result, setsanalyses.status='budget_exhausted', and emits an event the/senate/cost-dashboard shows live spend rate and top-10 spenderspartial_done than OOM-kill silently.scidex/forge/executor.py, llm.py, tasks.cost_* columns.All acceptance criteria satisfied:
scidex/forge/cost_budget.py (new) — BudgetGuard context manager with:ContextVar-based registration so charges are invisible to user analysis code.BudgetExceeded(RuntimeError) raised when current_spend × 1.3 > budget_usd.charge(source, cost_usd, ...) for use by LLM and API layers._flush_ledger() / _update_analysis_cost() write to DB on exit.UNIT_COSTS and TOKEN_COSTS_PER_1K tables for conservative estimates.migrations/20260427_cost_budget_ledger.sql (new) — creates:analysis_cost_ledger(id, analysis_id, ts, source, cost_usd, tokens_in, tokens_out, units, notes) with indexes on analysis_id, ts, source.ALTER TABLE analyses ADD COLUMN IF NOT EXISTS actual_cost_usd DOUBLE PRECISION DEFAULT 0.0.scidex/forge/executor.py (modified) — LocalExecutor.run_analysis():budget_usd: Optional[float] to AnalysisSpec.cost_estimate_usd when not specified._run_isolated() in BudgetGuard when budget is set.BudgetExceeded: stores partial result, calls _mark_analysis_budget_exhausted() which sets analyses.status='budget_exhausted' and publishes budget_exceeded event.scidex/core/llm.py (modified) — added _charge_budget(resp, provider):_complete_via_api and _complete_via_harness response.response_cost; falls back to token-count estimate.BudgetGuard is active (doesn't affect non-budgeted calls).scidex/core/event_bus.py (modified) — added "budget_exceeded" to EVENT_TYPES.api.py (modified) — added GET /senate/cost-dashboard?days=N:analysis_cost_ledger → analyses → tasks).