Skip to content

fix(ai-client): keep the replay's answer when a rejoin starts with reasoning - #1942

Open
aayushbtw wants to merge 5 commits into
TanStack:mainfrom
aayushbtw:fix/rejoin-drops-replay-message
Open

aayushbtw wants to merge 5 commits into
TanStack:mainfrom
aayushbtw:fix/rejoin-drops-replay-message

Conversation

@aayushbtw

@aayushbtw aayushbtw commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Rejoining a run lost the answer when the replay started with reasoning. This happens for readable and for encrypted-only reasoning, because chat() puts metadata on every adapter event. The client now drops only the assistant message that hydrate returned, not the message that the replay already started.

🎯 Changes

  • resumeInFlightRun reads the id of the trailing hydrated assistant message before the replay starts. At the first rebuild trigger, it drops only the message with that id.
  • dropTrailingInFlightAssistant() is now dropHydratedAssistant(id).
  • REJOIN_REBUILD_TRIGGERS does not change.
  • Unit tests: the reasoning rejoin cases now send the reasoning start events with call metadata, as chat() does.
  • E2E: join-run-client-tool.spec.ts has a reload in the middle of a run that reasons before it answers.
  • Docs: no change. docs/persistence/client-persistence.md already says that a replay rebuilds a run that starts with reasoning. This fix makes that statement true again.

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested code changes locally with pnpm run test:pr, or these tests do not apply to this pull request.
  • I fully understand the code in this pull request, including any code generated with AI assistance.
  • Docs: I updated docs/ for this change, or this change is not user-facing.
  • Changeset: I added a changeset (pnpm changeset), or this PR does not change a published package.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Root cause

Issue. A reload during a run that starts with reasoning loses the answer. If hydrate ends on a user turn, no assistant message is left. If hydrate ends on an in-flight partial, the stale partial stays and the answer is lost.

Cause. chat() stamps call metadata on every adapter event (withCallMetadata). Since #1789, a REASONING_MESSAGE_START with metadata creates the assistant message. That event comes before every rebuild trigger. At the first trigger, dropTrailingInFlightAssistant() removed the last assistant message, which was now the replay's own message. #1357 fixed #1422 before #1789 landed, so #1789 opened #1422 again for real streams. Encrypted-only reasoning is one case of this.

Fix. Before the replay, the client reads the id of the trailing hydrated assistant message. At the first trigger, it drops only that id. The client does not touch the messages that the replay creates.

Side effect. On main, an ACTIVITY_SNAPSHOT before the first trigger made the activity the last message. Then the drop removed nothing, and the stale partial stayed next to the new answer. The drop by id removes the partial. A local run showed this (main: [ 'u1', 'partial', 'act-1', 'reply' ], branch: no partial). This PR does not add a test for it.

Possible alternatives

  • Add REASONING_START and REASONING_MESSAGE_START to REJOIN_REBUILD_TRIGGERS. That moves the drop before this event. Any other event that creates a message before a trigger still has the same fault.
  • Do not create a message on a reasoning start with metadata. That reverts fix(ai): keep REASONING_MESSAGE_START metadata on the assistant message #1789 for every live stream, not only for rejoin.

Testing

Commands run

  • pnpm test:pr: pass (Successfully ran targets test:sherif, test:knip, test:docs, test:kiira, test:maintainer, test:ai-review, test:oxlint, test:lib, test:types, test:build, build for 113 projects).
  • pnpm --filter @tanstack/ai-e2e exec playwright test tests/join-run-client-tool.spec.ts: 3 passed. I did not run the full E2E suite.

Unit test, pnpm --filter @tanstack/ai-client exec vitest run tests/chat-client-reasoning-rejoin.test.ts, with src/chat-client.ts from main:

 × keeps the thread when replay starts with 'readable reasoning' and hydrate ends with 'a user turn'
 × keeps the thread when replay starts with 'readable reasoning' and hydrate ends with 'an in-flight assistant'
 × keeps the thread when replay starts with 'encrypted-only reasoning' and hydrate ends with 'a user turn'
 × keeps the thread when replay starts with 'encrypted-only reasoning' and hydrate ends with 'an in-flight assistant'
AssertionError: expected [] to have a length of 1 but got +0
AssertionError: expected 'partial' not to be 'partial' // Object.is equality
      Tests  4 failed (4)

