superlocalmemory 3.3.20 → 3.3.21

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.
Files changed (78) hide show
  1. package/package.json +1 -1
  2. package/pyproject.toml +9 -1
  3. package/src/superlocalmemory/cli/commands.py +138 -22
  4. package/src/superlocalmemory/cli/daemon.py +372 -0
  5. package/src/superlocalmemory/cli/main.py +8 -0
  6. package/src/superlocalmemory/cli/pending_store.py +158 -0
  7. package/src/superlocalmemory/cli/setup_wizard.py +39 -6
  8. package/src/superlocalmemory/code_graph/__init__.py +46 -0
  9. package/src/superlocalmemory/code_graph/blast_radius.py +177 -0
  10. package/src/superlocalmemory/code_graph/bridge/__init__.py +36 -0
  11. package/src/superlocalmemory/code_graph/bridge/entity_resolver.py +464 -0
  12. package/src/superlocalmemory/code_graph/bridge/event_listeners.py +195 -0
  13. package/src/superlocalmemory/code_graph/bridge/fact_enricher.py +159 -0
  14. package/src/superlocalmemory/code_graph/bridge/hebbian_linker.py +170 -0
  15. package/src/superlocalmemory/code_graph/bridge/temporal_checker.py +152 -0
  16. package/src/superlocalmemory/code_graph/changes.py +363 -0
  17. package/src/superlocalmemory/code_graph/communities.py +299 -0
  18. package/src/superlocalmemory/code_graph/config.py +88 -0
  19. package/src/superlocalmemory/code_graph/database.py +482 -0
  20. package/src/superlocalmemory/code_graph/extractors/__init__.py +78 -0
  21. package/src/superlocalmemory/code_graph/extractors/python.py +413 -0
  22. package/src/superlocalmemory/code_graph/extractors/typescript.py +556 -0
  23. package/src/superlocalmemory/code_graph/flows.py +350 -0
  24. package/src/superlocalmemory/code_graph/git_hooks.py +226 -0
  25. package/src/superlocalmemory/code_graph/graph_engine.py +295 -0
  26. package/src/superlocalmemory/code_graph/graph_store.py +158 -0
  27. package/src/superlocalmemory/code_graph/incremental.py +200 -0
  28. package/src/superlocalmemory/code_graph/models.py +130 -0
  29. package/src/superlocalmemory/code_graph/parser.py +507 -0
  30. package/src/superlocalmemory/code_graph/resolver.py +321 -0
  31. package/src/superlocalmemory/code_graph/search.py +460 -0
  32. package/src/superlocalmemory/code_graph/service.py +95 -0
  33. package/src/superlocalmemory/code_graph/watcher.py +207 -0
  34. package/src/superlocalmemory/core/embedding_worker.py +4 -2
  35. package/src/superlocalmemory/core/embeddings.py +8 -2
  36. package/src/superlocalmemory/core/engine.py +32 -0
  37. package/src/superlocalmemory/core/engine_wiring.py +5 -0
  38. package/src/superlocalmemory/core/store_pipeline.py +23 -1
  39. package/src/superlocalmemory/encoding/fact_extractor.py +68 -7
  40. package/src/superlocalmemory/infra/event_bus.py +5 -0
  41. package/src/superlocalmemory/mcp/server.py +23 -0
  42. package/src/superlocalmemory/mcp/tools_code_graph.py +1592 -0
  43. package/src/superlocalmemory/retrieval/engine.py +137 -2
  44. package/src/superlocalmemory/retrieval/semantic_channel.py +6 -2
  45. package/src/superlocalmemory/retrieval/spreading_activation.py +5 -3
  46. package/src/superlocalmemory/retrieval/strategy.py +16 -0
  47. package/src/superlocalmemory/server/api.py +4 -2
  48. package/src/superlocalmemory/server/ui.py +5 -2
  49. package/src/superlocalmemory/storage/schema_code_graph.py +239 -0
  50. package/src/superlocalmemory/ui/index.html +1879 -0
  51. package/src/superlocalmemory/ui/js/agents.js +192 -0
  52. package/src/superlocalmemory/ui/js/auto-settings.js +399 -0
  53. package/src/superlocalmemory/ui/js/behavioral.js +276 -0
  54. package/src/superlocalmemory/ui/js/clusters.js +206 -0
  55. package/src/superlocalmemory/ui/js/compliance.js +252 -0
  56. package/src/superlocalmemory/ui/js/core.js +246 -0
  57. package/src/superlocalmemory/ui/js/dashboard.js +110 -0
  58. package/src/superlocalmemory/ui/js/events.js +178 -0
  59. package/src/superlocalmemory/ui/js/fact-detail.js +92 -0
  60. package/src/superlocalmemory/ui/js/feedback.js +333 -0
  61. package/src/superlocalmemory/ui/js/graph-core.js +447 -0
  62. package/src/superlocalmemory/ui/js/graph-filters.js +220 -0
  63. package/src/superlocalmemory/ui/js/graph-interactions.js +351 -0
  64. package/src/superlocalmemory/ui/js/graph-ui.js +214 -0
  65. package/src/superlocalmemory/ui/js/ide-status.js +102 -0
  66. package/src/superlocalmemory/ui/js/init.js +45 -0
  67. package/src/superlocalmemory/ui/js/learning.js +435 -0
  68. package/src/superlocalmemory/ui/js/lifecycle.js +298 -0
  69. package/src/superlocalmemory/ui/js/math-health.js +98 -0
  70. package/src/superlocalmemory/ui/js/memories.js +264 -0
  71. package/src/superlocalmemory/ui/js/modal.js +357 -0
  72. package/src/superlocalmemory/ui/js/patterns.js +93 -0
  73. package/src/superlocalmemory/ui/js/profiles.js +236 -0
  74. package/src/superlocalmemory/ui/js/recall-lab.js +292 -0
  75. package/src/superlocalmemory/ui/js/search.js +59 -0
  76. package/src/superlocalmemory/ui/js/settings.js +224 -0
  77. package/src/superlocalmemory/ui/js/timeline.js +32 -0
  78. package/src/superlocalmemory/ui/js/trust-dashboard.js +73 -0
