Skip to content

feat(ai): add abort signals and timeouts to media generation activities - #1047

Merged
AlemTuzlak merged 2 commits into
mainfrom
981-featai-add-abort-signals-and-timeouts-to-media-generation-activities
Aug 7, 2026
Merged

feat(ai): add abort signals and timeouts to media generation activities#1047
AlemTuzlak merged 2 commits into
mainfrom
981-featai-add-abort-signals-and-timeouts-to-media-generation-activities

Conversation

@tombeckenham

@tombeckenham tombeckenham commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds optional timeout and abortSignal to media activities (generateImage, generateAudio, generateVideo, generateSpeech, generateTranscription, summarize).
  • Core composes caller + timeout signals, races the adapter call so hung providers reject, clears timers on settle, and routes aborts to middleware onAbort (not onError).
  • @tanstack/ai-fal forwards the request-specific signal to fal.subscribe() / fal.queue.submit() — never via global fal.config().

Closes #981

Test plan

  • Core unit tests: timeout aborts + forwards signal; caller signal; first reason wins; timer cleared on success; onAbort once not onError
  • Fal image adapter: fal.subscribe() receives request-specific abortSignal
  • pnpm test:types for @tanstack/ai and @tanstack/ai-fal
  • Related media stream/debug tests pass

Usage

const controller = new AbortController()

await generateImage({
  adapter: falImage('fal-ai/nano-banana-2'),
  prompt: 'A cinematic landscape',
  timeout: 10 * 60 * 1000,
  abortSignal: controller.signal,
})

Summary by CodeRabbit

  • New Features
    • Added optional timeout and cancellation support for audio, image, video, speech, transcription, and summarization activities.
    • Forwarded caller-provided cancellation signals to provider requests on a per-request basis.
    • Video polling and streaming operations now respond to cancellation.
  • Bug Fixes
    • Improved cleanup after completion, failure, cancellation, or timeout.
    • Aborted operations now report cancellation reasons separately from other failures.
  • Documentation
    • Documented timeout and cancellation options for media generation activities.

Media generation activities accept optional `timeout` and `abortSignal`.
Core composes them into a request-specific effective signal, races the
adapter call so hung providers reject, clears timeout resources on settle,
and routes aborts to middleware `onAbort`. Fal adapters forward the signal
to fal.subscribe/queue.submit per request rather than via global fal.config.
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8cfd93e3-c69e-421b-af9c-ef2c12a75f88

📥 Commits

Reviewing files that changed from the base of the PR and between b0f66ce and 3b61c4e.

📒 Files selected for processing (11)
  • packages/ai-fal/src/adapters/audio.ts
  • packages/ai-fal/src/adapters/image.ts
  • packages/ai-fal/src/adapters/speech.ts
  • packages/ai-fal/src/adapters/transcription.ts
  • packages/ai-fal/src/adapters/video.ts
  • packages/ai/src/activities/generateAudio/index.ts
  • packages/ai/src/activities/generateImage/index.ts
  • packages/ai/src/activities/generateSpeech/index.ts
  • packages/ai/src/activities/generateTranscription/index.ts
  • packages/ai/src/activities/generateVideo/index.ts
  • packages/ai/tests/activity-abort.test.ts
🚧 Files skipped from review as they are similar to previous changes (11)
  • packages/ai-fal/src/adapters/speech.ts
  • packages/ai-fal/src/adapters/video.ts
  • packages/ai-fal/src/adapters/transcription.ts
  • packages/ai-fal/src/adapters/image.ts
  • packages/ai/src/activities/generateImage/index.ts
  • packages/ai/src/activities/generateTranscription/index.ts
  • packages/ai/tests/activity-abort.test.ts
  • packages/ai/src/activities/generateAudio/index.ts
  • packages/ai-fal/src/adapters/audio.ts
  • packages/ai/src/activities/generateSpeech/index.ts
  • packages/ai/src/activities/generateVideo/index.ts

📝 Walkthrough

Walkthrough

Media activities now support caller abort signals and timeouts. Core composes and manages cancellation, routes aborts through middleware, and forwards signals to adapters. FAL adapters pass signals to request-level provider calls without global configuration.

Changes

Media Activity Cancellation

