From a52434e5c2770fa80878ff49fe5b4a8040756553 Mon Sep 17 00:00:00 2001 From: dj-oyu <68707227+dj-oyu@users.noreply.github.com> Date: Tue, 24 Feb 2026 13:14:59 +0900 Subject: [PATCH] docs: add storage protection design analysis for microSD write reduction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Map each persisted data type by in-process vs out-of-process consumers to identify write deferral opportunities. Session files and stats have no real-time external readers during operation, making write-behind caching safe. Estimate 97% write reduction (720→24 writes/hour) via 5-min periodic flush + shutdown hook. Detail turn-scoped read cache for MEMORY.md to eliminate redundant ReadLongTerm() calls. Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 92b143723..e5c605ccf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -320,3 +320,86 @@ func Truncate(s string, max int) string { ``` 文字数を正しく数えるために `[]rune` へ変換するのは正しい。しかし **①変換前に `len(s)` で byte 長をチェックして早期 return できる**(ASCII なら byte 長 == rune 長)、**②実際の入力が ASCII 主体であれば `utf8.RuneCountInString` + `utf8.RuneError` チェックでアロケーションなしに処理できる**。`[]rune(s)` は文字列全体をヒープにコピーするため、長い文字列では無視できないコストになる。**utils/string.go の2関数、git/worktree.go で観察された。** + +--- + +## ストレージ保護設計 — 書き込みの遅延・バッチ化 + +> 追記 2026-02-24。microSD上で動作する前提でのFS書き込み最適化。 + +### 「誰がこのデータを必要とするか」マップ + +現状の永続化データを**消費者**と**書き込み頻度**で整理すると、書き込みを遅延できる余地が大きく異なる。 + +| データ | プロセス内読者 | プロセス外読者 | 書き込み頻度(現状) | 損失許容度 | +|--------|--------------|--------------|-----------------|----------| +| `sessions/*.json` | AgentLoop (ターン毎 `GetHistory`) | **なし**(起動時ロードのみ) | **メッセージ毎** | 中(会話消失は困るが致命ではない) | +| `state/stats.json` | StatusAPI, Mini App(in-process) | CLI `cmd_status` | **LLM呼び出し毎 + ユーザーメッセージ毎** | 低(数件のロスは許容) | +| `memory/MEMORY.md` | AgentLoop (ターン毎) | CLI, Mini App, **外部エディタ** | ステップ完了毎・LLM edit_file | 高(プラン状態が失われると復帰不能) | +| `memory/YYYYMM/DD.md` | AgentLoop(プランなし時) | 外部エディタ | 日次ノート追記時(低頻度) | 低 | + +### 重要な観察: セッションファイルはプロセス内専用データ + +`sessions/*.json` は**稼働中に外部プロセスが読まない**。唯一の利用タイミングは起動時の `loadSessions()`。つまり書き込みの目的は「クラッシュリカバリ」だけであり、**メッセージ毎の即時書き込みは過剰**。 + +同様に `state/stats.json` も、Mini App や CLI はプロセス内の `Tracker.GetStats()` 経由でメモリから読む。ファイルはプロセス再起動時の引き継ぎ専用。 + +### 推奨書き込み戦略 + +#### sessions/*.json — Write-behind (ダーティフラグ + 定期フラッシュ) + +``` +AddFullMessage() → in-memory のみ更新、dirty フラグ立て + ↓ + 定期タイマー (5分) or メッセージ数閾値 (20件) + またはシャットダウンフック → Save() +``` + +- リカバリウィンドウ: 最大5分 or 20メッセージ分 +- 書き込み回数削減率: 会話速度次第だが **10〜50倍** +- 実装: `SessionManager` に `dirtyKeys map[string]bool` + バックグラウンドフラッシャーgoroutine + +#### state/stats.json — 定期フラッシュのみ + +``` +RecordUsage() / RecordPrompt() → in-memory のみ更新 + ↓ + 定期タイマー (5分) → save() + + シャットダウンフック +``` + +- 損失リスク: 最大5分分の統計カウント(許容範囲) +- 書き込み回数削減率: **LLM呼び出し頻度 × 5分** = 数十〜数百倍 + +#### memory/MEMORY.md — ターンスコープキャッシュ (書き込みは即時維持) + +書き込みは現状通り即時。読み取りの問題だけ解決する。 + +``` +エージェントターン開始 → content := ReadLongTerm() を1回だけ + ↓ content を引数として全ヘルパーに渡す + (HasActivePlan(content), GetPlanStatus(content), ...) +エージェントターン終了 → content キャッシュ破棄 +``` + +- 外部エディタとの整合: ターン境界でリフレッシュされるので1ターン以内の外部編集のみ見逃す(許容範囲) +- LLM の edit_file 経由の書き込み: ファイルシステムに即座に書かれるため次ターンで自動反映 +- 読み取り回数削減: 1ターンあたり `5回以上 → 1回` + +### microSD 寿命への影響試算 + +一般的な会話セッション(1時間、60メッセージ、10 LLM呼び出し/分)の場合: + +| データ | 現状の書き込み回数/時 | 改善後 | 削減率 | +|--------|-------------------|--------|-------| +| sessions/*.json | ~60回 (メッセージ毎) | ~12回 (5分毎) | **80%減** | +| stats.json | ~660回 (LLM呼+prompt毎) | ~12回 (5分毎) | **98%減** | +| MEMORY.md | ステップ数分(変わらず) | 同左 | — | +| **合計** | **720+ 回/時** | **~24回/時** | **97%減** | + +### 実装上の注意点 + +- **シャットダウンフック必須**: `SIGTERM` / `SIGINT` で dirty なデータを強制フラッシュ。フラッシュ失敗時はログに記録。 +- **クラッシュ後のリカバリ**: dirty データが失われた場合、セッション履歴は最後のチェックポイント以降が消える。ユーザーへの通知が必要か検討。 +- **フラッシュ中の競合**: フラッシュgoroutineと `Save()` の同時呼び出しを防ぐため、既存の mutex を流用。 +- **MEMORY.md のターンキャッシュ**: `edit_file` ツールが MEMORY.md を書き込んだ場合、**同ターン内のキャッシュを無効化**する仕組みが必要(`MemoryStore.InvalidateCache()` を edit_file のコールバックから呼ぶなど)。そうしないと同ターン内の後続の `GetPlanStatus()` などが古いキャッシュを読む。