Skip to content

fix(ai-persistence): preserve cancelled tool resumes - #1090

Merged
AlemTuzlak merged 3 commits into
TanStack:mainfrom
mikemikimike:fix/1088-cancelled-client-tool
Aug 12, 2026
Merged

fix(ai-persistence): preserve cancelled tool resumes#1090
AlemTuzlak merged 3 commits into
TanStack:mainfrom
mikemikimike:fix/1088-cancelled-client-tool

Conversation

@mikemikimike

@mikemikimike mikemikimike commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Preserve cancelled approval and client-tool IDs in persisted resume state.
  • Keep cancellation-only resumes non-empty so the chat engine consumes them.
  • Add regression coverage for both cancellation paths.

Fixes #1088

Validation

  • vitest run packages/ai-persistence/tests: 172 passed
  • tsc -p packages/ai-persistence/tsconfig.json --noEmit: passed
  • oxlint packages/ai-persistence/src --type-aware: passed
  • vite build in packages/ai-persistence: passed
  • publint --strict: passed
  • git diff --check: passed

The repository Playwright/Chromium install was blocked during Windows extraction, so the E2E suite was not run.

Summary by CodeRabbit

  • Bug Fixes
    • Cancelled tool interruptions are now preserved when conversations are resumed.
    • Cancelled approval and client-tool calls are accurately tracked during resume.
    • Cancelled client-tool operations now return a cancellation error, complete the run, and clear pending interruptions.
    • Cancellation-only resume requests no longer trigger another client-tool interruption.
    • Approval-denial behavior continues to work as expected.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fc61afca-0b71-400b-ac15-e14d23a2a852

📥 Commits

Reviewing files that changed from the base of the PR and between 0d4e430 and 151423d.

📒 Files selected for processing (1)
  • .changeset/cancelled-client-tool-resume.md

📝 Walkthrough

Walkthrough

Persistence resume-state reconstruction now includes cancelled tool-call IDs. Cancellation tests verify approval and client-tool handling, including persisted client-tool cancellation, completion, and clearing of pending interrupts.

Changes

Cancelled resume state

Layer / File(s) Summary
Resume-state cancellation tracking
packages/ai-persistence/src/middleware.ts
Cancelled interrupts with tool-call IDs populate cancelledToolCallIds. Resume state remains present when it contains only cancellations.
Cancellation test coverage
packages/ai-persistence/tests/interrupts.test.ts
Tests cover persisted client-tool cancellation, approval cancellation, and client-tool cancellation state. Shared helpers reduce duplicated test setup.
Release metadata
.changeset/cancelled-client-tool-resume.md
A patch changeset documents cancelled client-tool and approval resume handling.

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

Sequence Diagram(s)

sequenceDiagram
  participant PersistedInterrupts
  participant PersistenceMiddleware
  participant ToolExecutor
  PersistedInterrupts->>PersistenceMiddleware: provide cancelled interrupt entries
  PersistenceMiddleware->>PersistenceMiddleware: collect cancelledToolCallIds
  PersistenceMiddleware->>ToolExecutor: return cancellation-only resumeToolState
  ToolExecutor->>ToolExecutor: emit cancellation error result
Loading

Possibly related PRs

  • TanStack/ai#984: This PR introduced related persisted resume and interrupt cancellation behavior.

Suggested labels: persistence

