Skip to content

feat: split persistence into state (middleware) + delivery (transport/durable-streams) durability#923

Merged
AlemTuzlak merged 9 commits into
feat/persistencefrom
feat/persistence-v2
Jul 9, 2026
Merged

feat: split persistence into state (middleware) + delivery (transport/durable-streams) durability#923
AlemTuzlak merged 9 commits into
feat/persistencefrom
feat/persistence-v2

Conversation

@AlemTuzlak

Copy link
Copy Markdown
Contributor

Summary

Reworks the persistence feature (#785) by splitting the two concerns that were fused under one "persistence" banner, per review feedback:

  • State durability → middleware layer. withChatPersistence / withGenerationPersistence persist thread messages, run status, interrupts, and generation artifacts — nothing else.
  • Delivery durability → transport layer. A client can disconnect / reload / open a second tab and still receive the full ordered stream. This is now a thin, pluggable StreamDurability sink on the transport helpers, backed by the durable streams protocol. We own zero delivery-event storage — the home-grown event-log/cursor/resume subsystem (a database wearing a transport costume, whose DurableRunStream was a dead interface) is deleted.

This is a branch-targeted rework of the unreleased #785, so removed APIs are deleted outright (no deprecation shims).

Delivery durability (transport)

  • toServerSentEvents(stream, { durability })durability is a StreamDurability adapter; same option on every transport helper. Transport stays the user's choice; durable streams is the backing.
  • Adapters: memoryStream (zero-infra dev default) and @tanstack/ai-durable-stream's durableStream(request, { server }) (durable-streams protocol) — new package.
  • chat() is lazy: on a resume request the helper replays from the log and never iterates the stream, so the provider is never re-invoked. Batching lives in the helper.
  • Client uses plain resumable SSE — native Last-Event-ID, zero cursor machinery. chat() loses its cursor param; StreamChunk loses its in-band cursor.
  • Deleted: publicEvents/internalEvents stores + tables, DurableRunStream, cursor.ts, core resume.ts/maybeResume/ResumeSource, resume-source.ts, and the client cursor/auto-resume machinery.

State durability (middleware + backends)

  • withChatPersistence / withGenerationPersistence — state-only, sharing one store contract + run-id space (a future Gemini-nested-generation unification is deferred to its own RFC).
  • Three entry points, one AIPersistence contract: sqlPersistence({ dialect:'sqlite', url }) (batteries), drizzlePersistence(db) (BYO), prismaPersistence(prisma) (BYO). Each ORM owns its own migrations natively — no more hand-authored DDL.
  • Deleted the SQL sprawl: ai-persistence-sql + -sqlite/-postgres/-cloudflare packages, the SqlDriver abstraction, and the hand-rolled migration/CLI code — folded into the drizzle + prisma backends.
  • A shared conformance suite (@tanstack/ai-persistence/testkit) runs against memory + drizzle + prisma to keep the backends honest and the drizzle↔prisma schemas in sync (enforced by a coupling entry).

Review

Ran a full CR loop to convergence (2 fix rounds). Highlights caught and fixed: a durableStream default-batch exactly-once violation on mid-batch reconnect (now per-chunk offset tagging), an offset-codec now-sentinel throw, memoryStream in-flight-join truncation + a join-hang, thrown-provider-error not persisted, a drizzle/prisma blob-prefix wildcard bug, sqlPersistence dialect type-honesty, and removal of a mis-firing generation resume surface.

Validation

  • test:sherif, test:knip, doc-link check, and test:kiira (808 snippets) green.
  • test:eslint/test:types/test:lib/test:build green across all 12 affected library packages; test:types/build green across all example apps + testing/**.
  • Conformance suite green for memory + drizzle + prisma; delivery e2e (disconnect→reconnect exactly-once, second-tab join) green.

Known follow-ups (not blocking; tracked as notes)

  • Angular injectChat doesn't expose the resumeState/pendingInterrupts/resumeInterrupts surface the other 5 frameworks do — pre-existing, untouched by this PR.
  • The auto-injected in-memory LockStore satisfies validatePersistenceFeatures(['locks']) while being process-local — documented; a distinguishable dev-default marker could make validation warn.
  • The durable-streams "own-offset == Stream-Next-Offset, strictly-after read" assumption is faithfully modeled by the test fakes but warrants a one-time confirmation against a live durable-streams service.
  • No framework-level joinRun affordance yet (transparent SSE auto-resume covers the common case); would need a ChatClient.joinRun.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: db2f9057-f550-435e-b1c3-aba52565f1f4

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/persistence-v2

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.

@socket-security

socket-security Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedprisma@​6.19.3761009896100
Added@​prisma/​client@​6.19.3941008598100
Addeddrizzle-orm@​0.45.2881008897100
Addeddrizzle-kit@​0.31.10991009698100

View full report

@socket-security

socket-security Bot commented Jul 9, 2026

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: npm drizzle-orm is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: packages/ai-persistence-drizzle/package.jsonnpm/drizzle-orm@0.45.2

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/drizzle-orm@0.45.2. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm effect is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: pnpm-lock.yamlnpm/prisma@6.19.3npm/effect@3.21.0

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/effect@3.21.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn Medium
Deprecated by its maintainer: npm @esbuild-kit/core-utils

Reason: Merged into tsx: https://tsx.is

From: pnpm-lock.yamlnpm/drizzle-kit@0.31.10npm/@esbuild-kit/core-utils@3.3.2

ℹ Read more on: This package | This alert | What is a deprecated package?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Research the state of the package and determine if there are non-deprecated versions that can be used, or if it should be replaced with a new, supported solution.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@esbuild-kit/core-utils@3.3.2. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn Medium
Deprecated by its maintainer: npm @esbuild-kit/esm-loader

Reason: Merged into tsx: https://tsx.is

From: pnpm-lock.yamlnpm/drizzle-kit@0.31.10npm/@esbuild-kit/esm-loader@2.6.5

ℹ Read more on: This package | This alert | What is a deprecated package?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Research the state of the package and determine if there are non-deprecated versions that can be used, or if it should be replaced with a new, supported solution.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@esbuild-kit/esm-loader@2.6.5. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

14 package(s) bumped directly, 35 bumped as dependents.

🟥 Major bumps

Package Version Reason
@tanstack/ai-angular 0.2.3 → 1.0.0 Changeset
@tanstack/ai-durable-stream 0.1.0 → 1.0.0 Changeset
@tanstack/ai-gemini 0.19.1 → 1.0.0 Changeset
@tanstack/ai-openai 0.16.0 → 1.0.0 Changeset
@tanstack/ai-persistence 0.1.0 → 1.0.0 Changeset
@tanstack/ai-persistence-drizzle 0.1.0 → 1.0.0 Changeset
@tanstack/ai-persistence-prisma 0.1.0 → 1.0.0 Changeset
@tanstack/ai-preact 0.10.3 → 1.0.0 Changeset
@tanstack/ai-react 0.16.4 → 1.0.0 Changeset
@tanstack/ai-solid 0.14.3 → 1.0.0 Changeset
@tanstack/ai-svelte 0.14.3 → 1.0.0 Changeset
@tanstack/ai-vue 0.14.3 → 1.0.0 Changeset
@tanstack/ai-acp 0.2.1 → 1.0.0 Dependent
@tanstack/ai-anthropic 0.16.1 → 1.0.0 Dependent
@tanstack/ai-bedrock 0.1.2 → 1.0.0 Dependent
@tanstack/ai-claude-code 0.2.1 → 1.0.0 Dependent
@tanstack/ai-code-mode 0.3.6 → 1.0.0 Dependent
@tanstack/ai-code-mode-skills 0.3.9 → 1.0.0 Dependent
@tanstack/ai-codex 0.2.1 → 1.0.0 Dependent
@tanstack/ai-elevenlabs 0.2.32 → 1.0.0 Dependent
@tanstack/ai-fal 0.9.10 → 1.0.0 Dependent
@tanstack/ai-grok 0.14.7 → 1.0.0 Dependent
@tanstack/ai-grok-build 0.2.1 → 1.0.0 Dependent
@tanstack/ai-groq 0.5.1 → 1.0.0 Dependent
@tanstack/ai-isolate-node 0.1.45 → 1.0.0 Dependent
@tanstack/ai-isolate-quickjs 0.1.45 → 1.0.0 Dependent
@tanstack/ai-mistral 0.2.1 → 1.0.0 Dependent
@tanstack/ai-ollama 0.8.14 → 1.0.0 Dependent
@tanstack/ai-opencode 0.2.1 → 1.0.0 Dependent
@tanstack/ai-openrouter 0.15.8 → 1.0.0 Dependent
@tanstack/ai-react-ui 0.8.13 → 1.0.0 Dependent
@tanstack/ai-sandbox 0.2.2 → 1.0.0 Dependent
@tanstack/ai-sandbox-cloudflare 0.2.2 → 1.0.0 Dependent
@tanstack/ai-sandbox-daytona 0.2.0 → 1.0.0 Dependent
@tanstack/ai-sandbox-docker 0.2.0 → 1.0.0 Dependent
@tanstack/ai-sandbox-local-process 0.2.0 → 1.0.0 Dependent
@tanstack/ai-sandbox-sprites 0.2.1 → 1.0.0 Dependent
@tanstack/ai-sandbox-vercel 0.2.0 → 1.0.0 Dependent
@tanstack/ai-solid-ui 0.7.12 → 1.0.0 Dependent
@tanstack/openai-base 0.9.7 → 1.0.0 Dependent

🟨 Minor bumps

Package Version Reason
@tanstack/ai 0.40.0 → 0.41.0 Changeset
@tanstack/ai-client 0.20.0 → 0.21.0 Changeset

🟩 Patch bumps

Package Version Reason
@tanstack/ai-devtools-core 0.4.22 → 0.4.23 Dependent
@tanstack/ai-isolate-cloudflare 0.2.36 → 0.2.37 Dependent
@tanstack/ai-mcp 0.2.3 → 0.2.4 Dependent
@tanstack/ai-vue-ui 0.2.31 → 0.2.32 Dependent
@tanstack/preact-ai-devtools 0.1.65 → 0.1.66 Dependent
@tanstack/react-ai-devtools 0.2.65 → 0.2.66 Dependent
@tanstack/solid-ai-devtools 0.2.65 → 0.2.66 Dependent

@nx-cloud

nx-cloud Bot commented Jul 9, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 254ea03

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ✅ Succeeded 1m 21s View ↗
nx run-many --targets=build --exclude=examples/... ✅ Succeeded 2s View ↗

☁️ Nx Cloud last updated this comment at 2026-07-09 17:51:14 UTC

@pkg-pr-new

pkg-pr-new Bot commented Jul 9, 2026

Copy link
Copy Markdown

Open in StackBlitz

@tanstack/ai

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

@tanstack/ai-acp

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

@tanstack/ai-angular

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

@tanstack/ai-anthropic

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

@tanstack/ai-bedrock

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

@tanstack/ai-claude-code

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

@tanstack/ai-client

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

@tanstack/ai-code-mode

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

@tanstack/ai-code-mode-skills

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

@tanstack/ai-codex

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

@tanstack/ai-devtools-core

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

@tanstack/ai-durable-stream

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

@tanstack/ai-elevenlabs

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

@tanstack/ai-event-client

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

@tanstack/ai-fal

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

@tanstack/ai-gemini

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

@tanstack/ai-grok

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

@tanstack/ai-grok-build

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

@tanstack/ai-groq

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

@tanstack/ai-isolate-cloudflare

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

@tanstack/ai-isolate-node

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

@tanstack/ai-isolate-quickjs

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

@tanstack/ai-mcp

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

@tanstack/ai-mistral

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

@tanstack/ai-ollama

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

@tanstack/ai-openai

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

@tanstack/ai-opencode

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

@tanstack/ai-openrouter

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

@tanstack/ai-persistence

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

@tanstack/ai-persistence-drizzle

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

@tanstack/ai-persistence-prisma

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

@tanstack/ai-preact

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

@tanstack/ai-react

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

@tanstack/ai-react-ui

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

@tanstack/ai-sandbox

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

@tanstack/ai-sandbox-cloudflare

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

@tanstack/ai-sandbox-daytona

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

@tanstack/ai-sandbox-docker

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

@tanstack/ai-sandbox-local-process

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

@tanstack/ai-sandbox-sprites

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

@tanstack/ai-sandbox-vercel

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

@tanstack/ai-solid

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

@tanstack/ai-solid-ui

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

@tanstack/ai-svelte

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

@tanstack/ai-utils

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

@tanstack/ai-vue

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

@tanstack/ai-vue-ui

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

@tanstack/openai-base

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

@tanstack/preact-ai-devtools

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

@tanstack/react-ai-devtools

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

@tanstack/solid-ai-devtools

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

commit: 254ea03

The docs/reference/** API docs are auto-generated (pnpm generate-docs) and
regenerated in dedicated PRs; keeping that churn out of this feature PR.
Restored to the pre-PR base.
@AlemTuzlak
AlemTuzlak merged commit d5e8af7 into feat/persistence Jul 9, 2026
8 of 10 checks passed
@AlemTuzlak
AlemTuzlak deleted the feat/persistence-v2 branch July 9, 2026 18:14
AlemTuzlak added a commit that referenced this pull request Jul 22, 2026
* feat(ai): cursor + chat() resume seam, shared locks capability, CUSTOM-event catalog

- Add optional in-band `cursor` to StreamChunk + `cursor` input on chat().
- Add a *replayAndResume() seam: when a cursor is supplied and a ResumeSource
  capability is provided, replay the persisted event tail; if the run is still
  running and the adapter supports re-attach, continue live. No-op without a
  resume source (a non-persisted run is unchanged).
- Move the generic LockStore + LocksCapability ('locks') into core so it is a
  single shared token across the sandbox and persistence layers.
- Add ResumeSource capability/contract and a typed catalog of well-known CUSTOM
  event names (file.changed, process.*, approval.*, artifact.*, sandbox.*).

* feat(persistence): @tanstack/ai-persistence + SQL core + backends + sandbox bridge

- @tanstack/ai-persistence: store contracts, withPersistence middleware,
  memoryPersistence, cursor utilities, approval controller, resume-source
  adapter, history projection. Fully optional; works with and without sandbox.
- @tanstack/ai-persistence-sql: one SQL store impl behind a minimal SqlDriver
  (sqlite|postgres dialect) + versioned migrations + DDL.
- Backends: -sqlite (node:sqlite/better-sqlite3), -postgres (pg), -cloudflare
  (D1, compile-verified), -drizzle and -prisma (BYO).
- @tanstack/ai-sandbox-persistence: durable SQL-backed SandboxStore +
  withPersistenceBridge wiring durable store/locks into withSandbox (agent mode).

* feat(ai-client): in-session auto-resume

- Track the latest in-band cursor per active run; expose getResumeState(),
  resume(), and maybeAutoResume() with an `autoResume` opt-out (default on).
- Pass `cursor` through the connection adapter's RunAgentInput payload so the
  server can replay a run. streamResponse reuses the original runId on resume.

* docs(persistence): overview page, ai-persistence skill, changeset, knip config

- New docs/persistence/overview.md (+ nav entry, addedAt) with server + client +
  agent-mode usage.
- New ai-core/persistence agent skill.
- Changeset covering the feature; knip ignore for the optional pg peer dep.

* ci: apply automated fixes

* feat: finalize ai persistence

* feat: add mysql persistence chat example

* fix: quote mysql usage column

* feat: add cloudflare r2 artifact persistence

* feat: add cloudflare blob persistence primitives

* docs: split persistence guide by scenario

* feat(persistence): client resume + framework use-chat persistence updates

* feat(sandbox): consume persistence without bridge

* feat(persistence): add orm migration CLIs

* feat(sandbox): persist managed workspace files

* fix(ai): tolerate seed messages without parts

* docs(persistence): type-check persistence examples

* fix(client): preserve pending interrupts on mount

* ci: apply automated fixes

* feat: add generation persistence and resumability

* ci: apply automated fixes

* fix: address persistence ci failures

* ci: apply automated fixes

* fix(persistence): default migrations to opt-in

* feat(persistence): add SQL migration CLI docs

* docs(persistence): reorganize persistence guides

* fix(persistence): document internals and r2 prefixes

* Use shared base64 utils

* feat(persistence): split chat and generation middleware

Replace the dual-typed withPersistence cast with honestly typed
withChatPersistence (ChatMiddleware) and withGenerationPersistence
(GenerationMiddleware), sharing feature/run store helpers.

Update callers, tests, docs, skills, and changeset. Document that
runIds must stay unique across activities when both middlewares share
one AIPersistence backend, and how default run ids are allocated.

* ci: apply automated fixes

* feat: split persistence into state (middleware) + delivery (transport/durable-streams) durability (#923)

* feat(ai)!: split state vs delivery durability — StreamDurability transport, remove event-log/cursor/resume seam

* feat(ai-durable-stream): durable-streams protocol StreamDurability adapter

* feat(persistence)!: state-only middleware + memory backend + shared conformance suite; remove delivery stores

* feat(persistence)!: drizzle + prisma state backends; remove SQL-driver sprawl

* feat(client, frameworks)!: resumable SSE + interrupt-only resume; drop cursor & generation delivery-resume

* docs(persistence): state vs delivery durability guides, skills, regenerated API reference

* chore(persistence): migrate examples + e2e; changeset; workspace/knip config

* ci: apply automated fixes

* docs: drop regenerated API reference from this PR

The docs/reference/** API docs are auto-generated (pnpm generate-docs) and
regenerated in dedicated PRs; keeping that churn out of this feature PR.
Restored to the pre-PR base.

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

* refactor(ai)!: group batch under durability option ({ adapter, batch })

Transport helpers now take durability: { adapter, batch? } instead of
sibling durability + batch props. batch is a property of how the durability
sink buffers, not a peer of the response init.

* feat: add persistence and delivery durability

* docs: design full AG-UI interrupt support

* docs: plan full AG-UI interrupt support

* feat(ai-persistence-drizzle): user-owned schemas via injection + schema-emit CLI

Let projects own the TanStack AI Drizzle schema instead of the bundled copy:

- drizzlePersistence(db, { schema }) — stores now operate over injected table
  objects, so renamed tables/columns (incl. drizzle `casing` conventions) and
  extra app-owned columns (e.g. a userId on messages) work through the same
  code paths. TanstackAiSchema pins only column data shapes; construction
  validates tables/columns and throws DrizzleSchemaError with per-item detail.
- tanstack-ai-drizzle-schema CLI (--out/--stdout/--force) emits the schema
  module into the project so the user's own drizzle-kit journal owns the DDL
  (verified: drizzle-kit generate on the emitted file reproduces the bundled
  migration exactly).
- Conformance suite runs against a fully renamed variant schema with an
  app-owned ownership column; sync test pins the emitted asset structurally
  identical to the bundled schema; type-level tests cover assignability.
- Docs: drizzle.md "Own the schema", migrations.md cross-ref, persistence
  SKILL.md; package contract tests updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6Arc9bWdLq1aRRnDRCLMt

* feat: implement AG-UI interrupt lifecycle

* test: allow slow persistence integration setup

* feat(ai-persistence-prisma): renameable models via delegate mapping

Let applications rename the TanStack AI models in their copy of the fragment
(e.g. to avoid collisions with an existing Message/Run model) and map each
store to the renamed client delegate:

  prismaPersistence(prisma, { models: { messages: 'chatMessage' } })

- New model-contract.ts: minimal structural delegate interfaces the stores
  operate over (field surface pinned, delegate names free), PrismaModelMap,
  and construction-time validation throwing PrismaModelError with per-store
  detail. Stores refactored to consume delegates instead of PrismaClient.
- Bytes inputs pinned to Uint8Array<ArrayBuffer> (what Prisma generates);
  blob.upsert result typed unknown with a re-read, matching the Drizzle
  sibling, so user-generated clients stay assignable.
- Conformance suite runs through a fully renamed client; type tests assert
  the generated delegates satisfy the structural contract; validation and
  partial-map tests added.
- Docs: prisma.md "Rename the models" section; persistence SKILL.md note.

Prisma counterpart of the Drizzle schema-injection change (659464a): DB
names were already free via @map/@@Map and generation already ships via the
models CLI — client-level model names were the remaining hardcoded surface.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6Arc9bWdLq1aRRnDRCLMt

* fix: support interrupts without persistence

* feat: add interrupt testing labs

* fix: complete interrupt resume workflows

* docs: split interrupts into its own section

Interrupts is a large feature; move it out of "Chat & Streaming" into a
dedicated top-level Interrupts section and rewrite the content to be
succinct and example-first with real, rendered React components.

- New docs/interrupts/ pages: overview, tool-approval, multiple, generic,
  client-tool-execution, persistence, migration
- Remove the monolithic docs/chat/interrupts.md and docs/migration/interrupts.md
- Add the Interrupts nav section and drop the old entries in docs/config.json
- Repoint inbound cross-links (chat, tools, persistence, architecture, and the
  generated toolDefinition reference) to the new paths

Examples now show handling a single interrupt, mapping/batching multiple
interrupts, and validating a generic responseSchema in the UI directly,
instead of abstract helper functions.

* docs: clarify interrupt kinds are pauses, not tool categories

The Overview "three kinds" table read as if a tool is either tool-approval
or client-tool-execution. Add a note plus a tool→interrupt matrix showing the
axes are independent: a client tool with needsApproval produces a tool-approval
interrupt first, then a client-tool-execution interrupt once approved.

* docs(interrupts): render examples by looping interrupts, not find+narrow

Replace the `interrupts.find(...)` + whole-component `return null` pattern in
the tool-approval, generic, and client-tool-execution examples with a
`interrupts.map()` that narrows on `kind` and renders per-kind UI. This handles
multiple pending items of a kind and matches the mixed-queue pattern on the
Multiple Interrupts page. Generic items get a per-item child component for their
editor state.

* docs: stop presenting client-tool execution as a resolvable interrupt

Client tools return results through their `.client()` implementation (which runs
automatically) or `addToolResult` — not by resolving a client-tool-execution
interrupt by hand. That manual path was redundant with the tool-result path and
misrepresented how client tools work.

- Delete docs/interrupts/client-tool-execution.md and its nav entry
- Overview: list only tool-approval and generic as kinds you resolve; note client
  tools run automatically and approval is a separate axis
- client-tools.md: drop the manual `execution.resolveInterrupt(...)` snippet;
  keep approval resolution + addToolResult as the manual escape hatch

* feat(interrupts)!: remove client-tool-execution from the public interrupt API

Client tools resolve through their `.client()` implementation (auto-executed)
or `addToolResult` — never as a bound interrupt. The `client-tool-execution`
kind was redundant public surface: `resolveInterrupt(output)` and
`addToolResult` were two doors to the same resolution, and it invited users to
hand-wire client-tool execution that already happens automatically.

The internal architecture is unchanged — the server still emits the
client-tool-execution interrupt as the browser handoff, the client processor
still auto-executes `.client()` tools off it, persistence still stores its
binding. Only the public exposure is removed.

- Remove `ClientToolExecutionInterrupt` from the `ChatInterrupt` union + exports
- Filter client-tool-execution items out of the published `interrupts` /
  `pendingInterrupts` snapshot while keeping them in the internal batch so
  `resolveClientToolOutput` (addToolResult) and auto-execution still work
- Exclude client-tool items from the `resolveInterrupts(cb)` completeness check
  and the boolean-bulk path (they resolve out-of-band; `maybeSubmit` still gates
  real submission on them)
- Update ai-client + framework type tests and the e2e to assert the client tool
  auto-executes and stays out of the public interrupts array

* fix: make interrupts build/typecheck/lint/test/docs green (ephemeral-only)

- Reconcile getChunkRunId precedence for interrupt continuation correlation
- Add resume + loadInterruptState to client connection-adapter surface
- Fix ai-react interrupt-state handler TDZ guard
- Strip recovery/persistence from e2e fixture + example lab
- Fix doc links + kiira snippets for removed persistence surface
- Defer recovery-specific client tests (it.skip) to the persistence PR

* fix(interrupts): ephemeral resume wiring + drop persistence-era e2e

- forward parentRunId from the e2e chat route to chat() so ephemeral
  interrupt resume correlates the child run (fixes tool-approval resume)
- resolve the approval tool-call's owning message from history when the
  live active-message maps were reset by a MESSAGES_SNAPSHOT, so the
  deprecated part.state approval UI still updates
- clear the deprecated approval part.state on approve/deny before the
  compat resolve path runs
- remove interrupts-v2 (durable) e2e routes/fixtures/specs and the
  persistence-era recovery tests/type assertions; these return with the
  stacked persistence PR
- restore the Provider cast on the $provider route dropped during extraction

* ci: apply automated fixes

* refactor(interrupts): remove persistence residue from ephemeral interrupts

The AG-UI interrupt lifecycle was extracted from a larger persistence stack
and shipped ephemeral-only, but carried persistence-shaped code that is never
exercised without a durable layer (which is not part of this package). Remove it.

Server (@tanstack/ai):
- Drop the never-provided InterruptPersistenceGateway capability
  (getInterruptPersistence/provideInterruptPersistence) and its input types
  (OpenInterruptBatchInput, CommitInterruptResolutionsInput). The two call
  sites now always take the ephemeral branch.
- Remove InterruptCommitResult, the InterruptResumeValidationError.recovery
  field, the recovery type guard, and the tanstack:interruptRecovery emission.

Client (@tanstack/ai-client):
- Remove the recovery state machine: getRecoveryState, getPersistedDrafts,
  reportRecoveryError, restorePersistedDrafts, recoverInterrupts,
  {start,recover}PersistedInterrupts, joinContinuationRun, the persisted-state
  branch of applyResumeSnapshot, and the conflict/replay branches of
  submitInterruptBatch (now a plain ephemeral submit).
- Delete interrupt-recovery-parse.ts; drop PersistedInterruptDraft and the
  ChatContinuationLoader option/type.

Kept as a dormant extension seam for a future persistence layer: the optional
ConnectionAdapter.loadInterruptState hook and its InterruptRecoveryStateV1 /
InterruptRecoveryQuery types.

Docs: trim the dangling withChatPersistence step and the persistence-only
migration checklist / recovery sections to match the ephemeral-only surface.

Runtime behavior is unchanged (persistence was never provided). Verified:
typecheck + lint clean across ai/ai-client/6 framework packages, full unit
suites green, docs links pass, and the interrupt lab round-trip re-verified
against the rebuilt dist.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: apply automated fixes

* chore(interrupts): drop non-interrupt residue from PR surface

Option A slim for #970: restore generation-resume, example/e2e media noise,
import-only churn, and unrelated provider test touch-ups to main while keeping
the AG-UI interrupt lifecycle (engine, InterruptManager, client chat
persistence needed by ChatClient, framework useChat, interrupt lab, docs).

Residue snapshot: branch chore/non-interrupt-residue-from-970 (pre-slim tip).

* chore(interrupts): strip test noise and storage unit tests

Restore pure import/style test churn to main. Drop dedicated storage-adapter,
chat-persistence-controller, and cleared-stream-tracker unit tests from the
ephemeral interrupts PR. Thin chat-client-resume to interrupt resume behavior
only (drop persistence.server hydrate/persist matrix). Remove unused
fake-indexeddb devDependency.

* fix(interrupts): resume race, error surfacing, docs/lab, bound API demos

Defer interrupt resume until the parent stream settles so auto-executed
client tools can continue. Surface client-tool validation failures, match
native CTE reasons, tighten binding types, and lock resume validation with
unit tests. Update docs/skill and e2e/example UIs to the bound interrupt API,
and link the Interrupts Lab from the chat example nav and home page.

* refactor(ai-client): drop storage-adapters and persistence controller from interrupts PR

Restore main's ChatPersistor for optional message storage and keep
ClearedStreamTracker for clear-during-stream. Durable resume adapters
(storage-adapters, ChatPersistenceController, client/server persistence
options) already live on feat/persistence and are out of the ephemeral
interrupt surface.

* fix(interrupts): resume client tools with history results; fix generic lab runId

Ephemeral resume now reconstructs client-tool pending items even when the
client already wrote tool results into message history for UI. Align generic
lab interrupt ids with the terminal provider runId so parentRunId correlates,
surface interrupt validation messages in the lab sanitizer, and match the
home-page Interrupts tile to other demo tiles.

* fix(ai): typecheck after main merge with typed tool-call events

SafeToolInput/Output treat schema generics defaulting to undefined as
unknown, and MCP manager tests use AnyServerTool so fixtures can carry
JSON Schema input without fighting the no-schema ServerTool default.

* chore: reset pnpm-lock to main and reinstall for branch deps

Drop residual Prisma lock entries left from the persistence-era history.
Re-resolve only this branch's package.json deltas on top of main's lock.

* ci: apply automated fixes

* fix: clear knip, lint, and example typecheck after interrupts merge

- Drop unused PublicInterruptBinding and knip ignore for @standard-schema/spec
- Clean interrupt-manager shadowing / unnecessary conditionals / async
- Include serverPersistence in e2e provider links
- Use AnyServerTool for heterogeneous tool arrays in code-mode example

* fix(interrupts): clear resume state after approve; fix e2e approval harness

- Clear interrupt/resume state on successful continuation streams even when
  provider run ids diverge; always clear after a successful interrupt batch
- Immediately reflect approval decisions on tool-call message parts (#532)
- Hide submitting interrupts from the public list; block sends only on
  actionable pending/staged interrupts
- Register client delete_file needsApproval stub so tools-test hydrates
  tool-approval interrupts; match resolve by toolCallId/approval id
- Only render pending approval prompts in e2e ChatUI

* fix(e2e): repair tools-test deny onClick syntax and format

Missing closing brace on the deny-button handler broke prettier parse.

* docs(interrupts): overhaul interrupt guides and strip persistence leftovers

- rewrite the interrupt guides to lead with the problem then the API:
  overview explains what pauses a run and how client tools fit; tool-approval
  covers server and client tools and renders the approval inline in the chat;
  multiple contrasts per-item vs root-helper resolution; generic frames the
  application-pause use case
- remove stale references to a separately-shipped durable persistence layer
  from the interrupt, tools, and architecture docs (ephemeral-only in this PR)
- address review feedback: explain client tools in the overview, render the
  chat alongside the approval UI, and validate the generic value directly
- refresh docs/config.json updatedAt for the touched pages

* Fixed hyphen

* fix(ai): restore tool-call output after MESSAGES_SNAPSHOT

Client-tool interrupt paths emit AG-UI MESSAGES_SNAPSHOT from ModelMessages,
which rebuild tool-call parts as input-complete without output and wipe the
complete/output state the client already applied from TOOL_CALL_END/RESULT.
Reconcile snapshots by folding tool-result content into matching tool-call
parts (and prefer pre-snapshot complete state when richer).

* test(ai): expect tool-call complete+output after snapshot anchor

MESSAGES_SNAPSHOT reconciliation now folds tool-result into the
sibling tool-call; update the anchor test to match.

* fix(ai): satisfy no-unnecessary-condition in snapshot enrichment

Avoid a mutated outer `changed` flag that control-flow analysis treats
as always falsy; detect part identity changes instead.

* ci: apply automated fixes

* docs(interrupts): show error/recovery handling with a real component

Replace the prose-only "when an answer is wrong" section in multiple.md with a
runnable component that renders item errors and root interruptErrors, gates
buttons on status and resuming (not just canResolve), and demonstrates both
recovery paths (clearResolution and retryInterrupts).

* docs(interrupts): show how the server consumes the approval decision

Add a "Consume the decision on the server" section to tool-approval.md: a
server execute snippet reading the (possibly edited) input, and an honest
account of the payload branches (reject payload becomes the tool result the
model reads; approve payload is decision metadata, not passed to execute, use
editedArgs for values the tool needs).

* refactor(interrupts): app owns wire-schema validation; drop ajv and @noble/hashes

The library no longer transforms a generic interrupt's wire JSON Schema into a
validator or validates the resolved value against it, on the client or the
server. Values pass through as-is; the application validates them (e.g. with
z.fromJSONSchema on the client and its own check on the server). Validation of a
tool's code-authored Standard Schema (approvalSchema / inputSchema) is unchanged.

- remove the ajv-based json-schema-validator and the ajv / ajv-formats deps
- generic items are always resolvable; canResolve no longer depends on the
  library being able to compile the wire schema
- replace @noble/hashes with a small bundled SHA-256 (same sha256:<hex> wire
  shape), verified against known vectors; drop the dependency
- update the ts-react-chat interrupt lab to validate the generic payload itself
  with zod, and fix the docs that claimed the library validates

* example(ts-react-chat): rebuild interrupt playground as a wildlife sanctuary

Replace the over-built interrupt lab (durable dead-code, error sanitizer, DI
harness, debug fetch, capability panels, ~1.1k lines of tests) with a lean,
button-driven "Willowbrook Sanctuary" playground: one tool per scenario, wired
to chat()/useChat, each triggered by a preset message.

Covers server + client tools for approval-only, shared approvalSchema, branch
(approve/reject) schemas, and edited args; a generic (app-emitted) interrupt;
and batched approvals resolved per-item or via the root resolveInterrupts.

Fixes found by clicking through every scenario:
- don't force provider tool_choice on a continuation (resume); forcing it made
  the model re-call the approved tool instead of answering (empty reply).
- key the generic interrupt id off the request runId (ctx.runId), not the
  provider chunk.runId, so the client's parentRunId correlates on resume
  (the mismatch surfaced as unknown-interrupt).
- echo edited args in assignEnclosure output so the continuation reflects them.

Add a regression test (api.interrupts.test.ts) covering both fixes, and give
the UI a cozier theme with per-card animal avatars.

* fix(interrupts): recover a denied approval on ephemeral resume

A denied approval writes a final tool result into history, so the tool call
reads as completed and dropped out of the reconstructed pending batch, while
the resume batch still references its `approval_` id. That failed the resume
as `unknown-interrupt` (every deny broke). Ephemeral reconstruction now
recovers `approval_`-referenced calls from history the same way it already
recovers finalized `client_tool_` calls.

- add a regression in chat.test.ts: resume a denial whose call already has a
  result in history resolves without RUN_ERROR and skips execution.
- fix the sibling batch-validation test to use a Standard Schema input; the
  earlier ajv removal means raw JSON Schema args are no longer library-
  validated, so the atomic-batch check now exercises the Standard Schema path.

* example(ts-react-chat): real animal photos + interrupt transparency

- swap the emoji card avatars for real animal photos (loremflickr, keyed by
  species) with an emoji fallback if the image can't load
- render tool calls and results in the transcript (name, input args including
  edits, output, and denial reasons) so it's clear what ran under the hood
- add a "Your decisions" log that captures exactly what was submitted, so
  approve payloads/notes are visible even though they are decision metadata
  the tool never receives

* example(ts-react-chat): restyle interrupt playground with a dark theme

Co-authored-by: Cursor <cursoragent@cursor.com>

* @
test(interrupts): port wildlife scenarios into the e2e harness

Add a deterministic interrupt playground (route /interrupts-test) that mirrors
the ts-react-chat wildlife example, plus 31 aimock-backed specs covering every
AG-UI interrupt shape across allow / deny / cancel via both per-item and root
batch resolution:

- server + client tool approvals: boolean, shared payload, branch payload,
  edited args
- a generic (non-tool) interrupt (resolve-with-payload and cancel)
- batch sets: root approve-all / deny-all / cancel-all, per-item, and the
  mixed resolver form

Server tool results are asserted via the approval-responded state (they are not
surfaced as client message parts); server edited-args are verified through an
emitCustomEvent echo. A shared approvalSchema requires a payload on the deny
decision too, so those scenarios carry a denyPayload. The ts-react-chat example
is unchanged.
@

* fix(interrupts): make interrupt ownership explicit, not assumed

An AG-UI `Interrupt` is a shared envelope. A workflow engine's durable
approval, or another agent framework's pause, can arrive on the same
stream. What makes a pause resumable through `chat()` is the binding this
package attaches to the interrupt's metadata — nothing else.

The client did not enforce that. `hydrateInterrupt` synthesised a binding
for any descriptor that lacked one, so a foreign interrupt rendered as a
resolvable generic interrupt whose answer was submitted against a run
with nothing pending — failing as `unknown-interrupt` only after the user
had filled in the form. That is the silent translation between approval
models the orchestration RFC warns about.

- Interrupts carrying no binding we understand surface as
  `kind: 'unbound'`, `canResolve: false`, with no resolver. Pre-binding
  TanStack descriptors keep their legacy metadata route. Unbound items
  are excluded from the batch-completeness gate so they cannot deadlock
  the interrupts that are ours.
- The binding carries a wire version. Readers reject a version they do
  not recognise instead of duck-typing its fields; a binding written
  before the field existed still reads. This is what lets a future
  workflow-owned binding be rejected cleanly rather than mis-read.
- Export `INTERRUPT_BINDING_METADATA_KEY`, `withInterruptBinding()` and
  `readInterruptBinding()` so other producers attach a binding through a
  supported API rather than copying the metadata key, which was a
  duplicated private const in three files.
- Classify off the binding alone. `Interrupt.reason` is free-form AG-UI
  text another producer can also spell `tool_call`, so it is a display
  hint only and never decides ownership. The emitted values are
  unchanged.
- Enumerate the interrupt export surface instead of `export *`.

REVERSES A DELIBERATE DECISION — please review: 2d26b39 kept
`ConnectionAdapter.loadInterruptState` plus `InterruptRecoveryStateV1` /
`InterruptRecoveryQuery` as "a dormant extension seam for a future
persistence layer", after stripping the recovery state machine that used
them. This commit removes them, along with the `persistence-required` /
`atomic-commit-unsupported` / `recovery-unavailable` batch codes.

Reasoning: they describe a persisted, AI-domain interrupt object resumed
through an AI-owned recovery endpoint — which is option 1 of the three
the orchestration RFC (#979) has yet to choose between. Publishing them
as stable types pre-decides that. Nothing consumes the seam today, and
`origin/feat/persistence` (#785) has zero references to any of the three.
Easy to restore if the RFC lands on option 1; hard to unpublish if it
does not. Happy to put them back if the seam is wanted.

Also fixes two pre-existing repo failures the gate surfaced: unresolved
relative tool imports in two interrupt doc snippets (kiira), and knip
flagging the type-only `toolApprovalCapability` brand symbol.

* fix(ai-client): restore per-branch autocomplete for resolveInterrupt options

Replace the two-overload resolveInterrupt signature with a single generic
call signature discriminated on `approved`. Overloads broke editor
autocomplete: a half-typed options literal satisfies neither overload, so
TS resolved no signature and offered no contextual completions. A generic
`TApproved extends boolean` is inferred from the first argument and selects
the matching branch for the rest params, so `payload`/`editedArgs` and the
correct approve/reject schema fields complete per-branch while the wrong
branch's fields are still rejected.

* ci: apply automated fixes

* test(framework): fix @ts-expect-error placement for resolveInterrupt wrong-branch payload

The generic resolveInterrupt signature now reports the wrong-branch payload
error precisely on the offending inner payload property rather than as a
blanket 'no overload matches' on the whole call. Move the @ts-expect-error
directive above that inner line in the framework type tests so it still
suppresses the (now more precise) error.

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Jack Herrington <jherr@pobox.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
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