fix(agent): retry on JSONDecodeError instead of aborting - #1
Conversation
- Replace flat 900s API timeout with httpx.Timeout (connect=15, read=300, write=60, pool=15) - Add explicit timeout retry logging visible in Modal logs - Add user_notes toolset (list, read, search) — read-only Supabase access with RLS defense-in-depth - Thread-local sandbox env for per-execution isolation in concurrent Modal containers - Inject Snowflake token and HERMES_TMP_DIR into sandbox PYTHONPATH - Exclude json.JSONDecodeError from non-retryable local validation errors Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
json.JSONDecodeError subclasses ValueError, so an unparseable provider response (truncated body, gateway HTML, or an SSE mismatch on a large/slow request) was bucketed as a non-retryable "local validation error" and aborted on attempt 1. Exclude JSONDecodeError from that bucket so transient parse failures retry with backoff like other transient API errors. Surfaced by a ~221K-token report-generation call that returned an unparseable body and hard-failed instead of retrying. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…basic/advanced modes) parallel-web 1.0 moved search/extract from the .beta namespace to the top-level client, replaced fast/one-shot/agentic modes with basic/advanced, dropped max_results (results now sliced client-side), and returns full_content by default. This has been running in deployment (Modal mounts the working tree) since the 1.0 breakage; committing so main matches what ships. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
read_file returns 'LINE_NUM|CONTENT' lines; models sometimes copy blocks from a read result into write_file content verbatim, persisting the gutter into the file — it corrupts <style> blocks and renders as visible junk (shipped broken report HTML to users three times). The patch path already strips this format (patch_parser._apply_update); write_file did not. strip_pasted_line_numbers() fires only on the unmistakable signature: >=4 prefixed lines covering >=50% of non-empty lines with >=90% sequentially increasing numbers — markdown tables, shell pipes, and 'id|value' data files pass through untouched. write_file_tool logs a warning and returns a corrective notice to the model when it fires. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…retry-fix # Conflicts: # run_agent.py
… fixes The parallel/tavily failover dispatched URLs before the website-policy check, which only lived inside the Firecrawl fall-through loop — blocked hosts sailed straight to the new backends, and redirected final URLs were only re-checked on the Firecrawl path. Hoist the policy check into the pre-dispatch filter (alongside SSRF) and re-check final URLs uniformly on every backend's results. Gate the Firecrawl fall-through on client constructibility instead of env sniffing (equivalent in prod; the client raises when unconfigured). Also: add force=True to the invalid-JSON retry-cap error vprint, and update test_blocked_tools_constant for the deliberate execute_code allowance in subagents (b0bf702) with an explicit companion assertion. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The cache used a constant '__default__' key and the disabled-policy fast path returned early without checking key or TTL — after HERMES_HOME changed at runtime, a stale 'disabled' entry from the previous home failed-open the new home's blocklist (and vice versa could serve stale blocks). Key the cache on the resolved default config path and make the fast path require key + freshness. Surfaced by CI once web_extract began calling check_website_access pre-dispatch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review of PR #1 — retry on JSONDecodeErrorTL;DR: this PR is already fully merged into The change itself (for the record)The core fix is sound and I'd have approved it on the merits:
Note the PR's visible diff is much larger than the one-line fix because (a) the base is the stale CI failure diagnosis (
|
Problem
On a large/slow model call, the provider can return a body the client can't parse as JSON (truncated response, gateway HTML, SSE mismatch) →
json.JSONDecodeError. Becausejson.JSONDecodeErrorsubclassesValueError, the retry classifier inrun_agent.pytreated it as a non-retryable local validation error and aborted on attempt 1 (logged as "Non-retryable client error (HTTP None)"), even though the failure is transient and usually clears on retry.Observed in a report-generation run: a ~221K-token call returned an unparseable body and hard-failed instead of retrying.
Fix
Exclude
json.JSONDecodeErrorfrom the local-validation bucket so it falls through to the normal retry/backoff path like other transient API errors. One-line change (+ comment).jsonis already imported. Compiles clean. Base isdev/brian/single-agent-refactor— the branch the current report pipeline runs.🤖 Generated with Claude Code