**Evaluating snapshot thresholds**

I'm looking into a bug where an old snapshot is causing some issues with thresholds, leading to zero hours and alert triggers. I think I need a stale guard and perhaps just the highest threshold to avoid alerts from expired data. I'll set a maximum age for snapshots, ideally around 12 hours. Since I should focus on the most urgent weekly threshold, I'll pick just one. Let's ensure we only send alerts if they've been recently accessed.

edit

ACTIONABLE_PRIMARY_FREE_PERCENT = float(os.environ.get("CODEX_ACTIONABLE_PRIMARY_FREE_PERCENT", "25"))
MAX_SNAPSHOT_AGE_HOURS = float(os.environ.get("CODEX_MAX_SNAPSHOT_AGE_HOURS", "12"))

def maybe_emit_alerts(s: Snapshot, state: dict[str, Any]) -> list[str]:
    sent: list[str] = []
    now = datetime.now(timezone.utc).timestamp()
    try:
        snapshot_ts = datetime.fromisoformat(s.ts.replace("Z", "+00:00")).timestamp()
    except Exception:
        snapshot_ts = now
    if now - snapshot_ts > MAX_SNAPSHOT_AGE_HOURS * 3600:
        state["stale_snapshot"] = {"ts": s.ts, "age_hours": round((now - snapshot_ts) / 3600, 1)}
        return sent
+    state.pop("stale_snapshot", None)

    for threshold_hours, threshold_remaining in WEEKLY_THRESHOLDS:
        key = f"{threshold_hours}h_{threshold_remaining}pct"
        if key in weekly_sent:
            continue
        if hours_left <= threshold_hours and weekly_free >= threshold_remaining:
            sent.append(build_weekly_message(s, hours_left, threshold_hours, threshold_remaining))
            weekly_sent.add(key)

    for threshold_hours, threshold_remaining in sorted(WEEKLY_THRESHOLDS, key=lambda x: x[0]):
        key = f"{threshold_hours}h_{threshold_remaining}pct"
        if key in weekly_sent:
            continue
        if hours_left <= threshold_hours and weekly_free >= threshold_remaining:
            sent.append(build_weekly_message(s, hours_left, threshold_hours, threshold_remaining))
            weekly_sent.add(key)
            break

    for level in PRIMARY_WARN_LEVELS:
        key = str(level)
        if key in primary_sent:
            continue
        if s.primary_used >= level:
            sent.append(build_primary_message(s, level))
            primary_sent.add(key)

    for level in sorted(PRIMARY_WARN_LEVELS, reverse=True):
        key = str(level)
        if key in primary_sent:
            continue
        if s.primary_used >= level:
            sent.append(build_primary_message(s, level))
            primary_sent.add(key)
            break


Successfully replaced 4 block(s) in /home/sebas/pi-config/bin/codex-usage-alert.

     ...
  20     (15, 20),
  21     (8, 10),
  22 ]
  23 ACTIONABLE_PRIMARY_FREE_PERCENT = float(os.environ.get("CODEX_ACTIONABLE_PRIMARY_FREE_PERCENT", "25"))
+ 24 MAX_SNAPSHOT_AGE_HOURS = float(os.environ.get("CODEX_MAX_SNAPSHOT_AGE_HOURS", "12"))
  24
  25
  26 @dataclass
  27 class Snapshot:
     ...
 144
 145 def maybe_emit_alerts(s: Snapshot, state: dict[str, Any]) -> list[str]:
 146     sent: list[str] = []
 147     now = datetime.now(timezone.utc).timestamp()
+149     try:
+150         snapshot_ts = datetime.fromisoformat(s.ts.replace("Z", "+00:00")).timestamp()
+151     except Exception:
+152         snapshot_ts = now
+153     if now - snapshot_ts > MAX_SNAPSHOT_AGE_HOURS * 3600:
+154         state["stale_snapshot"] = {"ts": s.ts, "age_hours": round((now - snapshot_ts) / 3600, 1)}
+155         return sent
+156 +    state.pop("stale_snapshot", None)
 148     state.setdefault("weekly", {})
 149     state.setdefault("primary", {})
 150     weekly_reset_key = str(s.secondary_resets_at)
 151     primary_reset_key = str(s.primary_resets_at)
     ...
 159         weekly_state.clear()
 160         weekly_state["reset"] = weekly_reset_key
 161         weekly_state["sent"] = []
 162     weekly_sent = set(weekly_state.get("sent") or [])
