# Memory System

Agenvoy 的 memory 層針對 conversation memory 有三個層級，另加一個跨 session 的 error memory 層。

| 層級 | 後端 | 範圍 |
|---|---|---|
| 1. Context window（16 則訊息 + summary） | `history.json` + `summary.json` | session |
| 2. 語意搜尋（近期） | ToriiDB `DBSessionHist`（vector） | session |
| 3. 全文封存（全部歷史） | 透過 go-sqlkit 的 SQLite FTS5 | session |
| Error memory | ToriiDB `error_memory`（90 天 TTL） | 跨 session |

操作者筆記（SQLite `note` + `note_fts5`、`find_note` 工具、`/v1/note*`）已於 v1.0.23 移除，不再有全域筆記層。

## 三層 conversation memory

### 1. Context window（`limits.max_history_messages`，預設 16）

每個 session 完整保留最近 N 則訊息並餵入 LLM context window。更舊的內容仍留在 `history.json`，但不送給 LLM。

滾動式 summary（`summary.json`）將較舊的對話濃縮，並於每個 turn 開頭注入 system prompt，讓較舊 context 得以在 N 則訊息窗口之外存續。

**增量游標：** `.summary_cursor`（per-session；舊的 `summary.meta.json` 在 v1.1.1 以前於啟動時遷移，v1.1.2 起不再讀取）持有 `last_message_time`（格式 `YYYY-MM-DD HH:MM:SS`，由訊息內容中的 timestamp 擷取）。每次 `summary.Generate` 呼叫時：

1. `filterAfterTime(histories, cursor)` 只保留 `t > cursor` 的訊息
2. 每個 chunk 執行**一次** `generatePass` LLM call（system prompt 已含 `{{.Summary}}`=舊 summary，因此合併在生成期間完成 —— 無獨立 `mergePass`，避免 2 倍成本）
3. 成功時，游標推進至該 chunk 的最大 timestamp + `SaveSummary` 觸發 mtime gate
4. `generatePass` 失敗 → `return`（不對後續 chunk 計費；下一次 cron tick 重試）

### 2. 語意搜尋 —— ToriiDB（近期對話）

`chat_history` 工具以 `mode=search` 搭配 `match=semantic` 透過 ToriiDB `db.VSearch` 執行 vector 相似度搜尋。每次命中觸發 context window 擴展：命中前 2 則 + 命中後 1 則。

ToriiDB 條目於 `history.json` compaction 期間清理 —— 早於 compact cutoff 的條目被移除，讓 ToriiDB 專注於近期對話。較舊資料存於 SQLite（層級 3）。

### 3. 全文封存 —— SQLite FTS5（全部歷史）

每則寫入 `history.json` 的訊息都會透過 go-sqlkit **雙寫**至 SQLite（`~/.config/agenvoy/.store/history.db`）。即使 `history.json` 已 compact，SQLite 始終持有完整對話歷史。

`chat_history` 工具以 `mode=search` 搭配 `match=keyword` 對 SQLite 封存執行 FTS5 全文搜尋 + 對近期條目執行 ToriiDB 子字串比對，並合併結果。

**Compaction：** 當 `history.json` 超過 `max_history_bytes`（自 v1.0.12 起預設 4 MiB，即 1 MiB 文件上限的四倍）時，最舊訊息在完整的 user+assistant pair 邊界上裁剪至 80%。cutoff timestamp 記錄於 SQLite `message_meta.start_at`，使 keyword 搜尋排除 `history.json` 已存在的條目（避免重複）。早於 cutoff 的 ToriiDB 條目亦被移除。

**Backfill：** 首次遇到（SQLite 對某 session 無資料但 `history.json` 有內容）時，整份既有歷史 backfill 進 SQLite。

**Timestamps：** 以 UTC unix 奈秒儲存。訊息內容中的 timestamp 透過 `time.ParseInLocation`（本地時區）解析並轉為 UTC 儲存。搜尋查詢使用 `time.Now().UnixNano()`（已為 UTC）。

## 儲存佈局

| 儲存 | 內容 | 生命週期 |
|---|---|---|
| `history.json` | 近期訊息（hot，LLM 每 turn 讀取） | 4 MiB 時自動 compact |
| ToriiDB `DBSessionHist` | 帶 embeddings 的近期訊息 | compact 時清理（移除早於 cutoff 的條目） |
| SQLite `messages` | 曾寫入的所有訊息（雙寫） | reset / remove-session 時清除 |
| SQLite `message_meta` | `start_at` —— compact cutoff timestamp | reset / remove-session 時清除 |
| `summary.json` | 滾動式 summary blob | reset 後存續 |
| ToriiDB `error_memory` | 已解決的 tool 錯誤記錄與其修正方式 | 90 天 TTL（命中時續期） |

## Migration 注意

Session 與 error memory 過去存於 per-session JSON 檔，現已改置於 `~/.config/agenvoy/.store/` 底下的嵌入式 store——ToriiDB（`db_0` 工具快取、`db_1` 對話向量、`db_2` 錯誤記憶、`db_3` online marker），`history.db` 承載 session 設定、訊息封存、action 歷史、檔案歷史與用量。筆記於 v0.35.3 自 ToriiDB 移入 SQLite `note` 表，並於 v1.0.23 連同 daemon 啟動時的筆記 migration 一併移除；`state` 表於 v0.35.3 移除。`bot.json` / `bot.md` / `config.json` / `status.json` / `usage.log` / `summary.meta.json` 在 v1.1.1 以前於 daemon 啟動時遷移進來；v1.1.2 移除這些遷移，以及 v1.0 以前資料庫的 SQLite 欄位升級，這麼舊的安裝請先經過 v1.1.1。勿重新引入 JSON 路徑。

***

> [!NOTE]
> 本文件由 Claude 讀取完整原始碼後自動生成。
