Skip to content

Fix #276: WebFetch/WebSearch honor abort_controller (ESC-cancel)#307

Open
ericleepi314 wants to merge 1 commit into
fix/issue-275-trust-dialog-wiringfrom
fix/issue-276-web-abort
Open

Fix #276: WebFetch/WebSearch honor abort_controller (ESC-cancel)#307
ericleepi314 wants to merge 1 commit into
fix/issue-275-trust-dialog-wiringfrom
fix/issue-276-web-abort

Conversation

@ericleepi314

Copy link
Copy Markdown
Collaborator

Closes #276
Closes #170 (duplicate)

Stacked on #306 (← #305#304). Merge in order; GitHub retargets automatically.

Summary

ESC during a slow WebFetch/WebSearch blocked the agent until the urllib socket timeout (15–20s), while Bash/Read/Grep already cancel in ~50–100ms. urllib has no cancellation primitive, so this builds it from two mechanisms in a new src/utils/abortable_net.py:

  • call_with_abort(fn, signal) — runs the blocking call on a daemon worker thread and polls the abort signal every 50ms. On ESC the caller unblocks immediately (raises AbortError); the worker dies at its bounded socket timeout, and a late-arriving response is closed rather than leaked.
  • abortable_read(resp, max_bytes, signal) — chunked 64KB body read with an abort listener that shuts the socket down (SHUT_RDWR) before closing. Empirically necessary: resp.close() alone does not interrupt a recv blocked on another thread — the first version of the test stalled the full 8s timeout until the shutdown was added.

Wiring:

  • WebFetch: signal threaded through the redirect loop (checked per hop), the opener.open, the body read, and the Cloudflare-UA retry recursion. Cached results skip fetching entirely.
  • WebSearch: the Tavily urlopen+read runs under call_with_abort.

Both raise AbortError, which the dispatch layer already converts to the user-cancel REJECT_MESSAGE (same path as grep/ripgrep cancellation).

Test plan

  • 13 new tests in tests/test_web_abort.py: unit coverage for both primitives (pre-aborted short-circuit, abort-mid-call <2s, worker exception relay, late-result close, listener cleanup) plus integration against a real local stalling HTTP server — WebFetch mid-body abort and Tavily abort both unblock in ~0.2s while the server is still stalling (3s) and the socket timeout is 8s+
  • Full suite on the stack: 7785 passed, 0 failed, 5 skipped

🤖 Generated with Claude Code

ESC during a slow fetch blocked the agent until the urllib socket
timeout (15-20s) — the longest interactive stall left in the tool set.

New src/utils/abortable_net.py primitives:
- call_with_abort: blocking call on a daemon worker thread, abort
  polled at 50ms; the caller unblocks immediately on ESC while the
  worker dies at its (bounded) socket timeout
- abortable_read: chunked body read with an abort listener that shuts
  the socket down (close() alone does not interrupt a recv blocked on
  another thread) so mid-body stalls cancel instantly

WebFetch threads the signal through the redirect loop, the open, the
body read, and the Cloudflare-UA retry; WebSearch wraps the Tavily
request. Both raise AbortError, which dispatch already renders as the
user-cancel message.

Closes #276, closes #170

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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