-163     for threshold_hours, threshold_remaining in WEEKLY_THRESHOLDS:
+172     for threshold_hours, threshold_remaining in sorted(WEEKLY_THRESHOLDS, key=lambda x: x[0]):
 164         key = f"{threshold_hours}h_{threshold_remaining}pct"
 165         if key in weekly_sent:
 166             continue
 167         if hours_left <= threshold_hours and weekly_free >= threshold_remaining:
 168             sent.append(build_weekly_message(s, hours_left, threshold_hours, threshold_remaining))
 169             weekly_sent.add(key)
+179             break
 170     weekly_state["sent"] = sorted(weekly_sent)
 171     weekly_state["last_weekly_free"] = weekly_free
 172     weekly_state["last_primary_free"] = primary_free
 173
     ...
 176         primary_state.clear()
 177         primary_state["reset"] = primary_reset_key
 178         primary_state["sent"] = []
 179     primary_sent = set(primary_state.get("sent") or [])
-180     for level in PRIMARY_WARN_LEVELS:
+190     for level in sorted(PRIMARY_WARN_LEVELS, reverse=True):
 181         key = str(level)
 182         if key in primary_sent:
 183             continue
 184         if s.primary_used >= level:
 185             sent.append(build_primary_message(s, level))
 186             primary_sent.add(key)
+197             break
 187     primary_state["sent"] = sorted(primary_sent, key=int)
 188     primary_state["last_primary_used"] = s.primary_used
 189
 190     state["last_snapshot"] = {
     ...

--- /home/sebas/pi-config/bin/codex-usage-alert
+++ /home/sebas/pi-config/bin/codex-usage-alert
@@ -20,8 +20,9 @@
     (15, 20),
     (8, 10),
 ]
 ACTIONABLE_PRIMARY_FREE_PERCENT = float(os.environ.get("CODEX_ACTIONABLE_PRIMARY_FREE_PERCENT", "25"))
+MAX_SNAPSHOT_AGE_HOURS = float(os.environ.get("CODEX_MAX_SNAPSHOT_AGE_HOURS", "12"))

 @dataclass
 class Snapshot:
@@ -144,8 +145,16 @@

 def maybe_emit_alerts(s: Snapshot, state: dict[str, Any]) -> list[str]:
     sent: list[str] = []
     now = datetime.now(timezone.utc).timestamp()
+    try:
+        snapshot_ts = datetime.fromisoformat(s.ts.replace("Z", "+00:00")).timestamp()
+    except Exception:
+        snapshot_ts = now
+    if now - snapshot_ts > MAX_SNAPSHOT_AGE_HOURS * 3600:
+        state["stale_snapshot"] = {"ts": s.ts, "age_hours": round((now - snapshot_ts) / 3600, 1)}
+        return sent
++    state.pop("stale_snapshot", None)
     state.setdefault("weekly", {})
     state.setdefault("primary", {})
     weekly_reset_key = str(s.secondary_resets_at)
     primary_reset_key = str(s.primary_resets_at)
@@ -159,15 +168,16 @@
         weekly_state.clear()
         weekly_state["reset"] = weekly_reset_key
         weekly_state["sent"] = []
     weekly_sent = set(weekly_state.get("sent") or [])
-    for threshold_hours, threshold_remaining in WEEKLY_THRESHOLDS:
+    for threshold_hours, threshold_remaining in sorted(WEEKLY_THRESHOLDS, key=lambda x: x[0]):
         key = f"{threshold_hours}h_{threshold_remaining}pct"
         if key in weekly_sent:
             continue
         if hours_left <= threshold_hours and weekly_free >= threshold_remaining:
             sent.append(build_weekly_message(s, hours_left, threshold_hours, threshold_remaining))
             weekly_sent.add(key)
