Skip to content

fix(sdk): preserve partial assistant message on chat stream failure#4348

Open
matt-aitken wants to merge 5 commits into
mainfrom
fix/chat-preserve-partial-on-stream-failure
Open

fix(sdk): preserve partial assistant message on chat stream failure#4348
matt-aitken wants to merge 5 commits into
mainfrom
fix/chat-preserve-partial-on-stream-failure

Conversation

@matt-aitken

Copy link
Copy Markdown
Member

Summary

When a chat.agent (or chat.createSession) turn's model stream fails mid-response (e.g. a transport timeout like UND_ERR_BODY_TIMEOUT), the assistant output that already streamed was dropped. onTurnComplete fired with responseMessage: undefined, and the manual loop's turn.complete() rethrew without keeping the partial. Apps that register hydrateMessages are hit hardest: boot-time tail-replay recovery is off by design, so the partial couldn't be reclaimed later either.

This preserves the partial while still reporting the turn as errored, so persistence keeps the streamed-so-far output.

Fix

Follow-up to #4304, which introduced chat.pipeAndCapture (buffer chunks, classify instead of throw, reconstruct the partial from buffered chunks). That fix landed for custom agents but not the two other loops:

  • Managed loop (chat.agent): buffer chunks as they flow to the pipe, and on the error path reconstruct the partial (falling back to the onFinish message if it fired). It's now passed to onTurnComplete via responseMessage, rawResponseMessage, uiMessages, and newUIMessages. Empty for non-stream failures, preserving prior behavior.
  • Manual loop (chat.createSession / turn.complete()): pipeAndCapture already reconstructs the partial, but it was discarded on the error branch. Now it's accumulated (so turn.uiMessages reflects it and the caller can persist after catching) before turn.complete() rethrows.

onBeforeTurnComplete is intentionally still skipped on the error path: it's the "stream still open" hook that hands out a writer, which is meaningless once the source stream has broken.

Tests

New chat-agent-source-stream-error.test.ts drives a source that streams a partial then errors, for both loops. Both cases fail against the unpatched code and pass with the fix. Full @trigger.dev/sdk suite passes (317 tests).

When a chat turn's model stream fails mid-response (e.g. a transport
timeout), the streamed-so-far output is no longer dropped. chat.agent
passes the recovered partial to onTurnComplete, and chat.createSession
accumulates it before turn.complete() rethrows, so it survives for
persistence even when hydrateMessages disables boot-time replay recovery.
The turn is still reported as errored.
@changeset-bot

changeset-bot Bot commented Jul 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a225c36

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

This PR includes changesets to release 26 packages
Name Type
@trigger.dev/sdk Patch
@trigger.dev/python Patch
@internal/dashboard-agent Patch
@internal/sdk-compat-tests Patch
@trigger.dev/build 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/database Patch
@trigger.dev/otlp-importer Patch
@trigger.dev/rbac Patch
@trigger.dev/sso Patch
trigger.dev Patch
@internal/cache Patch
@internal/clickhouse Patch
@internal/llm-model-catalog Patch
@internal/redis Patch
@internal/replication Patch
@internal/run-engine Patch
@internal/run-store Patch
@internal/schedule-engine Patch
@internal/testcontainers Patch
@internal/tracing Patch
@internal/tsql 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 Jul 23, 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: a6aa7c5d-11da-4f9e-b18b-4290750a3d23

📥 Commits

Reviewing files that changed from the base of the PR and between b6ea02f and a225c36.

📒 Files selected for processing (2)
  • packages/trigger-sdk/src/v3/ai.ts
  • packages/trigger-sdk/test/chat-agent-source-stream-error.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/trigger-sdk/test/chat-agent-source-stream-error.test.ts
  • packages/trigger-sdk/src/v3/ai.ts
