570d648dc38f84af628ba12494e8dc9ea23cdabb
7237 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
570d648dc3 | feat(supervisor): wide events on workload server + sockets | ||
|
|
4361370f73 | feat(supervisor): wide events on dequeue + warm-start trace | ||
|
|
5a3a442db6 | feat(supervisor): wide-event module + kill switch | ||
|
|
436b7a9ea1 |
fix(webapp): fold S2 token scope into access-token cache key (#3668)
## Summary
The S2 access-token cache key was `${basin}:${streamPrefix}` — purely
server-derived but blind to the **scope/ops list** hardcoded one method
away. When the ops list changes in code (e.g. #3644 added `trim` so
`chat.agent`'s per-turn trim chain can issue `AppendRecord.trim()`),
pre-deploy tokens still in cache get returned to SDK callers for up to
the token's TTL (24h default), surfacing as `Operation not permitted`
403s on any op outside the old scope.
## Fix
Lift the ops list to a module constant and fold its sorted-join
fingerprint into the cache key:
```ts
const S2_TOKEN_OPS = ["append", "create-stream", "trim"] as const;
const S2_TOKEN_OPS_FINGERPRINT = [...S2_TOKEN_OPS].sort().join(",");
// in getS2AccessToken
const cacheKey = `${this.basin}:${this.streamPrefix}:${S2_TOKEN_OPS_FINGERPRINT}`;
// in s2IssueAccessToken
scope: { /* ... */ ops: [...S2_TOKEN_OPS], /* ... */ }
```
The fingerprint is derived from the single source of truth, so any
future scope change auto-invalidates without anyone remembering to bump
a literal version. The Unkey L1 (in-memory LRU) and L2 (Redis) layers
share the same key derivation, so both reset together on the next deploy
with no manual cache busting.
## Test plan
- [ ] `pnpm run typecheck --filter webapp`
- [ ] Run a multi-turn `chat.agent` chat via `references/ai-chat` and
confirm no `chat.agent: trim failed; will retry next turn` warn span
fires across turn-completes.
|
||
|
|
2fbac48e0d |
feat(webapp): prompt to clear TRIGGER_VERSION on disabling Vercel atomic deployments (#3666)
- Ask user if they want to remove TRIGGER_VERSION when they disable atomic deployments, and explain what is the situation if they leave it as it is - Install TRIGGER_SECRET keys as sensitive values in Vercel <img width="1136" height="714" alt="image" src="https://github.com/user-attachments/assets/a7351da1-5b2a-44e5-acdd-d30c9359f3ed" /> <img width="1136" height="714" alt="image" src="https://github.com/user-attachments/assets/e773ede2-74cb-438e-811c-338f678d2f7d" /> <img width="1136" height="714" alt="image" src="https://github.com/user-attachments/assets/c7b235a8-e06d-48d3-ac28-c5c9aacc6069" /> |
||
|
|
e825409f0e |
ci(release): exit changeset pre mode before snapshot prerelease (#3665)
## Summary The prerelease (snapshot) path of the release workflow fails immediately whenever `main` carries an active `.changeset/pre.json` (i.e. during an in-progress RC cycle, like the current v4 RC): ``` 🦋 error Snapshot release is not allowed in pre mode 🦋 To resolve this exit the pre mode by running `changeset pre exit` ``` This blocks `chat-prerelease` snapshots from main even though the snapshots are unrelated to the RC cycle. Adds a conditional `changeset pre exit` step right before `Snapshot version` in the prerelease job. The job runs on a checkout with `persist-credentials: false`, so the `pre.json` deletion stays on the runner's working tree — main's persisted pre-mode state is untouched, and v4 RC publishes keep working normally. ## Test plan - [ ] Re-run the `🦋 Changesets Release` workflow with `type=prerelease`, `ref=main`, `prerelease_tag=chat-prerelease` and confirm it gets past the snapshot step and publishes. - [ ] Confirm `.changeset/pre.json` on `main` is unchanged after the run. |
||
|
|
2f261e5e69 |
fix(webapp): catch loader/action throws before Remix serializes them (#3664)
## Summary Companion to #3536, which patched routes that already had a leaking `catch (e) { return json({error: e.message}, 500) }`. That pattern can't reach routes which have no catch in the first place — when those throw, Remix's default error path serializes `error.message` into the response body, and the SDK then wraps the leaked string as `TriggerApiError`. Across 28 raw api.v1 loaders/actions plus one dashboard polling endpoint, each handler now: - Wraps its body in `try { ... } catch (error) { ... }`. - Re-throws `Response` instances so auth helpers' `throw json(...)` / `throw redirect(...)` pass through unchanged. - Logs non-Response errors via `logger.error` so server-side visibility is preserved. - Returns a generic body — `{"error": "Internal Server Error"}` 500 for raw API routes, or `{ changelogs: [] }` 200 for the polling widget (degrade silently across transient blips; the consumer hook already coped with empty payloads). For six routes where #3536 left an inner try/catch covering only a service call (`alertChannels`, `batches.results`, `deployments.finalize`, `deployments.background-workers`, `deployments.promote`, `projects.background-workers`): an outer try/catch is added so auth/parsing failures are also sanitized. Inner typed-error handling (`ServiceValidationError` → 422 with message, etc.) is preserved exactly. For two routes whose existing catch returned 400 + `error.message` (`api.v1.authorization-code`, `api.v1.orgs.\$orgParam.projects` action): the body is sanitized to a generic per-route string. **Status code stays 400** — clients that key on the 4xx/5xx distinction (and the SDK's no-retry-on-4xx behavior) are unaffected. ## Test plan - [x] \`pnpm run typecheck --filter webapp\` - [x] Per-route synthetic-throw probe: inject \`throw new Error("SYNTHETIC ...")\` at the top of each catch'd try, curl the route with a dummy bearer, confirm the response body is the generic shape and that the synthetic message lands server-side via \`logger.error\`. 29 routes verified. - [x] Real-P1001 probe on the envvars loader: \`docker stop database\` mid-flight, confirm response is generic 500 (not the leaked Prisma message). - [x] Sampled legitimate 4xx/2xx paths across each pattern variant (naked-wrap, partial-expanded, 400-preserved) to confirm the wraps don't interfere with normal control flow. |
||
|
|
5dacab0c72 |
fix: validate email format on magic link login (#3660)
Reject non-email strings at the magic link form instead of accepting any string and proceeding through rate-limit / authenticator steps. |
||
|
|
02d61afc1c |
fix(webapp): sanitize OTel attributes on ClickHouse JSON parse rejection (#3659)
Before fix: <img width="1264" height="987" alt="image" src="https://github.com/user-attachments/assets/24b8b85c-b89f-4109-9004-8d6af61d2849" /> After fix: <img width="1264" height="987" alt="image" src="https://github.com/user-attachments/assets/89bbc587-c50a-45ab-b203-dbe91028e918" /> |
||
|
|
427d9e078a |
feat(sdk): functional baseURL and fetch override on chat transports (#3655)
## Summary
`TriggerChatTransport`, `AgentChat`, and `chat.createStartSessionAction`
now accept a string-or-function `baseURL` so callers can route per
endpoint — e.g. `.in/append` through a trusted edge proxy while keeping
`.out` SSE direct. The same surfaces add a `fetch` override for header
injection, custom retries, or proxy rewrites that go beyond URL routing.
SSE GETs are covered too via a new `fetchClient` option on
`SSEStreamSubscription`.
```ts
// TriggerChatTransport / AgentChat — endpoints: "in" | "out"
baseURL: ({ endpoint }) =>
endpoint === "out" ? DIRECT : PROXY,
fetch: (url, init, ctx) => {
init.headers = new Headers(init.headers);
init.headers.set("traceparent", currentTraceparent());
return globalThis.fetch(url, init);
},
// chat.createStartSessionAction — endpoints: "sessions" | "auth"
chat.createStartSessionAction("my-agent", {
baseURL: ({ endpoint }) => (endpoint === "sessions" ? PROXY : DIRECT),
});
```
`streamBaseURL` on `TriggerChatTransport` is kept as a backwards-compat
alias and continues to win for the `"out"` endpoint when set.
Plain-string `baseURL` still applies to every endpoint, matching prior
behavior.
|
||
|
|
8b98e21b4b |
fix(sdk,core): cache realtime-stream credentials per slot with refresh on writer failure (#3658)
## Summary Hot-loop writers — `streams.writer` / `streams.pipe` on the run-scoped side, `chat.response.write` / `chat.stream.*` on the session side — were issuing a fresh `PUT` to mint S2 credentials for every chunk. On run streams, each PUT also pushed the streamId onto `TaskRun.realtimeStreams`, so a chat-agent turn writing N chunks produced N PUTs and N duplicate array pushes against the same row. The SDK now caches the initialize response per cache slot: `(runId, key)` for run streams, the session id for session streams. First call PUTs as before; subsequent calls reuse the cached promise. Hot-loop writers do one PUT per slot for the lifetime of the cache. S2 access tokens have a 1-day TTL. If a writer's `wait()` rejects (auth error, expired token, network blip), the cache evicts the matching slot so the next call re-PUTs and mints fresh credentials, identity-checked so a concurrent caller's fresh promise isn't accidentally cleared. ## chat.agent guardrail `streams.pipe / writer / append / read` called inside a `chat.agent` run now logs a one-time warning pointing at `chat.response.write` / `chat.stream.*` — `streams.*` is run-scoped and isn't visible on the chat session. The ai-chat docs are updated to drop the old guidance toward run-scoped streams. |
||
|
|
906d5fafb6 |
feat(mollifier): trigger burst smoothing — Phase 1 (monitoring) (#3614)
## Summary - Introduce the Mollifier: a Redis-backed buffer for `trigger()` API calls during traffic spikes, with a per-env trip evaluator and a drainer ack-loop. - Phase 1 is dual-write monitoring — every mollified trigger is buffered to Redis AND continues to `engine.trigger`. No customer-facing behaviour change. - Telemetry events: `mollifier.would_mollify`, `mollifier.buffered`, `mollifier.drained`, plus the `mollifier.decisions` counter. - Gated behind a feature flag (default off). ## Test plan - [x] `pnpm run test --filter @trigger.dev/redis-worker` - [x] `pnpm run test --filter webapp -- mollifier` - [x] Manual: with flag off, no behaviour change vs main - [x] Manual: with flag on + threshold lowered, observe `mollifier.buffered` + `mollifier.drained` log pairs with matching `runId` --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
6c9f1f197e |
chore: parameterize docker host ports and wire s2-lite by default (#3642)
## Summary
Two papercuts new contributors hit running this repo locally:
1. Fresh clones default to v1 (Redis-only) realtime streams, so Sessions
and `chat.agent` error with `"S2 configuration is missing"`, even though
the `s2` service is already in `docker/docker-compose.yml` and pre-seeds
a `trigger-local` basin. Wire `REALTIME_STREAMS_S2_*` to it in
`.env.example` so the new-contributor flow just works. (Also drop the s2
healthcheck: the image is distroless, so the `wget` check always reports
unhealthy.)
2. Two clones can't both run `pnpm run docker` because ports, project
name, and container names are all hardcoded. Parameterize every host
port as `${VAR:-default}`, drive the project name via
`COMPOSE_PROJECT_NAME` (with a top-level `name:` field as the default),
prefix container names with `${CONTAINER_PREFIX:-}`, and pass
`--env-file .env` so compose reads the same root `.env` the webapp does.
The "Running multiple instances side by side" block in `.env.example`
lists every overridable knob.
Also split the optional services (`electric-shard-1`, `ch-ui`,
`toxiproxy`, `nginx-h2`, `otel-collector`, `prometheus`, `grafana`) into
`docker-compose.extras.yml` behind a new `pnpm run docker:full` script.
The core stack keeps everything the webapp actually needs to boot:
postgres, redis, electric, minio, clickhouse + migrator, s2-lite.
Defaults match every previous hardcoded value, so existing setups keep
working without touching `.env`.
## Test plan
- [x] `pnpm run docker` on a clean clone brings up the core services on
the standard ports under the `triggerdotdev-docker` project name.
- [x] Setting `COMPOSE_PROJECT_NAME=triggerdotdev-docker-alt` + the
`*_HOST_PORT` overrides in `.env` brings up a second stack alongside the
default one with no port or container-name clashes.
- [x] Webapp boots cleanly against the default `.env.example` values;
`/healthcheck` returns 200, no S2 errors.
- [x] s2-lite basin `trigger-local` accepts an append + read via the
same REST endpoints the webapp uses.
- [x] `pnpm run docker:full` brings up the optional services alongside
the core ones in the same project.
|
||
|
|
82853debea |
feat(webapp,core,sdk,cli): bound session.out via per-turn trim (#3644)
## Summary Long-running chat agents were filling `session.out` forever — every `chat.agent` turn appended to the same S2 stream with no trim, and the Sessions dashboard re-streamed the entire history from `seq_num=0` on every page load. After this change the agent appends an S2 `trim` command record after each `trigger:turn-complete`, pointing back at the previous turn-complete's seq_num. `session.out` stays roughly one turn long at steady state, regardless of session age. `trigger:turn-complete` and `trigger:upgrade-required` move from `chunk.type`-shaped data records into header-form control records under a uniform `trigger-control` namespace. Built-in transports (`TriggerChatTransport`, `AgentChat`, the dashboard's `AgentView`) handle the new shape transparently. Custom transports need a one-line filter on the `trigger-control` header — see the rewritten "Records on session.out" section in the client-protocol docs. The Sessions detail page in the dashboard fetches the agent's per-turn S3 snapshot via a presigned URL and seeds the transcript view, then SSE-tails from the snapshot's `lastOutEventId`. Bandwidth and time-to-first-render scale with unread turns instead of session lifetime. Resume contract is now explicit: single-turn-boundary resume always works (the prior turn-complete is still on the stream), the S2 trim is eventually consistent over 10-60s, and multi-turn-away resume falls back to a snapshot reload. |
||
|
|
f88d4018cc |
fix(webapp): dedupe realtimeStreams array push on stream create (#3653)
## Summary
The PUT handler at `/realtime/v1/streams/:runId/:target/:streamId` ran
`taskRun.update({ realtimeStreams: { push: streamId } })` on every call,
even when the `streamId` was already present. SDK call patterns that
re-initialize the same stream key on every chunk produce a per-write row
UPDATE, duplicate entries pile up in the array, and the row-lock + TOAST
rewrite cost grows unbounded on long-running stream sessions.
## Fix
Mirror the sibling append handler: read the array first and only push
when the `streamId` isn't already present. Identical behavior for
first-time stream creation; repeat creates short-circuit to a single
indexed read. The dashboard's per-run stream listing keeps working
because the first create still records the entry.
## Test plan
- [ ] A fresh PUT for a new `(run, streamId)` adds the entry to the
array
- [ ] A repeat PUT for the same pair leaves the array unchanged
- [ ] 404 is returned when the run doesn't exist; 400 when the run is
completed
|
||
|
|
9623e88b05 |
fix(webapp): collapse Prisma P1001 errors into a single Sentry issue (#3632)
## Summary
- Adds a `beforeSend` rule in `apps/webapp/sentry.server.ts` that
collapses Prisma `P1001` ("Can't reach database server") errors into a
single Sentry issue regardless of which call site threw, by setting
`event.fingerprint = ["prisma-p1001-db-unreachable"]` and tagging
`db_unreachable:true`.
- Matches both `err.code === "P1001"` (Prisma's `KnownRequestError` when
a connection drops mid-query) and `err.errorCode === "P1001"`
(`InitializationError` when the client fails to connect at startup).
- Implemented as a small extensible `FINGERPRINT_RULES` table so further
fan-out errors can be added with one entry.
## Verification
End-to-end verified locally with `debug: true` on the SDK:
- Real Prisma `P1001` thrown from a loader (DB stopped mid-request) is
captured by Sentry's Remix auto-instrumentation
- `beforeSend` fires with `originalException.code === "P1001"`, rule
matches
- `event.fingerprint = ["prisma-p1001-db-unreachable"]` and
`tags.db_unreachable = "true"` applied
- Event lands in Sentry under the new fingerprint
## Test plan
- [ ] Deploy to staging; confirm P1001 events appear under a single
`prisma-p1001-db-unreachable` issue rather than fanning out
- [ ] Confirm `db_unreachable:true` tag is filterable in Sentry
- [ ] Verify non-P1001 errors are unaffected (event passes through
`beforeSend` untouched)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
55fa2d4967 |
fix(cli): TRIGGER_BUILD_SKIP_REWRITE_TIMESTAMP escape hatch for local self-hosted builds (#3618)
## Summary Local self-hosted deploys (`trigger deploy --local-build --push --builder orbstack` or any other buildx setup using the **docker** driver) fail at the push step with: ``` ERROR: failed to build: failed to solve: exporter option "rewrite-timestamp" conflicts with "unpack" ``` The docker driver auto-enables `unpack=true` when pushing, and that's incompatible with `rewrite-timestamp` (which the CLI sets for reproducible-build hashing). Adds a simple env-var opt-out so contributors can keep using their default builder. The flag is only read by the local-build code path; remote/cloud builds are unaffected. ```bash TRIGGER_BUILD_SKIP_REWRITE_TIMESTAMP=1 \ pnpm exec trigger deploy --profile default --local-build --push --builder orbstack ``` The trade-off: skipping `rewrite-timestamp` means layer timestamps reflect actual build time, so two identical builds produce different layer hashes. Fine for a local-dev registry; the only real consumer of timestamp-stability is registry-layer cache hit rates. ## Test plan - [x] Manual: ran `trigger deploy --profile default --local-build --push --builder orbstack` against the localhost webapp + a local Docker registry on port 5001 — first failed with the rewrite-timestamp/unpack error, then succeeded after setting `TRIGGER_BUILD_SKIP_REWRITE_TIMESTAMP=1`. - [x] Full chat.agent smoke sweep (15 tests, including suspend/resume, deepResearch subtask, AgentChat orchestrator) against the deployed image — all pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
627e059298 |
feat(trigger-sdk): add streamBaseURL to TriggerChatTransport (#3641)
`TriggerChatTransport` had a single `baseURL` option covering both the
`.in/append` POSTs and the long-lived `.out` SSE subscription. Customers
wanting to route the SSE through a proxy (e.g. a Cloudflare worker
capturing JA4 fingerprints for bot detection) had to send every append
through the proxy too, adding a hop to every user message.
New optional `streamBaseURL` overrides the SSE base URL only; appends
keep using `baseURL`. Falls back to `baseURL` when unset, so existing
transports are unchanged.
```ts
const transport = new TriggerChatTransport({
task: "ai-chat",
baseURL: "https://api.trigger.dev",
streamBaseURL: "https://chat-proxy.example.com",
accessToken,
startSession,
});
```
Verified with a new test in `chat.test.ts` that asserts `.in/append`
routes through `baseURL` and `.out` SSE routes through `streamBaseURL`.
All existing tests still pass.
|
||
|
|
a8280f125c |
ci: fix code path filter negation (#3637)
`dorny/paths-filter` defaults to OR semantics across the pattern array, so the leading `**` matched every file and the `!...` excludes were no-ops. The `code` filter has been returning `true` for every PR since #3615. Split into two filter steps: `code` moves into its own step with `predicate-quantifier: every` so excludes actually subtract. The two re-include workflow files become a separate `typecheck_self` filter that the `typecheck` job ORs into its `if:`. Side effect: workflow-file-only PRs that don't touch `pr_checks.yml` or `typecheck.yml` no longer trigger typecheck. Previously they did because the filter was broken-true. |
||
|
|
05d3ab1059 |
docs(clickhouse): require max+1 numbering and idempotent DDL (#3633)
## Summary Codify two rules for ClickHouse migration authors that came out of the 029/030 ordering incident on the TRI-9367 test cloud deploy: 1. **Number files to `max(existing) + 1`, never slot in below the latest.** Goose runs in strict mode in the cloud deploy pipeline and refuses to apply a missing version below the current version — slotting a file in below an already-applied number blocks the next deploy. 2. **DDL must be idempotent** (`ADD COLUMN IF NOT EXISTS`, `DROP COLUMN IF EXISTS`, `CREATE TABLE IF NOT EXISTS`, etc.) so a retry or out-of-order apply (`goose up --allow-missing` for local recovery, manual fixups) is a no-op rather than an error. ## Where the rules live - `internal-packages/clickhouse/CLAUDE.md` — full rules + example for migration authors (and AI agents writing migrations). - `.claude/REVIEW.md` — added a 🔴 finding under "What makes a 🔴 Important finding" so PR reviewers flag either fault as blocking. The existing migration files are left untouched; the idempotency requirement applies going forward. ## Test plan - [ ] Next ClickHouse migration PR uses `IF NOT EXISTS` / `IF EXISTS` forms - [ ] No new migration files numbered below an already-applied version on test/prod |
||
|
|
032b5a117a |
fix(clickhouse): renumber task_kind migration 029 → 031 (#3631)
## Summary Renumber `029_add_task_kind_to_task_runs_v2.sql` → `031_add_task_kind_to_task_runs_v2.sql` to fix a deploy-blocking out-of-order migration, and make the DDL idempotent with `ADD COLUMN IF NOT EXISTS` / `DROP COLUMN IF EXISTS`. ## Root cause - Migration `030_create_sessions_v1.sql` landed on main on 2026-04-28 (PR #3417) and was applied to test cloud ClickHouse on a subsequent deploy. Current goose version on test ClickHouse: **30**. - Migration `029_add_task_kind_to_task_runs_v2.sql` was authored later on 2026-05-10 as part of the Sessions primitive PR series (`be1a6cf8`). - The next test cloud deploy failed because goose strict-mode refused to apply a missing version *before* the current version: ``` goose run: error: found 1 missing migrations before current version 30: version 29: 029_add_task_kind_to_task_runs_v2.sql ``` ## Fix 1. **Rename to `031_*`** (next available number after 030). Goose now treats it as a new migration after 030 and applies it cleanly on test/prod where the column does not yet exist. 2. **Make the DDL idempotent** (`ADD COLUMN IF NOT EXISTS`). The original 029 may have been applied in environments that ran goose with `--allow-missing` (e.g. some local dev databases) — those would have the column already, and the rename causes goose to see 031 as new and re-attempt the ADD. Idempotent DDL keeps that path safe. The `Down` mirrors with `DROP COLUMN IF EXISTS`. ## Test plan - [ ] Test cloud deploy (after this lands) successfully runs the ClickHouse migration step - [ ] `task_kind` column shows up on `trigger_dev.task_runs_v2` post-migration - [ ] Local environments that had previously applied 029 do not error on the next `goose up` |
||
|
|
5788573b4f |
chore: enter prerelease mode (rc) to ship v4.5.0-rc.0 (#3630)
## Summary Adds `.changeset/pre.json` to put the repo into changesets pre mode with dist-tag `rc`. After this merges, the changesets bot regenerates the existing release PR as `chore: release v4.5.0-rc.0`. Merging that PR publishes the first release candidate of 4.5.0 to npm under `@rc`. The pre-mode plumbing landed in #3628. The release content (chat.agent + sessions + ai prompts + dashboard server-changes) landed in #3629. ## What ships when the bot PR merges Under dist-tag `rc`: - `@trigger.dev/{sdk,core,build,react-hooks,redis-worker,plugins,python,rsc,schema-to-json}@4.5.0-rc.0` - `trigger.dev@4.5.0-rc.0` Plus: - Docker image `ghcr.io/triggerdotdev/trigger.dev:v4.5.0-rc.0` (immutable tag only — `:v4-beta` is not touched) - Helm chart `oci://ghcr.io/triggerdotdev/charts/trigger.dev:4.5.0-rc.0` - GitHub release `v4.5.0-rc.0` marked as Pre-release (no Latest badge) What does NOT happen: - npm `latest` stays at 4.4.6 - No marketing-site changelog PR (gated on `is_prerelease != 'true'`) - Docker `:latest` not touched (we never push it anyway in this repo) ## Iteration For subsequent rc.N: add a regular changeset to main, bot regenerates the release PR as `v4.5.0-rc.N`. Merge to ship. ## Exiting pre mode When ready to ship stable: `pnpm exec changeset pre exit`, push, merge regenerated PR. That publishes `4.5.0` under `latest` and fires the marketing-site dispatch. |
||
|
|
eedde2793d |
chore: rewrite v4.5.0 release content around AI Agents (#3629)
## Summary
Refocuses the v4.5.0 changeset and server-changes content on the
public-facing AI features story, replacing the pre-release-internal diff
framing that had accumulated in `.changeset/` and `.server-changes/`.
Pairs with the RC support PR — the next bot regeneration will pick up
this content.
## What's in here
### Changeset rewrites
- **`chat-agent.md` rewritten as the headline AI Agents entry** —
written from the `docs/ai-chat/` surface (not from internal pre-release
diffs). Covers useChat integration, multi-turn durability via Sessions,
lifecycle hooks, stop generation, tool approvals (HITL), pending
messages + background injection, actions, typed state primitives,
`chat.toStreamTextOptions()`, multi-tab coordination, network
resilience, and the first-turn fast path (`chat.headStart`).
- **New `ai-prompts.md`** — announces the Prompts feature publicly for
the first time. Code-defined templates, deploy-versioning, dashboard
overrides, AI SDK telemetry integration, `chat.agent` integration via
`chat.prompt.set()` + `chat.toStreamTextOptions()`, full management SDK.
- **`sessions-primitive.md` expanded** — calls out
`tasks.triggerAndSubscribe()` and `sessions.list` as standalone
primitives (not just chat.agent infrastructure).
- **`chat-agent-on-boot-hook.md` trimmed** — drops "if you previously…"
pre-release migration framing.
- **Deletes 4 changesets** that described pre-release-internal
migrations or were circular ("groundwork for the upcoming chat.agent" —
chat.agent ships in the same release).
### Server-changes rewrites (`.server-changes/`)
Five new entries for the dashboard surface of the AI feature set:
- Agents list page
- Agent Playground
- Sessions dashboard
- Prompts dashboard (list with usage sparklines + detail with template /
Generations / Metrics / Versions tabs + override UI)
- Models registry (provider-grouped catalog with cross-tenant usage
metrics)
- AI generation span inspector on run traces
- Runs list Task source filter (Standard / Scheduled / Agent)
- Run-detail Agent view (segmented control)
Each entry is 1–2 sentences, no bullets, no implementation file paths —
fits as a single bullet in a future changelog.
Three older `.server-changes/` files were merged or split into the
cleaner taxonomy above and deleted.
## Out of scope
Non-AI-feature server-changes (admin-tabs, queue-length-cap fix,
worker-deployment race, streamdown upgrade, etc.) and changesets
(idempotency-key cap, sigsegv retry, locals-key fix, plugin auth, region
filters, etc.) are untouched.
|
||
|
|
dfa3ede209 |
feat(ci): support release candidates via changesets pre mode (#3628)
## Summary Enables shipping `X.Y.Z-rc.N` prereleases of `@trigger.dev/*` via changesets pre mode. RCs publish under the `rc` npm dist-tag, never claim `latest`, and don't trigger marketing-site changelog PRs. The plumbing is hyphen-in-version detection in `release.yml` — no separate workflow, no opt-in flag at publish time. Validated end-to-end against a sandbox repo (real npm publishes, Docker builds, Helm chart pushes, GitHub releases) before porting back. Full RC lifecycle tested: pre enter → rc.0 → iterate to rc.1 → pre exit → stable. Plus interaction with the existing release-branch hotfix flow. ## What changes ### `release.yml` - New `is_prerelease` output (hyphen-in-version) - GitHub release adds `--prerelease` flag for RC publishes (Pre-release badge, not Latest) - `dispatch-changelog` job gated on `is_prerelease != 'true'` — no marketing-site PR per RC ### Docker workflows - Removes the `:v4-beta` floating tag entirely from `publish-webapp.yml` and `publish-worker-v4.yml`. v4 is GA; the tag is a misnomer and is already inconsistent with the npm side (npm `v4-beta` dist-tag was frozen at 4.0.4 months ago while Docker `:v4-beta` kept bumping). Self-hosters should pin to a versioned tag going forward — the last value of `:v4-beta` stays frozen wherever it currently points. ### CLI version-check fix (`packages/cli-v3/src/utilities/initialBanner.ts`) Switches the "new version available" comparison from JavaScript `localeCompare` to `semver.lt`. The old comparison handled `X.Y.Z-rc.N` vs `X.Y.Z` incorrectly — a user on `4.5.0-rc.0` would never be prompted to upgrade once `4.5.0` stable shipped (lex order put the prerelease ahead of the bare version). Real semver gets this right. Stable users were never affected: the check queries the `@latest` dist-tag, which by convention never points at a prerelease. ## How an RC actually publishes after this 1. `pnpm exec changeset pre enter rc` on main, push the `pre.json` 2. Bot regenerates the release PR as `chore: release v<X.Y.Z>-rc.0` 3. Merge → `release.yml` runs `changeset publish` which reads `pre.json.tag` and publishes under `--tag rc`. GitHub release marked Pre-release. No marketing-site dispatch. 4. Iterate by adding changesets normally; bot bumps to `rc.1`, `rc.2`, … 5. When ready: `pnpm exec changeset pre exit`, push, merge regenerated PR → stable ships under `latest` and the marketing-site dispatch fires. |
||
|
|
4c42f6cc0b |
feat(webapp,core,cli): filter runs by region in dashboard, API, and MCP (#3612)
## Summary
Adds a Region column and Region filter (under More filters) to the runs
list dashboard, the same filter on the public runs list API
(`filter[region]`), and a matching `region` input on the MCP `list_runs`
tool. Each run's executing region is also surfaced as a new optional
`region` field on the runs list and run retrieve responses, populated
from the worker instance group's `masterQueue` identifier.
Useful when you run tasks across multiple regions and want to slice the
runs list — or your existing run-querying scripts — by where the run
actually executed.
## Design
The filter value in the URL / API is the `masterQueue` identifier (the
same string already persisted on `TaskRun` and replicated to ClickHouse
as `worker_queue`), so the query just becomes `worker_queue IN (...)`
with no server-side translation. The Region dropdown options come from a
new resource loader backed by `RegionsPresenter`, which now also exposes
`masterQueue` alongside the existing region metadata.
```ts
// public API
const runs = await runs.list({ region: ["us-east-1", "eu-west-1"] });
// each item: { id, status, ..., region?: "us-east-1" }
```
```ts
// MCP
list_runs({ environment: "prod", region: "us-east-1" })
```
|
||
|
|
454f0c949a |
perf(webapp): cache task metadata in Redis for the trigger hotpath (#3625)
## Summary The trigger-task hotpath used to early-return without a DB query when a caller passed both a queue override and a per-trigger TTL — the hottest configuration on the trigger API. Adding `triggerSource` to the resolver so the runs-list "Source" filter could distinguish STANDARD / SCHEDULED / AGENT runs removed those early-returns, costing +2 DB queries per trigger on non-locked calls and +1 on locked calls. This change caches `BackgroundWorkerTask` metadata (`ttl`, `triggerSource`, `queueId`, `queueName`) in Redis so the resolver can satisfy every caller configuration with a single `HGET` on the warm path. PG fallback on miss back-fills the cache. Follow-up to #3542. ## Design Two key spaces: - `task-meta:env:{envId}` — the "current worker" view, refreshed at every deploy promotion. 24h safety TTL. - `task-meta:by-worker:{workerId}` — used for `lockToVersion` triggers. Immutable post-create. 30d sliding TTL so historical workers age out. Cache writes use Lua scripts via `defineCommand` so `DEL` + `HSET` + `EXPIRE` land atomically — concurrent readers never see the empty intermediate state of a naive pipeline. Read-path back-fill uses single-field upserts so concurrent back-fills don't wipe each other's siblings. The cache lives behind its own `TASK_META_CACHE_REDIS_*` env-var prefix that falls back to the default `REDIS_*` set, so operators can route the cache to a dedicated Redis instance if they want. The service/instance file split (`taskMetadataCache.server.ts` for the pure class, `taskMetadataCacheInstance.server.ts` for the env-wired singleton) mirrors the existing `runsReplicationService` / `runsReplicationInstance` pattern. ## Test plan - [ ] `pnpm run typecheck --filter webapp` - [ ] `pnpm run test ./test/engine/triggerTask.test.ts --run` — 8 existing tests untouched + 5 new tests covering warm cache, cold miss with back-fill, queue + ttl path, by-worker vs env keyspace, and the promotion cache write - [ ] End-to-end against a dev worker: registering writes both keyspaces with the expected TTLs, and `redis-cli HGETALL "tr:task-meta:env:<envId>"` returns the cached entries ## Benchmark Measured `DefaultQueueManager.resolveQueueProperties` against a real Postgres + Redis (vitest `containerTest`, single-host docker). 500 sequential calls and 2,000 parallel calls (concurrency=50) per scenario, request shaped as `{ taskId, queue: "bench-queue", ttl: "5m" }` — the hot path this PR restores. ``` sequential (one in flight at a time): [noop cache (baseline)] n=500 mean=1.423ms p50=1.394ms p95=1.735ms p99=2.629ms max=11.100ms [redis cache, cold ] n=500 mean=1.346ms p50=1.283ms p95=1.688ms p99=2.463ms max=5.058ms [redis cache, warm ] n=500 mean=0.084ms p50=0.078ms p95=0.105ms p99=0.156ms max=1.129ms speedup (warm vs baseline, sequential): 16.95x parallel (concurrency=50): [noop cache (baseline)] n=2000 mean=10.069ms p50=8.850ms p95=14.718ms p99=31.887ms total=405ms ops/s=4,940 [redis cache, warm ] n=2000 mean=0.614ms p50=0.568ms p95=1.189ms p99=1.432ms total=25ms ops/s=80,389 throughput speedup (warm vs baseline, parallel): 16.27x ``` Read: - **Warm cache cuts resolver latency 17×** at p50 — from ~1.4 ms to ~78 µs per call. - **Cold cache is on par with baseline** — the extra `HGET` miss adds <50 µs against the two Postgres queries that follow, so the worst case is not worse than today. - **Under burst load (50 concurrent triggers)**, the baseline's p99 jumps to ~32 ms as Postgres connections queue up; warm stays at ~1.4 ms. The cache moves the saturation point from ~5k ops/s (PG pool) to ~80k ops/s (single-client Redis pipelining). Caveats: single-host docker, local Postgres + Redis, resolver-only measurement (excludes the rest of the trigger transaction). Prod adds region-local Redis RTT (~0.3–0.8 ms) which shifts warm absolute numbers up but keeps the ratio intact. |
||
|
|
bff4b46b22 |
fix(webapp): log Google auth conflict as warn instead of error (#3627)
## Summary A "Google auth conflict" Sentry alert fires whenever a user signs in via Google whose Google account is linked to one user row but whose Google-provided email is now on a *different* user row. The handler in `apps/webapp/app/models/user.server.ts:236` already does the right thing — it returns the existing auth-linked user and skips the update path so neither row gets mutated — but it logs the situation with `logger.error`, which routes to Sentry as an exception and pages the on-call channel. There's no exception to chase here: the branch is the intended outcome for a known data shape (user changed their email on one account after originally signing up via Google on another). Downgrading the call to `logger.warn` keeps the diagnostic record in our logs (with all the same context fields — email, both user IDs, authIdentifier) but stops it firing the production error alert. ## Change - `logger.error` → `logger.warn` for the conflict branch in `findOrCreateGoogleUser`. Context payload is unchanged. ## Test plan - [x] Typecheck only — there's no behavioural change to test, the log level is the entire diff. |
||
|
|
ac02c0f709 |
fix(core): drop unique-symbol brand on LocalsKey to fix dual-package builds (#3626)
## Summary `LocalsKey<T>` (the type returned by `locals.create()`) was branded with a module-level `declare const __local: unique symbol`. Each such declaration is its own nominal type, and `tshy` emits separate `.d.ts` files for the ESM and CJS outputs — each gets its own `__local` symbol. Under certain pnpm hoisting layouts a single TypeScript compilation can resolve `LocalsKey` from both the ESM source path and the CJS dist path within the same call site, producing two structurally-incompatible variants of the same type. TS surfaces this as the misleading error: ``` Argument of type 'LocalsKey<X>' is not assignable to parameter of type 'LocalsKey<X>'. Property '[__local]' is missing in type 'LocalsKey<X>' but required in type 'BrandLocal<X>'. ``` The error has been hitting CI on PRs opened since the chat.agent stack landed (e.g. #3625 typecheck job), but doesn't reproduce on developer machines where the pnpm node_modules layout was built up incrementally. ## Fix Replace the `unique symbol` brand with an optional phantom field that carries `T` at the type level: ```ts // before declare const __local: unique symbol; type BrandLocal<T> = { [__local]: T }; export type LocalsKey<T> = BrandLocal<T> & { readonly id: string; readonly __type: unique symbol; }; // after export type LocalsKey<T> = { readonly id: string; readonly __type: symbol; /** Phantom carrier for the value type — never read at runtime. */ readonly __valueType?: T; }; ``` The ESM and CJS `.d.ts` outputs now produce structurally identical types, so cross-output resolution no longer produces a mismatch. `T` is still carried at the type level via the optional phantom field. The runtime shape is unchanged — `manager.ts` was already casting via `as unknown`, which is no longer needed. ## Test plan - [ ] `pnpm run typecheck --filter @trigger.dev/core --filter @trigger.dev/sdk` - [ ] `pnpm run build --filter @trigger.dev/core --filter @trigger.dev/sdk` (clean rebuild) — confirms the ESM and CJS dist `.d.ts` outputs no longer carry distinct `unique symbol` declarations - [ ] `pnpm --filter @trigger.dev/core test test/mockTaskContext.test.ts --run` - [ ] `pnpm --filter @trigger.dev/sdk test test/mockChatAgent.test.ts --run` |
||
|
|
0510fd6661 |
ci: skip typecheck for refs and non-code file PRs (#3624)
Follow-up to #3615. The `code` filter currently fires typecheck for any change outside `docs/`, `.changeset/`, `hosting/`, or `.github/` - so a docs-only PR like #3623 (touching `references/ai-chat/.env.example` + `README.md`) triggered the typecheck job. None of the `references/*` packages declare a `typecheck` script either, so even when a real code change lands there, `turbo run typecheck` skips them. Running the job is pure cost. Tightens the filter to also exclude: - `references/**` - playground projects, none of them contribute to `turbo run typecheck` today - `**/*.md` - markdown anywhere - `**/.env.example` - example env files anywhere Two known gaps left open: - references/ have no real CI typecheck coverage. Separate question - either add `typecheck` scripts to each (or top-level `tsc -p`), or accept playground status. - `changes` job still runs (it's a path-filter step) but the dependent jobs all skip on irrelevant PRs. |
||
|
|
e59291e35f | docs(ai-chat): clarify local setup with .env.example (#3623) | ||
|
|
15b7cde8e7 |
feat: ai-chat reference project + MCP agent-chat tooling (4/4) (#3546)
## Summary A complete Next.js reference project that exercises `chat.agent` end-to-end, plus the CLI MCP tools that let Claude Code, Cursor, and similar IDE agents drive a deployed `chat.agent` task from the editor. Builds on #3545. ## Design `references/ai-chat` is a full Next.js app: prisma-backed persistence, multi-chat sidebar, per-chat model picker, debug panel, tool examples (`getCurrentTime`, `searchHackerNews`, `createGithubIssue`, PR review helpers, code sandbox), and smoke tests. It's intended both as a copy-paste starting point and as a place to regression-test SDK changes. The CLI gains MCP tools (`start_agent_chat`, `send_agent_message`, `close_agent_chat`, `list_agents`) so an IDE agent can converse with a deployed `chat.agent` task. The dev runtime adds one-shot OOM kill on the run controller and skills bundling in the build pipeline. |
||
|
|
8673d42c80 |
feat: ai-chat reference project + MCP agent-chat tooling
Top of the chat.agent stack: a full Next.js reference project that exercises chat.agent end-to-end, plus the CLI MCP tools that drive agent runs from Claude Code / Cursor / etc. references/ai-chat: - Full Next.js app with prisma persistence, multi-chat sidebar, per-chat model picker, debug panel, tool examples, smoke tests - Reference tools: getCurrentTime, searchHackerNews, createGithubIssue, PR review helpers, code sandbox - chat-client-test orchestrator for concurrent-send stress - references/hello-world chatAgent + triggerAndSubscribe examples CLI MCP tooling for chat.agent: - mcp/tools/agentChat.ts (start_agent_chat, send_agent_message, close_agent_chat) - mcp/tools/agents.ts + tasks.ts (list agents, agent run details) - dev-run-controller OOM kill + taskRunProcessPool tweaks - dev/managed entry-point hooks for skills bundling - buildWorker + bundleSkills (agent skills support) Includes ai-tool-helpers + mcp-agent-chat-sessions changesets, plus the streamdown@2 patch and pnpm-lock reconciliation. (Will be renamed to feature/ai-chat-reference-and-cli before push.) fix(cli): preserve lastEventId after sendMessage fallback to avoid stale turn-complete replay |
||
|
|
16538f6fdb |
feat(webapp): agent-view dashboard for chat.agent runs (3/4) (#3545)
## Summary A chat-aware run inspector and a `/playground` UI for testing `chat.agent` tasks interactively. Builds on #3543's runtime. ## Design The run inspector grows a new tab that renders the conversation chain for any `chat.agent`-kind run. It subscribes to the run's session streams, threads chat parts through a per-message renderer, and uses a shared markdown + Shiki component for code highlighting (also used by the test-payload panel). The playground is a standalone `/playground` route that lets you drive a deployed chat agent from the dashboard — pick a task, send messages, watch tool calls render, and see span detail on every turn. The matching `/agents` list view shows all deployed agents in the project. |
||
|
|
23ff763359 |
feat(webapp): agent-view dashboard for chat.agent runs
Dashboard surfaces for inspecting and debugging chat.agent runs. Depends on the Sessions primitive (L1) and chat.agent runtime (L2+L3). Run inspector — chat-aware: - AgentView + AgentMessageView (run inspector tab for chat.agent runs) - AIChatMessages + AISpanDetails + types.ts (per-span chat message rendering, tool-call/tool-output handling) - PromptSpanDetails (gen_ai.* span detail panel) - StreamdownRenderer + shikiTheme (markdown renderer with shiki highlighting and v2 patch) - useAutoScrollToBottom hook Playground UI (interactive chat.agent debugger): - /playground index + /playground/$agentParam routes - /agents route + AgentListPresenter - PlaygroundPresenter (per-org basin variants, clientData wiring) - realtime session routes for playground + run inspector chat - AI-generate-payload + AIPayloadTabContent for the test panel Navigation + theming: - SideMenu links for Agents and Playground - BlankStatePanels copy updates - tailwind config + tailwind.css storybook hooks - streamdown@2 dep in apps/webapp/package.json Includes agent-view-sessions, playground-trigger-config-fields, run-agent-view, and streamdown-v2-upgrade .server-changes. |
||
|
|
5022769f70 |
fix(webapp): retry on version collision when initializing a deployment (#3610)
Concurrent `POST /api/v1/deployments` requests for the same environment race on the `WorkerDeployment(environmentId, version)` unique constraint. Both requests read the same latest deployment via `findFirst`, compute the same next version via `calculateNextBuildVersion`, and both attempt `prisma.workerDeployment.create()` — one wins, the other crashes with Prisma `P2002`. The bug is a classic TOCTOU between the version read and the version write; it's been latent since the version-assignment logic was first added but only fires when two deploys land within milliseconds of each other (CI matrices, retried CLI calls, webhook-triggered redeploys). ## Approach Extracts the version assignment + create into a small helper `createDeploymentWithNextVersion` (`apps/webapp/app/v3/services/initializeDeployment/createDeploymentWithNextVersion.server.ts`). The helper retries on `P2002 (environmentId, version)` up to 5 times with randomised 5–50ms jitter so N concurrent racers don't loop in lockstep. Each attempt re-reads the latest version, recomputes via `calculateNextBuildVersion`, and re-runs the caller's `buildData` callback so version-dependent fields (image ref tag, friendlyId) are always consistent with the version actually persisted. A `logger.warn` fires per collision so the retry rate is observable in production logs. When retries are exhausted, the helper throws a dedicated `DeploymentVersionCollisionError` carrying `environmentId`, `attempts`, and `lastAttemptedVersion`, with the original `PrismaClientKnownRequestError` attached as `cause`. Sentry walks the `cause` chain natively, so contention exhaustion shows up as a distinguishable wrapper exception linked to the underlying `P2002` rather than a generic unique-constraint violation that looks identical to every other duplicate-key bug. The behavioural change is limited to "catch P2002 and retry instead of crashing." The image ref computation stays inside the builder callback (same call site as before the refactor), so ECR / non-ECR behaviour, S2 stream creation order, and all downstream side effects are unchanged. ## Non-goals - No new database migrations, no schema changes, no isolation-level / locking changes. A serialisable transaction or advisory lock would also fix this; retry-on-conflict is the smaller change that keeps the existing version-allocation logic intact. - Does not touch the analogous `calculateNextBuildVersion` call in `createBackgroundWorker.server.ts`, which likely has the same race shape against `BackgroundWorker`'s unique constraint — flagged as a follow-up. ## Test plan - [x] `pnpm run typecheck --filter webapp` passes (no new errors in the modified files). - [x] Three real-Postgres tests in `apps/webapp/test/createDeploymentWithNextVersion.test.ts` via `containerTest`: - 5 concurrent calls all produce distinct, persistable versions (`Set(versions).size === concurrency`). The naive read-then-create version of the helper fails this test with the exact same `P2002` seen in production; the retry version passes. - Non-`P2002` errors raised from the `buildData` callback propagate immediately without retry, builder invoked exactly once. - With `maxRetries: 0`, concurrent racers surface the wrapped `DeploymentVersionCollisionError` (not a raw `P2002`); `environmentId`, `attempts`, `lastAttemptedVersion` are populated and `error.cause.code === "P2002"`. - [x] Existing `apps/webapp/test/getDeploymentImageRef.test.ts` still green (the file was untouched in the final diff). ## Follow-ups (not in this PR) - `createBackgroundWorker.server.ts` likely has the same TOCTOU shape against its background-worker version unique constraint — should use the same helper. - Sentry visibility check: confirm `error.cause` chain renders as a linked exception in the Sentry UI when the wrapped error fires (requires a sandboxed triggering of the exhaustion path). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
a97365d1d5 |
feat(sdk): chat.agent — runtime + browser transport (2/4) (#3543)
## Summary
Adds `chat.agent({...})`, a durable conversational task runtime, plus
the browser-side `TriggerChatTransport` + `AgentChat` that drive it from
a React or Next.js app. Conversations survive page refreshes, network
blips, idle suspend, and process restarts, with built-in tools, HITL
approvals, multi-turn state, and stop-mid-stream cancellation. Builds on
#3542.
## Design
Each `/in/append` request carries at most one new message. The agent
reconstructs prior history at run boot from an object-store snapshot
plus a `session.out` replay tail, so conversation context lives
server-side instead of bloating the wire. Awaited snapshot writes after
every `onTurnComplete` keep the chain durable across idle suspend.
Registering `hydrateMessages` short-circuits both paths for customers
who own their own conversation store.
Lifecycle hooks — `onChatStart`, `onTurnStart`, `onTurnComplete`,
`onAction`, `onValidateMessages`, `hydrateMessages` — cover validation,
persistence, and post-turn work. `chat.history` exposes read primitives
(`getPendingToolCalls`, `getResolvedToolCalls`, `extractNewToolResults`,
`findMessage`, `all`) for HITL flows. `chat.local` gives per-run typed
state with Proxy access and dirty tracking. `chat.headStart` bridges
first-turn TTFC via a customer HTTP handler. `oomMachine` opts a chat
into one-shot OOM-retry on a larger machine.
`TriggerChatTransport` is a `Transport` implementation for Vercel's
ai-sdk `useChat`: delta-only wire sends, SSE reconnection with
`lastEventId` resume, stop/abort cleanup, dynamic `accessToken` refresh,
`X-Peek-Settled` fast-close. `AgentChat` is the direct programmatic
equivalent. A cross-tab coordinator does leader election so multiple
open tabs share a single SSE.
```ts
import { chat } from "@trigger.dev/sdk/ai";
import { streamText } from "ai";
export const myChat = chat.agent({
id: "my-chat",
run: async ({ messages, signal }) =>
streamText({ model: openai("gpt-4o"), messages, abortSignal: signal }),
});
```
|
||
|
|
16720a5e62 |
feat(sdk): chat.agent — runtime + browser transport
Adds the chat.agent({...}) task definition (server runtime) and the
browser-side TriggerChatTransport + AgentChat that drives it from a
React or Next.js app. The runtime sits on top of the Sessions primitive
and handles the durable conversational task lifecycle.
Server runtime:
- chat.agent({...}) — session-aware task definition
- Lifecycle hooks: onChatStart, onTurnStart, onTurnComplete, onAction,
onValidateMessages, hydrateMessages
- chat.history read primitives for HITL flows
- chat.local, chat.headStart, chat.handover, oomMachine
- Delta-only wire + S3 snapshot reconstruction at run boot
- Actions are no longer turns
Browser transport:
- TriggerChatTransport (ai-sdk Transport): delta-only wire sends,
SSE reconnection with lastEventId resume, stop/abort cleanup,
dynamic accessToken refresh
- AgentChat: direct programmatic API
- useTriggerChatTransport (React hook)
- chat-tab-coordinator: cross-tab leader election
Includes the chat-agent, chat-agent-delta-wire-snapshots,
chat-history-read-primitives, chat-head-start, chat-actions-no-turn,
chat-session-attributes, agent-skills, and mock-chat-agent-test-harness
changesets.
|
||
|
|
b62c8a19c2 |
ci: add dependabot weekly summary workflow (#3616)
Adds a Mon 08:00 UTC workflow that posts a summary of open Dependabot alerts and PRs to Slack. Uses env-scoped secrets so the alerts PAT and Slack token are only available to this workflow. |
||
|
|
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>
|