Layer / File(s) Summary
Shared abort controls
packages/ai/src/utilities/activity-abort.ts, packages/ai/src/types.ts
Shared utilities compose signals, validate timeouts, race promises against aborts, clean up timers, and classify abort errors. Media option types now expose abortSignal.
Activity execution and middleware
packages/ai/src/activities/generateAudio/index.ts, packages/ai/src/activities/generateImage/index.ts, packages/ai/src/activities/generateSpeech/index.ts, packages/ai/src/activities/generateTranscription/index.ts, packages/ai/src/activities/summarize/index.ts
These activities accept timeout and abortSignal, forward effective signals, clean up abort controls, and route cancellations to runGenerationAbort.
Video submission and streaming cancellation
packages/ai/src/activities/generateVideo/index.ts
Video submission, streaming, and polling observe composed cancellation signals. Completion, failure, and generator cleanup clear abort controls.
Cancellation behavior tests
packages/ai/tests/activity-abort.test.ts
Tests cover timeout cancellation, caller cancellation, reason precedence, cleanup, signal forwarding, and middleware behavior.
FAL request signal forwarding
packages/ai-fal/src/adapters/*.ts, packages/ai-fal/tests/image-adapter.test.ts, .changeset/activity-abort-timeout.md
FAL adapters forward request-specific signals to provider calls. Image adapter tests verify signal isolation and timeout signal state. The changeset documents the behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant MediaActivity
  participant MediaAdapter
  participant ProviderSDK
  participant GenerationMiddleware
  Caller->>MediaActivity: invoke with timeout or abortSignal
  MediaActivity->>MediaAdapter: call with effective abortSignal
  MediaAdapter->>ProviderSDK: submit request with abortSignal
  ProviderSDK-->>MediaAdapter: result or cancellation
  MediaActivity->>GenerationMiddleware: runGenerationAbort on cancellation
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the addition of abort signals and timeouts to media generation activities.
Description check ✅ Passed The description explains the changes, motivation, testing, usage, and linked issue with sufficient detail.
Linked Issues check ✅ Passed The implementation addresses issue #981 by adding composed cancellation, timeouts, cleanup, middleware routing, and provider signal forwarding.
Out of Scope Changes check ✅ Passed The changeset, adapter updates, core utilities, activity changes, and tests all support the linked issue objectives.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 981-featai-add-abort-signals-and-timeouts-to-media-generation-activities

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

21 package(s) bumped directly, 31 bumped as dependents.

🟥 Major bumps

Package Version Reason
@tanstack/ai-angular 0.3.1 → 1.0.0 Changeset
@tanstack/ai-byteplus 0.0.0 → 1.0.0 Changeset
@tanstack/ai-durable-stream 0.0.0 → 1.0.0 Changeset
@tanstack/ai-fal 0.9.12 → 1.0.0 Changeset
@tanstack/ai-memory 0.0.0 → 1.0.0 Changeset
@tanstack/ai-openai 0.17.1 → 1.0.0 Changeset
@tanstack/ai-openrouter 0.15.10 → 1.0.0 Changeset
@tanstack/ai-persistence 0.0.0 → 1.0.0 Changeset
@tanstack/ai-preact 0.11.1 → 1.0.0 Changeset
@tanstack/ai-react 0.18.1 → 1.0.0 Changeset
@tanstack/ai-sandbox 0.2.4 → 1.0.0 Changeset
@tanstack/ai-solid 0.15.1 → 1.0.0 Changeset
@tanstack/ai-svelte 0.15.1 → 1.0.0 Changeset
@tanstack/ai-vue 0.15.1 → 1.0.0 Changeset
@tanstack/openai-base 0.9.9 → 1.0.0 Changeset
@tanstack/ai-acp 0.2.3 → 1.0.0 Dependent
@tanstack/ai-anthropic 0.16.3 → 1.0.0 Dependent
@tanstack/ai-bedrock 0.1.4 → 1.0.0 Dependent
@tanstack/ai-claude-code 0.2.3 → 1.0.0 Dependent
@tanstack/ai-code-mode 0.3.8 → 1.0.0 Dependent
@tanstack/ai-code-mode-skills 0.3.11 → 1.0.0 Dependent
@tanstack/ai-codex 0.2.3 → 1.0.0 Dependent
@tanstack/ai-elevenlabs 0.2.34 → 1.0.0 Dependent
@tanstack/ai-gemini 0.20.1 → 1.0.0 Dependent
@tanstack/ai-grok 0.14.9 → 1.0.0 Dependent
@tanstack/ai-grok-build 0.2.3 → 1.0.0 Dependent
@tanstack/ai-groq 0.5.3 → 1.0.0 Dependent
@tanstack/ai-isolate-node 0.1.47 → 1.0.0 Dependent
@tanstack/ai-isolate-quickjs 0.1.47 → 1.0.0 Dependent
@tanstack/ai-mistral 0.2.3 → 1.0.0 Dependent
@tanstack/ai-ollama 0.8.16 → 1.0.0 Dependent
@tanstack/ai-opencode 0.2.3 → 1.0.0 Dependent
@tanstack/ai-react-ui 0.8.15 → 1.0.0 Dependent
@tanstack/ai-sandbox-cloudflare 0.2.4 → 1.0.0 Dependent
@tanstack/ai-sandbox-daytona 0.2.0 → 1.0.0 Dependent
@tanstack/ai-sandbox-docker 0.2.0 → 1.0.0 Dependent
@tanstack/ai-sandbox-local-process 0.2.0 → 1.0.0 Dependent
@tanstack/ai-sandbox-sprites 0.2.1 → 1.0.0 Dependent
@tanstack/ai-sandbox-vercel 0.2.0 → 1.0.0 Dependent
@tanstack/ai-solid-ui 0.7.14 → 1.0.0 Dependent

🟨 Minor bumps

Package Version Reason
@tanstack/ai 0.42.0 → 0.43.0 Changeset
@tanstack/ai-client 0.22.1 → 0.23.0 Changeset
@tanstack/ai-devtools-core 0.4.24 → 0.5.0 Changeset
@tanstack/ai-event-client 0.6.8 → 0.7.0 Changeset
@tanstack/ai-utils 0.3.1 → 0.4.0 Changeset

🟩 Patch bumps

Package Version Reason
@tanstack/ai-mcp 0.2.5 → 0.2.6 Changeset
@tanstack/ai-isolate-cloudflare 0.2.38 → 0.2.39 Dependent
@tanstack/ai-vue-ui 0.2.34 → 0.2.35 Dependent
@tanstack/preact-ai-devtools 0.1.67 → 0.1.68 Dependent
@tanstack/react-ai-devtools 0.2.67 → 0.2.68 Dependent
@tanstack/solid-ai-devtools 0.2.67 → 0.2.68 Dependent
ag-ui 0.0.2 → 0.0.3 Dependent

@nx-cloud

nx-cloud Bot commented Aug 4, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 3b61c4e

Command Status Duration Result
nx run-many --targets=build --exclude=examples/... ✅ Succeeded 1m 56s View ↗

☁️ Nx Cloud last updated this comment at 2026-08-04 02:45:49 UTC

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (3)
packages/ai/src/utilities/activity-abort.ts (1)

175-183: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Classify by error shape first, and share ABORT_ERROR_NAMES.

isActivityAbortError returns true whenever the signal is aborted, before it inspects the error. If the timeout fires while an unrelated provider error propagates, the real failure routes to onAbort and never reaches onError. Check the error name first, then fall back to the signal state.

packages/ai/src/activities/error-payload.ts defines its own ABORT_ERROR_NAMES set for toRunErrorPayload. Export one shared constant so the two abort classifiers cannot drift.

♻️ Proposed ordering change
 export function isActivityAbortError(
   error: unknown,
   signal?: AbortSignal,
 ): boolean {
-  if (signal?.aborted) return true
-  if (!error || typeof error !== 'object') return false
-  const name = (error as { name?: unknown }).name
-  return typeof name === 'string' && ABORT_ERROR_NAMES.has(name)
+  if (error && typeof error === 'object') {
+    const name = (error as { name?: unknown }).name
+    if (typeof name === 'string') return ABORT_ERROR_NAMES.has(name)
+  }
+  return signal?.aborted === true
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai/src/utilities/activity-abort.ts` around lines 175 - 183, Update
isActivityAbortError to classify the error name first, returning true only for
recognized abort names, then fall back to signal?.aborted when no abort error
shape is present. Export the existing ABORT_ERROR_NAMES constant from
error-payload.ts and reuse that shared constant in activity-abort.ts, removing
the duplicate definition.
packages/ai/tests/activity-abort.test.ts (1)

127-150: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the timer count to prove the timer was cleared.

The test name claims the timer is cleared, but expect(generateImages).toHaveBeenCalledTimes(1) does not verify that. Nothing in this test would call the adapter a second time, so the assertion passes even if the timeout timer is still pending. Use vi.getTimerCount() to check the pending-timer state directly.

Consider also adding a test that asserts a long-lived caller AbortSignal accumulates no listeners across repeated generateImage calls. That guards the composed-signal cleanup path.

💚 Proposed assertion
     await expect(resultPromise).resolves.toMatchObject({
       id: 'img-1',
     })
 
-    // Advancing past the original timeout must not throw or leave a hanging
-    // timer that would abort a subsequent unrelated operation.
-    await vi.advanceTimersByTimeAsync(5_000)
-    expect(generateImages).toHaveBeenCalledTimes(1)
+    // The activity must clear its timeout timer on success.
+    expect(vi.getTimerCount()).toBe(0)
+
+    // Advancing past the original timeout must not throw.
+    await vi.advanceTimersByTimeAsync(5_000)
+    expect(generateImages).toHaveBeenCalledTimes(1)

As per coding guidelines: "Use Vitest for unit tests".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai/tests/activity-abort.test.ts` around lines 127 - 150, Update the
successful-completion test around generateImage to assert vi.getTimerCount() is
zero after resolution and remains zero after advancing timers, replacing the
ineffective generateImages call-count assertion as the timer-cleanup check. Also
add a Vitest test covering repeated generateImage calls with a long-lived caller
AbortSignal, verifying composed-signal listeners do not accumulate.

Source: Coding guidelines

packages/ai-fal/tests/image-adapter.test.ts (1)

152-194: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Place this test alongside the adapter source.

Move this test file to packages/ai-fal/src/adapters/image.test.ts. The coding guideline requires colocated *.test.ts files.

As per coding guidelines, “Test files should be placed alongside source code as *.test.ts files using Vitest with happy-dom for DOM testing.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-fal/tests/image-adapter.test.ts` around lines 152 - 194, Move the
image adapter test suite containing the request-specific and timeout abortSignal
cases to the adapter source directory as image.test.ts, preserving its Vitest
setup and test behavior unchanged.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/ai-fal/tests/image-adapter.test.ts`:
- Around line 167-175: Strengthen the cancellation test around mockSubscribe by
capture the abort signal passed in options, abort the existing caller controller
with a specific reason, and assert that the captured signal is aborted with that
reason. Keep the existing request-scoped options and fal.config() assertions
unchanged.

In `@packages/ai/src/activities/generateAudio/index.ts`:
- Around line 236-247: Update the adapter option interfaces used by
generateAudio, specifically AudioGenerationOptions and any provider-specific
extensions, to declare the optional abortSignal passed by runGenerateAudio.
Ensure the adapter.generateAudio call preserves and accepts this signal without
type errors, while retaining existing provider option fields.

In `@packages/ai/src/activities/generateImage/index.ts`:
- Around line 290-293: Ensure the middleware-start phase invoking
runGenerationStart is covered by cleanup for abortControls, so a rejection
clears the timeout before propagating the error. Update the surrounding flow in
the image-generation activity without changing normal successful execution or
later error handling.

In `@packages/ai/src/activities/summarize/index.ts`:
- Around line 102-113: Update runStreamingSummarize to compose and pass the
timeout and abortSignal controls to adapter.summarizeStream, matching
runSummarize. In its non-streaming fallback, preserve the caller’s run identity
and forward the effective abort controls when invoking runSummarize, while
retaining the existing streaming behavior.

In `@packages/ai/src/utilities/activity-abort.ts`:
- Around line 25-40: Update combineAbortSignals to return both the combined
signal and a disposer that removes its abort listeners, while preserving
existing undefined, aborted, and propagation behavior. Store and invoke that
disposer from ActivityAbortControls.clear() alongside timer cleanup, ensuring
repeated or settled activities release listeners.

---

Nitpick comments:
In `@packages/ai-fal/tests/image-adapter.test.ts`:
- Around line 152-194: Move the image adapter test suite containing the
request-specific and timeout abortSignal cases to the adapter source directory
as image.test.ts, preserving its Vitest setup and test behavior unchanged.

In `@packages/ai/src/utilities/activity-abort.ts`:
- Around line 175-183: Update isActivityAbortError to classify the error name
first, returning true only for recognized abort names, then fall back to
signal?.aborted when no abort error shape is present. Export the existing
ABORT_ERROR_NAMES constant from error-payload.ts and reuse that shared constant
in activity-abort.ts, removing the duplicate definition.

In `@packages/ai/tests/activity-abort.test.ts`:
- Around line 127-150: Update the successful-completion test around
generateImage to assert vi.getTimerCount() is zero after resolution and remains
zero after advancing timers, replacing the ineffective generateImages call-count
assertion as the timer-cleanup check. Also add a Vitest test covering repeated
generateImage calls with a long-lived caller AbortSignal, verifying
composed-signal listeners do not accumulate.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 494f418f-6908-44a9-b3e1-6570afa7ff0c

📥 Commits

Reviewing files that changed from the base of the PR and between a7ba44b and b0f66ce.

📒 Files selected for processing (16)
  • .changeset/activity-abort-timeout.md
  • packages/ai-fal/src/adapters/audio.ts
  • packages/ai-fal/src/adapters/image.ts
  • packages/ai-fal/src/adapters/speech.ts
  • packages/ai-fal/src/adapters/transcription.ts
  • packages/ai-fal/src/adapters/video.ts
  • packages/ai-fal/tests/image-adapter.test.ts
  • packages/ai/src/activities/generateAudio/index.ts
  • packages/ai/src/activities/generateImage/index.ts
  • packages/ai/src/activities/generateSpeech/index.ts
  • packages/ai/src/activities/generateTranscription/index.ts
  • packages/ai/src/activities/generateVideo/index.ts
  • packages/ai/src/activities/summarize/index.ts
  • packages/ai/src/types.ts
  • packages/ai/src/utilities/activity-abort.ts
  • packages/ai/tests/activity-abort.test.ts

Comment on lines +167 to +175
expect(mockSubscribe).toHaveBeenCalledTimes(1)
const [, options] = mockSubscribe.mock.calls[0]!
expect(options.abortSignal).toBeInstanceOf(AbortSignal)
// Must be request-scoped options, not a side effect of fal.config().
expect(mockConfig).toHaveBeenCalled()
for (const call of mockConfig.mock.calls) {
expect(call[0]).not.toHaveProperty('abortSignal')
}
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Verify caller cancellation propagation.

The assertion only verifies the signal type. A new unrelated signal would pass this test.

Abort controller and verify that the signal captured from fal.subscribe() becomes aborted with the caller reason.

Proposed test change
     const [, options] = mockSubscribe.mock.calls[0]!
     expect(options.abortSignal).toBeInstanceOf(AbortSignal)
+    controller.abort('caller cancelled')
+    expect(options.abortSignal.aborted).toBe(true)
+    expect(options.abortSignal.reason).toBe('caller cancelled')
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expect(mockSubscribe).toHaveBeenCalledTimes(1)
const [, options] = mockSubscribe.mock.calls[0]!
expect(options.abortSignal).toBeInstanceOf(AbortSignal)
// Must be request-scoped options, not a side effect of fal.config().
expect(mockConfig).toHaveBeenCalled()
for (const call of mockConfig.mock.calls) {
expect(call[0]).not.toHaveProperty('abortSignal')
}
})
expect(mockSubscribe).toHaveBeenCalledTimes(1)
const [, options] = mockSubscribe.mock.calls[0]!
expect(options.abortSignal).toBeInstanceOf(AbortSignal)
controller.abort('caller cancelled')
expect(options.abortSignal.aborted).toBe(true)
expect(options.abortSignal.reason).toBe('caller cancelled')
// Must be request-scoped options, not a side effect of fal.config().
expect(mockConfig).toHaveBeenCalled()
for (const call of mockConfig.mock.calls) {
expect(call[0]).not.toHaveProperty('abortSignal')
}
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-fal/tests/image-adapter.test.ts` around lines 167 - 175,
Strengthen the cancellation test around mockSubscribe by capture the abort
signal passed in options, abort the existing caller controller with a specific
reason, and assert that the captured signal is aborted with that reason. Keep
the existing request-scoped options and fal.config() assertions unchanged.

Comment on lines +236 to +247
const rawResult = await raceWithAbort(
adapter.generateAudio({
...rest,
model,
logger,
...(abortControls.signal
? { abortSignal: abortControls.signal }
: {}),
}),
abortControls.signal,
)
abortControls.clear()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check that media adapter option interfaces declare abortSignal.
set -euo pipefail

fd -e ts . packages/ai/src/activities --exec rg -n -C4 'abortSignal' {} \
  | rg -n 'GenerationOptions|Options|abortSignal'

echo '--- option interface declarations ---'
for name in AudioGenerationOptions TTSOptions TTSGenerationOptions TranscriptionOptions VideoJobCreateOptions ImageGenerationOptions SummarizeOptions; do
  echo "== $name =="
  ast-grep run --pattern "interface $name { \$\$\$ }" --lang typescript packages/ai/src || true
done

Repository: TanStack/ai

Length of output: 2663


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- candidate interface names and declarations ---'
rg -n "interface .*Options|abortSignal\\??:\\s*AbortSignal" packages/ai/src/activities -g '*.ts' | rg "interface|abortSignal" | head -200

echo '--- files mentioning generateAudio / generateSpeech / generateSpeechToText / createVideoJob / generateImage ---'
rg -n "generateAudio|generateSpeech|generateSpeechToText|createVideoJob|generateImage" packages/ai/src/activities -g '*.ts' | head -200

echo '--- audio activity relevant section ---'
cat -n packages/ai/src/activities/generateAudio/index.ts | sed -n '1,130p;220,260p'

echo '--- related activity option exports/imports ---'
for f in packages/ai/src/activities/generateAudio/index.ts packages/ai/src/activities/generateSpeech/index.ts packages/ai/src/activities/generateSpeechToText/index.ts packages/ai/src/activities/createVideoJob/index.ts packages/ai/src/activities/generateImage/index.ts; do
  if [ -f "$f" ]; then
    echo "==$f=="
    cat -n "$f" | sed -n '1,80p;220,260p'
  fi
done

Repository: TanStack/ai

Length of output: 27941


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- package files and scripts ---'
git ls-files 'package.json' 'packages/*/package.json' | sed -n '1,80p'
for f in package.json packages/ai/package.json; do
  if [ -f "$f" ]; then
    echo "== $f =="
    sed -n '1,180p' "$f"
  fi
