voro-core 0.2.0

Core logic for Voro: the SQLite store, task state machine, scheduler, and scoring.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
//! Reading a usage cap out of an agent session's own output (DESIGN.md §8).
//!
//! Two callers want the same answer from the same text. The reconciler asks it
//! of a session that has *died*, to record `capped` rather than `failed`; and it
//! asks it of a session that is still *alive*, because a cap does not kill a
//! supervisor-owned launch — the session sits waiting for the window to reset,
//! and without this it rides the running strip looking healthy for hours.
//!
//! Everything here is pure: a string in, a reading out. The process that
//! produces the string — an agent's `logs` verb, or the launch log for an agent
//! that defines none — lives in the `voro` crate, so this side stays testable
//! without a terminal or a subprocess.
//!
//! The text is a *terminal capture*, not a log: the built-in `claude` spelling
//! replays a background session's screen, escape sequences and all, so
//! [`strip_ansi`] runs before any matching. Cursor movement is spatial and
//! becomes a space; colour is not and vanishes, which is what stops a style
//! change mid-phrase from splitting the phrase it is styling.
//!
//! Matching stays deliberately narrow, as it always has: a missed cap reads as
//! an ordinary session, which is the failure everyone already lives with, while
//! a false one would badge healthy work as stuck and teach the operator to
//! disbelieve the badge. That asymmetry is why [`NOT_CAP_QUALIFIERS`] exists —
//! the agent says "approaching" and "not your usage limit" in text that
//! otherwise matches, and both mean the session is working fine.
//!
//! A matched cap is then read for one thing beyond its reset time: whether the
//! agent is *retrying* the rejected request ([`CapReading::retrying`]). A
//! usage cap never is — Claude Code declines to retry one at all (§8) — but a
//! plain rate limit is retried, and the agent labels that retry with the API's
//! own wording, which contains "rate limit". So the text that says a session
//! is held and the text that says it is mid-turn differ by the retry phrase
//! alone, and everything else about the two is identical: `blocked` in the
//! listing, supervisor alive, cap phrase on screen.
//!
//! *When* the window reopens has a second source, and a better one. The clock
//! time on screen is a bare `6:40pm`: no date, so it is read as whichever
//! occurrence is nearest and is ambiguous by half a day either way. An agent
//! that can say the same thing as an instant — [`AccountCap`], read through its
//! `cap` verb — says it exactly, and [`CapWindow`] is where the two, and the
//! retry above, are resolved into the one answer the badge and the sweep both
//! read. That reading is about the *account* rather than the session, so it
//! serves every session asking the same of it; the parse stays for every agent
//! and every moment that has none.

/// Phrases that mean "held at a usage cap", checked case-insensitively.
///
/// The first three are the original list and cover agents that word it
/// generically. The rest are what Claude Code actually renders, which none of
/// the first three catch: a five-hour cap says "Session limit reached", the
/// weekly one "Weekly limit reached", and the per-model and overage ones name
/// the model or the credit. A cap that says none of these is reported as an
/// ordinary failure, which is the same landing either way (§8).
pub const CAP_SIGNATURES: [&str; 8] = [
    "usage limit",
    "rate limit",
    "quota exceeded",
    "session limit",
    "weekly limit",
    "opus limit",
    "sonnet limit",
    "credit limit",
];

/// Phrases that take a matched signature back. Each is something an agent says
/// in the same breath as a limit while still working normally: a
/// warning ahead of the cap ("Approaching usage limit", "You've used 80% of
/// your session limit") or a server-side error explicitly disclaiming one
/// ("Server is temporarily limiting requests (not your usage limit)").
const NOT_CAP_QUALIFIERS: [&str; 4] = ["approaching", "% of your", "not your", "close to your"];

/// Phrases that mean the signature after them is not a *report* at all. Every
/// real limit message ends with the upgrade prompt — `/upgrade to increase your
/// usage limit.` — which contains a signature of its own and, being last, would
/// otherwise be the one that decides.
///
/// That matters twice over. It is the *only* signature in a genuine cap whose
/// window holds no reset time, so letting it decide drops the time from every
/// real cap; and it says nothing about whether the session is held, so a warning
/// that ever trailed the same prompt would badge as a cap. Both go away once the
/// prompt is read as the boilerplate it is: skipped when choosing which
/// signature speaks, rather than negating like a qualifier — a qualifier means
/// "this one is not a cap", and skipping instead would let a genuine earlier cap
/// speak past a warning that had since replaced it.
const MENTION_PREFIXES: [&str; 2] = ["/upgrade", "increase your"];

/// Phrases that mean the agent is *retrying* the request the cap rejected
/// rather than having ended its turn on it. Read in the same window as the
/// reset time, since the agent renders both on one line with the retry half
/// after the label: `Session limit reached · Retrying in 5m (9:50pm) ·
/// attempt 2/10`.
const RETRYING: [&str; 1] = ["retrying in"];

