feat/hosted-webhooks-api
1991 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
693c646424 |
fix(sdk): await the in-flight stream edit before the final channel egress
In stream delivery the debounce editor's stop() cleared the pending timer but returned immediately even with an edit request already in flight. The turn then issued the authoritative final edit to the same message ref, so a slower in-flight partial-text edit could land after it and leave truncated text as the last write. stop() now awaits any in-flight edit (and makeChannelStreamTap's flush/cancel and the turn finally await stop()), so the final edit is always the last write. |
||
|
|
093d0cd652 |
fix(sdk,slack): move the Slack webhook source into @trigger.dev/slack
The generic webhooks.slack() SDK source was auto-generated from the core provider config, which carries only the HMAC verifier. Slack additionally needs the one-time url_verification handshake (before an Event Subscriptions URL can be saved) and form-encoded interactivity parsing, so an endpoint built from webhooks.slack() could never be connected. Drop slack from the generated SDK producers (runtime + type) and export webhookSource() from @trigger.dev/slack, which carries the handshake + formPayload the connector already uses. Users get a plain Slack webhook source from webhookSource(), or the full chat channel from slack(). Core's slack config stays for server-side verification. |
||
|
|
b9797ec886 |
fix(sdk): guard success-path channel reaction callbacks so a throw can't fail a delivered turn
The turn-start 'working' resolver and the success-path 'done' resolver both awaited user-supplied reactions.working / reactions.done callbacks unguarded. A rejection propagated into the turn catch, which then posted an error to the channel and skipped onTurnComplete / the snapshot write, turning an already-delivered answer into a reported failure. Wrapped both in try/catch that degrade to a warn, matching the error-path reaction handling. |
||
|
|
7ddc72d5d2 |
fix(slack): update self-message-guard test for unspaced in-list; fix ai.ts formatting
The SELF_MESSAGE_GUARD in-list was unspaced earlier to match the documented filter style, but its unit test still asserted the spaced substring. Update the assertion. Also apply oxfmt to a wrapped line in the channel error-reaction block that exceeded the width limit. |
||
|
|
cc9f8c6949 | docs(sdk): note the interaction-resume double-call in hydrateMessages jsdoc | ||
|
|
9f8015ee92 |
fix(slack): make chat.update edits authoritative by clearing stale blocks
A text-only reply issued chat.update with text alone, so Slack kept whatever blocks were already on the message (e.g. approval controls previously rendered into the same ts). Send blocks: [] on edits when the outgoing message has no blocks, so an edit is authoritative. postMessage still omits blocks when empty. |
||
|
|
c2f761a289 |
fix(sdk,slack): correct GitLab verifier and Slack secret provisioning, skip unroutable channel turns
- webhooks.gitlab used the Svix preset (svix-id/svix-timestamp/svix-signature,
HMAC over {id}.{timestamp}.{body}), but GitLab doesn't sign bodies: it echoes
the configured token in X-Gitlab-Token. Every GitLab delivery failed
verification. Switched it to the shared-secret scheme (header, x-gitlab-token).
- The Slack connector declared secretProvisioning 'integrator', so the Connect
panel offered a Generate-secret button, but Slack mints its own Signing Secret
that must be pasted (core already classifies slack as 'provider'). Changed to
'provider'.
- A verified channel delivery naming a connector the running agent doesn't have
fell through to a normal turn with an empty incoming message, so the agent
answered nothing and burned tokens. Handle the unresolved-connector case like a
stale interaction: warn with the connectorId/deliveryId and skip the turn
without consuming a turn.
|
||
|
|
f8d545956e |
fix(sdk,slack): hydrate for channel HITL whenever the pending tool call is absent; unspace Slack guard in-list
- The channel-interaction pre-hydrate was gated on an empty accumulator, so a hydrate-backed agent whose in-memory accumulator was non-empty but no longer held the pending tool part (after a chat.history mutation or a compaction trim) would skip the hydrate and drop the click as stale. Gate instead on the pending toolCallId being absent from the accumulator, so the persisted chain is reloaded whenever the call can't be found (a duplicate click on an already-resolved call still short-circuits without a needless hydrate, since the call is present). - Unspace the Slack SELF_MESSAGE_GUARD in-list to match the documented filter style. The runtime parser already skips whitespace so this is cosmetic, but it keeps the guard consistent with the docs example. |
||
|
|
d921d0152b |
fix(sdk): stop leaking reasoning to channels, fix fallback-key namespacing, don't clobber posted answers
- channelReplyText selected parts with 'text' in p, which also matches AI
SDK reasoning parts ({ type: 'reasoning', text }). With sendReasoning
defaulting to true, a reasoning-capable model's private chain-of-thought
was concatenated into the text posted to Slack. Narrowed the selection to
parts whose type is exactly 'text'.
- normalizeKeyString namespaced only the whole placeholder, so a fallback
key like {a || b} (the Slack connector's DEFAULT_KEY form) left every
alternative after the first bare, diverging from ValidatedWebhookKey which
validates each || side and defaults bare sides to the body. It now splits
on ||, trims, and namespaces each alternative. Added test cases.
- On the turn-error path the error text was sent with previousRef pointing
at the ack placeholder. If a hook after the final answer was posted threw
(e.g. onTurnComplete), the delivered answer was edited into the error
string. Track whether the final answer was posted and, if so, send the
error as a fresh follow-up instead of editing over the answer.
|
||
|
|
811286b4a1 |
fix(sdk): hoist clientData parse so channel-interaction hydrate doesn't hit a TDZ
The channel-interaction pre-hydrate added for hydrateMessages agents referenced clientData, but clientData was declared with const later in the same turn block. The early reference hit the temporal dead zone and threw ReferenceError, which the local try/catch swallowed, so hydration silently never ran and the button click was still dropped as a stale interaction. Parse clientData once above the channel-event block so both the interaction hydrate and the rest of the turn share the same value. |
||
|
|
92fe257148 |
fix(sdk): drop duplicate channel interaction clicks and harden channel error egress
- buildInteractionResolutionMessage matched a tool part by toolCallId without checking it was still input-available, so a duplicate or retried approval/deny click on an already-answered tool call built a fresh resolution and ran the decision a second time. Gate on state === 'input-available' so a resolved call resolves to a dropped stale interaction, matching the function's documented contract. - The turn-error channel egress only edited the start-of-turn ack placeholder, so a connector configured with ack: null posted nothing on a failed turn (the user saw silence). Send a fresh final message when there is no placeholder ref. The error text is now run through the agent's onError option (same sanitizer the browser stream uses) instead of forwarding turnError.message verbatim to the channel surface. |
||
|
|
e2f3dc60bd |
fix(sdk): resolve channel HITL against the persisted chain for hydrateMessages agents
A channel interaction (e.g. a Slack approval click) is resolved at the top of the turn by matching its toolCallId against the in-memory accumulator. For agents that register hydrateMessages, the accumulator is empty on a fresh continuation / OOM-retry boot (those agents own persistence, so the boot seeding block is skipped and the hydrate hook only runs later in the turn). The click therefore matched nothing, was classified as a stale interaction, and was dropped, losing the human's decision and leaving the paused turn unresolved. When an interaction arrives and hydrateMessages is registered but the accumulator is empty, load the persisted chain via the hydrate hook first so the interaction can find its pending tool call. The per-turn hydrate later in the turn still runs to persist the synthesized resolution message. Guarded on an empty accumulator so warm turns don't re-hydrate. |
||
|
|
f86d3cd3fe |
fix(sdk): stop losing channel deliveries mid-turn and dying on channel error callbacks
Two channel-connector fixes in the chat.agent turn loop: - Under a pendingMessages-enabled agent, the mid-turn session.in handler returned early for any record without a steerable `.message`. Channel deliveries carry `channelEvent` and no `.message`, so an inbound Slack message or block_actions approval click arriving mid-turn was neither steered nor buffered, and never became a turn. Channel records now fall through to pendingWireMessages so they dispatch as the next turn. - In the turn error handler, resolveReactionChoice (which invokes the user's reactions.error callback) and channelConn.outbound ran unguarded. A throw escaped the catch, skipping the error chunk, turn-complete, and wait-for-next-message, killing the whole conversation. Both are now wrapped so a bad user callback degrades to a warn. |
||
|
|
e75a7ca1ae |
fix(sdk): detach the per-turn message listener when dropping a stale channel interaction
A duplicate or late channel interaction (e.g. a repeated Slack approval click) whose toolCallId matches no pending tool call was dropped without detaching the per-turn session.in listener. That listener marks inbound records CONSUMED, so after the drop the next user message was swallowed into pendingWireMessages (already drained before the wait) and the turn idled until the timeout, ending the run and losing the message. Mirror the action path: detach msgSub and decrement turn (a dropped interaction is not a turn) before falling through to the wait. |
||
|
|
8dd16986e7 |
docs(sdk): correct the channel-connector default in the chat.agent test harness
sendChannelEvent/deliverChannelEvent default connectorId to DEFAULT_TEST_CONNECTOR_ID, not to a sole configured connector's id as the previous doc implied. Describe the actual default (which lines up with recordingChannelConnector's own default id). |
||
|
|
478943d24f |
fix(slack): keep approve/deny controls for other pending tool calls after one decision
defaultSlackFinalizeInteraction re-rendered the message by dropping every actions block, so once one tool call was approved or denied the other pending tool calls in the same message lost their buttons and their turn could not resume. Drop only the resolved tool call's actions block (matched by the toolCallId:: button value prefix) and keep the rest. |
||
|
|
b3bb085fe3 |
fix(sdk): bound the webhook key-template regex against ReDoS
The key placeholder regex used [^}]+, so a run of "{" with no closing brace
backtracks quadratically: every start position rescans to the end. Exclude "{"
from the character class so a non-matching position fails immediately (a valid
{path} placeholder never contains "{"), making normalization linear.
|
||
|
|
7a4ae6d7de |
test(sdk): channel interaction coverage in the chat.agent test harness
Add a fire-and-forget deliverChannelEvent to the harness and loop-level tests for the channel interaction paths: a resolved interaction callback resumes the pending tool and finalizes the controls, and a stale callback that matches no pending tool is dropped without running a turn or posting anything. |
||
|
|
ff7b512eff |
fix(sdk): channel stream editor tail, abort cleanup, and error egress
Three fixes to chat.agent channel egress: The debounced stream editor now re-arms after an edit it skipped because a previous edit was still in flight, so text buffered during that window still reaches the channel instead of stalling until the next delta. The stream editor is stopped when the reply stream is aborted or cancelled, not just on normal completion, so a late timer can no longer edit the channel message after the turn has ended. A turn that throws now edits the placeholder to show the error, so a channel user sees the failure instead of a message stuck on the loading placeholder. |
||
|
|
7d6eb9f011 |
test(sdk): channel connector support in the chat.agent test harness
Add channel-event delivery to the mockChatAgent harness (sendChannelEvent) and a recordingChannelConnector helper to @trigger.dev/sdk/ai/test, so a chat.agent's channel round-trip (inbound mapping, ack placeholder, egress send, edit-in-place, and lifecycle reactions) can be driven and asserted entirely offline. |
||
|
|
4ebed26e1c |
fix(slack): bound the markdown link regex to avoid quadratic backtracking
The link replacement in toSlackMrkdwn used unbounded character classes, which can backtrack quadratically on pathological input. Bound the link text and URL lengths and exclude newlines. Also tightened two test assertions that used unsafe optional chaining. |
||
|
|
9a23c2b49a |
chore(slack): declare @types/node explicitly instead of relying on hoisting
The Slack package sets types: ["node"] in its tsconfig but did not declare @types/node, so type resolution depended on workspace hoisting. Declare it at the repo-pinned version, matching the other packages that opt into node types. vitest stays root-provided, consistent with every other package. |
||
|
|
3ccb9c91cc |
fix(slack): render approval controls for every pending tool call
The HITL renderer only posted approve/deny buttons for the first pending tool call, so when a turn paused on multiple tool approvals the rest never got controls and the turn could not finish. It now renders a section plus an approve/deny pair for each pending call. |
||
|
|
52fc043ec4 |
fix(sdk): drop stale channel interaction callbacks instead of starting a turn
A channel interaction callback (for example a Slack button click) that resolves to a tool call with no matching pending tool part is a stale or duplicate callback. It was falling through to the inbound-message path, which acked, reacted, and ran a full agent turn. Such callbacks are now dropped: no turn runs and the run returns to its idle wait for the next message. |
||
|
|
b7fffa3ad5 |
fix(slack): truncate oversized tool input in approval blocks
The HITL approval block serialized the tool input without a bound into a Slack section text field, which is capped near 3000 characters. A large input made chat.postMessage fail with invalid_blocks so the approve and deny controls never appeared. The serialized input is now capped to keep the block within the limit. |
||
|
|
494d8e9964 |
fix(cli,core): fail indexing on duplicate webhook ids
Two webhook() declarations sharing an id used to silently overwrite each other in the worker manifest. Indexing now fails with the colliding ids and their file paths, matching how duplicate task ids are already handled. |
||
|
|
8573e7241f |
fix(cli): send declared webhooks in the deploy worker metadata
The deploy path now forwards declared webhooks to the server the same way dev does, so hosted webhook endpoints are created and stay active on deploy instead of only working under trigger dev. |
||
|
|
cf566adb81 | feat(sdk,slack): webhook sources, agent channels, and human-in-the-loop | ||
|
|
c0b84595a3 |
feat(webapp): hosted webhook ingress, delivery pipeline, and dashboard (#4344)
## Summary The server half of hosted webhooks: the public ingress endpoint, signature verification, the delivery pipeline (Postgres partitioned storage + ClickHouse for ordering), the in-app partition manager, the HTTP API, and the dashboard (Deliveries, Endpoints, and the in-app test console). The public SDK and docs half is #4537. That PR carries the user-facing API (`webhook()`, `chat.event` / `chat.channels`, the `@trigger.dev/slack` connector) and builds on the shared `@trigger.dev/core` schemas that ship here. ## Shipping behind a flag A `WEBHOOK_ENABLED` env var (default off) gates the public ingress route and the engine worker plus partition cron, so merging and deploying this changes nothing in production until it is flipped on per environment. The dashboard is separately gated per org by the `hasWebhooksAccess` feature flag. ## Note on packages This PR includes the `@trigger.dev/core` schema additions the server compiles against, but carries no changeset. Core is not consumed independently of the SDK, so it is released together with the SDK via #4537. Keeping its changeset off `main` means no release cut from `main` publishes it early. |
||
|
|
c4b5e27258 |
feat(cli): build deployment images on prebuilt base images (#4602)
The generated deploy Containerfile now starts from the prebuilt base images published by base-images/ (`triggerdotdev/node` and `triggerdotdev/bun` on DockerHub, pinned by digest) instead of installing system packages during every project's build. Uncustomized projects run no apt at all and their base layers are identical across every project, so worker nodes cache one copy fleet-wide. The build stage uses the -build toolchain variant for uncustomized and package-only projects; projects with image instructions build FROM base so instructions and their downloads run exactly once. ### Notes - User packages install in their own sorted RUN with --allow-downgrades (a pin of a preinstalled package is a downgrade against the prebuilt base), preceded by a dpkg repair whenever instructions came first, since apt-get install refuses to run on state a dpkg -i instruction left broken. - Deployed runtime images inherit newer package versions than today's live-archive installs (the published bases upgrade everything to their snapshot), plus the base images' OCI labels. Runtime env, user, workdir, and entrypoint are unchanged. |
||
|
|
3e7964e7fa |
feat: surface cron windows in webapp, cli, sdk (#4572)
## Summary Adds execution-window product surfaces for both declarative and imperative schedules. - Declarative schedules can set `window` through `schedules.task()`, with support for whole-minute, hour, and percentage values. - Imperative schedules can create, update, clear, and inspect windows through the API and dashboard. - Schedule API responses preserve `nextRun` as the nominal CRON time and expose `nextRunEffectiveAt` as the stable assigned time. - The dashboard displays configured windows alongside assigned upcoming-run times. - Deploy output summarizes declarative schedules and suggests adding a wider window when the default 60-second placement range is used. ## Design Window validation remains authoritative on the server and ensures each window is compatible with the schedule cadence. Omitting a window uses the default 60-second range, while explicit zero-duration windows remain supported. Deployment summaries are derived from the deployment's stored task metadata, so they reflect the declarations associated with that deployment. |
||
|
|
fa7eea39d8 |
fix(core): stop custom metric exporters breaking the metrics export (#4613)
## Summary Projects that configure their own `metricExporters` or `metricReaders` in `trigger.config.ts` were losing task metrics on nearly every run, and seeing an unexplained `Failed to flush tracingSDK` alongside `OTLPExporterError: Bad Request` in their run logs. Spans and logs kept working, so the runs otherwise looked healthy. ## Root cause and fix Every configured exporter gets its own `PeriodicExportingMetricReader`, and `meterProvider.forceFlush()` fans out across all readers with `Promise.all`, so two collections can land on the same millisecond. `@opentelemetry/host-metrics` divides by the elapsed interval to compute `process.cpu.utilization` ([common.ts](https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/packages/host-metrics/src/stats/common.ts)), so a zero interval yields `0/0`. `JSON.stringify(NaN)` is `null`, and a collector rejects `"asDouble": null` with a 400 that drops the **entire** request, not just the offending point. `flush()` and `shutdown()` now walk the metric readers one at a time, so collections can no longer share a timestamp. Each reader is isolated, so one failing reader cannot skip the readers behind it, and every failure is logged with the reader that produced it. The first error is still rethrown, so callers see failures exactly as before. As a second layer, non-finite data points are dropped just before our own export, so a metric that divides by zero cannot take the rest of the batch with it. Exporters and readers supplied through `trigger.config.ts` are untouched by that filter and still receive raw data. The trade-off is that configured exporters now flush after the built-in one rather than alongside it, so flush latency is the sum rather than the max. An internal test package's dependency on core was replaced with a local helper, because core now needs that package in `devDependencies` and the two together formed a workspace cycle. ## Verification Tested against a real collector in a container: a batch containing a `NaN` reading is rejected with a 400 without the fix and accepted with it, and a single flush is asserted to collect from one reader at a time. |
||
|
|
1114d9d6f9 |
fix(redis-worker): stop fair queue leaking concurrency slots (#4540)
## Summary Fair queue consumers could leak the per-tenant concurrency slots that gate admission. Slots were freed on some paths and skipped on others, and once enough leaked slots accumulated for a tenant, every queue that tenant owned stopped being served until someone cleared the set by hand. This PR frees slots on every path and, more importantly, makes the remaining failure modes self-healing. ## Design The fix applies one rule uniformly: releasing a concurrency slot is best-effort cleanup and must never block the message's primary state transition. Blocking completion re-delivers the message, which duplicates customer work; blocking a retry loses the attempt increment, so the message can circle forever; blocking a reclaim strands the message in flight. A leaked slot is the better failure in every one of those trades because it is the only one that is recoverable. A failed release is therefore logged and the transition proceeds. Leaked slots then heal through two mechanisms: - `reserve` re-admits a message that is already a member of its own concurrency set, since re-admitting it does not increase concurrency. A message whose earlier release failed can no longer be blocked by its own leftover slot. - A reconcile loop periodically removes any set member with no in-flight record (interval configurable via `reconcileIntervalMs`, default 60s). The check-and-remove is atomic, and it is sound because a message is always registered in flight before its slot is reserved, so a member with no in-flight record can only be a leak. This also covers leaks this PR cannot prevent directly, such as a release that resolves the wrong concurrency group from queue metadata. Ordering hardening from earlier revisions stays: slots are released before the in-flight record needed to describe them is discarded, the release Lua scripts write the message back to the queue before removing it from in-flight (Lua does not roll back on error), and dangling in-flight entries with no payload are dropped instead of being rescanned forever. Every guard test was verified to fail without its specific fix, including the duplicate-execution case: completing a message while its slot release fails used to re-deliver and re-execute it. |
||
|
|
6685cbd599 |
chore: release v4.5.11 (#4557)
## Summary 4 new features, 24 improvements, 10 bug fixes. ## Highlights - Allow `trigger deploy` to authenticate with an environment API key from `TRIGGER_ACCESS_TOKEN`. ([#4561](https://github.com/triggerdotdev/trigger.dev/pull/4561)) ## Improvements - Chat in the browser now reconnects when the connection drops mid-turn, instead of leaving the reply stuck as if it were still generating. Reports can be fetched as structured data with the `json` format, and the shortest report period is now one minute (`1m`, `30m`, `1h`, `7d`). The `mint-token` command's help is clearer too: a token minted without `--cap` is read-only, and `--ttl` shows the correct maximum lifetime of 7 days. ([#4418](https://github.com/triggerdotdev/trigger.dev/pull/4418)) - The dev environment onboarding now tracks real progress. After you run `init`, the setup checklist marks your project as initialized, and it updates live as your dev server connects and your tasks register. The blank state also adds a "Copy AI agent prompt" button that copies a ready-to-paste setup prompt (pre-filled with your project reference) for Claude Code, Cursor, or any coding agent. ([#4563](https://github.com/triggerdotdev/trigger.dev/pull/4563)) The `init` scaffold now imports from `@trigger.dev/sdk` instead of the deprecated `@trigger.dev/sdk/v3` subpath. - Deployed images now ship dependencies and bundled task code as separate layers. Repeat deploys with unchanged dependencies typically push and pull far less data, making deploys and worker image pulls faster. ([#4551](https://github.com/triggerdotdev/trigger.dev/pull/4551)) - The current-worker API now reports each task's queue, so you can see which tasks write to a given queue. ([#4525](https://github.com/triggerdotdev/trigger.dev/pull/4525)) - Watch-mode chat streams now survive quiet windows and page reloads, and a reply cut off by a lost connection shows an error instead of appearing finished. Aborting a resumed subscription only closes your local stream — call `stopGeneration(chatId)` or pass `stopOnAbort: true` to stop the run. Also fixed a race where quickly restarting a stream could break stop and reconnect, and stopping a chat now hands it back to your other tabs instead of leaving them read-only. ([#4516](https://github.com/triggerdotdev/trigger.dev/pull/4516)) ## Server changes These changes affect the self-hosted Docker image and Trigger.dev Cloud: - The dashboard agent now has a monthly message allowance and plan-based limits on watches. Queries stay read-only with clearer errors when busy, and messages with unusual characters no longer fail to send. ([#4516](https://github.com/triggerdotdev/trigger.dev/pull/4516)) - Meet the dashboard agent: a chat in every environment that answers questions about your runs, queues, errors and health with real data and links, replacing Ask AI everywhere it used to appear. Investigate a failed run, an error, a backed-up queue or a run that hasn't started to get a worked-through answer — what happened, why, and how to fix it, with every claim linked to the runs, errors and deploys behind it. It reads your data read-only, works on preview and dev branches with that branch's own data, and reads the same everywhere — dashboard, terminal, editor. A very long chat keeps working: the agent summarises the earlier part and carries on. **Watch…** on a run, queue, error or the health report tells you when things change: a run finishes, a queue clears or grows past a number you pick, an error comes back, an environment recovers. The answer arrives in the chat and, if you want, by email, Slack or webhook — and the agent can look into bad news on its own. A watch reaches you on any browser you sign in from, without opening the chat first. A sample of conversations is scored automatically so the agent keeps getting better; only the score and a one-line summary are kept, never your messages, data or code, and we can switch it off for your organization on request. Ask the agent instead of the Docs buttons in page headers — they stay there when the agent isn't available to you. Separately, a queue's wait times, peak depth, throughput and throttling can now be read from the API. ([#4418](https://github.com/triggerdotdev/trigger.dev/pull/4418)) - Add backend support for delaying cron schedules within a specified window with a minimum of 60 seconds. ([#4566](https://github.com/triggerdotdev/trigger.dev/pull/4566)) - Reduced recurring background database load from the billing-limit recovery check, so paused environments are reconciled with less overhead. ([#4590](https://github.com/triggerdotdev/trigger.dev/pull/4590)) - Validating a schedule when deploying or updating a schedule now does less work on projects with many preview branches, so those operations stay fast as branches accumulate. ([#4598](https://github.com/triggerdotdev/trigger.dev/pull/4598)) - Project pages now load faster for projects with a large number of preview branches, by no longer loading archived branch environments that aren't shown. ([#4595](https://github.com/triggerdotdev/trigger.dev/pull/4595)) - Database queries that filter on a list of values now reuse cached query plans more consistently, instead of forcing the database to re-plan whenever the list length changes. ([#4480](https://github.com/triggerdotdev/trigger.dev/pull/4480)) - Routine cleanup of old dashboard agent data now runs on its own schedule. ([#4599](https://github.com/triggerdotdev/trigger.dev/pull/4599)) - Database connection metrics are now reported for every configured database connection instead of only the primary one, and stay accurate regardless of connection type. ([#4541](https://github.com/triggerdotdev/trigger.dev/pull/4541)) - Deployment-related API endpoints now draw from their own generous rate limit budget, configurable via the `DEPLOYMENT_RATE_LIMIT_*` environment variables, so runtime API traffic no longer competes with deployments for the same per-environment budget. ([#4565](https://github.com/triggerdotdev/trigger.dev/pull/4565)) - Deleting or editing a secret environment variable is now fast and no longer slows down as a project accumulates variables. ([#4555](https://github.com/triggerdotdev/trigger.dev/pull/4555)) - Speed up personal access token lookups by indexing them on their owner ([#4588](https://github.com/triggerdotdev/trigger.dev/pull/4588)) - Switching project or organization in the sidebar now keeps you on the same page instead of sending you back to Tasks. Pages for a specific run, deploy or other single item open the matching list instead. ([#4585](https://github.com/triggerdotdev/trigger.dev/pull/4585)) - Reduced database load when loading the dashboard by removing an unused organization member count that was being calculated on every page navigation. ([#4587](https://github.com/triggerdotdev/trigger.dev/pull/4587)) - The environment variables page now loads a page at a time, keeping it fast for projects with a large number of variables. Search matches variable names across every page. ([#4597](https://github.com/triggerdotdev/trigger.dev/pull/4597)) - Groundwork for an alternative database connection driver, gated behind configuration and disabled by default, so there is no change to default behavior. ([#4539](https://github.com/triggerdotdev/trigger.dev/pull/4539)) - Deleting an alert channel is now fast and no longer slows down as a project builds up alert history. ([#4554](https://github.com/triggerdotdev/trigger.dev/pull/4554)) - Reduced internal overhead on the API under high load. ([#4532](https://github.com/triggerdotdev/trigger.dev/pull/4532)) - Out-of-date upgrade prompts no longer appear in the dashboard: the "V4" badges and the notices saying preview branches and the queues table need V4 have been removed. The side menu still warns you when a project is on v3, with updated wording and a link to the v4 upgrade guide. ([#4589](https://github.com/triggerdotdev/trigger.dev/pull/4589)) - Make background worker registration cheaper for projects with many scheduled tasks by scoping declarative schedule reconciliation to the current environment and dropping redundant schedule lookups. ([#4577](https://github.com/triggerdotdev/trigger.dev/pull/4577)) - Speed up setting and importing environment variables for projects with many variables. ([#4579](https://github.com/triggerdotdev/trigger.dev/pull/4579)) - Loading the deployments list is now faster, especially when filtering by deployment status on projects with many deployments. ([#4591](https://github.com/triggerdotdev/trigger.dev/pull/4591)) - Fixed the billing limits page timing out for organizations with many preview branches, especially while a spend limit was being enforced. The page now loads quickly, so you can raise or resolve your limit without delay. ([#4594](https://github.com/triggerdotdev/trigger.dev/pull/4594)) - Fix the Concurrency page showing the plan's default concurrency for the dev environment instead of the environment's actual limit. ([#4596](https://github.com/triggerdotdev/trigger.dev/pull/4596)) - Creating an organization sometimes left you back on the creation form even though the organization had already been created, so clicking Create again made a duplicate. Creating an organization now completes and takes you to your new organization. ([#4530](https://github.com/triggerdotdev/trigger.dev/pull/4530)) - Ensure creating a project completes instead of returning to its creation form after a navigation error. ([#4584](https://github.com/triggerdotdev/trigger.dev/pull/4584)) - Renaming a project now keeps you on the project settings page and tells you what happened, instead of silently moving you to the tasks page or clearing the form with no explanation. ([#4601](https://github.com/triggerdotdev/trigger.dev/pull/4601)) - Fixed support threads showing no account details for some customers, so the team can see your plan, organizations and projects when you get in touch. ([#4575](https://github.com/triggerdotdev/trigger.dev/pull/4575)) - In the light theme, the Format, Clear and Copy buttons on the query editor no longer blend into the query text behind them. ([#4592](https://github.com/triggerdotdev/trigger.dev/pull/4592)) - The health report now says start latency is "unknown" when there is no data for it, instead of showing a healthy-looking 0ms ([#4544](https://github.com/triggerdotdev/trigger.dev/pull/4544)) - Realtime streams written inside a chat session run now use the same backend as the session itself, and runs are no longer created against a backend that cannot serve them. ([#4564](https://github.com/triggerdotdev/trigger.dev/pull/4564)) - The grouped "watch updates" notification now shows the total number of results waiting, instead of only the most recent batch's count. ([#4525](https://github.com/triggerdotdev/trigger.dev/pull/4525)) <details> <summary>Raw changeset output</summary> # Releases ## @trigger.dev/build@4.5.11 ### Patch Changes - Updated dependencies: - `@trigger.dev/core@4.5.11` ## trigger.dev@4.5.11 ### Patch Changes - Chat in the browser now reconnects when the connection drops mid-turn, instead of leaving the reply stuck as if it were still generating. Reports can be fetched as structured data with the `json` format, and the shortest report period is now one minute (`1m`, `30m`, `1h`, `7d`). The `mint-token` command's help is clearer too: a token minted without `--cap` is read-only, and `--ttl` shows the correct maximum lifetime of 7 days. ([#4418](https://github.com/triggerdotdev/trigger.dev/pull/4418)) - Allow `trigger deploy` to authenticate with an environment API key from `TRIGGER_ACCESS_TOKEN`. ([#4561](https://github.com/triggerdotdev/trigger.dev/pull/4561)) - The dev environment onboarding now tracks real progress. After you run `init`, the setup checklist marks your project as initialized, and it updates live as your dev server connects and your tasks register. The blank state also adds a "Copy AI agent prompt" button that copies a ready-to-paste setup prompt (pre-filled with your project reference) for Claude Code, Cursor, or any coding agent. ([#4563](https://github.com/triggerdotdev/trigger.dev/pull/4563)) The `init` scaffold now imports from `@trigger.dev/sdk` instead of the deprecated `@trigger.dev/sdk/v3` subpath. - Deployed images now ship dependencies and bundled task code as separate layers. Repeat deploys with unchanged dependencies typically push and pull far less data, making deploys and worker image pulls faster. ([#4551](https://github.com/triggerdotdev/trigger.dev/pull/4551)) - Updated dependencies: - `@trigger.dev/core@4.5.11` - `@trigger.dev/build@4.5.11` - `@trigger.dev/schema-to-json@4.5.11` ## @trigger.dev/core@4.5.11 ### Patch Changes - Chat in the browser now reconnects when the connection drops mid-turn, instead of leaving the reply stuck as if it were still generating. Reports can be fetched as structured data with the `json` format, and the shortest report period is now one minute (`1m`, `30m`, `1h`, `7d`). The `mint-token` command's help is clearer too: a token minted without `--cap` is read-only, and `--ttl` shows the correct maximum lifetime of 7 days. ([#4418](https://github.com/triggerdotdev/trigger.dev/pull/4418)) - The current-worker API now reports each task's queue, so you can see which tasks write to a given queue. ([#4525](https://github.com/triggerdotdev/trigger.dev/pull/4525)) ## @trigger.dev/python@4.5.11 ### Patch Changes - Updated dependencies: - `@trigger.dev/core@4.5.11` - `@trigger.dev/sdk@4.5.11` - `@trigger.dev/build@4.5.11` ## @trigger.dev/react-hooks@4.5.11 ### Patch Changes - Updated dependencies: - `@trigger.dev/core@4.5.11` ## @trigger.dev/redis-worker@4.5.11 ### Patch Changes - Updated dependencies: - `@trigger.dev/core@4.5.11` ## @trigger.dev/rsc@4.5.11 ### Patch Changes - Updated dependencies: - `@trigger.dev/core@4.5.11` ## @trigger.dev/schema-to-json@4.5.11 ### Patch Changes - Updated dependencies: - `@trigger.dev/core@4.5.11` ## @trigger.dev/sdk@4.5.11 ### Patch Changes - Chat in the browser now reconnects when the connection drops mid-turn, instead of leaving the reply stuck as if it were still generating. Reports can be fetched as structured data with the `json` format, and the shortest report period is now one minute (`1m`, `30m`, `1h`, `7d`). The `mint-token` command's help is clearer too: a token minted without `--cap` is read-only, and `--ttl` shows the correct maximum lifetime of 7 days. ([#4418](https://github.com/triggerdotdev/trigger.dev/pull/4418)) - Watch-mode chat streams now survive quiet windows and page reloads, and a reply cut off by a lost connection shows an error instead of appearing finished. Aborting a resumed subscription only closes your local stream — call `stopGeneration(chatId)` or pass `stopOnAbort: true` to stop the run. Also fixed a race where quickly restarting a stream could break stop and reconnect, and stopping a chat now hands it back to your other tabs instead of leaving them read-only. ([#4516](https://github.com/triggerdotdev/trigger.dev/pull/4516)) - Updated dependencies: - `@trigger.dev/core@4.5.11` </details> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
480bede0ad |
feat(webapp,sdk): dashboard agent plan enforcement, component gallery — and fixes (#4516)
Plan enforcement for the dashboard agent — message quota and watch limits — plus the component gallery, fixes and test hardening from the same stack (#4548, #4549, #4550, #4552, #4556 merged here). ## Plan enforcement ([TRI-12863](https://linear.app/triggerdotdev/issue/TRI-12863)) **Agent message quota.** The Free-plan allowance becomes a real server-side limit with a durable counter. New `agent_message_usage` table keyed `(organization_id, period)` — deliberately not joined to chats, so deleting a chat can't free quota within the period. Both send paths count one user message (wakes never count) and refuse at the cap with `403 message_quota_reached`, which the client renders as an upgrade panel, never a silent drop. The refusal code is a single shared constant on both sides. **Watch limits.** A watch whose window exceeds the plan's `agentWatchMaxHours`, or that would push the org past its `agentWatchers` count, is refused with `watch_limit_reached` (409 on the API, an upgrade hint on the card). Plan limits only tighten the existing code ceilings (`min(plan, 24h)`, per-chat cap of 3 still applies). A plan limit of zero means zero, not unlimited. Questions answerable instantly are answered before any plan refusal — a one-shot consumes no slot and never sees an upgrade nag. **Fails open by design.** Cloud ships the actual per-plan numbers separately (TRI-12863 P0). Until then absent limits resolve to the unlimited sentinel and the upgrade UI is gated on billing presence — self-hosted sees no cap, no upsell, with tests proving the fallback. Both quotas are nudges, not security boundaries: a failing limit read never blocks a send. ## Component gallery An admin-only gallery of every agent card state: five `storybook.agent-*` pages (chat UI, view blocks, report, investigation, watch) with their shared shell and manifest, demo fixtures, two demo-only cards, toast examples, and the screenshot script. No LLM and no data — every state renders from fixtures under `dashboard-agent/demo/`, never reachable from a production path. Designers and reviewers can look at every state, including the report states, without seeding anything. ## And fixes **SDK: watch-mode chat subscriptions survive quiet windows** (TRI-13065, TRI-13070) — watch mode keeps reconnecting across empty long-poll windows and only stops on abort or a settled session; a passive subscriber can no longer stop a turn it doesn't own (`stopOnAbort` is explicit, default off). Review findings fixed alongside: a superseded stream's async teardown no longer removes the live successor's abort controller or multi-tab claim, and stopping a generation hands the chat back to the user's other tabs. **Query boundary pinned end-to-end** ([TRI-11165](https://linear.app/triggerdotdev/issue/TRI-11165)) — a route-level test drives `api.v1.query` with a real signed environment JWT (writes refused before ClickHouse, a read passes); `readonly=1` made non-overridable; a per-turn cap stops the model burning a turn rewriting a query it can't fix (deterministic SQL errors only — busy/transport rejections don't count). **chat.agent durability regression suite** ([TRI-11166](https://linear.app/triggerdotdev/issue/TRI-11166)) — testcontainers-backed coverage of the two audit criticals (cross-tenant isolation, no duplicate mid-stream turn, both control-broken) plus crash-resume, cursor-based refresh, clean rollback of a mid-write turn failure (torn by a real constraint violation), and OOM-restart replay. **Investigation sweep backoff** — stale investigations get an attempt counter and backoff so a poison row can't pin the sweep queue head (migration `0005`: `sweep_attempts`, `last_sweep_attempt_at`). ## Screenshots <img width="1440" height="791" alt="Screenshot 2026-08-06 at 00 36 19" src="https://github.com/user-attachments/assets/6a68cd42-8580-469d-afe7-e28d1eef18e1" /> 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
ed1bb72fb8 |
feat: implement cron window spread backend (#4566)
- New DB fields on Schedule and ScheduleInstance - Use `queueTimestamp` for the "effectiveAt" delayed start time, propagate it to Clickhouse TaskRun table - Disable fastpath for delayed jobs - Add schedule timing logic, API endpoints with windows, persistence - Calculate phase for every schedule, only persist when window is non-null - Additional o11y for phased rollout |
||
|
|
7b390e5984 | feat(cli,webapp): allow deploys with environment API keys (#4561) | ||
|
|
0b750d00dd |
feat(webapp): dashboard agent — Watch (#4525)
Watch is the agent noticing something later: you ask it to tell you when a condition holds, and it answers when it does — or when it can't any more. A watch is a **durable one-shot promise**. The condition is checked on a schedule by deterministic code (no LLM in the checks), the answer lands in the chat once, and then the watch is over. Ten kinds: three on a run, five on a queue, error recurrence, health recovery. ## Stack Stacked on **#4529** (UI), which is stacked on **#4418** (chat, reports, investigate). Merge those first. **#4516** (storybook gallery) sits on top of this branch. ## How to review [**GUIDEBOOK.md**](https://github.com/triggerdotdev/trigger.dev/blob/feat/dashboard-agent-flows-watch/internal-packages/dashboard-agent/GUIDEBOOK.md) on this branch is the behaviour reference — it states the conditions rather than the code, so you can predict what happens without running anything. "The ten watch kinds, and what makes each fire" and "Creating a watch" describe exactly this PR, and the tables there are the spec the code is written against. ## What's inside - **Ten watch kinds**, one deterministic check each (`dashboardAgentWatch*Checks.ts`), with the spec union in `dashboard-agent-contracts/src/watch.ts`. - **Scheduling** — each watch schedules its own next check; due watches of one `(environment, cadence)` group can be checked together in one batch pass, with a sweep as the backstop for expiry, redelivery and retention. - **Delivery** — the in-chat wake and card, an optional email alert (new `DASHBOARD_AGENT_WATCH` alert channel, so it shows on the project's Alerts page with one-click unsubscribe), and an optional investigation when the outcome needs attention. - **Submission ledger** — `watch_submissions`, keyed `(chat_id, client_request_id)`, so a retried card submission replays the recorded outcome instead of creating a second watch. - **Watch token** — a delayed-execution credential accepted only by the watch endpoints, re-checked against the user's live access on every tick. - **Unread work** — the panel polls for wakes that landed while it was closed, so a chat can go unread and light the launcher dot. ## Key decisions **A check result is a 4-way, and only two of them are verdicts.** `satisfied` / `terminal_unsatisfied` are answers; `pending` and `unavailable` are not. Any exception inside any check is caught in one place and becomes `unavailable` with an unverified observation — a check that failed is never evidence. **A completed window is an answer, and whether it is good or bad news is declared per kind, never inferred.** There is a table for that in the guidebook: `run_failed` completing its window is *good* news ("hasn't failed"), `backlog_drain` completing it is not. One rule overrides the table: a window that completed on an unverified observation is neutral and says only that the watch ended without a confirmed answer. **An unreadable source is never a negative answer** — and, because investigations only open on `attention`, it never starts one either. **Identity is `(chat, project, environment)` plus the condition,** enforced by a partial unique index over active rows (`watches_chat_active_identity_key`), not by the read-then-insert check. Cadence, window, note and `ticks` are deliberately not part of it. Two different chats may watch the same thing — a watch is a promise to a chat. **The server resolves the target's name, whatever the model calls it.** The model can't tell a task queue (`task/<id>`) from a custom queue, so both spellings are tried and the stored one wins — and the rewrite happens **before** identity and before the row is written, so the identity, the checks, the link and the wording all see one spelling. **Freshness fences.** Depth falls back from the live counter to the newest 60 s ClickHouse bucket, which only counts as current within 60 s of now. A non-current reading at or below the *quiet line* is refused as `unavailable` rather than believed, so a stale empty bucket is never read as "drained". The stall streak is the one piece of carried state: it lives in the previous check's facts and *freezes* on an unreadable reading rather than breaking. **Chain reliability.** There is no shared cron — each watch (or batch group) schedules its own next tick, so the failure mode to review is the chain dying. A failed batch check is caught, the next tick is scheduled anyway and the run resolves rather than failing, so the chain survives a check that couldn't run; the sweep re-arms groups and finalizes anything still active past its deadline, even when delivery isn't configured. Wake redelivery is id-deduped rather than conditional, because the sweep can't know whether the user was already told. Access is re-authorized on **every** check against the primary — replica lag would extend access the user has already lost. **Wording lives in one place.** `watch-wording.ts` is read by the card, banner, toast, email and the agent's own narration, and the numbers come from the frozen observation rather than a fresh read, so a retry produces the same sentence. Replay reproduces the **recorded** decision instead of deciding again — the transcript is append-once, so a second decision would contradict it forever. **Cancellation is the ending without an answer** — no resolution, no wake. One exception, decided during testing: a watch the *user* cancelled leaves a single neutral transcript line ("Stopped watching …"), keyed off the watch id so a retry can't repeat it. The other four reasons stay silent. **Email is opt-in and only a fired watch emails.** An expiry is narrated in the chat and nowhere else. Both gates (agent access, a configured email transport) are checked at subscribe time *and* again at delivery, and the subscription outcome is frozen on the ledger row so a retry replays it. Neither gate is a plan check. **One watch offer per turn.** The prompt and the renderer guard this independently — if the turn already proposed a watch card, the action button is dropped, because the card is the better affordance. Two eval cases pin the prompt side: exactly one offer with the line last and the button after it, and zero offers when the rendered card already carries one — deterministic assertions, over a real-model run. ## Testing Unit tests (vitest, testcontainers, no mocks) under `apps/webapp/test/dashboardAgentWatch*.test.ts` and `internal-packages/dashboard-agent/src/watch-*.test.ts` cover the invariants above: the 4-way check results and the freshness fences, identity/dedup and the submission ledger, queue-name resolution, the batch chain surviving a failed check, sweep boundaries and alert-once, tenancy and the watch token's scope, and the wording snapshot. The load-bearing ones were verified by control-breaking the guard first and checking the test goes red. Live-tested end to end against a local stack, following the guidebook: all ten watch kinds firing and expiring, cancellation, the email pair (a fired watch mails, an expired one does not), and watch recovery from a health report. |
||
|
|
4569657923 |
feat(webapp): dashboard agent — chat, reports, investigate (#4418)
## What & why This is the system behind the Dashboard Agent — an assistant that answers questions about a project's runs, errors, queues, deploys and health, and can investigate failures end to end. The agent runs as a chat.agent task in its own Trigger project. It has no access to the main database or ClickHouse; all platform data is read through the public API using a delegated, read-only user token. Everything here is behind `canAccessDashboardAgent` and inert with the flag off. The UI that mounts the panel lands in #4529. ## Stack `#4418` (this, base) ← `#4529` UI ← `#4525` Watch ← `#4516` storybook gallery. The scenario/contract reference for the whole stack is `internal-packages/dashboard-agent/GUIDEBOOK.md` (it lands on the Watch branch): it states, per feature, what makes each thing happen and where that is decided. ## What's inside **Agent runtime and tools** — `internal-packages/dashboard-agent`: prompt, tool set (API reads, TRQL query, docs, navigation, evidence/investigations, repo source), conversation compaction, a prompt-prefix token budget pinned by snapshot test, and sampled LLM-judged turn evals. The package cannot import webapp server code, which is what makes the "no DB access" claim structural rather than a convention. **Contracts** — `internal-packages/dashboard-agent-contracts`: `trigger://` URIs, intents, and the block envelope every rendered card travels in. **Conversation store** — `internal-packages/dashboard-agent-db`: drizzle over postgres-js in its own `trigger_dashboard_agent` Postgres schema, plus one additive migration. **Auth boundary** — the user-actor token gains an optional environment claim; one guard (`userActorEnvironment.server.ts`) enforces it so routes don't each re-derive the rule. Token minting, cap ceiling, and the RBAC fallback path for self-hosted. **Transport** — webapp resource routes that mint the token and proxy each turn, and SDK-side mid-turn reconnect. **Public API the agent reads through** — orgs, projects, environments, runs, queue metrics, workers, a run's commit metadata, repo snapshot, reports, and `POST /api/v1/query`. **Reports** — the health report's layout is declared once and shared by the card, the markdown surface and the JSON/MCP surface, so the same report reads the same in the dashboard, the terminal and an editor. **Block renderers** — the report and investigation cards the flows above already emit (`app/components/dashboard-agent/`). The panel that hosts them, and the rest of the chat UI, is #4529. **Query safety and CSP** — see below. ## Key decisions - **The agent is a separate Trigger project, not webapp code.** It reads platform data over the public API with a delegated user-actor token whose `cap` ceilings it to read scopes. No Prisma, no ClickHouse, no webapp imports. - **The PAT-only auth helper now refuses user-actor tokens.** This is an intentional behavioral change: its callers consume only a bare userId and do not enforce delegated-token capabilities. Actor-aware routes continue through the scoped route builders instead. - **RBAC fallback builds a delegated token's ability from its own cap**, never the blanket ability a PAT gets (read-only when the token declares none). Without this, the agent's read-only cap would buy a write JWT on self-hosted. - **Org creation checks RBAC only for user-actor tokens, and only after the env gate**, so an install with `ORG_CREATION_API_ENABLED` off returns 404 rather than 403, and an ordinary PAT never consults an ability the route has no org to scope. Both orderings are pinned by test. - **The query path is read-only in depth.** TRQL rejects write statements at the grammar level (they don't parse, rather than being filtered), ClickHouse runs with `readonly=1`, and the org/project/env filters are injected server-side from the credential — the request body cannot widen scope. An unparseable query denies instead of falling through to the permissive resource. - **Document-wide img-src CSP.** Remote images are an outbound-request/exfiltration surface, so the policy permits only own-origin/data/blob, the required SSO avatar hosts, and the favicon endpoint. Operators can add exact origins through CSP_IMG_SRC_ALLOWLIST; wildcard hosts and bare schemes are intentionally not allowed. - **The chat transport reconnects on a mid-turn EOF** (`@trigger.dev/sdk`). A body that ends without a turn-complete is terminal only when the server says `X-Session-Settled: true`; otherwise the transport resubscribes from `lastEventId` with bounded backoff, and any record re-earns the budget. Previously a closed long-poll window or a proxy restart left the reply stuck as if still generating. - **Conversations live in their own datastore**, schema-scoped and foreign-key-free (it references `organizationId`/`userId` by id, because in cloud it is a different database). It is a display read-model for the History tab and transport resume; `chat.agent`'s object-store snapshot remains the model's source of truth. - **Deterministic first.** Reports and health checks contain no LLM — they are computed from the same data the dashboard shows, and the model only narrates and links them. That is what makes a number in an answer auditable. ## Testing - 63 new test files, run with `pnpm run test --filter webapp` and per-package vitest. Heaviest coverage on the auth boundary (`userActorPatOnlyBoundary`, `userActorTokenClaimsAndScopes`, `contextlessPatRoutes`, `rbacFallbackBranch`), TRQL read-only, the report layout, and the SDK reconnect. - The agent package has a separate eval lane (`pnpm run test:evals`, `vitest.eval.config.ts`) that hits the real model, so it never runs in `pnpm test`. - Live-tested against a local stack scenario by scenario; the GUIDEBOOK lists the condition each behaviour is expected under, which is what those runs were checked against. ## Changelog `.server-changes/dashboard-agent.md`, plus changesets for `@trigger.dev/core` (report schemas), `@trigger.dev/sdk` (chat reconnect) and the CLI's `mint-token` help text. |
||
|
|
6449a644b9 |
feat(webapp,cli,database): track real dev onboarding progress (#4563)
## Summary
The dev environment "Get set up" panel used to be a static list of CLI
commands that only disappeared once your tasks registered, so nothing
ever changed after you ran `init` and people assumed it was stuck. It
now tracks real progress: `trigger init` records the project as
initialized, so step 1 checks off, and the panel updates live as the dev
server connects and your tasks register.
It also adds a prominent "Copy AI agent prompt" button, presented as a
clear alternative ("or") to the manual CLI steps, that copies a
ready-to-paste setup prompt pre-filled with your project reference for
Claude Code, Cursor, or any coding agent.
## Notes
- Adds a `Project.initializedAt` column (migration
`20260811065646_add_project_initialized_at`); the CLI `init` command
calls a new project-scoped `POST /api/v1/projects/:ref/init` best-effort
at the end of setup.
- The `init` scaffold now imports from `@trigger.dev/sdk` instead of the
deprecated `/v3` subpath.
## Screenshots
<img width="2400" height="1794" alt="v7-redesigned-card"
src="https://github.com/user-attachments/assets/c2fb4fa1-9484-4700-8bd3-110d66f5a44e"
/>
|
||
|
|
bd8ce4a50f |
feat(deployments): split project dependencies and code into separate layers (#4551)
Deploy images previously shipped node_modules and the bundled task code in a single layer, so every deploy re-pushed and re-pulled the full dependency tree even when nothing in it changed. The generated Containerfile now copies `/app/node_modules` as its own layer and the app files separately. With unchanged dependencies the dependency layer is identical across deploys, so registries and workers already have it and only the code layer moves. |
||
|
|
72f50c2dad |
chore: release v4.5.10 (#4440)
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 0s
🚀 Publish Trigger.dev Docker / units (push) Failing after 0s
🚀 Publish Trigger.dev Docker / publish-webapp (push) Has been skipped
🚀 Publish Trigger.dev Docker / publish-worker-v4 (push) Has been skipped
🚀 Publish Trigger.dev Docker / scan-webapp (push) Has been skipped
🚀 Publish Trigger.dev Docker / scan-supervisor (push) Has been skipped
🧭 Helm Chart Release / lint-and-test (push) Has been cancelled
🚀 Publish Trigger.dev Docker / 📣 Dispatch main image (push) Has been cancelled
🧭 Helm Chart Release / release (push) Has been cancelled
|
||
|
|
04f9c4e1a5 |
fix(webapp,run-engine,core): drop the hidden debounce ceiling, fail fast on an unusable maxDelay (#4521)
Debouncing with a `delay` longer than an hour did nothing at all. The engine applied a server-side ceiling on how long a debounced run could be pushed back, measured from the run's `createdAt` and defaulting to one hour. A run is only pushed back while its new execution time stays inside that ceiling, so a `delay` at or above it could never push anything: the waiting run was released, the trigger started its own run, and the next trigger repeated it. A `delay: "12h"` produced one run per trigger, each correctly delayed by 12h, with no error raised and nothing on the run to show the debounce key had been ignored. The ceiling is now unset by default. A debounce key with no `maxDelay` keeps collapsing triggers for as long as they keep arriving, which is what the docs have always described. Self-hosters who want a bound can still set `RUN_ENGINE_MAXIMUM_DEBOUNCE_DURATION_MS`. That has a consequence worth stating plainly, so the docs now carry a warning for it: with no `maxDelay`, a continuously triggered key never executes. Set `maxDelay` when the work has to happen eventually. **Failing fast on an unusable `maxDelay`.** A caller who sets `maxDelay` no longer than their `delay` hits exactly the dead end described above, so that pair is now rejected at trigger time instead of silently behaving as if no debounce were set: ``` debounce.maxDelay (1h) must be longer than debounce.delay (12h). A debounced run is only pushed back while it stays inside maxDelay, so with these values every trigger would create its own run. ``` An unparseable `maxDelay` is rejected too, rather than quietly falling back to no bound at all, and so is a `delay` given as a date rather than a duration, which could never work because the value is re-applied on every push. The same check runs against a configured server ceiling, so a self-hosted deployment that sets `RUN_ENGINE_MAXIMUM_DEBOUNCE_DURATION_MS` gets the error rather than the silent failure this PR is about. With no `maxDelay` and no configured ceiling, which is the default, there is nothing to conflict with and nothing is rejected. The docs, the `TriggerOptions` JSDoc and the engine option all now state that the room available to push is the gap between `delay` and `maxDelay`. The run engine suite gains the case that motivated this: four triggers on one key with a 12h delay now collapse to a single run. |
||
|
|
c084fa6e29 |
fix(sdk,react-hooks): forward debounce when batch triggering with an array (#4520)
Passing `debounce` in the per-item options of a batch trigger did
nothing when the items were an array. The option was accepted by the
types and by the API, then dropped before the request went out, so every
item created its own run instead of collapsing onto the debounce key.
Four public entry points were affected: `task.batchTrigger`,
`task.batchTriggerAndWait`, `tasks.batchTrigger`, and
`tasks.batchTriggerAndWait`. The streaming (async iterable) forms of the
same calls were already correct, as were `batch.trigger`,
`batch.triggerAndWait`, `batch.triggerByTask`, and
`batch.triggerByTaskAndWait`.
`useTaskTrigger` in `@trigger.dev/react-hooks` had the same silent drop
on the single-trigger path, so that is fixed here too. It also drops
`machine`, `priority`, `region`, `idempotencyKeyTTL`, and
`idempotencyKeyOptions`; those are left alone, since forwarding them is
a behaviour change beyond this bug.
Each batch item builder constructs its options field by field, which is
why one of them could fall behind without anything catching it.
TypeScript did not help: the literal is returned from a `.map` callback
inside `Promise.all`, so excess-property checking never fired against
the `BatchItemNDJSON[]` annotation, and the server's schema silently
strips unknown keys. A misspelled option name therefore reproduced this
bug with no compile error and no server error. Every builder now ends in
`satisfies BatchItemNDJSON`, which does catch it:
```
error TS2561: Object literal may only specify known properties, but 'debounceTYPO'
does not exist in type '{ ... debounce?: {...} | undefined; }'.
Did you mean to write 'debounce'?
```
The new test drives all six public batch surfaces in both array and
async-iterable form and asserts on the NDJSON that actually reaches the
wire. Each item carries a distinct debounce key so the test catches a
wrong item-to-option pairing, not just a wholesale drop.
Fixes #3304
|
||
|
|
9409ddf9bc |
feat(webapp): add multiple environment API key management (#4390)
## Summary Projects can create, inspect, expire, and revoke multiple API keys for each environment. Plaintext values are shown only at creation; stored credentials are hashed and the API keys page displays only an obfuscated suffix afterward. Self-hosted installations support full-access additional keys by default. Authorization extensions can provide additional access presets and optional task selection. Additional keys can also mint scoped public access tokens through the Trigger.dev API without receiving the environment signing key. ## Feature notes - Only admin+ can create API keys (Developer can make in Development branch). - JWT self-signing will be a server call when used with new `_ak_` keys. - JWTs with long expiry can keep working even with api key deleted (gets priveleges from api key, signed with root key) - Unfiltered session listings intentionally preserve the existing broad task-read behavior. Filtered listings enforce task-level scopes for every requested task. - Buffered runs without a task identifier are not safely authorizable, so cancel/replay requests fail closed rather than resolving an unscoped run. - Batch and waitpoint endpoints intentionally return server-minted, narrowly scoped public tokens to all callers. These tokens have bounded lifetimes and may remain valid until expiry after API-key revocation. ## Deployment notes Deploy the management UI and public-token endpoint with new key creation disabled. Enable creation for selected organizations after the authentication path and released SDK have been verified, then expand availability gradually. Revoking an API key prevents new bearer requests and new token minting. Public tokens already minted by that key remain valid until their own expiration because they are signed by the environment signing key. ## TODO - [x] Add "Created by" to the key table - [x] Document that streamed batch ingestion is non-atomic and may partially accept items before a validation or authorization error. ## Follow-ups - [x] Add an organization-level feature flag for the API key management UI and creation action. - [x] Document rollout ordering: enable additional-key lookup before enabling issuance. - [x] Add a system-wide gate that can stop new key issuance without disabling authentication for existing keys. - [x] Replace the generic SDK compatibility warning with the first published compatible version. Old SDK will mint an unusable token if given an `_ak_` key. - [x] Add public documentation covering creation, storage, expiration, revocation, SDK compatibility, and public-token lifetime behavior. - [x] Add observability for key creation, revocation, policy preparation failures, and public-token mint failures. - [ ] Exercise create, copy-once display, authenticate, mint, expire, and revoke flows end to end before broad enablement. |
||
|
|
1a16d61a37 |
fix(build): support decorator metadata with TypeScript 7 (#4505)
## Summary Allow projects using TypeScript 7 to enable `emitDecoratorMetadata()` without adding the TypeScript 6 compiler to every Trigger.dev CLI installation. Addresses #4500. ## Fix The extension now resolves TypeScript from the project and feature-detects the legacy compiler API. TypeScript 5 and 6 continue using the project's compiler, while TypeScript 7 projects can install Microsoft's optional `@typescript/typescript6` compatibility package alongside TypeScript 7. When no compatible compiler API is available, the build reports an actionable installation error. The extension documentation includes setup commands for npm, pnpm, and Bun. Verified with TypeScript 5, TypeScript 6, TypeScript 7 with and without the compatibility package, emitted decorator metadata, packed ESM and CommonJS consumers, package export checks, and typechecking. |
||
|
|
85f5b37c68 |
chore: upgrade to TypeScript 7 (#4318)
## Summary Upgrade the monorepo to TypeScript 7.0.2 and update package build tooling for compatibility with the native compiler. ## Design Package builds now use `tshy` 4, while the packages still using `tsup` move to `tsdown`. The few scripts that depend on the legacy TypeScript compiler API use an explicit TypeScript 6 alias; declaration portability coverage invokes the TypeScript 7 CLI directly. Turbo is updated so workspace tasks can read the regenerated pnpm lockfile. --------- Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> |
||
|
|
c01a4f18f4 |
feat(supervisor): cancel a resumed run's in-flight checkpoint (#4502)
A run controller must call the continue route to resume, so the supervisor already knows synchronously that any checkpoint still running for that run is pointless. It only acted on that for the compute path. The continue route now cancels it for the Kubernetes path too, matching what completion already does since #4493. Called after the reply so the runner is never delayed, and skipped when there is no checkpoint client or when the compute path owns the run. The request is bounded by a 5s timeout so a hung call cannot leave the handler pending. `checkpoint_cancel_requests_total{result}` records the outcome, using the same label names as the delete path where they overlap: `sent`, `no_client`, `not_applicable`, `http_error`. No changeset: `CheckpointClient` is a server-only internal API, same as #4493. refs TRI-12915 |
||
|
|
4f69c43e6b |
feat(supervisor): reclaim a run's checkpoint storage when it finishes (#4493)
When a run reaches a terminal state, ask the checkpoint service to
reclaim the storage its checkpoints occupied. Storage for finished runs
is not otherwise reclaimed, so nothing frees it today.
**Off by default** behind `DELETE_CHECKPOINTS_ON_COMPLETION`, and the
service-side handler ships separately, so merging this changes no
behaviour.
## Where the tenancy comes from
Addressing a run's checkpoints needs org, project, environment,
deployment version and run id. All five are already in hand at
`attempt.complete`, and three are **signed** by the deployment token:
| Value | Source | Trust |
| -- | -- | -- |
| org | claim `org_id` | signed |
| environment | claim `environment_id` | signed |
| deployment version | claim `deployment_version` | signed |
| project ref | `x-trigger-workload-project-ref` header |
runner-supplied |
| run | route param | runner-supplied |
`authorizeWorkloadRequest` previously returned only `environment_id`,
and only in enforce mode, so it now also returns the verified `claims`.
That difference is deliberate and documented on the method: claims are
used to address a run's **own** resources locally, never to scope the
platform, which is why `environmentId` stays enforce-only.
The two runner-supplied values are safe because the signed ones are
outermost - a runner lying about either can only name something inside
its own org and environment, and a project ref that doesn't pair with
its signed environment matches nothing. The run id is read from
`params.runFriendlyId`, the same value the platform just validated,
rather than from the body or a header. Where both a claim and a header
exist (`deployment_version`), the claim wins.
## Placement
The call sits after `reply.json(...)`, so the runner sees no added
latency - the same shape the suspend route already uses. The service
enqueues and returns 202, so it is one fast local hop.
Terminal means `RUN_FINISHED` **or `RUN_PENDING_CANCEL`** - a run
cancelled mid-execution never restores, and skipping it would leave its
storage behind. Retries are excluded deliberately: reclamation is
per-run, so a retry is covered by the final completion.
Also gated on `!snapshotService`, so it stays inert where checkpoints
aren't the kind this reclaims.
## Observability
`checkpoint_delete_requests_total{result}` counts `sent` **and every
reason we decide not to send**: `disabled`, `not_terminal`, `no_claims`,
`no_project_ref`, `http_error`.
The negative labels are the point - without them, "no requests are
happening" looks identical to the feature being switched off.
`no_claims` is reachable even under enforcement, since enforce only
rejects a *present-but-invalid* token; an absent or legacy id still
passes with no claims attached.
## Notes for review
- **No changeset**: `CheckpointClient` is `core/v3/serverOnly`, an
internal service-to-service API rather than customer-facing surface.
- **No `.server-changes/` note**: there is nothing a dashboard user
would notice here. Happy to add one if you disagree.
- `pnpm run typecheck` can't complete in my checkout -
`@trigger.dev/database` fails to build on a missing `tsc` in the pnpm
store, unrelated to this diff. Verified with `tsc --noEmit` against the
supervisor project instead: **zero errors in `apps/supervisor/src`**.
Worth noting it caught a real bug here - the completion response is
wrapped, so the status is `data.result.attemptStatus`.
refs TRI-12789
|
||
|
|
763b5dc582 |
feat(webapp): enforce scopes for environment API keys (#4389)
## Summary Environment API keys backed by the additional-key table can authenticate API requests using their stored effective scopes. Revoked and expired keys are rejected, branch environments retain their existing routing behavior, and last-used timestamps are updated on a throttled best-effort basis. ## Design API route builders receive the resolved ability and reject restricted keys on routes without an authorization declaration. Existing deployment, environment variable, queue, run, task, batch, session, and waitpoint routes declare the resources they access. Trigger and batch responses return server-signed public access tokens, so additional keys never need access to the environment signing secret. Root-key rotation also keeps public tokens valid for the existing grace window. ## Feature notes - Root environment keys remain unrestricted for backward compatibility. Additional keys enforce their persisted scopes and fail closed on routes without an authorization declaration. - Machine-key requests never exchange one credential for another. Additional keys cannot retrieve the root key, and rotated root keys are not upgraded during their grace window. - Public JWT validation remains host-owned, while installed RBAC plugins continue to supply root-key abilities. - Unfiltered session and run listings preserve existing broad task-read behavior. Filtered requests enforce the supplied task identifiers. - Related-run summaries remain embedded in run retrieval for API compatibility. Retrieving or mutating a related run independently still requires permission for that run. - Queue management authorizes at collection scope, matching the queue permissions currently issued. - Batch responses deliberately include server-signed public access tokens for all clients. Selected-task credentials continue using their original credential for per-item authorization. - Two-phase batches authorize declared task identifiers before creation and authorize every streamed item. Streaming paths that cannot declare the complete task set remain fail closed. - Authentication telemetry records successful credential resolution separately from subsequent resource-authorization failures. - API keys are high-entropy random tokens. SHA-256 is intentionally used for deterministic indexed lookup, not password hashing. ## Deployment notes The schema migration must be present before this code is deployed. Because bearer resolution runs on every authenticated request, deploy the resolver with additional-key lookup disabled, verify root-key and public-token parity, then enable lookup before any additional keys can be issued. The multi-task authorization tightening changes the result for narrowly scoped tokens that request tasks outside their grants. Observe would-deny results before enforcing that check. Request-idempotency keys are also newly isolated by environment and task, so a retry crossing the deployment boundary may execute once more before old cache entries expire. ## Follow-ups - [x] Add a system-wide kill switch for additional-key lookup, defaulted off for the initial deployment. - [x] Add authentication observability by credential kind, result, latency, and lookup path without recording credential values. - [ ] ~Add would-deny observability and an independent enforcement switch for multi-task authorization.~ - [ ] ~Add an independent switch for server-issued batch tokens while root-key parity is verified.~ - [ ] Confirm every API route reachable by a restricted key has an explicit authorization declaration or intentionally fails closed. - [x] Verify root-key rotation, revoked-key grace, and public-token validation through each bearer resolver path. |