@shadowforge0/aquifer-memory 1.5.9 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/.env.example +23 -0
  2. package/README.md +96 -73
  3. package/README_CN.md +659 -0
  4. package/README_TW.md +680 -0
  5. package/aquifer.config.example.json +34 -0
  6. package/consumers/claude-code.js +11 -11
  7. package/consumers/cli.js +374 -39
  8. package/consumers/codex-handoff.js +152 -0
  9. package/consumers/codex.js +1549 -0
  10. package/consumers/default/daily-entries.js +23 -4
  11. package/consumers/default/index.js +2 -2
  12. package/consumers/default/prompts/summary.js +6 -6
  13. package/consumers/mcp.js +131 -7
  14. package/consumers/openclaw-ext/index.js +0 -1
  15. package/consumers/openclaw-plugin.js +44 -4
  16. package/consumers/shared/config.js +28 -0
  17. package/consumers/shared/factory.js +2 -0
  18. package/consumers/shared/ingest.js +1 -1
  19. package/consumers/shared/normalize.js +14 -3
  20. package/consumers/shared/recall-format.js +53 -0
  21. package/consumers/shared/summary-parser.js +151 -0
  22. package/core/aquifer.js +384 -18
  23. package/core/finalization-review.js +319 -0
  24. package/core/insights.js +210 -58
  25. package/core/mcp-manifest.js +69 -2
  26. package/core/memory-bootstrap.js +188 -0
  27. package/core/memory-consolidation.js +1236 -0
  28. package/core/memory-promotion.js +544 -0
  29. package/core/memory-recall.js +247 -0
  30. package/core/memory-records.js +581 -0
  31. package/core/memory-safety-gate.js +224 -0
  32. package/core/session-finalization.js +350 -0
  33. package/core/storage.js +456 -2
  34. package/docs/getting-started.md +99 -0
  35. package/docs/postprocess-contract.md +2 -2
  36. package/docs/setup.md +51 -2
  37. package/package.json +31 -9
  38. package/pipeline/normalize/adapters/codex.js +106 -0
  39. package/pipeline/normalize/detect.js +3 -2
  40. package/schema/001-base.sql +3 -0
  41. package/schema/007-v1-foundation.sql +273 -0
  42. package/schema/008-session-finalizations.sql +50 -0
  43. package/schema/009-v1-assertion-plane.sql +193 -0
  44. package/schema/010-v1-finalization-review.sql +160 -0
  45. package/schema/011-v1-compaction-claim.sql +46 -0
  46. package/schema/012-v1-compaction-lease.sql +39 -0
  47. package/schema/013-v1-compaction-lineage.sql +193 -0
  48. package/scripts/backfill-canonical-key.js +250 -0
  49. package/scripts/codex-recovery.js +532 -0
  50. package/consumers/miranda/context-inject.js +0 -119
  51. package/consumers/miranda/daily-entries.js +0 -224
  52. package/consumers/miranda/index.js +0 -364
  53. package/consumers/miranda/instance.js +0 -55
  54. package/consumers/miranda/llm.js +0 -99
  55. package/consumers/miranda/profile.json +0 -145
  56. package/consumers/miranda/prompts/summary.js +0 -303
  57. package/consumers/miranda/recall-format.js +0 -76
  58. package/consumers/miranda/render-daily-md.js +0 -186
  59. package/consumers/miranda/workspace-files.js +0 -91
  60. package/scripts/drop-entity-state-history.sql +0 -17
  61. package/scripts/drop-insights.sql +0 -12
  62. package/scripts/install-openclaw.sh +0 -59
  63. package/scripts/queries.json +0 -45
  64. package/scripts/retro-recall-bench.js +0 -409
  65. package/scripts/sample-bench-queries.sql +0 -75