/// How much text after a matched signature is read for the reset time that
/// goes on the badge, and for the retry wording that says the turn is still
/// running.
const WINDOW: usize = 200;

/// How much text before a matched signature is read for the qualifiers that
/// take it back. Deliberately short: every qualifier attaches directly to the
/// phrase it modifies, so a wider look-back would let an *earlier* warning
/// speak for a later, genuine cap — which is the one reading that loses a real
/// cap rather than merely missing an unworded one.
const QUALIFIER_WINDOW: usize = 32;

/// What a session's output says about a usage cap. Held in memory only: it is a
/// reading of the current output tail, retaken on the next pass, so it clears
/// itself once the operator continues the session and new output displaces the
/// cap message (§8). Nothing about it reaches the database.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct CapReading {
    /// When the window reopens, as minutes past local midnight, if the message
    /// named a time. Best-effort by design: an unparsed time badges without
    /// one rather than suppressing the badge.
    pub reset_minutes: Option<u16>,
    /// Whether the agent said it is retrying the rejected request rather than
    /// having ended its turn on it.
    ///
    /// This is the one capped shape a nudge must not touch. The session is
    /// mid-turn: it holds the request itself and will resume when the retry
    /// lands, so stopping it to send *continue* kills a turn that needed no
    /// help — and the sweep stops its target unconditionally (§8), so there is
    /// nothing else between a misread and a killed turn. Both shapes otherwise
    /// read identically: `blocked` in the agent's listing, supervisor alive,
    /// the same cap phrase in the output.
    ///
    /// The risk this guards is not that a *cap* retries — Claude Code refuses
    /// to retry a usage-limit rejection at all (§8) — but that a retry reads
    /// as a cap. A rate limit that carries no usage-limit headers is retried,
    /// and the agent labels that retry with the API's own message, which says
    /// "rate limit" in it: a signature this module matches, riding a session
    /// that is working perfectly well.
    pub retrying: bool,
}

impl CapReading {
    /// The reset time as a 24-hour `21:50`, for the badge.
    pub fn reset_label(&self) -> Option<String> {
        self.reset_minutes
            .map(|m| format!("{:02}:{:02}", m / 60, m % 60))
    }

    /// Whether the named reset has already gone by, given the local wall clock
    /// as minutes past midnight — and so whether this session is waiting on a
    /// human rather than on the clock. A retrying session is never either: it
    /// is waiting on its own request, and its reset time is when that request
    /// goes out, not when someone should intervene.
    ///
    /// The agent names a bare clock time — `9:50pm`, no date — so the occurrence
    /// meant is the one *nearest* now, in either direction. Taking the next one
    /// instead would be self-defeating: a minute after the window reopened,
    /// "the next 9:50pm" is tomorrow's, and the badge would claim another 24
    /// hours of waiting exactly when it should be saying the opposite. Half a
    /// day is the widest a bare clock time can be read unambiguously, and a cap
    /// window the operator is watching is never further off than that.
    pub fn reset_passed(&self, now_minutes: u16) -> bool {
        if self.retrying {
            return false;
        }
        let Some(reset) = self.reset_minutes else {
            return false;
        };
        // Signed distance wrapped into (-720, 720]: negative is behind us.
        let delta = (i32::from(reset) - i32::from(now_minutes)).rem_euclid(1440);
        let delta = if delta > 720 { delta - 1440 } else { delta };
        delta <= 0
    }
}

/// How far behind the present a reported reset may fall and still be believed:
/// a window that reopened while the operator was away is exactly the reading
/// the sweep is waiting for, so a day of slack costs nothing.
const EPOCH_BEHIND: i64 = 24 * 60 * 60;

/// How far ahead of the present a reported reset may fall and still be
/// believed. The longest window an agent bills in is a week, so a month is
/// generous — the bound is here to refuse a number that is not a timestamp at
/// all, not to second-guess the agent.
const EPOCH_AHEAD: i64 = 30 * 24 * 60 * 60;

/// What an agent's `cap` verb says about the *account* it dispatches on: the
/// instant its usage window reopens (DESIGN.md §8).
///
/// This is the same quantity the badge parses off a session's screen, without
/// the parse. It is a fact the agent reports rather than a heuristic standing
/// in for one, and it is account-wide, so a single reading answers for every
/// session on the strip — where the screen reading has to be taken per session,
/// costing a subprocess each.
///
/// The label is the agent's instant in the operator's own timezone, rendered
/// where the reading is taken because that is off the render path and this
/// crate has no clock. Absent when it could not be rendered, in which case the
/// badge shows the cap without a time exactly as an unparsed one does.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccountCap {
    /// When the window reopens, in seconds since the Unix epoch.
    pub reset_epoch: i64,
    /// That instant as a local `21:50`, for the badge.
    pub reset_label: Option<String>,
}

