superlocalmemory 3.6.22 → 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 +275 -72
- package/bin/slm-npm +43 -89
- package/docs/pi-dev-integration.md +43 -0
- 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 +19 -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 +490 -195
- 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 +23 -22
- 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 +126 -39
- 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 +241 -55
- 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 +13 -8
- 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 +14 -13
- 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 +29 -4
- 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/ui.py +6 -3
- 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 -513
- 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,248 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
|
|
4
|
+
"""Namespace-aware identity for the local SuperLocalMemory daemon.
|
|
5
|
+
|
|
6
|
+
PID files and listening ports are liveness hints, not ownership credentials.
|
|
7
|
+
This module binds a daemon process to one canonical SLM data root and one
|
|
8
|
+
random process instance through a private, atomically written descriptor.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import getpass
|
|
14
|
+
import hashlib
|
|
15
|
+
import hmac
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import secrets
|
|
19
|
+
import time
|
|
20
|
+
import uuid
|
|
21
|
+
from dataclasses import asdict, dataclass
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any, Mapping
|
|
24
|
+
|
|
25
|
+
from superlocalmemory.infra.data_root import canonical_data_root
|
|
26
|
+
|
|
27
|
+
DAEMON_DESCRIPTOR_SCHEMA = 1
|
|
28
|
+
DAEMON_PROTOCOL = 1
|
|
29
|
+
DAEMON_SERVICE = "superlocalmemory-daemon"
|
|
30
|
+
_NAMESPACE_DOMAIN = b"superlocalmemory-daemon-namespace-v1\0"
|
|
31
|
+
_CAPABILITY_DOMAIN = b"superlocalmemory-daemon-capability-v1\0"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _canonical_path(value: str | Path) -> Path:
|
|
35
|
+
expanded = Path(value).expanduser().resolve(strict=False)
|
|
36
|
+
return Path(os.path.normcase(str(expanded)))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def namespace_id_for(data_root: str | Path) -> str:
|
|
40
|
+
"""Return a stable, non-reversible identifier for a canonical data root."""
|
|
41
|
+
canonical = _canonical_path(data_root)
|
|
42
|
+
normalized = os.path.normcase(str(canonical)).encode("utf-8")
|
|
43
|
+
return hashlib.sha256(_NAMESPACE_DOMAIN + normalized).hexdigest()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def capability_fingerprint(capability: str) -> str:
|
|
47
|
+
"""Hash the private local capability before exposing it through health."""
|
|
48
|
+
return hashlib.sha256(_CAPABILITY_DOMAIN + capability.encode("utf-8")).hexdigest()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def owner_id() -> str:
|
|
52
|
+
"""Cross-platform local owner identifier used for accidental isolation."""
|
|
53
|
+
getuid = getattr(os, "getuid", None)
|
|
54
|
+
if getuid is not None:
|
|
55
|
+
return f"uid:{getuid()}"
|
|
56
|
+
return f"user:{getpass.getuser()}"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def process_create_time_for(pid: int) -> float:
|
|
60
|
+
"""Return OS process creation time, falling back only when unavailable."""
|
|
61
|
+
try:
|
|
62
|
+
import psutil
|
|
63
|
+
|
|
64
|
+
return float(psutil.Process(pid).create_time())
|
|
65
|
+
except Exception:
|
|
66
|
+
return time.time()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class DaemonDescriptor:
|
|
71
|
+
schema: int
|
|
72
|
+
service: str
|
|
73
|
+
daemon_protocol: int
|
|
74
|
+
namespace_id: str
|
|
75
|
+
instance_id: str
|
|
76
|
+
capability: str
|
|
77
|
+
capability_fingerprint: str
|
|
78
|
+
owner_id: str
|
|
79
|
+
data_root: str
|
|
80
|
+
pid: int
|
|
81
|
+
process_create_time: float
|
|
82
|
+
port: int
|
|
83
|
+
state: str
|
|
84
|
+
version: str
|
|
85
|
+
started_at: float
|
|
86
|
+
|
|
87
|
+
def public_health_fields(self) -> dict[str, Any]:
|
|
88
|
+
"""Identity fields safe to expose on the loopback health endpoint."""
|
|
89
|
+
return {
|
|
90
|
+
"service": self.service,
|
|
91
|
+
"daemon_protocol": self.daemon_protocol,
|
|
92
|
+
"namespace_id": self.namespace_id,
|
|
93
|
+
"instance_id": self.instance_id,
|
|
94
|
+
"capability_fingerprint": self.capability_fingerprint,
|
|
95
|
+
"owner_id": self.owner_id,
|
|
96
|
+
"pid": self.pid,
|
|
97
|
+
"port": self.port,
|
|
98
|
+
"state": self.state,
|
|
99
|
+
"version": self.version,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def build_descriptor(
|
|
104
|
+
*,
|
|
105
|
+
data_root: str | Path | None = None,
|
|
106
|
+
port: int,
|
|
107
|
+
version: str,
|
|
108
|
+
pid: int | None = None,
|
|
109
|
+
process_create_time: float | None = None,
|
|
110
|
+
instance_id: str | None = None,
|
|
111
|
+
capability: str | None = None,
|
|
112
|
+
state: str = "starting",
|
|
113
|
+
started_at: float | None = None,
|
|
114
|
+
) -> DaemonDescriptor:
|
|
115
|
+
"""Build one daemon-process descriptor for the selected namespace."""
|
|
116
|
+
root = _canonical_path(data_root) if data_root is not None else canonical_data_root()
|
|
117
|
+
actual_pid = int(pid if pid is not None else os.getpid())
|
|
118
|
+
actual_capability = capability or secrets.token_urlsafe(32)
|
|
119
|
+
return DaemonDescriptor(
|
|
120
|
+
schema=DAEMON_DESCRIPTOR_SCHEMA,
|
|
121
|
+
service=DAEMON_SERVICE,
|
|
122
|
+
daemon_protocol=DAEMON_PROTOCOL,
|
|
123
|
+
namespace_id=namespace_id_for(root),
|
|
124
|
+
instance_id=instance_id or str(uuid.uuid4()),
|
|
125
|
+
capability=actual_capability,
|
|
126
|
+
capability_fingerprint=capability_fingerprint(actual_capability),
|
|
127
|
+
owner_id=owner_id(),
|
|
128
|
+
data_root=str(root),
|
|
129
|
+
pid=actual_pid,
|
|
130
|
+
process_create_time=float(
|
|
131
|
+
process_create_time
|
|
132
|
+
if process_create_time is not None
|
|
133
|
+
else process_create_time_for(actual_pid)
|
|
134
|
+
),
|
|
135
|
+
port=int(port),
|
|
136
|
+
state=state,
|
|
137
|
+
version=version,
|
|
138
|
+
started_at=float(started_at if started_at is not None else time.time()),
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def descriptor_path(data_root: str | Path | None = None) -> Path:
|
|
143
|
+
root = _canonical_path(data_root) if data_root is not None else canonical_data_root()
|
|
144
|
+
return root / "daemon.json"
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def write_descriptor(
|
|
148
|
+
descriptor: DaemonDescriptor,
|
|
149
|
+
*,
|
|
150
|
+
data_root: str | Path | None = None,
|
|
151
|
+
) -> Path:
|
|
152
|
+
"""Atomically publish a mode-0600 descriptor in its owned namespace."""
|
|
153
|
+
root = _canonical_path(data_root or descriptor.data_root)
|
|
154
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
155
|
+
destination = root / "daemon.json"
|
|
156
|
+
temporary = root / f".daemon.{os.getpid()}.{secrets.token_hex(8)}.tmp"
|
|
157
|
+
payload = json.dumps(asdict(descriptor), sort_keys=True, separators=(",", ":"))
|
|
158
|
+
fd = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
159
|
+
try:
|
|
160
|
+
with os.fdopen(fd, "w", encoding="utf-8") as stream:
|
|
161
|
+
stream.write(payload)
|
|
162
|
+
stream.flush()
|
|
163
|
+
os.fsync(stream.fileno())
|
|
164
|
+
os.replace(temporary, destination)
|
|
165
|
+
try:
|
|
166
|
+
os.chmod(destination, 0o600)
|
|
167
|
+
except OSError:
|
|
168
|
+
pass
|
|
169
|
+
finally:
|
|
170
|
+
temporary.unlink(missing_ok=True)
|
|
171
|
+
return destination
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def read_descriptor(
|
|
175
|
+
*, data_root: str | Path | None = None,
|
|
176
|
+
) -> DaemonDescriptor | None:
|
|
177
|
+
"""Read and validate the local descriptor; malformed state fails closed."""
|
|
178
|
+
path = descriptor_path(data_root)
|
|
179
|
+
try:
|
|
180
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
181
|
+
descriptor = DaemonDescriptor(**raw)
|
|
182
|
+
except (OSError, ValueError, TypeError, json.JSONDecodeError):
|
|
183
|
+
return None
|
|
184
|
+
if descriptor.schema != DAEMON_DESCRIPTOR_SCHEMA:
|
|
185
|
+
return None
|
|
186
|
+
if descriptor.service != DAEMON_SERVICE:
|
|
187
|
+
return None
|
|
188
|
+
if descriptor.daemon_protocol != DAEMON_PROTOCOL:
|
|
189
|
+
return None
|
|
190
|
+
expected_root = (
|
|
191
|
+
_canonical_path(data_root) if data_root is not None else canonical_data_root()
|
|
192
|
+
)
|
|
193
|
+
if Path(descriptor.data_root).resolve(strict=False) != expected_root:
|
|
194
|
+
return None
|
|
195
|
+
if descriptor.namespace_id != namespace_id_for(expected_root):
|
|
196
|
+
return None
|
|
197
|
+
if descriptor.capability_fingerprint != capability_fingerprint(
|
|
198
|
+
descriptor.capability
|
|
199
|
+
):
|
|
200
|
+
return None
|
|
201
|
+
if descriptor.owner_id != owner_id():
|
|
202
|
+
return None
|
|
203
|
+
if not (1 <= descriptor.port <= 65535) or descriptor.pid <= 0:
|
|
204
|
+
return None
|
|
205
|
+
return descriptor
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def descriptor_matches_health(
|
|
209
|
+
descriptor: DaemonDescriptor,
|
|
210
|
+
health: Mapping[str, Any],
|
|
211
|
+
) -> bool:
|
|
212
|
+
"""Require exact owned-daemon identity before a client attaches."""
|
|
213
|
+
expected = descriptor.public_health_fields()
|
|
214
|
+
identity_keys = (
|
|
215
|
+
"service",
|
|
216
|
+
"daemon_protocol",
|
|
217
|
+
"namespace_id",
|
|
218
|
+
"instance_id",
|
|
219
|
+
"capability_fingerprint",
|
|
220
|
+
"owner_id",
|
|
221
|
+
"version",
|
|
222
|
+
"pid",
|
|
223
|
+
"port",
|
|
224
|
+
)
|
|
225
|
+
for key in identity_keys:
|
|
226
|
+
left = str(expected[key])
|
|
227
|
+
right = str(health.get(key, ""))
|
|
228
|
+
if not hmac.compare_digest(left, right):
|
|
229
|
+
return False
|
|
230
|
+
return True
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def clear_descriptor(
|
|
234
|
+
instance_id: str,
|
|
235
|
+
*,
|
|
236
|
+
data_root: str | Path | None = None,
|
|
237
|
+
) -> bool:
|
|
238
|
+
"""Delete the descriptor only when the caller still owns that instance."""
|
|
239
|
+
descriptor = read_descriptor(data_root=data_root)
|
|
240
|
+
if descriptor is None or not hmac.compare_digest(
|
|
241
|
+
descriptor.instance_id, instance_id
|
|
242
|
+
):
|
|
243
|
+
return False
|
|
244
|
+
try:
|
|
245
|
+
descriptor_path(data_root).unlink()
|
|
246
|
+
return True
|
|
247
|
+
except OSError:
|
|
248
|
+
return False
|
|
@@ -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:
|