Skip to content

fix(ai): keep thinking in the afterModel interrupt snapshot - #1587

Open
AlemTuzlak wants to merge 1 commit into
mainfrom
fix/after-model-interrupt-thinking
Open

AlemTuzlak wants to merge 1 commit into
mainfrom
fix/after-model-interrupt-thinking

Conversation

@AlemTuzlak

@AlemTuzlak AlemTuzlak commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

An afterModel generic interrupt on a turn with no tool calls dropped that turn's thinking. The interrupt's MESSAGES_SNAPSHOT kept only the assistant text. When the client loaded it, the thinking and its signature (signed or redacted) were gone. A turn with thinking but no text was left out of the snapshot entirely. This PR records the paused turn the same way a finished run does.

CodeRabbit found this on #1579. It is older than that PR: it comes from #1102 and affects every provider.

🎯 Changes

  • packages/ai/src/activities/chat/index.ts: at an afterModel interrupt with no tool calls, emitBoundaryInterrupts now calls addTerminalAssistantMessages(). That method closes the open thinking step and records the thinking and the text. The removed helper addAssistantTextMessageForInterrupt recorded only accumulatedContent.
  • Docs: no change. docs/interrupts/boundaries.md describes ctx while the afterModel hook runs. The fix records the turn only after the hook returns its interrupts, so that page stays correct.
  • Changeset: @tanstack/ai patch.

✅ 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. Middleware returns an interrupt from onInterruptBoundary at afterModel, and the model turn has no tool calls. The interrupt snapshot then has no reasoning messages for that turn. When the turn has thinking but no text, the snapshot has no assistant message at all. This affects every provider and both signed and redacted thinking.

Cause. emitBoundaryInterrupts records the paused turn before it emits the interrupt. With tool calls, it calls addAssistantToolCallMessage, which keeps thinking. Without tool calls, it called addAssistantTextMessageForInterrupt. That helper appended only { role: 'assistant', content: accumulatedContent }, and it returned early when the text was empty. It never closed the open thinking step and never copied accumulatedThinking. run() returns after the interrupt, so the normal terminal recording never ran either.

Fix. The no-tool-call branch calls addTerminalAssistantMessages(), the same method that records a finished run. It closes the thinking step and writes thinking, text, and structured output.

Possible alternatives

  • Keep the helper and add thinking to it. It would copy finalizeCurrentThinkingStep and the thinking field into a second place. Not taken: addTerminalAssistantMessages() already does this, and it is the path a finished run uses.
  • Keep the helper as a wrapper that calls addTerminalAssistantMessages(). CodeRabbit proposed this. Not taken: the helper has one caller, so the call goes there and the helper is deleted.

Testing

Commands run

  1. packages/ai unit tests: 1950 passed. test:types and test:oxlint passed.
  2. E2E generic-middleware-interrupts.spec.ts: 16 passed, including the new test.
  3. Full E2E suite: 431 passed, 3 skipped, 1 failed. The failure was resolves a typed generic interrupt at afterModel: the click on "Resolve review" did not take effect under full-suite load. The saved page state shows the thinking part in place, so the fix worked there. Run alone with --repeat-each=5, both afterModel tests passed 10 of 10.
  4. The pnpm test:pr steps, one by one, because the script's VITEST_MAX_WORKERS=1 nx … syntax does not run in Windows cmd. test:react-native, test:dts, and the oxfmt check passed. nx affected ran 401 tasks for 100 projects, and 2 failed. Both fail the same way on main, so this PR did not cause them:
    • @tanstack/ai-solid: tests/chat-ui/text-part.test.tsx does not load on Windows (Received 'file:///@solid-refresh').
    • @tanstack/ai-sandbox-docker: tests/sbx.test.ts (measures whether kill() stops the in-VM process) runs against a local sbx VM.

Repro on clean main (3a09cf044), then on this branch

Unit test keeps thinking in the afterModel snapshot of a turn without tool calls, on main:

× keeps thinking in the afterModel snapshot of a turn without tool calls (with text)
× keeps thinking in the afterModel snapshot of a turn without tool calls (thinking only)
AssertionError: expected [] to deeply equal [ 'sig-1', 'opaque-1' ]
Tests  2 failed | 95 skipped (97)

The same test on this branch: Tests 2 passed | 95 skipped (97).

E2E test keeps the afterModel turn thinking in the interrupt snapshot, with main's @tanstack/ai build:

expect(locator('#mw-messages-json')).toContainText('AFTER_MODEL_REASONING') failed
- Expected substring: AFTER_MODEL_REASONING
+ Received string: [ { "role": "user", … }, … ]
1 failed

The same spec on this branch: 16 passed.