/// Read an agent's `cap` verb output as the instant its window reopens, or
/// `None` when it said nothing (DESIGN.md §8).
///
/// The verb's contract is one line of shell away from trivial on purpose: print
/// a Unix epoch when the account is *currently* refused, print nothing
/// otherwise. Silence is the whole of the negative answer — an account that is
/// not capped, a verb that failed, an agent that defines none all read the same
/// and all fall back to the screen parse — which is how every other reading in
/// this module degrades.
///
/// A plausibility band around `now` is the only judgement applied: the output is
/// under the agent template's control, and a stray number in it must not become
/// a badge claiming the window reopens in 1970. The *last* plausible number
/// wins, so a verb that prints a line per window ends with the one it means.
pub fn parse_reset_epoch(out: &str, now_epoch: i64) -> Option<i64> {
    out.split(|c: char| !c.is_ascii_digit())
        .filter_map(|run| run.parse::<i64>().ok())
        .rfind(|epoch| (now_epoch - EPOCH_BEHIND..=now_epoch + EPOCH_AHEAD).contains(epoch))
}

/// When a capped session's window reopens and whether it has: the one answer
/// the badge and the nudge sweep both read, resolved from the two sources that
/// can give it (DESIGN.md §8).
///
/// The account's own reading decides whenever there is one. It is the same
/// quantity the screen states, minus the ambiguity: a bare `6:40pm` carries no
/// date, so "has it passed?" is answered by nearest occurrence and is a
/// half-day guess in both directions, while an instant is simply compared. The
/// screen parse remains the answer for an agent with no `cap` verb, for an
/// account that is not itself refused, and for every reading taken before the
/// verb has run.
///
/// One caveat rides the precedence and is worth naming: 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 — and the account reading names its own. The
/// sweep is then early for that session, which §8 already prices as the cheap
/// failure: the nudge lands, the turn re-caps at once, and the badge returns.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct CapWindow {
    /// The reset as a local `21:50`, absent when neither source named one.
    pub label: Option<String>,
    /// Whether the window has reopened.
    pub passed: bool,
    /// Whether anything named a reset at all. A cap with no time is the case
    /// the operator's own judgement stands in for ([`CapWindow::due`]).
    pub timed: bool,
    /// Whether the session is retrying the rejected request rather than sitting
    /// on it ([`CapReading::retrying`]). Carried through rather than resolved
    /// away, because it is the one badged shape that wants nothing done about
    /// it and the badge has to say so.
    pub retrying: bool,
}

impl CapWindow {
    /// Resolve the sources for one session, the account's reading first.
    ///
    /// A session that is *retrying* takes neither: the account's instant says
    /// when the window reopens, and a retrying session is not waiting on the
    /// window — it is mid-turn on a request of its own, and the time it names
    /// is when that request goes out (§8). Reading an account instant onto it
    /// would mark it due the moment the window opened, which is precisely the
    /// session a nudge must not touch.
    pub fn resolve(
        reading: &CapReading,
        account: Option<&AccountCap>,
        now_minutes: Option<u16>,
        now_epoch: Option<i64>,
    ) -> CapWindow {
        if reading.retrying {
            return CapWindow {
                label: reading.reset_label(),
                passed: false,
                timed: reading.reset_minutes.is_some(),
                retrying: true,
            };
        }
        if let Some(account) = account {
            return CapWindow {
                label: account
                    .reset_label
                    .clone()
                    .or_else(|| reading.reset_label()),
                passed: now_epoch.is_some_and(|now| account.reset_epoch <= now),
                timed: true,
                retrying: false,
            };
        }
        CapWindow {
            label: reading.reset_label(),
            passed: now_minutes.is_some_and(|now| reading.reset_passed(now)),
            timed: reading.reset_minutes.is_some(),
            retrying: false,
        }
    }

    /// Whether this session is waiting on a human rather than on the clock —
    /// what the sweep nudges.
    ///
    /// An untimed cap counts, and that is the operator's judgement standing in
    /// for the clock's: they pressed the key, and a nudge that turns out to be
    /// early is refused by the agent rather than doing harm (§8). It is also
    /// the gap the account reading closes — a cap whose time never parsed is
    /// timed after all once the account has said when — which is what an
    /// automatic sweep, with no keypress behind it, needs.
    ///
    /// A retrying session is never due, whatever it named: it is working, and
    /// the sweep stops its target before resuming it, so a nudge there ends a
    /// turn rather than adding one. That answer lives here rather than only in
    /// the sweep, so a caller reading `due` alone cannot miss it.
    pub fn due(&self) -> bool {
        !self.retrying && (!self.timed || self.passed)
    }
}

