Let a session resume itself when its usage limit lifts
Off by default and per session: it spends quota the moment quota exists,
with nobody watching, which is not a thing a default may decide. Switched
on from the session settings dialog, with the message it sends editable
("continue" unless something else is typed).
Running out of quota becomes a state rather than an error. The Claude
driver recognises its dialect's sentence -- `Claude AI usage limit
reached|1788546972` -- and reports `LimitReached` with the reset time it
gave; nothing above a driver matches on a string. The transcript draws it
as a divider, like a clear or a compaction.
The schedule is a plan to *ask*, never a plan to send. Both reset times
available are untrustworthy in the direction that matters -- the dialect's
is written when the turn fails, the endpoint's moves when the window does
-- so the wait ends in a question to the usage meter, and only `ok` with
no window at 100% sends anything. A window still spent reschedules to its
own reset time, which is what makes a limit that lifts late wait longer
and one that lifts early resume sooner. A meter that cannot be asked is a
longer wait too, never a send. A day after the limit was hit the wait
gives up and says so in the transcript, so a machine that can never be
asked is not retried for ever.
The schedule is persisted on the session: a five-hour window outlasts a
backend restart, and a wait forgotten across one never comes back.
Driven end to end with echo, never a real account: `/limit [minutes]`
reports the same event a real driver does and `/usage` sets what the meter
answers, deliberately separate so the two can disagree. The wait moved
from the dialect's two minutes to the meter's seven when the meter changed
its mind, and the message went out on the first check after the meter came
back under the limit.
Also makes the settings dialog scrollable, which these two controls made
necessary: at a 1.5x system font it clipped the last of them with nothing
on screen to say so.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4821a02bd3
commit
6bdec6e785
17 files changed
+1405
-20
No files matched your search
@@ -596,6 +596,54 @@ always running. So absent means **not running**, and only a timestamp that
|
||||
arrives and cannot be parsed is unknown. `WindowEnd` in `ResetCountdown.kt`
|
||||
is the one rule both readers go through.
|
||||
|
||||
### Auto-resume (2026-09-05)
|
||||
|
||||
**A session may pick itself back up when the account's usage limit lifts.**
|
||||
Off unless somebody switched that session to it, because it spends quota the
|
||||
moment quota exists and does so with nobody looking — that is not a thing a
|
||||
default may decide. It sends one message, `continue` unless another was
|
||||
typed, and then it is done; there is no retry loop around the conversation
|
||||
itself.
|
||||
|
||||
**Running out of quota is a state, not an error.** `Event::LimitReached`
|
||||
carries the dialect's reset time where it gave one, and recognising it
|
||||
belongs to the driver — the Claude CLI ends the turn with `is_error` and
|
||||
`Claude AI usage limit reached|1788546972`, and nothing above the driver
|
||||
matches on a string. The transcript draws it as a divider, like a clear or a
|
||||
compaction: what a reader scrolling back wants from it is why the
|
||||
conversation stops at that line.
|
||||
|
||||
**The schedule is a plan to ask, never a plan to send.** Every reset time
|
||||
available here is untrustworthy in the direction that matters: the dialect's
|
||||
is written when the turn fails, and the endpoint's moves when the window
|
||||
does. So the wait ends in a question to `usage.rs`, and only `ok` with no
|
||||
window at 100% sends anything. A window still spent reschedules to *its own*
|
||||
reset time — which is what makes a limit that lifts later than promised wait
|
||||
longer, and one that lifts sooner resume sooner. A meter that cannot be
|
||||
asked at all is a longer wait too, never a send: "we could not find out"
|
||||
must not be able to produce the same action as "there is room".
|
||||
|
||||
Bounded, because something has to be: a day after the limit was hit the wait
|
||||
stops and says so in the session's own transcript. A machine that can never
|
||||
be asked would otherwise be retried for ever with nothing on screen saying
|
||||
so.
|
||||
|
||||
The schedule is persisted on the session (`resume: Some(ScheduledResume)`),
|
||||
not held in memory: a five-hour window routinely outlasts a backend restart,
|
||||
and a wait forgotten across one is a session that silently never comes back.
|
||||
`resume.rs` is the top layer — it holds the manager and the monitor and
|
||||
neither holds it — which is what lets the decision be a pure function of a
|
||||
snapshot and a clock. The pump reports limits downward on a broadcast, for
|
||||
the reason `Shared` exists: the pump runs underneath the manager.
|
||||
|
||||
**Exercised with echo, never with a real account.** `/limit [minutes]` in an
|
||||
echo session reports the same event a real driver does, and `/usage` sets
|
||||
what the meter answers — deliberately two commands, because the two
|
||||
disagreeing is the state the whole design is about. The loop was driven end
|
||||
to end that way on 2026-09-05: the wait moved from the dialect's two minutes
|
||||
to the meter's seven when the meter changed its mind, and the message went
|
||||
out on the first check after the meter came back under the limit.
|
||||
|
||||
### HTTP surface
|
||||
|
||||
**`routes.rs`'s module doc comment is the table.** REST for actions, one SSE
|
||||
|
||||
Reference in new issue
Block a user