Manual test

  1. On main, add "reasoning": "AFTER_MODEL_REASONING" to testing/e2e/fixtures/middleware-test/generic-after-model.json. Open /middleware-test with scenario generic-after-model and middleware mode generic-lifecycle, then run it.
  2. When the review interrupt shows, look at the messages JSON. The thinking text is missing.
  3. On this branch, do the same. The messages JSON has a thinking part with AFTER_MODEL_REASONING.

How this PR makes testing easy

  • A unit test in packages/ai/tests/chat.test.ts. It covers a signed block and a redacted block, with text and without text.
  • An E2E test in testing/e2e/tests/generic-middleware-interrupts.spec.ts. The generic-after-model fixture now streams reasoning, and the test reads the client messages after the interrupt.

Risk / rollback

Low. At an afterModel interrupt without tool calls, the snapshot now has the turn's reasoning messages, and an assistant message for a thinking-only turn. Code that counted on the old, shorter snapshot sees more messages. To undo, revert this PR.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Reasoning and signature information are now preserved in the snapshot when a turn pauses after model processing without making tool calls, including turns that contain reasoning but no text.
  • Tests
    • Added coverage for paused-turn snapshots with and without assistant text, and for reasoning retained while an interrupt is pending.

At an afterModel generic interrupt with no tool calls, the engine
recorded the paused turn with addAssistantTextMessageForInterrupt. It
kept only the assistant text and skipped a turn with no text, so the
interrupt's MESSAGES_SNAPSHOT lost the thinking and its signature.

The engine now records the turn with addTerminalAssistantMessages, the
same method a finished run uses. The old helper had one caller and is
removed.
@coderabbitai

coderabbitai Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: TanStack/ai/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 68d93103-dfce-4351-bdbb-92b536b58a52

📥 Commits

Reviewing files that changed from the base of the PR and between 3a09cf0 and 6cf94d6.

📒 Files selected for processing (5)
  • .changeset/after-model-interrupt-thinking.md
  • packages/ai/src/activities/chat/index.ts
  • packages/ai/tests/chat.test.ts
  • testing/e2e/fixtures/middleware-test/generic-after-model.json
  • testing/e2e/tests/generic-middleware-interrupts.spec.ts

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


📝 Walkthrough

Walkthrough

The afterModel interrupt path now records terminal assistant messages when a turn has no tool calls. Unit and end-to-end tests check that snapshots retain thinking for turns with text and turns with thinking only.

Changes

AfterModel interrupt snapshots

Layer / File(s) Summary
Record and verify assistant snapshot
packages/ai/src/activities/chat/index.ts, packages/ai/tests/chat.test.ts, testing/e2e/fixtures/middleware-test/generic-after-model.json, testing/e2e/tests/generic-middleware-interrupts.spec.ts, .changeset/after-model-interrupt-thinking.md
The path uses addTerminalAssistantMessages for turns without tool calls. Unit tests cover turns with text and thinking only. The end-to-end test checks that the interrupt snapshot retains reasoning. The changeset describes the update.

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix

Suggested reviewers: kolaworld

Merge Risk: ⚪ Minimal · up to 6cf94

Generic afterModel interrupts now keep a turn's thinking in the paused snapshot, including turns with no text. Tests cover the change, and no merge-blocking risk was found.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 6cf94

Interrupted turns now retain signed and redacted reasoning using the same completion behavior as finished turns. No new recipient or tool-execution path was identified, but consumers receive richer snapshots and external handling was not verified.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The direct exposure change is additional reasoning in snapshots received by existing chat-stream consumers when an afterModel interrupt pauses a turn without tool calls. The changed branch introduces no additional recipient channel or tool invocation. Application-specific storage and recipient authorization remain outside the inspected scope.

Trust Boundaries and Controls

  • observed — The existing registered-definition identity check remains ahead of the changed state mutation. Terminal assistant recording is selected only when no tool calls exist; tool-call interrupts retain their separate recording path.

Resilience and Maintainability Implications

  • observed — The snapshot remains ordered after assistant-state finalization and successful interrupt-terminal middleware processing. If that terminal processing fails, the existing error path returns before emitting the snapshot, rather than publishing it as a successfully paused turn.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: preserving thinking in the afterModel interrupt snapshot.
Description check ✅ Passed The description follows the required template and explains the problem, cause, fix, testing, release impact, changeset, and rollback risk. The test checklist item remains unchecked even though the des…
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 3…
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
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


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.

@nx-cloud

nx-cloud Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 6cf94d6

Command Status Duration Result
nx run-many --targets=build --exclude=examples/... ✅ Succeeded 2m 16s View ↗

☁️ Nx Cloud last updated this comment at 2026-10-01 09:44:24 UTC

@pkg-pr-new

pkg-pr-new Bot commented Oct 1, 2026