/// Read a session's output tail as a cap reading, or `None` when nothing in it
/// says the session is capped.
///
/// The *last* signature in the text decides, since a session that hit a cap,
/// was continued, and hit another has the live one at the end — and a warning
/// earlier in the same tail must not speak for a genuine cap later.
pub fn read_cap(tail: &str) -> Option<CapReading> {
    let text = strip_ansi(tail).to_lowercase();
    let (at, signature) = last_signature(&text)?;
    if look_back(&text, at, &NOT_CAP_QUALIFIERS) {
        return None;
    }
    let after = &text[at..ceil_boundary(&text, (at + signature.len() + WINDOW).min(text.len()))];
    Some(CapReading {
        reset_minutes: parse_clock(after),
        retrying: RETRYING.iter().any(|p| after.contains(p)),
    })
}

/// The position and text of the last cap signature in `text` that reports
/// something, which must already be lowercased. Signatures the upgrade prompt
/// merely mentions ([`MENTION_PREFIXES`]) are not candidates.
fn last_signature(text: &str) -> Option<(usize, &'static str)> {
    CAP_SIGNATURES
        .iter()
        .flat_map(|sig| text.match_indices(sig).map(|(at, _)| (at, *sig)))
        .filter(|(at, _)| !look_back(text, *at, &MENTION_PREFIXES))
        .max_by_key(|(at, _)| *at)
}

/// Whether any of `phrases` appears in the short span of `text` before `at`.
fn look_back(text: &str, at: usize, phrases: &[&str]) -> bool {
    let before = &text[floor_boundary(text, at.saturating_sub(QUALIFIER_WINDOW))..at];
    phrases.iter().any(|p| before.contains(p))
}

/// Drop terminal escape sequences, keeping the spacing the surviving text had
/// on screen.
///
/// A cursor move stands for the gap between two words, so it becomes a space —
/// without that, `claude logs` output runs its words together and no phrase
/// matches. A colour change stands for nothing spatial and is simply dropped,
/// so a phrase styled halfway through stays one word. Other control characters
/// (newlines and tabs included) are spacing too.
pub fn strip_ansi(raw: &str) -> String {
    let mut out = String::with_capacity(raw.len());
    let mut chars = raw.chars().peekable();
    while let Some(c) = chars.next() {
        if c != '\u{1b}' {
            out.push(if c.is_control() { ' ' } else { c });
            continue;
        }
        match chars.next() {
            // CSI: parameters and intermediates, then a final byte in @..~.
            Some('[') => {
                let mut final_byte = None;
                for c in chars.by_ref() {
                    if ('\u{40}'..='\u{7e}').contains(&c) {
                        final_byte = Some(c);
                        break;
                    }
                }
                // `m` is SGR — styling, no position — and only it is spaceless.
                if final_byte != Some('m') {
                    out.push(' ');
                }
            }
            // OSC: runs to BEL or to a string terminator.
            Some(']') => {
                while let Some(c) = chars.next() {
                    if c == '\u{7}' {
                        break;
                    }
                    if c == '\u{1b}' {
                        chars.next();
                        break;
                    }
                }
            }
            // A two-character escape, or a stray ESC at the end of the tail.
            _ => {}
        }
    }
    out
}

/// Minutes past midnight for the first `9pm` / `9:50pm` clock time in `text`,
/// which must already be lowercased.
///
/// This is the shape Claude Code renders a reset time in when it is less than a
/// day out, which a cap window the operator is looking at always is. A longer
/// horizon is spelled with a date (`Aug 14, 9pm`) and the clock half still
/// reads, which is the right answer for a badge that shows a time and not a
/// date.
fn parse_clock(text: &str) -> Option<u16> {
    let bytes = text.as_bytes();
    for i in 0..bytes.len().saturating_sub(1) {
        let meridiem = match (bytes[i], bytes[i + 1]) {
            (b'a', b'm') => 0,
            (b'p', b'm') => 12,
            _ => continue,
        };
        // `9pm` must not be read out of `9pmx` or a word ending in `am`.
        if bytes.get(i + 2).is_some_and(|c| c.is_ascii_alphanumeric()) {
            continue;
        }
        let mut j = i;
        while j > 0 && bytes[j - 1] == b' ' {
            j -= 1;
        }
        let end = j;
        while j > 0 && (bytes[j - 1].is_ascii_digit() || bytes[j - 1] == b':') {
            j -= 1;
        }
        let Some(minutes) = clock_minutes(&text[j..end], meridiem) else {
            continue;
        };
        return Some(minutes);
    }
    None
}

