9caf4ceaf19b4c5a7733a18751e2c91bc93be58f
7199 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9caf4ceaf1 |
ci: skip typecheck for workflow-only PRs (#3619)
The `code` paths filter currently matches `**` minus a tiny exclusion list, so a PR that only touches `.github/workflows/*.yml` still flips `code == true` and runs typecheck (~2 min on the runner). Exclude `.github/**` from `code`, then re-include just `pr_checks.yml` and `typecheck.yml` so a change to either of those still triggers the full code check matrix. Effect: - workflow-only PRs (this one, future dependabot/codeql/etc.) skip typecheck; `all-checks` treats the skipped job as non-failure so the required status passes. - modifying `pr_checks.yml` or `typecheck.yml` themselves still triggers typecheck. - the existing per-suite filters (`webapp`, `packages`, `internal`, `cli`, `sdk`) already re-include the specific workflows that gate them, so they're unaffected. |
||
|
|
979655c281 |
feat: Sessions dashboard, task_kind, and chat-ready hardening (1/4) (#3542)
## Summary A `/sessions` dashboard for inspecting durable Sessions, an `AGENT` / `SCHEDULED` task-kind filter for the runs list, and the server-side hardening (rate-limit exemption for packets, retry-with-backoff on stream appends, typed too-large-chunk error) that the `chat.agent` runtime in #3543 needs. Builds on the Sessions primitive shipped in #3417. ## Design The Sessions list + detail routes mirror the run inspector pattern. `TaskTriggerSource` gains `AGENT` and `SCHEDULED` values, persisted on `BackgroundWorker.taskKind` and `TaskRun.taskKind` (plus a matching Clickhouse column), so the runs list can filter by kind. New `@trigger.dev/core` modules — `sessionStreams`, `inputStreams`, a `sessionStreamInstance` for realtime streams, and the `realtime-streams-api` / `session-streams-api` surfaces — expose the typed shapes that chat.agent will use to drive `session.out`. `ChatChunkTooLargeError` lets the runtime drop oversized chunks with a typed surface instead of failing the run. `s2Append` retries transient failures with exponential backoff. `/api/v[12]/packets/*` is exempt from customer rate limits so chat snapshot reads and writes don't get throttled under load. ## Stack Part of a 4-PR stack. Merge bottom-up. 1. **This PR** (#3542) → `main` 2. #3543 → #3542 — `chat.agent` runtime + browser transport 3. #3545 → #3543 — agent-view dashboard 4. #3546 → #3545 — ai-chat reference + MCP tooling Replaces #3173 (closed). <!-- GitButler Footer Boundary Top --> --- This is **part 5 of 5 in a stack** made with GitButler: - <kbd> 5 </kbd> #3612 - <kbd> 4 </kbd> #3546 - <kbd> 3 </kbd> #3545 - <kbd> 2 </kbd> #3543 - <kbd> 1 </kbd> #3542 👈 <!-- GitButler Footer Boundary Bottom --> |
||
|
|
09f5354a03 |
fix(core): cap idempotencyKey length at the API boundary (#3560)
`tasks.trigger`, `tasks.batchTrigger`, `batch.create`, `wait.createToken`, `wait.forDuration`, and the input/session stream waitpoint endpoints all accept a caller-supplied `idempotencyKey` and store it verbatim against a composite-unique index on `TaskRun`, `BatchTaskRun`, or `Waitpoint`. The schemas had no length cap, so a sufficiently long high-entropy key produced an index row larger than the underlying storage layer can hold. The insert failed at the database, and the caller saw a generic 500 from `RunEngineTriggerTaskService.call()` / `CreateBatchService` / waitpoint creation, depending on the endpoint. Keys produced by `idempotencyKeys.create()` are 64-character SHA-256 hashes and never trip this — it only manifests for direct REST callers (or SDK callers passing a raw string they generated themselves). Low-entropy keys also sail through, because the storage layer compresses repeated bytes before they reach the index, which is why the failure mode is intermittent and tied to caller-side key shape. ## Fix Add `.max(2048, "<field> must be 2048 characters or less")` to the seven schemas that feed an indexed `idempotencyKey` column: - `TriggerTaskRequestBody.options.idempotencyKey` - `BatchTriggerTaskItem.options.idempotencyKey` - `CreateBatchRequestBody.idempotencyKey` - `CreateWaitpointTokenRequestBody.idempotencyKey` - `CreateInputStreamWaitpointRequestBody.idempotencyKey` - `CreateSessionStreamWaitpointRequestBody.idempotencyKey` - `WaitForDurationRequestBody.idempotencyKey` Plus the `idempotency-key` HTTP header on the trigger route (and the three batch routes that re-export `HeadersSchema`). The header schema is lifted out of `api.v1.tasks.$taskId.trigger.ts` into `apps/webapp/app/v3/triggerHeaders.server.ts` so it can be exercised in tests without dragging the route's import-time side effects. The 2048 character ceiling is chosen to sit safely under the per-row index limit while staying generous against existing callers — keys that fit before still fit. Oversized keys now return a structured Zod 400 instead of a generic 500. Limit is documented under `Idempotency key` in `docs/limits.mdx` and as a `<Note>` on `docs/idempotency.mdx`. ## Test plan - [x] 15 schema unit tests added (`packages/core/src/v3/schemas/idempotencyKey.test.ts`, `apps/webapp/test/routes/triggerHeaders.test.ts`) — rejection-with-message + boundary acceptance for each capped schema. The webapp test exercises the extracted `TriggerHeadersSchema` directly with no mocks. - [x] `pnpm run build --filter @trigger.dev/core` - [x] `pnpm run typecheck --filter webapp` - [x] End-to-end verified locally: baseline (small key) → 200; 3000-char high-entropy header → 400 with the expected Zod error; same key at the 2048 boundary → 200; same key with the cap reverted → the database rejected the insert and the route returned 500 to the caller. Cap restored. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
be1a6cf8de |
feat: Sessions primitive — durable run-aware streams + dashboard
Adds Sessions, a durable, run-aware stream primitive that scopes session.in / session.out records to a session (not a single run). Records survive run boundaries; reconnect-from-last-event-id is built in. Server foundation: - New /realtime/v1/sessions/:session/:io/append + /records routes - sessionRunManager + sessionsRepository + clickhouseSessionsRepository - mintRunToken for short-lived per-session tokens - s2Append retry-with-backoff + undici cause diagnostics - /api/v[12]/packets/* exempt from customer rate limits - BackgroundWorker schema gains taskKind enum (TASK, AGENT, SCHEDULED) - TaskRun.taskKind column + clickhouse 029_add_task_kind_to_task_runs_v2 Core types: - new sessionStreams, inputStreams, realtimeStreams packages in @trigger.dev/core - session-streams-api / realtime-streams-api surface Sessions dashboard UI (the primitive's own viewer): - /sessions index + detail routes - SessionsTable, SessionFilters, SessionStatus, CloseSessionDialog - AGENT/SCHEDULED filter in RunFilters + TaskTriggerSource Includes the sessions-primitive changeset. |
||
|
|
8ba067d8b0 |
feat(webapp): preserve admin tabs search query between Users and Organizations (#3609)
Switching between the Users and Organizations tabs in the admin dashboard now keeps the current `?search=` value, so you can flip between the two without re-typing your filter. Other admin tabs don't take `search` and so don't carry it. |
||
|
|
d144220174 |
ci: path-based skip and gate job in pr_checks (#3615)
`pr_checks` runs the full matrix on every PR. #3609 touched only `apps/webapp/app/routes/admin.tsx` and still ran the 4-job CLI e2e matrix and 5-job sdk-compat suite. Adds a `changes` job using `dorny/paths-filter` and gates each tier: - webapp + e2e-webapp: `apps/webapp/**`, `packages/**`, `internal-packages/**` - packages: `packages/**` - internal: `internal-packages/**` + `packages/**` (cross-deps) - e2e (cli-v3): `packages/{cli-v3,build,core,schema-to-json}/**` - sdk-compat: `packages/{trigger-sdk,core}/**` `.configs/**`, `package.json`, `pnpm-lock.yaml`, `pnpm-workspace.yaml`, `turbo.json` are also included in every filter since they affect the whole workspace. Inlines the `units` reusable-workflow children so each can be gated independently (status check names also flatten from `units / webapp / ...` to `webapp / ...`). `unit-tests.yml` is unaffected - still used by `publish.yml`. Adds an `all-checks` gate that always runs and short-circuits to success when every dependent is success-or-skipped. With this in place a single required status check (`All PR Checks`) is enough; before this, `paths-ignore` would have left required checks Pending on docs/changeset PRs ([gh docs](https://docs.github.com/en/actions/managing-workflow-runs/skipping-workflow-runs)). |
||
|
|
b23740bf7d |
docs(self-hosting): NodeLocal DNS and ClickHouse task events (#3568)
## Summary - Recommend deploying NodeLocal DNS and lowering `ndots` to `1` in the Kubernetes self-hosting guide. - Recommend storing task events in ClickHouse (`EVENT_REPOSITORY_DEFAULT_STORE=clickhouse_v2`) in both the Docker and Kubernetes guides, plus a new row in the webapp env var reference. |
||
|
|
759214eabd |
fix(webapp): Evict legacy resizable-panel localStorage on client boot (#3564)
## Summary
- Users on production are hitting `QuotaExceededError: Failed to execute
'setItem' on 'Storage'` when navigating runs, because their localStorage
is full of orphaned `panel-group-react-aria<n>-:<rid>:` entries.
- Each entry is a session-unique key written by the resizable panel
library; they accumulated to thousands per user over the last two months
and now block legitimate `setItem` calls (the run-view inspector can no
longer persist its layout, and the page crashes mid-render).
- This PR evicts the legacy entries once on client boot. The leak itself
is already plugged by the v1.1.3 upgrade in #XXXX — this is the cleanup
that recovers the wasted quota on existing users' machines.
## Root cause (already fixed, for context)
In v0.4.1 of the underlying library, `PanelGroupImpl` defaulted
`autosaveStrategy` to `"localStorage"` unconditionally — so *every*
`PanelGroup` wrote to localStorage on every autosave trigger, including
the four in `QueryEditor`, the one in `ReplayRunDialog`, the storybook
routes, etc. Without an `autosaveId`, the key fell back to
`panel-group-${useId()}`, and React Aria's `useId()` produces a new
session-unique prefix each visit. Result: entries accumulated without
bound across sessions.
The condition was introduced when
[#3282](https://github.com/triggerdotdev/trigger.dev/pull/3282) removed
the wrapper's explicit `autosaveStrategy="cookie"` override (to fix HTTP
431 cookie-size errors). That worked, but the library default that took
over silently caused this leak.
The v1.1.3 upgrade in the resizable-panel PR changed the default to
`autosaveStrategy = autosaveId ? "localStorage" : undefined`, so no new
entries are being written. Existing residue still needs to be removed
from users' browsers.
## Changes
- New file
[`apps/webapp/app/clientBeforeFirstRender.ts`](apps/webapp/app/clientBeforeFirstRender.ts)
— exports a `clientBeforeFirstRender()` function that runs
synchronously, before React hydrates. Encapsulates a small cleanup
helper that scans `localStorage` and removes:
- Every key starting with `panel-group-react-aria` (the legacy
auto-generated keys).
- The orphan `panel-run-parent-v2` key from before the autosaveId v2→v3
bump.
- [`apps/webapp/app/entry.client.tsx`](apps/webapp/app/entry.client.tsx)
— imports and invokes `clientBeforeFirstRender()` once, before
`hydrateRoot()`. This guarantees the cleanup completes before any
`ResizablePanelGroup` mounts and tries to write.
The cleanup is wrapped in `try/catch` so private-browsing /
disabled-storage scenarios fail silently. Idempotent: subsequent loads
find no matching keys and exit immediately.
## Test plan
- [x] Locally seed ~50 fake `panel-group-react-aria…` entries plus a
`panel-run-parent-v2` entry via DevTools console, hard reload → legacy
entries gone, real entries (`panel-run-parent-v3`, `panel-run-tree`)
preserved.
- [x] Idempotency: reload a second time, no errors, no state changes.
- [x] Add a control entry (`panel-run-parent-v3-but-different-suffix`) —
confirmed not over-matched.
- [x] Simulate broken `Storage.setItem` throwing — page still renders,
cleanup swallows the error.
- [x] Typecheck clean.
## Notes
- Customer report: `QuotaExceededError: Failed to execute 'setItem' on
'Storage': Setting the value of 'panel-run-parent-v3' exceeded the
quota.`
- The cleanup runs once per page load. Once a user has loaded the app
after this deploys, their localStorage is clean and the function becomes
a no-op forever.
|
||
|
|
6b0e78f1db |
chore: raise REVIEW.md drift-audit turn budget and steer selective sampling (#3567)
## Summary Follow-up to #3561. The drift-audit workflow timed out on PR #3542 (92 files, +5962 lines) by hitting `--max-turns 15` before reaching a verdict, leaving a red ❌ on that PR with no sticky comment. ## Changes - `--max-turns` bumped from 15 to 30. - Prompt now opens with an explicit "Strategy" section: read REVIEW.md once, scan the file-list only, open at most 5 files (3-5 on PRs >50 files), and bias toward finishing over exploring. - Final rule: *"when in doubt between one more file read and finish now — finish now."* The audit is allowed to miss things. It is not allowed to time out and leave a red X. ## Test plan - [ ] Verify this PR's audit posts `✅ REVIEW.md looks current for this PR.` (small diff) - [ ] After merge, retry the audit on #3542 or a similarly large PR and confirm it completes |
||
|
|
5c4e06479d |
chore(docker): disable ClickHouse system log tables in local dev (#3565)
## Summary Local ClickHouse was burning ~325% CPU endlessly merging its own telemetry tables (`metric_log`, `asynchronous_metric_log`, `part_log`, `trace_log`) after the container had been running long enough to accumulate hundreds of GB of system-log data. OrbStack Helper reflected this on the host (~400% CPU). These tables are not used by anything in the dev stack. They only exist for ClickHouse to log itself, so disabling them eliminates the merge churn entirely. ## Changes - Adds `docker/config/clickhouse-disable-system-logs.xml`, mounted into `/etc/clickhouse-server/config.d/`, that removes the noisy system log tables via `<table remove="1"/>`. - Mounts the override file in `docker/docker-compose.yml`. After applying, idle CPU dropped from 325% to ~12% on my machine. ## Test plan - [ ] `pnpm run docker` brings up the stack cleanly - [ ] `docker stats clickhouse` shows low idle CPU - [ ] App functionality unaffected (system log tables are not queried by the webapp) |
||
|
|
e8ef374fe0 |
fix(webapp,run-engine): honor per-queue length cap on concurrency-key queues (#3558)
## Summary Queues that use concurrency keys can no longer bypass the per-queue length cap, and the "Queued | Running" columns in the dashboard now show the true total across all CK variants instead of 0. The cap and the dashboard both relied on `ZCARD` of the base queue key, but CK-keyed runs live under `<base>:ck:<variant>` keys. Any queue that used concurrency keys read 0 — letting a single CK variant grow unbounded past the user's configured cap. ## Fix Two per-base-queue counters are maintained inside the CK Lua scripts: `<base>:lengthCounter` and `<base>:runningCounter`. Non-CK enqueue/dequeue paths are untouched. Counters are lazy-initialized the first time a CK enqueue (or nack) lands on a queue: the Lua script sums `ZCARD` across the variants tracked by `ckIndex`, sets the counter, then `INCR`s. Pre-existing CK backlog on already-populated queues is captured automatically — no batch migration required. `INCR`/`DECR` is gated on `ZADD`/`SADD` returning 1 (a new entry vs an idempotent no-op), so duplicate enqueues or re-dequeues don't inflate the counter. The counter is `SET` with a 24-hour TTL on init. `INCR`/`DECR` do not extend the TTL, so the counter expires daily and the next CK operation re-seeds it from `ckIndex`. This bounds any drift that accumulates during the rolling-deploy overlap window — where old (un-Tracked) and new (Tracked) webapp instances briefly coexist — to ≤24 hours, with no admin sweep or background reconciler needed. Read paths pipeline `ZCARD`/`SCARD` on the base key + `GET` on the counter and sum. A missing counter is treated as 0, so pure non-CK queues see the same answer as before. The counter-aware scripts ship alongside the originals with a `Tracked` suffix for rolling-deploy safety; a follow-up PR will drop the originals once this has rolled out. ## Test plan - [ ] `pnpm run test --filter @internal/run-engine` — 116 tests pass, including a new `ckCounters.test.ts` covering lazy init from pre-existing backlog, churn, floor-at-zero, the non-CK regression case, mixed CK + non-CK on the same base queue, idempotent re-enqueue (ZADD-already-exists), 24h TTL on the counter, and nack re-seeding after counter expiry. - [ ] Verified end-to-end against a live local environment: - Triggered 24 CK enqueues across 4 variants → `lengthCounter=16`, `runningCounter=8`, dashboard showed Queued=16 / Running=8 for the CK queue. - Set the env queue cap to 16, triggered 12 more enqueues → 8 succeeded, 4 rejected with `QueueSizeLimitExceededError`. - Deleted the counter on a queue with 31 messages already sitting in CK variants, triggered one more enqueue → counter materialized to 31 from the `ckIndex` sum, then INCR'd. |
||
|
|
e4981d1b11 |
feat(webapp): consolidate auth path + add comprehensive auth tests (#3499)
## Summary
Consolidates the webapp's authentication and authorization into a small
set of route helpers, replacing the ad-hoc `requireUser` /
`requireUserId` / `authenticatedEnvironmentForAuthentication` calls
scattered across routes. Same security model, but the per-request flow
(authenticate → authorize → load) now lives in one place per route
family.
Introduces a plugin seam (`@trigger.dev/plugins`) that lets the cloud
build install a richer RBAC implementation without touching webapp code.
The OSS fallback keeps the pre-RBAC permissive behaviour intact, so
self-hosted deployments work unchanged.
Adds a comprehensive end-to-end auth test suite that didn't exist before
— 193 `it()` blocks (vitest reports ~199 after `it.each` expansion)
covering API key, PAT and JWT auth across the public API surface, plus
dashboard session auth for admin pages.
## Changes
### Plugin contract — `@trigger.dev/plugins`
`RoleBaseAccessController` interface authoritative for both OSS
(fallback) and cloud (enterprise plugin):
- `authenticateBearer(request, { allowJWT? })` — API-key / public-JWT
auth, returns env + ability
- `authenticateSession(request, { userId, organizationId?, projectId?
})` — dashboard auth, caller resolves `userId` from the session cookie
and passes it in (no `helpers.getSessionUserId` callback — decouples the
plugin host from session-cookie code)
- `authenticatePat(request, { organizationId?, projectId? })` — PAT
auth, returns identity + `lastAccessedAt` so the host can throttle the
per-request update
- `authenticateAuthorize*` variants for the auth-and-check-in-one-call
cases
- `isUsingPlugin(): Promise<boolean>` — capability flag for UI /
branching where plugin-present-ness matters; replaces the
sentinel-string coupling that had `personalAccessToken.server` matching
`"RBAC plugin not installed"` literally
### Dashboard auth (started, partial rollout)
Admin and settings pages migrated to a unified `dashboardLoader` /
`dashboardAction` helper that authenticates the session, runs an
authorization check, and exposes the result to the route. Other
dashboard routes still on the old pattern; remaining migration tracked
in TRI-8730.
Migrated routes:
- `admin.*` (14 admin / back-office / feature-flags / LLM-models /
notifications / orgs / concurrency pages)
- `_app.orgs.$organizationSlug.settings.team`
- `_app.orgs.$organizationSlug.settings.roles`
### API / realtime / engine auth (complete for the migrated families)
71 routes migrated to a unified `apiBuilder` that centralizes Bearer /
PAT / Public-JWT authentication and applies the per-route authorization
check before the handler runs. Includes:
- `api.v1.*` and `api.v2.*` and `api.v3.*` — tasks, runs, batches,
queues, prompts, deployments, query, sessions, waitpoints, packets,
workers, idempotency keys
- `realtime.v1.*` — runs, batches, sessions, streams
- `engine.v1.*` — dev / worker-action protocols
29 routes still on the legacy `authenticateApiRequest*` helpers —
tracked as a post-deploy follow-up in TRI-9228.
Multi-resource auth direction is now explicit at the call site via
`anyResource(...)` (OR) and `everyResource(...)` (AND). Bare arrays no
longer typecheck — fixes a class of bug where a JWT scoped to one
resource could implicitly access others under OR semantics.
PAT auth path consolidated: was three DB queries per request (legacy
`authenticateApiRequestWithPersonalAccessToken` findFirst +
`rbac.authenticatePat` join + `lastAccessedAt` update). Now one query in
the steady state — plugin returns `lastAccessedAt`, host smart-skips the
update via JS-side throttle when fresh.
Side effect: action aliases preserved historic JWT scope semantics where
the new model is stricter (e.g. a `write:tasks` JWT now also satisfies
`trigger` / `batchTrigger` / `update` actions on the same resource —
matched at the auth boundary, not in the route handler).
### Backwards-compat fixes
The strict-match model regressed several real-world JWT shapes. Each
preserved via explicit `anyResource(...)` entries in the route's authz
block:
- **Batch retrieve routes** (`api.v1.batches.$batchId`, `api.v2.*`,
`realtime.v1.batches.*`) accept `read:runs` JWTs again (pre-RBAC
literal-match superScope behaviour)
- **Runs list routes** (`api.v1.runs`, `realtime.v1.runs`) accept
type-level `read:tasks` / `read:tags` on unfiltered queries (matched the
legacy `Object.keys` iteration semantic)
- **PAT/OAT auth shape** normalized through `toAuthenticated` so all
auth methods return the same slim `AuthenticatedEnvironment` (was:
API-key returned the slim shape but PAT/OAT returned raw Prisma
`Decimal` / no `orgMember`)
- **Scope `:` preservation** in resource ids — `read:tags:env:staging`
now correctly identifies the tag id as `env:staging`, not `env`
### Slim `AuthenticatedEnvironment`
Extracted to `@trigger.dev/core/v3/auth/environment` — a structural
shape independent of `@trigger.dev/database`. The plugin contract
returns this; webapp consumers import from there; the cloud plugin
(Drizzle) returns the same shape without Prisma's `Decimal` class
leaking into the public surface. Lets internal-packages (run-engine,
etc.) refer to `AuthenticatedEnvironment` without pulling Prisma in.
### Auth test suite (new — `*.e2e.full.test.ts`)
193 e2e tests run against a real spawned webapp + Postgres (no mocks).
Coverage matrix:
- **API key auth** — read / write / trigger / batchTrigger / deploy
actions across runs, batches, deployments, prompts, queues, query,
sessions, input-streams, waitpoints, tasks, idempotency keys; multi-key
resources (a run carries batch / tag / task identifiers — auth must
accept any matching scope)
- **Personal Access Token auth** — comprehensive matrix: scope match,
scope mismatch, missing scope, expired token, malformed token
- **Public JWT auth** — sub-vs-URL environment resolution, expired JWTs,
signature verification, scope checking, otu (one-time-use) token
semantics, branch-environment signing-key fallback
- **Dashboard session auth** — admin-only pages reject non-admins;
per-action gating
- **Cross-cutting edge cases** — revoked API key grace window, JWT
cross-environment isolation, MissingResource branch behaviour
### Hygiene cleanups
- Deleted dead `app/services/authorization.server.ts` (legacy
`checkAuthorization` + types — no live consumers post-migration) and its
orphaned test
- Dropped the never-populated `scopes` field from
`ApiAuthenticationResultSuccess`
- `scheduleEmail` moved out of `email.server.ts` into its own module —
breaks a `commonWorker → marqs/V1` import chain that was poisoning the
auth test graph
- OSS Roles page shows a deployment-aware empty state ("Roles aren't
available in this self-hosted deployment" vs the plan-upsell copy) via
`rbac.isUsingPlugin()`
- Team action handler: explicit per-intent ability gates
(`manage:billing` for purchase-seats, `manage:members` for set-role +
remove-member with self-leave carve-out)
### Cross-repo coordination
All public-package contract changes paired in `triggerdotdev/cloud#763`
(rbac-packages branch) — the enterprise plugin implements the same
`RoleBaseAccessController` interface against Drizzle.
## Test plan
- [x] `pnpm run typecheck --filter webapp` clean
- [x] `pnpm --filter webapp exec vitest run --config
vitest.e2e.full.config.ts` — 193/193 pass (requires Docker for
testcontainers)
- [x] Spot-check an authed API endpoint with a valid + invalid API key
against a local stack
- [x] Spot-check the migrated admin pages render and gate non-admins
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
3cbe9f2307 |
chore: add .claude/REVIEW.md with CI drift check (#3561)
## Summary
Adds `.claude/REVIEW.md` — a repo-specific source of truth for what AI /
agent code reviewers should treat as critical in this codebase
(rolling-deploy safety, hot-table indexes, recovery-path queries,
testcontainers usage, etc.). Pairs with a Claude-based PR audit that
flags drift between REVIEW.md and the code as it evolves.
## How the audit works
Mirrors the existing `.github/workflows/claude-md-audit.yml` pattern. On
non-draft, non-fork PRs that touch code, `anthropics/claude-code-action`
reads REVIEW.md, samples the PR diff, and posts a sticky comment with up
to 3 of:
- `[stale]` — rule cites a path / function / table that's been removed
or renamed
- `[contradiction]` — code in the PR violates a current rule
- `[missing]` — PR introduces a new pattern future reviewers should know
about
- `[obsolete]` — rule asserts a constraint the repo has moved past
If nothing's off, posts `✅ REVIEW.md looks current for this PR.`
## Test plan
- [ ] Convert this PR to ready-for-review, confirm the audit runs and
posts a sticky comment
- [ ] Verify the audit doesn't run on fork PRs (gated by
`head.repo.full_name == github.repository`)
- [ ] Verify suggestions are actionable on at least one follow-up PR
|
||
|
|
8e675a4e93 |
fix(core): retry TASK_PROCESS_SIGSEGV under the user's retry policy (#3552)
Closes [TRI-9234](https://linear.app/triggerdotdev/issue/TRI-9234/retry-task-process-sigsegv-errors-respecting-user-retry-config) ## What this changes SIGSEGV crashes (`TASK_PROCESS_SIGSEGV`) will now be **retried when an attempt fails**, in line with the task's configured retry settings (`retry.maxAttempts` etc.) — the same path SIGTERM and uncaught exceptions already use. Previously SIGSEGV was hard-classified as non-retriable and failed the run on the first segfault, ignoring the user's retry policy. Tasks without a retry policy still fail fast on the first SIGSEGV. Behaviour is unchanged for OOM kills (separate machine-bump retry path) and SIGKILL_TIMEOUT. ## Deploy **Only the webapp needs to ship.** The retry decision lives entirely in the webapp: - V2 path: `internal-packages/run-engine` (bundled into the webapp) - V1 path: `apps/webapp/app/v3/services/completeAttempt.server.ts` No supervisor, CLI, SDK, or customer-task-image changes required. Customers do not need to redeploy. The `@trigger.dev/core` changeset is just keeping the public package in sync — the published npm version isn't what makes the fix work. ## Why retry SIGSEGV in Node tasks is frequently non-deterministic across processes: - **Native addon races** (`sharp`, `canvas`, `better-sqlite3`, `node-rdkafka`, `bcrypt`, …) — libuv thread-pool work stepping on V8 handles. Different heap layout / thread schedule on a fresh process → retry often succeeds. - **JIT / GC interaction** — V8 turbofan deopt or GC during a native callback. Timing-dependent. - **Near-OOM in native code** — when RSS approaches the cgroup limit, native allocations fail and poorly-written addons dereference NULL → SIGSEGV instead of clean OOM-kill. - **Host / hardware issues** — bit flips, kernel quirks. Retry lands on a different host. The genuinely deterministic case (a user-code bug always tripping the same addon) is real, but a subset — and `maxAttempts` bounds the damage. ## Pre-existing inconsistency this resolves - `shouldRetryError` returned `false` for `TASK_PROCESS_SIGSEGV` → `fail_run`. - `shouldLookupRetrySettings` already listed `TASK_PROCESS_SIGSEGV` as retry-config-aware — but that branch was unreachable because `shouldRetryError` short-circuited first in `retrying.ts:86-90`. - We already retry `TASK_RUN_UNCAUGHT_EXCEPTION` (clearly a user-code bug) under the user's retry policy; refusing to retry SIGSEGV was the odd one out. ## Test plan - [x] `pnpm exec vitest run test/errors.test.ts` in `packages/core` — 26/26 pass (4 new) - [x] `pnpm run build --filter @trigger.dev/core` - [ ] CI green on PR 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
41a486ea7e |
chore: release v4.4.6 (#3501)
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 0s
🚀 Publish Trigger.dev Docker / publish-webapp (push) Has been skipped
🚀 Publish Trigger.dev Docker / publish-worker (push) Has been skipped
🚀 Publish Trigger.dev Docker / publish-worker-v4 (push) Has been skipped
🚀 Publish Trigger.dev Docker / units (push) Failing after 5s
🧭 Helm Chart Release / lint-and-test (push) Has been cancelled
🧭 Helm Chart Release / release (push) Has been cancelled
## Summary
1 improvement, 1 bug fix.
## Improvements
- Fail attempts on uncaught exceptions instead of hanging to
`MAX_DURATION_EXCEEDED`. A Node `EventEmitter` (e.g. `node-redis`)
emitting `"error"` with no `.on("error", ...)` listener escalates to
`uncaughtException`, which the worker previously reported but did not
act on — runs drifted to maxDuration with empty attempts. They now fail
fast with the original error and status `FAILED`, and respect the task's
normal retry policy. You should still attach `.on("error", ...)`
listeners to long-lived clients to handle errors gracefully.
([#3529](https://github.com/triggerdotdev/trigger.dev/pull/3529))
## Bug fixes
- Fix dev workers spinning at 100% CPU after the parent CLI disconnects.
Orphaned `trigger-dev-run-worker` (and indexer) processes were caught in
an `uncaughtException` feedback loop: a periodic IPC send via
`process.send` would throw `ERR_IPC_CHANNEL_CLOSED` once the parent
closed the channel, which re-entered the same handler that itself called
`process.send`, scheduled via `setImmediate` and amplified by
source-map-support's `prepareStackTrace`. Fixed by (1) silently dropping
packets in `ZodIpcConnection` when the channel is disconnected, (2)
adding a `process.on("disconnect", ...)` handler in dev workers so they
exit cleanly when the CLI closes the IPC channel, and (3) wrapping all
`uncaughtException`-path `process.send` calls in a `safeSend` guard that
checks `process.connected` and swallows synchronous throws.
([#3491](https://github.com/triggerdotdev/trigger.dev/pull/3491))
<details>
<summary>Raw changeset output</summary>
# Releases
## @trigger.dev/build@4.4.6
### Patch Changes
- Updated dependencies:
- `@trigger.dev/core@4.4.6`
## trigger.dev@4.4.6
### Patch Changes
- Fix dev workers spinning at 100% CPU after the parent CLI disconnects.
Orphaned `trigger-dev-run-worker` (and indexer) processes were caught in
an `uncaughtException` feedback loop: a periodic IPC send via
`process.send` would throw `ERR_IPC_CHANNEL_CLOSED` once the parent
closed the channel, which re-entered the same handler that itself called
`process.send`, scheduled via `setImmediate` and amplified by
source-map-support's `prepareStackTrace`. Fixed by (1) silently dropping
packets in `ZodIpcConnection` when the channel is disconnected, (2)
adding a `process.on("disconnect", ...)` handler in dev workers so they
exit cleanly when the CLI closes the IPC channel, and (3) wrapping all
`uncaughtException`-path `process.send` calls in a `safeSend` guard that
checks `process.connected` and swallows synchronous throws.
([#3491](https://github.com/triggerdotdev/trigger.dev/pull/3491))
- Fail attempts on uncaught exceptions instead of hanging to
`MAX_DURATION_EXCEEDED`. A Node `EventEmitter` (e.g. `node-redis`)
emitting `"error"` with no `.on("error", ...)` listener escalates to
`uncaughtException`, which the worker previously reported but did not
act on — runs drifted to maxDuration with empty attempts. They now fail
fast with the original error and status `FAILED`, and respect the task's
normal retry policy. You should still attach `.on("error", ...)`
listeners to long-lived clients to handle errors gracefully.
([#3529](https://github.com/triggerdotdev/trigger.dev/pull/3529))
- Updated dependencies:
- `@trigger.dev/core@4.4.6`
- `@trigger.dev/build@4.4.6`
- `@trigger.dev/schema-to-json@4.4.6`
## @trigger.dev/core@4.4.6
### Patch Changes
- Fix dev workers spinning at 100% CPU after the parent CLI disconnects.
Orphaned `trigger-dev-run-worker` (and indexer) processes were caught in
an `uncaughtException` feedback loop: a periodic IPC send via
`process.send` would throw `ERR_IPC_CHANNEL_CLOSED` once the parent
closed the channel, which re-entered the same handler that itself called
`process.send`, scheduled via `setImmediate` and amplified by
source-map-support's `prepareStackTrace`. Fixed by (1) silently dropping
packets in `ZodIpcConnection` when the channel is disconnected, (2)
adding a `process.on("disconnect", ...)` handler in dev workers so they
exit cleanly when the CLI closes the IPC channel, and (3) wrapping all
`uncaughtException`-path `process.send` calls in a `safeSend` guard that
checks `process.connected` and swallows synchronous throws.
([#3491](https://github.com/triggerdotdev/trigger.dev/pull/3491))
- Fail attempts on uncaught exceptions instead of hanging to
`MAX_DURATION_EXCEEDED`. A Node `EventEmitter` (e.g. `node-redis`)
emitting `"error"` with no `.on("error", ...)` listener escalates to
`uncaughtException`, which the worker previously reported but did not
act on — runs drifted to maxDuration with empty attempts. They now fail
fast with the original error and status `FAILED`, and respect the task's
normal retry policy. You should still attach `.on("error", ...)`
listeners to long-lived clients to handle errors gracefully.
([#3529](https://github.com/triggerdotdev/trigger.dev/pull/3529))
## @trigger.dev/python@4.4.6
### Patch Changes
- Updated dependencies:
- `@trigger.dev/core@4.4.6`
- `@trigger.dev/build@4.4.6`
- `@trigger.dev/sdk@4.4.6`
## @trigger.dev/react-hooks@4.4.6
### Patch Changes
- Updated dependencies:
- `@trigger.dev/core@4.4.6`
## @trigger.dev/redis-worker@4.4.6
### Patch Changes
- Updated dependencies:
- `@trigger.dev/core@4.4.6`
## @trigger.dev/rsc@4.4.6
### Patch Changes
- Updated dependencies:
- `@trigger.dev/core@4.4.6`
## @trigger.dev/schema-to-json@4.4.6
### Patch Changes
- Updated dependencies:
- `@trigger.dev/core@4.4.6`
## @trigger.dev/sdk@4.4.6
### Patch Changes
- Updated dependencies:
- `@trigger.dev/core@4.4.6`
</details>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
v.docker.4.4.6
v4.4.6
helm-v4.4.6
|
||
|
|
1e4b896c30 |
Fix(webapp): Notification style updates (#3553)
### Style updates to the notifications - Tightened up the typography - Brighter background to make it stand out a bit more - A bit more padding to make it more readable - Show the close button on hover instead - Turned the notification into a separate component as it's shared on the admin page modal - Minor tweaks to the behavior of toggling the notification beween open/closed side menu states ### Before <img width="224" height="313" alt="before" src="https://github.com/user-attachments/assets/c9a9377c-4a3b-4477-921a-3c86385d3f0b" /> ### After (with image) <img width="239" height="284" alt="CleanShot 2026-05-11 at 17 22 01" src="https://github.com/user-attachments/assets/311b4dbc-4853-4e6c-9f83-8173b38bd466" /> ### After (no image) <img width="239" height="189" alt="after" src="https://github.com/user-attachments/assets/884e062b-3608-4cb3-a462-d50597257753" /> --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> |
||
|
|
2301ed608c |
refactor(run-engine): make taskIdentifier optional on run-queue messages (#3559)
## Summary Make `taskIdentifier` optional on the run-queue message schema. No behavior change in this PR; readers continue to accept payloads that include the field. A separate change will stop writing it on the wire to shrink the per-run payload that lives in Redis while runs wait to be dequeued. ## Design The field is written into every payload at enqueue time but no consumer reads it back on the dequeue path. Both the run-engine and supervisor derive `taskIdentifier` from the loaded `TaskRun` row instead. Relaxing the schema first means readers tolerate payloads that omit it, so the writer-side change can ship without producing schema-parse errors during a rolling deploy. `projectId` is left required: `WorkerQueueResolver.#getOverride` reads it for project-scoped runtime worker-queue overrides. ## Test plan - [x] `pnpm run typecheck --filter @internal/run-engine` - [x] `pnpm run typecheck --filter webapp` - [x] `pnpm run test ./src/run-queue/tests/enqueueMessage.test.ts ./src/run-queue/tests/workerQueueResolver.test.ts --run` (28/28 passing) |
||
|
|
5f1a3e1653 |
refactor(run-engine): route TTL expiration through the batch path only (#3554)
## Summary
TTL expiration on queued runs was being scheduled twice: once via a
per-run `expireRun` worker job (the original implementation) and once
via the batch TTL system (added more recently). Both paths attempt to
flip the same run to `EXPIRED`. The per-run job almost always won the
race, leaving the batch consumer to observe runs already expired by the
older path.
This collapses TTL expiration onto the batch path so every queued TTLed
run goes through a single Redis-backed sorted set + batch consumer
instead of also getting its own scheduled redis-worker job.
## Design
`engine.trigger` and `delayedRunSystem.enqueueDelayedRun` no longer call
`ttlSystem.scheduleExpireRun`. The remaining `enqueueSystem.enqueueRun({
includeTtl: true })` already adds the run to the TTL sorted set;
`TtlSystem.expireRunsBatch` flips it to `EXPIRED` when the TTL fires.
Delayed runs get the same coverage by passing `includeTtl: true` on
their post-delay enqueue, so the TTL is armed from the moment the run
enters the queue (matching how the old job behaved —
`parseNaturalLanguageDuration` is evaluated at enqueue time).
The new path explicitly does not re-expire runs once they have been
allocated a concurrency slot. That is intentional: TTL is for runs that
are queued and have never started. Once a run has a slot it is on its
way to executing.
## Test plan
- [x] `pnpm run test --filter @internal/run-engine
./src/engine/tests/ttl.test.ts` — 15 tests, including a new "Re-enqueued
runs are not expired by TTL once they have started" that locks in the
queued-and-never-started contract.
- [x] `pnpm run test --filter @internal/run-engine
./src/engine/tests/delays.test.ts` — 5 tests, including "Delayed run
with a ttl" which now also asserts the TTL is armed from queue-enter
time, not `createdAt`.
- [x] `pnpm run test --filter @internal/run-engine
./src/engine/tests/lazyWaitpoint.test.ts` — 12 tests.
- [x] `pnpm run typecheck --filter @internal/run-engine`.
|
||
|
|
2b845455b0 |
feat(webapp): admin back-office editors for org max projects and batch rate limit (#3475)
## Summary - Adds admin-only editors on the back-office org page for `Organization.maximumProjectCount` and `Organization.batchRateLimitConfig`, alongside the existing API rate limit editor. - Splits the back-office org page into per-section components (`ApiRateLimitSection`, `BatchRateLimitSection`, `MaxProjectsSection`) so each tool is self-contained — adding new sections later doesn't bloat the route. - Generalizes the rate-limit form into a reusable `RateLimitSection` component + `RateLimitDomain` server config so API and batch share the same UI, validation, and action handler. Each domain only owns its env defaults, DB column, and logger key. - "Saved." banner and validation errors are scoped to the section that submitted, not the page. Heads-up: the API rate-limit log key was renamed `admin.backOffice.rateLimit` → `admin.backOffice.apiRateLimit` for symmetry with the new `admin.backOffice.batchRateLimit`. ## Test plan - [ ] As an admin, visit `/admin/back-office/orgs/:orgId` and confirm all three sections render with the org's current values (or system defaults). - [ ] Edit and save each section; confirm only that section shows the "Saved." banner. - [ ] Submit invalid input (e.g. `0` tokens, malformed interval); confirm errors render in the offending form only and the other sections stay closed. - [ ] Confirm a non-admin user is redirected away from the route. - [ ] After saving a rate-limit override, hit the org with traffic and confirm the new limit is enforced (API rate limit + batch rate limit code paths read the column at request time). |
||
|
|
a5ba406530 |
feat(webapp,redis): handle UNBLOCKED during ElastiCache role change (#3549)
## Summary When ElastiCache demotes a primary to replica — during a Multi-AZ failover or a vertical node-type change — the demoting primary issues an `UNBLOCKED` reply to any in-flight blocking commands (`BLPOP`, `BRPOP`, `BLMOVE`, `XREADGROUP ... BLOCK`, etc.) to clear them before the role flips. ioredis surfaces these as `ReplyError` to caller code. The shared `defaultReconnectOnError` added in #3548 only matches `READONLY` and `LOADING`. This extends it to `UNBLOCKED` so the disconnect-reconnect-retry cycle handles BLPOP-shaped errors the same way the existing two cases handle non-blocking-command errors. ## Fix ```ts export function defaultReconnectOnError(err: Error): boolean | 1 | 2 { const msg = err.message ?? ""; if ( msg.startsWith("READONLY") || msg.startsWith("LOADING") || msg.startsWith("UNBLOCKED") ) { return 2; } return false; } ``` Returning `2` tells ioredis to disconnect, reconnect, and re-issue the command. For a BLPOP that means a fresh BLPOP against the new primary instead of the `UNBLOCKED` error escaping to the caller. ## Test plan - [ ] CI green - [ ] Trigger a Multi-AZ failover or a vertical scale event on an ElastiCache replication group whose clients are running blocking commands and confirm no `UNBLOCKED` errors surface to caller code during the cutover. |
||
|
|
567e2a2c32 |
feat(webapp,redis): handle READONLY / LOADING during ElastiCache failover (#3548)
## Summary
During an ElastiCache role swap (failover) or node-type change (vertical
scale), the ioredis TCP/TLS connection stays open but the server starts
answering with `READONLY` (the client is talking to a node that became a
replica) or `LOADING` (node still loading data from disk). Without an
explicit hook, those errors surface to caller code as `ReplyError`
instances — every write op on the affected connection fails until the
cluster fully cuts over.
This PR adds `reconnectOnError` to every prod ioredis client so the
disconnect + reconnect + retry cycle absorbs these errors and caller
code never sees them.
## Fix
```ts
export function defaultReconnectOnError(err: Error): boolean | 1 | 2 {
const msg = err.message ?? "";
if (msg.startsWith("READONLY") || msg.startsWith("LOADING")) return 2;
return false;
}
```
Returning `2` tells ioredis to disconnect, reconnect, and re-issue the
failed command. After reconnect, DNS / SG state routes the new socket to
a writable node.
The helper lives in `@internal/redis` and is wired into both the shared
`createRedisClient` (which covers RunQueue, schedule-engine,
redis-worker, and every other internal-package consumer) and the direct
`new Redis(...)` call sites in the webapp.
V1-only marqs files are intentionally not migrated.
## Test plan
- [x] `pnpm run typecheck --filter webapp`
- [x] `pnpm run typecheck --filter @internal/run-engine`
- [x] Verified end-to-end against a live ElastiCache vertical-scale
event — caller-surfaced errors went from tens of thousands during the
cutover window down to a handful per ioredis client
- [ ] Confirm steady-state behavior unchanged after deploy
|
||
|
|
6cdd8814a3 |
fix(webapp): Fix for resizable side panel getting stuck at its min-size (#3538)
## Summary - Run-view inspector panel was glitching out on Firefox: visual flicker on close, locking up at min size, and intermittent `panelHasSpace` invariant errors. Root cause is the underlying `react-window-splitter` library's collapse animation, which uses `@react-spring/rafz` and interacts poorly with Firefox. - Disabled the library's collapse animation on Firefox only, app-wide (every consumer of `RESIZABLE_PANEL_ANIMATION`). Chromium and Safari behaviour is unchanged. ## Changes - **Firefox animation skip** in `RESIZABLE_PANEL_ANIMATION` — UA-detected at module load, resolves to `undefined` for Firefox so the library's animation actor completes in one frame instead of running its rAF loop. - **Inspector min raised 50px → 250px** so dragging can't shrink the panel into a near-useless width. - **`autosaveId` bumped `v2` → `v3`** to invalidate stale persisted snapshots (the library has a `// TODO` branch that ignores prop changes for already-registered panels, so existing users would otherwise still see the old 50px min). - **`react-window-splitter` pinned** to exact `0.4.1` to protect the patch from drifting if line offsets change in a patch release. - **Two hunks added to the existing `@window-splitter/state` patch:** - Removed the library's auto-collapse-on-drag block entirely. Every collapsible panel in the app is parent-controlled, and that block was triggering state-machine deadlocks when handlers were no-ops. Drag-to-collapse is now disabled across the app; collapse is only triggered explicitly (close button, ESC, URL change, etc.). - In `getDeltaForEvent`, fall back to the panel's `default` before its `min` when expanding — so the first ever click on a span opens the inspector at 500px, not 250px. ## Local testing confirmed - [x] Firefox: open a run, click various spans → panel opens instantly at 500px, drags freely between 250px and max, closes instantly to 0. No console errors. - [x] Chrome/Chromium: same flow, but with smooth open/close animation as before. - [x] Safari: same as Chrome. - [x] Reload mid-session → panel restores cleanly to the dragged size. - [x] Other resizable panels in the app (logs, deployments, schedules, batches, bulk-actions, runs index) still animate on Chromium/Safari. ## Notes - Linear: TRI-8584 - Branch contains intermediate commits exploring an unsuccessful snapshot-validator approach; they're reverted by the final commit. Cumulative diff is 6 files. Squash on merge if you'd prefer a clean history. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
ead1e5a53d |
feat(webapp): reload LLM pricing registry on Redis pub/sub (#3534)
## Summary Adds a Redis pub/sub reload path to the webapp's in-memory LLM pricing registry. When enabled on a process, the registry reloads from the database whenever a publish lands on the configured channel — instead of waiting for the existing 5-minute interval. Lets pricing/model changes propagate to cost enrichment within seconds. Subscription is **off by default** and opt-in per process. Only OTel-ingesting services need real-time freshness; dashboard and worker services run fine on the periodic interval and shouldn't pile onto each publish with a full-table reload. ## Design When `LLM_PRICING_RELOAD_PUBSUB_ENABLED=true`, subscribes via `createRedisClient` against `COMMON_WORKER_REDIS_*` and listens on `LLM_PRICING_RELOAD_CHANNEL` (default `llm-registry:reload`). The 5-minute periodic reload stays as a backstop, and a SIGTERM/SIGINT handler closes the subscription cleanly. The publisher side lives outside this PR — any process running in the same Redis namespace can trigger a reload by `PUBLISH llm-registry:reload <anything>`. Includes a `.server-changes/` note for the changelog. ### Debounced reload Bursts of publishes are coalesced. The first publish schedules a reload at T+`LLM_PRICING_RELOAD_DEBOUNCE_MS` (default 1s); subsequent publishes during that window are no-ops because the trailing reload picks up everything when it queries the DB. Bounds reload rate to at most 1 per debounce window regardless of publisher chattiness, so a runaway upstream publisher can't fan out into a flood of full-table-scan reloads. ## Test plan - [ ] With `LLM_PRICING_RELOAD_PUBSUB_ENABLED=false` (default): `redis-cli PUBSUB NUMSUB llm-registry:reload` returns `0` while the webapp is up - [ ] With it set to `true`: returns `>= 1` - [ ] `redis-cli PUBLISH llm-registry:reload test` returns `1` (one subscriber received) on a subscribed process - [ ] Mutate an `LlmModel` row externally, publish on the channel, observe the registry's match() picks up the change without waiting for the 5-min tick - [ ] Publish 100x in rapid succession; confirm only one reload fires within the debounce window |
||
|
|
f7a2bc7c96 |
Feat(webapp) filters UX update (#3451)
## Lots of filter UX improvements across lots of routes ### General - Promoted important filters out of the "More filters" so they're always visible - SearchInput primitive is now reusable and Esc now clears the field (AI filter input also clears with Esc) - Tooltips + keyboard shortcuts on every primary filter button - Brighter text on selected filter items / queue items - Filter dropdowns reordered for better hierarchy - Removed debounce on Tasks page search for faster filtering ### Tasks page search - Esc now clears the field - ENTER submits a search to improve performance when you have lots of tasks https://github.com/user-attachments/assets/4b30521e-dbc4-4468-b2af-8c85bdfb9002 ### Runs filters - Moves Status and Tasks out of the More filters menu - "Root only" toggle is set to false when you filter for a Task. This state isn't stored and flips back to the stored value if filters are cleared <img width="1690" height="986" alt="CleanShot 2026-04-26 at 19 24 08@2x" src="https://github.com/user-attachments/assets/b07da73c-140e-451f-a7bf-c32129317f63" /> ### Batches filters - General consistency improvements <img width="1429" height="948" alt="CleanShot 2026-05-08 at 09 50 35" src="https://github.com/user-attachments/assets/e5ec267f-2aa3-43ef-991e-93bf01bdaea5" /> ### Schedules - General consistency improvements <img width="1567" height="1141" alt="CleanShot 2026-05-08 at 09 51 11" src="https://github.com/user-attachments/assets/34b7da88-87c6-4e4d-a70f-fe13ea9f87ec" /> ### Queues - General consistency improvements <img width="824" height="416" alt="CleanShot 2026-05-08 at 09 52 02" src="https://github.com/user-attachments/assets/b4adc102-8192-4a68-b199-a175c2645a6c" /> ### Waitpoint tokens - General consistency improvements <img width="941" height="363" alt="CleanShot 2026-05-08 at 09 52 19" src="https://github.com/user-attachments/assets/d43aeb3f-7f80-454d-b183-fd077a4e3ff7" /> ### Models - General consistency improvements <img width="1570" height="509" alt="CleanShot 2026-05-08 at 09 53 17" src="https://github.com/user-attachments/assets/066d7646-4672-4cae-8ec0-e30a82889914" /> ### AI metrics - General consistency improvements <img width="1568" height="624" alt="CleanShot 2026-05-08 at 09 53 43" src="https://github.com/user-attachments/assets/fdfc4806-26fa-458d-a5ed-5c226b3bbc9f" /> ### Logs - General consistency improvements <img width="1267" height="752" alt="CleanShot 2026-05-08 at 09 54 30" src="https://github.com/user-attachments/assets/3e9ba871-b9dd-490e-aded-5d87134fd2bb" /> ### Errors - General consistency improvements <img width="1568" height="670" alt="CleanShot 2026-05-08 at 09 54 50" src="https://github.com/user-attachments/assets/fdda027a-e24f-4804-b4bb-203a6c2db960" /> ### Query - General consistency improvements - History, Scope, Triggered (date) filters all have shortcut tooltips - Scope filter now reuses the metrics ScopeFilter component <img width="1566" height="716" alt="CleanShot 2026-05-08 at 09 55 22" src="https://github.com/user-attachments/assets/0130b4a2-9daf-4edc-bada-3380aff4022a" /> ### Dashboards - General consistency improvements - Scope filter gets nicer icons and a shortcut - Nice icons for the Scope menu items <img width="1567" height="769" alt="CleanShot 2026-05-08 at 09 56 10" src="https://github.com/user-attachments/assets/7bea25f7-6c33-4d4a-a36d-3a1cb56afe09" /> ### Custom dashboard - General consistency improvements - Add chart, Add title, and the kebab menu now have tooltips + shortcuts <img width="1566" height="782" alt="CleanShot 2026-05-08 at 09 58 11" src="https://github.com/user-attachments/assets/9df4db25-b2c0-43a2-b92f-00256337d5a9" /> ### Environment variables - General consistency improvements <img width="1569" height="930" alt="CleanShot 2026-05-08 at 09 58 55" src="https://github.com/user-attachments/assets/26e614b4-88e7-400b-aa6d-a96bad488fb8" /> ### Preview branches - General consistency improvements <img width="1570" height="986" alt="CleanShot 2026-05-08 at 09 59 17" src="https://github.com/user-attachments/assets/57a2b939-3670-4252-ab2c-d6dc65bdda1b" /> |
||
|
|
3cb6b5e9c4 | docs(bun): note WebSocket limitation with remote browser connections (#3537) | ||
|
|
f8ddb766fa | feat: Plain customer cards (#2933) | ||
|
|
61ae67cc02 |
fix(webapp): stop leaking exception messages on 5xx API responses (#3536)
When a webapp API route's catch-all 500 branch handles a non-typed
exception, it returns the raw `error.message` to the caller. If the
exception originates from an internal subsystem (the ORM client, an
infra dependency, etc.) the server-side error string is surfaced
verbatim in the response body — exposing implementation details the API
surface shouldn't carry.
The leak shows up in three shapes across the routes:
- `return json({ error: error.message }, { status: 500 })`
- `return json({ error: error instanceof Error ? error.message :
"Internal Server Error" }, { status: 500 })`
- ``return json({ error: `Internal server error: ${error.message}` }, {
status: 500 })``
(plus a couple of analogous neverthrow-Result variants on admin routes.)
## Fix
Across 19 webapp routes, replace each leaking branch with a generic body
(`"Something went wrong"` / `"Internal Server Error"` to match the
file's existing fallback) and add `logger.error(...)` so full visibility
is preserved server-side. Catch blocks that branch on typed user-input
errors (`ServiceValidationError`, `EngineServiceValidationError`,
`OutOfEntitlementError`, `PrismaClientKnownRequestError`) are left
intact — those messages are constructed deliberately and intended to be
customer-facing.
## Test plan
- [x] `pnpm run typecheck --filter webapp`
- [x] Per-route manual probe: inject a synthetic `Error` at the top of
the catch'd `try` block (or fake the wrapped call's rejection / Result
error), curl the route with the dev API key, confirm the response body
changed from the synthetic message verbatim → generic body. 21/21 leak
sites verified end-to-end.
- [x] 4xx-typed-error paths spot-checked: throwing
`ServiceValidationError` from inside the catch'd try still surfaces its
message at 422 as intended.
|
||
|
|
749dc467f1 |
feat(webapp): link Sentry events to OTel traces via trace_id (#3531)
## Summary Stamps the active OpenTelemetry `trace_id` and `span_id` onto every Sentry event captured from the webapp, so engineers can copy a `trace_id` from a Sentry issue and search for the corresponding trace in any OTel-aware backend. Also adds an `otel_sampled` tag to indicate whether the trace was head-sampled — a cheap signal for whether the link will resolve to span data or hit a missing trace. ## Why Sentry and OTel were OTel-disconnected: `apps/webapp/sentry.server.ts` initialised Sentry with `skipOpenTelemetrySetup: true`, and no error-capture site (`logger.server.ts`, the Remix-wrapped `handleError`, the root `ErrorBoundary`) attached OTel context to the event. With many spans/sec across services, getting from a Sentry issue to its trace was guesswork. ## Approach Single global Sentry event processor, registered immediately after `Sentry.init`. On each event it reads `trace.getActiveSpan()?.spanContext()` via `@opentelemetry/api`, then writes: - `event.contexts.trace.trace_id` and `event.contexts.trace.span_id` (Sentry's native trace context fields) - `event.tags.otel_sampled` = `"true"` | `"false"` (derived from `traceFlags`) If no active span (module-load errors, scheduled timers without a context, primary cluster process), the processor returns the event unmodified — Sentry's default propagation context fills in. Implementation is co-located in `apps/webapp/sentry.server.ts` (no separate helper module — `sentry.server.ts` is built standalone by esbuild and a separate import would have required a new bundling step). Helper functions are exported so the unit tests can reach them without re-running `Sentry.init`. ## Non-goals (deliberate) - No sample rate change. ~95% of Sentry events will carry a `trace_id` that returns no spans in the tracing backend (head-sampled out). The `otel_sampled` tag makes that obvious at a glance. Raising find-rate is a separate conversation with cost trade-offs. - No user/org tags or `Sentry.setUser` (would need auth-helper + per-request scope wiring across multiple worker entrypoints — separate ticket). - Webapp image only. No changes to supervisor or CLI workers. ## Test plan - [x] Unit tests in `apps/webapp/test/sentryTraceContext.server.test.ts` — 9 tests covering: helper returns \`undefined\` with no active span; returns \`traceId\`/\`spanId\`/\`sampled=true\` for a recording span; returns \`sampled=false\` for a non-recording span; processor leaves the event unchanged with no active span; processor stamps \`trace_id\`/\`span_id\` onto \`contexts.trace\`; preserves existing \`contexts.trace\` fields; tags \`otel_sampled\` correctly for both sampled and non-sampled cases; never throws if \`@opentelemetry/api\` access throws. - [x] \`pnpm run typecheck --filter webapp\` passes. - [x] Manually verified end-to-end against a sandboxed Sentry project: confirmed both sampled and non-sampled traces correctly populate \`contexts.trace.trace_id\` matching the OTel ids logged from the loader, and the \`otel_sampled\` tag appears with the expected value. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3e6458f9b5 |
ci(claude): switch Claude Code actions to ANTHROPIC_API_KEY (#3532)
## Summary
Both Claude Code workflows (`claude.yml` and `claude-md-audit.yml`)
authenticated via `CLAUDE_CODE_OAUTH_TOKEN`, which broke when the org
disabled Claude subscription access for Claude Code:
> Your organization has disabled Claude subscription access for Claude
Code · Use an Anthropic API key instead, or ask your admin to enable
access
This switches both workflows to `anthropic_api_key: ${{
secrets.ANTHROPIC_API_KEY }}` (secret already added to the repo).
## Test plan
- [ ] Confirm `📝 CLAUDE.md Audit` runs to completion on this PR
- [ ] Confirm `@claude` mention in a PR comment still triggers the
`Claude Code` workflow successfully
|
||
|
|
62e006617e |
fix(cli): fail attempt on uncaught exception instead of hanging to maxDuration (TRI-9117) (#3529)
⚒️ Publish Worker (v4) / build (supervisor) (push) Has been cancelled
When a Node EventEmitter (e.g. node-redis) emits an "error" event with
no
listener attached, Node escalates it to process.on("uncaughtException")
in
the task worker. The worker reported the error via the
UNCAUGHT_EXCEPTION
IPC event but did not exit, and the supervisor-side handler in
taskRunProcess only logged the message at debug level — leaving the
run()
promise orphaned until maxDuration fired and producing empty attempts
(durationMs=0, costInCents=0).
The supervisor now rejects the in-flight attempt with an
UncaughtExceptionError and gracefully terminates the worker (preserving
the OTEL flush window) on UNCAUGHT_EXCEPTION. The attempt fails fast
with
TASK_EXECUTION_FAILED, surfacing the original error name, message, and
stack trace, and falls under the normal retry policy. This mirrors the
existing indexing-side behavior in indexWorkerManifest. Apply the same
handling to unhandled promise rejections, which Node already routes
through uncaughtException by default.
re2-prod-uncaught-exception
re2-test-uncaught-exception
|
||
|
|
6e8b039a4e |
ci: GHCR commit-SHA tag, OCI labels, and build provenance (#3528)
- Tags webapp images by full commit SHA on `main` pushes (`ghcr.io/triggerdotdev/trigger.dev:<sha>`) so any commit can be resolved to a digest easily. - Adds OCI labels (`source`, `revision`, `version`, `created`) so `docker inspect`, vulnerability scanners, and registry browsers see source/commit/version directly. - Signs each pushed digest with SLSA build provenance via `actions/attest-build-provenance@v4.1.0` (pinned by SHA), enabling `gh attestation verify oci://...` against the source commit and workflow. |
||
|
|
31999afcaa |
perf(webapp): trim BackgroundWorker.metadata to the schedule slice on create (#3525)
Large deploys (projects with many tasks or source files) blocked the
webapp event loop for several seconds inside Prisma's client-side
serializer on `BackgroundWorker.create`, tail-latencying every other
in-flight request on the same Node process. The `metadata` JSON column
was being written with the full deploy manifest — every task's config,
every queue and prompt, and the full source of every file — all of which
already live on dedicated columns or in dedicated tables.
Fix: project the manifest to `{ packageVersion, contentHash, tasks: [{
id, filePath, schedule }] }` on insert. The only post-write read site is
`changeCurrentDeployment`, which feeds `tasks[].schedule` into
`syncDeclarativeSchedules` at deploy promotion. The retained top-level
keys and per-task `filePath` are kept solely so
`BackgroundWorkerMetadata.safeParse` still succeeds on read.
## Test plan
- [ ] Deploy a project with declarative schedules; verify schedules are
created on first deploy
- [ ] Modify / remove schedules across subsequent deploys; verify sync
- [ ] Roll back to a previous deploy; verify `changeCurrentDeployment`
re-syncs schedules
- [ ] Inspect `BackgroundWorker.metadata` on a fresh deploy — should be
a small object, not the full manifest
|
||
|
|
14920ce2c4 |
fix(webapp): downgrade expected user-input error logs to warn (#3523)
`dac9c83bd` added `ignoreErrors: /^ServiceValidationError(?::|$)/` in
`apps/webapp/sentry.server.ts` to drop SVEs before they reach Sentry.
The
filter only matches when the captured event's *type* is
`ServiceValidationError`, but nine call sites in the webapp catch SVE
(and
analogous user-input error types — `OutOfEntitlementError`,
`CreateDeclarativeScheduleError`, `QueryError`) and call
`logger.error("wrapper message", { error: e })` *before* the type check.
The captured event is then titled with the wrapper message, with the
inner
error buried in `extra.error` — invisible to the SDK filter. Result: a
steady stream of expected user-input failures escalating as
`error`-level
events when they should be `warn`.
Each catch block now type-discriminates first, logs expected types at
`warn`,
and keeps unknown-error fall-throughs at `error`. For service sites that
wrap into SVE (`createBackgroundWorker`,
`createDeploymentBackgroundWorkerV4`),
the inner error is logged at `error` before wrapping — mirrors the
`waitpointCompletionPacket.server.ts` pattern from `dac9c83bd`.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
a8966a40ab |
fix(helm): bump clickhouse subchart 9.3.7 -> 9.4.4 (clickhouse 25.7.5) (#3524)
Fixes #3520. The bundled bitnami clickhouse subchart was pinned at `9.3.7` (clickhouse `25.6.1-debian-12-r0`), which hits a memory-tracker accounting bug under sustained ingest - the global counter overflows to ~7 EiB and every query gets rejected by OvercommitTracker until the pod is restarted. Self-hosters running 4.0.5 through 4.4.5 are exposed regardless of chart version since the subchart pin hadn't moved. Bumping to `9.4.4` (clickhouse `25.7.5-debian-12-r0`) pulls in the 25.7.x memory-tracker fixes. This is also the latest publicly packaged release at `oci://registry-1.docker.io/bitnamicharts` - that registry has been frozen since 2025-08-28 (Bitnami catalog changes), but the chart source remains under Apache 2 on `bitnami/charts`. The image continues to resolve via `bitnamilegacy/clickhouse` per the existing `values.yaml` override, since `bitnami/clickhouse` itself moved to paid-only. Verified locally: `helm dependency update` + `helm lint` + `helm template` + kubeconform across all 57 rendered manifests. Rendered statefulset image is `docker.io/bitnamilegacy/clickhouse:25.7.5-debian-12-r0`. |
||
|
|
386b4f65ff |
feat(webapp): per-org S2 basin migration (#3516)
## Summary Move from a single shared S2 basin to **per-org basins** with retention tied to the org's billing plan. Stops S2 from deleting streams out from under live chat sessions when basin retention fires before the chat ends, and unlocks per-org cost attribution. OSS / s2-lite installs are unaffected: provisioning is gated by `REALTIME_STREAMS_PER_ORG_BASINS_ENABLED` (default `false`), and the read precedence falls back to the global basin env var when an entity has no stamped basin. ``` basin = run.streamBasinName ?? session.streamBasinName ?? env.REALTIME_STREAMS_S2_BASIN ``` ## Design Three nullable `streamBasinName` columns (`Organization`, `TaskRun`, `Session`) plus a provisioner that idempotently creates the basin and reconfigures retention on plan changes. The trigger and session-create paths stamp the org's basin onto new rows; the realtime read path picks the basin from the entity context. Admin routes back-fill existing orgs and force-reconfigure a single org. ## Test plan - [x] `pnpm run typecheck --filter webapp --filter @internal/run-engine` - [x] Backfill admin route end-to-end (provision + DB stamp + S2 basin config). - [x] Reconfigure on plan change (all retention tiers). - [x] chat.agent multi-turn drives streams into the per-org basin. - [x] Legacy fallback when entity has no stamped basin. - [x] Provisioner is a no-op when the flag is off. |
||
|
|
3d418a9482 |
ci: add zizmor workflow security scanner (#3506)
Adds zizmor alongside the actionlint job from #3503. Both now run as parallel jobs in a single `.github/workflows/workflow-checks.yml`, triggered on `.github/workflows/**` and `.github/actions/**` changes. Zizmor is configured with `unpinned-uses: hash-pin` policy via `.github/zizmor.yml`, so any future unpinned action will fail CI. Findings upload SARIF to the Security tab alongside CodeQL. Bulk of the diff is cleanup of the findings zizmor surfaced on first run. `zizmor --fix=all` handled most of them mechanically; the rest were judgment calls. |
||
|
|
5dab2ae714 | docs(private links): refresh PrivateLink setup screenshots, add ElastiCache IP-finding tip and NLB inbound-rules step (#3517) | ||
|
|
45ec23cc73 |
feat(webapp): app auto session logout (#3473)
<img width="2284" height="2028" alt="CleanShot 2026-05-01 at 18 53 50@2x" src="https://github.com/user-attachments/assets/4f58cbb1-0168-40fb-a523-017f2ba625a1" /> ## Performance - **Per-request DB hit**: `getUserId` runs `getEffectiveSessionDuration` (User lookup + Org `aggregate`) on *every* authenticated request, including each fetcher poll. Consider caching the effective duration in the session cookie with a short TTL (e.g. 60s) and revalidating in the background. - **Double session commit in `root.tsx`**: `getUser` already runs the expiry check; then `commitAuthenticatedSessionLazy` commits the cookie again. Fine, but doubles `Set-Cookie` headers on every page load — worth a quick perf check. ## Correctness / Edge cases - **Lazy backfill assumes a root.tsx hit first**: users whose first post-deploy request is a fetcher/API route (`/resources/*`) skip the backfill until they navigate to a page. Not a security hole, but `getUserId` could backfill itself for completeness. - **No upper bound on `Organization.maxSessionDuration`**: admin API accepts `1` second, which would instant-logout every member on next request. Add a `min(60)` (or `min(300)` to match the lowest user option) to the Zod schema. - **No clock-skew tolerance**: `isSessionExpired` is exact-millisecond. Multi-instance deploys with skewed clocks could log users out a few seconds early/late. Probably fine for the 5-min minimum, but worth noting. ## Security - **Auto-logout audit log lacks IP/orgId**: HIPAA forensics typically wants source IP and which org context. Currently logs only `userId` + path. IP isn't PII for audit purposes; orgIds help correlate. Add both. - **Cookie `Max-Age` is 1 year regardless of user's setting**: intentional (server-side `issuedAt` is the source of truth), but reviewers will ask. Add a one-line comment on the cookie config explaining why. ## API surface - **`maxSessionDuration` is admin-PAT only**: no in-app UI for org owners to set/change their own cap. If this is "Trigger staff sets it during HIPAA onboarding", say so in the PR description; otherwise add an org-settings UI. - **Auto-submit dropdown has no confirmation**: misclicking "5 minutes" immediately shortens the user's session window with no undo. Consider a save button or 3-sec undo toast. ## Schema / migration - **`User.sessionDuration NOT NULL DEFAULT 31556952`**: instant on PG 11+ (metadata-only), but call out in the PR description so reviewers don't worry about a table rewrite on the User table. - **No DB-level constraint matching `SESSION_DURATION_OPTIONS`**: if the option list changes, existing users keep orphaned values. The dropdown's tag-along behaviour hides this — fine for now, but if you ever drop an option you'll need a backfill. ## UX - **Session expiry only fires on next request**: an idle authenticated tab keeps showing UI past the cap (until SSE/polling catches it, ~60s). Add a client-side timer based on the user's effective duration that triggers a fetcher to `/account` or `/logout` at expiry. - **No "you were signed out" message on logout**: users hitting their cap are bounced to `/` with no explanation. Was intentionally reverted in this PR — call that out so reviewers don't request it. ## Tests - Unit coverage on `sessionDuration.server.ts` is solid (215 lines). Missing: integration test for `getUserId` → expired session → redirect to `/logout`, and one for the loader's clamping fix (the most recent bug). Add at least the second one to lock in the regression. --------- Co-authored-by: Matt Aitken <matt@mattaitken.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
8c56d85f90 |
fix(helm): supervisor OTLP endpoint resolves cross-namespace (#3504)
Reported by external contributor. The supervisor template hardcoded a short DNS name for `OTEL_EXPORTER_OTLP_ENDPOINT`, which the supervisor then propagates verbatim into runner pods (`apps/supervisor/src/workloadManager/kubernetes.ts:196`). When runners are spawned in a different namespace via `supervisor.config.kubernetes.namespace`, the short name doesn't resolve and span/log export silently fails - runs complete fine but the dashboard shows nothing. Same FQDN pattern the chart already uses for `TRIGGER_WORKLOAD_API_DOMAIN` (line 203). Verified with `helm template trigger . --namespace my-ns` - renders `http://trigger-webapp.my-ns.svc.cluster.local:3030/otel`. Cheers Niels |
||
|
|
dc98ae4e28 |
chore: clean up stranded .server-changes/ files from v4.4.5 (#3509)
## Summary Delete 34 `.server-changes/*.md` files that should have been cleaned up automatically when v4.4.5 (#3406) was merged but were stranded by a workflow race. ## Why these are stale The `update-lockfile` job in `.github/workflows/changesets-pr.yml` is what cleans up consumed `.server-changes/*.md` files on the release branch. When v4.4.5 was merged on 2026-05-01, the post-merge workflow run on `main` failed at `pnpm install --frozen-lockfile` (stale lockfile in the merge commit), and `cancel-in-progress: true` cancelled the in-flight run from the previous push — so `update-lockfile` never reached the cleanup step. Result: the 34 files described changes that v4.4.5 already shipped, and they were re-appearing in the v4.4.6 release PR (#3501) under "Server changes" plus showing up as deletions in its diff. ## What this PR keeps - `fix-rollback-schedule-sync.md` — genuinely new for v4.4.6 (#3468), the only server change introduced after v4.4.5 - `README.md`, `.gitkeep` — directory infrastructure - `dev-cli-disconnect-md` — leaving alone (typo'd filename from March, no `.md` extension, not picked up by the cleanup glob anyway) ## After merge The next run of `changesets-pr.yml` will refresh #3501 with a "Server changes" section that only lists the v4.4.6 entry, and the only `.server-changes/` deletion in its diff will be `fix-rollback-schedule-sync.md`. ## Related - #3505 is the proper underlying fix — collapses the three-job graph into a single atomic commit by `changesets/action` so this race can't strand the cleanup again. This PR is just the one-time catch-up for the files that already got stranded. |
||
|
|
cad8791859 |
chore: make changeset:version atomic (#3505)
Follow-up to the v4.4.5 release incident where the release PR (#3406) was merged with a stale lockfile and stale Chart.yaml, breaking npm + helm releases. The two automation jobs (`update-lockfile`, `bump-chart-version`) got cancelled mid-flight by `cancel-in-progress` when the merge fired the workflow again on `main`. This restructures `changeset:version` so all the post-version-bump fixups happen in the same script and end up in a single atomic commit on `changeset-release/main`, via `changesets/action`'s normal commit step. Pattern borrowed from Cloudflare workers-sdk, Astro, shadcn/ui. ## Before ``` push: main └── release-pr (changeset version → bumps package.jsons, opens PR) └── update-lockfile (separate job, separate commit) └── bump-chart-version (separate job, separate commit) ``` Three jobs, three commits to the release branch. ## After ``` push: main └── release-pr └── changesets/action runs: changeset version pnpm install --lockfile-only node scripts/bump-helm-chart.mjs node scripts/cleanup-server-changes.mjs ...all staged and committed as ONE commit by the action ``` One job, one commit. |
||
|
|
b19cf6df25 |
ci: add actionlint workflow (#3503)
Adds an `actionlint` job that runs on changes to `.github/workflows/**` and `.github/actions/**`. Catches workflow bugs at PR time — expression typos, deprecated runner labels, broken matrices, and shellcheck issues in `run:` blocks. Run from the official `docker://rhysd/actionlint` image, digest-pinned alongside everything else. Existing workflows had 6 shellcheck findings, all fixed. |
||
|
|
57cca979c6 |
docs: refresh compute private beta page with may 1 updates (#3502)
Updates the compute private beta page with the May 1 release entry, plus a deploy-time warning when `us-east-1-next` is the project default. The new What's new entry, verbatim: ### May 1, 2026 - **Cold starts are faster across all machine sizes.** Every preset starts faster, including `micro` and `small-1x` - there's no longer a cold-start penalty for picking a smaller machine. - **First runs after a deploy are faster on every preset.** Boot snapshot creation is significantly quicker across the board, so the cold path is consistently snappier. - **`large-1x` and `large-2x` no longer hard-fail.** They're still not recommended - cold-start performance trails the smaller presets and we're ironing out reliability issues. Follow-up to #3472 and #3479. |
||
|
|
b65a04eb37 |
fix(cli,core): stop dev workers spinning at 100% CPU after parent CLI disconnect (#3491)
Orphaned `trigger-dev-run-worker` processes were pinning CPU at 100% after the dev CLI exited — stuck in an uncaughtException feedback loop where a closed IPC channel kept throwing `ERR_IPC_CHANNEL_CLOSED` back into a handler that itself called `process.send`. Fix: - `ZodIpcConnection` no-ops sends when the channel is disconnected. - Dev workers exit on `process.disconnect` instead of being re-parented to init. - All worker `uncaughtException` handlers route through a `safeSend` guard so the handler can never re-enter itself. Verified end-to-end: `kill -9` of the dev CLI now cleans up all child workers within ~2s. |
||
|
|
706a0b88c9 |
chore: upgrade pnpm to 10.33.2 with security hardening (#3489)
## Summary - Upgrade pnpm from 10.23.0 → 10.33.2 (latest minor) - Enable `blockExoticSubdeps: true` for supply-chain defense - Update all version references across the repo ## Security improvements in 10.28.2+ - Path traversal protection in `directories.bin` - Symlink-escape protection for `file:/git:` dependencies (prevents reading `/etc/passwd`, `~/.ssh/...`) - https://pnpm.io/settings#blockexoticsubdeps ## Files updated - `package.json` — `packageManager` field - `docker/Dockerfile` — 5 `corepack prepare` calls - `apps/supervisor/Containerfile` — 1 `corepack prepare` call - `pnpm-workspace.yaml` — added `blockExoticSubdeps: true` - `CLAUDE.md`, `AGENTS.md`, `CONTRIBUTING.md`, `ai/references/repo.md` — version references ## Verification - `pnpm install --frozen-lockfile` succeeds (no lockfile regen needed) - `pnpm install` (plain) produces zero lockfile diff - All CI checks pass Slack thread: https://triggerdotdev.slack.com/archives/C061L2MHW93/p1777625600974279?thread_ts=1777622248.762639&cid=C061L2MHW93 https://claude.ai/code/session_01G759MUqmjsPh9k1qDxbdjG --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
159408074a |
ci: swap buildjet/setup-node for actions/setup-node v6.4.0 (#3497)
Last action still firing the Node 20 deprecation warning after #3494. `buildjet/setup-node@v4.0.4` (the latest tag) declares `runs: using: 'node20'` and the repo hasn't shipped a node24 update. Workflows here run on `ubuntu-latest` (not buildjet runners), so the buildjet fork wasn't giving us anything we don't get from `actions/setup-node` directly. Swapping to `actions/setup-node@v6.4.0` (node24 runtime) silences the warning. |
||
|
|
04bdf4b90b |
perf(webapp): throttle PAT + OAT lastAccessedAt writes to once per 5 min (#3493)
## Summary
Each successful PAT (`PersonalAccessToken`) or OAT
(`OrganizationAccessToken`) authentication issues a `prisma.X.update({
lastAccessedAt: new Date() })` to bump the timestamp. For tokens used at
high frequency (CLI clients, integrations) this generates a per-request
DB write that is mostly redundant — the `lastAccessedAt` field is only
surfaced on the settings page so users can decide which tokens to
revoke, and "within the last 5 minutes" is plenty of granularity for
that.
## Design
Replace each unconditional `update` with a conditional `updateMany`
whose `WHERE` requires the existing `lastAccessedAt` to be `NULL` or
strictly older than 5 minutes:
```ts
await prisma.personalAccessToken.updateMany({
where: {
id: personalAccessToken.id,
OR: [
{ lastAccessedAt: null },
{ lastAccessedAt: { lt: new Date(Date.now() - PAT_LAST_ACCESSED_THROTTLE_MS) } },
],
},
data: { lastAccessedAt: new Date() },
});
```
The conditional runs inside the SQL `UPDATE`, so concurrent auths can't
race into a double-write.
No schema change. No migration. No new infrastructure. Throttle is a
hardcoded constant (`5 * 60 * 1000`) — easy to revisit.
## Test plan
- [x] `pnpm run typecheck --filter webapp`
- [x] `pnpm vitest run ./test/services/personalAccessToken.test.ts
./test/services/organizationAccessToken.test.ts` — 6/6 pass, verifying
the throttle `WHERE` clause is constructed correctly and the `update` is
skipped on token-not-found / wrong-prefix paths
|
||
|
|
1acdc506ea |
chore: bump helm chart version to 4.4.5 (#3500)
Follow-up to v4.4.5 release. The `bump-chart-version` job on the release PR was cancelled before it could run, so Chart.yaml was merged still pointing at 4.4.4. The helm release job ([failed run](https://github.com/triggerdotdev/trigger.dev/actions/runs/25218553990/job/73947054128)) caught it via its version-match guard. Once this merges I'll re-run the helm release workflow manually.helm-v4.4.5 |
||
|
|
30bd567d48 |
fix: sync declarative schedules on deployment rollback (#3468)
## ✅ Checklist - [x] I have followed every step in the [contributing guide](https://github.com/triggerdotdev/trigger.dev/blob/main/CONTRIBUTING.md) - [x] The PR title follows the convention. - [x] I ran and tested the code works --- ## Testing - Reviewed the code flow for deployment rollback (`ChangeCurrentDeploymentService`) and confirmed it was missing schedule sync - Verified all 4 callers of `ChangeCurrentDeploymentService` (UI rollback, UI promote, API promote, finalize deployment) are now covered - Ran `pnpm run typecheck --filter webapp` — passes cleanly --- ## Changelog When rolling back (or manually promoting) a deployment, declarative schedules were not being synced to match the target deployment's worker metadata. Schedules remained as configured by the most recent deployment rather than reflecting the target version's schedule configuration. This fix adds a call to `syncDeclarativeSchedules` in `ChangeCurrentDeploymentService` after the deployment promotion is updated. It parses the target deployment's stored `BackgroundWorkerMetadata` to restore the correct schedule state. This covers both rollback and promote paths (UI and API). Errors are handled gracefully so they don't block the deployment change itself. --- ## Screenshots N/A — backend-only change. 💯 Link to Devin session: https://app.devin.ai/sessions/0debf012b58c4132be778f8ea88cd2b6 --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: nick <55853254+nicktrn@users.noreply.github.com>v4.4.5 |
||
|
|
139cccf27e |
fix: update pnpm-lock.yaml for v4.4.5 release (#3498)
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 0s
🚀 Publish Trigger.dev Docker / publish-webapp (push) Has been skipped
🚀 Publish Trigger.dev Docker / publish-worker (push) Has been skipped
🚀 Publish Trigger.dev Docker / publish-worker-v4 (push) Has been skipped
🚀 Publish Trigger.dev Docker / units (push) Failing after 0s
## Summary The v4.4.5 release PR (#3406) was merged before the automated lockfile-update job in [\`changesets-pr.yml\`](.github/workflows/changesets-pr.yml) could push its commit. As a result main now has \`package.json\` bumped to \`4.4.5\` but \`pnpm-lock.yaml\` still pinned to \`4.4.4\`. This blocks every subsequent \`pnpm install --frozen-lockfile\` run, including: - \`release.yml\` for v4.4.5 publish ([run #25217579660](https://github.com/triggerdotdev/trigger.dev/actions/runs/25217579660)) — never published packages to npm - \`changesets-pr.yml\` on the next push to main ([run #25217579645](https://github.com/triggerdotdev/trigger.dev/actions/runs/25217579645)) ## Root cause (from CI logs) \`\`\` ERR_PNPM_OUTDATED_LOCKFILE Cannot install with "frozen-lockfile" because pnpm-lock.yaml is not up to date with <ROOT>/packages/build/package.json - @trigger.dev/core (lockfile: workspace:4.4.4, manifest: workspace:4.4.5) \`\`\` Regenerated via \`pnpm install --lockfile-only\` against current main. The diff is exactly what the canceled \`update-lockfile\` job would have produced: - 12 \`workspace:4.4.4\` → \`workspace:4.4.5\` specifier bumps - pnpm metadata refresh (deprecation annotations on transitive deps, one optional \`bufferutil\` peer resolution on \`react-email\`) No new direct dependencies, no version drops. ## Follow-ups (separate PRs) 1. **Re-run release.yml** via \`workflow_dispatch\` (\`type: release\`, \`ref\` = merge commit on main once this lands) to actually publish 4.4.5 to npm. 2. **Workflow fix** to prevent recurrence: fold the lockfile update into \`changeset:version\` so the \`release-pr\` job creates a single commit with version bumps + lockfile in sync. Removes the race window where the release PR is mergeable before \`update-lockfile\` runs.v.docker.4.4.5 |