th-memory-mcp 2.1.0 → 2.2.1
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 +66 -51
- package/README.md +2 -2
- package/README.th.md +2 -2
- package/design.md +59 -0
- 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/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 +6 -0
- package/dist/tools/history.js +1 -1
- package/dist/tools/import_memory.js +10 -0
- package/dist/tools/profile.js +10 -0
- package/dist/tools/recall.js +3 -3
- package/dist/tools/recent_interactions.js +1 -1
- package/package.json +15 -3
package/ARCHITECTURE_v2.md
CHANGED
|
@@ -541,8 +541,6 @@ Create a `supersedes` memory link when useful.
|
|
|
541
541
|
|
|
542
542
|
If deterministic rules cannot safely decide, preserve both records and mark the relationship `contradicts`; do not silently destroy information.
|
|
543
543
|
|
|
544
|
-
Optional AI-assisted resolution may later select the current truth based on explicit user evidence.
|
|
545
|
-
|
|
546
544
|
---
|
|
547
545
|
|
|
548
546
|
# 13. Hybrid Retrieval
|
|
@@ -919,8 +917,9 @@ src/
|
|
|
919
917
|
scripts/
|
|
920
918
|
└── claude-capture.mjs # Claude Code hook capture
|
|
921
919
|
test/
|
|
922
|
-
└── *.test.mjs #
|
|
923
|
-
# retrieval, graph, context, consolidation, benchmark, security,
|
|
920
|
+
└── *.test.mjs # 20 suites (capture, distill, lifecycle, temporal, conflict,
|
|
921
|
+
# retrieval, graph, context, consolidation, benchmark, security,
|
|
922
|
+
# tools_v21, smoke, e2e_transport, retrieval_benchmark, recall_regression, scope, profile, entity_extraction, conflict_benchmark)
|
|
924
923
|
```
|
|
925
924
|
|
|
926
925
|
Compatibility wrappers keep the old `remember`/`recall`/etc. tool names; v2 internals live under `core/`, `memory/`, `retrieval/`.
|
|
@@ -1346,7 +1345,6 @@ Where behavior changes, document it explicitly in `MIGRATION_v2.md`.
|
|
|
1346
1345
|
- clustering
|
|
1347
1346
|
- derived memories
|
|
1348
1347
|
- provenance
|
|
1349
|
-
- optional AI-assisted summarization
|
|
1350
1348
|
|
|
1351
1349
|
## Phase 9 — Benchmark/security
|
|
1352
1350
|
|
|
@@ -1410,63 +1408,63 @@ The release is acceptable only when all are true:
|
|
|
1410
1408
|
|
|
1411
1409
|
### Data
|
|
1412
1410
|
|
|
1413
|
-
- [
|
|
1414
|
-
- [
|
|
1415
|
-
- [
|
|
1416
|
-
- [
|
|
1417
|
-
- [
|
|
1411
|
+
- [x] v1 DB can be backed up and migrated.
|
|
1412
|
+
- [x] preferences map correctly to PREFERENCE memories.
|
|
1413
|
+
- [x] lessons map correctly to LESSON memories.
|
|
1414
|
+
- [x] interactions remain queryable.
|
|
1415
|
+
- [x] profile remains available as a projection.
|
|
1418
1416
|
|
|
1419
1417
|
### Memory
|
|
1420
1418
|
|
|
1421
|
-
- [
|
|
1422
|
-
- [
|
|
1423
|
-
- [
|
|
1424
|
-
- [
|
|
1425
|
-
- [
|
|
1426
|
-
- [
|
|
1419
|
+
- [x] unified memory model works.
|
|
1420
|
+
- [x] lifecycle states work.
|
|
1421
|
+
- [x] temporal validity works.
|
|
1422
|
+
- [x] supersession works.
|
|
1423
|
+
- [x] duplicate detection works.
|
|
1424
|
+
- [x] conflict handling preserves ambiguous evidence.
|
|
1427
1425
|
|
|
1428
1426
|
### Retrieval
|
|
1429
1427
|
|
|
1430
|
-
- [
|
|
1431
|
-
- [
|
|
1432
|
-
- [
|
|
1433
|
-
- [
|
|
1434
|
-
- [
|
|
1435
|
-
- [
|
|
1428
|
+
- [x] FTS retrieval works.
|
|
1429
|
+
- [x] local semantic retrieval works.
|
|
1430
|
+
- [x] RRF fusion works.
|
|
1431
|
+
- [x] metadata/project scope works.
|
|
1432
|
+
- [x] stale/superseded filtering works.
|
|
1433
|
+
- [x] bounded output works.
|
|
1436
1434
|
|
|
1437
1435
|
### Context
|
|
1438
1436
|
|
|
1439
|
-
- [
|
|
1440
|
-
- [
|
|
1441
|
-
- [
|
|
1442
|
-
- [
|
|
1437
|
+
- [x] `get_context` works.
|
|
1438
|
+
- [x] token/character budget is enforced.
|
|
1439
|
+
- [x] current project context is prioritized.
|
|
1440
|
+
- [x] critical constraints are prioritized.
|
|
1443
1441
|
|
|
1444
1442
|
### Graph
|
|
1445
1443
|
|
|
1446
|
-
- [
|
|
1447
|
-
- [
|
|
1448
|
-
- [
|
|
1449
|
-
- [
|
|
1444
|
+
- [x] entity/relation persistence works.
|
|
1445
|
+
- [x] memory links work.
|
|
1446
|
+
- [x] graph traversal is bounded.
|
|
1447
|
+
- [x] graph failure does not break retrieval.
|
|
1450
1448
|
|
|
1451
1449
|
### Security
|
|
1452
1450
|
|
|
1453
|
-
- [
|
|
1454
|
-
- [
|
|
1455
|
-
- [
|
|
1456
|
-
- [
|
|
1451
|
+
- [x] secrets are filtered before storage.
|
|
1452
|
+
- [x] exports are confined to the allowed directory.
|
|
1453
|
+
- [x] memory cannot become executable instructions.
|
|
1454
|
+
- [x] import validates schema/version.
|
|
1457
1455
|
|
|
1458
1456
|
### Reliability
|
|
1459
1457
|
|
|
1460
|
-
- [
|
|
1461
|
-
- [
|
|
1462
|
-
- [
|
|
1458
|
+
- [x] DB errors do not crash the MCP server.
|
|
1459
|
+
- [x] plugin errors do not crash the host.
|
|
1460
|
+
- [x] stdout remains protocol-safe.
|
|
1463
1461
|
|
|
1464
1462
|
### Quality
|
|
1465
1463
|
|
|
1466
|
-
- [
|
|
1467
|
-
- [
|
|
1468
|
-
- [
|
|
1469
|
-
- [
|
|
1464
|
+
- [x] retrieval benchmark passes project targets.
|
|
1465
|
+
- [x] conflict benchmark meets >=95% target.
|
|
1466
|
+
- [x] migration tests pass.
|
|
1467
|
+
- [x] performance targets are measured and documented.
|
|
1470
1468
|
|
|
1471
1469
|
---
|
|
1472
1470
|
|
|
@@ -1564,19 +1562,36 @@ This section folds in the former `design.md` build log. All v2 engine phases are
|
|
|
1564
1562
|
- [x] Phase 6 — Graph engine
|
|
1565
1563
|
- [x] Phase 7 — Context engine (`get_context`)
|
|
1566
1564
|
- [x] Phase 8 — Consolidation (`consolidate`)
|
|
1567
|
-
- [x] Phase 9 — Benchmark + security suites (
|
|
1565
|
+
- [x] Phase 9 — Benchmark + security suites (20 test suites total)
|
|
1568
1566
|
- [x] Phase 10 — Docs + v2.0.0 release (npm, GitHub Release, Official MCP Registry, Glama)
|
|
1569
|
-
- [x] v2.1.0 — `link_memory` / `merge_memory` / `update_memory` / `import_memory` / `extract_memories` (16 tools,
|
|
1567
|
+
- [x] v2.1.0 — `link_memory` / `merge_memory` / `update_memory` / `import_memory` / `extract_memories` (16 tools, 20 suites)
|
|
1570
1568
|
|
|
1571
1569
|
## Test status
|
|
1572
|
-
All
|
|
1573
|
-
|
|
1574
|
-
##
|
|
1575
|
-
- Retrieval
|
|
1576
|
-
-
|
|
1577
|
-
-
|
|
1578
|
-
-
|
|
1570
|
+
All 20 test suites pass (capture, distill, lifecycle 17, temporal 7, conflict 14, retrieval 7, graph 7, context 7, consolidation 5, benchmark 2, security 5, tools_v21 21, smoke 16-tool, e2e_transport, retrieval_benchmark, recall_regression, scope, profile, entity_extraction, conflict_benchmark).
|
|
1571
|
+
|
|
1572
|
+
## Resolved gaps vs original spec (all addressed — no regressions)
|
|
1573
|
+
- ✅ DONE — Retrieval quality benchmark (§26): measured in-repo (`test/retrieval_benchmark.test.mjs`), meets targets (Recall@5=1.00, Precision@5=0.92, MRR=1.00 on a 700-memory baseline).
|
|
1574
|
+
- ✅ DONE — Conflict-resolution quality benchmark (§27, ≥95% correct classification): measured in-repo (`test/conflict_benchmark.test.mjs`), 100% accuracy on a 14-case labeled set covering all 7 required categories (exact/paraphrase duplicate, preference update, direct contradiction, temporary exception, two valid scoped memories, ambiguous conflict). Ambiguous opposite-preference pairs preserved as `contradiction` (linked), never silently superseded.
|
|
1575
|
+
- ✅ DONE — Perf targets (§29): measured in CI via the perf benchmark suite (`test/benchmark.test.mjs`); all per-op latencies meet targets.
|
|
1576
|
+
- ✅ DONE — USER scope (migration 007: `users` table + `memories.user_id`). Clients pass `userId` (external identity) to `import_memory`, `extract_memories`, and `get_context`; `createMemory` derives `USER` scope and auto-creates the user row. `scopeFactorFor` boosts USER-scoped memories (1.0) for the matching user and penalizes foreign ones (0.3); SESSION/PROJECT/GLOBAL isolation unchanged. Preferences and lessons remain global (no user column).
|
|
1577
|
+
- Trust model: `userId` is client-declared (no authentication). This is acceptable for the intended single-user local deployment where the SQLite DB file is private to its owner. Multi-user isolation, if ever required, should be solved at the DB-file level (one DB per user/session), not by adding auth to the engine.
|
|
1578
|
+
- ✅ DONE — Auto entity extraction in consolidation (`src/core/entity-extractor.ts`, wired into `consolidate`).
|
|
1579
|
+
|
|
1580
|
+
## Future-feature backlog (resolved)
|
|
1581
|
+
All previously-deferred future features are implemented. AI-assisted extraction was dropped by owner decision and is not developed.
|
|
1582
|
+
- [x] E2E MCP transport test — `test/e2e_transport.test.mjs`
|
|
1583
|
+
- [x] Retrieval quality benchmark (§26) — `test/retrieval_benchmark.test.mjs`
|
|
1584
|
+
- [x] Perf benchmark (§29) in CI — `test/benchmark.test.mjs` + `.github/workflows/ci.yml`
|
|
1585
|
+
- [x] CI pipeline — `.github/workflows/ci.yml` (ubuntu-latest, node 20, `npm ci`, `npm test`)
|
|
1586
|
+
- [x] Scope hierarchy USER/SESSION/PROJECT/GLOBAL — migrations 006 + 007
|
|
1587
|
+
- [x] Profile auto-projection — `src/tools/profile.ts`
|
|
1588
|
+
- [x] Auto entity extraction in consolidation — `src/core/entity-extractor.ts`
|
|
1589
|
+
- [x] Conflict-resolution quality benchmark (§27) — `test/conflict_benchmark.test.mjs`
|
|
1579
1590
|
|
|
1580
1591
|
## Release
|
|
1581
1592
|
- v2.0.0: released — npm `th-memory-mcp@2.0.0` (latest), GitHub Release `v2.0.0`, Official MCP Registry `io.github.worakorn-prince/th-memory-mcp@2.0.0`, Glama listed.
|
|
1582
|
-
- v2.1.0: implemented and tested locally;
|
|
1593
|
+
- v2.1.0: implemented and tested locally; publish skipped (superseded by v2.2.0).
|
|
1594
|
+
- v2.2.0: fully released — tag + GitHub Release, **npm published** (`th-memory-mcp@2.2.0`), Official MCP Registry auto-ingested from npm (manual `mcp-publisher publish` is redundant and errors `duplicate version`), Glama synced. Only `npm publish` + Glama Sync are required.
|
|
1595
|
+
- v2.2.1: Glama quality fix (server failed to start — better-sqlite3 native binding missing under pnpm 10 on Node 24). Added `pnpm.onlyBuiltDependencies: ["better-sqlite3"]` so pnpm runs the install script that downloads the Node 24 prebuild; bumped better-sqlite3 to `^12.9.0`; added `ip-address@^10.2.0` overrides (npm + pnpm) to clear transitive XSS via `@modelcontextprotocol/sdk`. No code changes. Pending `npm publish` + Glama re-test.
|
|
1596
|
+
|
|
1597
|
+
**Packaging note (Glama / Smithery):** these registries build with `pnpm install` + `pnpm run build` on **Node 24** and disable postinstall scripts by default (pnpm 10). Any native dependency (e.g. `better-sqlite3`) MUST be listed in `pnpm.onlyBuiltDependencies` in `package.json`, otherwise its native binary is never fetched and the server crashes at startup with `Could not locate the bindings file`.
|
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
|
|
|
@@ -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) |
|
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
|
|
|
@@ -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) |
|
package/design.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
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, **npm publish เรียบร้อย**, Official MCP Registry ดึงจาก npm อัตโนมัติ (ไม่ต้องรัน mcp-publisher แยก), Glama Sync เรียบร้อย
|
|
59
|
+
- v2.2.1 — แก้ Glama quality: เพิ่ม `pnpm.onlyBuiltDependencies: ["better-sqlite3"]` ให้ pnpm รันสคริปต์ดาวน์โหลด native binary สำหรับ Node 24, ขยับ better-sqlite3 เป็น `^12.9.0`, เพิ่ม override `ip-address@^10.2.0` (npm+pnpm) อุดช่องโหว่ XSS ผ่าน MCP SDK ไม่มีการเปลี่ยนโค้ด รอ `npm publish` + Glama ทดสอบใหม่
|
|
@@ -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
|
+
}
|
|
@@ -21,6 +21,14 @@ const NEGATION = [
|
|
|
21
21
|
/เลิก/i,
|
|
22
22
|
/อย่า/i,
|
|
23
23
|
];
|
|
24
|
+
// Opposite-value lexicon for ambiguous preference conflicts (preserve, don't
|
|
25
|
+
// silently supersede). A pair is contradictory when each side names a different
|
|
26
|
+
// antonym from this set.
|
|
27
|
+
const ANTONYMS = new Set([
|
|
28
|
+
"tabs", "spaces", "vim", "emacs", "light", "dark",
|
|
29
|
+
"mysql", "postgres", "windows", "mac", "linux",
|
|
30
|
+
"react", "vue", "ios", "android",
|
|
31
|
+
]);
|
|
24
32
|
export function isContradiction(a, b) {
|
|
25
33
|
const na = NEGATION.some((re) => re.test(a));
|
|
26
34
|
const nb = NEGATION.some((re) => re.test(b));
|
|
@@ -34,6 +42,22 @@ export function isContradiction(a, b) {
|
|
|
34
42
|
overlap++;
|
|
35
43
|
return overlap >= 2;
|
|
36
44
|
}
|
|
45
|
+
// True when the two texts each name a *different* antonym from ANTONYMS
|
|
46
|
+
// (e.g. "Prefer tabs" vs "Prefer spaces"). Used to preserve ambiguous
|
|
47
|
+
// conflicts as contradictions instead of destructively superseding them.
|
|
48
|
+
export function hasAntonymPair(a, b) {
|
|
49
|
+
const ta = tokenSet(a);
|
|
50
|
+
const tb = tokenSet(b);
|
|
51
|
+
let aAnt;
|
|
52
|
+
let bAnt;
|
|
53
|
+
for (const t of ta)
|
|
54
|
+
if (ANTONYMS.has(t))
|
|
55
|
+
aAnt = t;
|
|
56
|
+
for (const t of tb)
|
|
57
|
+
if (ANTONYMS.has(t))
|
|
58
|
+
bAnt = t;
|
|
59
|
+
return !!aAnt && !!bAnt && aAnt !== bAnt;
|
|
60
|
+
}
|
|
37
61
|
function tokenSet(s) {
|
|
38
62
|
return new Set(s.toLowerCase().match(/[a-z0-9ก-์]+/gi) ?? []);
|
|
39
63
|
}
|
|
@@ -47,12 +71,24 @@ export function classifyRelationship(candidate, relatedId) {
|
|
|
47
71
|
if (!relVecRow)
|
|
48
72
|
return "unrelated";
|
|
49
73
|
const score = cosine(embed(candidate.content), deserialize(relVecRow.vec));
|
|
74
|
+
const ta = tokenSet(candidate.content);
|
|
75
|
+
const tb = tokenSet(rel.content);
|
|
76
|
+
let overlap = 0;
|
|
77
|
+
for (const t of ta)
|
|
78
|
+
if (tb.has(t))
|
|
79
|
+
overlap++;
|
|
80
|
+
const jac = overlap / (ta.size + tb.size - overlap || 1);
|
|
50
81
|
if (score >= SIM_DUP)
|
|
51
82
|
return "duplicate";
|
|
52
83
|
if (isContradiction(candidate.content, rel.content) && score >= SIM_CONTRA) {
|
|
53
84
|
return "contradiction";
|
|
54
85
|
}
|
|
55
|
-
if (
|
|
86
|
+
if (hasAntonymPair(candidate.content, rel.content)) {
|
|
87
|
+
return "contradiction";
|
|
88
|
+
}
|
|
89
|
+
if (score >= 0.6 && jac >= 0.5)
|
|
90
|
+
return "duplicate";
|
|
91
|
+
if (score >= SIM_UPDATE && jac >= 0.25)
|
|
56
92
|
return "update";
|
|
57
93
|
return "unrelated";
|
|
58
94
|
}
|
package/dist/retrieval/scorer.js
CHANGED
|
@@ -8,12 +8,41 @@ export function finalScore(input) {
|
|
|
8
8
|
clamp01(input.recency) *
|
|
9
9
|
clamp01(input.scopeFactor));
|
|
10
10
|
}
|
|
11
|
-
export function scopeFactorFor(mem,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
export function scopeFactorFor(mem, opts = {}) {
|
|
12
|
+
const { projectId, sessionId, userId } = opts;
|
|
13
|
+
const memScope = mem.scope ?? "GLOBAL";
|
|
14
|
+
if (sessionId) {
|
|
15
|
+
if (memScope === "SESSION" && mem.session_id === sessionId)
|
|
16
|
+
return 1.0;
|
|
17
|
+
if (memScope === "PROJECT" && mem.project_id === projectId)
|
|
18
|
+
return 0.8;
|
|
19
|
+
if (memScope === "USER" && mem.user_id === userId)
|
|
20
|
+
return 0.8;
|
|
21
|
+
if (memScope === "GLOBAL")
|
|
22
|
+
return 0.6;
|
|
23
|
+
return 0.2;
|
|
24
|
+
}
|
|
25
|
+
if (projectId) {
|
|
26
|
+
if (memScope === "PROJECT" && mem.project_id === projectId)
|
|
27
|
+
return 1.0;
|
|
28
|
+
if (memScope === "USER" && mem.user_id === userId)
|
|
29
|
+
return 0.8;
|
|
30
|
+
if (memScope === "GLOBAL")
|
|
31
|
+
return 0.7;
|
|
32
|
+
return 0.3;
|
|
33
|
+
}
|
|
34
|
+
if (userId) {
|
|
35
|
+
if (memScope === "USER" && mem.user_id === userId)
|
|
36
|
+
return 1.0;
|
|
37
|
+
if (memScope === "GLOBAL")
|
|
38
|
+
return 0.7;
|
|
39
|
+
return 0.3;
|
|
40
|
+
}
|
|
41
|
+
if (memScope === "GLOBAL")
|
|
42
|
+
return 0.8;
|
|
43
|
+
if (memScope === "PROJECT")
|
|
17
44
|
return 0.7;
|
|
18
|
-
|
|
45
|
+
if (memScope === "USER")
|
|
46
|
+
return 0.6;
|
|
47
|
+
return 0.4;
|
|
19
48
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { clusterMemories, createDerivedMemory, } from "../core/consolidation-engine.js";
|
|
3
|
+
import { linkEntitiesForMemory, linkMemoriesBySharedEntities, } from "../core/entity-extractor.js";
|
|
3
4
|
import { db, ok } from "../db/index.js";
|
|
4
5
|
export const consolidateInput = {
|
|
5
6
|
threshold: z
|
|
@@ -38,6 +39,16 @@ export function consolidateHandler(args) {
|
|
|
38
39
|
.get(id);
|
|
39
40
|
return ` - [${id}] ${m?.content ?? "?"}`;
|
|
40
41
|
});
|
|
42
|
+
// Auto entity extraction (item 5): persist entities + co-occurrence, then
|
|
43
|
+
// link memories in the cluster that share an entity.
|
|
44
|
+
for (const id of c) {
|
|
45
|
+
const m = db
|
|
46
|
+
.prepare("SELECT content FROM memories WHERE id = ?")
|
|
47
|
+
.get(id);
|
|
48
|
+
if (m)
|
|
49
|
+
linkEntitiesForMemory(id, m.content);
|
|
50
|
+
}
|
|
51
|
+
linkMemoriesBySharedEntities(c);
|
|
41
52
|
lines.push(`Cluster (${c.length}):\n${contents.join("\n")}`);
|
|
42
53
|
if (args.derive === true) {
|
|
43
54
|
const summary = c
|
package/dist/tools/context.js
CHANGED
|
@@ -12,6 +12,11 @@ export const contextInput = {
|
|
|
12
12
|
.optional()
|
|
13
13
|
.describe("Scope context to a project"),
|
|
14
14
|
sessionId: z.string().nullable().optional(),
|
|
15
|
+
userId: z
|
|
16
|
+
.string()
|
|
17
|
+
.nullable()
|
|
18
|
+
.optional()
|
|
19
|
+
.describe("Scope context to a user (USER scope)"),
|
|
15
20
|
limit: z
|
|
16
21
|
.number()
|
|
17
22
|
.int()
|
|
@@ -40,6 +45,7 @@ export function contextHandler(args) {
|
|
|
40
45
|
query: typeof args.query === "string" ? args.query : "",
|
|
41
46
|
projectId: typeof args.projectId === "string" ? args.projectId : null,
|
|
42
47
|
sessionId: typeof args.sessionId === "string" ? args.sessionId : null,
|
|
48
|
+
userId: typeof args.userId === "string" ? args.userId : null,
|
|
43
49
|
limit: typeof args.limit === "number" ? args.limit : undefined,
|
|
44
50
|
maxTokens: typeof args.maxTokens === "number" ? args.maxTokens : undefined,
|
|
45
51
|
includeHistory: args.includeHistory === true,
|
|
@@ -40,6 +40,11 @@ export const extractMemoriesInput = {
|
|
|
40
40
|
.boolean()
|
|
41
41
|
.optional()
|
|
42
42
|
.describe("Create the proposed memories (default false = propose only)"),
|
|
43
|
+
userId: z
|
|
44
|
+
.string()
|
|
45
|
+
.nullable()
|
|
46
|
+
.optional()
|
|
47
|
+
.describe("Scope extracted memories to a user (USER scope)"),
|
|
43
48
|
};
|
|
44
49
|
export function extractMemoriesHandler(args) {
|
|
45
50
|
try {
|
|
@@ -73,6 +78,7 @@ export function extractMemoriesHandler(args) {
|
|
|
73
78
|
type: c.type,
|
|
74
79
|
content: c.content,
|
|
75
80
|
source: "captured",
|
|
81
|
+
userId: typeof args.userId === "string" ? args.userId : null,
|
|
76
82
|
});
|
|
77
83
|
n++;
|
|
78
84
|
}
|
package/dist/tools/history.js
CHANGED
|
@@ -14,7 +14,7 @@ const ITEM_BUDGET = 200;
|
|
|
14
14
|
const searchPrompts = db.prepare("SELECT ts, content FROM interactions WHERE kind = 'prompt' AND content LIKE ? ESCAPE '\\' ORDER BY ts DESC LIMIT ?");
|
|
15
15
|
export async function searchHistoryHandler(args) {
|
|
16
16
|
try {
|
|
17
|
-
const limit = args.limit;
|
|
17
|
+
const limit = args.limit ?? 10;
|
|
18
18
|
const like = `%${escapeLike(args.query)}%`;
|
|
19
19
|
const rows = searchPrompts.all(like, limit);
|
|
20
20
|
if (rows.length === 0) {
|
|
@@ -19,6 +19,11 @@ export const importMemoryInput = {
|
|
|
19
19
|
.boolean()
|
|
20
20
|
.optional()
|
|
21
21
|
.describe("Actually insert memories (default false = dry run, just report)"),
|
|
22
|
+
userId: z
|
|
23
|
+
.string()
|
|
24
|
+
.nullable()
|
|
25
|
+
.optional()
|
|
26
|
+
.describe("Scope imported memories to a user (USER scope)"),
|
|
22
27
|
};
|
|
23
28
|
export function importMemoryHandler(args) {
|
|
24
29
|
try {
|
|
@@ -79,6 +84,11 @@ export function importMemoryHandler(args) {
|
|
|
79
84
|
importance: typeof it.importance === "number" ? it.importance : 0.5,
|
|
80
85
|
projectId: typeof it.projectId === "string" ? it.projectId : null,
|
|
81
86
|
sessionId: typeof it.sessionId === "string" ? it.sessionId : null,
|
|
87
|
+
userId: typeof it.userId === "string"
|
|
88
|
+
? it.userId
|
|
89
|
+
: typeof args.userId === "string"
|
|
90
|
+
? args.userId
|
|
91
|
+
: null,
|
|
82
92
|
validFrom: typeof it.validFrom === "string" ? it.validFrom : null,
|
|
83
93
|
validUntil: typeof it.validUntil === "string" ? it.validUntil : null,
|
|
84
94
|
metadata: it.metadata ?? null,
|
package/dist/tools/profile.js
CHANGED
|
@@ -9,6 +9,7 @@ const LESSON_CORRECTION_MAX = 150;
|
|
|
9
9
|
const profileRows = db.prepare("SELECT section, content FROM profile");
|
|
10
10
|
const topPrefs = db.prepare("SELECT category, key, value, confidence FROM preferences ORDER BY confidence DESC, updated_at DESC LIMIT 15");
|
|
11
11
|
const recentLessons = db.prepare("SELECT situation, mistake, correction FROM lessons ORDER BY created_at DESC, id DESC LIMIT 5");
|
|
12
|
+
const topMemories = db.prepare("SELECT type, content, importance, confidence FROM memories WHERE status = 'active' ORDER BY importance * confidence DESC, updated_at DESC LIMIT 15");
|
|
12
13
|
export function buildProfileText() {
|
|
13
14
|
const parts = [];
|
|
14
15
|
const prof = profileRows.all();
|
|
@@ -34,6 +35,15 @@ export function buildProfileText() {
|
|
|
34
35
|
}
|
|
35
36
|
parts.push(block);
|
|
36
37
|
}
|
|
38
|
+
// Auto-projection: surface top memories from the unified store (spec §7 / item 7).
|
|
39
|
+
const mems = topMemories.all();
|
|
40
|
+
if (mems.length > 0) {
|
|
41
|
+
let block = "[memories]";
|
|
42
|
+
for (const m of mems) {
|
|
43
|
+
block += `\n- (${m.type} c${m.confidence.toFixed(2)}) ${truncate(m.content, 200)}`;
|
|
44
|
+
}
|
|
45
|
+
parts.push(block);
|
|
46
|
+
}
|
|
37
47
|
return truncate(parts.join("\n\n"), PROFILE_BUDGET);
|
|
38
48
|
}
|
|
39
49
|
export async function getProfileHandler() {
|
package/dist/tools/recall.js
CHANGED
|
@@ -32,7 +32,7 @@ function lessonLine(id) {
|
|
|
32
32
|
}
|
|
33
33
|
export async function recallHandler(args) {
|
|
34
34
|
try {
|
|
35
|
-
const limit = args.limit;
|
|
35
|
+
const limit = args.limit ?? 8;
|
|
36
36
|
const parts = [];
|
|
37
37
|
const seen = new Set();
|
|
38
38
|
let prefLines = "";
|
|
@@ -41,14 +41,14 @@ export async function recallHandler(args) {
|
|
|
41
41
|
const rows = searchIndexed.all(buildFtsMatch(args.topic), limit);
|
|
42
42
|
for (const r of rows) {
|
|
43
43
|
if (r.ref_table === "preferences") {
|
|
44
|
-
const line = prefLine(r.ref_id);
|
|
44
|
+
const line = prefLine(Number(r.ref_id));
|
|
45
45
|
if (line) {
|
|
46
46
|
prefLines += line + "\n";
|
|
47
47
|
seen.add(`p:${r.ref_id}`);
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
else if (r.ref_table === "lessons") {
|
|
51
|
-
const line = lessonLine(r.ref_id);
|
|
51
|
+
const line = lessonLine(Number(r.ref_id));
|
|
52
52
|
if (line) {
|
|
53
53
|
lessonLines += line + "\n";
|
|
54
54
|
seen.add(`l:${r.ref_id}`);
|
|
@@ -20,7 +20,7 @@ const selectAny = db.prepare("SELECT id, ts, kind, content FROM interactions ORD
|
|
|
20
20
|
const selectByKind = db.prepare("SELECT id, ts, kind, content FROM interactions WHERE kind = ? ORDER BY id DESC LIMIT ?");
|
|
21
21
|
export async function getRecentInteractionsHandler(args) {
|
|
22
22
|
try {
|
|
23
|
-
const limit = args.limit;
|
|
23
|
+
const limit = args.limit ?? 20;
|
|
24
24
|
const rows = (args.kind
|
|
25
25
|
? selectByKind.all(args.kind, limit)
|
|
26
26
|
: selectAny.all(limit));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "th-memory-mcp",
|
|
3
|
-
"version": "2.1
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"mcpName": "io.github.worakorn-prince/th-memory-mcp",
|
|
5
5
|
"description": "Adaptive Memory MCP server - SQLite-backed memory for OpenCode",
|
|
6
6
|
"author": "worakorn-prince",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"README.md",
|
|
19
19
|
"LICENSE",
|
|
20
20
|
"ARCHITECTURE_v2.md",
|
|
21
|
+
"design.md",
|
|
21
22
|
"opencode.example.json",
|
|
22
23
|
"AGENTS.memory.example.md"
|
|
23
24
|
],
|
|
@@ -26,7 +27,7 @@
|
|
|
26
27
|
"prepublishOnly": "npm run build",
|
|
27
28
|
"start": "node dist/index.js",
|
|
28
29
|
"inspect": "npx @modelcontextprotocol/inspector node dist/index.js",
|
|
29
|
-
"test": "npm run build && node test
|
|
30
|
+
"test": "npm run build && node --test test/*.test.mjs",
|
|
30
31
|
"distill": "node dist/distill.js",
|
|
31
32
|
"quickstart": "npm run build && node scripts/quickstart.mjs"
|
|
32
33
|
},
|
|
@@ -35,9 +36,20 @@
|
|
|
35
36
|
},
|
|
36
37
|
"dependencies": {
|
|
37
38
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
38
|
-
"better-sqlite3": "^12.
|
|
39
|
+
"better-sqlite3": "^12.9.0",
|
|
39
40
|
"zod": "^3.25.0"
|
|
40
41
|
},
|
|
42
|
+
"overrides": {
|
|
43
|
+
"ip-address": "^10.2.0"
|
|
44
|
+
},
|
|
45
|
+
"pnpm": {
|
|
46
|
+
"onlyBuiltDependencies": [
|
|
47
|
+
"better-sqlite3"
|
|
48
|
+
],
|
|
49
|
+
"overrides": {
|
|
50
|
+
"ip-address": "^10.2.0"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
41
53
|
"devDependencies": {
|
|
42
54
|
"@types/better-sqlite3": "^7.6.0",
|
|
43
55
|
"@types/node": "^22.0.0",
|