done

echo '--- local TypeScript/tsc availability ---'
if command -v tsc >/dev/null 2>&1; then tsc --version; else echo 'tsc not on path'; fi
if [ -d packages/ai/node_modules/typescript ]; then tsc --version; else echo 'no packages/ai/node_modules/typescript'; fi

echo '--- exact generated option interfaces ---'
for file in \
  packages/ai/src/activities/generateAudio/index.ts \
  packages/ai/src/activities/generateSpeech/index.ts \
  packages/ai/src/activities/generateTranscription/index.ts \
  packages/ai/src/activities/generateVideo/index.ts \
  packages/ai/src/activities/generateImage/index.ts \
  packages/ai/src/activities/summarize/index.ts; do
  if [ -f "$file" ]; then
    echo "== $file =="
    sed -n '50,120p;120,145p;150,180p' "$file"
  fi
done

echo '--- adapter method signatures ---'
for file in \
  packages/ai/src/activities/generateAudio/adapter.ts \
  packages/ai/src/activities/generateSpeech/adapter.ts \
  packages/ai/src/activities/generateTranscription/adapter.ts \
  packages/ai/src/activities/generateVideo/adapter.ts \
  packages/ai/src/activities/generateImage/adapter.ts; do
  if [ -f "$file" ]; then
    echo "== $file =="
    sed -n '65,95p;170,195p' "$file"
  fi
