Update trigger - #4
Open
tylerc-govsignals wants to merge 1699 commits into
Open
Conversation
Adds KUBERNETES_RUNNER_SECURITY_CONTEXT (off | baseline | restricted), selecting how constrained the run container is. baseline drops the capability bounding set and blocks privilege escalation. restricted additionally pins the container to a non-root uid, chosen by runtime so bun images get their own. Default is off, so this is inert on merge.
<!-- ccr-slack-attribution --> _Requested by **Matt Aitken** · [Slack thread](https://triggerdotdev.slack.com/archives/C0BKB98B84W/p1787045331358929)_ Copy-only reword of the banner shown to org admins who have not set a billing limit yet. **Before** — the banner read "Protect your organization from unexpected usage spikes." with a button labelled "Configure billing limit". **After** — it reads "Add a billing limit to your account to prevent overspending" with a button labelled "Billing limit settings". The new wording names the action up front and matches the destination it sends you to, so the banner reads as a settings link rather than a one-off setup step. ## ✅ Checklist - [x] I have followed every step in the [contributing guide](https://github.com/triggerdotdev/trigger.dev/blob/main/CONTRIBUTING.md) - [x] The PR title follows the convention. - [x] I ran and tested the code works --- ## Testing Formatting and linting pass (`oxfmt --check`, `oxlint`). No tests or snapshots assert this copy. The change is two string literals in one component, with no behaviour attached. --- ## Changelog Reworded the billing-limit banner for organizations without a limit configured, and relabelled its button to "Billing limit settings". --- ## How Both strings live in `NoLimitConfiguredBanner` in `apps/webapp/app/components/billing/OrgBanner.tsx`: the heading is the `canManageBillingLimits` branch of the banner's children, and the label is the `<span>` inside the `LinkButton`. Only those two literals changed. The button still points at `v3BillingLimitsPath(organization)` (`/orgs/{slug}/settings/billing-limits`), so routing, permissions and the non-admin variant of the message are untouched. Co-authored-by: Claude <noreply@anthropic.com>
## What Runs that carry no external trace context (schedules, task-to-task triggers) fall back to a trace id generated once in the [`TracingSDK` constructor](https://github.com/triggerdotdev/trigger.dev/blob/main/packages/core/src/v3/otel/tracingSDK.ts#L165). With `experimental_processKeepAlive` the SDK outlives the run, so every run on a warm process is exported to the external OTLP endpoint under that one id. Across our production traces, 80.3% contained spans from more than one run, worst case 25. Per-run cost and latency attribution is unusable as a result. This is the same warm-start hazard c043c4a fixed for the external-context path, which left the fallback captured at construction. ## How `FallbackExternalTraceIds` hands out one id per internal trace, shared by the span and log wrappers so a run's spans and logs agree. The id is keyed off the record's own internal trace id rather than ambient state at export time, because batch processors drain asynchronously and a run's records routinely export after the next run has started. The map is bounded and evicts least-recently-used, so a run that is still exporting can't lose its id. Granularity follows the internal trace, so a run and the runs it triggers stay on one trace. **Risk:** the wrappers only exist when `exporters` / `logExporters` are configured, so deployments that don't export externally are untouched. Nothing outside `tracingSDK.ts` changes. **Known gap (pre-existing):** sampling and id selection still branch on ambient `getExternalTraceContext()`, so records draining across a run boundary in mixed mode are misplaced in both directions. It can't use the approach here — the external id comes from the run's incoming `traceparent`, which isn't carried on the record — so closing it means capturing `internalTraceId -> external context` in a span processor. Happy to follow up separately. --- ## Testing `packages/core` suite passes. `pnpm run format` and `pnpm run lint:fix` produce no diff. Six cases in `externalSpanExporterWrapper.test.ts`, each mutation-checked rather than just observed passing: one id per run, stability within a run, correct id when records drain after the next run started (spans and logs together), external export stays off when unconfigured, retention of a run still exporting while the map churns, and the bound itself. **CI:** the five failing `webapp` shards are the ones containing `containerTest` suites. Fork PRs receive no repository secrets, so `unit-tests-webapp.yml` skips the DockerHub login and the image pre-pull (both gated on `env.DOCKERHUB_USERNAME`) and the container tests time out at 60s. Same five shards across five runs, every failure a 60s timeout, and those shards pass on internal PRs. Happy to be corrected if you can run them with secrets available. --- ## Changelog Unrelated runs are no longer merged into a single trace in your external observability tool when they happen to execute on the same warm worker process. A run and the runs it triggers still share one trace, so a run tree stays together. --- ## Screenshots _n/a_ --- _Supersedes #4526 (auto-closed before I was vouched) and #4533 (opened ready rather than as a draft). GitHub won't reopen either._ --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Iss <74388823+isshaddad@users.noreply.github.com> Co-authored-by: nicktrn <55853254+nicktrn@users.noreply.github.com>
Adds an optional priority class for run pods.
```
KUBERNETES_RUN_POD_PRIORITY_CLASS_NAME
```
When set, the value is applied as `priorityClassName` on the run pod
spec. When unset, pods are created exactly as before.
Off by default, and inert unless set. It sits beside the existing
`KUBERNETES_SCHEDULER_NAME` option and follows the same conditional
shape:
```ts
...(env.KUBERNETES_RUN_POD_PRIORITY_CLASS_NAME
? { priorityClassName: env.KUBERNETES_RUN_POD_PRIORITY_CLASS_NAME }
: {}),
```
## Verification
`typecheck --filter supervisor`, `format` and `lint` clean. No changeset
or `.server-changes/` note: off by default, no user-visible behaviour
change.
## Summary Enable additional lint rules that catch unsafe optional-chain assertions, inherited-property iteration, anonymous symbols, and unsafe external links. The existing violations now use explicit values and own-property checks, so the rules can prevent those patterns from returning.
## What The `Pre-pull testcontainer images` step is gated on `env.DOCKERHUB_USERNAME`. Fork PRs receive no repository secrets, so that variable is empty and the step is skipped along with the DockerHub login it was grouped with. ## Why With the pre-pull skipped, testcontainers pulls images lazily — inside the first test that resolves the fixture, against that test's `testTimeout`. On PR #4534 that pushed five webapp shards past their 60s cap across three runs, each failing as `Test timed out in 60000ms` while 42 of 43 files in the shard passed. Measured cost of the missing pre-pull, comparing the delta from vitest start to the first container fixture on the same runner class: | Run | Delta | | --- | --- | | internal x2 | +139.9s, +139.4s | | fork x2 | +149.7s, +149.4s | A 10.0s penalty, bimodal to within 0.3s. Note the pulls themselves succeed anonymously — there are no rate-limit errors in any of the failing logs. Only the login needs credentials, so the pre-pull can run unconditionally. ## Scope Removes the `if:` from the pre-pull step in all five workflows that have one. The DockerHub login stays gated, since it genuinely needs secrets.
## What
Three corrections to the pre-pull lists, each verified against what the
suites actually use.
## Changes
**`ryuk:0.11.0` -> `0.14.0`** in `e2e-webapp.yml` and
`e2e-webapp-auth-full.yml`. The installed testcontainers hardcodes the
image it starts:
```js
// testcontainers@11.14.0 build/reaper/reaper.js
: ImageName.fromString("testcontainers/ryuk:0.14.0").string;
```
So those two lines were pre-pulling an image nothing starts, and the one
actually used was never pre-pulled. The other three workflows already
say 0.14.0.
**`postgres:17` added** to `unit-tests-webapp.yml`. The webapp suite
references `docker.io/postgres:17` across 10 files but only
`postgres:14` was pre-pulled. `unit-tests-internal.yml` already pulls
both.
**Electric pinned to its digest** in `unit-tests-webapp.yml`. The tests
run `electricsql/electric:1.2.4@sha256:20da...` while the pre-pull asked
for the bare tag, so the pre-pull did not necessarily populate the
manifest the tests then request.
## Not changed
The otel collector and s2 images are pulled by other workflows but are
not used by the webapp suite, so they are deliberately not added here.
`postgresAndRedisTest` uses per-test containers by design and needs
nothing pre-pulled.
## What The one-off worker container boot is billed to whichever test resolves the fixture first. This moves it into a `beforeAll` with its own timeout. ## Why vitest runs the fixture chain *inside* the test timer: ```js // @vitest/runner 4.1.7 setFn(task, withTimeout(...withFixtures(handler)..., timeout, ...)) ``` There is no `fixtureTimeout`. So booting Postgres (plus `CREATE DATABASE`, schema push, ClickHouse and Redis) lands on the first test and consumes a budget sized for test work. That is why losing the image pre-pull on fork PRs was fatal rather than merely slower: the extra ~10s crossed the 60s cap. Since fork time is roughly internal + 10s and forks exceed 60s, internal runs were already clearing that cap by under 10s — a latent flake regardless of forks. ## How `withWarmup` wraps each fixture family and lazily registers a `beforeAll` on first touch, with its own generous timeout. Registration is lazy so only files that actually use a family pay for it — `@internal/testcontainers` is imported by hundreds of test files, many of which only need Redis. It registers once per file, since `isolate` gives each file a fresh module registry. Eight families are wrapped. `isolatedRedisTest`, `replicationContainerTest` and `postgresAndRedisTest` are deliberately untouched: they use per-test containers by design, so there is no one-off boot to hoist. No test file or CI changes, and it applies to every package using these fixtures. ## Verification Proven by mutation. `src/warmup.test.ts` runs container tests under a deliberately tight cap: | | Result | | --- | --- | | with the warm-up | passes | | warm-up neutered | fails, `Test timed out` | It is kept as a regression test — without it, unwrapping a fixture would break nothing visibly. `triggerFailedTask.call.test.ts`, one of the five shard casualties, passes locally in 20.4s. ## Also here `@internal/testcontainers` had no `test` script, so `turbo run test --filter "@internal/*"` skipped the package and its existing `heteroDedicated.test.ts` never ran in CI. Adding the script (matching the sibling packages') runs both files; verified green through turbo exactly as CI invokes it.
## Summary Allow the logs search schema migration to run on ClickHouse versions that require text index options to be literals. ## Root cause The text index declared `lowerUTF8(search_text)` as a preprocessor option. Some ClickHouse versions reject that column expression while parsing index settings. The projected `search_text` is already normalized to lowercase before insertion, so removing the redundant preprocessor preserves search behavior. Verified with the task events search integration tests.
… row (#4703) ## Summary Listing schedules could block the event loop for seconds. A page of 100 timezone-aware schedules spent over two seconds on cron arithmetic alone, after the database work was already done, which stalls every other request on that process. The same page now resolves in tens of milliseconds. ## Root cause and fix `cron-parser` walks the calendar unit by unit, and under a named timezone every step goes through luxon. Parsing an expression is cheap (single-digit microseconds); *stepping* it is not, ranging from a couple of hundred microseconds for a common expression to several milliseconds for a sparse one like `0 0 29 2 *`. The presenter did three independent walks per row, one backwards for "last run" and two forwards (re-parsing each time) for the next run and the occurrence after it. At 100 rows that is 300 calendar walks in one uninterrupted tick. Run times now resolve for the whole page in one pass, in a new `resolveScheduleTimings` that takes plain values rather than Prisma rows so it can be tested and benchmarked on its own. - **Nominal times are cached per `(cron, timezone)`** against a single `now` pinned for the batch, so cost scales with the number of distinct expressions instead of the number of rows. Rows in one response also stop disagreeing about the current time. - **The backwards walk is opt-in.** It is the most expensive of the three and only the dashboard renders the column; the public API never returned it at all. - **Windowless schedules take one step instead of two.** The second step only measures the interval to the following occurrence, and that interval reaches the result solely through `min(intervalMs, max(MINIMUM_SCHEDULE_RANGE_MS, windowMs))`. With no window `windowMs` is 0, and `CronPattern` rejects expressions with a seconds field, so occurrences are always at least `MINIMUM_SCHEDULE_RANGE_MS` apart and that `min` can never bind. It is also the costlier step, since it walks a whole period rather than the remainder of the current one. - **`nextScheduledTimestamps` steps one parsed expression** instead of re-parsing per step, which also helps the single-schedule callers. Behaviour is unchanged, error semantics included: a malformed expression still throws for the next run and still degrades to an undefined last run. ## Verification Measured inside a real request against a live environment, 100 schedules: sparse expressions went from 2250-2652 ms to 23-30 ms, and five distinct timezone expressions from 463-500 ms to 9.7-10.6 ms. The new suite checks the optimized code against an inline copy of the previous implementation across eleven cron and timezone combinations plus five DST transitions, so the rewrite is verified as behaviour-preserving rather than just faster. Separate tests pin the invariant the single-step path depends on, so if sub-minute crons are ever allowed they fail loudly instead of the timings quietly going wrong. Worth knowing for later: `cron-parser` v5 is a much faster rewrite on exactly this workload (`prev()` under a timezone drops from roughly 2700 to 60 microseconds), but it is a breaking API change across several call sites including the schedule engine, so it belongs on its own. The differential test added here is the tool to de-risk it.
`@grpc/grpc-js` sat at 1.12.6 in the lockfile. `dockerode` is the only consumer and already declares `^1.11.1`, so a scoped override is enough: ```json "@grpc/grpc-js@>=1.12.0 <1.12.7": "1.12.7" ``` Pinned exactly to stay on the 1.12 line; a caret would pull 1.14.x.
## Summary Enforce stable React hook ordering in the dashboard and React hooks package. Conditional hook calls now keep a consistent order, and overloaded realtime stream arguments are resolved before entering the shared hook implementation. Base: `main`
## Summary Add explicit types to native dashboard buttons and enforce `react/button-has-type`. This prevents action buttons from accidentally submitting a surrounding form. Shared button primitives retain their caller-selected submit and reset semantics with documented lint exceptions. Base: [#4691](#4691)
## Summary When a chat.agent run boots to continue a session (a version handover, or a retry after a crash), it replays the unacknowledged user messages off `session.in` and dispatches them itself. A previous change stopped the live tail from re-answering those same messages by folding them into the resume cursor in one step. That cursor is what the next boot reads to know where to resume, and folding in every recovered message at once let it advance past a message the run had not answered yet. So if the run answered the first recovered message, wrote its turn boundary, then crashed before dispatching the rest, the next boot resumed past those messages and they were never answered. ## Fix A recovered message is now claimed on the session-stream router instead of folded into the cursor. A claim does two independent things: - it drops the message however late the live tail re-delivers it, so a recovered message is never answered twice; - it holds the resume cursor behind that message until the boot has dispatched it, so a turn boundary never publishes a cursor past a message still waiting for a turn. The boot settles each claim as it dispatches the message, or right away for a message it folds into the seed chain or deliberately skips, so the cursor only advances over messages that have actually been handled. A claimed record whose route re-read never arrives over the tail degrades to being answered twice on the next boot, never to being dropped. Covered by router-level unit tests for the claim/settle floor and a chat.agent boot test asserting the cursor published after the first recovered turn stays behind the still-unanswered ones.
## Summary The bundled `trigger-chat-agent-advanced` skill still told agents to answer from an action by returning a value, an API #4816 removed. Code generated from it fails at runtime with the `chat.turn()` error. Before, the skill said: ```ts onAction: async ({ action, streamText }) => { if (action.type === "regenerate") { chat.history.slice(0, -1); return streamText({ model, messages }); } }, ``` After: ```ts onAction: async ({ action }) => { if (action.type === "undo") chat.history.slice(0, -2); // edit only if (action.type === "regenerate") { chat.history.slice(0, -1); return chat.turn(); // answer the edited history } }, ``` The section now covers edit-only actions, `chat.turn()` and the `action-turn` trigger, persistence for both the platform-managed and `hydrateMessages` models, and sending actions through `useChat` (`body.action` or `useChatActions`) so the answer renders, with `transport.sendAction` noted as the raw-stream path. Docs-only change to an SDK-bundled skill; no changeset, since the `chat.turn()` release note from #4816 already covers the behavior. Raised by Devin on #4884 after merge. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01AxuSksX18bj1yhnLpkcQ6a --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ericallam <534+ericallam@users.noreply.github.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Eric Allam <ericallam@users.noreply.github.com>
…mantics No user-facing change. The new decorator remains uninstalled and cannot be selected by production configuration in this PR. Mono-RevId: 8637f7ed56d1fc641d7c7038bae2ac4f4c36a4f2
trigger-dev-repo-ops
Bot
force-pushed
the
main
branch
from
September 10, 2026 09:08
4f8587c to
05bf16a
Compare
Adds GET /api/v1/deployments/:id/artifact-url, which returns a short-lived presigned URL for the deployment's artifact after verifying the key belongs to the caller's environment and the object still exists. Mono-RevId: a8cc8df3b0d9e6ea86247562cee63593879cac71
Document the authorization trust boundary for self-hosted installs. Organization membership is the trust boundary on a self-hosted install: everyone in an organization is someone you invited to your own deployment. Separating what an Admin may do from what a Member may do comes from an RBAC plugin that is not part of this distribution, so the permission layer falls back to a permissive ability by design. SECURITY.md now states this explicitly and scopes vulnerability reports against it. Reaching data or actions belonging to an organization you are not a member of, or bypassing authentication, is in scope on every deployment. A member performing a privileged action inside their own organization is out of scope for self-hosted. The webapp contributor guide records the matching rule for code here: enforce tenancy, and leave role separation to the plugin rather than growing the fallback. Mono-RevId: a4f245a2b053007a801e5ef15006148b13735a87
Mono-RevId: ce4fab99f3b829a0405b305382a84b94bbe26c97
…ency resolver No user-facing change. This adds an optional internal route field while the snapshot-store rollout remains inactive. Mono-RevId: 3da62651b5918e6873a83e0d30c2a05ad24b6542
Span and log attributes are now serialized by the server before being stored, so the attribute text shown in the dashboard is exactly what your task sent. Mono-RevId: b789b40bd993d5096f0e10ca57554ecb0bfd12e9
Reduces sensitive values retained in CLI and SDK diagnostics. Build command logs redact build arguments, environment pulls secure destination permissions before writing, Git metadata removes URL credentials, and retry telemetry records only a small safe set of URL and response details without changing requests or retry behavior. Mono-RevId: e6854bf5379682e208a49fcfa7477d8b799dc849
Enforce the existing dashboard permissions on queue and environment mutations, prompt detail reads, idempotency key resets, paid add-on purchases, and organization changes. Controls you cannot use are disabled with an explanation rather than failing on submit, and allocation and quota-increase requests stay available. Billing notices now render from a fixed set of known message keys instead of caller-supplied copy, and the schedules add-on returns stable error messages. Permission checks scoped to a project now pass that project through, so a project-level role override applies wherever one is configured. Branch archival now respects the configured permissions for dashboard sessions and personal access tokens, with disabled controls explaining denied access. Existing API-key scope requirements are unchanged. Mono-RevId: 5a7c999c836e2b7d9a72f17f3ca48fefe5d46e6d
GitHub App installation callbacks now verify that the authenticated installing user can access the installation before linking it to an account. Mono-RevId: c59ade6435294a39335d1b85cc881a0afd1d1452
Mono-RevId: c223e3c56d33e50c024789514cf218f46eded002
Waitpoint registration now verifies that the target run belongs to the authenticated environment. Mono-RevId: 0998236ba421350cd59c5c90fa09e918df0074cb
…ead of building a pod Adds an alternative Kubernetes workload manager behind a new `KUBERNETES_RUN_CRD_ENABLED` setting, off by default. Instead of building a run pod directly, it creates a `Runner` object in the `compute.trigger.dev/v1alpha1` API group and stops, leaving pod construction to the operator that reconciles that object. This keeps the reasoning about uid pinning, node selection and the label set a network policy selects on in one place instead of copying it a third time. The Helm chart exposes it as `supervisor.config.kubernetes.runCrdEnabled`, also off by default, which sets that variable and grants the supervisor's Role the two writes the backend needs: creating the `Runner`, and creating and patching the Secret that carries the deployment token. Turning it on requires the `Runner` CRD and a controller reconciling it to be installed, or the create fails. The existing pod-building path is unchanged and remains the default. The two are mutually exclusive by construction: whichever one runs is the only thing that creates a workload for a cold start. Mono-RevId: 0665f61da14cbc8ec45c7fb75b013cf45f93fdc8
…pen on slow SET Mono-RevId: 97abba33b1d2895851eca6d9225c05fb8a99e027
Run state notifications were broadcast to both the deployed worker and dev worker socket.io namespaces for every run, regardless of environment. The dev worker namespace only accepts connections from development environments, so that second broadcast was discarded for every non-development run. Notifications now reach the dev worker namespace only for runs in a development environment. This removes the redundant broadcast and roughly halves the Redis pub/sub traffic generated by the run notification path. Mono-RevId: 84cb5aead09067daeb979d793e3974c2a1da57c8
# Chat history reads a page at a time, and the model's context stays
private
Reading a `chat.agent` conversation's history used to download and parse
the whole conversation, so
it got slower as a chat grew. A conversation's stored transcript now
carries an index, so reading
the most recent messages fetches only those messages.
The same change closes a narrower problem: the model-side context an
agent keeps, its compacted
history and any injected context, was reachable from a browser. It is
not part of a transcript and
is no longer served with one.
## Before
```mermaid
sequenceDiagram
participant Browser
participant Webapp
participant Store as Object store
Browser->>Webapp: open a chat
Webapp-->>Browser: presigned URL for the whole snapshot
Browser->>Store: GET the entire object
Store-->>Browser: messages AND model context
Note over Browser,Store: the whole conversation, including context a transcript never shows
Webapp->>Store: GET the entire object (page read)
Webapp->>Webapp: parse all of it, return 50 messages
```
Every read paid for the whole conversation: request-thread CPU for the
API, bytes over the wire for
the browser. Paging alone could not fix it, because the messages a page
needs sit at the end of one
JSON document, so finding them meant parsing all of it.
## After
```mermaid
sequenceDiagram
participant Browser
participant Webapp
participant Store as Object store
Browser->>Webapp: open a chat
Webapp->>Store: ranged GET, end of the object
Store-->>Webapp: index + newest entries
Webapp-->>Browser: messages and a cursor
Note over Webapp,Store: one ranged read, and the browser never touches the store
```
The stored transcript is a header line holding the cursors and the
agent's own state, one line per
message, an index, and a fixed-width trailer giving the index's length.
A reader takes the end of
the object and decodes only the bytes holding the page it was asked for.
## Safety contract
- The private state lives in the header, so a page read cannot return it
by layout, not by
remembering to strip a field.
- A cursor the transcript no longer holds yields an empty page, never
the newest entries, which
would present recent messages as older ones.
- Page timestamps come from a message's position in the whole
transcript, not in its page, so pages
fetched newest-first still merge into conversation order.
- Trimming is expressed relative to the compaction watermark, because
everything after it is live
context the next boot converts and must never be dropped.
## Scope and impact
A conversation saved by an earlier version still reads correctly: the
reader recognises the old
format and reads the whole object. Each conversation moves to the new
layout the next time it
saves, so there is no migration step. An older reader cannot read the
new format, so roll forward
rather than back.
The built-in storage is deliberately basic about long conversations:
once an agent has compacted it
keeps roughly the last hundred messages and drops the rest, so what it
rewrites each turn stops
growing. A conversation that never compacts is kept whole. An app that
renders history further back
than that keeps its own transcript storage.
## Rollout controls
No flag. The read change is the same data through a cheaper path, and
gating the exposure fix would
mean leaving it open by default. Revert is a deploy revert: stored
objects are untouched and the
state is still written and read on the private path. Presigned URLs
already issued stay valid for
their lifetime, so the exposure fix is not retroactive for the few
minutes before a deploy.
What a regression looks like: `chat.agent: snapshot version/shape
mismatch` at run boot, or
`transcript endpoint: ranged read failed` in the webapp logs. The first
would mean a conversation a
later run cannot read, which is the one worth paging on.
## Verification
Tests prove: paging over byte offsets, including multibyte content where
a character-indexed table
would slice mid-message; reading the previous format; a conversation
whose stored media type
disagrees with its contents; a cursor the transcript no longer holds;
each page's position in the
whole transcript; that no byte a page read fetches contains the private
state; that a trimmed
transcript still restores the model's context; and that a watermark
outside the retained window
does not discard the summary. One test asserts the failure that guard
prevents, so it cannot be
dropped quietly. Real runs additionally cover a cold continuation
booting from a trimmed snapshot
and snapshot size plateauing over 130 turns.
Mono-RevId: 7e57e5e9b859592827719a35e6a624c1cd7123f6
…og line Mono-RevId: ab2161b49c12d48a7fb6102f0d6a4142c97931ff
Stops writing task event attributes to the native JSON column; the serialized attributes text is now the stored form. Mono-RevId: c1a17fb9ea4ced31daa63c6e4b00425c1cc62894
Request idempotency was doing nothing. The key a client sends is hashed together with its environment and task scope before it is looked up, but the idempotency cache still required the raw UUID format and rejected the hashed value, so every trigger, batch trigger and create-batch request skipped deduplication. A request the server had already accepted, whose response the client never received, would therefore create a second run or batch when the SDK retried it. The cache now accepts the derived key, so a retried request returns the original run or batch. The client-supplied `x-trigger-request-idempotency-key` header is still validated as a v4 UUID, now at the point where it is read; a header of any other shape is ignored as before, so the request itself still succeeds. Keys stay scoped to a single environment, task set and request type. Mono-RevId: 85b6e31f5bdb722d99db28f66a2d5b533a0f3ce4
… updates Mono-RevId: 53978f5b05eb06b35f284e821daab76dc45eaa01
Mono-RevId: 749cbfed4729e03d99a710f4ce62acdc0beba8ca
Mono-RevId: c8312b2935b569d642257e27711fe21cf8648ae2
…pt storage (#4916) ## Summary Adds `docs/ai-chat/migrating-from-hydrate-messages.mdx`, a step-by-step guide for agents that persist history through the deprecated `hydrateMessages` hook and want to move onto a `TranscriptStorage`. The transcript storage page kept a three-line migration list; this is the full walkthrough, written from doing exactly this migration on the durable chat example. Before, the transcript storage page said: ```md 1. Implement `TranscriptStorage` over your existing tables. Your `hydrateMessages` body becomes `loadContext`; the writes you did in hooks become `save`. ``` After, that list says the read becomes `load`, `loadContext` is only for a database that decides the model's context, and points at the guide. Porting a `hydrateMessages` body into `loadContext` wholesale carries the recovery and compaction code the runtime now owns, which is the mistake the guide is there to prevent. The guide covers: - a table of what changes hands between the hook and the storage - `load` and `save` over a row-per-message table, with the ordering rules a row store has to follow (position assigned on first insert only, never a write timestamp) - when `loadContext` is needed, and when it is not - what to delete from each hook: message and cursor writes, partial rows and run-state flags, tool-call repair, the summary watermark, empty-response filters - the frontend history read with `createLoadTranscriptAction` and `useLoadTranscript` - the conformance suite, and a checklist Registered in `docs.json` after the transcript storage page. Docs only; no changeset. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01AxuSksX18bj1yhnLpkcQ6a
…age (#4917) Adds an explicit ordering rule to the "Writing your own storage" list in the transcript storage guide. The ordering contract was implicit: the page said `put` for a known id keeps its position and that `load` returns the whole conversation in order, but never told an adapter author how to preserve order in a row-per-message store, and never warned against a write-timestamp column. That gap is easy to trip over (reaching for a `seq`/timestamp column) because the contract deliberately has no sequence number, only an order. The new bullet states it outright: order is a transcript position, not a write time; a document store gets it from `transcript.entries`; a row store needs an order column set once when a `put` first inserts an id, in `put`-arrival order, and left unchanged on an in-place replace; don't sort by a write timestamp, because a replaced message must keep its place and a mid-turn steering message sorts before the answer it shaped. Docs-only. Verified against the runtime's `applyChanges`/`buildChanges` in `transcriptStorage.ts`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
…k extend Run replication now recovers on its own after a Redis restart or outage. The leader could previously lose its lock without noticing and log "Cannot extend an already-expired lock" every few seconds while holding the replication slot open, until the server was restarted. It now re-acquires the lock, or steps down once and re-elects. Deployments running under a process supervisor can set `RUN_REPLICATION_MAX_RESUBSCRIBE_ATTEMPTS` (or the session equivalent) to exit and be restarted when a replication stream cannot recover. It defaults to 0, which keeps retrying and preserves existing behavior. Mono-RevId: 465d1f9bd0bb720320c8dd294dde3c3a9b88f1d9
Reloading a chat while the agent is still answering now shows the
message being answered. The incoming message was previously persisted
only once the turn finished, so a refresh mid-answer rendered the reply
with no question above it.
The runtime now writes the message at the start of the turn, carrying
the previous turn's stream cursors so a mid-answer reload still resumes
from the last completed turn. That write is not awaited before the model
runs. The output stream is held on it instead, so no part of the answer
reaches the frontend before the message is durable, and time to first
token is unchanged.
The same ordering is available to your own writes as
`chat.deferBeforeOutput()`:
```ts
onTurnStart: async ({ chatId, uiMessages }) => {
chat.deferBeforeOutput(
db.chat.update({ where: { id: chatId }, data: { messages: uiMessages } })
);
},
```
It runs alongside the model like `chat.defer()`, but the answer waits
for it, so the next page load always sees the write. It orders the write
against what the frontend can see and not against the model, so a write
that a tool reads back during the same turn still needs to be awaited.
Mono-RevId: a84c08af51b376f5a49091d001ed1ec59149881f
… reads secret-key only
Session public tokens can now be narrowed to one stream with
`read:sessions:{id}:out`, and reading a session's `.in` channel now
requires a secret key. The dashboard agent's browser token uses the
narrowed scope.
Mono-RevId: 1214e6dd60256c7f1323e891580c16d500c8fd9a
## Docs: intro rework, agent anatomy rewrite, and more AI agent examples - **Introduction.** Reworked the docs landing page to lead with AI agents and workflows: a cleaner hero, a core concepts section, and short sections for building agents, scaling and scheduling, and self hosting. Corrected the licensing wording and refreshed the card styling. - **Anatomy of an agent.** Rewrote the page so it teaches the three parts of a chat agent (the agent task, the durable session, and the frontend transport) and traces a single message through them, instead of only linking out to other pages. - **AI agent examples.** Added more example projects to the AI agents overview: an ElevenLabs voice agent, the ask Trigger chat agent, a batch LLM evaluator, and a Claude thinking chatbot. Mono-RevId: fb6db42f103bdc6d35af62163ee33427fb67211d
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #
✅ Checklist
Testing
[Describe the steps you took to test this change]
Changelog
[Short description of what has changed]
Screenshots
[Screenshots]
💯