Skip to content

Quota guard pauses capture on seven_day_overage_included "rejected" even though the request succeeded (15.3.0) #5182

Description

@beckman55

Summary

On 15.3.0 with Claude subscription auth, the quota guard pauses capture whenever the seven_day_overage_included rate-limit window reports status: "rejected" — even though the request itself succeeded on the normal weekly allowance. Every cooldown probe then succeeds, stores an observation, and immediately re-arms a 30-minute cooldown, so capture stays paused until the overage-included window resets (days), while the account has plenty of normal allowance left.

Environment

  • claude-mem 15.3.0 (worker on Linux, bun), Claude Code CLI v2.1.312
  • CLAUDE_MEM_PROVIDER=claude, CLAUDE_MEM_CLAUDE_AUTH_METHOD=cli (Claude Code OAuth token, Max subscription)
  • Health endpoint rate limits at the time: five_hour 2%, seven_day 52%, seven_day_overage_included = rejected

Evidence (worker log, one probe)

12:39:13.126 [SDK]     Starting SDK query {sessionDbId=279, ...}
12:39:18.998 [SDK]     [session-279] ← Response received (1498 chars) {promptNumber=71} <observation>
12:39:19.004 [DB]      [session-279] STORED | ... obsCount=1 | obsIds=[15033]
12:39:19.012 [SDK]     Aborting session for quota guard: quota:seven_day_overage_included rejected by provider {window=seven_day_overage_included, ...}
12:39:19.780 [SESSION] [session-279] Generator paused for quota; preserving buffered work {pendingCount=76}

The same sequence repeated at 11:37, 12:08 and 12:39 — each probe got a real response and stored an observation, then the guard aborted and re-armed quota-cooldown.json (until = armedAt + 30 min). Between probes:

Skipping generator start while the provider cooldown is active {provider=claude, window=seven_day_overage_included, ...}
Primary in quota cooldown and the quota fallback cannot serve; capture waits until it clears {primary=claude, fallback=claude}

SessionStart banner: "The memory observer's allowance on claude is used up … Claude rejected the request because its weekly overage-included usage allowance is exhausted."

Changing CLAUDE_MEM_MODEL does not help (the cooldown is per provider and the next event re-arms it), and neither does a restart.

Root cause

In the bundled worker-service.cjs the hard-rejection branch (minified OLe) treats status === "rejected" on any window as exhaustion:

function OLe(t,e){
  if (e.status==="rejected" || t==="overage" && e.overageStatus==="rejected")
    return {abort:!0, window:t, reason:`quota:${t} rejected by provider`, pause:{kind:"quota_exhausted", ...}}
}

and g7 includes seven_day_overage_included in the windows it walks. For that window, "rejected" only means the included-overage pool is spent — the request is still served from the normal allowance (as the stored observation proves). The overage window already gets special handling via overageStatus; seven_day_overage_included needs the same kind of exception.

Workaround / suggested fix

Local one-line patch (applied to both the marketplace and cache copies), then clear ~/.claude-mem/quota-cooldown.json and restart the worker:

