Measures the claim the caps bench left argued-not-measured: the per-task total
cap's real purpose is cross-TASK isolation, not cross-key. Two keyless tasks share
one env; capping the heavy task (its real per-queue concurrency gate, which for a
keyless task equals its total cap) cuts the light task's wait 475 to 2 under the
production FairQueueSelectionStrategy, versus failing at the cross-key grain. Still
not work-conserving (makespan 2039 to 3039); SFQ protects the light task too and
stays work-conserving. Adds a perQueueCap hook to the base-queue driver.
Blind two-model review (a third stalled). Folds in: consistent relative-ranking
register instead of proven/disproven; reframes the total cap as a capacity-
confounded cross-task knob, not a cross-key fairness failure; leads the sybil
result with the mechanism (unbounded per-key sum) plus the real Lua's 3-wide scan
window (ZRANGEBYSCORE ... LIMIT 0, maxCount*3), which governs why per-key caps
work with one heavy key and give no wait improvement once a tenant shards (sybil
bumped to 20 attacker keys, now flat vs baseline). Adds the shipped combined
total+per-key config and a fully-layered total+per-key+sfq treatment. Marks the
caps-first sequencing as interpretation, not measured. Corrects the makespan doc
(last dequeue, arrival-confounded off ckHeavyIdle), defines worstWait, notes the
contention-share seed spread, and comments the speculative order() coupling.
Runs the plan's concurrency caps (per-key limit via the real per-queue gate,
total cap via a driver group gate) head-to-head with SFQ/DRR through the real
CK-dequeue Lua. Per-key caps fix a starved key's wait when one key floods
(eligibility-aware dequeue) but fail when a tenant shards its backlog across many
keys (the sybil split), and are not work-conserving; a total cap only lowers the
ceiling and worsens cross-key wait; scheduling fixes every case including sybil
and stays work-conserving. Adds a makespan work-conservation metric.
The plan of record ships concurrency caps (per-task total, per-key limit) and
leaves the fair-selection scoring untouched, the opposite lever from the earlier
spike's SFQ recommendation. Adds the reconciliation design and the queueing-theory
research grounding it (WFQ/SFQ wait bounds, concurrency-cap fairness limits, CoDel
scope, real multi-tenant systems, Little's law).
Blind two-model review found the ckSkew/ckBalanced baseline starvation was a
Redis lexicographic tie-break on equal enqueue timestamps, not age order.
Redesigned scenarios so the backlog key keeps a genuinely old head and other
keys arrive via poisson, so the baseline now exercises real age-order
starvation (ckSkew baseline 0.187, worst key waits 1321ms; SFQ cuts it to 16ms).
FINDINGS corrected: lead with per-key wait (contention share is volume-confounded
for low-volume keys), and add the key fidelity limit the review surfaced: the
harness serves one key per Lua call, while production batches and re-scores a
served key mid-call, so this proves the ordering fix only at maxCount=1 and a
production fix must advance per-key state inside the batched Lua. Softened
'viable' to 'worth a design spike'.
At the concurrency-key grain the production age-order baseline starves keys
(worst contention share 0.000 on skew); SFQ/stride/DRR fix it driving the real
CK-dequeue Lua via ckIndex rescoring. Confirms the base-queue spike's ranking at
the real #2617 seam.
Second review round (blind multi-model) found real issues:
- Contention metric counted poisson tenants as contending before their runs
arrived, so trickle numbers measured arrival shape not fairness; it now only
counts a tenant once it has arrived, unserved work.
- SFQ/stride floor was not monotonic, so a returning idle tenant could
monopolise service (the CFS min_vruntime guarantee the comments claimed but
did not implement). Floor is now monotonic non-decreasing.
- Dropped the misleading EEVDF eligibility term from SFQ (it never changed the
ordering); it is plain start-time WFQ.
- FINDINGS corrected throughout: the DRR shortfall is a batch-drain measurement
artifact (and virtual-time clusters queues at ties too, so the earlier
explanation was wrong); worstWaitP99 is not an anti-staleness signal; the
baseline age bias is not exercised here; cost was not rigorously measured;
CoDel actively hurts under trickle arrival. Added the definitional-advantage
and age-bias caveats up front.
- Persist rough cost proxies to results JSON; note flushdb scope; drop dead
seed field from scenarios.
Responds to a multi-model review of the spike:
- Runs every scenario over multiple seeds and reports the min..max spread;
the baseline's fairness turns out seed-variant while the virtual-time
schemes are seed-stable. An earlier single-seed claim that DRR dipped below
baseline did not survive multiple seeds.
- Adds a poisson trickle scenario so queue heads actually age (the earlier
suite only used bulk arrival, which left the baseline's age bias and the
CoDel wrapper both inert).
- Adds a codel-baseline variant. The data shows the CoDel wrapper is a no-op
under bulk arrival and slightly harmful under trickle arrival, so FINDINGS
no longer recommends shipping it.
- Fixes a vacuous smoke-test assertion (renamed metric field) and switches the
metric to contention-window share plus per-tenant wait, since final
throughput share is fixed by the workload and cannot distinguish selectors.
- Rewrites FINDINGS to match, including the corrected CoDel explanation.
Adds the scenario definitions and the selector-by-scenario bench, plus the
metric rework the real data forced: because the sim drains every run, final
throughput share is fixed by the workload, so fairness is measured as
contention-window share (share while >=2 tenants have work) and per-tenant
wait. Reader now reads head ages from the master-queue scores in one call.
Results JSON captured under results/.
Design doc for a throwaway spike that ranks four fair-queueing methods
(SFQ virtual-time tags, hierarchical DRR, CoDel staleness monitor,
stride/lottery baseline) against the current FairQueueSelectionStrategy.
Drives the real RunQueue behind its selection-strategy interface on a
testcontainers Redis and ranks on fairness, tail latency, and cost.
Addresses the sub-environment fairness gap in #2617.
## Summary
The split run-store's id-set read path (`#findRunsByIdSet`, used by the
runs-list hydrate, the realtime hydrator, and engine sweeps) queried the
new store for the entire id set and then probed the legacy store for the
misses. A run's residency is a total function of its id (run-ops ids
live in the new store, every other id in legacy), so each id belongs to
exactly one store. Route each id to its owner and query each store only
for its own ids, in parallel. Same result set, and while a split is
active with most runs still on legacy it removes a wasted new-store
query from every id-set read.
## Change
`#findRunsByIdSet` now partitions the ids by `classifyResidency` and
runs one bounded query per store (skipping an empty side), in parallel,
mirroring `expireRunsBatch` and the single-run `#route`. `finalizeRows`
still applies orderBy/take/skip globally over the merged set.
This drops the id-set path's cross-store fallback, which existed to
prefer the new-store copy when the same id was present in both stores.
That collision cannot arise when each id maps to exactly one store
(nothing writes a legacy-shaped id into the new store), so the fallback
is dead code. The two id-set tests that asserted "new copy wins on
collision" now assert the routing invariant: a legacy-shaped id resolves
to the legacy store and the path never consults the new store.
The open-predicate path (`#findRunsOpen`) is unchanged: an open `where`
has no id to route on, so it still unions both stores and dedupes.
## Default billing alerts + billing limit page UX
- New orgs get default billing alerts: $5, $100, $500, $1000, $2500.
Existing orgs are backfilled by a billing-side data migration (companion
[PR](https://github.com/triggerdotdev/cloud/pull/1657)).
- The billing limit form starts with nothing selected for orgs that
never set a limit — the save button appears once an option is picked.
- The yellow banner now also shows on the billing limits page itself,
asking to configure a limit. Hidden everywhere for members who can't
manage billing.
- Also fixes billing limit alert preview.
Tests
- `apps/webapp/test/billingLimitsRoute.test.ts` — dirty logic for
empty/selected mode
- `apps/webapp/test/billingAlertsDefaults.test.ts` — default values
- `apps/webapp/test/billingAlertsFormat.test.ts` — preview after a limit
change
## Summary
Adds two counters to the native realtime backend so we can see how much
duplicate row serialization the change router does per batch. When a run
changes it can match several held feeds at once (a run subscription plus
one or more tag/list feeds), and today each matching feed serializes
that run's wire value independently. These counters quantify that
fan-out so we can decide whether a shared serialization step is worth
it.
## What they measure
- `realtime_native.emission_run_serializations`: total wire-value
serializations performed across feeds per batch (what the current path
does).
- `realtime_native.emission_distinct_serializations`: distinct (columns,
run) rows those serializations cover (what a serialize-once-per-batch
step would do).
Average feeds-per-run is `run_serializations / distinct_serializations`,
and `1 - distinct / run_serializations` is the serialization work a
shared step could save. Wired through a new optional `onEmissionFanout`
callback on the router. No behavior change.
## Summary
5 improvements, 5 bug fixes.
## Improvements
- Add `node-24` and `node-26` as supported `runtime` options in
`trigger.config.ts`. The `experimental-node-24` and
`experimental-node-26` names are now deprecated aliases and emit a
deprecation warning; switch to `node-24` / `node-26` instead.
([#4337](https://github.com/triggerdotdev/trigger.dev/pull/4337))
```ts
import { defineConfig } from "@trigger.dev/sdk";
export default defineConfig({
runtime: "node-24",
project: "<your-project-ref>",
});
```
- Avoid logging task run environment variable values at debug level
([#4336](https://github.com/triggerdotdev/trigger.dev/pull/4336))
- Custom chat agent loops get two ergonomic wins for owning the turn
loop. ([#4304](https://github.com/triggerdotdev/trigger.dev/pull/4304))
`chat.writeTurnComplete()` now returns the turn boundary's resume
cursors (`lastEventId` for the output stream and `sessionInEventId` for
the input stream), so you can persist them straight from the task
instead of round-tripping them back from the client.
```ts
const { lastEventId, sessionInEventId } = await
chat.writeTurnComplete();
await db.chats.update(chatId, { lastEventId, sessionInEventId });
```
`chat.pipeAndCapture()` no longer throws when a stream is stopped or
fails. It now returns a `PipeAndCaptureResult` whose `message` holds any
partial output captured before the stop or failure, alongside a typed
`status` (`"complete" | "aborted" | "error"`) and, on failure, the
`error`. Read the message off the result:
```ts
const { message, status, error } = await chat.pipeAndCapture(result, {
signal,
});
if (message) conversation.addResponse(message);
if (status === "error") logger.error("turn failed", { error });
```
Note: `pipeAndCapture` previously resolved to `UIMessage | undefined`.
Update call sites to read `.message` from the returned result.
- Suppress a build-time warning that could appear in Vite-based projects
when the optional `@ai-sdk/otel` package is not installed.
([#4188](https://github.com/triggerdotdev/trigger.dev/pull/4188))
## Bug fixes
- Fixes intermittent `trigger dev` run crashes where a run could fail at
boot with a cryptic `Cannot find module .../dev-run-worker.mjs` after a
rebuild had cleaned up the build directory the run was launched against.
Dev runs now retry cleanly instead of hard-crashing when their build
directory is missing, the dev watchdog no longer removes the build tree
of a still-running session, and a run assigned to a worker version that
was superseded by a rebuild now fails fast with a clear message instead
of silently hanging until it times out.
([#4276](https://github.com/triggerdotdev/trigger.dev/pull/4276))
## Server changes
These changes affect the self-hosted Docker image and Trigger.dev Cloud:
- Refreshed the side menu: separate organization and account menus, a
new project switcher, and the menu is now resizable by dragging its
edge. The account Profile page has also been redesigned.
([#4066](https://github.com/triggerdotdev/trigger.dev/pull/4066))
- Allow different organization members to use the same development
branch name without sharing or colliding with each other's branch
environments.
([#4323](https://github.com/triggerdotdev/trigger.dev/pull/4323))
- Limit account settings email input to 254 characters.
([#4330](https://github.com/triggerdotdev/trigger.dev/pull/4330))
- Prevent duplicate Staging and Preview environments when account setup
requests overlap
([#4261](https://github.com/triggerdotdev/trigger.dev/pull/4261))
- Fix the docs link on the empty Prompts page, which pointed to a page
that no longer exists.
([#4247](https://github.com/triggerdotdev/trigger.dev/pull/4247))
<details>
<summary>Raw changeset output</summary>
# Releases
## @trigger.dev/build@4.5.7
### Patch Changes
- Updated dependencies:
- `@trigger.dev/core@4.5.7`
## trigger.dev@4.5.7
### Patch Changes
- Fixes intermittent `trigger dev` run crashes where a run could fail at
boot with a cryptic `Cannot find module .../dev-run-worker.mjs` after a
rebuild had cleaned up the build directory the run was launched against.
Dev runs now retry cleanly instead of hard-crashing when their build
directory is missing, the dev watchdog no longer removes the build tree
of a still-running session, and a run assigned to a worker version that
was superseded by a rebuild now fails fast with a clear message instead
of silently hanging until it times out.
([#4276](https://github.com/triggerdotdev/trigger.dev/pull/4276))
- Add `node-24` and `node-26` as supported `runtime` options in
`trigger.config.ts`. The `experimental-node-24` and
`experimental-node-26` names are now deprecated aliases and emit a
deprecation warning; switch to `node-24` / `node-26` instead.
([#4337](https://github.com/triggerdotdev/trigger.dev/pull/4337))
```ts
import { defineConfig } from "@trigger.dev/sdk";
export default defineConfig({
runtime: "node-24",
project: "<your-project-ref>",
});
```
- Avoid logging task run environment variable values at debug level
([#4336](https://github.com/triggerdotdev/trigger.dev/pull/4336))
- Updated dependencies:
- `@trigger.dev/core@4.5.7`
- `@trigger.dev/build@4.5.7`
- `@trigger.dev/schema-to-json@4.5.7`
## @trigger.dev/core@4.5.7
### Patch Changes
- Add `node-24` and `node-26` as supported `runtime` options in
`trigger.config.ts`. The `experimental-node-24` and
`experimental-node-26` names are now deprecated aliases and emit a
deprecation warning; switch to `node-24` / `node-26` instead.
([#4337](https://github.com/triggerdotdev/trigger.dev/pull/4337))
```ts
import { defineConfig } from "@trigger.dev/sdk";
export default defineConfig({
runtime: "node-24",
project: "<your-project-ref>",
});
```
## @trigger.dev/python@4.5.7
### Patch Changes
- Updated dependencies:
- `@trigger.dev/sdk@4.5.7`
- `@trigger.dev/core@4.5.7`
- `@trigger.dev/build@4.5.7`
## @trigger.dev/react-hooks@4.5.7
### Patch Changes
- Updated dependencies:
- `@trigger.dev/core@4.5.7`
## @trigger.dev/redis-worker@4.5.7
### Patch Changes
- Updated dependencies:
- `@trigger.dev/core@4.5.7`
## @trigger.dev/rsc@4.5.7
### Patch Changes
- Updated dependencies:
- `@trigger.dev/core@4.5.7`
## @trigger.dev/schema-to-json@4.5.7
### Patch Changes
- Updated dependencies:
- `@trigger.dev/core@4.5.7`
## @trigger.dev/sdk@4.5.7
### Patch Changes
- Custom chat agent loops get two ergonomic wins for owning the turn
loop. ([#4304](https://github.com/triggerdotdev/trigger.dev/pull/4304))
`chat.writeTurnComplete()` now returns the turn boundary's resume
cursors (`lastEventId` for the output stream and `sessionInEventId` for
the input stream), so you can persist them straight from the task
instead of round-tripping them back from the client.
```ts
const { lastEventId, sessionInEventId } = await
chat.writeTurnComplete();
await db.chats.update(chatId, { lastEventId, sessionInEventId });
```
`chat.pipeAndCapture()` no longer throws when a stream is stopped or
fails. It now returns a `PipeAndCaptureResult` whose `message` holds any
partial output captured before the stop or failure, alongside a typed
`status` (`"complete" | "aborted" | "error"`) and, on failure, the
`error`. Read the message off the result:
```ts
const { message, status, error } = await chat.pipeAndCapture(result, {
signal,
});
if (message) conversation.addResponse(message);
if (status === "error") logger.error("turn failed", { error });
```
Note: `pipeAndCapture` previously resolved to `UIMessage | undefined`.
Update call sites to read `.message` from the returned result.
- Suppress a build-time warning that could appear in Vite-based projects
when the optional `@ai-sdk/otel` package is not installed.
([#4188](https://github.com/triggerdotdev/trigger.dev/pull/4188))
- Updated dependencies:
- `@trigger.dev/core@4.5.7`
</details>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Wraps the workload_auth_gate_total Counter in the singleton helper (same
pattern as reloadingRegistry.server.ts) so a dev hot reload doesn't
crash with "A metric with the name workload_auth_gate_total has already
been registered". No production behavior change.
## Summary
Adds `node-24` and `node-26` as first-class `runtime` options in
`trigger.config.ts`. Previously these Node versions were only reachable
via the `experimental-node-24` / `experimental-node-26` names.
Those experimental names are now **deprecated aliases**: they still
resolve to `node-24` / `node-26` for backwards compatibility, but
loading a config that uses them prints a deprecation warning pointing at
the new name.
```ts
export default defineConfig({
runtime: "node-24",
project: "<your-project-ref>",
});
```
## Details
- `ConfigRuntime` (the public config schema) now accepts `node-24` and
`node-26` directly; the internal `BuildRuntime` already supported them,
so base images and the deploy path are unchanged.
- `resolveBuildRuntime` passes the new names straight through and keeps
mapping the experimental aliases to their replacements.
- Renamed the runtime helper from `isExperimentalConfigRuntime` to
`isDeprecatedConfigRuntime` and added `deprecatedRuntimeReplacement` so
the CLI can name the replacement in its warning.
- Docs snippet updated to list the new versions and flag the deprecated
names.
Add `.worktrees/` to the repo `.gitignore`.
The pre-push hook runs `oxfmt --check .` and `oxlint .` over the whole
tree, and those tools only read the in-repo ignore files (not a user's
global gitignore). Local git-worktree checkouts placed under
`.worktrees/` therefore got linted/formatted, failing the hook on
unrelated code. Ignoring the directory keeps both tools out of worktree
checkouts. No source changes.
Add a Prometheus gauge `workload_token_enforcement_mode` set to 1 for
the active `WORKLOAD_TOKEN_ENFORCEMENT` value
(`disabled`/`log`/`enforce`), emitted at startup on the shared registry.
The existing mint/verify counters don't distinguish `log` from `enforce`
(the verify outcome is recorded before the reject decision), so
dashboards can't tell which mode a cluster is running. This gauge makes
the active mode queryable at a glance. Supervisor typecheck passes.
Restores the debug panel on the **Tasks** and **Runs** pages, and makes
the data it shows copyable.
Admin/impersonation only — no change for regular users, so there's no
`.server-changes`
<img width="909" height="1420" alt="CleanShot 2026-07-22 at 12 05 27@2x"
src="https://github.com/user-attachments/assets/ce2da167-dc23-422f-83f3-4f4aee9ed32c"
/>
## Summary
The empty-state panel on the Prompts page linked to a docs path that no
longer exists, so the "Prompts docs" button returned a 404. It now
points to the current prompts documentation at /docs/ai/prompts,
matching the link already used in the page header.
## What
The organization side menu previously showed an "Enterprise" badge next
to the SSO & Directory Sync item for any org not on the enterprise plan.
That badge is now removed so the item renders without it.
## Screenshot (before)
<img width="1428" height="649" alt="CleanShot 2026-07-10 at 08 24 41"
src="https://github.com/user-attachments/assets/b9787363-972f-4dd5-bf61-486680f49f4c"
/>
## Summary
Limits user account email addresses to 254 characters in profile
settings and onboarding. Oversized values are rejected before the
uniqueness lookup, and the form fields enforce the same limit in the
browser.
## Fix
Both email update flows use a shared bounded email schema. Basic
validation completes before the uniqueness lookup runs.
## Summary
Allow each organization member to use the same development branch name
without colliding with another member's environment. Fixes#4320.
## Fix
Development branches now use the existing member-scoped project, slug,
and organization-member key for upserts. Preview branches retain their
project-wide shortcode behavior.
New development branches receive distinct shortcodes while keeping their
readable, member-scoped slugs. Existing branches continue to resolve
through the member-scoped key, so this requires no migration or
backfill.
Two small corrections to the organization **Usage** page credits
display.
### 1. Label the credits panel "Credits" (was "Promo credits")
The panel surfaces any credit balance, not only promo-code redemptions,
so "Promo credits" is misleading when the credits come from another
source. Renamed the heading to "Credits".
### 2. Don't show "Included usage" for Enterprise orgs
Enterprise inherits the Pro plan's `includedUsage` value, so the Usage
bar rendered an "Included usage: $50" tier marker for Enterprise
organizations. Enterprise bills against prepaid credits rather than a
per-month included-usage tier, so the marker was misleading. The
`tierLimit` marker is now suppressed for Enterprise (`plan.type ===
"enterprise"`).
Verified with `pnpm run typecheck --filter webapp`.
Redesign of the main side menu: separates Projects and Accounts from the
Organization menu and makes the menu resizable.
**Main changes**
- **Organization & Account menus**: the top-left is now a dedicated
organization menu (Settings, Usage, Billing, Team, SSO, integrations),
with a separate account menu beside it (Profile, PATs, Security,
Logout).
- **Project switcher**: a new Project section above the Environment
selector.
- **Resizable side menu**: drag the right edge to set a custom width
(saved per user), or click the edge to collapse/expand.
- **Environment selector**: reworked to match the Project menu,
including dev-branch handling.
- **Account Profile page**: redesigned into the Security page's
row-and-divider layout.
Preview URL: https://samejr-org-menu-update.triggerlabs.dev/https://github.com/user-attachments/assets/9b199576-6037-4ea6-9bdb-3ee15265b8c2
## Summary
Two ergonomic additions for custom chat-agent loops that own the turn
loop (`chat.customAgent`, `chat.createSession`, and the hand-rolled
primitives).
`chat.writeTurnComplete()` now resolves to `{ lastEventId }`, the resume
cursor for the start of the next turn. A custom loop can persist it
straight from the task instead of round-tripping it back from the client
after the turn ends. The value was already produced internally by the
turn-complete write; the public wrapper simply discarded it.
`chat.pipeAndCapture()` no longer throws when a stream is stopped or
fails. It now resolves to a `PipeAndCaptureResult` carrying any partial
`message` captured before the stop or failure, a typed `status`
(`"complete" | "aborted" | "error"`), and the `error` on failure.
Previously a failed stream threw and the partial was lost, and an abort
was captured only when the AI SDK happened to fire `onFinish` in time.
```ts
const { message, status, error } = await chat.pipeAndCapture(result, { signal });
if (message) conversation.addResponse(message);
if (status === "error") logger.error("turn failed", { error });
const { lastEventId } = await chat.writeTurnComplete();
await db.chats.update(chatId, { lastEventId });
```
## Design
`pipeAndCapture` wraps the pipe in a `try/catch` and classifies the
outcome from the abort signal (a stop drains the source stream cleanly
rather than throwing) versus a thrown error. It also races the
`onFinish` capture against a timeout so a hard stop that prevents
`onFinish` from firing can't hang the caller. This mirrors the capture
path `chat.agent` already uses internally.
The `finishReason` from `onFinish` is surfaced too, since it was already
captured on the built-in path.
The internal `turn.complete()` helper keeps its existing contract: it
still returns `UIMessage | undefined`, still throws on a genuine stream
failure, and still discards output on a full run cancel.
## Breaking change
`chat.pipeAndCapture` previously resolved to `UIMessage | undefined`.
Call sites now read `.message` off the result. This is a young,
low-level API; the docs examples are updated in this PR.
Fixes TRI-12078
## Summary
Prevents concurrent environment setup requests from creating duplicate
Staging and Preview environments.
## Fix
Adds database-enforced uniqueness for root Staging and Preview
environments.
If two requests race, the losing request loads the environment created
by the winner and continues successfully instead of creating a duplicate
or returning an error.
## Summary
Upgrades the workspace to TypeScript 6.0.3 and applies the compiler,
type, and build configuration changes required to preserve package
layouts and existing runtime behavior, apart from correcting the HTTP
status field used for deployment connection errors.
## Compatibility
- Centralizes TypeScript 6.0.3 through the pnpm workspace catalog.
- Replaces compiler options and module resolution modes that TypeScript
6 no longer accepts.
- Restores explicit Node types where TypeScript 6 no longer includes
them transitively.
- Adds explicit declaration build roots that preserve each package's
existing output layout.
- Patches tsup to stop injecting the removed `baseUrl` option during
declaration builds.
- Uses type-only assertions for stricter typed-array and stream
definitions without changing runtime behavior.
- Reads the EventSource v3 HTTP status from `code`, so deployment
connection errors include it correctly.
- Keeps standalone CLI compatibility fixtures pinned to their existing
TypeScript version and lockfiles.
`turbo run typecheck` and the complete PR test suite are green.
## What
The `/promo` page heading rendered with overlapping lines — the two
lines of "Promo codes are for new accounts" collided.
## Why
The page used `Header2` stretched to display sizes (`sm:text-2xl
md:text-3xl lg:text-4xl`), but `Header2` bakes in a fixed `leading-6`
(24px). A 36px font in a 24px line box makes wrapped lines overlap. It
only showed at `sm`+ widths and only on headings that wrap to 2+ lines,
which is why it slipped through — the short single-line headings on the
same page looked fine.
## Fix
Switch both headings to `Header1` — the page-title primitive the sibling
login pages (`login._index`, `login.magic`) already use for exactly this
size. Add `leading-tight` (relative line-height, scales with font size,
and this heading uniquely wraps to two lines) and `pb-4` to match the
login pages' spacing convention.
## Testing
Manually verified the signed-in view (`/promo` while logged in) renders
as two clean, non-overlapping lines across breakpoints. Pure CSS/layout
change — no automated test.
## Summary
5 improvements, 9 bug fixes.
## Breaking changes
- Self-hosted deployments no longer ship shared default credentials;
fresh installs generate their own. If yours still uses a previously
published default, set a unique value before upgrading, or set
`ALLOW_INSECURE_DEFAULT_SECRETS=true` to keep booting while you migrate.
([#4316](https://github.com/triggerdotdev/trigger.dev/pull/4316))
## Improvements
- Require explicit browser approval for CLI and MCP login, with
resilient polling while approval is pending.
([#4316](https://github.com/triggerdotdev/trigger.dev/pull/4316))
- Deployed task telemetry now reports the deployment identifier (e.g.
`deployment_abc123`) in the `worker.id` attribute, instead of an opaque
internal value. Upgrade to get the readable identifier in your own
OpenTelemetry exporters.
([#4316](https://github.com/triggerdotdev/trigger.dev/pull/4316))
- Prevent prototype pollution when applying run metadata operations or
reconstructing nested telemetry attributes, while preserving legitimate
`constructor` and `prototype` fields.
([#4316](https://github.com/triggerdotdev/trigger.dev/pull/4316))
- Add helpers to mint and verify the deployment-scoped token used to
authenticate run controllers to the platform.
([#4316](https://github.com/triggerdotdev/trigger.dev/pull/4316))
## Server changes
These changes affect the self-hosted Docker image and Trigger.dev Cloud:
- Added optional request rate limiting for telemetry ingestion
endpoints.
([#4316](https://github.com/triggerdotdev/trigger.dev/pull/4316))
- Background-worker deployment lookups are now scoped to the
authenticated environment.
([#4316](https://github.com/triggerdotdev/trigger.dev/pull/4316))
- Updating a GitHub App installation from the callback flow is now
scoped to your own organization, so an installation ID belonging to
another organization can no longer be used to refresh that
organization's installation record. The GitHub App installation session
is also now single-use, so completing an installation callback
invalidates its state and it can no longer be replayed.
([#4316](https://github.com/triggerdotdev/trigger.dev/pull/4316))
- Scope schedule and environment-variable writes to the caller's project
and environment
([#4316](https://github.com/triggerdotdev/trigger.dev/pull/4316))
- Reject compute snapshot callbacks that do not match the snapshot
request that created them.
([#4316](https://github.com/triggerdotdev/trigger.dev/pull/4316))
- Require secret-key authentication to initialize the session out
(agent→client) stream, matching the append route.
([#4316](https://github.com/triggerdotdev/trigger.dev/pull/4316))
- Live run and trace subscriptions now validate their identifiers more
strictly and only return data from your own organization.
([#4316](https://github.com/triggerdotdev/trigger.dev/pull/4316))
- Window-function names in the query compiler are now validated against
the allowlist, matching how other function calls are handled.
([#4316](https://github.com/triggerdotdev/trigger.dev/pull/4316))
- Authenticate run controllers to the platform with a signed,
deployment-scoped token.
([#4316](https://github.com/triggerdotdev/trigger.dev/pull/4316))
- Verify that worker actions (starting, completing, and continuing a
run, and reading its snapshots) target a run belonging to the caller's
environment.
([#4316](https://github.com/triggerdotdev/trigger.dev/pull/4316))
<details>
<summary>Raw changeset output</summary>
# Releases
## @trigger.dev/build@4.5.6
### Patch Changes
- Updated dependencies:
- `@trigger.dev/core@4.5.6`
## trigger.dev@4.5.6
### Patch Changes
- Require explicit browser approval for CLI and MCP login, with
resilient polling while approval is pending.
([#4316](https://github.com/triggerdotdev/trigger.dev/pull/4316))
- Deployed task telemetry now reports the deployment identifier (e.g.
`deployment_abc123`) in the `worker.id` attribute, instead of an opaque
internal value. Upgrade to get the readable identifier in your own
OpenTelemetry exporters.
([#4316](https://github.com/triggerdotdev/trigger.dev/pull/4316))
- Updated dependencies:
- `@trigger.dev/core@4.5.6`
- `@trigger.dev/build@4.5.6`
- `@trigger.dev/schema-to-json@4.5.6`
## @trigger.dev/core@4.5.6
### Patch Changes
- Prevent prototype pollution when applying run metadata operations or
reconstructing nested telemetry attributes, while preserving legitimate
`constructor` and `prototype` fields.
([#4316](https://github.com/triggerdotdev/trigger.dev/pull/4316))
- Require explicit browser approval for CLI and MCP login, with
resilient polling while approval is pending.
([#4316](https://github.com/triggerdotdev/trigger.dev/pull/4316))
- Add helpers to mint and verify the deployment-scoped token used to
authenticate run controllers to the platform.
([#4316](https://github.com/triggerdotdev/trigger.dev/pull/4316))
## @trigger.dev/python@4.5.6
### Patch Changes
- Updated dependencies:
- `@trigger.dev/core@4.5.6`
- `@trigger.dev/build@4.5.6`
- `@trigger.dev/sdk@4.5.6`
## @trigger.dev/react-hooks@4.5.6
### Patch Changes
- Updated dependencies:
- `@trigger.dev/core@4.5.6`
## @trigger.dev/redis-worker@4.5.6
### Patch Changes
- Updated dependencies:
- `@trigger.dev/core@4.5.6`
## @trigger.dev/rsc@4.5.6
### Patch Changes
- Updated dependencies:
- `@trigger.dev/core@4.5.6`
## @trigger.dev/schema-to-json@4.5.6
### Patch Changes
- Updated dependencies:
- `@trigger.dev/core@4.5.6`
## @trigger.dev/sdk@4.5.6
### Patch Changes
- Updated dependencies:
- `@trigger.dev/core@4.5.6`
</details>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
## Summary
5 improvements, 5 bug fixes.
## Improvements
- Add experimental Node.js 24 and 26 task runtimes. Set `runtime` to
`experimental-node-24` or `experimental-node-26` in `trigger.config.ts`.
([#4085](https://github.com/triggerdotdev/trigger.dev/pull/4085))
- Add `defaultRegion` to the project GET and list API responses; null
when unset.
([#4146](https://github.com/triggerdotdev/trigger.dev/pull/4146))
## Server changes
These changes affect the self-hosted Docker image and Trigger.dev Cloud:
- Transient internal sync failures are now retried quietly instead of
surfacing as errors.
([#4270](https://github.com/triggerdotdev/trigger.dev/pull/4270))
- Optionally route ClickHouse read traffic to a read replica while
writes stay on the primary. Set `CLICKHOUSE_READER_URL` to move all
reads, or target the busiest paths with `RUNS_LIST_CLICKHOUSE_URL` (runs
list) and `EVENTS_READER_CLICKHOUSE_URL` (traces, spans, logs). All
optional; unset keeps current behavior.
([#4081](https://github.com/triggerdotdev/trigger.dev/pull/4081))
- Remove the deprecated realtime stream write endpoint used by retired
v3 task clients.
([#4250](https://github.com/triggerdotdev/trigger.dev/pull/4250))
- Fix batchTrigger requests that set a per-item idempotency key failing
with an error instead of creating and deduplicating the runs
([#4271](https://github.com/triggerdotdev/trigger.dev/pull/4271))
- Speed up idempotency checks on `batchTrigger` calls that use
idempotency keys. Large batches against a task with a big run history no
longer degrade to multi-second lookups.
([#4255](https://github.com/triggerdotdev/trigger.dev/pull/4255))
- The "Preview branches" usage on the Limits page now counts only
preview branches.
([#4283](https://github.com/triggerdotdev/trigger.dev/pull/4283))
- Avoid opening a redundant database connection pool when the legacy and
primary databases are the same server, preventing connection usage from
doubling.
([#4253](https://github.com/triggerdotdev/trigger.dev/pull/4253))
- Fix pages occasionally loading unstyled or failing to load during a
deploy. The dashboard now reloads automatically to recover.
([#4282](https://github.com/triggerdotdev/trigger.dev/pull/4282))
<details>
<summary>Raw changeset output</summary>
# Releases
## @trigger.dev/build@4.5.5
### Patch Changes
- Updated dependencies:
- `@trigger.dev/core@4.5.5`
## trigger.dev@4.5.5
### Patch Changes
- Add experimental Node.js 24 and 26 task runtimes. Set `runtime` to
`experimental-node-24` or `experimental-node-26` in `trigger.config.ts`.
([#4085](https://github.com/triggerdotdev/trigger.dev/pull/4085))
- Updated dependencies:
- `@trigger.dev/core@4.5.5`
- `@trigger.dev/build@4.5.5`
- `@trigger.dev/schema-to-json@4.5.5`
## @trigger.dev/core@4.5.5
### Patch Changes
- Add experimental Node.js 24 and 26 task runtimes. Set `runtime` to
`experimental-node-24` or `experimental-node-26` in `trigger.config.ts`.
([#4085](https://github.com/triggerdotdev/trigger.dev/pull/4085))
- Add `defaultRegion` to the project GET and list API responses; null
when unset.
([#4146](https://github.com/triggerdotdev/trigger.dev/pull/4146))
## @trigger.dev/python@4.5.5
### Patch Changes
- Updated dependencies:
- `@trigger.dev/core@4.5.5`
- `@trigger.dev/build@4.5.5`
- `@trigger.dev/sdk@4.5.5`
## @trigger.dev/react-hooks@4.5.5
### Patch Changes
- Updated dependencies:
- `@trigger.dev/core@4.5.5`
## @trigger.dev/redis-worker@4.5.5
### Patch Changes
- Updated dependencies:
- `@trigger.dev/core@4.5.5`
## @trigger.dev/rsc@4.5.5
### Patch Changes
- Updated dependencies:
- `@trigger.dev/core@4.5.5`
## @trigger.dev/schema-to-json@4.5.5
### Patch Changes
- Updated dependencies:
- `@trigger.dev/core@4.5.5`
## @trigger.dev/sdk@4.5.5
### Patch Changes
- Updated dependencies:
- `@trigger.dev/core@4.5.5`
</details>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
## What
Run the shared Trivy image scan on every published image through a
single reusable workflow.
## Changes
- Generalise the image-scan workflow (`trivy-image-webapp.yml` ->
`trivy-image.yml`) - it was already parameterised by `image-ref`; only
the run-summary label was image-specific.
- `publish-worker-v4.yml`: expose `version` + `image_repo` as workflow
outputs (single-entry matrix, so unambiguous).
- `publish.yml`: run the shared scan from each publish job
(`scan-webapp`, `scan-supervisor`).
Report-only (writes a table to the run summary), OS packages only
(`vuln-type: os` - library deps stay with Dependabot), never blocks the
publish.