done

echo '--- TypeScript behavior probe for omitting unused value ---'
node - 2>&1 <<'JS'
const code = `
interface ProvidedOptions {
  prompt: string;
  duration?: number;
  modelOptions?: object;
  model: string;
  logger: unknown;
}
async function generateAudio(options: ProvidedOptions) { return 1; }
const rest = { prompt: 'x', duration: 1, model: 'm', logger: {}, abortSignal: new AbortController().signal };
await generateAudio({
  ...rest,
  ...(rest.abortSignal ? { abortSignal: rest.abortSignal } : {})
});
`
console.log(code)
JS

echo '--- search adapter option declarations with abortSignal ---'
rg -n "abortSignal\\??:\\s*AbortSignal" packages/ai/src/activities/generate{Audio,Speech,Transcription,Video,Image}/adapter.ts || true

Repository: TanStack/ai

Length of output: 41635


Add abortSignal to the adapter option interfaces.

runGenerateAudio now spreads abortSignal into adapter.generateAudio(...), but generateAudio is typed with AudioGenerationOptions<TProviderOptions> while AudioActivityOptions is the value type. If provider adapters define AudioGenerationOptions, make those option interfaces include abortSignal; otherwise the adapter call drops the timeout/caller signal or fails type checking.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai/src/activities/generateAudio/index.ts` around lines 236 - 247,
Update the adapter option interfaces used by generateAudio, specifically
AudioGenerationOptions and any provider-specific extensions, to declare the
optional abortSignal passed by runGenerateAudio. Ensure the
adapter.generateAudio call preserves and accepts this signal without type
errors, while retaining existing provider option fields.

Comment on lines +290 to +293
const abortControls = createActivityAbortControls({
timeout,
abortSignal: callerAbortSignal,
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Clear abort controls when pre-request middleware fails.

If runGenerationStart() rejects at Line 307, execution does not enter the later try/catch. The timeout timer remains active until expiry.

Put the middleware-start phase inside cleanup coverage, or clear abortControls before rethrowing its error. This violates the stated timer-cleanup objective.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai/src/activities/generateImage/index.ts` around lines 290 - 293,
Ensure the middleware-start phase invoking runGenerationStart is covered by
cleanup for abortControls, so a rejection clears the timeout before propagating
the error. Update the surrounding flow in the image-generation activity without
changing normal successful execution or later error handling.

