Skip to content

fix: abort in-flight AI request when idle timeout fires (plan 005)#73

Merged
DavertMik merged 5 commits into
mainfrom
fix/abort-ai-request-on-timeout
Jul 11, 2026
Merged

fix: abort in-flight AI request when idle timeout fires (plan 005)#73
DavertMik merged 5 commits into
mainfrom
fix/abort-ai-request-on-timeout

Conversation

@DavertMik

Copy link
Copy Markdown
Contributor

Implements plan 005 (plans/005-abort-ai-request-on-idle-timeout.md) — a P2 correctness/cost fix.

Problem

The provider races every LLM call against rejectAfterIdle(timeout) (default 30s). When the timer wins, it rejects with 'AI request timeout' and the retry condition retries — but nothing aborts the original request. The losing generateText/generateObject keeps running to completion while a retry starts, so a legitimately slow call (reasoning models, large HTML contexts) produces duplicate concurrent LLM calls — extra tokens/cost, and sometimes a spurious failure even though the first call would have succeeded. The only abort wired in was the process-global executionController signal; the per-call idle timeout cancelled nothing.

Fix

At both race sites (generateWithTools, generateObject):

  • Create a per-attempt AbortController inside the withRetry callback.
  • Combine it with the global signal: AbortSignal.any([controller.signal, executionController.getAbortSignal()].filter(Boolean)) (the global signal can be undefined, hence the filter).
  • Pass abortSignal: combinedSignal after the ...config spread so it wins.
  • rejectAfterIdle now calls controller.abort() before rejecting.

Verification

  • New tests in tests/unit/provider.test.ts: (1) idle timeout → the in-flight SDK abortSignal becomes aborted (the regression for the duplicate-call bug); (2) the global execution abort still propagates through the combined signal.
  • bun test tests/unit → 728 pass, 0 fail; bun run lint clean.
  • bun test tests/integration/planner.test.ts (aimock, exercises the provider path) → 10 pass, 0 fail.

Notes

  • The stale config.abortSignal set upstream is now overridden by combinedSignal — harmless but dead.
  • Base branch note: plans 007 and 017 also touch provider.ts; I'm stacking those PRs on top of this one to avoid conflicts.
  • Deferred (per plan): making the 30s idle timeout configurable per agent.

🤖 Generated with Claude Code

The provider races each LLM call against rejectAfterIdle(timeout). When the
timer won it only rejected (and the retry condition retries on that message),
but nothing aborted the original request — so a legitimately slow generateText/
generateObject kept running while a retry started, producing duplicate
concurrent LLM calls (extra tokens/cost, spurious failures).

Create a per-attempt AbortController inside the withRetry callback, combine it
with the global executionController signal via AbortSignal.any, pass the combined
signal to the SDK call (after the ...config spread so it wins), and call
controller.abort() inside rejectAfterIdle before rejecting. Applied at both the
generateWithTools and generateObject race sites.

Deferred (see plan 005): making the 30s idle timeout configurable per agent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 11, 2026

Copy link
Copy Markdown

Explorbot Self-Regression

Commit 222a565 · run

Scenario Result Attempts Duration
basic (native) PASS 1/3 9m
experience: control OK — failed as expected 1/1 2m
experience: seeded PASS 1/3 1m

Attempt details

  • basic (native) Feat/researcher improvement #1 — PASS: login evidence: PASS (post-login plan=true, post-login research=true); research: PASS (files=5, wellFormed=true, keywords=7/3); scenarios: PASS (tests=6/5, features=4/3); tests passed: PASS (5 passed, 0 failed (reporter: 4 passed, 1 failed))
  • experience: control Feat/researcher improvement #1 — PASS: control: OK — failed as expected (0 passed, 1 failed)
  • experience: seeded Feat/researcher improvement #1 — PASS: seeded: PASS (1 passed, 0 failed)

Session analysis — basic (native):

Session Analysis

The Issues page feature was explored across 5 test scenarios covering creation, search, and filtering flows. All core functionality works correctly. The main observation is that issue titles receive an automated system suffix during creation, which is unexpected from a user perspective but does not block functionality.

Coverage

  • Pages: /issues
  • Features: Create issue, Search issues, Filter by label, Filter by status, Clear filters

What works

  • Issue creationET-1 Create a new issue via New Issue button
  • Issue searchET-2 Search for a specific issue by keyword
  • Label filteringET-3 Filter issues by label using label dropdown
  • Status filteringET-4 Filter issues by status using status combobox
  • Clear filtersET-5 Clear label filter and view all issues

UX issues

  • Issue title suffix — The system appends a random suffix to issue titles during creation (e.g., "Test Issue from UI EfficientTiredPurple276" instead of "Test Issue from UI"). This is unexpected behavior that could confuse users who expect their entered title to appear unchanged. (ET-1)

Execution Issues

  • ET-1 — Required multiple verification attempts due to conflicting logs about issue existence in table
  • ET-2 — Multiple failed selector attempts to enter search term before succeeding with alternate selector

DavertMik and others added 2 commits July 11, 2026 16:37
…ad abortSignal

Per re-review of the idle-timeout abort fix:
- Remove the now-dead `abortSignal: executionController.getAbortSignal()` from the
  generateWithTools/generateObject config builders — both are overridden by the
  combined signal at the race site, so they were misleading.
- Collapse the duplicated `AbortSignal.any([...].filter(Boolean) as AbortSignal[])`
  at both race sites into a `combinedAbortSignal(controller)` helper (also drops
  the cast).
- Rename `rejectAfterIdle` -> `abortAfterIdle` (it now aborts, not just rejects)
  and its sentinel param `signal` -> `cancel` (it is not an AbortSignal).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
DavertMik and others added 2 commits July 12, 2026 01:08
'global' shadowed the Node.js global object and didn't say what the signal was;
it's the execution controller's abort signal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@DavertMik DavertMik merged commit 1e4ea4c into main Jul 11, 2026
3 checks passed
@DavertMik DavertMik deleted the fix/abort-ai-request-on-timeout branch July 11, 2026 23:50
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