diff --git a/llms-full.txt b/llms-full.txt
index eac0f6f99..1e01b0383 100644
--- a/llms-full.txt
+++ b/llms-full.txt
@@ -96037,13 +96037,14 @@ Next.js app that runs the same tool on QStash and on Workflow, with a live log o
Source: https://upstash.com/docs/redis/sdks/agentkit/tanstack-ai
[TanStack AI](https://tanstack.com/ai) defines the contracts for an agent's production state — chat
-persistence, resumable streaming, locks, memory — and ships in-memory implementations that only work
+persistence, session history, stored credentials, resumable streaming, locks, memory — and ships in-memory implementations that only work
inside one process. `@upstash/agentkit-tanstack-ai` implements them on Upstash Redis, so they hold
across serverless instances, page reloads, and devices.
| Import | Plugs into | Feature |
| --- | --- | --- |
-| `upstashPersistence` | `withPersistence()`, `withGenerationPersistence()` | Transcripts, runs, human-in-the-loop interrupts, metadata, generation jobs, and generated files. |
+| `upstashPersistence` | `withPersistence()`, `withGenerationPersistence()` | Transcripts, runs, human-in-the-loop interrupts, metadata, generation jobs, generated files, the session index, and the session log. |
+| `upstashCredentials` | `CredentialStore` (`@tanstack/ai/byok`) | Provider API keys and OAuth tokens that users or organizations saved, optionally encrypted at rest. |
| `upstashStream` | `durability` on the response | Resume a stream after a reload, or open the same thread on another device. |
| `upstashLocks` | `withLocks()` | Distributed locks for TanStack AI middleware, such as its sandbox setup. |
| `upstashMemory` | `memoryMiddleware()` | Long-term memory ranked in [Redis Search](/docs/redis/search/introduction). |
@@ -96054,6 +96055,11 @@ across serverless instances, page reloads, and devices.
npm install @upstash/agentkit-tanstack-ai @tanstack/ai
```
+
+ Version 0.3.0 and later need **TanStack AI 1.0 or later**: `@tanstack/ai` and `@tanstack/ai-persistence` must be
+ `>=1.0.0`. If you are still on TanStack AI 0.x, stay on `@upstash/agentkit-tanstack-ai@0.2.0`.
+
+
AgentKit reads `UPSTASH_REDIS_REST_URL` / `UPSTASH_REDIS_REST_TOKEN` from the environment by default.
Pass a `redis` client to any helper to use a different one.
@@ -96079,7 +96085,8 @@ chat({ adapter, messages, threadId, middleware: [withPersistence(persistence)] }
```
This covers every TanStack AI persistence store: `messages`, `runs`, `interrupts`, and `metadata` for
-chats, plus `generationRuns` and `artifacts` for one-shot generation jobs such as images or speech. The
+chats, `generationRuns` and `artifacts` for one-shot generation jobs such as images or speech, and the
+`sessions` and `log` stores described [below](#how-to-keep-a-session-index-in-redis). The
`blobs` store for generated bytes is added when you pass an Upstash Blob bucket:
```ts
@@ -96102,6 +96109,7 @@ Each write is one command or one Lua script, so concurrent instances cannot inte
prefix: "agentkit:tanstack", // optional: base key prefix
messagesTtlSeconds: 60 * 60 * 24 * 30, // optional: expire idle transcripts (default: never)
bucket: Bucket.fromEnv(), // optional: Upstash Blob bucket for generated files
+ logPollIntervalMs: 250, // optional: how often the session log checks for writes from other machines
});
```
@@ -96109,6 +96117,122 @@ Each write is one command or one Lua script, so concurrent instances cannot inte
[Upstash Blob](/docs/blob/overall/quickstart). Without it, there is no `blobs` store.
+## How to keep a session index in Redis
+
+`upstashPersistence()` also returns a `sessions` store, TanStack AI's `SessionIndexStore`. It holds one
+entry per `(tenantId, threadId)` that a "your chats" list reads: members, title, harness, token usage,
+pinned and archived flags, and your own `metadata`. It holds no messages, which stay in the other stores.
+
+```ts
+const { sessions } = upstashPersistence().stores;
+
+await sessions.upsert({
+ tenantId,
+ threadId,
+ members: [{ userId, role: "owner" }],
+ title: "Refund question",
+ createdAt: Date.now(),
+ updatedAt: Date.now(),
+});
+
+// Newest first, one page at a time.
+const { entries, cursor, truncated } = await sessions.list({ tenantId, memberId: userId, limit: 20 });
+```
+
+* `list` is newest-first per tenant, and filters by `memberId`, `search` (title, case-insensitive), `harness`,
+ `parentThreadId`, `metadata`, and `withArchived`. Pass the returned `cursor` back for the next page.
+* `upsert` replaces the whole entry. To change one field safely from several machines, read the entry with
+ `getVersioned` and write the changed copy with `upsertIf`, which only writes at the revision you read.
+* `delete` removes only the entry, not the thread's data.
+
+To use the index on its own, import `upstashSessionIndex(redis, prefix)` from
+`@upstash/agentkit-tanstack-ai/persistence`.
+
+## How to keep the session log in Redis
+
+`upstashPersistence()` also returns a `log` store, TanStack AI's `LogStore`: the append-only log a host
+keeps for a session tree (events, transcript, inputs, and tool steps), with what several machines need to
+share it safely.
+
+```ts
+const { log } = upstashPersistence().stores;
+
+// One owner at a time holds the log; every fenced write carries its epoch.
+const lease = await log.acquireOwnership({ logId, harness: "support", ownerId: hostId, ttlMs: 30_000 });
+if (lease) {
+ await log.append({ logId, seq: 1, epoch: lease.epoch, records: [{ type: "message", text: "hi" }] });
+ const entries = await log.read({ logId, after: 0 });
+}
+```
+
+* **Append:** `append` writes a batch at the next position, together with the pending inputs it consumes, in
+ one atomic step. A second writer at the same position gets a `LogConflictError` and nothing is written.
+ One call takes at most 10,000 records.
+* **Ownership:** `acquireOwnership`, `renewOwnership`, and `releaseOwnership` give one host a lease with an
+ epoch. A host that lost the log gets an `OwnershipLostError` on its next fenced write. Lease expiry uses
+ the Redis server clock, so your machines need no clock sync.
+* **Input lane:** `offerInput` lets any machine queue an input, and `listLogsWithoutOwner` finds logs whose
+ owner expired, that have pending inputs, or whose wake time is due. `markHostAlive` and `pinToWorkers`
+ keep a log on one group of workers.
+* **Snapshots:** `putSnapshot` and `truncate` keep a log short. Reading before the truncation point throws
+ `LogTruncatedError`.
+* **Subscriptions:** `subscribe` calls your listener right after writes through the same store, and polls
+ for writes from other machines every `logPollIntervalMs` (250 ms by default), because the REST API keeps
+ no blocking reads. Treat a call as "something changed" and `read` to catch up.
+
+The store passes all of TanStack AI's `log`, ownership, input-lane, snapshot, and cross-instance
+conformance cases. `upstashLogStore(redis, prefix, { pollIntervalMs })` is also exported from
+`@upstash/agentkit-tanstack-ai/persistence` for standalone use.
+
+## How to store provider API keys and OAuth tokens
+
+`upstashCredentials()` is a `CredentialStore` (from `@tanstack/ai/byok`) for API keys and OAuth tokens that
+users or organizations saved. TanStack AI's `storedProviderKeys()` reads from it.
+
+```ts
+import { storedProviderKeys } from "@tanstack/ai/byok/server";
+import { upstashCredentials } from "@upstash/agentkit-tanstack-ai/byok";
+
+const credentials = upstashCredentials({ encryptionKey: process.env.CREDENTIALS_KEY });
+
+await credentials.set({
+ scope: { tenantId, userId },
+ id: "openai",
+ credential: { type: "api_key", value: apiKey },
+});
+
+// The user's own key, else the tenant's.
+const keys = storedProviderKeys(credentials, { tenantId, userId });
+```
+
+Each owner (`tenantId` plus `userId`) has its own hash, so `list` returns ids and types only and never a
+secret. `getVersioned` and `setIf` are compare-and-set on a revision that never comes back after a
+`delete`, so several instances refreshing one OAuth token cannot overwrite each other.
+
+
+ Set `encryptionKey` to encrypt secrets with AES-256-GCM. It is 32 bytes: a `Uint8Array`, or a base64,
+ base64url, or hex string (`openssl rand -base64 32`). Without it, secrets are stored as plain JSON.
+
+ ```ts
+ upstashCredentials({
+ redis, // optional: defaults to Redis.fromEnv()
+ prefix: "agentkit:tanstack", // optional: base key prefix
+ encryptionKey: process.env.CREDENTIALS_KEY, // optional but strongly recommended
+ requireEncryption: true, // optional: refuse plaintext rows once a key is set
+ });
+ ```
+
+ * The ciphertext is bound to the key `prefix`, the owner, the credential id, and the type and expiry that
+ `list` shows. **Changing `prefix`, or copying rows to another prefix, makes every encrypted row
+ undecryptable.**
+ * Credentials saved before you added a key are still read as plaintext. Anyone who can *write* to your
+ Redis could swap an encrypted credential for a plaintext one. After you re-save your credentials with
+ the key, set `requireEncryption: true` to refuse plaintext rows.
+ * The revision is not part of the ciphertext, so a writer to your Redis could replay an older ciphertext of
+ the same credential. Encryption protects the data from being read, not from a writer.
+ * The store does not expire `secret_answer` credentials. Delete them after reading.
+
+
## How to resume a TanStack AI stream after a reload
```ts
@@ -96275,13 +96399,14 @@ off, set `UPSTASH_DISABLE_TELEMETRY`, or pass `enableTelemetry: false` to a help
Source: https://upstash.com/docs/redis/sdks/agentkit/tanstack-ai
[TanStack AI](https://tanstack.com/ai) defines the contracts for an agent's production state — chat
-persistence, resumable streaming, locks, memory — and ships in-memory implementations that only work
+persistence, session history, stored credentials, resumable streaming, locks, memory — and ships in-memory implementations that only work
inside one process. `@upstash/agentkit-tanstack-ai` implements them on Upstash Redis, so they hold
across serverless instances, page reloads, and devices.
| Import | Plugs into | Feature |
| --- | --- | --- |
-| `upstashPersistence` | `withPersistence()`, `withGenerationPersistence()` | Transcripts, runs, human-in-the-loop interrupts, metadata, generation jobs, and generated files. |
+| `upstashPersistence` | `withPersistence()`, `withGenerationPersistence()` | Transcripts, runs, human-in-the-loop interrupts, metadata, generation jobs, generated files, the session index, and the session log. |
+| `upstashCredentials` | `CredentialStore` (`@tanstack/ai/byok`) | Provider API keys and OAuth tokens that users or organizations saved, optionally encrypted at rest. |
| `upstashStream` | `durability` on the response | Resume a stream after a reload, or open the same thread on another device. |
| `upstashLocks` | `withLocks()` | Distributed locks for TanStack AI middleware, such as its sandbox setup. |
| `upstashMemory` | `memoryMiddleware()` | Long-term memory ranked in [Redis Search](/docs/redis/search/introduction). |
@@ -96292,6 +96417,11 @@ across serverless instances, page reloads, and devices.
npm install @upstash/agentkit-tanstack-ai @tanstack/ai
```
+
+ Version 0.3.0 and later need **TanStack AI 1.0 or later**: `@tanstack/ai` and `@tanstack/ai-persistence` must be
+ `>=1.0.0`. If you are still on TanStack AI 0.x, stay on `@upstash/agentkit-tanstack-ai@0.2.0`.
+
+
AgentKit reads `UPSTASH_REDIS_REST_URL` / `UPSTASH_REDIS_REST_TOKEN` from the environment by default.
Pass a `redis` client to any helper to use a different one.
@@ -96317,7 +96447,8 @@ chat({ adapter, messages, threadId, middleware: [withPersistence(persistence)] }
```
This covers every TanStack AI persistence store: `messages`, `runs`, `interrupts`, and `metadata` for
-chats, plus `generationRuns` and `artifacts` for one-shot generation jobs such as images or speech. The
+chats, `generationRuns` and `artifacts` for one-shot generation jobs such as images or speech, and the
+`sessions` and `log` stores described [below](#how-to-keep-a-session-index-in-redis). The
`blobs` store for generated bytes is added when you pass an Upstash Blob bucket:
```ts
@@ -96340,6 +96471,7 @@ Each write is one command or one Lua script, so concurrent instances cannot inte
prefix: "agentkit:tanstack", // optional: base key prefix
messagesTtlSeconds: 60 * 60 * 24 * 30, // optional: expire idle transcripts (default: never)
bucket: Bucket.fromEnv(), // optional: Upstash Blob bucket for generated files
+ logPollIntervalMs: 250, // optional: how often the session log checks for writes from other machines
});
```
@@ -96347,6 +96479,122 @@ Each write is one command or one Lua script, so concurrent instances cannot inte
[Upstash Blob](/docs/blob/overall/quickstart). Without it, there is no `blobs` store.
+## How to keep a session index in Redis
+
+`upstashPersistence()` also returns a `sessions` store, TanStack AI's `SessionIndexStore`. It holds one
+entry per `(tenantId, threadId)` that a "your chats" list reads: members, title, harness, token usage,
+pinned and archived flags, and your own `metadata`. It holds no messages, which stay in the other stores.
+
+```ts
+const { sessions } = upstashPersistence().stores;
+
+await sessions.upsert({
+ tenantId,
+ threadId,
+ members: [{ userId, role: "owner" }],
+ title: "Refund question",
+ createdAt: Date.now(),
+ updatedAt: Date.now(),
+});
+
+// Newest first, one page at a time.
+const { entries, cursor, truncated } = await sessions.list({ tenantId, memberId: userId, limit: 20 });
+```
+
+* `list` is newest-first per tenant, and filters by `memberId`, `search` (title, case-insensitive), `harness`,
+ `parentThreadId`, `metadata`, and `withArchived`. Pass the returned `cursor` back for the next page.
+* `upsert` replaces the whole entry. To change one field safely from several machines, read the entry with
+ `getVersioned` and write the changed copy with `upsertIf`, which only writes at the revision you read.
+* `delete` removes only the entry, not the thread's data.
+
+To use the index on its own, import `upstashSessionIndex(redis, prefix)` from
+`@upstash/agentkit-tanstack-ai/persistence`.
+
+## How to keep the session log in Redis
+
+`upstashPersistence()` also returns a `log` store, TanStack AI's `LogStore`: the append-only log a host
+keeps for a session tree (events, transcript, inputs, and tool steps), with what several machines need to
+share it safely.
+
+```ts
+const { log } = upstashPersistence().stores;
+
+// One owner at a time holds the log; every fenced write carries its epoch.
+const lease = await log.acquireOwnership({ logId, harness: "support", ownerId: hostId, ttlMs: 30_000 });
+if (lease) {
+ await log.append({ logId, seq: 1, epoch: lease.epoch, records: [{ type: "message", text: "hi" }] });
+ const entries = await log.read({ logId, after: 0 });
+}
+```
+
+* **Append:** `append` writes a batch at the next position, together with the pending inputs it consumes, in
+ one atomic step. A second writer at the same position gets a `LogConflictError` and nothing is written.
+ One call takes at most 10,000 records.
+* **Ownership:** `acquireOwnership`, `renewOwnership`, and `releaseOwnership` give one host a lease with an
+ epoch. A host that lost the log gets an `OwnershipLostError` on its next fenced write. Lease expiry uses
+ the Redis server clock, so your machines need no clock sync.
+* **Input lane:** `offerInput` lets any machine queue an input, and `listLogsWithoutOwner` finds logs whose
+ owner expired, that have pending inputs, or whose wake time is due. `markHostAlive` and `pinToWorkers`
+ keep a log on one group of workers.
+* **Snapshots:** `putSnapshot` and `truncate` keep a log short. Reading before the truncation point throws
+ `LogTruncatedError`.
+* **Subscriptions:** `subscribe` calls your listener right after writes through the same store, and polls
+ for writes from other machines every `logPollIntervalMs` (250 ms by default), because the REST API keeps
+ no blocking reads. Treat a call as "something changed" and `read` to catch up.
+
+The store passes all of TanStack AI's `log`, ownership, input-lane, snapshot, and cross-instance
+conformance cases. `upstashLogStore(redis, prefix, { pollIntervalMs })` is also exported from
+`@upstash/agentkit-tanstack-ai/persistence` for standalone use.
+
+## How to store provider API keys and OAuth tokens
+
+`upstashCredentials()` is a `CredentialStore` (from `@tanstack/ai/byok`) for API keys and OAuth tokens that
+users or organizations saved. TanStack AI's `storedProviderKeys()` reads from it.
+
+```ts
+import { storedProviderKeys } from "@tanstack/ai/byok/server";
+import { upstashCredentials } from "@upstash/agentkit-tanstack-ai/byok";
+
+const credentials = upstashCredentials({ encryptionKey: process.env.CREDENTIALS_KEY });
+
+await credentials.set({
+ scope: { tenantId, userId },
+ id: "openai",
+ credential: { type: "api_key", value: apiKey },
+});
+
+// The user's own key, else the tenant's.
+const keys = storedProviderKeys(credentials, { tenantId, userId });
+```
+
+Each owner (`tenantId` plus `userId`) has its own hash, so `list` returns ids and types only and never a
+secret. `getVersioned` and `setIf` are compare-and-set on a revision that never comes back after a
+`delete`, so several instances refreshing one OAuth token cannot overwrite each other.
+
+
+ Set `encryptionKey` to encrypt secrets with AES-256-GCM. It is 32 bytes: a `Uint8Array`, or a base64,
+ base64url, or hex string (`openssl rand -base64 32`). Without it, secrets are stored as plain JSON.
+
+ ```ts
+ upstashCredentials({
+ redis, // optional: defaults to Redis.fromEnv()
+ prefix: "agentkit:tanstack", // optional: base key prefix
+ encryptionKey: process.env.CREDENTIALS_KEY, // optional but strongly recommended
+ requireEncryption: true, // optional: refuse plaintext rows once a key is set
+ });
+ ```
+
+ * The ciphertext is bound to the key `prefix`, the owner, the credential id, and the type and expiry that
+ `list` shows. **Changing `prefix`, or copying rows to another prefix, makes every encrypted row
+ undecryptable.**
+ * Credentials saved before you added a key are still read as plaintext. Anyone who can *write* to your
+ Redis could swap an encrypted credential for a plaintext one. After you re-save your credentials with
+ the key, set `requireEncryption: true` to refuse plaintext rows.
+ * The revision is not part of the ciphertext, so a writer to your Redis could replay an older ciphertext of
+ the same credential. Encryption protects the data from being read, not from a writer.
+ * The store does not expire `secret_answer` credentials. Delete them after reading.
+
+
## How to resume a TanStack AI stream after a reload
```ts
diff --git a/llms.txt b/llms.txt
index 270ca1187..371e9535e 100644
--- a/llms.txt
+++ b/llms.txt
@@ -774,8 +774,8 @@ Search these docs: GET https://upstash.com/docs/search?q= returns JSON {
- [MCP Events with Redis and QStash](https://upstash.com/docs/redis/sdks/agentkit/mcp-events.md): Let agents subscribe to events on your MCP server and get a signed webhook when one happens. Subscriptions live in Upstash Redis and deliveries are retried by QStash.
- [Long-Running MCP Tools with Redis and QStash](https://upstash.com/docs/redis/sdks/agentkit/mcp-tasks.md): Long-running tools for MCP servers: start in memory with no setup, then make tasks durable with Upstash Redis and QStash or Workflow. Clients that support MCP Tasks poll natively, and every other client gets a task id the model can poll.
- [Long-Running MCP Tools with Redis and QStash](https://upstash.com/docs/redis/sdks/agentkit/mcp-tasks.md): Long-running tools for MCP servers: start in memory with no setup, then make tasks durable with Upstash Redis and QStash or Workflow. Clients that support MCP Tasks poll natively, and every other client gets a task id the model can poll.
-- [TanStack AI Persistence, Resumable Streams & Memory with Redis](https://upstash.com/docs/redis/sdks/agentkit/tanstack-ai.md): Production backends for TanStack AI on Upstash Redis — chat persistence, resumable streams, distributed locks, long-term memory, tool caching, rate limiting, and RAG tools.
-- [TanStack AI Persistence, Resumable Streams & Memory with Redis](https://upstash.com/docs/redis/sdks/agentkit/tanstack-ai.md): Production backends for TanStack AI on Upstash Redis — chat persistence, resumable streams, distributed locks, long-term memory, tool caching, rate limiting, and RAG tools.
+- [TanStack AI Persistence, Resumable Streams & Memory with Redis](https://upstash.com/docs/redis/sdks/agentkit/tanstack-ai.md): Production backends for TanStack AI on Upstash Redis — chat persistence, session index and session log, stored provider keys, resumable streams, distributed locks, long-term memory, tool caching, rate limiting, and RAG tools.
+- [TanStack AI Persistence, Resumable Streams & Memory with Redis](https://upstash.com/docs/redis/sdks/agentkit/tanstack-ai.md): Production backends for TanStack AI on Upstash Redis — chat persistence, session index and session log, stored provider keys, resumable streams, distributed locks, long-term memory, tool caching, rate limiting, and RAG tools.
- [Upstash Redis MCP](https://upstash.com/docs/redis/sdks/mcp.md)
- [ARCOUNT](https://upstash.com/docs/redis/sdks/py/commands/array/arcount.md): Count the occupied slots in an array.
- [ARDEL](https://upstash.com/docs/redis/sdks/py/commands/array/ardel.md): Delete array values without shifting later indexes.
diff --git a/redis/sdks/agentkit/tanstack-ai.mdx b/redis/sdks/agentkit/tanstack-ai.mdx
index c89473317..f2fc94e31 100644
--- a/redis/sdks/agentkit/tanstack-ai.mdx
+++ b/redis/sdks/agentkit/tanstack-ai.mdx
@@ -1,17 +1,18 @@
---
title: "TanStack AI Persistence, Resumable Streams & Memory with Redis"
sidebarTitle: "TanStack AI"
-description: "Production backends for TanStack AI on Upstash Redis — chat persistence, resumable streams, distributed locks, long-term memory, tool caching, rate limiting, and RAG tools."
+description: "Production backends for TanStack AI on Upstash Redis — chat persistence, session index and session log, stored provider keys, resumable streams, distributed locks, long-term memory, tool caching, rate limiting, and RAG tools."
---
[TanStack AI](https://tanstack.com/ai) defines the contracts for an agent's production state — chat
-persistence, resumable streaming, locks, memory — and ships in-memory implementations that only work
+persistence, session history, stored credentials, resumable streaming, locks, memory — and ships in-memory implementations that only work
inside one process. `@upstash/agentkit-tanstack-ai` implements them on Upstash Redis, so they hold
across serverless instances, page reloads, and devices.
| Import | Plugs into | Feature |
| --- | --- | --- |
-| `upstashPersistence` | `withPersistence()`, `withGenerationPersistence()` | Transcripts, runs, human-in-the-loop interrupts, metadata, generation jobs, and generated files. |
+| `upstashPersistence` | `withPersistence()`, `withGenerationPersistence()` | Transcripts, runs, human-in-the-loop interrupts, metadata, generation jobs, generated files, the session index, and the session log. |
+| `upstashCredentials` | `CredentialStore` (`@tanstack/ai/byok`) | Provider API keys and OAuth tokens that users or organizations saved, optionally encrypted at rest. |
| `upstashStream` | `durability` on the response | Resume a stream after a reload, or open the same thread on another device. |
| `upstashLocks` | `withLocks()` | Distributed locks for TanStack AI middleware, such as its sandbox setup. |
| `upstashMemory` | `memoryMiddleware()` | Long-term memory ranked in [Redis Search](/redis/search/introduction). |
@@ -22,6 +23,11 @@ across serverless instances, page reloads, and devices.
npm install @upstash/agentkit-tanstack-ai @tanstack/ai
```
+
+ Version 0.3.0 and later need **TanStack AI 1.0 or later**: `@tanstack/ai` and `@tanstack/ai-persistence` must be
+ `>=1.0.0`. If you are still on TanStack AI 0.x, stay on `@upstash/agentkit-tanstack-ai@0.2.0`.
+
+
AgentKit reads `UPSTASH_REDIS_REST_URL` / `UPSTASH_REDIS_REST_TOKEN` from the environment by default.
Pass a `redis` client to any helper to use a different one.
@@ -47,7 +53,8 @@ chat({ adapter, messages, threadId, middleware: [withPersistence(persistence)] }
```
This covers every TanStack AI persistence store: `messages`, `runs`, `interrupts`, and `metadata` for
-chats, plus `generationRuns` and `artifacts` for one-shot generation jobs such as images or speech. The
+chats, `generationRuns` and `artifacts` for one-shot generation jobs such as images or speech, and the
+`sessions` and `log` stores described [below](#how-to-keep-a-session-index-in-redis). The
`blobs` store for generated bytes is added when you pass an Upstash Blob bucket:
```ts
@@ -70,6 +77,7 @@ Each write is one command or one Lua script, so concurrent instances cannot inte
prefix: "agentkit:tanstack", // optional: base key prefix
messagesTtlSeconds: 60 * 60 * 24 * 30, // optional: expire idle transcripts (default: never)
bucket: Bucket.fromEnv(), // optional: Upstash Blob bucket for generated files
+ logPollIntervalMs: 250, // optional: how often the session log checks for writes from other machines
});
```
@@ -77,6 +85,122 @@ Each write is one command or one Lua script, so concurrent instances cannot inte
[Upstash Blob](/blob/overall/quickstart). Without it, there is no `blobs` store.
+## How to keep a session index in Redis
+
+`upstashPersistence()` also returns a `sessions` store, TanStack AI's `SessionIndexStore`. It holds one
+entry per `(tenantId, threadId)` that a "your chats" list reads: members, title, harness, token usage,
+pinned and archived flags, and your own `metadata`. It holds no messages, which stay in the other stores.
+
+```ts
+const { sessions } = upstashPersistence().stores;
+
+await sessions.upsert({
+ tenantId,
+ threadId,
+ members: [{ userId, role: "owner" }],
+ title: "Refund question",
+ createdAt: Date.now(),
+ updatedAt: Date.now(),
+});
+
+// Newest first, one page at a time.
+const { entries, cursor, truncated } = await sessions.list({ tenantId, memberId: userId, limit: 20 });
+```
+
+- `list` is newest-first per tenant, and filters by `memberId`, `search` (title, case-insensitive), `harness`,
+ `parentThreadId`, `metadata`, and `withArchived`. Pass the returned `cursor` back for the next page.
+- `upsert` replaces the whole entry. To change one field safely from several machines, read the entry with
+ `getVersioned` and write the changed copy with `upsertIf`, which only writes at the revision you read.
+- `delete` removes only the entry, not the thread's data.
+
+To use the index on its own, import `upstashSessionIndex(redis, prefix)` from
+`@upstash/agentkit-tanstack-ai/persistence`.
+
+## How to keep the session log in Redis
+
+`upstashPersistence()` also returns a `log` store, TanStack AI's `LogStore`: the append-only log a host
+keeps for a session tree (events, transcript, inputs, and tool steps), with what several machines need to
+share it safely.
+
+```ts
+const { log } = upstashPersistence().stores;
+
+// One owner at a time holds the log; every fenced write carries its epoch.
+const lease = await log.acquireOwnership({ logId, harness: "support", ownerId: hostId, ttlMs: 30_000 });
+if (lease) {
+ await log.append({ logId, seq: 1, epoch: lease.epoch, records: [{ type: "message", text: "hi" }] });
+ const entries = await log.read({ logId, after: 0 });
+}
+```
+
+- **Append:** `append` writes a batch at the next position, together with the pending inputs it consumes, in
+ one atomic step. A second writer at the same position gets a `LogConflictError` and nothing is written.
+ One call takes at most 10,000 records.
+- **Ownership:** `acquireOwnership`, `renewOwnership`, and `releaseOwnership` give one host a lease with an
+ epoch. A host that lost the log gets an `OwnershipLostError` on its next fenced write. Lease expiry uses
+ the Redis server clock, so your machines need no clock sync.
+- **Input lane:** `offerInput` lets any machine queue an input, and `listLogsWithoutOwner` finds logs whose
+ owner expired, that have pending inputs, or whose wake time is due. `markHostAlive` and `pinToWorkers`
+ keep a log on one group of workers.
+- **Snapshots:** `putSnapshot` and `truncate` keep a log short. Reading before the truncation point throws
+ `LogTruncatedError`.
+- **Subscriptions:** `subscribe` calls your listener right after writes through the same store, and polls
+ for writes from other machines every `logPollIntervalMs` (250 ms by default), because the REST API keeps
+ no blocking reads. Treat a call as "something changed" and `read` to catch up.
+
+The store passes all of TanStack AI's `log`, ownership, input-lane, snapshot, and cross-instance
+conformance cases. `upstashLogStore(redis, prefix, { pollIntervalMs })` is also exported from
+`@upstash/agentkit-tanstack-ai/persistence` for standalone use.
+
+## How to store provider API keys and OAuth tokens
+
+`upstashCredentials()` is a `CredentialStore` (from `@tanstack/ai/byok`) for API keys and OAuth tokens that
+users or organizations saved. TanStack AI's `storedProviderKeys()` reads from it.
+
+```ts
+import { storedProviderKeys } from "@tanstack/ai/byok/server";
+import { upstashCredentials } from "@upstash/agentkit-tanstack-ai/byok";
+
+const credentials = upstashCredentials({ encryptionKey: process.env.CREDENTIALS_KEY });
+
+await credentials.set({
+ scope: { tenantId, userId },
+ id: "openai",
+ credential: { type: "api_key", value: apiKey },
+});
+
+// The user's own key, else the tenant's.
+const keys = storedProviderKeys(credentials, { tenantId, userId });
+```
+
+Each owner (`tenantId` plus `userId`) has its own hash, so `list` returns ids and types only and never a
+secret. `getVersioned` and `setIf` are compare-and-set on a revision that never comes back after a
+`delete`, so several instances refreshing one OAuth token cannot overwrite each other.
+
+
+ Set `encryptionKey` to encrypt secrets with AES-256-GCM. It is 32 bytes: a `Uint8Array`, or a base64,
+ base64url, or hex string (`openssl rand -base64 32`). Without it, secrets are stored as plain JSON.
+
+ ```ts
+ upstashCredentials({
+ redis, // optional: defaults to Redis.fromEnv()
+ prefix: "agentkit:tanstack", // optional: base key prefix
+ encryptionKey: process.env.CREDENTIALS_KEY, // optional but strongly recommended
+ requireEncryption: true, // optional: refuse plaintext rows once a key is set
+ });
+ ```
+
+ - The ciphertext is bound to the key `prefix`, the owner, the credential id, and the type and expiry that
+ `list` shows. **Changing `prefix`, or copying rows to another prefix, makes every encrypted row
+ undecryptable.**
+ - Credentials saved before you added a key are still read as plaintext. Anyone who can *write* to your
+ Redis could swap an encrypted credential for a plaintext one. After you re-save your credentials with
+ the key, set `requireEncryption: true` to refuse plaintext rows.
+ - The revision is not part of the ciphertext, so a writer to your Redis could replay an older ciphertext of
+ the same credential. Encryption protects the data from being read, not from a writer.
+ - The store does not expire `secret_answer` credentials. Delete them after reading.
+
+
## How to resume a TanStack AI stream after a reload
```ts