Take the cap reset time as an epoch from the agent, not parsed off its screen - #181
Merged
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
capagent verb prints whenthe 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,
extractQuotaStatusFromErrorbuilds therate-limit info from the response's
anthropic-ratelimit-unified-*headers withstatus: "rejected"andresetsAttaken fromunified-reset, callsemitStatusChange, and the print-mode listener enqueues arate_limit_eventonto 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) isinternal, with no
claude usagesubcommand exposing it. The probe measured at$0.0177 and ~4s per call, mostly cache creation.
The shape
capagent verb (voro-core/src/agent.rs): opaque likelogs, and theonly 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 forcodex.CapWindow::resolve(voro-core/src/cap.rs): the one place the account'sinstant 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_epochreadsthe verb's output, refusing any number that is not a timestamp near the
present.
AccountCapProbe(voro/src/probe.rs): keyed by agent rather than bytask, 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.
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 warningsandcargo fmt --checkclean. 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
logsverb says only "Weekly limit reached" — no time on screen at all — andwhose
capverb prints an instant. The row badged⚠ capped ↻23:42, flipped to⚠ capped · reset passedwhen the instant was behind the clock, andureleasedthe session (
stopfired atref-1), sent continue, and dropped the badge.The real built-in template was also run through
shexactly 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-453and touches the same threefunctions (
CapReading,reset_passed, the badge and the sweep) to addretrying. 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::resolvealongside the precedence rule once both exist.