docs: add storage protection design analysis for microSD write reduction

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 <noreply@anthropic.com>
This commit is contained in:
dj-oyu 2026-02-24 13:14:59 +09:00
parent c54a6172b6
commit fa15cb9d2d

View file

@ -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 Appin-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()` などが古いキャッシュを読む。