Suggested reviewers: jherr

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes preserving cancelled tool resumes, which is the primary change.
Description check ✅ Passed The description explains the changes, linked issue, validation results, and the unavailable E2E test.
Linked Issues check ✅ Passed The changes add cancelled approval and client-tool IDs, preserve cancellation-only resume state, and retain resolved client-tool results as required by issue #1088.
Out of Scope Changes check ✅ Passed The middleware change, regression tests, and changeset directly support the linked issue objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/ai-persistence/tests/interrupts.test.ts`:
- Around line 381-397: Update the client-tool fixtures used by the test around
persistClientToolTurn and chat to use toolDefinition() with shared metadata
defined by a Zod schema such as z.object({ query: z.string() }). Keep the
fixture client-only by leaving it without execute or marking it with .client(),
and do not use .server().
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 45f26a9a-92b8-46bf-aeed-37b61331b835

📥 Commits

Reviewing files that changed from the base of the PR and between 05f7090 and 0d4e430.

📒 Files selected for processing (1)
  • packages/ai-persistence/tests/interrupts.test.ts

Comment on lines +381 to +397
it('completes a cancelled client-tool resume from persisted state with empty client messages', async () => {
const persistence = memoryPersistence()
await persistClientToolTurn(persistence, [clientTool('clientSearch')])

const pending = await persistence.stores.interrupts!.get(
'client_tool_tool-call-1',
)
expect(pending?.status).toBe('pending')

const afterCancel = mockAdapter([
[runStarted(), text('cancelled-and-done'), runFinished('r1')],
])
const chunks = await collect(
chat({
adapter: afterCancel.adapter,
messages: [],
tools: [clientTool('clientSearch')],

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find repository examples of the required tool builder and implementations.
rg -n -C 4 --glob '*.{ts,tsx}' '\btoolDefinition\s*\(' packages
rg -n -C 3 --glob '*.{ts,tsx}' '\.(server|client)\s*\(' packages

Repository: TanStack/ai

Length of output: 50368


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- target file imports and fixture definitions ---'
sed -n '1,130p' packages/ai-persistence/tests/interrupts.test.ts
rg -n -C 8 'clientTool|persistClientToolTurn|toolDefinition|from .*zod' packages/ai-persistence/tests/interrupts.test.ts

echo '--- toolDefinition API declarations and focused examples ---'
rg -n -C 6 'export .*toolDefinition|function toolDefinition|class ToolDefinition|\.server\(|\.client\(' packages/ai-core packages/ai-client packages/ai-persistence --glob '*.{ts,tsx}' | head -n 240

Repository: TanStack/ai

Length of output: 26750


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- candidate source files ---'
fd -i 'tool' packages --type f | grep -E '(definition|types|index)\.(ts|tsx)$' | head -n 120

echo '--- toolDefinition declarations and Tool types ---'
rg -n -C 12 'toolDefinition|export type Tool|interface Tool|type ToolDefinition' packages --glob '*.{ts,tsx}' \
  | grep -v '/tests/' | head -n 320

echo '--- persistence test imports and all fixture uses ---'
rg -n -C 3 'from .*(ai|zod)|clientTool\(|approvalClientTool\(' packages/ai-persistence/tests/interrupts.test.ts

Repository: TanStack/ai

Length of output: 24031


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- tool-definition implementation ---'
wc -l packages/ai/src/activities/chat/tools/tool-definition.ts
cat -n packages/ai/src/activities/chat/tools/tool-definition.ts

echo '--- exports and core tool types ---'
rg -n -C 8 'tool-definition|ToolDefinition|ServerTool|ClientTool|interface Tool|type Tool' packages/ai/src packages/ai-client/src --glob '*.{ts,tsx}' | head -n 360

echo '--- nearby persistence test tool fixtures and imports ---'
sed -n '1,145p' packages/ai-persistence/tests/interrupts.test.ts

Repository: TanStack/ai

Length of output: 45004


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- runtime classification of tools without execute ---'
rg -n -C 10 'execute\?|typeof .*execute|__toolSide|ClientToolRequest|client tool|client-tool' packages/ai/src/activities packages/ai/src --glob '*.{ts,tsx}' | head -n 360

echo '--- target test assertions for the client-tool fixtures ---'
sed -n '260,455p' packages/ai-persistence/tests/interrupts.test.ts

echo '--- imports and package test conventions for Zod/toolDefinition ---'
rg -n -C 5 'from .*(zod|`@tanstack/ai`)|toolDefinition\(\{' packages/ai-persistence/tests packages/ai-persistence/src --glob '*.{ts,tsx}' | head -n 220

Repository: TanStack/ai

Length of output: 50367


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- exact tool execution branch ---'
sed -n '760,900p' packages/ai/src/activities/chat/tools/tool-calls.ts

echo '--- Tool base type and schema conversion ---'
rg -n -C 14 'export interface Tool|export type Tool|type AnyTool|inputSchema' packages/ai/src/types.ts packages/ai/src --glob '*.ts' \
  | head -n 260

echo '--- direct-definition documentation and client-side examples ---'
sed -n '276,330p' packages/ai/src/activities/chat/tools/tool-definition.ts
rg -n -C 5 'toolDefinition\(\{[^}]*name: .*(client|lookup|search)|\.client\(\)' packages/ai-client/tests packages/ai/src --glob '*.{ts,tsx}' | head -n 180

Repository: TanStack/ai

Length of output: 31148


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

test = Path("packages/ai-persistence/tests/interrupts.test.ts").read_text()
definition = Path("packages/ai/src/activities/chat/tools/tool-definition.ts").read_text()
calls = Path("packages/ai/src/activities/chat/tools/tool-calls.ts").read_text()

raw = re.search(
    r"const clientTool = \(name: string\): Tool => \(\{(?P<body>.*?)\n\}\)",
    test,
    re.S,
)
assert raw, "clientTool fixture not found"
body = raw.group("body")

assert "execute" not in body
assert "inputSchema" not in body
assert "toolDefinition" not in body

assert "Used directly in chat() on the server" in definition
assert re.search(r"server:\s*<", definition)
assert re.search(r"client:\s*<", definition)

client_branch = re.search(
    r"// CASE 1: Client-side tool \(no execute function\)(?P<body>.*?)(?=\n\s*// CASE 2:|\Z)",
    calls,
    re.S,
)
assert client_branch, "client-side execution branch not found"
assert "if (!tool.execute)" in calls
assert "if (tool.inputSchema && isStandardSchema(tool.inputSchema))" in calls

print("raw_fixture_has_execute:", "execute" in body)
print("raw_fixture_has_input_schema:", "inputSchema" in body)
print("tool_definition_supports_direct_server_use:", True)
print("tool_definition_exposes_server_and_client_builders:", True)
print("runtime_classifies_no_execute_as_client_tool:", True)
print("runtime_validates_standard_input_schema:", True)
PY

Repository: TanStack/ai

Length of output: 419


Use toolDefinition() for the client-tool fixtures.

Define the shared metadata with a Zod schema, such as z.object({ query: z.string() }). Keep this client-only fixture as a bare definition or use .client(); .server() is not required because chat() treats tools without execute as client-side.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-persistence/tests/interrupts.test.ts` around lines 381 - 397,
Update the client-tool fixtures used by the test around persistClientToolTurn
and chat to use toolDefinition() with shared metadata defined by a Zod schema
such as z.object({ query: z.string() }). Keep the fixture client-only by leaving
it without execute or marking it with .client(), and do not use .server().

