Claude Media
SessionStoreでworktreeのセッションが空で返る不具合 — Agent SDK Python

SessionStoreでworktreeのセッションが空で返る不具合 — Agent SDK Python

Agent SDK PythonのSessionStoreで、git worktreeのセッションが親リポジトリ経由だと空で返り、resumeも黙って新規になる不具合の仕組みと回避策です。

Python版のAgent SDKでSessionStoreを使い、git worktreeの中で動いたセッションを親リポジトリのパスで読むと、履歴が見つかりません。一覧は空、resumeは新規セッションの開始になり、エラーも出ません。

2026年9月27日にGitHubのissue #1320として報告された不具合で、10月10日の時点でissueはopenのままです。直すPR #1327も、マージされていません。

何が起きるのか

結論から言うと、ストアへの書き込みと読み込みでproject_keyの作り方が食い違っています。

project_keyは、セッションがどのプロジェクトのものかを示すSessionStoreのキーです。ストアの契約では、作業ディレクトリを安全にエンコードした値として説明されています。

経路キーの作り方
import_session_to_storeとライブのミラー書き込みキーの作り方ディスク上のプロジェクトディレクトリ名をそのまま使う
list_sessions_from_storeなどの読み込みキーの作り方要求されたディレクトリをサニタイズした値を使う
materialize_resume_session(resumeの復元)キーの作り方同じく要求ディレクトリのサニタイズ値

通常のプロジェクトなら、ディレクトリ名とサニタイズ値は一致します。ずれるのはworktreeのときです。ディスク上の履歴は、親リポジトリのパスではなくworktreeのパスに対応するディレクトリに入っています。インポート時の解決処理はそちらへ移り、worktree側のディレクトリ名でキーを作ります。読み込みは、渡されたリポジトリのパスからキーを作ります。同じセッションなのに、両者が別のキーを見ることになります。

症状は読み込みだけではない

issueの報告者は、親リポジトリ(REPO)とworktree(WORKTREE)を使って再現しています。

# issue #1320 の再現を要約した例
import_session_to_store(sid, store, directory=REPO)
list_sessions_from_store(store, directory=REPO)      # -> []
list_sessions_from_store(store, directory=WORKTREE)  # -> [該当セッション]
materialize_resume_session(
    resume=sid, cwd=REPO, store=store
)                                                    # -> None

materialize_resume_sessionがNoneを返すのは、ストアにエントリが無いときの正規の戻り値です。そのため、session_storeとresumeとcwd=REPOを渡したClaudeAgentOptionsは、エラーなしで新しいセッションを始めます。過去の会話が消えたように見えるだけで、失敗の表示はありません。

同じissueに別の報告者が、書き込み系の操作の再現を追加しました。インポートがworktreeのキーに入れたセッションを、あとからREPO経由で操作した結果です。

操作(REPO経由)観測された結果
rename_session_via_store / tag_session_via_store観測された結果custom-titleやtagだけの2本目のキーができ、元の履歴は変わらない
delete_session_via_store観測された結果正常終了を返すが、元の履歴もサブエージェントの履歴も残る
fork_session_via_store観測された結果例外になる
リネーム後のresumeの復元観測された結果custom-titleだけのJSONLが復元され、ユーザーとアシスタントの発話は入らない

同じ呼び出しをWORKTREEのパスで行うと、5つの対照ケースが通ったと報告されています。キーは1本のままで、リネームは元の履歴に追記され、削除はサブエージェントの履歴にも及びます。

とくに削除が厄介です。リネームとタグ付けは、custom-titleだけのキーという痕跡が残ります。削除は戻り値だけ見ると成功と区別がつきません。履歴を消す前提のデータ保持の運用では、消えていない履歴が残ります。

ディスクの読み込みには救済があり、ストアには無い

issueが指摘する不均衡が、もう1つあります。ディスクを読むlist_sessionsには、worktreeのパスを探すフォールバックがあります。長いパスで接頭辞を走査する処理もあります。ストアを読む関数には、どちらもありません。

その結果、list_sessions(REPO)は同じセッションを見つけるのに、list_sessions_from_store(store, directory=REPO)は空を返します。ディスクを元にストアへ入れたはずなのに、ストアのほうが空に見えるわけです。

ドキュメントの食い違いもあります。インポート関数のdirectory引数は「list_sessionsと同じ意味」と説明されています。ところがストア版の一覧関数には、include_worktreesが効かないという注記があります。書き込み側はどちらの意味にも従っていません。

回避策: worktreeのパスを渡す

issueの再現は、worktreeのパスをdirectoryやcwdに渡せば、読み込みも復元も更新も通ることを示しています。

# worktreeの実パスを直接指定する(例)
WORKTREE = "/path/to/repo-worktrees/feature-a"
 
sessions = await list_sessions_from_store(
    store, directory=WORKTREE
)

更新系の*_via_store関数にも、同じ要領でworktreeのパスをdirectoryとして渡します。

worktreeの実パスは、親リポジトリでgit worktree list --porcelainを実行すると取れます。各ブロックのworktree行がパスです。

git worktree list --porcelain
# worktree /path/to/repo
# worktree /path/to/repo-worktrees/feature-a

セッションを始める時点で、IDと実パスを対にして保存しておけば、あとで読むときに迷いません。

import json, pathlib
 
# 実行前に決めたworktreeのパスと、セッションIDを対で保存する
def remember(sid: str, worktree: str) -> None:
    path = pathlib.Path("session-paths.json")
    table = json.loads(path.read_text()) if path.exists() else {}
    table[sid] = worktree
    path.write_text(json.dumps(table, indent=2))

読むときは保存したパスをdirectoryやcwdに渡します。

運用では、次の2点を押さえておくと被害を避けられます。

  • ストアに入れる側と読む側で、同じworktreeのパスを使う
  • 親リポジトリ経由のdelete_session_via_storeは、成功を返しても削除を確かめる

削除の確認は、worktreeのパスを指定した一覧で行えます。

await delete_session_via_store(store, sid, directory=REPO)
 
remaining = await list_sessions_from_store(
    store, directory=WORKTREE
)
# sid がまだ残っていれば、親リポジトリ経由の削除は効いていない
assert all(s.session_id != sid for s in remaining)

残っていたら、同じ削除をWORKTREEのパスで呼び直します。

すでに親リポジトリ経由でリネームやタグ付けをした場合は、custom-titleだけのキーが残っている可能性があります。修正PRの説明によると、このような既存の重複キーはPRでは移行されません。残ったキーが、worktree側の履歴を隠すことがあるとも書かれています。

修正PR #1327が扱う範囲

issueを立てた報告者は、直し方として2案を挙げて、メンテナーと相談して決めたいと書いていました。

案内容懸念
読み込み側で解決する内容ディスクの読み込みと同じく、要求ディレクトリのキー、worktreeのキー、長いパスの接頭辞走査の順に試す懸念書き込み側のキーは変わらない
書き込み側を揃える内容すべての書き込みでproject_key_for_directory(directory)を使い、書き込みと読み込みを構造的に一致させる懸念ミラー書き込みがファイルパスから導くキーが変わる。インポート側のコメントは、ディスク上の名前を意図して選んだと書いている

PR #1327は、前者に近い読み込み側の解決を採りました。ストアに入ったキーを書き換えないので、既存のデータはそのまま引けます。

issueの報告者の一人が、既知のセッションIDを解決する共通の処理を入れたPR #1327を出しています。方針は次のとおりです。

  1. 要求されたディレクトリのキーにすでに存在するものがあれば、それを優先する
  2. なければ、ローカルに登録されているgit worktreeを探して、インポートされたキーに寄せる
  3. フォールバックの一致が複数あれば、任意に選ばず拒否する
  4. resumeは要求したcwdの下に復元し、以降のミラー書き込みは元のキーへ戻す

対象は、読み込み、4つの更新系関数、明示的なresume、その後のミラー書き込みです。PRの説明によれば、修正前のコミットでは60ケースのテストのうち28が失敗し、修正後は通ります。10月6日の追試では、削除済みworktreeのケースを足した64ケースがすべて通過したと書かれています。いずれも合成した履歴を使った検証で、実際のCLIやモデルを動かしたresumeの確認ではないと明記されています。

まだ決まっていない論点

PRはissue全体を閉じるものではないと説明されています。次の3点は対象外です。

  • 一覧とcontinueの集約: list_sessions_from_storeとcontinue_conversationは、単一のproject_keyだけを見る現行の契約のままです。複数のworktreeにまたがって列挙するかどうかは、APIの方針決定が必要とされています
  • 削除済みworktreeの発見: 親リポジトリからは、すでに消えたworktreeを探し出せません
  • 長いパスのハッシュ差: サニタイズ後に200文字を超えるパスでは、CLIとSDKでハッシュの方式が違うため、同じ壁に当たります。別のissue #1170で扱われています

2つ目は、使い捨てのworktreeでエージェントを回す環境で効いてきます。別の参加者が自分のワークステーションで数えた例では、プロジェクトキー62本のうち41本が、1セッションだけを持つworktreeのキーでした。そのうち7本はworktree自体が削除済みで、履歴だけが残っていたといいます。この数値は報告者の1台の環境での値で、issueの報告者は再現していません。

削除済みでも、元のパスを覚えていれば、そのキーは引けます。PRの追試では、実在のworktreeを削除したあとも、元のパスで読み込み・一覧・リネーム・タグ付け・フォーク・削除・resumeの復元ができると確かめています。親リポジトリからの探索は、削除済みのworktreeを見つけなくなります。ただし保存済みのデータは壊れません。キーの導出にディレクトリの実在は要りません。問題は、パスを失ったあとに履歴を見つけ出す手段が無いことです。エージェントを使い捨てのworktreeで回すなら、セッションIDと一緒に実パスを控えておく運用が必要になります。

自分の構成が影響を受けるか

構成影響
worktreeを使わず、同じcwdで書いて読む影響影響なし
worktreeで動かし、worktreeのパスで読む影響影響なし
worktreeで動かし、親リポジトリのパスで読む・再開する影響一覧が空、resumeが黙って新規になる
親リポジトリ経由でリネーム・タグ付け・削除をする影響2本目のキーができる、または削除が効かない
一覧を親リポジトリ全体の単位で出したい影響修正が入っても、集約は別の決定待ち

issueが扱うのはPythonのSDKの*_from_store系の関数です。TypeScript版で同じ現象が起きるかどうかは、issueに記載がありません。

関連する既存の仕様

SessionStoreのドキュメントは、projectKeyが作業ディレクトリのエンコードであるため、ストアからのresumeは元の実行と一致する作業ディレクトリから行うよう求めています。今回の不具合は、この前提が「worktreeでも成り立つ」とは限らないことを示したものです。

ストアを使わない通常のresumeは別の挙動です。Claude Code本体のresumeはバージョン2.1.223以降、現在のプロジェクトディレクトリの外も探すようになりました。それ以前は、現在のプロジェクトディレクトリとそのgit worktreeまでが検索範囲でした。ディスクの検索はこの範囲にworktreeを含んでいたため、ストア側だけが取り残されている形です。バージョン2.1.223より前のCLIを同梱するSDKは、今も当時の範囲で動きます。

ストア経由のresumeの流れはSessionStoreの解説にまとめています。作業ディレクトリの指定が反映されない別の問題は、ClaudeCodeOptionsのcwdが反映されない理由が扱っています。worktreeの作り方や並列運用の落とし穴は、Worktree実践ガイドの領分です。複数ホストでの運用設計は本番ホスティングの記事を見てください。

まとめ

worktreeを使うエージェント基盤でSessionStoreを組むなら、修正が入るまで「書いたパスで読む」を徹底するのが安全です。親リポジトリのパスで読むと、履歴が消えたように見えるだけで、エラーは出ません。更新系も、成功を返しながら別のキーに書く場合があります。

PR #1327がマージされても、一覧の集約と削除済みworktreeの発見は残ります。セッションIDと実パスの対応を自前で持つ設計は、修正後も無駄になりません。

この記事を共有:XはてブLinkedIn