cd /news/ai-agents/2-pitfalls-in-priority-probes-lettin… · home topics ai-agents article
[ARTICLE · art-134844] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=· neutral

2 Pitfalls in Priority Probes: Letting One Real Request Through an Open Circuit Breaker

A developer extended the claude-quota-guard.py circuit breaker so that a single priority job can still send one real request every 30 minutes while the circuit is open, after uniform job blocking dropped the xpilot.autopost posting job to a 43% execution rate (231 runs vs. 308 skips). The fix introduces claim_priority_probe and a --priority flag in run_job, sharing one probe slot across all priority jobs to avoid wasting quota. The developer also reports two pitfalls encountered, one of which silently dropped a daily job for four days from 2026-09-13 to 16.

by read7 min views1 publishedSep 20, 2026

A circuit breaker that stops everything is easy to reason about — until the one job that can't afford to wait gets stopped along with everything else. In my setup, that job was posting: it fell to a 43% execution rate (231 runs vs. 308 skips) because engagement jobs had burned through the quota first. This post is about the fix inside claude-quota-guard.pyclaim_priority_probe and run_job, which let a single real request through while the circuit is still open — and the two pitfalls I hit along the way, one of which silently dropped a daily job for four days (2026-09-13 to 16).

Last time, I wrote about how the gate blocks one row, not the whole batch. Before I get to quota-catchup.py — the script that re-runs everything once it detects quota recovery — I want to look at how the SKIPPED markers it reads are actually produced.

When claude-quota-guard.py detects the quota limit, it returns EXIT_CIRCUIT_OPEN (75) until open_until and stops all 15 guarded jobs uniformly. A comment in the code explains why that wasn't enough:

(claude-quota-guard.py:18-24)

The measured result: stopping everything uniformly meant the posting job (xpilot.autopost) got caught in the crossfire of the quota-hungry engagement jobs and dropped to a 43% execution rate — effectively lower priority than threadspilot.engage (64%). On top of that, open_until is determined either by "the reset time parsed from the limit message" or by "a 6-hour cooldown" (record_claude_result), so even if the actual quota comes back earlier, the circuit dutifully stays open until that time. For a job like posting, where "if you miss the time slot, it never gets filled," that's not something you can ignore.

--priority jobs one attempt every 30 minutes The fix is: "even while the circuit is open, let priority jobs — and only priority jobs — send one real request at a fixed interval." The caller passes --priority when handing a command to run_job.

def run_job(label: str, command: list[str], priority: bool = False) -> int:
    if not command:
        print("claude quota guard: --job requires a command after --", file=sys.stderr)
        return 2
    status = circuit_status()
    probe_claimed = False
    if status["is_open"]:
        if not (priority and claim_priority_probe()):
            print(
                "CLAUDE_QUOTA_JOB_SKIPPED "
                f"job={label} reason={status['reason']} remaining={status['remaining_seconds']}s ts={now()}",
                file=sys.stderr,
            )
            return 0
        probe_claimed = True
        print(
            "CLAUDE_QUOTA_JOB_PRIORITY_PROBE "
            f"job={label} reason={status['reason']} remaining={status['remaining_seconds']}s ts={now()}",
            file=sys.stderr,
        )

(claude-quota-guard.py:471-490)

Even with priority set, whether the job can actually attempt anything is decided by claim_priority_probe().

def claim_priority_probe() -> bool:
    """circuit が開いている間、優先ジョブに試行権を1つ渡す。

    間隔は全優先ジョブで共有する(=1本が使ったら次の枠まで他も待つ)。上限に本当に
    達している間に何本も叩いてもクォータは戻らないため、叩く回数自体を絞る。
    """
    if PRIORITY_PROBE_INTERVAL <= 0:
        return False
    with locked_state() as state:
        last = int(state.get("last_priority_probe") or 0)
        current = now()
        if current - last < PRIORITY_PROBE_INTERVAL:
            return False
        state["last_priority_probe"] = current
        return True

(claude-quota-guard.py:454-468; PRIORITY_PROBE_INTERVAL defaults to 1800 seconds at claude-quota-guard.py:25)

last_priority_probe is a single timestamp stored in locked_state() (a JSON-persisted state guarded by fcntl.flock). The key point is that this value lives per circuit, not per job: it doesn't care who probed, only "has it been 30 minutes since anyone last probed?"

The side that wins the attempt passes CLAUDE_QUOTA_PRIORITY_PROBE=1 in the environment of the child process it launches via subprocess.run.

    guard = str(Path(__file__).resolve())
    env = os.environ.copy()
    env["CLAUDE_AUTOMATION_GUARD"] = "1"
    env["CLAUDE"] = guard
    env["CLAUDE_BIN"] = guard
    if probe_claimed:
        env["CLAUDE_QUOTA_PRIORITY_PROBE"] = "1"

(claude-quota-guard.py:491-498)

The receiver of this flag is run_claude. When the job command internally invokes the real claude binary, PATH has been rewired so the call goes through the guard itself — which means a second circuit_status() check runs inside the child process.

def run_claude(arguments: list[str]) -> int:
    status = circuit_status()
    if status["is_open"]:
        if os.environ.get("CLAUDE_QUOTA_PRIORITY_PROBE") != "1":
            print(
                "CLAUDE_QUOTA_CIRCUIT_OPEN "
                f"reason={status['reason']} remaining={status['remaining_seconds']}s",
                file=sys.stderr,
            )
            return EXIT_CIRCUIT_OPEN
        print(
            "CLAUDE_QUOTA_PRIORITY_PROBE_PASS "
            f"reason={status['reason']} remaining={status['remaining_seconds']}s",
            file=sys.stderr,
        )

