Skip to content

fix(core): accept IPC acks for void callbacks under zod 4.4+ - #4980

Merged
matt-aitken merged 4 commits into
triggerdotdev:mainfrom
NERLOE:fix/zod-ipc-ack-optional-message
Sep 28, 2026
Merged

matt-aitken merged 4 commits into
triggerdotdev:mainfrom
NERLOE:fix/zod-ipc-ack-optional-message

Conversation

@NERLOE

@NERLOE NERLOE commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Closes #4979

Credit to @marcus-dk for finding and root-causing this in #4979, including the zod version bisect and the end-to-end confirmation with a patched worker.

Summary

FLUSH and CANCEL declare callback: z.void(), so the task run process acks them with { type: "ACK", id, message: undefined }. Node's JSON IPC serialization drops the undefined message key. Under zod 4.4+ a z.any() object key is required at parse time (it was optional under zod 3 and 4.0–4.3), so Packet.safeParse rejects every such ack, #handlePacket returns silently, and sendWithAck waits out its full timeout.

Because zodIpc.ts imports zod/v4 and @trigger.dev/core resolves zod from the user's project, every project on zod ≥ 4.4 pays this on every run: ~6 s between the task finishing and the completion reaching the engine (FLUSH: 5 s + 1 s), and ~31 s on cancel.

The fix makes the ACK packet's message optional again (z.any().optional()). Only the ACK packet can legitimately carry undefined, so the other packets are unchanged.

✅ Checklist

  • I have followed every step in the contributing guide
  • The PR title follows the convention.
  • I ran and tested the code works

Testing

  • Added packages/core/test/zodIpc.test.ts: forks a real child process (test/fixtures/zodIpcChild.ts) and sends FLUSH (void callback) and PING (payload callback) over Node's IPC channel, so the ack goes through the same JSON serialization as between a worker and its task run process. On main (zod 4.5.4 in this repo) the void-callback test fails with sendWithAck() timeout; with the fix it resolves immediately.
  • pnpm run build --filter @trigger.dev/core passes.
  • Production evidence from our self-hosted instance (completed_at - started_at - usage_duration_ms on successful runs):
SDK runs p10 gap p50 gap
4.5.12 2.6M 179 ms 244 ms
4.5.15 5.0M 176 ms 251 ms
4.6.0 1.6M 6,162 ms 6,196 ms
4.6.2 4.0M 6,180 ms 6,240 ms

Changelog

Fix a ~6 second delay between a task finishing and its run completing (and a ~31 second delay when cancelling a run) in projects that use zod 4.4 or newer.

🤖 Generated with Claude Code

@changeset-bot

changeset-bot Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f5dd1bc

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

This PR includes changesets to release 27 packages
Name Type
@trigger.dev/core Patch
@trigger.dev/build Patch
trigger.dev Patch
@trigger.dev/python Patch
@trigger.dev/redis-worker Patch
@trigger.dev/schema-to-json Patch
@trigger.dev/sdk Patch
@internal/clickhouse Patch
@internal/llm-model-catalog Patch
@internal/metrics-pipeline Patch
@trigger.dev/rbac Patch
@internal/redis Patch
@internal/replication Patch
@internal/run-engine Patch
@internal/run-store Patch
@internal/schedule-engine Patch
@internal/tracing Patch
@internal/webhook-engine Patch
@internal/webhook-sources Patch
@internal/dashboard-agent Patch
@internal/cache Patch
@trigger.dev/react-hooks Patch
@trigger.dev/rsc Patch
@trigger.dev/database Patch
@trigger.dev/otlp-importer Patch
@trigger.dev/sso Patch
@internal/testcontainers Patch

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

coderabbitai Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 4cb686b3-8ed5-42cf-93df-e9a8cea28752

📥 Commits

Reviewing files that changed from the base of the PR and between 42f254f and f5dd1bc.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 7701ccf6-3b15-49da-8f76-3666e604fafa

📥 Commits

Reviewing files that changed from the base of the PR and between 41287e7 and 42f254f.

📒 Files selected for processing (1)
  • .changeset/void-ipc-acks.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • .changeset/void-ipc-acks.md

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.

