prism-mcp-server 2.3.11 → 2.5.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 +125 -15
- package/dist/server.js +13 -2
- package/dist/storage/sqlite.js +70 -3
- package/dist/storage/supabase.js +35 -0
- package/dist/tools/index.js +2 -2
- package/dist/tools/sessionMemoryDefinitions.js +88 -0
- package/dist/tools/sessionMemoryHandlers.js +285 -42
- package/dist/utils/tracing.js +139 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -2,19 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/prism-mcp-server)
|
|
4
4
|
[](https://registry.modelcontextprotocol.io)
|
|
5
|
-
[](https://glama.ai/mcp/servers/dcostenco/
|
|
5
|
+
[](https://glama.ai/mcp/servers/dcostenco/prism-mcp)
|
|
6
6
|
[](https://smithery.ai/server/prism-mcp-server)
|
|
7
7
|
[](LICENSE)
|
|
8
8
|
[](https://www.typescriptlang.org/)
|
|
9
9
|
[](https://nodejs.org/)
|
|
10
10
|
|
|
11
|
-
> **Your AI agent's memory that survives between sessions.** Prism MCP is a Model Context Protocol server that gives Claude Desktop, Cursor, Windsurf, and any MCP client **persistent memory**, **time travel**, **visual context**, **multi-agent sync**, and **
|
|
11
|
+
> **Your AI agent's memory that survives between sessions.** Prism MCP is a Model Context Protocol server that gives Claude Desktop, Cursor, Windsurf, and any MCP client **persistent memory**, **time travel**, **visual context**, **multi-agent sync**, **GDPR-compliant deletion**, **memory tracing**, and **LangChain integration** — all running locally with zero cloud dependencies.
|
|
12
12
|
>
|
|
13
|
-
> Built with **SQLite + F32_BLOB vector search**, **optimistic concurrency control**, **MCP Prompts & Resources**, **auto-compaction**, **Gemini-powered Morning Briefings**, and optional **Supabase cloud sync**.
|
|
13
|
+
> Built with **SQLite + F32_BLOB vector search**, **optimistic concurrency control**, **MCP Prompts & Resources**, **auto-compaction**, **Gemini-powered Morning Briefings**, **MemoryTrace explainability**, and optional **Supabase cloud sync**.
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
17
|
-
## What's New in v2.
|
|
17
|
+
## What's New in v2.5.0 — Enterprise Memory 🏗️
|
|
18
|
+
|
|
19
|
+
| Feature | Description |
|
|
20
|
+
|---|---|
|
|
21
|
+
| 🔍 **Memory Tracing (Phase 1)** | Every search now returns a structured `MemoryTrace` with latency breakdown (`embedding_ms`, `storage_ms`, `total_ms`), search strategy, and scoring metadata — surfaced as a separate `content[1]` block for LangSmith integration. |
|
|
22
|
+
| 🛡️ **GDPR Memory Deletion (Phase 2)** | New `session_forget_memory` tool with soft-delete (tombstoning via `deleted_at`) and hard-delete. Ownership guards prevent cross-user deletion. `deleted_reason` column captures GDPR Article 17 justification. Top-K Hole solved by filtering inside SQL, not post-query. |
|
|
23
|
+
| 🔗 **LangChain Integration (Phase 3)** | `PrismMemoryRetriever` and `PrismKnowledgeRetriever` — async-first `BaseRetriever` subclasses that wrap Prism MCP's traced search endpoints. Trace metadata flows automatically into `Document.metadata["trace"]` for LangSmith visibility. |
|
|
24
|
+
| 🧩 **LangGraph Research Agent** | Full example in `examples/langgraph-agent/` — a 5-node agentic research loop with MCP bridge, persistent memory, and `EnsembleRetriever` hybrid search. |
|
|
25
|
+
|
|
26
|
+
<details>
|
|
27
|
+
<summary><strong>What's in v2.3.12 — Stability & Fixes</strong></summary>
|
|
18
28
|
|
|
19
29
|
| Feature | Description |
|
|
20
30
|
|---|---|
|
|
@@ -22,6 +32,8 @@
|
|
|
22
32
|
| 📝 **Debug Logging** | Gated verbose startup logs behind `PRISM_DEBUG_LOGGING` for a cleaner default experience. |
|
|
23
33
|
| ⚡ **Excess Loading Fixes** | Performance improvements to resolve excess loading loops. |
|
|
24
34
|
|
|
35
|
+
</details>
|
|
36
|
+
|
|
25
37
|
<details>
|
|
26
38
|
<summary><strong>What's in v2.3.8 — LangGraph Research Agent</strong></summary>
|
|
27
39
|
|
|
@@ -82,6 +94,9 @@
|
|
|
82
94
|
| **Auto-Compaction** | ✅ Gemini rollups | ❌ | ❌ | ❌ | ❌ |
|
|
83
95
|
| **Morning Briefing** | ✅ Gemini synthesis | ❌ | ❌ | ❌ | ❌ |
|
|
84
96
|
| **OCC (Concurrency)** | ✅ Version-based | ❌ | ❌ | ❌ | ❌ |
|
|
97
|
+
| **GDPR Compliance** | ✅ Soft/hard delete + audit trail | ❌ | ❌ | ❌ | ❌ |
|
|
98
|
+
| **Memory Tracing** | ✅ MemoryTrace with latency breakdown | ❌ | ❌ | ❌ | ❌ |
|
|
99
|
+
| **LangChain Native** | ✅ BaseRetriever adapters | ❌ | ❌ | ❌ | ❌ |
|
|
85
100
|
| **MCP Native** | ✅ stdio (Claude Desktop, Cursor) | ✅ stdio | ❌ Python SDK / REST | ✅ HTTP + MCP | ✅ stdio |
|
|
86
101
|
| **Language** | TypeScript | TypeScript | Python | Python | Python |
|
|
87
102
|
|
|
@@ -316,32 +331,39 @@ Verification pattern (same for both clients):
|
|
|
316
331
|
```mermaid
|
|
317
332
|
graph TB
|
|
318
333
|
Client["AI Client<br/>(Claude Desktop / Cursor / Windsurf)"]
|
|
334
|
+
LangChain["LangChain / LangGraph<br/>(Python Retrievers)"]
|
|
319
335
|
MCP["Prism MCP Server<br/>(TypeScript)"]
|
|
320
336
|
|
|
321
337
|
Client -- "MCP Protocol (stdio)" --> MCP
|
|
338
|
+
LangChain -- "JSON-RPC via MCP Bridge" --> MCP
|
|
322
339
|
|
|
340
|
+
MCP --> Tracing["MemoryTrace Engine<br/>Latency + Strategy + Scoring"]
|
|
323
341
|
MCP --> Dashboard["Mind Palace Dashboard<br/>localhost:3000"]
|
|
324
342
|
MCP --> Brave["Brave Search API<br/>Web + Local + AI Answers"]
|
|
325
343
|
MCP --> Gemini["Google Gemini API<br/>Analysis + Briefings"]
|
|
326
344
|
MCP --> Sandbox["QuickJS Sandbox<br/>Code-Mode Templates"]
|
|
327
345
|
MCP --> SyncBus["SyncBus<br/>Agent Telepathy"]
|
|
346
|
+
MCP --> GDPR["GDPR Engine<br/>Soft/Hard Delete + Audit"]
|
|
328
347
|
|
|
329
348
|
MCP --> Storage{"Storage Backend"}
|
|
330
349
|
Storage --> SQLite["SQLite (Local)<br/>libSQL + F32_BLOB vectors"]
|
|
331
350
|
Storage --> Supabase["Supabase (Cloud)<br/>PostgreSQL + pgvector"]
|
|
332
351
|
|
|
333
|
-
SQLite --> Ledger["session_ledger"]
|
|
352
|
+
SQLite --> Ledger["session_ledger<br/>(+ deleted_at tombstoning)"]
|
|
334
353
|
SQLite --> Handoffs["session_handoffs"]
|
|
335
354
|
SQLite --> History["history_snapshots<br/>(Time Travel)"]
|
|
336
355
|
SQLite --> Media["media vault<br/>(Visual Memory)"]
|
|
337
356
|
|
|
338
357
|
style Client fill:#4A90D9,color:#fff
|
|
358
|
+
style LangChain fill:#1C3D5A,color:#fff
|
|
339
359
|
style MCP fill:#2D3748,color:#fff
|
|
360
|
+
style Tracing fill:#D69E2E,color:#fff
|
|
340
361
|
style Dashboard fill:#9F7AEA,color:#fff
|
|
341
362
|
style Brave fill:#FB542B,color:#fff
|
|
342
363
|
style Gemini fill:#4285F4,color:#fff
|
|
343
364
|
style Sandbox fill:#805AD5,color:#fff
|
|
344
365
|
style SyncBus fill:#ED64A6,color:#fff
|
|
366
|
+
style GDPR fill:#E53E3E,color:#fff
|
|
345
367
|
style Storage fill:#2D3748,color:#fff
|
|
346
368
|
style SQLite fill:#38B2AC,color:#fff
|
|
347
369
|
style Supabase fill:#3ECF8E,color:#fff
|
|
@@ -390,6 +412,14 @@ graph TB
|
|
|
390
412
|
|------|---------|----------|---------|
|
|
391
413
|
| `session_health_check` | Scan brain for integrity issues (`fsck`) | `auto_fix` (boolean) | Health report & auto-repairs |
|
|
392
414
|
|
|
415
|
+
### v2.5 Enterprise Memory Tools
|
|
416
|
+
|
|
417
|
+
| Tool | Purpose | Key Args | Returns |
|
|
418
|
+
|------|---------|----------|---------|
|
|
419
|
+
| `session_forget_memory` | GDPR-compliant deletion (soft/hard) | `memory_id`, `hard_delete`, `reason` | Deletion confirmation + audit |
|
|
420
|
+
| `session_search_memory` | Semantic search with `enable_trace` | `query`, `enable_trace` | Results + `MemoryTrace` in `content[1]` |
|
|
421
|
+
| `knowledge_search` | Knowledge search with `enable_trace` | `query`, `enable_trace` | Results + `MemoryTrace` in `content[1]` |
|
|
422
|
+
|
|
393
423
|
### Code Mode Templates (v2.1)
|
|
394
424
|
|
|
395
425
|
Instead of writing custom JavaScript, pass a `template` name for instant extraction:
|
|
@@ -409,6 +439,53 @@ Instead of writing custom JavaScript, pass a `template` name for instant extract
|
|
|
409
439
|
|
|
410
440
|
---
|
|
411
441
|
|
|
442
|
+
## LangChain / LangGraph Integration
|
|
443
|
+
|
|
444
|
+
Prism MCP includes first-class Python adapters for the LangChain ecosystem, located in `examples/langgraph-agent/`:
|
|
445
|
+
|
|
446
|
+
| Component | File | Purpose |
|
|
447
|
+
|-----------|------|---------|
|
|
448
|
+
| **MCP Bridge** | `mcp_client.py` | JSON-RPC 2.0 client with `call_tool()` and `call_tool_raw()` (preserves `MemoryTrace`) |
|
|
449
|
+
| **Semantic Retriever** | `prism_retriever.py` | `PrismMemoryRetriever(BaseRetriever)` — async-first vector search |
|
|
450
|
+
| **Keyword Retriever** | `prism_retriever.py` | `PrismKnowledgeRetriever(BaseRetriever)` — FTS5 keyword search |
|
|
451
|
+
| **Forget Tool** | `tools.py` | `forget_memory()` — GDPR deletion bridge |
|
|
452
|
+
| **Research Agent** | `agent.py` | 5-node LangGraph agent (plan→search→analyze→decide→answer→save) |
|
|
453
|
+
|
|
454
|
+
### Hybrid Search with EnsembleRetriever
|
|
455
|
+
|
|
456
|
+
Combine both retrievers for hybrid (semantic + keyword) search with a single line:
|
|
457
|
+
|
|
458
|
+
```python
|
|
459
|
+
from langchain.retrievers import EnsembleRetriever
|
|
460
|
+
from prism_retriever import PrismMemoryRetriever, PrismKnowledgeRetriever
|
|
461
|
+
|
|
462
|
+
retriever = EnsembleRetriever(
|
|
463
|
+
retrievers=[PrismMemoryRetriever(...), PrismKnowledgeRetriever(...)],
|
|
464
|
+
weights=[0.7, 0.3], # 70% semantic, 30% keyword
|
|
465
|
+
)
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
### MemoryTrace in LangSmith
|
|
469
|
+
|
|
470
|
+
When `enable_trace=True`, each `Document.metadata["trace"]` contains:
|
|
471
|
+
|
|
472
|
+
```json
|
|
473
|
+
{
|
|
474
|
+
"strategy": "vector_cosine_similarity",
|
|
475
|
+
"latency": { "embedding_ms": 45, "storage_ms": 12, "total_ms": 57 },
|
|
476
|
+
"result_count": 5,
|
|
477
|
+
"threshold": 0.7
|
|
478
|
+
}
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
This metadata flows automatically into LangSmith traces for observability.
|
|
482
|
+
|
|
483
|
+
### Async Architecture
|
|
484
|
+
|
|
485
|
+
The retrievers use `_aget_relevant_documents` as the primary path with `asyncio.to_thread()` to wrap the synchronous MCP bridge. This prevents the `RuntimeError: This event loop is already running` crash that plagues most LangGraph deployments.
|
|
486
|
+
|
|
487
|
+
---
|
|
488
|
+
|
|
412
489
|
## Environment Variables
|
|
413
490
|
|
|
414
491
|
| Variable | Required | Description |
|
|
@@ -422,6 +499,7 @@ Instead of writing custom JavaScript, pass a `template` name for instant extract
|
|
|
422
499
|
| `PRISM_USER_ID` | No | Multi-tenant user isolation (default: `"default"`) |
|
|
423
500
|
| `PRISM_AUTO_CAPTURE` | No | Set `"true"` to auto-capture HTML snapshots of dev servers |
|
|
424
501
|
| `PRISM_CAPTURE_PORTS` | No | Comma-separated ports to scan (default: `3000,3001,5173,8080`) |
|
|
502
|
+
| `PRISM_DEBUG_LOGGING` | No | Set `"true"` to enable verbose debug logs (default: quiet) |
|
|
425
503
|
|
|
426
504
|
---
|
|
427
505
|
|
|
@@ -590,6 +668,29 @@ Every `session_save_ledger` and `session_save_handoff` automatically extracts ke
|
|
|
590
668
|
| **By age** | `older_than_days: 30` | Forget entries older than 30 days |
|
|
591
669
|
| **Dry run** | `dry_run: true` | Preview what would be deleted |
|
|
592
670
|
|
|
671
|
+
### GDPR-Compliant Deletion (v2.5)
|
|
672
|
+
|
|
673
|
+
Prism supports surgical, per-entry deletion for GDPR Article 17 compliance:
|
|
674
|
+
|
|
675
|
+
```json
|
|
676
|
+
// Soft delete (tombstone — reversible, keeps audit trail)
|
|
677
|
+
{ "name": "session_forget_memory", "arguments": {
|
|
678
|
+
"memory_id": "abc123",
|
|
679
|
+
"reason": "User requested data deletion"
|
|
680
|
+
}}
|
|
681
|
+
|
|
682
|
+
// Hard delete (permanent — irreversible)
|
|
683
|
+
{ "name": "session_forget_memory", "arguments": {
|
|
684
|
+
"memory_id": "abc123",
|
|
685
|
+
"hard_delete": true
|
|
686
|
+
}}
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
**How it works:**
|
|
690
|
+
- **Soft delete** sets `deleted_at = NOW()` + `deleted_reason`. The entry stays in the DB for audit but is excluded from ALL search results (vector, FTS5, and context loading).
|
|
691
|
+
- **Hard delete** physically removes the row. FTS5 triggers auto-clean the full-text index.
|
|
692
|
+
- **Top-K Hole Prevention**: `deleted_at IS NULL` filtering happens INSIDE the SQL query, BEFORE the `LIMIT` clause — so `LIMIT 5` always returns 5 live results, never fewer.
|
|
693
|
+
|
|
593
694
|
---
|
|
594
695
|
|
|
595
696
|
## Supabase Setup (Cloud Mode)
|
|
@@ -657,12 +758,12 @@ See [`vertex-ai/`](vertex-ai/) for setup and benchmarks.
|
|
|
657
758
|
|
|
658
759
|
```
|
|
659
760
|
├── src/
|
|
660
|
-
│ ├── server.ts # MCP server core +
|
|
761
|
+
│ ├── server.ts # MCP server core + tool routing
|
|
661
762
|
│ ├── config.ts # Environment management
|
|
662
763
|
│ ├── storage/
|
|
663
|
-
│ │ ├── interface.ts # StorageBackend abstraction
|
|
664
|
-
│ │ ├── sqlite.ts # SQLite local storage (libSQL + F32_BLOB)
|
|
665
|
-
│ │ ├── supabase.ts # Supabase cloud storage
|
|
764
|
+
│ │ ├── interface.ts # StorageBackend abstraction (+ GDPR delete methods)
|
|
765
|
+
│ │ ├── sqlite.ts # SQLite local storage (libSQL + F32_BLOB + deleted_at migration)
|
|
766
|
+
│ │ ├── supabase.ts # Supabase cloud storage (+ soft/hard delete)
|
|
666
767
|
│ │ └── index.ts # Backend factory (auto-selects based on PRISM_STORAGE)
|
|
667
768
|
│ ├── sync/
|
|
668
769
|
│ │ ├── interface.ts # SyncBus abstraction (Telepathy)
|
|
@@ -675,21 +776,30 @@ See [`vertex-ai/`](vertex-ai/) for setup and benchmarks.
|
|
|
675
776
|
│ ├── templates/
|
|
676
777
|
│ │ └── codeMode.ts # 8 pre-built QuickJS extraction templates
|
|
677
778
|
│ ├── tools/
|
|
678
|
-
│ │ ├── definitions.ts #
|
|
779
|
+
│ │ ├── definitions.ts # Search & analysis tool schemas
|
|
679
780
|
│ │ ├── handlers.ts # Search & analysis handlers
|
|
680
|
-
│ │ ├── sessionMemoryDefinitions.ts # Memory +
|
|
681
|
-
│ │ ├── sessionMemoryHandlers.ts # Memory handlers (OCC,
|
|
781
|
+
│ │ ├── sessionMemoryDefinitions.ts # Memory tools + GDPR + tracing schemas
|
|
782
|
+
│ │ ├── sessionMemoryHandlers.ts # Memory handlers (OCC, GDPR, Tracing, Time Travel)
|
|
783
|
+
│ │ ├── compactionHandler.ts # Gemini-powered ledger compaction
|
|
682
784
|
│ │ └── index.ts # Tool registration & re-exports
|
|
683
785
|
│ └── utils/
|
|
786
|
+
│ ├── tracing.ts # MemoryTrace types + factory (Phase 1)
|
|
787
|
+
│ ├── logger.ts # Debug logging (gated by PRISM_DEBUG_LOGGING)
|
|
684
788
|
│ ├── braveApi.ts # Brave Search REST client
|
|
685
789
|
│ ├── googleAi.ts # Gemini SDK wrapper
|
|
686
790
|
│ ├── executor.ts # QuickJS sandbox executor
|
|
687
791
|
│ ├── autoCapture.ts # Dev server HTML snapshot utility
|
|
688
|
-
│ ├── healthCheck.ts # Brain integrity engine
|
|
689
|
-
│ ├── factMerger.ts # Async LLM contradiction resolution
|
|
792
|
+
│ ├── healthCheck.ts # Brain integrity engine + security scanner
|
|
793
|
+
│ ├── factMerger.ts # Async LLM contradiction resolution
|
|
690
794
|
│ ├── git.ts # Git state capture + drift detection
|
|
691
795
|
│ ├── embeddingApi.ts # Embedding generation (Gemini)
|
|
692
796
|
│ └── keywordExtractor.ts # Zero-dependency NLP keyword extraction
|
|
797
|
+
├── examples/langgraph-agent/ # LangChain/LangGraph integration
|
|
798
|
+
│ ├── agent.py # 5-node LangGraph research agent
|
|
799
|
+
│ ├── mcp_client.py # MCP Bridge (call_tool + call_tool_raw)
|
|
800
|
+
│ ├── prism_retriever.py # PrismMemoryRetriever + PrismKnowledgeRetriever
|
|
801
|
+
│ ├── tools.py # Agent tools + GDPR forget_memory
|
|
802
|
+
│ └── demo_retriever.py # Standalone retriever demo
|
|
693
803
|
├── supabase/migrations/ # Cloud mode SQL schemas
|
|
694
804
|
├── vertex-ai/ # Vertex AI hybrid search pipeline
|
|
695
805
|
├── index.ts # Server entry point
|
|
@@ -704,4 +814,4 @@ MIT
|
|
|
704
814
|
|
|
705
815
|
---
|
|
706
816
|
|
|
707
|
-
<sub>**Keywords:** MCP server, Model Context Protocol, Claude Desktop memory, persistent session memory, AI agent memory, local-first, SQLite MCP, Mind Palace, time travel, visual memory, agent telepathy, multi-agent sync, reality drift detection, morning briefing, code mode templates, cursor MCP server, windsurf MCP server, cline MCP server, pgvector semantic search, progressive context loading, MCP Prompts, MCP Resources, knowledge management AI, Brave Search MCP, Gemini analysis, optimistic concurrency control, zero config</sub>
|
|
817
|
+
<sub>**Keywords:** MCP server, Model Context Protocol, Claude Desktop memory, persistent session memory, AI agent memory, local-first, SQLite MCP, Mind Palace, time travel, visual memory, agent telepathy, multi-agent sync, reality drift detection, morning briefing, code mode templates, cursor MCP server, windsurf MCP server, cline MCP server, pgvector semantic search, progressive context loading, MCP Prompts, MCP Resources, knowledge management AI, Brave Search MCP, Gemini analysis, optimistic concurrency control, zero config, GDPR compliant, memory tracing, LangChain retriever, LangGraph agent, soft delete, memory lineage, explainability, enterprise AI memory</sub>
|
package/dist/server.js
CHANGED
|
@@ -79,7 +79,9 @@ MEMORY_HISTORY_TOOL, MEMORY_CHECKOUT_TOOL,
|
|
|
79
79
|
// ─── v2.0: Visual Memory tool definitions ───
|
|
80
80
|
SESSION_SAVE_IMAGE_TOOL, SESSION_VIEW_IMAGE_TOOL,
|
|
81
81
|
// ─── v2.2.0: Health Check tool definition ───
|
|
82
|
-
SESSION_HEALTH_CHECK_TOOL,
|
|
82
|
+
SESSION_HEALTH_CHECK_TOOL,
|
|
83
|
+
// ─── Phase 2: GDPR Memory Deletion tool definition ───
|
|
84
|
+
SESSION_FORGET_MEMORY_TOOL, sessionSaveLedgerHandler, sessionSaveHandoffHandler, sessionLoadContextHandler, knowledgeSearchHandler, knowledgeForgetHandler,
|
|
83
85
|
// ─── v0.4.0: New tool handlers ───
|
|
84
86
|
compactLedgerHandler, sessionSearchMemoryHandler,
|
|
85
87
|
// ─── v2.0: Time Travel handlers ───
|
|
@@ -87,7 +89,9 @@ memoryHistoryHandler, memoryCheckoutHandler,
|
|
|
87
89
|
// ─── v2.0: Visual Memory handlers ───
|
|
88
90
|
sessionSaveImageHandler, sessionViewImageHandler,
|
|
89
91
|
// ─── v2.2.0: Health Check handler ───
|
|
90
|
-
sessionHealthCheckHandler,
|
|
92
|
+
sessionHealthCheckHandler,
|
|
93
|
+
// ─── Phase 2: GDPR Memory Deletion handler ───
|
|
94
|
+
sessionForgetMemoryHandler, } from "./tools/index.js";
|
|
91
95
|
// ─── Dynamic Tool Registration ───────────────────────────────────
|
|
92
96
|
// Base tools: always available regardless of configuration
|
|
93
97
|
const BASE_TOOLS = [
|
|
@@ -118,6 +122,8 @@ const SESSION_MEMORY_TOOLS = [
|
|
|
118
122
|
SESSION_VIEW_IMAGE_TOOL, // session_view_image — retrieve image from vault (v2.0)
|
|
119
123
|
// ─── v2.2.0: Health Check tool ───
|
|
120
124
|
SESSION_HEALTH_CHECK_TOOL, // session_health_check — brain integrity checker (v2.2.0)
|
|
125
|
+
// ─── Phase 2: GDPR Memory Deletion tool ───
|
|
126
|
+
SESSION_FORGET_MEMORY_TOOL, // session_forget_memory — GDPR-compliant memory deletion (Phase 2)
|
|
121
127
|
];
|
|
122
128
|
// Combine: always list ALL tools so scanners (Glama, Smithery, MCP Registry)
|
|
123
129
|
// can enumerate the full capability set. Runtime guards in the CallTool handler
|
|
@@ -493,6 +499,11 @@ export function createServer() {
|
|
|
493
499
|
if (!SESSION_MEMORY_ENABLED)
|
|
494
500
|
throw new Error("Session memory not configured. Set SUPABASE_URL and SUPABASE_KEY.");
|
|
495
501
|
return await sessionHealthCheckHandler(args);
|
|
502
|
+
// ─── Phase 2: GDPR Memory Deletion Tool ───
|
|
503
|
+
case "session_forget_memory":
|
|
504
|
+
if (!SESSION_MEMORY_ENABLED)
|
|
505
|
+
throw new Error("Session memory not configured. Set SUPABASE_URL and SUPABASE_KEY.");
|
|
506
|
+
return await sessionForgetMemoryHandler(args);
|
|
496
507
|
default:
|
|
497
508
|
return {
|
|
498
509
|
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
package/dist/storage/sqlite.js
CHANGED
|
@@ -152,6 +152,36 @@ export class SqliteStorage {
|
|
|
152
152
|
CREATE INDEX IF NOT EXISTS idx_history_version
|
|
153
153
|
ON session_handoffs_history(project, version);
|
|
154
154
|
`);
|
|
155
|
+
// ─── Phase 2 Migration: GDPR Soft Delete Columns ──────────
|
|
156
|
+
//
|
|
157
|
+
// SQLITE GOTCHA: Unlike CREATE TABLE IF NOT EXISTS, ALTER TABLE
|
|
158
|
+
// throws a fatal error if the column already exists. We MUST
|
|
159
|
+
// wrap each ALTER TABLE in a try/catch and only ignore
|
|
160
|
+
// "duplicate column name" errors.
|
|
161
|
+
//
|
|
162
|
+
// This migration runs on every boot but is idempotent — the
|
|
163
|
+
// try/catch ensures it's safe to run repeatedly.
|
|
164
|
+
try {
|
|
165
|
+
await this.db.execute(`ALTER TABLE session_ledger ADD COLUMN deleted_at TEXT DEFAULT NULL`);
|
|
166
|
+
debugLog("[SqliteStorage] Phase 2 migration: added deleted_at column");
|
|
167
|
+
}
|
|
168
|
+
catch (e) {
|
|
169
|
+
// "duplicate column name" = column already exists from prior boot.
|
|
170
|
+
// Any other error is a real problem — rethrow it.
|
|
171
|
+
if (!e.message?.includes("duplicate column name"))
|
|
172
|
+
throw e;
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
await this.db.execute(`ALTER TABLE session_ledger ADD COLUMN deleted_reason TEXT DEFAULT NULL`);
|
|
176
|
+
debugLog("[SqliteStorage] Phase 2 migration: added deleted_reason column");
|
|
177
|
+
}
|
|
178
|
+
catch (e) {
|
|
179
|
+
if (!e.message?.includes("duplicate column name"))
|
|
180
|
+
throw e;
|
|
181
|
+
}
|
|
182
|
+
// Index for fast WHERE deleted_at IS NULL queries.
|
|
183
|
+
// CREATE INDEX IF NOT EXISTS is safe to run repeatedly (no try/catch needed).
|
|
184
|
+
await this.db.execute(`CREATE INDEX IF NOT EXISTS idx_ledger_deleted ON session_ledger(deleted_at)`);
|
|
155
185
|
}
|
|
156
186
|
// ─── PostgREST Filter Parser ───────────────────────────────
|
|
157
187
|
//
|
|
@@ -341,6 +371,37 @@ export class SqliteStorage {
|
|
|
341
371
|
});
|
|
342
372
|
return entries;
|
|
343
373
|
}
|
|
374
|
+
// ─── Phase 2: GDPR-Compliant Memory Deletion ──────────────
|
|
375
|
+
//
|
|
376
|
+
// These methods are SURGICAL — they operate on a single entry by ID.
|
|
377
|
+
// They MUST verify user_id ownership to prevent cross-user deletion.
|
|
378
|
+
//
|
|
379
|
+
// softDeleteLedger: Sets deleted_at + deleted_reason. Entry stays in
|
|
380
|
+
// DB for audit trail. All search queries filter it out via
|
|
381
|
+
// "AND deleted_at IS NULL". Reversible.
|
|
382
|
+
//
|
|
383
|
+
// hardDeleteLedger: Physical DELETE. Irreversible. FTS5 triggers
|
|
384
|
+
// automatically clean up the full-text index.
|
|
385
|
+
async softDeleteLedger(id, userId, reason) {
|
|
386
|
+
// UPDATE (not DELETE): sets tombstone fields while preserving the row.
|
|
387
|
+
// The JS-side datetime('now') matches SQLite's native format.
|
|
388
|
+
await this.db.execute({
|
|
389
|
+
sql: `UPDATE session_ledger
|
|
390
|
+
SET deleted_at = datetime('now'), deleted_reason = ?
|
|
391
|
+
WHERE id = ? AND user_id = ?`,
|
|
392
|
+
args: [reason || null, id, userId],
|
|
393
|
+
});
|
|
394
|
+
debugLog(`[SqliteStorage] Soft-deleted ledger entry ${id} (reason: ${reason || "none"})`);
|
|
395
|
+
}
|
|
396
|
+
async hardDeleteLedger(id, userId) {
|
|
397
|
+
// Physical DELETE — row is permanently removed.
|
|
398
|
+
// FTS5 trigger (ledger_fts_delete) automatically cleans up the index.
|
|
399
|
+
await this.db.execute({
|
|
400
|
+
sql: `DELETE FROM session_ledger WHERE id = ? AND user_id = ?`,
|
|
401
|
+
args: [id, userId],
|
|
402
|
+
});
|
|
403
|
+
debugLog(`[SqliteStorage] Hard-deleted ledger entry ${id}`);
|
|
404
|
+
}
|
|
344
405
|
// ─── Handoff Operations (OCC) ──────────────────────────────
|
|
345
406
|
async saveHandoff(handoff, expectedVersion) {
|
|
346
407
|
// CASE 1: No expectedVersion → UPSERT (create or force-update)
|
|
@@ -471,10 +532,11 @@ export class SqliteStorage {
|
|
|
471
532
|
context.key_context = handoff.key_context;
|
|
472
533
|
if (level === "standard") {
|
|
473
534
|
// Add recent ledger entries as summaries
|
|
535
|
+
// Phase 2: AND deleted_at IS NULL — exclude soft-deleted entries
|
|
474
536
|
const recentLedger = await this.db.execute({
|
|
475
537
|
sql: `SELECT summary, decisions, session_date, created_at
|
|
476
538
|
FROM session_ledger
|
|
477
|
-
WHERE project = ? AND user_id = ? AND archived_at IS NULL
|
|
539
|
+
WHERE project = ? AND user_id = ? AND archived_at IS NULL AND deleted_at IS NULL
|
|
478
540
|
ORDER BY created_at DESC
|
|
479
541
|
LIMIT 5`,
|
|
480
542
|
args: [project, userId],
|
|
@@ -487,10 +549,11 @@ export class SqliteStorage {
|
|
|
487
549
|
return context;
|
|
488
550
|
}
|
|
489
551
|
// Deep: add full session history
|
|
552
|
+
// Phase 2: AND deleted_at IS NULL — exclude soft-deleted entries
|
|
490
553
|
const fullLedger = await this.db.execute({
|
|
491
554
|
sql: `SELECT summary, decisions, files_changed, todos, session_date, created_at
|
|
492
555
|
FROM session_ledger
|
|
493
|
-
WHERE project = ? AND user_id = ? AND archived_at IS NULL
|
|
556
|
+
WHERE project = ? AND user_id = ? AND archived_at IS NULL AND deleted_at IS NULL
|
|
494
557
|
ORDER BY created_at DESC
|
|
495
558
|
LIMIT 50`,
|
|
496
559
|
args: [project, userId],
|
|
@@ -529,6 +592,7 @@ export class SqliteStorage {
|
|
|
529
592
|
AND l.project = ?
|
|
530
593
|
AND l.user_id = ?
|
|
531
594
|
AND l.archived_at IS NULL
|
|
595
|
+
AND l.deleted_at IS NULL
|
|
532
596
|
ORDER BY rank
|
|
533
597
|
LIMIT ?
|
|
534
598
|
`;
|
|
@@ -544,6 +608,7 @@ export class SqliteStorage {
|
|
|
544
608
|
WHERE ledger_fts MATCH ?
|
|
545
609
|
AND l.user_id = ?
|
|
546
610
|
AND l.archived_at IS NULL
|
|
611
|
+
AND l.deleted_at IS NULL
|
|
547
612
|
ORDER BY rank
|
|
548
613
|
LIMIT ?
|
|
549
614
|
`;
|
|
@@ -573,7 +638,7 @@ export class SqliteStorage {
|
|
|
573
638
|
}
|
|
574
639
|
/** Fallback search using LIKE when FTS5 query syntax fails */
|
|
575
640
|
async searchKnowledgeFallback(params) {
|
|
576
|
-
const conditions = ["user_id = ?", "archived_at IS NULL"];
|
|
641
|
+
const conditions = ["user_id = ?", "archived_at IS NULL", "deleted_at IS NULL"];
|
|
577
642
|
const args = [params.userId];
|
|
578
643
|
if (params.project) {
|
|
579
644
|
conditions.push("project = ?");
|
|
@@ -626,6 +691,7 @@ export class SqliteStorage {
|
|
|
626
691
|
AND l.user_id = ?
|
|
627
692
|
AND l.project = ?
|
|
628
693
|
AND l.archived_at IS NULL
|
|
694
|
+
AND l.deleted_at IS NULL
|
|
629
695
|
ORDER BY similarity DESC
|
|
630
696
|
LIMIT ?
|
|
631
697
|
`;
|
|
@@ -640,6 +706,7 @@ export class SqliteStorage {
|
|
|
640
706
|
WHERE l.embedding IS NOT NULL
|
|
641
707
|
AND l.user_id = ?
|
|
642
708
|
AND l.archived_at IS NULL
|
|
709
|
+
AND l.deleted_at IS NULL
|
|
643
710
|
ORDER BY similarity DESC
|
|
644
711
|
LIMIT ?
|
|
645
712
|
`;
|
package/dist/storage/supabase.js
CHANGED
|
@@ -67,6 +67,41 @@ export class SupabaseStorage {
|
|
|
67
67
|
const result = await supabaseDelete("session_ledger", params);
|
|
68
68
|
return Array.isArray(result) ? result : [];
|
|
69
69
|
}
|
|
70
|
+
// ─── Phase 2: GDPR-Compliant Memory Deletion ──────────────
|
|
71
|
+
//
|
|
72
|
+
// These methods are SURGICAL — they operate on a single entry by ID.
|
|
73
|
+
// They MUST verify user_id ownership to prevent cross-user deletion.
|
|
74
|
+
//
|
|
75
|
+
// softDeleteLedger: Sets deleted_at + deleted_reason. Entry stays in
|
|
76
|
+
// DB for audit trail. Supabase RPCs and TypeScript queries filter
|
|
77
|
+
// it out via "WHERE deleted_at IS NULL". Reversible.
|
|
78
|
+
//
|
|
79
|
+
// hardDeleteLedger: Physical DELETE. Irreversible. For GDPR Article 17
|
|
80
|
+
// "right to erasure" when the audit trail must also be removed.
|
|
81
|
+
async softDeleteLedger(id, userId, reason) {
|
|
82
|
+
// PATCH (not DELETE): sets tombstone fields while preserving the row.
|
|
83
|
+
// The deleted_at timestamp is set server-side for consistency.
|
|
84
|
+
// deleted_reason captures the GDPR justification (e.g., "User requested",
|
|
85
|
+
// "Data retention policy", "GDPR Article 17 request").
|
|
86
|
+
await supabasePatch("session_ledger", {
|
|
87
|
+
deleted_at: new Date().toISOString(),
|
|
88
|
+
deleted_reason: reason || null,
|
|
89
|
+
}, {
|
|
90
|
+
id: `eq.${id}`,
|
|
91
|
+
user_id: `eq.${userId}`, // Ownership guard — prevents cross-user deletion
|
|
92
|
+
});
|
|
93
|
+
debugLog(`[SupabaseStorage] Soft-deleted ledger entry ${id} (reason: ${reason || "none"})`);
|
|
94
|
+
}
|
|
95
|
+
async hardDeleteLedger(id, userId) {
|
|
96
|
+
// Physical DELETE — row is permanently removed from the database.
|
|
97
|
+
// This is irreversible. The FTS5 index (if any) is cleaned up by
|
|
98
|
+
// Supabase's built-in trigger handling.
|
|
99
|
+
await supabaseDelete("session_ledger", {
|
|
100
|
+
id: `eq.${id}`,
|
|
101
|
+
user_id: `eq.${userId}`, // Ownership guard
|
|
102
|
+
});
|
|
103
|
+
debugLog(`[SupabaseStorage] Hard-deleted ledger entry ${id}`);
|
|
104
|
+
}
|
|
70
105
|
// ─── Handoff Operations ────────────────────────────────────
|
|
71
106
|
async saveHandoff(handoff, expectedVersion) {
|
|
72
107
|
// Direct mapping from sessionSaveHandoffHandler line 214
|
package/dist/tools/index.js
CHANGED
|
@@ -26,8 +26,8 @@ export { webSearchHandler, braveWebSearchCodeModeHandler, localSearchHandler, br
|
|
|
26
26
|
// This file always exports them — server.ts decides whether to include them in the tool list.
|
|
27
27
|
//
|
|
28
28
|
// v0.4.0: Added SESSION_COMPACT_LEDGER_TOOL and SESSION_SEARCH_MEMORY_TOOL
|
|
29
|
-
export { SESSION_SAVE_LEDGER_TOOL, SESSION_SAVE_HANDOFF_TOOL, SESSION_LOAD_CONTEXT_TOOL, KNOWLEDGE_SEARCH_TOOL, KNOWLEDGE_FORGET_TOOL, SESSION_COMPACT_LEDGER_TOOL, SESSION_SEARCH_MEMORY_TOOL, MEMORY_HISTORY_TOOL, MEMORY_CHECKOUT_TOOL, SESSION_SAVE_IMAGE_TOOL, SESSION_VIEW_IMAGE_TOOL, SESSION_HEALTH_CHECK_TOOL } from "./sessionMemoryDefinitions.js";
|
|
30
|
-
export { sessionSaveLedgerHandler, sessionSaveHandoffHandler, sessionLoadContextHandler, knowledgeSearchHandler, knowledgeForgetHandler, sessionSearchMemoryHandler, backfillEmbeddingsHandler, memoryHistoryHandler, memoryCheckoutHandler, sessionSaveImageHandler, sessionViewImageHandler, sessionHealthCheckHandler } from "./sessionMemoryHandlers.js";
|
|
29
|
+
export { SESSION_SAVE_LEDGER_TOOL, SESSION_SAVE_HANDOFF_TOOL, SESSION_LOAD_CONTEXT_TOOL, KNOWLEDGE_SEARCH_TOOL, KNOWLEDGE_FORGET_TOOL, SESSION_COMPACT_LEDGER_TOOL, SESSION_SEARCH_MEMORY_TOOL, MEMORY_HISTORY_TOOL, MEMORY_CHECKOUT_TOOL, SESSION_SAVE_IMAGE_TOOL, SESSION_VIEW_IMAGE_TOOL, SESSION_HEALTH_CHECK_TOOL, SESSION_FORGET_MEMORY_TOOL } from "./sessionMemoryDefinitions.js";
|
|
30
|
+
export { sessionSaveLedgerHandler, sessionSaveHandoffHandler, sessionLoadContextHandler, knowledgeSearchHandler, knowledgeForgetHandler, sessionSearchMemoryHandler, backfillEmbeddingsHandler, memoryHistoryHandler, memoryCheckoutHandler, sessionSaveImageHandler, sessionViewImageHandler, sessionHealthCheckHandler, sessionForgetMemoryHandler } from "./sessionMemoryHandlers.js";
|
|
31
31
|
// ── Compaction Handler (v0.4.0 — Enhancement #2) ──
|
|
32
32
|
// The compaction handler is in a separate file because it's significantly
|
|
33
33
|
// more complex than the other session memory handlers (chunked Gemini
|
|
@@ -114,6 +114,10 @@ export const SESSION_LOAD_CONTEXT_TOOL = {
|
|
|
114
114
|
},
|
|
115
115
|
};
|
|
116
116
|
// ─── Knowledge Search ─────────────────────────────────────────
|
|
117
|
+
// Phase 1 Change: Added `enable_trace` optional boolean.
|
|
118
|
+
// When true, the handler returns a separate content[1] block with a
|
|
119
|
+
// MemoryTrace object (strategy="keyword", latency, result metadata).
|
|
120
|
+
// Default: false — output is identical to pre-Phase 1 behavior.
|
|
117
121
|
export const KNOWLEDGE_SEARCH_TOOL = {
|
|
118
122
|
name: "knowledge_search",
|
|
119
123
|
description: "Search accumulated knowledge across all sessions by keywords, category, or free text. " +
|
|
@@ -144,6 +148,14 @@ export const KNOWLEDGE_SEARCH_TOOL = {
|
|
|
144
148
|
description: "Maximum results to return (default: 10, max: 50).",
|
|
145
149
|
default: 10,
|
|
146
150
|
},
|
|
151
|
+
// Phase 1: Explainability — when true, appends a MemoryTrace JSON
|
|
152
|
+
// object as content[1] in the response array.
|
|
153
|
+
// MCP clients can parse content[1] programmatically for debugging.
|
|
154
|
+
enable_trace: {
|
|
155
|
+
type: "boolean",
|
|
156
|
+
description: "If true, returns a separate MEMORY TRACE content block with search strategy, " +
|
|
157
|
+
"latency breakdown, and scoring metadata for explainability. Default: false.",
|
|
158
|
+
},
|
|
147
159
|
},
|
|
148
160
|
},
|
|
149
161
|
};
|
|
@@ -265,6 +277,15 @@ export const SESSION_SEARCH_MEMORY_TOOL = {
|
|
|
265
277
|
description: "Minimum similarity score 0-1 (default: 0.7). Higher = more relevant, fewer results.",
|
|
266
278
|
default: 0.7,
|
|
267
279
|
},
|
|
280
|
+
// Phase 1: Explainability — when true, appends a MemoryTrace JSON
|
|
281
|
+
// object as content[1] in the response array. For semantic search,
|
|
282
|
+
// the trace includes embedding_ms (Gemini API time) vs storage_ms
|
|
283
|
+
// (pgvector query time) to pinpoint performance bottlenecks.
|
|
284
|
+
enable_trace: {
|
|
285
|
+
type: "boolean",
|
|
286
|
+
description: "If true, returns a separate MEMORY TRACE content block with search strategy, " +
|
|
287
|
+
"latency breakdown (embedding vs storage), and scoring metadata. Default: false.",
|
|
288
|
+
},
|
|
268
289
|
},
|
|
269
290
|
required: ["query"],
|
|
270
291
|
},
|
|
@@ -306,6 +327,9 @@ export const SESSION_BACKFILL_EMBEDDINGS_TOOL = {
|
|
|
306
327
|
export function isKnowledgeForgetArgs(args) {
|
|
307
328
|
return typeof args === "object" && args !== null;
|
|
308
329
|
}
|
|
330
|
+
// Phase 1: Added enable_trace to the type guard.
|
|
331
|
+
// Optional boolean — when true, the handler returns a MemoryTrace content block.
|
|
332
|
+
// Default: false, so existing callers see no change in behavior.
|
|
309
333
|
export function isKnowledgeSearchArgs(args) {
|
|
310
334
|
return typeof args === "object" && args !== null;
|
|
311
335
|
}
|
|
@@ -328,6 +352,9 @@ export function isSessionSaveHandoffArgs(args) {
|
|
|
328
352
|
typeof args.project === "string");
|
|
329
353
|
}
|
|
330
354
|
// ─── v0.4.0: Type guard for semantic search ──────────────────
|
|
355
|
+
// Phase 1: Added enable_trace to the type guard.
|
|
356
|
+
// Optional boolean — when true, a MemoryTrace block (with embedding_ms,
|
|
357
|
+
// storage_ms, top_score, etc.) is appended as content[1] in the response.
|
|
331
358
|
export function isSessionSearchMemoryArgs(args) {
|
|
332
359
|
return (typeof args === "object" &&
|
|
333
360
|
args !== null &&
|
|
@@ -500,3 +527,64 @@ export const SESSION_HEALTH_CHECK_TOOL = {
|
|
|
500
527
|
export function isSessionHealthCheckArgs(args) {
|
|
501
528
|
return typeof args === "object" && args !== null; // any object is valid
|
|
502
529
|
}
|
|
530
|
+
// ─── Phase 2: GDPR-Compliant Memory Deletion Tool ────────────
|
|
531
|
+
//
|
|
532
|
+
// This tool enables SURGICAL deletion of individual memory entries by ID.
|
|
533
|
+
// It supports two modes:
|
|
534
|
+
// 1. Soft Delete (default): Sets deleted_at = NOW(). The entry remains
|
|
535
|
+
// in the database for audit trails but is excluded from ALL search
|
|
536
|
+
// queries (both FTS5 and vector). This prevents the Top-K Hole
|
|
537
|
+
// problem where LIMIT N queries return fewer results than expected.
|
|
538
|
+
// 2. Hard Delete: Physical removal from the database. Irreversible.
|
|
539
|
+
// Use only when GDPR Article 17 requires complete erasure.
|
|
540
|
+
//
|
|
541
|
+
// DESIGN DECISION: This is intentionally separate from knowledge_forget,
|
|
542
|
+
// which operates on bulk filter criteria (project, category, age).
|
|
543
|
+
// session_forget_memory is surgical — one entry at a time — for
|
|
544
|
+
// precise GDPR compliance.
|
|
545
|
+
export const SESSION_FORGET_MEMORY_TOOL = {
|
|
546
|
+
name: "session_forget_memory",
|
|
547
|
+
description: "Forget (delete) a specific memory entry by its ID. " +
|
|
548
|
+
"Supports two modes:\n\n" +
|
|
549
|
+
"- **Soft delete** (default): Tombstones the entry — it stays in the database " +
|
|
550
|
+
"for audit trails but is excluded from all search results. Reversible.\n" +
|
|
551
|
+
"- **Hard delete**: Permanently removes the entry from the database. Irreversible. " +
|
|
552
|
+
"Use only when GDPR Article 17 requires complete erasure.\n\n" +
|
|
553
|
+
"⚠️ Soft delete is recommended for most use cases. The entry can be " +
|
|
554
|
+
"restored in the future if needed.",
|
|
555
|
+
inputSchema: {
|
|
556
|
+
type: "object",
|
|
557
|
+
properties: {
|
|
558
|
+
memory_id: {
|
|
559
|
+
type: "string",
|
|
560
|
+
description: "The UUID of the memory (ledger) entry to forget. " +
|
|
561
|
+
"You can find this ID in search results returned by " +
|
|
562
|
+
"session_search_memory or knowledge_search.",
|
|
563
|
+
},
|
|
564
|
+
hard_delete: {
|
|
565
|
+
type: "boolean",
|
|
566
|
+
description: "If true, permanently removes the entry (irreversible). " +
|
|
567
|
+
"If false (default), soft-deletes by setting deleted_at timestamp. " +
|
|
568
|
+
"Soft-deleted entries are excluded from searches but remain in the database.",
|
|
569
|
+
},
|
|
570
|
+
reason: {
|
|
571
|
+
type: "string",
|
|
572
|
+
description: "Optional GDPR Article 17 justification for the deletion. " +
|
|
573
|
+
"Examples: 'User requested', 'Data retention policy', 'Outdated information'. " +
|
|
574
|
+
"Stored alongside the tombstone for audit trail purposes.",
|
|
575
|
+
},
|
|
576
|
+
},
|
|
577
|
+
required: ["memory_id"],
|
|
578
|
+
},
|
|
579
|
+
};
|
|
580
|
+
/**
|
|
581
|
+
* Type guard for session_forget_memory arguments.
|
|
582
|
+
* Validates that memory_id (required) is present and is a string.
|
|
583
|
+
* hard_delete and reason are optional.
|
|
584
|
+
*/
|
|
585
|
+
export function isSessionForgetMemoryArgs(args) {
|
|
586
|
+
return (typeof args === "object" &&
|
|
587
|
+
args !== null &&
|
|
588
|
+
"memory_id" in args &&
|
|
589
|
+
typeof args.memory_id === "string");
|
|
590
|
+
}
|
|
@@ -20,9 +20,17 @@ import { getStorage } from "../storage/index.js";
|
|
|
20
20
|
import { toKeywordArray } from "../utils/keywordExtractor.js";
|
|
21
21
|
import { generateEmbedding } from "../utils/embeddingApi.js";
|
|
22
22
|
import { getCurrentGitState, getGitDrift } from "../utils/git.js";
|
|
23
|
+
// ─── Phase 1: Explainability & Memory Lineage ────────────────
|
|
24
|
+
// These utilities provide structured tracing metadata for search operations.
|
|
25
|
+
// When `enable_trace: true` is passed to session_search_memory or knowledge_search,
|
|
26
|
+
// a separate MCP content block (content[1]) is returned with a MemoryTrace object
|
|
27
|
+
// containing: strategy, scores, latency breakdown (embedding/storage/total), and metadata.
|
|
28
|
+
// See src/utils/tracing.ts for full type definitions and design decisions.
|
|
29
|
+
import { createMemoryTrace, traceToContentBlock } from "../utils/tracing.js";
|
|
23
30
|
import { GOOGLE_API_KEY, PRISM_USER_ID, PRISM_AUTO_CAPTURE, PRISM_CAPTURE_PORTS } from "../config.js";
|
|
24
31
|
import { captureLocalEnvironment } from "../utils/autoCapture.js";
|
|
25
32
|
import { isSessionSaveLedgerArgs, isSessionSaveHandoffArgs, isSessionLoadContextArgs, isKnowledgeSearchArgs, isKnowledgeForgetArgs, isSessionSearchMemoryArgs, isBackfillEmbeddingsArgs, isMemoryHistoryArgs, isMemoryCheckoutArgs, isSessionHealthCheckArgs, // v2.2.0: health check type guard
|
|
33
|
+
isSessionForgetMemoryArgs, // Phase 2: GDPR-compliant memory deletion type guard
|
|
26
34
|
} from "./sessionMemoryDefinitions.js";
|
|
27
35
|
import { notifyResourceUpdate } from "../server.js";
|
|
28
36
|
// ─── Save Ledger Handler ──────────────────────────────────────
|
|
@@ -471,15 +479,43 @@ export async function sessionLoadContextHandler(args) {
|
|
|
471
479
|
// ─── Knowledge Search Handler ─────────────────────────────────
|
|
472
480
|
/**
|
|
473
481
|
* Searches accumulated knowledge across all past sessions.
|
|
482
|
+
*
|
|
483
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
484
|
+
* PHASE 1 CHANGES (Explainability & Memory Lineage):
|
|
485
|
+
*
|
|
486
|
+
* Added `enable_trace` optional parameter (default: false).
|
|
487
|
+
* When enabled, appends a MemoryTrace content block to the response
|
|
488
|
+
* with strategy="keyword", timing data, and result metadata.
|
|
489
|
+
*
|
|
490
|
+
* TIMING INSTRUMENTATION:
|
|
491
|
+
* - totalStart: captured before any work begins
|
|
492
|
+
* - storageStart/storageMs: isolates database query time
|
|
493
|
+
* - embeddingMs: always 0 for keyword search (no embedding needed)
|
|
494
|
+
* - totalMs: end-to-end including keyword extraction overhead
|
|
495
|
+
*
|
|
496
|
+
* BACKWARD COMPATIBILITY:
|
|
497
|
+
* When enable_trace is false (default), the response is identical
|
|
498
|
+
* to the pre-Phase 1 implementation. Zero breaking changes.
|
|
499
|
+
*
|
|
500
|
+
* MCP OUTPUT ARRAY:
|
|
501
|
+
* content[0] = human-readable search results (unchanged)
|
|
502
|
+
* content[1] = machine-readable MemoryTrace JSON (only when enable_trace=true)
|
|
503
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
474
504
|
*/
|
|
475
505
|
export async function knowledgeSearchHandler(args) {
|
|
476
506
|
if (!isKnowledgeSearchArgs(args)) {
|
|
477
507
|
throw new Error("Invalid arguments for knowledge_search");
|
|
478
508
|
}
|
|
479
|
-
|
|
509
|
+
// Phase 1: destructure enable_trace (defaults to false for backward compat)
|
|
510
|
+
const { project, query, category, limit = 10, enable_trace = false } = args;
|
|
480
511
|
debugLog(`[knowledge_search] Searching: project=${project || "all"}, query="${query || ""}", category=${category || "any"}, limit=${limit}`);
|
|
512
|
+
// Phase 1: Capture total start time for latency measurement
|
|
513
|
+
const totalStart = performance.now();
|
|
481
514
|
const searchKeywords = query ? toKeywordArray(query) : [];
|
|
482
515
|
const storage = await getStorage();
|
|
516
|
+
// Phase 1: Capture storage-specific start time to isolate DB latency
|
|
517
|
+
// from keyword extraction and other overhead
|
|
518
|
+
const storageStart = performance.now();
|
|
483
519
|
const data = await storage.searchKnowledge({
|
|
484
520
|
project: project || null,
|
|
485
521
|
keywords: searchKeywords,
|
|
@@ -488,27 +524,60 @@ export async function knowledgeSearchHandler(args) {
|
|
|
488
524
|
limit: Math.min(limit, 50),
|
|
489
525
|
userId: PRISM_USER_ID,
|
|
490
526
|
});
|
|
527
|
+
const storageMs = performance.now() - storageStart;
|
|
528
|
+
const totalMs = performance.now() - totalStart;
|
|
491
529
|
if (!data) {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
text: `🔍 No knowledge found matching your search.\n` +
|
|
496
|
-
(query ? `Query: "${query}"\n` : "") +
|
|
497
|
-
(category ? `Category: ${category}\n` : "") +
|
|
498
|
-
(project ? `Project: ${project}\n` : "") +
|
|
499
|
-
`\nTip: Try session_search_memory for semantic (meaning-based) search ` +
|
|
500
|
-
`if keyword search doesn't find what you need.`,
|
|
501
|
-
}],
|
|
502
|
-
isError: false,
|
|
503
|
-
};
|
|
504
|
-
}
|
|
505
|
-
return {
|
|
506
|
-
content: [{
|
|
530
|
+
// Phase 1: Use contentBlocks array instead of inline object
|
|
531
|
+
// so we can conditionally push the trace block at content[1]
|
|
532
|
+
const contentBlocks = [{
|
|
507
533
|
type: "text",
|
|
508
|
-
text:
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
534
|
+
text: `🔍 No knowledge found matching your search.\n` +
|
|
535
|
+
(query ? `Query: "${query}"\n` : "") +
|
|
536
|
+
(category ? `Category: ${category}\n` : "") +
|
|
537
|
+
(project ? `Project: ${project}\n` : "") +
|
|
538
|
+
`\nTip: Try session_search_memory for semantic (meaning-based) search ` +
|
|
539
|
+
`if keyword search doesn't find what you need.`,
|
|
540
|
+
}];
|
|
541
|
+
// Phase 1: Append trace block even on empty results — this tells
|
|
542
|
+
// the developer the search DID execute, it just found nothing.
|
|
543
|
+
// topScore and threshold are null for keyword search (no scoring system).
|
|
544
|
+
if (enable_trace) {
|
|
545
|
+
const trace = createMemoryTrace({
|
|
546
|
+
strategy: "keyword",
|
|
547
|
+
query: query || "",
|
|
548
|
+
resultCount: 0,
|
|
549
|
+
topScore: null, // keyword search doesn't produce similarity scores
|
|
550
|
+
threshold: null, // keyword search has no threshold concept
|
|
551
|
+
embeddingMs: 0, // no embedding needed for keyword search
|
|
552
|
+
storageMs,
|
|
553
|
+
totalMs,
|
|
554
|
+
project: project || null,
|
|
555
|
+
});
|
|
556
|
+
contentBlocks.push(traceToContentBlock(trace));
|
|
557
|
+
}
|
|
558
|
+
return { content: contentBlocks, isError: false };
|
|
559
|
+
}
|
|
560
|
+
// Phase 1: Wrap in contentBlocks array for optional trace attachment
|
|
561
|
+
const contentBlocks = [{
|
|
562
|
+
type: "text",
|
|
563
|
+
text: `🧠 Found ${data.count} knowledge entries:\n\n${JSON.stringify(data, null, 2)}`,
|
|
564
|
+
}];
|
|
565
|
+
// Phase 1: Attach MemoryTrace with strategy="keyword" and timing data
|
|
566
|
+
if (enable_trace) {
|
|
567
|
+
const trace = createMemoryTrace({
|
|
568
|
+
strategy: "keyword",
|
|
569
|
+
query: query || "",
|
|
570
|
+
resultCount: data.count,
|
|
571
|
+
topScore: null, // keyword search doesn't produce similarity scores
|
|
572
|
+
threshold: null, // keyword search has no threshold concept
|
|
573
|
+
embeddingMs: 0, // no embedding needed for keyword search
|
|
574
|
+
storageMs,
|
|
575
|
+
totalMs,
|
|
576
|
+
project: project || null,
|
|
577
|
+
});
|
|
578
|
+
contentBlocks.push(traceToContentBlock(trace));
|
|
579
|
+
}
|
|
580
|
+
return { content: contentBlocks, isError: false };
|
|
512
581
|
}
|
|
513
582
|
// ─── Knowledge Forget Handler ─────────────────────────────────
|
|
514
583
|
/**
|
|
@@ -581,15 +650,55 @@ export async function knowledgeForgetHandler(args) {
|
|
|
581
650
|
}
|
|
582
651
|
// ─── Semantic Search Handler ──────────────────────────────────
|
|
583
652
|
/**
|
|
584
|
-
* Searches session history semantically using embeddings.
|
|
653
|
+
* Searches session history semantically using vector embeddings.
|
|
654
|
+
*
|
|
655
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
656
|
+
* PHASE 1 CHANGES (Explainability & Memory Lineage):
|
|
657
|
+
*
|
|
658
|
+
* Added `enable_trace` optional parameter (default: false).
|
|
659
|
+
* When enabled, appends a MemoryTrace content block to the response.
|
|
660
|
+
*
|
|
661
|
+
* TIMING INSTRUMENTATION (3 checkpoints):
|
|
662
|
+
* 1. totalStart: before any work begins
|
|
663
|
+
* 2. embeddingStart/embeddingMs: isolates Gemini API call latency
|
|
664
|
+
* (this is the most variable — 50ms to 2000ms depending on load)
|
|
665
|
+
* 3. storageStart/storageMs: isolates pgvector/SQLite query time
|
|
666
|
+
*
|
|
667
|
+
* WHY SEPARATE EMBEDDING FROM STORAGE:
|
|
668
|
+
* A single latency_ms number is misleading. Example:
|
|
669
|
+
* - 500ms total could be 480ms Gemini API + 20ms pgvector
|
|
670
|
+
* → Fix: cache embeddings or switch to a faster model
|
|
671
|
+
* - 500ms total could be 20ms Gemini API + 480ms pgvector
|
|
672
|
+
* → Fix: add an index or reduce vector dimensions
|
|
673
|
+
*
|
|
674
|
+
* SCORE BUBBLING:
|
|
675
|
+
* The `topScore` in the trace comes from results[0].similarity,
|
|
676
|
+
* which is the cosine distance returned by SemanticSearchResult
|
|
677
|
+
* (see src/storage/interface.ts L104-112). No storage layer
|
|
678
|
+
* modifications were needed — the score was already there.
|
|
679
|
+
*
|
|
680
|
+
* MCP OUTPUT ARRAY:
|
|
681
|
+
* content[0] = human-readable search results (unchanged)
|
|
682
|
+
* content[1] = machine-readable MemoryTrace JSON (only when enable_trace=true)
|
|
683
|
+
*
|
|
684
|
+
* BACKWARD COMPATIBILITY:
|
|
685
|
+
* When enable_trace is false (default), the response is byte-for-byte
|
|
686
|
+
* identical to the pre-Phase 1 implementation. Zero breaking changes.
|
|
687
|
+
* Existing tests pass without modification.
|
|
688
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
585
689
|
*/
|
|
586
690
|
export async function sessionSearchMemoryHandler(args) {
|
|
587
691
|
if (!isSessionSearchMemoryArgs(args)) {
|
|
588
692
|
throw new Error("Invalid arguments for session_search_memory");
|
|
589
693
|
}
|
|
590
|
-
const { query, project, limit = 5, similarity_threshold = 0.7,
|
|
694
|
+
const { query, project, limit = 5, similarity_threshold = 0.7,
|
|
695
|
+
// Phase 1: enable_trace defaults to false for full backward compatibility.
|
|
696
|
+
// When true, a MemoryTrace JSON block is appended as content[1].
|
|
697
|
+
enable_trace = false, } = args;
|
|
591
698
|
debugLog(`[session_search_memory] Semantic search: query="${query}", ` +
|
|
592
699
|
`project=${project || "all"}, limit=${limit}, threshold=${similarity_threshold}`);
|
|
700
|
+
// Phase 1: Start total latency timer BEFORE any work (embedding + storage)
|
|
701
|
+
const totalStart = performance.now();
|
|
593
702
|
// Step 1: Generate embedding for the search query
|
|
594
703
|
if (!GOOGLE_API_KEY) {
|
|
595
704
|
return {
|
|
@@ -603,6 +712,9 @@ export async function sessionSearchMemoryHandler(args) {
|
|
|
603
712
|
};
|
|
604
713
|
}
|
|
605
714
|
let queryEmbedding;
|
|
715
|
+
// Phase 1: Start embedding latency timer — isolates Gemini API call time.
|
|
716
|
+
// This is the most variable component: 50ms on a good day, 2000ms under load.
|
|
717
|
+
const embeddingStart = performance.now();
|
|
606
718
|
try {
|
|
607
719
|
queryEmbedding = await generateEmbedding(query);
|
|
608
720
|
}
|
|
@@ -616,9 +728,15 @@ export async function sessionSearchMemoryHandler(args) {
|
|
|
616
728
|
isError: true,
|
|
617
729
|
};
|
|
618
730
|
}
|
|
731
|
+
// Phase 1: Capture embedding API latency
|
|
732
|
+
const embeddingMs = performance.now() - embeddingStart;
|
|
619
733
|
// Step 2: Search via storage backend
|
|
620
734
|
try {
|
|
621
735
|
const storage = await getStorage();
|
|
736
|
+
// Phase 1: Start storage latency timer — isolates DB query time.
|
|
737
|
+
// For Supabase: this measures the pgvector cosine distance RPC call.
|
|
738
|
+
// For SQLite: this measures the local sqlite-vec similarity search.
|
|
739
|
+
const storageStart = performance.now();
|
|
622
740
|
const results = await storage.searchMemory({
|
|
623
741
|
queryEmbedding: JSON.stringify(queryEmbedding),
|
|
624
742
|
project: project || null,
|
|
@@ -626,20 +744,38 @@ export async function sessionSearchMemoryHandler(args) {
|
|
|
626
744
|
similarityThreshold: similarity_threshold,
|
|
627
745
|
userId: PRISM_USER_ID,
|
|
628
746
|
});
|
|
747
|
+
// Phase 1: Capture storage query latency and compute total
|
|
748
|
+
const storageMs = performance.now() - storageStart;
|
|
749
|
+
const totalMs = performance.now() - totalStart;
|
|
629
750
|
if (results.length === 0) {
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
751
|
+
// Phase 1: Use contentBlocks array so we can optionally push trace at [1]
|
|
752
|
+
const contentBlocks = [{
|
|
753
|
+
type: "text",
|
|
754
|
+
text: `🔍 No semantically similar sessions found for: "${query}"\n` +
|
|
755
|
+
(project ? `Project: ${project}\n` : "") +
|
|
756
|
+
`Similarity threshold: ${similarity_threshold}\n\n` +
|
|
757
|
+
`Tips:\n` +
|
|
758
|
+
`• Lower the similarity_threshold (e.g., 0.5) for broader results\n` +
|
|
759
|
+
`• Try knowledge_search for keyword-based matching\n` +
|
|
760
|
+
`• Ensure sessions have been saved with embeddings (requires GOOGLE_API_KEY)`,
|
|
761
|
+
}];
|
|
762
|
+
// Phase 1: Trace is still valuable on empty results — it proves the search
|
|
763
|
+
// executed and reveals whether the bottleneck was embedding or storage.
|
|
764
|
+
if (enable_trace) {
|
|
765
|
+
const trace = createMemoryTrace({
|
|
766
|
+
strategy: "semantic",
|
|
767
|
+
query,
|
|
768
|
+
resultCount: 0,
|
|
769
|
+
topScore: null, // no results = no top score
|
|
770
|
+
threshold: similarity_threshold,
|
|
771
|
+
embeddingMs,
|
|
772
|
+
storageMs,
|
|
773
|
+
totalMs,
|
|
774
|
+
project: project || null,
|
|
775
|
+
});
|
|
776
|
+
contentBlocks.push(traceToContentBlock(trace));
|
|
777
|
+
}
|
|
778
|
+
return { content: contentBlocks, isError: false };
|
|
643
779
|
}
|
|
644
780
|
// Format results with similarity scores
|
|
645
781
|
const formatted = results.map((r, i) => {
|
|
@@ -652,13 +788,33 @@ export async function sessionSearchMemoryHandler(args) {
|
|
|
652
788
|
(r.decisions?.length ? ` Decisions: ${r.decisions.join("; ")}\n` : "") +
|
|
653
789
|
(r.files_changed?.length ? ` Files: ${r.files_changed.join(", ")}\n` : "");
|
|
654
790
|
}).join("\n");
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
791
|
+
// Phase 1: content[0] = human-readable results (unchanged from pre-Phase 1)
|
|
792
|
+
const contentBlocks = [{
|
|
793
|
+
type: "text",
|
|
794
|
+
text: `🧠 Found ${results.length} semantically similar sessions:\n\n${formatted}`,
|
|
795
|
+
}];
|
|
796
|
+
// Phase 1: content[1] = machine-readable MemoryTrace (only when enable_trace=true)
|
|
797
|
+
// topScore is read from results[0].similarity — this is the cosine distance
|
|
798
|
+
// already returned by SemanticSearchResult in the storage interface.
|
|
799
|
+
// No storage layer modifications were needed ("Score Bubbling" reviewer level-up).
|
|
800
|
+
if (enable_trace) {
|
|
801
|
+
const topScore = results.length > 0 && typeof results[0].similarity === "number"
|
|
802
|
+
? results[0].similarity
|
|
803
|
+
: null;
|
|
804
|
+
const trace = createMemoryTrace({
|
|
805
|
+
strategy: "semantic",
|
|
806
|
+
query,
|
|
807
|
+
resultCount: results.length,
|
|
808
|
+
topScore,
|
|
809
|
+
threshold: similarity_threshold,
|
|
810
|
+
embeddingMs,
|
|
811
|
+
storageMs,
|
|
812
|
+
totalMs,
|
|
813
|
+
project: project || null,
|
|
814
|
+
});
|
|
815
|
+
contentBlocks.push(traceToContentBlock(trace));
|
|
816
|
+
}
|
|
817
|
+
return { content: contentBlocks, isError: false };
|
|
662
818
|
}
|
|
663
819
|
catch (err) {
|
|
664
820
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -1194,3 +1350,90 @@ export async function sessionHealthCheckHandler(args) {
|
|
|
1194
1350
|
};
|
|
1195
1351
|
}
|
|
1196
1352
|
}
|
|
1353
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1354
|
+
// Phase 2: GDPR-Compliant Memory Deletion Handler
|
|
1355
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1356
|
+
//
|
|
1357
|
+
// This handler implements the session_forget_memory MCP tool.
|
|
1358
|
+
// It provides SURGICAL deletion of individual memory entries by ID,
|
|
1359
|
+
// supporting both soft-delete (tombstoning) and hard-delete (physical removal).
|
|
1360
|
+
//
|
|
1361
|
+
// WHY THIS IS SEPARATE FROM knowledgeForgetHandler:
|
|
1362
|
+
// knowledgeForgetHandler operates on BULK criteria (project, category, age).
|
|
1363
|
+
// sessionForgetMemoryHandler operates on a SINGLE entry by ID.
|
|
1364
|
+
// This surgical approach is required for GDPR Article 17 compliance,
|
|
1365
|
+
// where a data subject requests deletion of specific personal data.
|
|
1366
|
+
//
|
|
1367
|
+
// THE TOP-K HOLE PROBLEM (Solved):
|
|
1368
|
+
// Without deleted_at filtering inside the database queries (both SQL and RPCs),
|
|
1369
|
+
// a LIMIT 5 query might return 5 rows where 4 are soft-deleted. Post-filtering
|
|
1370
|
+
// in TypeScript would strip them, leaving only 1 result. This destroys the
|
|
1371
|
+
// agent's recall capability. By adding "AND deleted_at IS NULL" to ALL
|
|
1372
|
+
// search queries (done in sqlite.ts and Supabase RPCs), the filtering
|
|
1373
|
+
// happens BEFORE the LIMIT is applied, guaranteeing full Top-K results.
|
|
1374
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1375
|
+
export async function sessionForgetMemoryHandler(args) {
|
|
1376
|
+
try {
|
|
1377
|
+
// ─── Input Validation ───
|
|
1378
|
+
if (!isSessionForgetMemoryArgs(args)) {
|
|
1379
|
+
return {
|
|
1380
|
+
content: [{
|
|
1381
|
+
type: "text",
|
|
1382
|
+
text: "Invalid arguments. Required: memory_id (string). Optional: hard_delete (boolean), reason (string).",
|
|
1383
|
+
}],
|
|
1384
|
+
isError: true,
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
const { memory_id, hard_delete = false, reason } = args;
|
|
1388
|
+
// ─── Get Storage Backend ───
|
|
1389
|
+
const storage = await getStorage();
|
|
1390
|
+
// ─── Execute Deletion ───
|
|
1391
|
+
// The storage methods verify user_id ownership internally,
|
|
1392
|
+
// preventing cross-user deletion attacks.
|
|
1393
|
+
if (hard_delete) {
|
|
1394
|
+
// IRREVERSIBLE: Physical removal from the database.
|
|
1395
|
+
// FTS5 triggers (SQLite) or Supabase cascades clean up indexes.
|
|
1396
|
+
await storage.hardDeleteLedger(memory_id, PRISM_USER_ID);
|
|
1397
|
+
debugLog(`[session_forget_memory] Hard-deleted entry ${memory_id}`);
|
|
1398
|
+
return {
|
|
1399
|
+
content: [{
|
|
1400
|
+
type: "text",
|
|
1401
|
+
text: `🗑️ **Hard Deleted** memory entry \`${memory_id}\`.\n\n` +
|
|
1402
|
+
`This entry has been permanently removed from the database. ` +
|
|
1403
|
+
`It cannot be recovered. All associated embeddings and FTS indexes ` +
|
|
1404
|
+
`have been cleaned up.`,
|
|
1405
|
+
}],
|
|
1406
|
+
isError: false,
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
else {
|
|
1410
|
+
// REVERSIBLE: Soft-delete (tombstone) — sets deleted_at + deleted_reason.
|
|
1411
|
+
// The entry remains in the database but is excluded from ALL search
|
|
1412
|
+
// queries (vector, FTS5, and context loading).
|
|
1413
|
+
await storage.softDeleteLedger(memory_id, PRISM_USER_ID, reason);
|
|
1414
|
+
debugLog(`[session_forget_memory] Soft-deleted entry ${memory_id} (reason: ${reason || "none"})`);
|
|
1415
|
+
return {
|
|
1416
|
+
content: [{
|
|
1417
|
+
type: "text",
|
|
1418
|
+
text: `🔇 **Soft Deleted** memory entry \`${memory_id}\`.\n\n` +
|
|
1419
|
+
`The entry has been tombstoned (deleted_at = NOW()). ` +
|
|
1420
|
+
`It will no longer appear in any search results, but remains ` +
|
|
1421
|
+
`in the database for audit trail purposes.\n\n` +
|
|
1422
|
+
(reason ? `📋 **Reason**: ${reason}\n\n` : "") +
|
|
1423
|
+
`To permanently remove this entry, call again with \`hard_delete: true\`.`,
|
|
1424
|
+
}],
|
|
1425
|
+
isError: false,
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
catch (error) {
|
|
1430
|
+
console.error(`[session_forget_memory] Error: ${error}`);
|
|
1431
|
+
return {
|
|
1432
|
+
content: [{
|
|
1433
|
+
type: "text",
|
|
1434
|
+
text: `Error forgetting memory: ${error instanceof Error ? error.message : String(error)}`,
|
|
1435
|
+
}],
|
|
1436
|
+
isError: true,
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Trace — Phase 1 Explainability & Lineage
|
|
3
|
+
*
|
|
4
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
5
|
+
* PURPOSE:
|
|
6
|
+
* Provides structured tracing metadata for every search/recall
|
|
7
|
+
* operation in Prism MCP. When `enable_trace: true` is passed to
|
|
8
|
+
* `session_search_memory` or `knowledge_search`, the response
|
|
9
|
+
* includes a separate MCP content block with a MemoryTrace object.
|
|
10
|
+
*
|
|
11
|
+
* WHY THIS EXISTS:
|
|
12
|
+
* Without tracing, developers have no visibility into *why* a
|
|
13
|
+
* memory was returned — was it a semantic match? A keyword hit?
|
|
14
|
+
* How confident was the score? Was the 500ms latency caused by
|
|
15
|
+
* the embedding API or the database query?
|
|
16
|
+
*
|
|
17
|
+
* This module answers all of those questions by providing:
|
|
18
|
+
* - strategy: "semantic" | "keyword" → which search path was used
|
|
19
|
+
* - top_score: the cosine similarity / relevance score of the best result
|
|
20
|
+
* - latency: { embedding_ms, storage_ms, total_ms } → pinpoints bottlenecks
|
|
21
|
+
* - result_count, threshold, project, query, timestamp → full context
|
|
22
|
+
*
|
|
23
|
+
* DESIGN DECISIONS:
|
|
24
|
+
*
|
|
25
|
+
* 1. NO OPENTELEMETRY SDK IN PHASE 1
|
|
26
|
+
* We get the data structures right in-memory first. OTel
|
|
27
|
+
* integration (W3C traceparent headers, span export to
|
|
28
|
+
* Datadog/LangSmith) layers on top in a follow-up without
|
|
29
|
+
* any code changes to the MemoryTrace types.
|
|
30
|
+
*
|
|
31
|
+
* 2. SEPARATE MCP CONTENT BLOCK (The "Output Array Trick")
|
|
32
|
+
* Instead of concatenating trace JSON into the human-readable
|
|
33
|
+
* text response (content[0]), we return it as content[1].
|
|
34
|
+
*
|
|
35
|
+
* Why?
|
|
36
|
+
* - Prevents LLMs from accidentally blending trace JSON into
|
|
37
|
+
* their reasoning (they sometimes try to "interpret" inline JSON)
|
|
38
|
+
* - Programmatic MCP clients can grab content[1] directly
|
|
39
|
+
* without parsing/splitting string output
|
|
40
|
+
* - Clean separation of concerns: content[0] = human-readable,
|
|
41
|
+
* content[1] = machine-readable trace metadata
|
|
42
|
+
*
|
|
43
|
+
* 3. LATENCY BREAKDOWN (Not just total)
|
|
44
|
+
* A single `latency_ms` number is misleading. A 500ms total could
|
|
45
|
+
* be 480ms embedding API + 20ms DB, or 20ms embedding + 480ms DB.
|
|
46
|
+
* These are very different problems requiring different fixes.
|
|
47
|
+
*
|
|
48
|
+
* We capture three timestamps:
|
|
49
|
+
* - Before embedding API call → after = embedding_ms
|
|
50
|
+
* - Before storage.searchMemory() → after = storage_ms
|
|
51
|
+
* - Start to finish = total_ms (includes overhead, serialization, etc.)
|
|
52
|
+
*
|
|
53
|
+
* 4. SCORE BUBBLING (No storage layer changes needed)
|
|
54
|
+
* The existing SemanticSearchResult interface (interface.ts L104-112)
|
|
55
|
+
* already includes `similarity: number`. We read this directly from
|
|
56
|
+
* results[0].similarity — no modifications to the storage layer.
|
|
57
|
+
* For keyword search, top_score is null since keyword search doesn't
|
|
58
|
+
* return relevance scores in the current implementation.
|
|
59
|
+
*
|
|
60
|
+
* 5. BACKWARD COMPATIBILITY
|
|
61
|
+
* When `enable_trace` is not set (default: false), the response
|
|
62
|
+
* is identical to pre-Phase 1 output. Zero breaking changes.
|
|
63
|
+
* Existing tests pass without modification.
|
|
64
|
+
*
|
|
65
|
+
* USAGE:
|
|
66
|
+
* This module is imported by sessionMemoryHandlers.ts. It is NOT
|
|
67
|
+
* imported by the storage layer, server.ts, or any other module.
|
|
68
|
+
*
|
|
69
|
+
* FILES THAT IMPORT THIS:
|
|
70
|
+
* - src/tools/sessionMemoryHandlers.ts (search handlers)
|
|
71
|
+
*
|
|
72
|
+
* RELATED FILES:
|
|
73
|
+
* - src/tools/sessionMemoryDefinitions.ts (enable_trace param definition)
|
|
74
|
+
* - src/storage/interface.ts (SemanticSearchResult with similarity score)
|
|
75
|
+
*
|
|
76
|
+
* FUTURE EXTENSIONS (Phase 1.5+):
|
|
77
|
+
* - Add OpenTelemetry span creation using these same trace objects
|
|
78
|
+
* - Add `reranked_score` field when re-ranking is implemented
|
|
79
|
+
* - Add `graph_hops` field when graph-based recall is added
|
|
80
|
+
* - Add PII sanitization flags for GDPR-strict deployments
|
|
81
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
82
|
+
*/
|
|
83
|
+
// ─── Factory ──────────────────────────────────────────────────
|
|
84
|
+
/**
|
|
85
|
+
* Create a MemoryTrace object from search operation metrics.
|
|
86
|
+
*
|
|
87
|
+
* This is a pure factory function — no side effects, no I/O.
|
|
88
|
+
* Called by the search handlers after both the embedding API call
|
|
89
|
+
* and storage query have completed.
|
|
90
|
+
*
|
|
91
|
+
* Latency values are rounded to nearest integer for cleaner output
|
|
92
|
+
* (sub-millisecond precision is noise, not signal).
|
|
93
|
+
*
|
|
94
|
+
* @param params.strategy - "semantic" or "keyword"
|
|
95
|
+
* @param params.query - Original search query string
|
|
96
|
+
* @param params.resultCount - Number of results returned
|
|
97
|
+
* @param params.topScore - Best similarity score, or null for keyword
|
|
98
|
+
* @param params.threshold - Threshold used, or null for keyword
|
|
99
|
+
* @param params.embeddingMs - Time for embedding API call (0 for keyword)
|
|
100
|
+
* @param params.storageMs - Time for database query
|
|
101
|
+
* @param params.totalMs - Total end-to-end time
|
|
102
|
+
* @param params.project - Project filter, or null for all
|
|
103
|
+
* @returns A complete MemoryTrace object ready for serialization
|
|
104
|
+
*/
|
|
105
|
+
export function createMemoryTrace(params) {
|
|
106
|
+
return {
|
|
107
|
+
strategy: params.strategy,
|
|
108
|
+
query: params.query,
|
|
109
|
+
result_count: params.resultCount,
|
|
110
|
+
top_score: params.topScore,
|
|
111
|
+
threshold: params.threshold,
|
|
112
|
+
latency: {
|
|
113
|
+
embedding_ms: Math.round(params.embeddingMs),
|
|
114
|
+
storage_ms: Math.round(params.storageMs),
|
|
115
|
+
total_ms: Math.round(params.totalMs),
|
|
116
|
+
},
|
|
117
|
+
timestamp: new Date().toISOString(),
|
|
118
|
+
project: params.project,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Format a MemoryTrace into an MCP content block.
|
|
123
|
+
*
|
|
124
|
+
* Returns a single content block to push into the content[] array
|
|
125
|
+
* at index [1]. The "=== MEMORY TRACE ===" header makes it visually
|
|
126
|
+
* distinct from the human-readable search results at content[0].
|
|
127
|
+
*
|
|
128
|
+
* The trace is pretty-printed (2-space indent) for readability in
|
|
129
|
+
* console output and MCP inspector tools.
|
|
130
|
+
*
|
|
131
|
+
* @param trace - A MemoryTrace object from createMemoryTrace()
|
|
132
|
+
* @returns An MCP content block: { type: "text", text: "..." }
|
|
133
|
+
*/
|
|
134
|
+
export function traceToContentBlock(trace) {
|
|
135
|
+
return {
|
|
136
|
+
type: "text",
|
|
137
|
+
text: `=== MEMORY TRACE ===\n${JSON.stringify(trace, null, 2)}`,
|
|
138
|
+
};
|
|
139
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prism-mcp-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"mcpName": "io.github.dcostenco/prism-mcp",
|
|
5
5
|
"description": "The Mind Palace for AI Agents — local-first MCP server with persistent memory (SQLite/Supabase), visual dashboard, time travel, multi-agent sync, Morning Briefings, reality drift detection, code mode templates, semantic vector search, and Brave Search + Gemini analysis. Zero-config local mode.",
|
|
6
6
|
"module": "index.ts",
|
|
@@ -80,6 +80,7 @@
|
|
|
80
80
|
"@google/generative-ai": "^0.24.1",
|
|
81
81
|
"@libsql/client": "^0.17.2",
|
|
82
82
|
"@modelcontextprotocol/sdk": "^1.9.0",
|
|
83
|
+
"@supabase/supabase-js": "^2.99.3",
|
|
83
84
|
"dotenv": "^16.5.0",
|
|
84
85
|
"quickjs-emscripten": "^0.32.0"
|
|
85
86
|
}
|