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
@@ -33,6 +33,13 @@
|
||||
//! `/usage 42 never`, `/usage notloggedin`, `/usage unreachable`,
|
||||
//! `/usage failed`. The vocabulary is `usage::Fixture`'s, where the states
|
||||
//! live.
|
||||
//! - `/limit [minutes]` -- a turn that stops because the account is out of
|
||||
//! quota, saying the limit lifts in `minutes` (default 5, and `never` for a
|
||||
//! limit with no stated reset). What it exists for is auto-resume, which is
|
||||
//! otherwise reachable only by actually exhausting somebody's account: pair
|
||||
//! it with `/usage 100 5` for a meter that agrees, and then `/usage 20` for
|
||||
//! the moment the limit lifts. The wait itself is decided by the meter, so
|
||||
//! those two commands are the whole rig.
|
||||
//! - `/compact` -- a compaction, start to finish.
|
||||
//! - `/stream N` -- one long answer in N small pieces, 50ms apart: the shape a
|
||||
//! real model's reply arrives in, and the one where the row a reader is
|
||||
@@ -344,6 +351,39 @@ impl EchoDriver {
|
||||
return;
|
||||
}
|
||||
|
||||
// A turn that ends the way a real one does when the account runs out:
|
||||
// the same event a real driver reports, so what acts on it -- the
|
||||
// transcript row and `crate::resume` -- is exercised rather than
|
||||
// imitated. The meter it should agree with is `/usage`'s fixture,
|
||||
// deliberately separate: the two disagreeing is a state worth being
|
||||
// able to produce, since it is what a stale reset time looks like.
|
||||
if let Some(rest) = text.strip_prefix("/limit") {
|
||||
if announce {
|
||||
self.emit(Event::MessageTaken {
|
||||
id: None,
|
||||
text: text.clone(),
|
||||
attachments,
|
||||
});
|
||||
}
|
||||
let rest = rest.trim();
|
||||
let resets_at = match rest {
|
||||
"never" | "none" => None,
|
||||
"" => Some(super::now() + 5.0 * 60.0),
|
||||
minutes => Some(super::now() + minutes.parse::<f64>().unwrap_or(5.0) * 60.0),
|
||||
};
|
||||
self.emit(Event::Status {
|
||||
state: SessionStatus::Running,
|
||||
});
|
||||
self.emit(Event::AssistantText {
|
||||
delta: "Working on it".to_string(),
|
||||
});
|
||||
self.emit(Event::LimitReached { resets_at });
|
||||
self.emit(Event::Status {
|
||||
state: SessionStatus::Idle,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// The same word the real CLI takes, so a phone drives both the same way.
|
||||
// `Driver::compact` is what the manager's route calls; this is the typed
|
||||
// path onto it.
|
||||
|
||||
Reference in new issue
Block a user