Skip to content

feat(sdk,core,webapp,react-hooks): named side channels on a Session - #4815

Open
ericallam wants to merge 17 commits into
mainfrom
feature/tri-13516-session-side-channels-named-two-way-cross-run-realtime
Open

feat(sdk,core,webapp,react-hooks): named side channels on a Session#4815
ericallam wants to merge 17 commits into
mainfrom
feature/tri-13516-session-side-channels-named-two-way-cross-run-realtime

Conversation

@ericallam

@ericallam ericallam commented Aug 28, 2026

Copy link
Copy Markdown
Member

Summary

Adds named side channels to a Session: durable, two-way realtime streams that outlive a single run and are shared across every run of the session. Today a Session has exactly one reserved .in/.out pair (the chat transcript). This lets a session hold any number of named channels alongside it, each its own .in/.out pair, so an agent can stream out-of-band data (a feed of frames, telemetry, a control channel) on a stream separate from the transcript while many clients read it live.

The two properties a named channel adds over the reserved pair:

  1. It is addressed by a name that outlives a run and is shared across runs, not welded to the chat turn loop.
  2. Writing its .in does not wake or trigger a run. A run observes it by subscribing; an external client writes it without spawning anything.

This is the generalization half of the Momentic ask (stream browser screenshots from a chat.agent to the frontend on a channel separate from the chat). It builds directly on the start-from-latest / useSessionStream subscribe seam from #4811.

Usage

Declare the channel's record types once and infer them on both sides:

// channels.ts (shared, client imports it type-only)
import { sessions } from "@trigger.dev/sdk";

export const screenshots = sessions.defineChannel<{ out: ScreenshotFrame; in: ViewportControl }>(
  "screenshots"
);

Open a channel from a session handle (sessions.open(id) returns one for a known session id). Writing its .out is durable, cross-run, and wakes nothing; a run observes its .in by tailing, without suspending:

import { sessions } from "@trigger.dev/sdk";
import { screenshots } from "./channels";

const channel = sessions.open(sessionId).channel(screenshots);
await channel.out.append(frame);             // frame: ScreenshotFrame (typed from the definition)
channel.in.on((control) => { /* ... */ });    // control: ViewportControl, tail, no suspend

Passing the definition types .out.append / .in.on on the producer side; a bare name string also works, with records typed unknown.

An external client writes the .in without waking a run, and reads the .out from React:

sessions.open(sessionId).channel("screenshots").in.send({ paused: true });

const { records } = useSessionStreamChannel<typeof screenshots>("screenshots", {
  sessionId,
  accessToken,
  io: "out",
  from: "latest",
  maxRecords: 1,
});

session.channel(name) returns the same { in, out } handle shape as the reserved pair, so append / pipe / writer / read / writeControl / trimTo on .out and send / on / once / peek on .in all carry over. Passing a name other than the declared one is a type error; a bare-string call without the generic stays valid with records typed unknown.

With chat.agent

This is the motivating case: a chat.agent answers on the reserved transcript as usual, and streams screenshot frames on a side channel in parallel. chat.channel(name) opens a channel on the current run's own Session, so there's no id to thread:

import { chat } from "@trigger.dev/sdk/ai";
import { streamText } from "ai";
import { screenshots } from "./channels";

export const browserAgent = chat.agent({
  id: "browser-agent",
  run: async ({ messages, signal }) => {
    const frames = chat.channel(screenshots);

    // client pause/resume arrives here without waking a turn
    frames.in.on((control: ViewportControl) => applyViewport(control));

    // frames stream on their own channel, not the chat transcript
    driveBrowser({ signal, onFrame: (frame) => frames.out.append(frame) });

    // the assistant reply still goes to the reserved transcript
    return streamText({ model: openai("gpt-4o"), messages, abortSignal: signal });
  },
});

chat.channel(name) is a shortcut for chat.session().channel(name); chat.session() returns the current run's full SessionHandle if you need it.

The frontend renders the transcript with useChat as before, and the screenshots with useSessionStreamChannel<typeof screenshots>("screenshots", { sessionId: chatId, io: "out", from: "latest", maxRecords: 1 }): a live view of the newest frame that survives across turns (each turn is a new run), because the channel is keyed on the session, not the run.

How it works

Addressing. A channel is a stream name segment: sessions/{id}/channels/{name}/{io}. The reserved pair keeps its two-part sessions/{id}/{io} name for back-compat, and the channels/ segment means a user channel named in/out can never collide with it. The channel dimension is threaded through the session stream manager (keyed on (session, channel, io), reserved = absent), subscribeToSessionStream, the session apiClient methods, and the realtime.v1.sessions.$session.channels.$channel.$io.{ts,append,records} routes. The reserved-pair routes are untouched. The start-from-latest tail path from #4811 is channel-agnostic, so from: "latest" and maxRecords compose unchanged.

