Skip to content

feat(sdk): AI SDK custom useChat transport & chat.task harness#3173

Draft
ericallam wants to merge 119 commits intomainfrom
feature/tri-7532-ai-sdk-chat-transport-and-chat-task-system
Draft

feat(sdk): AI SDK custom useChat transport & chat.task harness#3173
ericallam wants to merge 119 commits intomainfrom
feature/tri-7532-ai-sdk-chat-transport-and-chat-task-system

Conversation

@ericallam
Copy link
Copy Markdown
Member

No description provided.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Mar 4, 2026

🦋 Changeset detected

Latest commit: c295732

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 30 packages
Name Type
@trigger.dev/sdk Minor
@trigger.dev/core Minor
@trigger.dev/build Minor
trigger.dev Minor
@trigger.dev/python Minor
@internal/sdk-compat-tests Patch
references-ai-chat Patch
d3-chat Patch
references-d3-openai-agents Patch
references-nextjs-realtime Patch
references-realtime-hooks-test Patch
references-realtime-streams Patch
references-telemetry Patch
@trigger.dev/redis-worker Minor
@trigger.dev/schema-to-json Minor
@internal/cache Patch
@internal/clickhouse Patch
@internal/llm-model-catalog Patch
@internal/redis Patch
@internal/replication Patch
@internal/run-engine Patch
@internal/schedule-engine Patch
@internal/testcontainers Patch
@internal/tracing Patch
@internal/tsql Patch
@internal/zod-worker Patch
@trigger.dev/react-hooks Minor
@trigger.dev/rsc Minor
@trigger.dev/database Minor
@trigger.dev/otlp-importer Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 4, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a browser-safe chat transport and factory (TriggerChatTransport, createChatTransport) and a React hook (useTriggerChatTransport) under @trigger.dev/sdk/chat. Extends the backend AI SDK (@trigger.dev/sdk/ai) with chat primitives (chatTask, pipeChat, createChatAccessToken, CHAT_STREAM_KEY), many chat-related types, and runtime helpers. Implements per-item oversized NDJSON handling (OversizedItemMarker, extractIndexAndTask) and removes BatchItemTooLargeError/related size checks. Adds InputStreamManager methods (setLastSeqNum, shiftBuffer, disconnectStream) and introduces StreamWriteResult and new realtime options (spanName, collapsed). Updates package exports, docs, tests, and package-installation guidance.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~150 minutes

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is entirely missing. The author provided no description content, violating the template requirement for testing details, changelog, and confirmation of following contributing guidelines. Add a detailed PR description including testing steps, a changelog summary, and confirmation that contributing guidelines were followed per the provided template.
Docstring Coverage ⚠️ Warning Docstring coverage is 68.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly summarizes the main change: introducing AI SDK custom useChat transport and chat.task harness, which aligns with the extensive additions across chat transport, backend task handling, and React integration.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/tri-7532-ai-sdk-chat-transport-and-chat-task-system

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 and usage tips.

@ericallam ericallam changed the title feature/tri-7532-ai-sdk-chat-transport-and-chat-task-system feat(sdk): AI SDK custom useChat transport & chat.task harness Mar 4, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
packages/trigger-sdk/src/v3/chat.test.ts (1)

22-23: Consider resetting messageIdCounter in beforeEach.

The messageIdCounter is a module-level variable that accumulates across all tests. While this doesn't cause functional issues (IDs just need to be unique within each test), resetting it in beforeEach would make tests more deterministic and easier to debug.

♻️ Suggested fix
 beforeEach(() => {
   originalFetch = global.fetch;
+  messageIdCounter = 0;
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/trigger-sdk/src/v3/chat.test.ts` around lines 22 - 23, The
module-level variable messageIdCounter in chat.test.ts accumulates across tests
making IDs non-deterministic; add a beforeEach hook in the test file that resets
messageIdCounter = 0 so each test starts with a fresh counter (locate the
variable by name messageIdCounter and place the reset inside the existing or new
beforeEach block in the test suite).
packages/trigger-sdk/src/v3/chat-react.ts (1)

76-84: Good memoization pattern, but options are captured only on first render.

The useRef pattern correctly preserves the transport instance across re-renders. Note that if baseURL, headers, streamKey, or other options change after the initial render, the transport won't pick up those changes. This is likely intentional (and documented via "created once"), but worth keeping in mind if callers expect reactive updates to options other than accessToken.

The accessToken function pattern works correctly for dynamic tokens since it's called per-request.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/trigger-sdk/src/v3/chat-react.ts` around lines 76 - 84, The hook
useTriggerChatTransport currently creates a single TriggerChatTransport with the
options captured only on first render, so changes to baseURL, headers,
streamKey, etc. after mount are ignored; modify the hook to either (A) expose or
call an update method on the transport (e.g., ref.current.updateOptions or
ref.current.setConfig) and update relevant fields when
options.baseURL/headers/streamKey change, or (B) recreate the transport when
those non-dynamic options change by tracking them in a useEffect and replacing
ref.current = new TriggerChatTransport(options) (preserving any needed cleanup),
while keeping the accessToken-as-function behavior unchanged so per-request
tokens remain dynamic. Ensure you reference useTriggerChatTransport and
TriggerChatTransport and only treat accessToken as dynamically invoked per
request.
packages/trigger-sdk/src/v3/chat.ts (1)

198-212: Consider logging the error for debugging.

The silent catch when sending to an existing run's input stream makes debugging harder when things go wrong unexpectedly. While the fallthrough to trigger a new run is correct, a debug-level log could help diagnose issues.

🔧 Optional: Add debug logging
       } catch {
-        // If sending fails (run died, etc.), fall through to trigger a new run.
+        // If sending fails (run died, etc.), fall through to trigger a new run.
+        // Note: Consider adding debug logging here if debugging becomes difficult
         this.sessions.delete(chatId);
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/trigger-sdk/src/v3/chat.ts` around lines 198 - 212, The catch block
swallowing errors after ApiClient.sendInputStream makes failures hard to trace;
update the catch to log the caught error (including context like session.runId
and chatId) before deleting the session so you still fall through to start a new
run — use the class logger (e.g., this.logger.debug) if available or
console.debug as a fallback, referencing ApiClient.sendInputStream,
subscribeToStream, and this.sessions.delete to locate the code to change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.scratch/plan-graceful-oversized-batch-items.md:
- Around line 216-238: The fenced code block that starts with "NDJSON bytes
arrive" is missing a language identifier which triggers markdownlint MD040;
update the opening fence to include a language (e.g., change ``` to ```text) so
the block is explicitly marked as plain text (ensure the closing fence remains
```), preserving the existing block contents and indentation.

In `@packages/trigger-sdk/src/v3/ai.ts`:
- Around line 560-563: The handler currently captures multiple messages into
pendingMessages via messagesInput.on (msgSub) but later only consumes the first
entry, discarding the rest; change the logic that extracts from pendingMessages
(where it currently uses a single element) to drain and preserve the entire
backlog — e.g., move all items from pendingMessages into the processing queue
(or append them to the existing messages array) before proceeding, then clean up
the subscription (msgSub) as before so no buffered messages are lost; update
references to pendingMessages, msgSub, and messagesInput.on to reflect this
full-drain behavior.
- Around line 268-269: The global _chatPipeCount is race-prone; make the pipe
counter scoped to each chat run/turn instead. Remove the module-global
_chatPipeCount and initialize a run-scoped counter (for example add a numeric
property like run.__chatPipeCount or turn.chatPipeCount on the ChatRun/ChatTurn
object when a run/turn is created or at the start of the function that currently
uses _chatPipeCount), then replace every reference to _chatPipeCount (including
uses around lines 360-361 and 548-583) with the run/turn-scoped property and
update increment/decrement logic to use that property so concurrent runs don’t
share state. Ensure the counter is initialized to 0 at run start and cleaned up
or left on the run object when finished.

---

Nitpick comments:
In `@packages/trigger-sdk/src/v3/chat-react.ts`:
- Around line 76-84: The hook useTriggerChatTransport currently creates a single
TriggerChatTransport with the options captured only on first render, so changes
to baseURL, headers, streamKey, etc. after mount are ignored; modify the hook to
either (A) expose or call an update method on the transport (e.g.,
ref.current.updateOptions or ref.current.setConfig) and update relevant fields
when options.baseURL/headers/streamKey change, or (B) recreate the transport
when those non-dynamic options change by tracking them in a useEffect and
replacing ref.current = new TriggerChatTransport(options) (preserving any needed
cleanup), while keeping the accessToken-as-function behavior unchanged so
per-request tokens remain dynamic. Ensure you reference useTriggerChatTransport
and TriggerChatTransport and only treat accessToken as dynamically invoked per
request.

In `@packages/trigger-sdk/src/v3/chat.test.ts`:
- Around line 22-23: The module-level variable messageIdCounter in chat.test.ts
accumulates across tests making IDs non-deterministic; add a beforeEach hook in
the test file that resets messageIdCounter = 0 so each test starts with a fresh
counter (locate the variable by name messageIdCounter and place the reset inside
the existing or new beforeEach block in the test suite).

In `@packages/trigger-sdk/src/v3/chat.ts`:
- Around line 198-212: The catch block swallowing errors after
ApiClient.sendInputStream makes failures hard to trace; update the catch to log
the caught error (including context like session.runId and chatId) before
deleting the session so you still fall through to start a new run — use the
class logger (e.g., this.logger.debug) if available or console.debug as a
fallback, referencing ApiClient.sendInputStream, subscribeToStream, and
this.sessions.delete to locate the code to change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: ffd316a7-f5af-4416-88b4-2e7c865da47c

📥 Commits

Reviewing files that changed from the base of the PR and between c013322 and d7817e0.

⛔ Files ignored due to path filters (13)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • references/ai-chat/next-env.d.ts is excluded by !references/**
  • references/ai-chat/next.config.ts is excluded by !references/**
  • references/ai-chat/package.json is excluded by !references/**
  • references/ai-chat/postcss.config.mjs is excluded by !references/**
  • references/ai-chat/src/app/actions.ts is excluded by !references/**
  • references/ai-chat/src/app/globals.css is excluded by !references/**
  • references/ai-chat/src/app/layout.tsx is excluded by !references/**
  • references/ai-chat/src/app/page.tsx is excluded by !references/**
  • references/ai-chat/src/components/chat.tsx is excluded by !references/**
  • references/ai-chat/src/trigger/chat.ts is excluded by !references/**
  • references/ai-chat/trigger.config.ts is excluded by !references/**
  • references/ai-chat/tsconfig.json is excluded by !references/**
📒 Files selected for processing (18)
  • .changeset/ai-sdk-chat-transport.md
  • .claude/rules/package-installation.md
  • .scratch/plan-graceful-oversized-batch-items.md
  • CLAUDE.md
  • docs/docs.json
  • docs/guides/ai-chat.mdx
  • packages/core/src/v3/inputStreams/index.ts
  • packages/core/src/v3/inputStreams/manager.ts
  • packages/core/src/v3/inputStreams/noopManager.ts
  • packages/core/src/v3/inputStreams/types.ts
  • packages/core/src/v3/realtimeStreams/types.ts
  • packages/trigger-sdk/package.json
  • packages/trigger-sdk/src/v3/ai.ts
  • packages/trigger-sdk/src/v3/chat-constants.ts
  • packages/trigger-sdk/src/v3/chat-react.ts
  • packages/trigger-sdk/src/v3/chat.test.ts
  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/streams.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (27)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (6, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (1, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (6, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (5, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (3, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (1, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (7, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (8, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (7, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (5, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (2, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (2, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (3, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (4, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (4, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (8, 8)
  • GitHub Check: sdk-compat / Bun Runtime
  • GitHub Check: units / packages / 🧪 Unit Tests: Packages (1, 1)
  • GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - pnpm)
  • GitHub Check: sdk-compat / Cloudflare Workers
  • GitHub Check: sdk-compat / Node.js 20.20 (ubuntu-latest)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - pnpm)
  • GitHub Check: typecheck / typecheck
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - npm)
  • GitHub Check: sdk-compat / Deno Runtime
  • GitHub Check: sdk-compat / Node.js 22.12 (ubuntu-latest)
  • GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - npm)
🧰 Additional context used
📓 Path-based instructions (14)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Use types over interfaces for TypeScript
Avoid using enums; prefer string unions or const objects instead

**/*.{ts,tsx}: In TypeScript SDK usage, always import from @trigger.dev/sdk, never from @trigger.dev/sdk/v3 or use deprecated client.defineJob
Import from @trigger.dev/core subpaths only, never from the root
Use the Run Engine 2.0 (@internal/run-engine) and redis-worker for all new work, not legacy V1 MarQS queue or deprecated V1 functions

Files:

  • packages/core/src/v3/inputStreams/manager.ts
  • packages/core/src/v3/inputStreams/index.ts
  • packages/core/src/v3/inputStreams/noopManager.ts
  • packages/trigger-sdk/src/v3/chat.ts
  • packages/core/src/v3/inputStreams/types.ts
  • packages/trigger-sdk/src/v3/chat-constants.ts
  • packages/trigger-sdk/src/v3/streams.ts
  • packages/trigger-sdk/src/v3/chat.test.ts
  • packages/trigger-sdk/src/v3/chat-react.ts
  • packages/core/src/v3/realtimeStreams/types.ts
  • packages/trigger-sdk/src/v3/ai.ts
{packages/core,apps/webapp}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use zod for validation in packages/core and apps/webapp

Files:

  • packages/core/src/v3/inputStreams/manager.ts
  • packages/core/src/v3/inputStreams/index.ts
  • packages/core/src/v3/inputStreams/noopManager.ts
  • packages/core/src/v3/inputStreams/types.ts
  • packages/core/src/v3/realtimeStreams/types.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use function declarations instead of default exports

Files:

  • packages/core/src/v3/inputStreams/manager.ts
  • packages/core/src/v3/inputStreams/index.ts
  • packages/core/src/v3/inputStreams/noopManager.ts
  • packages/trigger-sdk/src/v3/chat.ts
  • packages/core/src/v3/inputStreams/types.ts
  • packages/trigger-sdk/src/v3/chat-constants.ts
  • packages/trigger-sdk/src/v3/streams.ts
  • packages/trigger-sdk/src/v3/chat.test.ts
  • packages/trigger-sdk/src/v3/chat-react.ts
  • packages/core/src/v3/realtimeStreams/types.ts
  • packages/trigger-sdk/src/v3/ai.ts
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/otel-metrics.mdc)

**/*.ts: When creating or editing OTEL metrics (counters, histograms, gauges), ensure metric attributes have low cardinality by using only enums, booleans, bounded error codes, or bounded shard IDs
Do not use high-cardinality attributes in OTEL metrics such as UUIDs/IDs (envId, userId, runId, projectId, organizationId), unbounded integers (itemCount, batchSize, retryCount), timestamps (createdAt, startTime), or free-form strings (errorMessage, taskName, queueName)
When exporting OTEL metrics via OTLP to Prometheus, be aware that the exporter automatically adds unit suffixes to metric names (e.g., 'my_duration_ms' becomes 'my_duration_ms_milliseconds', 'my_counter' becomes 'my_counter_total'). Account for these transformations when writing Grafana dashboards or Prometheus queries

Files:

  • packages/core/src/v3/inputStreams/manager.ts
  • packages/core/src/v3/inputStreams/index.ts
  • packages/core/src/v3/inputStreams/noopManager.ts
  • packages/trigger-sdk/src/v3/chat.ts
  • packages/core/src/v3/inputStreams/types.ts
  • packages/trigger-sdk/src/v3/chat-constants.ts
  • packages/trigger-sdk/src/v3/streams.ts
  • packages/trigger-sdk/src/v3/chat.test.ts
  • packages/trigger-sdk/src/v3/chat-react.ts
  • packages/core/src/v3/realtimeStreams/types.ts
  • packages/trigger-sdk/src/v3/ai.ts
**/*.{js,ts,jsx,tsx,json,md,yaml,yml}

📄 CodeRabbit inference engine (AGENTS.md)

Format code using Prettier before committing

Files:

  • packages/core/src/v3/inputStreams/manager.ts
  • packages/core/src/v3/inputStreams/index.ts
  • packages/core/src/v3/inputStreams/noopManager.ts
  • docs/docs.json
  • packages/trigger-sdk/src/v3/chat.ts
  • CLAUDE.md
  • packages/core/src/v3/inputStreams/types.ts
  • packages/trigger-sdk/src/v3/chat-constants.ts
  • packages/trigger-sdk/src/v3/streams.ts
  • packages/trigger-sdk/src/v3/chat.test.ts
  • packages/trigger-sdk/src/v3/chat-react.ts
  • packages/trigger-sdk/package.json
  • packages/core/src/v3/realtimeStreams/types.ts
  • packages/trigger-sdk/src/v3/ai.ts
packages/core/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (packages/core/CLAUDE.md)

Never import the root package (@trigger.dev/core). Always use subpath imports such as @trigger.dev/core/v3, @trigger.dev/core/v3/utils, @trigger.dev/core/logger, or @trigger.dev/core/schemas

Files:

  • packages/core/src/v3/inputStreams/manager.ts
  • packages/core/src/v3/inputStreams/index.ts
  • packages/core/src/v3/inputStreams/noopManager.ts
  • packages/core/src/v3/inputStreams/types.ts
  • packages/core/src/v3/realtimeStreams/types.ts
docs/**/*.{md,mdx}

📄 CodeRabbit inference engine (CLAUDE.md)

Docs in docs/ directory should use Mintlify MDX format following conventions in docs/CLAUDE.md

Files:

  • docs/guides/ai-chat.mdx
docs/**/*.mdx

📄 CodeRabbit inference engine (docs/CLAUDE.md)

docs/**/*.mdx: MDX documentation pages must include frontmatter with title (required), description (required), and sidebarTitle (optional) in YAML format
Use Mintlify components for structured content: , , , , , , /, /
Always import from @trigger.dev/sdk in code examples (never from @trigger.dev/sdk/v3)
Code examples must be complete and runnable where possible
Use language tags in code fences: typescript, bash, json

Files:

  • docs/guides/ai-chat.mdx
docs/**/docs.json

📄 CodeRabbit inference engine (docs/CLAUDE.md)

docs/**/docs.json: Main documentation config must be defined in docs.json which includes navigation structure, theme, and metadata
Navigation structure in docs.json should be organized using navigation.dropdowns with groups and pages

Files:

  • docs/docs.json
packages/trigger-sdk/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

In the Trigger.dev SDK (packages/trigger-sdk), prefer isomorphic code like fetch and ReadableStream instead of Node.js-specific code

Files:

  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/chat-constants.ts
  • packages/trigger-sdk/src/v3/streams.ts
  • packages/trigger-sdk/src/v3/chat.test.ts
  • packages/trigger-sdk/src/v3/chat-react.ts
  • packages/trigger-sdk/src/v3/ai.ts
packages/trigger-sdk/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (packages/trigger-sdk/CLAUDE.md)

Always import from @trigger.dev/sdk. Never use @trigger.dev/sdk/v3 (deprecated path alias)

Files:

  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/chat-constants.ts
  • packages/trigger-sdk/src/v3/streams.ts
  • packages/trigger-sdk/src/v3/chat.test.ts
  • packages/trigger-sdk/src/v3/chat-react.ts
  • packages/trigger-sdk/src/v3/ai.ts
**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use vitest for all tests in the Trigger.dev repository

Files:

  • packages/trigger-sdk/src/v3/chat.test.ts
**/*.test.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.test.{ts,tsx,js,jsx}: Test files should live beside the files under test and use descriptive describe and it blocks
Tests should avoid mocks or stubs and use the helpers from @internal/testcontainers when Redis or Postgres are needed
Use vitest for running unit tests

Files:

  • packages/trigger-sdk/src/v3/chat.test.ts
**/*.test.{ts,tsx,js}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.test.{ts,tsx,js}: Use vitest exclusively for testing - never mock anything, use testcontainers instead
Place test files next to source files with .test.ts naming convention (e.g., MyService.ts -> MyService.test.ts)
Test files using Redis or PostgreSQL should use testcontainers helpers (redisTest, postgresTest, containerTest) instead of mocks

Files:

  • packages/trigger-sdk/src/v3/chat.test.ts
🧠 Learnings (51)
📓 Common learnings
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use `.withStreams()` to subscribe to realtime streams from task metadata in addition to run changes
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-27T16:26:37.432Z
Learning: Applies to packages/trigger-sdk/**/*.{ts,tsx} : In the Trigger.dev SDK (packages/trigger-sdk), prefer isomorphic code like fetch and ReadableStream instead of Node.js-specific code
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-27T16:26:37.432Z
Learning: The SDK at packages/trigger-sdk is an isomorphic TypeScript SDK
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use `trigger.dev/sdk/v3` for all imports in Trigger.dev tasks
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/webapp.mdc:0-0
Timestamp: 2025-11-27T16:26:58.661Z
Learning: Applies to apps/webapp/**/*.{ts,tsx} : When importing from `trigger.dev/core` in the webapp, use subpath exports from the package.json instead of importing from the root path
📚 Learning: 2026-03-02T12:43:34.140Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: packages/cli-v3/CLAUDE.md:0-0
Timestamp: 2026-03-02T12:43:34.140Z
Learning: Keep SDK documentation in `rules/` and `.claude/skills/trigger-dev-tasks/` synchronized when features are added or changed