-function OLe(t,e){if(e.status==="rejected"||
+function OLe(t,e){if(e.status==="rejected"&&t!=="seven_day_overage_included"||

After the restart, capture resumed immediately (new observation stored, no guard abort, no cooldown re-armed).

A more robust fix in source might be: only treat a window's rejected status as exhaustion when the request actually failed (no assistant response / an error result), or exclude informational windows like seven_day_overage_included from the hard-rejection branch while still letting the utilization thresholds apply.

Related: #5012 (different stale-entry wedge; its "smaller related observation" about overage/overageStatus is the same family as this one).

Activity

  1. thedotmack commented on Oct 29, 2026

    @thedotmack
    Owner

    Thanks @beckman55, confirmed on main. providerRejection in src/services/worker/RateLimitStore.ts:512-516 treats status: 'rejected' on any window as exhaustion, including seven_day_overage_included, so ClaudeProvider.ts:582-596 aborts the session even when the observer's own request succeeded. That contradicts the window's own contract comment (RateLimitStore.ts:49-59): only the provider refusing the observer's request should stop it on that bucket.

    The fix is to ignore a rejected status on seven_day_overage_included unless the request itself was refused, with a test that feeds a successful response carrying that header. Happy to help land a PR if you or anyone wants to take it.

  2. beckman55 commented on Oct 29, 2026

    @beckman55
    Author

    Thanks for confirming. Here's a patch against main (342dfe0) you can commit as-is. If you want to keep attribution, add this trailer to the commit:

    Co-authored-by: beckman55 <11038775+beckman55@users.noreply.github.com>
    

    Change: providerRejection skips seven_day_overage_included. That window's headers ride on every response of an account with the bucket, so a rejected status there can arrive on a request that succeeded. A request the provider actually refuses still pauses capture through the existing paths: quota-limit prose → ResponseProcessor (quota:observer_text), or a thrown quota error → ClaudeProvider (quota:unknown:quota_exhausted). Every other window's rejected handling is unchanged.

    Tests: replaces aborts on a rejected overage-included weekly window (it asserted the buggy behavior) with:

    • a rejected overage-included snapshot from a successful response → { abort: false } (fails on main, passes with the patch)
    • a rejected overage-included window next to a rejected seven_day → still aborts on seven_day

    All 14 test files that reference the quota guard / RateLimitStore: 364 pass, 0 fail. tsc --noEmit is clean.

    Apply with git apply 5182.patch:

    5182.patch
    diff --git a/src/services/worker/RateLimitStore.ts b/src/services/worker/RateLimitStore.ts
    index 3743ef48a..dd70f18fc 100644
    --- a/src/services/worker/RateLimitStore.ts
    +++ b/src/services/worker/RateLimitStore.ts
    @@ -54,8 +54,9 @@ export type RateLimitWindow =
        * whatever model the request used, so the figure (and any warning the CLI
        * derives from it) is not evidence that the observer draws on it. Above 1
        * it is usage that legitimately ran past the cap, not a refusal. Only the
    -   * provider refusing the observer's own request (`rejected`) stops the
    -   * observer on it.
    +   * provider refusing the observer's own request stops the observer on it;
    +   * a `rejected` status in the headers of a response that succeeded does
    +   * not (#5182).
        */
       | 'seven_day_overage_included'
       | 'overage';
    @@ -508,8 +509,15 @@ export function shouldAbortForQuota(
      * The provider has already declared the bucket exhausted: status='rejected',
      * or overageStatus='rejected' on the overage window. Stop regardless of
      * whether utilization is reported.
    + *
    + * `seven_day_overage_included` is the exception: its headers ride on every
    + * response of an account with the bucket, so a `rejected` status there can
    + * arrive on a request that succeeded (#5182). A request the provider actually
    + * refused comes back as quota-limit prose or a thrown quota error, and those
    + * paths pause the generator themselves.
      */
     function providerRejection(window: RateLimitWindow, entry: RateLimitEntry): QuotaDecision | undefined {
    +  if (window === 'seven_day_overage_included') return undefined;
       const isRejected =
         entry.status === 'rejected' ||
         (window === 'overage' && entry.overageStatus === 'rejected');
    diff --git a/tests/worker/rate-limit-store.test.ts b/tests/worker/rate-limit-store.test.ts
    index dcc6ac439..20fafe7c8 100644
    --- a/tests/worker/rate-limit-store.test.ts
    +++ b/tests/worker/rate-limit-store.test.ts
    @@ -176,15 +176,34 @@ describe('shouldAbortForQuota — cli/oauth auth', () => {
         expect(shouldAbortForQuota(cliAuth, store, FIXED_NOW)).toEqual({ abort: false });
       });
     
    -  it('aborts on a rejected overage-included weekly window', () => {
    +  // The overage-included headers ride on every response, so `rejected` there
    +  // can arrive on a request that succeeded. A real refusal comes back as
    +  // quota-limit prose or a thrown quota error, which pause on their own (#5182).
    +  it('does not abort on a rejected overage-included status from a successful response', () => {
         store.set({
           rateLimitType: 'seven_day_overage_included',
           status: 'rejected',
    +      utilization: 1.05,
           resetsAt: FIXED_NOW + 24 * 60 * 60 * 1000,
         });
    +    expect(store.getMostRecentByWindow().seven_day_overage_included?.status).toBe('rejected');
    +    expect(shouldAbortForQuota(cliAuth, store, FIXED_NOW)).toEqual({ abort: false });
    +  });
    +
    +  it('still aborts on a rejected window the observer draws on next to a rejected overage-included one', () => {
    +    store.set({
    +      rateLimitType: 'seven_day_overage_included',
    +      status: 'rejected',
    +      resetsAt: FIXED_NOW + 24 * 60 * 60 * 1000,
    +    });
    +    store.set({
    +      rateLimitType: 'seven_day',
    +      status: 'rejected',
    +      resetsAt: FIXED_NOW + 2 * 24 * 60 * 60 * 1000,
    +    });
         const decision = shouldAbortForQuota(cliAuth, store, FIXED_NOW);
         expect(decision.abort).toBe(true);
    -    expect(decision.window).toBe('seven_day_overage_included');
    +    expect(decision.window).toBe('seven_day');
       });
     
       it('does not abort on inactive overage at 100% utilization', () => {
  3. MohamedFBoussaid commented on Oct 30, 2026

    @MohamedFBoussaid

    Same false positive on the plain overage window in 15.7.0, on an account without extra usage enabled.

    Environment

    • claude-mem 15.7.0 (worker on macOS), CLAUDE_MEM_PROVIDER=claude, CLAUDE_MEM_CLAUDE_AUTH_METHOD=cli (subscription, OAuth token from the credential store)
    • Extra usage is not enabled on the account, so the overage window always reports rejected

    Evidence (worker log, three consecutive probes)

    20:21:53.217 [SDK] [session-1561] ← Response received (1534 chars) {promptNumber=23} <observation>
    20:21:53.299 [SDK] Aborting session for quota guard: quota:overage rejected by provider {window=overage, authMethod=Claude Code OAuth token ...}
    20:21:53.832 [SESSION] [session-1561] Generator paused for quota; preserving buffered work {pendingCount=87}
    
    20:52:47.319 [SESSION] Primary quota cooldown elapsed; probing primary {primary=claude}
    20:52:51.269 [SDK] [session-1561] ← Response received (98 chars) {promptNumber=29}
    20:52:51.327 [SDK] Aborting session for quota guard: quota:overage rejected by provider {window=overage ...}
    
    21:23:51.720 [SDK] [session-1561] ← Response received (81 chars) {promptNumber=29}
    21:23:51.788 [SDK] Aborting session for quota guard: quota:overage rejected by provider {window=overage ...}
    

    Every probe succeeds, then the guard re-arms a 30-minute cooldown. quota-cooldown.json shows "window": "overage", "auth": "subscription", and the user-facing message reads "Claude rejected the request because its extra-usage usage allowance is exhausted." Meanwhile the main CLI session on the same account keeps working on its normal allowance.

    Where it comes from in 15.7.0

    providerRejection aborts on e.status === "rejected" for any window, and additionally on t === "overage" && e.overageStatus === "rejected". For an account without extra usage, the overage bucket is permanently rejected, so the guard can never clear on its own.

    History on this install

    Before 15.5.0 the only window that ever paused capture here was seven_day (22 occurrences, all genuine). quota:overage rejected appears only from 15.5.0 onwards (7 occurrences, all against a successful response).

    Suggested rule

    Treat a rejected status on overage (and seven_day_overage_included, as in the fix discussed above) as informational only. The observer should pause on these buckets only when its own request was actually refused by the provider.

    Workaround that works

    CLAUDE_MEM_QUOTA_FALLBACK_PROVIDER=openrouter (or gemini) routes capture through the fallback while the cooldown is active, and the buffered observations drain.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions