v4.5.1
7543 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1cf56e5d29 |
ci: gate optional publish/notify jobs behind repository variables (#3950)
## Summary Several optional workflow jobs fail on forks and private mirrors that lack org-specific secrets or registry permissions. This adds per-job repository-variable gates so those deployments can switch them off without editing workflows — matching the pattern from #3901 (`ENABLE_CLAUDE_CODE` / `ENABLE_WORKFLOW_SECURITY_SCAN`). Two variables, both **default-enabled** (a job runs unless its variable is explicitly `'false'`), so canonical-repo behaviour is unchanged where the variables are unset: **`ENABLE_HELM_PRERELEASE`** — gates the chart-publish jobs that push to `oci://ghcr.io/<owner>/charts` (needs `write_package` on the owner's charts namespace): - `helm-prerelease.yml` → `prerelease` job - `release-helm.yml` → `release` job Without the permission these fail with `403: denied: permission_denied: write_package` on every PR / `helm-v*` tag. The `lint-and-test` jobs (lint + template + kubeconform, no push) always run, so chart validity is still enforced everywhere. **`ENABLE_DEPENDABOT_ALERTS`** — gates the Dependabot notifier crons that need `DEPENDABOT_ALERTS_TOKEN` / `SLACK_BOT_TOKEN` and post to a specific Slack: - `dependabot-critical-alerts.yml` → `alert` job (daily cron) - `dependabot-weekly-summary.yml` → `summary` job (weekly cron) On a fork/mirror these otherwise fire on schedule and fail (or post nowhere) indefinitely. ## Test plan - Variables unset (default): all jobs run as today. - `ENABLE_HELM_PRERELEASE=false`: helm `lint-and-test` runs, publish jobs skip — no 403 on repos lacking `write_package`. - `ENABLE_DEPENDABOT_ALERTS=false`: the two cron jobs skip cleanly (neutral, not failed). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
af526dea18 |
feat(webapp): chat AI UI improvements, new task landing pages and side menu (#3941)
Major dashboard restructure plus the new task landing pages and self-serve schedules add-on integration. ## Side menu - Full restructure: standalone Tasks / Runs / Sessions block at the top; new collapsible sections for AI, Observability, Deployments, Manage - Persisted collapse state per section in `dashboardPreferences` - New / updated icons across the menu - Dashboards section: built-in Run metrics + AI metrics + custom dashboards, with drag-to-reorder via ReactGridLayout (`DashboardList.tsx`) - DevPresence connection indicator in the env selector (DEV + V2) ## Tasks (`_index` — unified Tasks page) - Replaces the separated Agents / Standard / Schedules listing pages with one table - New `UnifiedTaskListPresenter` composes `TaskListPresenter` + `AgentListPresenter` (shared `currentWorker` lookup) - Columns: Type (with kind badge), ID, File, Running (numeric for tasks; running + suspended pills for agents), Activity (24h stacked-by-status), sticky menu - Search + "Task type" multi-select filter (URL-synced) - Client-side pagination at 25/page - Right-hand "useful links" panel (cookie-persisted state) - Live-reload SSE: page revalidates on `WORKER_CREATED` so onboarding `trigger dev` flips the blank state automatically ## Agent landing page (`/agents/$agentParam`) - New per-agent detail page - Top tabs (Sessions / Runs) toggle both the chart panel and the table - Three dashboard-style chart cards: Sessions/Runs activity, LLM spend, Tokens - `AgentDetailPresenter` queries ClickHouse for run activity, session activity (with FINAL on `sessions_v1`), and LLM cost/token activity from `llm_metrics_v1` - TimeFilter at the top drives all three charts - Sticky table header, resizable horizontal handle, sidebar with Test agent button + properties - Docs link → `ai-chat/overview` ## Standard Task landing page (`/tasks/standard/$taskParam`) - New per-task detail page mirroring the Agent layout - `TaskDetailPresenter` for activity + properties - Chart panel wrapped in a Card with "Runs by status" header - Top bar with title, TimeFilter, pagination - Right sidebar: Test task + identifier, queue, machine, retry, TTL, payload schema, etc. ## Scheduled Task landing page (`/tasks/scheduled/$taskParam`) - New per-task detail page mirroring the Agent / Standard layout - Top-bar actions (right → left): pagination, Bulk replay…, View all runs, TimeFilter, Create schedule - Connected schedules mini-table in the sidebar - **Self-serve schedules add-on integration** (reincarnated from the now-removed `/schedules` listing page during the `origin/main` merge): - Bottom usage bar pinned via `grid-rows-[auto_1fr_auto]` — progress ring + "X/Y of your schedules" + Purchase / Upgrade / Request CTA - At-limit "Create schedule" intercept dialog - `PurchaseSchedulesModal` extracted as a shared component (`apps/webapp/app/components/schedules/PurchaseSchedulesModal.tsx`) handling increase / decrease / above-quota / need-to-delete states - New resource action route at `/resources/orgs/$organizationSlug/schedules-addon` ## Sessions - Index page: list, filters, blank state, help tooltip rework - Detail page: combined input/output chronological view (replaces split tabs) - Improved raw-message view layout (full-height) - AI payload UI: `data-*` parts grouped under "AI SDK data parts:" label - `toSafeUrl` helper guards rendered URLs from streamed content - Fix: duplicate assistant content on inspector tab switch ## Playground (Test agent) - Restructured top menu; back button + agent-selector popover - Improved blank state - Recent agent chat history moved into the tabbed menu - Better message-scroll container (full height) ## Dashboards - New Dashboards landing page (`/dashboards`) — Run metrics, AI metrics, Create your own CTAs - `BuiltInDashboards` updated; new `TasksDashboardPresenter` for the tasks overview - Custom dashboards section gains drag-to-reorder; cosmetic fix for active-row drag-handle blending ## PageHeader / shared primitives - `PageTitle` gains an `accessory` prop supporting string (auto-wrapped in tooltip) and ReactNode - Help tooltips on Tasks, Runs, Sessions PageTitles explaining the concept and sub-categories - `Card` primitive used for dashboard-style chart panels throughout ## Code review fixes (last batch on this branch) - ClickHouse activity queries hardened: `FINAL` + `_is_deleted = 0` on `task_runs_v2` (ReplacingMergeTree); `organization_id` + `project_id` filters for sort-key prefix; `inserted_at` partition filter on `llm_metrics_v1` - `UnifiedTaskListPresenter`: shared `currentWorker` lookup; slug-collision guard in `mergeRunningStates`; off-by-one fixed in 24h bucket alignment - `ScheduleListPresenter`: halved platform RPCs by deriving limit from `currentPlan` instead of calling `getLimit` - Sessions detail: stopped IntersectionObserver / scroll listener re-attach on every chunk; `requestAnimationFrame` deferral on auto-scroll to avoid virtualizer race - URL hardening: `?types=` validated against known kinds; new `parseFiniteInt` helper applied to `from`/`to`/`page` params - AgentView: HITL resolution buffer now cleared once parts reach a terminal state (was an unbounded Map on long sessions); subscription effect deps documented with eslint suppression - `PurchaseSchedulesModal`: bundle state resets on each open instead of persisting stale drafts ## Manual testing Manual smoke-test plan is tracked under [TRI-10883](https://linear.app/triggerdotdev/issue/TRI-10883), broken into 20 sub-issues covering onboarding, self-serve schedules, side menu, the four landing pages, sessions, runs, dashboards, regressions and performance. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b7ef51d763 |
fix(webapp): make SDK bundle-docs build step work in pruned Docker image (#3947)
## Summary The webapp Docker image build runs `pnpm run build --filter=webapp...`, which builds `@trigger.dev/sdk` as a dependency. The SDK's `build` script recently gained a `bundle-docs` step (`tsx ../../scripts/bundleSdkDocs.ts`), but the build couldn't run it in the pruned image, breaking the image build. Two things were missing: - `docker/Dockerfile` copied `scripts/updateVersion.ts` into the builder stage but not `scripts/bundleSdkDocs.ts`, so the step failed with `ERR_MODULE_NOT_FOUND`. - Even with the script present, the repo-level `docs/` tree it reads is a separate workspace package that isn't in webapp's dependency graph, so `turbo prune --scope=webapp` excludes it — the script's missing-docs guard would then fail the build. ## Design The Dockerfile now copies `bundleSdkDocs.ts` alongside `updateVersion.ts`. `bundleSdkDocs.ts` skips gracefully when the repo `docs/` tree is absent, which is exactly the pruned-dependency-build case (the SDK is compiled there but never published). Publishing always runs from the full monorepo where `docs/` exists, so the missing-docs guard still protects releases — it only fires when `docs/` is present but a cited doc is genuinely missing, rather than when the whole tree was pruned away. This avoids dragging 27M of docs into a throwaway builder stage. ## Test plan - [x] `bundle-docs` from the full monorepo still bundles all cited docs (exit 0) - [x] Simulated pruned tree without `docs/` skips cleanly instead of failing - [ ] Webapp Docker image build succeeds in CI --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ef998a518b |
fix(webapp): make native realtime change publishing fail-safe (#3946)
Two defensive fixes to the native realtime backend's run-change publishing (behind a feature flag, off by default), so turning it on can never destabilize the run lifecycle. **Never throws at the caller.** Publish sites run synchronously on the run-engine event bus and the metadata flush loop. The internal publish was already wrapped in try/catch, but lazy construction (singleton + metrics) and record encoding ran before that guard, so a throw could propagate into a run lifecycle operation. The public `publishChangeRecord` / `publishManyChangeRecords` helpers now wrap the whole call and log-and-drop on failure. **Bounds outage buffering.** The publisher connection caps `maxRetriesPerRequest` at 1 (vs ioredis's default of 20), so during a pub/sub Redis outage a publish rejects after ~1 reconnect cycle instead of holding commands in memory for ~20s. A dropped publish is latency-only, since the consumer has a periodic backstop full-resolve. The offline queue stays on, so the first publish after a process boots still flushes once the connection is ready. |
||
|
|
f073d8708a |
ci: gate optional Claude and security-scan jobs behind repository variables (#3901)
## Summary Add per-job `if:` gates so deployments that don't want — or can't run — these jobs can switch them off via repository variables, without editing workflows. - `ENABLE_CLAUDE_CODE` gates the Claude jobs: interactive `@claude`, the CLAUDE.md audit, and the REVIEW.md drift audit. - `ENABLE_WORKFLOW_SECURITY_SCAN` gates the Zizmor job, which uploads SARIF and so needs GitHub code scanning enabled. Both default to **enabled**: a job runs unless its variable is explicitly set to `'false'`, so behaviour is unchanged anywhere the variables are unset. The sibling `actionlint` job and the report-only Trivy scan are untouched. ## Test plan - [x] `actionlint` clean on the four edited workflows - [x] YAML parses for all four files |
||
|
|
a7312b1b86 |
fix(webapp): stop logging expected auth/restore conditions as errors (#3931)
Two expected, non-failure conditions were being logged at `error` level, which surfaces them as exceptions in error tracking and adds noise without signal. This downgrades both to `warn`. The first is the checkpoint-restore path when a `RESTORE` event already exists for a checkpoint — a benign idempotency skip on a duplicate or retried event. The second is the `/api/v1/token` endpoint when the authorization code is invalid or expired, which is the expected steady state while the CLI polls the endpoint during login; genuinely unexpected failures there still log at `error`. No behavior or response changes — the token endpoint still returns 400 in the same cases. |
||
|
|
911a1cff80 |
docs: document the Sessions HTTP API (reference, channels, scopes) (#3942)
## Summary Documents the Sessions HTTP API for non-SDK and server-to-server callers, which until now appeared only in the conceptual [ai-chat/sessions](https://trigger.dev/docs/ai-chat/sessions) page. ## What's covered - **Sessions API reference** — `create`/`list`/`retrieve`/`update`/`close` added to the OpenAPI spec and a new "Sessions API" group (`management/sessions/*`), mirroring the Runs API. - **Channel endpoints** — a reference page for the `.in`/`.out` realtime HTTP endpoints (append, SSE read, records drain), the wire protocol, `Last-Event-ID` resume, and the per-direction auth boundary (`.out` append is secret-key only). - **Session scopes** — `read:sessions:{id}` / `write:sessions:{id}` in the authentication docs, with the capability boundary and the 1h token TTL. Cross-linked with the SDK-side `ai-chat/sessions` page. Verified by rendering each page on the Mintlify dev server. |
||
|
|
e092919c3f |
feat(sdk,cli): bundle agent skills + docs in the SDK for zero-drift (#3937)
## Summary `@trigger.dev/sdk` now ships the Trigger.dev agent skills and a curated snapshot of the docs those skills cite. The skills that `trigger skills` installs into your coding agent are thin pointers that read this bundled content from `node_modules`, so the guidance always matches the SDK version installed in your project. Previously the full skill text was copied into your repo at install time and went stale until you reinstalled after an upgrade. ## How it works The SDK's `files[]` now includes `skills/` (the full skill text) and `docs/` (a curated snapshot generated at build time). The docs manifest is derived from each skill's own `sources:` frontmatter, so a skill only ships the docs it references, and a skill that cites a missing doc fails the build. The CLI installs thin skills whose body points the agent at `node_modules/@trigger.dev/sdk/skills/<name>/SKILL.md` and `node_modules/@trigger.dev/sdk/docs/`. They keep the high-value "Common mistakes" anti-patterns inline so the trigger and the guardrails survive even if the agent does not follow the pointer. `getting-started` stays self-contained in the CLI because it runs before the SDK is installed. |
||
|
|
1f1a3666ee |
fix(sdk): custom agent loop parity for continuations, steering, and subtasks (#3936)
## Summary
Three fixes that bring custom agent loops (`chat.customAgent`
hand-rolled loops and `chat.createSession`) up to the behavior
`chat.agent` users already get, and that the docs already promise:
- **Continuation runs no longer replay already-answered messages.** A
chat continuing after a cancel, crash, or upgrade re-delivered every
prior user message into the loop's first wait, so the model re-answered
an old message while the real new one had to arrive via steering. The
`.in` resume cursor is now seeded before any listener attaches, using
the same boot logic as `chat.agent`.
- **Mid-stream steering no longer wipes the in-flight response.**
`chat.pipeAndCapture` (also backing `turn.complete()`) streamed without
a server-generated message id, so a `prepareStep` injection regenerated
the assistant id mid-stream and the frontend replaced the partial
message, discarding everything streamed before the injection.
- **Task-backed tools now work from custom agent loops.** A child task
triggered via `ai.toolExecute` failed with "chat.agent session handle is
not initialized" because the parent's chatId only threaded from the
per-turn context that hand-rolled loops never set. It now falls back to
the session handle the `chat.customAgent` wrapper binds at run boot, so
children can stream progress into the chat with `chat.stream.writer({
target: "root" })` (the documented sub-agent pattern).
## Root cause on the replay fix
Attaching any `.in` listener (`chat.createStopSignal`,
`chat.messages.on`, the first wait) opens the SSE tail with
`Last-Event-ID` taken from the seq cursor at attach time. Custom loops
attached before any cursor existed, so S2 replayed from seq 0. The fix
resolves the cursor from the latest turn-complete header and seeds both
manager cursors (`setLastSeqNum` drives the SSE resume point,
`setLastDispatchedSeqNum` gates waiter dispatch) before attach;
`chat.createSession` now creates its stop signal lazily on the first
iteration, after the seed. Seeding only the first cursor after attach
does not work, which is why the earlier attempt at this was reverted.
All three were reproduced red-green against the references ai-chat
project: the replay repro showed the continuation wait consuming a stale
message in 403ms with the real message arriving via steering injection;
post-fix the wait consumes the real message directly with no injection.
Steering now preserves the full in-flight response, and the deepResearch
sub-agent streams its progress parts into a raw-loop parent. Existing
behavior verified unchanged: full SDK unit suite, `chat.agent` steering,
and stop-then-continue on `chat.createSession`.
|
||
|
|
85d93ffe0e |
perf(webapp): skip queue search count (#3925)
### Summary Queue searches previously executed both a count query and a page query with identical filters. This PR switches filtered searches to `hasMore` pagination, removing the extra count query while preserving existing search behavior. ### Testing cd apps/webapp && pnpm run test ./test/queueListSearch.test.ts --run passes ### Changelog Improve filtered queue search performance. |
||
|
|
034058bce1 |
feat(webapp): add task metadata cache resolution metrics (#3934)
## Summary Adds observability to the task metadata cache that backs the trigger hot path. Follow-up to #3930, which made locked-version triggers fall back to the primary when the read replica returns no row; this makes the cache's effectiveness (and that fallback) measurable instead of inferred. ## What it emits A single bounded counter `task_meta_cache.resolve`, labeled by lookup path (`locked` / `current`) and the source that satisfied it (`cache` / `replica` / `writer` / `miss`): - `cache / total` is the cache hit rate (its inverse is how cold the cache runs). - `writer / total` is how often the read replica returned empty for a row the primary had (the condition #3930 recovers from). Labels are bounded, with no per-env / worker / slug cardinality. TRI-10873 |
||
|
|
8b405711ac |
feat(supervisor): workload create duration histogram with backend and outcome labels (#3928)
Adds a `workload_create_duration_seconds` Prometheus histogram to the supervisor, observed around the workload manager `create()` call: - `backend` label: `kubernetes` | `compute` | `docker` — set once from the configured workload manager - `outcome` label: `success` | `error` — the per-outcome counts double as a create error rate Registered on the supervisor's existing metrics registry, so it's exposed on the existing `/metrics` endpoint with no config changes. Notes: - Covers cold creates only; warm starts and restores return before reaching `create()`. - A create may include backend-internal retries, so one observation can span multiple attempts. - Fixed low cardinality: 2 active label sets per deployment × 10 buckets. |
||
|
|
52320679ab |
fix(webapp): stop locked-version triggers failing on stale replica reads (#3930)
## Summary `triggerAndWait` (and other locked-version triggers) could intermittently fail with `Task '<id>' not found on locked version '<version>'` for a task that was registered on that version. The failures came in bursts and recovered on their own, so a retry minutes later would succeed. ## Root cause For a locked-version trigger, the queue resolver looks up the task's `BackgroundWorkerTask` metadata from the read replica (behind a Redis cache). On a cache miss it queried the replica, and a `null` result was treated as "task not registered" and turned into a non-retryable 422. A read replica can return an empty result for a row that already exists on the primary, so a momentarily-behind replica produced a false negative even though the locked worker (resolved on the primary in the same request) clearly had the task. ## Fix On a cache miss, when the replica returns no row the resolver now re-checks the primary before concluding the task is missing. If the primary has the row it is used (and the cache is back-filled); the error fires only when the primary genuinely lacks it, which is the only case where the 422 is correct. The extra read happens on the cache-miss-and-replica-empty path only, so the hot path is unchanged. Verified with a unit test (replica stub vs. real primary) and end-to-end against a local streaming replica with replication paused to reproduce the stale read. TRI-10868 |
||
|
|
3d5cffc255 |
fix(cli): point to init when dev or update runs without a project (#3929)
## Summary Running `trigger.dev dev` before setting up a project crashed with a raw `Cannot find matching package.json` stack trace from a transitive dependency, instead of telling the user what to do next. It happens whenever `dev` (or `update`) runs in a directory with no `package.json` in it or any parent directory, for example right after creating an empty project folder, or when `init` was exited before it scaffolded anything. The CLI now detects the missing project and prints actionable guidance pointing at `init`. ## Fix `dev` runs an embedded package-version check before it loads any project config. That check resolved `package.json` through a helper that throws when nothing is found up the tree, and nothing caught it. It is now wrapped, so a missing `package.json` produces a clear "run init" message and a clean exit. The config loader had the same latent crash on the `--skip-update-check` path. Its resolvers for `package.json`, the lockfile, and the workspace root all ran before the friendly "couldn't find your trigger.config.ts" check, so any of them throwing masked it. That check now runs first and short-circuits before the resolvers touch the filesystem. Verified live: in an empty directory, `dev`, `dev --skip-update-check`, and `update` all print a "run init" message and exit cleanly; in a configured project, `dev` still resolves config and boots normally. |
||
|
|
43b493628c |
docs(ai-chat): add the 4.5.0-rc.6 changelog entry (#3927)
## Summary Adds the 4.5.0-rc.6 entry to the AI chat changelog, covering the chat-facing items shipping in [#3870](https://github.com/triggerdotdev/trigger.dev/pull/3870): the chat.agent reliability batch, the continuation boot latency fix, the chat.headStart hydration and reasoning fixes, the chat.createSession stop and continuation fixes, and the new trigger skills installer. Should merge alongside the release so the changelog matches the published version. |
||
|
|
3bc3a1796f |
docs(ai-chat): custom agents page, backend decision table, and a building-agents anatomy entry (#3921)
## Summary Documents the two lower-level chat backend APIs and restructures the Building agents section so it has a sane reading order. **Custom agents page.** `chat.customAgent()` was effectively undocumented (one passing mention) and `chat.createSession()` was buried at the bottom of the Backend page, prompted by a customer asking whether dropping down a level was supported at all. Both now live on one dedicated page framed as a composition: register with `customAgent`, then drive turns with the managed `createSession` iterator or a hand-rolled primitives loop. The page covers the patterns the managed lifecycle otherwise handles for you, each verified against a running agent: seeding history on continuation runs (and why the seed must go through the turn-0 `addIncoming`, which replaces the accumulator), persisting the user message before streaming so a mid-stream reload keeps it, racing `totalUsage` after a stop so the loop cannot wedge, and the single-message wire shape. **Backend page.** Now leads with a decision table across the three abstraction levels and focuses on `chat.agent()`, routing to the new page. Stale examples that read a plural `messages` field off the wire payload are fixed (copy-pasting them broke turn accumulation), and the ChatSessionOptions / ChatTurn reference tables gain their missing rows (`compaction`, `pendingMessages`, usage fields, `setMessages`, `prepareStep`). **Anatomy page + reorder.** The Building agents group opened with the long How it works mechanics page, a wall right after the Quick Start. A short Anatomy page now leads the group: the three moving parts, one annotated example where each region names the page that covers it, and a routing table. How it works moves to the end of the group as the depth payoff, matching where peer docs put their internals pages. All pages visually verified against a local Mintlify build; cross-links and anchors updated across the section. |
||
|
|
84809b02ca |
docs(ai-chat): head-start persistence contract and a clearer sessions page (#3908)
## Summary Two documentation improvements for the AI chat docs. **Head-start persistence contract.** The fast starts page now documents what your hooks can rely on across a head-start handover: one stable assistant `messageId` for the whole turn, `onTurnComplete` as the canonical persistence point, reasoning parts flowing into durable history, and how Head Start composes with `hydrateMessages` (the first-turn history arrives as `incomingMessages`, and the runtime splices the warm partial onto the hydrated chain, deduplicated by id). The hydrate examples on the lifecycle hooks and database persistence pages now upsert their conversation row, since head-start first turns run without a preload to create it. **Sessions page.** The page opened with "a durable, task-bound, bi-directional I/O channel pair", which reads as jargon and omitted run orchestration entirely. It now leads with the plain mental model (a pair of durable streams: input carries user messages, output carries everything the agent produces) plus the Session's role orchestrating runs, a diagram, a minimal runnable example, and a section on the one-session-many-runs lifecycle. Documents behavior shipping in [#3907](https://github.com/triggerdotdev/trigger.dev/pull/3907). |
||
|
|
51af9ae14c |
docs(ai-chat): correct chat.agent reference drift (#3892)
## Summary Accuracy fixes across the AI chat docs: drop the non-existent per-call option from `transport.preload`, clarify that `onValidateMessages` only fires on turns carrying incoming messages, soften the turn-complete token-refresh wording (the header is optional), document the new `onTurnComplete` `error` field and `finishReason`, and correct the idle-timeout default to 30 seconds. |
||
|
|
b8a576a348 |
docs: document the trigger skills installer (replaces agent rules) (#3871)
## Summary Updates the AI-tooling docs for the new `trigger skills` installer that shipped in #3868. The Skills page now documents `trigger skills` (skills bundled with the CLI, version-matched to your SDK) and the four bundled skills: `authoring-tasks`, `realtime-and-frontend`, `authoring-chat-agent`, `chat-agent-advanced`. The old Agent Rules page becomes a short "rules are now skills" redirect (kept because existing redirects and the CLI link point at it), and the Building with AI overview collapses the three-way Skills/Rules/MCP comparison into Skills vs MCP. Hold until the v4.5 CLI release ships, since `trigger skills` is not on npm until then. |
||
|
|
97c12e2510 |
docs(management): document TriggerClient for multi-target SDK usage (#3694)
## Summary Docs follow-up for #3683 (`TriggerClient` for per-instance SDK configuration). Adds a dedicated reference page and threads the new pattern through the existing management + preview-branches docs. ## What's in **New page** `docs/management/multiple-clients.mdx` — when to use `TriggerClient` vs `configure()` vs `auth.withAuth`, env-var fallback rules, isolation contract, namespace surface, `inheritContext` opt-in, and a when-to-use-what table. **Updated pages** - `docs/management/authentication.mdx` — rewrote the `auth.withAuth` section to reflect the now-ALS-backed semantics (the prior version warned about concurrency races and pointed at issue #3298 as a tracked fix; that fix landed in #3683). Added `tr_preview_*` to the key prefix list. Reframed the multi-target use case to lead with `TriggerClient`, with `auth.withAuth` as the temporary-override helper. - `docs/management/overview.mdx` — added a `Multiple clients in one process` subsection. - `docs/deployment/preview-branches.mdx` — added a `Triggering across multiple branches from one process` example. - `docs/triggering.mdx` — one-liner pointing at the new page for cross-project triggering. - `docs/docs.json` — slotted `management/multiple-clients` into the Management API nav, right after authentication. Paired with #3683. ## Test plan - [ ] Mintlify preview renders cleanly - [ ] Code samples in each updated page run as documented - [ ] Cross-page links resolve (`/management/multiple-clients`, `/management/authentication`) |
||
|
|
5fab8cafcf |
chore: release v4.5.0-rc.6 (#3870)
🚀 Publish Trigger.dev Docker / units (push) Failing after 4s
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 5s
🚀 Publish Trigger.dev Docker / publish-worker-v4 (push) Has been skipped
🚀 Publish Trigger.dev Docker / scan-webapp (push) Has been skipped
🚀 Publish Trigger.dev Docker / publish-webapp (push) Has been skipped
🚀 Publish Trigger.dev Docker / publish-worker (push) Has been skipped
🧭 Helm Chart Release / lint-and-test (push) Has been cancelled
🧭 Helm Chart Release / release (push) Has been cancelled
🚀 Publish Trigger.dev Docker / 📣 Dispatch main image (push) Has been cancelled
## Summary 7 improvements, 1 bug fix. ## Improvements - `trigger init` now sets up your AI coding assistant as part of project setup: pick the MCP server, the agent skills, or both, then scaffold with the CLI or hand off to your assistant. Adds a new `getting-started` agent skill that teaches assistants how to bootstrap Trigger.dev (install the SDK, write `trigger.config.ts`, create a first task, run `trigger dev`), so the AI-driven setup path works end to end. It ships in the CLI alongside the existing skills, version-matched to your SDK. ([#3872](https://github.com/triggerdotdev/trigger.dev/pull/3872)) - `dev` and `deploy` now fail with a clear error when two tasks are defined with the same id, including across different task types (e.g. a scheduled task and a regular task sharing an id). Previously the second definition silently overwrote the first, so one of the tasks would vanish with no warning. Task ids are detected as duplicates during indexing (naming each offending id and the files it was found in), and the same rule is enforced server-side when the background worker is registered. ([#3865](https://github.com/triggerdotdev/trigger.dev/pull/3865)) - `trigger skills` installs Trigger.dev agent skills into your coding agent so it knows how to write tasks, schedules, realtime, and chat.agent code. The skills ship with the CLI and are copied into each tool's native skills directory (Claude Code, Cursor, GitHub Copilot, and Codex / AGENTS.md), and `trigger dev` offers to install them on first run. ([#3868](https://github.com/triggerdotdev/trigger.dev/pull/3868)) - Reliability fixes for `chat.agent`. A user message sent while the agent is streaming is no longer delivered twice (which could run a duplicate turn), input appends now carry an idempotency key so a retried send can't duplicate a message, stopping a generation clears the streaming state so a page reload doesn't replay the stopped turn, and runs can now carry the full set of dashboard tags instead of being silently truncated. `onTurnComplete` now fires on errored turns (with the thrown error attached) and the failed turn's user message is persisted so it isn't lost on the next run. Custom agents and manual `chat.writeTurnComplete` callers now trim the output stream, sending a custom action no longer leaves a second stream reader running, and a long-lived `watch` subscription no longer grows its dedupe set without bound. ([#3891](https://github.com/triggerdotdev/trigger.dev/pull/3891)) - Continuation chat boots no longer stall for around 10 seconds before the first turn. The `session.in` resume cursor is now found with a non-blocking records read instead of draining an SSE long-poll (which always waited out its full 5 second inactivity window, twice per boot), the boot reads run concurrently, and chat snapshots carry the cursor so subsequent boots skip the scan entirely. ([#3907](https://github.com/triggerdotdev/trigger.dev/pull/3907)) - Record client-side dequeue API latency in the supervisor consumer pool as a Prometheus histogram (`queue_consumer_pool_dequeue_duration_seconds`, labelled by `outcome`: success/empty/error). ([#3887](https://github.com/triggerdotdev/trigger.dev/pull/3887)) - Add `GetProjectEnvironmentsResponseBody` and `ProjectEnvironment` schemas for the new `GET /api/v1/projects/{projectRef}/environments` endpoint, which lists the parent environments (dev, staging, preview, prod) a personal access token can access for a project. Dev is scoped to the token owner and branch (preview child) environments are excluded. ([#3880](https://github.com/triggerdotdev/trigger.dev/pull/3880)) ## Bug fixes - Fix two `chat.createSession()` bugs: stopping a generation no longer wedges the run (the turn loop raced a `totalUsage` promise that never settles after a stop-abort), and continuation runs now wait for the next message instead of invoking the model with an empty prompt. ([#3920](https://github.com/triggerdotdev/trigger.dev/pull/3920)) <details> <summary>Raw changeset output</summary> ⚠️⚠️⚠️⚠️⚠️⚠️ `main` is currently in **pre mode** so this branch has prereleases rather than normal releases. If you want to exit prereleases, run `changeset pre exit` on `main`. ⚠️⚠️⚠️⚠️⚠️⚠️ # Releases ## @trigger.dev/build@4.5.0-rc.6 ### Patch Changes - Updated dependencies: - `@trigger.dev/core@4.5.0-rc.6` ## trigger.dev@4.5.0-rc.6 ### Patch Changes - `trigger init` now sets up your AI coding assistant as part of project setup: pick the MCP server, the agent skills, or both, then scaffold with the CLI or hand off to your assistant. Adds a new `getting-started` agent skill that teaches assistants how to bootstrap Trigger.dev (install the SDK, write `trigger.config.ts`, create a first task, run `trigger dev`), so the AI-driven setup path works end to end. It ships in the CLI alongside the existing skills, version-matched to your SDK. ([#3872](https://github.com/triggerdotdev/trigger.dev/pull/3872)) - `dev` and `deploy` now fail with a clear error when two tasks are defined with the same id, including across different task types (e.g. a scheduled task and a regular task sharing an id). Previously the second definition silently overwrote the first, so one of the tasks would vanish with no warning. Task ids are detected as duplicates during indexing (naming each offending id and the files it was found in), and the same rule is enforced server-side when the background worker is registered. ([#3865](https://github.com/triggerdotdev/trigger.dev/pull/3865)) - `trigger skills` installs Trigger.dev agent skills into your coding agent so it knows how to write tasks, schedules, realtime, and chat.agent code. The skills ship with the CLI and are copied into each tool's native skills directory (Claude Code, Cursor, GitHub Copilot, and Codex / AGENTS.md), and `trigger dev` offers to install them on first run. ([#3868](https://github.com/triggerdotdev/trigger.dev/pull/3868)) ```bash trigger skills --target claude-code ``` Replaces the previous `install-rules` command, which stays as an alias. - Updated dependencies: - `@trigger.dev/core@4.5.0-rc.6` - `@trigger.dev/build@4.5.0-rc.6` - `@trigger.dev/schema-to-json@4.5.0-rc.6` ## @trigger.dev/core@4.5.0-rc.6 ### Patch Changes - Reliability fixes for `chat.agent`. A user message sent while the agent is streaming is no longer delivered twice (which could run a duplicate turn), input appends now carry an idempotency key so a retried send can't duplicate a message, stopping a generation clears the streaming state so a page reload doesn't replay the stopped turn, and runs can now carry the full set of dashboard tags instead of being silently truncated. `onTurnComplete` now fires on errored turns (with the thrown error attached) and the failed turn's user message is persisted so it isn't lost on the next run. Custom agents and manual `chat.writeTurnComplete` callers now trim the output stream, sending a custom action no longer leaves a second stream reader running, and a long-lived `watch` subscription no longer grows its dedupe set without bound. ([#3891](https://github.com/triggerdotdev/trigger.dev/pull/3891)) - Continuation chat boots no longer stall for around 10 seconds before the first turn. The `session.in` resume cursor is now found with a non-blocking records read instead of draining an SSE long-poll (which always waited out its full 5 second inactivity window, twice per boot), the boot reads run concurrently, and chat snapshots carry the cursor so subsequent boots skip the scan entirely. ([#3907](https://github.com/triggerdotdev/trigger.dev/pull/3907)) - Record client-side dequeue API latency in the supervisor consumer pool as a Prometheus histogram (`queue_consumer_pool_dequeue_duration_seconds`, labelled by `outcome`: success/empty/error). ([#3887](https://github.com/triggerdotdev/trigger.dev/pull/3887)) - `dev` and `deploy` now fail with a clear error when two tasks are defined with the same id, including across different task types (e.g. a scheduled task and a regular task sharing an id). Previously the second definition silently overwrote the first, so one of the tasks would vanish with no warning. Task ids are detected as duplicates during indexing (naming each offending id and the files it was found in), and the same rule is enforced server-side when the background worker is registered. ([#3865](https://github.com/triggerdotdev/trigger.dev/pull/3865)) - Add `GetProjectEnvironmentsResponseBody` and `ProjectEnvironment` schemas for the new `GET /api/v1/projects/{projectRef}/environments` endpoint, which lists the parent environments (dev, staging, preview, prod) a personal access token can access for a project. Dev is scoped to the token owner and branch (preview child) environments are excluded. ([#3880](https://github.com/triggerdotdev/trigger.dev/pull/3880)) ## @trigger.dev/python@4.5.0-rc.6 ### Patch Changes - Updated dependencies: - `@trigger.dev/sdk@4.5.0-rc.6` - `@trigger.dev/core@4.5.0-rc.6` - `@trigger.dev/build@4.5.0-rc.6` ## @trigger.dev/react-hooks@4.5.0-rc.6 ### Patch Changes - Updated dependencies: - `@trigger.dev/core@4.5.0-rc.6` ## @trigger.dev/redis-worker@4.5.0-rc.6 ### Patch Changes - Updated dependencies: - `@trigger.dev/core@4.5.0-rc.6` ## @trigger.dev/rsc@4.5.0-rc.6 ### Patch Changes - Updated dependencies: - `@trigger.dev/core@4.5.0-rc.6` ## @trigger.dev/schema-to-json@4.5.0-rc.6 ### Patch Changes - Updated dependencies: - `@trigger.dev/core@4.5.0-rc.6` ## @trigger.dev/sdk@4.5.0-rc.6 ### Patch Changes - Reliability fixes for `chat.agent`. A user message sent while the agent is streaming is no longer delivered twice (which could run a duplicate turn), input appends now carry an idempotency key so a retried send can't duplicate a message, stopping a generation clears the streaming state so a page reload doesn't replay the stopped turn, and runs can now carry the full set of dashboard tags instead of being silently truncated. `onTurnComplete` now fires on errored turns (with the thrown error attached) and the failed turn's user message is persisted so it isn't lost on the next run. Custom agents and manual `chat.writeTurnComplete` callers now trim the output stream, sending a custom action no longer leaves a second stream reader running, and a long-lived `watch` subscription no longer grows its dedupe set without bound. ([#3891](https://github.com/triggerdotdev/trigger.dev/pull/3891)) - Continuation chat boots no longer stall for around 10 seconds before the first turn. The `session.in` resume cursor is now found with a non-blocking records read instead of draining an SSE long-poll (which always waited out its full 5 second inactivity window, twice per boot), the boot reads run concurrently, and chat snapshots carry the cursor so subsequent boots skip the scan entirely. ([#3907](https://github.com/triggerdotdev/trigger.dev/pull/3907)) - Fix `chat.headStart` when `hydrateMessages` is registered. The warm route's step-1 partial now reaches the agent's accumulator on the hydrate path, so `onTurnComplete` carries the full first turn (the head-start user message included), tool-call handovers resume from step 2 instead of re-running step 1, and the assistant `messageId` stays stable across the handover. ([#3907](https://github.com/triggerdotdev/trigger.dev/pull/3907)) - Preserve reasoning parts across the `chat.headStart` handover. Extended-thinking models' step-1 reasoning now lands in the durable session history (and `onTurnComplete`) under the same assistant `messageId`, with provider metadata intact so Anthropic thinking signatures survive replays. ([#3907](https://github.com/triggerdotdev/trigger.dev/pull/3907)) - Fix two `chat.createSession()` bugs: stopping a generation no longer wedges the run (the turn loop raced a `totalUsage` promise that never settles after a stop-abort), and continuation runs now wait for the next message instead of invoking the model with an empty prompt. ([#3920](https://github.com/triggerdotdev/trigger.dev/pull/3920)) - Updated dependencies: - `@trigger.dev/core@4.5.0-rc.6` ## @trigger.dev/plugins@4.5.0-rc.6 ### Patch Changes - Updated dependencies: - `@trigger.dev/core@4.5.0-rc.6` </details> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>helm-v4.5.0-rc.6 v.docker.4.5.0-rc.6 v4.5.0-rc.6 |
||
|
|
002c441f50 |
feat(webapp): self serve schedules add-on (#3811)
Adds the purchase UI for extra schedules, mirroring preview branches ## Changes - `setSchedulesAddOn` platform client + `SetSchedulesAddOnService` (purchase + quota-increase via Plain). - `ScheduleListPresenter` surfaces add-on / quota / pricing; `checkSchedule` counts purchased schedules toward the limit (`base + purchased`). - `PurchaseSchedulesModal` on the Schedules page — bought in **bundles of 1,000 ($10/mo each)**; bundle increments enforced client-side and in the action's zod schema. |
||
|
|
47834198fc |
fix(sdk): stop chat.createSession wedging on stop and erroring on continuation boots (#3920)
## Summary
Two `chat.createSession()` bugs that break chats at its abstraction
level:
1. **Stopping a generation wedged the run forever.** `turn.complete()`
bare-awaited the AI SDK's `totalUsage` promise, which never settles
after a stop-abort. The run stayed stuck inside the stopped turn (trace
shows a permanently partial `ai.streamText` span and no further `waiting
for next message`), so the chat could never take another message. Fixed
with the same 2s `Promise.race` guard `chat.agent`'s turn loop already
uses.
2. **Continuation runs invoked the model with an empty prompt.** The
first turn only waited for a message on `preload` boots. A continuation
run (spawned after a cancel, crash, or version upgrade) arrives with the
boot payload stripped, so the loop ran a turn with zero messages and
errored with `AI_InvalidPromptError: messages must not be empty`.
Message-less continuation boots now wait for the next session input
("waiting for first message (continuation)"), and `turn.continuation` is
preserved across the wait so user code can seed stored history off it.
Both reproduced and verified end-to-end against a live environment (stop
followed by a next turn; cancel followed by a continuation turn with
seeded history), plus the existing unit suite.
|
||
|
|
a04cdffda6 |
fix(webapp): stop replica lag from double-triggering session runs and 404ing fresh sessions (#3914)
## Summary Two read-replica races on the session APIs could break chats whose first activity lands inside the replication window (or any time the replica lags): 1. A session's first `.in` append or `.out` subscribe could fail with a 404 for a session that exists on the writer, because the route resolved the Session row on the replica only. 2. `ensureRunForSession` probed run liveness on the replica, so a probe miss on a run triggered moments earlier was judged "run is dead" and a second live run was spawned for the same session. Both runs then consumed the same input stream, producing duplicated turns and doubled responses (and doubled LLM cost). ## Fix Liveness now re-probes the writer before declaring the current run dead (the old code already fell back to the writer, but only to recover the friendlyId, after the wrong verdict was made). Session resolution on the append and subscribe/init routes goes through a new `resolveSessionWithWriterFallback`, which stays replica-first on the hot path and only touches the writer on a miss. Reproduced and verified against a local streaming replica with an artificial apply delay: pre-fix, a send immediately after session creation reliably produced either the 404 or two executing runs with a doubled response; post-fix, the same flow produces exactly one run and one response. Also rides along: the local docker replica's default apply delay drops from 150ms to a realistic 20ms (override via `REPLICA_APPLY_DELAY` when you want to deliberately widen the race window). |
||
|
|
eb498d137f |
fix(plugins): drop unused gitBranch re-export from the package entry (#3923)
`@trigger.dev/plugins` re-exported `sanitizeBranchName`/`isValidGitBranchName` from `@trigger.dev/core` as a convenience forwarder. Nothing actually imports them through this package — every consumer (webapp, `@trigger.dev/rbac`, …) imports them directly from `@trigger.dev/core/v3/utils/gitBranch`. Removing the forwarder keeps the package entry free of **runtime** core imports (only type re-exports + `buildJwtAbility` remain), so consumers that bundle `@trigger.dev/plugins` from source don't pull an unrelated core subpath into their build. No behavior change; the helpers remain available from `@trigger.dev/core` where they're defined. |
||
|
|
f48c89752c |
perf(webapp): parallelize streaming batch-item ingest (#3777)
## Problem
The item-streaming endpoint of the two-phase batch API (`POST
/api/v3/batches/:batchId/items`) processed streamed items strictly
sequentially. For a batch of many large payloads, each offloaded to
object storage inline, this serialized N object-store round-trips inside
a single request and could exceed Node's default `server.requestTimeout`
(300s). The webapp then returned `408`, which the SDK reads as `408
terminated` and retries up to 5 times, turning a slow ingest into a
failure that takes tens of minutes to surface.
## Fix
Ingest now runs through `p-map` over the NDJSON async iterable with
bounded concurrency (`STREAMING_BATCH_INGEST_CONCURRENCY`, default 10):
- `p-map` pulls lazily from the stream, so at most `concurrency` items
are read and in-flight at once. Peak memory stays bounded to roughly
`concurrency × STREAMING_BATCH_ITEM_MAXIMUM_SIZE` and request-body
backpressure is preserved.
- Set the env to `1` for fully sequential ingestion (escape hatch).
## Why this is safe (ordering and idempotency unchanged)
- Ordering derives from each item's index (enqueue `timestamp =
batch.createdAt + index`), not enqueue order.
- Dedup is atomic per index in `enqueueBatchItem`.
- The NDJSON parser now stamps oversized-item markers with their emit
position, removing the consumer's sequential `lastIndex` assumption (the
only order-dependent bit).
- The count-check and conditional-seal path is untouched.
## Scope
This speeds up every batch ingested through the streaming endpoint, not
just large-payload batches. Each item does a per-item Redis enqueue
regardless of size, and those now overlap. Large payloads benefit most
because they add an object-store offload round-trip on top of the
enqueue.
## Verification
Added an integration test (`streamBatchItems.test.ts`) that drives the
real service against Postgres + Redis + RunEngine and times a 150-item
batch at increasing concurrency. Object-store offload is modelled as a
fixed per-item latency (local round-trips are too small to compare
meaningfully):
```
runCount=150
large payloads (10ms/item offload):
concurrency=1 1739ms
concurrency=10 192ms (9.1x faster)
concurrency=50 57ms (30.7x faster)
small payloads (Redis enqueue only, no offload):
concurrency=1 90ms
concurrency=10 24ms (3.7x faster)
```
The test asserts correctness at every concurrency (all items accepted,
sealed, enqueued exactly once), that parallel ingest beats the
sequential floor, and that the small-payload case is strictly faster
than sequential, so the win is not specific to large payloads.
Also exercised end-to-end over real HTTP against a local server: a
20-item batch (12MB body) ingests and seals, a re-stream of the sealed
batch returns `sealed: true` with zero re-accepted items (idempotent
retry), and an oversized item still seals at its correct index.
Existing coverage stays green: concurrent ingest of a 100-item batch,
in-flight processing never exceeding the configured concurrency,
concurrent dedup on streaming retry, and emit-position marker indexing.
## Follow-ups (not in this PR)
- SDK pre-offload of large item payloads (send `application/store` refs
instead of raw blobs) to remove object-store work from the request hot
path and shrink the request body.
- Optional `server.requestTimeout` bump as a safety net.
## CI fix
Added `.github/workflows/codeql.yml` to replace GitHub's automatic
("dynamic") CodeQL scanning. The dynamic setup was failing to upload
SARIF results because the auto-generated `GITHUB_TOKEN` lacked the
`security-events: write` permission. The explicit workflow grants that
permission at the job level and pins all actions to commit SHAs,
consistent with the repo's security conventions.
## ✅ Checklist
- [ ] I have followed every step in the [contributing
guide](https://github.com/triggerdotdev/trigger.dev/blob/main/CONTRIBUTING.md)
- [ ] The PR title follows the convention.
- [ ] I ran and tested the code works
---
## Testing
- Integration test (`streamBatchItems.test.ts`) validates correctness
and performance at concurrency 1, 10, and 50 for both large and small
payloads.
- End-to-end verified over real HTTP: 20-item/12MB batch ingests and
seals, idempotent retry returns `sealed: true`, oversized item seals at
correct index.
---
## Changelog
Streaming batch ingest now processes items with bounded concurrency
instead of one at a time, so batches of many large payloads ingest far
faster and no longer time out. Concurrency is configurable via
`STREAMING_BATCH_INGEST_CONCURRENCY` (default 10); set it to 1 for fully
sequential ingestion.
---
## Screenshots
_[Screenshots]_
💯
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
5d6ea33166 |
refactor: share the public-token JWT scope decoder; make @trigger.dev/plugins internal (#3919)
## What `buildJwtAbility` — the decoder for public-token scope strings (`read:tags:…`, `read:runs:run_abc`, `admin`, …) — now lives in `@trigger.dev/plugins` as the single source of truth. `@trigger.dev/rbac` re-exports it, so the built-in fallback and any auth plugin interpret a token identically. Scope strings are split on only the first **two** colons (`action:type:id`), so a resource id that itself contains colons — e.g. a tag like `user:123` — is matched in full rather than truncated to its first segment. (The fallback already did this; this makes it the one shared implementation.) `@trigger.dev/plugins` is now **private (unpublished)** and gains a `@triggerdotdev/source` export condition, so consumers bundle it from source per-commit like `@trigger.dev/core` instead of resolving a published version — no cross-version coordination. ## Why Two hand-maintained copies of the scope grammar drift, and the difference silently changes what a token grants. One shared decoder removes that class of bug. ## Notes - No changeset: `@trigger.dev/plugins` is now private and `@trigger.dev/rbac` is internal — neither is published. - Unit coverage for the colon-id path lives in `internal-packages/rbac/src/ability.test.ts` (now exercising the shared function). |
||
|
|
78b7136bf7 |
chore: vouch saasjesus as a contributor (#3917)
Vouches `saasjesus` as a contributor (vouch request #3915) so their PRs clear the vouch check instead of being auto-closed. |
||
|
|
de8231cb9d |
chore: bump shell-quote to 1.8.4 (#3913)
Refreshes the locked `shell-quote` to 1.8.4 (transitive, lockfile-only). <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/triggerdotdev/trigger.dev/pull/3913?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
954ee5c572 |
fix(webapp): deliver realtime changes with current content when the read replica lags (#3910)
## Summary When the realtime runs feed (the backend behind the `realtimeBackend` feature flag) hydrates a change from a Postgres read replica, the read can race the replica's apply of the very write that triggered it. The delivered row then carries the previous change's content, and an isolated final change (for example a last `metadata.set` before a run goes quiet) is not corrected until the roughly 20 second backstop poll. Measured against a replica with deliberate apply delay, every delivery trailed exactly one change behind and a final change stranded for the full backstop interval. ## Fix Publishers stamp each change record with the committed row's `updatedAt`, taken from writes they already perform, so the stamp costs no extra queries. The router delays its wake hydrate until the replica's measured lag has passed, anchored to that timestamp: a record that has already spent longer than the lag in transit is hydrated immediately, so only the racing leading edge ever waits. After hydrating, a tripwire compares each row against its record's watermark. Still-stale rows are withheld and retried briefly, and each detection feeds the lag estimate. If retries run out, the rows are delivered anyway (liveness over freshness) and follow-up re-hydrates emit the fresh version through the normal working-set diff once the replica catches up, with the backstop as the terminal net. Replica lag is sampled reader-side only, and only while feeds are active. Aurora reports live lag via `aurora_replica_status()`; vanilla Postgres can only report "caught up or not" (mid-apply lag is not honestly measurable from a replica), so tripwire observations floor the estimate there. Deployments without a replica resolve to zero lag and skip the gate entirely. Tunables live under `REALTIME_BACKEND_NATIVE_REPLICA_LAG_*`, and `realtime_native.stale_hydrates` plus `realtime_native.replica_lag_estimate_ms` make replica health observable. Two adjacent fixes: a metadata update that writes nothing no longer publishes a change record, and buffered parent and root metadata operations now publish when the flusher writes them, so those changes wake live feeds instead of waiting for the backstop. For local testing, `docker-compose` gains an opt-in `database-replica` service (compose profile `replica`) with a configurable `recovery_min_apply_delay`, which reproduces replica-lag behavior deterministically. With the gate disabled this rig reproduces the one-change-behind delivery exactly; with it enabled, deliveries arrive with current content at roughly the true replica lag, across write rates faster and slower than the lag itself. |
||
|
|
8dc77c0ccd |
fix(webapp): only load env var values for displayed environments (#3903)
## Summary The environment variables page loaded every variable value in the project, unfiltered by environment. Archiving a preview branch does not delete its environment variable value rows, so projects that churn preview branches accumulate values forever, and every page view loaded all of them. On large projects this made the page loader take many seconds and stalled the server while deserializing the oversized result. ## Fix The presenter now loads the displayed environments first and filters the `values` relation to those environment IDs. That matches the display semantics exactly (per-user dev environments and active branch environments included), and the lookup is covered by the existing unique index on `(variableId, environmentId)`. Values in archived branch environments are no longer fetched at all. Covered by a new testcontainers test asserting that values from active environments (including branch environments) are returned while archived branch environments are excluded. |
||
|
|
cc9eabd14d |
test(webapp): use relative fixture dates in runs cursor pagination tests (#3912)
## Summary `test/runsRepositoryCursor.test.ts` pinned its fixture runs to `createdAt = 2026-06-04T16:55:07Z`. `listRuns` applies the default 7 day window when no time filter is given, so the fixtures aged out of the window at 16:55 UTC on 2026-06-11 and all five tests started failing for every branch, regardless of what the branch changed. The tests were green on their own CI two days earlier because the fixtures were only five days old at the time. This switches the fixture base to a relative timestamp (one hour ago), so the fixtures stay inside the default window permanently. Verified the suite goes 5/5 green with this change on the same environment where the pinned dates fail 5/5. |
||
|
|
187c0476c3 |
perf(webapp): shrink run trace loader payload and add trace span cap controls (#3906)
## Summary The run trace page loader serialized every span's raw OTel events (with full properties) into the response, even though the tree UI only renders the derived `timelineEvents` and the span detail panel refetches what it needs. On event-heavy traces that inflated both the loader payload and the server-side heap copies built per request. This PR keeps raw span events server-side and pairs that with a few related trace-view improvements: - A new optional `TRACE_VIEW_EMERGENCY_SPAN_CAP` env var (unset by default) clamps the trace summary and detailed trace summary span limits on both event store paths, including the public run trace endpoint, so operators can bound trace query sizes in one place without retuning the per-store limits. - The TreeView virtualizer resolved every rendered row with a linear scan over the whole tree (and `getNodeProps` did the same via `findIndex`); rows now resolve through memoized id lookup maps, which matters once traces reach tens of thousands of spans. - The run stream SSE lookup now applies the same organization membership scoping as the rest of the run page presenters, for consistency. Behavior is unchanged by default: the trace tree renders from the same `timelineEvents` it always has, and the new cap only takes effect when set. |
||
|
|
2b6d2492fe |
fix(sdk,core): head-start handover correctness and continuation boot latency (#3907)
## Summary Three related fixes for `chat.headStart` and continuation boots, found while investigating customer reports. **1. `chat.headStart` now works with `hydrateMessages`.** The turn-0 handover splice only ran on the default accumulation path, so agents registering `hydrateMessages` silently lost the warm route's step-1 response: pure-text turns fired `onTurnComplete` with no assistant message (and an empty durable write), tool-call turns re-ran step 1 from scratch under a fresh `messageId`, and the head-start user message never reached the hydrate hook at all. The first-turn history now reaches `hydrateMessages` as `incomingMessages`, and the splice runs after both accumulation branches, deduplicated by the handover `messageId`. **2. Reasoning parts survive the handover.** The synthesized partial only mapped text and tool-call parts, so an extended-thinking model's step-1 reasoning streamed to the browser but never reached durable history. Reasoning parts now map through with provider metadata, so Anthropic thinking signatures survive a UIMessage round trip on hydrate replays. **3. Continuation boots no longer stall for ~10 seconds.** The `.in` resume cursor was found by draining an SSE subscription that only closes after its 5 second inactivity window, and the scan ran twice per boot. It is now a non-blocking records read of the latest turn-complete header, runs at most once per boot, the boot reads run concurrently, and chat snapshots carry the cursor so subsequent boots skip the scan entirely. Measured locally on a cancel-then-continue repro: pre-turn continuation latency dropped from ~11s to ~0.5s. Every fix was verified red-green: new unit tests reproduced each failure before the fix, and end-to-end smoke tests against a live local stack covered both handover legs, reasoning persistence with extended thinking (including a follow-up turn that round-trips the persisted signed reasoning back to the provider), and the boot timing comparison. ## Rollout SDK-only; no server change required. A new SDK against a server that does not serialize record headers degrades to the existing no-cursor fallback. Old SDKs ignore the new snapshot field, and new SDKs fall back to the records scan on snapshots written before it existed. |
||
|
|
93b4715967 |
feat(webapp): hipaa baa add-on on paid pricing tiers (#3904)
## Summary HIPAA BAA is offered as a paid add-on on every paid plan. Each paid tier on the in-app pricing card now has a "HIPAA BAA add-on" row with a "Request a BAA" link that opens the existing contact dialog pre-filled with a new `hipaa` inquiry type, prompting the user for their company name and a brief description of the PHI workload. The contact form's `feedbackTypes` are restructured to match the marketing /contact form: every inquiry type carries a Plain label ID and a "Contact form: ..." thread title, so threads land in Plain identically whether they come from the dashboard or the marketing site. The included-compute line on each tier also picks up the credits wording from the marketing pricing page, and the Enterprise tier lifts its title above the features row. |
||
|
|
d0b2d79b3b |
fix(supervisor): cancel pending delayed snapshots when the run completes or disconnects (#3894)
⚒️ Publish Worker (v4) / build (supervisor) (push) Has been cancelled
The compute suspend flow delays snapshots by `snapshotDelayMs` (~30s) so short-lived waitpoints skip the snapshot entirely, with the intent that a run continuing before the delay expires cancels the pending snapshot. But the only `cancel()` call site was the `/continue` action, which runners only invoke when restoring from an already-taken snapshot — so pending snapshots were never cancelled (zero `snapshot.canceled` events ever emitted in prod). When a run resumed and completed inside the window, the stale snapshot fired ~30s later anyway, pausing the VM 6–13s mid warm-start long-poll; the frozen guest couldn't fire its abort timer or send a FIN, causing stalls and run-engine driven retries. ### Change - Cancel the pending snapshot on `attempt.complete` — after the platform accepts the completion, before the HTTP reply (so it can't reorder with the runner's next `/suspend`). - Cancel on `runDisconnected` (crash, exit, or run replaced on the socket). - Both cancels are guarded by a runnerId match (new `TimerWheel.peek()`): a stale duplicate runner for a reassigned run must not cancel the fresh runner's pending snapshot. A missing runnerId falls through to an unconditional cancel (the pre-existing `/continue` behavior is unchanged). Waitpoint suspensions keep the runner socket connected and the attempt incomplete, so neither hook touches a snapshot that is still wanted. Known limitation (fail-safe direction): `socket.data.runnerId` is frozen at the websocket handshake, so after a same-supervisor restore the disconnect-path guard refuses the cancel. The `attempt.complete` path uses the runner's current header id and is unaffected.re2-prod-supervisor-tri-10293 re2-test-supervisor-tri-10293 |
||
|
|
2397ca2999 |
fix(supervisor): retry transient instance create failures in compute workload manager (#3902)
`ComputeWorkloadManager.create` swallows gateway errors currently, so a cold start that fails placement (e.g. a netns slot with a busy tap, a full node disk) silently abandons the dequeued run until the run engine's `PENDING_EXECUTING` heartbeat timeout redrives it via stall detection. ### Changes - Retry `instances.create` with short backoff (default 3 attempts, 250ms backoff), recording `createAttempts` in the wide event. - **Only statuses where the create definitely did not commit are retried**: 500 (agent/fcrun create failed) and 503 (no placement). 502/504 are excluded — the gateway emits those when it fails to reach the node or read its response, which can happen *after* the agent committed the create; the gateway only records the instance name on a clean 201, so a same-name retry would miss the collision check and could double-create the VM on another node. Network-level fetch failures are retried (if the gateway processed the create, its name index is populated and the retry 409s harmlessly). Timeouts are not retried. - **Retry attempts after a 5xx use a deterministic `-rN` name suffix**: a failed create can leave its name registered until async cleanup runs. Attempt 1 keeps the unsuffixed name. |
||
|
|
7b4443a437 |
test(webapp): stop streamBatchItems container tests timing out on cold start (#3900)
Fixes an intermittent `Test timed out in 30000ms` in the `streamBatchItems` suite. Not a logic hang — the 30s budget covers container setup, and each case boots its own per-test Redis container + a full `RunEngine`, so under CI Docker contention a cold boot can cross 30s (which is why the failure moved between tests). - New `containerTestWithIsolatedRedisNoClickhouse` fixture (Postgres clone + per-test Redis, no ClickHouse) — this suite never uses ClickHouse, but the old fixture's auto `resetClickhouse` forced a ClickHouse boot + migration onto the cold-start test. - Raised `testTimeout` 30s → 120s, matching the run-engine convention for this footprint. |
||
|
|
1c7e64acde | feat(supervisor): stamp org identity label on compute microVMs (#3899) | ||
|
|
f5f29ceb26 |
fix(sdk,core): chat.agent delivery, idempotency, and recovery fixes (#3891)
## Summary A batch of reliability fixes for `chat.agent`: - A user message sent while the agent is streaming is no longer delivered twice (which could run a duplicate turn). - Input appends carry an idempotency key (`X-Part-Id`) so a retried send can't duplicate a message. - `onTurnComplete` now fires on errored turns with the thrown error attached, and the failed turn's user message is persisted so it isn't lost on the next run. - Stopping a generation clears the streaming state, so a page reload doesn't replay the stopped turn. - Custom agents and manual `chat.writeTurnComplete` callers trim the output stream, sending a custom action no longer leaves a second stream reader running, a long-lived `watch` subscription no longer grows its dedupe set without bound, promoting a queued message to steering no longer risks a double-send, and runs keep the full set of dashboard tags. The `X-Part-Id` header is accepted by current servers (they just don't dedupe on it yet), so this is safe to ship ahead of the matching server change. |
||
|
|
b82d100483 |
fix(webapp): harden the realtime session routes (#3890)
## Summary Reliability and authorization fixes for realtime chat sessions: - Session-stream waitpoint delivery is scoped to the environment, so two environments using the same session `externalId` can no longer complete each other's waitpoints. - The session snapshot-url routes now enforce per-session authorization, and appending to a session's `out` channel requires secret-key auth, so a session-scoped token can't read another session's snapshot or forge assistant output. - Appends that carry an `X-Part-Id` header are deduplicated on retry, so a retried send can't duplicate a message. - Session creation rejects expired sessions (instead of triggering a run that can never receive input), `externalId` is immutable after creation, and the sessions list endpoint returns friendly `run_*` ids to match the single-session routes. ## Rollout The waitpoint cache key gains an environment prefix. To keep waitpoints registered by the previous deploy working across the boundary, the drain reads both the new and the previous key for this release; the legacy read can be removed a release later once no pre-deploy waitpoints remain. |
||
|
|
f9d57d3bd5 |
feat(webapp): add a new backend for the realtime runs feed (#3864)
## Summary Adds a second backend for the realtime runs feed (`useRealtimeRun`, `subscribeToRunsWithTag`, `subscribeToBatch`), built to stay healthy when a single busy environment has many subscribers watching many runs at once. It is gated behind a feature flag with the existing backend as the default, so nothing changes for users until it is enabled per environment. ## Design A run change is published once, as a small self-describing record, to a single per-environment channel. Every feed is then a predicate over that one stream rather than owning a channel: - A per-instance router indexes the currently-held feeds by run, tag, and batch. When a run changes it hydrates the affected rows once and serializes them once, then fans the result to every matching feed. One hot shared tag watched by many subscribers costs a single database query and serialize, not one per subscriber. - Feeds that don't match a change are never woken, wake delivery per environment is coalesced on a leading edge (250ms default) so a burst of changes costs one wake, and cold reads coalesce onto a single short-TTL-cached resolve. - An admission gate bounds how many cold ClickHouse resolves run concurrently, so a mass reconnect across many distinct filters queues instead of stampeding the database. - Changes that land while a client is between long-polls are delivered on its next poll instead of waiting for the periodic backstop: each environment buffers its recent change records, subscriptions linger briefly after the last feed closes, and a newly-armed poll replays exactly the connection's gap. - The per-connection replay cursors behind that are shared across instances via Redis (a single timestamp each), so a poll landing on a different instance behind the load balancer still reads the connection's true gap instead of falling back to a cold resolve. Cursor reads have a bounded deadline and degrade to the cold-read path on any Redis trouble. - Tag subscriptions with multiple tags match runs carrying all of the tags, mirroring the existing backend's filter semantics, and live long-polls hold for about 20 seconds to match its cadence. - The per-environment channel supports Redis Cluster sharded pub/sub, so the wake path scales horizontally across shards by environment. - The backend reports its health through OpenTelemetry metrics (delivery lag, poll resolution paths, backstop outcomes, replay and cursor-store activity), with a provisioned Grafana dashboard for local development. Everything is behind the feature flag and tunable via env vars; the existing backend remains the default. |
||
|
|
6afc9bfa4c |
fix(run-engine): retry getSnapshotsSince on the replica then primary when the read replica lags (#3889)
## Summary
When `RUN_ENGINE_READ_REPLICA_SNAPSHOTS_SINCE_ENABLED` is on,
`RunEngine.getSnapshotsSince` reads from the read replica. During write
spikes the replica can briefly lag, so the snapshot id a runner just
learned from the writer isn't visible there yet: the lookup threw, the
worker route returned a 500, and the runner waited for its next poll —
turning sub-second snapshot notifications into poll-interval latency
exactly when things are busiest. This PR makes the flag safe to enable:
a replica miss of the since snapshot gets one jittered retry on the
replica (most lag windows are shorter than the ~50–200ms wait, so the
writer is never touched), then falls back to the primary, observed via a
new `run_engine.snapshots_since.replica_miss` counter with an `outcome`
attribute (`replica_retry` vs `primary`). Only genuine misses — absent
on the primary too — remain errors.
## Design
- `getExecutionSnapshotsSince` now throws a typed
`ExecutionSnapshotNotFoundError` so the engine can distinguish the
expected lag miss from real failures. The message string is unchanged
and the error never leaves the engine.
- The recovery path only engages when the flag is on, a distinct replica
client is configured, and no transaction client was passed. With the
flag off, the path is behaviorally identical to before.
- Retry delay bounds are configurable
(`RUN_ENGINE_SNAPSHOTS_SINCE_REPLICA_RETRY_MIN_MS`/`MAX_MS`, default
50/200; `MAX_MS=0` skips the replica retry and goes straight to the
primary).
- The warn log fires only when the primary serves the read (the writer
spill is the operationally interesting event); replica-retry recoveries
are counted but quiet. A permanently-missing snapshot id stays an
error-level failure with a `failedDuring` field, so lag metrics aren't
polluted by bogus ids.
- Stale-tail lag (replica has the since snapshot but not newer rows)
deliberately still returns the replica's view; the next poll catches up.
- The since-snapshot anchor lookup is now scoped to the polled run
(`where: { id, runId }`), so a snapshot id from a different run raises
not-found instead of silently anchoring a too-wide window of the run's
snapshots.
## Test plan
All vitest + testcontainers, no mocks. A new `schemaOnlyPrisma` fixture
(migrated-but-empty clone database) simulates a replica that hasn't
caught up, and a real in-memory OTel meter pins the counter semantics
per outcome.
- [x] Replica catches up during the jittered retry window → served by
the replica, `outcome=replica_retry` = 1, primary never consulted
- [x] Replica permanently missing the since snapshot → served by the
primary, `outcome=primary` = 1
- [x] Snapshot missing on both replica and primary → null, counter = 0
- [x] Replica has the since snapshot but lags by one → the replica's
view is served, no fallback (verified discriminating power: the test
fails if reads secretly hit the primary)
- [x] Flag off with a replica configured → primary serves the read
- [x] Transaction client provided → bypasses the replica entirely
- [x] Since snapshot belonging to a different run → null
- [x] Existing getSnapshotsSince + waitpoints suites green; run-engine,
testcontainers, and webapp typechecks pass
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
081b6bac17 |
feat(supervisor): publish client-side dequeue API latency as a Prometheus histogram (#3887)
⚒️ Publish Worker (v4) / build (supervisor) (push) Has been cancelled
The supervisor's dequeue round-trip time (`POST /engine/v1/worker-actions/dequeue`) was measured but only flowed into wide events and OTel span attributes — there was no Prometheus series, so latency percentiles and error rates weren't queryable. This adds `queue_consumer_pool_dequeue_duration_seconds` (histogram, label `outcome=success|empty|error`) to the existing consumer-pool metrics, scraped automatically by the existing ServiceMonitors on queue-raider/schedule-raider/supervisor. - Records every dequeue call, including failed ones, which previously emitted no timing at all - The pool's shared `ConsumerPoolMetrics` instance is injected into each consumer (mirrors the `BackpressureMetrics` → `BackpressureMonitor` wiring) - Buckets extend to 30s because `wrapZodFetch` retries internally (5 attempts, ≥7.5s backoff before a retryable error surfaces) - Existing `dequeueResponseMs` wide-event/span behavior unchangedre2-prod-client-deqeueue-metrics |
||
|
|
459dce2a97 |
ci: make the main-image dispatch repo and ref configurable (#3883)
The `dispatch-main-image` job was hard-gated to `triggerdotdev/trigger.dev` on the `main` ref. This makes it configurable via repository variables, all defaulting to the current values: - `MAIN_IMAGE_DISPATCH_REPO` — the repo allowed to dispatch (default `triggerdotdev/trigger.dev`). - `MAIN_IMAGE_DISPATCH_REF_PREFIX` — the ref-name prefix that dispatches, matched with `startsWith(github.ref_name, …)` (default `main`). - `MAIN_IMAGE_DISPATCH_TARGET` — the `repository_dispatch` target (default `triggerdotdev/cloud`). The job is additionally gated on `github.event_name == 'push'`. This is necessary, not just defensive: the gate now keys off `github.ref_name` rather than the computed image tag, and `ref_name` is still `main` when `release.yml` invokes this workflow via `workflow_call` during a release — so without the event guard the job would fire during every release and fail on the absent `CROSS_REPO_PAT`. A version-equality check can't replace it because `build-*` tags strip the prefix to the version output. Behaviour note: the intended dispatch paths — push to `main`, and push of a `<prefix>*` tag in a downstream repo — are `push` events and are unchanged. The one case that no longer dispatches is a manual `workflow_dispatch` run of `publish.yml` on `main` (it previously did, via the old `version == 'main'` check). That path is indistinguishable from a manual release by event name, so `push`-only is the clean discriminator. Dispatching still requires `CROSS_REPO_PAT`, so setting the variables alone doesn't enable anything. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
87448ccaf2 |
feat(webapp,core): add an endpoint to list a project's environments (#3880)
## Summary
Adds `GET /api/v1/projects/{projectRef}/environments` (personal access
token auth), which lists the base environments a user can access for a
project — their own dev environment plus the project's staging, preview,
and production environments.
## Details
- Built on the PAT route builder, so it inherits org-membership auth and
the per-resource ability check.
- `dev` is scoped to the token owner; archived environments are
excluded.
- Returns the branchable **parent** preview environment — preview branch
children are not included. A consumer targets the parent; branch-level
overrides are handled separately.
- Sorted to match the dashboard's environment switcher (dev → staging →
preview → prod), and never returns API keys.
Example response:
```json
[
{ "id": "...", "slug": "dev", "type": "DEVELOPMENT", "isBranchableEnvironment": false, "branchName": null, "paused": false },
{ "id": "...", "slug": "stg", "type": "STAGING", "isBranchableEnvironment": false, "branchName": null, "paused": false },
{ "id": "...", "slug": "preview", "type": "PREVIEW", "isBranchableEnvironment": true, "branchName": null, "paused": false },
{ "id": "...", "slug": "prod", "type": "PRODUCTION", "isBranchableEnvironment": false, "branchName": null, "paused": false }
]
```
|
||
|
|
b28c6d0b90 |
fix(webapp): sanitize streamed agent URLs before rendering in the agent view (#3882)
## Summary The dashboard's Agent view rendered `source-url` and `file` message parts by putting their `url` straight into an `href`/`src`. Those URLs come from streamed agent and tool data, so a tool that emitted something like `javascript:alert(1)` produced a clickable XSS payload in the dashboard. ## Fix A `toSafeUrl` helper now gates every URL before it reaches an `href`/`src`: it allows only `http:`/`https:`/`blob:` (and `data:image/...` for inline images) and returns `null` for anything else. Unsafe values render as plain text instead of a link or image, so a hostile or malformed URL degrades gracefully rather than becoming clickable. Safe URLs render exactly as before. Covered by a unit test over the allow/deny list. |
||
|
|
bc01f6ea3a |
fix(webapp): stop writer DB connectivity errors leaking to trigger() API clients (#3874)
## Summary
During `trigger()` worker-queue resolution, `getWorkerQueue` wrapped any
error from `getDefaultWorkerGroupForProject` into a client-facing
`ServiceValidationError` (HTTP 422) carrying `error.message`. That
method runs `project.findFirst` on the **writer**; when the writer is
unreachable Prisma throws a connection error (P1001) whose message
includes the database host, and that raw message was returned to the API
client and surfaced in the run view via the SDK's `TriggerApiError`.
It also mis-classifies a transient outage: a 422 is not retried by the
SDK, so triggers failed permanently instead of riding out a brief writer
blip.
## Design
This is the only place on the trigger path that folds a *caught* error's
message into a client-facing error — every other DB failure on the path
propagates to the route's generic 500 handler (scrubbed, and retried by
the SDK). So the fix is local:
- Add `isInfrastructureError()` — true for Prisma connection-level
failures (the DB-unreachable family: P1001/P1002/P1008/P1017, plus the
init/panic/unknown client error classes), false for query/validation
errors (e.g. P2002).
- At the wrap site, rethrow infrastructure errors so they reach the
generic 500 handler (no raw message, and retryable). Genuine domain
failures (e.g. "Project not found.") still become a 422.
Only P1001 ("can't reach database server") has been observed in
practice; the rest of the connection family is included as same-class
forward-proofing.
## Test plan
- [x] Unit: `isInfrastructureError` classifies a P1001 (incl. the Prisma
6.x `PrismaClientKnownRequestError` shape) and init errors as
infrastructure; P2002 and a plain `Error` as not
- [x] `getWorkerQueue` rethrows a P1001 unchanged instead of wrapping it
in a `ServiceValidationError`; still wraps a domain failure as a
`ServiceValidationError` — RED on current code, GREEN after
- [ ] (optional) toxiproxy e2e: trigger with the writer cut → HTTP 500
generic body, no DB host in the response
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
3bc88c453e |
perf(webapp): memoize react-router per-request route matching via pnpm patch (#3877)
## Summary Under high request load the webapp spends most of its CPU inside react-router's `matchRoutes`, not in application code. `@remix-run/router@1.23.2` (the React Router v6 / Remix 2 core) re-flattens, re-ranks, and recompiles the entire route table on every request, and with the webapp's ~436 routes that cost dominates once request rates climb. There is no `NODE_ENV` gate, so production pays it too. This adds a pnpm patch that memoizes the parts that depend only on the static route manifest: it caches the flattened/ranked branches per route tree, hoists the loop-invariant `decodePath` out of the match loop, and caches compiled path regexes. ## Benchmark CPU profile over the same load (100 concurrent tag feeds, ~425 req/s), `NODE_ENV=production`, before vs after the patch: | Metric | Before | After | | --- | --- | --- | | Active CPU (self-time over the window) | 28.3s | 18.5s (-34%) | | Route-matching self-time | 19.2s | 7.5s (-61%) | | Event-loop lag p99 | 322ms | 113ms (-65%) | | Idle headroom | 26% | 52% | Application/realtime code was ~0% of CPU in both profiles; the bottleneck was entirely generic per-request route matching. ## Why a patch instead of an upgrade The inefficiency is acknowledged upstream ([remix-run/react-router#8653](https://github.com/remix-run/react-router/issues/8653)). A contributor PR doing exactly this ([remix-run/react-router#14866](https://github.com/remix-run/react-router/pull/14866)) was closed in favor of a narrower fix ([remix-run/react-router#14967](https://github.com/remix-run/react-router/pull/14967), branch caching only, shipped in React Router v7), with the maintainer suggesting patch-package as the interim until the Remix 3 route-pattern rewrite (see [remix-run/remix#4786](https://github.com/remix-run/remix/discussions/4786)). We are on the v6-era core and cannot pick up even the partial fix without a framework migration, so this patch is the sanctioned stopgap, and it also includes the compiled-regex cache the merged PR left out. [`patches/README.md`](https://github.com/triggerdotdev/trigger.dev/blob/perf/react-router-route-matching/patches/README.md) documents the full rationale, the safety argument (deterministic, internal-only, bounded caches), and when to remove the patch. |
||
|
|
d97335902b |
docs: add a database connections guide for tasks (#3881)
## Summary A new guide for connecting a database to your tasks: where to create the client, how to size the connection pool against your provider's limit, when to reach for a pooler, and how to release connections at waits so you don't hit "too many connections" or crash on resume. It covers node-postgres, Prisma, Drizzle, and MongoDB, with researched direct and pooled connection limits for the common Postgres providers (Supabase, Neon, RDS, PlanetScale) and MongoDB Atlas. The page lives under Documentation, Troubleshooting, and is linked from the chat agent docs (overview, lifecycle hooks, chat.local, and the database persistence pattern). |