Skip to content

feat(loop-automation): handle session-limit reset + 529 Overloaded banners#24

Merged
miguelrisero merged 3 commits into
mainfrom
mr/loop-retry-session-limit-and-529
Jul 2, 2026
Merged

feat(loop-automation): handle session-limit reset + 529 Overloaded banners#24
miguelrisero merged 3 commits into
mainfrom
mr/loop-retry-session-limit-and-529

Conversation

@miguelrisero

@miguelrisero miguelrisero commented Jul 2, 2026

Copy link
Copy Markdown
Owner

What

Two more stall reasons the agentic-loop ("Keep going") supervisor now detects and auto-recovers from, extending the banner classifier from #23.

1. Session-limit reset banner

You've hit your session limit · resets 3:10pm (UTC)

  • Classified as a usage-/session-window limit (same recovery path as the 5-hour/weekly cap; folded into UsageLimited).
  • Timezone-aware reset parsing → UTC. Explicit (UTC)/(GMT)/Z/±HH[:MM] are honored; when no zone is stated the time is read in the host's local zone (CLI agents render reset clocks in local time — the supervisor runs on the same host) and converted to UTC. A malformed offset (UTC+99) falls back to the default zone rather than silently becoming UTC.
  • Wakes 5 min after the reset (usage_wake_at), and only rolls to the next day when that buffered wake has already elapsed — so a reset seen a few minutes late wakes soon instead of ~24h later.

2. 529 Overloaded

API Error: 529 Overloaded …

  • New transient LimitSignal::Overloaded with a 4-minute backoff (your "3–5 min"), retried until it clears. Backed by a new WakeupKind::OverloadRetry (plain TEXT column — no migration).
  • Detection is line-scoped (529 and overloaded on the same line) to avoid false positives from incidental pane text.

Correctness guarantees (added under review)

  • Detection order is rate-limit → usage/session → 529, so a stale "overloaded" line above a live usage banner can't mask the window's reset.
  • Delivery matches the wakeup's kind to the currently-visible limit, so an overload retry can't fire into a now-different cap and burn an attempt. Manual wakes still always deliver; an Unknown kind (rolled-back/foreign row) is skipped rather than delivered unconditionally.
  • A transient pane-capture failure leaves the wake pending (retried next tick) instead of dropping it.

Not changed (deliberately)

  • The migration's kind comment is left stale on purpose — editing an already-applied migration changes its SQLx checksum and breaks validation on existing DBs.
  • The overload backoff is a fixed 4 min (your explicit ask); the attempt cap bounds any sustained-outage hammering.

Review & tests

Ran through a multi-agent review battery: Codex review, a 4-persona council, /code-review xhigh (18 agents), 4 /simplify agents, and CodeRabbit. Findings triaged, real ones applied, false positives disproven with regression tests (UTF-8 boundary safety, +0530 HHMM offset).

Backend unit tests 8 → 30 across the two crates (timezone/local-default conversion, dotted-meridiem offset, usage_wake_at buffer/roll boundary, signal→kind mapping, usage-wins-over-stale-529, line-scoped 529, unknown-kind parse). cargo clippy clean, frontend typechecks, shared types regenerated (WakeupKind gains overload_retry + unknown).

Deploy note

Fork build at /opt needs a rebuild + restart to pick this up.

🤖 Generated with Claude Code

…nners

Extends the agentic-loop supervisor's banner detection with two more stall
reasons so the "Keep going" loop recovers from them automatically:

- "You've hit your session limit · resets 3:10pm (UTC)" is now classified as a
  usage-/session-window limit. The reset time is parsed timezone-aware
  ("(UTC)", "(GMT)", "Z", or an explicit +/-HH[:MM] offset) and converted to
  UTC, and the wake fires 5 minutes after the stated reset so the window has
  actually rolled (was a 60s buffer).
