superlocalmemory 2.7.3 → 2.7.5
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/CHANGELOG.md +17 -0
- package/README.md +1 -1
- package/hooks/post-recall-hook.js +53 -0
- package/mcp_server.py +348 -17
- package/package.json +2 -1
- package/skills/slm-recall/SKILL.md +1 -0
- package/src/auto_backup.py +64 -31
- package/src/learning/adaptive_ranker.py +70 -1
- package/src/learning/feature_extractor.py +71 -17
- package/src/learning/feedback_collector.py +114 -0
- package/src/learning/learning_db.py +158 -34
- package/src/learning/tests/test_adaptive_ranker.py +5 -4
- package/src/learning/tests/test_aggregator.py +4 -3
- package/src/learning/tests/test_feedback_collector.py +7 -4
- package/src/learning/tests/test_signal_inference.py +399 -0
- package/src/learning/tests/test_synthetic_bootstrap.py +1 -1
- package/ui/index.html +38 -0
- package/ui/js/feedback.js +333 -0
- package/ui/js/learning.js +117 -0
- package/ui/js/modal.js +22 -1
- package/ui/js/profiles.js +8 -0
- package/ui/js/settings.js +58 -1
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,23 @@ SuperLocalMemory V2 - Intelligent local memory system for AI coding assistants.
|
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
+
## [2.7.4] - 2026-02-16
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- Per-profile learning — each profile has its own preferences and feedback
|
|
23
|
+
- Thumbs up/down and pin feedback on memory cards
|
|
24
|
+
- Learning data management in Settings (backup + reset)
|
|
25
|
+
- "What We Learned" summary card in Learning tab
|
|
26
|
+
|
|
27
|
+
### Improved
|
|
28
|
+
- Smarter learning from your natural usage patterns
|
|
29
|
+
- Recall results improve automatically over time
|
|
30
|
+
- Privacy notice for all learning features
|
|
31
|
+
- Learning and backup databases protected together
|
|
32
|
+
- All dashboard tabs refresh on profile switch
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
19
36
|
## [2.7.3] - 2026-02-16
|
|
20
37
|
|
|
21
38
|
### Improved
|
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="https://superlocalmemory.com/assets/
|
|
2
|
+
<img src="https://superlocalmemory.com/assets/logo-mark.png" alt="SuperLocalMemory V2" width="200"/>
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<h1 align="center">SuperLocalMemory V2</h1>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SuperLocalMemory V2 - Post-Recall Hook (v2.7.4)
|
|
4
|
+
* Copyright (c) 2026 Varun Pratap Bhardwaj
|
|
5
|
+
* Licensed under MIT License
|
|
6
|
+
*
|
|
7
|
+
* Claude Code hook that tracks recall events for implicit signal collection.
|
|
8
|
+
* This hook fires after the slm-recall skill completes, recording timing
|
|
9
|
+
* data that the signal inference engine uses to detect satisfaction/dissatisfaction.
|
|
10
|
+
*
|
|
11
|
+
* Installation: Automatically registered via install-skills.sh
|
|
12
|
+
* All data stays 100% local.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const os = require('os');
|
|
18
|
+
|
|
19
|
+
const MEMORY_DIR = path.join(os.homedir(), '.claude-memory');
|
|
20
|
+
const HOOK_LOG = path.join(MEMORY_DIR, 'recall-events.jsonl');
|
|
21
|
+
|
|
22
|
+
// Parse input from Claude Code hook system
|
|
23
|
+
function main() {
|
|
24
|
+
try {
|
|
25
|
+
const timestamp = Date.now();
|
|
26
|
+
const args = process.argv.slice(2);
|
|
27
|
+
|
|
28
|
+
// Extract query from args if available
|
|
29
|
+
let query = '';
|
|
30
|
+
for (let i = 0; i < args.length; i++) {
|
|
31
|
+
if (args[i] && !args[i].startsWith('--')) {
|
|
32
|
+
query = args[i];
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!query) return;
|
|
38
|
+
|
|
39
|
+
// Append recall event to JSONL log (lightweight, append-only)
|
|
40
|
+
const event = JSON.stringify({
|
|
41
|
+
type: 'recall',
|
|
42
|
+
query: query.substring(0, 100), // Truncate for privacy
|
|
43
|
+
timestamp: timestamp,
|
|
44
|
+
source: 'claude-code-hook',
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
fs.appendFileSync(HOOK_LOG, event + '\n', { flag: 'a' });
|
|
48
|
+
} catch (e) {
|
|
49
|
+
// Hook failures must be silent
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
main();
|
package/mcp_server.py
CHANGED
|
@@ -25,14 +25,16 @@ Usage:
|
|
|
25
25
|
python3 mcp_server.py --transport http --port 8001
|
|
26
26
|
"""
|
|
27
27
|
|
|
28
|
-
from mcp.server.fastmcp import FastMCP
|
|
28
|
+
from mcp.server.fastmcp import FastMCP, Context
|
|
29
29
|
from mcp.types import ToolAnnotations
|
|
30
30
|
import sys
|
|
31
31
|
import os
|
|
32
32
|
import json
|
|
33
33
|
import re
|
|
34
|
+
import time
|
|
35
|
+
import threading
|
|
34
36
|
from pathlib import Path
|
|
35
|
-
from typing import Optional
|
|
37
|
+
from typing import Optional, Dict, List, Any
|
|
36
38
|
|
|
37
39
|
# Add src directory to path (use existing code!)
|
|
38
40
|
MEMORY_DIR = Path.home() / ".claude-memory"
|
|
@@ -250,8 +252,48 @@ def get_learning_components():
|
|
|
250
252
|
}
|
|
251
253
|
|
|
252
254
|
|
|
253
|
-
def
|
|
254
|
-
"""
|
|
255
|
+
def _get_client_name(ctx: Optional[Context] = None) -> str:
|
|
256
|
+
"""Extract client name from MCP context, or return default.
|
|
257
|
+
|
|
258
|
+
Reads clientInfo.name from the MCP initialize handshake via
|
|
259
|
+
ctx.session.client_params. This identifies Perplexity, Codex,
|
|
260
|
+
Claude Desktop, etc. as distinct agents.
|
|
261
|
+
"""
|
|
262
|
+
if ctx:
|
|
263
|
+
try:
|
|
264
|
+
# Primary: session.client_params.clientInfo.name (from initialize handshake)
|
|
265
|
+
session = getattr(ctx, 'session', None)
|
|
266
|
+
if session:
|
|
267
|
+
params = getattr(session, 'client_params', None)
|
|
268
|
+
if params:
|
|
269
|
+
client_info = getattr(params, 'clientInfo', None)
|
|
270
|
+
if client_info:
|
|
271
|
+
name = getattr(client_info, 'name', None)
|
|
272
|
+
if name:
|
|
273
|
+
return str(name)
|
|
274
|
+
except Exception:
|
|
275
|
+
pass
|
|
276
|
+
try:
|
|
277
|
+
# Fallback: ctx.client_id (per-request, may be null)
|
|
278
|
+
client_id = ctx.client_id
|
|
279
|
+
if client_id:
|
|
280
|
+
return str(client_id)
|
|
281
|
+
except Exception:
|
|
282
|
+
pass
|
|
283
|
+
return "mcp-client"
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _register_mcp_agent(agent_name: str = "mcp-client", ctx: Optional[Context] = None):
|
|
287
|
+
"""Register the calling MCP agent and record activity. Non-blocking.
|
|
288
|
+
|
|
289
|
+
v2.7.4: Extracts real client name from MCP context when available,
|
|
290
|
+
so Perplexity, Codex, Claude Desktop show as distinct agents.
|
|
291
|
+
"""
|
|
292
|
+
if ctx:
|
|
293
|
+
detected = _get_client_name(ctx)
|
|
294
|
+
if detected != "mcp-client":
|
|
295
|
+
agent_name = detected
|
|
296
|
+
|
|
255
297
|
registry = get_agent_registry()
|
|
256
298
|
if registry:
|
|
257
299
|
try:
|
|
@@ -264,6 +306,264 @@ def _register_mcp_agent(agent_name: str = "mcp-client"):
|
|
|
264
306
|
pass
|
|
265
307
|
|
|
266
308
|
|
|
309
|
+
# ============================================================================
|
|
310
|
+
# RECALL BUFFER & SIGNAL INFERENCE ENGINE (v2.7.4 — Silent Learning)
|
|
311
|
+
# ============================================================================
|
|
312
|
+
# Tracks recall operations and infers implicit feedback signals from user
|
|
313
|
+
# behavior patterns. Zero user effort — all signals auto-collected.
|
|
314
|
+
#
|
|
315
|
+
# Signal Types:
|
|
316
|
+
# implicit_positive_timegap — long pause (>5min) after recall = satisfied
|
|
317
|
+
# implicit_negative_requick — quick re-query (<30s) = dissatisfied
|
|
318
|
+
# implicit_positive_reaccess — same memory in consecutive recalls
|
|
319
|
+
# implicit_positive_cross_tool — same memory recalled by different agents
|
|
320
|
+
# implicit_positive_post_update — memory updated after being recalled
|
|
321
|
+
# implicit_negative_post_delete — memory deleted after being recalled
|
|
322
|
+
#
|
|
323
|
+
# Research: Hu et al. 2008 (implicit feedback), BPR Rendle 2009 (pairwise)
|
|
324
|
+
# ============================================================================
|
|
325
|
+
|
|
326
|
+
class _RecallBuffer:
|
|
327
|
+
"""Thread-safe buffer tracking recent recall operations for signal inference.
|
|
328
|
+
|
|
329
|
+
Stores the last recall per agent_id so we can compare consecutive recalls
|
|
330
|
+
and infer whether the user found results useful.
|
|
331
|
+
|
|
332
|
+
Rate limiting: max 5 implicit signals per agent per minute to prevent gaming.
|
|
333
|
+
"""
|
|
334
|
+
|
|
335
|
+
def __init__(self):
|
|
336
|
+
self._lock = threading.Lock()
|
|
337
|
+
# {agent_id: {query, result_ids, timestamp, result_id_set}}
|
|
338
|
+
self._last_recall: Dict[str, Dict[str, Any]] = {}
|
|
339
|
+
# Global last recall (for cross-agent comparison)
|
|
340
|
+
self._global_last: Optional[Dict[str, Any]] = None
|
|
341
|
+
# Rate limiter: {agent_id: [timestamp, timestamp, ...]}
|
|
342
|
+
self._signal_timestamps: Dict[str, List[float]] = {}
|
|
343
|
+
# Set of memory_ids from the most recent recall (for post-action tracking)
|
|
344
|
+
self._recent_result_ids: set = set()
|
|
345
|
+
# Recall counter for passive decay auto-trigger
|
|
346
|
+
self._recall_count: int = 0
|
|
347
|
+
# Adaptive threshold: starts at 300s (5min), adjusts based on user patterns
|
|
348
|
+
self._positive_threshold: float = 300.0
|
|
349
|
+
self._inter_recall_times: List[float] = []
|
|
350
|
+
|
|
351
|
+
def record_recall(
|
|
352
|
+
self,
|
|
353
|
+
query: str,
|
|
354
|
+
result_ids: List[int],
|
|
355
|
+
agent_id: str = "mcp-client",
|
|
356
|
+
) -> List[Dict[str, Any]]:
|
|
357
|
+
"""Record a recall and infer signals from previous recall comparison.
|
|
358
|
+
|
|
359
|
+
Returns a list of inferred signal dicts: [{memory_id, signal_type, query}]
|
|
360
|
+
"""
|
|
361
|
+
now = time.time()
|
|
362
|
+
signals: List[Dict[str, Any]] = []
|
|
363
|
+
|
|
364
|
+
with self._lock:
|
|
365
|
+
self._recall_count += 1
|
|
366
|
+
result_id_set = set(result_ids)
|
|
367
|
+
self._recent_result_ids = result_id_set
|
|
368
|
+
|
|
369
|
+
current = {
|
|
370
|
+
"query": query,
|
|
371
|
+
"result_ids": result_ids,
|
|
372
|
+
"result_id_set": result_id_set,
|
|
373
|
+
"timestamp": now,
|
|
374
|
+
"agent_id": agent_id,
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
# --- Compare with previous recall from SAME agent ---
|
|
378
|
+
prev = self._last_recall.get(agent_id)
|
|
379
|
+
if prev:
|
|
380
|
+
time_gap = now - prev["timestamp"]
|
|
381
|
+
|
|
382
|
+
# Track inter-recall times for adaptive threshold
|
|
383
|
+
self._inter_recall_times.append(time_gap)
|
|
384
|
+
if len(self._inter_recall_times) > 100:
|
|
385
|
+
self._inter_recall_times = self._inter_recall_times[-100:]
|
|
386
|
+
|
|
387
|
+
# Update adaptive threshold (median of recent times, min 60s, max 1800s)
|
|
388
|
+
if len(self._inter_recall_times) >= 10:
|
|
389
|
+
sorted_times = sorted(self._inter_recall_times)
|
|
390
|
+
median = sorted_times[len(sorted_times) // 2]
|
|
391
|
+
self._positive_threshold = max(60.0, min(median * 0.8, 1800.0))
|
|
392
|
+
|
|
393
|
+
# Signal: Quick re-query with different query = negative
|
|
394
|
+
if time_gap < 30.0 and query != prev["query"]:
|
|
395
|
+
for mid in prev["result_ids"][:5]: # Top 5 only
|
|
396
|
+
signals.append({
|
|
397
|
+
"memory_id": mid,
|
|
398
|
+
"signal_type": "implicit_negative_requick",
|
|
399
|
+
"query": prev["query"],
|
|
400
|
+
"rank_position": prev["result_ids"].index(mid) + 1,
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
# Signal: Long pause = positive for previous results
|
|
404
|
+
elif time_gap > self._positive_threshold:
|
|
405
|
+
for mid in prev["result_ids"][:3]: # Top 3 only
|
|
406
|
+
signals.append({
|
|
407
|
+
"memory_id": mid,
|
|
408
|
+
"signal_type": "implicit_positive_timegap",
|
|
409
|
+
"query": prev["query"],
|
|
410
|
+
"rank_position": prev["result_ids"].index(mid) + 1,
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
# Signal: Same memory re-accessed = positive
|
|
414
|
+
overlap = result_id_set & prev["result_id_set"]
|
|
415
|
+
for mid in overlap:
|
|
416
|
+
signals.append({
|
|
417
|
+
"memory_id": mid,
|
|
418
|
+
"signal_type": "implicit_positive_reaccess",
|
|
419
|
+
"query": query,
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
# --- Compare with previous recall from DIFFERENT agent (cross-tool) ---
|
|
423
|
+
global_prev = self._global_last
|
|
424
|
+
if global_prev and global_prev["agent_id"] != agent_id:
|
|
425
|
+
cross_overlap = result_id_set & global_prev["result_id_set"]
|
|
426
|
+
for mid in cross_overlap:
|
|
427
|
+
signals.append({
|
|
428
|
+
"memory_id": mid,
|
|
429
|
+
"signal_type": "implicit_positive_cross_tool",
|
|
430
|
+
"query": query,
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
# Update buffers
|
|
434
|
+
self._last_recall[agent_id] = current
|
|
435
|
+
self._global_last = current
|
|
436
|
+
|
|
437
|
+
return signals
|
|
438
|
+
|
|
439
|
+
def check_post_action(self, memory_id: int, action: str) -> Optional[Dict[str, Any]]:
|
|
440
|
+
"""Check if a memory action (update/delete) follows a recent recall.
|
|
441
|
+
|
|
442
|
+
Returns signal dict if the memory was in recent results, else None.
|
|
443
|
+
"""
|
|
444
|
+
with self._lock:
|
|
445
|
+
if memory_id not in self._recent_result_ids:
|
|
446
|
+
return None
|
|
447
|
+
|
|
448
|
+
if action == "update":
|
|
449
|
+
return {
|
|
450
|
+
"memory_id": memory_id,
|
|
451
|
+
"signal_type": "implicit_positive_post_update",
|
|
452
|
+
"query": self._global_last["query"] if self._global_last else "",
|
|
453
|
+
}
|
|
454
|
+
elif action == "delete":
|
|
455
|
+
return {
|
|
456
|
+
"memory_id": memory_id,
|
|
457
|
+
"signal_type": "implicit_negative_post_delete",
|
|
458
|
+
"query": self._global_last["query"] if self._global_last else "",
|
|
459
|
+
}
|
|
460
|
+
return None
|
|
461
|
+
|
|
462
|
+
def check_rate_limit(self, agent_id: str, max_per_minute: int = 5) -> bool:
|
|
463
|
+
"""Return True if agent is within rate limit, False if exceeded."""
|
|
464
|
+
now = time.time()
|
|
465
|
+
with self._lock:
|
|
466
|
+
if agent_id not in self._signal_timestamps:
|
|
467
|
+
self._signal_timestamps[agent_id] = []
|
|
468
|
+
|
|
469
|
+
# Clean old timestamps (older than 60s)
|
|
470
|
+
self._signal_timestamps[agent_id] = [
|
|
471
|
+
ts for ts in self._signal_timestamps[agent_id]
|
|
472
|
+
if now - ts < 60.0
|
|
473
|
+
]
|
|
474
|
+
|
|
475
|
+
if len(self._signal_timestamps[agent_id]) >= max_per_minute:
|
|
476
|
+
return False
|
|
477
|
+
|
|
478
|
+
self._signal_timestamps[agent_id].append(now)
|
|
479
|
+
return True
|
|
480
|
+
|
|
481
|
+
def get_recall_count(self) -> int:
|
|
482
|
+
"""Get total recall count (for passive decay trigger)."""
|
|
483
|
+
with self._lock:
|
|
484
|
+
return self._recall_count
|
|
485
|
+
|
|
486
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
487
|
+
"""Get buffer statistics for diagnostics."""
|
|
488
|
+
with self._lock:
|
|
489
|
+
return {
|
|
490
|
+
"recall_count": self._recall_count,
|
|
491
|
+
"tracked_agents": len(self._last_recall),
|
|
492
|
+
"positive_threshold_s": round(self._positive_threshold, 1),
|
|
493
|
+
"recent_results_count": len(self._recent_result_ids),
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
# Module-level singleton
|
|
498
|
+
_recall_buffer = _RecallBuffer()
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def _emit_implicit_signals(signals: List[Dict[str, Any]], agent_id: str = "mcp-client") -> int:
|
|
502
|
+
"""Emit inferred implicit signals to the feedback collector.
|
|
503
|
+
|
|
504
|
+
Rate-limited: max 5 signals per agent per minute.
|
|
505
|
+
All errors swallowed — signal collection must NEVER break operations.
|
|
506
|
+
|
|
507
|
+
Returns number of signals actually stored.
|
|
508
|
+
"""
|
|
509
|
+
if not LEARNING_AVAILABLE or not signals:
|
|
510
|
+
return 0
|
|
511
|
+
|
|
512
|
+
stored = 0
|
|
513
|
+
try:
|
|
514
|
+
feedback = get_feedback_collector()
|
|
515
|
+
if not feedback:
|
|
516
|
+
return 0
|
|
517
|
+
|
|
518
|
+
for sig in signals:
|
|
519
|
+
if not _recall_buffer.check_rate_limit(agent_id):
|
|
520
|
+
break # Rate limit exceeded for this agent
|
|
521
|
+
try:
|
|
522
|
+
feedback.record_implicit_signal(
|
|
523
|
+
memory_id=sig["memory_id"],
|
|
524
|
+
query=sig.get("query", ""),
|
|
525
|
+
signal_type=sig["signal_type"],
|
|
526
|
+
source_tool=agent_id,
|
|
527
|
+
rank_position=sig.get("rank_position"),
|
|
528
|
+
)
|
|
529
|
+
stored += 1
|
|
530
|
+
except Exception:
|
|
531
|
+
pass # Individual signal failure is fine
|
|
532
|
+
except Exception:
|
|
533
|
+
pass # Never break the caller
|
|
534
|
+
|
|
535
|
+
return stored
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _maybe_passive_decay() -> None:
|
|
539
|
+
"""Auto-trigger passive decay every 10 recalls in a background thread."""
|
|
540
|
+
try:
|
|
541
|
+
if not LEARNING_AVAILABLE:
|
|
542
|
+
return
|
|
543
|
+
if _recall_buffer.get_recall_count() % 10 != 0:
|
|
544
|
+
return
|
|
545
|
+
|
|
546
|
+
feedback = get_feedback_collector()
|
|
547
|
+
if not feedback:
|
|
548
|
+
return
|
|
549
|
+
|
|
550
|
+
def _run_decay():
|
|
551
|
+
try:
|
|
552
|
+
count = feedback.compute_passive_decay(threshold=5)
|
|
553
|
+
if count > 0:
|
|
554
|
+
import logging
|
|
555
|
+
logging.getLogger("superlocalmemory.mcp").info(
|
|
556
|
+
"Passive decay: %d signals emitted", count
|
|
557
|
+
)
|
|
558
|
+
except Exception:
|
|
559
|
+
pass
|
|
560
|
+
|
|
561
|
+
thread = threading.Thread(target=_run_decay, daemon=True)
|
|
562
|
+
thread.start()
|
|
563
|
+
except Exception:
|
|
564
|
+
pass
|
|
565
|
+
|
|
566
|
+
|
|
267
567
|
# ============================================================================
|
|
268
568
|
# MCP TOOLS (Functions callable by AI)
|
|
269
569
|
# ============================================================================
|
|
@@ -277,7 +577,8 @@ async def remember(
|
|
|
277
577
|
content: str,
|
|
278
578
|
tags: str = "",
|
|
279
579
|
project: str = "",
|
|
280
|
-
importance: int = 5
|
|
580
|
+
importance: int = 5,
|
|
581
|
+
ctx: Context = None,
|
|
281
582
|
) -> dict:
|
|
282
583
|
"""
|
|
283
584
|
Save content to SuperLocalMemory with intelligent indexing.
|
|
@@ -304,8 +605,8 @@ async def remember(
|
|
|
304
605
|
remember("JWT auth with refresh tokens", tags="security,auth", importance=8)
|
|
305
606
|
"""
|
|
306
607
|
try:
|
|
307
|
-
# Register MCP agent (v2.5 — agent tracking)
|
|
308
|
-
_register_mcp_agent()
|
|
608
|
+
# Register MCP agent (v2.5 — agent tracking, v2.7.4 — client detection)
|
|
609
|
+
_register_mcp_agent(ctx=ctx)
|
|
309
610
|
|
|
310
611
|
# Trust enforcement (v2.6) — block untrusted agents from writing
|
|
311
612
|
try:
|
|
@@ -372,12 +673,16 @@ async def remember(
|
|
|
372
673
|
async def recall(
|
|
373
674
|
query: str,
|
|
374
675
|
limit: int = 10,
|
|
375
|
-
min_score: float = 0.3
|
|
676
|
+
min_score: float = 0.3,
|
|
677
|
+
ctx: Context = None,
|
|
376
678
|
) -> dict:
|
|
377
679
|
"""
|
|
378
680
|
Search memories using semantic similarity and knowledge graph.
|
|
681
|
+
Results are personalized based on your usage patterns — the more you
|
|
682
|
+
use SuperLocalMemory, the better results get. All learning is local.
|
|
379
683
|
|
|
380
|
-
|
|
684
|
+
After using results, call memory_used(memory_id) for memories you
|
|
685
|
+
referenced to help improve future recall quality.
|
|
381
686
|
|
|
382
687
|
Args:
|
|
383
688
|
query: Search query (required)
|
|
@@ -405,6 +710,18 @@ async def recall(
|
|
|
405
710
|
recall("FastAPI", limit=5, min_score=0.5)
|
|
406
711
|
"""
|
|
407
712
|
try:
|
|
713
|
+
# Register MCP agent (v2.7.4 — client detection for agent tab)
|
|
714
|
+
_register_mcp_agent(ctx=ctx)
|
|
715
|
+
|
|
716
|
+
# Track recall in agent registry
|
|
717
|
+
registry = get_agent_registry()
|
|
718
|
+
if registry:
|
|
719
|
+
try:
|
|
720
|
+
agent_name = _get_client_name(ctx)
|
|
721
|
+
registry.record_recall(f"mcp:{agent_name}")
|
|
722
|
+
except Exception:
|
|
723
|
+
pass
|
|
724
|
+
|
|
408
725
|
# Use existing MemoryStoreV2 class
|
|
409
726
|
store = get_store()
|
|
410
727
|
|
|
@@ -445,6 +762,17 @@ async def recall(
|
|
|
445
762
|
except Exception:
|
|
446
763
|
pass # Tracking failure must never break recall
|
|
447
764
|
|
|
765
|
+
# v2.7.4: Implicit signal inference from recall patterns
|
|
766
|
+
try:
|
|
767
|
+
result_ids = [r.get('id') for r in results if r.get('id')]
|
|
768
|
+
signals = _recall_buffer.record_recall(query, result_ids)
|
|
769
|
+
if signals:
|
|
770
|
+
_emit_implicit_signals(signals)
|
|
771
|
+
# Auto-trigger passive decay every 10 recalls
|
|
772
|
+
_maybe_passive_decay()
|
|
773
|
+
except Exception:
|
|
774
|
+
pass # Signal inference must NEVER break recall
|
|
775
|
+
|
|
448
776
|
# Filter by minimum score
|
|
449
777
|
filtered_results = [
|
|
450
778
|
r for r in results
|
|
@@ -708,7 +1036,7 @@ async def backup_status() -> dict:
|
|
|
708
1036
|
@mcp.tool(annotations=ToolAnnotations(
|
|
709
1037
|
readOnlyHint=False,
|
|
710
1038
|
destructiveHint=False,
|
|
711
|
-
openWorldHint=
|
|
1039
|
+
openWorldHint=True,
|
|
712
1040
|
))
|
|
713
1041
|
async def memory_used(
|
|
714
1042
|
memory_id: int,
|
|
@@ -716,15 +1044,18 @@ async def memory_used(
|
|
|
716
1044
|
usefulness: str = "high"
|
|
717
1045
|
) -> dict:
|
|
718
1046
|
"""
|
|
719
|
-
|
|
720
|
-
|
|
1047
|
+
Call this tool whenever you use information from a recalled memory in
|
|
1048
|
+
your response. This is the most important feedback signal — it teaches
|
|
1049
|
+
SuperLocalMemory which memories are truly useful and dramatically
|
|
1050
|
+
improves future recall quality. All data stays 100% local.
|
|
721
1051
|
|
|
722
|
-
|
|
723
|
-
|
|
1052
|
+
Best practice: After using recall() results, call memory_used() for
|
|
1053
|
+
each memory ID you referenced. This takes <1ms and helps the system
|
|
1054
|
+
learn your preferences.
|
|
724
1055
|
|
|
725
1056
|
Args:
|
|
726
|
-
memory_id: ID of the useful memory
|
|
727
|
-
query: The recall query that found it (optional)
|
|
1057
|
+
memory_id: ID of the useful memory (from recall results)
|
|
1058
|
+
query: The recall query that found it (optional but recommended)
|
|
728
1059
|
usefulness: How useful - "high", "medium", or "low" (default "high")
|
|
729
1060
|
|
|
730
1061
|
Returns:
|
|
@@ -1227,7 +1558,7 @@ if __name__ == "__main__":
|
|
|
1227
1558
|
# Print startup message to stderr (stdout is used for MCP protocol)
|
|
1228
1559
|
print("=" * 60, file=sys.stderr)
|
|
1229
1560
|
print("SuperLocalMemory V2 - MCP Server", file=sys.stderr)
|
|
1230
|
-
print("Version: 2.7.
|
|
1561
|
+
print("Version: 2.7.4", file=sys.stderr)
|
|
1231
1562
|
print("=" * 60, file=sys.stderr)
|
|
1232
1563
|
print("Created by: Varun Pratap Bhardwaj (Solution Architect)", file=sys.stderr)
|
|
1233
1564
|
print("Repository: https://github.com/varun369/SuperLocalMemoryV2", file=sys.stderr)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "superlocalmemory",
|
|
3
|
-
"version": "2.7.
|
|
3
|
+
"version": "2.7.5",
|
|
4
4
|
"description": "Your AI Finally Remembers You - Local-first intelligent memory system for AI assistants. Works with Claude, Cursor, Windsurf, VS Code/Copilot, Codex, and 17+ AI tools. 100% local, zero cloud dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-memory",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"url": "https://github.com/varun369/SuperLocalMemoryV2.git"
|
|
36
36
|
},
|
|
37
37
|
"homepage": "https://superlocalmemory.com",
|
|
38
|
+
"mcpName": "io.github.varun369/superlocalmemory",
|
|
38
39
|
"bugs": {
|
|
39
40
|
"url": "https://github.com/varun369/SuperLocalMemoryV2/issues"
|
|
40
41
|
},
|