Agent SDK SessionStoreでセッションをS3やRedisに永続化する
Agent SDKのSessionStoreインターフェースで、セッション履歴をS3やRedisへミラーし、複数ホスト間でresumeする実装方法を解説します。
Agent SDK SessionStoreとは何か
Agent SDKは既定で、セッションの文字起こし(トランスクリプト)をローカルの~/.claude/projects/配下にJSONLファイルとして書き込みます。SessionStoreは、このトランスクリプトをS3やRedis、任意のデータベースへミラーするアダプタです。一方のホストで作ったセッションを、別のホストのプロセスからresumeできるようにします。
用途は大きく3つです。
- サーバーレス関数やオートスケールするワーカーのようにファイルシステムを共有しないデプロイで、レプリカ同士がセッションを引き継ぐ場合
- コンテナの再起動やデプロイをまたいでセッションを残したい場合
- 保持期間・暗号化・アクセス制御を自社の統治下に置きたい監査要件がある場合
単一プロセスで完結するローカル開発では、SessionStoreを導入する理由はありません。
SessionStoreインターフェースが持つメソッド
SessionStoreはappendとloadの2つを必須メソッドに持ち、listSessions・listSessionSummaries・delete・listSubkeysの4つを任意メソッドに持つオブジェクトです。SDKは各バッチのトランスクリプトエントリをローカルへ書き込んだ直後にappendを呼び、resume時にはloadで読み戻します。
| メソッド | 必須 | 呼ばれるタイミング |
|---|---|---|
append | 必須必須 | 呼ばれるタイミングローカル書き込み直後、バッチ単位で呼ばれる |
load | 必須必須 | 呼ばれるタイミングresume指定時、またはcontinue: trueが最新セッションを解決するとき |
listSessions | 必須任意 | 呼ばれるタイミングlistSessions({ sessionStore })実行時、continue: trueのセッション一覧化時 |
listSessionSummaries | 必須任意 | 呼ばれるタイミング全セッションのメタデータを1回の呼び出しでまとめて読むとき |
delete | 必須任意 | 呼ばれるタイミングdeleteSession({ sessionStore })実行時 |
listSubkeys | 必須任意 | 呼ばれるタイミングresume時にサブエージェントのトランスクリプトを探索するとき |
appendとloadだけ実装すれば、resumeの基本動作は成立します。deleteを実装しなければ削除は単なる無処理になり、追記専用のバックエンドではそれで構いません。listSubkeysを実装しないと、resume時にメインのトランスクリプトしか復元されない点は覚えておく必要があります。
各エントリはSessionKeyという3つのフィールドで特定します。作業ディレクトリを安全にエンコードしたprojectKey、セッションのUUIDであるsessionId、そしてサブエージェントや付随ファイルに使う任意のsubpathです。subpathが未指定のときはメインのトランスクリプトを指します。ここで注意したいのは、projectKeyが作業ディレクトリから導かれる値だという点です。resumeやcontinueをストア経由で行うには、元のセッションを作ったときと一致する作業ディレクトリからプロセスを起動する必要があります。これを回避する公式の方法があります。TypeScriptではqueryのenvにCLAUDE_CONFIG_DIRと並べてCLAUDE_CODE_PROJECT_DIR_NAMEを設定すると、そのクエリのエントリとresume/continueの探索がその名前でキーイングされます(Agent SDK v0.3.234以降)。ただしlistSessionsやdeleteSessionのような単体ヘルパーはenvを取らずホストプロセスの環境変数を読むため、ホストプロセス側にも同じCLAUDE_CODE_PROJECT_DIR_NAMEを設定しておく必要があります。マルチホストでのresumeという運用そのものに直結する解決策なので、複数ホスト構成を組むなら合わせて押さえておきたい設定です。
InMemorySessionStoreで動作を確認する
SDKは開発・テスト用にInMemorySessionStoreを同梱しています。最初のクエリでストアを渡してセッションIDを取得し、2回目のクエリで同じストアインスタンスとresumeを渡すと、SDKはローカルファイルではなくストアからトランスクリプトを読み込みます。
import { query, InMemorySessionStore } from "@anthropic-ai/claude-agent-sdk";
const store = new InMemorySessionStore();
let sessionId: string | undefined;
for await (const message of query({
prompt: "List the TypeScript files under src/",
options: { sessionStore: store },
})) {
if (message.type === "result") sessionId = message.session_id;
}
// 同じプロセス内で同じstoreを渡せば、2回目のクエリは文脈を引き継ぐ(別ホストでの再開は次節のS3/Redis実装の話)
for await (const message of query({
prompt: "Summarize what those files do",
options: { sessionStore: store, resume: sessionId },
})) {
if (message.type === "result" && message.subtype === "success") {
console.log(message.result);
}
}2回目のクエリは、1回目で読んだファイルの要約を返します。エージェントがストア経由で文脈を引き継いでいる証拠です。InMemorySessionStoreはプロセスが終われば消える実装なので、本番の複数ホスト構成では次に紹介する公式リファレンス実装に差し替えます。
S3・Redis・Postgresの公式リファレンス実装を使う
TypeScript SDKのリポジトリには、S3・Redis・Postgres向けの動作するリファレンスアダプタがexamples/session-stores/配下に用意されています。npmには公開されていないため、必要なsrc/ファイルを自分のプロジェクトへコピーし、対応するバックエンドのクライアントライブラリを別途インストールして使います。
| アダプタ | バックエンドクライアント | 保存形式 |
|---|---|---|
S3SessionStore | バックエンドクライアント@aws-sdk/client-s3 | 保存形式append()ごとに1つのJSONLパートファイル。load()で一覧・ソート・結合する |
RedisSessionStore | バックエンドクライアントioredis | 保存形式トランスクリプトごとのRPUSH/LRANGEリストと、セッション索引用のソート済みセット |
PostgresSessionStore | バックエンドクライアントpg | 保存形式jsonb型テーブルに1エントリ1行、BIGSERIALで順序を保証 |
どのアダプタも設定済みのクライアントインスタンスを引数に取る設計なので、認証情報・TLS・リージョン・コネクションプーリングは呼び出し側が握ります。S3の場合は次のように、既存のS3クライアントをそのまま渡すだけです。
import { query } from "@anthropic-ai/claude-agent-sdk";
import { S3Client } from "@aws-sdk/client-s3";
import { S3SessionStore } from "./S3SessionStore"; // examples/session-stores/s3からコピー
const store = new S3SessionStore({
bucket: "my-claude-sessions",
prefix: "transcripts",
client: new S3Client({ region: "us-east-1" }),
});
for await (const message of query({
prompt: "Hello!",
options: { sessionStore: store },
})) {
if (message.type === "result" && message.subtype === "success") {
console.log(message.result);
}
}すでにRedisクラスタやS3バケットを運用しているチームなら、この3つのアダプタをそのままコピーするだけで永続化が動き始めます。独自の暗号化やアクセス制御を挟みたい場合は、クライアント側の設定で対応できるため、アダプタのコード自体に手を入れる必要はほとんどありません。
独自アダプタを書いて適合性を検証する
appendとloadさえ自分のバックエンド向けに実装すれば、最小限のアダプタは完成します。listSessions・listSessionSummaries・delete・listSubkeysを足すごとに、それぞれlistSessions()関数・1回呼び出しでのメタデータ取得・deleteSession()・サブエージェントのresumeが動くようになります。deleteを実装する場合は契約に注意が必要です。メインキー(subpathなし)の削除は、そのセッションの全subkeyへカスケードし、サマリーエントリも削除しなければなりません。これを守らないと、削除したはずのセッションがlistSessionSummariesに出続けてしまいます。
appendが受け取るエントリはSessionStoreEntry型で、中身は不透明なJSON安全値として扱います。順序を保ったまま永続化し、loadは追記した順序のまま返す必要があります。バイト単位で同一である必要はなく、Postgresのjsonbのようにオブジェクトのキー順を並べ替えるバックエンドでも問題ありません。
両SDKには、append・load・任意メソッドが満たすべき振る舞いの契約を検証する適合性テストスイートが同梱されています。TypeScriptではexamples/session-stores/shared/conformance.tsを自分のテストスイートへコピーし、Pythonではパッケージに同梱されたrun_session_store_conformanceを使います。
import pytest
from claude_agent_sdk.testing import run_session_store_conformance
@pytest.mark.anyio
async def test_my_store_conformance():
await run_session_store_conformance(MyRedisStore)引数にはコンストラクタが引数を取らないクラスをそのまま渡せますが、設定済みクライアントを取るアダプタの場合はラムダでインスタンスを構築します。契約は同じセッションキーを使い回すため、ラムダが呼ばれるたびに空のストレージから始まるよう、専用のインメモリフェイクや一意なキープレフィックス、テスト用データベースを用意します。
セッションはローカルとストアへ二重に書き込まれる
Claude Codeのサブプロセスは、各バッチのトランスクリプトエントリを必ずローカルディスクへ先に書き込み、その後で同じバッチをストアのappend()へ転送します。つまりストアはローカルの複製であって、置き換えではありません。どちらのコピーが実行後も残るかは、実行の開始方法で変わります。
新規セッション、またはストアに何もない状態でのresumeでは、設定ディレクトリ配下のローカルトランスクリプトが実行後も残り、ストアはその複製を受け取るだけです。一方、後述する「ストアから再開する」経路で実行した場合は、実行終了時にローカルの複製が削除され、ストアだけが唯一の永続コピーになります。
新規セッションでローカルディスクに何も残したくない場合は、options.envでCLAUDE_CONFIG_DIRを一時ディレクトリへ向けます。ストアから再開した実行はもともとローカル複製を削除するため、この設定は不要です。アプリがOAuth認証情報やapiKeyHelperのような設定ディレクトリ内のファイルでサインインしている場合は、事前にそれらを一時ディレクトリへコピーするか、envにANTHROPIC_API_KEYを直接設定してください。しないとNot logged inで実行が失敗します。
persistSession: false(TypeScript)とファイルチェックポイント機能(enableFileCheckpointing)は、ストアのミラー機構と競合します。前者はミラーの元になるローカル書き込み自体を止め、後者はバックアップファイルをローカルへ直書きしてストアへは反映しません。どちらかをストアと組み合わせると、SDKは起動時にエラーを投げます。
ストアから再開したときに何が起きるか
resume、あるいはTypeScriptのcontinue: trueやPythonのcontinue_conversation=Trueをストアと一緒に渡すと、SDKはサブプロセスを起動する前にストアへトランスクリプトを問い合わせます。resumeは指定したIDのセッションを、continue系は最新のセッションを要求します。
ストアがトランスクリプトを返すと、SDKはそれを一時的な設定ディレクトリへ書き込み、CLAUDE_CONFIG_DIRがそこを指すようにサブプロセスを起動し、実行終了時にディレクトリごと削除します。この経路で実行が書くローカルトランスクリプトも同時に削除されるため、ストアだけが唯一の永続コピーとして残ります。
このとき本物の設定ディレクトリから一時ディレクトリへコピーされるファイルは、言語によって範囲が違います。TypeScriptは認証情報・.claude.json・ユーザーのsettings.jsonをコピーし、一時ディレクトリ下で誤動作するキー(enabledPluginsなど)をsettings.jsonから取り除きます。apiKeyHelperのような認証設定はストアからの再開でも機能します。Pythonは認証情報と.claude.jsonのみをコピーするため、ユーザーのsettings.jsonにあるapiKeyHelperで認証しているアプリは、ストアから再開するとNot logged inで失敗します。マネージド設定やプロジェクト設定に置いたapiKeyHelperは、CLAUDE_CONFIG_DIRの影響を受けない場所から読まれるため、Pythonでも動作します。
ストアに何もない場合は本物の設定ディレクトリで実行され、挙動はオプションごとに分かれます。resumeは両SDKともIDをサブプロセスへそのまま渡し、ストアなしのresumeと同じくローカルトランスクリプトを再開します。TypeScriptのcontinue: trueは新規セッションを開始し、Pythonのcontinue_conversation=Trueは最新のローカルセッションから続きます。
SessionStoreに対応するAgent SDK関数
TypeScript SDKでは、次の関数がsessionStoreオプションを受け取り、指定するとローカルファイルシステムの代わりにストアへ問い合わせます。
| 関数 | 役割 |
|---|---|
query() / startup() | 役割セッションを開始・再開する |
listSessions() | 役割セッション一覧を取得する |
getSessionInfo() / getSessionMessages() | 役割セッションのメタデータとメッセージ履歴を読む |
renameSession() / tagSession() | 役割セッションの名前やタグを更新する |
deleteSession() / forkSession() | 役割セッションを削除・複製する |
listSubagents() / getSubagentMessages() | 役割サブエージェントのトランスクリプトを扱う |
Python SDKでは、ClaudeAgentOptionsにsession_storeを設定するとquery()がストア相手に動きます。残りの操作はlist_sessions_from_store()やget_session_messages_from_store()のように、それぞれストアを引数に取る専用関数として個別に提供されており、startup()に相当するPython関数はありません。標準のlist_sessions()のようなヘルパーは、ストアを渡さない限りローカルのセッションファイルを読みます。
よくあるつまずき
getSessionMessagesはコンパクション後の履歴しか返しません。ストアに503件の生エントリがあっても、自動コンパクションの後ならgetSessionMessagesは要約後の18件しか返しません。コンパクション前の生履歴が必要なら、store.load(key)を直接呼びます。
forkSessionはバイトコピーではありません。ソースのエントリを読み、すべてのsessionIdフィールドとメッセージUUIDを書き換えたうえで新しいキーへ追記します。アダプタ側でオブジェクトの単純コピーやクラウドストレージのコピー機能を使うと、古いセッションIDを参照したままのトランスクリプトができてしまうため、SDKはあえてその近道を使いません。
ミラーへの書き込みはベストエフォートです。append()が失敗すると、SDKは短い間隔を空けて最大2回まで再試行し、合計3回まで試みます。タイムアウトした呼び出しは再試行されません。それでも失敗すると、SDKはエラーをログに出し、{ type: "system", subtype: "mirror_error" }メッセージをイテレータへ流し、そのバッチを諦めてクエリ自体は続行します。再試行されたバッチが既に届いたエントリを重複して送る可能性があるため、append()実装側でentry.uuidによる重複排除が必要です。ストアの障害はエージェントの実行を止めません。ローカル書き込みが常に先に完了しているためです。ただし例外があります。ストアから再開した実行では、取りこぼしたバッチはローカル複製も残らないため、実行終了後に復旧できるコピーが存在しません。
保持期間の管理はアダプタの責任です。SDKは自分からストアのデータを削除しません。TTLやS3のライフサイクルポリシー、定期クリーンアップは自分で実装します。CLAUDE_CONFIG_DIR配下のローカルトランスクリプトはcleanupPeriodDays設定で独立して掃除されますが、ストアから再開した実行はローカルに何も残さないため、その実行についてはストアの保持設定だけが唯一の保持期間になります。
SessionStoreを使うべきかの早見表
| 実行環境 | SessionStoreの必要度 | 理由 |
|---|---|---|
| サーバーレス関数・オートスケールワーカー | SessionStoreの必要度◎ 必須級 | 理由レプリカ間でファイルシステムを共有できず、ストアなしではresumeが成立しない |
| コンテナのデプロイ・再起動を頻繁に行う運用 | SessionStoreの必要度○ 有効 | 理由ローカルは消えるが前提で、ストアが唯一の永続層になる |
| 監査・保持要件があるコンプライアンス用途 | SessionStoreの必要度○ 有効 | 理由保持ルール・暗号化・アクセス制御を自社側で握れる |
| 単一プロセスのローカル開発・検証 | SessionStoreの必要度△ 不要 | 理由ローカルJSONLで完結し、導入コストに見合わない |
よくある質問
複数のプロセスから同時に同じセッションへappendしても問題ありませんか
公式ドキュメントが排他制御を明記しているのは、foldSessionSummaryによるサマリー集計の部分だけです。読み込み・畳み込み・書き込みをトランザクションや比較交換、セッション単位のロックで直列化する必要があります。エントリ本体の同時append耐性はバックエンドの特性に依存するため、subpathでキーを分けるなど、書き込み経路自体を衝突しにくい設計にしておくのが安全です。
既存のRedisやS3のインフラにそのまま載せられますか
RedisSessionStoreやS3SessionStoreは設定済みのクライアントインスタンスを受け取る設計なので、認証情報・TLS・リージョン・プーリングは既存のインフラ設定をそのまま使えます。アダプタのコード自体に手を入れる必要は基本的にありません。
SessionStoreを使うとレイテンシは増えますか
サブプロセスは常にローカルディスクへ先に書き込み、ストアへの転送はローカル書き込みが終わったあとに行われる設計です。ストアへの転送が遅延したり失敗したりしても、エージェントの応答自体はローカル書き込みの完了を起点に進みます。ただし転送失敗を検知するmirror_errorは監視しておく必要があります。
まとめ
SessionStoreは、ローカルJSONLでは完結しないマルチホスト運用・耐障害性・監査要件のために用意されたミラー機構です。appendとloadさえ実装すれば最小限は動き、公式のS3・Redis・Postgresアダプタをコピーすれば大半のチームはコードを書かずに導入できます。導入時に必ず押さえておきたいのは、ローカルとストアの二重書き込みという設計と、persistSession: falseやファイルチェックポイントとは併用できないという制約です。単一プロセスのローカル開発では不要な機構なので、まずは自分のデプロイ形態がストアを必要とする側かどうかを、上の早見表で確認してから導入を検討してください。
関連する実装はClaude Agent SDK入門で最小構成を、Managed Agentsの設計思想でセッション・ハーネス・サンドボックス分離の全体像を、ストリーミング入力とシングルメッセージ入力の使い分けでresumeやcontinueと組み合わせる入力モードの選び方を確認できます。