Comment on lines +102 to +113
/**
* Maximum duration of this activity invocation in milliseconds.
* No SDK-wide default — choose a value suitable for the provider and job.
* Composed with {@link abortSignal}; the first abort wins.
*/
timeout?: number
/**
* Caller cancellation signal (request disconnects, job/runtime cancellation).
* Composed with {@link timeout} into an effective signal forwarded to the
* adapter. Request-specific — not stored on global provider client config.
*/
abortSignal?: AbortSignal

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check whether runStreamingSummarize consumes timeout/abortSignal.
set -euo pipefail

f=packages/ai/src/activities/summarize/index.ts
ast-grep outline "$f" --items all

echo '--- runStreamingSummarize body ---'
ast-grep run --pattern 'async function* runStreamingSummarize($$$) { $$$ }' --lang typescript "$f" || \
ast-grep run --pattern 'function runStreamingSummarize($$$) { $$$ }' --lang typescript "$f"

echo '--- abort references in file ---'
rg -n 'abortSignal|abortControls|timeout|raceWithAbort' "$f"

Repository: TanStack/ai

Length of output: 1422


🏁 Script executed:

#!/bin/bash
set -euo pipefail

f=packages/ai/src/activities/summarize/index.ts
echo '--- file line count ---'
wc -l "$f"

