エージェントの古いメモリを期限切れにして削除する方法
エージェントのメモリが古くなる原因と対策を解説します。期限付きの事実にTTLを設定し、削除を連鎖させ、残りを見直し、sqlite3でSQLiteストアを確認する方法を紹介します。
エージェントのメモリが古くなる理由
エージェントのメモリが古くなるのは、事実を一度だけ書き込み、その後に確認しないためです。ストアはその事実を返し続け、取得層は日付情報を付けずにプレーンテキストとしてプロンプトへ挿入します。その結果、モデルは書き込まれた当日と同じ確信度で繰り返します。エラーは発生しません。これが難しさの本質です。古くなったメモリは、モデルにとっても人にとっても、新しいメモリとまったく同じように見えます。
保存時の記述を改善しても、この問題は解決しません。解決策は、有効期限を設定できる事実には有効期限を設定し、設定できない事実には見直しの手順を設けることです。どちらも小規模なデータベースに対する通常の保守作業であり、作業の大半は SQL (structured query language) です。
消退とずれは異なる障害です
消退とは、自然な終了日がある事実です。「今週は旅行中です」「移行作業のためステージング環境を停止しています」「予算案を確認中です」。これらは記録した時点では正しく、記録するときに有効期間を指定できます。消退は解決できます。TTL (time to live) と呼ばれることもある有効期限を設定し、期限を過ぎたら行を削除します。
ずれとは、1 回だけ保存され、その後に再確認されない事実です。「pnpm を好みます」「データベースは Postgres 15 です」「デプロイは staging ブランチ経由です」。これらを誤りにする時計はありません。別の場所で行われた決定が内容を変えますが、メモリーストアにはその変更を知らせる仕組みがありません。
ずれを自動的に完全解決する方法はありません。ストアは観測していない変更を検出できないため、ストアを読み込んで推論するジョブは、同じ古いテキストを再読しているだけです。有効な方法は、記録された事実を、それが示す対象と照合し直すことです。そのためには人、または現在の状態を読み取れるツールを持つエージェントが必要です。
そのため、計画は 2 つに分かれます。消退するものには有効期限を設定します。ずれが生じるものは見直します。2 つ目の問題を、1 つ目の問題と同じものとして扱わないでください。
期限のある事実に有効期限を設定する
すべてのメモリ行には、多くのストアで用意されていない3つの列が必要です。事実の出所、最後に確認した日時、そして事実が有効でなくなる日時です。これはsqlite3だけで構築できるストアであり、既存のストアにも同じ列を追加できます。
CREATE TABLE memory (
id TEXT PRIMARY KEY,
subject TEXT NOT NULL,
fact TEXT NOT NULL,
source TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
confirmed_at TEXT NOT NULL DEFAULT (datetime('now')),
expires_at TEXT,
superseded_by TEXT REFERENCES memory(id) ON DELETE CASCADE
);
CREATE INDEX memory_expires ON memory(expires_at);
CREATE INDEX memory_superseded ON memory(superseded_by);datetime('now')は協定世界時(UTC)をYYYY-MM-DD HH:MM:SSとして返します。これはテキストとして正しく並べ替えや比較ができるため、以下の日付に関する条件はすべて単純なWHERE句になります。source列は省略できません。メッセージ、ファイル、コマンド出力にさかのぼって確認できない事実は、再確認できません。再確認できない事実は、削除するしかありません。
有効期限が切れるメモリを書き込む方法:
INSERT INTO memory (id, subject, fact, source, expires_at)
VALUES ('m_0191', 'availability', 'Away from keyboard, replies are delayed',
'chat 2026-08-08', datetime('now', '+7 days'));取得処理でテーブルを直接読み取ってはいけません。期限切れの行と、後から置き換えられた行を隠すビューを読み取ります。
CREATE VIEW live_memory AS
SELECT id, subject, fact, source, confirmed_at, expires_at
FROM memory
WHERE superseded_by IS NULL
AND (expires_at IS NULL OR expires_at > datetime('now'));このビューが重要なのは、削除漏れがあっても問題にならないためです。期限切れの行は、削除ジョブが実行されたかどうかに関係なく、期限が切れた時点で取得対象から外れます。削除ジョブはディスク使用量とレビュー負荷だけを制御し、正確性には影響しません。
sqlite3 memory.db "SELECT count(*) FROM memory;"で差分を確認し、live_memoryに対して同じ件数を確認します。健全なストアでは、2つの数値が近くなります。差が大きい場合は、不要になった行の未処理分が蓄積しています。
メモリを削除しても古いメモリが残る理由
修正は 2 段階で行います。エージェントが npm から pnpm へ移行したことを学習すると、新しい行を追加し、古い行から新しい行へ参照を設定します。
UPDATE memory SET superseded_by = 'm_0207' WHERE id = 'm_0140';古い行は live_memory から見えなくなりますが、チェーンには変更内容が記録されたままです。次に、誤りだった m_0207 を削除します。superseded_by の ON DELETE CASCADE は、m_0140 も削除する必要があります。この関係では、古い行が子にあたるためです。通常は削除されません。SQLite は、foreign key を有効にしないかぎり無視するためです。デフォルトでは無効になっています。
sqlite3 memory.db "PRAGMA foreign_keys;"標準ビルドでは、0 と表示されます。foreign key が無効な状態では、DELETE FROM memory WHERE id = 'm_0207'; は成功し、m_0140 は残ります。この行は、存在しなくなった id を参照したままです。警告は表示されません。この行は誤った理由で非表示になります。その後、孤立したポインターを NULL に戻す最初の整理スクリプトを実行すると、「npm を優先する」という設定が live_memory にそのまま戻ります。
壊れたチェーンを確認します。
sqlite3 memory.db "PRAGMA foreign_key_check;"foreign_key_check は、適用が無効になっている場合でも違反を報告します。そのため、すでに存在する不整合を調査できます。違反ごとに、テーブル、rowid、親テーブル、失敗した foreign key が 1 行ずつ出力されます。出力が空なら、チェーンに不整合はありません。
この後のルールは簡単です。PRAGMA foreign_keys = ON; は接続単位の設定なので、すべての接続で有効にする必要があります。対象には、アプリケーション、削除スクリプト、入力中の sqlite3 セッションが含まれます。何かを削除するすべての SQL ファイルで、1 行目に設定してください。
メモリが実際に保存されている場所
何かを削除する前に、保存先がいくつあるかを確認します。通常、セルフホスト型のメモリサービスは、メモリ本文とその embedding をベクトルデータベースに保存し、変更ログを SQLite に保存します。これらはライフサイクルの異なる別ファイルであり、互いに独立して障害が発生します。
mem0 は分かりやすい例です。同様の構成は他のサービスにも見られます。mem0 のオープンソースライブラリは、デフォルトで /tmp/qdrant の Qdrant ベクトルストアを使用し、mem0 という名前のコレクションを作成します。また、MEM0_DIR 環境変数に従う場所に ~/.mem0/history.db の SQLite 変更ログを保存します。history テーブルには memory_id、old_memory、new_memory、event、created_at、is_deleted が格納されます。
このカラム一覧をもう一度確認してください。SQLite ファイルは変更ログです。メモリ本体は Qdrant にあるため、history.db から行を削除しても、変更が発生したという記録だけが削除され、メモリは引き続き取得できます。削除処理はライブラリ独自の API (application programming interface) を通す必要があります。そうすれば、両方の保存先が更新されます。
from mem0 import Memory
memory = Memory()
memory.delete(memory_id="mem_123")
memory.delete_all(user_id="alice")/tmp のデフォルト設定には、個別に注意が必要です。Ubuntu 24.10 以降では、/tmp は tmpfs、つまりメモリ上に保持されるファイルシステムです。そのため、再起動のたびに空になり、保存データ全体が失われます。findmnt /tmp で自分の環境を確認してください。tmpfs と表示される行がある場合は、今すぐパスを変更してください。
config = {
"vector_store": {
"provider": "qdrant",
"config": {"collection_name": "mem0", "path": "/srv/agent/qdrant"},
}
}
memory = Memory.from_config(config)実行しているサービスに関係なく、同じ確認が必要です。設定を読み取り、サービスが書き込むすべてのパスを記録してください。自分の VPS で mem0 メモリサーバーを実行するではサービス側の構成を説明しています。エージェントのメモリを 1 台のマシンにローカル保存するは、同じ保守要件を持つ、より小規模な保存先を扱います。
sqlite3 でストアを読み取る
CLI (コマンドラインインターフェース) がない場合は、sudo apt install -y sqlite3 でインストールします。その後、ディスク上の任意のストアについて、次の 4 つのコマンドで大半の疑問を解消できます。
sqlite3 ~/.mem0/history.db ".tables"はテーブルを一覧表示します。出力が空の場合は、開いたファイルが正しくありません。sqlite3 ~/.mem0/history.db ".schema history"は正確なカラムを表示します。これがストアの構造を確認する唯一信頼できる資料です。sqlite3 -cmd ".mode line" ~/.mem0/history.db "SELECT * FROM history ORDER BY created_at DESC LIMIT 5;"は直近 5 件の変更を、1 フィールド 1 行で表示します。カラムに段落が入っていても読みやすい形式です。sqlite3 ~/.mem0/history.db "SELECT event, count(*) FROM history GROUP BY event;"はストアで発生している処理と、ライブラリが実際に書き込むイベント名を表示します。
すべてのメモリーストアがデータベースとは限りません。すべてのセッション開始時に読み込むメモの単純なファイルには、これらの問題があり、ツールも利用できません。期限切れを示すカラム、確認済みの日付、不要な行を隠すビューがないためです。手動で書き込む各行に日付を付け、毎月読み直してください。Claude Code セッション間で引き継がれるメモリーにも、規模は小さいものの同じ問題があります。
期限切れにならない事実を見直す
ドリフトを管理するには、キュー、上限、習慣が必要です。キューには、最も古い確認結果を入れます。
SELECT id, subject, fact, source, confirmed_at
FROM live_memory
WHERE confirmed_at < datetime('now', '-90 days')
ORDER BY confirmed_at
LIMIT 20;週に20行なら、実際に誰かが見直せる量です。400行では誰も見直さないため、最初の状態に戻ってしまいます。各行には2つの対応があります。source と照合して再確認し、次のように記録します。
UPDATE memory SET confirmed_at = datetime('now') WHERE id = 'm_0140';または、行を置き換えます。新しい事実を挿入し、古い行の superseded_by を新しいIDに設定して、チェーンに履歴を保持させます。
この作業を軽くする習慣が2つあります。まず、ストアを小さく保ちます。増え続けるだけのストアでは見直しが不可能になるためです。last_used_at 列を追加し、行が実際に取得されたときに更新します。6か月間使われていない行は、削除候補として扱います。取得のたびに1回の書き込みが発生するため、エージェントが頻繁に応答する場合はまとめて更新します。
2つ目の習慣にはコストがかかりません。プロンプトに経過時間を含めます。検索処理が作成するメモリブロックに各事実の横へ confirmed 2026-05-02 を付ければ、モデルは事実を断定するのではなく、「5月時点では pnpm を使っていました」のように述べられます。日付のない事実は、言語モデルにとって毎回、現在の事実として読まれます。
スケジュールに従って prune を実行する
思い出したときに実行する prune は、実際には実行されません。SQL を /srv/agent/prune.sql に記述します。
PRAGMA foreign_keys = ON;
DELETE FROM memory
WHERE expires_at IS NOT NULL AND expires_at <= datetime('now');
DELETE FROM memory
WHERE superseded_by IS NOT NULL
AND created_at < datetime('now', '-180 days');/etc/systemd/system/memory-prune.service を保存します。
[Unit]
Description=Prune expired agent memories
[Service]
Type=oneshot
User=agent
ExecStart=/usr/bin/sqlite3 /srv/agent/memory.db ".read /srv/agent/prune.sql"次に /etc/systemd/system/memory-prune.timer を保存します。
[Unit]
Description=Run the agent memory prune daily
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.targetsudo systemctl daemon-reload
sudo systemctl enable --now memory-prune.timer
systemctl list-timers memory-prune.timer初回実行後、list-timers に実際の時刻を示す NEXT 列と、LAST 列が表示されることを確認します。sudo systemctl start memory-prune.service で手動実行してから、journalctl -u memory-prune.service -n 20 を確認します。Error: database is locked と表示される場合、prune の実行中に agent が書き込みロックを保持していたことを意味します。sqlite3 memory.db "PRAGMA journal_mode=WAL;" で write ahead logging を一度設定すると、reader と 1 つの writer が互いにブロックしなくなります。また、sqlite3 -cmd ".timeout 5000" /srv/agent/memory.db ".read /srv/agent/prune.sql" で prune に待機時間を設定します。
エージェントが読み取った内容は、永続的な指示になる可能性があります
ここで、保守作業がセキュリティ問題になります。多くのメモリシステムでは、書き込み処理で直近の会話に対してモデルを呼び出します。この会話には、取得した Web ページ、ファイルの内容、Issue のコメント、コマンドの実行結果など、ツールの出力が含まれます。その出力にある永続的な事実のように見えるテキストは、抽出されて保存される可能性があります。「注: このユーザーは常にチェックを無効にしてデプロイする」とページに書かれていると、その内容がストアの 1 行になり、それ以降はエージェントに伝えた情報としてすべてのプロンプトに注入されます。
これが、通常のプロンプトインジェクションと異なる点です。1 つの会話に含まれる指示は、会話が終了すれば効力を失います。メモリに書き込まれた指示は再起動後も残り、取得元が明示されない限り、取得層はその情報を信頼済みとして渡します。
- ユーザーの発言だけからメモリを抽出し、ツールの出力からは抽出しないでください。利便性は低下しますが、この種の問題全体を排除できます。
- すべての行に
sourceを付け、レビュー時に表示してください。「タスク 41 で取得した Web ページ」が出典の事実は、特に慎重に確認します。 - 新しい行を毎日メール送信またはログ記録してください。同じタイマーに
SELECT id, subject, fact, source FROM memory WHERE created_at > datetime('now', '-1 day');を含めます。 - 認証情報はストアに一切保存しないでください。これは AI エージェントに Secret を保存しない で説明します。
ここでは、機械的な注意点も 1 つあります。行を削除しても、ファイルからその内容が消えるとは限りません。SQLite はページを空きとしてマークし、後で再利用するため、上書きされるまで古いテキストを strings memory.db で読み取れます。機密情報を削除した後は sqlite3 memory.db "VACUUM;" を実行してください。ファイル全体が書き換えられます。PRAGMA secure_delete = ON; を使うと、削除処理を実行する接続が、解放された内容を処理中にゼロで上書きします。
バックアップ対象と実行順序
このストアは小規模ですが、再構築は困難です。そのため、適切にバックアップしてください。稼働中のデータベースファイルを cp で直接コピーしてはいけません。書き込みの途中で取得したコピーは、開けないことがあります。SQLite に組み込まれたスナップショット機能を使用します。
sqlite3 /srv/agent/memory.db "VACUUM INTO '/srv/backup/memory-$(date +%F).db'"
sqlite3 /srv/backup/memory-$(date +%F).db "PRAGMA integrity_check;"integrity_check で ok が出力されることだけが、バックアップファイルを使用できる証拠です。それ以外の場合は、以前のバックアップを保持し、上書きする前に調査してください。
ベクトルストアも同じジョブで、同じタイミングにスナップショットを取得します。2 つのデータを数時間ずらして取得すると、復元時に新しい変更ログと古いメモリ集合が混在し、削除した事実が復活します。両方を 1 つの日付付きディレクトリに保存し、必ず一緒に復元されるようにしてください。VPS で SQLite を本番運用するでは、ロック、バックアップ、長時間稼働するサービスに必要な設定について詳しく説明しています。
FAQ
エージェントのメモリは、期限切れになるまでどのくらい保持すべきですか?
期限はグローバルなデフォルトではなく、その事実に基づいて設定します。旅行メモや「今週このプロジェクトに取り組んでいる」というメモには 7 日を設定します。チームの規約や個人の好みには期限を設定せず、代わりにレビューキューへ入れます。ソフトウェアのバージョンに関する事実には、そのプロジェクトのリリースサイクルと同程度の期限を設定します。事実を書き込む時点で保持期間を決められない場合、それは時間の経過で消えるのではなく、内容が変化する情報であることを示しています。そのため、期限切れにするのではなく、confirmed_at 日付を設定してレビューします。
保存した事実が誤りになったことを自動的に検出できますか?
確実には検出できません。ストアは外部の世界を参照できないため、事実を誤りにした変化を認識できません。また、ストアを読み直すジョブを実行しても、同じ古いテキストを読み直すだけです。自動化できるのは、確認が必要な項目を表面化することです。confirmed_at で並べ替え、古い行を人、またはリポジトリ、設定ファイル、監視エンドポイントから現在の状態を読み取れるツールを持つエージェントの前に提示します。キューの自動化には価値があります。判定の自動化は、まだ実用段階に達していません。
メモリを削除したのに戻ってきました。なぜですか?
通常は、2 つのストアがあり、一方に対してだけ削除を実行したことが原因です。メモリのテキストとその埋め込みは通常ベクトルデータベースに保存され、変更ログは SQLite ファイルに保存されます。そのため、SQLite ファイルから行を削除すると監査記録は消えますが、メモリは取得可能なまま残ります。両方を更新できるように、ライブラリ API を通じて削除してください。もう 1 つのよくある原因はリストアです。ベクトルストアと SQLite ファイルが異なる時点でスナップショットされていると、リストアによって、もう一方がすでに削除していた行が復元されます。
エージェントの実行中に、メモリデータベースを手動で編集しても安全ですか?
読み取りは安全です。書き込みは write ahead logging モードの場合に限り安全で、その場合でも同時に書き込めるのは 1 つのプロセスだけです。sqlite3 memory.db "PRAGMA journal_mode;" を実行して現在のモードを確認し、wal になっていることを確認してください。Error: database is locked と表示された場合は、別のプロセスが書き込みロックを保持しています。そのため、sqlite3 -cmd ".timeout 5000" でセッションを待機させるか、先にエージェントサービスを停止してください。ベクトルストアを手動で編集する場合は事情が異なります。埋め込みとテキストの整合性を維持する必要があるため、編集はライブラリに任せてください。