th-memory-mcp 2.0.0 → 2.2.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.
- package/ARCHITECTURE_v2.md +1594 -0
- package/README.md +13 -8
- package/README.th.md +10 -5
- package/design.md +58 -98
- package/dist/core/context-engine.js +2 -0
- package/dist/core/entity-extractor.js +83 -0
- package/dist/core/retrieval-engine.js +6 -1
- package/dist/db/migrations.js +25 -0
- package/dist/db/repositories/memories.js +16 -5
- package/dist/db/repositories/users.js +23 -0
- package/dist/index.js +30 -0
- package/dist/memory/conflict-resolver.js +37 -1
- package/dist/retrieval/scorer.js +36 -7
- package/dist/tools/consolidate.js +11 -0
- package/dist/tools/context.js +6 -0
- package/dist/tools/extract_memories.js +96 -0
- package/dist/tools/history.js +1 -1
- package/dist/tools/import_memory.js +105 -0
- package/dist/tools/link_memory.js +31 -0
- package/dist/tools/merge_memory.js +49 -0
- package/dist/tools/profile.js +10 -0
- package/dist/tools/recall.js +3 -3
- package/dist/tools/recent_interactions.js +1 -1
- package/dist/tools/update_memory.js +98 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
Long-term memory MCP server for OpenCode — stores preferences, lessons, and usage history in a single local SQLite file (100% local, no external API) so the AI can "remember and adapt" to the user through context-based learning.
|
|
9
9
|
|
|
10
|
-
**Status:** v2.
|
|
10
|
+
**Status:** v2.2.0 — a temporal, conflict-aware, hybrid-retrieval memory engine. 16 MCP tools, 20 passing test suites. Non-destructive schema migration from v1 (all v1 data preserved). New in v2: lifecycle states, temporal validity, conflict/dedup resolution with USER/SESSION/PROJECT/GLOBAL scope, hybrid FTS+vector retrieval (RRF), memory graph, `get_context` assembly, periodic consolidation, and `link_memory` / `merge_memory` / `update_memory` / `import_memory` / `extract_memories`.
|
|
11
11
|
|
|
12
12
|
## Requirements
|
|
13
13
|
|
|
@@ -63,12 +63,12 @@ setx MEMORY_DB_PATH "$PWD/data/memory.db"
|
|
|
63
63
|
```
|
|
64
64
|
OpenCode ──┬─ Plugin learning-capture (Bun) ── auto-captures prompts/tool/error into DB
|
|
65
65
|
│ └─ injects profile back into context on compaction
|
|
66
|
-
└─ MCP th-memory-mcp (Node.js stdio) ──
|
|
66
|
+
└─ MCP th-memory-mcp (Node.js stdio) ── 16 tools read/write the same SQLite DB
|
|
67
67
|
▲
|
|
68
68
|
Global instructions (memory-protocol.md) teach the AI to use the tools
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
See [
|
|
71
|
+
See [ARCHITECTURE_v2.md](ARCHITECTURE_v2.md) for the full architecture spec.
|
|
72
72
|
|
|
73
73
|
## Why th-memory-mcp?
|
|
74
74
|
|
|
@@ -78,7 +78,7 @@ LLMs don't remember you between sessions — every new chat starts blank. th-mem
|
|
|
78
78
|
- **100% local & private** — a single SQLite file, no cloud, no external API. Secrets are filtered before anything is stored.
|
|
79
79
|
- **Low overhead** — each tool call is capped (latency < 10 ms, bounded output size) and the AI only queries memory when it's actually useful, so it never bloats your context.
|
|
80
80
|
- **Resilient** — every tool degrades gracefully; if the DB is unavailable the AI keeps working instead of crashing.
|
|
81
|
-
- **Open & extensible** — MIT licensed,
|
|
81
|
+
- **Open & extensible** — MIT licensed, 16 documented tools, a rule-based distill, and an auto-capture plugin you can adapt.
|
|
82
82
|
|
|
83
83
|
## Works with other harnesses
|
|
84
84
|
|
|
@@ -89,7 +89,7 @@ our hooks bridge; Codex and Cursor use the tools manually (no hook runtime yet).
|
|
|
89
89
|
|
|
90
90
|
| Feature | OpenCode | Claude Code | Qwen Code | Codex | Cursor |
|
|
91
91
|
|---|---|---|---|---|---|
|
|
92
|
-
|
|
|
92
|
+
| 16 MCP tools | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
93
93
|
| Auto-capture (background) | ✅ plugin | ✅ [hooks](CLAUDE_CODE_HOOKS.md) | ⚠️ adapter | ❌ manual | ❌ Rules |
|
|
94
94
|
| Profile injection | ✅ compaction | ✅ UserPromptSubmit | ❌ `get_profile` | ❌ `get_profile` | ❌ `get_profile` |
|
|
95
95
|
| Local semantic search | ✅ (v2.0) | ✅ (v2.0) | ✅ (v2.0) | ✅ (v2.0) | ✅ (v2.0) |
|
|
@@ -136,7 +136,7 @@ anywhere is readable everywhere.
|
|
|
136
136
|
| `npm run build` | compile TypeScript → `dist/` |
|
|
137
137
|
| `npm start` | run the MCP server (stdio) from `dist/index.js` |
|
|
138
138
|
| `npm run distill` | rule-based distill: interactions → profile sections + prune old data (env `RETENTION_DAYS` default 30) |
|
|
139
|
-
| `npm test` | full suite: capture, distill, lifecycle, temporal, conflict, retrieval, graph, context, consolidation, benchmark, security, smoke |
|
|
139
|
+
| `npm test` | full suite: capture, distill, lifecycle, temporal, conflict, retrieval, graph, context, consolidation, benchmark, security, tools_v21, smoke, e2e_transport, retrieval_benchmark, recall_regression, scope, profile, entity_extraction, conflict_benchmark |
|
|
140
140
|
| `node test/capture.test.mjs` | test capture-core (filter secrets, dedupe, truncate, insert SQL) |
|
|
141
141
|
| `node test/distill.test.mjs` | test distill-core (Thai tokenize, stats, profile sections, prune) |
|
|
142
142
|
| `node test/lifecycle.test.mjs` | test lifecycle engine (states, decay, supersession) |
|
|
@@ -148,9 +148,9 @@ anywhere is readable everywhere.
|
|
|
148
148
|
| `node test/consolidation.test.mjs` | test clustering + derived memories |
|
|
149
149
|
| `node test/benchmark.test.mjs` | latency benchmark over 300 memories |
|
|
150
150
|
| `node test/security.test.mjs` | injection / safety checks |
|
|
151
|
-
| `node test/smoke.mjs` | end-to-end smoke test over JSON-RPC (
|
|
151
|
+
| `node test/smoke.mjs` | end-to-end smoke test over JSON-RPC (16 tools) |
|
|
152
152
|
|
|
153
|
-
## Tools (
|
|
153
|
+
## Tools (16)
|
|
154
154
|
|
|
155
155
|
| Tool | Description |
|
|
156
156
|
|------|-------------|
|
|
@@ -165,6 +165,11 @@ anywhere is readable everywhere.
|
|
|
165
165
|
| `export_memory` | export memory to JSON under `data/exports/` only (filename auto-sanitized) |
|
|
166
166
|
| `get_context` | assemble relevant memories for the current task via hybrid retrieval (+ optional graph expansion) with token budgeting |
|
|
167
167
|
| `consolidate` | cluster similar memories via embedding similarity; optionally create derived/consolidated memories linked via `derived_from` |
|
|
168
|
+
| `link_memory` | create a typed relationship between two memories in the graph (supports/contradicts/supersedes/derived_from/related_to/caused_by/depends_on) |
|
|
169
|
+
| `merge_memory` | merge a duplicate/near-duplicate into a canonical memory (source becomes superseded, provenance in `metadata.merged_from`) |
|
|
170
|
+
| `update_memory` | update mutable fields in place, or create a superseding memory when `content` changes (set `supersede=false` to edit in place) |
|
|
171
|
+
| `import_memory` | import memories from JSON (validates type, dedupes against existing, never overwrites blindly); dry-run by default, `apply=true` to insert |
|
|
172
|
+
| `extract_memories` | scan recent captured interactions for memory-intent phrases and propose memory candidates (deterministic, no LLM); dry-run by default, `apply=true` to create (source=captured) |
|
|
168
173
|
|
|
169
174
|
## Install with OpenCode
|
|
170
175
|
|
package/README.th.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
MCP server ความจำระยะยาวสำหรับ OpenCode — เก็บ preferences, lessons, ประวัติการใช้งาน ลง SQLite ไฟล์เดียว (local 100%, ไม่มี external API) เพื่อให้ AI "จำและปรับตัว" กับผู้ใช้ผ่าน context-based learning
|
|
9
9
|
|
|
10
|
-
**สถานะ:** v2.
|
|
10
|
+
**สถานะ:** v2.2.0 — engine ความจำแบบ temporal, conflict-aware, hybrid-retrieval 16 MCP tools, 20 ชุดเทสผ่าน อัปเกรด schema แบบ non-destructive จาก v1 (ข้อมูล v1 ทั้งหมดถูกเก็บรักษา) ฟีเจอร์ใหม่ใน v2: lifecycle states, temporal validity, การแก้ conflict/dedup พร้อม scope USER/SESSION/PROJECT/GLOBAL, hybrid FTS+vector retrieval (RRF), memory graph, ประกอบ `get_context`, consolidation, และ `link_memory` / `merge_memory` / `update_memory` / `import_memory` / `extract_memories`
|
|
11
11
|
|
|
12
12
|
> English: [README.md](README.md)
|
|
13
13
|
|
|
@@ -65,12 +65,12 @@ setx MEMORY_DB_PATH "$PWD/data/memory.db"
|
|
|
65
65
|
```
|
|
66
66
|
OpenCode ──┬─ Plugin learning-capture (Bun) ── จับ prompt/tool/error ลง DB อัตโนมัติ
|
|
67
67
|
│ └─ ฉีด profile กลับ context ตอน compaction
|
|
68
|
-
|
|
68
|
+
└─ MCP th-memory-mcp (Node.js stdio) ── tools 16 ตัว อ่าน/เขียน SQLite เดียวกัน
|
|
69
69
|
▲
|
|
70
70
|
Global instructions (memory-protocol.md) สอน AI ใช้ tools
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
รายละเอียดสถาปัตยกรรมเต็มอยู่ใน [ARCHITECTURE_v2.md](ARCHITECTURE_v2.md) — คู่มืออัปเกรดจาก v1 ดูได้ที่ [MIGRATION_v2.md](MIGRATION_v2.md)
|
|
74
74
|
|
|
75
75
|
## ทำไมต้องใช้ th-memory-mcp?
|
|
76
76
|
|
|
@@ -93,7 +93,7 @@ LLM ไม่ได้จำคุณข้าม session — แชทใหม
|
|
|
93
93
|
| `npm run build` | compile TypeScript → `dist/` |
|
|
94
94
|
| `npm start` | รัน MCP server (stdio) จาก `dist/index.js` |
|
|
95
95
|
| `npm run distill` | rule-based distill: interactions → profile sections + prune ข้อมูลเก่า (env `RETENTION_DAYS` default 30) |
|
|
96
|
-
| `npm test` | ชุดเทสครบ: capture, distill, lifecycle, temporal, conflict, retrieval, graph, context, consolidation, benchmark, security, smoke |
|
|
96
|
+
| `npm test` | ชุดเทสครบ: capture, distill, lifecycle, temporal, conflict, retrieval, graph, context, consolidation, benchmark, security, tools_v21, smoke, e2e_transport, retrieval_benchmark, recall_regression, scope, profile, entity_extraction, conflict_benchmark |
|
|
97
97
|
| `node test/capture.test.mjs` | ทดสอบ capture-core (filter secrets, dedupe, truncate, insert SQL) |
|
|
98
98
|
| `node test/distill.test.mjs` | ทดสอบ distill-core (tokenize ไทย, stats, profile sections, prune) |
|
|
99
99
|
| `node test/lifecycle.test.mjs` | ทดสอบ lifecycle engine (states, decay, supersession) |
|
|
@@ -107,7 +107,7 @@ LLM ไม่ได้จำคุณข้าม session — แชทใหม
|
|
|
107
107
|
| `node test/security.test.mjs` | ตรวจการ injection / ความปลอดภัย |
|
|
108
108
|
| `node test/smoke.mjs` | smoke test end-to-end ผ่าน JSON-RPC (11 tools) |
|
|
109
109
|
|
|
110
|
-
## Tools (
|
|
110
|
+
## Tools (16)
|
|
111
111
|
|
|
112
112
|
| Tool | คำอธิบาย |
|
|
113
113
|
|------|----------|
|
|
@@ -122,6 +122,11 @@ LLM ไม่ได้จำคุณข้าม session — แชทใหม
|
|
|
122
122
|
| `export_memory` | export ความจำเป็น JSON ลง `data/exports/` เท่านั้น (sanitize filename ให้เอง) |
|
|
123
123
|
| `get_context` | ประกอบความจำที่เกี่ยวข้องกับงานปัจจุบันผ่าน hybrid retrieval (+ ขยายผ่าน memory graph ได้) พร้อม token budgeting |
|
|
124
124
|
| `consolidate` | จัดคลัสเตอร์ความจำที่คล้ายกันด้วย embedding cosine และสร้าง derived/consolidated memory ที่ผูกด้วย `derived_from` ได้ |
|
|
125
|
+
| `link_memory` | สร้างความสัมพันธ์แบบมีประเภทระหว่างความจำสองอันในกราฟ |
|
|
126
|
+
| `merge_memory` | รวมความจำที่ซ้ำเข้ากับความจำหลัก (ต้นทางถูก superseded, เก็บ provenance ไว้ใน `metadata.merged_from`) |
|
|
127
|
+
| `update_memory` | อัปเดตฟิลด์ที่เปลี่ยนได้แบบไม่เปลี่ยนตัวตน หรือสร้างความจำแทนที่เมื่อ `content` เปลี่ยน (ตั้ง `supersede=false` เพื่อแก้ในที่) |
|
|
128
|
+
| `import_memory` | นำเข้าความจำจาก JSON (ตรวจสอบ type, dedup กับของเดิม, ไม่เขียนทับแบบมืดบอด); ค่าเริ่มต้น dry-run, ตั้ง `apply=true` เพื่อเพิ่ม |
|
|
129
|
+
| `extract_memories` | สแกน interactions ล่าสุดหาเจตนาบันทึกความจำ และเสนอ/สร้างความจำ (ไม่ใช้ LLM); ค่าเริ่มต้น dry-run, ตั้ง `apply=true` เพื่อสร้าง (source=captured) |
|
|
125
130
|
|
|
126
131
|
## ติดตั้งกับ OpenCode
|
|
127
132
|
|
package/design.md
CHANGED
|
@@ -1,98 +1,58 @@
|
|
|
1
|
-
# th-memory-mcp
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
##
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
##
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
-
|
|
54
|
-
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
-
|
|
58
|
-
-
|
|
59
|
-
- [x] `src/retrieval/fusion.ts` — `rrfFuse` (Reciprocal Rank Fusion, k=60).
|
|
60
|
-
- [x] `src/retrieval/scorer.ts` — `finalScore` (RRF × confidence × importance × recency × scope) + `scopeFactorFor`.
|
|
61
|
-
- [x] `src/core/retrieval-engine.ts` — `retrieve` (FTS + vector → RRF → scoring/rerank → filter → topK).
|
|
62
|
-
- [x] `searchMemories` in repository now delegates to `retrieve` (hybrid). `buildFtsMatch` switched to OR for better recall.
|
|
63
|
-
- [x] `test/retrieval.test.mjs` — 7 checks. Added to `npm test`.
|
|
64
|
-
|
|
65
|
-
### Phase 6 — Graph engine (DONE)
|
|
66
|
-
- [x] `src/core/graph-engine.ts` — `createEntity` (canonical dedup, aliases in metadata), `addRelation` (source_entity_id/relation/target_entity_id), `linkMemories`, `traverse` (bounded BFS over `memory_links`, maxDepth 1–5, relationFilter), `neighbors`.
|
|
67
|
-
- [x] `test/graph.test.mjs` — 7 checks (linking, bounded traversal depth, relation filter, entity dedup, relation insert). Added to `npm test`.
|
|
68
|
-
- [x] Note: `entities` columns are `(name, canonical_name, type, metadata)`; `relations` use `source_entity_id/relation/target_entity_id`; `memory_links` PK `(source_memory_id, relation, target_memory_id)`.
|
|
69
|
-
|
|
70
|
-
### Phase 7 — Context engine (DONE)
|
|
71
|
-
- [x] `src/core/context-engine.ts` — `getContext` (hybrid retrieve → optional graph expansion → temporal validity filter → token budgeting/truncation).
|
|
72
|
-
- [x] `src/tools/context.ts` — `contextInput` (zod) + `contextHandler` (returns assembled context text).
|
|
73
|
-
- [x] Wired `get_context` MCP tool into `index.ts` (now 10 tools total).
|
|
74
|
-
- [x] `test/context.test.mjs` — 7 checks (assembly, graph expansion, token budget, temporal validity). Added to `npm test`.
|
|
75
|
-
- [x] Updated `test/smoke.mjs` to expect 10 tools.
|
|
76
|
-
|
|
77
|
-
### Phase 8 — Consolidation (DONE)
|
|
78
|
-
- [x] `src/core/consolidation-engine.ts` — `clusterMemories` (embedding cosine + union-find), `createDerivedMemory` (type DERIVED, source consolidated, links `derived_from`), `getProvenance`.
|
|
79
|
-
- [x] `src/tools/consolidate.ts` — `consolidateInput` + `consolidateHandler` (read-only cluster listing + optional `derive`).
|
|
80
|
-
- [x] Wired `consolidate` MCP tool into `index.ts` (now 11 tools total).
|
|
81
|
-
- [x] `test/consolidation.test.mjs` — 5 checks. Added to `npm test`.
|
|
82
|
-
- [x] Added `DERIVED` to `MEMORY_TYPES`; added `DERIVED` lambda to `decay.ts`.
|
|
83
|
-
|
|
84
|
-
### Phase 9 — Benchmark & security suite (DONE)
|
|
85
|
-
- [x] `test/benchmark.test.mjs` — 2 checks (retrieve over 300 memories < 2000ms).
|
|
86
|
-
- [x] `test/security.test.mjs` — 5 checks (FTS injection quoting, safe retrieve, malicious content stored verbatim, extreme budget, parameterized SQL).
|
|
87
|
-
- [x] Both added to `npm test` (now 12 suites).
|
|
88
|
-
|
|
89
|
-
### Phase 10 — v2 release (DOCS DONE; PUBLISH PENDING USER)
|
|
90
|
-
- [x] `MIGRATION_v2.md` written (non-destructive upgrade guide, rollback notes).
|
|
91
|
-
- [x] `README.md` updated to v2.0.0 (11 tools, v2 architecture, new test scripts).
|
|
92
|
-
- [ ] Version bump to `2.0.0` + `npm publish --otp=CODE` (needs user OTP).
|
|
93
|
-
- [ ] `git commit` + `git push` + GitHub Release v2.0.0 (needs user).
|
|
94
|
-
- [ ] `.\mcp-publisher.exe publish` (Official MCP Registry; needs user GitHub OAuth + OTP).
|
|
95
|
-
- [ ] Glama: claim ownership + sync from GitHub.
|
|
96
|
-
|
|
97
|
-
## Next
|
|
98
|
-
All v2 engine phases (0–9) complete and tested. Remaining: user-driven release steps above. After release, future work could include automatic entity extraction in consolidation and a periodic auto-consolidate scheduler.
|
|
1
|
+
# th-memory-mcp — Design Notes (ปัจจุบัน)
|
|
2
|
+
|
|
3
|
+
เอกสารนี้อัปเดตล่าสุดสอดคล้องกับสถานะจริงของโค้ด (หลังจบแผนฟีเจอร์อนาคตทั้งหมด ยกเว้น AI-assisted extraction ที่ตัดออก)
|
|
4
|
+
สเปคฉบับเต็มอยู่ที่ `ARCHITECTURE_v2.md` (canonical spec) ไฟล์นี้สรุปภาพรวมและสถานะปัจจุบันเพื่อความสะดวก
|
|
5
|
+
|
|
6
|
+
## สถานะปัจจุบัน
|
|
7
|
+
- **เวอร์ชัน:** `package.json` = `2.2.0`
|
|
8
|
+
- **MCP tools:** 16 tools (`remember`, `recall`, `get_context`, `link_memory`, `merge_memory`, `update_memory`, `import_memory`, `extract_memories`, `consolidate`, `forget`, `history`, `recent_interactions`, `profile`, `lesson`, `memory_stats`, `export_memory`)
|
|
9
|
+
- **ชุดเทสต์:** 20 suites ผ่านหมด (0 fail) — รันผ่าน `npm test` (มี CI บน GitHub Actions)
|
|
10
|
+
|
|
11
|
+
## องค์ประกอบหลัก (src/)
|
|
12
|
+
- `db/` — better-sqlite3 (WAL mode), migrations เชิงเส้น (M001–M007), repositories (`memories`, `users`, `preferences`, `lessons`)
|
|
13
|
+
- `lib/embed.ts` — semantic vector แบบ hashing-trick (ไม่พึ่ง LLM/network)
|
|
14
|
+
- `retrieval/` — FTS5 + vector → RRF fusion → scorer (confidence × importance × recency × scope)
|
|
15
|
+
- `memory/` — types, lifecycle-engine (decay/source-weights), conflict-resolver, deduplicator
|
|
16
|
+
- `core/` — retrieval-engine, context-engine, graph-engine, consolidation-engine, entity-extractor
|
|
17
|
+
- `tools/` — 16 MCP tool handlers
|
|
18
|
+
- `index.ts` — MCP stdio server
|
|
19
|
+
|
|
20
|
+
## ฟีเจอร์ที่ทำเสร็จแล้ว
|
|
21
|
+
- ✅ Temporal model — validity intervals, point-in-time retrieval, supersession chains, change detection
|
|
22
|
+
- ✅ Conflict/dedup — normalize → exact → similar → classify (duplicate/update/contradiction/unrelated); ambiguous conflicts ถูกเก็บไว้ (link `contradicts`) ไม่เขียนทับเงียบๆ
|
|
23
|
+
- ✅ Hybrid retrieval (FTS + vector, RRF)
|
|
24
|
+
- ✅ Memory graph — entities/relations + bounded traversal (`link_memory`)
|
|
25
|
+
- ✅ Context engine — `get_context` with token budgeting, temporal filter, graph expansion
|
|
26
|
+
- ✅ Consolidation — clustering + derived memories (`derived_from` provenance)
|
|
27
|
+
- ✅ Scope hierarchy — `USER` / `SESSION` / `PROJECT` / `GLOBAL` (migrations 006 + 007)
|
|
28
|
+
- `createMemory` อนุมาน scope ตามลำดับ SESSION > PROJECT > USER > GLOBAL
|
|
29
|
+
- `scopeFactorFor` boost ความจำที่เข้าข่ายบริบทปัจจุบัน (USER=1.0, PROJECT/SESSION ตามบริบท, GLOBAL เป็น base)
|
|
30
|
+
- ✅ Profile auto-projection — `profile.ts` ดึงความจำสำคัญมาแทรกใน `[memories]`
|
|
31
|
+
- ✅ Auto entity extraction — `entity-extractor.ts` สกัด entity แบบ heuristic (ไม่ใช้ LLM) ผูกเข้า graph ตอน consolidate
|
|
32
|
+
- ✅ Benchmark in-repo:
|
|
33
|
+
- Retrieval quality (§26) — `test/retrieval_benchmark.test.mjs` (Recall@5=1.00, Precision@5=0.92, MRR=1.00)
|
|
34
|
+
- Perf (§29) — `test/benchmark.test.mjs` วัด latency ต่อ op ผ่าน CI
|
|
35
|
+
- Conflict quality (§27) — `test/conflict_benchmark.test.mjs` (100% บนชุด 14 เคส ครบ 7 หมวด)
|
|
36
|
+
- E2E transport — `test/e2e_transport.test.mjs` (spawn server ผ่าน StdioClientTransport)
|
|
37
|
+
- ✅ CI pipeline — `.github/workflows/ci.yml` (ubuntu-latest, node 20, `npm ci`, `npm test`)
|
|
38
|
+
|
|
39
|
+
## Scope model (รายละเอียด)
|
|
40
|
+
| Scope | เงื่อนไข | พฤติกรรม |
|
|
41
|
+
|-------|----------|----------|
|
|
42
|
+
| SESSION | มี `sessionId` | ผูกกับ session นั้น |
|
|
43
|
+
| PROJECT | มี `projectId` (ไม่มี session) | ผูกกับ project นั้น |
|
|
44
|
+
| USER | มี `userId` (ไม่มี session/project) | ผูกกับ user นั้น (auto-create row ใน `users`) |
|
|
45
|
+
| GLOBAL | ไม่มีอะไรเลย | ความจำร่วมกันทั้งระบบ |
|
|
46
|
+
|
|
47
|
+
`userId` ที่รับจาก client เป็น external identity (string) — ระบบไม่มีการ authenticate; ตัวตัดความเป็นของ client ทั้งหมด
|
|
48
|
+
`preferences` และ `lessons` ยังคงเป็น global (ไม่มี user column)
|
|
49
|
+
|
|
50
|
+
## ข้อจำกัดที่รู้อยู่ (known limitations)
|
|
51
|
+
- **Trust model:** ไม่มี user authentication — `userId` คือสิ่งที่ client แจ้งมา (client-declared) เหมาะกับการ deploy แบบ local single-user ที่ไฟล์ SQLite เป็นของเจ้าของคนเดียว หากต้องการแยกผู้ใช้หลายคน แนะนำแก้ที่ระดับไฟล์ DB (หนึ่ง DB ต่อผู้ใช้) ไม่ใช่เพิ่ม auth ลงใน engine
|
|
52
|
+
- `preferences` / `lessons` ไม่ถูกแบ่งตาม user (ยังเป็น global) — ยอมรับได้สำหรับ single-user
|
|
53
|
+
- Semantic embedding ใช้ hashing-trick (deterministic, offline) — ไม่ใช่ embedding ระดับ LLM จึงมีขีดจำกัดเรื่อง paraphrase ที่ห่างกันมาก
|
|
54
|
+
- **AI-assisted extraction ไม่พัฒนาต่อ** — เจ้าของตัดสินใจตัดหัวข้อนี้ออก `extract_memories` จึงเป็น deterministic heuristic เท่านั้น (ไม่ใช้ LLM) ตามหลักการออกแบบที่ว่า core engine ต้องไม่พึ่งพา external LLM API
|
|
55
|
+
|
|
56
|
+
## Release
|
|
57
|
+
- v2.0.0 ปล่อยแล้ว (npm, GitHub Release, Official MCP Registry, Glama)
|
|
58
|
+
- v2.2.0 — tag + GitHub Release สร้างโดย build agent; `npm` / Official MCP Registry / Glama publish รอเจ้าของ re-auth (publish token หมดอายุ)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { db } from "../db/index.js";
|
|
2
|
+
import { createEntity, addRelation, linkMemories } from "./graph-engine.js";
|
|
3
|
+
import { STOPWORDS } from "../lib/distill-core.js";
|
|
4
|
+
// Heuristic entity extraction (no LLM). Catches quoted strings, CamelCase /
|
|
5
|
+
// PascalCase identifiers, capitalized proper nouns, kebab-case, file paths, URLs,
|
|
6
|
+
// and generic technical tokens (length >= 4, non-stopword).
|
|
7
|
+
export function extractEntities(text) {
|
|
8
|
+
const found = new Set();
|
|
9
|
+
if (!text)
|
|
10
|
+
return [];
|
|
11
|
+
const add = (s) => {
|
|
12
|
+
if (s && s.trim().length >= 2)
|
|
13
|
+
found.add(s.trim());
|
|
14
|
+
};
|
|
15
|
+
for (const m of text.matchAll(/"([^"]{2,60})"|'([^']{2,60})'/g))
|
|
16
|
+
add(m[1] ?? m[2]);
|
|
17
|
+
for (const m of text.matchAll(/[A-Z][a-z0-9]+(?:[A-Z][a-z0-9]+)+/g))
|
|
18
|
+
add(m[0]);
|
|
19
|
+
for (const m of text.matchAll(/\b[A-Z][a-z]{2,}\b/g))
|
|
20
|
+
add(m[0]);
|
|
21
|
+
for (const m of text.matchAll(/\b[a-z]+(?:-[a-z]+){1,}\b/g))
|
|
22
|
+
add(m[0]);
|
|
23
|
+
for (const m of text.matchAll(/\b[\w./-]+\.(ts|js|mjs|py|json|md|yaml|yml)\b/gi))
|
|
24
|
+
add(m[0]);
|
|
25
|
+
for (const m of text.matchAll(/\bhttps?:\/\/\S+/gi))
|
|
26
|
+
add(m[0]);
|
|
27
|
+
for (const m of text.matchAll(/\b[A-Za-z][A-Za-z0-9_-]*\b/g)) {
|
|
28
|
+
const t = m[0];
|
|
29
|
+
if (t.length >= 4 && !STOPWORDS.has(t.toLowerCase()))
|
|
30
|
+
add(t);
|
|
31
|
+
}
|
|
32
|
+
return [...found].filter((e) => e.length >= 2).slice(0, 12);
|
|
33
|
+
}
|
|
34
|
+
// Extract entities from a memory's content, persist them, and record
|
|
35
|
+
// co-occurrence relations (sourced from this memory). Returns entity ids.
|
|
36
|
+
export function linkEntitiesForMemory(memoryId, content) {
|
|
37
|
+
const names = extractEntities(content);
|
|
38
|
+
const ids = [];
|
|
39
|
+
for (const name of names) {
|
|
40
|
+
ids.push(createEntity({ name, type: "concept" }));
|
|
41
|
+
}
|
|
42
|
+
for (let i = 0; i < ids.length; i++) {
|
|
43
|
+
for (let j = i + 1; j < ids.length; j++) {
|
|
44
|
+
addRelation({
|
|
45
|
+
subjectId: ids[i],
|
|
46
|
+
predicate: "co_occurs",
|
|
47
|
+
objectId: ids[j],
|
|
48
|
+
confidence: 0.6,
|
|
49
|
+
sourceMemoryId: memoryId,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return ids;
|
|
54
|
+
}
|
|
55
|
+
// Link memories that share at least one extracted entity (item 5).
|
|
56
|
+
export function linkMemoriesBySharedEntities(memoryIds) {
|
|
57
|
+
const memEntities = new Map();
|
|
58
|
+
for (const id of memoryIds) {
|
|
59
|
+
const rows = db
|
|
60
|
+
.prepare(`SELECT DISTINCT e FROM (
|
|
61
|
+
SELECT target_entity_id AS e FROM relations WHERE source_memory_id = ?
|
|
62
|
+
UNION
|
|
63
|
+
SELECT source_entity_id AS e FROM relations WHERE source_memory_id = ?
|
|
64
|
+
)`)
|
|
65
|
+
.all(id, id);
|
|
66
|
+
memEntities.set(id, new Set(rows.map((r) => r.e)));
|
|
67
|
+
}
|
|
68
|
+
for (let i = 0; i < memoryIds.length; i++) {
|
|
69
|
+
for (let j = i + 1; j < memoryIds.length; j++) {
|
|
70
|
+
const a = memEntities.get(memoryIds[i]);
|
|
71
|
+
const b = memEntities.get(memoryIds[j]);
|
|
72
|
+
let shared = false;
|
|
73
|
+
for (const e of a) {
|
|
74
|
+
if (b.has(e)) {
|
|
75
|
+
shared = true;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (shared)
|
|
80
|
+
linkMemories(memoryIds[i], memoryIds[j], "shares_entity");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -4,6 +4,7 @@ import { vectorSearch } from "../retrieval/vector.js";
|
|
|
4
4
|
import { rrfFuse } from "../retrieval/fusion.js";
|
|
5
5
|
import { finalScore, scopeFactorFor } from "../retrieval/scorer.js";
|
|
6
6
|
import { recencyFactorFor } from "../memory/decay.js";
|
|
7
|
+
import { resolveUserId } from "../db/repositories/users.js";
|
|
7
8
|
// Hybrid retrieval pipeline (spec §13): FTS + vector -> RRF fusion -> scoring/rerank -> filter -> topK
|
|
8
9
|
export function retrieve(query, opts = {}) {
|
|
9
10
|
const limit = Math.min(Math.max(opts.limit ?? 10, 1), 50);
|
|
@@ -26,7 +27,11 @@ export function retrieve(query, opts = {}) {
|
|
|
26
27
|
(mem.status === "deleted" || mem.status === "archived"))
|
|
27
28
|
continue;
|
|
28
29
|
const recency = recencyFactorFor(mem.type, mem.updated_at, now);
|
|
29
|
-
const scope = scopeFactorFor(mem,
|
|
30
|
+
const scope = scopeFactorFor(mem, {
|
|
31
|
+
projectId: opts.projectId,
|
|
32
|
+
sessionId: opts.sessionId,
|
|
33
|
+
userId: opts.userId ? resolveUserId(opts.userId) : null,
|
|
34
|
+
});
|
|
30
35
|
const fs = finalScore({
|
|
31
36
|
rrf,
|
|
32
37
|
confidence: mem.confidence,
|
package/dist/db/migrations.js
CHANGED
|
@@ -136,12 +136,37 @@ const M005_backfill_v1 = {
|
|
|
136
136
|
db.prepare("INSERT INTO schema_meta (key, value) VALUES ('v1_backfilled', '1') ON CONFLICT(key) DO UPDATE SET value = '1'").run();
|
|
137
137
|
},
|
|
138
138
|
};
|
|
139
|
+
const M006_scope = {
|
|
140
|
+
id: "006_scope",
|
|
141
|
+
up(db) {
|
|
142
|
+
db.exec(`ALTER TABLE memories ADD COLUMN scope TEXT NOT NULL DEFAULT 'GLOBAL';`);
|
|
143
|
+
db.exec(`UPDATE memories SET scope = 'SESSION' WHERE session_id IS NOT NULL;`);
|
|
144
|
+
db.exec(`UPDATE memories SET scope = 'PROJECT' WHERE session_id IS NULL AND project_id IS NOT NULL;`);
|
|
145
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_memories_scope ON memories(scope);`);
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
const M007_user = {
|
|
149
|
+
id: "007_user",
|
|
150
|
+
up(db) {
|
|
151
|
+
db.exec(`CREATE TABLE IF NOT EXISTS users (
|
|
152
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
153
|
+
external_id TEXT NOT NULL UNIQUE,
|
|
154
|
+
name TEXT,
|
|
155
|
+
created_at TEXT NOT NULL
|
|
156
|
+
);`);
|
|
157
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_users_external ON users(external_id);`);
|
|
158
|
+
db.exec(`ALTER TABLE memories ADD COLUMN user_id INTEGER REFERENCES users(id);`);
|
|
159
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_memories_user ON memories(user_id);`);
|
|
160
|
+
},
|
|
161
|
+
};
|
|
139
162
|
export const MIGRATIONS = [
|
|
140
163
|
M001_schema_meta,
|
|
141
164
|
M002_memories,
|
|
142
165
|
M003_entities_relations,
|
|
143
166
|
M004_memory_links,
|
|
144
167
|
M005_backfill_v1,
|
|
168
|
+
M006_scope,
|
|
169
|
+
M007_user,
|
|
145
170
|
];
|
|
146
171
|
export function runMigrations(db) {
|
|
147
172
|
db.exec(`CREATE TABLE IF NOT EXISTS schema_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);`);
|
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import { db, syncSearchIndex, removeSearchIndex, upsertEmbedding, removeEmbedding, nowISO, } from "../../db/index.js";
|
|
2
2
|
import { embed } from "../../lib/embed.js";
|
|
3
3
|
import { retrieve } from "../../core/retrieval-engine.js";
|
|
4
|
+
import { ensureUser } from "./users.js";
|
|
4
5
|
export function createMemory(input) {
|
|
5
6
|
const ts = nowISO();
|
|
7
|
+
const uid = input.userId ? ensureUser(input.userId) : null;
|
|
8
|
+
const scope = input.sessionId
|
|
9
|
+
? "SESSION"
|
|
10
|
+
: input.projectId
|
|
11
|
+
? "PROJECT"
|
|
12
|
+
: uid
|
|
13
|
+
? "USER"
|
|
14
|
+
: "GLOBAL";
|
|
6
15
|
const info = db
|
|
7
16
|
.prepare(`INSERT INTO memories
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
17
|
+
(type, content, summary, status, source, confidence, importance, salience,
|
|
18
|
+
project_id, session_id, user_id, scope, created_at, updated_at, last_accessed_at, access_count,
|
|
19
|
+
valid_from, valid_until, metadata)
|
|
20
|
+
VALUES (@type, @content, @summary, @status, @source, @confidence, @importance, @salience,
|
|
21
|
+
@projectId, @sessionId, @userId, @scope, @ts, @ts, NULL, 0, @validFrom, @validUntil, @metadata)`)
|
|
13
22
|
.run({
|
|
14
23
|
type: input.type,
|
|
15
24
|
content: input.content,
|
|
@@ -21,6 +30,8 @@ export function createMemory(input) {
|
|
|
21
30
|
salience: input.salience ?? 0.5,
|
|
22
31
|
projectId: input.projectId ?? null,
|
|
23
32
|
sessionId: input.sessionId ?? null,
|
|
33
|
+
userId: uid,
|
|
34
|
+
scope,
|
|
24
35
|
ts,
|
|
25
36
|
validFrom: input.validFrom ?? null,
|
|
26
37
|
validUntil: input.validUntil ?? null,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { db, nowISO } from "../index.js";
|
|
2
|
+
// Resolve or create a user by external identity (what clients pass as `userId`).
|
|
3
|
+
// Returns the internal user id.
|
|
4
|
+
export function ensureUser(externalId, name) {
|
|
5
|
+
const existing = db
|
|
6
|
+
.prepare("SELECT id FROM users WHERE external_id = ?")
|
|
7
|
+
.get(externalId);
|
|
8
|
+
if (existing)
|
|
9
|
+
return existing.id;
|
|
10
|
+
const res = db
|
|
11
|
+
.prepare("INSERT INTO users (external_id, name, created_at) VALUES (?, ?, ?)")
|
|
12
|
+
.run(externalId, name ?? null, nowISO());
|
|
13
|
+
return Number(res.lastInsertRowid);
|
|
14
|
+
}
|
|
15
|
+
// Map an external user id to its internal id, or null if unknown/absent.
|
|
16
|
+
export function resolveUserId(externalId) {
|
|
17
|
+
if (!externalId)
|
|
18
|
+
return null;
|
|
19
|
+
const r = db
|
|
20
|
+
.prepare("SELECT id FROM users WHERE external_id = ?")
|
|
21
|
+
.get(externalId);
|
|
22
|
+
return r ? r.id : null;
|
|
23
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,11 @@ import { recentInteractionsInput, getRecentInteractionsHandler, } from "./tools/
|
|
|
12
12
|
import { exportMemoryInput, exportMemoryHandler, } from "./tools/export_memory.js";
|
|
13
13
|
import { contextInput, contextHandler } from "./tools/context.js";
|
|
14
14
|
import { consolidateInput, consolidateHandler } from "./tools/consolidate.js";
|
|
15
|
+
import { linkMemoryInput, linkMemoryHandler } from "./tools/link_memory.js";
|
|
16
|
+
import { mergeMemoryInput, mergeMemoryHandler } from "./tools/merge_memory.js";
|
|
17
|
+
import { updateMemoryInput, updateMemoryHandler } from "./tools/update_memory.js";
|
|
18
|
+
import { importMemoryInput, importMemoryHandler } from "./tools/import_memory.js";
|
|
19
|
+
import { extractMemoriesInput, extractMemoriesHandler } from "./tools/extract_memories.js";
|
|
15
20
|
import { VERSION } from "./lib/config.js";
|
|
16
21
|
const server = new McpServer({
|
|
17
22
|
name: "th-memory-mcp",
|
|
@@ -72,6 +77,31 @@ server.registerTool("consolidate", {
|
|
|
72
77
|
description: "Cluster similar memories via embedding similarity and optionally create derived/consolidated memories linked via 'derived_from'. Use during periodic consolidation.",
|
|
73
78
|
inputSchema: consolidateInput,
|
|
74
79
|
}, (args) => consolidateHandler(args));
|
|
80
|
+
server.registerTool("link_memory", {
|
|
81
|
+
title: "Link two memories",
|
|
82
|
+
description: "Create a typed relationship between two memories in the graph (supports/contradicts/supersedes/derived_from/related_to/caused_by/depends_on).",
|
|
83
|
+
inputSchema: linkMemoryInput,
|
|
84
|
+
}, (args) => linkMemoryHandler(args));
|
|
85
|
+
server.registerTool("merge_memory", {
|
|
86
|
+
title: "Merge memories",
|
|
87
|
+
description: "Merge a duplicate/near-duplicate memory into a canonical one. The source becomes superseded and provenance is recorded in metadata.merged_from.",
|
|
88
|
+
inputSchema: mergeMemoryInput,
|
|
89
|
+
}, (args) => mergeMemoryHandler(args));
|
|
90
|
+
server.registerTool("update_memory", {
|
|
91
|
+
title: "Update a memory",
|
|
92
|
+
description: "Update mutable fields (summary/importance/confidence/valid_until/metadata) in place. If content changes, a superseding memory is created by default (set supersede=false to edit in place).",
|
|
93
|
+
inputSchema: updateMemoryInput,
|
|
94
|
+
}, (args) => updateMemoryHandler(args));
|
|
95
|
+
server.registerTool("import_memory", {
|
|
96
|
+
title: "Import memories",
|
|
97
|
+
description: "Import memories from a JSON array or a .json file inside data/exports/. Validates type, dedupes against existing memories, and never overwrites active memory blindly. Dry-run by default; pass apply=true to insert.",
|
|
98
|
+
inputSchema: importMemoryInput,
|
|
99
|
+
}, (args) => importMemoryHandler(args));
|
|
100
|
+
server.registerTool("extract_memories", {
|
|
101
|
+
title: "Extract memories from interactions",
|
|
102
|
+
description: "Scan recent captured interactions for memory-intent phrases and propose memory candidates (deterministic, no LLM). Dry-run by default; pass apply=true to create them (source=captured).",
|
|
103
|
+
inputSchema: extractMemoriesInput,
|
|
104
|
+
}, (args) => extractMemoriesHandler(args));
|
|
75
105
|
async function main() {
|
|
76
106
|
const transport = new StdioServerTransport();
|
|
77
107
|
await server.connect(transport);
|