+            break
     weekly_state["sent"] = sorted(weekly_sent)
     weekly_state["last_weekly_free"] = weekly_free
     weekly_state["last_primary_free"] = primary_free

@@ -176,15 +186,16 @@
         primary_state.clear()
         primary_state["reset"] = primary_reset_key
         primary_state["sent"] = []
     primary_sent = set(primary_state.get("sent") or [])
-    for level in PRIMARY_WARN_LEVELS:
+    for level in sorted(PRIMARY_WARN_LEVELS, reverse=True):
         key = str(level)
         if key in primary_sent:
             continue
         if s.primary_used >= level:
             sent.append(build_primary_message(s, level))
             primary_sent.add(key)
+            break
     primary_state["sent"] = sorted(primary_sent, key=int)
     primary_state["last_primary_used"] = s.primary_used

     state["last_snapshot"] = {


#!/usr/bin/env python3
from __future__ import annotations

import json
import math
import os
import subprocess
import sys
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

SESSIONS_DIR = Path(os.environ.get("CODEX_SESSIONS_DIR", "/home/sebas/.codex/sessions"))
STATE_FILE = Path(os.environ.get("CODEX_USAGE_ALERT_STATE", "/home/sebas/runtime/codex-usage-alert/state.json"))
TELEGRAM_NOTIFY = Path(os.environ.get("CODEX_TELEGRAM_NOTIFY", "/home/sebas/.agents/skills/telegram-notify/telegram-notify"))
PRIMARY_WARN_LEVELS = tuple(int(x) for x in os.environ.get("CODEX_PRIMARY_WARN_LEVELS", "80,90,95").split(",") if x)
WEEKLY_THRESHOLDS = [
    (30, 35),
    (15, 20),
    (8, 10),
]
ACTIONABLE_PRIMARY_FREE_PERCENT = float(os.environ.get("CODEX_ACTIONABLE_PRIMARY_FREE_PERCENT", "25"))
MAX_SNAPSHOT_AGE_HOURS = float(os.environ.get("CODEX_MAX_SNAPSHOT_AGE_HOURS", "12"))

@dataclass
class Snapshot:
    ts: str
    primary_used: float
    primary_window_min: int
    primary_resets_at: int
    secondary_used: float
    secondary_window_min: int
    secondary_resets_at: int
    plan_type: str | None

def load_latest_snapshot() -> Snapshot | None:
    latest: tuple[float, Snapshot] | None = None
    if not SESSIONS_DIR.exists():
        return None
    for path in SESSIONS_DIR.rglob("*.jsonl"):
        try:
            mtime = path.stat().st_mtime
        except OSError:
            continue
        if latest and mtime < latest[0] - 86400:
            continue
        snap = load_last_snapshot_from_file(path)
        if not snap:
            continue
        if latest is None or mtime > latest[0]:
            latest = (mtime, snap)
    return latest[1] if latest else None

def load_last_snapshot_from_file(path: Path) -> Snapshot | None:
    last: Snapshot | None = None
    try:
        with path.open() as fh:
            for line in fh:
                try:
                    data = json.loads(line)
                except Exception:
                    continue
                payload = data.get("payload") or {}
                if payload.get("type") != "token_count":
                    continue
                info = payload.get("info") or {}
                rate_limits = info.get("rate_limits") or payload.get("rate_limits")
                if not rate_limits:
                    continue
                primary = rate_limits.get("primary") or {}
                secondary = rate_limits.get("secondary") or {}
                if not primary or not secondary:
                    continue
                try:
                    last = Snapshot(
                        ts=str(data.get("timestamp") or ""),
                        primary_used=float(primary.get("used_percent") or 0.0),
                        primary_window_min=int(primary.get("window_minutes") or 0),
                        primary_resets_at=int(primary.get("resets_at") or 0),
                        secondary_used=float(secondary.get("used_percent") or 0.0),
                        secondary_window_min=int(secondary.get("window_minutes") or 0),
                        secondary_resets_at=int(secondary.get("resets_at") or 0),
                        plan_type=rate_limits.get("plan_type"),
                    )
                except Exception:
                    continue
    except OSError:
        return None
    return last

def load_state() -> dict[str, Any]:
    try:
        return json.loads(STATE_FILE.read_text())
    except Exception:
        return {}

def save_state(state: dict[str, Any]) -> None:
    STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
    STATE_FILE.write_text(json.dumps(state, indent=2, sort_keys=True) + "\n")

def fmt_hours(seconds: float) -> str:
    hours = seconds / 3600
    if hours >= 10:
        return f"{round(hours):.0f}h"
    return f"{hours:.1f}h"

def build_weekly_message(s: Snapshot, hours_left: float, threshold_hours: int, threshold_remaining: int) -> str:
    weekly_free = max(0.0, 100.0 - s.secondary_used)
    primary_free = max(0.0, 100.0 - s.primary_used)
    now = datetime.now(timezone.utc).timestamp()
    seconds_left = max(0.0, s.secondary_resets_at - now)
    primary_window_sec = max(1, s.primary_window_min * 60)
    primary_resets_left = max(0, math.floor(seconds_left / primary_window_sec))
    status = "actionable_now" if primary_free >= ACTIONABLE_PRIMARY_FREE_PERCENT else "blocked_by_5h"
    return (
        f"Codex semanal desaprovechado. "
        f"Faltan {fmt_hours(hours_left * 3600)}. "
        f"Libre semanal {weekly_free:.0f}%. "
        f"Libre 5h {primary_free:.0f}%. "
        f"Resets 5h antes del cierre: {primary_resets_left}. "
        f"Estado: {status}."
    )

def build_primary_message(s: Snapshot, level: int) -> str:
    primary_free = max(0.0, 100.0 - s.primary_used)
    reset_in = max(0, s.primary_resets_at - int(datetime.now(timezone.utc).timestamp()))
    return (
        f"Codex 5h alto uso: {s.primary_used:.0f}% usado, {primary_free:.0f}% libre. "
        f"Reset en {fmt_hours(reset_in)}."
    )

def send_telegram(message: str) -> None:
    subprocess.run([str(TELEGRAM_NOTIFY), message], check=True)

def maybe_emit_alerts(s: Snapshot, state: dict[str, Any]) -> list[str]:
    sent: list[str] = []
    now = datetime.now(timezone.utc).timestamp()
    try:
        snapshot_ts = datetime.fromisoformat(s.ts.replace("Z", "+00:00")).timestamp()
    except Exception:
        snapshot_ts = now
    if now - snapshot_ts > MAX_SNAPSHOT_AGE_HOURS * 3600:
        state["stale_snapshot"] = {"ts": s.ts, "age_hours": round((now - snapshot_ts) / 3600, 1)}
        return sent
+    state.pop("stale_snapshot", None)
    state.setdefault("weekly", {})
    state.setdefault("primary", {})
    weekly_reset_key = str(s.secondary_resets_at)
    primary_reset_key = str(s.primary_resets_at)

    hours_left = max(0.0, s.secondary_resets_at - now) / 3600
    weekly_free = max(0.0, 100.0 - s.secondary_used)
    primary_free = max(0.0, 100.0 - s.primary_used)

    weekly_state = state["weekly"]
    if weekly_state.get("reset") != weekly_reset_key:
        weekly_state.clear()
        weekly_state["reset"] = weekly_reset_key
        weekly_state["sent"] = []
    weekly_sent = set(weekly_state.get("sent") or [])
    for threshold_hours, threshold_remaining in sorted(WEEKLY_THRESHOLDS, key=lambda x: x[0]):
        key = f"{threshold_hours}h_{threshold_remaining}pct"
        if key in weekly_sent:
            continue
        if hours_left <= threshold_hours and weekly_free >= threshold_remaining:
            sent.append(build_weekly_message(s, hours_left, threshold_hours, threshold_remaining))
            weekly_sent.add(key)
            break
    weekly_state["sent"] = sorted(weekly_sent)
    weekly_state["last_weekly_free"] = weekly_free
    weekly_state["last_primary_free"] = primary_free

    primary_state = state["primary"]
    if primary_state.get("reset") != primary_reset_key:
        primary_state.clear()
        primary_state["reset"] = primary_reset_key
        primary_state["sent"] = []
    primary_sent = set(primary_state.get("sent") or [])
    for level in sorted(PRIMARY_WARN_LEVELS, reverse=True):
        key = str(level)
        if key in primary_sent:
            continue
        if s.primary_used >= level:
            sent.append(build_primary_message(s, level))
            primary_sent.add(key)
            break
    primary_state["sent"] = sorted(primary_sent, key=int)
    primary_state["last_primary_used"] = s.primary_used

    state["last_snapshot"] = {
        "ts": s.ts,
        "primary_used": s.primary_used,
        "primary_free": primary_free,
        "primary_resets_at": s.primary_resets_at,
        "secondary_used": s.secondary_used,
        "secondary_free": weekly_free,
        "secondary_resets_at": s.secondary_resets_at,
        "plan_type": s.plan_type,
    }
    return sent

def print_status(s: Snapshot | None, state: dict[str, Any]) -> None:
    print(json.dumps({
        "snapshot": None if s is None else {
            "ts": s.ts,
            "primary_used": s.primary_used,
            "primary_window_min": s.primary_window_min,
            "primary_resets_at": s.primary_resets_at,

[36 more lines in file. Use offset=221 to continue.]


Successfully replaced 1 block(s) in /home/sebas/pi-config/bin/codex-usage-alert.

     ...
 152         snapshot_ts = now
 153     if now - snapshot_ts > MAX_SNAPSHOT_AGE_HOURS * 3600:
 154         state["stale_snapshot"] = {"ts": s.ts, "age_hours": round((now - snapshot_ts) / 3600, 1)}
 155         return sent
-156 +    state.pop("stale_snapshot", None)
+156     state.pop("stale_snapshot", None)
 157     state.setdefault("weekly", {})
 158     state.setdefault("primary", {})
 159     weekly_reset_key = str(s.secondary_resets_at)
 160     primary_reset_key = str(s.primary_resets_at)
     ...

--- /home/sebas/pi-config/bin/codex-usage-alert
+++ /home/sebas/pi-config/bin/codex-usage-alert
@@ -152,9 +152,9 @@
         snapshot_ts = now
     if now - snapshot_ts > MAX_SNAPSHOT_AGE_HOURS * 3600:
         state["stale_snapshot"] = {"ts": s.ts, "age_hours": round((now - snapshot_ts) / 3600, 1)}
         return sent
-+    state.pop("stale_snapshot", None)
+    state.pop("stale_snapshot", None)
     state.setdefault("weekly", {})
     state.setdefault("primary", {})
     weekly_reset_key = str(s.secondary_resets_at)
     primary_reset_key = str(s.primary_resets_at)


bash

/home/sebas/pi-config/bin/codex-usage-alert --dry-run && /home/sebas/pi-config/bin/codex-usage-alert --status


rm -f /home/sebas/runtime/codex-usage-alert/state.json
/home/sebas/pi-config/bin/codex-usage-alert --status


**Considering timer installation**

I think I might need to install a timer now. I need to check how to use systemctl for user services. This seems like a straightforward task, but I want to make sure I'm doing it correctly. Should I look up specific commands for that? I feel like I might be overlooking something, so let me double-check the steps needed to ensure the process goes smoothly. Let’s figure this out!

cd /home/sebas/pi-config && ./scripts/install-codex-usage-alert.sh


Created symlink '/home/sebas/.config/systemd/user/timers.target.wants/codex-usage-alert.timer' → '/home/sebas/.config/systemd/user/codex-usage-alert.timer'.
● codex-usage-alert.timer - Run Codex usage alert checker periodically
     Loaded: loaded (/home/sebas/.config/systemd/user/codex-usage-alert.timer; enabled; preset: enabled)
     Active: active (running) since Thu 2026-06-04 10:26:03 -03; 9ms ago
 Invocation: 226a296485a8407487c3bee5feb08edc
    Trigger: n/a
   Triggers: ● codex-usage-alert.service

Jun 04 10:26:03 ballbox-first systemd[1289]: Started codex-usage-alert.timer - Run Codex usage alert checker periodically.


systemctl --user start codex-usage-alert.service && systemctl --user status --no-pager codex-usage-alert.service | sed -n '1,40p' && /home/sebas/pi-config/bin/codex-usage-alert --status


○ codex-usage-alert.service - Codex usage alert checker
     Loaded: loaded (/home/sebas/.config/systemd/user/codex-usage-alert.service; static)
     Active: inactive (dead) since Thu 2026-06-04 10:26:06 -03; 10ms ago
 Invocation: 952f2a4b81494f85a9121c863c41440b
TriggeredBy: ● codex-usage-alert.timer
    Process: 637274 ExecStart=/usr/bin/env python3 /home/sebas/pi-config/bin/codex-usage-alert (code=exited, status=0/SUCCESS)
   Main PID: 637274 (code=exited, status=0/SUCCESS)
        CPU: 71ms

Jun 04 10:26:06 ballbox-first systemd[1289]: Starting codex-usage-alert.service - Codex usage alert checker...
Jun 04 10:26:06 ballbox-first systemd[1289]: Finished codex-usage-alert.service - Codex usage alert checker.
{
  "snapshot": {
    "ts": "2026-03-23T17:30:47.913Z",
    "primary_used": 4.0,
    "primary_window_min": 300,
    "primary_resets_at": 1774288853,
    "secondary_used": 77.0,
    "secondary_window_min": 10080,
    "secondary_resets_at": 1774460760,
    "plan_type": "plus"
  },
  "state": {
    "stale_snapshot": {
      "age_hours": 1747.9,
      "ts": "2026-03-23T17:30:47.913Z"
    }
  }
}


todo

systemctl --user enable --now codex-usage-alert.timer

Telegram alerts for 5h high usage and weekly underuse near reset; stale snapshots older than 12h do not alert.


Updated #2 (in_progress → completed)

Inspect Codex limit handling and propose unused-usage alerts

Find current Codex usage/limit tracking in local config or tools, understand daily and weekly limits, then propose alert logic that accounts for both windows so alerts are actionable.

Codex session logs expose rate_limits. Observed primary window_minutes=300 and secondary window_minutes=10080 on plan_type=plus. Proposal should treat primary as rolling 5h cap, not calendar day, and gate weekly-unused alerts by primary headroom / reset opportunities.

Implement Codex usage alerts to Telegram

Add a checker that reads Codex local session rate_limits, computes actionable 5h/7d alerts, and sends alerts to Telegram.


Listo.

Hecho:
- agregué checker:
  `~/pi-config/bin/codex-usage-alert`
- manda alertas por Telegram
- instalé timer user:
  `codex-usage-alert.timer`
- corre cada 30 min

Alertas:
- 5h alto uso:
  - 80 / 90 / 95%
- semanal desaprovechado:
  - 30h si queda >=35%
  - 15h si queda >=20%
  - 8h si queda >=10%
- incluye:
  - libre semanal
  - libre 5h
  - resets 5h que quedan
  - `actionable_now` o `blocked_by_5h`

Importante:
- ahora no está alertando porque el último snapshot Codex local está viejo
- dejé guard:
  - si el dato tiene >12h, no manda basura a Telegram

Estado:
- timer activo
- install metido en bootstrap también

Chequeos:
- `systemctl --user status codex-usage-alert.timer`
- `~/pi-config/bin/codex-usage-alert --status`

Si querés, siguiente paso:
- fuerzo un test real a Telegram con mensaje dummy
- o ajusto thresholds/frecuencia.


/new


queued new Telegram Pi session: pi-telegram