The same test on this branch:

 ✓ keeps the thread when replay starts with 'readable reasoning' and hydrate ends with 'a user turn'
 ✓ keeps the thread when replay starts with 'readable reasoning' and hydrate ends with 'an in-flight assistant'
 ✓ keeps the thread when replay starts with 'encrypted-only reasoning' and hydrate ends with 'a user turn'
 ✓ keeps the thread when replay starts with 'encrypted-only reasoning' and hydrate ends with 'an in-flight assistant'
      Tests  4 passed (4)

Two checks on main show that the metadata from #1789 causes the fault:

E2E, with @tanstack/ai-client built from main:

  ✘  3 [chromium] › tests/join-run-client-tool.spec.ts:99:3 › joinRun replay that starts with reasoning › a reload mid run keeps the thinking and the answer (17.9s)
    Locator: getByTestId('assistant-text')
    Expected substring: "REASON_OK"
    Received string:    "none"
  1 failed
  2 passed (27.5s)

The same spec on this branch:

  ✓  3 [chromium] › tests/join-run-client-tool.spec.ts:99:3 › joinRun replay that starts with reasoning › a reload mid run keeps the thinking and the answer (5.4s)
  3 passed (16.6s)

Manual test

  1. On main, run pnpm --filter @tanstack/ai-client exec vitest run tests/chat-client-reasoning-rejoin.test.ts. All 4 cases fail.
  2. On this branch, run the same command. All 4 cases pass.
  3. Build @tanstack/ai-client, then run pnpm --filter @tanstack/ai-e2e exec playwright test tests/join-run-client-tool.spec.ts. The reasoning case passes.

How this PR makes testing easy. tests/chat-client-reasoning-rejoin.test.ts is one table: readable or encrypted-only reasoning, against a hydrate that ends on a user turn or on an in-flight partial. Each case asserts one assistant message with the thinking part (and its signature), the tool call, and the answer. The E2E case reloads a held run and asserts the thinking and the answer after the replay.

Linked issues

Follow-up to #1422, which #1357 closed. #1789 opened it again for streams from chat().

Risk / rollback

Low. Only the rejoin path changes. If hydrate ends on an assistant message, the client drops that message, as before. If hydrate ends on a user turn, the client drops nothing. To undo, revert the PR.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed chat replay when reasoning events arrive first, preserving the rebuilt assistant response and its reasoning, tool results, and text without creating a duplicate assistant message.
    • Improved recovery for conversations ending with either a user message or a partially generated assistant response.
    • Verified that rejoining a replay after reload displays the completed answer and reasoning and clears the loading state.

@changeset-bot

changeset-bot Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0056aa3

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

This PR includes changesets to release 2 packages
Name Type
@tanstack/ai-client Patch
ag-ui 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 Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: TanStack/ai
  • Branch: main
  • File: .coderabbit.yaml
  • Commit: 669fd31
  • Layers: Repository YAML
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: a0eeb81e-fa46-414b-adc8-ffeece7eb403
Commits

Reviewing files that changed from the base of the PR and between e14f91f and 0056aa3.

Files selected for processing: 7
  • .changeset/rejoin-keeps-replay-message.md
  • packages/ai-client/src/chat-client.ts
  • packages/ai-client/tests/chat-client-reasoning-rejoin.test.ts
  • testing/e2e/README.md
  • testing/e2e/src/routes/api.join-run-client-tool.ts
  • testing/e2e/src/routes/join-run-client-tool.tsx
  • testing/e2e/tests/join-run-client-tool.spec.ts
Files skipped from review as they are similar to previous changes: 1
  • .changeset/rejoin-keeps-replay-message.md

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 6 remain after this review.

Walkthrough

During run rejoin, ChatClient records the ID of the last hydrated assistant message. When replay triggers an assistant rebuild, the client removes that specific message. Unit and end-to-end tests cover reasoning-first replay.

Changes: Rejoin replay