@@ -0,0 +1,207 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under the MIT License - see LICENSE file
3
+ # Part of SuperLocalMemory v3.4 — CodeGraph Module
4
+
5
+ """CodeGraphWatcher — file system watcher with debounce.
6
+
7
+ Wraps watchdog Observer in a daemon thread. 300ms debounce coalesces
8
+ rapid file changes. Only reacts to supported source extensions.
9
+ Ignored directories (node_modules, .git, __pycache__, etc.) are skipped.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ import os
16
+ import threading
17
+ import time
18
+ from pathlib import Path
19
+ from typing import TYPE_CHECKING
20
+
21
+ if TYPE_CHECKING:
22
+ from superlocalmemory.code_graph.service import CodeGraphService
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ # ---------------------------------------------------------------------------
27
+ # Constants
28
+ # ---------------------------------------------------------------------------
29
+
30
+ SUPPORTED_EXTENSIONS: frozenset[str] = frozenset({
31
+ ".py", ".ts", ".tsx", ".js", ".jsx",
32
+ })
33
+
34
+ IGNORED_DIRS: frozenset[str] = frozenset({
35
+ "node_modules", ".git", "__pycache__", ".venv", "venv",
36
+ "dist", "build", ".tox", ".mypy_cache", ".pytest_cache",
37
+ "coverage", ".next", ".nuxt", ".eggs", "egg-info",
38
+ })
39
+
40
+ DEBOUNCE_SECONDS: float = 0.3
41
+
42
+
43
+ # ---------------------------------------------------------------------------
44
+ # Event handler
45
+ # ---------------------------------------------------------------------------
46
+
47
+ class _DebouncedHandler:
48
+ """File system event handler with debounce logic.
49
+
50
+ Accumulates changed file paths and flushes them after DEBOUNCE_SECONDS
51
+ of inactivity. Uses a threading.Timer for the debounce.
52
+ """
53
+
54
+ def __init__(self, repo_root: str, callback) -> None:
55
+ self._repo_root = repo_root
56
+ self._callback = callback
57
+ self._pending: dict[str, float] = {}
58
+ self._timer: threading.Timer | None = None
59
+ self._lock = threading.Lock()
60
+
61
+ def on_event(self, src_path: str, is_delete: bool = False) -> None:
62
+ """Handle a file system event."""
63
+ # Filter by extension
64
+ ext = os.path.splitext(src_path)[1]
65
+ if ext not in SUPPORTED_EXTENSIONS:
66
+ return
67
+
68
+ # Filter by ignored directories
69
+ parts = Path(src_path).parts
70
+ for part in parts:
71
+ if part in IGNORED_DIRS:
72
+ return
73
+
74
+ with self._lock:
75
+ if is_delete:
76
+ # Deletions are immediate
77
+ self._flush_immediate([src_path])
78
+ return
79
+
80
+ self._pending[src_path] = time.time()
81
+ if self._timer is not None:
82
+ self._timer.cancel()
83
+ self._timer = threading.Timer(DEBOUNCE_SECONDS, self._flush)
84
+ self._timer.daemon = True
85
+ self._timer.start()
86
+
87
+ def _flush(self) -> None:
88
+ """Flush pending changes to the callback."""
89
+ with self._lock:
90
+ paths = list(self._pending.keys())
91
+ self._pending.clear()
92
+ self._timer = None
93
+
94
+ if paths:
95
+ try:
96
+ self._callback(paths)
97
+ except Exception as exc:
98
+ logger.warning("Watcher callback failed: %s", exc)
99
+
100
+ def _flush_immediate(self, paths: list[str]) -> None:
101
+ """Flush specific paths immediately (for deletions)."""
102
+ try:
103
+ self._callback(paths)
104
+ except Exception as exc:
105
+ logger.warning("Watcher callback failed: %s", exc)
106
+
107
+ def cancel(self) -> None:
108
+ """Cancel any pending timer."""
109
+ with self._lock:
110
+ if self._timer is not None:
111
+ self._timer.cancel()
112
+ self._timer = None
113
+ self._pending.clear()
114
+
115
+
116
+ # ---------------------------------------------------------------------------
117
+ # CodeGraphWatcher
118
+ # ---------------------------------------------------------------------------
119
+
120
+ class CodeGraphWatcher:
121
+ """Watches a repository for file changes and triggers incremental updates.
122
+
123
+ Runs in a daemon thread — never blocks the main thread (HR-6).
124
+ """
125
+
126
+ def __init__(self, repo_root: str, service: CodeGraphService) -> None:
127
+ self._repo_root = str(repo_root)
128
+ self._service = service
129
+ self._observer = None
130
+ self._handler = None
131
+ self._running = False
132
+
133
+ def start(self) -> None:
134
+ """Start watching. Non-blocking — spawns a daemon thread."""
135
+ try:
136
+ from watchdog.observers import Observer
137
+ from watchdog.events import FileSystemEventHandler
138
+ except ImportError:
139
+ raise ImportError(
140
+ "watchdog not installed. Install with: pip install superlocalmemory[code-graph]"
141
+ )
142
+
143
+ def _on_changes(paths: list[str]) -> None:
144
+ """Called by debounced handler with accumulated changes."""
145
+ try:
146
+ rel_paths = []
147
+ root = Path(self._repo_root)
148
+ for p in paths:
149
+ try:
150
+ rel = str(Path(p).relative_to(root))
151
+ rel_paths.append(rel)
152
+ except ValueError:
153
+ rel_paths.append(p)
154
+
155
+ logger.info(
156
+ "Watcher detected %d changed files, updating graph",
157
+ len(rel_paths),
158
+ )
159
+ # Placeholder: incremental update would go here
160
+ # The service doesn't have an incremental_update method yet
161
+ # but this is where it would be called
162
+ except Exception as exc:
163
+ logger.warning("Watcher update failed: %s", exc)
164
+
165
+ self._handler = _DebouncedHandler(self._repo_root, _on_changes)
166
+
167
+ class _WatchdogBridge(FileSystemEventHandler):
168
+ """Bridge between watchdog events and our debounced handler."""
169
+
170
+ def __init__(self, handler: _DebouncedHandler) -> None:
171
+ self._handler = handler
172
+
173
+ def on_modified(self, event):
174
+ if not event.is_directory:
175
+ self._handler.on_event(event.src_path)
176
+
177
+ def on_created(self, event):
178
+ if not event.is_directory:
179
+ self._handler.on_event(event.src_path)
180
+
181
+ def on_deleted(self, event):
182
+ if not event.is_directory:
183
+ self._handler.on_event(event.src_path, is_delete=True)
184
+
185
+ self._observer = Observer()
186
+ bridge = _WatchdogBridge(self._handler)
187
+ self._observer.schedule(bridge, self._repo_root, recursive=True)
188
+ self._observer.daemon = True
189
+ self._observer.start()
190
+ self._running = True
191
+ logger.info("CodeGraphWatcher started for %s", self._repo_root)
192
+
193
+ def stop(self) -> None:
194
+ """Stop watching and clean up."""
195
+ if self._handler is not None:
196
+ self._handler.cancel()
197
+ if self._observer is not None:
198
+ self._observer.stop()
199
+ self._observer.join(timeout=5)
200
+ self._observer = None
201
+ self._running = False
202
+ logger.info("CodeGraphWatcher stopped")
203
+
204
+ @property
205
+ def is_running(self) -> bool:
206
+ """Whether the watcher is currently running."""
207
+ return self._running
@@ -164,13 +164,15 @@ def _worker_main() -> None:
164
164
  except Exception as exc:
165
165
  _respond({"ok": False, "error": str(exc)})
166
166
 
167
- # V3.3.16: RSS watchdog — self-terminate if memory exceeds 1.5GB.
167
+ # V3.3.16: RSS watchdog — self-terminate if memory exceeds limit.
168
168
  # PyTorch on ARM64 Mac never returns memory to OS. After ~200 embeds
169
169
  # a worker that started at 300MB grows to 17GB+. Parent auto-respawns
170
170
  # a fresh worker on next request (existing mechanism in embeddings.py).
171
+ # V3.3.21: Configurable via SLM_EMBED_WORKER_RSS_LIMIT_MB (default 2500MB).
171
172
  import resource
173
+ _rss_limit = int(os.environ.get("SLM_EMBED_WORKER_RSS_LIMIT_MB", 2500))
172
174
  rss_mb = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024 / 1024
173
- if rss_mb > 2500:
175
+ if rss_mb > _rss_limit:
174
176
  sys.exit(0)
175
177
 
176
178
  continue
@@ -52,8 +52,14 @@ class DimensionMismatchError(RuntimeError):
52
52
  _IDLE_TIMEOUT_SECONDS = 120 # 2 minutes — kill worker after idle
53
53
  # V3.3.12: Configurable via SLM_EMBED_IDLE_TIMEOUT env var (seconds)
54
54
  _IDLE_TIMEOUT_SECONDS = int(os.environ.get("SLM_EMBED_IDLE_TIMEOUT", _IDLE_TIMEOUT_SECONDS))
55
- _SUBPROCESS_RESPONSE_TIMEOUT = 180 # V3.3.12: 180s (was 120s)respawns on stressed systems need more time
56
- _WORKER_RECYCLE_AFTER = 1000 # Recycle worker after N requests (C++ fragmentation prevention)
55
+ # V3.3.21: Configurable response timeout180s default, but batch ingestion
56
+ # (2-turn chunks across 10 conversations) needs 600s+ to survive cold-start
57
+ # model downloads and ARM64 ONNX compilation pauses.
58
+ _SUBPROCESS_RESPONSE_TIMEOUT = int(os.environ.get("SLM_EMBED_RESPONSE_TIMEOUT", 180))
59
+ # V3.3.21: Increase recycle threshold to 5000 (was 1000). With 2-turn chunks,
60
+ # a single conversation produces ~50-80 store calls. 10 conversations = 500-800.
61
+ # Recycling at 1000 caused mid-ingestion worker death → timeout cascade.
62
+ _WORKER_RECYCLE_AFTER = int(os.environ.get("SLM_EMBED_RECYCLE_AFTER", 5000))
57
63
 
58
64
 
59
65
  class EmbeddingService:
@@ -212,6 +212,38 @@ class MemoryEngine:
212
212
  self._config.mode.value, self._profile_id,
213
213
  )
214
214
 
215
+ # V3.3.21: Process any pending memories from failed async remember.
216
+ # Zero cost if no pending.db exists. Backward compatible.
217
+ self._process_pending_memories()
218
+
219
+ def _process_pending_memories(self) -> None:
220
+ """Process pending memories from store-first async pattern.
221
+
222
+ Called on initialize(). If pending.db doesn't exist or has no items,
223
+ returns immediately (~0ms). If items exist, processes them through the
224
+ normal store() pipeline and marks them done/failed.
225
+ """
226
+ try:
227
+ from superlocalmemory.cli.pending_store import (
228
+ get_pending, mark_done, mark_failed,
229
+ )
230
+ except ImportError:
231
+ return
232
+
233
+ base_dir = self._config.base_dir
234
+ pending = get_pending(base_dir, limit=20)
235
+ if not pending:
236
+ return
237
+
238
+ logger.info("Processing %d pending memories from async store", len(pending))
239
+ for item in pending:
240
+ try:
241
+ self.store(item["content"])
242
+ mark_done(item["id"], base_dir)
243
+ except Exception as exc:
244
+ logger.warning("Pending memory %d failed: %s", item["id"], exc)
245
+ mark_failed(item["id"], str(exc), base_dir)
246
+
215
247
  # -- Store operations ---------------------------------------------------
216
248
 
217
249
  def store(
@@ -334,6 +334,11 @@ def _init_spreading_activation(
334
334
  vector_store: Any,
335
335
  ) -> Any | None:
336
336
  """Create SpreadingActivation for Phase 3 5th retrieval channel."""
337
+ # V3.3.21: Guard against None vector_store. Without embeddings, SA's
338
+ # search() crashes with "'NoneType' has no attribute 'search'".
339
+ if vector_store is None:
340
+ logger.debug("SpreadingActivation skipped: no vector_store")
341
+ return None
337
342
  try:
338
343
  from superlocalmemory.retrieval.spreading_activation import (
339
344
  SpreadingActivation,
@@ -172,7 +172,12 @@ def run_store(
172
172
  # This ensures BM25 and semantic search can always find the original text.
173
173
  # V3.3.12: Extract entities from verbatim content so entity channel + temporal
174
174
  # channel can find it (was entities=[] which made 4/6 channels blind).
175
- if content.strip() and len(content.strip()) >= 20:
175
+ # V3.3.20: Stronger verbatim filter — skip greetings, filler, short phrases.
176
+ # Verbatim facts with just "Hey! How are you?" dilute embeddings and add noise.
177
+ _MIN_VERBATIM_WORDS = 8
178
+ if (content.strip()
179
+ and len(content.strip()) >= 40
180
+ and len(content.strip().split()) >= _MIN_VERBATIM_WORDS):
176
181
  import uuid
177
182
  import re as _re
178
183
  _verbatim_text = content.strip()
@@ -197,6 +202,23 @@ def run_store(
197
202
  if verbatim.content.strip().lower() not in extracted_texts:
198
203
  facts.append(verbatim)
199
204
 
205
+ # V3.3.21: If fact extraction produced nothing (short input like "this is test"),
206
+ # store the raw content as a minimal fact. User explicitly called `slm remember` —
207
+ # their data should NEVER be silently dropped. The min-length and min-word filters
208
+ # are designed for automatic conversation extraction, not explicit user storage.
209
+ if not facts and content.strip():
210
+ import uuid
211
+ facts = [AtomicFact(
212
+ fact_id=uuid.uuid4().hex[:16],
213
+ content=content.strip(),
214
+ fact_type=FactType.SEMANTIC,
215
+ entities=[],
216
+ session_id=session_id,
217
+ observation_date=parsed_date,
218
+ confidence=0.7,
219
+ importance=0.3,
220
+ )]
221
+
200
222
  if not facts:
201
223
  return []
202
224
 
@@ -212,7 +212,40 @@ def _try_parse_date(raw: str, reference_date: str | None = None) -> str | None:
212
212
  except Exception:
213
213
  pass
214
214
 
215
- # dateparser for relative dates (yesterday, last week, next Friday)
215
+ # V3.3.21: Rule-based relative date resolution (no dateparser dependency).
216
+ # Handles the 90% case: yesterday, today, tomorrow, last X, next X.
217
+ raw_lower = raw.strip().lower()
218
+ if reference_date:
219
+ try:
220
+ from datetime import datetime, timedelta
221
+ ref_dt = du_parser.parse(reference_date)
222
+ _RELATIVE_MAP: dict[str, int] = {
223
+ "yesterday": -1, "today": 0, "tomorrow": 1,
224
+ "the day before": -2, "the other day": -2,
225
+ "day before yesterday": -2,
226
+ }
227
+ if raw_lower in _RELATIVE_MAP:
228
+ resolved = ref_dt + timedelta(days=_RELATIVE_MAP[raw_lower])
229
+ return resolved.date().isoformat()
230
+ # "last week" = -7, "last month" ≈ -30, "last year" = -365
231
+ if raw_lower == "last week":
232
+ return (ref_dt - timedelta(days=7)).date().isoformat()
233
+ if raw_lower == "last month":
234
+ month = ref_dt.month - 1 or 12
235
+ year = ref_dt.year if ref_dt.month > 1 else ref_dt.year - 1
236
+ return f"{year}-{month:02d}-{ref_dt.day:02d}"
237
+ if raw_lower == "last year":
238
+ return f"{ref_dt.year - 1}-{ref_dt.month:02d}-{ref_dt.day:02d}"
239
+ if raw_lower == "next week":
240
+ return (ref_dt + timedelta(days=7)).date().isoformat()
241
+ if raw_lower == "next month":
242
+ month = ref_dt.month + 1 if ref_dt.month < 12 else 1
243
+ year = ref_dt.year if ref_dt.month < 12 else ref_dt.year + 1
244
+ return f"{year}-{month:02d}-{ref_dt.day:02d}"
245
+ except Exception:
246
+ pass
247
+
248
+ # dateparser for complex relative dates (optional dependency)
216
249
  try:
217
250
  import dateparser
218
251
  settings: dict[str, Any] = {"PREFER_DATES_FROM": "past"}
@@ -223,6 +256,8 @@ def _try_parse_date(raw: str, reference_date: str | None = None) -> str | None:
223
256
  result = dateparser.parse(raw, settings=settings)
224
257
  if result:
225
258
  return result.date().isoformat()
259
+ except ImportError:
260
+ pass
226
261
  except Exception:
227
262
  pass
228
263
 
@@ -521,19 +556,45 @@ class FactExtractor:
521
556
  if _is_filler(sent):
522
557
  continue
523
558
  normalized = sent.strip()
524
- if normalized in seen_texts or len(normalized) < 10:
559
+ if normalized in seen_texts or len(normalized) < 20:
525
560
  continue
526
561
  seen_texts.add(normalized)
527
562
 
528
- # Resolve [Speaker]: prefix to "Speaker" in content
529
- # "[Caroline]: I went to..." → "Caroline: I went to..."
563
+ # V3.3.21: Resolve [Speaker]: prefix AND first-person pronouns.
564
+ # "[Caroline]: I went to the gym" → "Caroline went to the gym"
565
+ # This makes facts self-contained and entity-rich for retrieval.
566
+ # Previous: just prepended "Caroline: I went..." which left pronouns.
530
567
  import re as _re
531
- _spk_match = _re.match(r"^\[([A-Za-z ]+)\]:\s*", normalized)
568
+ _resolved_speaker: str | None = None
569
+ _spk_match = _re.match(r"^\[([A-Za-z ]+)\]:?\s*", normalized)
532
570
  if _spk_match:
533
- speaker_name = _spk_match.group(1)
534
- normalized = f"{speaker_name}: {normalized[_spk_match.end():]}"
571
+ _resolved_speaker = _spk_match.group(1)
572
+ rest = normalized[_spk_match.end():]
573
+ # Replace first-person pronouns with speaker name
574
+ rest = _re.sub(r"\bI'm\b", f"{_resolved_speaker} is", rest)
575
+ rest = _re.sub(r"\bI've\b", f"{_resolved_speaker} has", rest)
576
+ rest = _re.sub(r"\bI'll\b", f"{_resolved_speaker} will", rest)
577
+ rest = _re.sub(r"\bI'd\b", f"{_resolved_speaker} would", rest)
578
+ rest = _re.sub(r"\bI was\b", f"{_resolved_speaker} was", rest)
579
+ rest = _re.sub(r"\bI am\b", f"{_resolved_speaker} is", rest)
580
+ rest = _re.sub(r"\bI\b", _resolved_speaker, rest)
581
+ rest = _re.sub(r"\bmy\b", f"{_resolved_speaker}'s", rest, flags=_re.IGNORECASE)
582
+ rest = _re.sub(r"\bme\b", _resolved_speaker, rest)
583
+ rest = _re.sub(r"\bmyself\b", _resolved_speaker, rest, flags=_re.IGNORECASE)
584
+ normalized = rest
585
+
586
+ # V3.3.21 R4: Post-resolution filler + length check.
587
+ # After stripping [Speaker]: prefix, the remaining text may be
588
+ # just "Hey!", "Yeah totally!", "Thanks Mel!" — pure noise.
589
+ # These passed the pre-resolution filler check because the prefix
590
+ # was still attached. Re-check after resolution.
591
+ if _is_filler(normalized) or len(normalized.strip()) < 20:
592
+ continue
535
593
 
536
594
  entities = _extract_entities(normalized)
595
+ # Ensure resolved speaker is in entities list
596
+ if _resolved_speaker and _resolved_speaker not in entities:
597
+ entities = [_resolved_speaker] + entities
537
598
  fact_type = _classify_sentence(normalized)
538
599
 
539
600
  # Three-date model: extract and resolve relative dates
@@ -39,6 +39,11 @@ VALID_EVENT_TYPES = frozenset([
39
39
  "trust.signal", # V3: trust score change
40
40
  "compliance.audit", # V3: compliance event logged
41
41
  "learning.feedback", # V3: learning feedback received
42
+ # CodeGraph events (v3.4) — NOTE: "graph.updated" is SLM entity graph, "code_graph.*" is AST code graph
43
+ "code_graph.built", # Full code graph build completed
44
+ "code_graph.updated", # Incremental code graph update completed
45
+ "code_graph.node_changed", # Function/class signature or body changed
46
+ "code_graph.node_deleted", # Function/class/file removed from codebase
42
47
  ])
43
48
 
44
49
 
@@ -114,6 +114,7 @@ from superlocalmemory.mcp.tools_v3 import register_v3_tools
114
114
  from superlocalmemory.mcp.tools_active import register_active_tools
115
115
  from superlocalmemory.mcp.tools_v33 import register_v33_tools
116
116
  from superlocalmemory.mcp.resources import register_resources
117
+ from superlocalmemory.mcp.tools_code_graph import register_code_graph_tools
117
118
 
118
119
  register_core_tools(_target, get_engine)
119
120
  register_v28_tools(_target, get_engine)
@@ -121,6 +122,28 @@ register_v3_tools(_target, get_engine)
121
122
  register_active_tools(_target, get_engine)
122
123
  register_v33_tools(_target, get_engine)
123
124
  register_resources(server, get_engine) # Resources always registered (not tools)
125
+ register_code_graph_tools(server, get_engine) # CodeGraph: registered on `server` (always visible when installed)
126
+
127
+
128
+ # V3.3.21: Eager engine warmup — start initializing BEFORE first tool call.
129
+ # The MCP server process starts when the IDE launches. Previously, the engine
130
+ # was lazy-loaded on first tool call → 23s cold start for the user.
131
+ # Now: engine starts warming in a background thread immediately. By the time
132
+ # the first tool call arrives (1-2s later), the engine is already warm.
133
+ # This applies to ALL IDEs: Claude Code, Cursor, Antigravity, Gemini CLI, etc.
134
+ def _eager_warmup() -> None:
135
+ """Pre-warm engine in background thread."""
136
+ import logging
137
+ _logger = logging.getLogger(__name__)
138
+ try:
139
+ get_engine()
140
+ _logger.info("MCP engine pre-warmed successfully")
141
+ except Exception as exc:
142
+ _logger.debug("MCP engine pre-warmup failed (non-fatal): %s", exc)
143
+
144
+ import threading
145
+ _warmup_thread = threading.Thread(target=_eager_warmup, daemon=True, name="mcp-warmup")
146
+ _warmup_thread.start()
124
147
 
125
148
 
126
149
  if __name__ == "__main__":