Skip to content

Cap claude CLI usage and filter scraped API noise from discovery - #2

Open
msohailse wants to merge 2 commits into
add-anthropic-claude-providerfrom
claude-cli-usage-safety-v2
Open

Cap claude CLI usage and filter scraped API noise from discovery#2
msohailse wants to merge 2 commits into
add-anthropic-claude-providerfrom
claude-cli-usage-safety-v2

Conversation

@msohailse

Copy link
Copy Markdown
Owner

Summary

Follow-up safety work for the Claude CLI provider path (upstream Pickle-Pixel#96), kept as its own PR/diff so it's reviewable separately from the provider addition itself. Based on add-anthropic-claude-provider since these commits modify _chat_claude_cli/ClaudeUsageLimitError, which only exist there until Pickle-Pixel#96 merges -- will retarget to main once it does.

  • Cap claude CLI calls per run: LLMClient now enforces a shared CLAUDE_CLI_CALL_BUDGET (default 40) across every stage (score/judge/tailor/cover), raising before spawning another subprocess once hit, instead of relying on each stage to remember to check. Also fixes scorer.py/cover_letter.py swallowing the usage-limit exception inside a blind except Exception and burning through every remaining job anyway. Bumped the CLI subprocess timeout 120s -> 240s (legitimate tailoring prompts were hitting the old timeout, not just rate limits).
  • Stop discovery from capturing non-job API noise as jobs: the response listener in smartextract.py was capturing any response with /api/ in the URL, including telemetry/auth/consent endpoints job boards fire on page load. These were reaching the LLM judge and, on judge error (fail-open), getting stored as jobs -- wasting a chunk of every run's LLM budget on garbage. Added a denylist at capture time and as a DB-insert backstop, and made the judge fail closed instead of open.

Test plan

  • ruff check / ruff format --check on all changed files -- no new findings beyond pre-existing debt in files touched
  • Verified the call-budget hard stop fires by setting CLAUDE_CLI_CALL_BUDGET low and running the pipeline live against my own Claude CLI subscription
  • Verified scorer.py/cover_letter.py now stop cleanly on a real usage-limit hit instead of looping through every remaining job
  • Verified the noise-URL denylist against the actual junk endpoints seen in a live run (talent.com auth/telemetry, onetrust.com geolocation)

🤖 Generated with Claude Code

The CLI path had no ceiling on how many subprocess calls a single
`applypilot run` could make. Score, judge, tailor, and cover all share
one LLMClient, so a bad batch (e.g. a pile of junk scraped "jobs") could
burn through the entire subscription window before the first stage even
finished, with no way to stop it short of killing the process.

- LLMClient tracks claude_cli_call_count against a CLAUDE_CLI_CALL_BUDGET
  env var (default 40); _chat_claude_cli raises ClaudeUsageLimitError
  before spawning another subprocess once the budget is hit, so the cap
  applies uniformly to every stage without each one having to check it.
- scorer.py and cover_letter.py were catching ClaudeUsageLimitError
  inside a blind `except Exception`, converting it into a fake per-job
  error and continuing -- so even a real limit hit never stopped the
  loop, it just burned through every remaining job. Both now let it
  propagate and break the loop, flushing whatever's already done (same
  pattern tailor.py already had).
- Bumped the CLI subprocess timeout from 120s to 240s -- the tailoring
  prompt (skills boundary, hard rules, full resume) is large enough that
  a cold `claude -p` call was hitting the old timeout on legitimate
  requests, not just rate limits.
collect_page_intelligence()'s response listener captures anything with
"/api/" in the URL, which also catches telemetry, auth, and consent
endpoints a job board's own frontend fires on every page load (e.g.
talent.com's /api/auth/get-session and /api/telemetry/web-vitals,
onetrust.com's geolocation lookup). These were reaching the LLM judge
and, on judge error, getting kept and stored as "jobs" -- so a chunk of
every run's LLM budget (see previous commit) went toward scoring/judging
garbage that was never a job posting.

- Added a denylist (_is_noise_url) applied at capture time, before a
  response is even considered for judging, and again as a backstop right
  before the DB insert in case anything slips through.
- judge_api_responses() was also fail-open: on any error (LLM or
  otherwise) it kept the response rather than dropping it, which is how
  a real usage-limit hit turned into "keep everything for the rest of
  the batch." Now it fails closed and stops the judging pass entirely on
  a usage-limit hit instead of grinding through the rest.
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