Layer / File(s) Summary
Track the hydrated assistant and verify replay
packages/ai-client/src/chat-client.ts, packages/ai-client/tests/chat-client-reasoning-rejoin.test.ts, .changeset/rejoin-keeps-replay-message.md
ChatClient captures the hydrated assistant ID and removes only that message at a rebuild trigger. Unit tests cover readable and encrypted reasoning with different hydrated tails. The changeset declares a patch release for @tanstack/ai-client.
Exercise reasoning replay end to end
testing/e2e/src/routes/api.join-run-client-tool.ts, testing/e2e/src/routes/join-run-client-tool.tsx, testing/e2e/tests/join-run-client-tool.spec.ts, testing/e2e/README.md
The route emits reasoning before assistant text for the reason scenario. The page exposes assistant thinking content. The Playwright test checks the replayed answer, thinking text, assistant message count, and loading state after reload. The README description includes the reasoning replay case.

Priority: Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix · Severity of issue fixed: Medium

Possibly related PRs

  • TanStack/ai#1893: Its reconnect and joinRun support includes replayed reasoning and partial assistant message tests.
  • TanStack/ai#1899: Its rejoin path hydrates history with an in-flight assistant message, a history shape covered by this change.

Suggested reviewers: alemtuzlak

Merge Risk: Minimal · up to 0056a

The rejoin change preserves the replayed answer on the inspected path. No merge-blocking issue remains after normal checks.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentat… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title names the fix in packages/ai-client: the rejoin path keeps the replay's answer when the replay starts with reasoning. This matches the main change in the changeset, chat-client.ts, and the r…
Description check Passed The description follows the template. It has the Changes section, the Checklist with each item marked, and the Release Impact section. It also adds Root cause, Possible alternatives, Testing, Linked i…
Linked Issues check Passed No active directly linked issue remains. Closed issue #1422 is historical context only, so it supplies no coding requirements for this pull request.
Out of Scope Changes check Passed The changes remain within the rejoin fix described by the pull request. The client change targets hydrated-message removal, unit tests cover readable and encrypted reasoning, and the E2E changes cover…
Full details: Title check

Explanation

The title names the fix in packages/ai-client: the rejoin path keeps the replay's answer when the replay starts with reasoning. This matches the main change in the changeset, chat-client.ts, and the reasoning-rejoin tests. The title is specific and concise, and it has no file list or vague terms.

Full details: Description check

Explanation

The description follows the template. It has the Changes section, the Checklist with each item marked, and the Release Impact section. It also adds Root cause, Possible alternatives, Testing, Linked issues, and Risk/rollback sections. The Changes section covers the resumeInFlightRun change, the helper rename, the unchanged REJOIN_REBUILD_TRIGGERS, the unit test update, the E2E spec, and the docs decision. Testing lists the commands run, the failing and passing results on main and on the branch, and a manual test procedure. The description gives the reasons for the change and the rollback path. Minor gaps: the Changes section does not mention the e2e route and page changes (REASON_OK scenario and thinking display), and the PR objectives name the fix as encrypted reasoning, while the description says both readable and encrypted reasoning. These gaps do not block the check.

Full details: Out of Scope Changes check

Explanation

The changes remain within the rejoin fix described by the pull request. The client change targets hydrated-message removal, unit tests cover readable and encrypted reasoning, and the E2E changes cover the reasoning replay path. The changeset supports the published package change. No unrelated change is shown.

Full details: Docstring Coverage

Explanation

Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 1 functions across 5 files. (2 skipped: 2 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

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.

One table covers readable and encrypted-only reasoning against a hydrate that
ends on a user turn or an in-flight partial. Each case checks one assistant
message with the thinking part, the tool call and the answer.
@aayushbtw aayushbtw changed the title fix(ai-client): keep the replay's answer when a rejoin starts with encrypted reasoning fix(ai-client): keep the replay's answer when a rejoin starts with reasoning Oct 29, 2026
@github-actions
github-actions Bot requested a review from AlemTuzlak October 29, 2026 10:03
@github-actions

Copy link
Copy Markdown
Contributor

Thanks for the PR, @aayushbtw! 🙌 @AlemTuzlak will take a look.

Automated pre-review checks

  • ✅ CI passing
  • ✅ No merge conflicts
  • ✅ Changeset present
  • ✅ E2E test changes included

Automated triage — a human review follows.

@github-actions github-actions Bot added the waiting-on: maintainer The ball is in the maintainers’ court label Oct 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

waiting-on: maintainer The ball is in the maintainers’ court

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Rejoining an in-flight run loses the assistant message when the stream starts with reasoning

2 participants