3 improvements and 1 server change.
Highlights
Realtime stream improvements: from: "latest", cursor resumption, and useSessionStream
Realtime stream subscriptions get three new capabilities in this release.
Start at the current tail. Pass from: "latest" to useRealtimeStream, streams.read(), or fetchStream to skip history and only receive live updates from the point you connect. Pair it with maxParts to keep the accumulated parts array bounded, which is useful for "last value" views like a live progress indicator.
Resume from a saved cursor. useRealtimeStream now accepts a lastEventId option and returns lastEventId in its result, so you can persist the position across page reloads and resume exactly where you left off. An onParts callback delivers each throttled batch alongside their event IDs. A reconnect or remount resumes from the last record seen, so no records are missed or replayed.
const { parts, lastEventId } = useRealtimeStream<Frame>(runId, "frames", { from: "latest", // skip history, start at the current tail maxParts: 1, // keep only the most recent frame lastEventId: savedCursor, // resume from a persisted cursor onParts: (batch) => save(batch.at(-1)?.id), // track the cursor accessToken,});
New useSessionStream hook. Read a chat agent Session's realtime channel from React without hand-rolling the API client, record accumulation, unmount cleanup, throttling, or cursor tracking. Because a Session is durable and can span multiple runs, the channel it reads outlives any single run and supports multiple concurrent subscribers, which makes it the natural way to render a chat.agent session's output as it streams.
Pass io: "out" (the default) to read the agent's output channel, or io: "in" to read its input. It takes the same subscription controls as the run-stream hooks, from: "latest", maxRecords, and lastEventId, plus an onRecords callback that delivers each throttled batch of records with their event ids and an onControl callback for control records like turn-complete (control records never enter the records array). It returns { records, lastEventId, lastControl, error, stop }.
"use client";import { useSessionStream } from "@trigger.dev/react-hooks";function AgentOutput({ sessionId, accessToken }: { sessionId: string; accessToken: string }) { const { records, error } = useSessionStream<string>(sessionId, { accessToken, // a token scoped read:sessions:{id} io: "out", // the agent's output channel (the default) from: "latest", // only new output from the point you connect maxRecords: 200, // keep the last 200 records in memory }); if (error) return <div>Error: {error.message}</div>; return <div>{records.join("")}</div>;}
Where useRealtimeStream reads a single run's stream, useSessionStream reads a Session's channel, so it keeps streaming across the runs that make up a conversation. It reads only, and requires a Public Access Token scoped read:sessions:{id}. (#4811)
Token refresh. A Public Access Token is short-lived (15 minutes by default), so a realtime subscription that watches a long-running run or a session stream can outlive its token. Until now that surfaced as an auth error with no recovery short of tearing the subscription down and creating a new one. The new refreshAccessToken option lets a subscription mint a fresh token and reconnect on its own.
It's an async callback that resolves to a fresh token, typically fetched from your backend where your secret key lives:
import { useRealtimeStream } from "@trigger.dev/react-hooks";const { parts } = useRealtimeStream<Frame>(runId, "frames", { accessToken, refreshAccessToken: async () => { const res = await fetch("/api/realtime-token"); // your backend mints a fresh public token return (await res.json()).token; },});
Set it on any realtime subscription (run streams, realtime runs, and session streams), or once on TriggerAuthContext.Provider so every hook underneath shares one refresher:
<TriggerAuthContext.Provider value={{ accessToken, refreshAccessToken }}>
The refresh is reactive: when a connection is rejected with an auth error, the subscription calls refreshAccessToken once, retries with the new token, and resumes from its last-seen record, so there is no gap and no replay. It is bounded to one refresh per connection so a rejected token can't drive a retry loop, and hooks that share a refresher dedupe a single in-flight mint. It is fully opt-in: with no refreshAccessToken supplied, auth errors stay terminal exactly as before. (#4811)
Improvements
- Native build server deploys now show a single updating log line by default. Pass
--build-logs fullto stream every line, which is always the behavior in CI and when output is not a terminal. (#4817)
Server changes
These changes are included in the v4.5.14 Docker image and are already live on Trigger.dev Cloud:
- Task retries that wait in the queue no longer count against the queue's internal redelivery limit, fixing runs with many long-delay retries being wrongly failed with
TASK_RUN_DEQUEUED_MAX_RETRIES. (#4810)
How to upgrade
Update the trigger.dev/* packages to v4.5.14 using your package manager:
npx trigger.dev@latest update # npmpnpm dlx trigger.dev@latest update # pnpmyarn dlx trigger.dev@latest update # yarnbunx trigger.dev@latest update # bun
Self-hosted users: update your Docker image to ghcr.io/triggerdotdev/trigger.dev:v4.5.14.




