#!/usr/bin/env python3
"""
Credit guard — turns an exhausted Anthropic balance into a clean, throttled skip.
Watches nothing on its own; it is the shared helper the scheduled loops (the
Mansu Musa weekly cadence, and any future paid-API cadence) call to classify a
failure as "no credits" and rate-limit how often the owner gets pinged about it.
Schedule: none — imported by loop scripts, never run on a timer itself.
Cost: $0 (pure stdlib file I/O; zero model / LLM / network calls).
State: chairman-agent/data/credit_skips.json, one entry per job:
{"<job>": {"last_skip": iso, "count": n, "last_notify": iso}}
No secret is read, logged, or written here.
"""
import json
from datetime import datetime, timedelta, timezone
from pathlib import Path
CREDIT_MARKER = "credit balance is too low"
SKIPS_FILE = Path(__file__).resolve().parent / "data" / "credit_skips.json"
def is_credit_error(exc):
"""True when an exception is Anthropic's 'credit balance too low' 400."""
return CREDIT_MARKER in str(exc).lower()
def _now():
return datetime.now(timezone.utc).isoformat()
def _load():
try:
if SKIPS_FILE.exists():
return json.loads(SKIPS_FILE.read_text())
except Exception:
pass
return {}
def _save(data):
try:
SKIPS_FILE.parent.mkdir(parents=True, exist_ok=True)
SKIPS_FILE.write_text(json.dumps(data, indent=2))
except Exception:
pass
def record_skip(job):
"""Record that `job` skipped a run; bumps its counter and last_skip time."""
data = _load()
row = data.get(job) or {}
row["last_skip"] = _now()
row["count"] = int(row.get("count", 0)) + 1
data[job] = row
_save(data)
return row
def should_notify(job, hours=24):
"""True if `job` has not sent an owner alert within the last `hours`."""
row = _load().get(job) or {}
last = row.get("last_notify")
if not last:
return True
try:
prev = datetime.fromisoformat(last)
except ValueError:
return True
if prev.tzinfo is None:
prev = prev.replace(tzinfo=timezone.utc)
return datetime.now(timezone.utc) - prev >= timedelta(hours=hours)
def mark_notified(job):
"""Stamp `job`'s last_notify to now so should_notify throttles for `hours`."""
data = _load()
row = data.get(job) or {}
row["last_notify"] = _now()
data[job] = row
_save(data)
return row