agent-sdk/session-storage.md +0 −259 deleted
File Deleted View Diff
1> ## Documentation Index
2> Fetch the complete documentation index at: https://code.claude.com/docs/llms.txt
3> Use this file to discover all available pages before exploring further.
4
5# Persist sessions to external storage
6
7> Mirror session transcripts to S3, Redis, or your own backend so any host can resume them.
8
9By default, the SDK writes session transcripts to JSONL files under `~/.claude/projects/` on the local filesystem. A `SessionStore` adapter lets you mirror those transcripts to your own backend, such as S3, Redis, or a database, so a session created on one host can be resumed on another.
10
11Common reasons to use a session store:
12
13* **Multi-host deployments.** Serverless functions, autoscaled workers, and CI runners don't share a filesystem. A shared store lets any replica resume any session.
14* **Durability.** Local containers are ephemeral. A store backed by S3 or a database survives restarts and redeploys.
15* **Compliance and audit.** Keep transcripts in storage you already govern, with your own retention rules, encryption, and access controls.
16
17## The `SessionStore` interface
18
19A `SessionStore` is an object with two required methods, `append` and `load`, and three optional methods. The SDK calls `append` to write transcript entries during a query and `load` to read them back for resume.
20
21<CodeGroup>
22 ```typescript TypeScript theme={null}
23 // Exported from @anthropic-ai/claude-agent-sdk as
24 // SessionStore, SessionKey, SessionStoreEntry.
25
26 type SessionKey = {
27 projectKey: string;
28 sessionId: string;
29 subpath?: string;
30 };
31
32 type SessionStore = {
33 // Required
34 append(key: SessionKey, entries: SessionStoreEntry[]): Promise<void>;
35 load(key: SessionKey): Promise<SessionStoreEntry[] | null>;
36
37 // Optional
38 listSessions?(
39 projectKey: string,
40 ): Promise<Array<{ sessionId: string; mtime: number }>>;
41 delete?(key: SessionKey): Promise<void>;
42 listSubkeys?(key: {
43 projectKey: string;
44 sessionId: string;
45 }): Promise<string[]>;
46 };
47 ```
48
49 ```python Python theme={null}
50 # Exported from claude_agent_sdk as
51 # SessionStore, SessionKey, SessionStoreEntry.
52
53 class SessionKey(TypedDict):
54 project_key: str
55 session_id: str
56 subpath: NotRequired[str]
57
58 class SessionStore(Protocol):
59 # Required
60 async def append(
61 self, key: SessionKey, entries: list[SessionStoreEntry]
62 ) -> None: ...
63 async def load(self, key: SessionKey) -> list[SessionStoreEntry] | None: ...
64
65 # Optional — omit or raise NotImplementedError
66 async def list_sessions(
67 self, project_key: str
68 ) -> list[SessionStoreListEntry]: ...
69 async def delete(self, key: SessionKey) -> None: ...
70 async def list_subkeys(self, key: SessionListSubkeysKey) -> list[str]: ...
71 ```
72</CodeGroup>
73
74`SessionKey` addresses one transcript. `projectKey` is a stable, filesystem-safe encoding of the working directory, `sessionId` is the session UUID, and `subpath` is set when the entry belongs to a subagent transcript or sidecar file rather than the main conversation. Treat `subpath` as an opaque key suffix; it follows the on-disk layout, for example `subagents/agent-<id>`. When `subpath` is undefined the key refers to the main transcript.
75
76| Method | Required | Called when |
77| :------------- | :------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
78| `append` | Yes | After each batch of transcript entries is written locally. Entries are JSON-safe objects, one per line in the local JSONL. |
79| `load` | Yes | Once before the subprocess spawns, when `resume` is set. Return `null` if the session is unknown. |
80| `listSessions` | No | By `listSessions({ sessionStore })` and by `query()`/`startup()` with `continue: true`. If undefined, those calls throw. |
81| `delete` | No | By `deleteSession({ sessionStore })`. Deleting the main key (no `subpath`) must cascade to all subkeys for that session. If undefined, deletion is a no-op, which suits append-only backends. |
82| `listSubkeys` | No | During resume, to discover subagent transcripts. If undefined, only the main transcript is restored. |
83
84## Quick start
85
86The SDK ships an `InMemorySessionStore` for development and testing. The example below runs a query with the store attached, captures the session ID from the result message, then resumes from the store in a second `query()` call. The second call passes the same store instance plus `resume`, so the SDK loads the transcript from the store instead of the local filesystem:
87
88<CodeGroup>
89 ```typescript TypeScript theme={null}
90 import { query, InMemorySessionStore } from "@anthropic-ai/claude-agent-sdk";
91
92 const store = new InMemorySessionStore();
93
94 let sessionId: string | undefined;
95 for await (const message of query({
96 prompt: "List the TypeScript files under src/",
97 options: { sessionStore: store },
98 })) {
99 if (message.type === "result") {
100 sessionId = message.session_id;
101 }
102 }
103
104 // Resume from the store. The agent has full context from the first call.
105 for await (const message of query({
106 prompt: "Summarize what those files do",
107 options: { sessionStore: store, resume: sessionId },
108 })) {
109 if (message.type === "result" && message.subtype === "success") {
110 console.log(message.result);
111 }
112 }
113 ```
114
115 ```python Python theme={null}
116 import asyncio
117 from claude_agent_sdk import (
118 ClaudeAgentOptions,
119 InMemorySessionStore,
120 ResultMessage,
121 query,
122 )
123
124 store = InMemorySessionStore()
125
126
127 async def main():
128 session_id = None
129 async for message in query(
130 prompt="List the Python files under src/",
131 options=ClaudeAgentOptions(session_store=store),
132 ):
133 if isinstance(message, ResultMessage):
134 session_id = message.session_id
135
136 # Resume from the store. The agent has full context from the first call.
137 async for message in query(
138 prompt="Summarize what those files do",
139 options=ClaudeAgentOptions(session_store=store, resume=session_id),
140 ):
141 if isinstance(message, ResultMessage) and message.subtype == "success":
142 print(message.result)
143
144
145 asyncio.run(main())
146 ```
147</CodeGroup>
148
149## Write your own adapter
150
151Implement `append` and `load` against your backend. Add `listSessions`, `delete`, and `listSubkeys` if you want `listSessions()`, `deleteSession()`, and subagent resume to work against the store.
152
153Entries passed to `append` are typed as `SessionStoreEntry` (a `{ type: string; ... }` object). Treat them as opaque JSON-safe values: persist them in order and return them from `load` in the same order. `load` must return entries that are deep-equal to what was appended; byte-equal serialization is not required, so backends like Postgres `jsonb` that reorder object keys are fine.
154
155## Reference implementations
156
157The TypeScript SDK repository includes runnable reference adapters for S3, Redis, and Postgres under [`examples/session-stores/`](https://github.com/anthropics/claude-agent-sdk-typescript/tree/main/examples/session-stores). They are not published to npm; copy the `src/` file you need into your project and install the corresponding backend client.
158
159| Adapter | Backend client | Storage model |
160| :----------------------------------------------------------------------------------------------------------------------------- | :------------------- | :--------------------------------------------------------------------------- |
161| [`S3SessionStore`](https://github.com/anthropics/claude-agent-sdk-typescript/tree/main/examples/session-stores/s3) | `@aws-sdk/client-s3` | One JSONL part file per `append()`; `load()` lists, sorts, and concatenates. |
162| [`RedisSessionStore`](https://github.com/anthropics/claude-agent-sdk-typescript/tree/main/examples/session-stores/redis) | `ioredis` | `RPUSH`/`LRANGE` list per transcript, plus a sorted-set session index. |
163| [`PostgresSessionStore`](https://github.com/anthropics/claude-agent-sdk-typescript/tree/main/examples/session-stores/postgres) | `pg` | One row per entry in a `jsonb` table, ordered by `BIGSERIAL`. |
164
165Each adapter takes a pre-configured client instance, so you control credentials, TLS, region, and pooling. For example, with S3:
166
167```typescript TypeScript theme={null}
168import { query } from "@anthropic-ai/claude-agent-sdk";
169import { S3Client } from "@aws-sdk/client-s3";
170import { S3SessionStore } from "./S3SessionStore"; // copied from examples/session-stores/s3
171
172const store = new S3SessionStore({
173 bucket: "my-claude-sessions",
174 prefix: "transcripts",
175 client: new S3Client({ region: "us-east-1" }),
176});
177
178for await (const message of query({
179 prompt: "Hello!",
180 options: { sessionStore: store },
181})) {
182 if (message.type === "result" && message.subtype === "success") {
183 console.log(message.result);
184 }
185}
186
187// Later, possibly on a different host:
188for await (const message of query({
189 prompt: "Continue where we left off",
190 options: { sessionStore: store, resume: "previous-session-id" },
191})) {
192 // ...
193}
194```
195
196### Validate your adapter
197
198Both SDKs ship a conformance suite that asserts the behavioral contract `append`, `load`, and the optional methods must satisfy. Tests for optional methods skip automatically when those methods are not implemented.
199
200In TypeScript, copy [`shared/conformance.ts`](https://github.com/anthropics/claude-agent-sdk-typescript/blob/main/examples/session-stores/shared/conformance.ts) from the example directory into your test suite. In Python, the suite ships in the package:
201
202```python Python theme={null}
203import pytest
204from claude_agent_sdk.testing import run_session_store_conformance
205
206
207@pytest.mark.asyncio
208async def test_my_store_conformance():
209 await run_session_store_conformance(MyRedisStore)
210```
211
212## Behavior notes
213
214### Dual-write architecture
215
216The store is a mirror, not a replacement. The Claude Code subprocess always writes to local disk first; the SDK then forwards each batch to `append()`. If you want the local copy to be ephemeral, point `CLAUDE_CONFIG_DIR` at a temp directory in `options.env`. Because the mirror depends on local writes, `sessionStore` cannot be combined with `persistSession: false`; the SDK throws if you set both. It also throws if combined with `enableFileCheckpointing`, since file-history backup blobs are written directly to local disk and are not mirrored to the store.
217
218### Mirror writes are best-effort
219
220If `append()` rejects or times out, the error is logged, a `{ type: "system", subtype: "mirror_error" }` message is emitted into the iterator, and the query continues. The local transcript is already durable on disk, so a store outage does not interrupt the agent or lose data locally. Batches that fail are not retried, so monitor for `mirror_error` if you need to detect store data loss.
221
222### `getSessionMessages` returns the post-compaction chain
223
224`getSessionMessages({ sessionStore })` returns the linked message chain the agent would see on resume. After auto-compaction, earlier turns are replaced by a summary, so a session whose store holds 503 raw entries may return 18 messages from `getSessionMessages`. For the full raw history, including pre-compaction turns and metadata entries, call `store.load(key)` directly.
225
226### `forkSession` is not a byte copy
227
228`forkSession({ sessionStore })` reads the source entries, rewrites every `sessionId` field and remaps message UUIDs, then appends the transformed entries under a new key. An adapter-level copy or `CopyObject` shortcut would produce a transcript that still references the old session ID, so the SDK does not use one.
229
230### Subagent transcripts
231
232Subagent transcripts are mirrored under `subpath: "subagents/agent-<id>"`. `listSubagents({ sessionStore })` requires the adapter to implement `listSubkeys`; `getSubagentMessages({ sessionStore })` uses it when available but falls back to the direct subpath when it is undefined. Resume also calls `listSubkeys` to restore subagent files; without it, only the main transcript is materialized.
233
234### Retention
235
236The SDK never deletes from your store on its own. Retention is the adapter's responsibility: implement TTLs, S3 lifecycle policies, or scheduled cleanup according to your compliance requirements. Local transcripts under `CLAUDE_CONFIG_DIR` are swept independently by the `cleanupPeriodDays` setting.
237
238## Supported on
239
240The following SDK functions accept a `sessionStore` option and operate against the store instead of the local filesystem when it is provided:
241
242* [`query()`](/en/agent-sdk/typescript#query)
243* [`startup()`](/en/agent-sdk/typescript#startup)
244* [`listSessions()`](/en/agent-sdk/typescript#list-sessions)
245* [`getSessionInfo()`](/en/agent-sdk/typescript#get-session-info)
246* [`getSessionMessages()`](/en/agent-sdk/typescript#get-session-messages)
247* [`renameSession()`](/en/agent-sdk/typescript#rename-session)
248* [`tagSession()`](/en/agent-sdk/typescript#tag-session)
249* [`deleteSession()`](/en/agent-sdk/typescript)
250* [`forkSession()`](/en/agent-sdk/typescript)
251* [`listSubagents()`](/en/agent-sdk/typescript)
252* [`getSubagentMessages()`](/en/agent-sdk/typescript)
253
254## Related resources
255
256* [Work with sessions](/en/agent-sdk/sessions): Continue, resume, and fork without a custom store
257* [Host the SDK](/en/agent-sdk/hosting): Deployment patterns for multi-host environments
258* [TypeScript `Options`](/en/agent-sdk/typescript#options): Full option reference
259* [`examples/session-stores/`](https://github.com/anthropics/claude-agent-sdk-typescript/tree/main/examples/session-stores): Runnable S3, Redis, and Postgres reference adapters