/// `9`, `9:50` plus a 0-or-12 hour offset, as minutes past midnight. A 12-hour
/// clock names noon and midnight as `12`, so that hour wraps to zero before the
/// offset applies.
fn clock_minutes(clock: &str, meridiem: u16) -> Option<u16> {
    let (hour, minute) = match clock.split_once(':') {
        Some((h, m)) if m.len() == 2 => (h, m.parse::<u16>().ok()?),
        Some(_) => return None,
        None => (clock, 0),
    };
    let hour: u16 = hour.parse().ok()?;
    if hour == 0 || hour > 12 || minute > 59 {
        return None;
    }
    Some((hour % 12 + meridiem) * 60 + minute)
}

/// The largest char boundary at or below `at`, so a window edge never lands
/// inside the multi-byte glyphs an agent's output is full of.
fn floor_boundary(text: &str, mut at: usize) -> usize {
    while at > 0 && !text.is_char_boundary(at) {
        at -= 1;
    }
    at
}

/// The smallest char boundary at or above `at`.
fn ceil_boundary(text: &str, mut at: usize) -> usize {
    while at < text.len() && !text.is_char_boundary(at) {
        at += 1;
    }
    at
}

#[cfg(test)]
mod tests {
    use super::*;

    /// The wording the original three signatures all missed: a five-hour cap
    /// says "Session limit reached" and nothing about usage, rates or quotas.
    ///
    /// The retry half is *binary-derived*, assembled from the agent's own
    /// template rather than seen on a session — and taking it for an
    /// observation is what sent a reviewer looking for a cap that retries.
    /// What the template shows is real enough: the agent renders exactly this
    /// line for a retried rate limit, reset time in parentheses and attempt
    /// counter behind it. What it does not show is a cap reaching it, because
    /// a usage limit is not among the errors the agent retries (§8). So the
    /// string stands as the shape [`CapReading::retrying`] must recognise, and
    /// stands for nothing about whether a *cap* wears it.
    #[test]
    fn a_five_hour_cap_reads_as_capped() {
        let reading = read_cap("Session limit reached · Retrying in 5m (9:50pm) · attempt 2/10")
            .expect("a cap");
        assert_eq!(reading.reset_label().as_deref(), Some("21:50"));
        assert!(reading.retrying);
    }

    /// The wording an actual five-hour cap turned out to use, captured from
    /// three live sessions on 2026-08-13 — the first real cap Voro has seen,
    /// every earlier case having been read out of the agent's own binary.
    ///
    /// The upgrade prompt riding along behind it is the whole point: it carries
    /// a signature of its own, it is last, and its window holds no time, so
    /// before it was read as boilerplate every genuine cap badged without the
    /// reset time it had actually named.
    #[test]
    fn the_real_cap_message_reads_with_its_reset_time() {
        let reading = read_cap(
            "You've hit your session limit · resets 6:40pm (Europe/London)\n\
             /upgrade to increase your usage limit.",
        )
        .expect("a cap");
        assert_eq!(reading.reset_label().as_deref(), Some("18:40"));
        // The half that matters for the sweep: the real thing names no retry,
        // so the turn is over and the session is waiting on a human.
        assert!(!reading.retrying);
    }

    /// A retry the operator's sessions really do hit, captured from a `--bg`
    /// session on 2026-08-13 while a sibling sat at the cap above. It is the
    /// live proof that a backgrounded session reaches the retry banner — and
    /// that the banner alone is not a cap: an overload names no limit, so
    /// nothing here matches and no badge is owed.
    #[test]
    fn an_overload_retry_is_not_a_cap() {
        assert_eq!(
            read_cap(
                "529 Overloaded · Retrying in 3s · attempt 3/10\n\
                 If it persists, check https://status.claude.com."
            ),
            None
        );
    }

    /// The case [`CapReading::retrying`] is built for: a rate limit the agent
    /// *does* retry, labelled with the API's own wording. It matches on "rate
    /// limit" like any cap, it badges like any cap — and the session behind it
    /// is mid-turn, so the sweep must let it alone.
    #[test]
    fn a_retried_rate_limit_reads_as_retrying() {
        let reading = read_cap(
            "429 Number of requests has exceeded your rate limit · Retrying in 30s · attempt 3/10",
        )
        .expect("a cap");
        assert!(reading.retrying);
        // And it is never due a nudge, whatever the clock says, because the
        // time it names is when its own request goes out.
        let timed = read_cap("Session limit reached · Retrying in 5m (9:50pm) · attempt 2/10")
            .expect("a cap");
        assert_eq!(timed.reset_minutes, Some(21 * 60 + 50));
        assert!(!timed.reset_passed(22 * 60 + 50));
    }