No-wake. The reserved .in append route ensures a run and drains waitpoints so a chat turn advances. The channel .in append route deliberately does neither: the record lands durably and a run picks it up when it next subscribes, so writing a side channel can't spawn or resume a run. A named channel's .in is therefore subscribe-only from the run side (.on / .once / .peek); .wait() / waitWithIdleTimeout() throw with a message pointing at the observe methods.

Auth. Channel scope folds into the existing resource id (sessions:<key>:channels:<channel>), so no RBAC grammar change. A channel route authorizes both the channel-folded id and the bare session id, which means a session-wide token grants every channel while a channel-scoped token grants only its own. The per-io rule is preserved per channel: writing .out requires secret-key auth so a browser can't forge frames; .in is writable with the session token.

Retention. A run-independent channel has no turn loop trimming it, so each channel gets a default native retention on the stream store (bounded age plus delete-on-empty), overridable per channel via session.channel(name, { retention }). It is applied server-side at initialize and cached per stream, so it costs one control-plane call per channel, never per record.

Spans. Channel writes carry channel and io attributes plus an accessory chip, so the span inspector shows which channel and direction a write was on.

Verification

  • Unit (core): the stream manager isolates channels: two channels on the same (session, io) never cross buffers, and a named channel is isolated from the reserved pair.
  • Full-stack e2e against a real stack (webapp, stream store, Postgres, real runs):
    • a named .out record is readable back after the triggering run has gone terminal (durable, cross-run);
    • a channel .in append creates no run, while a reserved .in append does wake one (the differential is the red/green);
    • from: "latest" on a named channel delivers the live record and does not replay the backlog from the start;
    • an invalid channel name is rejected.

Notes

  • Dashboard channel listing was dropped. It would need a stream-store list operation the self-hosted (OSS) stream store does not support, so it can't be an OSS feature. The retention above degrades the same way: native per-stream retention applies where the stream store supports it, and channels fall back to store-level retention where it doesn't.
  • The ~1 MiB per-record cap is unchanged. Large payloads (e.g. raw screenshots) still need object-store pointers on the channel rather than inline bytes; that's independent of this change.
  • Docs will follow in a separate PR per repo convention; the changeset covers the changelog entry.

Screenshots

CleanShot 2026-08-28 at 21 46 27@2x

Generalize a Session's reserved .in/.out pair into named side channels.
Core: SessionStreamManager/facade/interface key on an optional channel
(reserved = undefined), defineSessionChannel + type extractors, and a
channel arg on subscribeToSessionStream. Webapp: S2 stream names gain a
channels/{name}/ segment (default pair unchanged for back-compat), plus
server-side list/reconfigure control-plane ops, and three channel routes
(subscribe/append/records) with a no-wake append path and channel-folded
auth.
…etention

Add SessionHandle.channel(name, options) returning a named .in/.out pair,
threading the channel through the SDK write/read/control/trim paths and the
apiClient session methods. Channel writes carry channel + io span attributes
and accessory chips so the span inspector renders them. A side channel's .in
is subscribe-only (.on/.once/.peek); .wait()/waitWithIdleTimeout throw, since
a side channel does not wake a run. Named channels get a default native S2
retention (24h + delete-on-empty), overridable per channel, applied
server-side at initialize and cached per stream.
Two channels on the same (session, io) never cross buffers, and a named
channel is isolated from the reserved default channel.
…nels

Read one side of a named Session side channel from React, with record types
inferred from a defineSessionChannel declaration. Builds on subscribeToSessionStream
with the channel option; reuses the useSessionStream from/maxRecords/cursor-resume
behavior, keyed per channel.
@changeset-bot

changeset-bot Bot commented Aug 28, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f993d00

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 27 packages
Name Type
@trigger.dev/react-hooks Patch
@trigger.dev/core Patch
@trigger.dev/sdk Patch
@trigger.dev/build Patch
trigger.dev Patch
@trigger.dev/python Patch
@trigger.dev/redis-worker Patch
@trigger.dev/schema-to-json Patch
@internal/clickhouse Patch
@internal/llm-model-catalog Patch
@internal/metrics-pipeline Patch
@trigger.dev/rbac Patch
@internal/redis Patch
@internal/replication Patch
@internal/run-engine Patch
@internal/run-store Patch
@internal/schedule-engine Patch
@internal/tracing Patch
@internal/webhook-engine Patch
@internal/webhook-sources Patch
@internal/dashboard-agent Patch
@internal/cache Patch
@trigger.dev/rsc Patch
@trigger.dev/database Patch
@trigger.dev/otlp-importer Patch
@trigger.dev/sso Patch
@internal/testcontainers Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 729a3c67-a4b6-47b5-8843-39be389a3689

📥 Commits

Reviewing files that changed from the base of the PR and between 225edfe and 5705e51.

