feat(sdk,core,webapp,react-hooks): named side channels on a Session - #4815
feat(sdk,core,webapp,react-hooks): named side channels on a Session#4815ericallam wants to merge 17 commits into
Conversation
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 detectedLatest commit: f993d00 The changes in this PR will be included in the next version bump. This PR includes changesets to release 27 packages
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 |
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
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)
🧰 Additional context used📓 Path-based instructions (7)**Prefer static imports over dynamic imports.** Only use dynamic `import()` when:📄 CodeRabbit inference engine (AGENTS.md) Files:
Add crumbs as you write code — not just when debugging. Mark lines with📄 CodeRabbit inference engine (AGENTS.md) Files:
Main documentation config must be defined in `docs.json` which includes navigation structure, theme, and metadata📄 CodeRabbit inference engine (docs/CLAUDE.md) Files:
Use function declarations instead of default exports📄 CodeRabbit inference engine (.github/copilot-instructions.md) Files:
MDX documentation pages must include frontmatter with title (required), description (required), and sidebarTitle (optional) in YAML format📄 CodeRabbit inference engine (docs/CLAUDE.md) Files:
Use types over interfaces for TypeScript📄 CodeRabbit inference engine (.github/copilot-instructions.md) Files:
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:
🧠 Learnings (1)📚 Learning: 2026-04-30T20:30:29.458ZApplied to files:
🔇 Additional comments (4)
WalkthroughAdds named, durable, two-way Session side channels. The SDK exposes typed Merge Risk: 🟡 Moderate · up to 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)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation 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 checkExplanation 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 💡
🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
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. Comment |
Observability mapAs of 20/100 over 454 measured of 472 entry points (base 20, no change) What this PR changed
FIX FIRST
AUDIT 3 of 50 sensitive mutations record an actor. 47 without one. What the score is made ofThe 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. |
…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).
@trigger.dev/build
trigger.dev
@trigger.dev/core
@trigger.dev/python
@trigger.dev/react-hooks
@trigger.dev/redis-worker
@trigger.dev/rsc
@trigger.dev/schema-to-json
@trigger.dev/sdk
commit: |
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.
There was a problem hiding this comment.
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 winPreserve the default-channel span entity format.
At Line 520, an undefined channel produces
sessionId::out, not the documentedsessionId.outfallback. Lines 782-784 have the same empty channel component forSessionInputChannel.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
📒 Files selected for processing (4)
.changeset/session-side-channels.mdpackages/core/src/v3/sessionStreams/channels.tspackages/trigger-sdk/src/v3/ai.tspackages/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.tspackages/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.tspackages/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.tspackages/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.tspackages/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.tspackages/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.tspackages/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.tspackages/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.tspackages/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.
There was a problem hiding this comment.
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 winPreserve the default session span entity IDs.
When
this.channelis undefined,${this.channel ?? ""}inserts an empty segment. The default IDs becomesessionId::outandrunId:sessionId::ininstead 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 winAdd a crumb marker to the new request lifecycle path.
The changed
AbortControllerlifecycle code has no//@crumbs`` marker or@crumbsregion. 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//@Crumbsor 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
📒 Files selected for processing (7)
apps/webapp/app/routes/realtime.v1.sessions.$session.channels.$channel.$io.append.tsapps/webapp/app/routes/realtime.v1.sessions.$session.channels.$channel.$io.tspackages/core/src/v3/sessionStreams/channels.tspackages/react-hooks/src/hooks/useSessionStreamChannel.tspackages/trigger-sdk/src/v3/ai.tspackages/trigger-sdk/src/v3/sessions.test.tspackages/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.tspackages/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.tspackages/react-hooks/src/hooks/useSessionStreamChannel.tspackages/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.tspackages/react-hooks/src/hooks/useSessionStreamChannel.tspackages/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.tspackages/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.tspackages/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.tspackages/react-hooks/src/hooks/useSessionStreamChannel.tspackages/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.tspackages/react-hooks/src/hooks/useSessionStreamChannel.tspackages/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.tspackages/react-hooks/src/hooks/useSessionStreamChannel.tspackages/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.
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).
| 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); |
There was a problem hiding this comment.
🟡 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
| authorization: { | ||
| action: "write", | ||
| resource: (params) => anyResource(sessionChannelResources(params.channel, [params.session])), | ||
| }, |
There was a problem hiding this comment.
🔍 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.
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.
| 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 } | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
🔍 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
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/.outpair (the chat transcript). This lets a session hold any number of named channels alongside it, each its own.in/.outpair, 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:
.indoes 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.agentto the frontend on a channel separate from the chat). It builds directly on the start-from-latest /useSessionStreamsubscribe seam from #4811.Usage
Declare the channel's record types once and infer them on both sides:
Open a channel from a session handle (
sessions.open(id)returns one for a known session id). Writing its.outis durable, cross-run, and wakes nothing; a run observes its.inby tailing, without suspending:Passing the definition types
.out.append/.in.onon the producer side; a bare name string also works, with records typedunknown.An external client writes the
.inwithout waking a run, and reads the.outfrom React:session.channel(name)returns the same{ in, out }handle shape as the reserved pair, soappend/pipe/writer/read/writeControl/trimToon.outandsend/on/once/peekon.inall carry over. Passing a name other than the declared one is a type error; a bare-string call without the generic stays valid withrecordstypedunknown.With
chat.agentThis is the motivating case: a
chat.agentanswers 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:chat.channel(name)is a shortcut forchat.session().channel(name);chat.session()returns the current run's fullSessionHandleif you need it.The frontend renders the transcript with
useChatas before, and the screenshots withuseSessionStreamChannel<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-partsessions/{id}/{io}name for back-compat, and thechannels/segment means a user channel namedin/outcan 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 therealtime.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, sofrom: "latest"andmaxRecordscompose unchanged.No-wake. The reserved
.inappend route ensures a run and drains waitpoints so a chat turn advances. The channel.inappend 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.inis 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.outrequires secret-key auth so a browser can't forge frames;.inis 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
channelandioattributes plus an accessory chip, so the span inspector shows which channel and direction a write was on.Verification
(session, io)never cross buffers, and a named channel is isolated from the reserved pair..outrecord is readable back after the triggering run has gone terminal (durable, cross-run);.inappend creates no run, while a reserved.inappend 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;Notes
Screenshots