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
|
@@ -16,6 +16,7 @@ import logging
|
|
|
16
16
|
import os
|
|
17
17
|
import sys
|
|
18
18
|
from argparse import Namespace
|
|
19
|
+
from pathlib import Path
|
|
19
20
|
|
|
20
21
|
logger = logging.getLogger(__name__)
|
|
21
22
|
|
|
@@ -29,7 +30,13 @@ def _cmd_db_dispatch(args: Namespace) -> None:
|
|
|
29
30
|
if rc:
|
|
30
31
|
sys.exit(rc)
|
|
31
32
|
return
|
|
32
|
-
|
|
33
|
+
if sub == "scale":
|
|
34
|
+
from superlocalmemory.cli.scale_engine_cmd import cmd_db_scale
|
|
35
|
+
rc = cmd_db_scale(args)
|
|
36
|
+
if rc:
|
|
37
|
+
sys.exit(rc)
|
|
38
|
+
return
|
|
39
|
+
print("Usage: slm db migrate [--status] [--dry-run] | slm db scale <action>")
|
|
33
40
|
sys.exit(2)
|
|
34
41
|
|
|
35
42
|
|
|
@@ -139,7 +146,7 @@ def cmd_session(args: Namespace) -> None:
|
|
|
139
146
|
def dispatch(args: Namespace) -> None:
|
|
140
147
|
"""Route CLI command to the appropriate handler."""
|
|
141
148
|
# Auto-install/upgrade hooks on version change (single file read, ~0.1ms)
|
|
142
|
-
if args.command not in ("hooks", "init", "mcp"):
|
|
149
|
+
if args.command not in ("hooks", "codex", "init", "mcp"):
|
|
143
150
|
try:
|
|
144
151
|
from superlocalmemory.hooks.claude_code_hooks import auto_install_if_needed
|
|
145
152
|
auto_install_if_needed()
|
|
@@ -169,6 +176,7 @@ def dispatch(args: Namespace) -> None:
|
|
|
169
176
|
"dashboard": cmd_dashboard,
|
|
170
177
|
"profile": cmd_profile,
|
|
171
178
|
"hooks": cmd_hooks,
|
|
179
|
+
"codex": cmd_codex,
|
|
172
180
|
"session-context": cmd_session_context,
|
|
173
181
|
"session": cmd_session, # #49: local session open/close for hooks
|
|
174
182
|
"observe": cmd_observe,
|
|
@@ -200,6 +208,8 @@ def dispatch(args: Namespace) -> None:
|
|
|
200
208
|
"reconfigure": _cmd_escape_reconfigure,
|
|
201
209
|
"benchmark": _cmd_escape_benchmark,
|
|
202
210
|
"rotate-token": _cmd_escape_rotate_token,
|
|
211
|
+
"evidence": _cmd_evidence,
|
|
212
|
+
"diagnostics": _cmd_diagnostics,
|
|
203
213
|
# LLD-06 — `slm wrap <agent> [args...]` activates the Optimize proxy.
|
|
204
214
|
"wrap": _cmd_wrap,
|
|
205
215
|
# V3.6 Optimize subcommands (additive)
|
|
@@ -217,6 +227,20 @@ def dispatch(args: Namespace) -> None:
|
|
|
217
227
|
sys.exit(1)
|
|
218
228
|
|
|
219
229
|
|
|
230
|
+
def _cmd_evidence(args: Namespace) -> None:
|
|
231
|
+
"""Lazy-load the evidence/rebuild command surface."""
|
|
232
|
+
from superlocalmemory.cli.evidence_cmd import cmd_evidence
|
|
233
|
+
|
|
234
|
+
cmd_evidence(args)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _cmd_diagnostics(args: Namespace) -> None:
|
|
238
|
+
"""Lazy-load the local aggregate diagnostics export surface."""
|
|
239
|
+
from superlocalmemory.cli.diagnostics_cmd import cmd_diagnostics
|
|
240
|
+
|
|
241
|
+
cmd_diagnostics(args)
|
|
242
|
+
|
|
243
|
+
|
|
220
244
|
def _cmd_wrap(args: Namespace) -> None:
|
|
221
245
|
"""LLD-06 §6.6 — `slm wrap <agent> [args...]` activates the Optimize proxy.
|
|
222
246
|
|
|
@@ -319,29 +343,29 @@ def cmd_serve(args: Namespace) -> None:
|
|
|
319
343
|
print(" slm serve status — check daemon status")
|
|
320
344
|
print(" slm serve stop — stop daemon and free RAM")
|
|
321
345
|
else:
|
|
322
|
-
|
|
346
|
+
from superlocalmemory.infra.data_root import state_path
|
|
347
|
+
print(f"Failed to start daemon. Check {state_path('logs', 'daemon.log')}")
|
|
323
348
|
|
|
324
349
|
|
|
325
350
|
# -- Ingestion Adapters (V3.4.3) ------------------------------------------
|
|
326
351
|
|
|
327
352
|
|
|
328
353
|
def cmd_restart(args: Namespace) -> None:
|
|
329
|
-
"""
|
|
354
|
+
"""Restart the one daemon owned by the current SLM data namespace.
|
|
330
355
|
|
|
331
356
|
5-step pipeline:
|
|
332
|
-
1.
|
|
333
|
-
2.
|
|
357
|
+
1. Capability-stop the owned daemon and its children
|
|
358
|
+
2. Acquire the namespace start lock
|
|
334
359
|
3. Start fresh daemon
|
|
335
360
|
4. Wait for engine warmup + verify health
|
|
336
361
|
5. Optionally open dashboard
|
|
337
362
|
"""
|
|
338
|
-
import os
|
|
339
363
|
import time
|
|
340
|
-
from
|
|
364
|
+
from superlocalmemory.infra.daemon_identity import canonical_data_root
|
|
341
365
|
|
|
342
366
|
use_json = getattr(args, "json", False)
|
|
343
367
|
open_dashboard = getattr(args, "dashboard", False)
|
|
344
|
-
slm_dir =
|
|
368
|
+
slm_dir = canonical_data_root()
|
|
345
369
|
steps: list[dict] = []
|
|
346
370
|
|
|
347
371
|
def _log(step: int, name: str, status: str, detail: str = ""):
|
|
@@ -357,65 +381,9 @@ def cmd_restart(args: Namespace) -> None:
|
|
|
357
381
|
print(" " + "=" * 40)
|
|
358
382
|
print()
|
|
359
383
|
|
|
360
|
-
#
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
import psutil
|
|
364
|
-
my_pid = os.getpid()
|
|
365
|
-
targets = [
|
|
366
|
-
"superlocalmemory.server.unified_daemon",
|
|
367
|
-
"superlocalmemory.core.embedding_worker",
|
|
368
|
-
"superlocalmemory.core.recall_worker",
|
|
369
|
-
"superlocalmemory.core.reranker_worker",
|
|
370
|
-
"superlocalmemory.cli.daemon",
|
|
371
|
-
]
|
|
372
|
-
for proc in psutil.process_iter(["pid", "cmdline"]):
|
|
373
|
-
try:
|
|
374
|
-
if proc.pid == my_pid:
|
|
375
|
-
continue
|
|
376
|
-
cmdline = " ".join(proc.info.get("cmdline") or [])
|
|
377
|
-
if any(t in cmdline for t in targets):
|
|
378
|
-
for child in proc.children(recursive=True):
|
|
379
|
-
try:
|
|
380
|
-
child.kill()
|
|
381
|
-
killed += 1
|
|
382
|
-
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
383
|
-
pass
|
|
384
|
-
proc.kill()
|
|
385
|
-
killed += 1
|
|
386
|
-
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
387
|
-
pass
|
|
388
|
-
except ImportError:
|
|
389
|
-
# Fallback: pkill
|
|
390
|
-
import subprocess as _sp
|
|
391
|
-
for pattern in [
|
|
392
|
-
"superlocalmemory.server.unified_daemon",
|
|
393
|
-
"superlocalmemory.core.embedding_worker",
|
|
394
|
-
"superlocalmemory.core.recall_worker",
|
|
395
|
-
"superlocalmemory.core.reranker_worker",
|
|
396
|
-
]:
|
|
397
|
-
try:
|
|
398
|
-
r = _sp.run(["pkill", "-9", "-f", pattern], capture_output=True, timeout=5)
|
|
399
|
-
if r.returncode == 0:
|
|
400
|
-
killed += 1
|
|
401
|
-
except Exception:
|
|
402
|
-
pass
|
|
403
|
-
|
|
404
|
-
_log(1, "Kill all SLM processes", "ok", f"{killed} processes killed")
|
|
405
|
-
time.sleep(3)
|
|
406
|
-
|
|
407
|
-
# Step 2: Clean stale files + HOLD the lock to prevent races
|
|
408
|
-
# v3.4.13: Do NOT delete daemon.lock — HOLD it instead.
|
|
409
|
-
# If we delete it, `slm mcp` (still running in Claude) will see no lock,
|
|
410
|
-
# acquire a NEW lock, and start a second daemon during our restart.
|
|
411
|
-
cleaned = []
|
|
412
|
-
for fname in ("daemon.pid", "daemon.port", ".embedding-worker.pid", ".reranker-worker.pid"):
|
|
413
|
-
fpath = slm_dir / fname
|
|
414
|
-
if fpath.exists():
|
|
415
|
-
fpath.unlink(missing_ok=True)
|
|
416
|
-
cleaned.append(fname)
|
|
417
|
-
|
|
418
|
-
# Hold the lock file to block other processes from starting a daemon
|
|
384
|
+
# Acquire the namespace lock before requesting shutdown. Otherwise an
|
|
385
|
+
# auto-starting hook can observe the brief offline window and start a
|
|
386
|
+
# second daemon while this command is still waiting for the old process.
|
|
419
387
|
_LOCK_FILE = slm_dir / "daemon.lock"
|
|
420
388
|
_LOCK_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
421
389
|
restart_lock_fd = None
|
|
@@ -427,8 +395,49 @@ def cmd_restart(args: Namespace) -> None:
|
|
|
427
395
|
except Exception:
|
|
428
396
|
pass # Best-effort — don't block restart if lock fails
|
|
429
397
|
|
|
430
|
-
|
|
431
|
-
|
|
398
|
+
# Step 1: stop only the descriptor-owned daemon. Its graceful shutdown
|
|
399
|
+
# owns worker termination; process-name-wide scans are forbidden.
|
|
400
|
+
from superlocalmemory.cli.daemon import (
|
|
401
|
+
is_daemon_running,
|
|
402
|
+
read_descriptor,
|
|
403
|
+
stop_daemon,
|
|
404
|
+
wait_for_owned_daemon_shutdown,
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
was_running = is_daemon_running()
|
|
408
|
+
owned_descriptor = read_descriptor() if was_running else None
|
|
409
|
+
stopped = stop_daemon() if was_running else True
|
|
410
|
+
if stopped and was_running:
|
|
411
|
+
stopped = wait_for_owned_daemon_shutdown(owned_descriptor)
|
|
412
|
+
killed = 1 if was_running and stopped else 0
|
|
413
|
+
_log(
|
|
414
|
+
1,
|
|
415
|
+
"Stop owned SLM daemon",
|
|
416
|
+
"ok" if stopped else "fail",
|
|
417
|
+
"owned daemon stopped" if killed else (
|
|
418
|
+
"already stopped" if stopped else "owned daemon did not stop"
|
|
419
|
+
),
|
|
420
|
+
)
|
|
421
|
+
if not stopped:
|
|
422
|
+
if restart_lock_fd:
|
|
423
|
+
restart_lock_fd.close()
|
|
424
|
+
if use_json:
|
|
425
|
+
from superlocalmemory.cli.json_output import json_print
|
|
426
|
+
json_print(
|
|
427
|
+
"restart",
|
|
428
|
+
data={"steps": steps, "success": False},
|
|
429
|
+
next_actions=[{
|
|
430
|
+
"command": "slm doctor",
|
|
431
|
+
"description": "Diagnose the owned daemon",
|
|
432
|
+
}],
|
|
433
|
+
)
|
|
434
|
+
else:
|
|
435
|
+
print("\n Restart FAILED at step 1. The owned daemon was not stopped.")
|
|
436
|
+
return
|
|
437
|
+
|
|
438
|
+
# Step 2: the namespace lock was acquired before shutdown so hooks cannot
|
|
439
|
+
# auto-start another daemon inside the offline transition.
|
|
440
|
+
_log(2, "Acquire namespace start lock", "ok", str(_LOCK_FILE))
|
|
432
441
|
|
|
433
442
|
# Step 3: Start fresh daemon (lock still held — no races)
|
|
434
443
|
# v3.4.42: Call _start_daemon_subprocess() directly instead of
|
|
@@ -438,7 +447,6 @@ def cmd_restart(args: Namespace) -> None:
|
|
|
438
447
|
# fall into its lock-fail branch and time out after 60s while the
|
|
439
448
|
# actual daemon never gets started. Calling the helper directly
|
|
440
449
|
# bypasses that self-deadlock and starts the daemon as intended.
|
|
441
|
-
time.sleep(1)
|
|
442
450
|
from superlocalmemory.cli.daemon import _start_daemon_subprocess
|
|
443
451
|
started = _start_daemon_subprocess()
|
|
444
452
|
|
|
@@ -561,14 +569,15 @@ def cmd_config(args: Namespace) -> None:
|
|
|
561
569
|
slm config get evolution.backend
|
|
562
570
|
"""
|
|
563
571
|
import json
|
|
564
|
-
|
|
572
|
+
|
|
573
|
+
from superlocalmemory.infra.data_root import state_path
|
|
565
574
|
|
|
566
575
|
use_json = getattr(args, "json", False)
|
|
567
576
|
action = getattr(args, "action", "get")
|
|
568
577
|
key = getattr(args, "key", "")
|
|
569
578
|
value = getattr(args, "value", None)
|
|
570
579
|
|
|
571
|
-
config_path =
|
|
580
|
+
config_path = state_path("config.json")
|
|
572
581
|
|
|
573
582
|
# Read existing config
|
|
574
583
|
cfg: dict = {}
|
|
@@ -602,6 +611,7 @@ def cmd_config(args: Namespace) -> None:
|
|
|
602
611
|
_ALLOWED_CONFIG_KEYS = {
|
|
603
612
|
"evolution.enabled", "evolution.backend", "evolution.max_evolutions_per_cycle",
|
|
604
613
|
"mesh_enabled", "daemon_idle_timeout", "entity_compilation_enabled",
|
|
614
|
+
"graph_backend", "vector_backend", "scale_engine_state",
|
|
605
615
|
}
|
|
606
616
|
if key not in _ALLOWED_CONFIG_KEYS:
|
|
607
617
|
if use_json:
|
|
@@ -682,7 +692,8 @@ def cmd_evolve(args: Namespace) -> None:
|
|
|
682
692
|
If disabled, exits silently (zero output for fire-and-forget).
|
|
683
693
|
"""
|
|
684
694
|
import json
|
|
685
|
-
|
|
695
|
+
|
|
696
|
+
from superlocalmemory.infra.data_root import state_path
|
|
686
697
|
|
|
687
698
|
session_id = getattr(args, "session", "") or ""
|
|
688
699
|
profile = getattr(args, "profile", "default") or "default"
|
|
@@ -691,7 +702,7 @@ def cmd_evolve(args: Namespace) -> None:
|
|
|
691
702
|
return # Silent exit — nothing to do without a session
|
|
692
703
|
|
|
693
704
|
# Check if evolution is enabled via config.json
|
|
694
|
-
config_path =
|
|
705
|
+
config_path = state_path("config.json")
|
|
695
706
|
try:
|
|
696
707
|
cfg = json.loads(config_path.read_text()) if config_path.exists() else {}
|
|
697
708
|
except (json.JSONDecodeError, OSError):
|
|
@@ -705,7 +716,7 @@ def cmd_evolve(args: Namespace) -> None:
|
|
|
705
716
|
try:
|
|
706
717
|
from superlocalmemory.evolution.skill_evolver import SkillEvolver
|
|
707
718
|
|
|
708
|
-
db_path =
|
|
719
|
+
db_path = state_path("memory.db")
|
|
709
720
|
if not db_path.exists():
|
|
710
721
|
return
|
|
711
722
|
|
|
@@ -810,7 +821,7 @@ def cmd_provider(args: Namespace) -> None:
|
|
|
810
821
|
if args.action == "set":
|
|
811
822
|
from superlocalmemory.cli.setup_wizard import configure_provider
|
|
812
823
|
|
|
813
|
-
configure_provider(config)
|
|
824
|
+
configure_provider(config, provider_name=getattr(args, "provider", None))
|
|
814
825
|
else:
|
|
815
826
|
print(f"Provider: {config.llm.provider or 'none (Mode A)'}")
|
|
816
827
|
if config.llm.model:
|
|
@@ -882,6 +893,11 @@ def cmd_connect(args: Namespace) -> None:
|
|
|
882
893
|
agents_md_source=_agents_md_source_factory(),
|
|
883
894
|
)
|
|
884
895
|
|
|
896
|
+
if not result.get("error"):
|
|
897
|
+
from superlocalmemory.infra.local_diagnostics import record_operation
|
|
898
|
+
|
|
899
|
+
record_operation("activation", client=ide_arg)
|
|
900
|
+
|
|
885
901
|
if getattr(args, "json", False):
|
|
886
902
|
from superlocalmemory.cli.json_output import json_print
|
|
887
903
|
json_print("connect", data=result)
|
|
@@ -1050,57 +1066,67 @@ def cmd_remember(args: Namespace) -> None:
|
|
|
1050
1066
|
if isinstance(_sw_raw, str) and _sw_raw.strip() else _sw_raw
|
|
1051
1067
|
)
|
|
1052
1068
|
|
|
1053
|
-
#
|
|
1054
|
-
#
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1069
|
+
# Both paths use the one owned daemon. A second local engine for --sync
|
|
1070
|
+
# duplicates heavyweight workers and can block for minutes on cold models.
|
|
1071
|
+
daemon_owned = False
|
|
1072
|
+
try:
|
|
1073
|
+
from superlocalmemory.cli.daemon import (
|
|
1074
|
+
daemon_request, ensure_daemon, is_daemon_running,
|
|
1075
|
+
)
|
|
1076
|
+
daemon_owned = is_daemon_running() or ensure_daemon()
|
|
1077
|
+
if daemon_owned:
|
|
1078
|
+
path = "/remember?wait=true" if sync_mode else "/remember"
|
|
1079
|
+
result = daemon_request(
|
|
1080
|
+
"POST", path, {
|
|
1062
1081
|
"content": args.content,
|
|
1063
1082
|
"tags": args.tags or "",
|
|
1064
1083
|
"scope": scope,
|
|
1065
1084
|
"shared_with": shared_with,
|
|
1085
|
+
},
|
|
1086
|
+
timeout_seconds=30,
|
|
1087
|
+
)
|
|
1088
|
+
if result and "fact_ids" in result:
|
|
1089
|
+
if use_json:
|
|
1090
|
+
from superlocalmemory.cli.json_output import json_print
|
|
1091
|
+
json_print("remember", data=result)
|
|
1092
|
+
else:
|
|
1093
|
+
state = result.get("materialization_state", "queryable")
|
|
1094
|
+
operation_id = result.get("operation_id", "unknown")
|
|
1095
|
+
print(
|
|
1096
|
+
f"{state.capitalize()} \u2713 {result['count']} facts "
|
|
1097
|
+
f"(operation={operation_id})."
|
|
1098
|
+
)
|
|
1099
|
+
return
|
|
1100
|
+
if sync_mode:
|
|
1101
|
+
if use_json:
|
|
1102
|
+
from superlocalmemory.cli.json_output import json_print
|
|
1103
|
+
json_print("remember", error={
|
|
1104
|
+
"code": "SYNC_TIMEOUT",
|
|
1105
|
+
"message": (
|
|
1106
|
+
"Canonical ingestion did not complete within 30s; "
|
|
1107
|
+
"the durable operation remains available for retry."
|
|
1108
|
+
),
|
|
1109
|
+
})
|
|
1110
|
+
else:
|
|
1111
|
+
print(
|
|
1112
|
+
"Synchronous ingestion did not complete within 30s; "
|
|
1113
|
+
"the durable operation remains queued.",
|
|
1114
|
+
file=sys.stderr,
|
|
1115
|
+
)
|
|
1116
|
+
sys.exit(1)
|
|
1117
|
+
except SystemExit:
|
|
1118
|
+
raise
|
|
1119
|
+
except Exception:
|
|
1120
|
+
if sync_mode and daemon_owned:
|
|
1121
|
+
if use_json:
|
|
1122
|
+
from superlocalmemory.cli.json_output import json_print
|
|
1123
|
+
json_print("remember", error={
|
|
1124
|
+
"code": "SYNC_TIMEOUT",
|
|
1125
|
+
"message": "Owned daemon request failed before completion.",
|
|
1066
1126
|
})
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
json_print("remember", data=result)
|
|
1071
|
-
else:
|
|
1072
|
-
print(f"Stored \u2713 {result['count']} facts (via daemon).")
|
|
1073
|
-
return
|
|
1074
|
-
except Exception:
|
|
1075
|
-
pass # Fall through to pending store
|
|
1076
|
-
|
|
1077
|
-
# v3.4.13: Store to pending DB (zero data loss) — daemon processes in background.
|
|
1078
|
-
# NO subprocess spawn. Daemon's background loop picks up pending memories.
|
|
1079
|
-
from superlocalmemory.cli.pending_store import store_pending
|
|
1080
|
-
|
|
1081
|
-
# v3.6.15 multi-scope: carry an explicit non-personal scope into the
|
|
1082
|
-
# pending row's metadata so the materializer replays the right
|
|
1083
|
-
# visibility. Unset / personal carries nothing — byte-identical to
|
|
1084
|
-
# pre-3.6.15 pending rows.
|
|
1085
|
-
_pending_meta = None
|
|
1086
|
-
if scope and scope != "personal":
|
|
1087
|
-
_pending_meta = {"scope": scope}
|
|
1088
|
-
if shared_with:
|
|
1089
|
-
_pending_meta["shared_with"] = shared_with
|
|
1090
|
-
|
|
1091
|
-
row_id = store_pending(
|
|
1092
|
-
content=args.content,
|
|
1093
|
-
tags=args.tags or "",
|
|
1094
|
-
metadata=_pending_meta,
|
|
1095
|
-
)
|
|
1096
|
-
|
|
1097
|
-
if use_json:
|
|
1098
|
-
from superlocalmemory.cli.json_output import json_print
|
|
1099
|
-
json_print("remember", data={"queued": True, "async": True,
|
|
1100
|
-
"pending_id": row_id, "safe": True})
|
|
1101
|
-
else:
|
|
1102
|
-
print(f"Stored \u2713 (pending_id={row_id}) \u2014 processing in background.")
|
|
1103
|
-
return
|
|
1127
|
+
sys.exit(1)
|
|
1128
|
+
# Receipt-first writes may use the authenticated local fallback when
|
|
1129
|
+
# no owned daemon exists.
|
|
1104
1130
|
|
|
1105
1131
|
from superlocalmemory.core.engine import MemoryEngine
|
|
1106
1132
|
|
|
@@ -1111,10 +1137,21 @@ def cmd_remember(args: Namespace) -> None:
|
|
|
1111
1137
|
|
|
1112
1138
|
# v3.6.15: resolve an unset scope to the configured default_scope.
|
|
1113
1139
|
_scope = scope or getattr(getattr(config, "scope", None), "default_scope", "personal")
|
|
1140
|
+
from superlocalmemory.core.engine_ingestion import (
|
|
1141
|
+
canonical_store,
|
|
1142
|
+
local_trusted_actor_id,
|
|
1143
|
+
)
|
|
1144
|
+
|
|
1114
1145
|
metadata = {"tags": args.tags} if args.tags else {}
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1146
|
+
operation = canonical_store(
|
|
1147
|
+
engine,
|
|
1148
|
+
args.content,
|
|
1149
|
+
source_type="cli-sync" if sync_mode else "cli-offline-canonical",
|
|
1150
|
+
trusted_actor_id=local_trusted_actor_id("cli"),
|
|
1151
|
+
metadata=metadata,
|
|
1152
|
+
scope=_scope,
|
|
1153
|
+
shared_with=shared_with,
|
|
1154
|
+
return_receipt=True,
|
|
1118
1155
|
)
|
|
1119
1156
|
except Exception as exc:
|
|
1120
1157
|
if use_json:
|
|
@@ -1123,16 +1160,30 @@ def cmd_remember(args: Namespace) -> None:
|
|
|
1123
1160
|
sys.exit(1)
|
|
1124
1161
|
raise
|
|
1125
1162
|
|
|
1163
|
+
fact_ids = list(operation.fact_ids) if hasattr(operation, "fact_ids") else list(operation)
|
|
1164
|
+
operation_data = {
|
|
1165
|
+
"fact_ids": fact_ids,
|
|
1166
|
+
"count": len(fact_ids),
|
|
1167
|
+
"materialization_state": getattr(
|
|
1168
|
+
getattr(operation, "state", None), "value", "complete"
|
|
1169
|
+
),
|
|
1170
|
+
}
|
|
1171
|
+
if getattr(operation, "operation_id", None):
|
|
1172
|
+
operation_data["operation_id"] = operation.operation_id
|
|
1173
|
+
|
|
1126
1174
|
if use_json:
|
|
1127
1175
|
from superlocalmemory.cli.json_output import json_print
|
|
1128
|
-
json_print("remember", data=
|
|
1176
|
+
json_print("remember", data=operation_data,
|
|
1129
1177
|
next_actions=[
|
|
1130
1178
|
{"command": "slm recall '<query>' --json", "description": "Search your memories"},
|
|
1131
1179
|
{"command": "slm list --json -n 5", "description": "See recent memories"},
|
|
1132
1180
|
])
|
|
1133
1181
|
return
|
|
1134
1182
|
|
|
1135
|
-
print(
|
|
1183
|
+
print(
|
|
1184
|
+
f"Complete \u2713 {len(fact_ids)} facts "
|
|
1185
|
+
f"(operation={operation_data.get('operation_id', 'none')})."
|
|
1186
|
+
)
|
|
1136
1187
|
|
|
1137
1188
|
|
|
1138
1189
|
def cmd_recall(args: Namespace) -> None:
|
|
@@ -1214,7 +1265,10 @@ def cmd_recall(args: Namespace) -> None:
|
|
|
1214
1265
|
# v3.6.6: route the direct-fallback path through the SAME shared
|
|
1215
1266
|
# serializer the daemon uses, so CLI-without-daemon output is identical
|
|
1216
1267
|
# to CLI/MCP-with-daemon (budget + source discipline + no_confident_match).
|
|
1217
|
-
from superlocalmemory.server.recall_serializer import
|
|
1268
|
+
from superlocalmemory.server.recall_serializer import (
|
|
1269
|
+
recall_response_metadata,
|
|
1270
|
+
serialize_recall_response,
|
|
1271
|
+
)
|
|
1218
1272
|
_rc = getattr(config, "retrieval", None)
|
|
1219
1273
|
_ser, _no_match = serialize_recall_response(
|
|
1220
1274
|
response,
|
|
@@ -1228,21 +1282,12 @@ def cmd_recall(args: Namespace) -> None:
|
|
|
1228
1282
|
from superlocalmemory.cli.json_output import json_print
|
|
1229
1283
|
items = []
|
|
1230
1284
|
for d in _ser:
|
|
1231
|
-
|
|
1232
|
-
"fact_id": d["fact_id"], "content": d["content"],
|
|
1233
|
-
"score": round(d["score"], 3),
|
|
1234
|
-
}
|
|
1235
|
-
if d.get("channel_scores"):
|
|
1236
|
-
item["channel_scores"] = {k: round(v, 3) for k, v in d["channel_scores"].items()}
|
|
1237
|
-
if d.get("truncated"):
|
|
1238
|
-
item["truncated"] = True
|
|
1239
|
-
if d.get("stub"):
|
|
1240
|
-
item["stub"] = True
|
|
1241
|
-
items.append(item)
|
|
1285
|
+
items.append(dict(d))
|
|
1242
1286
|
json_print("recall", data={
|
|
1243
1287
|
"results": items, "count": len(items),
|
|
1244
1288
|
"query_type": getattr(response, "query_type", "unknown"),
|
|
1245
1289
|
"no_confident_match": _no_match,
|
|
1290
|
+
**recall_response_metadata(response),
|
|
1246
1291
|
}, next_actions=[
|
|
1247
1292
|
{"command": "slm list --json", "description": "List recent memories"},
|
|
1248
1293
|
])
|
|
@@ -1258,15 +1303,21 @@ def cmd_recall(args: Namespace) -> None:
|
|
|
1258
1303
|
print("No confident match." if _no_match else "No memories found.")
|
|
1259
1304
|
return
|
|
1260
1305
|
for i, d in enumerate(_ser, 1):
|
|
1261
|
-
print(f" {i}. [{d['
|
|
1306
|
+
print(f" {i}. [relevance {d['relevance_score']:.2f}] {d['content']}")
|
|
1262
1307
|
|
|
1263
1308
|
|
|
1264
1309
|
def _cli_record_signals(config, query, results):
|
|
1265
1310
|
"""Record learning signals from CLI recall (no MCP dependency)."""
|
|
1266
1311
|
from pathlib import Path
|
|
1312
|
+
|
|
1267
1313
|
from superlocalmemory.learning.feedback import FeedbackCollector
|
|
1268
1314
|
from superlocalmemory.learning.signals import LearningSignals
|
|
1269
|
-
|
|
1315
|
+
configured_root = getattr(config, "base_dir", None)
|
|
1316
|
+
if configured_root is not None:
|
|
1317
|
+
slm_dir = Path(configured_root)
|
|
1318
|
+
else:
|
|
1319
|
+
from superlocalmemory.infra.data_root import canonical_data_root
|
|
1320
|
+
slm_dir = canonical_data_root()
|
|
1270
1321
|
pid = config.active_profile
|
|
1271
1322
|
fact_ids = [r.fact.fact_id for r in results[:10]]
|
|
1272
1323
|
if not fact_ids:
|
|
@@ -1303,6 +1354,19 @@ def cmd_forget(args: Namespace) -> None:
|
|
|
1303
1354
|
|
|
1304
1355
|
dry_run = getattr(args, 'dry_run', False)
|
|
1305
1356
|
|
|
1357
|
+
def delete_fact_authorized_for_cli(fact_id: str) -> None:
|
|
1358
|
+
from superlocalmemory.core.engine_ingestion import local_trusted_actor_id
|
|
1359
|
+
from superlocalmemory.core.mutations import delete_fact_authorized
|
|
1360
|
+
|
|
1361
|
+
result = delete_fact_authorized(
|
|
1362
|
+
engine,
|
|
1363
|
+
fact_id,
|
|
1364
|
+
trusted_actor_id=local_trusted_actor_id("cli"),
|
|
1365
|
+
source_agent_id="cli",
|
|
1366
|
+
)
|
|
1367
|
+
if not result.get("ok"):
|
|
1368
|
+
raise RuntimeError(result.get("error", "delete failed"))
|
|
1369
|
+
|
|
1306
1370
|
if use_json:
|
|
1307
1371
|
from superlocalmemory.cli.json_output import json_print
|
|
1308
1372
|
if not matches:
|
|
@@ -1317,7 +1381,7 @@ def cmd_forget(args: Namespace) -> None:
|
|
|
1317
1381
|
return
|
|
1318
1382
|
if getattr(args, 'yes', False):
|
|
1319
1383
|
for f in matches:
|
|
1320
|
-
|
|
1384
|
+
delete_fact_authorized_for_cli(f.fact_id)
|
|
1321
1385
|
json_print("forget", data={
|
|
1322
1386
|
"matched_count": len(matches), "deleted_count": len(matches),
|
|
1323
1387
|
"deleted": [f.fact_id for f in matches],
|
|
@@ -1345,13 +1409,13 @@ def cmd_forget(args: Namespace) -> None:
|
|
|
1345
1409
|
return
|
|
1346
1410
|
if getattr(args, 'yes', False):
|
|
1347
1411
|
for f in matches:
|
|
1348
|
-
|
|
1412
|
+
delete_fact_authorized_for_cli(f.fact_id)
|
|
1349
1413
|
print(f"Deleted {len(matches)} memories.")
|
|
1350
1414
|
return
|
|
1351
1415
|
confirm = input(f"Delete {len(matches)} memories? [y/N] ").strip().lower()
|
|
1352
1416
|
if confirm in ("y", "yes"):
|
|
1353
1417
|
for f in matches:
|
|
1354
|
-
|
|
1418
|
+
delete_fact_authorized_for_cli(f.fact_id)
|
|
1355
1419
|
print(f"Deleted {len(matches)} memories.")
|
|
1356
1420
|
else:
|
|
1357
1421
|
print("Cancelled.")
|
|
@@ -1389,7 +1453,15 @@ def cmd_delete(args: Namespace) -> None:
|
|
|
1389
1453
|
sys.exit(1)
|
|
1390
1454
|
content = dict(rows[0]).get("content", "")
|
|
1391
1455
|
if getattr(args, "yes", False):
|
|
1392
|
-
|
|
1456
|
+
from superlocalmemory.core.engine_ingestion import local_trusted_actor_id
|
|
1457
|
+
from superlocalmemory.core.mutations import delete_fact_authorized
|
|
1458
|
+
|
|
1459
|
+
delete_fact_authorized(
|
|
1460
|
+
engine,
|
|
1461
|
+
fact_id,
|
|
1462
|
+
trusted_actor_id=local_trusted_actor_id("cli"),
|
|
1463
|
+
source_agent_id="cli",
|
|
1464
|
+
)
|
|
1393
1465
|
json_print("delete", data={"deleted": fact_id, "content": content[:120]},
|
|
1394
1466
|
next_actions=[
|
|
1395
1467
|
{"command": "slm list --json", "description": "Verify remaining memories"},
|
|
@@ -1416,7 +1488,15 @@ def cmd_delete(args: Namespace) -> None:
|
|
|
1416
1488
|
print("Cancelled.")
|
|
1417
1489
|
return
|
|
1418
1490
|
|
|
1419
|
-
|
|
1491
|
+
from superlocalmemory.core.engine_ingestion import local_trusted_actor_id
|
|
1492
|
+
from superlocalmemory.core.mutations import delete_fact_authorized
|
|
1493
|
+
|
|
1494
|
+
delete_fact_authorized(
|
|
1495
|
+
engine,
|
|
1496
|
+
fact_id,
|
|
1497
|
+
trusted_actor_id=local_trusted_actor_id("cli"),
|
|
1498
|
+
source_agent_id="cli",
|
|
1499
|
+
)
|
|
1420
1500
|
print(f"Deleted: {fact_id}")
|
|
1421
1501
|
|
|
1422
1502
|
|
|
@@ -1464,9 +1544,15 @@ def cmd_update(args: Namespace) -> None:
|
|
|
1464
1544
|
return
|
|
1465
1545
|
|
|
1466
1546
|
old_content = dict(rows[0]).get("content", "")
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1547
|
+
from superlocalmemory.core.engine_ingestion import local_trusted_actor_id
|
|
1548
|
+
from superlocalmemory.core.mutations import update_fact_authorized
|
|
1549
|
+
|
|
1550
|
+
update_fact_authorized(
|
|
1551
|
+
engine,
|
|
1552
|
+
fact_id,
|
|
1553
|
+
new_content,
|
|
1554
|
+
trusted_actor_id=local_trusted_actor_id("cli"),
|
|
1555
|
+
source_agent_id="cli",
|
|
1470
1556
|
)
|
|
1471
1557
|
|
|
1472
1558
|
if use_json:
|
|
@@ -1592,17 +1678,43 @@ def cmd_status(args: Namespace) -> None:
|
|
|
1592
1678
|
|
|
1593
1679
|
def cmd_health(args: Namespace) -> None:
|
|
1594
1680
|
"""Show math layer health status."""
|
|
1595
|
-
from superlocalmemory.core.engine import MemoryEngine
|
|
1596
1681
|
from superlocalmemory.core.config import SLMConfig
|
|
1597
1682
|
|
|
1598
1683
|
use_json = getattr(args, 'json', False)
|
|
1599
1684
|
try:
|
|
1600
1685
|
config = SLMConfig.load()
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1686
|
+
from superlocalmemory.cli.daemon import is_daemon_running
|
|
1687
|
+
if is_daemon_running():
|
|
1688
|
+
# A running daemon owns the writable SQLite connections. Opening a
|
|
1689
|
+
# second MemoryEngine re-runs schema initialization and can lock
|
|
1690
|
+
# the user's database. Health only needs aggregate counts, so use
|
|
1691
|
+
# a read-only snapshot connection instead.
|
|
1692
|
+
import sqlite3
|
|
1693
|
+
db_path = config.db_path
|
|
1694
|
+
conn = sqlite3.connect(
|
|
1695
|
+
f"file:{db_path}?mode=ro", uri=True, timeout=5,
|
|
1696
|
+
)
|
|
1697
|
+
try:
|
|
1698
|
+
row = conn.execute(
|
|
1699
|
+
"SELECT COUNT(*), "
|
|
1700
|
+
"SUM(CASE WHEN fisher_mean IS NOT NULL THEN 1 ELSE 0 END), "
|
|
1701
|
+
"SUM(CASE WHEN langevin_position IS NOT NULL THEN 1 ELSE 0 END) "
|
|
1702
|
+
"FROM atomic_facts WHERE profile_id = ?",
|
|
1703
|
+
(config.active_profile,),
|
|
1704
|
+
).fetchone()
|
|
1705
|
+
total_facts, fisher_count, langevin_count = row or (0, 0, 0)
|
|
1706
|
+
finally:
|
|
1707
|
+
conn.close()
|
|
1708
|
+
facts = [None] * int(total_facts or 0)
|
|
1709
|
+
fisher_count = int(fisher_count or 0)
|
|
1710
|
+
langevin_count = int(langevin_count or 0)
|
|
1711
|
+
else:
|
|
1712
|
+
from superlocalmemory.core.engine import MemoryEngine
|
|
1713
|
+
engine = MemoryEngine(config)
|
|
1714
|
+
engine.initialize()
|
|
1715
|
+
facts = engine._db.get_all_facts(engine.profile_id)
|
|
1716
|
+
fisher_count = sum(1 for f in facts if f.fisher_mean is not None)
|
|
1717
|
+
langevin_count = sum(1 for f in facts if f.langevin_position is not None)
|
|
1606
1718
|
except Exception as exc:
|
|
1607
1719
|
if use_json:
|
|
1608
1720
|
from superlocalmemory.cli.json_output import json_print
|
|
@@ -1642,7 +1754,7 @@ def _gather_optimize_surface_b() -> dict:
|
|
|
1642
1754
|
compress_runs, tokens_saved, cache_hits, cache_misses,
|
|
1643
1755
|
db_present, error
|
|
1644
1756
|
"""
|
|
1645
|
-
from
|
|
1757
|
+
from superlocalmemory.infra.data_root import state_path
|
|
1646
1758
|
from superlocalmemory.optimize.storage.db import CacheDB
|
|
1647
1759
|
|
|
1648
1760
|
result: dict = {
|
|
@@ -1672,7 +1784,7 @@ def _gather_optimize_surface_b() -> dict:
|
|
|
1672
1784
|
|
|
1673
1785
|
# Step 2: read persisted metrics from llmcache.db (daemon-flushed, ≤60s stale).
|
|
1674
1786
|
try:
|
|
1675
|
-
db_path =
|
|
1787
|
+
db_path = state_path("llmcache.db")
|
|
1676
1788
|
result["db_present"] = db_path.exists()
|
|
1677
1789
|
if result["db_present"]:
|
|
1678
1790
|
snap = CacheDB.get_default().metrics_load()
|
|
@@ -1687,6 +1799,38 @@ def _gather_optimize_surface_b() -> dict:
|
|
|
1687
1799
|
return result
|
|
1688
1800
|
|
|
1689
1801
|
|
|
1802
|
+
def _readline_with_timeout(
|
|
1803
|
+
stream, timeout_sec: float,
|
|
1804
|
+
) -> tuple[str | None, Exception | None]:
|
|
1805
|
+
"""Read one line from a pipe-like stream without POSIX-only select().
|
|
1806
|
+
|
|
1807
|
+
Windows select() only accepts sockets, not subprocess pipes. A bounded
|
|
1808
|
+
helper thread keeps the embedding-worker probe cross-platform while
|
|
1809
|
+
preserving the existing timeout behavior.
|
|
1810
|
+
"""
|
|
1811
|
+
import threading
|
|
1812
|
+
|
|
1813
|
+
result: dict[str, object] = {}
|
|
1814
|
+
|
|
1815
|
+
def _read() -> None:
|
|
1816
|
+
try:
|
|
1817
|
+
result["line"] = stream.readline()
|
|
1818
|
+
except Exception as exc: # noqa: BLE001 - returned as probe failure
|
|
1819
|
+
result["exc"] = exc
|
|
1820
|
+
|
|
1821
|
+
reader = threading.Thread(target=_read, daemon=True)
|
|
1822
|
+
reader.start()
|
|
1823
|
+
reader.join(timeout_sec)
|
|
1824
|
+
if reader.is_alive():
|
|
1825
|
+
return None, None
|
|
1826
|
+
line = result.get("line")
|
|
1827
|
+
exc = result.get("exc")
|
|
1828
|
+
return (
|
|
1829
|
+
line if isinstance(line, str) else None,
|
|
1830
|
+
exc if isinstance(exc, Exception) else None,
|
|
1831
|
+
)
|
|
1832
|
+
|
|
1833
|
+
|
|
1690
1834
|
def cmd_doctor(args: Namespace) -> None:
|
|
1691
1835
|
"""Comprehensive pre-flight check — verify everything works.
|
|
1692
1836
|
|
|
@@ -1746,7 +1890,7 @@ def cmd_doctor(args: Namespace) -> None:
|
|
|
1746
1890
|
ver = getattr(m, "__version__", "?")
|
|
1747
1891
|
core_ok.append(mod)
|
|
1748
1892
|
core_versions.append(f"{mod} {ver}")
|
|
1749
|
-
except
|
|
1893
|
+
except Exception: # dependency import may fail after module discovery
|
|
1750
1894
|
pass
|
|
1751
1895
|
if len(core_ok) == len(core_modules):
|
|
1752
1896
|
_check("Core deps", "PASS", ", ".join(core_versions[:4]) + "...")
|
|
@@ -1763,7 +1907,7 @@ def cmd_doctor(args: Namespace) -> None:
|
|
|
1763
1907
|
try:
|
|
1764
1908
|
__import__(mod)
|
|
1765
1909
|
search_ok.append(mod)
|
|
1766
|
-
except
|
|
1910
|
+
except Exception: # dependency import may fail after module discovery
|
|
1767
1911
|
pass
|
|
1768
1912
|
if len(search_ok) == len(search_mods):
|
|
1769
1913
|
_check("Search deps", "PASS", "sentence-transformers, torch, sklearn, geoopt")
|
|
@@ -1777,7 +1921,7 @@ def cmd_doctor(args: Namespace) -> None:
|
|
|
1777
1921
|
for mod in ["fastapi", "uvicorn", "websockets"]:
|
|
1778
1922
|
try:
|
|
1779
1923
|
__import__(mod)
|
|
1780
|
-
except
|
|
1924
|
+
except Exception: # dependency import may fail after module discovery
|
|
1781
1925
|
dash_ok = False
|
|
1782
1926
|
break
|
|
1783
1927
|
if dash_ok:
|
|
@@ -1790,7 +1934,7 @@ def cmd_doctor(args: Namespace) -> None:
|
|
|
1790
1934
|
try:
|
|
1791
1935
|
import lightgbm
|
|
1792
1936
|
_check("Learning deps", "PASS", f"lightgbm {lightgbm.__version__}")
|
|
1793
|
-
except
|
|
1937
|
+
except Exception: # dependency import may fail after module discovery
|
|
1794
1938
|
_check("Learning deps", "WARN", "lightgbm not installed",
|
|
1795
1939
|
"pip install lightgbm")
|
|
1796
1940
|
except OSError as exc:
|
|
@@ -1807,7 +1951,7 @@ def cmd_doctor(args: Namespace) -> None:
|
|
|
1807
1951
|
try:
|
|
1808
1952
|
__import__(mod)
|
|
1809
1953
|
perf_ok.append(mod)
|
|
1810
|
-
except
|
|
1954
|
+
except Exception: # dependency import may fail after module discovery
|
|
1811
1955
|
pass
|
|
1812
1956
|
if perf_ok:
|
|
1813
1957
|
_check("Performance deps", "PASS", "orjson")
|
|
@@ -1839,10 +1983,11 @@ def cmd_doctor(args: Namespace) -> None:
|
|
|
1839
1983
|
proc.stdin.write(_json.dumps({"cmd": "ping"}) + "\n")
|
|
1840
1984
|
proc.stdin.flush()
|
|
1841
1985
|
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1986
|
+
line, read_exc = _readline_with_timeout(proc.stdout, 30)
|
|
1987
|
+
if read_exc is not None:
|
|
1988
|
+
_check("Embedding worker", "FAIL", str(read_exc), "slm warmup")
|
|
1989
|
+
elif line is not None:
|
|
1990
|
+
resp = _json.loads(line or "{}")
|
|
1846
1991
|
if resp.get("ok"):
|
|
1847
1992
|
_check(
|
|
1848
1993
|
"Embedding worker", "PASS",
|
|
@@ -1930,7 +2075,8 @@ def cmd_doctor(args: Namespace) -> None:
|
|
|
1930
2075
|
pass # Config load failed — already caught above
|
|
1931
2076
|
|
|
1932
2077
|
# 9. Disk space
|
|
1933
|
-
|
|
2078
|
+
from superlocalmemory.infra.data_root import canonical_data_root
|
|
2079
|
+
slm_home = canonical_data_root()
|
|
1934
2080
|
try:
|
|
1935
2081
|
usage = shutil.disk_usage(slm_home if slm_home.exists() else Path.home())
|
|
1936
2082
|
free_gb = usage.free / (1024 ** 3)
|
|
@@ -1975,8 +2121,8 @@ def cmd_doctor(args: Namespace) -> None:
|
|
|
1975
2121
|
"WARN",
|
|
1976
2122
|
"System Python is externally managed (EXTERNALLY-MANAGED marker found). "
|
|
1977
2123
|
"pip install may fail with PEP 668 error.",
|
|
1978
|
-
"Use
|
|
1979
|
-
"
|
|
2124
|
+
"Use an isolated install: pipx install superlocalmemory "
|
|
2125
|
+
"or uv tool install superlocalmemory",
|
|
1980
2126
|
)
|
|
1981
2127
|
else:
|
|
1982
2128
|
_check(
|
|
@@ -1998,7 +2144,7 @@ def cmd_doctor(args: Namespace) -> None:
|
|
|
1998
2144
|
"disabled (optimize.json enabled=false) — caching/compression not active"
|
|
1999
2145
|
+ (f" [{_error}]" if _error else ""),
|
|
2000
2146
|
fix="Enable via dashboard Optimize tab or set enabled=true"
|
|
2001
|
-
" in
|
|
2147
|
+
f" in {slm_home / 'optimize.json'}",
|
|
2002
2148
|
)
|
|
2003
2149
|
else:
|
|
2004
2150
|
_surfaces = []
|
|
@@ -2063,14 +2209,85 @@ def cmd_doctor(args: Namespace) -> None:
|
|
|
2063
2209
|
|
|
2064
2210
|
def cmd_trace(args: Namespace) -> None:
|
|
2065
2211
|
"""Recall with per-channel score breakdown."""
|
|
2212
|
+
use_json = getattr(args, 'json', False)
|
|
2213
|
+
limit = getattr(args, 'limit', 10)
|
|
2214
|
+
|
|
2215
|
+
# Trace must use the same daemon-owned engine as recall. A direct CLI
|
|
2216
|
+
# engine cannot attach to the machine-wide embedding worker already owned
|
|
2217
|
+
# by the daemon; it then silently loses semantic, Hopfield, and spreading
|
|
2218
|
+
# activation channels. The daemon trace route keeps the loaded model,
|
|
2219
|
+
# graph, and retrieval state intact while returning the same score detail.
|
|
2220
|
+
try:
|
|
2221
|
+
from superlocalmemory.cli.daemon import (
|
|
2222
|
+
daemon_request, ensure_daemon, is_daemon_running,
|
|
2223
|
+
)
|
|
2224
|
+
if is_daemon_running() or ensure_daemon():
|
|
2225
|
+
result = daemon_request(
|
|
2226
|
+
"POST", "/api/v3/recall/trace",
|
|
2227
|
+
{"query": args.query, "limit": limit},
|
|
2228
|
+
)
|
|
2229
|
+
if result and "results" in result:
|
|
2230
|
+
if use_json:
|
|
2231
|
+
from superlocalmemory.cli.json_output import json_print
|
|
2232
|
+
json_print("trace", data={
|
|
2233
|
+
"query": result.get("query", args.query),
|
|
2234
|
+
"query_type": result.get("query_type", "unknown"),
|
|
2235
|
+
"retrieval_time_ms": round(
|
|
2236
|
+
float(result.get("retrieval_time_ms", 0)), 1,
|
|
2237
|
+
),
|
|
2238
|
+
"results": result["results"],
|
|
2239
|
+
"count": len(result["results"]),
|
|
2240
|
+
"no_confident_match": bool(
|
|
2241
|
+
result.get("no_confident_match", False),
|
|
2242
|
+
),
|
|
2243
|
+
"score_contract_version": result.get(
|
|
2244
|
+
"score_contract_version", "2",
|
|
2245
|
+
),
|
|
2246
|
+
"calibration_status": result.get(
|
|
2247
|
+
"calibration_status", "uncalibrated",
|
|
2248
|
+
),
|
|
2249
|
+
"calibration_id": result.get("calibration_id"),
|
|
2250
|
+
"answer_confidence": result.get("answer_confidence"),
|
|
2251
|
+
"abstained": bool(result.get("abstained", False)),
|
|
2252
|
+
"abstention_reason": result.get("abstention_reason"),
|
|
2253
|
+
}, next_actions=[
|
|
2254
|
+
{
|
|
2255
|
+
"command": "slm recall '<query>' --json",
|
|
2256
|
+
"description": "Standard recall",
|
|
2257
|
+
},
|
|
2258
|
+
])
|
|
2259
|
+
return
|
|
2260
|
+
print(f"Query: {result.get('query', args.query)}")
|
|
2261
|
+
print(
|
|
2262
|
+
f"Type: {result.get('query_type', 'unknown')} | Time: "
|
|
2263
|
+
f"{float(result.get('retrieval_time_ms', 0)):.0f}ms"
|
|
2264
|
+
)
|
|
2265
|
+
print(f"Results: {len(result['results'])}")
|
|
2266
|
+
for i, item in enumerate(result["results"], 1):
|
|
2267
|
+
print(
|
|
2268
|
+
f"\n {i}. [relevance "
|
|
2269
|
+
f"{float(item.get('relevance_score', item.get('score', 0))):.3f}] "
|
|
2270
|
+
f"{str(item.get('content', ''))[:100]}"
|
|
2271
|
+
)
|
|
2272
|
+
if item.get("ranking_score") is not None:
|
|
2273
|
+
print(
|
|
2274
|
+
" ranking utility: "
|
|
2275
|
+
f"{float(item['ranking_score']):.6f}"
|
|
2276
|
+
)
|
|
2277
|
+
for channel, score in (item.get("channel_scores") or {}).items():
|
|
2278
|
+
print(f" {channel}: {float(score):.3f}")
|
|
2279
|
+
return
|
|
2280
|
+
except Exception:
|
|
2281
|
+
# The direct path remains the offline escape hatch when a daemon is
|
|
2282
|
+
# unavailable or a local transport error occurs.
|
|
2283
|
+
pass
|
|
2284
|
+
|
|
2066
2285
|
from superlocalmemory.core.engine import MemoryEngine
|
|
2067
2286
|
from superlocalmemory.core.config import SLMConfig
|
|
2068
|
-
|
|
2069
|
-
use_json = getattr(args, 'json', False)
|
|
2070
2287
|
try:
|
|
2071
2288
|
config = SLMConfig.load()
|
|
2072
2289
|
engine = MemoryEngine(config)
|
|
2073
|
-
|
|
2290
|
+
engine.initialize()
|
|
2074
2291
|
response = engine.recall(args.query, limit=limit)
|
|
2075
2292
|
except Exception as exc:
|
|
2076
2293
|
if use_json:
|
|
@@ -2081,22 +2298,23 @@ def cmd_trace(args: Namespace) -> None:
|
|
|
2081
2298
|
|
|
2082
2299
|
if use_json:
|
|
2083
2300
|
from superlocalmemory.cli.json_output import json_print
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
items.append(item)
|
|
2301
|
+
from superlocalmemory.server.recall_serializer import (
|
|
2302
|
+
recall_response_metadata,
|
|
2303
|
+
serialize_recall_response,
|
|
2304
|
+
)
|
|
2305
|
+
items, no_confident_match = serialize_recall_response(
|
|
2306
|
+
response,
|
|
2307
|
+
limit=limit,
|
|
2308
|
+
per_fact_max=200,
|
|
2309
|
+
total_max=max(200, limit * 200),
|
|
2310
|
+
)
|
|
2095
2311
|
json_print("trace", data={
|
|
2096
2312
|
"query": args.query,
|
|
2097
2313
|
"query_type": getattr(response, "query_type", "unknown"),
|
|
2098
2314
|
"retrieval_time_ms": round(getattr(response, "retrieval_time_ms", 0), 1),
|
|
2099
2315
|
"results": items, "count": len(items),
|
|
2316
|
+
"no_confident_match": no_confident_match,
|
|
2317
|
+
**recall_response_metadata(response),
|
|
2100
2318
|
}, next_actions=[
|
|
2101
2319
|
{"command": "slm recall '<query>' --json", "description": "Standard recall"},
|
|
2102
2320
|
])
|
|
@@ -2106,7 +2324,9 @@ def cmd_trace(args: Namespace) -> None:
|
|
|
2106
2324
|
print(f"Type: {response.query_type} | Time: {response.retrieval_time_ms:.0f}ms")
|
|
2107
2325
|
print(f"Results: {len(response.results)}")
|
|
2108
2326
|
for i, r in enumerate(response.results, 1):
|
|
2109
|
-
print(f"\n {i}. [{r.
|
|
2327
|
+
print(f"\n {i}. [relevance {r.relevance_score:.3f}] {r.fact.content[:100]}")
|
|
2328
|
+
if r.ranking_score is not None:
|
|
2329
|
+
print(f" ranking utility: {r.ranking_score:.6f}")
|
|
2110
2330
|
if hasattr(r, "channel_scores") and r.channel_scores:
|
|
2111
2331
|
for ch, sc in r.channel_scores.items():
|
|
2112
2332
|
print(f" {ch}: {sc:.3f}")
|
|
@@ -2129,11 +2349,16 @@ def cmd_mcp(_args: Namespace) -> None:
|
|
|
2129
2349
|
from superlocalmemory.infra.process_reaper import (
|
|
2130
2350
|
ReaperConfig,
|
|
2131
2351
|
find_orphans,
|
|
2352
|
+
is_mcp_server_process,
|
|
2132
2353
|
kill_orphan,
|
|
2133
2354
|
)
|
|
2134
2355
|
_reaper_cfg = ReaperConfig(orphan_age_threshold_hours=0.0)
|
|
2135
2356
|
for _orphan in find_orphans(_reaper_cfg):
|
|
2136
|
-
|
|
2357
|
+
# A unified daemon is expected to be detached from the launching
|
|
2358
|
+
# shell and can therefore have PPID 1. The MCP reaper must never
|
|
2359
|
+
# treat that healthy shared daemon as an orphaned stdio server.
|
|
2360
|
+
if is_mcp_server_process(_orphan):
|
|
2361
|
+
kill_orphan(_orphan.pid, graceful_timeout_seconds=1.0)
|
|
2137
2362
|
except Exception:
|
|
2138
2363
|
pass # Never block MCP startup on cleanup failure
|
|
2139
2364
|
|
|
@@ -2381,7 +2606,6 @@ def _cmd_init_auto(
|
|
|
2381
2606
|
from pathlib import Path
|
|
2382
2607
|
from superlocalmemory.core.config import SLMConfig
|
|
2383
2608
|
from superlocalmemory.storage.models import Mode
|
|
2384
|
-
from superlocalmemory.cli.setup_wizard import _mark_complete
|
|
2385
2609
|
|
|
2386
2610
|
# Step 1: write mode-A config (create-if-absent or --force).
|
|
2387
2611
|
# Pass slm_data_dir explicitly so env-overridden paths are respected even
|
|
@@ -2395,8 +2619,7 @@ def _cmd_init_auto(
|
|
|
2395
2619
|
sys.exit(1)
|
|
2396
2620
|
|
|
2397
2621
|
# Step 2: mark complete (write .setup-complete sentinel).
|
|
2398
|
-
# Write the sentinel directly using
|
|
2399
|
-
# _SLM_HOME resolved at import time (tests set the env var after import).
|
|
2622
|
+
# Write the sentinel directly using the already-selected namespace.
|
|
2400
2623
|
try:
|
|
2401
2624
|
import platform
|
|
2402
2625
|
import time as _time
|
|
@@ -2428,7 +2651,6 @@ def _cmd_init_auto(
|
|
|
2428
2651
|
|
|
2429
2652
|
def cmd_init(args: Namespace) -> None:
|
|
2430
2653
|
"""One-command setup: mode + hooks + IDE connect + warmup."""
|
|
2431
|
-
from pathlib import Path
|
|
2432
2654
|
from superlocalmemory.cli._lazy_init import slm_home
|
|
2433
2655
|
from superlocalmemory.core.config import SLMConfig
|
|
2434
2656
|
|
|
@@ -2529,12 +2751,49 @@ def cmd_init(args: Namespace) -> None:
|
|
|
2529
2751
|
|
|
2530
2752
|
|
|
2531
2753
|
def cmd_hooks(args: Namespace) -> None:
|
|
2532
|
-
"""Manage Claude Code
|
|
2754
|
+
"""Manage additive Claude Code or Codex memory lifecycle hooks."""
|
|
2533
2755
|
from superlocalmemory.hooks.claude_code_hooks import (
|
|
2534
2756
|
install_hooks, remove_hooks, check_status,
|
|
2535
2757
|
)
|
|
2536
2758
|
|
|
2537
2759
|
action = getattr(args, "action", "status")
|
|
2760
|
+
agent = getattr(args, "agent", "claude")
|
|
2761
|
+
dry_run = getattr(args, "dry_run", False)
|
|
2762
|
+
if agent == "codex":
|
|
2763
|
+
from superlocalmemory.hooks.codex_hooks import (
|
|
2764
|
+
install_hooks as install_codex_hooks,
|
|
2765
|
+
remove_hooks as remove_codex_hooks,
|
|
2766
|
+
check_status as check_codex_hooks,
|
|
2767
|
+
)
|
|
2768
|
+
if action == "install":
|
|
2769
|
+
result = install_codex_hooks(dry_run=dry_run)
|
|
2770
|
+
if result["success"]:
|
|
2771
|
+
prefix = "would be installed" if dry_run else "installed"
|
|
2772
|
+
print(f"SLM hooks {prefix} in Codex: {result['path']}")
|
|
2773
|
+
if result.get("hooks_added"):
|
|
2774
|
+
print(f" Hook types: {', '.join(result['hooks_added'])}")
|
|
2775
|
+
print(" Review and trust the new hooks in Codex with /hooks.")
|
|
2776
|
+
else:
|
|
2777
|
+
print(f"Installation failed: {result['errors']}")
|
|
2778
|
+
return
|
|
2779
|
+
if action == "remove":
|
|
2780
|
+
result = remove_codex_hooks(dry_run=dry_run)
|
|
2781
|
+
if result["success"]:
|
|
2782
|
+
prefix = "would be removed" if dry_run else "removed"
|
|
2783
|
+
print(f"SLM hooks {prefix} from Codex: {result['path']}")
|
|
2784
|
+
else:
|
|
2785
|
+
print(f"Removal failed: {result['errors']}")
|
|
2786
|
+
return
|
|
2787
|
+
result = check_codex_hooks()
|
|
2788
|
+
if result["installed"] is None:
|
|
2789
|
+
print(f"SLM Codex hooks: INDETERMINATE ({result['error']})")
|
|
2790
|
+
elif result["installed"]:
|
|
2791
|
+
print("SLM Codex hooks: INSTALLED")
|
|
2792
|
+
print(f" Hook types: {', '.join(result['hook_types'])}")
|
|
2793
|
+
else:
|
|
2794
|
+
print("SLM Codex hooks: NOT INSTALLED")
|
|
2795
|
+
print(" Run: slm hooks install --agent codex")
|
|
2796
|
+
return
|
|
2538
2797
|
# Gate is OFF by default. --gate opts in (for brave users).
|
|
2539
2798
|
include_gate = getattr(args, "gate", False)
|
|
2540
2799
|
|
|
@@ -2570,6 +2829,34 @@ def cmd_hooks(args: Namespace) -> None:
|
|
|
2570
2829
|
print(" Or: slm init (full setup)")
|
|
2571
2830
|
|
|
2572
2831
|
|
|
2832
|
+
def cmd_codex(args: Namespace) -> None:
|
|
2833
|
+
"""Manage explicit, SLM-owned Codex skills, agents, and lifecycle hooks."""
|
|
2834
|
+
from superlocalmemory.hooks.codex_assets import install_assets, remove_assets, status_assets
|
|
2835
|
+
from superlocalmemory.hooks.codex_hooks import install_hooks, remove_hooks, check_status
|
|
2836
|
+
|
|
2837
|
+
action, dry_run = getattr(args, "action", "status"), getattr(args, "dry_run", False)
|
|
2838
|
+
if action == "install":
|
|
2839
|
+
assets, hooks = install_assets(dry_run=dry_run), install_hooks(dry_run=dry_run)
|
|
2840
|
+
if assets.get("success") and hooks.get("success"):
|
|
2841
|
+
print(f"SLM Codex add-ons {'would be installed' if dry_run else 'installed'}: 7 skills, 2 subagents, 4 lifecycle hooks.")
|
|
2842
|
+
print("MCP wiring remains explicit: run `slm connect codex` if it is not already configured.")
|
|
2843
|
+
print("Review and trust newly installed hooks in Codex with /hooks.")
|
|
2844
|
+
else:
|
|
2845
|
+
print(f"Codex integration failed: {assets.get('errors', []) + hooks.get('errors', [])}")
|
|
2846
|
+
return
|
|
2847
|
+
if action == "remove":
|
|
2848
|
+
assets, hooks = remove_assets(dry_run=dry_run), remove_hooks(dry_run=dry_run)
|
|
2849
|
+
if assets.get("success") and hooks.get("success"):
|
|
2850
|
+
print("SLM-owned Codex add-ons removed; your MCP and non-SLM settings were left intact.")
|
|
2851
|
+
else:
|
|
2852
|
+
print(f"Codex removal failed: {assets.get('errors', []) + hooks.get('errors', [])}")
|
|
2853
|
+
return
|
|
2854
|
+
assets, hooks = status_assets(), check_status()
|
|
2855
|
+
print(f"SLM Codex add-ons: {'INSTALLED' if assets['installed'] and hooks['installed'] else 'NOT INSTALLED'}")
|
|
2856
|
+
print(f" Skills: {len(assets['skills'])}/7; subagents: {len(assets['agents'])}/2")
|
|
2857
|
+
print(f" Hooks: {', '.join(hooks.get('hook_types', [])) or 'none'}")
|
|
2858
|
+
|
|
2859
|
+
|
|
2573
2860
|
def cmd_session_context(args: Namespace) -> None:
|
|
2574
2861
|
"""Print session context (for hook scripts and piping).
|
|
2575
2862
|
|
|
@@ -2795,7 +3082,15 @@ def cmd_observe(args: Namespace) -> None:
|
|
|
2795
3082
|
engine = MemoryEngine(config)
|
|
2796
3083
|
engine.initialize()
|
|
2797
3084
|
|
|
2798
|
-
|
|
3085
|
+
from superlocalmemory.core.engine_ingestion import (
|
|
3086
|
+
canonical_store_fn,
|
|
3087
|
+
local_trusted_actor_id,
|
|
3088
|
+
)
|
|
3089
|
+
auto = AutoCapture(store_fn=canonical_store_fn(
|
|
3090
|
+
engine,
|
|
3091
|
+
source_type="cli-observe",
|
|
3092
|
+
trusted_actor_id=local_trusted_actor_id("cli"),
|
|
3093
|
+
))
|
|
2799
3094
|
decision = auto.evaluate(content)
|
|
2800
3095
|
|
|
2801
3096
|
if decision.capture:
|
|
@@ -2840,7 +3135,7 @@ def cmd_decay(args: Namespace) -> None:
|
|
|
2840
3135
|
scheduler = ForgettingScheduler(
|
|
2841
3136
|
engine._db, ebbinghaus, config.forgetting,
|
|
2842
3137
|
)
|
|
2843
|
-
result = scheduler.run_decay_cycle(pid, force=True)
|
|
3138
|
+
result = scheduler.run_decay_cycle(pid, force=True, dry_run=dry_run)
|
|
2844
3139
|
except Exception as exc:
|
|
2845
3140
|
if use_json:
|
|
2846
3141
|
from superlocalmemory.cli.json_output import json_print
|
|
@@ -2905,7 +3200,7 @@ def cmd_quantize(args: Namespace) -> None:
|
|
|
2905
3200
|
scheduler = EAPScheduler(
|
|
2906
3201
|
engine._db, ebbinghaus, qstore, config.quantization,
|
|
2907
3202
|
)
|
|
2908
|
-
result = scheduler.run_eap_cycle(pid)
|
|
3203
|
+
result = scheduler.run_eap_cycle(pid, dry_run=dry_run)
|
|
2909
3204
|
except Exception as exc:
|
|
2910
3205
|
if use_json:
|
|
2911
3206
|
from superlocalmemory.cli.json_output import json_print
|