fix: abort in-flight AI request when idle timeout fires (plan 005)#73
Merged
Conversation
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>
Explorbot Self-RegressionCommit
Attempt details
Session analysis — basic (native): Session AnalysisThe 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
What works
UX issues
Execution Issues
|
…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>
'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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 losinggenerateText/generateObjectkeeps 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-globalexecutionControllersignal; the per-call idle timeout cancelled nothing.Fix
At both race sites (
generateWithTools,generateObject):AbortControllerinside thewithRetrycallback.AbortSignal.any([controller.signal, executionController.getAbortSignal()].filter(Boolean))(the global signal can beundefined, hence the filter).abortSignal: combinedSignalafter the...configspread so it wins.rejectAfterIdlenow callscontroller.abort()before rejecting.Verification
tests/unit/provider.test.ts: (1) idle timeout → the in-flight SDKabortSignalbecomesaborted(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 lintclean.bun test tests/integration/planner.test.ts(aimock, exercises the provider path) → 10 pass, 0 fail.Notes
config.abortSignalset upstream is now overridden bycombinedSignal— harmless but dead.provider.ts; I'm stacking those PRs on top of this one to avoid conflicts.🤖 Generated with Claude Code