package/README_TW.md ADDED
@@ -0,0 +1,680 @@
1
+ <div align="center">
2
+
3
+ # 🌊 Aquifer
4
+
5
+ **基於 PostgreSQL 的 AI Agent 長期記憶系統。**
6
+
7
+ *把 session 存進 PostgreSQL,做 enrich,再把精確的決策片段 recall 回來,不需要另外掛一個向量資料庫。*
8
+
9
+ [![npm version](https://img.shields.io/npm/v/@shadowforge0/aquifer-memory)](https://www.npmjs.com/package/@shadowforge0/aquifer-memory)
10
+ [![PostgreSQL 15+](https://img.shields.io/badge/PostgreSQL-15%2B-336791)](https://www.postgresql.org/)
11
+ [![pgvector](https://img.shields.io/badge/pgvector-0.7%2B-blue)](https://github.com/pgvector/pgvector)
12
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
13
+
14
+ [English](README.md) | [繁體中文](README_TW.md) | [简体中文](README_CN.md)
15
+
16
+ </div>
17
+
18
+ ---
19
+
20
+ ## 先從這裡開始
21
+
22
+ Aquifer 的預設路徑應該很短:先把 PostgreSQL + embedding 跑起來,執行 `quickstart` 驗證,最後把 MCP client 指到 `aquifer mcp`。
23
+
24
+ 如果你是要直接走程式化整合,往下跳到 [API 參考](#api-參考)。如果你想看比較完整但還是新手導向的說明,直接看 [docs/getting-started.md](docs/getting-started.md)。
25
+
26
+ ### 1. 起本機 stack
27
+
28
+ ```bash
29
+ docker compose up -d
30
+ # PostgreSQL 16 + pgvector 跟 Ollama(bge-m3 自動 pull)。
31
+ # 第一次跑會拉 model——`docker compose logs -f ollama-pull` 盯進度。
32
+ ```
33
+
34
+ 已經有 PostgreSQL + pgvector 跟 embedding 端點在跑?這步可以跳過。`quickstart` 會讀你現有的環境變數設定。
35
+
36
+ ### 2. 做端到端驗證
37
+
38
+ ```bash
39
+ npx --yes @shadowforge0/aquifer-memory quickstart
40
+ ```
41
+
42
+ `quickstart` 會自動偵測 `localhost:5432` 的 PostgreSQL 跟 `localhost:11434` 的 Ollama(步驟 1 起的或你自己的都行),跑 migration、embed 一個測試 session、recall 回來、清乾淨。看到 `✓ Aquifer is working` 就成功了。
43
+
44
+ 長期使用建議裝進專案省掉 `npx` 解析成本:`npm install @shadowforge0/aquifer-memory` 然後 `npx aquifer quickstart`。
45
+
46
+ 要用 OpenAI 不用 Ollama?跑 `quickstart` 前 `export EMBED_PROVIDER=openai` + `OPENAI_API_KEY=sk-...`——model 預設 `text-embedding-3-small`。
47
+
48
+ ### 3. 接到你的 MCP client
49
+
50
+ Claude Code、Claude Desktop 或任何支援 MCP 的 client——放進 `.mcp.json`(專案層級)或 `claude_desktop_config.json`:
51
+
52
+ ```jsonc
53
+ {
54
+ "mcpServers": {
55
+ "aquifer": {
56
+ "command": "npx",
57
+ "args": ["--yes", "@shadowforge0/aquifer-memory", "mcp"],
58
+ "env": {
59
+ "DATABASE_URL": "postgresql://aquifer:aquifer@localhost:5432/aquifer",
60
+ "EMBED_PROVIDER": "ollama",
61
+ "AQUIFER_MEMORY_SERVING_MODE": "legacy"
62
+ }
63
+ }
64
+ }
65
+ }
66
+ ```
67
+
68
+ 或直接跑:`DATABASE_URL=... EMBED_PROVIDER=ollama npx aquifer mcp`。MCP server 本身對 env 比較嚴格,`quickstart` 的 autodetect 是試用路徑,不是 production 路徑。
69
+
70
+ 第一輪 rollout 先維持 `AQUIFER_MEMORY_SERVING_MODE=legacy`。只有在你要讓 `session_recall` 跟 `session_bootstrap` 提供 active curated memory 時,才切到 `curated`;`evidence_recall` 會保留為顯式 audit/debug 路徑。要 rollback 只要把 env 或 config 切回 `legacy`。
71
+
72
+ ### 常用指令
73
+
74
+ | 目標 | 指令 |
75
+ |---|---|
76
+ | 驗證 setup | `npx aquifer quickstart` |
77
+ | 啟動 MCP server | `npx aquifer mcp` |
78
+ | 手動查記憶 | `npx aquifer recall "auth middleware"` |
79
+ | 規劃 curated memory 壓縮 | `npx aquifer compact --cadence daily --period-start 2026-04-27T00:00:00Z --period-end 2026-04-28T00:00:00Z` |
80
+ | 看儲存狀態 | `npx aquifer stats` |
81
+ | 補跑 pending session | `npx aquifer backfill` |
82
+
83
+ 需要 LLM 摘要、知識圖譜、OpenAI embedding、reranker 或維運細節,就往下看 [環境變數](#環境變數) 跟 [docs/setup.md](docs/setup.md)。
84
+
85
+ ---
86
+
87
+ ## 為什麼選 Aquifer?
88
+
89
+ 多數 AI 記憶系統會在旁邊掛一個向量資料庫。Aquifer 的做法不同:**PostgreSQL 就是記憶本體**。
90
+
91
+ Session、摘要、turn 級 embedding、實體圖譜,全部住在同一個資料庫裡,用同一個連線查詢。不需要同步層,沒有最終一致性問題,也不用額外基礎設施。
92
+
93
+ ### 跟典型做法的差異
94
+
95
+ | | Aquifer | 典型向量 DB 做法 |
96
+ |---|---|---|
97
+ | **儲存** | PostgreSQL + pgvector | 獨立向量 DB + 應用 DB |
98
+ | **粒度** | Turn 級 embedding(不只是 session 摘要) | Session 或文件切片 |
99
+ | **排序** | 三路 RRF:FTS + session embedding + turn embedding | 單一向量相似度 |
100
+ | **知識圖譜** | 內建實體擷取與共現關係 | 通常是獨立系統 |
101
+ | **多租戶** | 每張表都有 `tenant_id`,第一天就內建 | 通常是事後補做 |
102
+ | **依賴** | `pg` + MCP SDK | 多個 SDK |
103
+
104
+ ### 有和沒有的差別
105
+
106
+ **沒有 turn 級記憶,搜尋只能命中模糊的摘要:**
107
+
108
+ > 查詢:「我們對 auth middleware 做了什麼決定?」
109
+ > → 回傳一份 2000 字的 session 摘要,裡面某處提到了 auth
110
+
111
+ **有 Aquifer,搜尋直接命中精確的對話片段:**
112
+
113
+ > 查詢:「我們對 auth middleware 做了什麼決定?」
114
+ > → 回傳那句原話:「舊的 auth middleware 拆掉吧,法務說 session token 儲存方式不合規」
115
+
116
+ ---
117
+
118
+ ## 需求
119
+
120
+ | 元件 | 必要? | 用途 | 範例 |
121
+ |------|--------|------|------|
122
+ | Node.js >= 18 | 是 | Runtime | — |
123
+ | PostgreSQL 15+ | 是 | 儲存 session、摘要、實體 | 本機、Docker 或 managed |
124
+ | pgvector extension | 是 | 向量相似度搜尋 | `CREATE EXTENSION vector;`(`pgvector/pgvector` Docker image 內建) |
125
+ | Embedding 端點 | 是(recall 用) | Turn + session embedding | Ollama `bge-m3`、OpenAI `text-embedding-3-small`、任何 OpenAI 相容 API |
126
+ | LLM 端點 | 選用 | `enrich` 階段的內建摘要 | Ollama、OpenRouter、OpenAI,或自己傳 `summaryFn` |
127
+ | `@modelcontextprotocol/sdk` + `zod` | 是(MCP server 用) | MCP 協定 runtime | 已列入 dependencies,自動安裝 |
128
+
129
+ ---
130
+
131
+ ## Library API
132
+
133
+ ### 初始化
134
+
135
+ ```javascript
136
+ const { createAquifer } = require('@shadowforge0/aquifer-memory');
137
+
138
+ const aquifer = createAquifer({
139
+ db: 'postgresql://user:pass@localhost:5432/mydb', // 連線字串或 pg.Pool
140
+ schema: 'memory', // PG schema 名稱(預設 'aquifer')
141
+ tenantId: 'default', // 多租戶隔離
142
+ embed: {
143
+ fn: async (texts) => embeddings, // 你的 embedding 函式
144
+ dim: 1024, // 選填,維度提示
145
+ },
146
+ llm: {
147
+ fn: async (prompt) => text, // 你的 LLM 函式(內建摘要用)
148
+ },
149
+ entities: {
150
+ enabled: true,
151
+ scope: 'my-app', // 實體命名空間(預設 'default')
152
+ },
153
+ });
154
+
155
+ // 執行 migration(可重複執行)
156
+ await aquifer.migrate();
157
+ ```
158
+
159
+ ### 寫入路徑:commit + enrich
160
+
161
+ ```javascript
162
+ // 1. 儲存 session
163
+ await aquifer.commit('conv-001', [
164
+ { role: 'user', content: '我來說說新的 auth 做法...' },
165
+ { role: 'assistant', content: '了解,所以計畫是...' },
166
+ ], { agentId: 'main' });
167
+
168
+ // 2. 豐富化:摘要 + turn embedding + 實體擷取
169
+ const result = await aquifer.enrich('conv-001', {
170
+ agentId: 'main',
171
+ summaryFn: async (msgs) => ({ summaryText, structuredSummary, entityRaw }),
172
+ entityParseFn: (text) => [{ name, normalizedName, type, aliases }],
173
+ postProcess: async (ctx) => { /* 後處理 hook */ },
174
+ });
175
+ ```
176
+
177
+ ### 查詢路徑:recall
178
+
179
+ ```javascript
180
+ const results = await aquifer.recall('auth middleware 決定', {
181
+ agentId: 'main',
182
+ limit: 5,
183
+ entities: ['auth-middleware'], // 選填:實體感知搜尋
184
+ entityMode: 'all', // 'any'(加分)或 'all'(硬篩)
185
+ });
186
+ // 回傳排序後的 session,使用三路 RRF 融合
187
+ ```
188
+
189
+ ---
190
+
191
+ ## 主機整合(Host Integration)
192
+
193
+ MCP 是主要的整合介面。Agent 主機連接 Aquifer MCP 伺服器,伺服器提供八個工具:`session_recall`、`evidence_recall`、`session_feedback`、`memory_feedback`、`feedback_stats`、`session_bootstrap`、`memory_stats`、`memory_pending`。
194
+
195
+ | 整合方式 | 路由 | 狀態 | 使用時機 |
196
+ |----------|------|------|----------|
197
+ | MCP 伺服器 | `consumers/mcp.js` | 主要 | Claude Code、OpenClaw、Codex、任何支援 MCP 的主機 |
198
+ | Library API | `createAquifer()` | 主要 | 後端應用、自訂 pipeline、直接 Node.js 使用 |
199
+ | CLI | `consumers/cli.js` | 次要 | 維運、除錯、手動 recall/backfill |
200
+ | OpenCode 匯入 | `consumers/opencode.js` | 次要 | 從 OpenCode 的 SQLite DB 匯入 session |
201
+ | OpenClaw plugin | `consumers/openclaw-plugin.js` | 相容性 | 透過 `before_reset` 擷取 session——不用於工具傳遞 |
202
+
203
+ ### Claude Code
204
+
205
+ 加入你的專案 `.claude.json` 或使用者層級 MCP 設定:
206
+
207
+ ```json
208
+ {
209
+ "mcpServers": {
210
+ "aquifer": {
211
+ "type": "stdio",
212
+ "command": "node",
213
+ "args": ["/path/to/aquifer/consumers/mcp.js"],
214
+ "env": {
215
+ "DATABASE_URL": "postgresql://...",
216
+ "AQUIFER_EMBED_BASE_URL": "http://localhost:11434/v1",
217
+ "AQUIFER_EMBED_MODEL": "bge-m3"
218
+ }
219
+ }
220
+ }
221
+ }
222
+ ```
223
+
224
+ 工具會以 `mcp__aquifer__session_recall`、`mcp__aquifer__evidence_recall`、`mcp__aquifer__session_bootstrap`、`mcp__aquifer__session_feedback`、`mcp__aquifer__memory_feedback`、`mcp__aquifer__feedback_stats`、`mcp__aquifer__memory_stats`、`mcp__aquifer__memory_pending` 等名稱出現。
225
+
226
+ ### OpenClaw
227
+
228
+ 加入 `openclaw.json` 的 `mcp.servers` 區段:
229
+
230
+ ```json
231
+ {
232
+ "mcp": {
233
+ "servers": {
234
+ "aquifer": {
235
+ "command": "node",
236
+ "args": ["/path/to/aquifer/consumers/mcp.js"],
237
+ "env": {
238
+ "DATABASE_URL": "postgresql://...",
239
+ "AQUIFER_EMBED_BASE_URL": "http://localhost:11434/v1",
240
+ "AQUIFER_EMBED_MODEL": "bge-m3"
241
+ }
242
+ }
243
+ }
244
+ }
245
+ }
246
+ ```
247
+
248
+ 工具會以 `aquifer__session_recall`、`aquifer__evidence_recall`、`aquifer__session_feedback`、`aquifer__memory_feedback`、`aquifer__feedback_stats`、`aquifer__session_bootstrap`、`aquifer__memory_stats`、`aquifer__memory_pending` 等名稱出現(主機自動加上伺服器名稱前綴)。
249
+
250
+ OpenClaw plugin(`consumers/openclaw-plugin.js`)保留用於 `before_reset` session 擷取,但**不是**建議的工具傳遞方式。請用 MCP。
251
+
252
+ ### 其他支援 MCP 的主機
253
+
254
+ 任何支援 MCP stdio 的主機都能以同樣方式連接——指向 `node consumers/mcp.js` 並設定必要的環境變數。MCP 伺服器是對外的標準介面。
255
+
256
+ ---
257
+
258
+ ## 環境變數
259
+
260
+ | 變數 | 必填? | 用途 | 範例 |
261
+ |------|--------|------|------|
262
+ | `DATABASE_URL` | 是 | PostgreSQL 連線字串 | `postgresql://user:pass@localhost:5432/mydb` |
263
+ | `AQUIFER_SCHEMA` | 否 | PG schema 名稱(預設:`aquifer`) | `memory` |
264
+ | `AQUIFER_TENANT_ID` | 否 | 多租戶 key(預設:`default`) | `my-app` |
265
+ | `AQUIFER_EMBED_BASE_URL` | 是(recall 必需) | Embedding API base URL | `http://localhost:11434/v1` |
266
+ | `AQUIFER_EMBED_MODEL` | 是(recall 必需) | Embedding 模型名稱 | `bge-m3` |
267
+ | `AQUIFER_EMBED_API_KEY` | 視供應商 | 託管 embedding 供應商的 API key | `sk-...` |
268
+ | `AQUIFER_EMBED_DIM` | 否 | Embedding 維度覆寫(自動偵測) | `1024` |
269
+ | `AQUIFER_LLM_BASE_URL` | 否 | LLM API base URL(內建摘要用) | `http://localhost:11434/v1` |
270
+ | `AQUIFER_LLM_MODEL` | 否 | LLM 模型名稱 | `llama3.1` |
271
+ | `AQUIFER_LLM_API_KEY` | 視供應商 | 託管 LLM 供應商的 API key | `sk-...` |
272
+ | `AQUIFER_ENTITIES_ENABLED` | 否 | 啟用知識圖譜(預設:`false`) | `true` |
273
+ | `AQUIFER_ENTITY_SCOPE` | 否 | 實體命名空間(預設:`default`) | `my-app` |
274
+ | `AQUIFER_RERANK_ENABLED` | 否 | 啟用 cross-encoder reranking | `true` |
275
+ | `AQUIFER_RERANK_PROVIDER` | 否 | Reranker 供應商:`tei`、`jina`、`openrouter` | `tei` |
276
+ | `AQUIFER_RERANK_BASE_URL` | 否 | Reranker 端點 | `http://localhost:8080` |
277
+ | `AQUIFER_AGENT_ID` | 否 | 預設 agent ID | `main` |
278
+ | `AQUIFER_MEMORY_SERVING_MODE` | 否 | 對外 serving mode:預設 `legacy`,可 opt-in `curated` | `curated` |
279
+ | `AQUIFER_MEMORY_ACTIVE_SCOPE_KEY` | 否 | recall/bootstrap 預設 active curated scope | `project:aquifer` |
280
+ | `AQUIFER_MEMORY_ACTIVE_SCOPE_PATH` | 否 | curated scope inheritance 的順序路徑 | `global,project:aquifer` |
281
+ | `AQUIFER_MIGRATIONS_MODE` | 否 | 啟動 handshake 模式:`apply`(預設)、`check`、`off` | `apply` |
282
+ | `AQUIFER_MIGRATION_LOCK_TIMEOUT_MS` | 否 | advisory lock 等待上限,逾時拋 `AQ_MIGRATION_LOCK_TIMEOUT`(預設 30000) | `30000` |
283
+ | `AQUIFER_INSIGHTS_DEDUP_MODE` | 否 | insights 語意去重模式:`off`(預設)、`shadow`、`enforce`——此欄位 env 蓋過程式碼設定,讓 operator 不用重部署就能緊急關閉 | `shadow` |
284
+ | `AQUIFER_INSIGHTS_DEDUP_COSINE` | 否 | 語意合併的 cosine 閾值(預設 `0.88`,在 `[0.75, 0.95]` 外會發出警告) | `0.90` |
285
+ | `AQUIFER_INSIGHTS_DEDUP_CLOSE_BAND_FROM` | 否 | close-band(`dedupNear` metadata)下界,必須嚴格小於閾值(預設 `0.85`) | `0.82` |
286
+
287
+ 完整的環境變數對應設定在 [consumers/shared/config.js](consumers/shared/config.js)。
288
+
289
+ Curated serving 是 opt-in。若 rollout 時要回復舊路徑,設定 `AQUIFER_MEMORY_SERVING_MODE=legacy` 並重新啟動 MCP/CLI process 即可,不需要破壞性 DB rollback。
290
+
291
+ ### Insights 語意去重(1.5.10)
292
+
293
+ 當 cron extractor(`scripts/extract-insights-from-recent-sessions.js`)或其他呼叫者透過 `commitInsight` 寫 insights 時,canonical-key 層(1.5.3+)會對 `canonicalClaim + entities` 雜湊相同的 row 做去重。但 LLM 跨次產出的 `canonicalClaim` 不一定穩定,所以 1.5.10 加上第二層:`title + body` 做 embedding,對 `(tenant, agent, type)` 範圍內的 active row 做比對,top cosine 超過 `AQUIFER_INSIGHTS_DEDUP_COSINE` 就觸發 supersede(enforce 模式)或只記 would-merge metadata(shadow 模式)。落在 close band(`closeBandFrom ≤ cos < threshold`)的寫 `metadata.dedupNear`,不 supersede,讓 operator 調閾值前先觀察。
294
+
295
+ 推薦部署順序:`shadow` 跑一個 weekly cycle,檢查 `SELECT metadata->>'shadowMatch' FROM insights WHERE metadata ? 'shadowMatch'` 看有沒有錯誤合併候選,確認沒問題再切 `enforce`。Kill-switch:`AQUIFER_INSIGHTS_DEDUP_MODE=off` + 重啟。
296
+
297
+ 1.5.3 前的歷史 row(`canonical_key_v2 IS NULL`)會被語意層直接抓到,但不會走 canonical 路徑;啟動時的警告會提示跑一次性的 backfill:
298
+
299
+ ```bash
300
+ DATABASE_URL=... \
301
+ node scripts/backfill-canonical-key.js --schema <schema> --agent <id>
302
+ ```
303
+
304
+ Script 是 idempotent(`WHERE canonical_key_v2 IS NULL` 保護),可以多次重跑,與 live writer 並存也安全。
305
+
306
+ ---
307
+
308
+ ## 架構
309
+
310
+ ```
311
+ ┌─────────────────────────────────────────────────────────────┐
312
+ │ createAquifer(入口) │
313
+ │ 設定 · Migration · Ingest · Recall · Enrich │
314
+ └────────┬──────────┬──────────┬──────────┬───────────────────┘
315
+ │ │ │ │
316
+ ┌────▼───┐ ┌────▼────┐ ┌──▼───┐ ┌───▼──────────┐
317
+ │storage │ │hybrid- │ │entity│ │ pipeline/ │
318
+ │ .js │ │rank.js │ │ .js │ │summarize.js │
319
+ └────────┘ └─────────┘ └──────┘ │embed.js │
320
+ │ │ │extract-ent.js │
321
+ ┌────▼───────────┐ ┌───▼──┐ │rerank.js │
322
+ │ PostgreSQL │ │ LLM │ └───────────────┘
323
+ │ + pgvector │ │ API │
324
+ └────────────────┘ └──────┘
325
+
326
+ ┌───────────────────────────────────┐
327
+ │ schema/ │
328
+ │ 001-base.sql(sessions、 │
329
+ │ summaries、turns、FTS) │
330
+ │ 002-entities.sql(KG) │
331
+ │ 003-trust-feedback.sql(信任評分) │
332
+ └───────────────────────────────────┘
333
+ ```
334
+
335
+ ### 檔案說明
336
+
337
+ | 檔案 | 用途 |
338
+ |------|------|
339
+ | `index.js` | 入口——匯出 `createAquifer`、`createEmbedder`、`createReranker` |
340
+ | `core/aquifer.js` | 主 facade:`migrate()`、`ingest()`、`recall()`、`enrich()` |
341
+ | `core/storage.js` | Session/摘要/turn 的 CRUD、FTS 搜尋、embedding 搜尋 |
342
+ | `core/entity.js` | 實體 upsert、mention 追蹤、關係圖譜、名稱正規化 |
343
+ | `core/hybrid-rank.js` | 三路 RRF 融合、時間衰減、信任乘數、實體加分、open-loop 加分 |
344
+ | `pipeline/summarize.js` | LLM 驅動的 session 摘要(結構化輸出) |
345
+ | `pipeline/embed.js` | Embedding 客戶端(任何 OpenAI 相容 API) |
346
+ | `pipeline/extract-entities.js` | LLM 驅動的實體擷取(12 種類型) |
347
+ | `pipeline/rerank.js` | Cross-encoder reranking(TEI、Jina、OpenRouter) |
348
+ | `pipeline/normalize/` | Session 正規化,處理 Claude Code / gateway 噪音 |
349
+ | `consumers/opencode.js` | OpenCode SQLite 匯入消費者 |
350
+ | `schema/001-base.sql` | DDL:sessions、summaries、turn_embeddings、FTS 索引 |
351
+ | `schema/002-entities.sql` | DDL:entities、mentions、relations、entity_sessions |
352
+ | `schema/003-trust-feedback.sql` | DDL:trust_score 欄位、session_feedback 稽核表 |
353
+
354
+ ---
355
+
356
+ ## 核心功能
357
+
358
+ ### 三路混合檢索(RRF)
359
+
360
+ ```
361
+ 查詢 ──┬── FTS(BM25) ──┐
362
+ ├── Session embedding 搜尋 ──├── RRF 融合 → 時間衰減 → 實體加分 → 結果
363
+ └── Turn embedding 搜尋 ──┘
364
+ ```
365
+
366
+ - **全文搜尋** — PostgreSQL `tsvector`,支援多語言排序
367
+ - **Session embedding** — 對 session 摘要做 cosine 相似度
368
+ - **Turn embedding** — 對個別 user turn 做 cosine 相似度
369
+ - **Reciprocal Rank Fusion** — 合併三份排名清單(K=60)
370
+ - **時間衰減** — sigmoid 衰減,可設定中點與斜率
371
+ - **實體加分** — 包含查詢相關實體的 session 會獲得分數提升
372
+ - **信任評分** — 根據明確回饋(helpful/unhelpful)的乘法信任係數
373
+ - **Open-loop 加分** — 有未解決項目的 session 獲得輕微提升
374
+ - **選填 cross-encoder reranking** — 支援 TEI、Jina、OpenRouter,在 RRF 融合後對候選結果做二次精排
375
+
376
+ ### 實體交叉查詢
377
+
378
+ 明確指定要搜尋的實體時,可以做 AND 語意篩選:
379
+
380
+ ```javascript
381
+ const results = await aquifer.recall('auth 決定', {
382
+ entities: ['auth-middleware', 'legal-compliance'],
383
+ entityMode: 'all', // 只回傳同時包含兩個實體的 session
384
+ });
385
+ ```
386
+
387
+ - `entityMode: 'any'`(預設)— 提升匹配任一實體的 session
388
+ - `entityMode: 'all'` — 硬篩:只回傳包含所有指定實體的 session
389
+
390
+ ### 信任評分與回饋
391
+
392
+ Session 透過明確回饋累積信任分數。低信任的記憶在排序中會被壓制,無論相關性多高。
393
+
394
+ ```javascript
395
+ // 回傳結果有用
396
+ await aquifer.feedback('session-id', { verdict: 'helpful' });
397
+
398
+ // 回傳結果無關
399
+ await aquifer.feedback('session-id', { verdict: 'unhelpful' });
400
+ ```
401
+
402
+ - 非對稱:helpful +0.05,unhelpful −0.10(低品質記憶下沉更快)
403
+ - 排序中用乘法:trust=0.5 中性、trust=0 分數減半、trust=1.0 提升 50%
404
+ - 完整稽核記錄在 `session_feedback` 表
405
+
406
+ ### Turn 級 Embedding
407
+
408
+ 不只是 session 摘要——Aquifer 對每一則有意義的 user turn 獨立 embedding。
409
+
410
+ - 過濾噪音:短訊息、斜線指令、確認語(「ok」「好」「收到」)
411
+ - 截斷 2000 字元,跳過 5 字元以下的 turn
412
+ - 儲存 turn 原文 + embedding + 位置,實現精確檢索
413
+
414
+ ### 知識圖譜
415
+
416
+ 內建實體擷取與關係追蹤:
417
+
418
+ - **12 種實體類型**:person、project、concept、tool、metric、org、place、event、doc、task、topic、other
419
+ - **實體正規化**:NFKC + 同形字映射 + 大小寫折疊
420
+ - **共現關係**:無向邊,帶頻率追蹤
421
+ - **實體-session 映射**:哪些實體出現在哪些 session
422
+ - **排序加分**:包含相關實體的 session 分數更高
423
+
424
+ ### 多租戶
425
+
426
+ 每張表都包含 `tenant_id`(預設:`'default'`)。隔離在查詢層強制執行——不會有跨租戶資料洩漏。
427
+
428
+ ### Schema 隔離
429
+
430
+ 傳入 `schema: 'my_app'` 給 `createAquifer()`,所有表都建在該 PostgreSQL schema 下。同一個資料庫可以跑多個 Aquifer 實例互不衝突。
431
+
432
+ ---
433
+
434
+ ## API 參考
435
+
436
+ ### `createAquifer(config)`
437
+
438
+ 回傳一個 Aquifer 實例。設定:
439
+
440
+ ```javascript
441
+ {
442
+ db, // PG 連線字串或 Pool 實例(必填)
443
+ schema, // PG schema 名稱(預設 'aquifer')
444
+ tenantId, // 多租戶 key(預設 'default')
445
+ embed: { fn, dim }, // embedding 函式(recall 必需)
446
+ llm: { fn }, // LLM 函式(內建摘要必需)
447
+ entities: {
448
+ enabled, // 啟用知識圖譜(預設 false)
449
+ scope, // 實體命名空間(預設 'default')
450
+ mergeCall, // 合併實體擷取到摘要 LLM 呼叫(預設 true)
451
+ },
452
+ rank: { rrf, timeDecay, access, entityBoost }, // 權重覆寫
453
+ }
454
+ ```
455
+
456
+ #### `aquifer.init()`
457
+
458
+ 啟動 handshake——收斂所有 pending migration 並回 StartupEnvelope。Host 應在接客前 `await` 這個。`apply` 模式下 `ready=false` 就是叫上層中止 startup 的訊號。
459
+
460
+ ```javascript
461
+ const envelope = await aquifer.init();
462
+ // { ready, memoryMode: 'rw'|'ro'|'off', migrationMode, pendingMigrations,
463
+ // appliedMigrations, error, durationMs }
464
+ ```
465
+
466
+ MCP consumer(`consumers/mcp.js`)已經把 `aquifer.init()` 串進 `server.connect()` 之前,`apply` 模式拿到 `ready=false` 就 non-zero exit。
467
+
468
+ #### `aquifer.listPendingMigrations()` / `aquifer.getMigrationStatus()`
469
+
470
+ 靠 table / column signature probe 回 `{ required, applied, pending, lastRunAt }`;table 走 `pg_tables`,alter-only migration 會用 `information_schema.columns` 判斷欄位是否存在,完全不跑 DDL。適合 health check 或 consumer 在 `init()` 之前想先知道 drift 狀態。
471
+
472
+ #### `aquifer.migrate()`
473
+
474
+ 執行 SQL migration(冪等)。建立表、索引、trigger 和擴充套件。進階:advisory lock 從 blocking 改成 `pg_try_advisory_lock` + 250ms poll + `lockTimeoutMs`(預設 30s),逾時拋 `AQ_MIGRATION_LOCK_TIMEOUT`。成功回 `{ ok: true, durationMs, notices, ddlExecuted }`;失敗拋 error 帶 `err.notices` / `err.failedAt`。大多數 caller 應該走 `aquifer.init()`。
475
+
476
+ #### `aquifer.ensureMigrated()`
477
+
478
+ Lazy idempotent wrapper——第一次呼叫觸發 `migrate()`,之後 no-op。尊重 `migrations.mode`:`check` 只 probe、`off` 直接標 migrated 不碰 DB。
479
+
480
+ #### `aquifer.commit(sessionId, messages, opts)`
481
+
482
+ 儲存 session。回傳 `{ id, sessionId, isNew }`。
483
+
484
+ #### `aquifer.enrich(sessionId, opts)`
485
+
486
+ 豐富化已 commit 的 session:摘要、turn embedding、實體擷取。支援自訂 pipeline(`summaryFn`、`entityParseFn`)和 `postProcess` 後處理 hook。使用 optimistic locking,卡住超過 10 分鐘的 processing session 可被回收。
487
+
488
+ #### `aquifer.recall(query, opts)`
489
+
490
+ 三路混合搜尋。支援 `entities` + `entityMode` 做實體感知查詢。
491
+
492
+ ```javascript
493
+ const results = await aquifer.recall('搜尋關鍵字', {
494
+ agentId: 'main',
495
+ limit: 10,
496
+ entities: ['postgres', 'migration'],
497
+ entityMode: 'all',
498
+ weights: { rrf, timeDecay, access, entityBoost },
499
+ });
500
+ ```
501
+
502
+ #### `aquifer.feedback(sessionId, opts)`
503
+
504
+ 記錄信任回饋。回傳 `{ trustBefore, trustAfter, verdict }`。
505
+
506
+ #### `aquifer.bootstrap(opts)`
507
+
508
+ 基於時間的 session 上下文載入器,用於新對話啟動時取得近期記憶摘要。跨 session 去重、sentinel 過濾、maxChars 截斷。
509
+
510
+ ```javascript
511
+ const context = await aquifer.bootstrap({
512
+ agentId: 'main',
513
+ limit: 5, // 回傳筆數(預設 5)
514
+ lookbackDays: 14, // 回溯天數(預設 14)
515
+ maxChars: 4000, // 最大字元數截斷(預設 4000)
516
+ format: 'text', // 'text' | 'structured' | 'both'
517
+ });
518
+ ```
519
+
520
+ 參數說明:
521
+
522
+ | 參數 | 預設值 | 說明 |
523
+ |------|--------|------|
524
+ | `agentId` | — | Agent ID |
525
+ | `limit` | 5 | 回傳的 session 數量上限 |
526
+ | `lookbackDays` | 14 | 往前查詢的天數 |
527
+ | `maxChars` | 4000 | 輸出最大字元數,超過自動截斷 |
528
+ | `format` | `'text'` | 輸出格式:`'text'`(純文字)、`'structured'`(結構化)、`'both'`(兩者) |
529
+
530
+ 此方法對應 MCP 工具 `session_bootstrap`。
531
+
532
+ #### `aquifer.insights.commitInsight(opts)` / `recallInsights(query, opts)` / `markStale(id)` / `supersede(oldId, newId)`
533
+
534
+ 從 session 視窗蒸餾出來的高階觀察(preference / pattern / frustration / workflow)。用兩層 identity 拆身份:**canonical key** 描述這個觀察是關於什麼(claim identity,跨 LLM 用詞飄不動),**idempotency key** 描述 canonical claim 的哪一個 revision(body + evidenceWindow)。
535
+
536
+ ```javascript
537
+ await aquifer.insights.commitInsight({
538
+ agentId: 'main',
539
+ type: 'preference',
540
+ canonicalClaim: 'mk 在寫 code 前先看 context', // required,短、穩定、不含修辭與例子
541
+ title: '先看 context 再動手', // best-effort display
542
+ body: '…',
543
+ entities: ['mk', 'claude code'],
544
+ sourceSessionIds: ['sess-a', 'sess-b'],
545
+ evidenceWindow: { from: isoString, to: isoString },
546
+ importance: 0.9,
547
+ });
548
+ ```
549
+
550
+ 寫入規則:idempotency 命中就回 existing;同 canonical + 更新 evidence → INSERT 新 row + 內聯 UPDATE supersede 前一 active;同 canonical + 舊/同 window → INSERT 但不 supersede(back-fill revision);同 canonical + 同 body → stale replay 回 existing。1.5.6 前的舊 rows `canonical_key_v2` 保持 NULL 不 retrofit,自然老去。
551
+
552
+ #### `aquifer.close()`
553
+
554
+ 關閉 PostgreSQL 連線池(僅限 Aquifer 自行建立的 pool)。
555
+
556
+ ---
557
+
558
+ ## 設定
559
+
560
+ Aquifer 接受 `db` 連線(字串或 `pg.Pool`),加上選填的 `embed` 和 `llm` 函式:
561
+
562
+ ```javascript
563
+ createAquifer({
564
+ db: 'postgresql://user:pass@localhost/mydb', // 或既有 pg.Pool
565
+ schema: 'aquifer',
566
+ tenantId: 'default',
567
+ embed: {
568
+ fn: myEmbedFn, // async (texts: string[]) => number[][]
569
+ dim: 1024,
570
+ },
571
+ llm: {
572
+ fn: myLlmFn, // async (prompt: string) => string
573
+ },
574
+ entities: {
575
+ enabled: true,
576
+ scope: 'my-app', // 實體命名空間——與 agentId 解耦
577
+ mergeCall: true,
578
+ },
579
+ rank: { rrf: 0.65, timeDecay: 0.25, access: 0.10, entityBoost: 0.18 },
580
+ migrations: {
581
+ mode: 'apply', // 'apply' | 'check' | 'off'
582
+ lockTimeoutMs: 30000, // advisory lock 等待上限,逾時拋 AQ_MIGRATION_LOCK_TIMEOUT
583
+ startupTimeoutMs: 60000, // init() 整體 deadline
584
+ onEvent: null, // (e) => void — lifecycle 觀察 hook
585
+ },
586
+ });
587
+ ```
588
+
589
+ ### Startup 可觀察性
590
+
591
+ 掛 `migrations.onEvent` 可以不解析 log 就拿到 lifecycle。事件名稱:`init_started`、`check_completed`、`apply_started`、`apply_succeeded`、`apply_failed`。payload 帶 `schema` / `mode` / 計畫 / `ddlExecuted` / `durationMs`,失敗時多 `error` / `failedAt` / `notices`。沒掛 listener 就是零成本。
592
+
593
+ ### 實體作用域(Entity Scope)
594
+
595
+ `entities.scope` 定義實體身份的命名空間。唯一約束是 `(tenant_id, normalized_name, entity_scope)`——不同 scope 中的同名實體會建立獨立的實體。這讓實體身份與 `agentId` 解耦,允許多個 agent 共享同一個實體命名空間。
596
+
597
+ ---
598
+
599
+ ## 資料庫 Schema
600
+
601
+ ### 001-base.sql
602
+
603
+ | 表 | 用途 |
604
+ |----|------|
605
+ | `sessions` | 原始對話資料,含 messages(JSONB)、token 數、時間戳記 |
606
+ | `session_summaries` | LLM 生成的結構化摘要,含 embedding |
607
+ | `turn_embeddings` | 逐 turn 的 user 訊息 embedding,實現精確檢索 |
608
+
609
+ 重要索引:messages GIN、`tsvector` GiST、embedding ivfflat、tenant/agent/timestamp B-tree。
610
+
611
+ ### 002-entities.sql
612
+
613
+ | 表 | 用途 |
614
+ |----|------|
615
+ | `entities` | 正規化命名實體,含類型、別名、頻率、entity_scope、選填 embedding |
616
+ | `entity_mentions` | 實體 × session 關聯,含 mention 次數與上下文 |
617
+ | `entity_relations` | 共現邊(無向,`CHECK src < dst`) |
618
+ | `entity_sessions` | 實體-session 關聯,用於加分計算 |
619
+
620
+ 重要索引:實體名稱 trigram、embedding GiST、`(tenant_id, normalized_name, entity_scope)` 唯一索引。
621
+
622
+ ### 003-trust-feedback.sql
623
+
624
+ | 表 | 用途 |
625
+ |----|------|
626
+ | `session_feedback` | 明確回饋稽核記錄(helpful/unhelpful 判決、信任分數變動) |
627
+
628
+ 另在 `session_summaries` 新增 `trust_score` 欄位(預設 0.5,範圍 0–1)。
629
+
630
+ ### 005-entity-state-history.sql *(entities 啟用時)*
631
+
632
+ | 表 | 用途 |
633
+ |----|------|
634
+ | `entity_state_history` | 時序 state-change 追蹤,partial `UNIQUE (tenant, agent, entity, attribute) WHERE valid_to IS NULL` 強制 at-most-one-current。out-of-order backfill 用 predecessor/successor overlap 檢查 |
635
+
636
+ opt-in pipeline(`createAquifer({stateChanges: {enabled, whitelist, confidenceThreshold, timeoutMs, ...}})`)在 `enrich()` 裡跑 LLM 抽 temporal state 變化;預設 OFF 控 LLM cost。
637
+
638
+ ### 006-insights.sql
639
+
640
+ | 表 | 用途 |
641
+ |----|------|
642
+ | `insights` | 高階反思:TSTZRANGE evidence window、importance、GIN on source_session_ids、HNSW on 1024-dim embedding,以及 `canonical_key_v2` 非 unique partial index 撐 canonical/revision dedup contract |
643
+
644
+ 重要索引:`idx_insights_canonical_v2_active`(active rows + canonical key 非 null)、`idx_insights_idempotency_key`(revision key unique)。
645
+
646
+ ---
647
+
648
+ ## 疑難排解
649
+
650
+ **`error: type "vector" does not exist`** — pgvector 擴充套件未安裝。以 superuser 執行 `CREATE EXTENSION IF NOT EXISTS vector;`,或使用已內建 pgvector 的 `pgvector/pgvector` Docker image。
651
+
652
+ **`aquifer mcp requires @modelcontextprotocol/sdk and zod`** — 這些現在是正式依賴,應該會自動安裝。若看到此錯誤,重新執行 `npm install` 確保所有依賴已就位。
653
+
654
+ **Recall 沒有回傳結果** — 確認你在 `commit` 之後有執行 `enrich`。未豐富化的 session 無法被搜尋(需要摘要 + embedding)。用 `aquifer stats` 檢查摘要和 turn embedding 是否存在。
655
+
656
+ **OpenClaw 看不到工具** — 請在 `openclaw.json` 使用 `mcp.servers.aquifer`,不要用 plugin。工具會以 `aquifer__session_recall` 等名稱出現。Plugin(`consumers/openclaw-plugin.js`)僅用於 session 擷取。
657
+
658
+ **Embedding 供應商連線被拒** — 確認 `AQUIFER_EMBED_BASE_URL` 可以連通。若使用本地 Ollama,確保伺服器正在執行且模型已下載(`ollama pull bge-m3`)。
659
+
660
+ **啟動拋 `AQ_MIGRATION_LOCK_TIMEOUT`** — 有其他 process 持著 `aquifer:<schema>` 的 migration advisory lock。可能是另一個 `aquifer.init()` 在競爭(正常;贏家跑完輸家下一次會拿到 `pending=[]`),也可能是某個 crash 掉的 worker 把 lock 留下來。調高 `migrations.lockTimeoutMs`,或確認是哪個 pid 死了之後用 `SELECT pg_terminate_backend(pid) FROM pg_locks WHERE locktype='advisory'` 踢掉。
661
+
662
+ **MCP process 啟動就 non-zero exit** — 預期行為:`migrations.mode=apply` 而 `aquifer.init()` 回 `ready=false` 就會 abort。看 stderr 那行 `[aquifer-mcp] startup aborted` 拿 `error.code` / `failedAt`。如果想回到舊的「lazy migrate 等第一個 tool call」行為,設 `AQUIFER_MIGRATIONS_MODE=check`(自己跑 `migrate()`)或 `=off`。
663
+
664
+ ---
665
+
666
+ ## 依賴
667
+
668
+ | 套件 | 用途 |
669
+ |------|------|
670
+ | `pg` ≥ 8.13 | PostgreSQL 客戶端 |
671
+ | `@modelcontextprotocol/sdk` ≥ 1.29 | MCP 伺服器協定 |
672
+ | `zod` ≥ 3.25 | Schema 驗證(MCP 工具) |
673
+
674
+ LLM 和 embedding 呼叫使用原生 HTTP——不需要額外 SDK。
675
+
676
+ ---
677
+
678
+ ## 授權
679
+
680
+ MIT