Skip to content

claude.toml's env_fault pattern false-positives on ordinary prose — including this repo's own CHANGELOG and docs #507

Description

@pbean

Split out from #323, which covers the miss half (no profile classifies provider quota errors). This is the inverse defect, and it blocks the fix #323 prescribes.

Found while reviewing #324, which pins it as a strict xfail rather than leaving it undocumented.

The defect

src/bmad_loop/data/profiles/claude.toml:28 ships #194's original pattern:

API Error.*(Unable to connect|Connection ?(error|refused|reset|timed ?out)|ConnectionRefused|ECONNREFUSED|ETIMEDOUT|ENOTFOUND|EAI_AGAIN)

The unbounded .* between the anchor and the cause makes it match any line that merely mentions both. The claude adapter scans a tmux pane capture, which contains the model's own output — so a story that writes about provider errors trips it.

Compiling the shipped pattern and scanning every tracked file line-by-line yields 37 matching lines. The non-test ones:

  • CHANGELOG.md:81connection stayed alive but idle, printing `API Error: Unable to connect …` until the session
  • docs/FEATURES.md:70 — the #194 bullet describing an API Error … Connection refused-class transport failure
  • src/bmad_loop/adapters/profile.py:107an "API Error … Connection refused" or a "usage limit
  • src/bmad_loop/data/profiles/claude.toml:28the pattern's own line matches itself

The other 33 are in tests/test_engine.py, test_generic_tmux.py, test_escalation.py, test_sweep.py and test_plugin_workflows.py — i.e. exactly the text a dev session prints via pytest output, grep, or a diff hunk. A bmad-loop session working on bmad-loop is exposed on its own repo.

Impact

EnvFaultMixin._classify_env_fault (src/bmad_loop/adapters/env_fault.py:157) only scans a session whose status is timeout/stalled/crashed with result_json is None. So this does not affect healthy completed sessions.

The real harm is the other direction: a genuine timeout is misread as an environment fault, which escalation.decide_dev maps to Action.PAUSE with the budget re-armed instead of charging an attempt. That masks the real timeout and halts the run for an operator. A model can also reach it deliberately — emitting API Error: Connection refused launders its own failure into a non-charged pause.

Why this blocks #323

#323 prescribes:

Extend claude.toml the same way — its current pattern is correctly anchored but connection-class only. (#323, "Fix" section)

the shape claude.toml already uses (API Error.*<cause>). Its cause list is what is too narrow, not its structure.

Both premises are false per the scan above: the structure is the problem. Widening the cause list onto this anchor would inherit the defect and make it materially worse — quota vocabulary (quota, 429, rate limit, usage limit) appears far more often in ordinary story text than ECONNREFUSED does. The anchor must be fixed before the cause list is widened.

Note also that #194 is closed/completed; its fix (#272) is what shipped the pattern now indicted. "Pre-existing #194 debt" means left behind by #194, not tracked by it — hence this ticket.

Pin

Already pinned by #324:

tests/test_env_fault_patterns.py:405

@pytest.mark.xfail(strict=True, reason="pre-existing #194 defect: claude's pattern matches prose")
def test_claude_pattern_is_known_to_false_positive(line: str) -> None:

5 xfail cases, the suite's only xfails. Because it is strict=True, the day the anchor is fixed this test fails and forces the debt scaffolding to be removed with it — the fix should delete the marker plus the CLAUDE_KNOWN_FALSE_POSITIVES/LEGACY_PROFILES scaffolding, and move claude into SEEDED_PROFILES so it is held to the full BAIT corpus.

Fix shape

Needs an anchor a citation cannot reproduce, or a bounded non-greedy gap in the style of opencode's [^"]{0,400}? (src/bmad_loop/data/profiles/opencode.toml:44). Per the header of tests/test_env_fault_patterns.py, any candidate must clear the BAIT corpus; a quota extension additionally needs a captured Claude Code quota line before it is seeded.

Related: #323 (the quota-miss half), #194 (closed; shipped this pattern via #272), #324 (added the pin).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions