LockStore, InMemoryLockStore, LocksCapability and withLocks for multi-instance coordination in TanStack AI. Ships in @tanstack/ai — NOT in @tanstack/ai-persistence. Separate from AIPersistence state stores — not a stores key, not composable. InMemoryLockStore vs a distributed (e.g. Cloudflare Durable Object) lock, lease recovery, AbortSignal in critical sections. Use when sandbox or other middleware needs cross-worker mutual exclusion — NOT for storing messages/runs (use withPersistence).
Dependency note: This skill builds on ai-core and ai-core/middleware.
withLocksis a ChatMiddleware that provides a capability. Locks are not part ofAIPersistence.storesand are not composed withcomposePersistence— they ship in@tanstack/ai, independent of@tanstack/ai-persistence.
State stores answer "what is durable chat data?"
Locks answer "who may run this critical section right now?"
withPersistence does not automatically lock a whole turn. Take a
per-thread (or other) lock yourself when multi-writer races matter.
import { withLocks, InMemoryLockStore } from '@tanstack/ai/locks'
middleware: [
withLocks(new InMemoryLockStore()), // single process
]Alongside persistence — optional, locks do not require it:
import { withLocks, InMemoryLockStore } from '@tanstack/ai/locks'
import { withPersistence } from '@tanstack/ai-persistence'
middleware: [withPersistence(persistence), withLocks(new InMemoryLockStore())]withLocks provides LocksCapability for downstream middleware (e.g.
sandbox). Order: usually state first, locks alongside or after depending on
who consumes the capability.
interface LockStore {
withLock<T>(key: string, fn: (signal: AbortSignal) => Promise<T>): Promise<T>
}InMemoryLockStore ships in @tanstack/ai/locks: a per-key promise chain,
correct within a single process only. Multi-instance deployments need a
distributed implementation — you write it. The Cloudflare Durable Object recipe
is in ai-persistence/build-cloudflare-adapter (@tanstack/ai-persistence).
Type your own store with defineLock (autocomplete, no : LockStore
annotation), then hand it to withLocks. Acquire the key, run fn, release when
fn settles:
import { defineLock, withLocks } from '@tanstack/ai/locks'
import { acquire } from './my-lock-backend'
const locks = defineLock({
async withLock(key, fn) {
const { release, signal } = await acquire(key)
try {
return await fn(signal)
} finally {
release()
}
},
})
middleware: [withLocks(locks)]A good LockStore:
AbortSignal into the critical section via withLock; when the
lease is lost, abort so work stops starting external mutations.Callbacks must honor the signal and pass it to cancellable dependencies.
InMemoryLockStore never aborts its signal — within one process, ownership
cannot be lost.
The 'locks' capability token lives in @tanstack/ai/locks. Capability identity
is by object reference, so one shared token means a withLocks in the chain
reaches withSandbox automatically.
@tanstack/ai-persistenceThey are not exported there. Use @tanstack/ai.
locks on AIPersistence.storesNot supported. stores accepts only messages, runs, interrupts,
metadata — never locks. Use withLocks.
locks to composePersistence overridesSame rejection, at the override layer. Locks are not state.
'locks' to the conformance testkit's skipskip accepts only chat state store keys. The suite does not cover locks
at all — test lease expiry and abort separately.
InMemoryLockStore across multiple processesNo mutual exclusion between machines — use a distributed lock store.
Continuing work after losing the lease races other owners.
@tanstack/ai-persistence skills (skills/ai-persistence/SKILL.md in that package) -- ai-persistence/server (state middleware) and ai-persistence/build-cloudflare-adapter (Durable Object lock recipe)1120f0f
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.