fix(ai): keep thinking in the afterModel interrupt snapshot - #1587
AlemTuzlak wants to merge 1 commit into
Conversation
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.
|
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 configurationConfiguration used: Repository: TanStack/ai/.coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (5)
Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review. 📝 WalkthroughWalkthroughThe ChangesAfterModel interrupt snapshots
Priority: ➖ Normal Estimated code review effort: 2 (Simple) | ~10 minutes Change: Bug fix Suggested reviewers: Merge Risk: ⚪ Minimal · up to 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 ReviewSecurity architecture risk: 🔵 Low · up to 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 Security review detailsSecurity Blast Radius
Trust Boundaries and Controls
Resilience and Maintainability Implications
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 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 |
|
View your CI Pipeline Execution ↗ for commit 6cf94d6
☁️ Nx Cloud last updated this comment at |
@tanstack/ai
@tanstack/ai-acp
@tanstack/ai-angular
@tanstack/ai-anthropic
@tanstack/ai-bedrock
@tanstack/ai-byteplus
@tanstack/ai-claude-code
@tanstack/ai-client
@tanstack/ai-cloudflare
@tanstack/ai-code-mode
@tanstack/ai-code-mode-snippets
@tanstack/ai-codex
@tanstack/ai-cohere
@tanstack/ai-compaction
@tanstack/ai-devtools-core
@tanstack/ai-durable-stream
@tanstack/ai-elevenlabs
@tanstack/ai-event-client
@tanstack/ai-fal
@tanstack/ai-gemini
@tanstack/ai-grok
@tanstack/ai-grok-build
@tanstack/ai-groq
@tanstack/ai-isolate-cloudflare
@tanstack/ai-isolate-daytona
@tanstack/ai-isolate-node
@tanstack/ai-isolate-quickjs
@tanstack/ai-isolate-quickjs-bun
@tanstack/ai-llmgateway
@tanstack/ai-lovable
@tanstack/ai-mcp
@tanstack/ai-memory
@tanstack/ai-mistral
@tanstack/ai-octane
@tanstack/ai-ollama
@tanstack/ai-ollaya
@tanstack/ai-openai
@tanstack/ai-opencode
@tanstack/ai-openrouter
@tanstack/ai-perplexity
@tanstack/ai-persistence
@tanstack/ai-preact
@tanstack/ai-react
@tanstack/ai-react-ui
@tanstack/ai-reactor
@tanstack/ai-remix
@tanstack/ai-sandbox
@tanstack/ai-sandbox-blaxel
@tanstack/ai-sandbox-boxd
@tanstack/ai-sandbox-cloudflare
@tanstack/ai-sandbox-daytona
@tanstack/ai-sandbox-docker
@tanstack/ai-sandbox-e2b
@tanstack/ai-sandbox-local-process
@tanstack/ai-sandbox-sprites
@tanstack/ai-sandbox-upstash-box
@tanstack/ai-sandbox-vercel
@tanstack/ai-skills
@tanstack/ai-solid
@tanstack/ai-solid-ui
@tanstack/ai-svelte
@tanstack/ai-typesafe
@tanstack/ai-utils
@tanstack/ai-vercel-gateway
@tanstack/ai-vertex
@tanstack/ai-vue
@tanstack/ai-vue-ui
@tanstack/ai-worldlabs
@tanstack/openai-base
@tanstack/preact-ai-devtools
@tanstack/react-ai-devtools
@tanstack/solid-ai-devtools
@tanstack/svelte-ai-devtools
commit: |
An
afterModelgeneric interrupt on a turn with no tool calls dropped that turn's thinking. The interrupt'sMESSAGES_SNAPSHOTkept 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 anafterModelinterrupt with no tool calls,emitBoundaryInterruptsnow callsaddTerminalAssistantMessages(). That method closes the open thinking step and records the thinking and the text. The removed helperaddAssistantTextMessageForInterruptrecorded onlyaccumulatedContent.docs/interrupts/boundaries.mddescribesctxwhile theafterModelhook runs. The fix records the turn only after the hook returns its interrupts, so that page stays correct.@tanstack/aipatch.✅ Checklist
pnpm run test:pr, or these tests do not apply to this pull request.docs/for this change, or this change is not user-facing.pnpm changeset), or this PR does not change a published package.🚀 Release Impact
Root cause
Issue. Middleware returns an interrupt from
onInterruptBoundaryatafterModel, 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.
emitBoundaryInterruptsrecords the paused turn before it emits the interrupt. With tool calls, it callsaddAssistantToolCallMessage, which keeps thinking. Without tool calls, it calledaddAssistantTextMessageForInterrupt. 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 copiedaccumulatedThinking.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
finalizeCurrentThinkingStepand thethinkingfield into a second place. Not taken:addTerminalAssistantMessages()already does this, and it is the path a finished run uses.addTerminalAssistantMessages(). CodeRabbit proposed this. Not taken: the helper has one caller, so the call goes there and the helper is deleted.Testing
Commands run
packages/aiunit tests: 1950 passed.test:typesandtest:oxlintpassed.generic-middleware-interrupts.spec.ts: 16 passed, including the new test.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, bothafterModeltests passed 10 of 10.pnpm test:prsteps, one by one, because the script'sVITEST_MAX_WORKERS=1 nx …syntax does not run in Windowscmd.test:react-native,test:dts, and the oxfmt check passed.nx affectedran 401 tasks for 100 projects, and 2 failed. Both fail the same way onmain, so this PR did not cause them:@tanstack/ai-solid:tests/chat-ui/text-part.test.tsxdoes 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 localsbxVM.Repro on clean
main(3a09cf044), then on this branchUnit test
keeps thinking in the afterModel snapshot of a turn without tool calls, onmain:The same test on this branch:
Tests 2 passed | 95 skipped (97).E2E test
keeps the afterModel turn thinking in the interrupt snapshot, withmain's@tanstack/aibuild:The same spec on this branch:
16 passed.Manual test
main, add"reasoning": "AFTER_MODEL_REASONING"totesting/e2e/fixtures/middleware-test/generic-after-model.json. Open/middleware-testwith scenariogeneric-after-modeland middleware modegeneric-lifecycle, then run it.AFTER_MODEL_REASONING.How this PR makes testing easy
packages/ai/tests/chat.test.ts. It covers a signed block and a redacted block, with text and without text.testing/e2e/tests/generic-middleware-interrupts.spec.ts. Thegeneric-after-modelfixture now streams reasoning, and the test reads the client messages after the interrupt.Risk / rollback
Low. At an
afterModelinterrupt 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