TL;DR
- エージェントの memory を「会話履歴をどう圧縮するか」の問題として設計すると、長期運用では必ず詰まります。運用で効くのは会話ではなく、検証済みの知識をどう置き、どう捨てるかです。この記事はそれを schema / provenance / expiry / rollback の4軸で扱います。
- 同じリポジトリにある知識ストア3つを同じ4軸で測りました。結果ははっきり割れています。機械が読む列は 13 行すべてで埋まり、誰も読まない列は 31 件中 25 件が空でした(
社内データ、2fead50時点)。 - 一番腐っていたのは
expiryです。AGENT_LEARNINGS.mdはlast_verifiedを書式に持っているのに、実際に付いているのは 31 件中 6 件、90 日以内という基準で生きているのは 3 件(9.7%) でした(社内データ)。 - 腐り方には規則性がありました。後継が構造的に生まれる知識は期限が付き、後継が生まれない知識には一切付いていません。セッションのスナップショットは 7 件中 5 件が
[archived]と印されている一方、単発のルールは 26 件中 0 件でした(社内データ)。書き手の規律ではなく、後継の有無で決まっています。 provenanceについては、このリポジトリに機械が読める形が 1 つだけありました。レビュー結果の 1 行目に本文の SHA-256 を書き、使う直前に現在のハッシュと突合する仕組みです。「検証していない」をnot-applicableという明示値で持つのがポイントで、空欄と区別できます。rollbackは契約として書かれていますが、適用実績は 0 件でした。設計してあることと効いていることは別で、この記事では効いていると書きません。- 第 8 節に、手元の知識ストアの
expiry実効カバー率を測る POSIX シェルスクリプトを置きます。検査対象 0 件を合格と同じ出力にしないことを終了コードで分けてあります。ただしこのスクリプトが見るのは expiry の1軸だけで、schema / provenance / rollback は見ません。
はじめに:memory の話が「圧縮」から始まると失敗する
AI エージェントに長い仕事をさせようとすると、だいたい最初に memory の話になります。コンテキストが溢れる、前回の会話を覚えていない、要約すると細部が落ちる。そこで会話履歴の圧縮や検索の話に進みます。
半年ほど同じリポジトリでエージェントを運用してみて分かったのは、長期運用で効いてくるのは会話ではないということでした。効くのは「この書き方をすると CI が落ちる」「このガードは実は発火していない」といった、一度検証して結論が出た知識のほうです。会話履歴は捨てても翌日また作れますが、検証済みの知識は捨てると再検証コストがそのままかかります。
この2つは性質が違うので、置き場所も違います。
| conversational memory | operational memory | |
|---|---|---|
| 中身 | 直近のやりとり、作業の途中経過 | 検証して結論が出た知識、規約、失敗の記録 |
| 寿命 | セッション内、長くて数日 | 数か月〜。ただし黙って腐る |
| 失われたときのコスト | 低い(作り直せる) | 高い(再検証が要る) |
| 主な設計課題 | 圧縮・検索・想起 | どう捨てるか |
後者を operational memory と呼びます。そして後者の設計課題は、貯め方ではなく捨て方です。貯めるだけなら誰でもできますが、間違った知識が残り続けるストアは、無いストアより悪くなります。
この記事は一般論ではなく、このリポジトリの実物を測った記録です。組織としてナレッジをどう管理すべきかという提言はしません。基準は 2fead50(2026-09-08)で、数値はすべてその時点のものです。
なお、ツール固有の memory 階層の話は Claude Codeのセッションメモリ運用パターン、コンテキストが溜まって重くなる側の話は AI開発のコンテキスト負債 で扱っています。本記事はツールに依存しない形で、知識そのものの lifecycle に絞ります。
1. 知識の lifecycle を4つの軸で書く
operational memory を設計するときに決めることは、突き詰めると次の4つでした。
| 軸 | 決めること | 決めないとどうなるか |
|---|---|---|
| schema | 1件を何の列で持つか。値域は何か | 自由記述が混ざり、機械が読めなくなる |
| provenance | その知識が何を根拠に、いつの何を見て確定したか | 反証しようがなくなる。誰も消せない |
| expiry | いつ無効になるか。誰が再検証するか | 古い結論が現役のまま参照される |
| rollback | 間違っていたと分かったとき、何を戻すか | 撤回できず、訂正を上書きで積むしかなくなる |
言葉にすると当たり前ですが、実際に運用しているストアを開くと、この4つの埋まり方はまったく均等ではありません。次節でそれを数えます。
軸の言葉づかいは、データ来歴のモデルとして標準化されている W3C PROV-DM の entity / activity / agent の分け方に寄せてあります。エージェントの知識も「誰が、どの活動で、何を根拠に作った entity か」で書けると、あとから機械で辿れます。
2. 同じリポジトリの3ストアを同じ軸で測る
このリポジトリには、性格の違う知識ストアが3つあります。全部同じ人間とエージェントが書いています。
AGENT_LEARNINGS.md— 再利用できる学びを書式付きで貯めるファイル。2026-04-03 から運用spec/article_failure_ledger.md— 記事制作でのすり抜けを1行1観測で貯める台帳。2026-09-08 から運用- エージェントが毎回読む memory 索引(
MEMORY.mdと、そこから張られた個別メモ)— 1 行 1 エントリの自由記述。日付管理なし
測ったコマンドと結果です。
# 1. AGENT_LEARNINGS.md の件数と、期限フィールドの有無
grep -cE '^- date: [0-9]{4}-' AGENT_LEARNINGS.md # => 31
grep -c 'last_verified:' AGENT_LEARNINGS.md # => 7(うち1件は書式サンプル)
grep -n 'superseded_by' AGENT_LEARNINGS.md # => 11 行目と 24 行目の 2 件だけ
# 11 行目は「使い方」の説明文、24 行目はコードフェンス内の書式サンプル。
# つまり 31 件の実エントリでの使用は 0 件。
# 2. 台帳の schema 検証(値域・必須列・重複・集計の一致を機械で見る)
python3 scripts/check_failure_ledger.py; echo "exit=$?"
2 の出力はこうなります(件数は台帳が伸びれば変わります。2fead50 時点の値です)。
検査対象: spec/article_failure_ledger.md の観測行 13 件(列 9 個 / escape_mode 値域 9 種 / status 値域 5 種)
昇格漏れ検査: status:open 4 件 / escape_mode 4 種を集計しました(閾値: 同一 escape_mode 2 件以上)
(ここに mode 別の内訳が 4 行続く。件数は変わるので転記しない)
failure ledger check passed.
exit=0
3つを4軸で並べると、次のようになりました(すべて 社内データ)。
| 軸 | AGENT_LEARNINGS.md(31 件) | 失敗台帳(13 行) | memory 索引(40 件) |
|---|---|---|---|
| schema | 書式は定義済み。機械検証なし | 9 列・値域・重複を check_failure_ledger.py が検証 | 定義なし(1行の自由記述) |
| provenance | evidence: が 31/31 で埋まっている | evidence 列が 13/13。PR 番号かファイルパス | リンク先ファイルのみ |
| expiry | last_verified: が 6/31。90 日基準で生きているのは 3/31 | 列そのものが無い | [archived] 等の印が 6/40 |
| rollback | superseded_by: を書式に持つが、実エントリでの使用 0 件 | status: rolled_back + revert_pr を定義済み。適用 0 件 | 仕組みなし |
読み取れることが3つあります。
(1)schema と provenance は放っておいても埋まる。 evidence: は 31/31、台帳も 13/13 です。書いている最中は根拠が手元にあるので、書く動機が自然に働きます。
(2)機械が読む列だけが 100% になる。 台帳の必須列が全行埋まっているのは規律のおかげではなく、埋めないと check_failure_ledger.py が非 0 で落ちて PR が通らないからです。同じ人間が書いている AGENT_LEARNINGS.md の last_verified は 19% です。差は書き手ではなく、読み手が機械かどうかにあります。
(3)台帳には expiry の列が無い。 schema が機械検証されていても、そもそも列が無ければ期限は管理できません。schema が厳格であることと lifecycle が設計されていることは別です。
3. expiry だけが腐る理由は「後継の有無」だった
19% という数字だけ見ると「書き手が怠けた」という話に見えます。実際には、付いているものと付いていないものにきれいな分かれ方がありました。
memory 索引 40 件の内訳を数えます。
M="$HOME/path/to/memory/notionnext-blog" # 自分の memory ディレクトリに読み替える
grep -c '^- \[' "$M/MEMORY.md" # => 40 索引に載っているエントリ数
ls "$M" | grep -c '^project_session' # => 7 セッションのスナップショット
ls "$M" | grep -c '^feedback_' # => 26 単発の運用ルール
grep -n '\[archived\]\|【解決済】' "$M/MEMORY.md" | wc -l # => 6
[archived] が付いていた 6 件の内訳は、セッションのスナップショットが 5 件、解決済みの課題が 1 件でした。つまり次のようになります(社内データ)。
| 知識の種類 | 件数 | 期限の印が付いている | 割合 |
|---|---|---|---|
| セッションのスナップショット | 7 | 5 | 71% |
単発の運用ルール(feedback_*) | 26 | 0 | 0% |
スナップショットに印が付くのは、書き手が真面目だからではありません。次のスナップショットを書いた瞬間に、前のものが古いと構造的に確定するからです。後継が生まれると、前任を [archived] にする動機と根拠が同時に手に入ります。
単発のルールには後継が生まれません。「この操作は権限で弾かれる」という知識は、権限設定が変わった日に無効になりますが、その日に誰も知識ベースを開きません。だから 26 件中 0 件です。
ここから出てくる設計上の判断は1つです。このリポジトリの 33 件を見た範囲では、後継が構造的に生まれない知識に対して、時間で切る期限以外の無効化の経路が見つかりませんでした。 そして期限フィールドを置いただけでは 19% にしかならないので、それを読んで落ちる検査を1本置くところまでが expiry の設計になります。
この形の対策には先例があります。Software Engineering at Google の第10章 には、ドキュメントに "freshness dates" を付ける運用が書かれています。最終レビュー日を記録しておき、一定期間(例として3か月)触られていなければメタデータ側からリマインドを送る、という形です。期限フィールドと、それを読む機械の両方が揃っています。
一方、エージェントの記憶階層をどう分けるかという議論は MemGPT の論文 や RAG の原論文 が扱っていますが、いずれも主な関心は「どう思い出すか」です。運用で問題になるのは、思い出せてしまう古い知識のほうでした。運用知識が本番の変化と同じ速度で陳腐化することは The Site Reliability Workbook の On-Call 章 でも playbook の維持という文脈で触れられています。
4. provenance は「いつの何を見たか」を content hash で持つ
evidence: が 31/31 埋まっていると書きましたが、これは「何を根拠にしたか」であって「いつの何か」ではありません。参照先のファイルが後で変わると、根拠は静かにずれます。
このリポジトリで、そこを機械が読める形にしていた例が1つだけありました。レビュー結果の成果物です。spec/article_working_artifacts.md が、レビューの 1 行目に次のヘッダを置くよう定めています。
やっていることは単純で、レビュー時点の本文の SHA-256 を書いておき、そのレビューを判断根拠に使う直前に現在の本文のハッシュと突合します。一致しなければ stale として扱い、合格の根拠には使いません。
shasum -a 256 content/posts/agent-operational-memory-lifecycle/index.md | cut -d' ' -f1
設計として効いているのは、値域に not-applicable という第3の値があることです。本文がまだ存在しない企画段階のレビューは、本文のハッシュを取れません。ここで「空欄」を許すと、空欄が「検証した」と「そもそも見ていない」の両方を意味してしまいます。not-applicable を明示値にすることで、本文の合格根拠には使えないレビューを機械的に区別できます。
これは expiry にもそのまま効く考え方です。期限フィールドが空のとき、それは「まだ有効」ではなく「検証したことが無い」です。第 8 節のスクリプトでも、欠落と期限切れを別に数えています。
5. rollback は書いてあるが、まだ一度も使われていない
4軸のうち rollback は、契約としては一番よく書けています。台帳の status に rolled_back があり、revert_pr 列があり、spec/article_retrospective_loop.md には撤回したら根拠となった観測行を open に戻す、と書かれています。撤回しても失敗が消えたわけではないからです。
そのうえで、実測はこうです。
# ファイル全体では 2 件ヒットするが、どちらも本文の説明行
grep -c 'rolled_back' spec/article_failure_ledger.md # => 2
# データ行(`| L-…` で始まる観測行)に絞ると 0 件
grep -c '^| L-.*rolled_back' spec/article_failure_ledger.md || true # => 0
適用実績は 0 件でした。台帳の運用開始が 2026-09-08 で、この記事を書いている時点でまだ1日しか経っていないので、当然といえば当然です。
ここを「rollback が設計されている」と書いて終わりにしないことが、この記事で一番言いたいことに近いです。設計してあることと効いていることは別で、効いているかどうかは適用実績の件数でしか分かりません。0 件のものは「未検証」と書きます。
rollback を効かせるための前提は1つだけ機械的に効きます。撤回対象を単独の変更として入れておくことです。このリポジトリでは、改善の実装を記事の変更と同じ PR に混ぜないと決めています。混ざると git revert 1手で戻せず、撤回手順が文章だけになるからです。
6. 知識が「規約」に昇格しても穴は残る
貯めた知識の一部は、放っておくと腐るので規約に昇格させます。このリポジトリでは spec/guard_self_check.md の R-1 から R-7 がそれにあたり、ガードが実際に効いているかを検証するための規約が7本並んでいます。
ただし、この昇格には見落としやすい性質が2つあります。
(1)規約は事故のあとにしか増えない。 R-1 から R-7 は、それぞれ実際に起きた事故を根拠に事後追加されたものです。spec/article_retrospective_loop.md には「なぜ Harness をすり抜けたか」の型が並んでいますが、R-1 から R-7 が覆っていない型が現時点でも残っています。しかも、その対応関係を書いた表自体が「R-1〜R-7 は事故のたびに事後追加されたもので、次の失敗を先回りしていない」と自分で明言しています。最新の対応状況は同ファイルを直接読んでください(本記事に件数を転記すると、SSoT が更新された瞬間に記事が嘘になります。これは記事の後半で扱う転記ドリフトそのものです)。
(2)知識の入口そのものを誰も検査していない。 同じ文書の「機械検証が届いていない範囲」という節に、既知の穴が2つ明示されています。片方は、起票されなかった失敗は台帳を数えても現れないという構造的な限界です。台帳の内側を見るガードは、台帳に無い行を検出できません。実際に、確定した観測が1行も起票されないまま merge された事例が記録されています。
この2つ目は operational memory 全般に効きます。知識ストアの品質検査は、たいてい入っているものの品質を見ます。入らなかったものは、どの検査にも映りません。書き漏らしを検出したいなら、ストアの外側(差分、CI の失敗ログ、レビューの確定指摘)から「対応する行があるはずだ」と要求する検査が要ります。
穴を穴として文書に書いてあること自体は、この仕組みの良いところだと思っています。ポストモーテムを非難なしで回す運用 と同じで、分かっていない範囲を書いておかないと、CI が緑であることを「漏れが無い」と読んでしまいます。
7. 貯める前に決めておくこと
ここまでの実測から、新しく operational memory を作るときに先に決めておくと安くなる項目を挙げます。順番に意味があります。
- 何を1件とするか。 1件が「観測1つ」なのか「ルール1つ」なのかで、あとの全部が変わります。台帳は観測1件、
AGENT_LEARNINGS.mdは学び1件です。 - 必須列と値域を決め、検査を先に書く。 あとから検査を足すと、既存行の移行が要ります。台帳は検査と同時に作ったので、13 行すべてが最初から通っています。
- 値域は定義ファイルから実行時に読む。 ガード側に値域を書き写すと、定義が変わったときにガードだけが古くなります。同じ内容を2箇所に持たないのが一番安い対策です。
- expiry は「後継が生まれるか」で分ける。 後継が構造的に生まれる知識は supersede で、生まれない知識は日付で切ります。
- 空欄の意味を1つに決める。 空欄が「未検証」なのか「該当なし」なのかを混ぜないために、
not-applicableのような明示値を用意します。 - 撤回できる単位で入れる。 知識を根拠に何かを変えるときは、その変更を単独で入れます。
3 の「同じ内容を2箇所に持たない」は地味ですが効きます。転記した契約が片側だけ更新されて食い違う事故が、このリポジトリでは実際に起きています。詳しくは AIが迷わないリポジトリ設計 側で扱っている話に近く、知識ストアでも同じことが起きます。
8. 手元の知識ストアの expiry を測る
自分のリポジトリで測るためのスクリプトです。対象のファイルを読むだけで、書き込みも一時ファイルの作成もしません。POSIX シェルと awk と date で動きます。
実行を確認したのは macOS 標準の bash 3.2.57 と /usr/bin/awk、date は BSD 系(-v)の経路です。GNU 系(-d)の分岐はコードとしては置いてありますが、この記事では実測していません。
このスクリプトが見るのは expiry の1軸だけです。schema の妥当性も provenance の正しさも rollback の実績も見ません。第 2 節の表のうち1行分しかカバーしていない、と思ってください。
前提として、1 エントリは ENTRY_KEY で始まる行から次の ENTRY_KEY までとし、期限はそのエントリの別の行に EXPIRY_KEY: YYYY-MM-DD の形で書かれているものとします。エントリ行と同じ行に期限を書く形式は読みません。
#!/bin/sh
# knowledge-expiry-audit.sh
# 知識ストアの「期限切れ」を検査する。合格/不合格の前に「検査できたか」を報告する。
#
# exit 0 = 検査した結果、期限切れ・欠落ともに 0 件
# exit 1 = 検査した結果、期限切れまたは欠落を検知
# exit 2 = 検査できなかった(引数不正 / ファイル無し / 空ファイル / 対象 0 件 /
# フェンス未閉鎖 / date 実装非対応)
set -eu
ENTRY_KEY="${ENTRY_KEY:-- date:}"
EXPIRY_KEY="${EXPIRY_KEY:-last_verified:}"
MAX_AGE_DAYS="${MAX_AGE_DAYS:-90}"
if [ "$#" -eq 0 ]; then
echo "usage: $0 <file> [file...]" >&2
echo " env: ENTRY_KEY / EXPIRY_KEY / MAX_AGE_DAYS" >&2
exit 2
fi
case "$MAX_AGE_DAYS" in
''|0|*[!0-9]*) echo "設定ミス: MAX_AGE_DAYS は 1 以上の整数。現在値=${MAX_AGE_DAYS}" >&2; exit 2 ;;
esac
# BSD date と GNU date の両方を試す。どちらも失敗したら検査不能として 2 で落とす。
cutoff="$(date -v-"${MAX_AGE_DAYS}"d +%Y-%m-%d 2>/dev/null || true)"
if [ -z "$cutoff" ]; then
cutoff="$(date -d "${MAX_AGE_DAYS} days ago" +%Y-%m-%d 2>/dev/null || true)"
fi
if [ -z "$cutoff" ]; then
echo "検査不能: この環境の date が -v / -d のどちらにも対応していない" >&2
exit 2
fi
for f in "$@"; do
if [ ! -f "$f" ]; then
echo "検査不能: ファイルが存在しない: $f" >&2
exit 2
fi
# 空ファイルは awk に 1 行も渡らず、awk 側の 0 件検査が発火しない。ここで落とす。
if [ ! -s "$f" ]; then
echo "検査不能: 空ファイル。これは合格ではない: $f" >&2
exit 2
fi
done
echo "cutoff=$cutoff (MAX_AGE_DAYS=$MAX_AGE_DAYS) entry_key='$ENTRY_KEY' expiry_key='$EXPIRY_KEY'"
# ファイル単位で「検査できたか」を判定する。合算だけを見ると 0 件のファイルが埋もれるため。
awk -v entry_key="$ENTRY_KEY" -v expiry_key="$EXPIRY_KEY" -v cutoff="$cutoff" '
function report(file, line, text, why) {
printf " %s:%d [%s] %s\n", file, line, why, substr(text, 1, 60)
}
function flush_entry() {
if (!in_entry) return
if (expiry == "") { missing++; report(cur_file, entry_line, entry_text, "expiry欠落") }
else if (expiry < cutoff) { stale++; report(cur_file, entry_line, entry_text, "期限切れ(" expiry ")") }
else fresh++
in_entry = 0; expiry = ""
}
function finish_file() {
if (cur_file == "") return
flush_entry()
if (in_fence) {
printf "検査不能: 未閉鎖のコードフェンスがある(以降を読み飛ばした): %s\n", cur_file > "/dev/stderr"
unchecked++
} else if (file_entries == 0) {
printf "検査不能: 検査対象0件。これは合格ではない(entry_key 不一致の可能性): %s\n", cur_file > "/dev/stderr"
unchecked++
}
cur_file = ""; file_entries = 0; in_fence = 0
}
{ sub(/\r$/, "", $0) } # CRLF のストアで日付の照合が全件外れるのを防ぐ
FNR == 1 { finish_file(); cur_file = FILENAME; file_entries = 0; in_fence = 0 }
/^[ \t]*(```|~~~)/ { in_fence = !in_fence; next }
in_fence { next }
index($0, entry_key) == 1 {
flush_entry()
in_entry = 1; entry_line = FNR; entry_text = $0; expiry = ""; file_entries++
next
}
in_entry && (p = index($0, expiry_key)) > 0 {
v = substr($0, p + length(expiry_key))
gsub(/^[ \t]+|[ \t]+$/, "", v)
# ブレース区間 {4} は awk 実装によってリテラル扱いになるため、展開して書く。
if (v ~ /^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]$/) expiry = v
next
}
END {
finish_file()
printf "検査対象: %d 件(欠落 %d / 期限切れ %d / 有効 %d)/ 検査不能ファイル %d\n", \
missing + stale + fresh, missing, stale, fresh, unchecked
if (unchecked > 0) exit 2
if (missing + stale > 0) exit 1
exit 0
}
' "$@"
終了コードを3つに分けている理由
| exit | 意味 | 出したい場面 |
|---|---|---|
| 0 | 検査して、欠落も期限切れも 0 件 | 本当に全部生きている |
| 1 | 検査して、欠落または期限切れを検知 | 直す対象がある |
| 2 | 検査できなかった | 引数不正、ファイル無し、空ファイル、対象 0 件、フェンス未閉鎖、date が非対応 |
一番効くのは 2 の存在です。ENTRY_KEY が実データと合っていないと、エントリは1件も見つかりません。ここで 0 を返すと「全部生きている」と区別が付かなくなり、設定を間違えたまま緑を眺め続けることになります。
このリポジトリで実際に起きているのも同じ形で、検査対象 0 件を合格と同じ出力・同じ終了コードで報告していた事故が、台帳に複数行として記録されています。
判定をファイル単位で持っているのも同じ理由です。複数ファイルをまとめて渡したとき、合算した件数だけを見ると、1 つのファイルが 0 件でも他のファイルの件数に埋もれて exit 0 になります。初版はそうなっていました。
手元での実行結果
このリポジトリの AGENT_LEARNINGS.md に対して実行すると、次のようになりました(社内データ)。
$ sh knowledge-expiry-audit.sh AGENT_LEARNINGS.md; echo "exit=$?"
cutoff=2026-06-11 (MAX_AGE_DAYS=90) entry_key='- date:' expiry_key='last_verified:'
AGENT_LEARNINGS.md:37 [期限切れ(2026-04-15)] - date: 2026-04-15
AGENT_LEARNINGS.md:46 [期限切れ(2026-04-15)] - date: 2026-04-15
AGENT_LEARNINGS.md:65 [期限切れ(2026-04-25)] - date: 2026-04-25
AGENT_LEARNINGS.md:74 [expiry欠落] - date: 2026-04-25
(以下、欠落が 24 行続く)
検査対象: 31 件(欠落 25 / 期限切れ 3 / 有効 3)/ 検査不能ファイル 0
exit=1
最悪の状態でも指摘が出ることを確かめる
検査を書いたら、一番悪い入力で必ず指摘が出るかを先に確かめてください。「正常な入力で緑」だけを見ると、何も見ていない検査でも合格します。次の 13 通りの入力で試しました。
| 入力 | 期待 | 実際 |
|---|---|---|
| 引数なし | 使い方を出して検査不能 | usage / exit 2 |
| 存在しないファイル | 検査不能 | 検査不能: ファイルが存在しない / exit 2 |
| 空ファイル | 検査不能 | 検査不能: 空ファイル。これは合格ではない / exit 2 |
| 期限フィールドが全件欠落 | 全件を指摘 | 欠落 2 / exit 1 |
| 書式サンプルだけのファイル | 実エントリ 0 件として検査不能 | 検査対象0件。これは合格ではない / exit 2 |
| 全件が期限内 | 合格 | 有効 1 / exit 0 |
ENTRY_KEY を実データと合わない値にする | 検査不能 | exit 2 |
MAX_AGE_DAYS=abc | 設定ミス | 設定ミス: MAX_AGE_DAYS は 1 以上の整数。 / exit 2 |
MAX_AGE_DAYS=0 | 設定ミス | 同上 / exit 2 |
| 正常なファイル + 0 件のファイルを同時指定 | 0 件のほうを指摘 | 検査不能: 検査対象0件 / exit 2 |
| コードフェンスが閉じていないファイル | 読み飛ばしを指摘 | 検査不能: 未閉鎖のコードフェンスがある / exit 2 |
| CRLF 改行のファイル | 期限を正しく読む | 有効 1 / exit 0 |
~~~ でフェンスされた書式サンプル入り | フェンス内を除外する | 有効 1 / exit 0 |
「書式サンプルだけのファイル」は実際に踏みました。AGENT_LEARNINGS.md は先頭に書式サンプルをコードブロックで持っているので、素朴に数えると 31 件ではなく 32 件になります。フェンスの内側を除外する前は、実在しないエントリを1件多く数えていました。
下 4 行は、独立レビューで指摘されて後から足したものです。「正常なファイル + 0 件のファイル」と「フェンス未閉鎖」は、どちらも壊れているのに exit 0 を返す形で、この記事自身が批判している型をスクリプトが踏んでいました。1 ファイルずつ検査していれば気づかず、複数ファイルを渡したときだけ出ます。CRLF は逆に過検知(全件を欠落と誤判定)で、合格に丸める側ではありませんでしたが、「最悪の入力で必ず指摘が出る」と言う以上は表の穴でした。
初版の検査が 6 通りだったことも書いておきます。検査の網羅性そのものが、独立に見る人がいて初めて増えました。
9. この設計で解けないこと
- 入口は塞げません。 第 6 節のとおり、書かれなかった知識はストアを測っても現れません。第 8 節のスクリプトも同じで、無いエントリは数えられません。
- rollback の実効性は未検証です。 適用 0 件なので、この記事は「設計してある」までしか言いません。
- サンプルが 1 リポジトリです。 台帳は運用 1 日目、
AGENT_LEARNINGS.mdは約 5 か月です。後継の有無と期限付与率の関係は、このリポジトリの 33 件(スナップショット 7 + 単発ルール 26)で観測した傾向です。 expiryの妥当な日数は決められていません。 90 日は第 8 節のスクリプトの初期値であって、根拠のある値ではありません。変化の速い対象ほど短くする、という方向しか言えません。- AI が推論した知識を自動化へ昇格させるかどうかの判断は、この記事では扱いません。ここで扱ったのは、昇格するかどうかに関わらず知識をどう置き、どう捨てるかです。
まとめ
- operational memory の設計課題は貯め方ではなく捨て方です。schema / provenance / expiry / rollback の4軸で書くと、どこが空かが見えます。
- 同じ書き手でも、機械が読む列は 100% 埋まり、読まれない列は 19% しか埋まりません(
社内データ)。フィールドを定義しただけでは lifecycle になりません。 - expiry が腐るかどうかは、書き手の規律ではなく後継が構造的に生まれるかで決まっていました(スナップショット 71% 対 単発ルール 0%、
社内データ)。 - 空欄の意味を1つに決めてください。「未検証」と「該当なし」を同じ空欄に潰すと、検査が静かに効かなくなります。
- 検査を書いたら、最悪の入力で指摘が出ることを先に確かめてください。第 8 節のスクリプトは検査不能を exit 2 として分けています。
関連する話として、暗黙知を組織で扱う側面は 暗黙知を組織で回す設計、ガードが効いていない壊れ方の分類は 効いていないガードを見つける7つの型 にあります。
FAQ
Q. conversational memory と operational memory は、置き場所を分けるべきですか
このリポジトリでは分けています。理由は寿命と失われたときのコストが違うからです。会話履歴はセッション内で作り直せますが、検証済みの知識は再検証コストがかかります。同じストアに混ぜると、期限の切り方を片方に合わせざるを得なくなります。ただし、これは1リポジトリでの運用判断です。
Q. expiry の日数は何日にすればいいですか
根拠のある値は出せませんでした。本記事のスクリプトの初期値 90 日は、単に既定値として置いたものです。判断できるのは方向だけで、参照先の変化が速いほど短くします。ツールのバージョンに依存する知識は数週間、リポジトリの構造に関する知識は数か月、という当たりから始めて、期限切れの検出件数を見て調整するのが現実的でした。
Q. 期限フィールドを足したのに誰も更新しません
このリポジトリでも同じで、31 件中 6 件しか付いていませんでした。実測から言えるのは、フィールドを足すだけでは 19% 前後で止まるということです。差が付いたのは、その列を読んで非 0 で落ちる検査があるかどうかでした。まず1本、検査を足すところまでを設計に含めてください。
Q. 台帳のような厳格な schema は、小さいチームには重すぎませんか
列を9個持つかどうかは規模の問題ですが、検査を書くかどうかは規模の問題ではありませんでした。列が3つでも、その3つを読んで落ちる検査が1本あれば埋まります。逆に列を9個定義しても、検査が無ければ埋まりません。重さの主因は列数ではなく、移行コストです。運用を始めたあとに検査を足すと既存行の直しが要るので、先に書くほうが安くなります。
Q. 知識が「書かれなかった」ことは検出できますか
ストアの内側だけを見る検査では原理的に検出できません。検出したいなら、ストアの外側から要求する必要があります。このリポジトリで言えば、PR の差分に修正コミットがあるのに対応する観測行が無い、という形で外側から突き合わせる検査です。現時点では未実装で、既知の穴として文書に記録してあります。
References
- W3C PROV-DM: The PROV Data Model — 来歴を entity / activity / agent で記述する標準モデル
- MemGPT: Towards LLMs as Operating Systems (arXiv:2310.08560) — LLM の記憶階層を OS の仮想記憶になぞらえる提案
- Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks (arXiv:2005.11401) — 外部知識を検索して生成に使う枠組みの原論文
- Google SRE Book: Postmortem Culture — 失敗の記録を非難なしで資産化する運用
- Software Engineering at Google, Chapter 10: Documentation — ドキュメントに "freshness dates" を付け、放置されたらリマインドする運用の記述
- The Site Reliability Workbook, Chapter 8: On-Call — playbook は本番の変化と同じ速度で陳腐化する、という前提の記述
- Claude Docs: How Claude remembers your project — ツール側が提供する memory 階層の一次情報
- Martin Fowler: Two Hard Things — キャッシュ無効化と命名が難しい、という古典的な整理(技術的根拠ではなく比喩の出典)