📒 Files selected for processing (4)
  • docs/ai-chat/side-channels.mdx
  • docs/docs.json
  • docs/realtime/react-hooks/session-stream.mdx
  • packages/react-hooks/src/hooks/useSessionStreamChannel.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 4 remain after this review.

📜 Recent review details
⏰ Context from checks skipped due to timeout. (30)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (17, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (21, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (18, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (16, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (19, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (3, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (13, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (20, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (7, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (14, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (15, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (9, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (11, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (10, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (12, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (5, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (8, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (6, 24)
  • GitHub Check: e2e / 🧪 CLI v3 tests (warp-windows-latest-x64-8x - pnpm)
  • GitHub Check: packages / 🧪 Unit Tests: Packages (3, 3)
  • GitHub Check: e2e / 🧪 CLI v3 tests (warp-windows-latest-x64-8x - npm)
  • GitHub Check: packages / 🧪 Unit Tests: Packages (2, 3)
  • GitHub Check: packages / 🧪 Unit Tests: Packages (1, 3)
  • GitHub Check: sdk-compat / Deno Runtime
  • GitHub Check: internal / 🧪 Unit Tests: Internal
  • GitHub Check: sdk-compat / Cloudflare Workers
  • GitHub Check: e2e-webapp / 🧪 E2E Tests: Webapp (2, 2)
  • GitHub Check: e2e-webapp / 🧪 E2E Tests: Webapp (1, 2)
  • GitHub Check: 🛡️ E2E Auth Tests (full)
  • GitHub Check: 🛡️ E2E Auth Tests (full)
🧰 Additional context used
📓 Path-based instructions (7)
**Prefer static imports over dynamic imports.** Only use dynamic `import()` when:

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • packages/react-hooks/src/hooks/useSessionStreamChannel.ts
Add crumbs as you write code — not just when debugging. Mark lines with

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • docs/docs.json
  • docs/ai-chat/side-channels.mdx
  • docs/realtime/react-hooks/session-stream.mdx
  • packages/react-hooks/src/hooks/useSessionStreamChannel.ts
Main documentation config must be defined in `docs.json` which includes navigation structure, theme, and metadata

📄 CodeRabbit inference engine (docs/CLAUDE.md)

Files:

  • docs/docs.json
Use function declarations instead of default exports

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Files:

  • packages/react-hooks/src/hooks/useSessionStreamChannel.ts
MDX documentation pages must include frontmatter with title (required), description (required), and sidebarTitle (optional) in YAML format

📄 CodeRabbit inference engine (docs/CLAUDE.md)

Files:

  • docs/ai-chat/side-channels.mdx
  • docs/realtime/react-hooks/session-stream.mdx
Use types over interfaces for TypeScript

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Files:

  • packages/react-hooks/src/hooks/useSessionStreamChannel.ts
When creating or editing OTEL metrics (counters, histograms, gauges), ensure metric attributes have low cardinality by using only enums, booleans, bounded error codes, or bounded shard IDs

📄 CodeRabbit inference engine (.cursor/rules/otel-metrics.mdc)

Files:

  • packages/react-hooks/src/hooks/useSessionStreamChannel.ts
🧠 Learnings (1)
📚 Learning: 2026-04-30T20:30:29.458Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3226
File: docs/ai-chat/quick-start.mdx:13-13
Timestamp: 2026-04-30T20:30:29.458Z
Learning: In this repo’s documentation MDX files (`docs/**/*.mdx`), use `ts` and `tsx` (not `typescript`) as the code-fence language tags for TypeScript/TSX snippets. Do not flag `ts`/`tsx` code-fence language tags as incorrect in any docs MDX file, since this is the site-wide Mintlify-compatible convention.

Applied to files:

  • docs/ai-chat/side-channels.mdx
  • docs/realtime/react-hooks/session-stream.mdx
🔇 Additional comments (4)
packages/react-hooks/src/hooks/useSessionStreamChannel.ts (1)

40-44: LGTM!

docs/ai-chat/side-channels.mdx (1)

1-125: LGTM!

docs/docs.json (1)

100-100: LGTM!

docs/realtime/react-hooks/session-stream.mdx (1)

110-126: LGTM!


Walkthrough

Adds named, durable, two-way Session side channels. The SDK exposes typed .in and .out handles with retention options and blocks wait operations on named channels. Core stream state, API clients, realtime routes, S2 streams, authorization, and retention support channel-specific addressing. The React package adds useSessionStreamChannel for typed subscriptions with batching, cursors, limits, controls, and cancellation. Tests verify isolation between named and default channels.

Merge Risk: 🟡 Moderate · up to 5705e

This PR adds durable cross-run channels, but session-wide credentials can access every channel, retention may silently remain unenforced, and control messages may be delivered more than once during claim-store failures. Additional hook and telemetry regressions remain open, so the change needs explicit owner follow-up or acceptance before merging.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 12 functions across 18 files. (3 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely identifies the main change: named side channels added across the SDK, core, webapp, and React hooks.
Description check ✅ Passed The description is detailed and directly related to the changes. It covers the feature summary, usage, implementation details, verification steps, and screenshots. It does not include the template's i…
Full details: Docstring Coverage

Explanation

Docstring coverage is 41.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 12 functions across 18 files. (3 skipped: 3 unsupported.)

Full details: Description check

Explanation

The description is detailed and directly related to the changes. It covers the feature summary, usage, implementation details, verification steps, and screenshots. It does not include the template's issue-closing line, checklist, or explicit Changelog section, but the description is substantially complete.

✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/tri-13516-session-side-channels-named-two-way-cross-run-realtime

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Observability map

As of f993d00.

20/100 over 454 measured of 472 entry points (base 20, no change)

What this PR changed

route base head now failing
/realtime/v1/sessions/:session/channels/:channel/:io new 0 request-context
/realtime/v1/sessions/:session/channels/:channel/:io/append new 0 request-context
/realtime/v1/sessions/:session/channels/:channel/:io/records new 0 request-context
/resources/orgs/:organizationSlug/projects/:projectParam/env/:envParam/runs/:runParam/realtime/v1/sessions/:sessionId/channels/:channel/:io new 0 request-context

FIX FIRST

  • /api/v1/projects/:projectRef/envvars (sensitive) - auth-boundary, request-context
  • /auth/sso (sensitive) - auth-boundary, request-context
  • /_app/orgs/:organizationSlug/settings/team (sensitive) - error-classification, auth-scope, request-context

AUDIT 3 of 50 sensitive mutations record an actor. 47 without one.
CONTEXT 23 of 454 entry points name a tenant on a failure path. 352 appear only here, 39 of them sensitive, in the JSON rather than the fix list.

What the score is made of
CHECKS
  error-classification  184 applicable, 106 pass,   0 sole, global without it 12
  auth-boundary          62 applicable,  57 pass,   0 sole, global without it 16
  auth-scope             19 applicable,  17 pass,   0 sole, global without it 19
  request-context       454 applicable,  23 pass, 248 sole, global without it 65
  audit-trail            50 applicable,   3 pass,   0 sole, not in the score

The score and findings here are report-only and never gate the merge. Separately, a required test suite keeps this tool's symbol and route lists in sync with the code they name, and can fail a pull request that renames or removes a symbol they reference, or that adds the first route with a segment they anticipate. Each failure names the list to edit. The rules and their reasons: internal-packages/observability-map/README.md.

coderabbitai[bot]

This comment was marked as resolved.

…ssion

Ergonomic accessors so a chat.agent run can open a named side channel on its
own Session without threading the session id: chat.channel(name) is a shortcut
for chat.session().channel(name).
@pkg-pr-new

pkg-pr-new Bot commented Aug 28, 2026

Copy link
Copy Markdown

Open in StackBlitz

@trigger.dev/build

npm i https://pkg.pr.new/@trigger.dev/build@f993d00

trigger.dev

npm i https://pkg.pr.new/trigger.dev@f993d00

@trigger.dev/core

npm i https://pkg.pr.new/@trigger.dev/core@f993d00

@trigger.dev/python

npm i https://pkg.pr.new/@trigger.dev/python@f993d00

@trigger.dev/react-hooks

npm i https://pkg.pr.new/@trigger.dev/react-hooks@f993d00

@trigger.dev/redis-worker

npm i https://pkg.pr.new/@trigger.dev/redis-worker@f993d00

@trigger.dev/rsc

npm i https://pkg.pr.new/@trigger.dev/rsc@f993d00

@trigger.dev/schema-to-json

npm i https://pkg.pr.new/@trigger.dev/schema-to-json@f993d00

@trigger.dev/sdk

npm i https://pkg.pr.new/@trigger.dev/sdk@f993d00

commit: f993d00

Move the channel-definition helper off @trigger.dev/core's public surface onto
the sessions namespace as sessions.defineChannel (the channel types stay in
core, so react-hooks can infer them). Make the channel classes generic on their
record type so session.channel(def) / chat.channel(def) type .out.append and
.in on/once/peek/send from the definition; a bare name string still works,
typed unknown.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/trigger-sdk/src/v3/sessions.ts (1)

518-520: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve the default-channel span entity format.

At Line 520, an undefined channel produces sessionId::out, not the documented sessionId.out fallback. Lines 782-784 have the same empty channel component for SessionInputChannel.once(). This changes entity grouping for default-channel traces. Use separate named-channel and default-channel formats.

Proposed fix
-        [SemanticInternalAttributes.ENTITY_ID]: `${this.sessionId}:${this.channel ?? ""}:out`,
+        [SemanticInternalAttributes.ENTITY_ID]: this.channel
+          ? `${this.sessionId}:${this.channel}:out`
+          : `${this.sessionId}.out`,
...
-                    [SemanticInternalAttributes.ENTITY_ID]: `${runId}:${this.sessionId}:${this.channel ?? ""}:in`,
+                    [SemanticInternalAttributes.ENTITY_ID]: this.channel
+                      ? `${runId}:${this.sessionId}:${this.channel}:in`
+                      : `${runId}:${this.sessionId}:in`,

Also applies to: 781-784


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6addee6f-4211-4b7b-8179-4980949de498

📥 Commits

Reviewing files that changed from the base of the PR and between 5b1f61b and 436e455.

📒 Files selected for processing (4)
  • .changeset/session-side-channels.md
  • packages/core/src/v3/sessionStreams/channels.ts
  • packages/trigger-sdk/src/v3/ai.ts
  • packages/trigger-sdk/src/v3/sessions.ts
💤 Files with no reviewable changes (1)
  • packages/core/src/v3/sessionStreams/channels.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • .changeset/session-side-channels.md

Included review availability: Your plan provides up to 10 included reviews per hour; 5 remain after this review.

📜 Review details
⏰ Context from checks skipped due to timeout. (50)
  • GitHub Check: report
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (22, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (4, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (2, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (12, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (10, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (11, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (20, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (14, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (18, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (21, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (3, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (13, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (19, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (24, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (8, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (17, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (23, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (1, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (6, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (9, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (15, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (16, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (7, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (5, 24)
  • GitHub Check: sdk-compat / Node.js 22.23 (warp-ubuntu-latest-x64-4x)
  • GitHub Check: sdk-compat / Deno Runtime
  • GitHub Check: e2e / 🧪 CLI v3 tests (warp-windows-latest-x64-8x - npm)
  • GitHub Check: sdk-compat / Node.js 26.4 (warp-ubuntu-latest-x64-4x)
  • GitHub Check: sdk-compat / Node.js 24.18 (warp-ubuntu-latest-x64-4x)
  • GitHub Check: e2e / 🧪 CLI v3 tests (warp-ubuntu-latest-x64-4x - npm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (warp-windows-latest-x64-8x - pnpm)
  • GitHub Check: sdk-compat / Bun Runtime
  • GitHub Check: sdk-compat / Node.js 20.20 (warp-ubuntu-latest-x64-4x)
  • GitHub Check: e2e / 🧪 CLI v3 tests (warp-ubuntu-latest-x64-4x - pnpm)
  • GitHub Check: packages / 🧪 Unit Tests: Packages (1, 3)
  • GitHub Check: e2e-webapp / 🧪 E2E Tests: Webapp (1, 2)
  • GitHub Check: e2e-webapp / 🧪 E2E Tests: Webapp (2, 2)
  • GitHub Check: obsmap / 🧪 Unit Tests: Observability Map
  • GitHub Check: sdk-compat / Cloudflare Workers
  • GitHub Check: internal / 🧪 Unit Tests: Internal
  • GitHub Check: fk-cascade-guard / fk-cascade-guard
  • GitHub Check: runops-guard / runops-guard
  • GitHub Check: packages / 🧪 Unit Tests: Packages (3, 3)
  • GitHub Check: packages / 🧪 Unit Tests: Packages (2, 3)
  • GitHub Check: typecheck / typecheck
  • GitHub Check: code-quality / code-quality
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: 🛡️ E2E Auth Tests (full)
  • GitHub Check: Build and publish previews
🧰 Additional context used
📓 Path-based instructions (8)
Always import from `@trigger.dev/sdk`. Never use `@trigger.dev/sdk/v3` or deprecated `client.defineJob`.

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • packages/trigger-sdk/src/v3/ai.ts
  • packages/trigger-sdk/src/v3/sessions.ts
**Prefer static imports over dynamic imports.** Only use dynamic `import()` when:

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • packages/trigger-sdk/src/v3/ai.ts
  • packages/trigger-sdk/src/v3/sessions.ts
Add crumbs as you write code — not just when debugging. Mark lines with

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • packages/trigger-sdk/src/v3/ai.ts
  • packages/trigger-sdk/src/v3/sessions.ts
Always import from `@trigger.dev/sdk`. Never use `@trigger.dev/sdk/v3` (deprecated path alias)

📄 CodeRabbit inference engine (packages/trigger-sdk/CLAUDE.md)

Files:

  • packages/trigger-sdk/src/v3/ai.ts
  • packages/trigger-sdk/src/v3/sessions.ts
In the Trigger.dev SDK (packages/trigger-sdk), prefer isomorphic code like fetch and ReadableStream instead of Node.js-specific code

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Files:

  • packages/trigger-sdk/src/v3/ai.ts
  • packages/trigger-sdk/src/v3/sessions.ts
Use function declarations instead of default exports

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Files:

  • packages/trigger-sdk/src/v3/ai.ts
  • packages/trigger-sdk/src/v3/sessions.ts
Use types over interfaces for TypeScript

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Files:

  • packages/trigger-sdk/src/v3/ai.ts
  • packages/trigger-sdk/src/v3/sessions.ts
When creating or editing OTEL metrics (counters, histograms, gauges), ensure metric attributes have low cardinality by using only enums, booleans, bounded error codes, or bounded shard IDs

📄 CodeRabbit inference engine (.cursor/rules/otel-metrics.mdc)

Files:

  • packages/trigger-sdk/src/v3/ai.ts
  • packages/trigger-sdk/src/v3/sessions.ts
🔇 Additional comments (2)
packages/trigger-sdk/src/v3/sessions.ts (1)

24-29: LGTM!

Also applies to: 67-67, 262-315, 325-330, 335-348, 359-363, 384-415, 491-496, 572-574, 662-669, 683-683, 694-735, 748-753, 762-766, 807-808, 822-822, 846-846, 904-904, 974-974

packages/trigger-sdk/src/v3/ai.ts (1)

110-120: LGTM!

Also applies to: 11858-11869

- useSessionStreamChannel: only clear the abort-controller ref when it still
  points at the current request, so a finishing older request can't strand a
  newer subscription (CodeRabbit).
- Drop the unenforced keepLastN retention option until producer-side trim lands.
- Update the initializeSessionStream call-arity assertions in sessions.test.ts.
- Formatting.
… parity

Matches useSessionStream, which accepts an undefined id so the hook can render
before the session id resolves. The subscription already gates on a set id.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/trigger-sdk/src/v3/sessions.ts (1)

518-518: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve the default session span entity IDs.

When this.channel is undefined, ${this.channel ?? ""} inserts an empty segment. The default IDs become sessionId::out and runId:sessionId::in instead of the documented default formats. This can fragment existing telemetry entity grouping.

Build the channel segment only when a named channel is present.

Proposed fix
-        [SemanticInternalAttributes.ENTITY_ID]: `${this.sessionId}:${this.channel ?? ""}:out`,
+        [SemanticInternalAttributes.ENTITY_ID]: this.channel
+          ? `${this.sessionId}:${this.channel}:out`
+          : `${this.sessionId}.out`,
...
-                    `${runId}:${this.sessionId}:${this.channel ?? ""}:in`,
+                    this.channel
+                      ? `${runId}:${this.sessionId}:${this.channel}:in`
+                      : `${runId}:${this.sessionId}:in`,
🧹 Nitpick comments (1)
packages/react-hooks/src/hooks/useSessionStreamChannel.ts (1)

209-210: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a crumb marker to the new request lifecycle path.

The changed AbortController lifecycle code has no // @crumbs`` marker or @crumbs region. Add a marker around this block before merge.

Proposed fix
   const triggerRequest = useCallback(async () => {
+    // `@crumbs`
     let abortController: AbortController | null = null;

As per coding guidelines, **/*: Add crumbs as you write code — not just when debugging. Mark lines with // @Crumbs or wrap blocks in `// `#region` `@crumbs.

Also applies to: 244-246

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2e4d4434-494a-443a-9a82-a35a16273a36

📥 Commits

Reviewing files that changed from the base of the PR and between 436e455 and 225edfe.

📒 Files selected for processing (7)
  • apps/webapp/app/routes/realtime.v1.sessions.$session.channels.$channel.$io.append.ts
  • apps/webapp/app/routes/realtime.v1.sessions.$session.channels.$channel.$io.ts
  • packages/core/src/v3/sessionStreams/channels.ts
  • packages/react-hooks/src/hooks/useSessionStreamChannel.ts
  • packages/trigger-sdk/src/v3/ai.ts
  • packages/trigger-sdk/src/v3/sessions.test.ts
  • packages/trigger-sdk/src/v3/sessions.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/trigger-sdk/src/v3/ai.ts
  • apps/webapp/app/routes/realtime.v1.sessions.$session.channels.$channel.$io.ts
  • packages/core/src/v3/sessionStreams/channels.ts
  • apps/webapp/app/routes/realtime.v1.sessions.$session.channels.$channel.$io.append.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 5 remain after this review.

📜 Review details
⏰ Context from checks skipped due to timeout. (43)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (24, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (23, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (21, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (20, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (12, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (22, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (19, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (14, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (16, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (13, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (18, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (2, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (11, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (1, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (17, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (7, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (8, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (15, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (9, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (10, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (3, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (6, 24)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (5, 24)
  • GitHub Check: sdk-compat / Node.js 20.20 (warp-ubuntu-latest-x64-4x)
  • GitHub Check: sdk-compat / Node.js 24.18 (warp-ubuntu-latest-x64-4x)
  • GitHub Check: e2e / 🧪 CLI v3 tests (warp-ubuntu-latest-x64-4x - pnpm)
  • GitHub Check: sdk-compat / Cloudflare Workers
  • GitHub Check: sdk-compat / Bun Runtime
  • GitHub Check: sdk-compat / Node.js 26.4 (warp-ubuntu-latest-x64-4x)
  • GitHub Check: typecheck / typecheck
  • GitHub Check: e2e / 🧪 CLI v3 tests (warp-windows-latest-x64-8x - npm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (warp-windows-latest-x64-8x - pnpm)
  • GitHub Check: internal / 🧪 Unit Tests: Internal
  • GitHub Check: sdk-compat / Deno Runtime
  • GitHub Check: fk-cascade-guard / fk-cascade-guard
  • GitHub Check: packages / 🧪 Unit Tests: Packages (1, 3)
  • GitHub Check: e2e-webapp / 🧪 E2E Tests: Webapp (2, 2)
  • GitHub Check: runops-guard / runops-guard
  • GitHub Check: packages / 🧪 Unit Tests: Packages (3, 3)
  • GitHub Check: packages / 🧪 Unit Tests: Packages (2, 3)
  • GitHub Check: e2e-webapp / 🧪 E2E Tests: Webapp (1, 2)
  • GitHub Check: report
  • GitHub Check: 🛡️ E2E Auth Tests (full)
🧰 Additional context used
📓 Path-based instructions (10)
Always import from `@trigger.dev/sdk`. Never use `@trigger.dev/sdk/v3` or deprecated `client.defineJob`.

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • packages/trigger-sdk/src/v3/sessions.test.ts
  • packages/trigger-sdk/src/v3/sessions.ts
We use vitest exclusively. **Never mock anything** - use testcontainers instead.

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • packages/trigger-sdk/src/v3/sessions.test.ts
**Prefer static imports over dynamic imports.** Only use dynamic `import()` when:

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • packages/trigger-sdk/src/v3/sessions.test.ts
  • packages/react-hooks/src/hooks/useSessionStreamChannel.ts
  • packages/trigger-sdk/src/v3/sessions.ts
Add crumbs as you write code — not just when debugging. Mark lines with

📄 CodeRabbit inference engine (AGENTS.md)

Files:

  • packages/trigger-sdk/src/v3/sessions.test.ts
  • packages/react-hooks/src/hooks/useSessionStreamChannel.ts
  • packages/trigger-sdk/src/v3/sessions.ts
Always import from `@trigger.dev/sdk`. Never use `@trigger.dev/sdk/v3` (deprecated path alias)

📄 CodeRabbit inference engine (packages/trigger-sdk/CLAUDE.md)

Files:

  • packages/trigger-sdk/src/v3/sessions.test.ts
  • packages/trigger-sdk/src/v3/sessions.ts
In the Trigger.dev SDK (packages/trigger-sdk), prefer isomorphic code like fetch and ReadableStream instead of Node.js-specific code

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Files:

  • packages/trigger-sdk/src/v3/sessions.test.ts
  • packages/trigger-sdk/src/v3/sessions.ts
Use vitest for all tests in the Trigger.dev repository

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Files:

  • packages/trigger-sdk/src/v3/sessions.test.ts
Use function declarations instead of default exports

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Files:

  • packages/trigger-sdk/src/v3/sessions.test.ts
  • packages/react-hooks/src/hooks/useSessionStreamChannel.ts
  • packages/trigger-sdk/src/v3/sessions.ts
Use types over interfaces for TypeScript

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Files:

  • packages/trigger-sdk/src/v3/sessions.test.ts
  • packages/react-hooks/src/hooks/useSessionStreamChannel.ts
  • packages/trigger-sdk/src/v3/sessions.ts
When creating or editing OTEL metrics (counters, histograms, gauges), ensure metric attributes have low cardinality by using only enums, booleans, bounded error codes, or bounded shard IDs

📄 CodeRabbit inference engine (.cursor/rules/otel-metrics.mdc)

Files:

  • packages/trigger-sdk/src/v3/sessions.test.ts
  • packages/react-hooks/src/hooks/useSessionStreamChannel.ts
  • packages/trigger-sdk/src/v3/sessions.ts
🔇 Additional comments (2)
packages/react-hooks/src/hooks/useSessionStreamChannel.ts (1)

149-151: LGTM!

packages/trigger-sdk/src/v3/sessions.test.ts (1)

99-99: LGTM!

Also applies to: 153-154

Add a Side channels concept page (define, produce on .out, observe .in without
waking a run, read in React with useSessionStreamChannel, retention, the 1 MiB
pointer note, auth) and a Named side channels section on the useSessionStream
page. Register the new page in the nav.
Make the non-chat.agent path first-class: channels are a Session primitive, so
document producing on a channel from a task-bound Session run and from a backend
holding the secret key, not just from chat.channel().
Merge the channel-type import into the existing @trigger.dev/core/v3 import in
ai.ts, and drop the unused isValidSessionChannelName helper (routes validate via
the exported regex directly).
Clicking a session channel span now renders the channel's live records
(reusing the realtime stream viewer) instead of generic properties, and
session spans show the session icon. Covers named channels and the
reserved transcript pair.
@ericallam
ericallam marked this pull request as ready for review August 28, 2026 20:36
devin-ai-integration[bot]

This comment was marked as resolved.

The .in side is only ever created via the append route, never
PUT-initialized, so it never received the bounded-age plus
delete-on-empty retention channels advertise. Ensure it best-effort on
append (cached per stream).

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

This report is out of date. Scroll down for Devin Review's latest report on this PR.

Devin Review found 2 new potential issues.

Devin Review

Comment on lines +300 to +307
const stream = this.toSessionStreamName(friendlyId, io, channel);
if (this.#retentionEnsured.has(stream)) return;

const created = await this.#s2CreateStreamWithConfig(stream, retention);
if (!created) {
await this.#s2ReconfigureStream(stream, retention);
}
this.#retentionEnsured.add(stream);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Channel retention re-applied and reset on every write

A fresh stream instance is built per request, so #retentionEnsured never dedupes across requests. Every named-channel .in send re-runs create-then-reconfigure against the stream store, and ensureSessionChannelRetention reconfigures unconditionally, so a caller that opens the channel with no custom retention overwrites retention a prior producer set.

Prompt for agents
ensureSessionChannelRetention in apps/webapp/app/services/realtime/s2realtimeStreams.server.ts relies on the instance-level #retentionEnsured Set to run at most once per channel per process, but getRealtimeStreamInstance (v1StreamsGlobal.server.ts) returns a new S2RealtimeStreams for every request, so the Set is always empty and the guard never fires. Consequently every append (via the channel append route, which passes DEFAULT_SESSION_CHANNEL_RETENTION unconditionally) and every initialize issues a create-stream POST (which 409s once the stream exists) followed by a reconfigure PATCH. Two problems: (1) redundant control-plane calls on every write instead of once per channel; (2) because the reconfigure runs unconditionally with whatever retention the current caller passed, a producer opening channel(name) without a custom retention (or the append route's hardcoded default) overwrites a retention previously set by channel(name,{retention}). Consider moving the dedupe cache to a process-level/shared store (or the passed-in UnkeyCache), and only applying retention on stream creation (skip the reconfigure fallback, or reconfigure only when the stream was just created / caller explicitly supplied retention) so an omitted retention never clobbers an existing custom config.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +45 to +48
authorization: {
action: "write",
resource: (params) => anyResource(sessionChannelResources(params.channel, [params.session])),
},

@devin-ai-integration devin-ai-integration Bot Aug 28, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Channel initialize authorizes only the URL session form

The channel initialize action authorizes against only params.session, whereas resolveSessionWithWriterFallback in the append/records routes broadens the id set to friendlyId and externalId. A public token scoped to one session form cannot authorize a PUT addressed by the other form. Likely unexercised because the producer PUTs with the secret key, but worth confirming no public-token path initializes a channel.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Channel streams are created on demand on first write and inherit the
org basin's retention (bounded age plus delete-on-empty from its default
stream config), the same as the reserved chat streams. Drop the
per-stream create-with-config / reconfigure calls, the per-process
cache, and the channel retention option on the SDK, which added
control-plane latency to the write path for no gain.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

Devin Review

Comment on lines +100 to +139
let appendSeq: number | undefined;
if (wonClaim) {
const [appendError, seq] = await tryCatch(
realtimeStream.appendPartToSessionStream(
part,
partId,
addressingKey,
params.io,
params.channel
)
);
appendSeq = seq ?? undefined;

if (appendError) {
if (clientPartId) {
await releaseSessionStreamPart(
authentication.environment.id,
claimKey,
params.io,
clientPartId
);
}
if (appendError instanceof ServiceValidationError) {
return json(
{ ok: false, error: appendError.message },
{ status: appendError.status ?? 422 }
);
}
logger.error("Failed to append to session channel stream", {
sessionId: session.id,
io: params.io,
channel: params.channel,
error: appendError,
});
return json(
{ ok: false, error: "Something went wrong, please try again." },
{ status: 500 }
);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Channel .in append skips run-wake and input sanitization

The channel append handler omits the ensureRunForSession and drainSessionStreamWaitpoints steps of the reserved .in append (the intended no-wake design) and also skips stripClientWebhookActionSource. Side-channel .in records therefore bypass the webhook-source sanitization applied to reserved .in.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant