# Usage awareness: knowing when to stop, without lying about it

## TLDR/BLUF

**What this is.** The design, failure modes and rules for a hook that warns a session when the plan
pool it bills is running out.

**Why you should care.** The lessons are the sharpest instance in this repo of its recurring theme:
**an instrument that answers a narrower question than the one you asked, while looking completely
healthy.** Not for you as code -- **no such hook ships here**, because the mechanism depends on client
internals that are undocumented and can change without notice.

**How to use it.** Take the rules, not the implementation; those transfer and the code does not.
Start with the purpose, which is preventing lost work at a hard cutoff and is not budgeting.

---

A hook that warns a session when the plan pool it bills is running out is much harder to get right
than it looks. **No such hook ships in this repo** -- the mechanism depends on client internals that
are undocumented and can change without notice, so shipping one would be shipping a control that
breaks silently. What ships is the design, the failure modes, and the rules, because those transfer
and the code does not.

The lessons here are the sharpest instance in this repo of its recurring theme: **an instrument that
answers a narrower question than the one you asked, while looking completely healthy.**

---

## Why you want this at all

The purpose is **preventing lost work at a hard cutoff**. It is not budgeting.

That distinction decides everything downstream. A budgeting tool wants accuracy and can be wrong
quietly. A lost-work tool wants to fire *before* an agent is mid-refactor with nothing committed, and
must **refuse to report** rather than report wrongly, because the whole point is that somebody acts on
it.

When it fires, the correct response is not "stop". It is:

1. Commit what exists, even if partial. A checkpoint commit beats a clean tree you no longer have.
2. Push branches, or otherwise get the work somewhere that survives the session ending.
3. Write a handoff for whoever picks it up, including yourself in four hours.

## Rule 1: a percentage is meaningless without its account

This is the failure that forced the design.

An early version hardcoded which account to read. It reported **93 percent weekly** into a session
whose actual pool was at **5 percent**. Both the number and the account name were confident, formatted,
and wrong.

> **A hook that is confidently wrong is worse than no hook. It converts "I should check" into
> "I already know."**

And the obvious repair is not a repair. Pointing the hardcoded value at a different account does not
fix it; it relocates the same lie. A machine with several logins, where the client switches between
them, has no correct constant: any hardcoded account is wrong for every session on the others, and
wrong for all of them after the next switch.

**Resolve the pool per session, from the surface that actually knows which login the session bills.**
If you cannot resolve it, say so.

## Rule 2: several signals look authoritative and are wrong

Each of these was checked and each was wrong for the case it appeared to answer:

| Signal | Why it is wrong |
|---|---|
| The CLI's stored login | It is the *CLI's* login. A desktop session can bill a different account entirely, and did. |
| A cached utilization figure beside it | Wrong account **and** hours stale. Two independent defects in one field. |
| The CLI's credentials file | The CLI again. Same category error. |
| The per-session record on disk | Carries no account field at all, so it cannot answer the question. |
| Token-file modification times | They track whichever account the tool last polled, so they point at its own most recent behavior. **A signal derived from your own tool's activity is a mirror, not a measurement.** |

That last row is the general one. If your evidence for "which account is this" is a side effect of
your own polling, you have built a loop that confirms whatever it did last.

## Rule 3: cross-check against an independent sample, and refuse when they disagree

The reading is checked against a second, independently maintained sample for the same organization.
If the two disagree, the result is **UNKNOWN**, not the number.

Two design details matter more than they look:

- **Check the slow-moving figure, not the fast one.** The weekly percentage moves by single points per
  hour. The short-window figure does not: it drops to near zero the instant its window rolls, and was
  observed going from 22 percent to 2 percent across a single sample. Cross-checking on the fast
  figure would produce constant false disagreement; cross-checking on the slow one catches a
  wrong-account reading by construction. The original bug -- one account's 93 against the other's 5 --
  would have been caught on the first run.
- **Sample age is a validity condition.** Past a few missed sampling intervals, the second source is
  no longer evidence. Old enough, and it must stop being treated as a check at all.

## Rule 4: never print a band beside an account unless you established both

An UNKNOWN result names the pool by an opaque identifier and says the usage could not be determined.
It never names a login as "this session's" on the strength of a token file.

Half-established results are where confident wrongness comes from. If you know the account but not the
number, say that. If you know a number but not whose it is, that number is unusable -- do not print it
next to a name to make the output look complete.

## Rule 5: the diagnostics are the payload when something fails

A summary filter kept only lines containing the words for the two window names. It silently dropped
every refusal message the underlying tool produced -- which were exactly the sentences explaining
*why* the reading failed -- leaving an UNKNOWN with an empty reason.

**A filter written for the success case will strip the failure case.** When you filter output, check
what a failing run actually prints before deciding what to keep.

## Rule 6: a warning path must not be able to kill itself

Three constraints, each learned:

- **Never block or error a prompt.** Every path exits 0. A usage warning that breaks a session is
  worse than the cutoff it warns about.
- **ASCII output only.** A `UnicodeEncodeError` on print gets swallowed by the never-throw guard, so
  the warning vanishes *exactly when it was needed*. See [TIPS-AND-TRICKS.md](TIPS-AND-TRICKS.md) for
  why this is a general rule and not a style preference.
- **Persist the failure state too, with a short TTL.** An earlier version re-ran a 25-second
  subprocess on every prompt during an upstream outage, inside a hook with a 30-second budget. The
  failure mode of "retry until it works" is a hook that times out forever.

## Rule 7: cache per pool, never in one unlabeled slot

A single shared cache slot lets the first writer in a refresh window define what every later reader
reports, whatever account that reader asked about. Key the cache by pool. An unlabeled cache is an
unlabeled claim.

## Rule 8: a threshold is not a decision

**The one most likely to bite you, and the one two sessions here got wrong on the same day.**

A percentage alone cannot answer "should I stop". **7 percent remaining with 7 minutes until the
window resets is abundant. The same 7 percent with four hours left is scarce.** A rule that fires on
the number alone will pause work that had no reason to pause, and will fail to fire when the number is
comfortable but the reset is far away.

A peer instructed a pause at a threshold, then retracted it after checking the clock: the window was
minutes from resetting, which made the remaining budget effectively unlimited. Their own summary is
the better statement of it:

> The priority was right for a different reason than the one I gave. I conflated a genuine loss risk
> with a usage threshold and used the threshold to justify the priority.

So: **evaluate the number together with its time-to-reset, and say which one drove the decision.**
And note the corollary, which is the same shape as Rule 1: a percentage without its *account* does not
answer the question either. A usage figure needs three things to mean anything -- the number, whose
pool it is, and when the window rolls.

---

## Platform trap worth its own line

On Windows, a directory can be **visible to an interpreter launched by full path and invisible to the
same interpreter launched through an app-execution alias**, because of installer-level AppData
virtualization. A hook that works when you test it by hand can read an empty directory when the client
runs it.

Wire hooks by full interpreter path, and make a missing path report UNKNOWN rather than OK. "I could
not find the data" and "the data says you are fine" must never produce the same output.

## Related

- [TIPS-AND-TRICKS.md](TIPS-AND-TRICKS.md) -- the general form of most of the above
- [HOOKS.md](HOOKS.md) -- fail-open versus fail-closed, and declaring which you chose
- [CASE-STUDY-drift-audit.md](CASE-STUDY-drift-audit.md) -- auditing controls that look installed
