Skip to content

Seed a Claude Code usage-limit/quota env-fault pattern once a real line is captured #610

Closed

Description

@pbean

Follow-up to #323 and #507, whose PR replaced claude.toml's env_fault_patterns. It leaves the quota class unseeded on the claude adapter, and this records why plus what would lift the constraint.

What is missing

src/bmad_loop/data/profiles/claude.toml now classifies two failure classes on the claude adapter — a connection loss and a provider 5xx refusal — each seeded as a complete captured CLI error sentence. It classifies no subscription usage-limit refusal. So when an Anthropic plan's usage limit stops a session, the run still charges a dev attempt, exhausts max_dev_attempts and defers a story whose spec was fine. That is the same failure #323 reported on another provider, and it is still unshipped on this adapter.

Why it was not done in the #323/#507 PR

There is no captured Claude Code usage-limit line to seed from.

Searched on 2026-08-15:

usage limit reached and limit will reset do hit in the transcripts — 33 and 31 files respectively — but every hit is a citation, not an emission: issue #323's own AI_APICallError: Usage limit reached for 5 hour… line, which is opencode's provider text rather than Claude Code's; this repo's tests/test_env_fault_patterns.py corpus; the withdrawn draft pattern; and agent sessions discussing exactly those. None is a line Claude Code printed.

The standing bar lives in tests/test_env_fault_patterns.py, in test_unseeded_profiles_stay_inert's docstring: seed from a captured log line with the run it came from cited, never from a plausible-looking string. An unverified pattern is not a neutral bet — one that fires on a healthy session pauses the entire run, which is worse than the fault it was meant to catch.

Why the vocabulary cannot simply be added

The claude adapter's scanned log is a tmux pane capture (EnvFaultMixin.ENV_FAULT_LOG_SUFFIX), so it carries the model's own output. 429, rate limit, quota and usage limit are exactly what a story that implements rate limiting prints all day — in test names, fixtures, banner copy, acceptance criteria and diff hunks.

That is measured, not assumed. Compiling the narrowest plausible quota anchor, API Error: 429, against the corpus already in tests/test_env_fault_patterns.py false-positives on 3 lines sitting in ANCHOR_REACHING_BAIT today:

// TODO: surface "API Error: 429" to the user with a Retry-After hint
expect(banner).toBe("API Error: 429 rate_limit_error")
- [ ] AC-2: map API Error: 429 to a friendly rate limit message

A slightly looser form (the framing token plus up to 80 characters plus quota vocabulary) takes it to 4. Every one of those is ordinary healthy-session output, and every one would pause a working run.

This is the same trap #507 documented from the other side: widening a cause list onto a loose anchor makes the false-positive half worse, because quota vocabulary is far more common in ordinary story text than ECONNREFUSED is.

An option that would remove the constraint — offered as an option, not a commitment

Point the claude profile's env-fault scan at Claude Code's own JSONL transcript instead of the pane capture, and anchor on the structural key "isApiErrorMessage":true rather than on error vocabulary at all.

Why that anchor differs in kind: inside the transcript a model's own words live within a JSON string, so a citation of the key is escaped — \"isApiErrorMessage\" — while the structural key is not. A citation cannot physically reproduce the anchor. That is the property #507 asked for, and it is not available on a pane capture at all.

Measured across the same 2681 transcripts:

form occurrences files
unescaped structural key "isApiErrorMessage":true 11 7
JSON-escaped citation \"isApiErrorMessage\" 46 4

