Files
agent-skills/skills/nightshift/usage-window.py
T
naps62-yolo 3163bd26f0 feat(nightshift): long autonomous build skill with limit backoff
Takes an issue, ticket or plain description and works it for hours as an
architect: freeze the interface contract first, then delegate implementation
to subagents against frozen signatures, review, commit every milestone.

Checks the rolling 5-hour window before each dispatch and parks rather than
letting agents die mid-task. The usage script sums every Claude Code session
on the machine, and is explicit that it is a floor — it cannot see other
machines, claude.ai, or direct API use — so the reactive backstop of an
agent dying on a limit error is documented as the stronger signal.

Encodes what worked in practice: disjoint file ownership per agent, asking
agents to report disagreement (their pushback was the highest-value output),
sabotage-testing every safety net, and asking what a passing check is
structurally unable to see.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SSXDKsik8yYrezStVaDXt
2026-07-25 13:08:41 +00:00

180 lines
6.4 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.
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)
"""
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"
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 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 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",
f" tokens {report['tokens']:,} ({report['outputTokens']:,} output) "
f"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(
" FLOOR, not a quota: misses other machines, claude.ai and direct API use"
)
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")
args = parser.parse_args()
report = collect(args.hours)
if args.json:
print(json.dumps(report, indent=2))
else:
print(human(report))
if __name__ == "__main__":
main()