📜 Recent review details
⏰ Context from checks skipped due to timeout. (29)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (10, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (11, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (3, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (5, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (4, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (7, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (2, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (8, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (9, 12)
  • GitHub Check: sdk-compat / Node.js 26.4 (warp-ubuntu-latest-x64-4x)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (12, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (1, 12)
  • GitHub Check: webapp / 🧪 Unit Tests: Webapp (6, 12)
  • GitHub Check: sdk-compat / Node.js 22.23 (warp-ubuntu-latest-x64-4x)
  • GitHub Check: sdk-compat / Node.js 24.18 (warp-ubuntu-latest-x64-4x)
  • GitHub Check: sdk-compat / Node.js 20.20 (warp-ubuntu-latest-x64-4x)
  • GitHub Check: packages / 🧪 Unit Tests: Packages (1, 3)
  • GitHub Check: sdk-compat / Cloudflare Workers
  • GitHub Check: sdk-compat / Bun Runtime
  • GitHub Check: packages / 🧪 Unit Tests: Packages (2, 3)
  • GitHub Check: internal / 🧪 Unit Tests: Internal
  • GitHub Check: sdk-compat / Deno Runtime
  • GitHub Check: packages / 🧪 Unit Tests: Packages (3, 3)
  • GitHub Check: e2e-webapp / 🧪 E2E Tests: Webapp
  • GitHub Check: runops-guard / runops-guard
  • GitHub Check: typecheck / typecheck
  • GitHub Check: code-quality / code-quality
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Build and publish previews

Walkthrough

The SDK buffers streamed chunks and captures partial assistant responses during chat turns. When a source stream fails, chat.agent reconstructs or reuses the partial response and includes it in completion data while reporting an error. chat.createSession accumulates the partial assistant message before rethrowing. Tests cover managed-loop and session behavior, and a patch changeset documents the update.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: preserving partial assistant messages on chat stream failure.
Description check ✅ Passed The description covers summary, fix details, and testing, and mostly matches the template despite missing issue link, checklist, and screenshots.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/chat-preserve-partial-on-stream-failure

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[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

Commit the recovered partial to the canonical accumulator on the chat.agent
error path so the next turn and the reboot snapshot both carry it, matching
the success path. Fold queued response parts into the manual-loop error
partial too. Add a continuation regression test.
devin-ai-integration[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

Replace a same-id continuation partial in place instead of dropping it as a
dup, commit the errored user message unconditionally so it reaches the next
live turn, and push only the appended partial's model messages to preserve a
prior turn's compaction (reconvert only when replacing or folding).
@pkg-pr-new

pkg-pr-new Bot commented Jul 23, 2026

Copy link
Copy Markdown

Open in StackBlitz

@trigger.dev/build

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

trigger.dev

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

@trigger.dev/core

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

@trigger.dev/python

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

@trigger.dev/react-hooks

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

@trigger.dev/redis-worker

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

@trigger.dev/rsc

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

@trigger.dev/schema-to-json

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

@trigger.dev/sdk

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

commit: a225c36

…tep throws

The chat.agent try block spans the whole turn, so a throw from a post-response
step (a customer onBeforeTurnComplete/onTurnComplete hook, a late conversion)
lands in the error handler after the response was already committed. Track a
per-turn responseCommitted flag and keep capturedPartialResponse pointed at the
enriched committed message, so the error path reports it without re-recovering a
raw partial and clobbering the committed message, its queued data parts, or a
prior turn's compaction. Also use truthy id checks to match the success path.
@matt-aitken

Copy link
Copy Markdown
Member Author

Additional hardening from an adversarial self-review of the error path:

Fixed (real): the chat.agent try spans the whole turn, so a throw from a post-response step (a customer onBeforeTurnComplete/onTurnComplete hook, or a late toModelMessages) lands in the error handler after the response was already committed. The error path would then re-recover a raw partial and overwrite the committed (possibly enriched) message, dropping queued chat.response data parts and re-inflating a prior turn's compaction. Now a per-turn responseCommitted flag skips re-recovery in that case and capturedPartialResponse tracks the enriched committed message. Regression test added (fails without the fix).

Considered, no change needed:

  • Committing an incomplete-tool-call partial can't poison the next turn: toModelMessages uses ignoreIncompleteToolCalls: true, so dangling tool calls are dropped from the model messages. Incomplete parts are intentionally kept in the UI message (the point is to preserve what streamed), matching the success path which only runs cleanupAbortedParts on a user stop.
  • Per-turn chunk buffering retains chunk refs for the turn's duration; this mirrors the existing chat.pipeAndCapture buffering and is freed when the turn ends.

319 SDK tests pass, 0 typecheck errors.

devin-ai-integration[bot]

This comment was marked as resolved.

The error-path onTurnComplete added the partial to newUIMessages/uiMessages but
left newMessages (the model-message delta) empty, so apps persisting the model
delta missed the partial. Populate it from the same conversion.
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