Zero file overlap. And the 11 structural occurrences carry only 4 distinct texts, all genuine CLI errors:

  • API Error: 500 Internal server error…
  • API Error: 529 Overloaded…
  • API Error: Connection closed mid-response…
  • a model-selection error (There's an issue with the selected model…)

— despite those same files containing plenty of prose citing provider errors.

The cost, stated honestly, because it is not small:

  • a new per-profile field naming which source the env-fault scan reads;
  • a scan-source seam in EnvFaultMixin: _env_fault_evidence() takes only a task_id today and derives the path from ENV_FAULT_LOG_SUFFIX, whereas the transcript path arrives later, on the SessionResult;
  • porting the ~15 test_classify_env_fault_* tests in tests/test_generic_tmux.py, which write their fixture to whatever path the code resolves;
  • it is claude-specific — no other pane-capture profile has an equivalent structural transcript.

Worth weighing too: the transcript is written by the CLI, so repointing trades one soundness argument for a different one, and that argument has to be re-derived rather than inherited.

What to do when a line IS captured

  1. Add the captured line to CLAUDE_REAL in tests/test_env_fault_patterns.py, citing the run it came from.
  2. Extend claude.toml's env_fault_patterns with a pattern reproducing that complete sentence, keeping a single-character class so the pattern does not match its own source line.
  3. Re-run the corpus — test_seeded_profiles_never_classify_healthy_model_output, test_seeded_profiles_survive_bait_that_reaches_their_anchor, test_shipped_patterns_do_not_match_their_own_profile_line and test_pane_capture_patterns_do_not_match_this_repo are the gate.

Context: #323 (the quota-miss half) and #507 (the false-positive half).

Activity

  1. added
    bugSomething isn't working
    needs-designAwaiting a maintainer design decision before code
    area:adaptersCoding-CLI adapters and profiles
    P4Parked - needs design, blocked upstream/PR, or speculative
    on Aug 15, 2026
  2. coderabbitai commented on Aug 15, 2026

    @coderabbitai
    🔗 Related PRs

    #239 - feat(adapters): coding-CLI adapter registry (Seam A) — out-of-tree adapters with zero core edits [merged]
    #354 - fix(worktree): re-register the relay over a seeded hook config [merged]
    #522 - fix(adapters,engine): tell a lost mux session apart from an exited CLI [merged]
    #605 - fix(deferredwork): bound an entry at every CommonMark ATX heading (#516) [open]
    #609 - fix: close the codec except-family holes in the diag writer, tmux captures, and plugin hooks (#380, #383) [open]


    📝 Issue Planner

    Check the box below or use the @coderabbitai plan command to generate an implementation plan and prompts that you can use with your favorite coding assistant.

    • Create Plan

    🧪 Issue enrichment is currently in open beta.

    You can configure auto-planning by selecting labels in the issue_enrichment configuration.

    To disable automatic issue enrichment, add the following to your .coderabbit.yaml:

    issue_enrichment:
      auto_enrich:
        enabled: false

    💬 Have feedback or questions? Drop into our discord!

  3. sirendhead commented on Sep 7, 2026

    @sirendhead
    Contributor

    Captured lines — two real Claude Code quota sentences from unattended color-dev-loop runs (bmad-loop 0.11.1, claude profile, psmux on Windows 11, Claude Code classic renderer), verbatim from the ANSI-stripped pane logs:

    ⎿ You're out of usage credits. Run /usage-credits to keep using Fable 5.1 or /model to switch models.
    ⎿ You've hit your session limit · resets 12:50pm (Asia/Ho_Chi_Minh)
    

    Where (both files are pane captures the engine wrote itself):

    run pane log line occurrences
    skills-library 20260907-084618-824f .bmad-loop/runs/20260907-084618-824f/logs/TASK-2732-dev-1.log both sentences 5
    skills-library 20260907-084618-824f .bmad-loop/runs/20260907-084618-824f/logs/TASK-2732-review-1.log usage credits 3
    prompts-hub 20260907-090132-98e3 .bmad-loop/runs/20260907-090132-98e3/logs/TASK-1735-dev-{1,2}.log usage credits 3 + 6

    Observed engine behaviour without a seeded pattern: the session goes idle, the stall wake-nudge fires every ~10 min ("You appear idle in bmad-loop automation mode…"), each nudge reproduces the same quota line, and the session is on its way to dev_stall_nudges_cap → a charged attempt (review-1 on skills-library, dev-2 on prompts-hub — dev-1 had already been consumed the same way). I hard-stopped both runs to keep them resumable.

    Two distinct causes, worth two alternatives with the profile's own single-character-class trick so the profile line does not match itself:

    • You're out of usage credit[s]\. Run /usage-credits — credit exhaustion (this one is a money gate: no re-arm will succeed until the operator tops up or switches model).
    • You've hit your session limi[t] · resets — the 5-hour window; a pause + re-arm after the reset time is the right shape.

    Note the second sentence carries a reset time; a future refinement could parse it into the pause reason. Happy to open the PR (pattern + test_shipped_patterns_do_not_match_their_own_profile_line pins) once you confirm this is the evidentiary bar you wanted.

  4. sirendhead commented on Sep 9, 2026

    @sirendhead
    Contributor

    Captured line + PR: #774 seeds "You're out of usage credit[s]" in src/bmad_loop/data/profiles/claude.toml env_fault_patterns (single-character-class trick like the existing entries), with the real line verbatim in the comment — You're out of usage credits. Run /usage-credits to keep using Fable 5.1 or /model to switch models. — captured on 2026-09-07 in two unattended runs (four Claude Code sessions idled through the stall-nudge cap because nothing matched). Test: tests/test_env_fault_patterns.py gets the line in CLAUDE_REAL plus three near-miss bait lines; ablation (pattern removed) fails exactly test_each_seeded_profile_catches_its_own_cli_error_lines[claude-You're out of usage credits…]. The sibling session limit line stays unseeded (separate wording, not captured verbatim here). Prepared by a Claude Code sonnet subagent on the operator's go, from fork sirendhead/bmad-loop.

  5. nidelson commented on Sep 14, 2026

    @nidelson

    Second independent capture of the session limit sibling — different OS, different multiplexer, same sentence. Offered as the verbatim line #774's description says is still missing, plus one distinction the pattern has to survive.

    Captured line

    bmad-loop 0.11.1, claude profile, macOS 26.6.2 + tmux (the earlier capture was Windows 11 + psmux), engine-written pane log, ANSI-stripped:

    ⎿  You've hit your session limit · resets 7:20pm
    
    run pane log occurrences
    20260913-165153-b850 (story 5a) .bmad-loop/runs/20260913-165153-b850/logs/5a-dev-2.log (3,837,518 bytes) 2

    Matching the line reported on 2026-09-07 from psmux on Windows, minus the parenthesised timezone. The reset time is present in both; the timezone suffix is not stable across renderers, so an anchor that stops at · resets covers both captures.

    The distinction the pattern must survive: the same session prints a warning family

    This is the part I did not see recorded yet. The same pane, in the same session, carries two families built from nearly identical words:

    [6x] You've used 90% of your session limit · resets 7:20pm…    ← healthy session, advisory
    [4x] You've used 97% of your session limit · resets 7:20pm…    ← healthy session, advisory
    [3x] You've used 96% of your session limit · resets 7:20pm…    ← healthy session, advisory
    [2x] ⎿  You've hit your session limit · resets 7:20pm          ← actually blocked
    

    The advisory family outnumbers the real fault 13 to 2. A session printing You've used 90% of your session limit is working normally — pausing the run there would be exactly the false-positive hazard #507 is about, and it would fire on the majority of occurrences.

    The proposed anchor handles this correctly. Compiled against all four distinct forms from our log:

    pattern: You've hit your session limi[t] · resets
    
      CASA   -> ⎿  You've hit your session limit · resets 7:20pm             (2 occurrences)
      ignora -> You've used 90% of your session limit · resets 7:20pm…       (6)
      ignora -> You've used 96% of your session limit · resets 7:20pm…       (3)
      ignora -> You've used 97% of your session limit · resets 7:20pm…       (4)
    

    hit your vs used N% of your is what separates them; an anchor loosened to session limit · resets would match all four. Worth a bait line in test_env_fault_patterns.py — the advisory is a near-miss that a healthy run emits repeatedly, not a hypothetical one.

    Engine behaviour without the pattern — a different shape from the 2026-09-07 report

    The earlier report had the session going idle and the stall wake-nudge firing every ~10 min toward dev_stall_nudges_cap. Ours never idled: zero You appear idle in bmad-loop automation mode in the pane, zero stall/nudge events in the journal.

    17:58:16  session-start  5a-dev-2
      ~73 min blocked by the window limit
    19:11     window resets  →  the session RESUMES on its own (pane log grows again, +613 B/5 s at 19:26)
    19:28:17  session-end    5a-dev-2  status=timeout  tokens_weighted=3204837
    19:28:19  story deferred
    

    The session kept the pane alive throughout, so stall detection had nothing to arm on. What killed it was session_timeout_min: the clock had been running through the whole blocked stretch, so when the window reset at ~19:11 only 17 of the 90 minutes were left. The story deferred with its work uncommitted; it had to be recovered by hand.

    So on this adapter the class costs a full dev leg (3,204,837 weighted tokens, ~32% of the run's total) even when the session never idles and does come back by itself.

    One observation about the reset time, for whatever it's worth

    Both captures carry a reset time in the sentence itself (resets 7:20pm, resets 12:50pm (Asia/Ho_Chi_Minh)), and the window really does reopen at that time — ours did, unattended. That is a stronger signal than the credit-exhaustion sibling, which needs an operator. If the pause reason ever parses it, the re-arm has a known deadline rather than a poll.

    Whether the elapsed blocked time should also stop counting against session_timeout_min is a separate question from seeding the pattern, and I don't want to conflate the two here — happy to open that separately if it isn't already covered by something I missed.

    Raw evidence is a pane log the engine wrote itself; can quote more context around any of the four forms if useful.

  6. removed
    needs-designAwaiting a maintainer design decision before code
    on Sep 20, 2026
  7. added a commit that references this issue on Oct 25, 2026
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

    P4Parked - needs design, blocked upstream/PR, or speculativearea:adaptersCoding-CLI adapters and profilesbugSomething isn't workinghelp wantedExtra attention is needed

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions