Skip to content

Take the cap reset time as an epoch from the agent, not parsed off its screen - #181

Merged
MJohnson459 merged 4 commits into
mainfrom
cap-epoch-441
Aug 20, 2026
Merged

Take the cap reset time as an epoch from the agent, not parsed off its screen#181
MJohnson459 merged 4 commits into
mainfrom
cap-epoch-441

Conversation

@MJohnson459

Copy link
Copy Markdown
Contributor

The capped badge read "resets 6:40pm" off a session's terminal capture and
resolved it to whichever occurrence was nearest, because a bare clock time
carries no date and is ambiguous by half a day either way. The same quantity is
available exactly, and this takes it: an optional cap agent verb prints when
the account's usage window reopens as a Unix epoch, and where a reading exists
it decides both the time on the badge and whether the window has reopened.

The blocking question, settled

It rested on whether the probe still emits the event when the account is
capped, and the answer is yes — established from the CLI's own code rather than
by waiting for a cap to land. On a 429, extractQuotaStatusFromError builds the
rate-limit info from the response's anthropic-ratelimit-unified-* headers with
status: "rejected" and resetsAt taken from unified-reset, calls
emitStatusChange, and the print-mode listener enqueues a rate_limit_event
onto the stream-json output. A usage cap is also non-retryable (settled by #453
at the same source), so that happens on the first refusal rather than after a
retry loop — the event lands promptly in exactly the case it is wanted.

Two things could not be established that way and are worth knowing: the in-binary
rate-limit simulator is dead code in shipped builds, so a cap cannot be faked
locally; and the CLI's own cheap quota check (max_tokens: 1, headers only) is
internal, with no claude usage subcommand exposing it. The probe measured at
$0.0177 and ~4s per call, mostly cache creation.

The shape

  • cap agent verb (voro-core/src/agent.rs): opaque like logs, and the
    only verb that names nothing — a cap belongs to the account, not to any one
    conversation, so one reading answers for every session on the strip. Every
    placeholder is refused at config-validation time. Its negative answer is
    silence: print the instant while the account is refused, print nothing
    otherwise, so an uncapped account, a failed command and an agent defining none
    all land identically on the old parse. Built in for claude, absent for
    codex.
  • CapWindow::resolve (voro-core/src/cap.rs): the one place the account's
    instant and the session's parsed clock time resolve into the single answer the
    badge and the nudge sweep both read, so the two cannot disagree about which
    source spoke. The instant wins where there is one. parse_reset_epoch reads
    the verb's output, refusing any number that is not a timestamp near the
    present.
  • AccountCapProbe (voro/src/probe.rs): keyed by agent rather than by
    task, and the only runner whose debounce guards money rather than latency. It
    is asked only while a session is already badged capped, once per capped
    episode — once more only if the instant it named has since passed while the
    badge is still up — with a ten-minute floor as a backstop. A reading that came
    back empty is held too, which is what stops a session capped on a limit the
    probe does not hit from buying a call every interval.
  • The badge renders identically; what changed is that "reset passed" is now a
    fact rather than a nearest-occurrence guess, and a cap whose wording named no
    time at all gets one. That last part closes the gap #437 would otherwise have
    to work around: the sweep's untimed-cap rule ("the operator's judgement stands
    in for the clock") retires one session at a time as the account times them.

One caveat is priced in DESIGN.md rather than guarded: an account is refused by
one window while a session may be held by another (a weekly model limit behind
a five-hour account cap), so the instant can be early for that session — which is
the failure §8 already prices as cheap, a nudge that re-caps at once.

Verification

cargo test --workspace — 900 pass (500 voro + 399 voro-core + 1 integration),
cargo clippy --workspace --all-targets -- -D warnings and cargo fmt --check
clean. New tests cover the epoch parse and its plausibility band, the precedence
both ways, the sweep's due rule, the probe's debounce including the held-empty
case, the verb through a real subprocess, and the config validation.

Driven end-to-end in the TUI against a scratch store, with a stub agent whose
logs verb says only "Weekly limit reached" — no time on screen at all — and
whose cap verb prints an instant. The row badged ⚠ capped ↻23:42, flipped to
⚠ capped · reset passed when the instant was behind the clock, and u released
the session (stop fired at ref-1), sent continue, and dropped the badge.
The real built-in template was also run through sh exactly as Voro renders it:
it prints nothing while the account has room, which is the uncapped answer.

Note for merge

Task #453 is in flight on cap-retry-reading-453 and touches the same three
functions (CapReading, reset_passed, the badge and the sweep) to add
retrying. The two are additive to each other but will conflict textually —
land #453 first and rebase this on it; the retry flag belongs inside
CapWindow::resolve alongside the precedence rule once both exist.

…s screen

The capped badge read "resets 6:40pm" off a session's terminal capture and
resolved it to whichever occurrence was nearest, because a bare clock time
carries no date and is ambiguous by half a day either way. The same quantity is
available exactly: a cap is a property of the account, and Claude Code's stream
transport emits a rate_limit_event carrying resetsAt in Unix epoch seconds.

Adds an optional `cap` agent verb, an opaque contract like `logs` and the only
one that names nothing — no {session}, because one reading answers for every
session on the strip. Its negative answer is silence, so an uncapped account, a
failed command and an agent defining none all land identically on the old parse.
Where a reading exists it decides the badge's time and whether the window has
reopened; CapWindow is where the two sources resolve, so the badge and the nudge
sweep cannot disagree about which one spoke.

The verb spends an API call rather than a screen replay, so it is gated on a
badge already being on the strip, asked once per capped episode, and keyed by
agent — several capped sessions under one agent are one question.

Settled first, from the CLI's own code, the question this rested on: a capped
account does still emit the event. extractQuotaStatusFromError builds the info
from a 429's anthropic-ratelimit-unified-* headers with status "rejected" and
resetsAt intact, calls emitStatusChange, and the print-mode listener enqueues it
onto the stream — and a usage cap is non-retryable, so the turn ends at once
rather than after a retry loop.

Verified: cargo test --workspace (900 pass), clippy and fmt clean. Driven
end-to-end in the TUI against a scratch store with a stub agent whose logs verb
says only "Weekly limit reached" — no time on screen — and whose cap verb prints
an instant: the row badges "capped ↻23:42", flips to "capped · reset passed"
when the instant is behind, and `u` releases and nudges it. The real built-in
template was run through sh as Voro renders it, and prints nothing while the
account has room.
An account does not have one usage window. A Claude subscription meters the
five-hour pool, the weekly one, and a separate allowance for each strong model —
"Opus limit", "Sonnet limit", "Fable 5 limit" in the CLI's own wording, with fast
mode drawing on its own pools again. A reading taken with one model therefore
answers about that model's window, and answers earlier than the truth whenever a
cheaper pool reopens first: exactly the error that would fire a nudge into a
session still held.

So `cap` may carry {model} — the only placeholder it may carry — and Voro binds
the model the session launched under, resolved through the same rule the launch
used (model_for_depth, now shared with launch_command so the two cannot drift).
The reading is keyed by the rendered question rather than by the agent, which is
what makes both shapes fall out without either being special: a template naming
{model} is asked once per model in flight, one that names none is asked once for
the agent, and sessions asking the same thing share an answer.

Asking on the session's own model also makes the case that matters free — a
refused request is a rejection rather than a turn and bills nothing — so the only
probe that costs is one that finds the account healthy, and that one prints
nothing.

Verified: cargo test --workspace (902 pass), clippy and fmt clean. Driven in the
TUI with a stub whose cap verb answers per model: a deep task's row badged the
deep model's window (01:12) rather than the workhorse's earlier one (23:22),
which is the reading it would have taken before this change.
Resolves the conflict with #453, which landed `CapReading::retrying` in the same
three functions this branch rewrites. The resolution is not textual: a retrying
session must take no account instant either, since it is mid-turn on its own
request rather than waiting on the window, and an instant that had passed would
mark it due exactly when it is least safe to touch.

- `CapWindow` carries `retrying` through, `resolve` returns early on it, and
  `due` answers false for it — so a caller reading `due` alone cannot miss the
  rule the sweep already knew.
- The badge keeps all four shapes, now off the resolved window.
- The account probe is gated on a reading that is *held*, so a retrying session
  buys no API call.
Takes the v0.2.0 editing pass (#184), which hard-wrapped DESIGN.md, stripped
task-number citations from code comments, and cut comments that narrate what a
change replaced. Conflicts were the re-wrap crossing this branch's new prose and
the test section header it renamed.

DESIGN.md is taken from main wholesale and this branch's three edits re-applied
into the wrapped text, so the paragraph structure stays main's. The new comments
follow the pass's conventions: no task numbers, DESIGN.md section pointers kept,
and the two spots that said "before this" rewritten to say what the code is for.
@MJohnson459
MJohnson459 merged commit 822233f into main Aug 20, 2026
7 checks passed
@MJohnson459
MJohnson459 deleted the cap-epoch-441 branch August 20, 2026 11:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant