superlocalmemory 3.7.8 → 3.8.1
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/.claude-plugin/marketplace.json +1 -1
- package/ATTRIBUTION.md +1 -3
- package/CHANGELOG.md +129 -0
- package/README.md +205 -123
- package/package.json +12 -3
- package/plugin/.claude-plugin/plugin.json +2 -3
- package/plugin/CLAUDE.md +8 -8
- package/plugin/agents/slm-governance-advisor.md +80 -0
- package/plugin/agents/slm-loop-runner.md +71 -0
- package/plugin/agents/slm-memory-advisor.md +10 -5
- package/plugin/agents/slm-optimize-advisor.md +9 -3
- package/plugin/commands/slm-loop.md +31 -0
- package/plugin/hooks/hooks.json +79 -0
- package/plugin/requirements.txt +1 -1
- package/plugin/scripts/slm-launch +46 -7
- package/plugin/settings.json +9 -0
- package/plugin/skills/slm-cache/SKILL.md +9 -1
- package/plugin/skills/slm-compress/SKILL.md +8 -1
- package/plugin/skills/slm-governance/SKILL.md +248 -0
- package/plugin/skills/slm-graph/SKILL.md +17 -3
- package/plugin/skills/slm-loop/SKILL.md +99 -0
- package/plugin/skills/slm-mesh/SKILL.md +282 -0
- package/plugin/skills/slm-profile/SKILL.md +148 -0
- package/plugin/skills/slm-recall/SKILL.md +46 -10
- package/plugin/skills/slm-remember/SKILL.md +48 -1
- package/plugin/skills/slm-scope/SKILL.md +176 -0
- package/plugin/skills/slm-session/SKILL.md +24 -1
- package/plugin/skills/slm-status/SKILL.md +18 -1
- package/plugin-src/rules/AGENTS.md +57 -18
- package/plugin-src/skills/slm-cache/SKILL.md +9 -1
- package/plugin-src/skills/slm-compress/SKILL.md +8 -1
- package/plugin-src/skills/slm-graph/SKILL.md +17 -3
- package/plugin-src/skills/slm-recall/SKILL.md +46 -10
- package/plugin-src/skills/slm-remember/SKILL.md +48 -1
- package/plugin-src/skills/slm-session/SKILL.md +24 -1
- package/plugin-src/skills/slm-status/SKILL.md +18 -1
- package/pyproject.toml +2 -1
- package/scripts/postinstall/validation.js +2 -0
- package/scripts/postinstall-interactive.js +74 -2
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/access/__init__.py +3 -0
- package/src/superlocalmemory/access/rbac.py +477 -0
- package/src/superlocalmemory/cli/commands.py +228 -17
- package/src/superlocalmemory/cli/compress_cmd.py +17 -7
- package/src/superlocalmemory/cli/daemon.py +7 -0
- package/src/superlocalmemory/cli/loop_cmd.py +187 -0
- package/src/superlocalmemory/cli/main.py +49 -8
- package/src/superlocalmemory/cli/mesh_cmd.py +38 -0
- package/src/superlocalmemory/cli/optimize_cmd.py +3 -0
- package/src/superlocalmemory/cli/pending_store.py +49 -13
- package/src/superlocalmemory/cli/proxy_cmd.py +4 -0
- package/src/superlocalmemory/cli/scale_engine_cmd.py +6 -0
- package/src/superlocalmemory/cli/setup_wizard.py +22 -13
- package/src/superlocalmemory/cli/version_banner.py +17 -3
- package/src/superlocalmemory/compliance/audit.py +6 -0
- package/src/superlocalmemory/compliance/gdpr.py +128 -138
- package/src/superlocalmemory/compliance/retention.py +176 -45
- package/src/superlocalmemory/core/backend_orchestrator.py +23 -59
- package/src/superlocalmemory/core/community_summary.py +267 -0
- package/src/superlocalmemory/core/config.py +216 -3
- package/src/superlocalmemory/core/consolidation_engine.py +95 -22
- package/src/superlocalmemory/core/context_cache.py +61 -18
- package/src/superlocalmemory/core/embedding_worker.py +21 -7
- package/src/superlocalmemory/core/embeddings.py +131 -46
- package/src/superlocalmemory/core/engine.py +41 -22
- package/src/superlocalmemory/core/engine_ingestion.py +359 -43
- package/src/superlocalmemory/core/engine_wiring.py +13 -0
- package/src/superlocalmemory/core/entity_community.py +178 -0
- package/src/superlocalmemory/core/graph_analyzer.py +39 -2
- package/src/superlocalmemory/core/graph_pruner.py +13 -8
- package/src/superlocalmemory/core/ingestion_command.py +134 -25
- package/src/superlocalmemory/core/injection.py +12 -7
- package/src/superlocalmemory/core/key_expander.py +138 -0
- package/src/superlocalmemory/core/maintenance.py +23 -0
- package/src/superlocalmemory/core/maintenance_scheduler.py +17 -7
- package/src/superlocalmemory/core/modes.py +1 -1
- package/src/superlocalmemory/core/mutations.py +2 -2
- package/src/superlocalmemory/core/pii.py +105 -0
- package/src/superlocalmemory/core/progressive_abstraction.py +208 -0
- package/src/superlocalmemory/core/recall_pipeline.py +7 -3
- package/src/superlocalmemory/core/recall_worker.py +20 -6
- package/src/superlocalmemory/core/scale_engine.py +60 -1
- package/src/superlocalmemory/core/security_primitives.py +40 -2
- package/src/superlocalmemory/core/store_pipeline.py +186 -29
- package/src/superlocalmemory/core/worker_pool.py +21 -6
- package/src/superlocalmemory/encoding/entity_reflexion.py +200 -0
- package/src/superlocalmemory/encoding/entity_resolver.py +34 -24
- package/src/superlocalmemory/encoding/fact_extractor.py +26 -1
- package/src/superlocalmemory/encoding/temporal_validator.py +64 -1
- package/src/superlocalmemory/evolution/evolution_store.py +122 -45
- package/src/superlocalmemory/evolution/llm_dispatch.py +12 -1
- package/src/superlocalmemory/evolution/model_selection.py +160 -0
- package/src/superlocalmemory/evolution/mutation_generator.py +16 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +127 -42
- package/src/superlocalmemory/evolution/triggers.py +22 -13
- package/src/superlocalmemory/graph/cozo_backend.py +43 -20
- package/src/superlocalmemory/hooks/adapter_base.py +5 -1
- package/src/superlocalmemory/hooks/auto_recall.py +13 -1
- package/src/superlocalmemory/hooks/claude_code_hooks.py +11 -0
- package/src/superlocalmemory/hooks/codex_assets.py +64 -5
- package/src/superlocalmemory/hooks/hook_daemon.py +20 -3
- package/src/superlocalmemory/hooks/hook_handlers.py +6 -1
- package/src/superlocalmemory/hooks/memory_protocol.py +54 -0
- package/src/superlocalmemory/hooks/portable_kit.py +148 -3
- package/src/superlocalmemory/infra/backup.py +12 -1
- package/src/superlocalmemory/infra/daemon_identity.py +40 -4
- package/src/superlocalmemory/infra/data_root.py +43 -4
- package/src/superlocalmemory/infra/event_bus.py +107 -24
- package/src/superlocalmemory/infra/rate_limiter.py +93 -0
- package/src/superlocalmemory/ingestion/adapter_manager.py +4 -1
- package/src/superlocalmemory/ingestion/credentials.py +1 -1
- package/src/superlocalmemory/learning/cross_project.py +28 -19
- package/src/superlocalmemory/learning/model_rollback.py +3 -0
- package/src/superlocalmemory/learning/ranker_retrain_online.py +2 -0
- package/src/superlocalmemory/learning/reward.py +50 -0
- package/src/superlocalmemory/learning/reward_proxy.py +42 -9
- package/src/superlocalmemory/learning/source_quality.py +523 -1
- package/src/superlocalmemory/loops/__init__.py +56 -0
- package/src/superlocalmemory/loops/budget.py +58 -0
- package/src/superlocalmemory/loops/engine.py +164 -0
- package/src/superlocalmemory/loops/ledger.py +263 -0
- package/src/superlocalmemory/loops/models.py +152 -0
- package/src/superlocalmemory/loops/rules.py +52 -0
- package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
- package/src/superlocalmemory/mcp/_pool_adapter.py +2 -0
- package/src/superlocalmemory/mcp/profiles.py +103 -0
- package/src/superlocalmemory/mcp/server.py +32 -79
- package/src/superlocalmemory/mcp/tools_active.py +4 -7
- package/src/superlocalmemory/mcp/tools_code_graph.py +51 -5
- package/src/superlocalmemory/mcp/tools_core.py +12 -4
- package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
- package/src/superlocalmemory/mcp/tools_learning.py +2 -2
- package/src/superlocalmemory/mcp/tools_loops.py +300 -0
- package/src/superlocalmemory/mcp/tools_mesh.py +140 -4
- package/src/superlocalmemory/mcp/tools_optimize.py +15 -8
- package/src/superlocalmemory/mesh/broker.py +237 -129
- package/src/superlocalmemory/mesh/remote_sync.py +50 -8
- package/src/superlocalmemory/optimize/NOTICE +1 -6
- package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +1 -4
- package/src/superlocalmemory/optimize/adapters/openai_adapter.py +1 -4
- package/src/superlocalmemory/optimize/cache/semantic.py +27 -19
- package/src/superlocalmemory/optimize/compress/align.py +32 -26
- package/src/superlocalmemory/optimize/compress/ccr.py +14 -71
- package/src/superlocalmemory/optimize/compress/router.py +105 -22
- package/src/superlocalmemory/optimize/config/defaults.py +1 -1
- package/src/superlocalmemory/optimize/config/schema.py +87 -4
- package/src/superlocalmemory/optimize/metrics/counters.py +13 -4
- package/src/superlocalmemory/optimize/metrics/estimator.py +0 -3
- package/src/superlocalmemory/optimize/proxy/_helpers.py +31 -4
- package/src/superlocalmemory/optimize/storage/db.py +38 -9
- package/src/superlocalmemory/optimize/storage/schema.py +10 -0
- package/src/superlocalmemory/parameterization/pattern_extractor.py +6 -3
- package/src/superlocalmemory/retrieval/agentic.py +1 -1
- package/src/superlocalmemory/retrieval/bm25_channel.py +68 -10
- package/src/superlocalmemory/retrieval/engine.py +221 -47
- package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
- package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
- package/src/superlocalmemory/retrieval/reranker.py +3 -4
- package/src/superlocalmemory/retrieval/semantic_channel.py +114 -21
- package/src/superlocalmemory/retrieval/spreading_activation.py +11 -2
- package/src/superlocalmemory/retrieval/temporal_channel.py +48 -9
- package/src/superlocalmemory/retrieval/temporal_frame.py +102 -0
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +135 -0
- package/src/superlocalmemory/retrieval/time_window.py +181 -0
- package/src/superlocalmemory/server/api.py +4 -4
- package/src/superlocalmemory/server/config_file.py +90 -0
- package/src/superlocalmemory/server/origin.py +50 -0
- package/src/superlocalmemory/server/profile_runtime.py +125 -8
- package/src/superlocalmemory/server/rbac_enforce.py +142 -0
- package/src/superlocalmemory/server/recall_health.py +24 -3
- package/src/superlocalmemory/server/recall_serializer.py +19 -1
- package/src/superlocalmemory/server/routes/abstraction.py +115 -0
- package/src/superlocalmemory/server/routes/agents.py +128 -38
- package/src/superlocalmemory/server/routes/backup.py +317 -70
- package/src/superlocalmemory/server/routes/behavioral.py +349 -71
- package/src/superlocalmemory/server/routes/brain.py +69 -12
- package/src/superlocalmemory/server/routes/chat.py +10 -5
- package/src/superlocalmemory/server/routes/compliance.py +171 -21
- package/src/superlocalmemory/server/routes/config_api.py +438 -0
- package/src/superlocalmemory/server/routes/data_io.py +30 -8
- package/src/superlocalmemory/server/routes/entity.py +108 -26
- package/src/superlocalmemory/server/routes/events.py +24 -8
- package/src/superlocalmemory/server/routes/evolution.py +189 -68
- package/src/superlocalmemory/server/routes/helpers.py +16 -1
- package/src/superlocalmemory/server/routes/ingest.py +7 -4
- package/src/superlocalmemory/server/routes/insights.py +3 -3
- package/src/superlocalmemory/server/routes/learning.py +289 -118
- package/src/superlocalmemory/server/routes/learning_telemetry.py +153 -0
- package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
- package/src/superlocalmemory/server/routes/memories.py +182 -57
- package/src/superlocalmemory/server/routes/mesh.py +200 -31
- package/src/superlocalmemory/server/routes/optimize.py +33 -1
- package/src/superlocalmemory/server/routes/prewarm.py +2 -0
- package/src/superlocalmemory/server/routes/profiles.py +63 -17
- package/src/superlocalmemory/server/routes/ratelimit.py +132 -0
- package/src/superlocalmemory/server/routes/rbac.py +367 -0
- package/src/superlocalmemory/server/routes/stats.py +103 -158
- package/src/superlocalmemory/server/routes/tiers.py +11 -9
- package/src/superlocalmemory/server/routes/token.py +3 -13
- package/src/superlocalmemory/server/routes/v3_api.py +247 -89
- package/src/superlocalmemory/server/routes/ws.py +5 -2
- package/src/superlocalmemory/server/security_middleware.py +12 -5
- package/src/superlocalmemory/server/ui.py +20 -5
- package/src/superlocalmemory/server/unified_daemon.py +827 -72
- package/src/superlocalmemory/server/write_identity.py +38 -8
- package/src/superlocalmemory/storage/database.py +265 -53
- package/src/superlocalmemory/storage/migration_runner.py +132 -1
- package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +5 -0
- package/src/superlocalmemory/storage/migrations/M021_ingestion_log_profile.py +108 -0
- package/src/superlocalmemory/storage/migrations/M022_entity_aliases_profile.py +86 -0
- package/src/superlocalmemory/storage/migrations/M023_mesh_profile_isolation.py +194 -0
- package/src/superlocalmemory/storage/migrations/M024_rbac_users_roles.py +87 -0
- package/src/superlocalmemory/storage/migrations/M025_perf_indexes.py +90 -0
- package/src/superlocalmemory/storage/migrations/M026_rbac_memberships_fk.py +136 -0
- package/src/superlocalmemory/storage/migrations/M027_transferable_patterns_profile.py +163 -0
- package/src/superlocalmemory/storage/migrations/M028_fact_entity_associations.py +270 -0
- package/src/superlocalmemory/storage/migrations/M029_behavioral_history_indexes.py +137 -0
- package/src/superlocalmemory/storage/migrations/M030_entity_explorer_indexes.py +93 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +4 -0
- package/src/superlocalmemory/storage/models.py +4 -0
- package/src/superlocalmemory/storage/schema.py +136 -1
- package/src/superlocalmemory/storage/schema_v32.py +2 -0
- package/src/superlocalmemory/storage/schema_v343.py +24 -12
- package/src/superlocalmemory/storage/schema_v347.py +4 -0
- package/src/superlocalmemory/trust/gate.py +49 -8
- package/src/superlocalmemory/ui/assets/slm-icon-white.svg +64 -0
- package/src/superlocalmemory/ui/assets/slm-icon.svg +36 -0
- package/src/superlocalmemory/ui/css/design-system.css +621 -0
- package/src/superlocalmemory/ui/css/neural-glass.css +6 -0
- package/src/superlocalmemory/ui/css/od-bridge.css +158 -0
- package/src/superlocalmemory/ui/favicon.svg +35 -4
- package/src/superlocalmemory/ui/index.html +303 -173
- package/src/superlocalmemory/ui/js/brain.js +5 -20
- package/src/superlocalmemory/ui/js/core.js +100 -41
- package/src/superlocalmemory/ui/js/dashboard.js +403 -65
- package/src/superlocalmemory/ui/js/event-delegation.js +102 -0
- package/src/superlocalmemory/ui/js/knowledge-graph.js +11 -11
- package/src/superlocalmemory/ui/js/math-health.js +1 -1
- package/src/superlocalmemory/ui/js/memories.js +15 -4
- package/src/superlocalmemory/ui/js/memory-chat.js +7 -7
- package/src/superlocalmemory/ui/js/ng-entities.js +6 -8
- package/src/superlocalmemory/ui/js/ng-ingestion.js +4 -4
- package/src/superlocalmemory/ui/js/ng-mesh.js +4 -9
- package/src/superlocalmemory/ui/js/ng-shell.js +8 -8
- package/src/superlocalmemory/ui/js/ng-skills.js +54 -2
- package/src/superlocalmemory/ui/js/od-agents.js +544 -0
- package/src/superlocalmemory/ui/js/od-auth-gate.js +257 -0
- package/src/superlocalmemory/ui/js/od-backup.js +871 -0
- package/src/superlocalmemory/ui/js/od-brain.js +816 -0
- package/src/superlocalmemory/ui/js/od-entities.js +579 -0
- package/src/superlocalmemory/ui/js/od-graph.js +600 -0
- package/src/superlocalmemory/ui/js/od-health.js +539 -0
- package/src/superlocalmemory/ui/js/od-mcp.js +508 -0
- package/src/superlocalmemory/ui/js/od-memories.js +929 -0
- package/src/superlocalmemory/ui/js/od-mesh.js +553 -0
- package/src/superlocalmemory/ui/js/od-operations.js +1250 -0
- package/src/superlocalmemory/ui/js/od-optimize.js +787 -0
- package/src/superlocalmemory/ui/js/od-settings.js +1107 -0
- package/src/superlocalmemory/ui/js/od-shell.js +809 -0
- package/src/superlocalmemory/ui/js/od-skills.js +600 -0
- package/src/superlocalmemory/ui/js/od-team.js +258 -0
- package/src/superlocalmemory/ui/js/profiles.js +159 -46
- package/src/superlocalmemory/ui/js/settings.js +17 -3
- package/src/superlocalmemory/ui/js/timeline.js +34 -5
- package/src/superlocalmemory/ui/js/trust-dashboard.js +2 -2
- package/src/superlocalmemory/vector/lancedb_backend.py +8 -6
- package/plugin-src/.mcp.json +0 -12
- package/plugin-src/agents/slm-memory-advisor.md +0 -44
- package/plugin-src/agents/slm-optimize-advisor.md +0 -38
- package/plugin-src/hooks/.gitkeep +0 -0
- package/plugin-src/hooks/hooks.json +0 -23
- package/plugin-src/manifest.json +0 -25
- package/plugin-src/requirements.txt +0 -1
- package/plugin-src/rules/CLAUDE.md.fragment +0 -44
- package/plugin-src/scripts/ensure-venv.bat +0 -122
- package/plugin-src/scripts/ensure-venv.sh +0 -105
- package/plugin-src/scripts/slm-launch +0 -23
- package/plugin-src/scripts/slm-launch.bat +0 -23
- package/plugin-src/settings.json +0 -16
- package/src/superlocalmemory/learning/behavioral_listener.py +0 -94
|
@@ -211,6 +211,43 @@ class GraphAnalyzer:
|
|
|
211
211
|
result[node] = comm_id
|
|
212
212
|
return result
|
|
213
213
|
|
|
214
|
+
def detect_communities_louvain(
|
|
215
|
+
self,
|
|
216
|
+
graph: Any = None,
|
|
217
|
+
profile_id: str = "",
|
|
218
|
+
) -> dict[str, int]:
|
|
219
|
+
"""Detect communities via Louvain (modularity-optimizing).
|
|
220
|
+
|
|
221
|
+
Higher quality than Label Propagation (deterministic with a seed,
|
|
222
|
+
no giant-community collapse), pure-Python via networkx — no extra
|
|
223
|
+
binary deps. Falls back to Label Propagation if unavailable.
|
|
224
|
+
"""
|
|
225
|
+
import networkx as nx
|
|
226
|
+
|
|
227
|
+
if graph is None:
|
|
228
|
+
graph = self._build_networkx_graph(profile_id)
|
|
229
|
+
if graph.number_of_nodes() == 0:
|
|
230
|
+
return {}
|
|
231
|
+
|
|
232
|
+
undirected = graph.to_undirected()
|
|
233
|
+
try:
|
|
234
|
+
from networkx.algorithms.community import louvain_communities
|
|
235
|
+
|
|
236
|
+
communities = louvain_communities(
|
|
237
|
+
undirected, weight="weight", seed=42,
|
|
238
|
+
)
|
|
239
|
+
except Exception as exc:
|
|
240
|
+
logger.debug(
|
|
241
|
+
"Louvain unavailable/failed (%s); using Label Propagation", exc,
|
|
242
|
+
)
|
|
243
|
+
return self.detect_communities(graph, profile_id)
|
|
244
|
+
|
|
245
|
+
result: dict[str, int] = {}
|
|
246
|
+
for comm_id, community in enumerate(communities):
|
|
247
|
+
for node in community:
|
|
248
|
+
result[node] = comm_id
|
|
249
|
+
return result
|
|
250
|
+
|
|
214
251
|
# ── v3.4.1: Leiden Community Detection ────────────────────────
|
|
215
252
|
|
|
216
253
|
def detect_communities_leiden(
|
|
@@ -234,9 +271,9 @@ class GraphAnalyzer:
|
|
|
234
271
|
import igraph
|
|
235
272
|
except ImportError:
|
|
236
273
|
logger.info(
|
|
237
|
-
"leidenalg not installed, using
|
|
274
|
+
"leidenalg not installed, using Louvain fallback",
|
|
238
275
|
)
|
|
239
|
-
return self.
|
|
276
|
+
return self.detect_communities_louvain(graph, profile_id)
|
|
240
277
|
|
|
241
278
|
# Convert DiGraph -> undirected -> igraph
|
|
242
279
|
undirected = graph.to_undirected()
|
|
@@ -350,8 +350,12 @@ def _cap_node_degree(
|
|
|
350
350
|
Algorithm (single-pass window function — no Python loops):
|
|
351
351
|
1. ROW_NUMBER() OVER (PARTITION BY source_id ORDER BY weight DESC) ranks
|
|
352
352
|
every edge per node in one full table scan.
|
|
353
|
-
2.
|
|
354
|
-
|
|
353
|
+
2. Excess edge IDs are collected in a reusable temp table.
|
|
354
|
+
3. A single DELETE statement removes them; rowcount is returned.
|
|
355
|
+
4. The temp table is created once and cleared via DELETE (not DROP) —
|
|
356
|
+
DROP TABLE acquires an EXCLUSIVE lock that conflicts with concurrent
|
|
357
|
+
writers, causing "database is locked". Using CREATE...IF NOT EXISTS
|
|
358
|
+
plus DELETE FROM avoids that conflict while preserving rowcount.
|
|
355
359
|
"""
|
|
356
360
|
# gi-03: cap BOTH out-degree (PARTITION BY source_id) AND in-degree
|
|
357
361
|
# (PARTITION BY target_id). Previously only out-degree was capped, so hub
|
|
@@ -379,9 +383,13 @@ def _cap_node_degree(
|
|
|
379
383
|
)
|
|
380
384
|
return excess
|
|
381
385
|
|
|
382
|
-
#
|
|
383
|
-
|
|
384
|
-
|
|
386
|
+
# CREATE IF NOT EXISTS + DELETE FROM instead of DROP + CREATE.
|
|
387
|
+
# DROP TABLE acquires EXCLUSIVE which conflicts with concurrent writers.
|
|
388
|
+
# CREATE IF NOT EXISTS is idempotent; DELETE FROM clears prior contents.
|
|
389
|
+
c.execute(
|
|
390
|
+
"CREATE TEMP TABLE IF NOT EXISTS _slm_cap_del (edge_id TEXT PRIMARY KEY)"
|
|
391
|
+
)
|
|
392
|
+
c.execute("DELETE FROM _slm_cap_del")
|
|
385
393
|
c.execute(
|
|
386
394
|
"""
|
|
387
395
|
INSERT OR IGNORE INTO _slm_cap_del (edge_id)
|
|
@@ -396,7 +404,6 @@ def _cap_node_degree(
|
|
|
396
404
|
(profile_id, max_degree, max_degree),
|
|
397
405
|
)
|
|
398
406
|
|
|
399
|
-
# Step 2: delete the over-cap edges (single DELETE).
|
|
400
407
|
c.execute(
|
|
401
408
|
"""
|
|
402
409
|
DELETE FROM graph_edges
|
|
@@ -407,8 +414,6 @@ def _cap_node_degree(
|
|
|
407
414
|
)
|
|
408
415
|
deleted = c.rowcount
|
|
409
416
|
|
|
410
|
-
c.execute("DROP TABLE IF EXISTS _slm_cap_del")
|
|
411
|
-
|
|
412
417
|
logger.info(
|
|
413
418
|
"_cap_node_degree: deleted %d low-weight edges (max_degree=%d, in+out capped)",
|
|
414
419
|
deleted, max_degree,
|
|
@@ -24,6 +24,7 @@ from typing import Any, Callable
|
|
|
24
24
|
from superlocalmemory.storage.database import DatabaseManager
|
|
25
25
|
|
|
26
26
|
_MATERIALIZATION_LOCKS = tuple(threading.RLock() for _ in range(64))
|
|
27
|
+
_MAX_AUTOMATIC_MATERIALIZATION_ATTEMPTS = 10
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
def _materialization_lock(operation_id: str) -> threading.RLock:
|
|
@@ -51,6 +52,10 @@ class OperationInProgress(RuntimeError):
|
|
|
51
52
|
"""Another live lease owner is materializing this operation."""
|
|
52
53
|
|
|
53
54
|
|
|
55
|
+
class LeaseLost(OperationInProgress):
|
|
56
|
+
"""The materializer no longer owns its durable operation lease."""
|
|
57
|
+
|
|
58
|
+
|
|
54
59
|
def _canonical_json(value: Any) -> str:
|
|
55
60
|
return json.dumps(value, sort_keys=True, separators=(",", ":"))
|
|
56
61
|
|
|
@@ -284,12 +289,19 @@ class IngestionOperationRepository:
|
|
|
284
289
|
self._from_row(row)
|
|
285
290
|
for row in self.db.execute(
|
|
286
291
|
"SELECT * FROM ingestion_operations "
|
|
287
|
-
"WHERE
|
|
292
|
+
"WHERE attempt_count < ? AND ("
|
|
293
|
+
"(state='queryable' AND created_at <= "
|
|
288
294
|
"strftime('%Y-%m-%dT%H:%M:%fZ', 'now', ?)) "
|
|
289
295
|
"OR (state='failed' AND next_retry_at <= ?) "
|
|
290
|
-
"OR (state='enriching' AND lease_expires_at <= ?) "
|
|
296
|
+
"OR (state='enriching' AND lease_expires_at <= ?)) "
|
|
291
297
|
"ORDER BY created_at, rowid LIMIT ?",
|
|
292
|
-
(
|
|
298
|
+
(
|
|
299
|
+
_MAX_AUTOMATIC_MATERIALIZATION_ATTEMPTS,
|
|
300
|
+
grace_modifier,
|
|
301
|
+
now,
|
|
302
|
+
now,
|
|
303
|
+
max(1, int(limit)),
|
|
304
|
+
),
|
|
293
305
|
)
|
|
294
306
|
]
|
|
295
307
|
|
|
@@ -415,6 +427,27 @@ class IngestionOperationRepository:
|
|
|
415
427
|
)
|
|
416
428
|
return operation
|
|
417
429
|
|
|
430
|
+
def renew_enriching_lease(
|
|
431
|
+
self,
|
|
432
|
+
operation_id: str,
|
|
433
|
+
*,
|
|
434
|
+
owner: str,
|
|
435
|
+
lease_seconds: float,
|
|
436
|
+
) -> bool:
|
|
437
|
+
"""Extend a live lease only while the same owner still holds it."""
|
|
438
|
+
rows = self.db.execute(
|
|
439
|
+
"UPDATE ingestion_operations SET lease_expires_at=?, "
|
|
440
|
+
"updated_at=strftime('%Y-%m-%dT%H:%M:%fZ', 'now') "
|
|
441
|
+
"WHERE operation_id=? AND state='enriching' AND lease_owner=? "
|
|
442
|
+
"RETURNING operation_id",
|
|
443
|
+
(
|
|
444
|
+
time.time() + max(1.0, float(lease_seconds)),
|
|
445
|
+
operation_id,
|
|
446
|
+
owner,
|
|
447
|
+
),
|
|
448
|
+
)
|
|
449
|
+
return bool(rows)
|
|
450
|
+
|
|
418
451
|
def finish_enriching(
|
|
419
452
|
self,
|
|
420
453
|
operation_id: str,
|
|
@@ -469,6 +502,7 @@ class MaterializationResult:
|
|
|
469
502
|
|
|
470
503
|
fact_ids: tuple[str, ...]
|
|
471
504
|
derivation_state: dict[str, bool]
|
|
505
|
+
last_error: str = ""
|
|
472
506
|
|
|
473
507
|
|
|
474
508
|
QueryableWriter = Callable[[IngestionRequest, str], list[str]]
|
|
@@ -500,6 +534,47 @@ class IngestionCommand:
|
|
|
500
534
|
self._lease_seconds = max(1.0, float(lease_seconds))
|
|
501
535
|
self._owner = f"ingestion-worker:{uuid.uuid4().hex}"
|
|
502
536
|
|
|
537
|
+
def _run_with_lease_heartbeat(
|
|
538
|
+
self,
|
|
539
|
+
operation_id: str,
|
|
540
|
+
callback: Callable[[], Any],
|
|
541
|
+
) -> Any:
|
|
542
|
+
"""Run slow work while periodically renewing its owner-bound lease."""
|
|
543
|
+
stop = threading.Event()
|
|
544
|
+
lost = threading.Event()
|
|
545
|
+
interval = min(30.0, max(0.1, self._lease_seconds / 3.0))
|
|
546
|
+
|
|
547
|
+
def heartbeat() -> None:
|
|
548
|
+
while not stop.wait(interval):
|
|
549
|
+
try:
|
|
550
|
+
renewed = self.repository.renew_enriching_lease(
|
|
551
|
+
operation_id,
|
|
552
|
+
owner=self._owner,
|
|
553
|
+
lease_seconds=self._lease_seconds,
|
|
554
|
+
)
|
|
555
|
+
except sqlite3.Error:
|
|
556
|
+
continue
|
|
557
|
+
if not renewed:
|
|
558
|
+
lost.set()
|
|
559
|
+
return
|
|
560
|
+
|
|
561
|
+
thread = threading.Thread(
|
|
562
|
+
target=heartbeat,
|
|
563
|
+
name=f"ingestion-lease-heartbeat:{operation_id[:8]}",
|
|
564
|
+
daemon=True,
|
|
565
|
+
)
|
|
566
|
+
thread.start()
|
|
567
|
+
try:
|
|
568
|
+
result = callback()
|
|
569
|
+
finally:
|
|
570
|
+
stop.set()
|
|
571
|
+
thread.join(timeout=max(1.0, interval * 2))
|
|
572
|
+
if lost.is_set():
|
|
573
|
+
raise LeaseLost(
|
|
574
|
+
f"ingestion lease lost for operation {operation_id}"
|
|
575
|
+
)
|
|
576
|
+
return result
|
|
577
|
+
|
|
503
578
|
def submit(self, request: IngestionRequest) -> IngestionOperation:
|
|
504
579
|
operation, _created = self.submit_with_status(request)
|
|
505
580
|
return operation
|
|
@@ -548,27 +623,41 @@ class IngestionCommand:
|
|
|
548
623
|
)
|
|
549
624
|
if enriching.state is IngestionState.COMPLETE:
|
|
550
625
|
return enriching
|
|
551
|
-
if
|
|
626
|
+
if (
|
|
627
|
+
enriching.final_fact_ids
|
|
628
|
+
and all(enriching.derivation_state.values())
|
|
629
|
+
):
|
|
552
630
|
return self._project_and_complete(enriching)
|
|
553
631
|
try:
|
|
632
|
+
# Materialization is a durable saga, not one long SQLite
|
|
633
|
+
# transaction. Extractors, embedders, and local model calls can
|
|
634
|
+
# take minutes on a mature installation; keeping a write
|
|
635
|
+
# transaction open across that work blocks every interactive
|
|
636
|
+
# remember/update/delete and makes the dashboard appear dead.
|
|
637
|
+
#
|
|
638
|
+
# The materializer commits its relational checkpoints in short
|
|
639
|
+
# database operations. The operation lease and derivation state
|
|
640
|
+
# remain the recovery boundary, and only the final state-machine
|
|
641
|
+
# checkpoint is grouped atomically below.
|
|
642
|
+
materialized = self._run_with_lease_heartbeat(
|
|
643
|
+
operation_id,
|
|
644
|
+
lambda: self._materializer(enriching),
|
|
645
|
+
)
|
|
646
|
+
if isinstance(materialized, MaterializationResult):
|
|
647
|
+
fact_ids = tuple(materialized.fact_ids)
|
|
648
|
+
derivation_state = dict(materialized.derivation_state)
|
|
649
|
+
materialization_error = materialized.last_error
|
|
650
|
+
else:
|
|
651
|
+
fact_ids = tuple(materialized)
|
|
652
|
+
derivation_state = {"materializer": True}
|
|
653
|
+
materialization_error = ""
|
|
654
|
+
if not fact_ids:
|
|
655
|
+
raise RuntimeError("materialization produced no final facts")
|
|
656
|
+
incomplete = sorted(
|
|
657
|
+
name for name, complete in derivation_state.items()
|
|
658
|
+
if not complete
|
|
659
|
+
)
|
|
554
660
|
with self.repository.db.transaction():
|
|
555
|
-
materialized = self._materializer(enriching)
|
|
556
|
-
if isinstance(materialized, MaterializationResult):
|
|
557
|
-
fact_ids = tuple(materialized.fact_ids)
|
|
558
|
-
derivation_state = dict(materialized.derivation_state)
|
|
559
|
-
else:
|
|
560
|
-
fact_ids = tuple(materialized)
|
|
561
|
-
derivation_state = {"materializer": True}
|
|
562
|
-
if not fact_ids:
|
|
563
|
-
raise RuntimeError("materialization produced no final facts")
|
|
564
|
-
incomplete = sorted(
|
|
565
|
-
name for name, complete in derivation_state.items()
|
|
566
|
-
if not complete
|
|
567
|
-
)
|
|
568
|
-
if incomplete:
|
|
569
|
-
raise RuntimeError(
|
|
570
|
-
"incomplete derivation stages: " + ", ".join(incomplete)
|
|
571
|
-
)
|
|
572
661
|
checkpointed = self.repository.checkpoint_enriching(
|
|
573
662
|
operation_id,
|
|
574
663
|
final_fact_ids=fact_ids,
|
|
@@ -577,6 +666,21 @@ class IngestionCommand:
|
|
|
577
666
|
lease_owner=self._owner,
|
|
578
667
|
lease_seconds=self._lease_seconds,
|
|
579
668
|
)
|
|
669
|
+
if materialization_error or incomplete:
|
|
670
|
+
error = materialization_error or (
|
|
671
|
+
"incomplete derivation stages: " + ", ".join(incomplete)
|
|
672
|
+
)
|
|
673
|
+
return self.repository.finish_enriching(
|
|
674
|
+
operation_id,
|
|
675
|
+
owner=self._owner,
|
|
676
|
+
target=IngestionState.FAILED,
|
|
677
|
+
final_fact_ids=fact_ids,
|
|
678
|
+
derivation_version=self._derivation_version,
|
|
679
|
+
derivation_state=derivation_state,
|
|
680
|
+
last_error=error,
|
|
681
|
+
)
|
|
682
|
+
except LeaseLost:
|
|
683
|
+
raise
|
|
580
684
|
except Exception as exc:
|
|
581
685
|
return self.repository.finish_enriching(
|
|
582
686
|
operation_id,
|
|
@@ -598,10 +702,13 @@ class IngestionCommand:
|
|
|
598
702
|
owner=self._owner,
|
|
599
703
|
lease_seconds=self._lease_seconds,
|
|
600
704
|
)
|
|
601
|
-
projection_state = (
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
705
|
+
projection_state = self._run_with_lease_heartbeat(
|
|
706
|
+
operation.operation_id,
|
|
707
|
+
lambda: (
|
|
708
|
+
dict(self._projector(operation))
|
|
709
|
+
if self._projector is not None
|
|
710
|
+
else {}
|
|
711
|
+
),
|
|
605
712
|
)
|
|
606
713
|
combined = {**operation.derivation_state, **projection_state}
|
|
607
714
|
incomplete = sorted(
|
|
@@ -619,6 +726,8 @@ class IngestionCommand:
|
|
|
619
726
|
derivation_version=self._derivation_version,
|
|
620
727
|
derivation_state=combined,
|
|
621
728
|
)
|
|
729
|
+
except LeaseLost:
|
|
730
|
+
raise
|
|
622
731
|
except Exception as exc:
|
|
623
732
|
return self.repository.finish_enriching(
|
|
624
733
|
operation.operation_id,
|
|
@@ -128,20 +128,25 @@ def _load_injection_config():
|
|
|
128
128
|
|
|
129
129
|
|
|
130
130
|
def estimate_tokens(text: str) -> int:
|
|
131
|
-
"""
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
131
|
+
"""Estimate context tokens without loading native code by default.
|
|
132
|
+
|
|
133
|
+
The stable default is the deterministic ``chars / 4`` heuristic. Loading
|
|
134
|
+
the optional native ``tiktoken`` extension in a long-lived daemon that also
|
|
135
|
+
hosts torch/scipy workers can terminate the whole process on a native fault;
|
|
136
|
+
Python exception handling cannot recover from SIGBUS/SIGSEGV. Users who
|
|
137
|
+
explicitly need exact OpenAI token counts can opt in with
|
|
138
|
+
``SLM_INJECTION_EXACT_TOKENS=1``.
|
|
137
139
|
"""
|
|
138
140
|
if not text:
|
|
139
141
|
return 0
|
|
142
|
+
heuristic = max(1, len(text) // 4)
|
|
143
|
+
if os.environ.get("SLM_INJECTION_EXACT_TOKENS", "0") != "1":
|
|
144
|
+
return heuristic
|
|
140
145
|
try:
|
|
141
146
|
import tiktoken
|
|
142
147
|
return len(tiktoken.get_encoding("cl100k_base").encode(text))
|
|
143
148
|
except Exception:
|
|
144
|
-
return
|
|
149
|
+
return heuristic
|
|
145
150
|
|
|
146
151
|
|
|
147
152
|
def resolve_budget(mode: str, cfg) -> int:
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
|
+
|
|
5
|
+
"""Fact-augmented key expansion (Phase 4, T3b).
|
|
6
|
+
|
|
7
|
+
Generates *alternate keys* for a fact — synonyms, aliases, and paraphrases —
|
|
8
|
+
that get indexed in ``fact_expansion_fts`` and UNION'd into BM25 retrieval, so a
|
|
9
|
+
query for "automobile" or "the Big Apple" can match a fact that only says "car"
|
|
10
|
+
or "NYC".
|
|
11
|
+
|
|
12
|
+
Two tiers, matching the rest of SLM:
|
|
13
|
+
* Mode A (zero-LLM): pulls the fact's resolved entities' canonical names and
|
|
14
|
+
aliases from SLM's own entity graph — no model, no cost, and it reuses the
|
|
15
|
+
entity resolution already done at ingest.
|
|
16
|
+
* Mode B/C: additionally asks the LLM for a few paraphrase keywords (own
|
|
17
|
+
prompt, fail-open, bounded).
|
|
18
|
+
|
|
19
|
+
Keys already present in the fact's content are dropped — indexing them twice
|
|
20
|
+
adds nothing. Returns a single space-joined string ready for the FTS row.
|
|
21
|
+
|
|
22
|
+
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
23
|
+
License: AGPL-3.0-or-later
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import logging
|
|
29
|
+
from typing import TYPE_CHECKING, Any
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from superlocalmemory.storage.database import DatabaseManager
|
|
33
|
+
from superlocalmemory.storage.models import AtomicFact
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
# Bound the LLM enrichment so a pathological response can't bloat the index.
|
|
38
|
+
_MAX_LLM_KEYS = 8
|
|
39
|
+
_MAX_KEY_LEN = 60
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class KeyExpander:
|
|
43
|
+
"""Produces alternate search keys for a fact (T3b)."""
|
|
44
|
+
|
|
45
|
+
__slots__ = ("_db", "_llm")
|
|
46
|
+
|
|
47
|
+
def __init__(self, db: DatabaseManager, llm: Any = None) -> None:
|
|
48
|
+
self._db = db
|
|
49
|
+
self._llm = llm
|
|
50
|
+
|
|
51
|
+
def expand(self, fact: AtomicFact, profile_id: str, mode: str = "a") -> str:
|
|
52
|
+
"""Return space-joined alternate keys for ``fact`` (may be empty)."""
|
|
53
|
+
keys: set[str] = set()
|
|
54
|
+
keys |= self._alias_keys(fact, profile_id)
|
|
55
|
+
if mode in ("b", "c") and self._llm_available():
|
|
56
|
+
keys |= self._llm_keys(fact)
|
|
57
|
+
|
|
58
|
+
content_low = (getattr(fact, "content", "") or "").lower()
|
|
59
|
+
cleaned = {
|
|
60
|
+
k.strip() for k in keys
|
|
61
|
+
if k and k.strip() and len(k.strip()) <= _MAX_KEY_LEN
|
|
62
|
+
and k.strip().lower() not in content_low
|
|
63
|
+
}
|
|
64
|
+
return " ".join(sorted(cleaned))
|
|
65
|
+
|
|
66
|
+
# -- Mode A: entity aliases from SLM's own entity graph ------------------
|
|
67
|
+
|
|
68
|
+
def _alias_keys(self, fact: AtomicFact, profile_id: str) -> set[str]:
|
|
69
|
+
out: set[str] = set()
|
|
70
|
+
for name in (getattr(fact, "canonical_entities", None) or []):
|
|
71
|
+
if not name:
|
|
72
|
+
continue
|
|
73
|
+
try:
|
|
74
|
+
ent = self._db.get_entity_by_name(name, profile_id)
|
|
75
|
+
except Exception:
|
|
76
|
+
ent = None
|
|
77
|
+
if ent is None:
|
|
78
|
+
continue
|
|
79
|
+
canonical = getattr(ent, "canonical_name", None)
|
|
80
|
+
if canonical:
|
|
81
|
+
out.add(canonical)
|
|
82
|
+
entity_id = getattr(ent, "entity_id", None)
|
|
83
|
+
if not entity_id:
|
|
84
|
+
continue
|
|
85
|
+
try:
|
|
86
|
+
for alias in self._db.get_aliases_for_entity(entity_id, profile_id):
|
|
87
|
+
a = getattr(alias, "alias", None)
|
|
88
|
+
if a:
|
|
89
|
+
out.add(a)
|
|
90
|
+
except Exception:
|
|
91
|
+
continue
|
|
92
|
+
return out
|
|
93
|
+
|
|
94
|
+
# -- Mode B/C: LLM paraphrases (own prompt, fail-open) -------------------
|
|
95
|
+
|
|
96
|
+
def _llm_available(self) -> bool:
|
|
97
|
+
if self._llm is None:
|
|
98
|
+
return False
|
|
99
|
+
check = getattr(self._llm, "is_available", None)
|
|
100
|
+
try:
|
|
101
|
+
return bool(check()) if callable(check) else bool(check)
|
|
102
|
+
except Exception:
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
def _llm_keys(self, fact: AtomicFact) -> set[str]:
|
|
106
|
+
content = getattr(fact, "content", "") or ""
|
|
107
|
+
if not content:
|
|
108
|
+
return set()
|
|
109
|
+
prompt = (
|
|
110
|
+
"Give 3-6 short alternative search terms (synonyms, aliases, or "
|
|
111
|
+
"paraphrases) that a person might use to look up the memory below. "
|
|
112
|
+
"Output ONLY a comma-separated list, no numbering, no explanation.\n\n"
|
|
113
|
+
f"Memory: {content}"
|
|
114
|
+
)
|
|
115
|
+
raw = self._invoke_llm(prompt)
|
|
116
|
+
if not raw:
|
|
117
|
+
return set()
|
|
118
|
+
seen: list[str] = []
|
|
119
|
+
for part in raw.replace("\n", ",").split(","):
|
|
120
|
+
p = part.strip()
|
|
121
|
+
if p and p not in seen:
|
|
122
|
+
seen.append(p)
|
|
123
|
+
if len(seen) >= _MAX_LLM_KEYS:
|
|
124
|
+
break
|
|
125
|
+
return set(seen)
|
|
126
|
+
|
|
127
|
+
def _invoke_llm(self, prompt: str) -> str:
|
|
128
|
+
"""Call whatever generation method the injected LLM exposes; fail-open."""
|
|
129
|
+
for meth in ("generate", "complete", "chat"):
|
|
130
|
+
fn = getattr(self._llm, meth, None)
|
|
131
|
+
if callable(fn):
|
|
132
|
+
try:
|
|
133
|
+
out = fn(prompt)
|
|
134
|
+
return out if isinstance(out, str) else str(out or "")
|
|
135
|
+
except Exception as exc:
|
|
136
|
+
logger.debug("KeyExpander LLM (%s) failed: %s", meth, exc)
|
|
137
|
+
return ""
|
|
138
|
+
return ""
|
|
@@ -127,6 +127,7 @@ def run_maintenance(
|
|
|
127
127
|
"sheaf_checked": 0,
|
|
128
128
|
"entity_summaries_consolidated": 0, # V3.4.40
|
|
129
129
|
"orphan_metadata_gc": 0, # v3.6.4 (P1-3)
|
|
130
|
+
"expansion_backfilled": 0, # T3b
|
|
130
131
|
}
|
|
131
132
|
|
|
132
133
|
# P1-3 (embeddings-vector-02): sweep orphaned embedding_metadata left by
|
|
@@ -141,6 +142,28 @@ def run_maintenance(
|
|
|
141
142
|
if not facts:
|
|
142
143
|
return counts
|
|
143
144
|
|
|
145
|
+
# T3b: backfill fact-expansion alt-keys (Mode A, entity-alias based) for
|
|
146
|
+
# facts stored before expansion existed. Bounded per run + skips already-
|
|
147
|
+
# populated and entity-less facts, so it converges without re-work churn.
|
|
148
|
+
try:
|
|
149
|
+
from superlocalmemory.core.key_expander import KeyExpander
|
|
150
|
+
populated = {
|
|
151
|
+
dict(r)["fact_id"]
|
|
152
|
+
for r in db.execute("SELECT DISTINCT fact_id FROM fact_expansion_fts")
|
|
153
|
+
}
|
|
154
|
+
expander = KeyExpander(db)
|
|
155
|
+
for f in facts:
|
|
156
|
+
if counts["expansion_backfilled"] >= 500:
|
|
157
|
+
break
|
|
158
|
+
if f.fact_id in populated or not f.canonical_entities:
|
|
159
|
+
continue
|
|
160
|
+
alt = expander.expand(f, profile_id, mode="a")
|
|
161
|
+
if alt:
|
|
162
|
+
db.upsert_fact_expansion(f.fact_id, alt)
|
|
163
|
+
counts["expansion_backfilled"] += 1
|
|
164
|
+
except Exception as exc: # pragma: no cover — legacy DB / missing FTS
|
|
165
|
+
logger.debug("expansion backfill skipped: %s", exc)
|
|
166
|
+
|
|
144
167
|
# 1a. Backfill: seed uninitialized facts with metadata-aware positions (B+C)
|
|
145
168
|
if config.math.langevin_persist_positions:
|
|
146
169
|
try:
|
|
@@ -8,7 +8,8 @@ V3.3.13: Periodically triggers Langevin/Ebbinghaus/Sheaf maintenance
|
|
|
8
8
|
so users don't need to call run_maintenance manually.
|
|
9
9
|
|
|
10
10
|
Configurable interval via ForgettingConfig.scheduler_interval_minutes.
|
|
11
|
-
Defaults to 30 min.
|
|
11
|
+
Defaults to 30 min. Optional forgetting/math work follows
|
|
12
|
+
``config.forgetting.enabled``; tier evaluation and bounded housekeeping do not.
|
|
12
13
|
|
|
13
14
|
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
14
15
|
License: AGPL-3.0-or-later
|
|
@@ -79,12 +80,21 @@ class MaintenanceScheduler:
|
|
|
79
80
|
if not self._running:
|
|
80
81
|
return
|
|
81
82
|
for profile_id in self._profile_ids():
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
83
|
+
if self._config.forgetting.enabled:
|
|
84
|
+
try:
|
|
85
|
+
from superlocalmemory.core.maintenance import run_maintenance
|
|
86
|
+
counts = run_maintenance(self._db, self._config, profile_id)
|
|
87
|
+
logger.info(
|
|
88
|
+
"Scheduled maintenance complete for %s: %s",
|
|
89
|
+
profile_id,
|
|
90
|
+
counts,
|
|
91
|
+
)
|
|
92
|
+
except Exception as exc:
|
|
93
|
+
logger.warning(
|
|
94
|
+
"Scheduled maintenance failed for %s: %s",
|
|
95
|
+
profile_id,
|
|
96
|
+
exc,
|
|
97
|
+
)
|
|
88
98
|
|
|
89
99
|
# V3.4.11: Graph pruning (remove orphan edges)
|
|
90
100
|
try:
|
|
@@ -109,7 +109,7 @@ MODE_C = ModeCapabilities(
|
|
|
109
109
|
data_stays_local=False,
|
|
110
110
|
description=(
|
|
111
111
|
"FULL POWER — UNRESTRICTED. Best embeddings (text-embedding-3-large, 3072-dim). "
|
|
112
|
-
"Best LLMs (GPT-5
|
|
112
|
+
"Best configured cloud LLMs (e.g. GPT-5, Claude Opus 4). Agentic multi-round retrieval. "
|
|
113
113
|
"Cohere reranker option. No EU restriction. Target: 90%+"
|
|
114
114
|
),
|
|
115
115
|
)
|
|
@@ -59,7 +59,7 @@ def delete_fact_authorized(
|
|
|
59
59
|
if not rows:
|
|
60
60
|
return {"ok": False, "error": f"Memory {fact_id} not found"}
|
|
61
61
|
content_preview = dict(rows[0]).get("content", "")[:80]
|
|
62
|
-
engine._db.delete_fact(fact_id)
|
|
62
|
+
engine._db.delete_fact(fact_id, profile_id=profile_id)
|
|
63
63
|
try:
|
|
64
64
|
from superlocalmemory.core.backend_orchestrator import get_orchestrator
|
|
65
65
|
orchestrator = get_orchestrator()
|
|
@@ -120,7 +120,7 @@ def update_fact_authorized(
|
|
|
120
120
|
updates["fisher_variance"] = fisher_variance
|
|
121
121
|
except Exception as exc:
|
|
122
122
|
logger.warning("UPDATE embedding refresh failed: %s", exc)
|
|
123
|
-
engine._db.update_fact(fact_id, updates)
|
|
123
|
+
engine._db.update_fact(fact_id, updates, profile_id=profile_id)
|
|
124
124
|
try:
|
|
125
125
|
from superlocalmemory.core.backend_orchestrator import get_orchestrator
|
|
126
126
|
orchestrator = get_orchestrator()
|