Copy link
Copy Markdown

Open in StackBlitz

@tanstack/ai

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai@1587

@tanstack/ai-acp

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-acp@1587

@tanstack/ai-angular

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-angular@1587

@tanstack/ai-anthropic

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-anthropic@1587

@tanstack/ai-bedrock

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-bedrock@1587

@tanstack/ai-byteplus

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-byteplus@1587

@tanstack/ai-claude-code

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-claude-code@1587

@tanstack/ai-client

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-client@1587

@tanstack/ai-cloudflare

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-cloudflare@1587

@tanstack/ai-code-mode

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-code-mode@1587

@tanstack/ai-code-mode-snippets

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-code-mode-snippets@1587

@tanstack/ai-codex

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-codex@1587

@tanstack/ai-cohere

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-cohere@1587

@tanstack/ai-compaction

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-compaction@1587

@tanstack/ai-devtools-core

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-devtools-core@1587

@tanstack/ai-durable-stream

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-durable-stream@1587

@tanstack/ai-elevenlabs

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-elevenlabs@1587

@tanstack/ai-event-client

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-event-client@1587

@tanstack/ai-fal

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-fal@1587

@tanstack/ai-gemini

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-gemini@1587

@tanstack/ai-grok

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-grok@1587

@tanstack/ai-grok-build

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-grok-build@1587

@tanstack/ai-groq

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-groq@1587

@tanstack/ai-isolate-cloudflare

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-cloudflare@1587

@tanstack/ai-isolate-daytona

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-daytona@1587

@tanstack/ai-isolate-node

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-node@1587

@tanstack/ai-isolate-quickjs

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-quickjs@1587

@tanstack/ai-isolate-quickjs-bun

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-quickjs-bun@1587

@tanstack/ai-llmgateway

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-llmgateway@1587

@tanstack/ai-lovable

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-lovable@1587

@tanstack/ai-mcp

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-mcp@1587

@tanstack/ai-memory

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-memory@1587

@tanstack/ai-mistral

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-mistral@1587

@tanstack/ai-octane

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-octane@1587

@tanstack/ai-ollama

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-ollama@1587

@tanstack/ai-ollaya

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-ollaya@1587

@tanstack/ai-openai

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-openai@1587

@tanstack/ai-opencode

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-opencode@1587

@tanstack/ai-openrouter

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-openrouter@1587

@tanstack/ai-perplexity

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-perplexity@1587

@tanstack/ai-persistence

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-persistence@1587

@tanstack/ai-preact

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-preact@1587

@tanstack/ai-react

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-react@1587

@tanstack/ai-react-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-react-ui@1587

@tanstack/ai-reactor

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-reactor@1587

@tanstack/ai-remix

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-remix@1587

@tanstack/ai-sandbox

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox@1587

@tanstack/ai-sandbox-blaxel

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-blaxel@1587

@tanstack/ai-sandbox-boxd

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-boxd@1587

@tanstack/ai-sandbox-cloudflare

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-cloudflare@1587

@tanstack/ai-sandbox-daytona

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-daytona@1587

@tanstack/ai-sandbox-docker

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-docker@1587

@tanstack/ai-sandbox-e2b

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-e2b@1587

@tanstack/ai-sandbox-local-process

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-local-process@1587

@tanstack/ai-sandbox-sprites

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-sprites@1587

@tanstack/ai-sandbox-upstash-box

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-upstash-box@1587

@tanstack/ai-sandbox-vercel

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-vercel@1587

@tanstack/ai-skills

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-skills@1587

@tanstack/ai-solid

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-solid@1587

@tanstack/ai-solid-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-solid-ui@1587

@tanstack/ai-svelte

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-svelte@1587

@tanstack/ai-typesafe

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-typesafe@1587

@tanstack/ai-utils

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-utils@1587

@tanstack/ai-vercel-gateway

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-vercel-gateway@1587

@tanstack/ai-vertex

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-vertex@1587

@tanstack/ai-vue

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-vue@1587

@tanstack/ai-vue-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-vue-ui@1587

@tanstack/ai-worldlabs

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-worldlabs@1587

@tanstack/openai-base

npm i https://pkg.pr.new/TanStack/ai/@tanstack/openai-base@1587

@tanstack/preact-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/preact-ai-devtools@1587

@tanstack/react-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/react-ai-devtools@1587

@tanstack/solid-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/solid-ai-devtools@1587

@tanstack/svelte-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/svelte-ai-devtools@1587

commit: 6cf94d6

@github-actions
github-actions Bot requested a review from tombeckenham October 1, 2026 10:12
@github-actions github-actions Bot added the waiting-on: author Waiting for the author to respond or update label Oct 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

waiting-on: author Waiting for the author to respond or update

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants