朝の棚卸しスクリプトが、いつもより静かでした。
私は個人開発で、数本のアプリの運用をひとつの Managed Agents セッションに任せております。夜間にクロール状況・課金・問い合わせを見て、朝に要約を置いておく。そのために、プロジェクトごとのメモリストアへ「前回の判断」「触ってはいけない設定」を書き溜めております。
毎朝、そのメモリを一覧して差分を眺めるのが習慣でした。ところがある朝、一覧が拾ってくる件数が減っていました。前日は全プロジェクト合わせて 68 件だったものが、その朝は 39 件。ほぼ半分です。エラーは出ていません。例外も、警告も、何も。ただ静かに、いくつかのプロジェクトのメモリだけが結果から消えていました。
原因は、前夜に上げたベータヘッダーひとつでした。agent-memory-2026-07-22。
メモリストアの一覧を、なぜ自前で叩くのか
Managed Agents のメモリストアは、セッションのコンテナに /mnt/memory/ としてマウントされ、エージェントは通常のファイルツールで読み書きします。ここまでは、エージェントに任せておけば済む話です。
一覧 API を人間側から叩くのは、別の目的のためです。エージェントが書いたものをレビューする。誤った記憶を訂正する。定期的にエクスポートして棚卸しする。私の場合は毎朝の差分確認で、memories.list を path_prefix で絞り込みながらプロジェクト単位に巡回しておりました。
メモリは 1 件あたり 100 kB(およそ 25,000 トークン)が上限で、公式にも「大きな数本ではなく、小さく焦点を絞った多数のファイルに分けよ」と書かれています。私はこれに従い、/projects/alpha/decisions/2026-07.md のように、プロジェクト名を第1セグメントに置く木構造で運用しておりました。この設計が、今回の変更でちょうど急所を突かれた形になります。
agent-memory-2026-07-22 で変わった3点
ベータヘッダーを agent-memory-2026-07-22 に上げると、memories.list の挙動が3点変わります。順に見ていきます。
項目 変更前 変更後(agent-memory-2026-07-22)
一覧の順序 order_by / order で制御サーバ定義の安定順に固定。order_by / order は無視
depth の値任意の整数を受け付けていた 0・1・省略のみ。それ以外は 400
path_prefix の一致部分文字列(substring)一致 末尾スラッシュ必須。パスセグメント単位 の一致
順序固定と depth 制限は、読めば身構えられます。困るのは3つ目です。挙動が「エラーではなく、静かに違う結果」に振れるからです。
直感に反した発見 — 部分一致に頼っていた自分に気づく
私の棚卸しスクリプトは、プロジェクトを跨いで拾うために、こう書いておりました。
# 変更前。これで /projects/alpha/... も /projects/alpha_archive/... も拾えていた
memories = client.beta.memory_stores.memories.list(
memory_store_id = store_id,
path_prefix = "/projects/alpha" , # ← 末尾スラッシュが無い
view = "basic" ,
betas = [ "managed-agents-2026-04-01" ],
)
変更前の path_prefix は部分文字列一致でした。"/projects/alpha" は "/projects/alpha/decisions/2026-07.md" の接頭辞であると同時に、"/projects/alpha_archive/old.md" の接頭辞でもあります。私は当時それを「プロジェクトと、そのアーカイブをまとめて拾える便利な挙動」として、半ば無自覚に頼っておりました。
agent-memory-2026-07-22 では、path_prefix は末尾スラッシュが必須になり、パスセグメント単位で一致します。すると次のことが起きます。
第一に、末尾スラッシュの無い "/projects/alpha" は、もはや意図した範囲を拾いません。セグメント境界に合わないためです。第二に、正しく "/projects/alpha/" と書いた場合、それは /projects/alpha_archive/ を含みません 。alpha と alpha_archive は別セグメントだからです。
つまり、以前は「1回の呼び出しで本体とアーカイブを両方拾えていた」ものが、変更後は「本体しか返らない」に変わります。件数が半減して見えた正体は、これでした。エラーが出なかったのは、path_prefix の指定自体は文法的に正しく、ただ一致範囲だけが静かに狭まったからです。
ここが今回いちばん学びになった点です。破壊的変更のうち最も危ういのは、例外を投げるものではなく、黙って結果が変わるもの です。前者は必ず気づけますが、後者は監視していなければ見過ごします。
破壊的変更に耐えるアクセス層を書き直す
対処方針は3つです。プレフィックスを正規化する。順序に依存しない。木を明示的に歩く。これを踏まえて、棚卸し層を次のように書き直しました。実際に毎朝走っているコードから、鍵などを伏せて抜き出したものです。
from anthropic import Anthropic
client = Anthropic() # ANTHROPIC_API_KEY は環境変数から
BETAS = [ "managed-agents-2026-04-01" , "agent-memory-2026-07-22" ]
def normalize_prefix (prefix: str ) -> str :
"""path_prefix は必ず末尾スラッシュで終える。ルートは "/" に統一。"""
if not prefix.startswith( "/" ):
prefix = "/" + prefix
if not prefix.endswith( "/" ):
prefix = prefix + "/"
return prefix
def list_all_memories (store_id: str , prefix: str = "/" ) -> list[ dict ]:
"""
prefix 配下のメモリを全件返す。
- 返却順に依存しない(呼び出し側で必ずソートする)
- depth は 0/1/省略のみ許されるため、木は自分で再帰的に歩く
"""
prefix = normalize_prefix(prefix)
collected: list[ dict ] = []
cursor: str | None = None
while True :
page = client.beta.memory_stores.memories.list(
memory_store_id = store_id,
path_prefix = prefix,
depth = 1 , # 直下の子だけを取り、サブツリーは再帰で辿る
view = "basic" , # メタデータのみ。本文は必要な時だけ retrieve
after_id = cursor,
betas = BETAS ,
)
for item in page.data:
if item.type == "memory_prefix" :
# ディレクトリ相当のノード。1 段掘り下げる
collected.extend(list_all_memories(store_id, item.path))
else :
collected.append({ "id" : item.id, "path" : item.path})
if not getattr (page, "has_more" , False ):
break
cursor = page.last_id
# サーバ順は保証されないので、比較・差分のために自前で安定ソートする
collected.sort( key =lambda m: m[ "path" ])
return collected
ここでの要点を、コードの外側で補足しておきます。
まず depth=1 に固定し、memory_prefix(ディレクトリ相当のノード)に当たったら自分で再帰する形にしました。以前は大きな depth を渡して一気に取っていましたが、その値はもう 400 になります。木を自分で歩けば、深さの制限に縛られず、しかも各段で件数を把握できます。
次に、順序を一切あてにしていません。取得後に path で安定ソートしています。前日との差分は「順番」ではなく「集合」として比較すべきで、そう組んでおけばサーバ順が変わっても壊れません。
そして本文は view="basic" で取りません。棚卸しに要るのはパスと ID だけです。中身が要る時だけ、対象を絞って memories.retrieve します。100 kB ×多数を毎朝ダウンロードするのは、時間の面でも料金の面でも無駄でした。
「本体とアーカイブをまとめて」を、意図として書き直す
以前の部分一致に頼っていた「本体もアーカイブも拾う」挙動は、便利ではありましたが、意図が暗黙でした。移行を機に、これを明示的な列挙へ書き直しました。
def list_project_including_archive (store_id: str , project: str ) -> list[ dict ]:
"""プロジェクト本体とアーカイブを、意図的に別プレフィックスとして合算する。"""
live = list_all_memories(store_id, f "/projects/ { project } /" )
archive = list_all_memories(store_id, f "/projects/ { project } _archive/" )
return live + archive
書いてみて、以前のコードがいかに危うかったかがよく分かりました。alpha と名の付くプロジェクトが増えれば、部分一致は alpha2 や alpha_experimental まで巻き込んでいたはずです。セグメント一致は不便になったのではなく、私の曖昧な意図を明示させてくれたのだと、今は受け止めております。
安全な訂正 — content_sha256 プリコンディション
棚卸しの目的の半分は「誤った記憶の訂正」です。ここは仕様変更とは別に、最初から気をつけるべき点があります。エージェントと人間が同じメモリを触るため、上書き競合が起こり得ることです。
memories.update には楽観的並行制御があります。読んだ時の content_sha256 を渡し、保存側のハッシュがまだ一致する時だけ適用させます。
def safe_correct (store_id: str , memory_id: str , new_content: str ) -> bool :
current = client.beta.memory_stores.memories.retrieve(
memory_store_id = store_id, memory_id = memory_id, betas = BETAS ,
)
try :
client.beta.memory_stores.memories.update(
memory_store_id = store_id,
memory_id = memory_id,
content = new_content,
precondition = {
"type" : "content_sha256" ,
"content_sha256" : current.content_sha256,
},
betas = BETAS ,
)
return True
except Exception :
# ハッシュ不一致 = 誰か(多くはエージェント自身)が先に書いた。
# 黙って上書きせず、再読込して判断し直す
return False
夜間にエージェントが同じファイルへ追記していることは、実際にありました。プリコンディション無しで訂正していた頃は、その追記をたまに踏み潰していたはずです。すべての変更は不変の memory version(memver_...)として残るため、事後に気づけはしますが、踏まないに越したことはありません。
状況別の指針
移行にあたって迷いやすい判断を、状況別に整理しておきます。
状況 推奨
プレフィックスで範囲を絞っている すべての path_prefix を末尾スラッシュ付きに正規化。部分一致に頼っていた箇所を洗い出す
order_by で並びを制御していた取得後に自前でソート。ページングは after_id ベースへ寄せ、順序前提のロジックを外す
深い木を一括取得していた depth=1 + 再帰へ。memory_prefix ノードを辿る形に統一
人間とエージェントが同じ store を書く 訂正系は必ず content_sha256 プリコンディション。参照専用の store は read_only で attach
最後にひとつ。ベータヘッダーを上げた直後は、件数の総量を1度スナップショットしておくことをお勧めします。私が半減に気づけたのは、たまたま前日の件数を手元に残していたからでした。静かに変わるものは、静かに観測しておくしかありません。
メモリの仕様変更は、長期運用ほど効いてきます。今回の path_prefix の件も、木構造を素直に設計していた運用ほど深く刺さりました。設計思想の背景をもう少し掘りたい方は、Claude エージェントのメモリ運用でつまずいた点と対処 も合わせてご覧いただければと思います。
私自身、まだ手探りの部分が多い領域です。同じように静かな件数の変化に戸惑った方の、確認の手がかりになれば嬉しく思います。お読みいただき、ありがとうございました。