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/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.0.0 — a temporal, conflict-aware, hybrid-retrieval memory engine. 11 MCP tools, 12 passing test suites. Non-destructive schema migration from v1 (all v1 data preserved). New in v2: lifecycle states, temporal validity, conflict/dedup resolution, hybrid FTS+vector retrieval (RRF), memory graph, `get_context` assembly, and periodic consolidation.
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) ── 11 tools read/write the same SQLite DB
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 [design.md](design.md) for full details.
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, 11 documented tools, a rule-based distill, and an auto-capture plugin you can adapt.
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
- | 11 MCP tools | ✅ | ✅ | ✅ | ✅ | ✅ |
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 (11 tools) |
151
+ | `node test/smoke.mjs` | end-to-end smoke test over JSON-RPC (16 tools) |
152
152
 
153
- ## Tools (11)
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.0.0 — engine ความจำแบบ temporal, conflict-aware, hybrid-retrieval 11 MCP tools, 12 ชุดเทสผ่าน อัปเกรด schema แบบ non-destructive จาก v1 (ข้อมูล v1 ทั้งหมดถูกเก็บรักษา) ฟีเจอร์ใหม่ใน v2: lifecycle states, temporal validity, การแก้ conflict/dedup, hybrid FTS+vector retrieval (RRF), memory graph, ประกอบ `get_context`, และ consolidation
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
- └─ MCP th-memory-mcp (Node.js stdio) ── tools 11 ตัว อ่าน/เขียน SQLite เดียวกัน
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
- รายละเอียดเต็มอยู่ใน [design.md](design.md) — คู่มืออัปเกรดจาก v1 ดูได้ที่ [MIGRATION_v2.md](MIGRATION_v2.md)
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 (11)
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 v2 — Implementation Plan (design.md)
2
-
3
- **Source of truth:** `ARCHITECTURE_v2.md` (on GitHub, baseline v1.2.2).
4
- This file is the working plan for the Building Agent — read it before continuing implementation.
5
-
6
- ## Goal
7
- Evolve th-memory-mcp from a structured local memory MCP into a durable, temporal,
8
- conflict-aware, hybrid-retrieval memory engine. Local-first, offline, SQLite, no
9
- mandatory cloud/LLM. Keep v1 behavior working during the transition.
10
-
11
- ## Current v1 state (summary)
12
- - 9 MCP tools: remember, recall, get_profile, save_lesson, search_history, forget, memory_stats, get_recent_interactions, export_memory.
13
- - Schema (inline `CREATE TABLE IF NOT EXISTS` in `src/db.ts`): `interactions`, `preferences`, `lessons`, `profile`, FTS5 `search_index`, `embeddings` (BLOB, 512-dim hashing-trick vectors).
14
- - No migration system; no schema version table.
15
- - Capture logic triplicated: `src/lib/capture-core.ts`, `src/plugin/learning-capture.ts` (Bun), `scripts/claude-capture.mjs`.
16
- - Semantic search = full in-memory linear scan over all embeddings every `recall`.
17
- - Version metadata inconsistent: `package.json` 1.2.2 vs `config.ts` VERSION 1.1.0 vs `smoke.mjs` assertion 1.1.0 (fix in Phase 10).
18
-
19
- ## Decisions (Phase 1)
20
- - **Migrations are TS modules** (`src/db/migrations.ts`) exporting an ordered `MIGRATIONS` array + `runMigrations(db)`. Each `up(db)` is idempotent (`CREATE TABLE IF NOT EXISTS`) and tracked in `schema_meta`. This avoids `.sql` file-copy issues under `tsc` while keeping deterministic order (spec allows implementation differences).
21
- - **Reuse existing `search_index` + `embeddings`** for v2 `memories` (ref_table = `'memories'`). No new FTS table needed.
22
- - **Non-destructive:** v1 tables (`preferences`, `lessons`, `interactions`, `profile`) are preserved. v2 adds `memories`, `entities`, `relations`, `memory_links`, `schema_meta`.
23
- - **Backfill (M005):** map `preferences → memories(type=PREFERENCE)`, `lessons → memories(type=LESSON)`, sync FTS+embeddings. Guarded by `v1_backfilled` flag so it runs once. `recall` is unaffected because it filters by `ref_table IN ('preferences','lessons')`.
24
- - **No dual-write yet.** v1 tools keep writing only to v1 tables. v2 `memories` is seeded by backfill; new v2 tools (later phases) write to `memories`. Dedup/merge of backfilled vs new entries is Phase 4.
25
- - **Repository layer** (`src/db/repositories/memories.ts`) provides `createMemory`, `getMemoryById`, `setStatus`, `softDelete`, `syncMemoryIndex`, `searchMemories` (FTS + semantic blend, status/project filtering). Not yet wired to a public tool (that is Phase 7 `get_context`).
26
-
27
- ## Phased roadmap
28
- See `ARCHITECTURE_v2.md` §35 for the canonical phase list. Status tracked in the session todo list.
29
-
30
- ## This session (deliverables so far)
31
-
32
- ### Phase 1 — Core abstraction (DONE)
33
- - [x] `src/memory/types.ts` — unified `MemoryType`, `SourceType`, `LifecycleState`, `Scope`, `LinkRelation`, `MemoryRecord`.
34
- - [x] `src/db/migrations.ts` — migration engine + 5 migrations (schema_meta, memories+indexes, entities/relations, memory_links, v1 backfill).
35
- - [x] `src/db/repositories/memories.ts` — core CRUD + index sync + `searchMemories` (hybrid FTS + semantic blend).
36
- - [x] `src/db/index.ts` — call `runMigrations(db)` after existing DDL (non-destructive).
37
- - [x] Build + full test suite green (capture/distill/smoke).
38
-
39
- ### Phase 2 — Lifecycle engine (DONE)
40
- - [x] `src/memory/decay.ts` — `recencyFactor`, per-type `DECAY_LAMBDA_BY_TYPE` (policy classes, not constants).
41
- - [x] `src/memory/source-weights.ts` — `SOURCE_WEIGHTS` map (spec §8).
42
- - [x] `src/memory/scorer.ts` — `computeSalience` (weighted, configurable), `computeConfidence` (source weight + diminishing returns), `salienceForMemory`.
43
- - [x] `src/core/lifecycle-engine.ts` — `canTransition`, `transitionStatus`, `reinforce`, `touch`, `supersede` (sets old=superseded, new=active + `supersedes_id` + `memory_links`), `archive`, `softDelete`, `LifecycleError`.
44
- - [x] `test/lifecycle.test.mjs` — 17 checks (decay, scorer, transitions, supersession, archive). Added to `npm test`.
45
-
46
- ### Phase 3 — Temporal model (DONE)
47
- - [x] `src/core/temporal-engine.ts` — `setValidity`, `memoriesValidAt` (point-in-time truth), `supersessionChain` (oldest→newest), `changesBetween` (change detection).
48
- - [x] `test/temporal.test.mjs` — 7 checks (validity intervals, historical retrieval, supersession chains, change detection). Added to `npm test`.
49
-
50
- ### Phase 4 — Conflict & dedup (DONE)
51
- - [x] `src/memory/deduplicator.ts` — `normalizeText`, `findExactMatch`, `findSimilar`, `deduplicate` (spec §11).
52
- - [x] `src/memory/conflict-resolver.ts` — `isContradiction`, `classifyRelationship` (duplicate/update/contradiction/unrelated), `findRelated`, `resolveConflict` (merge duplicate / supersede update / link contradiction, preserving ambiguous evidence per §12).
53
- - [x] `test/conflict.test.mjs` — 14 checks. Added to `npm test`.
54
- - [x] **Bug fix (v1 too):** `src/lib/embed.ts` `serialize`/`deserialize` rewrote with `DataView` + explicit `byteOffset`. Old code used `Buffer.from(buf).buffer` which can carry a non-zero pool `byteOffset`, corrupting vectors (magnitude ~1e37). This silently broke v1 semantic search.
55
-
56
- ### Phase 5 — Hybrid retrieval (DONE)
57
- - [x] `src/retrieval/fts.ts` — `ftsSearch` (FTS5 over `search_index`, status/project filters, `ORDER BY rank`).
58
- - [x] `src/retrieval/vector.ts` — `vectorSearch` (cosine over `embeddings`, floor 0.15, filters).
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 หมดอายุ)
@@ -17,6 +17,8 @@ export function getContext(opts = {}) {
17
17
  const seeds = retrieve(query, {
18
18
  limit,
19
19
  projectId: opts.projectId,
20
+ sessionId: opts.sessionId,
21
+ userId: opts.userId,
20
22
  includeArchived: opts.includeHistory,
21
23
  });
22
24
  const seedScores = new Map();
@@ -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, opts.projectId);
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,
@@ -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
- (type, content, summary, status, source, confidence, importance, salience,
9
- project_id, session_id, created_at, updated_at, last_accessed_at, access_count,
10
- valid_from, valid_until, metadata)
11
- VALUES (@type, @content, @summary, @status, @source, @confidence, @importance, @salience,
12
- @projectId, @sessionId, @ts, @ts, NULL, 0, @validFrom, @validUntil, @metadata)`)
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);