- "API Error: 529 Overloaded" is a new transient LimitSignal::Overloaded with a
  4-minute backoff (within the provider's suggested 3-5 min), retried until it
  clears. Backed by a new WakeupKind::OverloadRetry (plain TEXT column, no
  migration; renders as a short retry in the UI).

The reset-time parser now returns the raw UTC instant and the scheduler owns
the post-reset buffer, so usage and session windows can differ cleanly. Adds
unit tests for the session-limit banner, 529 detection, and offset conversion
(UTC-5, UTC+05:30, bare offsets). Regenerated shared types.
Applies the consensus findings from a multi-agent review (Codex + council +
code-review xhigh) of the session-limit / 529 additions:

- Reset times with no explicit zone are now read in the host's local timezone
  (converted to UTC), not assumed-UTC — CLI agents render reset clocks in local
  time. Explicit (UTC)/(GMT)/Z/±HH[:MM] still win. `parse_reset_at` takes an
  injectable default_tz so this is deterministically testable.
- Delivery now checks the *same* limit kind is still visible (not just "some
  limit"), so an OverloadRetry can't fire into a now-different usage/rate cap
  and burn an attempt. Adds `LimitSignal::wakeup_kind()`.
- Detection order is rate-limit -> usage/session -> 529, so a stale "overloaded"
  line above a live usage banner no longer masks the window's reset (which would
  hammer a hard-capped agent every 4 min).
- 529 detection is line-scoped (529 and "overloaded" on the same line) to avoid
  false positives from incidental pane text.
- Day-roll fix: `parse_reset_at` returns today's clock instant; the new
  `usage_wake_at` adds the 5-min buffer and only rolls to tomorrow when the
  buffered wake has already elapsed — a reset seen minutes late wakes soon
  instead of ~24h later (matters across the frequent fork restarts).
- `advance_past_ampm` consumes the trailing dot of "3:10 p.m." so a following
  "(UTC-5)" isn't dropped.
- New `WakeupKind::Unknown`: unknown persisted kinds parse to it instead of
  `Manual`, so a rolled-back/foreign row is skipped by the kind-match gate
  rather than delivered unconditionally.
- Collapses the three near-identical scheduler arms into one shared insert.

Not changed (with reason): the migration comment listing kinds is left stale —
editing an already-applied migration changes its SQLx checksum and breaks
validation on existing DBs. The fixed 4-min overload backoff is intentional per
the desired 3-5 min retry; the attempt cap bounds any sustained-outage hammering.

Adds tests for local-tz default, dotted-meridiem offset, usage_wake_at buffer/
roll boundary, signal->kind mapping, usage-wins-over-stale-529, line-scoped 529,
and unknown-kind parsing. Regenerated shared types.
- Leave a due wake-up pending (retry next tick) when pane capture fails
  transiently, instead of marking it fired and losing the retry — the session
  still exists (checked earlier), so a capture blip must not drop the wake-up.
- parse_tz_offset_secs no longer collapses a malformed UTC/GMT offset (e.g.
  "UTC+99", "UTCx") to plain UTC; it returns None so the caller falls back to
  its default zone. Adds tests for both malformed forms.
@miguelrisero

Copy link
Copy Markdown
Owner Author

✅ ship-it complete — ready for review, CI green, CodeRabbit clean

Gate: isDraft=false · mergeState=CLEAN · all required checks green (backend-remote-checks skipped as expected on the fork) · 0 actionable CodeRabbit findings.

Review battery run: Codex review · 4-persona council (mixed) · /code-review xhigh (18 agents) · 4 /simplify agents · CodeRabbit (2 rounds).

Applied from review (consensus):

  • Bare/named-tz reset times read in the host local zone → UTC (was assumed-UTC).
  • Delivery kind-matches the visible limit (overload can't fire into a usage cap).
  • Detection order rate → usage/session → 529 (stale 529 line can't mask a live usage banner).
  • usage_wake_at owns buffer + day-roll — a just-passed reset wakes soon, not ~24h later.
  • Line-scoped 529; advance_past_ampm keeps a trailing-dot meridiem's offset.
  • WakeupKind::Unknown (rolled-back/foreign rows skipped, not delivered).
  • Transient pane-capture failure leaves the wake pending (CodeRabbit).
  • Malformed UTC+99 → default zone, not silent UTC (CodeRabbit).
  • Collapsed the 3 duplicated scheduler arms.

Rejected with reason: editing the applied migration's comment (SQLx checksum), greedy-digit / trailing-signed-offset parses (inputs Claude's banner grammar doesn't emit), fixed 4-min overload backoff (intentional per the 3–5 min ask).

Backend tests 8 → 30; clippy clean; frontend typechecks; shared types regenerated.

@miguelrisero miguelrisero merged commit e9de55c into main Jul 2, 2026
7 checks passed
@miguelrisero miguelrisero deleted the mr/loop-retry-session-limit-and-529 branch July 2, 2026 17:09
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