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 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/branding/icon-512.png" alt="SuperLocalMemory V2" width="200"/>
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 _register_mcp_agent(agent_name: str = "mcp-client"):
254
- """Register the calling MCP agent and record activity. Non-blocking."""
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
- This calls the SAME backend as /superlocalmemoryv2:recall skill.
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=False,
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
- Signal that a recalled memory was useful. Call this when you reference
720
- or apply a memory from recall results in your response.
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
- This helps SuperLocalMemory learn which memories are most relevant
723
- and improves future recall results.
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.0", file=sys.stderr)
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",
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
  },
@@ -142,6 +142,7 @@ Finds: Exact mentions of "PostgreSQL 15"
142
142
  - Graph distance
143
143
  - Recency (newer = slight boost)
144
144
  - Importance level
145
+ - Your usage patterns (results improve automatically over time)
145
146
 
146
147
  ## Advanced Usage
147
148