Applied to files:

  • docs/guides/ai-chat.mdx
  • CLAUDE.md
  • .claude/rules/package-installation.md
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use `trigger.dev/sdk/v3` for all imports in Trigger.dev tasks

Applied to files:

  • docs/guides/ai-chat.mdx
  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/chat-constants.ts
  • packages/trigger-sdk/src/v3/chat.test.ts
  • packages/trigger-sdk/src/v3/chat-react.ts
  • packages/trigger-sdk/package.json
  • .changeset/ai-sdk-chat-transport.md
  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2026-03-02T12:43:37.906Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: packages/core/CLAUDE.md:0-0
Timestamp: 2026-03-02T12:43:37.906Z
Learning: Exercise caution with changes to trigger.dev/core as they affect both the customer-facing SDK and server-side webapp - breaking changes can impact deployed user tasks and the platform simultaneously

Applied to files:

  • docs/guides/ai-chat.mdx
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use `.withStreams()` to subscribe to realtime streams from task metadata in addition to run changes

Applied to files:

  • docs/guides/ai-chat.mdx
  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/chat-constants.ts
  • packages/trigger-sdk/src/v3/streams.ts
  • packages/trigger-sdk/src/v3/chat.test.ts
  • packages/trigger-sdk/src/v3/chat-react.ts
  • .changeset/ai-sdk-chat-transport.md
  • packages/core/src/v3/realtimeStreams/types.ts
  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use the `task()` function from `trigger.dev/sdk/v3` to define tasks with id and run properties

Applied to files:

  • docs/guides/ai-chat.mdx
  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/chat-react.ts
  • .changeset/ai-sdk-chat-transport.md
  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use `yourTask.trigger()` to trigger a task from inside another task with specified payload

Applied to files:

  • docs/guides/ai-chat.mdx
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use `schemaTask()` from `trigger.dev/sdk/v3` with Zod schema for payload validation

Applied to files:

  • docs/guides/ai-chat.mdx
  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/chat-react.ts
  • packages/trigger-sdk/package.json
  • .changeset/ai-sdk-chat-transport.md
  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use `tasks.trigger()` with type-only imports to trigger tasks from backend code without importing the task implementation

Applied to files:

  • docs/guides/ai-chat.mdx
  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/chat-react.ts
  • packages/trigger-sdk/package.json
  • .changeset/ai-sdk-chat-transport.md
  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Use `TriggerAuthContext` provider to supply Public Access Token to Trigger.dev React hooks

Applied to files:

  • docs/guides/ai-chat.mdx
  • packages/trigger-sdk/src/v3/chat-react.ts
📚 Learning: 2026-02-25T17:28:20.456Z
Learnt from: isshaddad
Repo: triggerdotdev/trigger.dev PR: 3130
File: docs/v3-openapi.yaml:3134-3135
Timestamp: 2026-02-25T17:28:20.456Z
Learning: In the Trigger.dev codebase, the `publicAccessToken` returned by the SDK's `wait.createToken()` method is not part of the HTTP response body from `POST /api/v1/waitpoints/tokens`. The server returns only `{ id, isCached, url }`. The SDK's `prepareData` hook generates the JWT client-side from the `x-trigger-jwt-claims` response header after the HTTP call completes. The OpenAPI spec correctly documents only the HTTP response body, not SDK transformations.
<!-- [/add_learning]

Applied to files:

  • docs/guides/ai-chat.mdx
📚 Learning: 2025-11-27T16:26:37.432Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-27T16:26:37.432Z
Learning: The SDK at packages/trigger-sdk is an isomorphic TypeScript SDK

Applied to files:

  • docs/guides/ai-chat.mdx
  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/chat-constants.ts
  • packages/trigger-sdk/src/v3/chat.test.ts
  • packages/trigger-sdk/src/v3/chat-react.ts
  • packages/trigger-sdk/package.json
  • .changeset/ai-sdk-chat-transport.md
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use `tasks.batchTrigger()` to trigger multiple runs of a single task with different payloads

Applied to files:

  • docs/guides/ai-chat.mdx
  • packages/trigger-sdk/src/v3/chat.test.ts
  • .scratch/plan-graceful-oversized-batch-items.md
📚 Learning: 2026-03-02T12:43:02.539Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: docs/CLAUDE.md:0-0
Timestamp: 2026-03-02T12:43:02.539Z
Learning: Organize documentation across appropriate directories: `documentation/` for core conceptual docs, `guides/` for how-to guides, `config/` for configuration reference, `deployment/` for deployment guides, `tasks/` for task documentation, `realtime/` for real-time features, `runs/` for run management, and `images/` for assets

Applied to files:

  • docs/docs.json
📚 Learning: 2025-11-27T16:26:37.432Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-27T16:26:37.432Z
Learning: Applies to packages/trigger-sdk/**/*.{ts,tsx} : In the Trigger.dev SDK (packages/trigger-sdk), prefer isomorphic code like fetch and ReadableStream instead of Node.js-specific code

Applied to files:

  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/chat-constants.ts
  • packages/trigger-sdk/src/v3/chat.test.ts
  • packages/trigger-sdk/src/v3/chat-react.ts
  • packages/trigger-sdk/package.json
  • .changeset/ai-sdk-chat-transport.md
📚 Learning: 2026-03-02T12:43:48.124Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: packages/trigger-sdk/CLAUDE.md:0-0
Timestamp: 2026-03-02T12:43:48.124Z
Learning: Applies to packages/trigger-sdk/**/*.{ts,tsx,js,jsx} : Always import from `trigger.dev/sdk`. Never use `trigger.dev/sdk/v3` (deprecated path alias)

Applied to files:

  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/chat-constants.ts
  • packages/trigger-sdk/src/v3/chat-react.ts
  • packages/trigger-sdk/package.json
  • .changeset/ai-sdk-chat-transport.md
  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Export tasks with unique IDs within the project to enable proper task discovery and execution

Applied to files:

  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/chat-react.ts
  • packages/trigger-sdk/package.json
  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2026-03-02T12:42:41.110Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-02T12:42:41.110Z