    /// The real warning short of that cap, captured from a session that went on
    /// working — and which must stay unbadged even though the same upgrade
    /// prompt can follow it.
    #[test]
    fn the_real_warning_short_of_the_cap_is_not_capped() {
        assert_eq!(
            read_cap(
                "You've used 98% of your session limit · resets 6:40pm (Europe/London)\n\
                 /upgrade to keep using Claude Code"
            ),
            None
        );
        assert_eq!(
            read_cap(
                "You've used 99% of your session limit · resets 6:40pm (Europe/London)\n\
                 /upgrade to increase your usage limit."
            ),
            None
        );
    }

    /// The other wordings the agent uses for the same condition.
    #[test]
    fn every_cap_wording_reads_as_capped() {
        for text in [
            "You've hit your session limit · resets 9pm",
            "Weekly limit reached",
            "Opus limit reached · resets Aug 14, 9pm",
            "Usage credit limit reached",
            "usage limit reached — check plan",
            "rate limited — wait and retry",
            "sorry, hit the 5-hour usage limit — try again later",
            "quota exceeded",
        ] {
            assert!(read_cap(text).is_some(), "{text}");
        }
    }

    /// Ordinary output is not a cap, however much it talks about limits.
    #[test]
    fn ordinary_output_is_not_capped() {
        for text in [
            "",
            "running tests",
            "Context limit reached · /compact",
            "Concurrent subagent limit reached. You can run 10 subagents at once.",
            "error: recursion limit reached",
        ] {
            assert_eq!(read_cap(text), None, "{text}");
        }
    }

    /// The qualifiers that take a match back. Each of these appears while the
    /// session is working perfectly well, and badging it would be a lie the
    /// operator learns to ignore the badge over.
    #[test]
    fn a_warning_short_of_the_cap_is_not_capped() {
        for text in [
            "Approaching usage limit · resets 9pm",
            "You've used 80% of your session limit · resets 9pm",
            "Server is temporarily limiting requests (not your usage limit)",
            "You're close to your usage limit",
        ] {
            assert_eq!(read_cap(text), None, "{text}");
        }
    }

    /// A warning earlier in the same tail does not speak for a real cap later:
    /// the last signature decides, so a session that was warned and then
    /// genuinely capped still badges.
    #[test]
    fn the_last_signature_decides() {
        let tail = "Approaching usage limit · resets 9pm\n\
                    ... work continues ...\n\
                    Session limit reached · Retrying in 5m (9:50pm)";
        assert_eq!(
            read_cap(tail).expect("a cap").reset_label().as_deref(),
            Some("21:50")
        );

        // And the other order: a real cap the operator has since cleared, with
        // only a warning left at the end, is no longer capped.
        let tail = "Session limit reached\n... continued ...\nApproaching usage limit";
        assert_eq!(read_cap(tail), None);
    }

    /// A cap with no time still badges — the time is the optional half.
    #[test]
    fn a_cap_without_a_time_reads_without_one() {
        let reading = read_cap("Weekly limit reached").expect("a cap");
        assert_eq!(reading.reset_minutes, None);
        assert_eq!(reading.reset_label(), None);
        assert!(!reading.reset_passed(600));
    }

    #[test]
    fn clock_times_parse_to_minutes_past_midnight() {
        for (text, minutes) in [
            ("9pm", 21 * 60),
            ("9:50pm", 21 * 60 + 50),
            ("9am", 9 * 60),
            ("12am", 0),
            ("12:30am", 30),
            ("12pm", 12 * 60),
            ("12:30pm", 12 * 60 + 30),
            ("1:05am", 65),
        ] {
            assert_eq!(
                read_cap(&format!("Session limit reached · resets {text}"))
                    .expect("a cap")
                    .reset_minutes,
                Some(minutes),
                "{text}"
            );
        }
    }

    /// Nothing that only looks like a clock time is read as one.
    #[test]
    fn non_times_are_not_read_as_times() {
        for text in [
            "Session limit reached",
            "Session limit reached · stream",
            "Session limit reached · 25pm",
            "Session limit reached · 0am",
            "Session limit reached · 9:5pm",
            "Session limit reached · 9:75pm",
            "Session limit reached · 9pmx",
        ] {
            assert_eq!(read_cap(text).expect("a cap").reset_minutes, None, "{text}");
        }
    }

    /// Nearest-occurrence, in both directions: a reset an hour out is still
    /// ahead, one an hour back has gone by, and the answer holds across
    /// midnight where a naive comparison flips.
    #[test]
    fn a_reset_is_passed_by_nearest_occurrence() {
        let at = |m| CapReading {
            reset_minutes: Some(m),
            retrying: false,
        };
        // 21:50 reset, read at 20:50 — an hour to go.
        assert!(!at(1310).reset_passed(1250));
        // ...and at 22:50, an hour after it opened.
        assert!(at(1310).reset_passed(1370));
        // The reset instant itself counts as passed.
        assert!(at(1310).reset_passed(1310));
        // 00:30 reset read at 23:30 is half an hour ahead, not 23 behind.
        assert!(!at(30).reset_passed(1410));
        // 23:30 reset read at 00:30 is half an hour behind, not 23 ahead.
        assert!(at(1410).reset_passed(30));
    }