(claude-quota-guard.py:416-432)

If you forget to propagate the environment variable, run_job wins the attempt, but the inner run_claude checks the circuit again and immediately returns EXIT_CIRCUIT_OPEN. The one line that gets you through both layers of the gate is env["CLAUDE_QUOTA_PRIORITY_PROBE"] = "1".

And the result of the actual call flows into record_claude_result as usual. If the limit message still comes back, open_until is extended; if the call succeeds, the circuit closes. Whether the probe fails or succeeds, that single result directly determines the circuit's next state — which is what makes this "one real request let through."

As the docstring on claim_priority_probe says, PRIORITY_PROBE_INTERVAL is shared across all jobs, not tracked per job. Even if several --priority jobs are scheduled inside the same 30-minute window, the moment the first one passes claim_priority_probe(), last_priority_probe is updated. Every subsequent job evaluates to False at priority and claim_priority_probe() and falls through to SKIPPED without ever touching the real client.

This is intentional. While the limit is genuinely in effect, hammering it with multiple requests won't bring the quota back, so the design throttles the number of attempts themselves. Operationally, though, if you forget that "having multiple priority jobs does not mean each gets its own 30-minute opportunity," you will lose time wondering "why does this one job never get a turn to verify recovery?"

RAN exit≠0 This is the main subject of this post. After subprocess.run, run_job checks whether the probe came up empty and emits a different marker accordingly.

    try:
        result = subprocess.run(command, env=env, check=False)
    except OSError as exc:
        print(f"claude quota guard job={label}: {exc}", file=sys.stderr)
        return 127
    if probe_claimed and result.returncode != 0 and circuit_status()["is_open"]:
        print(
            "CLAUDE_QUOTA_JOB_SKIPPED "
            f"job={label} reason=priority-probe-quota exit={result.returncode} ts={now()}",
            file=sys.stderr,
        )
        return result.returncode
    print(
        f"CLAUDE_QUOTA_JOB_RAN job={label} exit={result.returncode} ts={now()}",
        file=sys.stderr,
    )
    return result.returncode

(claude-quota-guard.py:499-518)

Before this branch existed, the code only looked at the fact that the probe had won the attempt and actually launched a child process, and fell straight through to the trailing CLAUDE_QUOTA_JOB_RAN. Even when the probe hit the limit again and ended with exit≠0, what remained in the log was CLAUDE_QUOTA_JOB_RAN job=... exit=1.

The problem is that this RAN marker means "already done" as far as quota-catchup.py is concerned. Here is latest_job_marker, which quota-catchup.py uses to narrow down re-run candidates:

def latest_job_marker(paths: list[Path], label: str) -> Optional[Tuple[str, int]]:
    """Return the newest timestamped skip/run marker for one launchd label."""
    marker_pattern = re.compile(
        r"CLAUDE_QUOTA_JOB_(SKIPPED|RAN)\s+job="
        + re.escape(label)
        + r"(?=\s|$).*\bts=(\d+)(?=\s|$)"
    )
    ...

def latest_marker_is_today_skip(paths: list[Path], label: str, today: datetime.date) -> bool:
    marker = latest_job_marker(paths, label)
    if marker is None:
        return False
    marker_type, timestamp = marker
    return marker_type == "SKIPPED" and datetime.fromtimestamp(timestamp).astimezone().date() == today

(quota-catchup.py:160-193)

As you can see, this check only looks at the marker type (SKIPPED or RAN) and never inspects the exit code on the RAN side. Even if the probe failed with exit=1, as long as the last log line is CLAUDE_QUOTA_JOB_RAN, latest_marker_is_today_skip returns False and the job quietly drops out of find_candidates' re-run set.

The consequence: even after the circuit really closes, the job sits there with the wrong record — "already RAN today" — until its next scheduled time (the following morning, for instance). The comment records the real-world impact: note2-daily and codex-note-funnel both lost their daily run through this path between 2026-09-13 and 16.

The fix is simple. Check the condition "the probe came up empty while the circuit was open" (probe_claimed and result.returncode != 0 and circuit_status()["is_open"]) first, and only in that case emit CLAUDE_QUOTA_JOB_SKIPPED reason=priority-probe-quota instead of CLAUDE_QUOTA_JOB_RAN. The function's return value stays result.returncode, unchanged. launchd's LastExitStatus still records the actual failure correctly, while only the log marker that quota-catchup.py reads gets relabeled as "still needs a retry." That's the separation of concerns.

Note: The exit code and "should this be retried?" are separate axes. This bug happened because the two had been crammed into a single RAN marker, and the quota-catchup.py side never anticipated the exit≠0 case. When designing log markers, it's safer not to let "what actually happened" and "what the downstream batch should do next" share the same string.

claim_priority_probe through last_priority_probe in locked_state, and it is CLAUDE_QUOTA_PRIORITY_PROBE=1. Forget it, and the inner run_claude rejects the call at the second circuit checkCLAUDE_QUOTA_JOB_RAN exit≠0. The re-run check in quota-catchup.py only looks at the marker type and never at the exit code Next time, I'll cover how quota-catchup.py picks up these SKIPPED markers and decides how much to re-run after recovery.

If you run a circuit breaker in front of your own scheduled jobs: does your downstream retry logic distinguish "ran and failed" from "never really got a chance"?

*Written by Lily — I ship iOS apps and automate my content stack with Claude Code.

Follow along: Portfolio · X · GitHub*

── more in #ai-agents 4 stories · sorted by recency
── more on @claude-quota-guard.py 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/2-pitfalls-in-priori…] indexed:0 read:7min 2026-09-20 ·