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
|
@@ -13,7 +13,6 @@ import json as _json
|
|
|
13
13
|
import os
|
|
14
14
|
from argparse import Namespace
|
|
15
15
|
from pathlib import Path
|
|
16
|
-
from typing import Callable
|
|
17
16
|
|
|
18
17
|
from superlocalmemory.hooks.antigravity_adapter import AntigravityAdapter
|
|
19
18
|
from superlocalmemory.hooks.copilot_adapter import CopilotAdapter
|
|
@@ -53,9 +52,11 @@ def _get_recall_fn() -> RecallFn:
|
|
|
53
52
|
|
|
54
53
|
|
|
55
54
|
def _default_sync_log_db() -> Path:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
explicit = os.environ.get("SLM_MEMORY_DB", "").strip()
|
|
56
|
+
if explicit:
|
|
57
|
+
return Path(explicit)
|
|
58
|
+
from superlocalmemory.infra.data_root import state_path
|
|
59
|
+
return state_path("memory.db")
|
|
59
60
|
|
|
60
61
|
|
|
61
62
|
def build_default_adapters(
|
|
@@ -24,13 +24,23 @@ from __future__ import annotations
|
|
|
24
24
|
import json
|
|
25
25
|
import logging
|
|
26
26
|
import os
|
|
27
|
-
import
|
|
27
|
+
import socket
|
|
28
28
|
import sys
|
|
29
29
|
import time
|
|
30
|
-
from
|
|
31
|
-
|
|
32
|
-
import
|
|
33
|
-
|
|
30
|
+
from dataclasses import replace
|
|
31
|
+
|
|
32
|
+
from superlocalmemory.infra.daemon_identity import (
|
|
33
|
+
build_descriptor,
|
|
34
|
+
descriptor_matches_health,
|
|
35
|
+
descriptor_path,
|
|
36
|
+
process_create_time_for,
|
|
37
|
+
read_descriptor,
|
|
38
|
+
write_descriptor,
|
|
39
|
+
)
|
|
40
|
+
from superlocalmemory.infra.data_root import (
|
|
41
|
+
assert_no_durable_root_conflict,
|
|
42
|
+
state_path,
|
|
43
|
+
)
|
|
34
44
|
|
|
35
45
|
logger = logging.getLogger(__name__)
|
|
36
46
|
|
|
@@ -40,8 +50,8 @@ except ValueError:
|
|
|
40
50
|
_DEFAULT_PORT = 8765
|
|
41
51
|
_LEGACY_PORT = 8767 # backward-compat redirect
|
|
42
52
|
_DEFAULT_IDLE_TIMEOUT = 0 # v3.4.3: 24/7 default (was 1800)
|
|
43
|
-
_PID_FILE =
|
|
44
|
-
_PORT_FILE =
|
|
53
|
+
_PID_FILE = None # test-only override; runtime resolution stays dynamic
|
|
54
|
+
_PORT_FILE = None # test-only override; runtime resolution stays dynamic
|
|
45
55
|
|
|
46
56
|
|
|
47
57
|
# ---------------------------------------------------------------------------
|
|
@@ -61,83 +71,222 @@ def _is_pid_alive(pid: int) -> bool:
|
|
|
61
71
|
return False
|
|
62
72
|
|
|
63
73
|
|
|
64
|
-
def
|
|
65
|
-
"""
|
|
74
|
+
def _descriptor_process_is_alive(descriptor) -> bool:
|
|
75
|
+
"""Reject stale descriptors when a PID has been reused by another process."""
|
|
76
|
+
if not _is_pid_alive(descriptor.pid):
|
|
77
|
+
return False
|
|
78
|
+
try:
|
|
79
|
+
import psutil
|
|
80
|
+
|
|
81
|
+
process = psutil.Process(descriptor.pid)
|
|
82
|
+
# A terminated daemon can remain in the process table briefly as a
|
|
83
|
+
# zombie while its parent reaps it. PID existence is therefore not
|
|
84
|
+
# liveness and must not block a namespace-owned restart.
|
|
85
|
+
if not process.is_running() or process.status() == psutil.STATUS_ZOMBIE:
|
|
86
|
+
return False
|
|
87
|
+
actual = float(process.create_time())
|
|
88
|
+
except ImportError:
|
|
89
|
+
return True
|
|
90
|
+
except Exception:
|
|
91
|
+
return False
|
|
92
|
+
return abs(actual - float(descriptor.process_create_time)) <= 1.0
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _is_port_available(port: int) -> bool:
|
|
96
|
+
"""Return whether the daemon port can be exclusively bound right now."""
|
|
97
|
+
try:
|
|
98
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as candidate:
|
|
99
|
+
# The server binds with address reuse. Mirror that contract here:
|
|
100
|
+
# a recently closed listener may leave TCP connections in
|
|
101
|
+
# TIME_WAIT, which must not be mistaken for an active owner and
|
|
102
|
+
# block a safe restart for the full TCP timeout.
|
|
103
|
+
candidate.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
104
|
+
candidate.bind(("127.0.0.1", port))
|
|
105
|
+
return True
|
|
106
|
+
except OSError:
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _has_tcp_listener(port: int) -> bool:
|
|
111
|
+
"""Return whether a process is actively accepting on the daemon port."""
|
|
112
|
+
try:
|
|
113
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as candidate:
|
|
114
|
+
candidate.settimeout(0.5)
|
|
115
|
+
return candidate.connect_ex(("127.0.0.1", port)) == 0
|
|
116
|
+
except OSError:
|
|
117
|
+
return False
|
|
118
|
+
|
|
66
119
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
warming up (Ollama processing, model download, embedding init).
|
|
120
|
+
def wait_for_owned_daemon_shutdown(descriptor, timeout: float = 25.0) -> bool:
|
|
121
|
+
"""Wait for the stopped instance *and* its TCP listener to be gone.
|
|
70
122
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
123
|
+
Restart must never spawn a replacement just because the descriptor was
|
|
124
|
+
removed: a graceful shutdown can remove it before Uvicorn releases the
|
|
125
|
+
port. The 25-second budget covers Uvicorn's 10-second graceful drain plus
|
|
126
|
+
SLM worker cleanup. A descriptor carries process creation time, so PID
|
|
127
|
+
reuse cannot make this wait target an unrelated process.
|
|
75
128
|
"""
|
|
76
|
-
if
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
_PID_FILE.unlink(missing_ok=True)
|
|
86
|
-
_PORT_FILE.unlink(missing_ok=True)
|
|
87
|
-
except (ValueError, OSError):
|
|
88
|
-
_PID_FILE.unlink(missing_ok=True)
|
|
89
|
-
|
|
90
|
-
# No PID file — maybe daemon was started by MCP/hook without PID file.
|
|
91
|
-
# Try health check on known ports as last resort.
|
|
92
|
-
for try_port in (_DEFAULT_PORT, _LEGACY_PORT):
|
|
93
|
-
try:
|
|
94
|
-
import urllib.request
|
|
95
|
-
resp = urllib.request.urlopen(
|
|
96
|
-
f"http://127.0.0.1:{try_port}/health", timeout=2,
|
|
97
|
-
)
|
|
98
|
-
if resp.status == 200:
|
|
99
|
-
# Daemon running without PID file — write one for future checks
|
|
100
|
-
try:
|
|
101
|
-
import json as _json
|
|
102
|
-
data = _json.loads(resp.read().decode())
|
|
103
|
-
pid = data.get("pid")
|
|
104
|
-
if pid:
|
|
105
|
-
_PID_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
106
|
-
_PID_FILE.write_text(str(pid))
|
|
107
|
-
_PORT_FILE.write_text(str(try_port))
|
|
108
|
-
except Exception:
|
|
109
|
-
pass
|
|
110
|
-
return True
|
|
111
|
-
except Exception:
|
|
112
|
-
continue
|
|
129
|
+
port = descriptor.port if descriptor is not None else _DEFAULT_PORT
|
|
130
|
+
deadline = time.monotonic() + timeout
|
|
131
|
+
while time.monotonic() < deadline:
|
|
132
|
+
process_alive = (
|
|
133
|
+
descriptor is not None and _descriptor_process_is_alive(descriptor)
|
|
134
|
+
)
|
|
135
|
+
if not process_alive and _is_port_available(port):
|
|
136
|
+
return True
|
|
137
|
+
time.sleep(0.1)
|
|
113
138
|
return False
|
|
114
139
|
|
|
115
140
|
|
|
141
|
+
def is_daemon_running() -> bool:
|
|
142
|
+
"""Return True only for a daemon owned by this canonical data namespace.
|
|
143
|
+
|
|
144
|
+
A PID or an HTTP 200 proves liveness, not ownership. V3.7 requires the
|
|
145
|
+
private local descriptor and the health endpoint to agree on namespace,
|
|
146
|
+
process instance, capability fingerprint, owner, PID, protocol, and port.
|
|
147
|
+
"""
|
|
148
|
+
local_descriptor_path = descriptor_path()
|
|
149
|
+
descriptor = read_descriptor()
|
|
150
|
+
if descriptor is not None:
|
|
151
|
+
if not _descriptor_process_is_alive(descriptor):
|
|
152
|
+
return False
|
|
153
|
+
if descriptor.state == "starting":
|
|
154
|
+
return True
|
|
155
|
+
health = _fetch_health(descriptor.port)
|
|
156
|
+
return health is not None and descriptor_matches_health(descriptor, health)
|
|
157
|
+
|
|
158
|
+
# A malformed or foreign descriptor must fail closed; never fall through
|
|
159
|
+
# to legacy PID/port adoption in the same namespace.
|
|
160
|
+
if local_descriptor_path.exists():
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
legacy = _verified_legacy_health()
|
|
164
|
+
return legacy is not None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _fetch_health(port: int) -> dict | None:
|
|
168
|
+
"""Fetch loopback health without following cross-namespace discovery."""
|
|
169
|
+
try:
|
|
170
|
+
import urllib.request
|
|
171
|
+
|
|
172
|
+
expected_url = f"http://127.0.0.1:{port}/health"
|
|
173
|
+
response = urllib.request.urlopen(
|
|
174
|
+
expected_url, timeout=2,
|
|
175
|
+
)
|
|
176
|
+
if response.status != 200:
|
|
177
|
+
return None
|
|
178
|
+
geturl = getattr(response, "geturl", None)
|
|
179
|
+
final_url = geturl() if callable(geturl) else None
|
|
180
|
+
if final_url is not None and final_url != expected_url:
|
|
181
|
+
return None
|
|
182
|
+
payload = json.loads(response.read().decode())
|
|
183
|
+
return payload if isinstance(payload, dict) else None
|
|
184
|
+
except Exception:
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _is_verified_legacy_process(pid: int) -> bool:
|
|
189
|
+
"""One-release bridge for a same-root V3.6 unified-daemon process."""
|
|
190
|
+
if not _is_pid_alive(pid):
|
|
191
|
+
return False
|
|
192
|
+
try:
|
|
193
|
+
import psutil
|
|
194
|
+
|
|
195
|
+
process = psutil.Process(pid)
|
|
196
|
+
command = " ".join(process.cmdline())
|
|
197
|
+
return "superlocalmemory.server.unified_daemon" in command
|
|
198
|
+
except Exception:
|
|
199
|
+
return False
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _verified_legacy_health() -> dict | None:
|
|
203
|
+
"""Accept legacy health only with a verified same-root daemon PID file."""
|
|
204
|
+
pid_file = descriptor_path().with_name("daemon.pid")
|
|
205
|
+
port_file = descriptor_path().with_name("daemon.port")
|
|
206
|
+
try:
|
|
207
|
+
pid = int(pid_file.read_text().strip())
|
|
208
|
+
port = int(port_file.read_text().strip()) if port_file.exists() else _DEFAULT_PORT
|
|
209
|
+
except (OSError, ValueError):
|
|
210
|
+
return None
|
|
211
|
+
if not _is_verified_legacy_process(pid):
|
|
212
|
+
return None
|
|
213
|
+
health = _fetch_health(port)
|
|
214
|
+
if health is None or int(health.get("pid", -1)) != pid:
|
|
215
|
+
return None
|
|
216
|
+
# Identity-bearing health without a descriptor is not legacy and cannot
|
|
217
|
+
# be adopted. It belongs to another namespace or stale state.
|
|
218
|
+
if health.get("daemon_protocol") is not None:
|
|
219
|
+
return None
|
|
220
|
+
return {**health, "_legacy_port": port}
|
|
221
|
+
|
|
222
|
+
|
|
116
223
|
def _get_port() -> int:
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
224
|
+
descriptor = read_descriptor()
|
|
225
|
+
if descriptor is not None:
|
|
226
|
+
return descriptor.port
|
|
227
|
+
if descriptor_path().exists():
|
|
228
|
+
return _DEFAULT_PORT
|
|
229
|
+
legacy = _verified_legacy_health()
|
|
230
|
+
if legacy is not None:
|
|
231
|
+
return int(legacy["_legacy_port"])
|
|
122
232
|
return _DEFAULT_PORT
|
|
123
233
|
|
|
124
234
|
|
|
125
|
-
def daemon_request(
|
|
126
|
-
|
|
127
|
-
|
|
235
|
+
def daemon_request(
|
|
236
|
+
method: str,
|
|
237
|
+
path: str,
|
|
238
|
+
body: dict | None = None,
|
|
239
|
+
*,
|
|
240
|
+
timeout_seconds: float = 30.0,
|
|
241
|
+
) -> dict | None:
|
|
242
|
+
"""Send a request only after validating the owned daemon identity."""
|
|
243
|
+
descriptor = read_descriptor()
|
|
244
|
+
capability: str | None = None
|
|
245
|
+
if descriptor is not None:
|
|
246
|
+
health = _fetch_health(descriptor.port)
|
|
247
|
+
if health is None or not descriptor_matches_health(descriptor, health):
|
|
248
|
+
return None
|
|
249
|
+
if method.upper() == "GET" and path == "/health":
|
|
250
|
+
return health
|
|
251
|
+
port = descriptor.port
|
|
252
|
+
capability = descriptor.capability
|
|
253
|
+
elif descriptor_path().exists():
|
|
254
|
+
return None
|
|
255
|
+
else:
|
|
256
|
+
legacy = _verified_legacy_health()
|
|
257
|
+
if legacy is None:
|
|
258
|
+
return None
|
|
259
|
+
if method.upper() == "GET" and path == "/health":
|
|
260
|
+
return {key: value for key, value in legacy.items() if key != "_legacy_port"}
|
|
261
|
+
port = int(legacy["_legacy_port"])
|
|
128
262
|
try:
|
|
129
263
|
import urllib.request
|
|
130
264
|
url = f"http://127.0.0.1:{port}{path}"
|
|
131
265
|
data = json.dumps(body).encode() if body else None
|
|
132
266
|
headers = {"Content-Type": "application/json"} if data else {}
|
|
267
|
+
if capability is not None:
|
|
268
|
+
headers["X-SLM-Daemon-Capability"] = capability
|
|
269
|
+
headers["X-SLM-Target-Instance"] = descriptor.instance_id
|
|
133
270
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
134
|
-
resp = urllib.request.urlopen(req, timeout=
|
|
271
|
+
resp = urllib.request.urlopen(req, timeout=timeout_seconds)
|
|
135
272
|
return json.loads(resp.read().decode())
|
|
136
273
|
except Exception:
|
|
137
274
|
return None
|
|
138
275
|
|
|
139
276
|
|
|
140
|
-
_LOCK_FILE =
|
|
277
|
+
_LOCK_FILE = None # test-only override; runtime resolution stays dynamic
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _pid_file_path():
|
|
281
|
+
return _PID_FILE or state_path("daemon.pid")
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _port_file_path():
|
|
285
|
+
return _PORT_FILE or state_path("daemon.port")
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _lock_file_path():
|
|
289
|
+
return _LOCK_FILE or state_path("daemon.lock")
|
|
141
290
|
|
|
142
291
|
|
|
143
292
|
def _start_daemon_subprocess() -> bool:
|
|
@@ -161,8 +310,17 @@ def _start_daemon_subprocess() -> bool:
|
|
|
161
310
|
"""
|
|
162
311
|
if is_daemon_running():
|
|
163
312
|
return True
|
|
313
|
+
# Never create a descriptor for a child that cannot own the listener.
|
|
314
|
+
# A closed connection in TIME_WAIT is not a listener and is safe: the
|
|
315
|
+
# server reserves its socket with SO_REUSEADDR during bootstrap.
|
|
316
|
+
if _has_tcp_listener(_DEFAULT_PORT):
|
|
317
|
+
logger.warning("daemon port %d is already owned; refusing a second start", _DEFAULT_PORT)
|
|
318
|
+
return False
|
|
319
|
+
assert_no_durable_root_conflict()
|
|
164
320
|
|
|
165
321
|
import subprocess
|
|
322
|
+
|
|
323
|
+
from superlocalmemory import __version__ as _slm_version
|
|
166
324
|
# v3.6.9 (#33): pass SLM_DAEMON_PORT as explicit --port= so the daemon
|
|
167
325
|
# binds the right port even when the env var reaches the subprocess.
|
|
168
326
|
_target_port = _DEFAULT_PORT
|
|
@@ -170,7 +328,7 @@ def _start_daemon_subprocess() -> bool:
|
|
|
170
328
|
sys.executable, "-m", "superlocalmemory.server.unified_daemon",
|
|
171
329
|
"--start", f"--port={_target_port}",
|
|
172
330
|
]
|
|
173
|
-
log_dir =
|
|
331
|
+
log_dir = state_path("logs")
|
|
174
332
|
log_dir.mkdir(parents=True, exist_ok=True)
|
|
175
333
|
log_file = log_dir / "daemon.log"
|
|
176
334
|
|
|
@@ -189,14 +347,39 @@ def _start_daemon_subprocess() -> bool:
|
|
|
189
347
|
daemon_env = os.environ.copy()
|
|
190
348
|
daemon_env["OMP_NUM_THREADS"] = "1"
|
|
191
349
|
daemon_env["KMP_DUPLICATE_LIB_OK"] = "TRUE"
|
|
350
|
+
bootstrap_descriptor = build_descriptor(
|
|
351
|
+
port=_target_port,
|
|
352
|
+
version=_slm_version,
|
|
353
|
+
pid=os.getpid(),
|
|
354
|
+
state="starting",
|
|
355
|
+
)
|
|
356
|
+
daemon_env["SLM_DAEMON_INSTANCE_ID"] = bootstrap_descriptor.instance_id
|
|
357
|
+
daemon_env["SLM_DAEMON_CAPABILITY"] = bootstrap_descriptor.capability
|
|
192
358
|
kwargs["env"] = daemon_env
|
|
193
359
|
|
|
194
360
|
with open(log_file, "a") as lf:
|
|
195
361
|
proc = subprocess.Popen(cmd, stdout=lf, stderr=lf, **kwargs)
|
|
196
362
|
|
|
197
|
-
#
|
|
198
|
-
|
|
199
|
-
|
|
363
|
+
# Publish the exact child identity immediately so concurrent callers know
|
|
364
|
+
# this namespace is warming up. If the child won the race and already
|
|
365
|
+
# published the same instance as ready, never overwrite it with starting.
|
|
366
|
+
child_descriptor = replace(
|
|
367
|
+
bootstrap_descriptor,
|
|
368
|
+
pid=proc.pid,
|
|
369
|
+
process_create_time=process_create_time_for(proc.pid),
|
|
370
|
+
)
|
|
371
|
+
current = read_descriptor()
|
|
372
|
+
if not (
|
|
373
|
+
current is not None
|
|
374
|
+
and current.instance_id == child_descriptor.instance_id
|
|
375
|
+
and current.pid == child_descriptor.pid
|
|
376
|
+
and current.state == "ready"
|
|
377
|
+
):
|
|
378
|
+
write_descriptor(child_descriptor)
|
|
379
|
+
|
|
380
|
+
# One-release compatibility mirrors; never sufficient for ownership.
|
|
381
|
+
_pid_file_path().write_text(str(proc.pid))
|
|
382
|
+
_port_file_path().write_text(str(_target_port))
|
|
200
383
|
|
|
201
384
|
return _wait_for_daemon(timeout=60)
|
|
202
385
|
|
|
@@ -218,12 +401,21 @@ def ensure_daemon() -> bool:
|
|
|
218
401
|
"""
|
|
219
402
|
if is_daemon_running():
|
|
220
403
|
return True
|
|
404
|
+
if (
|
|
405
|
+
os.environ.get("SLM_TEST_ISOLATION") == "1"
|
|
406
|
+
and os.environ.get("SLM_TEST_ALLOW_DAEMON_SPAWN") != "1"
|
|
407
|
+
):
|
|
408
|
+
logger.debug(
|
|
409
|
+
"pytest isolation blocked daemon spawn; use an owned daemon fixture",
|
|
410
|
+
)
|
|
411
|
+
return False
|
|
221
412
|
|
|
222
413
|
# File lock — prevent concurrent starts from multiple CLI/MCP calls
|
|
223
414
|
lock_fd = None
|
|
224
415
|
try:
|
|
225
|
-
|
|
226
|
-
|
|
416
|
+
lock_file = _lock_file_path()
|
|
417
|
+
lock_file.parent.mkdir(parents=True, exist_ok=True)
|
|
418
|
+
lock_fd = open(lock_file, "w")
|
|
227
419
|
|
|
228
420
|
# Cross-platform file locking
|
|
229
421
|
if sys.platform == "win32":
|
|
@@ -276,135 +468,38 @@ def ensure_daemon() -> bool:
|
|
|
276
468
|
except Exception:
|
|
277
469
|
pass
|
|
278
470
|
try:
|
|
279
|
-
|
|
471
|
+
_lock_file_path().unlink(missing_ok=True)
|
|
280
472
|
except Exception:
|
|
281
473
|
pass
|
|
282
474
|
|
|
283
475
|
|
|
284
476
|
def _wait_for_daemon(timeout: int = 60) -> bool:
|
|
285
|
-
"""Wait for
|
|
286
|
-
then health endpoint (confirms HTTP server is bound)."""
|
|
477
|
+
"""Wait for matching owned health; liveness alone is never readiness."""
|
|
287
478
|
for _ in range(timeout * 2): # check every 0.5s
|
|
288
479
|
time.sleep(0.5)
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
return True # HTTP is ready
|
|
296
|
-
except Exception:
|
|
297
|
-
# PID alive but HTTP not ready — daemon is warming up, that's OK
|
|
480
|
+
descriptor = read_descriptor()
|
|
481
|
+
if descriptor is not None:
|
|
482
|
+
if not _descriptor_process_is_alive(descriptor):
|
|
483
|
+
continue
|
|
484
|
+
health = _fetch_health(descriptor.port)
|
|
485
|
+
if health is not None and descriptor_matches_health(descriptor, health):
|
|
298
486
|
return True
|
|
487
|
+
continue
|
|
488
|
+
if descriptor_path().exists():
|
|
489
|
+
continue
|
|
490
|
+
if _verified_legacy_health() is not None:
|
|
491
|
+
return True
|
|
299
492
|
return False
|
|
300
493
|
|
|
301
494
|
|
|
302
495
|
def stop_daemon() -> bool:
|
|
303
|
-
"""Stop
|
|
496
|
+
"""Stop only the daemon proven to belong to this data namespace.
|
|
304
497
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
accumulate from rapid restarts, MCP warmups, and concurrent sessions.
|
|
498
|
+
Machine-wide process-name scans are forbidden: they can kill another SLM
|
|
499
|
+
installation or a user's live workers during tests. V3.7 uses the owned
|
|
500
|
+
HTTP capability; the daemon itself terminates its child process tree.
|
|
309
501
|
"""
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
my_pid = os.getpid()
|
|
315
|
-
targets = [
|
|
316
|
-
"superlocalmemory.server.unified_daemon",
|
|
317
|
-
"superlocalmemory.core.embedding_worker",
|
|
318
|
-
"superlocalmemory.core.recall_worker",
|
|
319
|
-
"superlocalmemory.core.reranker_worker",
|
|
320
|
-
]
|
|
321
|
-
|
|
322
|
-
for proc in psutil.process_iter(["pid", "cmdline"]):
|
|
323
|
-
try:
|
|
324
|
-
if proc.pid == my_pid:
|
|
325
|
-
continue
|
|
326
|
-
cmdline = " ".join(proc.info.get("cmdline") or [])
|
|
327
|
-
if any(t in cmdline for t in targets):
|
|
328
|
-
# Kill children first, then process
|
|
329
|
-
for child in proc.children(recursive=True):
|
|
330
|
-
try:
|
|
331
|
-
child.kill()
|
|
332
|
-
killed += 1
|
|
333
|
-
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
334
|
-
pass
|
|
335
|
-
proc.kill()
|
|
336
|
-
killed += 1
|
|
337
|
-
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
338
|
-
pass
|
|
339
|
-
|
|
340
|
-
except ImportError:
|
|
341
|
-
# Fallback: pkill by pattern
|
|
342
|
-
try:
|
|
343
|
-
import subprocess as _sp
|
|
344
|
-
for pattern in [
|
|
345
|
-
"superlocalmemory.server.unified_daemon",
|
|
346
|
-
"superlocalmemory.core.embedding_worker",
|
|
347
|
-
"superlocalmemory.core.recall_worker",
|
|
348
|
-
"superlocalmemory.core.reranker_worker",
|
|
349
|
-
]:
|
|
350
|
-
result = _sp.run(
|
|
351
|
-
["pkill", "-9", "-f", pattern],
|
|
352
|
-
capture_output=True, timeout=5,
|
|
353
|
-
)
|
|
354
|
-
if result.returncode == 0:
|
|
355
|
-
killed += 1
|
|
356
|
-
except Exception:
|
|
357
|
-
pass
|
|
358
|
-
|
|
359
|
-
# Clean up PID/port files + worker PID files
|
|
360
|
-
_PID_FILE.unlink(missing_ok=True)
|
|
361
|
-
_PORT_FILE.unlink(missing_ok=True)
|
|
362
|
-
# v3.4.13: Clean worker PID files (singleton guards)
|
|
363
|
-
for pidfile in (".embedding-worker.pid", ".reranker-worker.pid"):
|
|
364
|
-
(Path.home() / ".superlocalmemory" / pidfile).unlink(missing_ok=True)
|
|
365
|
-
|
|
366
|
-
# v3.4.13: Wait for ALL workers to actually die before returning.
|
|
367
|
-
# Without this, `slm restart` starts a new daemon before old workers exit,
|
|
368
|
-
# causing duplicate embedding_workers (1.6GB each).
|
|
369
|
-
if killed:
|
|
370
|
-
logger.info("Stopped %d SLM processes, waiting for exit...", killed)
|
|
371
|
-
_wait_for_workers_dead(timeout=10)
|
|
372
|
-
|
|
373
|
-
return True
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
def _wait_for_workers_dead(timeout: int = 10) -> None:
|
|
377
|
-
"""Wait until no SLM worker processes remain alive."""
|
|
378
|
-
targets = [
|
|
379
|
-
"superlocalmemory.server.unified_daemon",
|
|
380
|
-
"superlocalmemory.core.embedding_worker",
|
|
381
|
-
"superlocalmemory.core.recall_worker",
|
|
382
|
-
"superlocalmemory.core.reranker_worker",
|
|
383
|
-
]
|
|
384
|
-
my_pid = os.getpid()
|
|
385
|
-
deadline = time.time() + timeout
|
|
386
|
-
|
|
387
|
-
while time.time() < deadline:
|
|
388
|
-
alive = False
|
|
389
|
-
try:
|
|
390
|
-
import psutil
|
|
391
|
-
for proc in psutil.process_iter(["pid", "cmdline"]):
|
|
392
|
-
try:
|
|
393
|
-
if proc.pid == my_pid:
|
|
394
|
-
continue
|
|
395
|
-
cmdline = " ".join(proc.info.get("cmdline") or [])
|
|
396
|
-
if any(t in cmdline for t in targets):
|
|
397
|
-
alive = True
|
|
398
|
-
break
|
|
399
|
-
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
400
|
-
pass
|
|
401
|
-
except ImportError:
|
|
402
|
-
# No psutil — just wait a fixed time
|
|
403
|
-
time.sleep(3)
|
|
404
|
-
return
|
|
405
|
-
|
|
406
|
-
if not alive:
|
|
407
|
-
return
|
|
408
|
-
time.sleep(0.5)
|
|
409
|
-
|
|
410
|
-
logger.warning("Some SLM workers still alive after %ds timeout", timeout)
|
|
502
|
+
if read_descriptor() is None and _verified_legacy_health() is None:
|
|
503
|
+
return False
|
|
504
|
+
response = daemon_request("POST", "/stop")
|
|
505
|
+
return bool(response and response.get("status") == "stopping")
|
|
@@ -18,11 +18,13 @@ from __future__ import annotations
|
|
|
18
18
|
from argparse import Namespace
|
|
19
19
|
from pathlib import Path
|
|
20
20
|
|
|
21
|
+
from superlocalmemory.infra.data_root import DynamicStatePath
|
|
22
|
+
|
|
21
23
|
|
|
22
24
|
# Canonical paths — match LLD-01 / LLD-07 layout. Callers can override
|
|
23
25
|
# via the ``learning_db_path`` / ``memory_db_path`` attributes on args
|
|
24
26
|
# (tests rely on this to point at fixture DBs).
|
|
25
|
-
DEFAULT_HOME =
|
|
27
|
+
DEFAULT_HOME = DynamicStatePath()
|
|
26
28
|
|
|
27
29
|
|
|
28
30
|
def _resolve_paths(args: Namespace) -> tuple[Path, Path]:
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
|
|
4
|
+
"""Explicit manual export for local aggregate diagnostics."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from argparse import Namespace
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def cmd_diagnostics(args: Namespace) -> None:
|
|
12
|
+
action = getattr(args, "diagnostics_command", None)
|
|
13
|
+
if action != "export":
|
|
14
|
+
raise SystemExit("choose a diagnostics subcommand: export")
|
|
15
|
+
|
|
16
|
+
from superlocalmemory.cli.json_output import json_print
|
|
17
|
+
from superlocalmemory.infra.local_diagnostics import default_diagnostics
|
|
18
|
+
|
|
19
|
+
default_diagnostics().export_json(args.destination)
|
|
20
|
+
data = {"exported": True, "reporting": "manual_export_only"}
|
|
21
|
+
if bool(getattr(args, "json", False)):
|
|
22
|
+
json_print("diagnostics", data=data)
|
|
23
|
+
return
|
|
24
|
+
print("Local aggregate diagnostics exported by explicit request.")
|
|
25
|
+
print("No automatic reporting was enabled.")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
__all__ = ["cmd_diagnostics"]
|