    /// The `claude logs` shape: a terminal capture whose words are separated by
    /// cursor-column moves and whose phrases are broken up by colour changes.
    /// Reading it as a plain string finds nothing at all.
    #[test]
    fn a_terminal_capture_reads_as_its_rendered_text() {
        let capture = "\u{1b}[?25l\u{1b}[H\u{1b}[38;2;215;119;87mSession\u{1b}[8G\u{1b}[1mlimit\
                       \u{1b}[39m\u{1b}[14Greached\u{1b}[22G·\u{1b}[24Gresets\u{1b}[31G9:50pm\
                       \u{1b}[39m\u{1b}[?25h";
        let reading = read_cap(capture).expect("a cap");
        assert_eq!(reading.reset_label().as_deref(), Some("21:50"));
    }

    /// The two halves of the stripping rule, stated on their own: a cursor move
    /// is a gap and a colour change is not.
    #[test]
    fn stripping_keeps_spacing_but_not_styling() {
        assert_eq!(
            strip_ansi("and\u{1b}[97Gthe\u{1b}[101Gform"),
            "and the form"
        );
        assert_eq!(
            strip_ansi("ses\u{1b}[1msion \u{1b}[38;5;153mlimit"),
            "session limit"
        );
        assert_eq!(strip_ansi("a\nb\tc"), "a b c");
        assert_eq!(strip_ansi("\u{1b}]0;a title\u{7}kept"), "kept");
        assert_eq!(strip_ansi("plain"), "plain");
    }

    /// A day in seconds, for writing the epoch tests in units a reader can
    /// hold.
    const DAY: i64 = 24 * 60 * 60;

    /// The `cap` verb's own output, which is one number: the instant the
    /// account's window reopens, exactly as the agent reported it.
    #[test]
    fn the_cap_verb_reads_as_an_instant() {
        let now = 1_786_722_000;
        assert_eq!(parse_reset_epoch("1786758000\n", now), Some(1_786_758_000));
        // Whitespace and a trailing newline are the shell's, not the agent's.
        assert_eq!(
            parse_reset_epoch("  1786758000  ", now),
            Some(1_786_758_000)
        );
        // Silence is the negative answer: not capped, verb failed, no verb.
        assert_eq!(parse_reset_epoch("", now), None);
        assert_eq!(parse_reset_epoch("\n", now), None);
    }

    /// A number that cannot be a reset is not one. The verb's output is
    /// whatever an agent template prints, so a stray count or id must not badge
    /// a session with a window that reopens in 1970 — or in 2031.
    #[test]
    fn only_a_plausible_instant_is_believed() {
        let now = 1_786_722_000;
        assert_eq!(parse_reset_epoch("42", now), None);
        assert_eq!(parse_reset_epoch("0", now), None);
        assert_eq!(parse_reset_epoch(&(now + 400 * DAY).to_string(), now), None);
        assert_eq!(parse_reset_epoch(&(now - 3 * DAY).to_string(), now), None);
        // The bounds themselves, since a window that reopened while the
        // operator slept is precisely the reading the sweep waits for.
        assert_eq!(
            parse_reset_epoch(&(now - DAY).to_string(), now),
            Some(now - DAY)
        );
        assert_eq!(
            parse_reset_epoch(&(now + 7 * DAY).to_string(), now),
            Some(now + 7 * DAY)
        );
    }

    /// A verb that prints more than one line ends with the one it means.
    #[test]
    fn the_last_plausible_instant_wins() {
        let now = 1_786_722_000;
        let out = format!("{}\n{}\n", now + 60, now + 3600);
        assert_eq!(parse_reset_epoch(&out, now), Some(now + 3600));
        // And an implausible number after a good one does not displace it.
        assert_eq!(
            parse_reset_epoch(&format!("{}\nattempt 2\n", now + 60), now),
            Some(now + 60)
        );
    }

