fix(core): accept IPC acks for void callbacks under zod 4.4+ - #4980
Conversation
🦋 Changeset detectedLatest commit: f5dd1bc The changes in this PR will be included in the next version bump. This PR includes changesets to release 27 packages
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 |
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. Important Review skippedReview was skipped as selected files did not have any reviewable changes. ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
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)
🧰 Additional context used🧠 Learnings (1)📓 Common learningsWalkthroughThe 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 The change addresses delayed completion and cancellation acknowledgments. No concrete merge-blocking risk is identified; normal checks remain appropriate. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to 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 Security review detailsSecurity Blast Radius
Security Findings and Attack Paths
Trust Boundaries and Controls
Resilience and Maintainability Implications
Hardening Proposals
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
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. Comment |
|
@coderabbitai On the retained reliability concern (an ACK without Changeset wording fixed in d098714 as well. For maintainers: the failing webapp unit/E2E checks are the fork-secrets issue ( |
|
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' || trueLength of output: 16548
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
You are interacting with an AI system. |
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>
42f254f to
f5dd1bc
Compare
## 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>
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
FLUSHandCANCELdeclarecallback: z.void(), so the task run process acks them with{ type: "ACK", id, message: undefined }. Node's JSON IPC serialization drops the undefinedmessagekey. Under zod 4.4+ az.any()object key is required at parse time (it was optional under zod 3 and 4.0–4.3), soPacket.safeParserejects every such ack,#handlePacketreturns silently, andsendWithAckwaits out its full timeout.Because
zodIpc.tsimportszod/v4and@trigger.dev/coreresolveszodfrom 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
messageoptional again (z.any().optional()). Only the ACK packet can legitimately carryundefined, so the other packets are unchanged.✅ Checklist
Testing
packages/core/test/zodIpc.test.ts: forks a real child process (test/fixtures/zodIpcChild.ts) and sendsFLUSH(void callback) andPING(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. Onmain(zod 4.5.4 in this repo) the void-callback test fails withsendWithAck() timeout; with the fix it resolves immediately.pnpm run build --filter @trigger.dev/corepasses.completed_at - started_at - usage_duration_mson successful runs):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