Files
agent-skills/skills/nightshift/usage-window.py
T
naps62 715420d0a9 feat(nightshift): calibrated quota verdict + scoped milestone checks
usage-window.py stores a calibrated window quota and emits an explicit
verdict, so a run can no longer park on an uncalibrated raw token count.
Milestone assess is scoped; full suite moves to the gated push points.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015YUXS63P1WCdC6bEKWcnAE
2026-07-27 12:45:06 +00:00

347 lines
13 KiB
Python
Executable File

#!/usr/bin/env python3
"""Estimate token usage inside the rolling 5-hour limit window.
The real quota is server-side and nothing local states it. What IS local is
every assistant message Claude Code has written, with a timestamp and a usage
block, in ~/.claude/projects/**/*.jsonl. Summing those over a rolling window
gives a usable proxy: not the quota, but a consistent measure of how hard the
window has been worked, plus when the oldest tokens in it age out.
A RAW TOKEN COUNT IS NOT A SIGNAL. It is a numerator. "4.8 million" sounds
alarming and means nothing without the denominator — on a large plan that can
be 30% of the window. Decisions about backing off MUST be made on percentage,
which means the quota has to be calibrated at least once. See --calibrate.
SCOPE — read this before trusting the number:
Counted: every Claude Code session on THIS machine, all projects, not
just the session asking. Parallel sessions in other repos land
in the same window and this picks them up.
NOT counted: Claude Code on any other machine, claude.ai web usage, direct
API calls, and anything else that never writes a transcript here.
So the figure is a floor, never a ceiling. Real usage is this or higher, and
the gap is however much the account is being used elsewhere. Size your margin
accordingly: if the user works on several machines, treat a "comfortable"
reading with suspicion.
Treat the number as a trend, not a truth. It is here so a long autonomous run
can back off *before* an agent dies mid-task, rather than discovering the limit
by being killed by it. The reactive backstop still matters: an agent that dies
on a limit error is telling you the truth this script only estimates.
Usage:
usage-window.py # human summary
usage-window.py --json # machine readable
usage-window.py --hours 5 # window size (default 5)
usage-window.py --quota 16e6 # one-off quota override
usage-window.py --calibrate 30 # "Claude Code says I'm at 30%" -> store implied quota
Calibration:
Ask the user what percentage their /status (or the Claude Code UI) reports,
then run --calibrate with it. The implied quota is written to
~/.claude/nightshift-quota.json and used by every later run. Re-calibrate
if the plan changes. One data point beats zero; two beats one.
"""
from __future__ import annotations
import argparse
import json
import os
from datetime import datetime, timedelta, timezone
from pathlib import Path
PROJECTS = Path.home() / ".claude" / "projects"
QUOTA_FILE = Path.home() / ".claude" / "nightshift-quota.json"
# Percentage bands. These are about RISK TO IN-FLIGHT WORK, not thrift — the
# window refills continuously, and an unused window is wasted capacity, not
# saved money. The only thing being avoided is agents dying mid-task.
BAND_CLEAR = 60.0 # below this: dispatch freely
BAND_WRAP = 80.0 # below this: keep going, prefer shorter tasks
# at or above BAND_WRAP: finish what's running, commit, push, park
def parse_ts(value: str | None) -> datetime | None:
if not value:
return None
try:
return datetime.fromisoformat(value.replace("Z", "+00:00"))
except ValueError:
return None
def billable(usage: dict) -> int:
"""Tokens that plausibly count against a quota.
Cache reads are excluded: they are the cheap path and counting them would
make a long cached session look far heavier than it is. Cache *creation* is
included, because writing the cache is real work.
"""
return (
int(usage.get("input_tokens") or 0)
+ int(usage.get("cache_creation_input_tokens") or 0)
+ int(usage.get("output_tokens") or 0)
)
def load_quota() -> tuple[int | None, str | None]:
"""Calibrated window quota, if the user has ever supplied one.
Precedence: --quota (handled by caller) > env > stored file > unknown.
Returns (quota, source) so the output can say where the number came from —
an uncalibrated guess must never masquerade as a measurement.
"""
env = os.environ.get("NIGHTSHIFT_WINDOW_QUOTA")
if env:
try:
return int(float(env)), "env NIGHTSHIFT_WINDOW_QUOTA"
except ValueError:
pass
try:
data = json.loads(QUOTA_FILE.read_text(encoding="utf-8"))
quota = int(data["windowTokenQuota"])
at = data.get("calibratedAt", "unknown date")
return quota, f"calibrated {at[:10]}"
except (OSError, KeyError, ValueError, TypeError):
return None, None
def save_quota(quota: int, observed_tokens: int, observed_pct: float) -> None:
QUOTA_FILE.parent.mkdir(parents=True, exist_ok=True)
QUOTA_FILE.write_text(
json.dumps(
{
"windowTokenQuota": quota,
"calibratedAt": datetime.now(timezone.utc).isoformat(),
"observedTokens": observed_tokens,
"observedPercent": observed_pct,
"note": (
"Implied quota = observedTokens / (observedPercent/100). "
"Derived from what Claude Code reported at one moment, so it "
"inherits this script's blind spots (other machines, web, API). "
"Re-run --calibrate after a plan change or if parking decisions "
"start feeling wrong."
),
},
indent=2,
)
+ "\n",
encoding="utf-8",
)
def collect(hours: float) -> dict:
now = datetime.now(timezone.utc)
cutoff = now - timedelta(hours=hours)
total = 0
output = 0
messages = 0
oldest: datetime | None = None
newest: datetime | None = None
per_project: dict[str, int] = {}
if not PROJECTS.is_dir():
return {
"error": f"no transcript directory at {PROJECTS}",
"windowHours": hours,
}
for path in PROJECTS.glob("*/*.jsonl"):
# Cheap skip: a file untouched since the cutoff cannot contribute.
try:
if datetime.fromtimestamp(path.stat().st_mtime, timezone.utc) < cutoff:
continue
except OSError:
continue
project = path.parent.name
try:
with path.open(encoding="utf-8", errors="replace") as handle:
for line in handle:
if '"usage"' not in line:
continue
try:
entry = json.loads(line)
except json.JSONDecodeError:
continue
stamp = parse_ts(entry.get("timestamp"))
if stamp is None or stamp < cutoff:
continue
usage = (entry.get("message") or {}).get("usage")
if not isinstance(usage, dict):
continue
tokens = billable(usage)
if tokens <= 0:
continue
total += tokens
output += int(usage.get("output_tokens") or 0)
messages += 1
per_project[project] = per_project.get(project, 0) + tokens
oldest = stamp if oldest is None or stamp < oldest else oldest
newest = stamp if newest is None or stamp > newest else newest
except OSError:
continue
# When the oldest tokens in the window age out. This is the soonest the
# window measurably loosens; it is NOT a quota reset time, which only the
# server knows.
ages_out_in = None
if oldest is not None:
ages_out_in = max(0, int(((oldest + timedelta(hours=hours)) - now).total_seconds()))
return {
"windowHours": hours,
"tokens": total,
"outputTokens": output,
"messages": messages,
"projects": dict(sorted(per_project.items(), key=lambda kv: -kv[1])),
"oldestInWindow": oldest.isoformat() if oldest else None,
"newestInWindow": newest.isoformat() if newest else None,
"oldestAgesOutInSeconds": ages_out_in,
"now": now.isoformat(),
}
def add_verdict(report: dict, quota: int | None, source: str | None) -> dict:
"""Attach percent-of-quota and an explicit act-on-this verdict.
Without a calibrated quota there is NO verdict — deliberately. An earlier
version of this tool emitted only a raw count, and the reading agent
invented a magnitude threshold to compare it against, then parked a run at
what turned out to be 30% of the window. Saying "unknown" is strictly better
than handing back a number that invites a made-up denominator.
"""
report["quota"] = quota
report["quotaSource"] = source
if not quota or quota <= 0:
report["percentUsed"] = None
report["verdict"] = "unknown"
report["verdictReason"] = (
"No calibrated quota. Percentage is unknown, so the raw token count says "
"NOTHING about how close the window is. Do not park on magnitude. "
"Ask the user what percent Claude Code reports, then run --calibrate."
)
return report
pct = 100.0 * report["tokens"] / quota
report["percentUsed"] = round(pct, 1)
if pct < BAND_CLEAR:
report["verdict"] = "clear"
report["verdictReason"] = "Dispatch freely. The window refills; unused capacity is wasted."
elif pct < BAND_WRAP:
report["verdict"] = "wrap-up"
report["verdictReason"] = (
"Keep working, but prefer shorter tasks over long fan-outs so nothing "
"large is in flight if the window tightens."
)
else:
report["verdict"] = "park"
report["verdictReason"] = (
"Finish what is running, commit, push, write the next step down, and park."
)
return report
def human(report: dict) -> str:
if "error" in report:
return f"usage: unavailable ({report['error']})"
lines = [
f"rolling {report['windowHours']}h window — all local Claude Code sessions",
]
quota = report.get("quota")
pct = report.get("percentUsed")
if quota and pct is not None:
lines.append(
f" usage {report['tokens']:,} / {quota:,} tokens = {pct:.1f}%"
f" [{report['quotaSource']}]"
)
else:
lines.append(f" tokens {report['tokens']:,} (no calibrated quota — percentage UNKNOWN)")
lines.append(
f" detail {report['outputTokens']:,} output over {report['messages']:,} messages"
)
if report["oldestAgesOutInSeconds"] is not None:
secs = report["oldestAgesOutInSeconds"]
lines.append(f" oldest entry ages out in {secs // 60}m{secs % 60:02d}s")
top = list(report["projects"].items())[:3]
if top:
lines.append(" busiest: " + ", ".join(f"{k} {v:,}" for k, v in top))
lines.append(f" sessions: {len(report['projects'])} project(s) contributing")
lines.append(f" VERDICT: {report['verdict']}{report['verdictReason']}")
lines.append(
" FLOOR, not a ceiling: misses other machines, claude.ai and direct API use"
)
if not quota:
lines.append(
" calibrate: ask the user their /status percentage, then "
"usage-window.py --calibrate <pct>"
)
return "\n".join(lines)
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--hours", type=float, default=5.0)
parser.add_argument("--json", action="store_true")
parser.add_argument(
"--quota",
type=float,
default=None,
help="Window token quota for this run only (accepts 16e6). Overrides stored calibration.",
)
parser.add_argument(
"--calibrate",
type=float,
default=None,
metavar="PCT",
help="Percent-used that Claude Code currently reports. Stores the implied quota.",
)
args = parser.parse_args()
report = collect(args.hours)
if args.calibrate is not None:
if "error" in report:
print(human(report))
raise SystemExit(1)
if not 0 < args.calibrate <= 100:
print("calibrate: percentage must be in (0, 100]")
raise SystemExit(2)
implied = int(report["tokens"] / (args.calibrate / 100.0))
save_quota(implied, report["tokens"], args.calibrate)
print(
f"calibrated: {report['tokens']:,} tokens reported as {args.calibrate}% "
f"=> window quota ~{implied:,}\n"
f"written to {QUOTA_FILE}\n"
"Note this inherits the script's blind spots (other machines, web, direct API),\n"
"so the true quota is this or LARGER. Re-calibrate after a plan change."
)
return
if args.quota is not None:
quota, source = int(args.quota), "--quota flag"
else:
quota, source = load_quota()
report = add_verdict(report, quota, source)
if args.json:
print(json.dumps(report, indent=2))
else:
print(human(report))
if __name__ == "__main__":
main()