    /// The precedence proper: the account's instant answers for a session
    /// whose screen named a time, and it answers exactly — 21:50 read a minute
    /// later has passed, where the parse would have to guess by nearest
    /// occurrence.
    #[test]
    fn the_accounts_instant_decides_over_the_parsed_clock() {
        let now = 1_786_722_000;
        let reading = read_cap("Session limit reached · resets 9:50pm").expect("a cap");
        let account = AccountCap {
            reset_epoch: now + 3600,
            reset_label: Some("21:50".into()),
        };
        let window = CapWindow::resolve(&reading, Some(&account), Some(20 * 60 + 50), Some(now));
        assert_eq!(window.label.as_deref(), Some("21:50"));
        assert!(!window.passed);
        assert!(!window.due());

        let account = AccountCap {
            reset_epoch: now - 60,
            ..account
        };
        let window = CapWindow::resolve(&reading, Some(&account), Some(20 * 60 + 50), Some(now));
        assert!(window.passed);
        assert!(window.due());
    }

    /// The gap the account reading closes, and the reason #437 wants it: a cap
    /// whose time never parsed is not timed at all, so the sweep can only fire
    /// on the operator's say-so. With the account's instant it is timed, and a
    /// clock can decide.
    #[test]
    fn an_untimed_cap_is_timed_by_the_account() {
        let now = 1_786_722_000;
        let reading = read_cap("Weekly limit reached").expect("a cap");
        let bare = CapWindow::resolve(&reading, None, Some(12 * 60), Some(now));
        assert!(!bare.timed);
        assert_eq!(bare.label, None);
        assert!(bare.due(), "an untimed cap is the operator's call");

        let account = AccountCap {
            reset_epoch: now + 3600,
            reset_label: Some("09:00".into()),
        };
        let timed = CapWindow::resolve(&reading, Some(&account), Some(12 * 60), Some(now));
        assert!(timed.timed);
        assert_eq!(timed.label.as_deref(), Some("09:00"));
        assert!(!timed.due(), "the window is known to be shut");
    }

    /// A retrying session takes no account instant, whatever the account says.
    /// It is not waiting on the window — it is mid-turn on its own request — so
    /// an instant that has passed must not mark it due: that is the one badged
    /// shape a nudge would interrupt rather than help (§8).
    #[test]
    fn a_retrying_session_takes_no_instant_and_is_never_due() {
        let now = 1_786_722_000;
        let reading = read_cap(
            "429 Number of requests has exceeded your rate limit · Retrying in 30s · attempt 3/10",
        )
        .expect("a cap");
        assert!(reading.retrying);
        let account = AccountCap {
            reset_epoch: now - 3600,
            reset_label: Some("21:50".into()),
        };
        let window = CapWindow::resolve(&reading, Some(&account), Some(12 * 60), Some(now));
        assert!(window.retrying);
        assert!(!window.passed, "the window's instant does not speak for it");
        assert!(!window.due());
        // And the badge still shows what the session itself named, if anything.
        let timed = read_cap("Session limit reached · Retrying in 5m (9:50pm) · attempt 2/10")
            .expect("a cap");
        let window = CapWindow::resolve(&timed, Some(&account), Some(12 * 60), Some(now));
        assert_eq!(window.label.as_deref(), Some("21:50"));
        assert!(!window.due());
    }

    /// With no account reading the badge is exactly what it was: the screen
    /// parse, judged by nearest occurrence against the local clock.
    #[test]
    fn without_an_account_reading_the_parse_still_answers() {
        let reading = read_cap("Session limit reached · resets 9:50pm").expect("a cap");
        let ahead = CapWindow::resolve(&reading, None, Some(20 * 60 + 50), Some(0));
        assert_eq!(ahead.label.as_deref(), Some("21:50"));
        assert!(!ahead.passed);
        assert!(ahead.timed);
        assert!(CapWindow::resolve(&reading, None, Some(22 * 60 + 50), Some(0)).passed);
    }

    /// An account reading Voro could not render a label for still decides
    /// whether the window is open — the instant is the load-bearing half, and
    /// the session's own time fills the badge behind it.
    #[test]
    fn an_unrendered_instant_still_decides() {
        let now = 1_786_722_000;
        let reading = read_cap("Session limit reached · resets 9:50pm").expect("a cap");
        let account = AccountCap {
            reset_epoch: now - 60,
            reset_label: None,
        };
        let window = CapWindow::resolve(&reading, Some(&account), Some(20 * 60 + 50), Some(now));
        assert_eq!(window.label.as_deref(), Some("21:50"));
        assert!(window.passed);

        // And with no clock to compare against, nothing claims it has passed.
        let window = CapWindow::resolve(&reading, Some(&account), Some(20 * 60 + 50), None);
        assert!(!window.passed);
        assert!(window.timed);
    }

    /// A signature landing at the very edge of the text windows the qualifier
    /// check over multi-byte output without panicking on a char boundary.
    #[test]
    fn windows_never_split_a_multibyte_glyph() {
        let padding = "·".repeat(400);
        assert!(read_cap(&format!("{padding}session limit reached{padding}")).is_some());
        assert!(read_cap("session limit").is_some());
    }
}