echo '--- summarize/export section ---'
sed -n '200,280p' "$f"

echo '--- runSummarize section ---'
sed -n '234,345p' "$f"

echo '--- all exported/defined sums ---'
rg -n 'export function summarize|function runStreamingSummarize|createSummarizeOptions|SummarizeActivityOptions|stream \?|stream:' "$f"

echo '--- broader summing/search ---'
rg -n -C 3 'runStreamingSummarize|runSummarize|timeout|abortSignal' "$f"

Repository: TanStack/ai

Length of output: 8960


🏁 Script executed:

#!/bin/bash
set -euo pipefail

f=packages/ai/src/activities/summarize/index.ts

echo '--- runStreamingSummarize body ---'
sed -n '368,460p' "$f" | cat -n

echo '--- imports and helper definitions ---'
sed -n '1,45p' "$f" | cat -n
rg -n 'createActivityAbortControls|raceWithAbort|isActivityAbortError|abortReasonMessage|runGenerationAbort|runGenerationError|runGenerationStart|runGenerationFinish|runGenerationUsage' packages/ai/src -g '*.ts'

Repository: TanStack/ai

Length of output: 19074


Wire timeout and abort controls into runStreamingSummarize.

summarize() dispatches stream: true to runStreamingSummarize, but only runSummarize creates abort controls. The native streaming path calls adapter.summarizeStream(summarizeOptions) without passing an abort signal, and the fallback runSummarize({ ...options, stream: false, ... }) loses the caller’s run identity and abort controls before calling the non-streaming adapter. Apply the same controls as runSummarize in both streaming branches, or remove these options from SummarizeActivityOptions for streaming.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai/src/activities/summarize/index.ts` around lines 102 - 113, Update
runStreamingSummarize to compose and pass the timeout and abortSignal controls
to adapter.summarizeStream, matching runSummarize. In its non-streaming
fallback, preserve the caller’s run identity and forward the effective abort
controls when invoking runSummarize, while retaining the existing streaming
behavior.

Comment on lines +25 to +40
export function combineAbortSignals(
a: AbortSignal | undefined,
b: AbortSignal | undefined,
): AbortSignal | undefined {
if (!a) return b
if (!b) return a
if (a.aborted) return a
if (b.aborted) return b
const controller = new AbortController()
const onAbort = (source: AbortSignal) => () => {
controller.abort(source.reason)
}
a.addEventListener('abort', onAbort(a), { once: true })
b.addEventListener('abort', onAbort(b), { once: true })
return controller.signal
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Remove the abort listeners when the activity settles.

combineAbortSignals registers an abort listener on the caller signal and never removes it. The listener closes over controller, so the combined AbortController stays reachable from the caller signal.

Callers often own a signal whose lifetime exceeds one activity call, for example a long-lived job or runtime signal reused across invocations. Each invocation then adds one permanent listener to that signal, so retained memory grows with the number of calls. ActivityAbortControls.clear() only clears the timer, so it does not release these listeners.

Return a disposer from combineAbortSignals and call it from clear().

♻️ Proposed fix: dispose composed listeners in clear()
-export function combineAbortSignals(
-  a: AbortSignal | undefined,
-  b: AbortSignal | undefined,
-): AbortSignal | undefined {
-  if (!a) return b
-  if (!b) return a
-  if (a.aborted) return a
-  if (b.aborted) return b
-  const controller = new AbortController()
-  const onAbort = (source: AbortSignal) => () => {
-    controller.abort(source.reason)
-  }
-  a.addEventListener('abort', onAbort(a), { once: true })
-  b.addEventListener('abort', onAbort(b), { once: true })
-  return controller.signal
-}
+export function combineAbortSignals(
+  a: AbortSignal | undefined,
+  b: AbortSignal | undefined,
+): { signal: AbortSignal | undefined; dispose: () => void } {
+  const noop = () => undefined
+  if (!a) return { signal: b, dispose: noop }
+  if (!b) return { signal: a, dispose: noop }
+  if (a.aborted) return { signal: a, dispose: noop }
+  if (b.aborted) return { signal: b, dispose: noop }
+  const controller = new AbortController()
+  const onA = () => controller.abort(a.reason)
+  const onB = () => controller.abort(b.reason)
+  a.addEventListener('abort', onA, { once: true })
+  b.addEventListener('abort', onB, { once: true })
+  return {
+    signal: controller.signal,
+    dispose: () => {
+      a.removeEventListener('abort', onA)
+      b.removeEventListener('abort', onB)
+    },
+  }
+}

Then wire it into the controls:

-  const signal = combineAbortSignals(options.abortSignal, timeoutSignal)
+  const composed = combineAbortSignals(options.abortSignal, timeoutSignal)
 
   return {
-    signal,
+    signal: composed.signal,
     clear: () => {
       if (timeoutId !== undefined) {
         clearTimeout(timeoutId)
         timeoutId = undefined
       }
+      composed.dispose()
     },
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai/src/utilities/activity-abort.ts` around lines 25 - 40, Update
combineAbortSignals to return both the combined signal and a disposer that
removes its abort listeners, while preserving existing undefined, aborted, and
propagation behavior. Store and invoke that disposer from
ActivityAbortControls.clear() alongside timer cleanup, ensuring repeated or
settled activities release listeners.