📜 Recent review details
⏰ Context from checks skipped due to timeout. (7)
  • GitHub Check: internal / 🧪 Unit Tests: Internal (1)
  • GitHub Check: e2e / 🧪 CLI v3 tests (warp-windows-latest-x64-8x - npm)
  • GitHub Check: packages / 🧪 Unit Tests: Packages (2, 3)
  • GitHub Check: e2e / 🧪 CLI v3 tests (warp-windows-latest-x64-8x - pnpm)
  • GitHub Check: packages / 🧪 Unit Tests: Packages (3, 3)
  • GitHub Check: internal / 🧪 Unit Tests: Internal (2)
  • GitHub Check: Analyze (javascript-typescript)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: NERLOE
URL: https://github.com/triggerdotdev/trigger.dev/pull/4980

Timestamp: 2026-09-25T11:45:20.033Z
Learning: In `packages/core/src/v3/zodIpc.ts`, ACK packets without `message` were accepted under zod 3 and zod 4.0–4.3 because the `z.any()` key was optional at parse time. PR `#4980` restores that behavior for zod 4.4+. Validating ACKs against their expected callback schemas would change `sendWithAck` semantics and is separate hardening work.

Walkthrough

The ACK packet schema now permits the message field to be absent. Tests use a forked child process to check void and payload acknowledgments. A patch changeset describes the task completion and cancellation delays reported for projects using Zod 4.4 or newer.

Priority: ⬆️ High

Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to 42f25

The change addresses delayed completion and cancellation acknowledgments. No concrete merge-blocking risk is identified; normal checks remain appropriate.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 41287

The change restores acknowledgments from child processes when callbacks return no value. No new externally reachable entrypoint or introduced security bypass was established, but the shared completion protocol warrants review.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The affected production boundary is communication with an IPC peer used by task-run processing, including FLUSH and CANCEL acknowledgments. The newly added fixture endpoints are reached by the test’s forked child, not an identified production caller.

Security Findings and Attack Paths

  • inferred — A peer able to send a matching ACK without a message could resolve a non-void callback as undefined. This is not an established new security bypass: the previous ACK field used z.any(), permitting that peer to send an arbitrary value instead. Whether an attacker can access the production IPC channel remains unestablished.

Trust Boundaries and Controls

  • observed — The connection reads process messages, parses their packet shape, and correlates ACKs by connection-local numeric ID. The inspected ACK handler has no operation-type, session, or callback-result check.

Resilience and Maintainability Implications

  • observed — Pending calls have separate IDs and timers; timeout rejects the call, while a matching ACK clears its timer. The inspected code does not remove the pending-map entry after either outcome, a behavior outside the schema edit.

Hardening Proposals

  • proposed — If IPC peers must be treated as untrusted, bind each pending ACK to its expected callback schema and validate the returned value before treating the operation as complete. This is hardening of the existing protocol, not an established vulnerability introduced by this PR.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main fix: accepting IPC acknowledgements for void callbacks under Zod 4.4 and newer.