Source: Coding guidelines

@nx-cloud

nx-cloud Bot commented Aug 12, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 151423d

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

☁️ Nx Cloud last updated this comment at 2026-08-12 09:46:24 UTC

@pkg-pr-new

pkg-pr-new Bot commented Aug 12, 2026

Copy link
Copy Markdown

Open in StackBlitz

@tanstack/ai

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

@tanstack/ai-acp

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

@tanstack/ai-angular

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

@tanstack/ai-anthropic

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

@tanstack/ai-bedrock

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

@tanstack/ai-byteplus

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

@tanstack/ai-claude-code

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

@tanstack/ai-client

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

@tanstack/ai-code-mode

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

@tanstack/ai-code-mode-skills

npm i https://pkg.pr.new/@tanstack/ai-code-mode-skills@1090

@tanstack/ai-codex

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

@tanstack/ai-cohere

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

@tanstack/ai-devtools-core

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

@tanstack/ai-durable-stream

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

@tanstack/ai-elevenlabs

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

@tanstack/ai-event-client

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

@tanstack/ai-fal

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

@tanstack/ai-gemini

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

@tanstack/ai-grok

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

@tanstack/ai-grok-build

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

@tanstack/ai-groq

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

@tanstack/ai-isolate-cloudflare

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

@tanstack/ai-isolate-daytona

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

@tanstack/ai-isolate-node

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

@tanstack/ai-isolate-quickjs

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

@tanstack/ai-isolate-quickjs-bun

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

@tanstack/ai-mcp

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

@tanstack/ai-memory

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

@tanstack/ai-mistral

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

@tanstack/ai-ollama

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

@tanstack/ai-openai

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

@tanstack/ai-opencode

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

@tanstack/ai-openrouter

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

@tanstack/ai-persistence

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

@tanstack/ai-preact

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

@tanstack/ai-react

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

@tanstack/ai-react-ui

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

@tanstack/ai-sandbox

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

@tanstack/ai-sandbox-cloudflare

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

@tanstack/ai-sandbox-daytona

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

@tanstack/ai-sandbox-docker

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

@tanstack/ai-sandbox-local-process

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

@tanstack/ai-sandbox-sprites

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

@tanstack/ai-sandbox-vercel

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

@tanstack/ai-solid

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

@tanstack/ai-solid-ui

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

@tanstack/ai-svelte

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

@tanstack/ai-utils

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

@tanstack/ai-vue

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

@tanstack/ai-vue-ui

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

@tanstack/openai-base

npm i https://pkg.pr.new/@tanstack/openai-base@1090

@tanstack/preact-ai-devtools

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

@tanstack/react-ai-devtools

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

@tanstack/solid-ai-devtools

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

commit: 151423d

@@ -829,6 +844,7 @@ describe('interrupt persistence', () => {
run.calls[0] as { clientToolResults?: ReadonlyMap<string, unknown> }
).clientToolResults

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.

[suggestion] The new assertions only prove the translator wrote cancelledToolCallIds. Both cancel tests still use empty messages, no tools, and no stored thread with a pending assistant tool call. The engine therefore never reaches checkForPendingToolCalls() / executeToolCalls(), so these tests cannot fail if a cancelled hydrated client tool still re-interrupts. The file already has the right shape for that path in applies persisted approval and client-tool resume decisions with empty client messages (persist tool-call turn, resume with empty client messages, assert chunks).

Suggestion: Add a regression that mirrors that hydrate path for status: 'cancelled': persist a client-tool interrupt plus the assistant tool-call message, resume with empty messages and the same tools, and assert a TOOL_CALL_RESULT of Tool execution cancelled plus no new client_tool_* interrupt. Keep the current onConfig Set check if you want, but do not treat it as coverage for #1088.

@AlemTuzlak
AlemTuzlak merged commit 24622c7 into TanStack:main Aug 12, 2026
9 checks passed
@github-actions github-actions Bot mentioned this pull request Aug 12, 2026
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.

withPersistence does not apply cancelled client-tool resume entries

2 participants