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.
- package/package.json +1 -1
- package/pyproject.toml +9 -1
- package/src/superlocalmemory/cli/commands.py +138 -22
- package/src/superlocalmemory/cli/daemon.py +372 -0
- package/src/superlocalmemory/cli/main.py +8 -0
- package/src/superlocalmemory/cli/pending_store.py +158 -0
- package/src/superlocalmemory/cli/setup_wizard.py +39 -6
- package/src/superlocalmemory/code_graph/__init__.py +46 -0
- package/src/superlocalmemory/code_graph/blast_radius.py +177 -0
- package/src/superlocalmemory/code_graph/bridge/__init__.py +36 -0
- package/src/superlocalmemory/code_graph/bridge/entity_resolver.py +464 -0
- package/src/superlocalmemory/code_graph/bridge/event_listeners.py +195 -0
- package/src/superlocalmemory/code_graph/bridge/fact_enricher.py +159 -0
- package/src/superlocalmemory/code_graph/bridge/hebbian_linker.py +170 -0
- package/src/superlocalmemory/code_graph/bridge/temporal_checker.py +152 -0
- package/src/superlocalmemory/code_graph/changes.py +363 -0
- package/src/superlocalmemory/code_graph/communities.py +299 -0
- package/src/superlocalmemory/code_graph/config.py +88 -0
- package/src/superlocalmemory/code_graph/database.py +482 -0
- package/src/superlocalmemory/code_graph/extractors/__init__.py +78 -0
- package/src/superlocalmemory/code_graph/extractors/python.py +413 -0
- package/src/superlocalmemory/code_graph/extractors/typescript.py +556 -0
- package/src/superlocalmemory/code_graph/flows.py +350 -0
- package/src/superlocalmemory/code_graph/git_hooks.py +226 -0
- package/src/superlocalmemory/code_graph/graph_engine.py +295 -0
- package/src/superlocalmemory/code_graph/graph_store.py +158 -0
- package/src/superlocalmemory/code_graph/incremental.py +200 -0
- package/src/superlocalmemory/code_graph/models.py +130 -0
- package/src/superlocalmemory/code_graph/parser.py +507 -0
- package/src/superlocalmemory/code_graph/resolver.py +321 -0
- package/src/superlocalmemory/code_graph/search.py +460 -0
- package/src/superlocalmemory/code_graph/service.py +95 -0
- package/src/superlocalmemory/code_graph/watcher.py +207 -0
- package/src/superlocalmemory/core/embedding_worker.py +4 -2
- package/src/superlocalmemory/core/embeddings.py +8 -2
- package/src/superlocalmemory/core/engine.py +32 -0
- package/src/superlocalmemory/core/engine_wiring.py +5 -0
- package/src/superlocalmemory/core/store_pipeline.py +23 -1
- package/src/superlocalmemory/encoding/fact_extractor.py +68 -7
- package/src/superlocalmemory/infra/event_bus.py +5 -0
- package/src/superlocalmemory/mcp/server.py +23 -0
- package/src/superlocalmemory/mcp/tools_code_graph.py +1592 -0
- package/src/superlocalmemory/retrieval/engine.py +137 -2
- package/src/superlocalmemory/retrieval/semantic_channel.py +6 -2
- package/src/superlocalmemory/retrieval/spreading_activation.py +5 -3
- package/src/superlocalmemory/retrieval/strategy.py +16 -0
- package/src/superlocalmemory/server/api.py +4 -2
- package/src/superlocalmemory/server/ui.py +5 -2
- package/src/superlocalmemory/storage/schema_code_graph.py +239 -0
- package/src/superlocalmemory/ui/index.html +1879 -0
- package/src/superlocalmemory/ui/js/agents.js +192 -0
- package/src/superlocalmemory/ui/js/auto-settings.js +399 -0
- package/src/superlocalmemory/ui/js/behavioral.js +276 -0
- package/src/superlocalmemory/ui/js/clusters.js +206 -0
- package/src/superlocalmemory/ui/js/compliance.js +252 -0
- package/src/superlocalmemory/ui/js/core.js +246 -0
- package/src/superlocalmemory/ui/js/dashboard.js +110 -0
- package/src/superlocalmemory/ui/js/events.js +178 -0
- package/src/superlocalmemory/ui/js/fact-detail.js +92 -0
- package/src/superlocalmemory/ui/js/feedback.js +333 -0
- package/src/superlocalmemory/ui/js/graph-core.js +447 -0
- package/src/superlocalmemory/ui/js/graph-filters.js +220 -0
- package/src/superlocalmemory/ui/js/graph-interactions.js +351 -0
- package/src/superlocalmemory/ui/js/graph-ui.js +214 -0
- package/src/superlocalmemory/ui/js/ide-status.js +102 -0
- package/src/superlocalmemory/ui/js/init.js +45 -0
- package/src/superlocalmemory/ui/js/learning.js +435 -0
- package/src/superlocalmemory/ui/js/lifecycle.js +298 -0
- package/src/superlocalmemory/ui/js/math-health.js +98 -0
- package/src/superlocalmemory/ui/js/memories.js +264 -0
- package/src/superlocalmemory/ui/js/modal.js +357 -0
- package/src/superlocalmemory/ui/js/patterns.js +93 -0
- package/src/superlocalmemory/ui/js/profiles.js +236 -0
- package/src/superlocalmemory/ui/js/recall-lab.js +292 -0
- package/src/superlocalmemory/ui/js/search.js +59 -0
- package/src/superlocalmemory/ui/js/settings.js +224 -0
- package/src/superlocalmemory/ui/js/timeline.js +32 -0
- 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
|
|
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 >
|
|
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
|
-
|
|
56
|
-
|
|
55
|
+
# V3.3.21: Configurable response timeout — 180s 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
|
-
|
|
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
|
-
#
|
|
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) <
|
|
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
|
|
529
|
-
# "[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
|
-
|
|
568
|
+
_resolved_speaker: str | None = None
|
|
569
|
+
_spk_match = _re.match(r"^\[([A-Za-z ]+)\]:?\s*", normalized)
|
|
532
570
|
if _spk_match:
|
|
533
|
-
|
|
534
|
-
|
|
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__":
|