@pkg-pr-new

pkg-pr-new Bot commented Aug 4, 2026

Copy link
Copy Markdown

Open in StackBlitz

@tanstack/ai

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai@1047

@tanstack/ai-acp

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-acp@1047

@tanstack/ai-angular

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-angular@1047

@tanstack/ai-anthropic

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-anthropic@1047

@tanstack/ai-bedrock

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-bedrock@1047

@tanstack/ai-byteplus

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-byteplus@1047

@tanstack/ai-claude-code

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-claude-code@1047

@tanstack/ai-client

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-client@1047

@tanstack/ai-code-mode

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-code-mode@1047

@tanstack/ai-code-mode-skills

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-code-mode-skills@1047

@tanstack/ai-codex

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-codex@1047

@tanstack/ai-devtools-core

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-devtools-core@1047

@tanstack/ai-durable-stream

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-durable-stream@1047

@tanstack/ai-elevenlabs

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-elevenlabs@1047

@tanstack/ai-event-client

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-event-client@1047

@tanstack/ai-fal

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-fal@1047

@tanstack/ai-gemini

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-gemini@1047

@tanstack/ai-grok

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-grok@1047

@tanstack/ai-grok-build

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-grok-build@1047

@tanstack/ai-groq

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-groq@1047

@tanstack/ai-isolate-cloudflare

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-cloudflare@1047