Learning: Applies to **/*.{ts,tsx} : In TypeScript SDK usage, always import from trigger.dev/sdk, never from trigger.dev/sdk/v3 or use deprecated client.defineJob

Applied to files:

  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/chat-react.ts
  • packages/trigger-sdk/package.json
  • .changeset/ai-sdk-chat-transport.md
📚 Learning: 2026-03-02T12:42:41.110Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-02T12:42:41.110Z
Learning: Applies to packages/**/*.{ts,tsx,js},integrations/**/*.{ts,tsx,js} : When modifying public packages (packages/* or integrations/*), add a changeset via pnpm run changeset:add

Applied to files:

  • CLAUDE.md
  • .claude/rules/package-installation.md
  • packages/trigger-sdk/package.json
📚 Learning: 2026-03-02T12:43:48.124Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: packages/trigger-sdk/CLAUDE.md:0-0
Timestamp: 2026-03-02T12:43:48.124Z
Learning: Do NOT update `.claude/skills/trigger-dev-tasks/` directory files unless explicitly asked - these are maintained in separate dedicated passes

Applied to files:

  • CLAUDE.md
📚 Learning: 2026-03-02T12:43:34.140Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: packages/cli-v3/CLAUDE.md:0-0
Timestamp: 2026-03-02T12:43:34.140Z
Learning: Applies to packages/cli-v3/.claude/skills/trigger-dev-tasks/**/* : Update `.claude/skills/trigger-dev-tasks/` in parallel with `rules/` when SDK features change

Applied to files:

  • CLAUDE.md
  • .claude/rules/package-installation.md
📚 Learning: 2026-03-02T12:42:41.110Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-02T12:42:41.110Z
Learning: Applies to docs/**/*.{md,mdx} : Docs in docs/ directory should use Mintlify MDX format following conventions in docs/CLAUDE.md

Applied to files:

  • CLAUDE.md
  • .claude/rules/package-installation.md
📚 Learning: 2026-03-02T12:43:34.140Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: packages/cli-v3/CLAUDE.md:0-0
Timestamp: 2026-03-02T12:43:34.140Z
Learning: Applies to packages/cli-v3/rules/**/* : Update `rules/` directory with versioned SDK documentation when SDK features change

Applied to files:

  • CLAUDE.md
  • .claude/rules/package-installation.md
📚 Learning: 2026-03-02T12:42:41.110Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-02T12:42:41.110Z
Learning: This is a pnpm 10.23.0 monorepo using Turborepo - run commands from root with pnpm run

Applied to files:

  • CLAUDE.md
  • .claude/rules/package-installation.md
📚 Learning: 2025-11-27T16:26:44.496Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/executing-commands.mdc:0-0
Timestamp: 2025-11-27T16:26:44.496Z
Learning: Execute most monorepo commands using `pnpm run` from the root directory, with `--filter` flag for specific packages (e.g., `pnpm run dev --filter webapp`)

Applied to files:

  • CLAUDE.md
  • .claude/rules/package-installation.md
📚 Learning: 2025-11-27T16:26:44.496Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/executing-commands.mdc:0-0
Timestamp: 2025-11-27T16:26:44.496Z
Learning: For running tests, navigate into the package directory and run `pnpm run test --run` to enable single-file test execution (e.g., `pnpm run test ./src/engine/tests/ttl.test.ts --run`)

Applied to files:

  • CLAUDE.md
  • .claude/rules/package-installation.md
📚 Learning: 2026-01-15T10:48:02.687Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-15T10:48:02.687Z
Learning: Use pnpm as the package manager (version 10.23.0 or later) and Node.js 20.20.0

Applied to files:

  • CLAUDE.md
  • .claude/rules/package-installation.md
📚 Learning: 2025-11-27T16:26:47.602Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/repo.mdc:0-0
Timestamp: 2025-11-27T16:26:47.602Z
Learning: Refer to the monorepo structure documentation at repo.md before making changes or adding new files

Applied to files:

  • CLAUDE.md
  • .claude/rules/package-installation.md
📚 Learning: 2026-03-02T12:43:17.177Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: internal-packages/database/CLAUDE.md:0-0
Timestamp: 2026-03-02T12:43:17.177Z
Learning: Edit Prisma schema at `prisma/schema.prisma` and generate migrations using `pnpm run db:migrate:dev:create --name "descriptive_name"` from the `internal-packages/database` directory

Applied to files:

  • CLAUDE.md
📚 Learning: 2026-03-02T12:43:37.906Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: packages/core/CLAUDE.md:0-0
Timestamp: 2026-03-02T12:43:37.906Z
Learning: Applies to packages/core/**/*.{ts,tsx,js,jsx} : Never import the root package (trigger.dev/core). Always use subpath imports such as trigger.dev/core/v3, trigger.dev/core/v3/utils, trigger.dev/core/logger, or trigger.dev/core/schemas

Applied to files:

  • .claude/rules/package-installation.md
  • packages/trigger-sdk/package.json
📚 Learning: 2025-11-27T16:26:37.432Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-27T16:26:37.432Z
Learning: Applies to **/*.{test,spec}.{ts,tsx} : Use vitest for all tests in the Trigger.dev repository

Applied to files:

  • packages/trigger-sdk/src/v3/chat.test.ts
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use `batch.triggerAndWait()` to batch trigger multiple different tasks and wait for results

Applied to files:

  • packages/trigger-sdk/src/v3/chat.test.ts
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use `batch.triggerByTaskAndWait()` to batch trigger tasks by passing task instances and wait for results

Applied to files:

  • packages/trigger-sdk/src/v3/chat.test.ts
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use `yourTask.batchTrigger()` to trigger multiple runs of a task from inside another task

Applied to files:

  • packages/trigger-sdk/src/v3/chat.test.ts
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use `yourTask.triggerAndWait()` to trigger a task and wait for its result from a parent task

Applied to files:

  • packages/trigger-sdk/src/v3/chat.test.ts
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use `yourTask.batchTriggerAndWait()` to batch trigger tasks and wait for all results from a parent task

Applied to files:

  • packages/trigger-sdk/src/v3/chat.test.ts
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Use `trigger.dev/react-hooks` package for realtime subscriptions in React components

Applied to files:

  • packages/trigger-sdk/src/v3/chat-react.ts
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Use `useRun`, `useRealtimeRun` and other SWR/realtime hooks from `trigger.dev/react-hooks` for data fetching

Applied to files:

  • packages/trigger-sdk/src/v3/chat-react.ts
📚 Learning: 2026-03-03T13:07:33.177Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3166
File: internal-packages/run-engine/src/batch-queue/tests/index.test.ts:711-713
Timestamp: 2026-03-03T13:07:33.177Z
Learning: In `internal-packages/run-engine/src/batch-queue/tests/index.test.ts`, test assertions for rate limiter stubs can use `toBeGreaterThanOrEqual` rather than exact equality (`toBe`) because the consumer loop may call the rate limiter during empty pops in addition to actual item processing, and this over-calling is acceptable in integration tests.

Applied to files:

  • .scratch/plan-graceful-oversized-batch-items.md
📚 Learning: 2026-03-03T13:08:03.862Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3166
File: packages/redis-worker/src/fair-queue/index.ts:1114-1121
Timestamp: 2026-03-03T13:08:03.862Z
Learning: In packages/redis-worker/src/fair-queue/index.ts, it's acceptable for the worker queue depth cap check to allow overshooting by up to batchClaimSize messages per iteration, as the next iteration will recheck and prevent sustained growth beyond the limit.

Applied to files:

  • .scratch/plan-graceful-oversized-batch-items.md
📚 Learning: 2025-11-27T16:26:58.661Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/webapp.mdc:0-0
Timestamp: 2025-11-27T16:26:58.661Z
Learning: Applies to apps/webapp/**/*.{ts,tsx} : When importing from `trigger.dev/core` in the webapp, use subpath exports from the package.json instead of importing from the root path

Applied to files:

  • packages/trigger-sdk/package.json
  • .changeset/ai-sdk-chat-transport.md
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger.config.ts : Configure build process in trigger.config.ts using `build` object with external packages, extensions, and JSX settings

Applied to files:

  • packages/trigger-sdk/package.json
📚 Learning: 2026-03-02T12:43:34.140Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: packages/cli-v3/CLAUDE.md:0-0
Timestamp: 2026-03-02T12:43:34.140Z
Learning: Applies to packages/cli-v3/src/build/**/* : Build system in `src/build/` should use configuration from `trigger.config.ts` in user projects to determine bundling, build extensions, and output structure

Applied to files:

  • packages/trigger-sdk/package.json
📚 Learning: 2026-03-02T12:42:41.110Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-02T12:42:41.110Z
Learning: Applies to **/*.{ts,tsx} : Import from trigger.dev/core subpaths only, never from the root

Applied to files:

  • packages/trigger-sdk/package.json
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger.config.ts : Specify task locations in trigger.config.ts using the `dirs` array, with automatic exclusion of .test and .spec files

Applied to files:

  • packages/trigger-sdk/package.json
📚 Learning: 2025-11-27T16:26:58.661Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/webapp.mdc:0-0
Timestamp: 2025-11-27T16:26:58.661Z
Learning: Applies to apps/webapp/**/*.{ts,tsx} : Follow the Remix 2.1.0 and Express server conventions when updating the main trigger.dev webapp

Applied to files:

  • packages/trigger-sdk/package.json
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger.config.ts : Use build extensions in trigger.config.ts (additionalFiles, additionalPackages, aptGet, prismaExtension, etc.) to customize the build

Applied to files:

  • packages/trigger-sdk/package.json
📚 Learning: 2025-11-26T14:40:07.146Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 2710
File: packages/schema-to-json/package.json:0-0
Timestamp: 2025-11-26T14:40:07.146Z
Learning: Node.js 24+ has native TypeScript support and can execute .ts files directly without tsx or ts-node for scripts that use only erasable TypeScript syntax (type annotations, interfaces, etc.). The trigger.dev repository uses Node.js 24.11.1+ and scripts like updateVersion.ts can be run with `node` instead of `tsx`.

Applied to files:

  • packages/trigger-sdk/package.json
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Attach metadata to task runs using the metadata option when triggering, and access/update it inside runs using metadata functions

Applied to files:

  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use metadata methods (set, del, replace, append, remove, increment, decrement, stream, flush) to update metadata during task execution

Applied to files:

  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use logger methods (debug, log, info, warn, error) from `trigger.dev/sdk/v3` for structured logging in tasks

Applied to files:

  • packages/trigger-sdk/src/v3/ai.ts
🪛 markdownlint-cli2 (0.21.0)
.scratch/plan-graceful-oversized-batch-items.md

[warning] 216-216: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🔇 Additional comments (24)
.claude/rules/package-installation.md (1)

10-22: Clear and actionable monorepo dependency install workflow.

The new guidance is explicit and practical for this repo setup.

CLAUDE.md (1)

9-10: Good top-level pointer to the package-installation rule.

This keeps root instructions concise while linking to the detailed process.

packages/core/src/v3/realtimeStreams/types.ts (1)

74-77: Nice API surface extension for tracing customization.

The new optional fields are additive and maintain compatibility.

Also applies to: 206-207, 244-245

packages/trigger-sdk/src/v3/chat-constants.ts (1)

1-13: Good centralization of chat stream identifiers.

This avoids string duplication across the chat transport stack.

packages/trigger-sdk/src/v3/streams.ts (1)

142-143: Trace/span and input-wait lifecycle updates look consistent.

The new options and waitpoint flow changes integrate cleanly with the stream abstractions.

Also applies to: 170-171, 644-645, 717-717, 754-804, 819-820

packages/trigger-sdk/package.json (1)

27-30: Export map and peer/type wiring for chat subpaths look solid.

./chat and ./chat/react are consistently mapped across build, runtime, and type resolution.

Also applies to: 42-48, 77-77, 90-100, 139-161

docs/docs.json (1)

77-77: Navigation update is correctly placed.

Adding guides/ai-chat under “Writing tasks” is consistent with the existing docs structure.

packages/core/src/v3/inputStreams/manager.ts (2)

43-61: LGTM! New stream management methods are well-implemented.

The setLastSeqNum correctly guards against backward sequence number movement, shiftBuffer properly handles cleanup when the buffer becomes empty, and both methods align with the documented behavior in types.ts.


181-188: LGTM! Clean stream disconnection implementation.

The disconnectStream method properly aborts the tail's AbortController and cleans up both the tail and buffer entries for the stream.

packages/core/src/v3/inputStreams/index.ts (1)

54-64: LGTM! API wrappers follow established patterns.

The new methods correctly delegate to the underlying manager and maintain consistency with the existing API surface.

packages/core/src/v3/inputStreams/noopManager.ts (1)

25-29: LGTM! Correct no-op implementations.

The no-op methods properly implement the InputStreamManager interface with appropriate default behavior: shiftBuffer returns false (nothing to shift), and the others are silent no-ops.

docs/guides/ai-chat.mdx (3)

1-5: LGTM! Well-structured documentation with proper Mintlify frontmatter.

The documentation follows the MDX guidelines with required frontmatter fields (title, description) and optional sidebarTitle.


69-112: Frontend example is clear and practical.

The React component example demonstrates the integration pattern well. The code is complete and follows React conventions.


19-21: Version requirement callout is clear and accurate. The @ai-sdk/react import path referenced in the file (line 72) is the correct path for AI SDK v5.0.0 and later. No issues found.

packages/core/src/v3/inputStreams/types.ts (1)

73-93: LGTM! Excellent documentation for the new interface methods.

The JSDoc comments clearly explain the purpose of each method in the context of waitpoint handling and SSE tail management. The documentation will help consumers understand when and why to use each method.

.changeset/ai-sdk-chat-transport.md (1)

1-42: LGTM! Well-documented changeset with clear examples.

The minor version bump is appropriate for adding new features. The examples clearly demonstrate the usage of both frontend and backend exports.

packages/trigger-sdk/src/v3/chat.test.ts (3)

49-59: LGTM! Good test setup/teardown pattern.

The beforeEach/afterEach correctly saves and restores global.fetch, and vi.restoreAllMocks() ensures clean state between tests.


919-1022: Excellent coverage of lastEventId tracking and SSE reconnection.

The tests thoroughly verify that Last-Event-ID is passed correctly on subsequent stream subscriptions, which is critical for proper SSE resumption behavior.


1264-1614: Comprehensive waitpoint/single-run mode test coverage.

The tests cover important edge cases: storing waitpoint tokens, completing waitpoints on subsequent messages, fallback behavior when streams close unexpectedly, and fallback when waitpoint completion fails. This is crucial for the durable chat session feature.

packages/trigger-sdk/src/v3/chat.ts (5)

1-31: LGTM!

Clear module documentation with a practical usage example. Imports are appropriate for a browser-safe module, using the ai package types and @trigger.dev/core/v3 utilities that are isomorphic.


38-116: LGTM!

Type definitions are well-structured with comprehensive JSDoc. The accessToken flexibility (supporting sync/async functions) is a nice pattern for dynamic token refresh and Next.js server actions.


308-389: Well-structured ReadableStream implementation.

The streaming logic correctly handles:

  • Turn completion via __trigger_turn_complete control chunks
  • Skip-to-turn-complete state for abort/resume scenarios
  • lastEventId tracking for stream resumption
  • Defensive cleanup with try-catch on controller.close()

The error handling properly distinguishes between AbortError (expected on stop) and other errors.


243-254: LGTM!

The reconnectToStream method correctly handles missing sessions by returning null. The createChatTransport factory function provides a clean functional alternative with good documentation.

Also applies to: 392-412


274-277: ⚠️ Potential issue | 🔴 Critical

Update minimum Node.js engine requirement to match AbortSignal.any() availability.

The SDK declares engines.node >= 18.20.0 in package.json, but uses AbortSignal.any() (line 276) which requires Node 20.3.0+. This mismatch will cause runtime errors for Node 18.x and Node 20.0–20.2.x users.

Either:

  • Update engines.node to >= 20.3.0 in package.json, or
  • Replace AbortSignal.any() with manual signal composition for backward compatibility
⛔ Skipped due to learnings
Learnt from: nicktrn
Repo: triggerdotdev/trigger.dev PR: 2593
File: packages/core/src/v3/workers/warmStartClient.ts:168-170
Timestamp: 2025-10-08T11:48:12.327Z
Learning: The trigger.dev runners execute only in Node 21 and 22 environments, so modern Node.js APIs like AbortSignal.any (introduced in v20.3.0) are supported.
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-27T16:26:37.432Z
Learning: Applies to packages/trigger-sdk/**/*.{ts,tsx} : In the Trigger.dev SDK (packages/trigger-sdk), prefer isomorphic code like fetch and ReadableStream instead of Node.js-specific code
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-15T10:48:02.687Z
Learning: Use pnpm as the package manager (version 10.23.0 or later) and Node.js 20.20.0

Comment on lines +216 to +238
```
NDJSON bytes arrive
|
createNdjsonParserStream
|-- Line <= limit --> parse JSON --> enqueue object
`-- Line > limit --> extractIndexAndTask(bytes) --> enqueue OversizedItemMarker
|
StreamBatchItemsService for-await loop
|-- OversizedItemMarker --> engine.enqueueBatchItem() with __error in options
`-- Normal item --> validate --> engine.enqueueBatchItem()
|
FairQueue consumer (#handleMessage)
|-- __error in options --> processItemCallback detects it
| --> TriggerFailedTaskService.call()
| --> Creates pre-failed TaskRun with SYSTEM_FAILURE status
| --> Proper waitpoint + TaskRunWaitpoint connections created
| --> Returns { success: true, runId: failedRunFriendlyId }
`-- Normal item --> TriggerTaskService.call() --> creates normal run
|
Batch sealing: enqueuedCount === runCount (all items go through enqueueBatchItem)
Batch completion: all items have runs (real or pre-failed), waitpoints resolve normally
Parent run: batchTriggerAndWait resolves with per-item results
```
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.

⚠️ Potential issue | 🟡 Minor

Add a language identifier to the fenced block (Line 216).

This fence is missing a language tag, which trips markdownlint MD040.

📝 Proposed fix
-```
+```text
 NDJSON bytes arrive
   |
 createNdjsonParserStream
   |-- Line <= limit --> parse JSON --> enqueue object
   `-- Line > limit  --> extractIndexAndTask(bytes) --> enqueue OversizedItemMarker
   |
 StreamBatchItemsService for-await loop
   |-- OversizedItemMarker --> engine.enqueueBatchItem() with __error in options
   `-- Normal item         --> validate --> engine.enqueueBatchItem()
   |
 FairQueue consumer (`#handleMessage`)
   |-- __error in options --> processItemCallback detects it
   |     --> TriggerFailedTaskService.call()
   |     --> Creates pre-failed TaskRun with SYSTEM_FAILURE status
   |     --> Proper waitpoint + TaskRunWaitpoint connections created
   |     --> Returns { success: true, runId: failedRunFriendlyId }
   `-- Normal item --> TriggerTaskService.call() --> creates normal run
   |
 Batch sealing: enqueuedCount === runCount (all items go through enqueueBatchItem)
 Batch completion: all items have runs (real or pre-failed), waitpoints resolve normally
 Parent run: batchTriggerAndWait resolves with per-item results
</details>

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

```suggestion

🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 216-216: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.scratch/plan-graceful-oversized-batch-items.md around lines 216 - 238, The
fenced code block that starts with "NDJSON bytes arrive" is missing a language
identifier which triggers markdownlint MD040; update the opening fence to
include a language (e.g., change ``` to ```text) so the block is explicitly
marked as plain text (ensure the closing fence remains ```), preserving the
existing block contents and indentation.

Comment thread packages/trigger-sdk/src/v3/ai.ts Outdated
Comment thread packages/trigger-sdk/src/v3/ai.ts Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
packages/trigger-sdk/src/v3/ai.ts (2)

541-545: ⚠️ Potential issue | 🔴 Critical

Drain buffered message backlog; current code drops all but the first buffered item.

Lines 541-545 capture multiple messages during streaming, but Line 626 only uses the first and the rest are discarded after msgSub.off(). This can lose user input.

💡 Suggested fix (preserve queue across turns)
       let currentWirePayload = payload;
+      const queuedMessages: ChatTaskWirePayload[] = [];

       // ...
       const pendingMessages: ChatTaskWirePayload[] = [];
       const msgSub = messagesInput.on((msg) => {
         pendingMessages.push(msg);
       });

       // ...
       if (pendingMessages.length > 0) {
-        currentWirePayload = pendingMessages[0]!;
+        currentWirePayload = pendingMessages.shift()!;
+        if (pendingMessages.length > 0) {
+          queuedMessages.push(...pendingMessages);
+        }
         return "continue";
       }

+      if (queuedMessages.length > 0) {
+        currentWirePayload = queuedMessages.shift()!;
+        return "continue";
+      }

Also applies to: 624-627

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/trigger-sdk/src/v3/ai.ts` around lines 541 - 545, The buffered
pendingMessages array (populated by messagesInput.on and unsubscribed via
msgSub.off) is currently only consuming the first item and discarding the rest;
update the logic that runs after msgSub.off() to drain the entire
pendingMessages queue in FIFO order instead of taking only pendingMessages[0]:
iterate through pendingMessages and push/emit each message back into the normal
processing path (or append them to the messages queue used by the turn) before
proceeding, and only then call msgSub.off(); reference pendingMessages,
messagesInput.on, msgSub.off and the consumer that currently reads
pendingMessages[0] to locate and replace the drop-with-first behavior.

245-245: ⚠️ Potential issue | 🔴 Critical

Scope pipe-call tracking per run/turn instead of module-global state.

Line 245 defines _chatPipeCount as shared module state. Concurrent chat runs can affect each other and incorrectly suppress auto-piping at Line 579.

💡 Suggested fix (run-scoped counter)
 import {
   accessoryAttributes,
   AnyTask,
   isSchemaZodEsque,
   SemanticInternalAttributes,
   Task,
+  taskContext,
   type inferSchemaIn,
   type PipeStreamOptions,
   type TaskIdentifier,
   type TaskOptions,
   type TaskSchema,
   type TaskWithSchema,
 } from "@trigger.dev/core/v3";

-let _chatPipeCount = 0;
+const _chatPipeCountByRun = new Map<string, number>();

 async function pipeChat(
   source: UIMessageStreamable | AsyncIterable<unknown> | ReadableStream<unknown>,
   options?: PipeChatOptions
 ): Promise<void> {
-  _chatPipeCount++;
+  const runId = taskContext.ctx?.run.id;
+  if (runId) {
+    _chatPipeCountByRun.set(runId, (_chatPipeCountByRun.get(runId) ?? 0) + 1);
+  }
   const streamKey = options?.streamKey ?? CHAT_STREAM_KEY;
   // ...
 }

 // inside chatTask turn
-  _chatPipeCount = 0;
+  const activeRunId = taskContext.ctx?.run.id;
+  if (activeRunId) _chatPipeCountByRun.set(activeRunId, 0);

-  if (_chatPipeCount === 0 && isUIMessageStreamable(result)) {
+  const pipeCount = activeRunId ? (_chatPipeCountByRun.get(activeRunId) ?? 0) : 0;
+  if (pipeCount === 0 && isUIMessageStreamable(result)) {
     // ...
   }

Also applies to: 337-338, 530-531, 579-579

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/trigger-sdk/src/v3/ai.ts` at line 245, The module-global counter
_chatPipeCount should be replaced with a run-scoped counter so concurrent chat
runs don't interfere: remove the top-level _chatPipeCount and add a per-run
property (e.g., set chatRun.chatPipeCount or add to the run/turn context object)
initialized when a chat run/turn starts; update all increments, decrements and
checks that currently reference _chatPipeCount (including the auto-piping check
referenced as the “auto-piping” logic) to read/update this per-run property so
each run tracks its own pipe count; ensure initialization, increment/decrement,
and any conditional suppression logic use the new run-scoped symbol instead of
_chatPipeCount.
🧹 Nitpick comments (1)
packages/trigger-sdk/src/v3/chat.ts (1)

27-31: Use shared CHAT_STREAM_KEY constant instead of duplicating "chat" literal.

Line 29 duplicates a value already defined in chat-constants.ts. Reusing the shared constant avoids drift between frontend and backend defaults.

💡 Suggested refactor
-import { CHAT_MESSAGES_STREAM_ID, CHAT_STOP_STREAM_ID } from "./chat-constants.js";
+import { CHAT_MESSAGES_STREAM_ID, CHAT_STOP_STREAM_ID, CHAT_STREAM_KEY } from "./chat-constants.js";

-const DEFAULT_STREAM_KEY = "chat";
+const DEFAULT_STREAM_KEY = CHAT_STREAM_KEY;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/trigger-sdk/src/v3/chat.ts` around lines 27 - 31, Replace the
duplicated "chat" literal with the shared constant from chat-constants.ts:
import CHAT_STREAM_KEY alongside CHAT_MESSAGES_STREAM_ID and CHAT_STOP_STREAM_ID
and set DEFAULT_STREAM_KEY = CHAT_STREAM_KEY instead of the hard-coded "chat"
string; this keeps frontend/backend defaults in sync and avoids drift (update
any references to DEFAULT_STREAM_KEY in this module if needed).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/trigger-sdk/src/v3/ai.ts`:
- Around line 382-425: chatTask currently hardcodes CHAT_STREAM_KEY for
auto-piping and turn-complete control messages which breaks non-default stream
keys; update the chat task plumbing so the run payload carries the effective
streamKey (threaded from TriggerChatTransport/transport config) and use that
payload.streamKey (or the task's configured streamKey) instead of
CHAT_STREAM_KEY in the auto-pipe logic and when emitting the turn-complete
control message (references: TriggerChatTransport, chatTask run
handler/auto-pipe, and the turn-complete control write locations that currently
use CHAT_STREAM_KEY). Ensure the ChatTaskRunPayload type includes streamKey,
pass the transport's streamKey into runs, and replace hardcoded CHAT_STREAM_KEY
usages with the dynamic payload.streamKey.

---

Duplicate comments:
In `@packages/trigger-sdk/src/v3/ai.ts`:
- Around line 541-545: The buffered pendingMessages array (populated by
messagesInput.on and unsubscribed via msgSub.off) is currently only consuming
the first item and discarding the rest; update the logic that runs after
msgSub.off() to drain the entire pendingMessages queue in FIFO order instead of
taking only pendingMessages[0]: iterate through pendingMessages and push/emit
each message back into the normal processing path (or append them to the
messages queue used by the turn) before proceeding, and only then call
msgSub.off(); reference pendingMessages, messagesInput.on, msgSub.off and the
consumer that currently reads pendingMessages[0] to locate and replace the
drop-with-first behavior.
- Line 245: The module-global counter _chatPipeCount should be replaced with a
run-scoped counter so concurrent chat runs don't interfere: remove the top-level
_chatPipeCount and add a per-run property (e.g., set chatRun.chatPipeCount or
add to the run/turn context object) initialized when a chat run/turn starts;
update all increments, decrements and checks that currently reference
_chatPipeCount (including the auto-piping check referenced as the “auto-piping”
logic) to read/update this per-run property so each run tracks its own pipe
count; ensure initialization, increment/decrement, and any conditional
suppression logic use the new run-scoped symbol instead of _chatPipeCount.

---

Nitpick comments:
In `@packages/trigger-sdk/src/v3/chat.ts`:
- Around line 27-31: Replace the duplicated "chat" literal with the shared
constant from chat-constants.ts: import CHAT_STREAM_KEY alongside
CHAT_MESSAGES_STREAM_ID and CHAT_STOP_STREAM_ID and set DEFAULT_STREAM_KEY =
CHAT_STREAM_KEY instead of the hard-coded "chat" string; this keeps
frontend/backend defaults in sync and avoids drift (update any references to
DEFAULT_STREAM_KEY in this module if needed).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0248001b-2508-4f06-980e-b32230fa359f

📥 Commits

Reviewing files that changed from the base of the PR and between d7817e0 and 6b4e3dd.

📒 Files selected for processing (3)
  • packages/trigger-sdk/src/v3/ai.ts
  • packages/trigger-sdk/src/v3/chat.test.ts
  • packages/trigger-sdk/src/v3/chat.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/trigger-sdk/src/v3/chat.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (27)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (6, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (4, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (5, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (8, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (5, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (2, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (2, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (3, 8)
  • GitHub Check: typecheck / typecheck
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (7, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (4, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (8, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (7, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (1, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (6, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (1, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (3, 8)
  • GitHub Check: sdk-compat / Node.js 22.12 (ubuntu-latest)
  • GitHub Check: sdk-compat / Cloudflare Workers
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - npm)
  • GitHub Check: units / packages / 🧪 Unit Tests: Packages (1, 1)
  • GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - pnpm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - pnpm)
  • GitHub Check: sdk-compat / Node.js 20.20 (ubuntu-latest)
  • GitHub Check: sdk-compat / Bun Runtime
  • GitHub Check: sdk-compat / Deno Runtime
  • GitHub Check: Analyze (javascript-typescript)
🧰 Additional context used
📓 Path-based instructions (6)
packages/trigger-sdk/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

In the Trigger.dev SDK (packages/trigger-sdk), prefer isomorphic code like fetch and ReadableStream instead of Node.js-specific code

Files:

  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/ai.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Use types over interfaces for TypeScript
Avoid using enums; prefer string unions or const objects instead

**/*.{ts,tsx}: In TypeScript SDK usage, always import from @trigger.dev/sdk, never from @trigger.dev/sdk/v3 or use deprecated client.defineJob
Import from @trigger.dev/core subpaths only, never from the root
Use the Run Engine 2.0 (@internal/run-engine) and redis-worker for all new work, not legacy V1 MarQS queue or deprecated V1 functions

Files:

  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/ai.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use function declarations instead of default exports

Files:

  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/ai.ts
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/otel-metrics.mdc)

**/*.ts: When creating or editing OTEL metrics (counters, histograms, gauges), ensure metric attributes have low cardinality by using only enums, booleans, bounded error codes, or bounded shard IDs
Do not use high-cardinality attributes in OTEL metrics such as UUIDs/IDs (envId, userId, runId, projectId, organizationId), unbounded integers (itemCount, batchSize, retryCount), timestamps (createdAt, startTime), or free-form strings (errorMessage, taskName, queueName)
When exporting OTEL metrics via OTLP to Prometheus, be aware that the exporter automatically adds unit suffixes to metric names (e.g., 'my_duration_ms' becomes 'my_duration_ms_milliseconds', 'my_counter' becomes 'my_counter_total'). Account for these transformations when writing Grafana dashboards or Prometheus queries

Files:

  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/ai.ts
**/*.{js,ts,jsx,tsx,json,md,yaml,yml}

📄 CodeRabbit inference engine (AGENTS.md)

Format code using Prettier before committing

Files:

  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/ai.ts
packages/trigger-sdk/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (packages/trigger-sdk/CLAUDE.md)

Always import from @trigger.dev/sdk. Never use @trigger.dev/sdk/v3 (deprecated path alias)

Files:

  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/ai.ts
🧠 Learnings (15)
📓 Common learnings
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-27T16:26:37.432Z
Learning: Applies to packages/trigger-sdk/**/*.{ts,tsx} : In the Trigger.dev SDK (packages/trigger-sdk), prefer isomorphic code like fetch and ReadableStream instead of Node.js-specific code
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use `.withStreams()` to subscribe to realtime streams from task metadata in addition to run changes
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-27T16:26:37.432Z
Learning: The SDK at packages/trigger-sdk is an isomorphic TypeScript SDK
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use `trigger.dev/sdk/v3` for all imports in Trigger.dev tasks
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Use `trigger.dev/react-hooks` package for realtime subscriptions in React components
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: packages/core/CLAUDE.md:0-0
Timestamp: 2026-03-02T12:43:37.906Z
Learning: Exercise caution with changes to trigger.dev/core as they affect both the customer-facing SDK and server-side webapp - breaking changes can impact deployed user tasks and the platform simultaneously
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use logger methods (debug, log, info, warn, error) from `trigger.dev/sdk/v3` for structured logging in tasks
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use metadata methods (set, del, replace, append, remove, increment, decrement, stream, flush) to update metadata during task execution
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: packages/trigger-sdk/CLAUDE.md:0-0
Timestamp: 2026-03-02T12:43:48.124Z
Learning: Applies to packages/trigger-sdk/**/*.{ts,tsx,js,jsx} : Always import from `trigger.dev/sdk`. Never use `trigger.dev/sdk/v3` (deprecated path alias)
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use `schemaTask()` from `trigger.dev/sdk/v3` with Zod schema for payload validation
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use `trigger.dev/sdk/v3` for all imports in Trigger.dev tasks

Applied to files:

  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2025-11-27T16:26:37.432Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-27T16:26:37.432Z
Learning: The SDK at packages/trigger-sdk is an isomorphic TypeScript SDK

Applied to files:

  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use `.withStreams()` to subscribe to realtime streams from task metadata in addition to run changes

Applied to files:

  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2025-11-27T16:26:37.432Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-27T16:26:37.432Z
Learning: Applies to packages/trigger-sdk/**/*.{ts,tsx} : In the Trigger.dev SDK (packages/trigger-sdk), prefer isomorphic code like fetch and ReadableStream instead of Node.js-specific code

Applied to files:

  • packages/trigger-sdk/src/v3/chat.ts
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use the `task()` function from `trigger.dev/sdk/v3` to define tasks with id and run properties

Applied to files:

  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2026-03-02T12:43:48.124Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: packages/trigger-sdk/CLAUDE.md:0-0
Timestamp: 2026-03-02T12:43:48.124Z
Learning: Applies to packages/trigger-sdk/**/*.{ts,tsx,js,jsx} : Always import from `trigger.dev/sdk`. Never use `trigger.dev/sdk/v3` (deprecated path alias)

Applied to files:

  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use `schemaTask()` from `trigger.dev/sdk/v3` with Zod schema for payload validation

Applied to files:

  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2026-03-02T12:42:41.110Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-02T12:42:41.110Z
Learning: Applies to **/*.{ts,tsx} : In TypeScript SDK usage, always import from trigger.dev/sdk, never from trigger.dev/sdk/v3 or use deprecated client.defineJob

Applied to files:

  • packages/trigger-sdk/src/v3/chat.ts
📚 Learning: 2026-03-03T13:08:03.862Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3166
File: packages/redis-worker/src/fair-queue/index.ts:1114-1121
Timestamp: 2026-03-03T13:08:03.862Z
Learning: In packages/redis-worker/src/fair-queue/index.ts, it's acceptable for the worker queue depth cap check to allow overshooting by up to batchClaimSize messages per iteration, as the next iteration will recheck and prevent sustained growth beyond the limit.

Applied to files:

  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Attach metadata to task runs using the metadata option when triggering, and access/update it inside runs using metadata functions

Applied to files:

  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use metadata methods (set, del, replace, append, remove, increment, decrement, stream, flush) to update metadata during task execution

Applied to files:

  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Export tasks with unique IDs within the project to enable proper task discovery and execution

Applied to files:

  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use logger methods (debug, log, info, warn, error) from `trigger.dev/sdk/v3` for structured logging in tasks

Applied to files:

  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use `tasks.trigger()` with type-only imports to trigger tasks from backend code without importing the task implementation

Applied to files:

  • packages/trigger-sdk/src/v3/ai.ts
🔇 Additional comments (1)
packages/trigger-sdk/src/v3/chat.ts (1)

264-397: Solid stop/resume stream lifecycle handling.

lastEventId tracking plus skipToTurnComplete handling is robust and fits turn-based chat streaming well.

Comment thread packages/trigger-sdk/src/v3/ai.ts Outdated
Comment on lines +382 to +425
export type ChatTaskOptions<TIdentifier extends string> = Omit<
TaskOptions<TIdentifier, ChatTaskWirePayload, unknown>,
"run"
> & {
/**
* The run function for the chat task.
*
* Receives a `ChatTaskRunPayload` with the conversation messages, chat session ID,
* trigger type, and abort signals (`signal`, `cancelSignal`, `stopSignal`).
*
* **Auto-piping:** If this function returns a value with `.toUIMessageStream()`,
* the stream is automatically piped to the frontend.
*/
run: (payload: ChatTaskRunPayload) => Promise<unknown>;

/**
* Maximum number of conversational turns (message round-trips) a single run
* will handle before ending. After this many turns the run completes
* normally and the next message will start a fresh run.
*
* @default 100
*/
maxTurns?: number;

/**
* How long to wait for the next message before timing out and ending the run.
* Accepts any duration string (e.g. `"1h"`, `"30m"`).
*
* @default "1h"
*/
turnTimeout?: string;

/**
* How long (in seconds) to keep the run warm after each turn before suspending.
* During this window the run stays active and can respond instantly to the
* next message. After this timeout, the run suspends (frees compute) and waits
* via `inputStream.wait()`.
*
* Set to `0` to suspend immediately after each turn.
*
* @default 30
*/
warmTimeoutInSeconds?: number;
};
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.

⚠️ Potential issue | 🟠 Major

chatTask does not honor custom streamKey end-to-end.

TriggerChatTransport subscribes using its configured stream key (packages/trigger-sdk/src/v3/chat.ts Line 307), but chatTask auto-pipe (Lines 585-586) and turn-complete control write (Lines 710-718) are fixed to CHAT_STREAM_KEY. With non-default keys, turn completion can hang on the frontend.

💡 Suggested fix (thread streamKey through chatTask)
 export type ChatTaskOptions<TIdentifier extends string> = Omit<
   TaskOptions<TIdentifier, ChatTaskWirePayload, unknown>,
   "run"
 > & {
+  /**
+   * Stream key to use for chat output/control chunks.
+   * Must match TriggerChatTransport.streamKey on the frontend.
+   * `@default` "chat"
+   */
+  streamKey?: string;
   run: (payload: ChatTaskRunPayload) => Promise<unknown>;
   maxTurns?: number;
   turnTimeout?: string;
   warmTimeoutInSeconds?: number;
 };

 function chatTask<TIdentifier extends string>(
   options: ChatTaskOptions<TIdentifier>
 ): Task<TIdentifier, ChatTaskWirePayload, unknown> {
   const {
     run: userRun,
+    streamKey = CHAT_STREAM_KEY,
     maxTurns = 100,
     turnTimeout = "1h",
     warmTimeoutInSeconds = 30,
     ...restOptions
   } = options;

   // ...
-  await pipeChat(uiStream, { signal: combinedSignal, spanName: "stream response" });
+  await pipeChat(uiStream, {
+    streamKey,
+    signal: combinedSignal,
+    spanName: "stream response",
+  });

-  await writeTurnCompleteChunk(currentWirePayload.chatId);
+  await writeTurnCompleteChunk(streamKey);
 }

-async function writeTurnCompleteChunk(chatId?: string): Promise<void> {
-  const { waitUntilComplete } = streams.writer(CHAT_STREAM_KEY, {
+async function writeTurnCompleteChunk(streamKey: string = CHAT_STREAM_KEY): Promise<void> {
+  const { waitUntilComplete } = streams.writer(streamKey, {

Also applies to: 585-586, 710-718

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/trigger-sdk/src/v3/ai.ts` around lines 382 - 425, chatTask currently
hardcodes CHAT_STREAM_KEY for auto-piping and turn-complete control messages
which breaks non-default stream keys; update the chat task plumbing so the run
payload carries the effective streamKey (threaded from
TriggerChatTransport/transport config) and use that payload.streamKey (or the
task's configured streamKey) instead of CHAT_STREAM_KEY in the auto-pipe logic
and when emitting the turn-complete control message (references:
TriggerChatTransport, chatTask run handler/auto-pipe, and the turn-complete
control write locations that currently use CHAT_STREAM_KEY). Ensure the
ChatTaskRunPayload type includes streamKey, pass the transport's streamKey into
runs, and replace hardcoded CHAT_STREAM_KEY usages with the dynamic
payload.streamKey.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (3)
packages/trigger-sdk/src/v3/ai.ts (3)

618-621: ⚠️ Potential issue | 🔴 Critical

Only the first buffered message is consumed; additional buffered messages are dropped.

When multiple messages arrive during a turn, everything after index 0 is lost.

💡 Proposed fix (preserve backlog)
       let currentWirePayload = payload;
+      const queuedWirePayloads: ChatTaskWirePayload[] = [];
@@
-              if (pendingMessages.length > 0) {
-                currentWirePayload = pendingMessages[0]!;
+              if (pendingMessages.length > 0) {
+                queuedWirePayloads.push(...pendingMessages);
+              }
+
+              if (queuedWirePayloads.length > 0) {
+                currentWirePayload = queuedWirePayloads.shift()!;
                 return "continue";
               }

Also applies to: 764-767

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/trigger-sdk/src/v3/ai.ts` around lines 618 - 621, The buffered
message handler currently pushes incoming messages into pendingMessages but
elsewhere only the first element is consumed, causing drops; change the
consumption logic to drain the entire backlog instead of taking index 0 — after
subscribing with messagesInput.on and collecting into pendingMessages, iterate
(or repeatedly shift) through pendingMessages until empty and process each
ChatTaskWirePayload, or swap/consume a copied array and clear pendingMessages
before processing, ensuring msgSub is still properly unsubscribed when done;
update all occurrences around pendingMessages/msgSub (including the similar
block at lines ~764-767) to use this draining approach.

423-495: ⚠️ Potential issue | 🟠 Major

Custom stream keys are not threaded through chatTask auto-pipe and turn-complete writes.

pipeChat and writeTurnCompleteChunk still default to CHAT_STREAM_KEY, so non-default frontend stream keys can hang/miss control chunks.

💡 Proposed fix (thread `streamKey` through chatTask)
 export type ChatTaskOptions<TIdentifier extends string> = Omit<
   TaskOptions<TIdentifier, ChatTaskWirePayload, unknown>,
   "run"
 > & {
+  streamKey?: string;
   run: (payload: ChatTaskRunPayload) => Promise<unknown>;
@@
   const {
     run: userRun,
     onChatStart,
     onTurnComplete,
+    streamKey = CHAT_STREAM_KEY,
     maxTurns = 100,
@@
-                  await pipeChat(uiStream, { signal: combinedSignal, spanName: "stream response" });
+                  await pipeChat(uiStream, {
+                    streamKey,
+                    signal: combinedSignal,
+                    spanName: "stream response",
+                  });
@@
-              await writeTurnCompleteChunk(currentWirePayload.chatId);
+              await writeTurnCompleteChunk(streamKey);
@@
-async function writeTurnCompleteChunk(chatId?: string): Promise<void> {
-  const { waitUntilComplete } = streams.writer(CHAT_STREAM_KEY, {
+async function writeTurnCompleteChunk(streamKey: string = CHAT_STREAM_KEY): Promise<void> {
+  const { waitUntilComplete } = streams.writer(streamKey, {

Also applies to: 698-699, 762-763, 930-935

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/trigger-sdk/src/v3/ai.ts` around lines 423 - 495, The auto-pipe and
turn-complete logic for chatTask currently always uses CHAT_STREAM_KEY causing
custom frontend stream keys to be ignored; update the flow so the stream key
from the run context is threaded and used instead. Concretely: add/ensure a
streamKey property is available on ChatTaskRunPayload (or passed into the chat
task runner), then replace usages of the hard-coded CHAT_STREAM_KEY in pipeChat
and writeTurnCompleteChunk calls with that payload.streamKey (falling back to
CHAT_STREAM_KEY only if undefined). Update chatTask invocation sites (the
chatTask runner/dispatcher and any call sites that call pipeChat or
writeTurnCompleteChunk — referenced symbols: chatTask, ChatTaskRunPayload,
pipeChat, writeTurnCompleteChunk, CHAT_STREAM_KEY) so the streamKey is
propagated through the lifecycle (auto-pipe and on-turn-complete) to fix missing
control chunks for non-default stream keys.

245-246: ⚠️ Potential issue | 🔴 Critical

_chatPipeCount is module-global and race-prone across concurrent runs.

Cross-run mutations can suppress/trigger auto-piping in the wrong run.

💡 Proposed fix (run-scoped counter)
 import {
   accessoryAttributes,
   AnyTask,
   isSchemaZodEsque,
   SemanticInternalAttributes,
   Task,
+  taskContext,
   type inferSchemaIn,
   type PipeStreamOptions,
   type TaskIdentifier,
   type TaskOptions,
   type TaskSchema,
   type TaskWithSchema,
 } from "@trigger.dev/core/v3";
@@
-let _chatPipeCount = 0;
+const _chatPipeCountByRun = new Map<string, number>();
@@
-  _chatPipeCount++;
+  const runId = taskContext.ctx?.run.id;
+  if (runId) {
+    _chatPipeCountByRun.set(runId, (_chatPipeCountByRun.get(runId) ?? 0) + 1);
+  }
@@
-              _chatPipeCount = 0;
+              const activeRunId = taskContext.ctx?.run.id;
+              if (activeRunId) _chatPipeCountByRun.set(activeRunId, 0);
@@
-                if (_chatPipeCount === 0 && isUIMessageStreamable(result)) {
+                const pipeCount = activeRunId ? (_chatPipeCountByRun.get(activeRunId) ?? 0) : 0;
+                if (pipeCount === 0 && isUIMessageStreamable(result)) {
@@
-      } finally {
+      } finally {
         stopSub.off();
+        const activeRunId = taskContext.ctx?.run.id;
+        if (activeRunId) _chatPipeCountByRun.delete(activeRunId);
       }

Also applies to: 337-338, 606-607, 692-693

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/trigger-sdk/src/v3/ai.ts` around lines 245 - 246, _chatPipeCount is
a module-global counter and races across concurrent runs; replace it with a
run-scoped counter stored on the run/instance/context object used by the chat
flow (e.g., add a chatPipeCount property on the ChatRun/AI instance or pass a
runContext with chatPipeCount into the functions that reference _chatPipeCount).
Update every reference to _chatPipeCount (including locations noted around
337-338, 606-607, 692-693) to use the run-scoped property (e.g.,
this.chatPipeCount or runContext.chatPipeCount), initialize it at run start, and
perform increments/decrements against that property so each run has its own
counter and avoids cross-run races. Ensure any helper functions like
startChatPipe/createChatStream/autoPipe accept the run context or are methods on
the run instance so they can access the run-scoped counter.
🧹 Nitpick comments (3)
packages/trigger-sdk/src/v3/chat.ts (1)

27-31: Use the shared CHAT_STREAM_KEY constant instead of "chat" literal.

This avoids drift between frontend/backend defaults.

💡 Proposed refactor
-import { CHAT_MESSAGES_STREAM_ID, CHAT_STOP_STREAM_ID } from "./chat-constants.js";
+import { CHAT_MESSAGES_STREAM_ID, CHAT_STOP_STREAM_ID, CHAT_STREAM_KEY } from "./chat-constants.js";
@@
-const DEFAULT_STREAM_KEY = "chat";
+const DEFAULT_STREAM_KEY = CHAT_STREAM_KEY;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/trigger-sdk/src/v3/chat.ts` around lines 27 - 31, Replace the string
literal default for DEFAULT_STREAM_KEY with the shared constant CHAT_STREAM_KEY:
import CHAT_STREAM_KEY from the central constants module and set
DEFAULT_STREAM_KEY = CHAT_STREAM_KEY (update the import section where
CHAT_MESSAGES_STREAM_ID and CHAT_STOP_STREAM_ID are imported); this ensures
DEFAULT_STREAM_KEY uses the shared CHAT_STREAM_KEY constant instead of the
hard-coded "chat" value.
docs/guides/ai-chat.mdx (1)

32-49: Use typescript fence labels instead of ts/tsx in MDX examples.

As per coding guidelines, "Code examples must use language tags in code fences: typescript, bash, json."

Also applies to: 55-63, 69-119, 158-174, 180-205, 211-230, 243-255, 275-288, 311-338, 348-361, 367-423, 430-555, 575-585, 597-603, 610-614, 621-638, 647-651, 658-662, 688-698, 710-718, 768-774

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/guides/ai-chat.mdx` around lines 32 - 49, Change all MDX code fence
language tags from the short forms "ts" or "tsx" to the canonical "typescript"
per docs guidelines; for example, replace the opening fence "```ts
trigger/chat.ts" (and any other occurrences listed) with "```typescript
trigger/chat.ts" so examples like the chat.task export and streamText usage
remain unchanged but use the required language tag. Search for "```ts" and
"```tsx" in docs/guides/ai-chat.mdx (including the ranges called out in the
review) and update each fence to "```typescript" while leaving file references
and code content intact.
packages/trigger-sdk/src/v3/ai.ts (1)

935-935: Extract the control-chunk type string to a shared constant.

"__trigger_turn_complete" is protocol-level and used in both backend (ai.ts) and frontend (chat.ts); centralizing it avoids drift.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/trigger-sdk/src/v3/ai.ts` at line 935, Replace the hard-coded
protocol string in write({ type: "__trigger_turn_complete" }) with a shared
exported constant: create and export a constant (e.g. TRIGGER_TURN_COMPLETE =
"__trigger_turn_complete") from a central module used by both backend and
frontend, import that constant into packages/trigger-sdk/src/v3/ai.ts (where
write(...) is called) and into the frontend chat code that currently uses the
same literal, and update any related type unions or checks to reference the
constant so both sides use the single canonical identifier.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/guides/ai-chat.mdx`:
- Around line 21-22: Update the ai package version requirement in the ai-chat
guide from "v5.0.0 or later" to "v6.0.0 or later" so it matches the reference
implementation; specifically edit the sentence containing "Requires
`@trigger.dev/sdk` version **4.4.0 or later** and the `ai` package **v5.0.0 or
later**" in docs/guides/ai-chat.mdx to use **v6.0.0 or later** instead.

In `@packages/trigger-sdk/src/v3/chat.test.ts`:
- Around line 103-104: The tests in packages/trigger-sdk/src/v3/chat.test.ts are
replacing global.fetch with vi.fn() mocks (e.g., the global.fetch =
vi.fn().mockImplementation(...) blocks) which violates the repo policy requiring
real integration-style tests via testcontainers and using vitest without
mocking; replace each mocked fetch usage with a testcontainers-backed HTTP
service (or a lightweight local server) that returns the desired responses and
update the tests to call that real endpoint via the existing client code,
removing vi.fn() mocks and any restore logic; keep test names and assertions
intact but point their HTTP base URL/host to the containerized server so the
suite exercises real network behavior under vitest.

In `@packages/trigger-sdk/src/v3/chat.ts`:
- Around line 277-279: TriggerChatTransport currently saves custom headers in
extraHeaders for SSE subscriptions but fails to forward them to triggerTask and
ApiClient.sendInputStream calls; update the calls inside TriggerChatTransport
(where triggerTask(...) is invoked and where new
ApiClient(...).sendInputStream(...) is invoked, including the stop/send stop
paths) to pass the stored extraHeaders as the headers option (e.g., pass {
headers: this.extraHeaders } or session.extraHeaders) so proxy/tenant routing
and custom auth headers are propagated to triggerTask and sendInputStream.

---

Duplicate comments:
In `@packages/trigger-sdk/src/v3/ai.ts`:
- Around line 618-621: The buffered message handler currently pushes incoming
messages into pendingMessages but elsewhere only the first element is consumed,
causing drops; change the consumption logic to drain the entire backlog instead
of taking index 0 — after subscribing with messagesInput.on and collecting into
pendingMessages, iterate (or repeatedly shift) through pendingMessages until
empty and process each ChatTaskWirePayload, or swap/consume a copied array and
clear pendingMessages before processing, ensuring msgSub is still properly
unsubscribed when done; update all occurrences around pendingMessages/msgSub
(including the similar block at lines ~764-767) to use this draining approach.
- Around line 423-495: The auto-pipe and turn-complete logic for chatTask
currently always uses CHAT_STREAM_KEY causing custom frontend stream keys to be
ignored; update the flow so the stream key from the run context is threaded and
used instead. Concretely: add/ensure a streamKey property is available on
ChatTaskRunPayload (or passed into the chat task runner), then replace usages of
the hard-coded CHAT_STREAM_KEY in pipeChat and writeTurnCompleteChunk calls with
that payload.streamKey (falling back to CHAT_STREAM_KEY only if undefined).
Update chatTask invocation sites (the chatTask runner/dispatcher and any call
sites that call pipeChat or writeTurnCompleteChunk — referenced symbols:
chatTask, ChatTaskRunPayload, pipeChat, writeTurnCompleteChunk, CHAT_STREAM_KEY)
so the streamKey is propagated through the lifecycle (auto-pipe and
on-turn-complete) to fix missing control chunks for non-default stream keys.
- Around line 245-246: _chatPipeCount is a module-global counter and races
across concurrent runs; replace it with a run-scoped counter stored on the
run/instance/context object used by the chat flow (e.g., add a chatPipeCount
property on the ChatRun/AI instance or pass a runContext with chatPipeCount into
the functions that reference _chatPipeCount). Update every reference to
_chatPipeCount (including locations noted around 337-338, 606-607, 692-693) to
use the run-scoped property (e.g., this.chatPipeCount or
runContext.chatPipeCount), initialize it at run start, and perform
increments/decrements against that property so each run has its own counter and
avoids cross-run races. Ensure any helper functions like
startChatPipe/createChatStream/autoPipe accept the run context or are methods on
the run instance so they can access the run-scoped counter.

---

Nitpick comments:
In `@docs/guides/ai-chat.mdx`:
- Around line 32-49: Change all MDX code fence language tags from the short
forms "ts" or "tsx" to the canonical "typescript" per docs guidelines; for
example, replace the opening fence "```ts trigger/chat.ts" (and any other
occurrences listed) with "```typescript trigger/chat.ts" so examples like the
chat.task export and streamText usage remain unchanged but use the required
language tag. Search for "```ts" and "```tsx" in docs/guides/ai-chat.mdx
(including the ranges called out in the review) and update each fence to
"```typescript" while leaving file references and code content intact.

In `@packages/trigger-sdk/src/v3/ai.ts`:
- Line 935: Replace the hard-coded protocol string in write({ type:
"__trigger_turn_complete" }) with a shared exported constant: create and export
a constant (e.g. TRIGGER_TURN_COMPLETE = "__trigger_turn_complete") from a
central module used by both backend and frontend, import that constant into
packages/trigger-sdk/src/v3/ai.ts (where write(...) is called) and into the
frontend chat code that currently uses the same literal, and update any related
type unions or checks to reference the constant so both sides use the single
canonical identifier.

In `@packages/trigger-sdk/src/v3/chat.ts`:
- Around line 27-31: Replace the string literal default for DEFAULT_STREAM_KEY
with the shared constant CHAT_STREAM_KEY: import CHAT_STREAM_KEY from the
central constants module and set DEFAULT_STREAM_KEY = CHAT_STREAM_KEY (update
the import section where CHAT_MESSAGES_STREAM_ID and CHAT_STOP_STREAM_ID are
imported); this ensures DEFAULT_STREAM_KEY uses the shared CHAT_STREAM_KEY
constant instead of the hard-coded "chat" value.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0763b3e5-6e7e-4109-b919-c15533ed514e

📥 Commits

Reviewing files that changed from the base of the PR and between 6b4e3dd and cc1ce9b.

⛔ Files ignored due to path filters (16)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • references/ai-chat/.gitignore is excluded by !references/**
  • references/ai-chat/package.json is excluded by !references/**
  • references/ai-chat/prisma.config.ts is excluded by !references/**
  • references/ai-chat/prisma/migrations/20260305112427_init/migration.sql is excluded by !references/**
  • references/ai-chat/prisma/migrations/migration_lock.toml is excluded by !references/**
  • references/ai-chat/prisma/schema.prisma is excluded by !references/**
  • references/ai-chat/src/app/actions.ts is excluded by !references/**
  • references/ai-chat/src/app/page.tsx is excluded by !references/**
  • references/ai-chat/src/components/chat-app.tsx is excluded by !references/**
  • references/ai-chat/src/components/chat-sidebar.tsx is excluded by !references/**
  • references/ai-chat/src/components/chat.tsx is excluded by !references/**
  • references/ai-chat/src/lib/models.ts is excluded by !references/**
  • references/ai-chat/src/lib/prisma.ts is excluded by !references/**
  • references/ai-chat/src/trigger/chat.ts is excluded by !references/**
  • references/ai-chat/trigger.config.ts is excluded by !references/**
📒 Files selected for processing (5)
  • docs/guides/ai-chat.mdx
  • packages/trigger-sdk/src/v3/ai.ts
  • packages/trigger-sdk/src/v3/chat-react.ts
  • packages/trigger-sdk/src/v3/chat.test.ts
  • packages/trigger-sdk/src/v3/chat.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/trigger-sdk/src/v3/chat-react.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (5, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (3, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (6, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (7, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (1, 8)
  • GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - npm)
  • GitHub Check: sdk-compat / Node.js 20.20 (ubuntu-latest)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - pnpm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - pnpm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - npm)
  • GitHub Check: Analyze (javascript-typescript)
🧰 Additional context used
📓 Path-based instructions (11)
docs/**/*.{md,mdx}

📄 CodeRabbit inference engine (CLAUDE.md)

Docs in docs/ directory should use Mintlify MDX format following conventions in docs/CLAUDE.md

Files:

  • docs/guides/ai-chat.mdx
docs/**/*.mdx

📄 CodeRabbit inference engine (docs/CLAUDE.md)

docs/**/*.mdx: MDX documentation pages must include frontmatter with title (required), description (required), and sidebarTitle (optional) in YAML format
Use Mintlify components for structured content: , , , , , , /, /
Always import from @trigger.dev/sdk in code examples (never from @trigger.dev/sdk/v3)
Code examples must be complete and runnable where possible
Use language tags in code fences: typescript, bash, json

Files:

  • docs/guides/ai-chat.mdx
packages/trigger-sdk/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

In the Trigger.dev SDK (packages/trigger-sdk), prefer isomorphic code like fetch and ReadableStream instead of Node.js-specific code

Files:

  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/chat.test.ts
  • packages/trigger-sdk/src/v3/ai.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Use types over interfaces for TypeScript
Avoid using enums; prefer string unions or const objects instead

**/*.{ts,tsx}: In TypeScript SDK usage, always import from @trigger.dev/sdk, never from @trigger.dev/sdk/v3 or use deprecated client.defineJob
Import from @trigger.dev/core subpaths only, never from the root
Use the Run Engine 2.0 (@internal/run-engine) and redis-worker for all new work, not legacy V1 MarQS queue or deprecated V1 functions

Files:

  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/chat.test.ts
  • packages/trigger-sdk/src/v3/ai.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use function declarations instead of default exports

Files:

  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/chat.test.ts
  • packages/trigger-sdk/src/v3/ai.ts
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/otel-metrics.mdc)

**/*.ts: When creating or editing OTEL metrics (counters, histograms, gauges), ensure metric attributes have low cardinality by using only enums, booleans, bounded error codes, or bounded shard IDs
Do not use high-cardinality attributes in OTEL metrics such as UUIDs/IDs (envId, userId, runId, projectId, organizationId), unbounded integers (itemCount, batchSize, retryCount), timestamps (createdAt, startTime), or free-form strings (errorMessage, taskName, queueName)
When exporting OTEL metrics via OTLP to Prometheus, be aware that the exporter automatically adds unit suffixes to metric names (e.g., 'my_duration_ms' becomes 'my_duration_ms_milliseconds', 'my_counter' becomes 'my_counter_total'). Account for these transformations when writing Grafana dashboards or Prometheus queries

Files:

  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/chat.test.ts
  • packages/trigger-sdk/src/v3/ai.ts
**/*.{js,ts,jsx,tsx,json,md,yaml,yml}

📄 CodeRabbit inference engine (AGENTS.md)

Format code using Prettier before committing

Files:

  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/chat.test.ts
  • packages/trigger-sdk/src/v3/ai.ts
packages/trigger-sdk/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (packages/trigger-sdk/CLAUDE.md)

Always import from @trigger.dev/sdk. Never use @trigger.dev/sdk/v3 (deprecated path alias)

Files:

  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/chat.test.ts
  • packages/trigger-sdk/src/v3/ai.ts
**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use vitest for all tests in the Trigger.dev repository

Files:

  • packages/trigger-sdk/src/v3/chat.test.ts
**/*.test.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.test.{ts,tsx,js,jsx}: Test files should live beside the files under test and use descriptive describe and it blocks
Tests should avoid mocks or stubs and use the helpers from @internal/testcontainers when Redis or Postgres are needed
Use vitest for running unit tests

Files:

  • packages/trigger-sdk/src/v3/chat.test.ts
**/*.test.{ts,tsx,js}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.test.{ts,tsx,js}: Use vitest exclusively for testing - never mock anything, use testcontainers instead
Place test files next to source files with .test.ts naming convention (e.g., MyService.ts -> MyService.test.ts)
Test files using Redis or PostgreSQL should use testcontainers helpers (redisTest, postgresTest, containerTest) instead of mocks

Files:

  • packages/trigger-sdk/src/v3/chat.test.ts
🧠 Learnings (20)
📓 Common learnings
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-27T16:26:37.432Z
Learning: The SDK at packages/trigger-sdk is an isomorphic TypeScript SDK
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-27T16:26:37.432Z
Learning: Applies to packages/trigger-sdk/**/*.{ts,tsx} : In the Trigger.dev SDK (packages/trigger-sdk), prefer isomorphic code like fetch and ReadableStream instead of Node.js-specific code
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use `trigger.dev/sdk/v3` for all imports in Trigger.dev tasks
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use `.withStreams()` to subscribe to realtime streams from task metadata in addition to run changes
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: packages/trigger-sdk/CLAUDE.md:0-0
Timestamp: 2026-03-02T12:43:48.124Z
Learning: Applies to packages/trigger-sdk/**/*.{ts,tsx,js,jsx} : Always import from `trigger.dev/sdk`. Never use `trigger.dev/sdk/v3` (deprecated path alias)
📚 Learning: 2026-03-02T12:43:34.140Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: packages/cli-v3/CLAUDE.md:0-0
Timestamp: 2026-03-02T12:43:34.140Z
Learning: Keep SDK documentation in `rules/` and `.claude/skills/trigger-dev-tasks/` synchronized when features are added or changed

Applied to files:

  • docs/guides/ai-chat.mdx
📚 Learning: 2026-03-02T12:43:37.906Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: packages/core/CLAUDE.md:0-0
Timestamp: 2026-03-02T12:43:37.906Z
Learning: Exercise caution with changes to trigger.dev/core as they affect both the customer-facing SDK and server-side webapp - breaking changes can impact deployed user tasks and the platform simultaneously

Applied to files:

  • docs/guides/ai-chat.mdx
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use `.withStreams()` to subscribe to realtime streams from task metadata in addition to run changes

Applied to files:

  • docs/guides/ai-chat.mdx
  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/chat.test.ts
  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use `trigger.dev/sdk/v3` for all imports in Trigger.dev tasks

Applied to files:

  • docs/guides/ai-chat.mdx
  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/chat.test.ts
  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use `schemaTask()` from `trigger.dev/sdk/v3` with Zod schema for payload validation

Applied to files:

  • docs/guides/ai-chat.mdx
  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2025-11-27T16:26:37.432Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-27T16:26:37.432Z
Learning: The SDK at packages/trigger-sdk is an isomorphic TypeScript SDK

Applied to files:

  • docs/guides/ai-chat.mdx
  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use `yourTask.trigger()` to trigger a task from inside another task with specified payload

Applied to files:

  • docs/guides/ai-chat.mdx
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use the `task()` function from `trigger.dev/sdk/v3` to define tasks with id and run properties

Applied to files:

  • docs/guides/ai-chat.mdx
  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2025-11-27T16:26:37.432Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-27T16:26:37.432Z
Learning: Applies to packages/trigger-sdk/**/*.{ts,tsx} : In the Trigger.dev SDK (packages/trigger-sdk), prefer isomorphic code like fetch and ReadableStream instead of Node.js-specific code

Applied to files:

  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/chat.test.ts
📚 Learning: 2025-11-27T16:26:37.432Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-27T16:26:37.432Z
Learning: Applies to **/*.{test,spec}.{ts,tsx} : Use vitest for all tests in the Trigger.dev repository

Applied to files:

  • packages/trigger-sdk/src/v3/chat.test.ts
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Subscribe to run updates using `runs.subscribeToRun()` for realtime monitoring of task execution

Applied to files:

  • packages/trigger-sdk/src/v3/chat.test.ts
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use `runs.subscribeToBatch()` to subscribe to changes for all runs in a batch

Applied to files:

  • packages/trigger-sdk/src/v3/chat.test.ts
📚 Learning: 2026-03-02T12:43:48.124Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: packages/trigger-sdk/CLAUDE.md:0-0
Timestamp: 2026-03-02T12:43:48.124Z
Learning: Applies to packages/trigger-sdk/**/*.{ts,tsx,js,jsx} : Always import from `trigger.dev/sdk`. Never use `trigger.dev/sdk/v3` (deprecated path alias)

Applied to files:

  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Export tasks with unique IDs within the project to enable proper task discovery and execution

Applied to files:

  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2026-03-03T13:08:03.862Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3166
File: packages/redis-worker/src/fair-queue/index.ts:1114-1121
Timestamp: 2026-03-03T13:08:03.862Z
Learning: In packages/redis-worker/src/fair-queue/index.ts, it's acceptable for the worker queue depth cap check to allow overshooting by up to batchClaimSize messages per iteration, as the next iteration will recheck and prevent sustained growth beyond the limit.

Applied to files:

  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Attach metadata to task runs using the metadata option when triggering, and access/update it inside runs using metadata functions

Applied to files:

  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use metadata methods (set, del, replace, append, remove, increment, decrement, stream, flush) to update metadata during task execution

Applied to files:

  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use logger methods (debug, log, info, warn, error) from `trigger.dev/sdk/v3` for structured logging in tasks

Applied to files:

  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use `tasks.trigger()` with type-only imports to trigger tasks from backend code without importing the task implementation

Applied to files:

  • packages/trigger-sdk/src/v3/ai.ts
🔇 Additional comments (2)
packages/trigger-sdk/src/v3/chat.ts (1)

415-507: Stop + turn-complete stream lifecycle handling looks robust.

The abort listener, skipToTurnComplete behavior, and control-chunk interception are implemented cleanly.

packages/trigger-sdk/src/v3/chat.test.ts (1)

49-1913: Excellent behavioral coverage across transport lifecycle and edge cases.

The suite meaningfully exercises streaming, resume, fallback, abort, token, and session-change paths.

Comment thread docs/guides/ai-chat.mdx Outdated
Comment on lines +21 to +22
Requires `@trigger.dev/sdk` version **4.4.0 or later** and the `ai` package **v5.0.0 or later**.
</Note>
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Compare declared AI package versions in repo vs docs note.
rg -n '"ai"\s*:' --glob '**/package.json'
echo "---"
rg -n 'Requires `@trigger.dev/sdk` version|`ai` package' docs/guides/ai-chat.mdx

Repository: triggerdotdev/trigger.dev

Length of output: 800


Update ai package version requirement to match reference implementation.

The documentation states the ai package v5.0.0 or later, but the ai-chat reference example at references/ai-chat/package.json requires v6.0.0. Update the version requirement in the guide to reflect the actual minimum version needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/guides/ai-chat.mdx` around lines 21 - 22, Update the ai package version
requirement in the ai-chat guide from "v5.0.0 or later" to "v6.0.0 or later" so
it matches the reference implementation; specifically edit the sentence
containing "Requires `@trigger.dev/sdk` version **4.4.0 or later** and the `ai`
package **v5.0.0 or later**" in docs/guides/ai-chat.mdx to use **v6.0.0 or
later** instead.

Comment thread packages/trigger-sdk/src/v3/chat.test.ts
Comment thread packages/trigger-sdk/src/v3/chat.ts Outdated
Comment on lines +277 to +279
const apiClient = new ApiClient(this.baseURL, session.publicAccessToken);
await apiClient.sendInputStream(session.runId, CHAT_MESSAGES_STREAM_ID, minimalPayload);
return this.subscribeToStream(
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "index.ts" | grep -E "apiClient|v3" | head -20

Repository: triggerdotdev/trigger.dev

Length of output: 930


🏁 Script executed:

rg -l "class ApiClient" packages/core

Repository: triggerdotdev/trigger.dev

Length of output: 155


🏁 Script executed:

# Check the ApiClient implementation
rg -A 20 "triggerTask\(" packages/core/src/v3/apiClient/index.ts | head -50

Repository: triggerdotdev/trigger.dev

Length of output: 755


🏁 Script executed:

# Check sendInputStream method
rg -A 20 "sendInputStream\(" packages/core/src/v3/apiClient/index.ts | head -50

Repository: triggerdotdev/trigger.dev

Length of output: 641


🏁 Script executed:

wc -l packages/trigger-sdk/src/v3/chat.ts

Repository: triggerdotdev/trigger.dev

Length of output: 108


🏁 Script executed:

sed -n '270,310p' packages/trigger-sdk/src/v3/chat.ts

Repository: triggerdotdev/trigger.dev

Length of output: 1541


🏁 Script executed:

sed -n '420,430p' packages/trigger-sdk/src/v3/chat.ts

Repository: triggerdotdev/trigger.dev

Length of output: 461


🏁 Script executed:

# Check the TriggerChatTransport class definition and header handling
rg -B 10 "extraHeaders|headers" packages/trigger-sdk/src/v3/chat.ts | head -80

Repository: triggerdotdev/trigger.dev

Length of output: 1930


Pass headers option to triggerTask and sendInputStream API calls.

TriggerChatTransport stores custom headers in extraHeaders and uses them for SSE subscriptions, but omits them from triggerTask, sendInputStream (message), and sendInputStream (stop) calls. This breaks proxy/tenant routing or custom auth flows that depend on these headers.

Proposed fix
-        await apiClient.sendInputStream(session.runId, CHAT_MESSAGES_STREAM_ID, minimalPayload);
+        await apiClient.sendInputStream(
+          session.runId,
+          CHAT_MESSAGES_STREAM_ID,
+          minimalPayload,
+          { headers: this.extraHeaders }
+        );
-    const triggerResponse = await apiClient.triggerTask(this.taskId, {
-      payload,
-      options: {
-        payloadType: "application/json",
-      },
-    });
+    const triggerResponse = await apiClient.triggerTask(
+      this.taskId,
+      {
+        payload,
+        options: {
+          payloadType: "application/json",
+        },
+      },
+      undefined,
+      { headers: this.extraHeaders }
+    );
-            api
-              .sendInputStream(session.runId, CHAT_STOP_STREAM_ID, { stop: true })
+            api
+              .sendInputStream(session.runId, CHAT_STOP_STREAM_ID, { stop: true }, { headers: this.extraHeaders })
               .catch(() => {}); // Best-effort

Applies to lines 277–279, 296–301, and 425–426.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/trigger-sdk/src/v3/chat.ts` around lines 277 - 279,
TriggerChatTransport currently saves custom headers in extraHeaders for SSE
subscriptions but fails to forward them to triggerTask and
ApiClient.sendInputStream calls; update the calls inside TriggerChatTransport
(where triggerTask(...) is invoked and where new
ApiClient(...).sendInputStream(...) is invoked, including the stop/send stop
paths) to pass the stored extraHeaders as the headers option (e.g., pass {
headers: this.extraHeaders } or session.extraHeaders) so proxy/tenant routing
and custom auth headers are propagated to triggerTask and sendInputStream.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/trigger-sdk/src/v3/streams.ts (1)

780-807: ⚠️ Potential issue | 🔴 Critical

Potential input event loss when advancing sequence after disconnect.

disconnectStream() aborts tailing asynchronously, so new tail events can still advance seq before shutdown. Then setLastSeqNum(prev + 1) at Line 806 can skip unseen events on reconnect.

🛠️ Safer local mitigation
-              inputStreams.disconnectStream(opts.id);
+              const seqBeforeDisconnect = inputStreams.lastSeqNum(opts.id);
+              inputStreams.disconnectStream(opts.id);

               // 3. Suspend the task
               const waitResult = await runtime.waitUntil(response.waitpointId);

@@
-                const prevSeq = inputStreams.lastSeqNum(opts.id);
-                inputStreams.setLastSeqNum(opts.id, (prevSeq ?? -1) + 1);
+                const seqAfterWait = inputStreams.lastSeqNum(opts.id);
+                if (seqAfterWait === seqBeforeDisconnect) {
+                  inputStreams.setLastSeqNum(opts.id, (seqBeforeDisconnect ?? -1) + 1);
+                }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/trigger-sdk/src/v3/streams.ts` around lines 780 - 807,
disconnectStream aborts tailing asynchronously so advancing seq using the stale
prevSeq can skip events that arrived during shutdown; modify the logic around
inputStreams.disconnectStream(opts.id) + inputStreams.setLastSeqNum(...) to
first await/ensure the stream is fully stopped (e.g., await the disconnect
promise or wait for a closed/aborted signal for opts.id), then read the latest
sequence via inputStreams.lastSeqNum(opts.id) and setLastSeqNum to (latestSeq ??
-1) + 1 (or otherwise max(prev+1, latest+1)) so you never advance past events
that arrived during the asynchronous disconnect; reference disconnectStream,
waitUntil, inputStreams.lastSeqNum, inputStreams.setLastSeqNum and opts.id when
locating the change.
🧹 Nitpick comments (3)
packages/trigger-sdk/src/v3/ai.ts (1)

718-731: Consider logging a warning when token creation fails.

If token creation fails, turnAccessToken remains an empty string and is passed to lifecycle hooks and the turn-complete chunk. The frontend may fail to reconnect. Consider logging a warning to aid debugging:

               } catch {
-                // Token creation failed
+                console.warn(`[chatTask] Failed to create access token for run ${currentRunId}`);
               }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/trigger-sdk/src/v3/ai.ts` around lines 718 - 731, The token creation
catch block swallows errors, leaving turnAccessToken empty and making frontend
reconnection debugging hard; update the catch in the currentRunId branch around
auth.createPublicToken (using chatAccessTokenTTL) to log a warning via the
existing logger (or processLogger) that includes the caught error and context
(e.g., currentRunId and that turnAccessToken will be empty) so lifecycle hooks
and the turn-complete chunk consumers can be diagnosed easily.
docs/guides/ai-chat.mdx (1)

218-231: Manual mode example missing message accumulation caveat.

The manual mode example uses a regular task() with ChatTaskPayload, but this won't get automatic message accumulation across turns. The warning at lines 233-235 mentions this, but the example itself doesn't show how to handle multi-turn conversations manually.

Consider adding a note that manual mode is single-turn only, or show how to implement accumulation manually:

 export const manualChat = task({
   id: "manual-chat",
   retry: { maxAttempts: 3 },
   queue: { concurrencyLimit: 10 },
-  run: async (payload: ChatTaskPayload) => {
+  // Note: This is single-turn only. For multi-turn, use chat.task()
+  run: async (payload: ChatTaskPayload) => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/guides/ai-chat.mdx` around lines 218 - 231, The manualChat task example
uses task() with ChatTaskPayload and calls streamText(...) then await
chat.pipe(result) but doesn't show that this is single-turn only; update the
example or add a short note: either state that manual mode (manualChat) does not
auto-accumulate messages across turns, or modify the task to explicitly
accumulate prior turns by merging stored conversation history with
payload.messages before calling streamText (i.e., gather previousMessages +
payload.messages into the messages argument), then pipe the result as shown;
reference manualChat, ChatTaskPayload, streamText, and chat.pipe so readers can
locate and implement the accumulation if they want multi-turn behavior.
packages/core/src/v3/realtimeStreams/types.ts (1)

33-40: Prefer type aliases for the updated stream contracts.

Since these signatures were touched, please migrate these interfaces to type aliases to match repo conventions.

As per coding guidelines: **/*.{ts,tsx}: Use types over interfaces for TypeScript.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/v3/realtimeStreams/types.ts` around lines 33 - 40, Replace
the two exported interfaces with exported type aliases: change
RealtimeStreamInstance and StreamsWriter from interface declarations to type
aliases while preserving their shapes and member names (ensure
RealtimeStreamInstance still exposes wait(): Promise<StreamWriteResult> and a
getter stream: AsyncIterableStream<T>, and StreamsWriter still exposes wait():
Promise<StreamWriteResult>); keep the same generic parameter T and export names
so public API is unchanged and update any references if needed to the new type
aliases.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/guides/ai-chat.mdx`:
- Around line 804-817: The docs table for ChatTaskOptions is missing the
chatAccessTokenTTL entry; update the ChatTaskOptions table in
docs/guides/ai-chat.mdx to include a row for `chatAccessTokenTTL` (property on
the ChatTaskOptions type) with Type `string`, Default `"1h"`, and a short
Description like "TTL for generated chat access tokens" so it matches the
`chatAccessTokenTTL` field defined in the ChatTaskOptions type in ai.ts.

---

Outside diff comments:
In `@packages/trigger-sdk/src/v3/streams.ts`:
- Around line 780-807: disconnectStream aborts tailing asynchronously so
advancing seq using the stale prevSeq can skip events that arrived during
shutdown; modify the logic around inputStreams.disconnectStream(opts.id) +
inputStreams.setLastSeqNum(...) to first await/ensure the stream is fully
stopped (e.g., await the disconnect promise or wait for a closed/aborted signal
for opts.id), then read the latest sequence via inputStreams.lastSeqNum(opts.id)
and setLastSeqNum to (latestSeq ?? -1) + 1 (or otherwise max(prev+1, latest+1))
so you never advance past events that arrived during the asynchronous
disconnect; reference disconnectStream, waitUntil, inputStreams.lastSeqNum,
inputStreams.setLastSeqNum and opts.id when locating the change.

---

Nitpick comments:
In `@docs/guides/ai-chat.mdx`:
- Around line 218-231: The manualChat task example uses task() with
ChatTaskPayload and calls streamText(...) then await chat.pipe(result) but
doesn't show that this is single-turn only; update the example or add a short
note: either state that manual mode (manualChat) does not auto-accumulate
messages across turns, or modify the task to explicitly accumulate prior turns
by merging stored conversation history with payload.messages before calling
streamText (i.e., gather previousMessages + payload.messages into the messages
argument), then pipe the result as shown; reference manualChat, ChatTaskPayload,
streamText, and chat.pipe so readers can locate and implement the accumulation
if they want multi-turn behavior.

In `@packages/core/src/v3/realtimeStreams/types.ts`:
- Around line 33-40: Replace the two exported interfaces with exported type
aliases: change RealtimeStreamInstance and StreamsWriter from interface
declarations to type aliases while preserving their shapes and member names
(ensure RealtimeStreamInstance still exposes wait(): Promise<StreamWriteResult>
and a getter stream: AsyncIterableStream<T>, and StreamsWriter still exposes
wait(): Promise<StreamWriteResult>); keep the same generic parameter T and
export names so public API is unchanged and update any references if needed to
the new type aliases.

In `@packages/trigger-sdk/src/v3/ai.ts`:
- Around line 718-731: The token creation catch block swallows errors, leaving
turnAccessToken empty and making frontend reconnection debugging hard; update
the catch in the currentRunId branch around auth.createPublicToken (using
chatAccessTokenTTL) to log a warning via the existing logger (or processLogger)
that includes the caught error and context (e.g., currentRunId and that
turnAccessToken will be empty) so lifecycle hooks and the turn-complete chunk
consumers can be diagnosed easily.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: ebda7b1b-2998-4f72-a46e-4207104ed513

📥 Commits

Reviewing files that changed from the base of the PR and between 5ee5959 and cfe56ab.

⛔ Files ignored due to path filters (4)
  • references/ai-chat/src/app/actions.ts is excluded by !references/**
  • references/ai-chat/src/components/chat-app.tsx is excluded by !references/**
  • references/ai-chat/src/components/chat.tsx is excluded by !references/**
  • references/ai-chat/src/trigger/chat.ts is excluded by !references/**
📒 Files selected for processing (10)
  • docs/guides/ai-chat.mdx
  • packages/core/src/v3/realtimeStreams/manager.ts
  • packages/core/src/v3/realtimeStreams/noopManager.ts
  • packages/core/src/v3/realtimeStreams/streamInstance.ts
  • packages/core/src/v3/realtimeStreams/streamsWriterV1.ts
  • packages/core/src/v3/realtimeStreams/streamsWriterV2.ts
  • packages/core/src/v3/realtimeStreams/types.ts
  • packages/trigger-sdk/src/v3/ai.ts
  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/streams.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (29)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (3, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (6, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (8, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (2, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (4, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (1, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (5, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (7, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (2, 8)
  • GitHub Check: sdk-compat / Node.js 22.12 (ubuntu-latest)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (4, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (5, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (8, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (1, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (7, 8)
  • GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - npm)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (6, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (3, 8)
  • GitHub Check: units / packages / 🧪 Unit Tests: Packages (1, 1)
  • GitHub Check: sdk-compat / Cloudflare Workers
  • GitHub Check: sdk-compat / Deno Runtime
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - pnpm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - pnpm)
  • GitHub Check: typecheck / typecheck
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - npm)
  • GitHub Check: sdk-compat / Node.js 20.20 (ubuntu-latest)
  • GitHub Check: sdk-compat / Bun Runtime
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (10)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Use types over interfaces for TypeScript
Avoid using enums; prefer string unions or const objects instead

**/*.{ts,tsx}: In TypeScript SDK usage, always import from @trigger.dev/sdk, never from @trigger.dev/sdk/v3 or use deprecated client.defineJob
Import from @trigger.dev/core subpaths only, never from the root
Use the Run Engine 2.0 (@internal/run-engine) and redis-worker for all new work, not legacy V1 MarQS queue or deprecated V1 functions

Files:

  • packages/core/src/v3/realtimeStreams/manager.ts
  • packages/trigger-sdk/src/v3/chat.ts
  • packages/core/src/v3/realtimeStreams/streamInstance.ts
  • packages/core/src/v3/realtimeStreams/noopManager.ts
  • packages/core/src/v3/realtimeStreams/types.ts
  • packages/core/src/v3/realtimeStreams/streamsWriterV2.ts
  • packages/trigger-sdk/src/v3/streams.ts
  • packages/core/src/v3/realtimeStreams/streamsWriterV1.ts
  • packages/trigger-sdk/src/v3/ai.ts
{packages/core,apps/webapp}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use zod for validation in packages/core and apps/webapp

Files:

  • packages/core/src/v3/realtimeStreams/manager.ts
  • packages/core/src/v3/realtimeStreams/streamInstance.ts
  • packages/core/src/v3/realtimeStreams/noopManager.ts
  • packages/core/src/v3/realtimeStreams/types.ts
  • packages/core/src/v3/realtimeStreams/streamsWriterV2.ts
  • packages/core/src/v3/realtimeStreams/streamsWriterV1.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use function declarations instead of default exports

Files:

  • packages/core/src/v3/realtimeStreams/manager.ts
  • packages/trigger-sdk/src/v3/chat.ts
  • packages/core/src/v3/realtimeStreams/streamInstance.ts
  • packages/core/src/v3/realtimeStreams/noopManager.ts
  • packages/core/src/v3/realtimeStreams/types.ts
  • packages/core/src/v3/realtimeStreams/streamsWriterV2.ts
  • packages/trigger-sdk/src/v3/streams.ts
  • packages/core/src/v3/realtimeStreams/streamsWriterV1.ts
  • packages/trigger-sdk/src/v3/ai.ts
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/otel-metrics.mdc)

**/*.ts: When creating or editing OTEL metrics (counters, histograms, gauges), ensure metric attributes have low cardinality by using only enums, booleans, bounded error codes, or bounded shard IDs
Do not use high-cardinality attributes in OTEL metrics such as UUIDs/IDs (envId, userId, runId, projectId, organizationId), unbounded integers (itemCount, batchSize, retryCount), timestamps (createdAt, startTime), or free-form strings (errorMessage, taskName, queueName)
When exporting OTEL metrics via OTLP to Prometheus, be aware that the exporter automatically adds unit suffixes to metric names (e.g., 'my_duration_ms' becomes 'my_duration_ms_milliseconds', 'my_counter' becomes 'my_counter_total'). Account for these transformations when writing Grafana dashboards or Prometheus queries

Files:

  • packages/core/src/v3/realtimeStreams/manager.ts
  • packages/trigger-sdk/src/v3/chat.ts
  • packages/core/src/v3/realtimeStreams/streamInstance.ts
  • packages/core/src/v3/realtimeStreams/noopManager.ts
  • packages/core/src/v3/realtimeStreams/types.ts
  • packages/core/src/v3/realtimeStreams/streamsWriterV2.ts
  • packages/trigger-sdk/src/v3/streams.ts
  • packages/core/src/v3/realtimeStreams/streamsWriterV1.ts
  • packages/trigger-sdk/src/v3/ai.ts
**/*.{js,ts,jsx,tsx,json,md,yaml,yml}

📄 CodeRabbit inference engine (AGENTS.md)

Format code using Prettier before committing

Files:

  • packages/core/src/v3/realtimeStreams/manager.ts
  • packages/trigger-sdk/src/v3/chat.ts
  • packages/core/src/v3/realtimeStreams/streamInstance.ts
  • packages/core/src/v3/realtimeStreams/noopManager.ts
  • packages/core/src/v3/realtimeStreams/types.ts
  • packages/core/src/v3/realtimeStreams/streamsWriterV2.ts
  • packages/trigger-sdk/src/v3/streams.ts
  • packages/core/src/v3/realtimeStreams/streamsWriterV1.ts
  • packages/trigger-sdk/src/v3/ai.ts
packages/core/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (packages/core/CLAUDE.md)

Never import the root package (@trigger.dev/core). Always use subpath imports such as @trigger.dev/core/v3, @trigger.dev/core/v3/utils, @trigger.dev/core/logger, or @trigger.dev/core/schemas

Files:

  • packages/core/src/v3/realtimeStreams/manager.ts
  • packages/core/src/v3/realtimeStreams/streamInstance.ts
  • packages/core/src/v3/realtimeStreams/noopManager.ts
  • packages/core/src/v3/realtimeStreams/types.ts
  • packages/core/src/v3/realtimeStreams/streamsWriterV2.ts
  • packages/core/src/v3/realtimeStreams/streamsWriterV1.ts
docs/**/*.{md,mdx}

📄 CodeRabbit inference engine (CLAUDE.md)

Docs in docs/ directory should use Mintlify MDX format following conventions in docs/CLAUDE.md

Files:

  • docs/guides/ai-chat.mdx
docs/**/*.mdx

📄 CodeRabbit inference engine (docs/CLAUDE.md)

docs/**/*.mdx: MDX documentation pages must include frontmatter with title (required), description (required), and sidebarTitle (optional) in YAML format
Use Mintlify components for structured content: , , , , , , /, /
Always import from @trigger.dev/sdk in code examples (never from @trigger.dev/sdk/v3)
Code examples must be complete and runnable where possible
Use language tags in code fences: typescript, bash, json

Files:

  • docs/guides/ai-chat.mdx
packages/trigger-sdk/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

In the Trigger.dev SDK (packages/trigger-sdk), prefer isomorphic code like fetch and ReadableStream instead of Node.js-specific code

Files:

  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/streams.ts
  • packages/trigger-sdk/src/v3/ai.ts
packages/trigger-sdk/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (packages/trigger-sdk/CLAUDE.md)

Always import from @trigger.dev/sdk. Never use @trigger.dev/sdk/v3 (deprecated path alias)

Files:

  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/streams.ts
  • packages/trigger-sdk/src/v3/ai.ts
🧠 Learnings (19)
📓 Common learnings
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-27T16:26:37.432Z
Learning: The SDK at packages/trigger-sdk is an isomorphic TypeScript SDK
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-27T16:26:37.432Z
Learning: Applies to packages/trigger-sdk/**/*.{ts,tsx} : In the Trigger.dev SDK (packages/trigger-sdk), prefer isomorphic code like fetch and ReadableStream instead of Node.js-specific code
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use `trigger.dev/sdk/v3` for all imports in Trigger.dev tasks
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use `.withStreams()` to subscribe to realtime streams from task metadata in addition to run changes
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: packages/trigger-sdk/CLAUDE.md:0-0
Timestamp: 2026-03-02T12:43:48.124Z
Learning: Applies to packages/trigger-sdk/**/*.{ts,tsx,js,jsx} : Always import from `trigger.dev/sdk`. Never use `trigger.dev/sdk/v3` (deprecated path alias)
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Use `trigger.dev/react-hooks` package for realtime subscriptions in React components
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use `.withStreams()` to subscribe to realtime streams from task metadata in addition to run changes

Applied to files:

  • packages/core/src/v3/realtimeStreams/manager.ts
  • docs/guides/ai-chat.mdx
  • packages/trigger-sdk/src/v3/chat.ts
  • packages/core/src/v3/realtimeStreams/types.ts
  • packages/core/src/v3/realtimeStreams/streamsWriterV2.ts
  • packages/trigger-sdk/src/v3/streams.ts
  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2026-03-02T12:43:34.140Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: packages/cli-v3/CLAUDE.md:0-0
Timestamp: 2026-03-02T12:43:34.140Z
Learning: Keep SDK documentation in `rules/` and `.claude/skills/trigger-dev-tasks/` synchronized when features are added or changed

Applied to files:

  • docs/guides/ai-chat.mdx
📚 Learning: 2026-03-02T12:43:37.906Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: packages/core/CLAUDE.md:0-0
Timestamp: 2026-03-02T12:43:37.906Z
Learning: Exercise caution with changes to trigger.dev/core as they affect both the customer-facing SDK and server-side webapp - breaking changes can impact deployed user tasks and the platform simultaneously

Applied to files:

  • docs/guides/ai-chat.mdx
📚 Learning: 2026-03-02T12:43:48.124Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: packages/trigger-sdk/CLAUDE.md:0-0
Timestamp: 2026-03-02T12:43:48.124Z
Learning: Applies to packages/trigger-sdk/**/*.{ts,tsx,js,jsx} : Always import from `trigger.dev/sdk`. Never use `trigger.dev/sdk/v3` (deprecated path alias)

Applied to files:

  • docs/guides/ai-chat.mdx
  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2026-03-02T12:43:02.539Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: docs/CLAUDE.md:0-0
Timestamp: 2026-03-02T12:43:02.539Z
Learning: Applies to docs/**/*.mdx : Always import from `trigger.dev/sdk` in code examples (never from `trigger.dev/sdk/v3`)

Applied to files:

  • docs/guides/ai-chat.mdx
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use the `task()` function from `trigger.dev/sdk/v3` to define tasks with id and run properties

Applied to files:

  • docs/guides/ai-chat.mdx
  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use `yourTask.trigger()` to trigger a task from inside another task with specified payload

Applied to files:

  • docs/guides/ai-chat.mdx
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use `trigger.dev/sdk/v3` for all imports in Trigger.dev tasks

Applied to files:

  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2025-11-27T16:26:37.432Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-27T16:26:37.432Z
Learning: The SDK at packages/trigger-sdk is an isomorphic TypeScript SDK

Applied to files:

  • packages/trigger-sdk/src/v3/chat.ts
📚 Learning: 2025-11-27T16:26:37.432Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-27T16:26:37.432Z
Learning: Applies to packages/trigger-sdk/**/*.{ts,tsx} : In the Trigger.dev SDK (packages/trigger-sdk), prefer isomorphic code like fetch and ReadableStream instead of Node.js-specific code

Applied to files:

  • packages/trigger-sdk/src/v3/chat.ts
  • packages/trigger-sdk/src/v3/streams.ts
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use `schemaTask()` from `trigger.dev/sdk/v3` with Zod schema for payload validation

Applied to files:

  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2026-03-02T12:42:41.110Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-02T12:42:41.110Z
Learning: Applies to **/*.{ts,tsx} : In TypeScript SDK usage, always import from trigger.dev/sdk, never from trigger.dev/sdk/v3 or use deprecated client.defineJob

Applied to files:

  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2026-03-03T13:08:03.862Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3166
File: packages/redis-worker/src/fair-queue/index.ts:1114-1121
Timestamp: 2026-03-03T13:08:03.862Z
Learning: In packages/redis-worker/src/fair-queue/index.ts, it's acceptable for the worker queue depth cap check to allow overshooting by up to batchClaimSize messages per iteration, as the next iteration will recheck and prevent sustained growth beyond the limit.

Applied to files:

  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Attach metadata to task runs using the metadata option when triggering, and access/update it inside runs using metadata functions

Applied to files:

  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use metadata methods (set, del, replace, append, remove, increment, decrement, stream, flush) to update metadata during task execution

Applied to files:

  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Export tasks with unique IDs within the project to enable proper task discovery and execution

Applied to files:

  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use logger methods (debug, log, info, warn, error) from `trigger.dev/sdk/v3` for structured logging in tasks

Applied to files:

  • packages/trigger-sdk/src/v3/ai.ts
📚 Learning: 2025-11-27T16:27:35.304Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2025-11-27T16:27:35.304Z
Learning: Applies to **/trigger/**/*.{ts,tsx,js,jsx} : Use `tasks.trigger()` with type-only imports to trigger tasks from backend code without importing the task implementation

Applied to files:

  • packages/trigger-sdk/src/v3/ai.ts
🔇 Additional comments (19)
packages/trigger-sdk/src/v3/chat.ts (4)

278-279: Pass headers option to sendInputStream API call.

The extraHeaders stored on the transport are used for SSE subscriptions (Line 415) but not passed to the sendInputStream call here. This breaks proxy/tenant routing or custom auth flows that depend on these headers.

Proposed fix
         const apiClient = new ApiClient(this.baseURL, session.publicAccessToken);
-        await apiClient.sendInputStream(session.runId, CHAT_MESSAGES_STREAM_ID, minimalPayload);
+        await apiClient.sendInputStream(session.runId, CHAT_MESSAGES_STREAM_ID, minimalPayload, {
+          headers: this.extraHeaders,
+        });

297-302: Pass headers option to triggerTask API call.

Same issue as sendInputStream — the extraHeaders are not forwarded to the triggerTask call, breaking custom auth or proxy routing.

Proposed fix
-    const triggerResponse = await apiClient.triggerTask(this.taskId, {
-      payload,
-      options: {
-        payloadType: "application/json",
-      },
-    });
+    const triggerResponse = await apiClient.triggerTask(
+      this.taskId,
+      {
+        payload,
+        options: {
+          payloadType: "application/json",
+        },
+      },
+      undefined,
+      { headers: this.extraHeaders }
+    );

438-441: Pass headers option to stop signal sendInputStream call.

The stop signal path also omits extraHeaders.

Proposed fix
             const api = new ApiClient(this.baseURL, session.publicAccessToken);
             api
-              .sendInputStream(session.runId, CHAT_STOP_STREAM_ID, { stop: true })
+              .sendInputStream(session.runId, CHAT_STOP_STREAM_ID, { stop: true }, {
+                headers: this.extraHeaders,
+              })
               .catch(() => {}); // Best-effort

459-550: LGTM — Stream subscription and chunk processing logic.

The SSE stream subscription correctly:

  • Handles __trigger_turn_complete control chunks and token refresh
  • Tracks lastEventId for stream resumption
  • Manages abort signals and graceful shutdown
  • Skips leftover chunks after stop via skipToTurnComplete
packages/trigger-sdk/src/v3/ai.ts (6)

247-248: Global _chatPipeCount is race-prone with concurrent chat runs.

_chatPipeCount is module-global, so concurrent chat runs can mutate the same counter and incorrectly suppress or trigger auto-piping in other runs.

Suggested fix — scope counter per run
-let _chatPipeCount = 0;
+const _chatPipeCountByRun = new Map<string, number>();

 async function pipeChat(
   source: UIMessageStreamable | AsyncIterable<unknown> | ReadableStream<unknown>,
   options?: PipeChatOptions
 ): Promise<void> {
-  _chatPipeCount++;
+  const runId = taskContext.ctx?.run.id;
+  if (runId) {
+    _chatPipeCountByRun.set(runId, (_chatPipeCountByRun.get(runId) ?? 0) + 1);
+  }
   // ...
 }

Then in the chatTask turn loop, reset and check the run-scoped counter instead of the global.


453-552: chatTask does not support custom streamKey end-to-end.

TriggerChatTransport allows configuring a custom streamKey, but chatTask hardcodes CHAT_STREAM_KEY for auto-piping (Line 801) and the turn-complete control chunk (Line 1047). If a user configures a non-default stream key on the frontend, the turn completion will hang because the frontend subscribes to a different stream than where the backend writes.

Suggested fix — add `streamKey` option to `chatTask`

Add a streamKey option to ChatTaskOptions and thread it through to pipeChat and writeTurnCompleteChunk:

 export type ChatTaskOptions<TIdentifier extends string> = Omit<...> & {
+  /** Stream key for output. Must match TriggerChatTransport.streamKey. `@default` "chat" */
+  streamKey?: string;
   run: (payload: ChatTaskRunPayload) => Promise<unknown>;
   // ...
 };

 function chatTask<TIdentifier extends string>(...) {
   const {
     run: userRun,
+    streamKey = CHAT_STREAM_KEY,
     // ...
   } = options;

   // In auto-pipe:
-  await pipeChat(uiStream, { signal: combinedSignal, spanName: "stream response" });
+  await pipeChat(uiStream, { streamKey, signal: combinedSignal, spanName: "stream response" });

   // In turn complete:
-  const turnCompleteResult = await writeTurnCompleteChunk(...);
+  const turnCompleteResult = await writeTurnCompleteChunk(streamKey, ...);
 }

-async function writeTurnCompleteChunk(chatId?: string, publicAccessToken?: string) {
-  const { waitUntilComplete } = streams.writer(CHAT_STREAM_KEY, {
+async function writeTurnCompleteChunk(streamKey: string = CHAT_STREAM_KEY, chatId?: string, publicAccessToken?: string) {
+  const { waitUntilComplete } = streams.writer(streamKey, {

880-884: Verify handling of multiple buffered messages.

When messages arrive during streaming, they're buffered in pendingMessages. However, only the first message is used (Line 882) — any additional messages are discarded. If multiple messages arrive during a streaming turn, subsequent ones will be lost.

Consider preserving the backlog:

+      const queuedMessages: ChatTaskWirePayload[] = [];
+
       // ... inside turn loop after processing ...
       if (pendingMessages.length > 0) {
-        currentWirePayload = pendingMessages[0]!;
+        currentWirePayload = pendingMessages.shift()!;
+        queuedMessages.push(...pendingMessages);
         return "continue";
       }
+
+      // Check queued messages before waiting
+      if (queuedMessages.length > 0) {
+        currentWirePayload = queuedMessages.shift()!;
+        return "continue";
+      }

The past review indicated this was addressed. Please verify the intended behavior — is dropping extra messages intentional (e.g., expecting only one message per turn)?


734-777: LGTM — Lifecycle hooks implementation.

The onChatStart and onTurnStart hooks are correctly sequenced and wrapped in traced spans. The scoped access token is minted per-turn and passed to callbacks for persistence.


691-713: LGTM — Message accumulation logic.

The accumulation correctly handles:

  • Turn 0: Full history from frontend initializes the accumulator
  • Regenerate: Full history resets the accumulator (removing last assistant message)
  • Submit (turn 1+): Only new messages appended to existing accumulator

This aligns with the frontend transport sending minimal payloads after the first turn.


1096-1115: LGTM — stripProviderMetadata helper.

This correctly strips ephemeral OpenAI itemId fields that would cause 404 errors when sent back in subsequent streamText calls. The implementation preserves other provider metadata while removing only the problematic fields.

docs/guides/ai-chat.mdx (2)

20-22: Verify the ai package version requirement.

The documentation states the ai package v5.0.0 or later is required. A past review indicated the reference implementation uses v6.0.0. Please verify and update the version requirement if needed.

#!/bin/bash
# Check ai package version in reference implementations and package.json files
rg -n '"ai"\s*:' --glob '**/package.json' | head -20

344-406: LGTM — Persistence documentation.

The persistence section clearly explains:

  • What needs to be persisted (messages + sessions)
  • Server-side persistence via onTurnStart (before streaming) and onTurnComplete (after)
  • The purpose of lastEventId for stream resumption
  • Complete code examples with database operations
packages/core/src/v3/realtimeStreams/types.ts (1)

29-31: StreamWriteResult is a clean API extension.

This shape keeps backward compatibility while enabling resume metadata.

packages/core/src/v3/realtimeStreams/manager.ts (1)

19-21: Type propagation for active stream waits is consistent.

Good alignment with the new wait(): Promise<StreamWriteResult> contract.

packages/core/src/v3/realtimeStreams/noopManager.ts (1)

18-18: Noop manager now matches the wait contract.

Returning an object here correctly aligns with StreamWriteResult.

packages/core/src/v3/realtimeStreams/streamsWriterV1.ts (1)

261-264: wait() return type update is correctly wired.

This keeps V1 aligned with the shared writer contract.

packages/core/src/v3/realtimeStreams/streamInstance.ts (1)

66-69: StreamInstance.wait() now correctly preserves writer output.

Returning writer.wait() is the right propagation point for StreamWriteResult.

packages/core/src/v3/realtimeStreams/streamsWriterV2.ts (1)

173-176: Capturing lastSeqNum and exposing it from wait() is a solid improvement.

This makes V2 waits materially more useful for resume flows.

Also applies to: 188-191

packages/trigger-sdk/src/v3/streams.ts (1)

143-201: Span customization and waitpoint entity attribution are well integrated.

spanName/collapsed wiring and waitpointId attribution improve trace clarity.

Also applies to: 757-775, 822-823

Comment thread docs/guides/ai-chat.mdx Outdated
Comment on lines +804 to +817
### ChatTaskOptions

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `id` | `string` | required | Task identifier |
| `run` | `(payload: ChatTaskRunPayload) => Promise<unknown>` | required | Handler for each turn |
| `onChatStart` | `(event: ChatStartEvent) => Promise<void> \| void` | — | Fires on turn 0 before `run()` |
| `onTurnStart` | `(event: TurnStartEvent) => Promise<void> \| void` | — | Fires every turn before `run()` |
| `onTurnComplete` | `(event: TurnCompleteEvent) => Promise<void> \| void` | — | Fires after each turn completes |
| `maxTurns` | `number` | `100` | Max conversational turns per run |
| `turnTimeout` | `string` | `"1h"` | How long to wait for next message |
| `warmTimeoutInSeconds` | `number` | `30` | Seconds to stay warm before suspending |

Plus all standard [TaskOptions](/tasks/overview) — `retry`, `queue`, `machine`, `maxDuration`, etc.
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.

⚠️ Potential issue | 🟡 Minor

Add chatAccessTokenTTL to ChatTaskOptions table.

The ChatTaskOptions type in ai.ts (lines 542-552) includes chatAccessTokenTTL with default "1h", but it's missing from the documentation table.

 | `turnTimeout` | `string` | `"1h"` | How long to wait for next message |
 | `warmTimeoutInSeconds` | `number` | `30` | Seconds to stay warm before suspending |
+| `chatAccessTokenTTL` | `string` | `"1h"` | How long the scoped access token remains valid |
📝 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
### ChatTaskOptions
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `id` | `string` | required | Task identifier |
| `run` | `(payload: ChatTaskRunPayload) => Promise<unknown>` | required | Handler for each turn |
| `onChatStart` | `(event: ChatStartEvent) => Promise<void> \| void` || Fires on turn 0 before `run()` |
| `onTurnStart` | `(event: TurnStartEvent) => Promise<void> \| void` || Fires every turn before `run()` |
| `onTurnComplete` | `(event: TurnCompleteEvent) => Promise<void> \| void` || Fires after each turn completes |
| `maxTurns` | `number` | `100` | Max conversational turns per run |
| `turnTimeout` | `string` | `"1h"` | How long to wait for next message |
| `warmTimeoutInSeconds` | `number` | `30` | Seconds to stay warm before suspending |
Plus all standard [TaskOptions](/tasks/overview)`retry`, `queue`, `machine`, `maxDuration`, etc.
### ChatTaskOptions
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `id` | `string` | required | Task identifier |
| `run` | `(payload: ChatTaskRunPayload) => Promise<unknown>` | required | Handler for each turn |
| `onChatStart` | `(event: ChatStartEvent) => Promise<void> \| void` || Fires on turn 0 before `run()` |
| `onTurnStart` | `(event: TurnStartEvent) => Promise<void> \| void` || Fires every turn before `run()` |
| `onTurnComplete` | `(event: TurnCompleteEvent) => Promise<void> \| void` || Fires after each turn completes |
| `maxTurns` | `number` | `100` | Max conversational turns per run |
| `turnTimeout` | `string` | `"1h"` | How long to wait for next message |
| `warmTimeoutInSeconds` | `number` | `30` | Seconds to stay warm before suspending |
| `chatAccessTokenTTL` | `string` | `"1h"` | How long the scoped access token remains valid |
Plus all standard [TaskOptions](/tasks/overview)`retry`, `queue`, `machine`, `maxDuration`, etc.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/guides/ai-chat.mdx` around lines 804 - 817, The docs table for
ChatTaskOptions is missing the chatAccessTokenTTL entry; update the
ChatTaskOptions table in docs/guides/ai-chat.mdx to include a row for
`chatAccessTokenTTL` (property on the ChatTaskOptions type) with Type `string`,
Default `"1h"`, and a short Description like "TTL for generated chat access
tokens" so it matches the `chatAccessTokenTTL` field defined in the
ChatTaskOptions type in ai.ts.

Comment thread references/ai-chat/src/trigger/chat.ts Fixed
Comment thread references/ai-chat/src/trigger/chat.ts Fixed
Comment thread references/ai-chat/src/trigger/chat.ts Fixed
Comment thread references/ai-chat/src/trigger/chat.ts Fixed
Comment thread references/ai-chat/src/trigger/chat.ts Fixed
Comment thread references/ai-chat/src/trigger/chat.ts Fixed
Comment thread references/ai-chat/src/trigger/chat.ts Fixed
Comment thread references/ai-chat/src/trigger/chat.ts Fixed
In-memory managers for locals, lifecycle hooks, runtime, input streams,
and realtime streams, plus a mock TaskContext. Lets task code be driven
end-to-end without hitting the Trigger.dev runtime — send data into
input streams, inspect chunks written to output streams, and pre-seed
locals for dependency injection.
Drives a chat.agent definition through real turns offline — send
messages, actions, and stop signals; inspect captured chunks; assert
on hook order. Pre-seed dependencies via setupLocals so hooks read
test instances (DB clients, stubs) via locals.get() instead of
leaking through untrusted clientData.

Adds ai-chat reference tests exercising the harness across basic
flow, onValidateMessages, hydrateMessages, and actions.
Surface the AI SDK's FinishReason on TurnCompleteEvent and
BeforeTurnCompleteEvent. Gives hooks a clean signal for distinguishing
a normal turn end from one paused on a pending tool call (HITL flows
like ask_user). Undefined for manual pipeChat() or aborted streams.
Exit the loop after the current turn completes without the
upgrade-required signal that chat.requestUpgrade() sends. Use when an
agent finishes its work on its own terms — one-shot responses, goal
achieved, budget exhausted — instead of waiting idle for the next
user message. Callable from run(), chat.defer(),
onBeforeTurnComplete, or onTurnComplete.

Resolves TRI-8391.
run() is invoked with trigger: "action" after onAction processes a
typed action, but the type previously omitted it. Adding it lets
users cleanly short-circuit the LLM call for actions that don't
need a response (e.g. user-initiated compaction):
  if (trigger === "action") return;
SkillMetadata + SkillManifest zod schemas alongside the existing
task/prompt ones. registerSkillMetadata / listSkillManifests /
getSkillManifest on ResourceCatalog (both Standard and Noop), wired
through the ResourceCatalogAPI facade. BuildManifest + WorkerManifest
gain an optional `skills` array so the built-in CLI bundler can
annotate discoveries and the indexer can report them.

Part 1/3 of Phase 1 for the new ai.skills primitive.
skills.define({ id, path }) registers a skill with the resource
catalog and returns a SkillHandle. SkillHandle.local() reads the
bundled SKILL.md from ./.trigger/skills/{id}/ at runtime, parses
frontmatter, and returns a ResolvedSkill ready for chat.skills.set().

chat.skills.set([...]) stores resolved skills for the current run.
chat.toStreamTextOptions() auto-injects the skills preamble into the
system prompt and merges three tools — loadSkill, readFile, bash —
scoped per-skill with path-traversal guards and output caps (64 KB
stdout/stderr, 1 MB readFile). Bash executes in the worker container
with the turn's abort signal, no sandbox — skills are developer code.

Shared packages/build/src/internal/copyFiles.ts extracted from the
additionalFiles extension so the CLI's built-in skill bundler and the
existing extension share one glob + copy implementation.

Part 2/3 of Phase 1 for the new ai.skills primitive.
New first-class bundling step (not a build extension): after esbuild
produces the worker bundle, fork the indexer locally to discover
skills registered via ai.skills.define(), validate each skill's
SKILL.md, and copy the folder into {outputPath}/.trigger/skills/{id}/.

Hooks into both buildWorker() (deploy) and devSession's updateBundle
(dev) right after createBuildManifestFromBundle and before the
extension onBuildComplete hook, so extensions can observe the
annotated manifest.skills. The existing Dockerfile COPY picks up the
new .trigger/skills/ subdirectory without changes.

Also: managed-index-worker and dev-index-worker now emit
resourceCatalog.listSkillManifests() in the INDEX_COMPLETE message
so downstream stages can see the skill list.

Part 3/3 of Phase 1 for the new ai.skills primitive.
Full Phase 1 for the new ai.skills primitive — SDK + CLI, no
backend. Developer-authored folders (SKILL.md + scripts/references/
assets) discovered at build time, bundled into the deploy image at
/app/.trigger/skills/{id}/, and auto-wired into streamText at
runtime via the loadSkill/readFile/bash tools.

Adds a time-utils example skill to the ai-chat reference: two bash
scripts (now.sh, add.sh) plus a timezones.txt cheat-sheet, wired
into the aiChat agent via chat.skills.set() in onChatStart and
onPreload. Exercises the full pipeline end-to-end.

Changeset: patch bump for @trigger.dev/sdk, @trigger.dev/core,
@trigger.dev/build, trigger.dev.
… layout

Two correctness fixes caught during end-to-end validation:

1. Resolve skill.path relative to the file that called skills.define(),
   not the project root. The resource catalog already captures filePath
   on each SkillManifest — use it to compute the source folder.

2. In dev, copy skill bundles to {workingDir}/.trigger/skills/ (where
   the worker's cwd resolves). In deploy, keep copying to
   {outputPath}/.trigger/skills/ so the Dockerfile COPY . /app lands
   them at /app/.trigger/skills/.

Also upgrade the skill-discovery failure log from debug to warn so
config mistakes surface in the dev console instead of disappearing
silently.

Finally, update the ai-chat reference's aiChat.run() to pass chatTools
through chat.toStreamTextOptions({ tools: chatTools }) instead of
spreading them after — the auto-injected loadSkill/readFile/bash tools
would otherwise get overwritten by the explicit tools: chatTools key.
Client-side pair to the Session primitive server PR (TRI-8627).
Run-scoped streams.pipe / streams.input are untouched.

@trigger.dev/core ApiClient
- createSession / retrieveSession / updateSession / closeSession —
  zodfetch against /api/v1/sessions control plane
- listSessions — CursorPagePromise<SessionItem>, follows the runs/waitpoints
  convention (page[size], page[after], page[before] + filter[*])
- initializeSessionStream — PUT /realtime/v1/sessions/:session/:io,
  returns S2 creds in headers (feeds StreamsWriterV2 directly)
- appendToSessionStream — POST …/append
- subscribeToSessionStream — reuses SSEStreamSubscription for SSE
  subscribes (auto-retry, Last-Event-ID resume, abort propagation), so
  session subscribers get the exact same semantics as runs.fetchStream.
  Returns AsyncIterableStream<T>.

@trigger.dev/sdk sessions namespace
- sessions.create / retrieve / update / close / list — wraps the ApiClient
  with the standard tracer + accessoryAttributes + mergeRequestOptions.
  Returns ApiPromise / CursorPagePromise.
- sessions.open(id) returns a SessionHandle with .out and .in
  SessionChannels. Each channel exposes append / send / subscribe /
  initialize. The handle is polymorphic on friendlyId or externalId.
- auth.ts adds the `sessions` permission on PublicTokenPermissionProperties
  so auth.createPublicToken({ read: { sessions: ["session_abc"] } }) works.

Reference
- references/hello-world/src/trigger/sessionsSmoke.ts — idempotent
  Trigger.dev task that exercises every code path (control-plane CRUD,
  polymorphic lookup, list with tag/type/status/externalId filters, cursor
  pagination, out.initialize + append + subscribe SSE round-trip, in.send,
  close + idempotent re-close). Trigger via
  mcp__trigger__trigger_task(taskId: "sessions-smoke").

Verified live against the local webapp (project hello-world): 10/10
steps pass end-to-end, S2 round-trip returns appended chunks through the
shared SSEStreamSubscription pipeline.
Build the client-side half of the Session channel extensions that the
sessions PR shipped on the server. Pairs with POST
/api/v1/runs/:runFriendlyId/session-streams/wait and the
append-fires-waitpoints wiring on the session append handler.

Extend SessionHandle with two asymmetric channels mirroring the
run-scoped streams primitives:

- .in (SessionInputChannel) mirrors streams.input. on / once / peek /
  wait / waitWithIdleTimeout for the task to consume; send for
  external clients to produce. .wait / .waitWithIdleTimeout suspend
  the run on a session-stream waitpoint; it resumes when a record
  lands on .in, same semantics as streams.input.wait on a run-scoped
  input stream.
- .out (SessionOutputChannel) mirrors streams.define. append / pipe /
  writer for the task to produce records — all three route through
  SessionStreamInstance -> StreamsWriterV2 for uniform parsed-object
  serialization on the subscribe side. read returns an SSE subscription
  for external consumers.

The two channels are disjoint classes with zero overlapping methods.
SessionHandle is { id, in, out } so directional tags stay at every
call site. No public initialize() — S2 credentials are an internal
detail of pipe / writer.

Core
- StandardSessionStreamManager + sessionStreams global: SSE-backed
  tail with once/on/peek buffering, auto-reconnect, lastSeqNum
  resume. Keyed on {sessionId, io}. Registered in dev- and managed-
  run workers; taskExecutor clears handlers at run end alongside
  input streams.
- SessionStreamInstance: S2-only parallel of StreamInstance. Fetches
  session S2 creds via initializeSessionStream and pipes through
  StreamsWriterV2.
- ApiClient.createSessionStreamWaitpoint — calls the new server route.

Reference
- references/hello-world/src/trigger/sessionsSmoke.ts now exercises
  .out.writer alongside .out.append.
- references/hello-world/src/trigger/sessionsWaitSmoke.ts (new) —
  end-to-end waitpoint validation. Orchestrator suspends on
  .in.waitWithIdleTimeout; a delayed sender task fires the waitpoint
  via .in.send; orchestrator resumes with the payload. match: true.
Rewires chat.agent's internal I/O and TriggerChatTransport's send +
subscribe paths onto the Session primitive. Minimum token-scope work
included so the transport's session endpoints actually authenticate.

Phase B — chat.agent internals (ai.ts)

- New ChatInputChunk tagged union (`kind: "message" | "stop"`) —
  replaces the two-stream split (chat-messages + chat-stop) with a
  single Session `.in` channel.
- New chatSessionHandleKey locals slot populated at run start from
  `payload.sessionId ?? payload.chatId`. Every module-level helper
  now resolves to the per-run session handle.
- Module-level `chatStream`, `messagesInput`, `stopInput` become thin
  facades over the session. `chatStream` mirrors
  `RealtimeDefinedStream<UIMessageChunk>` and delegates to
  `handle.out`. `messagesInput` / `stopInput` mirror
  `RealtimeDefinedInputStream<…>` and filter `.in` by kind — the two
  internal `.on()`/`.waitWithIdleTimeout()` callers and the
  `chat.messages` / `chat.createStopSignal` public exposures keep
  their existing shapes.
- Every `streams.writer(CHAT_STREAM_KEY, …)` callsite swaps to
  `chatStream.writer(…)` so all chat output flows through
  `session.out` → `SessionStreamInstance` → direct-to-S2.
- Threaded `sessionId` through `ChatTaskWirePayload` /
  `ChatTaskPayload` / `ChatTaskRunPayload` so advanced users can
  `sessions.open(sessionId)` directly from `run()`.

Phase C — TriggerChatTransport (chat.ts)

- `ChatSessionState` keys durable identity on `sessionId` (friendlyId);
  `runId` becomes an optional hint about whether a run is live.
- `ensureSession(chatId)` lazily upserts the Session via
  `apiClient.createSession({type: "chat.agent", externalId: chatId})`
  on the direct `accessToken` path. Idempotent — two tabs on the
  same chat converge.
- `sendMessages`, `sendPendingMessage`, `stopGeneration`,
  `sendAction` all go through `appendToSessionStream(sessionId, "in",
  serializeInputChunk({kind: …}))` — one endpoint, one tag per
  record.
- SSE subscribe URL moves from `/realtime/v1/streams/{runId}/chat` to
  `/realtime/v1/sessions/{sessionId}/out`. The old run-scoped
  `subscribeToStream` is replaced by `subscribeToSessionStream`.
  Incoming chunks come back as JSON strings on the session channel
  (server wraps records as `{data, id}` on S2), so the subscribe
  loop parses them back into objects to keep the rest of the control
  flow (turn-complete / upgrade-required / skipToTurnComplete)
  unchanged.
- Upgrade-required re-trigger keeps the same Session and swaps only
  the runId + token.
- `getSession` / `setSession` / `setOnSessionChange` / persistence
  shape all grow a `sessionId` field (runId now optional).

Phase E — minimum token scopes

- `chat.createTriggerAction` (server side) now creates the Session
  before triggering so it can (a) thread `sessionId` into the run
  payload and (b) mint a token with both run and session scopes.
  Returns `sessionId` in its result so the transport can skip its
  own `sessions.create` call on the server-side-trigger path.
- `TriggerChatTaskResult` gains optional `sessionId`.
- The two in-run PAT refresh sites (preloadAccessToken,
  turnAccessToken) add `read:sessions:{sessionId}` +
  `write:sessions:{sessionId}` alongside the existing run scopes.

Known follow-ups (deferred to later passes)

- Phase D: `AgentChat` / `ChatStream` in chat-client.ts still uses
  the old `/realtime/v1/streams/{runId}/chat` path. Used by server-
  side task-to-task compositions, not the browser transport.
- Phase F: delete CHAT_STREAM_KEY, CHAT_MESSAGES_STREAM_ID,
  CHAT_STOP_STREAM_ID from chat-constants.ts + ai-chat smoke verify.
Rewires the server-side AgentChat class in chat-client.ts onto the
Session primitive, matching the browser transport's shape.

- ChatSession persistence and SessionState internal state now key on
  sessionId (friendlyId). runId is optional, just a 'live run' hint.
- triggerNewRun upserts the backing Session via sessions.create
  (idempotent on externalId = chatId) before triggering so sessionId
  rides along in payload.
- sendRaw, steer, sendAction, close, stop all go through
  appendToSessionStream(sessionId, 'in',
  serializeInputChunk({kind: …})). Stop becomes {kind: 'stop'};
  messages become {kind: 'message', payload}; actions and close go
  through the message payload with trigger='action' / 'close'.
- Subscribe moves from /realtime/v1/streams/{runId}/chat to
  /realtime/v1/sessions/{sessionId}/out. Records arrive as JSON
  strings; the loop parses them back into objects before the
  trigger:turn-complete / trigger:upgrade-required dispatch.
- Upgrade-required path keeps the same Session, swaps runId only.
- Drops the CHAT_STREAM_KEY / CHAT_MESSAGES_STREAM_ID /
  CHAT_STOP_STREAM_ID imports from chat-constants.js — chat-client.ts
  no longer references the legacy stream keys (the constants file
  itself will be deleted in Phase F along with the three references
  still in ai.ts's re-exports and chat-constants.ts itself).

Server-side auth uses apiClientManager.accessToken (the env secret
key), which has full scopes — no token-scoping changes needed here.
The browser transport's token-scope updates (Phase E) already cover
the client side.
…test

pipeChat (the internal that auto-pipes a chat.agent's returned
streamText result to the chat output) was still calling
streams.pipe(CHAT_STREAM_KEY, stream) — a run-scoped run-streams
path. After the session migration, the module-level facades
(chatStream, messagesInput, stopInput) routed correctly, but
pipeChat bypassed the facade and went straight to the old
run-scoped pipe. Result: the turn-complete control chunk reached
the session.out subscriber (written via chatStream.writer in
writeTurnCompleteChunk) but every streamed UIMessageChunk from the
LLM's turn was written to the dead run-scoped stream and never
surfaced on session.out.

Swap the pipe target to chatStream.pipe (the session-routed
facade). The target / streamKey options still type-check for
API parity but are no longer meaningful — sessions are the
address, and sub-agents that need to write to a parent chat open
the parent's Session explicitly. Smoke now catches all 14 chunks
(start / start-step / text-start / 7x text-delta / text-end /
finish-step / finish / trigger:turn-complete) with ids 0 through
13 from session.out, match: true.

Also adds references/hello-world/src/trigger/chatAgentSmoke.ts —
end-to-end validation:
- sessions.create with externalId = chatId
- trigger test-agent with {chatId, sessionId, messages, …}
- handle.out.read(...) SSE subscribe, capture chunks by id+type
- sessions.close on completion

Triggered from the dashboard or MCP as chat-agent-smoke. Requires
OPENAI_API_KEY in the dev env (the test-agent uses
openai:gpt-4o-mini).
Unblocks the unit tests after the chat.agent -> Sessions migration
(phases B/C/D). Before: 43 passed / 43 failed (35 in chat.test.ts + 10
mockChatAgent + 2 skillsRuntime). After: 86 passed / 0 failed.

Core (@trigger.dev/core/v3/test)

- TestSessionStreamManager: in-memory SessionStreamManager keyed on
  (sessionId, io) mirroring TestInputStreamManager. Dispatch rules
  match production with one test-only tweak — when a record arrives
  and only handlers are registered (no .once waiter), it's buffered
  for the next once() instead of discarded. Production doesn't need
  this because the SSE tail naturally serializes records after the
  agent's turn-loop has re-registered a waiter; tests send
  synchronously right after turn-complete, so without the buffer
  the next waitWithIdleTimeout loses the message.
- runInMockTaskContext installs the manager via
  sessionStreams.setGlobalManager, exposes drivers.sessions.in.send
  / .close, and tears down on exit.

SDK (@trigger.dev/sdk/v3/test)

- __setSessionOpenImplForTests hook in sessions.ts lets the harness
  override sessions.open(id) with an in-memory SessionHandle.
  SessionHandle constructor now accepts { in?, out? } overrides.
- TestSessionOutputChannel extends SessionOutputChannel and
  intercepts pipe / writer / append into a shared TestSessionOutState
  (chunks + listener registry). Never constructs SessionStreamInstance
  so it avoids initializeSessionStream / StreamsWriterV2 entirely.
- mockChatAgent rewritten: drops CHAT_MESSAGES_STREAM_ID /
  CHAT_STOP_STREAM_ID / the "chat" output stream key. sendMessage /
  sendRegenerate / sendAction push ChatInputChunk { kind: "message",
  payload } via drivers.sessions.in.send. sendStop pushes
  { kind: "stop" }. Turn-complete detection moves from
  drivers.outputs.onWrite to a TestSessionOutputChannel listener.

chat.test.ts

- New URL-predicate helpers at the top (isSessionCreateUrl,
  isTriggerTaskUrl, isSessionOutSubscribeUrl,
  isSessionStreamAppendUrl) + defaultSessionCreateResponse /
  defaultAppendResponse so every global.fetch mock speaks the
  same vocabulary.
- Bulk-updated all 25 mock blocks: added session-create handler
  (transport's accessToken path now lazily upserts via POST
  /api/v1/sessions before trigger), swapped /realtime/v1/streams/
  for /realtime/v1/sessions/ URL matchers, and replaced
  (streams/ && /input/) append-URL matchers with
  isSessionStreamAppendUrl.
- Three tests updated for new semantics: onSessionChange fires
  twice on first message (ensureSession -> sessionId only, then
  triggerNewRun -> adds runId + isStreaming). Async-token call
  count goes 1 -> 2 on first message because ensureSession and
  trigger both resolve the token with purpose: "trigger".
- "minimal wire payloads" test's body parsing updated — session.in
  append body is a raw JSON.stringify({ kind, payload }) string,
  not a { data } wrapper.
- Replaced the vestigial "custom streamKey URL" test with a
  "subscribes to the backing Session's .out" assertion. streamKey
  option is a no-op under sessions; removal can land in a follow-up.
- One test (stream closes without control chunk) legitimately
  needs 9s for SSE-close fallback — bumped its timeout to 15s.
On-ramp doc for future Claude sessions and customer-facing docs.
Captures the state of the chat.agent system after the Session
migration (phases A-E + test infra) so the next session doesn't
have to reconstruct it from git log + code:

- Why the migration (run-scoped streams -> Session primitive).
- Session primitive crash course (externalId idempotency,
  SessionHandle.in / .out, SSE resume, S2 direct writes).
- Chat mapping: one Session per chat, externalId = chatId,
  ChatInputChunk tagged union, session-scoped PATs.
- ASCII flow diagrams for first message / subsequent turns / stop /
  upgrade-required.
- Module layout (SDK / core / webapp), token mint sites (3 of them),
  key invariants, public API surface (unchanged / grown / added).
- Known follow-ups (Phase F deferred: MCP agentChat tool, AgentView
  dashboard component, ai-chat Next.js UI smoke, constant deletion).
- Smoke test sequences + git trail so a future reader can bisect.

Lives under .claude/architecture/ (repo-local notes directory, not
shipped to customers).
Rewires the three MCP agent-chat tools onto the Session primitive so
they stay in sync with TriggerChatTransport and the server-side
AgentChat after the chat.agent -> Sessions migration.

Tools affected: start_agent_chat, send_agent_message, close_agent_chat.
All live in packages/cli-v3/src/mcp/tools/agentChat.ts.

Changes
- Drop imports of CHAT_STREAM_KEY / CHAT_MESSAGES_STREAM_ID /
  CHAT_STOP_STREAM_ID from @trigger.dev/core/v3/chat-client. Add a
  local ChatInputChunk type + serializeInputChunk helper that mirrors
  the transport's wire format (JSON.stringify({ kind, payload })).
- start_agent_chat: call apiClient.createSession({ type:
  "chat.agent", externalId: chatId }) before triggering. The call is
  idempotent on externalId so two MCP clients targeting the same
  chatId converge. Thread sessionId into the trigger payload so the
  agent's sessions.open(payload.sessionId) finds the backing session.
- send_agent_message: replace
  sendInputStream(runId, CHAT_MESSAGES_STREAM_ID, payload) with
  appendToSessionStream(sessionId, "in",
  serializeInputChunk({ kind: "message", payload })). Fall-back path
  on send failure re-triggers on the same session (reuse sessionId,
  swap runId) instead of creating a new chat.
- close_agent_chat: send { kind: "message", payload: { trigger:
  "close", ... } } via appendToSessionStream so the agent's turn loop
  exits cleanly — matches the transport's close semantics.
- collectAgentResponse: subscribe URL moves from
  /realtime/v1/streams/{runId}/chat to
  /realtime/v1/sessions/{sessionId}/out. Session SSE uses v2/batch
  format which already delivers parsed UIMessageChunk objects via
  record.body.data, so the chunk-switch logic is unchanged.
  trigger:upgrade-required path keeps the same session and triggers
  a new run — previously it reused the old /streams/{newRunId}/chat
  URL, now the URL is stable across runs on the same session.
- Scopes: write:inputStreams -> read:sessions + write:sessions. The
  former was the transport's old input-stream write capability; the
  session endpoints are the new surface.
- ChatSession state grows a sessionId field (friendlyId session_*).
  runId stays but is now a live-run hint rather than durable identity.

Known limitation: the MCP server binary was spawned by Claude Code
at session start from the pre-migration bundle and stays in memory
for the lifetime of the Claude session — runtime verification has to
wait for the next session restart. Build passes; dist bundle
contains the new createSession / appendToSessionStream /
realtime/v1/sessions / Session ID references.
Migrates the dashboard's Agent tab (span inspector) onto the backing
Session's .out / .in channels so it stays in sync with
TriggerChatTransport, the server-side AgentChat, and the MCP chat
tools after the chat.agent -> Sessions migration.

Webapp

- SpanPresenter.server.ts extracts agentSession from the run payload:
  prefers the explicit sessionId that TriggerChatTransport and
  chat.createTriggerAction now thread through; falls back to chatId
  for pre-Sessions agent runs (the session resource route accepts
  either form via resolveSessionByIdOrExternalId).
- Span route (runs.$runParam.spans.$spanParam) threads agentSession
  through AgentViewAuth. agentView is only minted when we have an
  identifiable session — runs without one render a loading spinner
  without subscribing.
- New dashboard resource route
  resources.orgs.../runs.$runParam/realtime/v1/sessions/$sessionId/$io
  proxies S2RealtimeStreams.streamResponseFromSessionStream under
  dashboard session auth. The run param binds the resource hierarchy
  (keeps callers from subscribing to arbitrary sessions); the session
  identity is verified against the environment. GET-only — appends go
  through the public session API, not the dashboard.
- AgentView.tsx:
  - AgentViewAuth grows `sessionId: string`; `useAgentRunMessages`
    threads it into the effect dep array and URL construction.
  - Subscription URLs collapse from two run-scoped paths
    (.../streams/{runId}/chat + .../streams/{runId}/input/chat-messages)
    to one session base (.../sessions/{sessionId}/{out|in}).
  - Local CHAT_STREAM_KEY / CHAT_MESSAGES_STREAM_ID constants dropped.
  - `.in` parser switches from raw ChatTaskWirePayload to ChatInputChunk
    tagged union: only kind: "message" chunks surface user messages
    (pulled from chunk.payload.messages); kind: "stop" is ignored.
  - `.out` parsing is unchanged — session v2 SSE already delivers
    parsed UIMessageChunk objects via record.body.data.

SDK type fixes (byproducts)

- TriggerChatTransportOptions.sessions.sessionId is now optional so
  pre-Sessions localStorage state (chatId -> {runId, token, lastEventId})
  hydrates without migration. The runtime already `continue`s when
  sessionId is missing and lets ensureSession upsert on next send;
  the type just catches up.
- chat.test.ts session-change accumulator shape widened to match the
  new runtime state (adds optional runId / sessionId fields).

Smoke

Opened a completed test-agent run (sessionId threaded via prior smoke
test) in the dashboard. Agent tab rendered:
- user message from initialMessages seed
- assistant reply streamed over session.out
Both SSE endpoints returned 200; no console errors. Full SDK test
suite still passes (86/86).
Two coupled changes that together unblock end-to-end browser smoke
testing of TriggerChatTransport in the ai-chat reference and, more
broadly, let Next.js + Webpack client bundles pull types from
@trigger.dev/sdk/ai without hitting node: imports.

@trigger.dev/sdk

- New subpath @trigger.dev/sdk/ai/skills-runtime
  (src/v3/agentSkillsRuntime.ts) owns the node-only skill tool
  impls: runBashInSkill (node:child_process) + readFileInSkill
  (node:fs/promises, node:path, with path-traversal guard).
- ai.ts drops the top-level node:child_process / node:fs/promises /
  node:path imports. The auto-injected loadSkill / readFile / bash
  tools in createAgentSkillTools() load the runtime via a
  computed-string dynamic import (let path = "./agentSkillsRuntime.js";
  await import(path)) — webpack can't statically trace the expression
  so it drops the dependency from the client graph. Worker runtimes
  resolve the relative import normally, so bash + readFile keep
  working end-to-end on the server.
- Why it matters: even type-only imports from @trigger.dev/sdk/ai
  (for example CompactionChunkData or the full tool-set type chain
  that derives ChatUiMessage via InferUITools) trigger webpack to
  trace ai.js. Pre-split, that trace hit node:child_process and
  failed the client build with UnhandledSchemeError. With the split,
  ai.ts's top-level graph is pure — no node: at the top — so type
  consumers compile cleanly.

references/ai-chat

- components/chat.tsx extends the window.__chat test bridge with
  session-era state (session / sessionId / lastEventId) and
  generic waiters (waitForStatus + waitForMessage +
  waitForFirstAssistantText) with configurable timeouts and clean
  rejection. A driver (Chrome DevTools MCP, Playwright, etc.) can
  now exercise the chat end-to-end through eval'd JS:
    await window.__chat.send("hi");
    const t = await window.__chat.waitForFirstAssistantText();
  No more click-driven smokes. Existing steerOnToolCall +
  steerAfterDelay / queueAfterDelay / promote helpers stay.
- Inlines a local structural CompactionChunkData type so
  chat.tsx doesn't pull from @trigger.dev/sdk/ai for a single type
  assertion. Defensive — the subpath split fixes the underlying
  build issue, this just keeps the chat.tsx module graph minimal.
- Fixes a stale ChatSessionState shape in the DebugPanel session
  prop type (sessionId is now optional, runId optional).

Known limitation (not in this commit)

Running the live UI smoke still requires a working chat-agent
backend for ai-chat, which depends on isolated-vm having a
prebuilt darwin-arm64 binary for node 20.20.0. On this machine
`pnpm rebuild isolated-vm` fails (node-gyp toolchain issue),
which is orthogonal to the session migration. Bridge
infrastructure is validated (all keys mount on window.__chat;
SDK tests 86/86 pass); exercising the send->stream->stop flow
end-to-end against the ai-chat agent is blocked on the native
build.
Fixes the last set of issues that were blocking TriggerChatTransport
from running end-to-end against the ai-chat reference. Smoke now
passes: new chat → send → streamed assistant reply in ~4s → second
turn reuses the same session + run, lastEventId advances 10 → 21.

SDK (@trigger.dev/sdk)

- RenewRunAccessTokenParams carries the durable sessionId alongside
  chatId + runId. Server-side renew handlers MUST mint the renewed
  PAT with read:sessions:{sessionId} + write:sessions:{sessionId}
  scopes (in addition to the existing run scopes) — without them,
  the first append after expiry 401s on session.in/append and sends
  the transport into a renew loop. transport.renewRunPatForSession
  looks up the cached sessionId off `this.sessions` so existing
  renew callers just need to spread the new field through.
- transport.preload(chatId) on the triggerTask callback path no
  longer calls apiClient.createSession from the browser. Matches
  sendMessages: when triggerTaskFn is configured the server action
  (chat.createTriggerAction) creates the Session with its secret
  key and returns sessionId alongside the run PAT. Browser
  deployments using the callback flow therefore never need
  write:sessions on any browser-facing token.
- chat.test.ts renew-spy assertions updated to match the new
  {chatId, runId, sessionId} shape — 86/86 tests still green.

Webapp

- POST /api/v1/sessions gets allowJWT: true + corsStrategy: "all".
  Pre-fix, the route rejected any CORS-preflighted browser call,
  which broke the transport's direct accessToken fallback path
  (sessions.create from the browser).
- POST /realtime/v1/sessions/:session/:io/append now exports both
  { action, loader }. The route builder installs the OPTIONS
  preflight handler on the loader; without a loader export, the
  preflight returned 400 ("No loader for route") and Chrome
  surfaced the follow-up POST as net::ERR_FAILED. Same pattern
  already in use on /api/v1/tasks/:id/trigger.

references/ai-chat

- Switch both chat-app.tsx and chat-view.tsx from
  accessToken: getChatToken to triggerTask: triggerChat. This path
  has the server action create the Session server-side with the
  secret key, so the browser never hits POST /api/v1/sessions and
  the returned PAT already carries the session scopes needed for
  session.in/out.
- renewRunAccessTokenForChat(chatId, runId, sessionId?) now mints
  tokens that include read:sessions:{sessionId} +
  write:sessions:{sessionId} alongside the run scopes. Both call
  sites thread the sessionId from the SDK's renew callback params.
- Drop executeJs / runInSecureSandbox / runInPRReviewSandbox to
  decouple ai-chat trigger dev from the isolated-vm native binary
  (its darwin-arm64 prebuild is broken against node 20.20.0 on
  the current toolchain). Deletes src/lib/secure-sandbox.ts and
  src/lib/pr-review-sandbox.ts, removes the executeJs tool from
  chatTools, the secure-exec-bridge esbuild plugin from
  trigger.config.ts (and its companion node-stdlib-browser-stub),
  and the `secure-exec` dependency from package.json. E2B-backed
  executeCode stays. If a future session needs the in-process V8
  sandbox back, reintroduce through a different module (or pin a
  prebuilt binary) to avoid this failure mode.

Smoke drove via the window.__chat bridge from Chrome DevTools MCP —
no click-based interaction needed.
Final Phase F cleanup — `CHAT_STREAM_KEY`, `CHAT_MESSAGES_STREAM_ID`,
and `CHAT_STOP_STREAM_ID` were meaningful only when chat.agent I/O
lived on run-scoped Redis streams. The Session migration moved all
chat I/O onto the backing Session's `.in` / `.out` channels, so these
constants stopped describing how anything is addressed months ago and
have been dead-weight re-exports since.

Dropped from the public surface:
- `@trigger.dev/core/v3/chat-client` no longer exports the three
  constants. The file keeps `ChatStoreChunk` + `applyChatStorePatch`
  (the chat.store primitive's shared types).
- `@trigger.dev/sdk/ai` no longer re-exports them via the
  `CHAT_STREAM_KEY` / `CHAT_MESSAGES_STREAM_ID` / `CHAT_STOP_STREAM_ID`
  aliases introduced by the migration commit.
- Deletes `packages/trigger-sdk/src/v3/chat-constants.ts` (the shim
  that bridged core's definitions to the SDK's public surface).

What stayed the same:
- `chat.stream.id` / `chat.messages.id` / `chat.stopSignal.id` still
  contain the literal strings `"chat"` / `"chat-messages"` /
  `"chat-stop"` — inlined as opaque breadcrumbs rather than
  user-consumable constants. Telemetry attrs keep the same values,
  so dashboards/spans don't shift.
- All runtime behavior is untouched. The `chatStream` / `messagesInput` /
  `stopInput` facades still delegate through the Session handle
  exactly as before; only the constant symbols are gone.

Migration note for external callers:
Anyone still importing the old constants should migrate to the
session primitives:
- `streams.writer(CHAT_STREAM_KEY, …)` → `sessions.open(sessionId).out.writer(…)`
- `streams.input(CHAT_MESSAGES_STREAM_ID)` → `sessions.open(sessionId).in.on(…)`
  (filtered by `chunk.kind === "message"`)
- `streams.input(CHAT_STOP_STREAM_ID)` → `sessions.open(sessionId).in.on(…)`
  (filtered by `chunk.kind === "stop"`)

Validated
- 86/86 SDK tests green.
- Webapp typecheck clean (core types used in SpanPresenter + AgentView
  are untouched).
- ai-chat UI smoke passes end-to-end: new chat → send "Say hi in
  three words." → first assistant text in 4.9s → sessionId + runId +
  lastEventId all set.
…ircuit

TriggerChatTransport.reconnectToStream previously returned null any time
state.isStreaming was falsy, which included undefined. That meant a
caller who dropped isStreaming from their ChatSession persistence (a
reasonable simplification now that the server can tell the client when
a session is settled via X-Session-Settled on the session.out SSE)
would get null on every reconnect and the UI would never resume
streaming.

Tighten the check to state.isStreaming === false so only an explicit
false triggers the fast-path skip. Undefined now falls through to open
the SSE and let the server decide — on a settled session the server
already closes the connection in ~1s via wait=0, so there is no 60s
hang to worry about.

Backward compatible: callers who still persist and hydrate isStreaming
(true/false) keep today's behavior exactly; callers who drop the flag
now get the server-authoritative path.
Three dashboard-scoped stream routes were passing request.signal into
realtimeStream.streamResponse. That signal is broken under
Remix+Express (see apps/webapp/CLAUDE.md, nodejs/node#55428 — the chain
is severed when Remix internally clones the Request), so when a user
closes their dashboard tab the signal never fires. The underlying
RedisRealtimeStreams.streamResponse loops while(!signal.aborted) over
XREAD BLOCK and only exits on its 15s inactivity timeout; the S2 path
keeps the upstream fetch open for up to its 60s wait window.

Thread getRequestAbortSignal() through:
  - resources/orgs/.../runs/$runParam/realtime/v1/streams/$runId/$streamId
  - resources/orgs/.../runs/$runParam/realtime/v1/streams/$runId/input/$streamId
  - resources/orgs/.../playground/realtime/v1/streams/$runId/$streamId
Each picks up the Express res.on('close')-backed signal that fires
reliably when the downstream client disconnects.
Pulls PENDING_MESSAGE_INJECTED_TYPE, ChatTaskWirePayload, and the
client-data inference helpers out of ai.ts (~7000 lines, statically
imports node:* via the skills runtime) into a new ai-shared.ts that
stays free of node-only imports. chat.ts and chat-react.ts now reach
for these via ai-shared so browser bundlers don't trace ai.ts's entire
module graph (Turbopack rejected the node: builtins outright).
The webapp's peek-tail-settled shortcut on /realtime/v1/sessions/:id/out
previously fired on every io=out subscription. That race-tripped active
send-a-message paths: the SSE peek would see the prior turn's
trigger:turn-complete record before the newly-triggered run wrote its
first chunk, return wait=0 + X-Session-Settled:true, and close the
stream before any of the new turn's records landed.

Make the peek opt-in via an X-Peek-Settled: 1 request header. Only
TriggerChatTransport.reconnectToStream sets it (true reload-resume case
where settling early is fine); sendMessages and the rest leave it off
and stay on the normal long-poll. On the server side,
streamResponseFromSessionStream gates the peek on options.peekSettled
and skips it otherwise.

- apps/webapp: read X-Peek-Settled from the request, thread to
  streamResponseFromSessionStream
- packages/trigger-sdk/chat.ts: peekSettled option on
  subscribeToSessionStream + reconnectToStream sets it; sendMessages
  does not
- docs/ai-chat/client-protocol.mdx + docs/sessions/reference.mdx:
  document the opt-in semantics
- .server-changes/session-out-settled-signal.md: record the change
Companion to the SDK opt-in. Webapp routes read X-Peek-Settled from the
request and skip the tail peek when it isn't set, so active
send-a-message paths can't race a stale trigger:turn-complete. Docs
note the opt-in semantics; .server-changes records the change for the
deploy log.
@ericallam ericallam force-pushed the feature/tri-7532-ai-sdk-chat-transport-and-chat-task-system branch from d3fa3d7 to c295732 Compare April 25, 2026 20:27
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.

4 participants