Skip to content

fix(trigger-sdk): resubscribe to session.out across empty long-poll windows (#5056) - #5063

Closed
ghazy2001 wants to merge 1 commit into
triggerdotdev:mainfrom
ghazy2001:fix/chat-head-start-empty-long-poll-resubscribe
Closed

ghazy2001 wants to merge 1 commit into
triggerdotdev:mainfrom
ghazy2001:fix/chat-head-start-empty-long-poll-resubscribe

Conversation

@ghazy2001

Copy link
Copy Markdown

Closes #5056

✅ Checklist

  • I have followed every step in the contributing guide
  • The PR title follows the convention.
  • I ran and tested the code works

Description

In chat.headStart, stitchHandoverStream reads the agent's session.out stream to relay chunks to the browser until turn-complete.

When a long-running tool or agent step executes for longer than the S2 long-poll deadline (~60s) without emitting chunks, the SSE connection ends with an empty window before turn-complete. Previously, this caused the relay to close prematurely, writing only trigger:session-state and cutting off the first turn while the agent run was still in progress. As a result, the browser transport cleared isStreaming, giving the user a premature completion while the agent was still running.

Solution

This PR loops the session.out subscription in stitchHandoverStream until turn-complete or request abort:

  • Resumes each subscription window from latestEventId ?? customerLastEventId.
  • Applies exponential jittered backoff via waitBeforeEofResubscribe between empty windows.
  • Resets consecutive empty windows count when records arrive (emptyWindows = windowHadRecords ? 0 : emptyWindows + 1).
  • Binds consecutive empty reconnects to MAX_EOF_RESUBSCRIBES (5) to prevent infinite loops if a turn is permanently stalled.

Testing

  1. Verified existing unit tests pass (chat-server.test.ts, chat-client.test.ts).
  2. Added unit test resubscribes to session.out when an empty long-poll window closes mid-turn (#5056) to verify that an initial empty window does not cut off the response, and subsequent chunks/turn-complete are relayed properly.
  3. Added unit test errors the stream once reconnect budget is exhausted on consecutive empty windows to verify that MAX_EOF_RESUBSCRIBES errors the stream cleanly when the turn fails to complete.
  4. Ran oxlint, oxfmt, and typechecking (tsc --noEmit).

Changelog

  • fix(trigger-sdk): Loop session.out read in chat.headStart across empty long-poll windows until turn-complete or abort.

…indows (triggerdotdev#5056)

In chat.headStart, stitchHandoverStream reads the agent's session.out stream to relay chunks to the browser until turn-complete. When a long-running tool or agent step executes for longer than the S2 long-poll deadline (~60s) without emitting chunks, the SSE connection ends with an empty window before turn-complete. Previously, this caused the relay to close prematurely, writing only trigger:session-state and cutting off the first turn while the agent run was still in progress.

This commit loops the session.out subscription until turn-complete or request abort:
- Resumes each subscription window from latestEventId ?? customerLastEventId.
- Applies exponential jittered backoff via waitBeforeEofResubscribe between empty windows.
- Resets consecutive empty windows count when chunks are received.
- Binds consecutive empty reconnects to MAX_EOF_RESUBSCRIBES to prevent infinite spins if a turn hangs permanently.
- Adds comprehensive unit tests covering empty window resubscribe and reconnect budget exhaustion.

Fixes triggerdotdev#5056
@changeset-bot

changeset-bot Bot commented Oct 25, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bf3834a

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

This PR includes changesets to release 30 packages
Name Type
@trigger.dev/sdk Patch
@trigger.dev/linear Patch
@trigger.dev/python Patch
@trigger.dev/slack Patch
@internal/dashboard-agent Patch
@trigger.dev/build Patch
trigger.dev Patch
@trigger.dev/core Patch
@trigger.dev/react-hooks Patch
@trigger.dev/redis-worker Patch
@trigger.dev/rsc Patch
@trigger.dev/schema-to-json Patch
@trigger.dev/billing Patch
@trigger.dev/database Patch
@trigger.dev/otlp-importer Patch
@trigger.dev/rbac Patch
@trigger.dev/sso Patch
@internal/clickhouse Patch
@internal/llm-model-catalog Patch
@internal/metrics-pipeline 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/testcontainers Patch
@internal/cache 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

@github-actions

Copy link
Copy Markdown
Contributor

Hi @ghazy2001, thanks for your interest in contributing!

This project requires that pull request authors are vouched, and you are not in the list of vouched users.

This PR will be closed automatically. See https://github.com/triggerdotdev/trigger.dev/blob/main/CONTRIBUTING.md for more details.

@github-actions github-actions Bot closed this Oct 25, 2026
@coderabbitai

coderabbitai Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Caution

Review failed

The pull request is closed.

Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@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 2 potential issues.

Devin Review

let outCalls = 0;
const requestedLastEventIds: Array<string | undefined> = [];

global.fetch = vi.fn().mockImplementation(async (url: string | URL, init?: RequestInit) => {

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.

🟡 New reconnect tests use prohibited mocks

Both new reconnect tests use vi.fn().mockImplementation to replace fetch. The repository's testing rules prohibit mocks in tests.

Learn more

The repository's testing rules prohibit mocks and require tests to use real services, with testcontainers where appropriate. Both new reconnect tests replace fetch with vi.fn().mockImplementation, so they violate that rule. The surrounding file already uses mocks, but the new cases add distinct violations.

Example: The empty-window test substitutes synthetic SSE responses for real session stream requests; the budget test substitutes five empty responses.

Recommended fix: Move these cases to an integration setup that exercises session streams without mocking fetch, while preserving assertions about resumed cursors and stream completion.

Devin Review


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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

In chat-server.test.ts, the entire test suite is structured as isolated unit tests for the route handler and SSE bridge, mocking global.fetch across all tests (lines 264, 440, 517, 622). The two new test cases follow this exact local unit-testing convention to verify backoff timing and stream error boundaries deterministically without network latency.

"@trigger.dev/sdk": patch
---

Fix `chat.headStart` relay ending the first turn after an empty `session.out` long-poll window. Resubscribes across empty windows until `turn-complete` with exponential jittered backoff.

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.

🟡 Release note exposes internal protocol details

The new changeset describes session.out long polls and backoff instead of the user's experience. Release-note rules require a plain user-facing sentence without infrastructure details.

Suggested change
Fix `chat.headStart` relay ending the first turn after an empty `session.out` long-poll window. Resubscribes across empty windows until `turn-complete` with exponential jittered backoff.
First chat responses now keep streaming while an agent finishes a long-running step.

Devin Review


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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Updated to the suggested user-facing release note phrasing in commit 438f671. Thanks!

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.

chat.headStart relay ends the first turn after one empty session.out long-poll window (SDK 4.7.7)

1 participant