@tanstack/ai-isolate-node

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-node@1047

@tanstack/ai-isolate-quickjs

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-quickjs@1047

@tanstack/ai-mcp

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-mcp@1047

@tanstack/ai-memory

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-memory@1047

@tanstack/ai-mistral

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-mistral@1047

@tanstack/ai-ollama

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-ollama@1047

@tanstack/ai-openai

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-openai@1047

@tanstack/ai-opencode

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-opencode@1047

@tanstack/ai-openrouter

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-openrouter@1047

@tanstack/ai-persistence

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-persistence@1047

@tanstack/ai-preact

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-preact@1047

@tanstack/ai-react

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-react@1047

@tanstack/ai-react-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-react-ui@1047

@tanstack/ai-sandbox

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox@1047

@tanstack/ai-sandbox-cloudflare

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-cloudflare@1047

@tanstack/ai-sandbox-daytona

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-daytona@1047

@tanstack/ai-sandbox-docker

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-docker@1047

@tanstack/ai-sandbox-local-process

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-local-process@1047

@tanstack/ai-sandbox-sprites

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-sprites@1047

@tanstack/ai-sandbox-vercel

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-vercel@1047

@tanstack/ai-solid

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-solid@1047

@tanstack/ai-solid-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-solid-ui@1047

@tanstack/ai-svelte

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-svelte@1047

@tanstack/ai-utils

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-utils@1047

@tanstack/ai-vue

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-vue@1047

@tanstack/ai-vue-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-vue-ui@1047

@tanstack/openai-base

npm i https://pkg.pr.new/TanStack/ai/@tanstack/openai-base@1047

@tanstack/preact-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/preact-ai-devtools@1047

@tanstack/react-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/react-ai-devtools@1047

@tanstack/solid-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/solid-ai-devtools@1047

commit: 3b61c4e

@tombeckenham
tombeckenham requested review from a team and AlemTuzlak August 4, 2026 09:17
@AlemTuzlak
AlemTuzlak merged commit 59aa8b5 into main Aug 7, 2026
10 checks passed
@AlemTuzlak
AlemTuzlak deleted the 981-featai-add-abort-signals-and-timeouts-to-media-generation-activities branch August 7, 2026 09:06
@github-actions github-actions Bot mentioned this pull request Aug 7, 2026
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.

feat(ai): add abort signals and timeouts to media generation activities

2 participants