Description check ✅ Passed The description includes the linked issue, completed checklist, technical summary, testing details, build result, and changelog entry. It omits the Screenshots section, but that omission is non-critic…
Linked Issues check ✅ Passed The PR meets the coding requirements in issue #4979. packages/core/src/v3/zodIpc.ts makes the ACK message field optional. This accepts ACK packets that omit message after JSON IPC serialization.…
Out of Scope Changes check ✅ Passed The changes stay within issue #4979. The schema change fixes rejected void IPC acknowledgements. The fixture changes and forked-process tests verify the affected serialization path. The changeset docu…
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 4 files. (1 skipped: 1 …
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Create a new PR

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.

coderabbitai[bot]

This comment was marked as resolved.

@NERLOE

NERLOE commented Sep 25, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai On the retained reliability concern (an ACK without message can now resolve a non-void request): this is the behaviour zodIpc had under zod 3 and zod 4.0–4.3, where a z.any() key was already optional at parse time. This PR restores it for zod 4.4+ rather than introducing it. Validating each ack against its expected callback schema would be a reasonable hardening, but it changes sendWithAck semantics beyond this bug fix, so I'd keep it out of scope here.

Changeset wording fixed in d098714 as well.

For maintainers: the failing webapp unit/E2E checks are the fork-secrets issue (DockerHub login skipped because secrets are not available, then quay.io/minio/minio pull is unauthorized). code-quality, typecheck and all package unit-test shards (which run the new test) pass.

@NERLOE
NERLOE marked this pull request as ready for review September 25, 2026 11:43
devin-ai-integration[bot]

This comment was marked as resolved.

@coderabbitai

coderabbitai Bot commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
fd -a '^(zodIpc\.ts|zodIpc\.test\.ts|void-ipc-acks\.md)$'
ast-grep outline packages/core/src/v3/zodIpc.ts --items all
ast-grep outline packages/core/test/zodIpc.test.ts --items all
sed -n '1,280p' packages/core/src/v3/zodIpc.ts
sed -n '1,180p' packages/core/test/zodIpc.test.ts
cat .changeset/void-ipc-acks.md
git show HEAD^:packages/core/src/v3/zodIpc.ts | sed -n '1,110p' || true

Length of output: 16548


@NERLOE, thanks for the clarification. I agree that per-callback ACK validation is a separate hardening change, not a requirement for this fix. I withdraw the retained reliability concern as a finding on this PR.

I also see the corrected changeset wording. Thanks for the context on the webapp checks; I have not independently verified their failure logs. Would you like a follow-up issue for ACK callback validation?


✏️ Learnings added
Learnt from: NERLOE
URL: https://github.com/triggerdotdev/trigger.dev/pull/4980

Timestamp: 2026-09-25T11:45:19.903Z
Learning: In `packages/core/src/v3/zodIpc.ts`, ACK packets without `message` were accepted under zod 3 and zod 4.0–4.3 because the `z.any()` key was optional at parse time. PR `#4980` restores that behavior for zod 4.4+. Validating ACKs against their expected callback schemas would change `sendWithAck` semantics and is separate hardening work.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

NERLOE and others added 4 commits September 28, 2026 10:32
FLUSH and CANCEL declare `callback: z.void()`, so the child acks with
`message: undefined`. Node's JSON IPC serialization drops that key, and
zod 4.4+ treats a `z.any()` key as required, so the ACK packet failed to
parse and `sendWithAck` waited out its full timeout: ~6s on every run
completion and ~31s on cancel. Make the ACK packet's `message` optional.

Fixes triggerdotdev#4979

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The `resolves` assertion already fails when the ack is rejected, and a
wall-clock bound could flake on a slow runner. Also hyphenate the
changeset's compound adjectives.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Replace the in-memory JSON channel with a forked child process so the ack
goes through Node's actual IPC serialization.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@carderne
carderne force-pushed the fix/zod-ipc-ack-optional-message branch from 42f254f to f5dd1bc Compare September 28, 2026 09:32
@matt-aitken
matt-aitken added this pull request to the merge queue Sep 28, 2026
Merged via the queue into triggerdotdev:main with commit 8f15fa5 Sep 28, 2026
65 checks passed
@github-actions github-actions Bot mentioned this pull request Sep 28, 2026
birhantprkc pushed a commit to birhantprkc/trigger.dev that referenced this pull request Oct 1, 2026
## Summary
6 new features, 17 improvements, 13 bug fixes.

## Improvements
- Chat agents can now scope concurrency per session. Pass
`concurrencyKey` (for example, your chat ID or tenant ID) and
trigger-time named limits via `triggerConfig.concurrency` when starting
a chat session, from `chat.createStartSessionAction`, the `AgentChat`
client, or a handover. Keys are never defaulted, so a session without
one shares the task's keyless pool.
([`ea9758117`](triggerdotdev@ea97581))
  
  ```ts
  const start = chat.createStartSessionAction("support-chat", {
  triggerConfig: { concurrencyKey: user.id },
  });
  ```
- Concurrency limits can now be paused and resumed, just like queues:
`concurrencyLimits.pause(name)` stops every run holding the limit from
being dequeued while keeping its configured bounds, and
`concurrencyLimits.resume(name)` starts them again.
([`fa94febb6`](triggerdotdev@fa94feb))
  
  ```ts
  import { concurrencyLimits } from "@trigger.dev/sdk";
  
  await concurrencyLimits.pause("openai");
  await concurrencyLimits.resume("openai");
  ```
- Control a task's concurrency with the new `concurrency` option, and
share limits across tasks with named concurrency limits. An inline shape
caps the task itself; `concurrencyLimit()` declares a limit any task can
hold (up to two named limits per task), and a trigger call can switch a
run's named limits with its own `concurrency` option.
([`ea9758117`](triggerdotdev@ea97581))
  
  ```ts
  import { concurrencyLimit, task } from "@trigger.dev/sdk";
  
export const openaiLimit = concurrencyLimit({ name: "openai", total: 25
});
  
  export const generateSummary = task({
  id: "generate-summary",
  concurrency: [{ perKey: 1, total: 5 }, openaiLimit],
  run: async (payload) => {},
  });
  ```
  
`perKey` caps each `concurrencyKey` pool and `total` caps across
everything, keys or not. The queue-level `concurrencyLimit` option keeps
working unchanged and is deprecated in favor of `concurrency`.
Enforcement happens server-side; servers without support accept the
option but do not enforce it yet.
  
Manage limits at runtime with the new `concurrencyLimits` namespace:
`list()` and `retrieve(name)` report each limit's bounds plus its live
`running` and `queued` counts, `override(name, { perKey, total })`
changes only the given bounds (overriding `total` to `0` pauses the
limit), and `reset(name)` restores the declared values.
  
Queue reads (`queues.list()` and `queues.retrieve()`) now report a
`version` that discriminates the shape: `V1` queues keep today's fields
(their own `concurrencyLimit` and its override state), while `V2` queues
(tasks declared with `concurrency`) carry no queue-level concurrency,
since their limits are read and overridden through `concurrencyLimits`
(a task's inline limit under its derived `task/<task-id>` name).
Existing reads keep compiling: a `V2` queue reports `concurrencyLimit`
as null and `concurrency` as undefined.
- Chat streams now report `Stream stalled: no records received` after
five retries of a connected stream that sends no records. Network
failures and browser wakeups retain automatic recovery. Healthy tool
calls with no records for about six minutes also reach this silence
limit. Watch subscriptions remain unlimited, and caller cancellation
still closes cleanly.
([triggerdotdev#4949](triggerdotdev#4949))
- Webhook verifier artifacts can now declare the provider's response
contract as data: a handshake `respondStatus`, the status codes returned
for accepted deliveries and rejected signatures, and a GET verification
flow (`getHandshake`) for providers that confirm a callback URL with a
challenge. HMAC verifiers can read the timestamp from a body field,
which the Linear provider config uses for its replay window, and the
dashboard's test-send re-signs a recorded sample as of now so it passes
that window.
([`5f54fb27f`](triggerdotdev@5f54fb2))
- Add an optional `onSettled` callback to
`TriggerChatTransport.sendAction()` so callers can confirm that their
action's input was processed, independently of whether the response
stream closes.
([triggerdotdev#4956](triggerdotdev#4956))
- Chat agents now return to a durable wait when a session wake does not
deliver a matching message. This prevents resumed runs from staying
active until their maximum duration and preserves the configured turn
timeout across repeated wakes.
([`e6ec2c4e8`](triggerdotdev@e6ec2c4))
- Deprecate `queues.overrideConcurrencyLimit` and
`queues.resetConcurrencyLimit`. These operate on the legacy model where
a queue carried its own concurrency limit; declare concurrency with the
task `concurrency` option and manage it with
`concurrencyLimits.override` and `concurrencyLimits.reset` instead.
([`414e5a268`](triggerdotdev@414e5a2))
- Steering messages now remain in context across agent steps and keep
their original position in saved conversations, including custom
response data written between steps.
([`5619acf26`](triggerdotdev@5619acf))
- Set up a new Trigger.dev account from the CLI. Login can prefill an
email address, save the user's full name after authorization, and resume
authorization later, while `init` can create the first organization,
activate its Free plan, and create the first project before scaffolding
the app.
([`fb3b26d4f`](triggerdotdev@fb3b26d))
- Added a `submit_feedback` MCP tool so coding agents can report a
confusing tool error, a docs mismatch, or a missing capability without
the user having to file it by hand. Turn it off with `--skip-telemetry`
or `TRIGGER_TELEMETRY_DISABLED`; the tool is hidden while it is off.
([`9d38ff508`](triggerdotdev@9d38ff5))
- Authenticate `trigger promote` with environment API keys supplied
through `TRIGGER_ACCESS_TOKEN`. Environment API key commands now use the
saved profile API URL when no explicit override is provided.
([`562c9433a`](triggerdotdev@562c943))
- Convert Zod 4 `z.date()` fields to date-time strings in JSON Schema
without weakening validation for other unsupported types. This prevents
MCP tool discovery from failing when a tool input schema contains a
date.
([`f8babdf9b`](triggerdotdev@f8babdf))

## Bug fixes
- Fix stale and empty project environment values in `trigger dev`, and
support empty values in `syncEnvVars()`.
([`f384e8334`](triggerdotdev@f384e83))
- Fixes warm starts silently failing for deployments built with 4.6.0 to
4.6.3 in projects that resolve `zod` to a 3.x release. The runner could
not parse the run handed to it by the warm-start service and exited,
leaving the run waiting until the platform redrove it a few minutes
later and started it cold. Redeploy to pick up the fix.
([triggerdotdev#4972](triggerdotdev#4972))
- Fix a ~6-second delay between a task finishing and its run completing
(and a ~31-second delay when cancelling a run) in projects that use zod
4.4 or newer. Run cost and billed usage was not impacted by this issue.
([triggerdotdev#4980](triggerdotdev#4980))
- Fix Windows deploys failing at indexing with `Cannot find module` on a
percent-encoded path when the project directory contains spaces or
non-ASCII characters.
([`b32c1d157`](triggerdotdev@b32c1d1))

## Server changes

These changes affect the self-hosted Docker image and Trigger.dev Cloud:

- One concurrency key with a large backlog no longer holds up runs
waiting on other keys on the same queue.
([triggerdotdev#4367](triggerdotdev#4367))
- API rate limit usage is now recorded per environment in the `metrics`
table as `api.rate_limit.allowed`, `api.rate_limit.denied`,
`api.rate_limit.remaining_min` and the limit itself
(`api.rate_limit.limit.per_second` and `api.rate_limit.limit.burst`), so
you can chart requests against your limit and 429s over time on the
Query page and dashboards.
- Archive queues you no longer use from the Concurrency page. Archived
queues are hidden from the list and no longer count towards allocated
concurrency.
- Automatically archive preview branches after a configurable period
without deployments, with protected branch names and a preview of
affected branches.
- Download a session’s original saved transcript file from the session
inspector.
- Organization Owners and Admins can now choose, in Settings under
Support Access, whether Trigger.dev support can open their dashboard
directly or only after an Owner or Admin approves a request, with each
approval lasting 7 days.
- Allocating extra concurrency to environments now requires billing
permissions, the same as purchasing it. Allocation changes are also
applied atomically, so simultaneous edits can no longer exceed your
purchased concurrency.
- Changing your account email address now sends a confirmation link to
the new address; the change only takes effect once that link is opened.
Requesting a magic link no longer creates an account until the link is
used.
- Removed the organization-wide Node.js 21 deprecation banner while
keeping runtime upgrade guidance in Projects settings.
- Creating a project now asks only for its name.
- Prevent batch waits from remaining suspended when batch completion is
briefly interrupted
- Reject batch-and-wait requests when the parent run belongs to another
environment
- Keep dashboard pages visible during network interruptions, with a
disconnected banner and a Refresh button instead of a full-page error.
- Allow deployments to replace declarative schedules at the schedule
limit when the resulting set stays within quota.
- "Deploy now" now tells you when the branch doesn't exist on GitHub
instead of showing a generic error, and a first deployment no longer
flags a harmless build-cache message as an error.
- Support empty-string environment values across the dashboard, API, and
Vercel sync behind a feature flag, disabled by default.
- Fixed a bug where a burst of telemetry could leave OpenTelemetry
ingest rejecting every batch for a long time. Batches that wait too long
are now dropped individually instead of restarting the processing
workers, so ingest recovers as soon as the burst passes.
- Keep scheduled task "Last run" times stable across unchanged
deployments and align them with configured schedule windows.
- Team members and pending invites on the organization Team page are now
listed in alphabetical order.

<details>
<summary>Raw changeset output</summary>

# Releases
## @trigger.dev/core@4.7.0

### Minor Changes

- Chat agents can now scope concurrency per session. Pass
`concurrencyKey` (for example, your chat ID or tenant ID) and
trigger-time named limits via `triggerConfig.concurrency` when starting
a chat session, from `chat.createStartSessionAction`, the `AgentChat`
client, or a handover. Keys are never defaulted, so a session without
one shares the task's keyless pool.
([`ea9758117`](triggerdotdev@ea97581))

  ```ts
  const start = chat.createStartSessionAction("support-chat", {
    triggerConfig: { concurrencyKey: user.id },
  });
  ```

- Concurrency limits can now be paused and resumed, just like queues:
`concurrencyLimits.pause(name)` stops every run holding the limit from
being dequeued while keeping its configured bounds, and
`concurrencyLimits.resume(name)` starts them again.
([`fa94febb6`](triggerdotdev@fa94feb))

  ```ts
  import { concurrencyLimits } from "@trigger.dev/sdk";

  await concurrencyLimits.pause("openai");
  await concurrencyLimits.resume("openai");
  ```

- Control a task's concurrency with the new `concurrency` option, and
share limits across tasks with named concurrency limits. An inline shape
caps the task itself; `concurrencyLimit()` declares a limit any task can
hold (up to two named limits per task), and a trigger call can switch a
run's named limits with its own `concurrency` option.
([`ea9758117`](triggerdotdev@ea97581))

  ```ts
  import { concurrencyLimit, task } from "@trigger.dev/sdk";

export const openaiLimit = concurrencyLimit({ name: "openai", total: 25
});

  export const generateSummary = task({
    id: "generate-summary",
    concurrency: [{ perKey: 1, total: 5 }, openaiLimit],
    run: async (payload) => {},
  });
  ```

`perKey` caps each `concurrencyKey` pool and `total` caps across
everything, keys or not. The queue-level `concurrencyLimit` option keeps
working unchanged and is deprecated in favor of `concurrency`.
Enforcement happens server-side; servers without support accept the
option but do not enforce it yet.

Manage limits at runtime with the new `concurrencyLimits` namespace:
`list()` and `retrieve(name)` report each limit's bounds plus its live
`running` and `queued` counts, `override(name, { perKey, total })`
changes only the given bounds (overriding `total` to `0` pauses the
limit), and `reset(name)` restores the declared values.

Queue reads (`queues.list()` and `queues.retrieve()`) now report a
`version` that discriminates the shape: `V1` queues keep today's fields
(their own `concurrencyLimit` and its override state), while `V2` queues
(tasks declared with `concurrency`) carry no queue-level concurrency,
since their limits are read and overridden through `concurrencyLimits`
(a task's inline limit under its derived `task/<task-id>` name).
Existing reads keep compiling: a `V2` queue reports `concurrencyLimit`
as null and `concurrency` as undefined.

### Patch Changes

- Fix stale and empty project environment values in `trigger dev`, and
support empty values in `syncEnvVars()`.
([`f384e8334`](triggerdotdev@f384e83))
- Chat streams now report `Stream stalled: no records received` after
five retries of a connected stream that sends no records. Network
failures and browser wakeups retain automatic recovery. Healthy tool
calls with no records for about six minutes also reach this silence
limit. Watch subscriptions remain unlimited, and caller cancellation
still closes cleanly.
([triggerdotdev#4949](triggerdotdev#4949))
- Fixes warm starts silently failing for deployments built with 4.6.0 to
4.6.3 in projects that resolve `zod` to a 3.x release. The runner could
not parse the run handed to it by the warm-start service and exited,
leaving the run waiting until the platform redrove it a few minutes
later and started it cold. Redeploy to pick up the fix.
([triggerdotdev#4972](triggerdotdev#4972))
- Fix a ~6-second delay between a task finishing and its run completing
(and a ~31-second delay when cancelling a run) in projects that use zod
4.4 or newer. Run cost and billed usage was not impacted by this issue.
([triggerdotdev#4980](triggerdotdev#4980))
- Webhook verifier artifacts can now declare the provider's response
contract as data: a handshake `respondStatus`, the status codes returned
for accepted deliveries and rejected signatures, and a GET verification
flow (`getHandshake`) for providers that confirm a callback URL with a
challenge. HMAC verifiers can read the timestamp from a body field,
which the Linear provider config uses for its replay window, and the
dashboard's test-send re-signs a recorded sample as of now so it passes
that window.
([`5f54fb27f`](triggerdotdev@5f54fb2))
## @trigger.dev/react-hooks@4.7.0

### Minor Changes

- Control a task's concurrency with the new `concurrency` option, and
share limits across tasks with named concurrency limits. An inline shape
caps the task itself; `concurrencyLimit()` declares a limit any task can
hold (up to two named limits per task), and a trigger call can switch a
run's named limits with its own `concurrency` option.
([`ea9758117`](triggerdotdev@ea97581))

  ```ts
  import { concurrencyLimit, task } from "@trigger.dev/sdk";

export const openaiLimit = concurrencyLimit({ name: "openai", total: 25
});

  export const generateSummary = task({
    id: "generate-summary",
    concurrency: [{ perKey: 1, total: 5 }, openaiLimit],
    run: async (payload) => {},
  });
  ```

`perKey` caps each `concurrencyKey` pool and `total` caps across
everything, keys or not. The queue-level `concurrencyLimit` option keeps
working unchanged and is deprecated in favor of `concurrency`.
Enforcement happens server-side; servers without support accept the
option but do not enforce it yet.

Manage limits at runtime with the new `concurrencyLimits` namespace:
`list()` and `retrieve(name)` report each limit's bounds plus its live
`running` and `queued` counts, `override(name, { perKey, total })`
changes only the given bounds (overriding `total` to `0` pauses the
limit), and `reset(name)` restores the declared values.

Queue reads (`queues.list()` and `queues.retrieve()`) now report a
`version` that discriminates the shape: `V1` queues keep today's fields
(their own `concurrencyLimit` and its override state), while `V2` queues
(tasks declared with `concurrency`) carry no queue-level concurrency,
since their limits are read and overridden through `concurrencyLimits`
(a task's inline limit under its derived `task/<task-id>` name).
Existing reads keep compiling: a `V2` queue reports `concurrencyLimit`
as null and `concurrency` as undefined.

### Patch Changes

- Updated dependencies:
  - `@trigger.dev/core@4.7.0`
## @trigger.dev/sdk@4.7.0

### Minor Changes

- Chat agents can now scope concurrency per session. Pass
`concurrencyKey` (for example, your chat ID or tenant ID) and
trigger-time named limits via `triggerConfig.concurrency` when starting
a chat session, from `chat.createStartSessionAction`, the `AgentChat`
client, or a handover. Keys are never defaulted, so a session without
one shares the task's keyless pool.
([`ea9758117`](triggerdotdev@ea97581))

  ```ts
  const start = chat.createStartSessionAction("support-chat", {
    triggerConfig: { concurrencyKey: user.id },
  });
  ```

- Concurrency limits can now be paused and resumed, just like queues:
`concurrencyLimits.pause(name)` stops every run holding the limit from
being dequeued while keeping its configured bounds, and
`concurrencyLimits.resume(name)` starts them again.
([`fa94febb6`](triggerdotdev@fa94feb))

  ```ts
  import { concurrencyLimits } from "@trigger.dev/sdk";

  await concurrencyLimits.pause("openai");
  await concurrencyLimits.resume("openai");
  ```

- Control a task's concurrency with the new `concurrency` option, and
share limits across tasks with named concurrency limits. An inline shape
caps the task itself; `concurrencyLimit()` declares a limit any task can
hold (up to two named limits per task), and a trigger call can switch a
run's named limits with its own `concurrency` option.
([`ea9758117`](triggerdotdev@ea97581))

  ```ts
  import { concurrencyLimit, task } from "@trigger.dev/sdk";

export const openaiLimit = concurrencyLimit({ name: "openai", total: 25
});

  export const generateSummary = task({
    id: "generate-summary",
    concurrency: [{ perKey: 1, total: 5 }, openaiLimit],
    run: async (payload) => {},
  });
  ```

`perKey` caps each `concurrencyKey` pool and `total` caps across
everything, keys or not. The queue-level `concurrencyLimit` option keeps
working unchanged and is deprecated in favor of `concurrency`.
Enforcement happens server-side; servers without support accept the
option but do not enforce it yet.

Manage limits at runtime with the new `concurrencyLimits` namespace:
`list()` and `retrieve(name)` report each limit's bounds plus its live
`running` and `queued` counts, `override(name, { perKey, total })`
changes only the given bounds (overriding `total` to `0` pauses the
limit), and `reset(name)` restores the declared values.

Queue reads (`queues.list()` and `queues.retrieve()`) now report a
`version` that discriminates the shape: `V1` queues keep today's fields
(their own `concurrencyLimit` and its override state), while `V2` queues
(tasks declared with `concurrency`) carry no queue-level concurrency,
since their limits are read and overridden through `concurrencyLimits`
(a task's inline limit under its derived `task/<task-id>` name).
Existing reads keep compiling: a `V2` queue reports `concurrencyLimit`
as null and `concurrency` as undefined.

### Patch Changes

- Add an optional `onSettled` callback to
`TriggerChatTransport.sendAction()` so callers can confirm that their
action's input was processed, independently of whether the response
stream closes.
([triggerdotdev#4956](triggerdotdev#4956))
- Chat agents now return to a durable wait when a session wake does not
deliver a matching message. This prevents resumed runs from staying
active until their maximum duration and preserves the configured turn
timeout across repeated wakes.
([`e6ec2c4e8`](triggerdotdev@e6ec2c4))
- Deprecate `queues.overrideConcurrencyLimit` and
`queues.resetConcurrencyLimit`. These operate on the legacy model where
a queue carried its own concurrency limit; declare concurrency with the
task `concurrency` option and manage it with
`concurrencyLimits.override` and `concurrencyLimits.reset` instead.
([`414e5a268`](triggerdotdev@414e5a2))
- Chat streams now report `Stream stalled: no records received` after
five retries of a connected stream that sends no records. Network
failures and browser wakeups retain automatic recovery. Healthy tool
calls with no records for about six minutes also reach this silence
limit. Watch subscriptions remain unlimited, and caller cancellation
still closes cleanly.
([triggerdotdev#4949](triggerdotdev#4949))
- Steering messages now remain in context across agent steps and keep
their original position in saved conversations, including custom
response data written between steps.
([`5619acf26`](triggerdotdev@5619acf))
- Updated dependencies:
  - `@trigger.dev/core@4.7.0`
## @trigger.dev/build@4.7.0

### Patch Changes

- Updated dependencies:
  - `@trigger.dev/core@4.7.0`
## trigger.dev@4.7.0

### Patch Changes

- Fix stale and empty project environment values in `trigger dev`, and
support empty values in `syncEnvVars()`.
([`f384e8334`](triggerdotdev@f384e83))
- Set up a new Trigger.dev account from the CLI. Login can prefill an
email address, save the user's full name after authorization, and resume
authorization later, while `init` can create the first organization,
activate its Free plan, and create the first project before scaffolding
the app.
([`fb3b26d4f`](triggerdotdev@fb3b26d))
- Added a `submit_feedback` MCP tool so coding agents can report a
confusing tool error, a docs mismatch, or a missing capability without
the user having to file it by hand. Turn it off with `--skip-telemetry`
or `TRIGGER_TELEMETRY_DISABLED`; the tool is hidden while it is off.
([`9d38ff508`](triggerdotdev@9d38ff5))
- Authenticate `trigger promote` with environment API keys supplied
through `TRIGGER_ACCESS_TOKEN`. Environment API key commands now use the
saved profile API URL when no explicit override is provided.
([`562c9433a`](triggerdotdev@562c943))
- Fix Windows deploys failing at indexing with `Cannot find module` on a
percent-encoded path when the project directory contains spaces or
non-ASCII characters.
([`b32c1d157`](triggerdotdev@b32c1d1))
- Updated dependencies:
  - `@trigger.dev/schema-to-json@4.7.0`
  - `@trigger.dev/core@4.7.0`
  - `@trigger.dev/build@4.7.0`
## @trigger.dev/python@4.7.0

### Patch Changes

- Updated dependencies:
  - `@trigger.dev/sdk@4.7.0`
  - `@trigger.dev/core@4.7.0`
  - `@trigger.dev/build@4.7.0`
## @trigger.dev/redis-worker@4.7.0

### Patch Changes

- Updated dependencies:
  - `@trigger.dev/core@4.7.0`
## @trigger.dev/rsc@4.7.0

### Patch Changes

- Updated dependencies:
  - `@trigger.dev/core@4.7.0`
## @trigger.dev/schema-to-json@4.7.0

### Patch Changes

- Convert Zod 4 `z.date()` fields to date-time strings in JSON Schema
without weakening validation for other unsupported types. This prevents
MCP tool discovery from failing when a tool input schema contains a
date.
([`f8babdf9b`](triggerdotdev@f8babdf))
- Updated dependencies:
  - `@trigger.dev/core@4.7.0`

</details>

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4.6.0+ with zod ≥ 4.4: every run completion waits a fixed 6 s (cancels 31 s) because FLUSH/CANCEL IPC acks are rejected

2 participants