superlocalmemory 3.6.23 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/README.md +271 -71
- package/bin/slm-npm +43 -89
- package/ide/configs/antigravity-mcp.json +2 -2
- package/ide/configs/chatgpt-desktop-mcp.json +1 -1
- package/ide/configs/claude-desktop-mcp.json +2 -2
- package/ide/configs/windsurf-mcp.json +2 -2
- package/ide/hooks/context-hook.js +6 -2
- package/ide/hooks/post-recall-hook.js +7 -3
- package/ide/hooks/tool-event-hook.sh +2 -1
- package/package.json +18 -10
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/_GENERATED.md +1 -1
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-session/SKILL.md +1 -1
- package/plugin-src/rules/AGENTS.md +1 -1
- package/pyproject.toml +40 -8
- package/scripts/postinstall-interactive.js +17 -94
- package/scripts/postinstall.js +185 -258
- package/scripts/preuninstall.js +9 -50
- package/src/superlocalmemory/__init__.py +2 -2
- package/src/superlocalmemory/attribution/mathematical_dna.py +1 -1
- package/src/superlocalmemory/attribution/signer.py +34 -19
- package/src/superlocalmemory/attribution/watermark.py +1 -1
- package/src/superlocalmemory/cli/_lazy_init.py +3 -5
- package/src/superlocalmemory/cli/commands.py +453 -191
- package/src/superlocalmemory/cli/context_commands.py +5 -4
- package/src/superlocalmemory/cli/daemon.py +282 -187
- package/src/superlocalmemory/cli/db_migrate.py +3 -1
- package/src/superlocalmemory/cli/diagnostics_cmd.py +28 -0
- package/src/superlocalmemory/cli/evidence_cmd.py +103 -0
- package/src/superlocalmemory/cli/ingest_cmd.py +7 -3
- package/src/superlocalmemory/cli/main.py +128 -31
- package/src/superlocalmemory/cli/pending_store.py +54 -38
- package/src/superlocalmemory/cli/scale_engine_cmd.py +37 -0
- package/src/superlocalmemory/cli/service_installer.py +57 -52
- package/src/superlocalmemory/cli/setup_wizard.py +142 -88
- package/src/superlocalmemory/cli/version_banner.py +2 -1
- package/src/superlocalmemory/code_graph/config.py +3 -1
- package/src/superlocalmemory/core/backend_orchestrator.py +81 -21
- package/src/superlocalmemory/core/config.py +65 -20
- package/src/superlocalmemory/core/consolidation_engine.py +9 -7
- package/src/superlocalmemory/core/context_cache.py +56 -8
- package/src/superlocalmemory/core/derivation_lineage.py +246 -0
- package/src/superlocalmemory/core/embedding_worker.py +32 -20
- package/src/superlocalmemory/core/embeddings.py +54 -18
- package/src/superlocalmemory/core/engine.py +150 -104
- package/src/superlocalmemory/core/engine_ingestion.py +513 -0
- package/src/superlocalmemory/core/engine_wiring.py +2 -0
- package/src/superlocalmemory/core/evidence_bundle.py +526 -0
- package/src/superlocalmemory/core/fact_consolidator.py +5 -11
- package/src/superlocalmemory/core/graph_analyzer.py +2 -2
- package/src/superlocalmemory/core/health_monitor.py +4 -2
- package/src/superlocalmemory/core/ingestion_command.py +636 -0
- package/src/superlocalmemory/core/injection.py +69 -18
- package/src/superlocalmemory/core/lifecycle_state.py +153 -0
- package/src/superlocalmemory/core/maintenance.py +1 -1
- package/src/superlocalmemory/core/maintenance_scheduler.py +51 -35
- package/src/superlocalmemory/core/mutations.py +143 -0
- package/src/superlocalmemory/core/platform_utils.py +7 -4
- package/src/superlocalmemory/core/ram_lock.py +16 -5
- package/src/superlocalmemory/core/rate_limit.py +1 -1
- package/src/superlocalmemory/core/recall_pipeline.py +60 -101
- package/src/superlocalmemory/core/recall_worker.py +76 -59
- package/src/superlocalmemory/core/registry.py +1 -1
- package/src/superlocalmemory/core/scale_engine.py +293 -0
- package/src/superlocalmemory/core/score_contract.py +62 -0
- package/src/superlocalmemory/core/security_primitives.py +3 -1
- package/src/superlocalmemory/core/slm_disabled.py +3 -5
- package/src/superlocalmemory/core/store_pipeline.py +172 -40
- package/src/superlocalmemory/core/tier_manager.py +32 -20
- package/src/superlocalmemory/core/worker_pool.py +13 -4
- package/src/superlocalmemory/dynamics/activation_guided_quantization.py +1 -1
- package/src/superlocalmemory/dynamics/eap_scheduler.py +10 -3
- package/src/superlocalmemory/dynamics/ebbinghaus_langevin_coupling.py +1 -1
- package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +1 -1
- package/src/superlocalmemory/encoding/auto_linker.py +1 -1
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +7 -16
- package/src/superlocalmemory/encoding/consolidator.py +22 -5
- package/src/superlocalmemory/encoding/fact_extractor.py +1 -1
- package/src/superlocalmemory/encoding/foresight.py +2 -0
- package/src/superlocalmemory/encoding/graph_builder.py +1 -1
- package/src/superlocalmemory/encoding/temporal_parser.py +2 -0
- package/src/superlocalmemory/evaluation/__init__.py +13 -0
- package/src/superlocalmemory/evaluation/calibration.py +308 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +2 -1
- package/src/superlocalmemory/graph/cozo_backend.py +256 -23
- package/src/superlocalmemory/hooks/_outcome_common.py +21 -11
- package/src/superlocalmemory/hooks/antigravity_adapter.py +10 -31
- package/src/superlocalmemory/hooks/auto_invoker.py +25 -27
- package/src/superlocalmemory/hooks/auto_recall.py +31 -6
- package/src/superlocalmemory/hooks/auto_recall_hook.py +13 -33
- package/src/superlocalmemory/hooks/before_web_hook.py +9 -7
- package/src/superlocalmemory/hooks/claude_code_hooks.py +123 -35
- package/src/superlocalmemory/hooks/codex_assets.py +59 -0
- package/src/superlocalmemory/hooks/codex_hooks.py +186 -0
- package/src/superlocalmemory/hooks/context_payload.py +1 -1
- package/src/superlocalmemory/hooks/copilot_adapter.py +9 -24
- package/src/superlocalmemory/hooks/cursor_adapter.py +10 -32
- package/src/superlocalmemory/hooks/hook_daemon.py +4 -2
- package/src/superlocalmemory/hooks/hook_handlers.py +219 -32
- package/src/superlocalmemory/hooks/memory_protocol.py +5 -3
- package/src/superlocalmemory/hooks/post_tool_async_hook.py +4 -2
- package/src/superlocalmemory/hooks/session_registry.py +15 -8
- package/src/superlocalmemory/hooks/stop_outcome_hook.py +10 -6
- package/src/superlocalmemory/hooks/topic_shift_hook.py +42 -12
- package/src/superlocalmemory/hooks/user_prompt_hook.py +9 -14
- package/src/superlocalmemory/hooks/user_prompt_rehash_hook.py +19 -11
- package/src/superlocalmemory/infra/auth_middleware.py +38 -5
- package/src/superlocalmemory/infra/backup.py +7 -5
- package/src/superlocalmemory/infra/cloud_backup.py +18 -8
- package/src/superlocalmemory/infra/daemon_identity.py +248 -0
- package/src/superlocalmemory/infra/data_root.py +199 -0
- package/src/superlocalmemory/infra/event_bus.py +3 -1
- package/src/superlocalmemory/infra/local_diagnostics.py +327 -0
- package/src/superlocalmemory/infra/process_reaper.py +23 -0
- package/src/superlocalmemory/ingestion/adapter_manager.py +27 -9
- package/src/superlocalmemory/ingestion/base_adapter.py +25 -31
- package/src/superlocalmemory/ingestion/calendar_adapter.py +13 -4
- package/src/superlocalmemory/ingestion/credentials.py +14 -7
- package/src/superlocalmemory/ingestion/gmail_adapter.py +13 -4
- package/src/superlocalmemory/ingestion/transcript_adapter.py +7 -2
- package/src/superlocalmemory/learning/consolidation_quantization_worker.py +1 -1
- package/src/superlocalmemory/learning/ensemble.py +11 -0
- package/src/superlocalmemory/learning/entity_compiler.py +1 -1
- package/src/superlocalmemory/learning/feedback.py +1 -1
- package/src/superlocalmemory/learning/forgetting_scheduler.py +12 -7
- package/src/superlocalmemory/learning/quantization_scheduler.py +1 -1
- package/src/superlocalmemory/learning/ranker.py +4 -1
- package/src/superlocalmemory/learning/source_quality.py +1 -1
- package/src/superlocalmemory/learning/trigram_index.py +3 -2
- package/src/superlocalmemory/llm/backbone.py +1 -1
- package/src/superlocalmemory/math/ebbinghaus.py +1 -1
- package/src/superlocalmemory/math/fisher.py +1 -1
- package/src/superlocalmemory/math/fisher_quantized.py +1 -1
- package/src/superlocalmemory/math/hopfield.py +1 -1
- package/src/superlocalmemory/math/langevin.py +1 -1
- package/src/superlocalmemory/math/polar_quant.py +3 -4
- package/src/superlocalmemory/math/qjl.py +1 -1
- package/src/superlocalmemory/math/sheaf.py +1 -1
- package/src/superlocalmemory/math/turbo_quant.py +3 -2
- package/src/superlocalmemory/mcp/_daemon_proxy.py +12 -11
- package/src/superlocalmemory/mcp/_pool_adapter.py +27 -0
- package/src/superlocalmemory/mcp/http_transport.py +53 -0
- package/src/superlocalmemory/mcp/server.py +39 -13
- package/src/superlocalmemory/mcp/shared.py +69 -3
- package/src/superlocalmemory/mcp/tools_active.py +141 -31
- package/src/superlocalmemory/mcp/tools_core.py +128 -29
- package/src/superlocalmemory/mcp/tools_evolution.py +5 -7
- package/src/superlocalmemory/mcp/tools_learning.py +42 -2
- package/src/superlocalmemory/mcp/tools_mesh.py +7 -23
- package/src/superlocalmemory/mcp/tools_optimize.py +8 -1
- package/src/superlocalmemory/mcp/tools_v28.py +23 -2
- package/src/superlocalmemory/mcp/tools_v3.py +26 -1
- package/src/superlocalmemory/mcp/tools_v33.py +56 -17
- package/src/superlocalmemory/mesh/broker.py +2 -0
- package/src/superlocalmemory/mesh/remote_sync.py +50 -12
- package/src/superlocalmemory/optimize/cache/manager.py +77 -1
- package/src/superlocalmemory/optimize/cache/semantic.py +23 -3
- package/src/superlocalmemory/optimize/compress/ccr.py +4 -0
- package/src/superlocalmemory/optimize/compress/router.py +6 -1
- package/src/superlocalmemory/optimize/config/__init__.py +5 -0
- package/src/superlocalmemory/optimize/config/store.py +6 -4
- package/src/superlocalmemory/optimize/proxy/_helpers.py +15 -5
- package/src/superlocalmemory/optimize/proxy/capture.py +3 -2
- package/src/superlocalmemory/optimize/proxy/server.py +2 -2
- package/src/superlocalmemory/optimize/storage/db.py +12 -12
- package/src/superlocalmemory/retrieval/agentic.py +1 -1
- package/src/superlocalmemory/retrieval/ann_index.py +1 -1
- package/src/superlocalmemory/retrieval/bm25_channel.py +35 -11
- package/src/superlocalmemory/retrieval/bridge_discovery.py +73 -8
- package/src/superlocalmemory/retrieval/engine.py +169 -79
- package/src/superlocalmemory/retrieval/entity_channel.py +289 -67
- package/src/superlocalmemory/retrieval/forgetting_filter.py +1 -1
- package/src/superlocalmemory/retrieval/fusion.py +1 -1
- package/src/superlocalmemory/retrieval/hopfield_channel.py +118 -30
- package/src/superlocalmemory/retrieval/profile_channel.py +1 -1
- package/src/superlocalmemory/retrieval/quantization_aware_search.py +16 -10
- package/src/superlocalmemory/retrieval/reranker.py +56 -20
- package/src/superlocalmemory/retrieval/scope_policy.py +85 -0
- package/src/superlocalmemory/retrieval/semantic_channel.py +122 -14
- package/src/superlocalmemory/retrieval/spreading_activation.py +141 -25
- package/src/superlocalmemory/retrieval/strategy.py +1 -1
- package/src/superlocalmemory/retrieval/temporal_channel.py +30 -15
- package/src/superlocalmemory/retrieval/vector_store.py +1 -1
- package/src/superlocalmemory/server/api.py +10 -7
- package/src/superlocalmemory/server/bandit_loops.py +4 -2
- package/src/superlocalmemory/server/recall_serializer.py +24 -0
- package/src/superlocalmemory/server/route_mutations.py +84 -0
- package/src/superlocalmemory/server/routes/agents.py +8 -6
- package/src/superlocalmemory/server/routes/brain.py +14 -12
- package/src/superlocalmemory/server/routes/chat.py +29 -12
- package/src/superlocalmemory/server/routes/data_io.py +55 -24
- package/src/superlocalmemory/server/routes/helpers.py +8 -63
- package/src/superlocalmemory/server/routes/ingest.py +53 -36
- package/src/superlocalmemory/server/routes/memories.py +104 -43
- package/src/superlocalmemory/server/routes/mesh.py +31 -0
- package/src/superlocalmemory/server/routes/profiles.py +26 -4
- package/src/superlocalmemory/server/routes/tiers.py +43 -11
- package/src/superlocalmemory/server/routes/timeline.py +5 -1
- package/src/superlocalmemory/server/routes/v3_api.py +76 -21
- package/src/superlocalmemory/server/security_middleware.py +1 -1
- package/src/superlocalmemory/server/unified_daemon.py +680 -293
- package/src/superlocalmemory/server/write_identity.py +147 -0
- package/src/superlocalmemory/storage/access_log.py +4 -3
- package/src/superlocalmemory/storage/database.py +118 -25
- package/src/superlocalmemory/storage/migration_runner.py +84 -1
- package/src/superlocalmemory/storage/migration_v33.py +1 -1
- package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +6 -60
- package/src/superlocalmemory/storage/migrations/M018_ingestion_operations.py +120 -0
- package/src/superlocalmemory/storage/migrations/M019_derivation_lineage.py +54 -0
- package/src/superlocalmemory/storage/migrations/M020_model_state_integrity.py +52 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +5 -0
- package/src/superlocalmemory/storage/models.py +16 -0
- package/src/superlocalmemory/storage/quantized_store.py +20 -3
- package/src/superlocalmemory/storage/v2_migrator.py +5 -3
- package/src/superlocalmemory/ui/favicon.svg +5 -0
- package/src/superlocalmemory/ui/index.html +1 -0
- package/src/superlocalmemory/ui/js/compliance.js +1 -1
- package/src/superlocalmemory/ui/js/core.js +49 -8
- package/src/superlocalmemory/ui/js/dashboard.js +23 -2
- package/src/superlocalmemory/ui/js/feedback.js +1 -1
- package/src/superlocalmemory/ui/js/graph-filters.js +1 -1
- package/src/superlocalmemory/ui/js/graph-ui.js +1 -1
- package/src/superlocalmemory/ui/js/lifecycle.js +1 -1
- package/src/superlocalmemory/ui/js/ng-mesh.js +15 -49
- package/src/superlocalmemory/ui/js/settings.js +4 -2
- package/src/superlocalmemory/vector/lancedb_backend.py +57 -9
- package/bin/slm +0 -59
- package/bin/slm.bat +0 -77
- package/bin/slm.cmd +0 -5
- package/ide/integrations/langchain/README.md +0 -106
- package/ide/integrations/langchain/langchain_superlocalmemory/__init__.py +0 -9
- package/ide/integrations/langchain/langchain_superlocalmemory/chat_message_history.py +0 -201
- package/ide/integrations/langchain/pyproject.toml +0 -38
- package/ide/integrations/langchain/tests/__init__.py +0 -3
- package/ide/integrations/langchain/tests/test_chat_message_history.py +0 -215
- package/ide/integrations/langchain/tests/test_security.py +0 -117
- package/ide/integrations/llamaindex/README.md +0 -81
- package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/__init__.py +0 -9
- package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/base.py +0 -316
- package/ide/integrations/llamaindex/pyproject.toml +0 -43
- package/ide/integrations/llamaindex/tests/__init__.py +0 -3
- package/ide/integrations/llamaindex/tests/test_chat_store.py +0 -294
- package/ide/integrations/llamaindex/tests/test_security.py +0 -241
- 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 -15
- package/plugin-src/scripts/slm-launch.bat +0 -17
- package/plugin-src/settings.json +0 -16
- package/plugin-src/skills/slm-cache/SKILL.md +0 -140
- package/plugin-src/skills/slm-compress/SKILL.md +0 -143
- package/plugin-src/skills/slm-graph/SKILL.md +0 -300
- package/plugin-src/skills/slm-recall/SKILL.md +0 -204
- package/plugin-src/skills/slm-remember/SKILL.md +0 -194
- package/plugin-src/skills/slm-session/SKILL.md +0 -207
- package/plugin-src/skills/slm-status/SKILL.md +0 -149
- package/scripts/__tests__/build-plugin.test.mjs +0 -613
- package/scripts/_savings_math.py +0 -270
- package/scripts/build-dmg.sh +0 -417
- package/scripts/build-plugin.js +0 -742
- package/scripts/build-slm-hook.ps1 +0 -40
- package/scripts/build-slm-hook.sh +0 -45
- package/scripts/build_entry.py +0 -452
- package/scripts/ci/stage5b_gate.sh +0 -50
- package/scripts/dogfood_savings.py +0 -490
- package/scripts/generate-thumbnails.py +0 -218
- package/scripts/install-skills.ps1 +0 -4
- package/scripts/install-skills.sh +0 -5
- package/scripts/install.ps1 +0 -701
- package/scripts/install.sh +0 -1015
- package/scripts/postinstall_binary.js +0 -287
- package/scripts/prepack.js +0 -33
- package/scripts/release_manifest.py +0 -273
- package/scripts/slm-hook.spec +0 -56
- package/scripts/start-dashboard.ps1 +0 -52
- package/scripts/start-dashboard.sh +0 -41
- package/scripts/sync-wiki.ps1 +0 -127
- package/scripts/sync-wiki.sh +0 -82
- package/scripts/test-dmg.sh +0 -161
- package/scripts/test-npm-package.ps1 +0 -252
- package/scripts/test-npm-package.sh +0 -207
- package/scripts/verify-install.ps1 +0 -294
- package/scripts/verify-install.sh +0 -266
- package/scripts/verify-v27.ps1 +0 -301
- package/scripts/verify-v27.sh +0 -233
- package/src/superlocalmemory.egg-info/PKG-INFO +0 -516
- package/src/superlocalmemory.egg-info/SOURCES.txt +0 -529
- package/src/superlocalmemory.egg-info/dependency_links.txt +0 -1
- package/src/superlocalmemory.egg-info/entry_points.txt +0 -2
- package/src/superlocalmemory.egg-info/requires.txt +0 -71
- package/src/superlocalmemory.egg-info/top_level.txt +0 -1
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
|
|
4
|
+
"""Single authority for SuperLocalMemory runtime-state paths.
|
|
5
|
+
|
|
6
|
+
The resolver is intentionally stdlib-only so CLI, daemon, MCP, and hook entry
|
|
7
|
+
points can use it before importing the engine. Environment selection is an
|
|
8
|
+
explicit process contract and always wins over persisted configuration.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
DATA_ROOT_ALIASES = ("SLM_DATA_DIR", "SL_MEMORY_PATH", "SLM_HOME")
|
|
18
|
+
_DURABLE_IDENTITY_NAMES = frozenset(
|
|
19
|
+
{
|
|
20
|
+
".install_token",
|
|
21
|
+
".signer_key",
|
|
22
|
+
"api_key",
|
|
23
|
+
"credentials.json",
|
|
24
|
+
},
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DataRootConflictError(RuntimeError):
|
|
29
|
+
"""Raised when two state-bearing roots make startup ambiguous."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _canonical_path(value: str | Path) -> Path:
|
|
33
|
+
expanded = Path(value).expanduser().resolve(strict=False)
|
|
34
|
+
return Path(os.path.normcase(str(expanded)))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def environment_data_root() -> Path | None:
|
|
38
|
+
"""Return the winning environment root, or ``None`` when none is set."""
|
|
39
|
+
for name in DATA_ROOT_ALIASES:
|
|
40
|
+
value = os.environ.get(name, "").strip()
|
|
41
|
+
if value:
|
|
42
|
+
return _canonical_path(value)
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _legacy_configured_root(default_root: Path) -> Path | None:
|
|
47
|
+
"""Read the bounded legacy ``config.json:base_dir`` relocation hint.
|
|
48
|
+
|
|
49
|
+
Only an absolute path is accepted. Relative paths historically depended on
|
|
50
|
+
the caller's working directory and cannot safely identify a process
|
|
51
|
+
namespace. Corrupt or unreadable configuration never changes the root.
|
|
52
|
+
"""
|
|
53
|
+
config_path = default_root / "config.json"
|
|
54
|
+
try:
|
|
55
|
+
payload = json.loads(config_path.read_text(encoding="utf-8"))
|
|
56
|
+
value = payload.get("base_dir") if isinstance(payload, dict) else None
|
|
57
|
+
if not isinstance(value, str) or not value.strip():
|
|
58
|
+
return None
|
|
59
|
+
candidate = Path(value).expanduser()
|
|
60
|
+
if not candidate.is_absolute():
|
|
61
|
+
return None
|
|
62
|
+
return _canonical_path(candidate)
|
|
63
|
+
except (OSError, ValueError, TypeError):
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def canonical_data_root(
|
|
68
|
+
configured_base_dir: str | Path | None = None,
|
|
69
|
+
*,
|
|
70
|
+
home: str | Path | None = None,
|
|
71
|
+
) -> Path:
|
|
72
|
+
"""Resolve the one runtime-state namespace for the current process.
|
|
73
|
+
|
|
74
|
+
Precedence:
|
|
75
|
+
1. ``SLM_DATA_DIR``
|
|
76
|
+
2. ``SL_MEMORY_PATH`` (legacy input shim)
|
|
77
|
+
3. ``SLM_HOME`` (legacy input shim)
|
|
78
|
+
4. explicit configured base directory
|
|
79
|
+
5. legacy default-root ``config.json:base_dir``
|
|
80
|
+
6. ``~/.superlocalmemory``
|
|
81
|
+
|
|
82
|
+
The function performs no writes and never moves data.
|
|
83
|
+
"""
|
|
84
|
+
selected = environment_data_root()
|
|
85
|
+
if selected is not None:
|
|
86
|
+
return selected
|
|
87
|
+
if configured_base_dir is not None:
|
|
88
|
+
return _canonical_path(configured_base_dir)
|
|
89
|
+
home_path = _canonical_path(home if home is not None else Path.home())
|
|
90
|
+
default_root = _canonical_path(home_path / ".superlocalmemory")
|
|
91
|
+
return _legacy_configured_root(default_root) or default_root
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _durable_markers(root: Path) -> tuple[Path, ...]:
|
|
95
|
+
"""Return bounded evidence that ``root`` contains durable SLM state.
|
|
96
|
+
|
|
97
|
+
The startup guard deliberately ignores ``config.json`` on its own because
|
|
98
|
+
the default-root file is the compatibility locator for legacy custom
|
|
99
|
+
roots. Database sidecars count even if the base file was lost.
|
|
100
|
+
"""
|
|
101
|
+
try:
|
|
102
|
+
children = tuple(root.iterdir())
|
|
103
|
+
except FileNotFoundError:
|
|
104
|
+
return ()
|
|
105
|
+
except OSError as exc:
|
|
106
|
+
raise DataRootConflictError(
|
|
107
|
+
f"SuperLocalMemory cannot inspect possible state root {root}: {exc}. "
|
|
108
|
+
"Startup is blocked until the root is readable or explicitly resolved."
|
|
109
|
+
) from exc
|
|
110
|
+
markers = []
|
|
111
|
+
for path in children:
|
|
112
|
+
name = path.name
|
|
113
|
+
if (
|
|
114
|
+
name in _DURABLE_IDENTITY_NAMES
|
|
115
|
+
or name.endswith((".db", ".db-wal", ".db-shm", ".sqlite", ".sqlite3"))
|
|
116
|
+
):
|
|
117
|
+
markers.append(path)
|
|
118
|
+
return tuple(sorted(markers))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def assert_no_durable_root_conflict(
|
|
122
|
+
*,
|
|
123
|
+
home: str | Path | None = None,
|
|
124
|
+
) -> None:
|
|
125
|
+
"""Refuse ambiguous startup when selected and default roots hold state.
|
|
126
|
+
|
|
127
|
+
This check never writes, copies, or deletes data. A default root that only
|
|
128
|
+
contains the legacy relocation config is safe and does not trigger it.
|
|
129
|
+
"""
|
|
130
|
+
home_path = _canonical_path(home if home is not None else Path.home())
|
|
131
|
+
default_root = _canonical_path(home_path / ".superlocalmemory")
|
|
132
|
+
selected_root = canonical_data_root(home=home_path)
|
|
133
|
+
if selected_root == default_root:
|
|
134
|
+
return
|
|
135
|
+
selected_markers = _durable_markers(selected_root)
|
|
136
|
+
default_markers = _durable_markers(default_root)
|
|
137
|
+
if not selected_markers or not default_markers:
|
|
138
|
+
return
|
|
139
|
+
raise DataRootConflictError(
|
|
140
|
+
"Conflicting SuperLocalMemory state roots detected: "
|
|
141
|
+
f"selected={selected_root} and default={default_root}. "
|
|
142
|
+
"SuperLocalMemory will not merge or move either root automatically. "
|
|
143
|
+
"Stop both namespaces, back them up, and choose an explicit migration "
|
|
144
|
+
"or one SLM_DATA_DIR before starting the daemon."
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def state_path(
|
|
149
|
+
*parts: str | Path,
|
|
150
|
+
configured_base_dir: str | Path | None = None,
|
|
151
|
+
home: str | Path | None = None,
|
|
152
|
+
) -> Path:
|
|
153
|
+
"""Return a canonical child path without permitting root escape."""
|
|
154
|
+
relative = Path()
|
|
155
|
+
for raw_part in parts:
|
|
156
|
+
part = Path(raw_part)
|
|
157
|
+
if part.is_absolute() or ".." in part.parts:
|
|
158
|
+
raise ValueError(f"state path must remain relative: {raw_part!s}")
|
|
159
|
+
relative /= part
|
|
160
|
+
root = canonical_data_root(configured_base_dir, home=home)
|
|
161
|
+
destination = _canonical_path(root / relative)
|
|
162
|
+
try:
|
|
163
|
+
destination.relative_to(root)
|
|
164
|
+
except ValueError as exc:
|
|
165
|
+
raise ValueError("state path escapes the canonical data root") from exc
|
|
166
|
+
return destination
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class DynamicStatePath(os.PathLike[str]):
|
|
170
|
+
"""Backward-compatible path proxy that never freezes the active root."""
|
|
171
|
+
|
|
172
|
+
__slots__ = ("_parts",)
|
|
173
|
+
|
|
174
|
+
def __init__(self, *parts: str | Path) -> None:
|
|
175
|
+
self._parts = parts
|
|
176
|
+
|
|
177
|
+
def _resolve(self) -> Path:
|
|
178
|
+
return state_path(*self._parts)
|
|
179
|
+
|
|
180
|
+
def __fspath__(self) -> str:
|
|
181
|
+
return str(self._resolve())
|
|
182
|
+
|
|
183
|
+
def __truediv__(self, other: str | Path) -> Path:
|
|
184
|
+
return self._resolve() / other
|
|
185
|
+
|
|
186
|
+
def __str__(self) -> str:
|
|
187
|
+
return str(self._resolve())
|
|
188
|
+
|
|
189
|
+
def __repr__(self) -> str:
|
|
190
|
+
return f"DynamicStatePath({self._parts!r})"
|
|
191
|
+
|
|
192
|
+
def __eq__(self, other: object) -> bool:
|
|
193
|
+
try:
|
|
194
|
+
return self._resolve() == Path(other) # type: ignore[arg-type]
|
|
195
|
+
except TypeError:
|
|
196
|
+
return False
|
|
197
|
+
|
|
198
|
+
def __getattr__(self, name: str):
|
|
199
|
+
return getattr(self._resolve(), name)
|
|
@@ -16,6 +16,8 @@ from datetime import datetime, timedelta, timezone
|
|
|
16
16
|
from pathlib import Path
|
|
17
17
|
from typing import Any, Callable, Dict, List, Optional
|
|
18
18
|
|
|
19
|
+
from superlocalmemory.infra.data_root import state_path
|
|
20
|
+
|
|
19
21
|
logger = logging.getLogger("superlocalmemory.events")
|
|
20
22
|
|
|
21
23
|
# Default retention windows (hours)
|
|
@@ -66,7 +68,7 @@ class EventBus:
|
|
|
66
68
|
def get_instance(cls, db_path: Optional[Path] = None) -> "EventBus":
|
|
67
69
|
"""Get or create the singleton EventBus for a database path."""
|
|
68
70
|
if db_path is None:
|
|
69
|
-
db_path =
|
|
71
|
+
db_path = state_path("memory.db")
|
|
70
72
|
|
|
71
73
|
key = str(db_path)
|
|
72
74
|
with cls._instances_lock:
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
|
|
4
|
+
"""Bounded, content-free operational diagnostics stored on this machine.
|
|
5
|
+
|
|
6
|
+
The store accepts only enumerated operations and normalized client families.
|
|
7
|
+
It persists daily counters, latency buckets, coarse error classes, and client
|
|
8
|
+
family transitions. Export is a deliberate local file operation; this module
|
|
9
|
+
has no automatic reporting mechanism.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import math
|
|
16
|
+
import os
|
|
17
|
+
import re
|
|
18
|
+
import sqlite3
|
|
19
|
+
import tempfile
|
|
20
|
+
from collections.abc import Callable, Iterable
|
|
21
|
+
from datetime import UTC, datetime, timedelta
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from superlocalmemory.infra.data_root import state_path
|
|
26
|
+
|
|
27
|
+
SCHEMA_VERSION = 1
|
|
28
|
+
RETENTION_DAYS = 31
|
|
29
|
+
MAX_COUNTER = 2**63 - 1
|
|
30
|
+
|
|
31
|
+
_OPERATIONS = frozenset(
|
|
32
|
+
{
|
|
33
|
+
"activation",
|
|
34
|
+
"remember",
|
|
35
|
+
"recall",
|
|
36
|
+
"observe",
|
|
37
|
+
"session_open",
|
|
38
|
+
"session_close",
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
_CLIENT_FAMILIES = (
|
|
42
|
+
"claude",
|
|
43
|
+
"codex",
|
|
44
|
+
"cursor",
|
|
45
|
+
"windsurf",
|
|
46
|
+
"copilot",
|
|
47
|
+
"vscode",
|
|
48
|
+
"jetbrains",
|
|
49
|
+
"dashboard",
|
|
50
|
+
"hook",
|
|
51
|
+
"mcp",
|
|
52
|
+
"api",
|
|
53
|
+
"cli",
|
|
54
|
+
)
|
|
55
|
+
_LATENCY_BOUNDS_MS = (5, 25, 100, 500, 2_000, 10_000, 60_000)
|
|
56
|
+
_ERROR_CLASSES = frozenset(
|
|
57
|
+
{"timeout", "authorization", "validation", "unavailable", "internal"}
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def normalize_client(value: str | None) -> str:
|
|
62
|
+
"""Collapse an arbitrary actor label to a fixed non-identifying family."""
|
|
63
|
+
tokens = frozenset(re.findall(r"[a-z0-9]+", str(value or "").lower()))
|
|
64
|
+
for family in _CLIENT_FAMILIES:
|
|
65
|
+
if family in tokens:
|
|
66
|
+
return family
|
|
67
|
+
return "other"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def classify_error(error: BaseException | None) -> str | None:
|
|
71
|
+
"""Map an exception type to a fixed class without inspecting its message."""
|
|
72
|
+
if error is None:
|
|
73
|
+
return None
|
|
74
|
+
if isinstance(error, TimeoutError):
|
|
75
|
+
return "timeout"
|
|
76
|
+
if isinstance(error, PermissionError):
|
|
77
|
+
return "authorization"
|
|
78
|
+
if isinstance(error, (TypeError, ValueError)):
|
|
79
|
+
return "validation"
|
|
80
|
+
if isinstance(error, (ConnectionError, FileNotFoundError)):
|
|
81
|
+
return "unavailable"
|
|
82
|
+
return "internal"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _latency_bucket(duration_ms: float | int | None) -> str | None:
|
|
86
|
+
if duration_ms is None:
|
|
87
|
+
return None
|
|
88
|
+
try:
|
|
89
|
+
value = float(duration_ms)
|
|
90
|
+
except (TypeError, ValueError, OverflowError):
|
|
91
|
+
return "invalid"
|
|
92
|
+
if not math.isfinite(value) or value < 0:
|
|
93
|
+
return "invalid"
|
|
94
|
+
for bound in _LATENCY_BOUNDS_MS:
|
|
95
|
+
if value <= bound:
|
|
96
|
+
return f"le_{bound}"
|
|
97
|
+
return "gt_60000"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class LocalDiagnostics:
|
|
101
|
+
"""SQLite-backed bounded aggregate diagnostics.
|
|
102
|
+
|
|
103
|
+
The schema intentionally has no free-text value column. Every persisted
|
|
104
|
+
dimension comes from a finite allow-list, so caller payloads cannot enter
|
|
105
|
+
the database or an export by accident.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
def __init__(
|
|
109
|
+
self,
|
|
110
|
+
db_file: str | Path,
|
|
111
|
+
*,
|
|
112
|
+
clock: Callable[[], datetime] | None = None,
|
|
113
|
+
retention_days: int = RETENTION_DAYS,
|
|
114
|
+
) -> None:
|
|
115
|
+
if retention_days < 1 or retention_days > RETENTION_DAYS:
|
|
116
|
+
raise ValueError(f"retention_days must be between 1 and {RETENTION_DAYS}")
|
|
117
|
+
self._db_file = Path(db_file)
|
|
118
|
+
self._clock = clock or (lambda: datetime.now(UTC))
|
|
119
|
+
self._retention_days = retention_days
|
|
120
|
+
|
|
121
|
+
def _connect(self) -> sqlite3.Connection:
|
|
122
|
+
self._db_file.parent.mkdir(parents=True, exist_ok=True)
|
|
123
|
+
if os.name == "posix":
|
|
124
|
+
os.chmod(self._db_file.parent, 0o700)
|
|
125
|
+
conn = sqlite3.connect(str(self._db_file), timeout=5.0)
|
|
126
|
+
conn.execute(
|
|
127
|
+
"CREATE TABLE IF NOT EXISTS diagnostic_aggregates ("
|
|
128
|
+
"day TEXT NOT NULL, group_name TEXT NOT NULL, "
|
|
129
|
+
"dimension TEXT NOT NULL, bucket TEXT NOT NULL, "
|
|
130
|
+
"value INTEGER NOT NULL CHECK(value >= 0), "
|
|
131
|
+
"PRIMARY KEY(day, group_name, dimension, bucket))"
|
|
132
|
+
)
|
|
133
|
+
conn.commit()
|
|
134
|
+
if os.name == "posix":
|
|
135
|
+
os.chmod(self._db_file, 0o600)
|
|
136
|
+
return conn
|
|
137
|
+
|
|
138
|
+
def _today(self) -> datetime:
|
|
139
|
+
current = self._clock()
|
|
140
|
+
if current.tzinfo is None:
|
|
141
|
+
current = current.replace(tzinfo=UTC)
|
|
142
|
+
return current.astimezone(UTC)
|
|
143
|
+
|
|
144
|
+
@staticmethod
|
|
145
|
+
def _increment(
|
|
146
|
+
conn: sqlite3.Connection,
|
|
147
|
+
day: str,
|
|
148
|
+
group_name: str,
|
|
149
|
+
dimension: str,
|
|
150
|
+
bucket: str,
|
|
151
|
+
) -> None:
|
|
152
|
+
conn.execute(
|
|
153
|
+
"INSERT INTO diagnostic_aggregates "
|
|
154
|
+
"(day, group_name, dimension, bucket, value) VALUES (?,?,?,?,1) "
|
|
155
|
+
"ON CONFLICT(day, group_name, dimension, bucket) DO UPDATE SET "
|
|
156
|
+
"value=MIN(diagnostic_aggregates.value + 1, ?)",
|
|
157
|
+
(day, group_name, dimension, bucket, MAX_COUNTER),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def record(
|
|
161
|
+
self,
|
|
162
|
+
operation: str,
|
|
163
|
+
*,
|
|
164
|
+
client: str | None = None,
|
|
165
|
+
duration_ms: float | int | None = None,
|
|
166
|
+
error: BaseException | None = None,
|
|
167
|
+
source_clients: Iterable[str] = (),
|
|
168
|
+
) -> None:
|
|
169
|
+
"""Record one operation using only fixed aggregate dimensions."""
|
|
170
|
+
if operation not in _OPERATIONS:
|
|
171
|
+
raise ValueError(f"unsupported diagnostic operation: {operation}")
|
|
172
|
+
current = self._today()
|
|
173
|
+
day = current.date().isoformat()
|
|
174
|
+
cutoff = (current.date() - timedelta(days=self._retention_days - 1)).isoformat()
|
|
175
|
+
client_family = normalize_client(client)
|
|
176
|
+
status = "error" if error is not None else "success"
|
|
177
|
+
duration_bucket = _latency_bucket(duration_ms)
|
|
178
|
+
error_class = classify_error(error)
|
|
179
|
+
transitions = {
|
|
180
|
+
f"{normalize_client(source)}_to_{client_family}"
|
|
181
|
+
for source in source_clients
|
|
182
|
+
if normalize_client(source) != client_family
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
conn = self._connect()
|
|
186
|
+
try:
|
|
187
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
188
|
+
conn.execute("DELETE FROM diagnostic_aggregates WHERE day < ?", (cutoff,))
|
|
189
|
+
self._increment(conn, day, "operations", operation, status)
|
|
190
|
+
self._increment(conn, day, "clients", client_family, operation)
|
|
191
|
+
if duration_bucket is not None:
|
|
192
|
+
self._increment(conn, day, "latency_ms", operation, duration_bucket)
|
|
193
|
+
if error_class is not None:
|
|
194
|
+
self._increment(conn, day, "errors", operation, error_class)
|
|
195
|
+
if operation == "activation":
|
|
196
|
+
self._increment(conn, day, "activations", client_family, "count")
|
|
197
|
+
for transition in sorted(transitions):
|
|
198
|
+
self._increment(conn, day, "handoffs", transition, "count")
|
|
199
|
+
conn.commit()
|
|
200
|
+
except BaseException:
|
|
201
|
+
conn.rollback()
|
|
202
|
+
raise
|
|
203
|
+
finally:
|
|
204
|
+
conn.close()
|
|
205
|
+
|
|
206
|
+
def export_payload(self) -> dict[str, Any]:
|
|
207
|
+
"""Return a deterministic, sanitized aggregate document."""
|
|
208
|
+
days: dict[str, dict[str, Any]] = {}
|
|
209
|
+
if self._db_file.is_file():
|
|
210
|
+
conn = self._connect()
|
|
211
|
+
try:
|
|
212
|
+
rows = conn.execute(
|
|
213
|
+
"SELECT day, group_name, dimension, bucket, value "
|
|
214
|
+
"FROM diagnostic_aggregates "
|
|
215
|
+
"ORDER BY day, group_name, dimension, bucket"
|
|
216
|
+
).fetchall()
|
|
217
|
+
finally:
|
|
218
|
+
conn.close()
|
|
219
|
+
for day, group_name, dimension, bucket, value in rows:
|
|
220
|
+
entry = days.setdefault(str(day), {"day": str(day)})
|
|
221
|
+
group = entry.setdefault(str(group_name), {})
|
|
222
|
+
dimension_values = group.setdefault(str(dimension), {})
|
|
223
|
+
dimension_values[str(bucket)] = int(value)
|
|
224
|
+
return {
|
|
225
|
+
"schema_version": SCHEMA_VERSION,
|
|
226
|
+
"retention_days": self._retention_days,
|
|
227
|
+
"privacy": {
|
|
228
|
+
"local_only": True,
|
|
229
|
+
"reporting": "manual_export_only",
|
|
230
|
+
"content_values": False,
|
|
231
|
+
"query_values": False,
|
|
232
|
+
"fact_identifiers": False,
|
|
233
|
+
"filesystem_locations": False,
|
|
234
|
+
"identity_values": False,
|
|
235
|
+
},
|
|
236
|
+
"days": [days[key] for key in sorted(days)],
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
def export_json(self, destination: str | Path) -> dict[str, Any]:
|
|
240
|
+
"""Atomically write a deterministic manual export with mode 0600."""
|
|
241
|
+
payload = self.export_payload()
|
|
242
|
+
output = Path(destination)
|
|
243
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
244
|
+
data = (json.dumps(payload, indent=2, sort_keys=True) + "\n").encode("utf-8")
|
|
245
|
+
temporary_name = ""
|
|
246
|
+
try:
|
|
247
|
+
with tempfile.NamedTemporaryFile(
|
|
248
|
+
mode="wb", dir=output.parent, prefix=f".{output.name}.", delete=False,
|
|
249
|
+
) as temporary:
|
|
250
|
+
temporary_name = temporary.name
|
|
251
|
+
os.chmod(temporary_name, 0o600)
|
|
252
|
+
temporary.write(data)
|
|
253
|
+
temporary.flush()
|
|
254
|
+
os.fsync(temporary.fileno())
|
|
255
|
+
os.replace(temporary_name, output)
|
|
256
|
+
if os.name == "posix":
|
|
257
|
+
os.chmod(output, 0o600)
|
|
258
|
+
finally:
|
|
259
|
+
if temporary_name:
|
|
260
|
+
Path(temporary_name).unlink(missing_ok=True)
|
|
261
|
+
return payload
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def default_diagnostics() -> LocalDiagnostics:
|
|
265
|
+
"""Return the diagnostics store for the active local state root."""
|
|
266
|
+
return LocalDiagnostics(state_path("diagnostics.db"))
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def record_operation(
|
|
270
|
+
operation: str,
|
|
271
|
+
*,
|
|
272
|
+
client: str | None = None,
|
|
273
|
+
duration_ms: float | int | None = None,
|
|
274
|
+
error: BaseException | None = None,
|
|
275
|
+
source_clients: Iterable[str] = (),
|
|
276
|
+
) -> None:
|
|
277
|
+
"""Best-effort hot-path adapter; diagnostics never affect correctness."""
|
|
278
|
+
try:
|
|
279
|
+
default_diagnostics().record(
|
|
280
|
+
operation,
|
|
281
|
+
client=client,
|
|
282
|
+
duration_ms=duration_ms,
|
|
283
|
+
error=error,
|
|
284
|
+
source_clients=source_clients,
|
|
285
|
+
)
|
|
286
|
+
except Exception:
|
|
287
|
+
return
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def record_recall(db: Any, response: Any, *, client: str | None) -> None:
|
|
291
|
+
"""Record recall and aggregate cross-client provenance transitions."""
|
|
292
|
+
source_clients: list[str] = []
|
|
293
|
+
fact_ids = [
|
|
294
|
+
str(result.fact.fact_id)
|
|
295
|
+
for result in getattr(response, "results", ())
|
|
296
|
+
if getattr(getattr(result, "fact", None), "fact_id", None)
|
|
297
|
+
]
|
|
298
|
+
profile_id = ""
|
|
299
|
+
if getattr(response, "results", None):
|
|
300
|
+
profile_id = str(getattr(response.results[0].fact, "profile_id", ""))
|
|
301
|
+
if fact_ids and profile_id:
|
|
302
|
+
try:
|
|
303
|
+
marks = ",".join("?" for _ in fact_ids)
|
|
304
|
+
rows = db.execute(
|
|
305
|
+
f"SELECT DISTINCT created_by FROM provenance "
|
|
306
|
+
f"WHERE profile_id=? AND fact_id IN ({marks})",
|
|
307
|
+
(profile_id, *fact_ids),
|
|
308
|
+
)
|
|
309
|
+
source_clients = [str(dict(row).get("created_by", "")) for row in rows]
|
|
310
|
+
except Exception:
|
|
311
|
+
source_clients = []
|
|
312
|
+
record_operation(
|
|
313
|
+
"recall",
|
|
314
|
+
client=client,
|
|
315
|
+
duration_ms=getattr(response, "retrieval_time_ms", None),
|
|
316
|
+
source_clients=source_clients,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
__all__ = [
|
|
321
|
+
"LocalDiagnostics",
|
|
322
|
+
"classify_error",
|
|
323
|
+
"default_diagnostics",
|
|
324
|
+
"normalize_client",
|
|
325
|
+
"record_operation",
|
|
326
|
+
"record_recall",
|
|
327
|
+
]
|
|
@@ -79,6 +79,29 @@ class SlmProcessInfo:
|
|
|
79
79
|
age_hours: float
|
|
80
80
|
|
|
81
81
|
|
|
82
|
+
def is_mcp_server_process(process: SlmProcessInfo) -> bool:
|
|
83
|
+
"""Return whether *process* is an MCP stdio server, not an SLM daemon.
|
|
84
|
+
|
|
85
|
+
The startup reaper is deliberately aggressive (zero age threshold), but
|
|
86
|
+
only an orphaned MCP process is safe to reap from ``slm mcp``. A unified
|
|
87
|
+
daemon is intentionally detached from the shell that launched it, so on
|
|
88
|
+
some hosts it has PPID 1 and looks like an orphan. Reaping that daemon at
|
|
89
|
+
MCP startup drops in-flight CLI and dashboard requests.
|
|
90
|
+
|
|
91
|
+
Keep this predicate narrow and based on the command line recorded by the
|
|
92
|
+
OS: every supported MCP entry point includes the standalone ``mcp``
|
|
93
|
+
argument or imports the MCP server module. The daemon's command does
|
|
94
|
+
neither.
|
|
95
|
+
"""
|
|
96
|
+
command = process.command.lower()
|
|
97
|
+
return (
|
|
98
|
+
"/slm mcp" in command
|
|
99
|
+
or " slm mcp" in command
|
|
100
|
+
or "superlocalmemory.cli.main mcp" in command
|
|
101
|
+
or "superlocalmemory.mcp.server" in command
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
82
105
|
# ---------------------------------------------------------------------------
|
|
83
106
|
# Windows no-op stubs (AUDIT FIX H0-HIGH-02)
|
|
84
107
|
# ---------------------------------------------------------------------------
|
|
@@ -8,7 +8,7 @@ All adapters run as separate subprocesses managed via PID files.
|
|
|
8
8
|
Config stored in ~/.superlocalmemory/adapters.json.
|
|
9
9
|
|
|
10
10
|
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
11
|
-
License:
|
|
11
|
+
License: AGPL-3.0-or-later
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
14
|
from __future__ import annotations
|
|
@@ -20,10 +20,12 @@ import subprocess
|
|
|
20
20
|
import sys
|
|
21
21
|
from pathlib import Path
|
|
22
22
|
|
|
23
|
+
from superlocalmemory.infra.data_root import canonical_data_root, state_path
|
|
24
|
+
|
|
23
25
|
logger = logging.getLogger("superlocalmemory.ingestion.manager")
|
|
24
26
|
|
|
25
|
-
_SLM_HOME =
|
|
26
|
-
_ADAPTERS_CONFIG =
|
|
27
|
+
_SLM_HOME = None # test-only compatibility override
|
|
28
|
+
_ADAPTERS_CONFIG = None # test-only compatibility override
|
|
27
29
|
_VALID_ADAPTERS = ("gmail", "calendar", "transcript")
|
|
28
30
|
|
|
29
31
|
# Module paths for each adapter
|
|
@@ -34,19 +36,35 @@ _ADAPTER_MODULES = {
|
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
|
|
39
|
+
def _slm_home() -> Path:
|
|
40
|
+
return Path(_SLM_HOME) if _SLM_HOME is not None else canonical_data_root()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _adapters_config_path() -> Path:
|
|
44
|
+
if _ADAPTERS_CONFIG is not None:
|
|
45
|
+
return Path(_ADAPTERS_CONFIG)
|
|
46
|
+
return state_path("adapters.json")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _adapter_log_dir() -> Path:
|
|
50
|
+
return _slm_home() / "logs"
|
|
51
|
+
|
|
52
|
+
|
|
37
53
|
def _load_config() -> dict:
|
|
38
|
-
|
|
39
|
-
|
|
54
|
+
config_path = _adapters_config_path()
|
|
55
|
+
if config_path.exists():
|
|
56
|
+
return json.loads(config_path.read_text())
|
|
40
57
|
return {name: {"enabled": False} for name in _VALID_ADAPTERS}
|
|
41
58
|
|
|
42
59
|
|
|
43
60
|
def _save_config(config: dict) -> None:
|
|
44
|
-
|
|
45
|
-
|
|
61
|
+
config_path = _adapters_config_path()
|
|
62
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
config_path.write_text(json.dumps(config, indent=2))
|
|
46
64
|
|
|
47
65
|
|
|
48
66
|
def _pid_file(name: str) -> Path:
|
|
49
|
-
return
|
|
67
|
+
return _slm_home() / f"adapter-{name}.pid"
|
|
50
68
|
|
|
51
69
|
|
|
52
70
|
def _is_running(name: str) -> tuple[bool, int | None]:
|
|
@@ -128,7 +146,7 @@ def start_adapter(name: str) -> dict:
|
|
|
128
146
|
return {"ok": False, "error": f"No module for {name}"}
|
|
129
147
|
|
|
130
148
|
cmd = [sys.executable, "-m", module]
|
|
131
|
-
log_dir =
|
|
149
|
+
log_dir = _adapter_log_dir()
|
|
132
150
|
log_dir.mkdir(parents=True, exist_ok=True)
|
|
133
151
|
log_path = log_dir / f"adapter-{name}.log"
|
|
134
152
|
|