superlocalmemory 4.1.13 → 4.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/CHANGELOG.md +103 -0
- package/README.md +3 -3
- package/package.json +1 -8
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/CLAUDE.md +3 -3
- package/plugin/agents/slm-governance-advisor.md +1 -1
- package/plugin/agents/slm-loop-runner.md +1 -1
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/agents/slm-optimize-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-cache/SKILL.md +1 -1
- package/plugin/skills/slm-compress/SKILL.md +1 -1
- package/plugin/skills/slm-governance/SKILL.md +1 -1
- package/plugin/skills/slm-graph/SKILL.md +1 -1
- package/plugin/skills/slm-loop/SKILL.md +1 -1
- package/plugin/skills/slm-mesh/SKILL.md +1 -1
- package/plugin/skills/slm-profile/SKILL.md +1 -1
- package/plugin/skills/slm-recall/SKILL.md +1 -1
- package/plugin/skills/slm-remember/SKILL.md +1 -1
- package/plugin/skills/slm-scope/SKILL.md +1 -1
- package/plugin/skills/slm-session/SKILL.md +1 -1
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/plugin-src/agents/slm-memory-advisor.md +1 -1
- package/plugin-src/agents/slm-optimize-advisor.md +1 -1
- package/plugin-src/rules/AGENTS.md +1 -1
- package/plugin-src/skills/slm-cache/SKILL.md +1 -1
- package/plugin-src/skills/slm-compress/SKILL.md +1 -1
- package/plugin-src/skills/slm-governance/SKILL.md +1 -1
- package/plugin-src/skills/slm-graph/SKILL.md +1 -1
- package/plugin-src/skills/slm-loop/SKILL.md +1 -1
- package/plugin-src/skills/slm-mesh/SKILL.md +1 -1
- package/plugin-src/skills/slm-profile/SKILL.md +1 -1
- package/plugin-src/skills/slm-recall/SKILL.md +1 -1
- package/plugin-src/skills/slm-remember/SKILL.md +1 -1
- package/plugin-src/skills/slm-scope/SKILL.md +1 -1
- package/plugin-src/skills/slm-session/SKILL.md +1 -1
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/scripts/postinstall.js +71 -2
- package/pyproject.toml +0 -250
- package/src/superlocalmemory/__init__.py +0 -82
- package/src/superlocalmemory/access/__init__.py +0 -3
- package/src/superlocalmemory/access/rbac.py +0 -575
- package/src/superlocalmemory/attribution/__init__.py +0 -9
- package/src/superlocalmemory/attribution/signer.py +0 -173
- package/src/superlocalmemory/attribution/watermark.py +0 -189
- package/src/superlocalmemory/brain/__init__.py +0 -5
- package/src/superlocalmemory/brain/truth.py +0 -418
- package/src/superlocalmemory/cli/__init__.py +0 -5
- package/src/superlocalmemory/cli/__main__.py +0 -17
- package/src/superlocalmemory/cli/_lazy_init.py +0 -115
- package/src/superlocalmemory/cli/cache_cmd.py +0 -198
- package/src/superlocalmemory/cli/commands.py +0 -4710
- package/src/superlocalmemory/cli/compress_cmd.py +0 -151
- package/src/superlocalmemory/cli/context_commands.py +0 -193
- package/src/superlocalmemory/cli/daemon.py +0 -909
- package/src/superlocalmemory/cli/db_migrate.py +0 -150
- package/src/superlocalmemory/cli/diagnostics_cmd.py +0 -101
- package/src/superlocalmemory/cli/escape_hatch.py +0 -220
- package/src/superlocalmemory/cli/evidence_cmd.py +0 -103
- package/src/superlocalmemory/cli/gdpr_cmd.py +0 -792
- package/src/superlocalmemory/cli/gdpr_io.py +0 -109
- package/src/superlocalmemory/cli/help_cmd.py +0 -197
- package/src/superlocalmemory/cli/host_upgrades.py +0 -189
- package/src/superlocalmemory/cli/ingest_cmd.py +0 -327
- package/src/superlocalmemory/cli/json_output.py +0 -81
- package/src/superlocalmemory/cli/loop_cmd.py +0 -187
- package/src/superlocalmemory/cli/main.py +0 -1145
- package/src/superlocalmemory/cli/mesh_cmd.py +0 -38
- package/src/superlocalmemory/cli/migrate_cmd.py +0 -55
- package/src/superlocalmemory/cli/ops_cmd.py +0 -281
- package/src/superlocalmemory/cli/optimize_cmd.py +0 -179
- package/src/superlocalmemory/cli/optimize_constants.py +0 -31
- package/src/superlocalmemory/cli/pending_store.py +0 -296
- package/src/superlocalmemory/cli/proxy_cmd.py +0 -108
- package/src/superlocalmemory/cli/scale_engine_cmd.py +0 -56
- package/src/superlocalmemory/cli/service_installer.py +0 -373
- package/src/superlocalmemory/cli/setup_wizard.py +0 -1162
- package/src/superlocalmemory/cli/summary_cmd.py +0 -215
- package/src/superlocalmemory/cli/version_banner.py +0 -202
- package/src/superlocalmemory/cli/wizard_v3426_options.py +0 -129
- package/src/superlocalmemory/code_graph/__init__.py +0 -46
- package/src/superlocalmemory/code_graph/blast_radius.py +0 -177
- package/src/superlocalmemory/code_graph/bridge/__init__.py +0 -36
- package/src/superlocalmemory/code_graph/bridge/entity_resolver.py +0 -490
- package/src/superlocalmemory/code_graph/bridge/event_listeners.py +0 -206
- package/src/superlocalmemory/code_graph/bridge/fact_enricher.py +0 -159
- package/src/superlocalmemory/code_graph/bridge/hebbian_linker.py +0 -170
- package/src/superlocalmemory/code_graph/bridge/maintenance.py +0 -220
- package/src/superlocalmemory/code_graph/bridge/temporal_checker.py +0 -152
- package/src/superlocalmemory/code_graph/changes.py +0 -363
- package/src/superlocalmemory/code_graph/communities.py +0 -299
- package/src/superlocalmemory/code_graph/config.py +0 -154
- package/src/superlocalmemory/code_graph/database.py +0 -526
- package/src/superlocalmemory/code_graph/extractors/__init__.py +0 -95
- package/src/superlocalmemory/code_graph/extractors/python.py +0 -413
- package/src/superlocalmemory/code_graph/extractors/typescript.py +0 -556
- package/src/superlocalmemory/code_graph/flows.py +0 -350
- package/src/superlocalmemory/code_graph/git_hooks.py +0 -226
- package/src/superlocalmemory/code_graph/graph_engine.py +0 -295
- package/src/superlocalmemory/code_graph/graph_store.py +0 -335
- package/src/superlocalmemory/code_graph/incremental.py +0 -200
- package/src/superlocalmemory/code_graph/models.py +0 -130
- package/src/superlocalmemory/code_graph/parser.py +0 -687
- package/src/superlocalmemory/code_graph/resolver.py +0 -321
- package/src/superlocalmemory/code_graph/search.py +0 -460
- package/src/superlocalmemory/code_graph/service.py +0 -95
- package/src/superlocalmemory/code_graph/watcher.py +0 -207
- package/src/superlocalmemory/compliance/__init__.py +0 -0
- package/src/superlocalmemory/compliance/abac.py +0 -204
- package/src/superlocalmemory/compliance/audit.py +0 -385
- package/src/superlocalmemory/compliance/eu_ai_act.py +0 -101
- package/src/superlocalmemory/compliance/gdpr.py +0 -1479
- package/src/superlocalmemory/compliance/lifecycle.py +0 -158
- package/src/superlocalmemory/compliance/retention.py +0 -415
- package/src/superlocalmemory/compliance/scheduler.py +0 -217
- package/src/superlocalmemory/contracts/__init__.py +0 -1
- package/src/superlocalmemory/contracts/schemas/agent-experience-v1.schema.json +0 -92
- package/src/superlocalmemory/contracts/schemas/agent-integration-contract-v2.schema.json +0 -46
- package/src/superlocalmemory/contracts/schemas/cognitive-turn-receipt-v1.schema.json +0 -59
- package/src/superlocalmemory/contracts/v402.py +0 -62
- package/src/superlocalmemory/core/__init__.py +0 -0
- package/src/superlocalmemory/core/actor_context.py +0 -166
- package/src/superlocalmemory/core/admission.py +0 -769
- package/src/superlocalmemory/core/backend_orchestrator.py +0 -637
- package/src/superlocalmemory/core/block_hygiene.py +0 -147
- package/src/superlocalmemory/core/community_summary.py +0 -267
- package/src/superlocalmemory/core/component_healer.py +0 -144
- package/src/superlocalmemory/core/component_registry.py +0 -514
- package/src/superlocalmemory/core/config.py +0 -2204
- package/src/superlocalmemory/core/consolidation_engine.py +0 -983
- package/src/superlocalmemory/core/context_cache.py +0 -574
- package/src/superlocalmemory/core/derivation_lineage.py +0 -246
- package/src/superlocalmemory/core/embedding_worker.py +0 -208
- package/src/superlocalmemory/core/embeddings.py +0 -1052
- package/src/superlocalmemory/core/engine.py +0 -1395
- package/src/superlocalmemory/core/engine_capabilities.py +0 -24
- package/src/superlocalmemory/core/engine_ingestion.py +0 -983
- package/src/superlocalmemory/core/engine_lock.py +0 -75
- package/src/superlocalmemory/core/engine_wiring.py +0 -776
- package/src/superlocalmemory/core/entity_community.py +0 -178
- package/src/superlocalmemory/core/error_envelope.py +0 -60
- package/src/superlocalmemory/core/evidence_bundle.py +0 -528
- package/src/superlocalmemory/core/fact_consolidator.py +0 -812
- package/src/superlocalmemory/core/file_lock.py +0 -92
- package/src/superlocalmemory/core/graph_analyzer.py +0 -456
- package/src/superlocalmemory/core/graph_metrics.py +0 -597
- package/src/superlocalmemory/core/graph_pruner.py +0 -939
- package/src/superlocalmemory/core/health_monitor.py +0 -338
- package/src/superlocalmemory/core/hooks.py +0 -65
- package/src/superlocalmemory/core/ingest_gate.py +0 -133
- package/src/superlocalmemory/core/ingest_policy.py +0 -38
- package/src/superlocalmemory/core/ingestion_command.py +0 -1042
- package/src/superlocalmemory/core/injection.py +0 -434
- package/src/superlocalmemory/core/install_detector.py +0 -131
- package/src/superlocalmemory/core/key_expander.py +0 -138
- package/src/superlocalmemory/core/lifecycle_state.py +0 -153
- package/src/superlocalmemory/core/maintenance.py +0 -777
- package/src/superlocalmemory/core/maintenance_scheduler.py +0 -507
- package/src/superlocalmemory/core/materialization_control.py +0 -20
- package/src/superlocalmemory/core/mcp_embedder_proxy.py +0 -89
- package/src/superlocalmemory/core/memory_health.py +0 -266
- package/src/superlocalmemory/core/mode_capability.py +0 -111
- package/src/superlocalmemory/core/modes.py +0 -168
- package/src/superlocalmemory/core/mutations.py +0 -688
- package/src/superlocalmemory/core/ollama_embedder.py +0 -266
- package/src/superlocalmemory/core/ollama_validator.py +0 -315
- package/src/superlocalmemory/core/operation_policy.py +0 -92
- package/src/superlocalmemory/core/operation_policy_registry.py +0 -542
- package/src/superlocalmemory/core/operation_request.py +0 -127
- package/src/superlocalmemory/core/ops_remediation.py +0 -542
- package/src/superlocalmemory/core/pii.py +0 -105
- package/src/superlocalmemory/core/platform_utils.py +0 -138
- package/src/superlocalmemory/core/profiles.py +0 -234
- package/src/superlocalmemory/core/progressive_abstraction.py +0 -208
- package/src/superlocalmemory/core/projection_drain.py +0 -380
- package/src/superlocalmemory/core/queue_consumer.py +0 -168
- package/src/superlocalmemory/core/ram_lock.py +0 -160
- package/src/superlocalmemory/core/rate_limit.py +0 -151
- package/src/superlocalmemory/core/recall_gate.py +0 -95
- package/src/superlocalmemory/core/recall_pipeline.py +0 -1337
- package/src/superlocalmemory/core/recall_queue.py +0 -377
- package/src/superlocalmemory/core/recall_worker.py +0 -414
- package/src/superlocalmemory/core/registry.py +0 -121
- package/src/superlocalmemory/core/remember_admission.py +0 -161
- package/src/superlocalmemory/core/remember_runtime.py +0 -1190
- package/src/superlocalmemory/core/remote_mode.py +0 -214
- package/src/superlocalmemory/core/reranker_worker.py +0 -338
- package/src/superlocalmemory/core/safe_fs.py +0 -108
- package/src/superlocalmemory/core/scale_autopromote.py +0 -196
- package/src/superlocalmemory/core/scale_engine.py +0 -915
- package/src/superlocalmemory/core/score_contract.py +0 -82
- package/src/superlocalmemory/core/security_primitives.py +0 -672
- package/src/superlocalmemory/core/session_identity.py +0 -98
- package/src/superlocalmemory/core/shadow_router.py +0 -319
- package/src/superlocalmemory/core/slm_disabled.py +0 -85
- package/src/superlocalmemory/core/status_contract.py +0 -108
- package/src/superlocalmemory/core/store_pipeline.py +0 -1404
- package/src/superlocalmemory/core/summarizer.py +0 -200
- package/src/superlocalmemory/core/tier_manager.py +0 -461
- package/src/superlocalmemory/core/topic_signature.py +0 -156
- package/src/superlocalmemory/core/transactions/__init__.py +0 -78
- package/src/superlocalmemory/core/transactions/concrete_owners.py +0 -604
- package/src/superlocalmemory/core/transactions/erasure.py +0 -825
- package/src/superlocalmemory/core/transactions/manifest.py +0 -255
- package/src/superlocalmemory/core/transactions/manifest_key.py +0 -155
- package/src/superlocalmemory/core/transactions/obligations.py +0 -272
- package/src/superlocalmemory/core/transactions/owners.py +0 -114
- package/src/superlocalmemory/core/transactions/reconciler.py +0 -285
- package/src/superlocalmemory/core/transactions/service.py +0 -330
- package/src/superlocalmemory/core/worker_pool.py +0 -377
- package/src/superlocalmemory/core/working_memory.py +0 -288
- package/src/superlocalmemory/dynamics/__init__.py +0 -0
- package/src/superlocalmemory/dynamics/activation_guided_quantization.py +0 -374
- package/src/superlocalmemory/dynamics/eap_scheduler.py +0 -294
- package/src/superlocalmemory/dynamics/ebbinghaus_langevin_coupling.py +0 -171
- package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +0 -227
- package/src/superlocalmemory/encoding/__init__.py +0 -0
- package/src/superlocalmemory/encoding/auto_linker.py +0 -308
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +0 -899
- package/src/superlocalmemory/encoding/consolidator.py +0 -472
- package/src/superlocalmemory/encoding/context_generator.py +0 -175
- package/src/superlocalmemory/encoding/emotional.py +0 -189
- package/src/superlocalmemory/encoding/entity_reflexion.py +0 -200
- package/src/superlocalmemory/encoding/entity_resolver.py +0 -687
- package/src/superlocalmemory/encoding/entropy_gate.py +0 -101
- package/src/superlocalmemory/encoding/fact_extractor.py +0 -877
- package/src/superlocalmemory/encoding/foresight.py +0 -93
- package/src/superlocalmemory/encoding/graph_builder.py +0 -346
- package/src/superlocalmemory/encoding/observation_builder.py +0 -177
- package/src/superlocalmemory/encoding/prospective_markers.py +0 -262
- package/src/superlocalmemory/encoding/scene_builder.py +0 -410
- package/src/superlocalmemory/encoding/signal_inference.py +0 -90
- package/src/superlocalmemory/encoding/temporal_parser.py +0 -432
- package/src/superlocalmemory/encoding/temporal_validator.py +0 -572
- package/src/superlocalmemory/encoding/type_router.py +0 -237
- package/src/superlocalmemory/evaluation/__init__.py +0 -13
- package/src/superlocalmemory/evaluation/calibration.py +0 -308
- package/src/superlocalmemory/evolution/__init__.py +0 -29
- package/src/superlocalmemory/evolution/blind_verifier.py +0 -122
- package/src/superlocalmemory/evolution/budget.py +0 -356
- package/src/superlocalmemory/evolution/evolution_store.py +0 -619
- package/src/superlocalmemory/evolution/llm_dispatch.py +0 -559
- package/src/superlocalmemory/evolution/model_selection.py +0 -175
- package/src/superlocalmemory/evolution/mutation_generator.py +0 -226
- package/src/superlocalmemory/evolution/skill_activator.py +0 -270
- package/src/superlocalmemory/evolution/skill_evolver.py +0 -928
- package/src/superlocalmemory/evolution/triggers.py +0 -376
- package/src/superlocalmemory/evolution/types.py +0 -114
- package/src/superlocalmemory/graph/__init__.py +0 -9
- package/src/superlocalmemory/graph/cozo_adjacency.py +0 -122
- package/src/superlocalmemory/graph/cozo_backend.py +0 -751
- package/src/superlocalmemory/hooks/__init__.py +0 -3
- package/src/superlocalmemory/hooks/_outcome_common.py +0 -523
- package/src/superlocalmemory/hooks/adapter_base.py +0 -347
- package/src/superlocalmemory/hooks/antigravity_adapter.py +0 -171
- package/src/superlocalmemory/hooks/auto_capture.py +0 -133
- package/src/superlocalmemory/hooks/auto_invoker.py +0 -521
- package/src/superlocalmemory/hooks/auto_parameterize.py +0 -147
- package/src/superlocalmemory/hooks/auto_recall.py +0 -191
- package/src/superlocalmemory/hooks/auto_recall_hook.py +0 -251
- package/src/superlocalmemory/hooks/before_web_hook.py +0 -131
- package/src/superlocalmemory/hooks/claude_code_hooks.py +0 -637
- package/src/superlocalmemory/hooks/codex_assets.py +0 -251
- package/src/superlocalmemory/hooks/codex_hooks.py +0 -186
- package/src/superlocalmemory/hooks/context_payload.py +0 -311
- package/src/superlocalmemory/hooks/copilot_adapter.py +0 -208
- package/src/superlocalmemory/hooks/cross_platform_connector.py +0 -90
- package/src/superlocalmemory/hooks/cursor_adapter.py +0 -173
- package/src/superlocalmemory/hooks/hook_daemon.py +0 -295
- package/src/superlocalmemory/hooks/hook_handlers.py +0 -822
- package/src/superlocalmemory/hooks/ide_connector.py +0 -246
- package/src/superlocalmemory/hooks/memory_protocol.py +0 -158
- package/src/superlocalmemory/hooks/portable_kit.py +0 -755
- package/src/superlocalmemory/hooks/post_tool_async_hook.py +0 -183
- package/src/superlocalmemory/hooks/post_tool_outcome_hook.py +0 -351
- package/src/superlocalmemory/hooks/prewarm_auth.py +0 -187
- package/src/superlocalmemory/hooks/rules_engine.py +0 -99
- package/src/superlocalmemory/hooks/session_registry.py +0 -330
- package/src/superlocalmemory/hooks/stop_outcome_hook.py +0 -138
- package/src/superlocalmemory/hooks/sync_loop.py +0 -114
- package/src/superlocalmemory/hooks/topic_shift_hook.py +0 -302
- package/src/superlocalmemory/hooks/user_prompt_hook.py +0 -131
- package/src/superlocalmemory/hooks/user_prompt_rehash_hook.py +0 -210
- package/src/superlocalmemory/infra/__init__.py +0 -3
- package/src/superlocalmemory/infra/auth_middleware.py +0 -145
- package/src/superlocalmemory/infra/backup.py +0 -974
- package/src/superlocalmemory/infra/backup_obligations.py +0 -423
- package/src/superlocalmemory/infra/cache_manager.py +0 -267
- package/src/superlocalmemory/infra/cloud_backup.py +0 -788
- package/src/superlocalmemory/infra/daemon_identity.py +0 -300
- package/src/superlocalmemory/infra/data_root.py +0 -238
- package/src/superlocalmemory/infra/event_bus.py +0 -637
- package/src/superlocalmemory/infra/local_diagnostics.py +0 -327
- package/src/superlocalmemory/infra/pid_manager.py +0 -193
- package/src/superlocalmemory/infra/process_identity.py +0 -180
- package/src/superlocalmemory/infra/process_reaper.py +0 -624
- package/src/superlocalmemory/infra/rate_limiter.py +0 -228
- package/src/superlocalmemory/infra/self_heal.py +0 -401
- package/src/superlocalmemory/infra/version_integrity.py +0 -229
- package/src/superlocalmemory/ingestion/__init__.py +0 -13
- package/src/superlocalmemory/ingestion/adapter_manager.py +0 -255
- package/src/superlocalmemory/ingestion/base_adapter.py +0 -171
- package/src/superlocalmemory/ingestion/calendar_adapter.py +0 -349
- package/src/superlocalmemory/ingestion/credentials.py +0 -125
- package/src/superlocalmemory/ingestion/gmail_adapter.py +0 -378
- package/src/superlocalmemory/ingestion/parsers.py +0 -100
- package/src/superlocalmemory/ingestion/transcript_adapter.py +0 -161
- package/src/superlocalmemory/integrations/__init__.py +0 -1
- package/src/superlocalmemory/integrations/bounded_loops_mcp.py +0 -431
- package/src/superlocalmemory/integrations/bounded_loops_v051.py +0 -236
- package/src/superlocalmemory/learning/__init__.py +0 -0
- package/src/superlocalmemory/learning/adaptive.py +0 -172
- package/src/superlocalmemory/learning/arm_catalog.py +0 -97
- package/src/superlocalmemory/learning/assertion_miner.py +0 -403
- package/src/superlocalmemory/learning/bandit.py +0 -654
- package/src/superlocalmemory/learning/bandit_cache.py +0 -131
- package/src/superlocalmemory/learning/behavioral.py +0 -542
- package/src/superlocalmemory/learning/bootstrap.py +0 -298
- package/src/superlocalmemory/learning/consolidation_cycle.py +0 -398
- package/src/superlocalmemory/learning/consolidation_quantization_worker.py +0 -115
- package/src/superlocalmemory/learning/consolidation_worker.py +0 -261
- package/src/superlocalmemory/learning/cross_project.py +0 -408
- package/src/superlocalmemory/learning/database.py +0 -698
- package/src/superlocalmemory/learning/dedup_hnsw.py +0 -413
- package/src/superlocalmemory/learning/engagement.py +0 -487
- package/src/superlocalmemory/learning/engagement_features.py +0 -279
- package/src/superlocalmemory/learning/ensemble.py +0 -309
- package/src/superlocalmemory/learning/entity_compiler.py +0 -356
- package/src/superlocalmemory/learning/fact_outcome_joins.py +0 -207
- package/src/superlocalmemory/learning/features.py +0 -138
- package/src/superlocalmemory/learning/feedback.py +0 -724
- package/src/superlocalmemory/learning/forgetting_scheduler.py +0 -375
- package/src/superlocalmemory/learning/hnsw_dedup.py +0 -69
- package/src/superlocalmemory/learning/labeler.py +0 -85
- package/src/superlocalmemory/learning/legacy_migration.py +0 -316
- package/src/superlocalmemory/learning/lightgbm_subprocess.py +0 -236
- package/src/superlocalmemory/learning/memory_merge.py +0 -175
- package/src/superlocalmemory/learning/model_cache.py +0 -267
- package/src/superlocalmemory/learning/model_rollback.py +0 -281
- package/src/superlocalmemory/learning/outcome_queue.py +0 -306
- package/src/superlocalmemory/learning/outcomes.py +0 -286
- package/src/superlocalmemory/learning/pattern_miner.py +0 -465
- package/src/superlocalmemory/learning/pattern_miner_constants.py +0 -90
- package/src/superlocalmemory/learning/pcos.py +0 -291
- package/src/superlocalmemory/learning/project_context.py +0 -366
- package/src/superlocalmemory/learning/propensity.py +0 -131
- package/src/superlocalmemory/learning/ranker.py +0 -300
- package/src/superlocalmemory/learning/ranker_common.py +0 -163
- package/src/superlocalmemory/learning/ranker_retrain_legacy.py +0 -210
- package/src/superlocalmemory/learning/ranker_retrain_online.py +0 -423
- package/src/superlocalmemory/learning/reward.py +0 -888
- package/src/superlocalmemory/learning/reward_archive.py +0 -223
- package/src/superlocalmemory/learning/reward_boost.py +0 -211
- package/src/superlocalmemory/learning/reward_from_outcomes.py +0 -365
- package/src/superlocalmemory/learning/reward_model.py +0 -144
- package/src/superlocalmemory/learning/reward_proxy.py +0 -578
- package/src/superlocalmemory/learning/shadow_test.py +0 -524
- package/src/superlocalmemory/learning/signal_kinds.py +0 -79
- package/src/superlocalmemory/learning/signal_worker.py +0 -268
- package/src/superlocalmemory/learning/signals.py +0 -646
- package/src/superlocalmemory/learning/skill_performance_miner.py +0 -422
- package/src/superlocalmemory/learning/source_quality.py +0 -828
- package/src/superlocalmemory/learning/trigram_index.py +0 -548
- package/src/superlocalmemory/learning/workflows.py +0 -309
- package/src/superlocalmemory/llm/__init__.py +0 -0
- package/src/superlocalmemory/llm/backbone.py +0 -364
- package/src/superlocalmemory/loops/__init__.py +0 -58
- package/src/superlocalmemory/loops/budget.py +0 -58
- package/src/superlocalmemory/loops/engine.py +0 -174
- package/src/superlocalmemory/loops/ledger.py +0 -298
- package/src/superlocalmemory/loops/models.py +0 -152
- package/src/superlocalmemory/loops/rules.py +0 -52
- package/src/superlocalmemory/math/__init__.py +0 -0
- package/src/superlocalmemory/math/ebbinghaus.py +0 -352
- package/src/superlocalmemory/math/fisher.py +0 -356
- package/src/superlocalmemory/math/fisher_quantized.py +0 -255
- package/src/superlocalmemory/math/hopfield.py +0 -282
- package/src/superlocalmemory/math/langevin.py +0 -411
- package/src/superlocalmemory/math/polar_quant.py +0 -414
- package/src/superlocalmemory/math/qjl.py +0 -115
- package/src/superlocalmemory/math/sheaf.py +0 -261
- package/src/superlocalmemory/math/turbo_quant.py +0 -318
- package/src/superlocalmemory/mcp/__init__.py +0 -0
- package/src/superlocalmemory/mcp/_daemon_proxy.py +0 -203
- package/src/superlocalmemory/mcp/_pool_adapter.py +0 -181
- package/src/superlocalmemory/mcp/_stdin_guard.py +0 -60
- package/src/superlocalmemory/mcp/agent_context.py +0 -115
- package/src/superlocalmemory/mcp/cli_fallback.py +0 -602
- package/src/superlocalmemory/mcp/http_transport.py +0 -85
- package/src/superlocalmemory/mcp/profiles.py +0 -154
- package/src/superlocalmemory/mcp/resources.py +0 -281
- package/src/superlocalmemory/mcp/server.py +0 -480
- package/src/superlocalmemory/mcp/session_binding.py +0 -98
- package/src/superlocalmemory/mcp/shared.py +0 -112
- package/src/superlocalmemory/mcp/tools.py +0 -18
- package/src/superlocalmemory/mcp/tools_active.py +0 -958
- package/src/superlocalmemory/mcp/tools_brain.py +0 -298
- package/src/superlocalmemory/mcp/tools_code_graph.py +0 -1717
- package/src/superlocalmemory/mcp/tools_context.py +0 -239
- package/src/superlocalmemory/mcp/tools_core.py +0 -1112
- package/src/superlocalmemory/mcp/tools_evolution.py +0 -343
- package/src/superlocalmemory/mcp/tools_learning.py +0 -393
- package/src/superlocalmemory/mcp/tools_loops.py +0 -345
- package/src/superlocalmemory/mcp/tools_mesh.py +0 -429
- package/src/superlocalmemory/mcp/tools_ops.py +0 -115
- package/src/superlocalmemory/mcp/tools_optimize.py +0 -322
- package/src/superlocalmemory/mcp/tools_summaries.py +0 -147
- package/src/superlocalmemory/mcp/tools_v28.py +0 -292
- package/src/superlocalmemory/mcp/tools_v3.py +0 -398
- package/src/superlocalmemory/mcp/tools_v33.py +0 -507
- package/src/superlocalmemory/mesh/__init__.py +0 -12
- package/src/superlocalmemory/mesh/broker.py +0 -812
- package/src/superlocalmemory/mesh/broker_security.py +0 -470
- package/src/superlocalmemory/mesh/discovery.py +0 -365
- package/src/superlocalmemory/mesh/lock_protocol.py +0 -313
- package/src/superlocalmemory/mesh/node_identity.py +0 -97
- package/src/superlocalmemory/mesh/outbox_remote.py +0 -429
- package/src/superlocalmemory/mesh/remote_sync.py +0 -829
- package/src/superlocalmemory/mesh/state_sync.py +0 -286
- package/src/superlocalmemory/migrations/__init__.py +0 -5
- package/src/superlocalmemory/migrations/v3_4_25_to_v3_4_26.py +0 -144
- package/src/superlocalmemory/optimize/NOTICE +0 -6
- package/src/superlocalmemory/optimize/__init__.py +0 -0
- package/src/superlocalmemory/optimize/adapters/__init__.py +0 -68
- package/src/superlocalmemory/optimize/adapters/_agent_registry.py +0 -120
- package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +0 -112
- package/src/superlocalmemory/optimize/adapters/openai_adapter.py +0 -122
- package/src/superlocalmemory/optimize/adapters/wrap.py +0 -228
- package/src/superlocalmemory/optimize/cache/__init__.py +0 -31
- package/src/superlocalmemory/optimize/cache/boundary_store.py +0 -488
- package/src/superlocalmemory/optimize/cache/centroid_store.py +0 -199
- package/src/superlocalmemory/optimize/cache/context_key.py +0 -67
- package/src/superlocalmemory/optimize/cache/exact.py +0 -88
- package/src/superlocalmemory/optimize/cache/invalidation.py +0 -36
- package/src/superlocalmemory/optimize/cache/key_builder.py +0 -111
- package/src/superlocalmemory/optimize/cache/manager.py +0 -737
- package/src/superlocalmemory/optimize/cache/semantic.py +0 -637
- package/src/superlocalmemory/optimize/cache/stampede.py +0 -50
- package/src/superlocalmemory/optimize/compress/__init__.py +0 -17
- package/src/superlocalmemory/optimize/compress/align.py +0 -159
- package/src/superlocalmemory/optimize/compress/ccr.py +0 -116
- package/src/superlocalmemory/optimize/compress/prose_llmlingua.py +0 -71
- package/src/superlocalmemory/optimize/compress/router.py +0 -667
- package/src/superlocalmemory/optimize/config/__init__.py +0 -56
- package/src/superlocalmemory/optimize/config/defaults.py +0 -43
- package/src/superlocalmemory/optimize/config/schema.py +0 -327
- package/src/superlocalmemory/optimize/config/store.py +0 -270
- package/src/superlocalmemory/optimize/metrics/__init__.py +0 -8
- package/src/superlocalmemory/optimize/metrics/counters.py +0 -155
- package/src/superlocalmemory/optimize/metrics/estimator.py +0 -87
- package/src/superlocalmemory/optimize/metrics/exporters.py +0 -77
- package/src/superlocalmemory/optimize/metrics/persistence.py +0 -115
- package/src/superlocalmemory/optimize/proxy/__init__.py +0 -28
- package/src/superlocalmemory/optimize/proxy/_helpers.py +0 -730
- package/src/superlocalmemory/optimize/proxy/anthropic_surface.py +0 -375
- package/src/superlocalmemory/optimize/proxy/capture.py +0 -550
- package/src/superlocalmemory/optimize/proxy/gemini_surface.py +0 -528
- package/src/superlocalmemory/optimize/proxy/lifecycle.py +0 -126
- package/src/superlocalmemory/optimize/proxy/openai_surface.py +0 -465
- package/src/superlocalmemory/optimize/proxy/server.py +0 -199
- package/src/superlocalmemory/optimize/proxy/vertex_surface.py +0 -246
- package/src/superlocalmemory/optimize/storage/__init__.py +0 -0
- package/src/superlocalmemory/optimize/storage/db.py +0 -1185
- package/src/superlocalmemory/optimize/storage/schema.py +0 -205
- package/src/superlocalmemory/parameterization/__init__.py +0 -47
- package/src/superlocalmemory/parameterization/cross_project.py +0 -12
- package/src/superlocalmemory/parameterization/pattern_extractor.py +0 -584
- package/src/superlocalmemory/parameterization/pii_filter.py +0 -106
- package/src/superlocalmemory/parameterization/prompt_injector.py +0 -219
- package/src/superlocalmemory/parameterization/prompt_lifecycle.py +0 -281
- package/src/superlocalmemory/parameterization/soft_prompt_generator.py +0 -542
- package/src/superlocalmemory/parameterization/workflow_miner.py +0 -17
- package/src/superlocalmemory/reliability/__init__.py +0 -45
- package/src/superlocalmemory/reliability/join_liveness.py +0 -301
- package/src/superlocalmemory/reliability/prior_distance.py +0 -243
- package/src/superlocalmemory/retrieval/__init__.py +0 -0
- package/src/superlocalmemory/retrieval/agentic.py +0 -367
- package/src/superlocalmemory/retrieval/ann_index.py +0 -235
- package/src/superlocalmemory/retrieval/bm25_channel.py +0 -451
- package/src/superlocalmemory/retrieval/bridge_discovery.py +0 -253
- package/src/superlocalmemory/retrieval/channel_registry.py +0 -154
- package/src/superlocalmemory/retrieval/channel_status.py +0 -117
- package/src/superlocalmemory/retrieval/engine.py +0 -1615
- package/src/superlocalmemory/retrieval/entity_channel.py +0 -994
- package/src/superlocalmemory/retrieval/forgetting_filter.py +0 -160
- package/src/superlocalmemory/retrieval/fusion.py +0 -81
- package/src/superlocalmemory/retrieval/graph_adjacency.py +0 -219
- package/src/superlocalmemory/retrieval/hopfield_channel.py +0 -465
- package/src/superlocalmemory/retrieval/profile_channel.py +0 -105
- package/src/superlocalmemory/retrieval/quantization_aware_search.py +0 -147
- package/src/superlocalmemory/retrieval/remote_reranker.py +0 -758
- package/src/superlocalmemory/retrieval/reranker.py +0 -674
- package/src/superlocalmemory/retrieval/scope_policy.py +0 -126
- package/src/superlocalmemory/retrieval/semantic_channel.py +0 -638
- package/src/superlocalmemory/retrieval/spreading.py +0 -288
- package/src/superlocalmemory/retrieval/spreading_activation.py +0 -616
- package/src/superlocalmemory/retrieval/strategy.py +0 -248
- package/src/superlocalmemory/retrieval/temporal_channel.py +0 -433
- package/src/superlocalmemory/retrieval/temporal_frame.py +0 -102
- package/src/superlocalmemory/retrieval/temporal_utils.py +0 -122
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +0 -499
- package/src/superlocalmemory/retrieval/time_window.py +0 -181
- package/src/superlocalmemory/retrieval/vector_store.py +0 -863
- package/src/superlocalmemory/server/__init__.py +0 -1
- package/src/superlocalmemory/server/api.py +0 -310
- package/src/superlocalmemory/server/asset_versions.py +0 -171
- package/src/superlocalmemory/server/bandit_loops.py +0 -158
- package/src/superlocalmemory/server/config_file.py +0 -90
- package/src/superlocalmemory/server/consolidation_runner.py +0 -140
- package/src/superlocalmemory/server/egress_policy.py +0 -258
- package/src/superlocalmemory/server/loopback.py +0 -85
- package/src/superlocalmemory/server/middleware/__init__.py +0 -11
- package/src/superlocalmemory/server/middleware/security_headers.py +0 -144
- package/src/superlocalmemory/server/origin.py +0 -55
- package/src/superlocalmemory/server/profile_runtime.py +0 -515
- package/src/superlocalmemory/server/rbac_enforce.py +0 -194
- package/src/superlocalmemory/server/recall_health.py +0 -343
- package/src/superlocalmemory/server/recall_serializer.py +0 -320
- package/src/superlocalmemory/server/route_mutations.py +0 -104
- package/src/superlocalmemory/server/routes/__init__.py +0 -4
- package/src/superlocalmemory/server/routes/abstraction.py +0 -314
- package/src/superlocalmemory/server/routes/adapters.py +0 -63
- package/src/superlocalmemory/server/routes/agents.py +0 -303
- package/src/superlocalmemory/server/routes/backup.py +0 -869
- package/src/superlocalmemory/server/routes/behavioral.py +0 -659
- package/src/superlocalmemory/server/routes/brain.py +0 -1892
- package/src/superlocalmemory/server/routes/chat.py +0 -393
- package/src/superlocalmemory/server/routes/compliance.py +0 -533
- package/src/superlocalmemory/server/routes/config_api.py +0 -703
- package/src/superlocalmemory/server/routes/data_io.py +0 -329
- package/src/superlocalmemory/server/routes/entity.py +0 -237
- package/src/superlocalmemory/server/routes/events.py +0 -214
- package/src/superlocalmemory/server/routes/evolution.py +0 -510
- package/src/superlocalmemory/server/routes/helpers.py +0 -499
- package/src/superlocalmemory/server/routes/ingest.py +0 -137
- package/src/superlocalmemory/server/routes/insights.py +0 -366
- package/src/superlocalmemory/server/routes/learning.py +0 -834
- package/src/superlocalmemory/server/routes/learning_telemetry.py +0 -154
- package/src/superlocalmemory/server/routes/lifecycle.py +0 -184
- package/src/superlocalmemory/server/routes/memories.py +0 -1661
- package/src/superlocalmemory/server/routes/mesh.py +0 -517
- package/src/superlocalmemory/server/routes/mesh_lock.py +0 -54
- package/src/superlocalmemory/server/routes/mesh_state.py +0 -63
- package/src/superlocalmemory/server/routes/optimize.py +0 -197
- package/src/superlocalmemory/server/routes/prewarm.py +0 -173
- package/src/superlocalmemory/server/routes/profiles.py +0 -292
- package/src/superlocalmemory/server/routes/ratelimit.py +0 -132
- package/src/superlocalmemory/server/routes/rbac.py +0 -366
- package/src/superlocalmemory/server/routes/stats.py +0 -385
- package/src/superlocalmemory/server/routes/tiers.py +0 -222
- package/src/superlocalmemory/server/routes/timeline.py +0 -258
- package/src/superlocalmemory/server/routes/token.py +0 -90
- package/src/superlocalmemory/server/routes/v3_api.py +0 -3023
- package/src/superlocalmemory/server/routes/ws.py +0 -171
- package/src/superlocalmemory/server/security_middleware.py +0 -89
- package/src/superlocalmemory/server/ui.py +0 -354
- package/src/superlocalmemory/server/unified_daemon.py +0 -6326
- package/src/superlocalmemory/server/write_identity.py +0 -195
- package/src/superlocalmemory/storage/__init__.py +0 -0
- package/src/superlocalmemory/storage/_migration_internals.py +0 -638
- package/src/superlocalmemory/storage/_schema_version.py +0 -174
- package/src/superlocalmemory/storage/access_log.py +0 -170
- package/src/superlocalmemory/storage/admission_codec.py +0 -129
- package/src/superlocalmemory/storage/admission_journal.py +0 -843
- package/src/superlocalmemory/storage/agent_experience.py +0 -546
- package/src/superlocalmemory/storage/backup.py +0 -531
- package/src/superlocalmemory/storage/correction_cases.py +0 -670
- package/src/superlocalmemory/storage/database.py +0 -3180
- package/src/superlocalmemory/storage/deferred_writes.py +0 -209
- package/src/superlocalmemory/storage/embedding_codec.py +0 -200
- package/src/superlocalmemory/storage/embedding_migrator.py +0 -672
- package/src/superlocalmemory/storage/erasure_fence.py +0 -45
- package/src/superlocalmemory/storage/execution_learning.py +0 -285
- package/src/superlocalmemory/storage/external_evidence.py +0 -359
- package/src/superlocalmemory/storage/generation_fence.py +0 -63
- package/src/superlocalmemory/storage/lineage_retention.py +0 -236
- package/src/superlocalmemory/storage/logical_edges.py +0 -86
- package/src/superlocalmemory/storage/memory_write.py +0 -115
- package/src/superlocalmemory/storage/migration_runner.py +0 -895
- package/src/superlocalmemory/storage/migration_v33.py +0 -140
- package/src/superlocalmemory/storage/migrations/M001_add_signal_features_columns.py +0 -67
- package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +0 -107
- package/src/superlocalmemory/storage/migrations/M003_migration_log.py +0 -38
- package/src/superlocalmemory/storage/migrations/M004_cross_platform_sync_log.py +0 -46
- package/src/superlocalmemory/storage/migrations/M005_bandit_tables.py +0 -75
- package/src/superlocalmemory/storage/migrations/M006_action_outcomes_reward.py +0 -75
- package/src/superlocalmemory/storage/migrations/M007_pending_outcomes.py +0 -63
- package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +0 -94
- package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +0 -80
- package/src/superlocalmemory/storage/migrations/M011_archive_and_merge.py +0 -87
- package/src/superlocalmemory/storage/migrations/M012_shadow_observations.py +0 -72
- package/src/superlocalmemory/storage/migrations/M013_bi_temporal_columns.py +0 -55
- package/src/superlocalmemory/storage/migrations/M014_v345_scale_ready.py +0 -45
- package/src/superlocalmemory/storage/migrations/M015_add_pinned_column.py +0 -58
- package/src/superlocalmemory/storage/migrations/M016_add_scope_support.py +0 -120
- package/src/superlocalmemory/storage/migrations/M017_ccq_scope_column.py +0 -79
- package/src/superlocalmemory/storage/migrations/M018_ingestion_operations.py +0 -120
- package/src/superlocalmemory/storage/migrations/M019_derivation_lineage.py +0 -54
- package/src/superlocalmemory/storage/migrations/M020_model_state_integrity.py +0 -52
- package/src/superlocalmemory/storage/migrations/M021_ingestion_log_profile.py +0 -108
- package/src/superlocalmemory/storage/migrations/M022_entity_aliases_profile.py +0 -86
- package/src/superlocalmemory/storage/migrations/M023_mesh_profile_isolation.py +0 -194
- package/src/superlocalmemory/storage/migrations/M024_rbac_users_roles.py +0 -87
- package/src/superlocalmemory/storage/migrations/M025_perf_indexes.py +0 -90
- package/src/superlocalmemory/storage/migrations/M026_rbac_memberships_fk.py +0 -136
- package/src/superlocalmemory/storage/migrations/M027_transferable_patterns_profile.py +0 -163
- package/src/superlocalmemory/storage/migrations/M028_fact_entity_associations.py +0 -305
- package/src/superlocalmemory/storage/migrations/M029_behavioral_history_indexes.py +0 -137
- package/src/superlocalmemory/storage/migrations/M030_entity_explorer_indexes.py +0 -93
- package/src/superlocalmemory/storage/migrations/M031_dead_letter_operations.py +0 -80
- package/src/superlocalmemory/storage/migrations/M032_write_coordinator_admission.py +0 -188
- package/src/superlocalmemory/storage/migrations/M033_projection_transactions.py +0 -148
- package/src/superlocalmemory/storage/migrations/M034_obligation_integrity.py +0 -58
- package/src/superlocalmemory/storage/migrations/M035_erasure_receipts.py +0 -113
- package/src/superlocalmemory/storage/migrations/M036_vector_row_map.py +0 -107
- package/src/superlocalmemory/storage/migrations/M037_manifest_hmac_version.py +0 -162
- package/src/superlocalmemory/storage/migrations/M038_learning_feedback_channel.py +0 -77
- package/src/superlocalmemory/storage/migrations/M039_scene_fact_members.py +0 -137
- package/src/superlocalmemory/storage/migrations/M040_agent_experience_receipts.py +0 -254
- package/src/superlocalmemory/storage/migrations/M041_external_evidence_receipts.py +0 -189
- package/src/superlocalmemory/storage/migrations/M042_correction_case_ledger.py +0 -245
- package/src/superlocalmemory/storage/migrations/M043_quarantine_display_summaries.py +0 -512
- package/src/superlocalmemory/storage/migrations/M044_play_carries_its_own_evidence.py +0 -127
- package/src/superlocalmemory/storage/migrations/M045_fact_outcome_score.py +0 -158
- package/src/superlocalmemory/storage/migrations/M046_prospective_memory_has_its_own_name.py +0 -620
- package/src/superlocalmemory/storage/migrations/M047_fisher_vectors_are_stored_like_every_other_vector.py +0 -306
- package/src/superlocalmemory/storage/migrations/M048_upcoming_holds_only_what_is_upcoming.py +0 -229
- package/src/superlocalmemory/storage/migrations/M049_a_schema_version_marker_is_one_row.py +0 -201
- package/src/superlocalmemory/storage/migrations/M050_execution_learning_v2.py +0 -70
- package/src/superlocalmemory/storage/migrations/__init__.py +0 -103
- package/src/superlocalmemory/storage/migrations.py +0 -333
- package/src/superlocalmemory/storage/models.py +0 -500
- package/src/superlocalmemory/storage/projection_outbox.py +0 -346
- package/src/superlocalmemory/storage/quantized_store.py +0 -280
- package/src/superlocalmemory/storage/read_connection.py +0 -115
- package/src/superlocalmemory/storage/retention_policy.py +0 -860
- package/src/superlocalmemory/storage/schema.py +0 -1108
- package/src/superlocalmemory/storage/schema_code_graph.py +0 -282
- package/src/superlocalmemory/storage/schema_v32.py +0 -382
- package/src/superlocalmemory/storage/schema_v3410.py +0 -159
- package/src/superlocalmemory/storage/schema_v3411.py +0 -149
- package/src/superlocalmemory/storage/schema_v343.py +0 -315
- package/src/superlocalmemory/storage/schema_v345.py +0 -109
- package/src/superlocalmemory/storage/schema_v347.py +0 -140
- package/src/superlocalmemory/storage/sqlite_vectors.py +0 -169
- package/src/superlocalmemory/storage/v2_migrator.py +0 -466
- package/src/superlocalmemory/storage/write_coordinator.py +0 -949
- package/src/superlocalmemory/storage/write_lock.py +0 -88
- package/src/superlocalmemory/summaries/__init__.py +0 -37
- package/src/superlocalmemory/summaries/base.py +0 -267
- package/src/superlocalmemory/summaries/daily_reflection.py +0 -340
- package/src/superlocalmemory/summaries/non_answer.py +0 -223
- package/src/superlocalmemory/summaries/project_work_log.py +0 -440
- package/src/superlocalmemory/summaries/session_summary.py +0 -311
- package/src/superlocalmemory/trust/__init__.py +0 -0
- package/src/superlocalmemory/trust/gate.py +0 -171
- package/src/superlocalmemory/trust/provenance.py +0 -124
- package/src/superlocalmemory/trust/scorer.py +0 -413
- package/src/superlocalmemory/trust/signals.py +0 -153
- package/src/superlocalmemory/ui/assets/slm-icon-white.svg +0 -64
- package/src/superlocalmemory/ui/assets/slm-icon.svg +0 -36
- package/src/superlocalmemory/ui/css/brain.css +0 -409
- package/src/superlocalmemory/ui/css/design-system.css +0 -696
- package/src/superlocalmemory/ui/css/legacy-dashboard.css +0 -663
- package/src/superlocalmemory/ui/css/neural-glass.css +0 -1599
- package/src/superlocalmemory/ui/css/od-bridge.css +0 -158
- package/src/superlocalmemory/ui/favicon.svg +0 -36
- package/src/superlocalmemory/ui/index.html +0 -1638
- package/src/superlocalmemory/ui/js/agents.js +0 -192
- package/src/superlocalmemory/ui/js/auto-settings.js +0 -624
- package/src/superlocalmemory/ui/js/brain.js +0 -1400
- package/src/superlocalmemory/ui/js/clusters.js +0 -326
- package/src/superlocalmemory/ui/js/compliance.js +0 -307
- package/src/superlocalmemory/ui/js/core.js +0 -566
- package/src/superlocalmemory/ui/js/dashboard.js +0 -503
- package/src/superlocalmemory/ui/js/event-delegation.js +0 -113
- package/src/superlocalmemory/ui/js/events.js +0 -178
- package/src/superlocalmemory/ui/js/fact-detail.js +0 -142
- package/src/superlocalmemory/ui/js/feedback.js +0 -339
- package/src/superlocalmemory/ui/js/graph-event-bus.js +0 -83
- package/src/superlocalmemory/ui/js/graph-filters.js +0 -220
- package/src/superlocalmemory/ui/js/graph-ui.js +0 -214
- package/src/superlocalmemory/ui/js/ide-status.js +0 -115
- package/src/superlocalmemory/ui/js/init.js +0 -54
- package/src/superlocalmemory/ui/js/knowledge-graph.js +0 -945
- package/src/superlocalmemory/ui/js/lifecycle.js +0 -387
- package/src/superlocalmemory/ui/js/math-health.js +0 -114
- package/src/superlocalmemory/ui/js/memories.js +0 -394
- package/src/superlocalmemory/ui/js/memory-chat.js +0 -371
- package/src/superlocalmemory/ui/js/memory-timeline.js +0 -265
- package/src/superlocalmemory/ui/js/modal.js +0 -733
- package/src/superlocalmemory/ui/js/ng-entities.js +0 -298
- package/src/superlocalmemory/ui/js/ng-health.js +0 -208
- package/src/superlocalmemory/ui/js/ng-ingestion.js +0 -203
- package/src/superlocalmemory/ui/js/ng-mesh.js +0 -374
- package/src/superlocalmemory/ui/js/ng-shell.js +0 -524
- package/src/superlocalmemory/ui/js/ng-skills.js +0 -663
- package/src/superlocalmemory/ui/js/od-agents.js +0 -588
- package/src/superlocalmemory/ui/js/od-auth-gate.js +0 -257
- package/src/superlocalmemory/ui/js/od-backup.js +0 -878
- package/src/superlocalmemory/ui/js/od-boundedloops.js +0 -324
- package/src/superlocalmemory/ui/js/od-brain.js +0 -1095
- package/src/superlocalmemory/ui/js/od-compliance-ext.js +0 -301
- package/src/superlocalmemory/ui/js/od-components.js +0 -147
- package/src/superlocalmemory/ui/js/od-entities.js +0 -622
- package/src/superlocalmemory/ui/js/od-graph.js +0 -776
- package/src/superlocalmemory/ui/js/od-health.js +0 -579
- package/src/superlocalmemory/ui/js/od-mcp.js +0 -508
- package/src/superlocalmemory/ui/js/od-memories.js +0 -1499
- package/src/superlocalmemory/ui/js/od-mesh.js +0 -645
- package/src/superlocalmemory/ui/js/od-operations.js +0 -1268
- package/src/superlocalmemory/ui/js/od-ops-health.js +0 -417
- package/src/superlocalmemory/ui/js/od-optimize.js +0 -828
- package/src/superlocalmemory/ui/js/od-settings.js +0 -1275
- package/src/superlocalmemory/ui/js/od-shell.js +0 -819
- package/src/superlocalmemory/ui/js/od-skills.js +0 -600
- package/src/superlocalmemory/ui/js/od-team.js +0 -265
- package/src/superlocalmemory/ui/js/optimize.js +0 -191
- package/src/superlocalmemory/ui/js/profiles.js +0 -362
- package/src/superlocalmemory/ui/js/quick-actions.js +0 -334
- package/src/superlocalmemory/ui/js/recall-lab.js +0 -373
- package/src/superlocalmemory/ui/js/search.js +0 -86
- package/src/superlocalmemory/ui/js/settings.js +0 -556
- package/src/superlocalmemory/ui/js/timeline.js +0 -62
- package/src/superlocalmemory/ui/js/trust-dashboard.js +0 -225
- package/src/superlocalmemory/ui/vendor/bootstrap-icons/bootstrap-icons.css +0 -2018
- package/src/superlocalmemory/ui/vendor/bootstrap-icons/fonts/bootstrap-icons.woff +0 -0
- package/src/superlocalmemory/ui/vendor/bootstrap-icons/fonts/bootstrap-icons.woff2 +0 -0
- package/src/superlocalmemory/ui/vendor/bootstrap.bundle.min.js +0 -7
- package/src/superlocalmemory/ui/vendor/bootstrap.min.css +0 -6
- package/src/superlocalmemory/ui/vendor/d3.v7.min.js +0 -2
- package/src/superlocalmemory/ui/vendor/graphology-library.min.js +0 -2
- package/src/superlocalmemory/ui/vendor/graphology.umd.min.js +0 -2
- package/src/superlocalmemory/ui/vendor/inter-ui/inter-variable.min.css +0 -8
- package/src/superlocalmemory/ui/vendor/inter-ui/variable/InterVariable-Italic.woff2 +0 -0
- package/src/superlocalmemory/ui/vendor/inter-ui/variable/InterVariable.woff2 +0 -0
- package/src/superlocalmemory/ui/vendor/sigma.min.js +0 -1
- package/src/superlocalmemory/vector/__init__.py +0 -9
- package/src/superlocalmemory/vector/lancedb_backend.py +0 -366
|
@@ -1,4710 +0,0 @@
|
|
|
1
|
-
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
-
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
-
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
|
-
|
|
5
|
-
"""CLI command implementations.
|
|
6
|
-
|
|
7
|
-
Each function handles one CLI command. Dispatch routes by name.
|
|
8
|
-
All data-returning commands support --json for agent-native output.
|
|
9
|
-
|
|
10
|
-
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
from __future__ import annotations
|
|
14
|
-
|
|
15
|
-
import logging
|
|
16
|
-
import os
|
|
17
|
-
import sys
|
|
18
|
-
from argparse import Namespace
|
|
19
|
-
from pathlib import Path
|
|
20
|
-
|
|
21
|
-
logger = logging.getLogger(__name__)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def _cli_projection_queue_depth(db_path: object) -> int:
|
|
25
|
-
"""Facts queued for the graph and vector projections, read straight from disk.
|
|
26
|
-
|
|
27
|
-
The no-daemon branch of ``slm status`` has no engine, so it opens the store
|
|
28
|
-
read-only rather than building one. A store that predates the queue reports
|
|
29
|
-
zero, which is the truth for it.
|
|
30
|
-
"""
|
|
31
|
-
import sqlite3
|
|
32
|
-
|
|
33
|
-
from superlocalmemory.core.status_contract import projection_queue_depth
|
|
34
|
-
|
|
35
|
-
try:
|
|
36
|
-
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
|
|
37
|
-
except sqlite3.Error:
|
|
38
|
-
return 0
|
|
39
|
-
try:
|
|
40
|
-
return projection_queue_depth(conn)
|
|
41
|
-
finally:
|
|
42
|
-
conn.close()
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def _daemon_unavailable(command: str, use_json: bool) -> None:
|
|
46
|
-
"""Exit a mutation client without opening a process-local writer.
|
|
47
|
-
|
|
48
|
-
The bare "owned daemon is unavailable" of earlier releases described a
|
|
49
|
-
stopped daemon, a recycled PID, an unreachable port and an identity
|
|
50
|
-
mismatch identically, which gave issue #104's reporter nothing to act on.
|
|
51
|
-
The diagnosis names the evidence and the next command to run.
|
|
52
|
-
"""
|
|
53
|
-
from superlocalmemory.cli.daemon import describe_daemon_unavailability
|
|
54
|
-
|
|
55
|
-
diagnosis = describe_daemon_unavailability()
|
|
56
|
-
error = {
|
|
57
|
-
"code": "DAEMON_UNAVAILABLE",
|
|
58
|
-
"reason": diagnosis["reason"],
|
|
59
|
-
"message": f"Owned daemon is unavailable: {diagnosis['message']}",
|
|
60
|
-
"hint": diagnosis["hint"],
|
|
61
|
-
"retryable": True,
|
|
62
|
-
}
|
|
63
|
-
if use_json:
|
|
64
|
-
from superlocalmemory.cli.json_output import json_print
|
|
65
|
-
|
|
66
|
-
json_print(command, error=error)
|
|
67
|
-
else:
|
|
68
|
-
print(
|
|
69
|
-
f"DAEMON_UNAVAILABLE ({diagnosis['reason']}): "
|
|
70
|
-
f"{diagnosis['message']} {diagnosis['hint']}",
|
|
71
|
-
file=sys.stderr,
|
|
72
|
-
)
|
|
73
|
-
raise SystemExit(1)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def _cmd_db_dispatch(args: Namespace) -> None:
|
|
77
|
-
"""Route ``slm db ...`` subcommands. LLD-06 §7.2."""
|
|
78
|
-
sub = getattr(args, "db_command", None)
|
|
79
|
-
if sub == "migrate":
|
|
80
|
-
from superlocalmemory.cli.db_migrate import cmd_db_migrate
|
|
81
|
-
rc = cmd_db_migrate(args)
|
|
82
|
-
if rc:
|
|
83
|
-
sys.exit(rc)
|
|
84
|
-
return
|
|
85
|
-
if sub == "scale":
|
|
86
|
-
from superlocalmemory.cli.scale_engine_cmd import cmd_db_scale
|
|
87
|
-
rc = cmd_db_scale(args)
|
|
88
|
-
if rc:
|
|
89
|
-
sys.exit(rc)
|
|
90
|
-
return
|
|
91
|
-
if sub == "reembed":
|
|
92
|
-
_cmd_db_reembed(args)
|
|
93
|
-
return
|
|
94
|
-
if sub == "regraph":
|
|
95
|
-
_cmd_db_regraph(args)
|
|
96
|
-
return
|
|
97
|
-
print(
|
|
98
|
-
"Usage: slm db migrate [--status] [--dry-run] "
|
|
99
|
-
"| slm db scale <action> "
|
|
100
|
-
"| slm db regraph [--check] [--profile NAME] "
|
|
101
|
-
"| slm db reembed [--missing-only] [--all-profiles] [--limit N]"
|
|
102
|
-
)
|
|
103
|
-
sys.exit(2)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def _cmd_db_regraph(args: Namespace) -> None:
|
|
107
|
-
"""Re-derive the graph copy of your memories from the store.
|
|
108
|
-
|
|
109
|
-
Your memories live in one place; a second copy of the connections between
|
|
110
|
-
them is kept in a graph store so that searching them is fast. The two are
|
|
111
|
-
kept in step by a queue. If the queue is empty and the two still disagree
|
|
112
|
-
-- after a crash, a disk problem, or a copy restored from elsewhere --
|
|
113
|
-
nothing notices, because "the queue is empty" is what "in step" looks like.
|
|
114
|
-
|
|
115
|
-
This is how you say so out loud. ``--check`` reports the difference and
|
|
116
|
-
changes nothing; without it, every memory is queued to be re-derived and
|
|
117
|
-
the background worker rebuilds the copy from the store, which is the one
|
|
118
|
-
that is authoritative.
|
|
119
|
-
"""
|
|
120
|
-
from superlocalmemory.core.config import SLMConfig
|
|
121
|
-
from superlocalmemory.storage import projection_outbox
|
|
122
|
-
from superlocalmemory.storage.database import DatabaseManager
|
|
123
|
-
from superlocalmemory.storage.logical_edges import count_logical_edges
|
|
124
|
-
|
|
125
|
-
config = SLMConfig.load()
|
|
126
|
-
db = DatabaseManager(config.db_path)
|
|
127
|
-
wanted = str(getattr(args, "profile", "") or "").strip()
|
|
128
|
-
try:
|
|
129
|
-
rows = db.execute(
|
|
130
|
-
"SELECT DISTINCT profile_id FROM atomic_facts ORDER BY profile_id"
|
|
131
|
-
)
|
|
132
|
-
profiles = [dict(r)["profile_id"] for r in rows]
|
|
133
|
-
except Exception as exc: # noqa: BLE001
|
|
134
|
-
print(f"[slm] could not list workspaces: {exc}")
|
|
135
|
-
sys.exit(1)
|
|
136
|
-
if wanted:
|
|
137
|
-
profiles = [p for p in profiles if p == wanted]
|
|
138
|
-
if not profiles:
|
|
139
|
-
print(f"[slm] no workspace named {wanted!r}")
|
|
140
|
-
sys.exit(1)
|
|
141
|
-
|
|
142
|
-
backend = None
|
|
143
|
-
try:
|
|
144
|
-
from superlocalmemory.core.backend_orchestrator import get_orchestrator
|
|
145
|
-
|
|
146
|
-
orchestrator = get_orchestrator()
|
|
147
|
-
backend = orchestrator.get_graph_backend() if orchestrator else None
|
|
148
|
-
except Exception as exc: # noqa: BLE001
|
|
149
|
-
print(f"[slm] no graph copy is configured on this store ({exc}).")
|
|
150
|
-
return
|
|
151
|
-
if backend is None:
|
|
152
|
-
print("[slm] no graph copy is configured on this store; nothing to do.")
|
|
153
|
-
return
|
|
154
|
-
|
|
155
|
-
total_queued = 0
|
|
156
|
-
for profile_id in profiles:
|
|
157
|
-
with db.raw_connection() as conn:
|
|
158
|
-
stored = count_logical_edges(conn, profile_id)
|
|
159
|
-
try:
|
|
160
|
-
result = backend._db.run(
|
|
161
|
-
"?[count(a)] := *edge{from_id: a, profile_id: $pid}",
|
|
162
|
-
{"pid": profile_id},
|
|
163
|
-
)
|
|
164
|
-
copied = int(result.values.tolist()[0][0]) if len(result) else 0
|
|
165
|
-
except Exception as exc: # noqa: BLE001
|
|
166
|
-
print(f" {profile_id}: the graph copy could not be read ({exc})")
|
|
167
|
-
continue
|
|
168
|
-
drift = abs(copied - stored) / float(stored) if stored else 0.0
|
|
169
|
-
pending = 0
|
|
170
|
-
try:
|
|
171
|
-
pending_rows = db.execute(
|
|
172
|
-
"SELECT COUNT(*) AS c FROM projection_outbox WHERE profile_id = ?",
|
|
173
|
-
(profile_id,),
|
|
174
|
-
)
|
|
175
|
-
pending = int(dict(pending_rows[0])["c"]) if pending_rows else 0
|
|
176
|
-
except Exception: # noqa: BLE001
|
|
177
|
-
pending = -1
|
|
178
|
-
print(
|
|
179
|
-
f" {profile_id}: {stored} connections stored, {copied} in the "
|
|
180
|
-
f"graph copy ({drift:.1%} apart), {pending} waiting to be applied"
|
|
181
|
-
)
|
|
182
|
-
if getattr(args, "check", False):
|
|
183
|
-
continue
|
|
184
|
-
|
|
185
|
-
fact_rows = db.execute(
|
|
186
|
-
"SELECT fact_id FROM atomic_facts WHERE profile_id = ?", (profile_id,)
|
|
187
|
-
)
|
|
188
|
-
fact_ids = [dict(r)["fact_id"] for r in fact_rows]
|
|
189
|
-
if not fact_ids:
|
|
190
|
-
continue
|
|
191
|
-
if not projection_outbox.is_available(db):
|
|
192
|
-
print(" (this store has no queue to rebuild through)")
|
|
193
|
-
continue
|
|
194
|
-
for start in range(0, len(fact_ids), 500):
|
|
195
|
-
projection_outbox.enqueue_many(
|
|
196
|
-
db, fact_ids[start:start + 500], profile_id,
|
|
197
|
-
op=projection_outbox.OP_UPSERT,
|
|
198
|
-
)
|
|
199
|
-
total_queued += len(fact_ids)
|
|
200
|
-
print(f" {profile_id}: queued {len(fact_ids)} memories to re-derive")
|
|
201
|
-
|
|
202
|
-
db.close()
|
|
203
|
-
if getattr(args, "check", False):
|
|
204
|
-
return
|
|
205
|
-
if total_queued:
|
|
206
|
-
print(
|
|
207
|
-
f"[slm] {total_queued} memories queued. The background worker "
|
|
208
|
-
f"rebuilds the copy from the store; watch `slm status` until the "
|
|
209
|
-
f"waiting count reaches zero."
|
|
210
|
-
)
|
|
211
|
-
else:
|
|
212
|
-
print("[slm] nothing to re-derive.")
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
def _cmd_db_reembed(args: Namespace) -> None:
|
|
216
|
-
"""Backfill NULL embeddings on atomic_facts.
|
|
217
|
-
|
|
218
|
-
Scans for atomic_facts rows with no embedding (facts stored while the
|
|
219
|
-
embedder was unavailable) and embeds them now. Safe to re-run — only
|
|
220
|
-
NULL rows are processed.
|
|
221
|
-
|
|
222
|
-
Usage:
|
|
223
|
-
slm db reembed --missing-only # backfill NULLs (default)
|
|
224
|
-
slm db reembed --missing-only --limit 500 # cap per run
|
|
225
|
-
slm db reembed --missing-only --all-profiles # cover every profile
|
|
226
|
-
"""
|
|
227
|
-
import time
|
|
228
|
-
|
|
229
|
-
from superlocalmemory.core.config import SLMConfig
|
|
230
|
-
from superlocalmemory.core.engine import MemoryEngine
|
|
231
|
-
from superlocalmemory.storage.embedding_migrator import backfill_missing_embeddings
|
|
232
|
-
|
|
233
|
-
use_json = getattr(args, "json", False)
|
|
234
|
-
missing_only = getattr(args, "missing_only", True)
|
|
235
|
-
all_profiles = getattr(args, "all_profiles", False)
|
|
236
|
-
limit = getattr(args, "limit", None)
|
|
237
|
-
|
|
238
|
-
if not missing_only:
|
|
239
|
-
if use_json:
|
|
240
|
-
from superlocalmemory.cli.json_output import json_print
|
|
241
|
-
json_print(
|
|
242
|
-
"db-reembed",
|
|
243
|
-
error={
|
|
244
|
-
"code": "NOT_IMPLEMENTED",
|
|
245
|
-
"message": "Only --missing-only is supported. "
|
|
246
|
-
"For full re-embed use 'slm db migrate'.",
|
|
247
|
-
},
|
|
248
|
-
)
|
|
249
|
-
else:
|
|
250
|
-
print("Only --missing-only mode is supported. "
|
|
251
|
-
"For full re-embed use 'slm db migrate'.")
|
|
252
|
-
sys.exit(2)
|
|
253
|
-
|
|
254
|
-
try:
|
|
255
|
-
config = SLMConfig.load()
|
|
256
|
-
engine = MemoryEngine(config)
|
|
257
|
-
engine.initialize()
|
|
258
|
-
except Exception as exc:
|
|
259
|
-
if use_json:
|
|
260
|
-
from superlocalmemory.cli.json_output import json_print
|
|
261
|
-
json_print(
|
|
262
|
-
"db-reembed",
|
|
263
|
-
error={"code": "ENGINE_INIT_ERROR", "message": str(exc)},
|
|
264
|
-
)
|
|
265
|
-
else:
|
|
266
|
-
print(f"Error initializing engine: {exc}", file=sys.stderr)
|
|
267
|
-
sys.exit(1)
|
|
268
|
-
|
|
269
|
-
embedder = getattr(engine, "_embedder", None)
|
|
270
|
-
if embedder is None:
|
|
271
|
-
if use_json:
|
|
272
|
-
from superlocalmemory.cli.json_output import json_print
|
|
273
|
-
json_print(
|
|
274
|
-
"db-reembed",
|
|
275
|
-
error={
|
|
276
|
-
"code": "NO_EMBEDDER",
|
|
277
|
-
"message": "Embedder is not available. "
|
|
278
|
-
"Start the daemon first or check embedding configuration.",
|
|
279
|
-
},
|
|
280
|
-
)
|
|
281
|
-
else:
|
|
282
|
-
print(
|
|
283
|
-
"Embedder is not available. "
|
|
284
|
-
"Start the daemon ('slm serve') or check embedding config.",
|
|
285
|
-
file=sys.stderr,
|
|
286
|
-
)
|
|
287
|
-
engine.close()
|
|
288
|
-
sys.exit(1)
|
|
289
|
-
|
|
290
|
-
t0 = time.monotonic()
|
|
291
|
-
try:
|
|
292
|
-
result = backfill_missing_embeddings(
|
|
293
|
-
config,
|
|
294
|
-
engine._db,
|
|
295
|
-
embedder,
|
|
296
|
-
batch_size=50,
|
|
297
|
-
limit=limit,
|
|
298
|
-
all_profiles=all_profiles,
|
|
299
|
-
)
|
|
300
|
-
except Exception as exc:
|
|
301
|
-
if use_json:
|
|
302
|
-
from superlocalmemory.cli.json_output import json_print
|
|
303
|
-
json_print(
|
|
304
|
-
"db-reembed",
|
|
305
|
-
error={"code": "BACKFILL_ERROR", "message": str(exc)},
|
|
306
|
-
)
|
|
307
|
-
else:
|
|
308
|
-
print(f"Backfill error: {exc}", file=sys.stderr)
|
|
309
|
-
engine.close()
|
|
310
|
-
sys.exit(1)
|
|
311
|
-
|
|
312
|
-
elapsed = time.monotonic() - t0
|
|
313
|
-
engine.close()
|
|
314
|
-
|
|
315
|
-
if use_json:
|
|
316
|
-
from superlocalmemory.cli.json_output import json_print
|
|
317
|
-
json_print(
|
|
318
|
-
"db-reembed",
|
|
319
|
-
data={
|
|
320
|
-
"scanned": result["scanned"],
|
|
321
|
-
"embedded": result["embedded"],
|
|
322
|
-
"remaining_null": result["remaining_null"],
|
|
323
|
-
"elapsed_s": round(elapsed, 2),
|
|
324
|
-
},
|
|
325
|
-
)
|
|
326
|
-
else:
|
|
327
|
-
print(
|
|
328
|
-
f"Backfill complete — "
|
|
329
|
-
f"scanned={result['scanned']} "
|
|
330
|
-
f"embedded={result['embedded']} "
|
|
331
|
-
f"remaining_null={result['remaining_null']} "
|
|
332
|
-
f"elapsed={elapsed:.1f}s"
|
|
333
|
-
)
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
def _cmd_mesh_dispatch(args: Namespace) -> None:
|
|
337
|
-
"""Route ``slm mesh ...`` inspection subcommands (M-03)."""
|
|
338
|
-
from superlocalmemory.cli.mesh_cmd import cmd_mesh
|
|
339
|
-
rc = cmd_mesh(args)
|
|
340
|
-
if rc:
|
|
341
|
-
sys.exit(rc)
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
def _cmd_escape_disable(args: Namespace) -> None:
|
|
345
|
-
from superlocalmemory.cli.escape_hatch import cmd_disable
|
|
346
|
-
cmd_disable(args)
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
def _cmd_escape_enable(args: Namespace) -> None:
|
|
350
|
-
from superlocalmemory.cli.escape_hatch import cmd_enable
|
|
351
|
-
cmd_enable(args)
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
def _cmd_escape_clear_cache(args: Namespace) -> None:
|
|
355
|
-
from superlocalmemory.cli.escape_hatch import cmd_clear_cache
|
|
356
|
-
cmd_clear_cache(args)
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
def _cmd_escape_reconfigure(args: Namespace) -> None:
|
|
360
|
-
from superlocalmemory.cli.escape_hatch import cmd_reconfigure
|
|
361
|
-
cmd_reconfigure(args)
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
def _cmd_escape_benchmark(args: Namespace) -> None:
|
|
365
|
-
from superlocalmemory.cli.escape_hatch import cmd_benchmark
|
|
366
|
-
cmd_benchmark(args)
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
def _cmd_escape_rotate_token(args: Namespace) -> None:
|
|
370
|
-
"""S-M07: rotate the install token."""
|
|
371
|
-
from superlocalmemory.cli.escape_hatch import cmd_rotate_token
|
|
372
|
-
cmd_rotate_token(args)
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
# ---- SLM v3.6 Optimize dispatch functions (additive) ----
|
|
376
|
-
|
|
377
|
-
def _cmd_optimize(args: Namespace) -> None:
|
|
378
|
-
from superlocalmemory.cli.optimize_cmd import cmd_optimize
|
|
379
|
-
cmd_optimize(args)
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
def _cmd_cache(args: Namespace) -> None:
|
|
383
|
-
from superlocalmemory.cli.cache_cmd import cmd_cache
|
|
384
|
-
cmd_cache(args)
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
def _cmd_compress(args: Namespace) -> None:
|
|
388
|
-
from superlocalmemory.cli.compress_cmd import cmd_compress
|
|
389
|
-
cmd_compress(args)
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
def _cmd_proxy(args: Namespace) -> None:
|
|
393
|
-
from superlocalmemory.cli.proxy_cmd import cmd_proxy
|
|
394
|
-
cmd_proxy(args)
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
def _cmd_help_optimize(args: Namespace) -> None:
|
|
398
|
-
from superlocalmemory.cli.help_cmd import cmd_help_optimize
|
|
399
|
-
cmd_help_optimize(args)
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
def _cmd_loop(args: Namespace) -> None:
|
|
403
|
-
from superlocalmemory.cli.loop_cmd import cmd_loop
|
|
404
|
-
cmd_loop(args)
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
def _cmd_ops(args: Namespace) -> None:
|
|
408
|
-
"""Operational recovery & admin remediation commands."""
|
|
409
|
-
from superlocalmemory.cli.ops_cmd import cmd_ops
|
|
410
|
-
cmd_ops(args)
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
def _cmd_gdpr_dispatch(args: Namespace) -> None:
|
|
414
|
-
"""V4.0.6: GDPR subject-rights CLI (Art.15/17/20)."""
|
|
415
|
-
from superlocalmemory.cli.gdpr_cmd import cmd_gdpr
|
|
416
|
-
cmd_gdpr(args)
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
def _cmd_summary_dispatch(args: Namespace) -> None:
|
|
420
|
-
"""V4.0.7: readable session/day/project summaries (issue #113)."""
|
|
421
|
-
from superlocalmemory.cli.summary_cmd import cmd_summary
|
|
422
|
-
cmd_summary(args)
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
# ---- end SLM v3.6 Optimize dispatch functions ----
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
def cmd_session(args: Namespace) -> None:
|
|
429
|
-
"""#49: Open/close a session locally via the daemon — no model roundtrip,
|
|
430
|
-
so a shell hook (e.g. Claude session-start / /quit) can call it directly.
|
|
431
|
-
"""
|
|
432
|
-
from superlocalmemory.cli.daemon import (
|
|
433
|
-
daemon_request, ensure_daemon, is_daemon_running,
|
|
434
|
-
)
|
|
435
|
-
|
|
436
|
-
action = getattr(args, "session_command", None)
|
|
437
|
-
if action not in ("open", "close"):
|
|
438
|
-
print("Usage: slm session {open|close} "
|
|
439
|
-
"[--session-id ID] [--project-path PATH] [--query Q]")
|
|
440
|
-
return
|
|
441
|
-
|
|
442
|
-
if not is_daemon_running():
|
|
443
|
-
ensure_daemon()
|
|
444
|
-
|
|
445
|
-
if action == "open":
|
|
446
|
-
body = {
|
|
447
|
-
"project_path": getattr(args, "project_path", "") or "",
|
|
448
|
-
"query": getattr(args, "query", "") or "",
|
|
449
|
-
"max_results": int(getattr(args, "max_results", 10) or 10),
|
|
450
|
-
}
|
|
451
|
-
resp = daemon_request("POST", "/session/open", body)
|
|
452
|
-
if resp and resp.get("ok"):
|
|
453
|
-
print(f"Session opened — warmed {resp.get('warmed', 0)} memories "
|
|
454
|
-
f"(query: {resp.get('query', '')})")
|
|
455
|
-
else:
|
|
456
|
-
print("Session open failed (daemon unreachable?)")
|
|
457
|
-
return
|
|
458
|
-
|
|
459
|
-
# close
|
|
460
|
-
body = {"session_id": getattr(args, "session_id", "") or ""}
|
|
461
|
-
resp = daemon_request("POST", "/session/close", body)
|
|
462
|
-
if resp and resp.get("ok"):
|
|
463
|
-
sid = resp.get("session_id") or "(most recent)"
|
|
464
|
-
print(f"Session closed: {sid} — "
|
|
465
|
-
f"{resp.get('summary_events_created', 0)} summary event(s) created")
|
|
466
|
-
else:
|
|
467
|
-
print("Session close failed (daemon unreachable?)")
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
def _cmd_upgrade_hosts(args: Namespace) -> None:
|
|
471
|
-
"""Run the consented host-integration refresh command."""
|
|
472
|
-
from superlocalmemory.cli.host_upgrades import cmd_upgrade_hosts
|
|
473
|
-
|
|
474
|
-
cmd_upgrade_hosts(args)
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
def dispatch(args: Namespace) -> None:
|
|
478
|
-
"""Route CLI command to the appropriate handler."""
|
|
479
|
-
handlers = {
|
|
480
|
-
"init": cmd_init,
|
|
481
|
-
"setup": cmd_setup,
|
|
482
|
-
"mode": cmd_mode,
|
|
483
|
-
"provider": cmd_provider,
|
|
484
|
-
"connect": cmd_connect,
|
|
485
|
-
"upgrade-hosts": _cmd_upgrade_hosts,
|
|
486
|
-
"migrate": cmd_migrate,
|
|
487
|
-
"list": cmd_list,
|
|
488
|
-
"remember": cmd_remember,
|
|
489
|
-
"recall": cmd_recall,
|
|
490
|
-
"search": cmd_recall, # v3.6.12 (parity-3): MCP exposes a `search` verb; give the CLI parity (recall is multi-channel incl. BM25/keyword).
|
|
491
|
-
"forget": cmd_forget,
|
|
492
|
-
"delete": cmd_delete,
|
|
493
|
-
"update": cmd_update,
|
|
494
|
-
"review-correction": cmd_review_correction,
|
|
495
|
-
"status": cmd_status,
|
|
496
|
-
"brain": cmd_brain,
|
|
497
|
-
"health": cmd_health,
|
|
498
|
-
"doctor": cmd_doctor,
|
|
499
|
-
"trace": cmd_trace,
|
|
500
|
-
"mcp": cmd_mcp,
|
|
501
|
-
"warmup": cmd_warmup,
|
|
502
|
-
"dashboard": cmd_dashboard,
|
|
503
|
-
"profile": cmd_profile,
|
|
504
|
-
"hooks": cmd_hooks,
|
|
505
|
-
"codex": cmd_codex,
|
|
506
|
-
"session-context": cmd_session_context,
|
|
507
|
-
"session": cmd_session, # #49: local session open/close for hooks
|
|
508
|
-
"observe": cmd_observe,
|
|
509
|
-
# V3.3 commands
|
|
510
|
-
"decay": cmd_decay,
|
|
511
|
-
"quantize": cmd_quantize,
|
|
512
|
-
"consolidate": cmd_consolidate,
|
|
513
|
-
"soft-prompts": cmd_soft_prompts,
|
|
514
|
-
"reap": cmd_reap,
|
|
515
|
-
# V3.3.21 daemon
|
|
516
|
-
"serve": cmd_serve,
|
|
517
|
-
# V3.4.9 nuclear restart
|
|
518
|
-
"restart": cmd_restart,
|
|
519
|
-
# V3.4.3 ingestion adapters
|
|
520
|
-
"adapters": cmd_adapters,
|
|
521
|
-
# V3.4.8 external observation ingestion
|
|
522
|
-
"ingest": cmd_ingest,
|
|
523
|
-
# V3.4.11 skill evolution
|
|
524
|
-
"config": cmd_config,
|
|
525
|
-
"evolve": cmd_evolve,
|
|
526
|
-
# V3.4.22 LLD-05 context pre-staging
|
|
527
|
-
"context": _cmd_context_dispatch,
|
|
528
|
-
# V3.4.22 LLD-06 additive schema migrations
|
|
529
|
-
"db": _cmd_db_dispatch,
|
|
530
|
-
# V3.7.9 M-03 — terminal mesh inspection
|
|
531
|
-
"mesh": _cmd_mesh_dispatch,
|
|
532
|
-
# V3.4.22 Stage 8 SB-5 — MASTER-PLAN §8 escape hatches.
|
|
533
|
-
"disable": _cmd_escape_disable,
|
|
534
|
-
"enable": _cmd_escape_enable,
|
|
535
|
-
"clear-cache": _cmd_escape_clear_cache,
|
|
536
|
-
"reconfigure": _cmd_escape_reconfigure,
|
|
537
|
-
"benchmark": _cmd_escape_benchmark,
|
|
538
|
-
"rotate-token": _cmd_escape_rotate_token,
|
|
539
|
-
"evidence": _cmd_evidence,
|
|
540
|
-
"diagnostics": _cmd_diagnostics,
|
|
541
|
-
# LLD-06 — `slm wrap <agent> [args...]` activates the Optimize proxy.
|
|
542
|
-
"wrap": _cmd_wrap,
|
|
543
|
-
# V3.6 Optimize subcommands (additive)
|
|
544
|
-
"optimize": _cmd_optimize,
|
|
545
|
-
"cache": _cmd_cache,
|
|
546
|
-
"compress": _cmd_compress,
|
|
547
|
-
"proxy": _cmd_proxy,
|
|
548
|
-
"help-optimize": _cmd_help_optimize,
|
|
549
|
-
# V3.8.0 bounded loops (gate-verified agent loops, SLM-backed ledger)
|
|
550
|
-
"loop": _cmd_loop,
|
|
551
|
-
# V3.8.2 super-help — grouped overview of every command + topics
|
|
552
|
-
"help": cmd_help,
|
|
553
|
-
# Operational recovery & admin remediation
|
|
554
|
-
"ops": _cmd_ops,
|
|
555
|
-
# V4.0.6: GDPR subject-rights CLI (Art.15/17/20)
|
|
556
|
-
"gdpr": _cmd_gdpr_dispatch,
|
|
557
|
-
"summary": _cmd_summary_dispatch,
|
|
558
|
-
}
|
|
559
|
-
handler = handlers.get(args.command)
|
|
560
|
-
if handler:
|
|
561
|
-
from superlocalmemory.cli.daemon import DaemonRefused
|
|
562
|
-
|
|
563
|
-
try:
|
|
564
|
-
handler(args)
|
|
565
|
-
except DaemonRefused as refusal:
|
|
566
|
-
# One place, because a command added next year will not remember to
|
|
567
|
-
# do this. A refusal is an answer: say so and stop, rather than
|
|
568
|
-
# printing a traceback or -- worse -- letting a command fall back to
|
|
569
|
-
# writing locally what the workspace has just declined.
|
|
570
|
-
print(
|
|
571
|
-
f"[slm] {refusal}. This workspace requires authentication; "
|
|
572
|
-
"set SLM_USER_SESSION or log in, then try again.",
|
|
573
|
-
flush=True,
|
|
574
|
-
)
|
|
575
|
-
sys.exit(1)
|
|
576
|
-
else:
|
|
577
|
-
print(f"Unknown command: {args.command}")
|
|
578
|
-
sys.exit(1)
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
def _cmd_evidence(args: Namespace) -> None:
|
|
582
|
-
"""Lazy-load the evidence/rebuild command surface."""
|
|
583
|
-
from superlocalmemory.cli.evidence_cmd import cmd_evidence
|
|
584
|
-
|
|
585
|
-
cmd_evidence(args)
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
def _cmd_diagnostics(args: Namespace) -> None:
|
|
589
|
-
"""Lazy-load the local aggregate diagnostics export surface."""
|
|
590
|
-
from superlocalmemory.cli.diagnostics_cmd import cmd_diagnostics
|
|
591
|
-
|
|
592
|
-
cmd_diagnostics(args)
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
def _cmd_wrap(args: Namespace) -> None:
|
|
596
|
-
"""LLD-06 §6.6 — `slm wrap <agent> [args...]` activates the Optimize proxy.
|
|
597
|
-
|
|
598
|
-
Routes the Optimize layer's per-agent launch/activation. See
|
|
599
|
-
optimize.adapters._agent_registry for the full agent table.
|
|
600
|
-
"""
|
|
601
|
-
if getattr(args, "list", False):
|
|
602
|
-
from superlocalmemory.optimize.adapters.wrap import list_agents
|
|
603
|
-
from superlocalmemory.optimize.adapters._agent_registry import AGENT_REGISTRY
|
|
604
|
-
print("Registered agents for `slm wrap`:")
|
|
605
|
-
for key in list_agents():
|
|
606
|
-
spec = AGENT_REGISTRY.get(key, {})
|
|
607
|
-
mech = spec.get("mechanism", "unknown")
|
|
608
|
-
print(f" {key:20s} mechanism={mech}")
|
|
609
|
-
return
|
|
610
|
-
|
|
611
|
-
agent = getattr(args, "agent", None)
|
|
612
|
-
if not agent:
|
|
613
|
-
print("Usage: slm wrap <agent> [args...]\n"
|
|
614
|
-
" slm wrap --list\n"
|
|
615
|
-
" slm wrap --help")
|
|
616
|
-
sys.exit(2)
|
|
617
|
-
|
|
618
|
-
agent_args = list(getattr(args, "agent_args", []) or [])
|
|
619
|
-
persistent = bool(getattr(args, "persistent", False))
|
|
620
|
-
dry_run = bool(getattr(args, "dry_run", False))
|
|
621
|
-
|
|
622
|
-
from superlocalmemory.optimize.adapters.wrap import wrap_agent
|
|
623
|
-
rc = wrap_agent(agent, agent_args, persistent=persistent, dry_run=dry_run)
|
|
624
|
-
if rc:
|
|
625
|
-
sys.exit(rc)
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
# -- Daemon serve mode (V3.3.21) ------------------------------------------
|
|
629
|
-
|
|
630
|
-
def cmd_serve(args: Namespace) -> None:
|
|
631
|
-
"""Start/stop the SLM daemon for instant CLI response."""
|
|
632
|
-
from superlocalmemory.cli.daemon import is_daemon_running, ensure_daemon, stop_daemon
|
|
633
|
-
|
|
634
|
-
action = getattr(args, 'action', 'start')
|
|
635
|
-
|
|
636
|
-
if action == 'stop':
|
|
637
|
-
if stop_daemon():
|
|
638
|
-
print("Daemon stopped.")
|
|
639
|
-
else:
|
|
640
|
-
print("Daemon was not running.")
|
|
641
|
-
return
|
|
642
|
-
|
|
643
|
-
if action == 'status':
|
|
644
|
-
if is_daemon_running():
|
|
645
|
-
from superlocalmemory.cli.daemon import daemon_request
|
|
646
|
-
status = daemon_request("GET", "/status")
|
|
647
|
-
if status:
|
|
648
|
-
print(f"Daemon: RUNNING (PID {status['pid']}, "
|
|
649
|
-
f"mode={status['mode']}, facts={status['fact_count']}, "
|
|
650
|
-
f"uptime={status['uptime_s']}s, idle={status['idle_s']}s)")
|
|
651
|
-
else:
|
|
652
|
-
print("Daemon: RUNNING (could not get status)")
|
|
653
|
-
else:
|
|
654
|
-
print("Daemon: NOT RUNNING")
|
|
655
|
-
# Also show OS service status
|
|
656
|
-
try:
|
|
657
|
-
from superlocalmemory.cli.service_installer import service_status
|
|
658
|
-
svc = service_status()
|
|
659
|
-
installed = svc.get("installed", False)
|
|
660
|
-
print(f"OS Service: {'INSTALLED' if installed else 'NOT INSTALLED'} "
|
|
661
|
-
f"({svc.get('service_type', svc.get('platform', '?'))})")
|
|
662
|
-
except Exception:
|
|
663
|
-
pass
|
|
664
|
-
return
|
|
665
|
-
|
|
666
|
-
if action == 'install':
|
|
667
|
-
# Install OS-level service for auto-start on boot/login
|
|
668
|
-
from superlocalmemory.cli.service_installer import install_service
|
|
669
|
-
print("Installing SLM as OS service (auto-start on login)...")
|
|
670
|
-
if install_service():
|
|
671
|
-
print("Service installed \u2713 — SLM will auto-start on login.")
|
|
672
|
-
print(" slm serve status — check service status")
|
|
673
|
-
print(" slm serve uninstall — remove auto-start")
|
|
674
|
-
else:
|
|
675
|
-
print("Failed to install service. Check logs.")
|
|
676
|
-
return
|
|
677
|
-
|
|
678
|
-
if action == 'uninstall':
|
|
679
|
-
from superlocalmemory.cli.service_installer import uninstall_service
|
|
680
|
-
if uninstall_service():
|
|
681
|
-
print("OS service removed \u2713 — SLM will no longer auto-start.")
|
|
682
|
-
else:
|
|
683
|
-
print("Failed to remove service.")
|
|
684
|
-
return
|
|
685
|
-
|
|
686
|
-
# Default: start
|
|
687
|
-
if is_daemon_running():
|
|
688
|
-
print("Daemon already running.")
|
|
689
|
-
return
|
|
690
|
-
|
|
691
|
-
print("Starting SLM daemon (engine warming up)...")
|
|
692
|
-
if ensure_daemon():
|
|
693
|
-
print("Daemon started \u2713 — CLI commands are now instant.")
|
|
694
|
-
print(" slm serve status — check daemon status")
|
|
695
|
-
print(" slm serve stop — stop daemon and free RAM")
|
|
696
|
-
else:
|
|
697
|
-
from superlocalmemory.infra.data_root import state_path
|
|
698
|
-
print(f"Failed to start daemon. Check {state_path('logs', 'daemon.log')}")
|
|
699
|
-
# INT-H-02: exit non-zero so callers can detect the failure. The plugin
|
|
700
|
-
# launcher (plugin/scripts/slm-launch) guards on this exit code and
|
|
701
|
-
# refuses to open a direct MCP writer against a broken daemon; without
|
|
702
|
-
# the non-zero exit that guard was a dead no-op.
|
|
703
|
-
sys.exit(1)
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
# -- Ingestion Adapters (V3.4.3) ------------------------------------------
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
def cmd_restart(args: Namespace) -> None:
|
|
710
|
-
"""Restart the one daemon owned by the current SLM data namespace.
|
|
711
|
-
|
|
712
|
-
5-step pipeline:
|
|
713
|
-
1. Capability-stop the owned daemon and its children
|
|
714
|
-
2. Acquire the namespace start lock
|
|
715
|
-
3. Start fresh daemon
|
|
716
|
-
4. Wait for engine warmup + verify health
|
|
717
|
-
5. Optionally open dashboard
|
|
718
|
-
"""
|
|
719
|
-
import time
|
|
720
|
-
from superlocalmemory.infra.daemon_identity import canonical_data_root
|
|
721
|
-
|
|
722
|
-
use_json = getattr(args, "json", False)
|
|
723
|
-
open_dashboard = getattr(args, "dashboard", False)
|
|
724
|
-
slm_dir = canonical_data_root()
|
|
725
|
-
steps: list[dict] = []
|
|
726
|
-
|
|
727
|
-
def _log(step: int, name: str, status: str, detail: str = ""):
|
|
728
|
-
entry = {"step": step, "name": name, "status": status, "detail": detail}
|
|
729
|
-
steps.append(entry)
|
|
730
|
-
if not use_json:
|
|
731
|
-
icon = {"ok": "+", "warn": "!", "fail": "x"}.get(status, " ")
|
|
732
|
-
print(f" [{icon}] Step {step}: {name}" + (f" — {detail}" if detail else ""))
|
|
733
|
-
|
|
734
|
-
if not use_json:
|
|
735
|
-
print()
|
|
736
|
-
print(" SLM Full System Restart")
|
|
737
|
-
print(" " + "=" * 40)
|
|
738
|
-
print()
|
|
739
|
-
|
|
740
|
-
# Acquire the namespace lock before requesting shutdown. Otherwise an
|
|
741
|
-
# auto-starting hook can observe the brief offline window and start a
|
|
742
|
-
# second daemon while this command is still waiting for the old process.
|
|
743
|
-
_LOCK_FILE = slm_dir / "daemon.lock"
|
|
744
|
-
_LOCK_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
745
|
-
restart_lock_fd = None
|
|
746
|
-
try:
|
|
747
|
-
restart_lock_fd = open(_LOCK_FILE, "w")
|
|
748
|
-
if sys.platform != "win32":
|
|
749
|
-
import fcntl
|
|
750
|
-
fcntl.flock(restart_lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
751
|
-
except Exception:
|
|
752
|
-
pass # Best-effort — don't block restart if lock fails
|
|
753
|
-
|
|
754
|
-
# Step 1: stop only the descriptor-owned daemon. Its graceful shutdown
|
|
755
|
-
# owns worker termination; process-name-wide scans are forbidden.
|
|
756
|
-
from superlocalmemory.cli.daemon import (
|
|
757
|
-
owned_daemon_process_alive,
|
|
758
|
-
read_descriptor,
|
|
759
|
-
stop_daemon,
|
|
760
|
-
wait_for_owned_daemon_shutdown,
|
|
761
|
-
)
|
|
762
|
-
|
|
763
|
-
# v4.1.4: process liveness, not HTTP health-readiness, decides whether a
|
|
764
|
-
# stop is owed. is_daemon_running() additionally requires a live /health
|
|
765
|
-
# response, which a daemon busy inside /maintenance/run or
|
|
766
|
-
# /consolidate/cognitive cannot give for the duration of that call even
|
|
767
|
-
# though it is fully alive and holding the port — see stop_daemon()'s
|
|
768
|
-
# docstring for the reproduced failure this fixes.
|
|
769
|
-
was_running = owned_daemon_process_alive()
|
|
770
|
-
owned_descriptor = read_descriptor() if was_running else None
|
|
771
|
-
stopped = stop_daemon() if was_running else True
|
|
772
|
-
if stopped and was_running:
|
|
773
|
-
stopped = wait_for_owned_daemon_shutdown(owned_descriptor)
|
|
774
|
-
killed = 1 if was_running and stopped else 0
|
|
775
|
-
_log(
|
|
776
|
-
1,
|
|
777
|
-
"Stop owned SLM daemon",
|
|
778
|
-
"ok" if stopped else "fail",
|
|
779
|
-
"owned daemon stopped" if killed else (
|
|
780
|
-
"already stopped" if stopped else "owned daemon did not stop"
|
|
781
|
-
),
|
|
782
|
-
)
|
|
783
|
-
if not stopped:
|
|
784
|
-
if restart_lock_fd:
|
|
785
|
-
restart_lock_fd.close()
|
|
786
|
-
if use_json:
|
|
787
|
-
from superlocalmemory.cli.json_output import json_print
|
|
788
|
-
json_print(
|
|
789
|
-
"restart",
|
|
790
|
-
data={"steps": steps, "success": False},
|
|
791
|
-
next_actions=[{
|
|
792
|
-
"command": "slm doctor",
|
|
793
|
-
"description": "Diagnose the owned daemon",
|
|
794
|
-
}],
|
|
795
|
-
)
|
|
796
|
-
else:
|
|
797
|
-
print("\n Restart FAILED at step 1. The owned daemon was not stopped.")
|
|
798
|
-
return
|
|
799
|
-
|
|
800
|
-
# Step 2: the namespace lock was acquired before shutdown so hooks cannot
|
|
801
|
-
# auto-start another daemon inside the offline transition.
|
|
802
|
-
_log(2, "Acquire namespace start lock", "ok", str(_LOCK_FILE))
|
|
803
|
-
|
|
804
|
-
# Step 3: Start fresh daemon (lock still held — no races)
|
|
805
|
-
# v3.4.42: Call _start_daemon_subprocess() directly instead of
|
|
806
|
-
# ensure_daemon(). The latter tries to acquire daemon.lock itself,
|
|
807
|
-
# which the SAME PROCESS holds via restart_lock_fd above — BSD-style
|
|
808
|
-
# flock blocks per-fd even within one process, so ensure_daemon would
|
|
809
|
-
# fall into its lock-fail branch and time out after 60s while the
|
|
810
|
-
# actual daemon never gets started. Calling the helper directly
|
|
811
|
-
# bypasses that self-deadlock and starts the daemon as intended.
|
|
812
|
-
from superlocalmemory.cli.daemon import _start_daemon_subprocess
|
|
813
|
-
started = _start_daemon_subprocess()
|
|
814
|
-
|
|
815
|
-
# Release restart lock — daemon is now running with its own lock
|
|
816
|
-
if restart_lock_fd:
|
|
817
|
-
try:
|
|
818
|
-
restart_lock_fd.close()
|
|
819
|
-
except Exception:
|
|
820
|
-
pass
|
|
821
|
-
_log(3, "Start fresh daemon", "ok" if started else "fail",
|
|
822
|
-
"daemon started" if started else "failed to start — check slm doctor")
|
|
823
|
-
|
|
824
|
-
if not started:
|
|
825
|
-
if use_json:
|
|
826
|
-
from superlocalmemory.cli.json_output import json_print
|
|
827
|
-
json_print("restart", data={"steps": steps, "success": False},
|
|
828
|
-
next_actions=[{"command": "slm doctor", "description": "Diagnose issues"}])
|
|
829
|
-
else:
|
|
830
|
-
print("\n Restart FAILED at step 3. Run: slm doctor")
|
|
831
|
-
return
|
|
832
|
-
|
|
833
|
-
# Step 4: Wait for warmup + verify health
|
|
834
|
-
if not use_json:
|
|
835
|
-
print(" [ ] Step 4: Waiting for engine warmup (up to 30s)...", end="", flush=True)
|
|
836
|
-
|
|
837
|
-
health = None
|
|
838
|
-
engine_ok = False
|
|
839
|
-
for attempt in range(15):
|
|
840
|
-
time.sleep(2)
|
|
841
|
-
try:
|
|
842
|
-
from superlocalmemory.cli.daemon import daemon_request
|
|
843
|
-
health = daemon_request("GET", "/health")
|
|
844
|
-
if health and health.get("engine") == "initialized":
|
|
845
|
-
engine_ok = True
|
|
846
|
-
break
|
|
847
|
-
except Exception:
|
|
848
|
-
pass
|
|
849
|
-
|
|
850
|
-
if not use_json:
|
|
851
|
-
print("\r", end="") # Clear the waiting line
|
|
852
|
-
|
|
853
|
-
if engine_ok:
|
|
854
|
-
version = health.get("version", "?")
|
|
855
|
-
pid = health.get("pid", "?")
|
|
856
|
-
_log(4, "Engine health verified", "ok", f"v{version}, PID {pid}, engine=initialized")
|
|
857
|
-
else:
|
|
858
|
-
engine_state = health.get("engine", "unknown") if health else "unreachable"
|
|
859
|
-
_log(4, "Engine health check", "warn",
|
|
860
|
-
f"engine={engine_state} — may still be warming up. Try again in 30s.")
|
|
861
|
-
|
|
862
|
-
# Step 5: Database integrity check
|
|
863
|
-
try:
|
|
864
|
-
from superlocalmemory.storage.memory_write import memory_read
|
|
865
|
-
|
|
866
|
-
db_path = slm_dir / "memory.db"
|
|
867
|
-
if db_path.exists():
|
|
868
|
-
with memory_read(db_path) as conn:
|
|
869
|
-
integrity = conn.execute("PRAGMA integrity_check").fetchone()[0]
|
|
870
|
-
fact_count = conn.execute("SELECT COUNT(*) FROM atomic_facts").fetchone()[0]
|
|
871
|
-
entity_count = conn.execute("SELECT COUNT(*) FROM canonical_entities").fetchone()[0]
|
|
872
|
-
_log(5, "Database integrity", "ok" if integrity == "ok" else "fail",
|
|
873
|
-
f"integrity={integrity}, {fact_count} facts, {entity_count} entities")
|
|
874
|
-
else:
|
|
875
|
-
_log(5, "Database check", "warn", "no database yet — will create on first use")
|
|
876
|
-
except Exception as exc:
|
|
877
|
-
_log(5, "Database check", "warn", str(exc))
|
|
878
|
-
|
|
879
|
-
# Step 6 (optional): Open dashboard
|
|
880
|
-
if open_dashboard:
|
|
881
|
-
try:
|
|
882
|
-
import webbrowser
|
|
883
|
-
from superlocalmemory.cli.daemon import _get_port
|
|
884
|
-
port = _get_port()
|
|
885
|
-
url = f"http://localhost:{port}"
|
|
886
|
-
webbrowser.open(url)
|
|
887
|
-
_log(6, "Dashboard opened", "ok", url)
|
|
888
|
-
except Exception as exc:
|
|
889
|
-
_log(6, "Dashboard open", "fail", str(exc))
|
|
890
|
-
|
|
891
|
-
# Summary
|
|
892
|
-
all_ok = all(s["status"] == "ok" for s in steps)
|
|
893
|
-
|
|
894
|
-
if use_json:
|
|
895
|
-
from superlocalmemory.cli.json_output import json_print
|
|
896
|
-
json_print("restart", data={
|
|
897
|
-
"steps": steps, "success": all_ok,
|
|
898
|
-
"processes_killed": killed,
|
|
899
|
-
"version": health.get("version") if health else None,
|
|
900
|
-
}, next_actions=[
|
|
901
|
-
{"command": "slm serve status", "description": "Check daemon status"},
|
|
902
|
-
{"command": "slm dashboard", "description": "Open dashboard"},
|
|
903
|
-
])
|
|
904
|
-
return
|
|
905
|
-
|
|
906
|
-
print()
|
|
907
|
-
if all_ok:
|
|
908
|
-
print(" All systems operational.")
|
|
909
|
-
else:
|
|
910
|
-
warnings = [s for s in steps if s["status"] != "ok"]
|
|
911
|
-
print(f" {len(warnings)} issue(s) — check details above.")
|
|
912
|
-
print()
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
def cmd_ingest(args: Namespace) -> None:
|
|
916
|
-
"""Import external observations into SLM learning pipeline."""
|
|
917
|
-
from superlocalmemory.cli.ingest_cmd import cmd_ingest as _ingest
|
|
918
|
-
_ingest(args)
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
# -- Config & Evolution (V3.4.11) -----------------------------------------------
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
def cmd_config(args: Namespace) -> None:
|
|
925
|
-
"""Get or set config values (dot-notation).
|
|
926
|
-
|
|
927
|
-
Usage:
|
|
928
|
-
slm config set evolution.enabled true
|
|
929
|
-
slm config set evolution.backend auto
|
|
930
|
-
slm config get evolution.enabled
|
|
931
|
-
slm config get evolution.backend
|
|
932
|
-
"""
|
|
933
|
-
import json
|
|
934
|
-
|
|
935
|
-
from superlocalmemory.infra.data_root import state_path
|
|
936
|
-
|
|
937
|
-
use_json = getattr(args, "json", False)
|
|
938
|
-
action = getattr(args, "action", "get")
|
|
939
|
-
key = getattr(args, "key", "")
|
|
940
|
-
value = getattr(args, "value", None)
|
|
941
|
-
|
|
942
|
-
config_path = state_path("config.json")
|
|
943
|
-
|
|
944
|
-
# Read existing config
|
|
945
|
-
cfg: dict = {}
|
|
946
|
-
if config_path.exists():
|
|
947
|
-
try:
|
|
948
|
-
cfg = json.loads(config_path.read_text())
|
|
949
|
-
except (json.JSONDecodeError, OSError):
|
|
950
|
-
pass
|
|
951
|
-
|
|
952
|
-
if action == "get":
|
|
953
|
-
# Parse dot-notation key (e.g. "evolution.enabled")
|
|
954
|
-
parts = key.split(".") if key else []
|
|
955
|
-
node = cfg
|
|
956
|
-
for part in parts:
|
|
957
|
-
if isinstance(node, dict) and part in node:
|
|
958
|
-
node = node[part]
|
|
959
|
-
else:
|
|
960
|
-
node = None
|
|
961
|
-
break
|
|
962
|
-
|
|
963
|
-
if use_json:
|
|
964
|
-
from superlocalmemory.cli.json_output import json_print
|
|
965
|
-
json_print("config", data={"key": key, "value": node})
|
|
966
|
-
else:
|
|
967
|
-
if node is None:
|
|
968
|
-
print(f"{key}: (not set)")
|
|
969
|
-
else:
|
|
970
|
-
print(f"{key}: {node}")
|
|
971
|
-
|
|
972
|
-
elif action == "set":
|
|
973
|
-
_ALLOWED_CONFIG_KEYS = {
|
|
974
|
-
"evolution.enabled", "evolution.backend", "evolution.max_evolutions_per_cycle",
|
|
975
|
-
"evolution.mutation_model", "evolution.verify_model",
|
|
976
|
-
"evolution.confirm_model",
|
|
977
|
-
"mesh_enabled", "daemon_idle_timeout", "entity_compilation_enabled",
|
|
978
|
-
"graph_backend", "vector_backend",
|
|
979
|
-
# scale_engine_state is deliberately NOT settable. It is a record of
|
|
980
|
-
# what has been done to the store, not a preference: writing
|
|
981
|
-
# "promoted" by hand makes the daemon skip projecting a store that
|
|
982
|
-
# was never projected, and writing "local_core" onto a promoted one
|
|
983
|
-
# makes it project again over live backends. A real installation was
|
|
984
|
-
# found declaring two backends it had never built.
|
|
985
|
-
"scope.default_scope", "scope.recall_include_global",
|
|
986
|
-
"scope.recall_include_shared",
|
|
987
|
-
}
|
|
988
|
-
if key not in _ALLOWED_CONFIG_KEYS:
|
|
989
|
-
if use_json:
|
|
990
|
-
from superlocalmemory.cli.json_output import json_print
|
|
991
|
-
json_print("config", error={
|
|
992
|
-
"code": "DISALLOWED_KEY",
|
|
993
|
-
"message": f"'{key}' is not a configurable key. Allowed: {', '.join(sorted(_ALLOWED_CONFIG_KEYS))}",
|
|
994
|
-
})
|
|
995
|
-
else:
|
|
996
|
-
print(f"Error: '{key}' is not a configurable key. Allowed: {', '.join(sorted(_ALLOWED_CONFIG_KEYS))}")
|
|
997
|
-
sys.exit(1)
|
|
998
|
-
|
|
999
|
-
if not key or value is None:
|
|
1000
|
-
if use_json:
|
|
1001
|
-
from superlocalmemory.cli.json_output import json_print
|
|
1002
|
-
json_print("config", error={
|
|
1003
|
-
"code": "INVALID_INPUT",
|
|
1004
|
-
"message": "Usage: slm config set <key> <value>",
|
|
1005
|
-
})
|
|
1006
|
-
else:
|
|
1007
|
-
print("Usage: slm config set <key> <value>")
|
|
1008
|
-
sys.exit(1)
|
|
1009
|
-
|
|
1010
|
-
# Parse value: booleans, numbers, strings
|
|
1011
|
-
parsed_value: object
|
|
1012
|
-
if value.lower() in ("true", "yes", "on"):
|
|
1013
|
-
parsed_value = True
|
|
1014
|
-
elif value.lower() in ("false", "no", "off"):
|
|
1015
|
-
parsed_value = False
|
|
1016
|
-
else:
|
|
1017
|
-
try:
|
|
1018
|
-
parsed_value = int(value)
|
|
1019
|
-
except ValueError:
|
|
1020
|
-
try:
|
|
1021
|
-
parsed_value = float(value)
|
|
1022
|
-
except ValueError:
|
|
1023
|
-
parsed_value = value
|
|
1024
|
-
|
|
1025
|
-
if key == "scope.default_scope" and parsed_value not in {
|
|
1026
|
-
"personal", "shared", "global",
|
|
1027
|
-
}:
|
|
1028
|
-
message = "scope.default_scope must be personal, shared, or global"
|
|
1029
|
-
if use_json:
|
|
1030
|
-
from superlocalmemory.cli.json_output import json_print
|
|
1031
|
-
json_print("config", error={
|
|
1032
|
-
"code": "INVALID_VALUE", "message": message,
|
|
1033
|
-
})
|
|
1034
|
-
else:
|
|
1035
|
-
print(f"Error: {message}")
|
|
1036
|
-
sys.exit(1)
|
|
1037
|
-
if key in {
|
|
1038
|
-
"scope.recall_include_global", "scope.recall_include_shared",
|
|
1039
|
-
} and not isinstance(parsed_value, bool):
|
|
1040
|
-
message = f"{key} must be true or false"
|
|
1041
|
-
if use_json:
|
|
1042
|
-
from superlocalmemory.cli.json_output import json_print
|
|
1043
|
-
json_print("config", error={
|
|
1044
|
-
"code": "INVALID_VALUE", "message": message,
|
|
1045
|
-
})
|
|
1046
|
-
else:
|
|
1047
|
-
print(f"Error: {message}")
|
|
1048
|
-
sys.exit(1)
|
|
1049
|
-
|
|
1050
|
-
_MODEL_KEYS = {
|
|
1051
|
-
"evolution.mutation_model", "evolution.verify_model",
|
|
1052
|
-
"evolution.confirm_model",
|
|
1053
|
-
}
|
|
1054
|
-
if key in _MODEL_KEYS:
|
|
1055
|
-
from superlocalmemory.evolution.model_selection import _MODEL_ALIASES
|
|
1056
|
-
|
|
1057
|
-
accepted = set(_MODEL_ALIASES) | {"", "auto"}
|
|
1058
|
-
sval = str(parsed_value)
|
|
1059
|
-
if sval not in accepted:
|
|
1060
|
-
allowed = ", ".join(["auto", *sorted(_MODEL_ALIASES)])
|
|
1061
|
-
message = f"{key} must be one of: {allowed}"
|
|
1062
|
-
if use_json:
|
|
1063
|
-
from superlocalmemory.cli.json_output import json_print
|
|
1064
|
-
json_print("config", error={
|
|
1065
|
-
"code": "INVALID_VALUE", "message": message,
|
|
1066
|
-
})
|
|
1067
|
-
else:
|
|
1068
|
-
print(f"Error: {message}")
|
|
1069
|
-
sys.exit(1)
|
|
1070
|
-
# Normalise the "auto" sentinel to the empty string the
|
|
1071
|
-
# resolver treats as "pick the cheapest for the backend".
|
|
1072
|
-
if sval == "auto":
|
|
1073
|
-
parsed_value = ""
|
|
1074
|
-
|
|
1075
|
-
if key.startswith("scope."):
|
|
1076
|
-
from superlocalmemory.cli.daemon import daemon_request, is_daemon_running
|
|
1077
|
-
|
|
1078
|
-
if is_daemon_running():
|
|
1079
|
-
field = key.split(".", 1)[1]
|
|
1080
|
-
result = daemon_request(
|
|
1081
|
-
"PUT", "/api/v3/scope/config", {field: parsed_value},
|
|
1082
|
-
)
|
|
1083
|
-
if not isinstance(result, dict) or result.get("success") is not True:
|
|
1084
|
-
message = "resident daemon rejected the scope configuration"
|
|
1085
|
-
if use_json:
|
|
1086
|
-
from superlocalmemory.cli.json_output import json_print
|
|
1087
|
-
json_print("config", error={
|
|
1088
|
-
"code": "CONFIG_APPLY_FAILED", "message": message,
|
|
1089
|
-
})
|
|
1090
|
-
else:
|
|
1091
|
-
print(f"Error: {message}")
|
|
1092
|
-
sys.exit(1)
|
|
1093
|
-
old_value = None
|
|
1094
|
-
if use_json:
|
|
1095
|
-
from superlocalmemory.cli.json_output import json_print
|
|
1096
|
-
json_print("config", data={
|
|
1097
|
-
"key": key, "old_value": old_value,
|
|
1098
|
-
"new_value": result.get(field), "runtime": "daemon",
|
|
1099
|
-
})
|
|
1100
|
-
else:
|
|
1101
|
-
print(f"{key}: applied to resident daemon -> {result.get(field)}")
|
|
1102
|
-
return
|
|
1103
|
-
|
|
1104
|
-
# Set via dot-notation (e.g. "evolution.enabled" -> cfg["evolution"]["enabled"])
|
|
1105
|
-
parts = key.split(".")
|
|
1106
|
-
node = cfg
|
|
1107
|
-
for part in parts[:-1]:
|
|
1108
|
-
if part not in node or not isinstance(node.get(part), dict):
|
|
1109
|
-
node[part] = {}
|
|
1110
|
-
node = node[part]
|
|
1111
|
-
old_value = node.get(parts[-1])
|
|
1112
|
-
node[parts[-1]] = parsed_value
|
|
1113
|
-
|
|
1114
|
-
# Write back
|
|
1115
|
-
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1116
|
-
config_path.write_text(json.dumps(cfg, indent=2) + "\n")
|
|
1117
|
-
|
|
1118
|
-
if use_json:
|
|
1119
|
-
from superlocalmemory.cli.json_output import json_print
|
|
1120
|
-
json_print("config", data={
|
|
1121
|
-
"key": key, "old_value": old_value, "new_value": parsed_value,
|
|
1122
|
-
})
|
|
1123
|
-
else:
|
|
1124
|
-
print(f"{key}: {old_value} -> {parsed_value}")
|
|
1125
|
-
if key == "evolution.enabled" and parsed_value is True:
|
|
1126
|
-
print(
|
|
1127
|
-
"\n⚠ Skill evolution is now ON. It makes background "
|
|
1128
|
-
"LLM calls during consolidation\n"
|
|
1129
|
-
" (capped at 10 calls/cycle, 3 cycles/day). It defaults "
|
|
1130
|
-
"to the lowest-cost model\n"
|
|
1131
|
-
" for your backend (Claude → Haiku, Ollama → "
|
|
1132
|
-
"local/free).\n"
|
|
1133
|
-
" Pick models: slm config set evolution.mutation_model "
|
|
1134
|
-
"<auto|haiku|sonnet|ollama>\n"
|
|
1135
|
-
" Turn off: slm config set evolution.enabled false"
|
|
1136
|
-
)
|
|
1137
|
-
|
|
1138
|
-
else:
|
|
1139
|
-
if use_json:
|
|
1140
|
-
from superlocalmemory.cli.json_output import json_print
|
|
1141
|
-
json_print("config", error={
|
|
1142
|
-
"code": "UNKNOWN_ACTION",
|
|
1143
|
-
"message": f"Unknown action: {action}. Use 'get' or 'set'.",
|
|
1144
|
-
})
|
|
1145
|
-
else:
|
|
1146
|
-
print(f"Unknown config action: {action}. Use 'get' or 'set'.")
|
|
1147
|
-
sys.exit(1)
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
def cmd_evolve(args: Namespace) -> None:
|
|
1151
|
-
"""Run skill evolution for a session (called from Stop hook).
|
|
1152
|
-
|
|
1153
|
-
Reads config.json to check if evolution is enabled.
|
|
1154
|
-
If enabled, imports SkillEvolver and runs run_post_session().
|
|
1155
|
-
If disabled, exits silently (zero output for fire-and-forget).
|
|
1156
|
-
"""
|
|
1157
|
-
import json
|
|
1158
|
-
|
|
1159
|
-
from superlocalmemory.infra.data_root import state_path
|
|
1160
|
-
|
|
1161
|
-
session_id = getattr(args, "session", "") or ""
|
|
1162
|
-
profile = getattr(args, "profile", "default") or "default"
|
|
1163
|
-
|
|
1164
|
-
if not session_id:
|
|
1165
|
-
return # Silent exit — nothing to do without a session
|
|
1166
|
-
|
|
1167
|
-
from superlocalmemory.core.admission import gate_cli_mutation
|
|
1168
|
-
from superlocalmemory.core.operation_request import OperationKind
|
|
1169
|
-
gate_cli_mutation(OperationKind.EVOLVE_SKILL)
|
|
1170
|
-
|
|
1171
|
-
# Check if evolution is enabled via config.json
|
|
1172
|
-
config_path = state_path("config.json")
|
|
1173
|
-
try:
|
|
1174
|
-
cfg = json.loads(config_path.read_text()) if config_path.exists() else {}
|
|
1175
|
-
except (json.JSONDecodeError, OSError):
|
|
1176
|
-
return # Config unreadable — silent exit
|
|
1177
|
-
|
|
1178
|
-
evolution_cfg = cfg.get("evolution", {})
|
|
1179
|
-
if not evolution_cfg.get("enabled", False):
|
|
1180
|
-
return # Disabled — silent exit
|
|
1181
|
-
|
|
1182
|
-
# Heavy imports only if enabled (this runs as a Popen child)
|
|
1183
|
-
try:
|
|
1184
|
-
from superlocalmemory.evolution.skill_evolver import SkillEvolver
|
|
1185
|
-
|
|
1186
|
-
db_path = state_path("memory.db")
|
|
1187
|
-
if not db_path.exists():
|
|
1188
|
-
return
|
|
1189
|
-
|
|
1190
|
-
evolver = SkillEvolver(db_path)
|
|
1191
|
-
evolver.run_post_session(session_id, profile)
|
|
1192
|
-
except Exception:
|
|
1193
|
-
pass # Best-effort — don't crash the Stop hook
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
def cmd_adapters(args: Namespace) -> None:
|
|
1197
|
-
"""Manage ingestion adapters (Gmail, Calendar, Transcript).
|
|
1198
|
-
|
|
1199
|
-
Usage:
|
|
1200
|
-
slm adapters list — show all adapters
|
|
1201
|
-
slm adapters enable <name> — enable an adapter
|
|
1202
|
-
slm adapters disable <name> — disable and stop
|
|
1203
|
-
slm adapters start <name> — start running
|
|
1204
|
-
slm adapters stop <name> — stop running
|
|
1205
|
-
slm adapters status — detailed status
|
|
1206
|
-
"""
|
|
1207
|
-
from superlocalmemory.ingestion.adapter_manager import handle_adapters_cli
|
|
1208
|
-
# args.rest contains everything after "adapters"
|
|
1209
|
-
rest = getattr(args, 'rest', []) or []
|
|
1210
|
-
handle_adapters_cli(rest)
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
# -- Setup & Config (no --json — interactive commands) ---------------------
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
def cmd_setup(args: Namespace) -> None:
|
|
1217
|
-
"""Run the interactive setup wizard."""
|
|
1218
|
-
from superlocalmemory.cli.setup_wizard import run_wizard
|
|
1219
|
-
|
|
1220
|
-
run_wizard(auto=getattr(args, "auto", False))
|
|
1221
|
-
sys.exit(0) # Force clean exit (background threads from imports may linger)
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
def cmd_mode(args: Namespace) -> None:
|
|
1225
|
-
"""Get or set the operating mode.
|
|
1226
|
-
|
|
1227
|
-
v3.4.43 behavior change: switching modes via this CLI now PRESERVES the
|
|
1228
|
-
user's existing embedding, retrieval, evolution, forgetting, and math
|
|
1229
|
-
settings. Previously the CLI called ``SLMConfig.for_mode(...)`` which
|
|
1230
|
-
re-derived every field from mode defaults — silently clobbering user
|
|
1231
|
-
customizations (e.g. a tuned cross-encoder model, a custom embedding
|
|
1232
|
-
endpoint, or custom forgetting half-lives). The v3.4.34 ``mode_change=True``
|
|
1233
|
-
guard only protected the ``mode`` field itself; everything else was lost.
|
|
1234
|
-
|
|
1235
|
-
New rules:
|
|
1236
|
-
- Only ``config.mode`` changes.
|
|
1237
|
-
- If the user has NO LLM provider configured AND is switching to a mode
|
|
1238
|
-
that typically needs one (B or C), mode-appropriate LLM defaults are
|
|
1239
|
-
populated to avoid the daemon coming up dead. Existing LLM config
|
|
1240
|
-
is preserved as-is.
|
|
1241
|
-
- Embedding / retrieval / evolution / forgetting / math: untouched.
|
|
1242
|
-
"""
|
|
1243
|
-
from superlocalmemory.core.config import SLMConfig
|
|
1244
|
-
from superlocalmemory.storage.models import Mode
|
|
1245
|
-
|
|
1246
|
-
config = SLMConfig.load()
|
|
1247
|
-
|
|
1248
|
-
if args.value:
|
|
1249
|
-
from superlocalmemory.core.admission import gate_cli_mutation
|
|
1250
|
-
from superlocalmemory.core.operation_request import OperationKind
|
|
1251
|
-
gate_cli_mutation(OperationKind.MODE_CHANGE)
|
|
1252
|
-
|
|
1253
|
-
if getattr(args, 'json', False):
|
|
1254
|
-
from superlocalmemory.cli.json_output import json_print
|
|
1255
|
-
if args.value:
|
|
1256
|
-
old_mode = config.mode.value.upper()
|
|
1257
|
-
updated = SLMConfig.switch_mode(args.value)
|
|
1258
|
-
json_print("mode", data={
|
|
1259
|
-
"previous_mode": old_mode, "current_mode": args.value.upper(),
|
|
1260
|
-
}, next_actions=[
|
|
1261
|
-
{"command": "slm status --json", "description": "Check system status"},
|
|
1262
|
-
])
|
|
1263
|
-
else:
|
|
1264
|
-
json_print("mode", data={"current_mode": config.mode.value.upper()},
|
|
1265
|
-
next_actions=[
|
|
1266
|
-
{"command": "slm mode a --json", "description": "Switch to zero-cloud mode"},
|
|
1267
|
-
{"command": "slm mode c --json", "description": "Switch to full-power mode"},
|
|
1268
|
-
])
|
|
1269
|
-
return
|
|
1270
|
-
|
|
1271
|
-
if args.value:
|
|
1272
|
-
updated = SLMConfig.switch_mode(args.value)
|
|
1273
|
-
print(f"Mode set to: {args.value.upper()}")
|
|
1274
|
-
print(f" Embedding: {updated.embedding.provider}/{updated.embedding.model_name}")
|
|
1275
|
-
if args.value.lower() != "a":
|
|
1276
|
-
print(f" LLM: {updated.llm.provider}/{updated.llm.model}")
|
|
1277
|
-
print(f" Reranker: ONNX cross-encoder (enabled)")
|
|
1278
|
-
|
|
1279
|
-
# V3.3.4: Warn if Mode C lacks cloud API key
|
|
1280
|
-
if args.value == "c" and not updated.llm.api_key:
|
|
1281
|
-
print(" ⚠ Mode C requires a cloud API key. Run: slm provider set")
|
|
1282
|
-
print(" ℹ Run `slm restart` to apply the new mode.")
|
|
1283
|
-
else:
|
|
1284
|
-
print(f"Current mode: {config.mode.value.upper()}")
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
def cmd_provider(args: Namespace) -> None:
|
|
1288
|
-
"""Get or set the LLM provider."""
|
|
1289
|
-
from superlocalmemory.core.config import SLMConfig
|
|
1290
|
-
|
|
1291
|
-
config = SLMConfig.load()
|
|
1292
|
-
|
|
1293
|
-
if args.action == "set":
|
|
1294
|
-
from superlocalmemory.cli.setup_wizard import configure_provider
|
|
1295
|
-
|
|
1296
|
-
configure_provider(config, provider_name=getattr(args, "provider", None))
|
|
1297
|
-
else:
|
|
1298
|
-
print(f"Provider: {config.llm.provider or 'none (Mode A)'}")
|
|
1299
|
-
if config.llm.model:
|
|
1300
|
-
print(f"Model: {config.llm.model}")
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
def _cmd_context_dispatch(args: Namespace) -> None:
|
|
1304
|
-
"""V3.4.22 LLD-05: ``slm context prestage``."""
|
|
1305
|
-
from superlocalmemory.cli.context_commands import cmd_context
|
|
1306
|
-
cmd_context(args)
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
def _agents_md_source_factory():
|
|
1310
|
-
"""Return a callable that reads the bundled AGENTS.md content, or None on failure.
|
|
1311
|
-
|
|
1312
|
-
Source: plugin-src/rules/AGENTS.md (relative to package root).
|
|
1313
|
-
Gracefully skips if absent — never fails the MCP write.
|
|
1314
|
-
"""
|
|
1315
|
-
from pathlib import Path
|
|
1316
|
-
import sysconfig
|
|
1317
|
-
|
|
1318
|
-
# Resolve relative to the package root (src/superlocalmemory/../../)
|
|
1319
|
-
_pkg_root = Path(__file__).resolve().parents[3]
|
|
1320
|
-
_candidates = (
|
|
1321
|
-
_pkg_root / "plugin-src" / "rules" / "AGENTS.md",
|
|
1322
|
-
Path(sysconfig.get_path("data")) / "share" / "superlocalmemory"
|
|
1323
|
-
/ "portable-kit" / "rules" / "AGENTS.md",
|
|
1324
|
-
)
|
|
1325
|
-
|
|
1326
|
-
def _read() -> str | None:
|
|
1327
|
-
for candidate in _candidates:
|
|
1328
|
-
if candidate.exists():
|
|
1329
|
-
return candidate.read_text(encoding="utf-8")
|
|
1330
|
-
logger.warning(
|
|
1331
|
-
"AGENTS.md is not bundled in this distribution — skipping AGENTS.md write"
|
|
1332
|
-
)
|
|
1333
|
-
return None
|
|
1334
|
-
|
|
1335
|
-
return _read
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
def cmd_connect(args: Namespace) -> None:
|
|
1339
|
-
"""Configure IDE integrations.
|
|
1340
|
-
|
|
1341
|
-
Dispatch priority:
|
|
1342
|
-
1. ``slm connect <ide>`` where ide ∈ IDE_MATRIX → portable_kit.connect_ide
|
|
1343
|
-
(MCP-wiring + AGENTS.md; claude-code short-circuits to the plugin pointer).
|
|
1344
|
-
2. ``--cross-platform`` / ``--disable`` → LLD-05 CrossPlatformConnector.
|
|
1345
|
-
3. Bare ``slm connect`` / ``--list`` → legacy IDEConnector (markdown-rules).
|
|
1346
|
-
"""
|
|
1347
|
-
ide_arg = getattr(args, "ide", None)
|
|
1348
|
-
|
|
1349
|
-
# Intercept known IDE_MATRIX ids before legacy branches (critical: must run first)
|
|
1350
|
-
if ide_arg is not None:
|
|
1351
|
-
from superlocalmemory.hooks.portable_kit import (
|
|
1352
|
-
IDE_MATRIX,
|
|
1353
|
-
connect_ide,
|
|
1354
|
-
supported_ides,
|
|
1355
|
-
)
|
|
1356
|
-
|
|
1357
|
-
if ide_arg in IDE_MATRIX:
|
|
1358
|
-
here = getattr(args, "here", False)
|
|
1359
|
-
profile = getattr(args, "profile", None)
|
|
1360
|
-
transport = getattr(args, "transport", "stdio")
|
|
1361
|
-
daemon_port = getattr(args, "daemon_port", 8765)
|
|
1362
|
-
verify = getattr(args, "verify", False)
|
|
1363
|
-
project = None
|
|
1364
|
-
if here:
|
|
1365
|
-
import pathlib
|
|
1366
|
-
project = pathlib.Path.cwd()
|
|
1367
|
-
|
|
1368
|
-
result = connect_ide(
|
|
1369
|
-
ide_arg,
|
|
1370
|
-
home=None,
|
|
1371
|
-
project=project,
|
|
1372
|
-
here=here,
|
|
1373
|
-
profile=profile,
|
|
1374
|
-
agents_md_source=_agents_md_source_factory(),
|
|
1375
|
-
dry_run=getattr(args, "dry_run", False),
|
|
1376
|
-
transport=transport,
|
|
1377
|
-
daemon_port=daemon_port,
|
|
1378
|
-
)
|
|
1379
|
-
|
|
1380
|
-
# --verify: probe daemon health after writing
|
|
1381
|
-
if verify and transport in ("http", "http-mcp-remote") and not result.get("error"):
|
|
1382
|
-
from superlocalmemory.hooks.portable_kit import _check_daemon_health
|
|
1383
|
-
reachable = _check_daemon_health(daemon_port)
|
|
1384
|
-
if reachable:
|
|
1385
|
-
print(f"[verify] Daemon reachable at http://127.0.0.1:{daemon_port}/api/v3/health")
|
|
1386
|
-
else:
|
|
1387
|
-
print(
|
|
1388
|
-
f"[verify] Warning: daemon NOT reachable at "
|
|
1389
|
-
f"http://127.0.0.1:{daemon_port}/api/v3/health. "
|
|
1390
|
-
f"Run `slm serve start` to start it.",
|
|
1391
|
-
file=sys.stderr,
|
|
1392
|
-
)
|
|
1393
|
-
|
|
1394
|
-
if not result.get("error") and not getattr(args, "dry_run", False):
|
|
1395
|
-
from superlocalmemory.infra.local_diagnostics import record_operation
|
|
1396
|
-
|
|
1397
|
-
record_operation("activation", client=ide_arg)
|
|
1398
|
-
|
|
1399
|
-
if getattr(args, "json", False):
|
|
1400
|
-
from superlocalmemory.cli.json_output import json_print
|
|
1401
|
-
json_print("connect", data=result)
|
|
1402
|
-
return
|
|
1403
|
-
|
|
1404
|
-
if result["error"]:
|
|
1405
|
-
print(f"Error: {result['error']}", file=sys.stderr)
|
|
1406
|
-
print(
|
|
1407
|
-
f"Supported IDEs: {', '.join(supported_ides())}",
|
|
1408
|
-
file=sys.stderr,
|
|
1409
|
-
)
|
|
1410
|
-
sys.exit(1)
|
|
1411
|
-
|
|
1412
|
-
status_sym = {"wrote": "[+]", "merged": "[~]", "unchanged": "[=]",
|
|
1413
|
-
"would_write": "[~]",
|
|
1414
|
-
"skipped": "[s]", "error": "[!]"}.get(
|
|
1415
|
-
result["mcp_config"], "[?]"
|
|
1416
|
-
)
|
|
1417
|
-
print(
|
|
1418
|
-
f"{status_sym} {ide_arg}: mcp_config={result['mcp_config']} "
|
|
1419
|
-
f"path={result['mcp_path']}"
|
|
1420
|
-
)
|
|
1421
|
-
print(f" agents_md={result['agents_md']}")
|
|
1422
|
-
return
|
|
1423
|
-
|
|
1424
|
-
# Unknown ide — list supported and exit non-zero
|
|
1425
|
-
from superlocalmemory.hooks.portable_kit import supported_ides
|
|
1426
|
-
print(
|
|
1427
|
-
f"Unknown IDE '{ide_arg}'.\nSupported: {', '.join(supported_ides())}",
|
|
1428
|
-
file=sys.stderr,
|
|
1429
|
-
)
|
|
1430
|
-
sys.exit(1)
|
|
1431
|
-
|
|
1432
|
-
# Route --disable <name> and --cross-platform to the LLD-05 orchestrator.
|
|
1433
|
-
if getattr(args, "disable", None) or getattr(args, "cross_platform", False):
|
|
1434
|
-
from superlocalmemory.cli.context_commands import (
|
|
1435
|
-
cmd_connect_cross_platform,
|
|
1436
|
-
)
|
|
1437
|
-
cmd_connect_cross_platform(args)
|
|
1438
|
-
return
|
|
1439
|
-
from superlocalmemory.hooks.ide_connector import IDEConnector
|
|
1440
|
-
|
|
1441
|
-
connector = IDEConnector()
|
|
1442
|
-
dry_run = bool(getattr(args, "dry_run", False))
|
|
1443
|
-
|
|
1444
|
-
if getattr(args, 'json', False):
|
|
1445
|
-
from superlocalmemory.cli.json_output import json_print
|
|
1446
|
-
if getattr(args, "list", False):
|
|
1447
|
-
json_print("connect", data={"ides": connector.get_status()},
|
|
1448
|
-
next_actions=[
|
|
1449
|
-
{"command": "slm connect --json", "description": "Auto-configure all IDEs"},
|
|
1450
|
-
])
|
|
1451
|
-
elif getattr(args, "ide", None):
|
|
1452
|
-
success = connector.connect(args.ide)
|
|
1453
|
-
json_print("connect", data={"ide": args.ide, "connected": success})
|
|
1454
|
-
elif dry_run:
|
|
1455
|
-
json_print(
|
|
1456
|
-
"connect",
|
|
1457
|
-
data={
|
|
1458
|
-
"dry_run": True,
|
|
1459
|
-
"would_configure": connector.get_status(),
|
|
1460
|
-
},
|
|
1461
|
-
)
|
|
1462
|
-
else:
|
|
1463
|
-
json_print("connect", data={"results": connector.connect_all()},
|
|
1464
|
-
next_actions=[
|
|
1465
|
-
{"command": "slm status --json", "description": "Check system status"},
|
|
1466
|
-
])
|
|
1467
|
-
return
|
|
1468
|
-
|
|
1469
|
-
if getattr(args, "list", False):
|
|
1470
|
-
status = connector.get_status()
|
|
1471
|
-
for s in status:
|
|
1472
|
-
mark = "[+]" if s["installed"] else "[-]"
|
|
1473
|
-
print(f" {mark} {s['name']:20s} {s['config_path']}")
|
|
1474
|
-
return
|
|
1475
|
-
if dry_run:
|
|
1476
|
-
status = connector.get_status()
|
|
1477
|
-
print("Dry run — no IDE, hook, config, or SLM data files were written.")
|
|
1478
|
-
for item in status:
|
|
1479
|
-
print(f" would inspect/configure: {item['name']} ({item['config_path']})")
|
|
1480
|
-
return
|
|
1481
|
-
if getattr(args, "ide", None):
|
|
1482
|
-
success = connector.connect(args.ide)
|
|
1483
|
-
print(f"{'Connected' if success else 'Failed'}: {args.ide}")
|
|
1484
|
-
else:
|
|
1485
|
-
results = connector.connect_all()
|
|
1486
|
-
for ide_id, ide_status in results.items():
|
|
1487
|
-
print(f" {ide_id}: {ide_status}")
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
def cmd_migrate(args: Namespace) -> None:
|
|
1491
|
-
"""Run V2 to V3 migration."""
|
|
1492
|
-
from superlocalmemory.cli.migrate_cmd import cmd_migrate as _migrate
|
|
1493
|
-
|
|
1494
|
-
_migrate(args)
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
# -- Memory Operations (all support --json) --------------------------------
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
def cmd_list(args: Namespace) -> None:
|
|
1501
|
-
"""List recent memories chronologically."""
|
|
1502
|
-
from superlocalmemory.core.config import CANONICAL_LIST_LIMIT, SLMConfig
|
|
1503
|
-
from superlocalmemory.core.engine import MemoryEngine
|
|
1504
|
-
|
|
1505
|
-
use_json = getattr(args, 'json', False)
|
|
1506
|
-
try:
|
|
1507
|
-
config = SLMConfig.load()
|
|
1508
|
-
engine = MemoryEngine(config)
|
|
1509
|
-
engine.initialize()
|
|
1510
|
-
|
|
1511
|
-
limit = getattr(args, "limit", CANONICAL_LIST_LIMIT)
|
|
1512
|
-
# The query already returns newest-first; pushing the bound into SQL
|
|
1513
|
-
# keeps this from deserializing the whole table to show twenty rows.
|
|
1514
|
-
facts = engine._db.get_all_facts(engine.profile_id, limit=limit)
|
|
1515
|
-
except Exception as exc:
|
|
1516
|
-
if use_json:
|
|
1517
|
-
from superlocalmemory.cli.json_output import json_print
|
|
1518
|
-
json_print("list", error={"code": "ENGINE_ERROR", "message": str(exc)})
|
|
1519
|
-
sys.exit(1)
|
|
1520
|
-
raise
|
|
1521
|
-
|
|
1522
|
-
if use_json:
|
|
1523
|
-
from superlocalmemory.cli.json_output import json_print
|
|
1524
|
-
items = []
|
|
1525
|
-
for f in facts:
|
|
1526
|
-
ftype_raw = getattr(f, "fact_type", "")
|
|
1527
|
-
ftype = ftype_raw.value if hasattr(ftype_raw, "value") else str(ftype_raw)
|
|
1528
|
-
items.append({
|
|
1529
|
-
"fact_id": f.fact_id, "content": f.content,
|
|
1530
|
-
"fact_type": ftype, "created_at": (f.created_at or "")[:19],
|
|
1531
|
-
})
|
|
1532
|
-
json_print("list", data={"results": items, "count": len(items)},
|
|
1533
|
-
next_actions=[
|
|
1534
|
-
{"command": "slm recall '<query>' --json", "description": "Search memories"},
|
|
1535
|
-
{"command": "slm delete <fact_id> --json --yes", "description": "Delete a memory"},
|
|
1536
|
-
])
|
|
1537
|
-
return
|
|
1538
|
-
|
|
1539
|
-
if not facts:
|
|
1540
|
-
print("No memories stored yet.")
|
|
1541
|
-
else:
|
|
1542
|
-
print(f"Recent memories ({len(facts)}):\n")
|
|
1543
|
-
for i, f in enumerate(facts, 1):
|
|
1544
|
-
date = (f.created_at or "")[:19]
|
|
1545
|
-
ftype_raw = getattr(f, "fact_type", "")
|
|
1546
|
-
ftype = ftype_raw.value if hasattr(ftype_raw, "value") else str(ftype_raw)
|
|
1547
|
-
content = f.content[:100] + ("..." if len(f.content) > 100 else "")
|
|
1548
|
-
print(f" {i:3d}. [{date}] ({ftype}) {content}")
|
|
1549
|
-
|
|
1550
|
-
# V3.3.21: Show pending memories (store-first pattern)
|
|
1551
|
-
try:
|
|
1552
|
-
from superlocalmemory.cli.pending_store import get_pending
|
|
1553
|
-
pending = get_pending(limit=10)
|
|
1554
|
-
if pending:
|
|
1555
|
-
print(f"\nPending (processing in background): {len(pending)}")
|
|
1556
|
-
for p in pending:
|
|
1557
|
-
content = p["content"][:80] + ("..." if len(p["content"]) > 80 else "")
|
|
1558
|
-
print(f" \u23f3 [{p['created_at'][:19]}] {content}")
|
|
1559
|
-
except Exception:
|
|
1560
|
-
pass
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
def cmd_remember(args: Namespace) -> None:
|
|
1564
|
-
"""Store a memory through the owned daemon."""
|
|
1565
|
-
|
|
1566
|
-
use_json = getattr(args, 'json', False)
|
|
1567
|
-
sync_mode = getattr(args, 'sync_mode', False)
|
|
1568
|
-
# v3.6.15 multi-scope: scope=None means "not specified" → resolve to the
|
|
1569
|
-
# configured default_scope (personal) at the daemon / engine boundary.
|
|
1570
|
-
# Shared memory is opt-in, so an unset --scope always stays private.
|
|
1571
|
-
scope = getattr(args, 'scope', None)
|
|
1572
|
-
# v3.6.15: --shared-with is a comma-separated string on the CLI, but the
|
|
1573
|
-
# daemon (RememberRequest) and engine expect list[str]. Parse here so an
|
|
1574
|
-
# explicit `--shared-with a,b` doesn't 422 at the daemon and silently fall
|
|
1575
|
-
# back to a personal write.
|
|
1576
|
-
_sw_raw = getattr(args, 'shared_with', None)
|
|
1577
|
-
shared_with = (
|
|
1578
|
-
[s.strip() for s in _sw_raw.split(",") if s.strip()]
|
|
1579
|
-
if isinstance(_sw_raw, str) and _sw_raw.strip() else _sw_raw
|
|
1580
|
-
)
|
|
1581
|
-
|
|
1582
|
-
try:
|
|
1583
|
-
from superlocalmemory.cli.daemon import (
|
|
1584
|
-
daemon_request, ensure_daemon, is_daemon_running,
|
|
1585
|
-
)
|
|
1586
|
-
if not (is_daemon_running() or ensure_daemon()):
|
|
1587
|
-
_daemon_unavailable("remember", use_json)
|
|
1588
|
-
path = "/remember?wait=true" if sync_mode else "/remember"
|
|
1589
|
-
result = daemon_request(
|
|
1590
|
-
"POST", path, {
|
|
1591
|
-
"content": args.content,
|
|
1592
|
-
"tags": args.tags or "",
|
|
1593
|
-
"scope": scope,
|
|
1594
|
-
"shared_with": shared_with,
|
|
1595
|
-
},
|
|
1596
|
-
timeout_seconds=30,
|
|
1597
|
-
)
|
|
1598
|
-
if result and "fact_ids" in result:
|
|
1599
|
-
if use_json:
|
|
1600
|
-
from superlocalmemory.cli.json_output import json_print
|
|
1601
|
-
json_print("remember", data=result)
|
|
1602
|
-
else:
|
|
1603
|
-
state = result.get("materialization_state", "queryable")
|
|
1604
|
-
operation_id = result.get("operation_id", "unknown")
|
|
1605
|
-
print(
|
|
1606
|
-
f"{state.capitalize()} \u2713 {result['count']} facts "
|
|
1607
|
-
f"(operation={operation_id})."
|
|
1608
|
-
)
|
|
1609
|
-
return
|
|
1610
|
-
except SystemExit:
|
|
1611
|
-
raise
|
|
1612
|
-
except Exception as exc:
|
|
1613
|
-
logger.warning("owned daemon remember request failed: %s", exc)
|
|
1614
|
-
_daemon_unavailable("remember", use_json)
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
def cmd_recall(args: Namespace) -> None:
|
|
1618
|
-
"""Search memories through the owned daemon without a local engine."""
|
|
1619
|
-
use_json = getattr(args, 'json', False)
|
|
1620
|
-
# v3.6.15: None = "not specified" → daemon/engine resolves the configured
|
|
1621
|
-
# default (shared-off). Only an explicit --include-global / --no-global
|
|
1622
|
-
# produces True/False here.
|
|
1623
|
-
include_global = getattr(args, 'include_global', None)
|
|
1624
|
-
include_shared = getattr(args, 'include_shared', None)
|
|
1625
|
-
# Phase-1/D2: clamp cross-profile scope flags before forwarding to daemon.
|
|
1626
|
-
from superlocalmemory.core.admission import enforce_read_scope
|
|
1627
|
-
include_global, include_shared = enforce_read_scope(include_global, include_shared)
|
|
1628
|
-
|
|
1629
|
-
# V3.3.21: Route through daemon for instant response (no cold start).
|
|
1630
|
-
# S9-DASH-02: pass a stable session_id derived from the shell's
|
|
1631
|
-
# parent PID so a sequence of CLI recalls in one terminal can be
|
|
1632
|
-
# grouped. The Stop hook on session end won't fire for CLI, so
|
|
1633
|
-
# these outcomes are closed by the reaper (TTL → neutral reward).
|
|
1634
|
-
try:
|
|
1635
|
-
from superlocalmemory.cli.daemon import is_daemon_running, daemon_request, ensure_daemon
|
|
1636
|
-
if is_daemon_running() or ensure_daemon():
|
|
1637
|
-
from urllib.parse import quote
|
|
1638
|
-
session_id = f"cli:{os.getppid()}"
|
|
1639
|
-
fast_qs = "&fast=true" if getattr(args, "fast", False) else ""
|
|
1640
|
-
# Only send scope flags when the user set them explicitly; absent =
|
|
1641
|
-
# let the daemon resolve the configured default. (Never emit
|
|
1642
|
-
# "none" — that would parse as a missing/false value.)
|
|
1643
|
-
scope_qs = ""
|
|
1644
|
-
if include_global is not None:
|
|
1645
|
-
scope_qs += f"&include_global={str(include_global).lower()}"
|
|
1646
|
-
if include_shared is not None:
|
|
1647
|
-
scope_qs += f"&include_shared={str(include_shared).lower()}"
|
|
1648
|
-
_window = getattr(args, "window", "") or ""
|
|
1649
|
-
window_qs = f"&window={quote(_window)}" if _window else ""
|
|
1650
|
-
_as_of = getattr(args, "as_of", "") or ""
|
|
1651
|
-
if _as_of:
|
|
1652
|
-
from superlocalmemory.retrieval.temporal_utils import normalize_as_of
|
|
1653
|
-
_as_of_norm = normalize_as_of(_as_of)
|
|
1654
|
-
if _as_of_norm is None:
|
|
1655
|
-
import sys as _sys
|
|
1656
|
-
_sys.stderr.write(
|
|
1657
|
-
f"Error: invalid --as-of value: {_as_of!r}\n"
|
|
1658
|
-
"Expected ISO 8601 UTC datetime, e.g. '2024-01-01T00:00:00Z'.\n"
|
|
1659
|
-
)
|
|
1660
|
-
_sys.exit(1)
|
|
1661
|
-
_as_of = _as_of_norm
|
|
1662
|
-
as_of_qs = f"&as_of={quote(_as_of)}" if _as_of else ""
|
|
1663
|
-
def _strict_time_qs(attr: str, parameter: str) -> str:
|
|
1664
|
-
raw = getattr(args, attr, "") or ""
|
|
1665
|
-
if not raw:
|
|
1666
|
-
return ""
|
|
1667
|
-
from superlocalmemory.retrieval.temporal_utils import normalize_as_of
|
|
1668
|
-
normalized = normalize_as_of(raw)
|
|
1669
|
-
if normalized is None:
|
|
1670
|
-
import sys as _sys
|
|
1671
|
-
_sys.stderr.write(
|
|
1672
|
-
f"Error: invalid --{attr.replace('_', '-')} value: {raw!r}\n"
|
|
1673
|
-
)
|
|
1674
|
-
_sys.exit(1)
|
|
1675
|
-
return f"&{parameter}={quote(normalized)}"
|
|
1676
|
-
known_as_of_qs = _strict_time_qs("known_as_of", "known_as_of")
|
|
1677
|
-
valid_at_qs = _strict_time_qs("valid_at", "valid_at")
|
|
1678
|
-
unknown_qs = "&include_unknown=true" if getattr(args, "include_unknown", False) else ""
|
|
1679
|
-
result = daemon_request(
|
|
1680
|
-
"GET",
|
|
1681
|
-
f"/recall?q={quote(args.query)}&limit={args.limit}"
|
|
1682
|
-
f"&session_id={quote(session_id)}{fast_qs}{scope_qs}{window_qs}{as_of_qs}"
|
|
1683
|
-
f"{known_as_of_qs}{valid_at_qs}{unknown_qs}",
|
|
1684
|
-
)
|
|
1685
|
-
if result and "results" in result:
|
|
1686
|
-
# Format daemon response same as engine response
|
|
1687
|
-
if use_json:
|
|
1688
|
-
from superlocalmemory.cli.json_output import json_print
|
|
1689
|
-
json_print("recall", data=result, next_actions=[
|
|
1690
|
-
{"command": "slm list --json", "description": "List recent memories"},
|
|
1691
|
-
])
|
|
1692
|
-
return
|
|
1693
|
-
if not result["results"]:
|
|
1694
|
-
print("No confident match."
|
|
1695
|
-
if result.get("no_confident_match")
|
|
1696
|
-
else "No matching memories found.")
|
|
1697
|
-
return
|
|
1698
|
-
# Text output.
|
|
1699
|
-
# PR #101: ``dict.get(k, 0)`` returns the DEFAULT only when the
|
|
1700
|
-
# key is ABSENT — a present-but-null value still reaches the
|
|
1701
|
-
# format spec and raises "unsupported format string passed to
|
|
1702
|
-
# NoneType.__format__". ``or 0`` covers both. Same for score,
|
|
1703
|
-
# which the keyword-fallback path returns as None.
|
|
1704
|
-
elapsed_ms = result.get('retrieval_time_ms') or 0
|
|
1705
|
-
print(
|
|
1706
|
-
"SpreadingActivation.search completed via daemon "
|
|
1707
|
-
f"({elapsed_ms:.0f}ms)"
|
|
1708
|
-
)
|
|
1709
|
-
for i, r in enumerate(result["results"], 1):
|
|
1710
|
-
score = r.get('score') or 0
|
|
1711
|
-
print(f" {i}. [{score:.2f}] {r['content']}")
|
|
1712
|
-
return
|
|
1713
|
-
except Exception as _exc: # noqa: BLE001
|
|
1714
|
-
logger.warning(
|
|
1715
|
-
"owned daemon recall request failed: %s", _exc
|
|
1716
|
-
)
|
|
1717
|
-
|
|
1718
|
-
_daemon_unavailable("recall", use_json)
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
def _cli_record_signals(config, query, results):
|
|
1722
|
-
"""Record learning signals from CLI recall (no MCP dependency)."""
|
|
1723
|
-
from pathlib import Path
|
|
1724
|
-
|
|
1725
|
-
from superlocalmemory.learning.feedback import FeedbackCollector
|
|
1726
|
-
from superlocalmemory.learning.signals import LearningSignals
|
|
1727
|
-
configured_root = getattr(config, "base_dir", None)
|
|
1728
|
-
if configured_root is not None:
|
|
1729
|
-
slm_dir = Path(configured_root)
|
|
1730
|
-
else:
|
|
1731
|
-
from superlocalmemory.infra.data_root import canonical_data_root
|
|
1732
|
-
slm_dir = canonical_data_root()
|
|
1733
|
-
pid = config.active_profile
|
|
1734
|
-
fact_ids = [r.fact.fact_id for r in results[:10]]
|
|
1735
|
-
if not fact_ids:
|
|
1736
|
-
return
|
|
1737
|
-
FeedbackCollector(slm_dir / "learning.db").record_implicit(
|
|
1738
|
-
profile_id=pid, query=query,
|
|
1739
|
-
fact_ids_returned=fact_ids, fact_ids_available=fact_ids,
|
|
1740
|
-
)
|
|
1741
|
-
signals = LearningSignals(slm_dir / "learning.db")
|
|
1742
|
-
signals.record_co_retrieval(pid, fact_ids)
|
|
1743
|
-
for fid in fact_ids[:5]:
|
|
1744
|
-
LearningSignals.boost_confidence(str(slm_dir / "memory.db"), fid)
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
def cmd_forget(args: Namespace) -> None:
|
|
1748
|
-
"""Delete daemon-queried memories matching a query."""
|
|
1749
|
-
from superlocalmemory.core.admission import gate_cli_mutation
|
|
1750
|
-
from superlocalmemory.core.operation_request import OperationKind
|
|
1751
|
-
gate_cli_mutation(OperationKind.FORGET)
|
|
1752
|
-
|
|
1753
|
-
import urllib.parse
|
|
1754
|
-
|
|
1755
|
-
from superlocalmemory.cli.daemon import (
|
|
1756
|
-
daemon_request,
|
|
1757
|
-
ensure_daemon,
|
|
1758
|
-
is_daemon_running,
|
|
1759
|
-
)
|
|
1760
|
-
|
|
1761
|
-
use_json = getattr(args, 'json', False)
|
|
1762
|
-
dry_run = getattr(args, 'dry_run', False)
|
|
1763
|
-
raw_query = getattr(args, 'query', None)
|
|
1764
|
-
|
|
1765
|
-
# F3: `query` is optional so `slm forget --dry-run` can preview every memory.
|
|
1766
|
-
# Deletion ALWAYS requires an explicit query — a bare `slm forget` must never
|
|
1767
|
-
# mass-delete. In dry-run mode a missing query means "match all" (preview).
|
|
1768
|
-
if raw_query is None and not dry_run:
|
|
1769
|
-
msg = (
|
|
1770
|
-
"A query is required to delete. Preview everything with "
|
|
1771
|
-
"'slm forget --dry-run', or delete matches with 'slm forget <query>'."
|
|
1772
|
-
)
|
|
1773
|
-
if use_json:
|
|
1774
|
-
from superlocalmemory.cli.json_output import json_print
|
|
1775
|
-
json_print("forget", error={"code": "QUERY_REQUIRED", "message": msg})
|
|
1776
|
-
else:
|
|
1777
|
-
print(msg)
|
|
1778
|
-
sys.exit(2)
|
|
1779
|
-
|
|
1780
|
-
query_lower = "" if raw_query is None else raw_query.lower()
|
|
1781
|
-
query_label = raw_query if raw_query is not None else "*all*"
|
|
1782
|
-
|
|
1783
|
-
if not (is_daemon_running() or ensure_daemon()):
|
|
1784
|
-
_daemon_unavailable("forget", use_json)
|
|
1785
|
-
|
|
1786
|
-
memories: list[dict[str, object]] = []
|
|
1787
|
-
offset = 0
|
|
1788
|
-
while True:
|
|
1789
|
-
page = daemon_request("GET", f"/api/memories?limit=200&offset={offset}")
|
|
1790
|
-
rows = page.get("memories") if isinstance(page, dict) else None
|
|
1791
|
-
if not isinstance(rows, list):
|
|
1792
|
-
_daemon_unavailable("forget", use_json)
|
|
1793
|
-
memories.extend(row for row in rows if isinstance(row, dict))
|
|
1794
|
-
if not page.get("has_more"):
|
|
1795
|
-
break
|
|
1796
|
-
offset += len(rows)
|
|
1797
|
-
if not rows:
|
|
1798
|
-
_daemon_unavailable("forget", use_json)
|
|
1799
|
-
|
|
1800
|
-
matches = [
|
|
1801
|
-
fact for fact in memories
|
|
1802
|
-
if query_lower in str(fact.get("content", "")).lower()
|
|
1803
|
-
]
|
|
1804
|
-
|
|
1805
|
-
def delete_from_daemon(fact_id: str) -> None:
|
|
1806
|
-
result = daemon_request(
|
|
1807
|
-
"DELETE",
|
|
1808
|
-
"/api/memories/" + urllib.parse.quote(fact_id, safe=""),
|
|
1809
|
-
)
|
|
1810
|
-
if not isinstance(result, dict) or not result.get("success"):
|
|
1811
|
-
_daemon_unavailable("forget", use_json)
|
|
1812
|
-
|
|
1813
|
-
if use_json:
|
|
1814
|
-
from superlocalmemory.cli.json_output import json_print
|
|
1815
|
-
if not matches:
|
|
1816
|
-
json_print("forget", data={"matched_count": 0, "deleted_count": 0, "matches": []})
|
|
1817
|
-
return
|
|
1818
|
-
match_items = [
|
|
1819
|
-
{"fact_id": f["id"], "content": str(f.get("content", ""))[:120]}
|
|
1820
|
-
for f in matches[:20]
|
|
1821
|
-
]
|
|
1822
|
-
if dry_run:
|
|
1823
|
-
json_print("forget", data={
|
|
1824
|
-
"matched_count": len(matches), "deleted_count": 0,
|
|
1825
|
-
"dry_run": True, "matches": match_items,
|
|
1826
|
-
})
|
|
1827
|
-
return
|
|
1828
|
-
if getattr(args, 'yes', False):
|
|
1829
|
-
for f in matches:
|
|
1830
|
-
delete_from_daemon(str(f["id"]))
|
|
1831
|
-
json_print("forget", data={
|
|
1832
|
-
"matched_count": len(matches), "deleted_count": len(matches),
|
|
1833
|
-
"deleted": [f["id"] for f in matches],
|
|
1834
|
-
}, next_actions=[
|
|
1835
|
-
{"command": "slm list --json", "description": "Verify remaining memories"},
|
|
1836
|
-
])
|
|
1837
|
-
else:
|
|
1838
|
-
json_print("forget", data={
|
|
1839
|
-
"matched_count": len(matches), "deleted_count": 0,
|
|
1840
|
-
"matches": match_items,
|
|
1841
|
-
"hint": "Add --yes to confirm deletion",
|
|
1842
|
-
}, next_actions=[
|
|
1843
|
-
{
|
|
1844
|
-
"command": f"slm forget '{query_label}' --json --yes",
|
|
1845
|
-
"description": "Confirm deletion",
|
|
1846
|
-
},
|
|
1847
|
-
])
|
|
1848
|
-
return
|
|
1849
|
-
|
|
1850
|
-
if not matches:
|
|
1851
|
-
print(f"No memories matching '{query_label}'")
|
|
1852
|
-
return
|
|
1853
|
-
print(f"Found {len(matches)} matching memories:")
|
|
1854
|
-
for f in matches[:10]:
|
|
1855
|
-
print(f" - {str(f['id'])[:8]}... {str(f.get('content', ''))[:80]}")
|
|
1856
|
-
if dry_run:
|
|
1857
|
-
print(f"(dry run — {len(matches)} would be deleted)")
|
|
1858
|
-
return
|
|
1859
|
-
if getattr(args, 'yes', False):
|
|
1860
|
-
for f in matches:
|
|
1861
|
-
delete_from_daemon(str(f["id"]))
|
|
1862
|
-
print(f"Deleted {len(matches)} memories.")
|
|
1863
|
-
return
|
|
1864
|
-
confirm = input(f"Delete {len(matches)} memories? [y/N] ").strip().lower()
|
|
1865
|
-
if confirm in ("y", "yes"):
|
|
1866
|
-
for f in matches:
|
|
1867
|
-
delete_from_daemon(str(f["id"]))
|
|
1868
|
-
print(f"Deleted {len(matches)} memories.")
|
|
1869
|
-
else:
|
|
1870
|
-
print("Cancelled.")
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
def cmd_delete(args: Namespace) -> None:
|
|
1874
|
-
"""Delete a specific memory by exact fact ID."""
|
|
1875
|
-
from superlocalmemory.core.admission import gate_cli_mutation
|
|
1876
|
-
from superlocalmemory.core.operation_request import OperationKind
|
|
1877
|
-
gate_cli_mutation(OperationKind.FORGET)
|
|
1878
|
-
|
|
1879
|
-
import urllib.parse
|
|
1880
|
-
|
|
1881
|
-
from superlocalmemory.cli.daemon import (
|
|
1882
|
-
daemon_request,
|
|
1883
|
-
ensure_daemon,
|
|
1884
|
-
is_daemon_running,
|
|
1885
|
-
)
|
|
1886
|
-
|
|
1887
|
-
use_json = getattr(args, 'json', False)
|
|
1888
|
-
fact_id = args.fact_id.strip()
|
|
1889
|
-
if is_daemon_running() or ensure_daemon():
|
|
1890
|
-
path = "/api/memories/" + urllib.parse.quote(fact_id, safe="")
|
|
1891
|
-
confirmed = getattr(args, "yes", False)
|
|
1892
|
-
content = ""
|
|
1893
|
-
if not confirmed:
|
|
1894
|
-
detail = daemon_request(
|
|
1895
|
-
"GET",
|
|
1896
|
-
"/api/facts/" + urllib.parse.quote(fact_id, safe=""),
|
|
1897
|
-
)
|
|
1898
|
-
if not isinstance(detail, dict):
|
|
1899
|
-
_daemon_unavailable("delete", use_json)
|
|
1900
|
-
content = str(detail.get("content") or "")
|
|
1901
|
-
if use_json:
|
|
1902
|
-
from superlocalmemory.cli.json_output import json_print
|
|
1903
|
-
json_print("delete", data={
|
|
1904
|
-
"fact_id": fact_id,
|
|
1905
|
-
"content": content[:120],
|
|
1906
|
-
"deleted": False,
|
|
1907
|
-
"hint": "Add --yes to confirm deletion",
|
|
1908
|
-
}, next_actions=[
|
|
1909
|
-
{
|
|
1910
|
-
"command": f"slm delete {fact_id} --json --yes",
|
|
1911
|
-
"description": "Confirm deletion",
|
|
1912
|
-
},
|
|
1913
|
-
])
|
|
1914
|
-
return
|
|
1915
|
-
print(f"Memory: {content[:120]}")
|
|
1916
|
-
confirmed = input("Delete this memory? [y/N] ").strip().lower() in (
|
|
1917
|
-
"y", "yes",
|
|
1918
|
-
)
|
|
1919
|
-
if not confirmed:
|
|
1920
|
-
print("Cancelled.")
|
|
1921
|
-
return
|
|
1922
|
-
|
|
1923
|
-
result = daemon_request("DELETE", path)
|
|
1924
|
-
if not isinstance(result, dict) or not result.get("success"):
|
|
1925
|
-
_daemon_unavailable("delete", use_json)
|
|
1926
|
-
if use_json:
|
|
1927
|
-
from superlocalmemory.cli.json_output import json_print
|
|
1928
|
-
json_print(
|
|
1929
|
-
"delete",
|
|
1930
|
-
data={"deleted": fact_id, "content": content[:120]},
|
|
1931
|
-
next_actions=[
|
|
1932
|
-
{
|
|
1933
|
-
"command": "slm list --json",
|
|
1934
|
-
"description": "Verify remaining memories",
|
|
1935
|
-
},
|
|
1936
|
-
],
|
|
1937
|
-
)
|
|
1938
|
-
else:
|
|
1939
|
-
print(f"Deleted: {fact_id}")
|
|
1940
|
-
return
|
|
1941
|
-
_daemon_unavailable("delete", use_json)
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
def cmd_update(args: Namespace) -> None:
|
|
1945
|
-
"""Update the content of a specific memory by exact fact ID."""
|
|
1946
|
-
from superlocalmemory.core.admission import gate_cli_mutation
|
|
1947
|
-
from superlocalmemory.core.operation_request import OperationKind
|
|
1948
|
-
gate_cli_mutation(OperationKind.CORRECT)
|
|
1949
|
-
|
|
1950
|
-
import urllib.parse
|
|
1951
|
-
|
|
1952
|
-
from superlocalmemory.cli.daemon import (
|
|
1953
|
-
daemon_request,
|
|
1954
|
-
ensure_daemon,
|
|
1955
|
-
is_daemon_running,
|
|
1956
|
-
)
|
|
1957
|
-
|
|
1958
|
-
use_json = getattr(args, 'json', False)
|
|
1959
|
-
fact_id = args.fact_id.strip()
|
|
1960
|
-
new_content = args.content.strip()
|
|
1961
|
-
|
|
1962
|
-
if not new_content:
|
|
1963
|
-
if use_json:
|
|
1964
|
-
from superlocalmemory.cli.json_output import json_print
|
|
1965
|
-
json_print("update", error={"code": "INVALID_INPUT", "message": "content cannot be empty"})
|
|
1966
|
-
sys.exit(1)
|
|
1967
|
-
print("Error: content cannot be empty")
|
|
1968
|
-
return
|
|
1969
|
-
|
|
1970
|
-
if is_daemon_running() or ensure_daemon():
|
|
1971
|
-
path = "/api/memories/" + urllib.parse.quote(fact_id, safe="")
|
|
1972
|
-
result = daemon_request("PATCH", path, {"content": new_content})
|
|
1973
|
-
if not isinstance(result, dict) or not result.get("success"):
|
|
1974
|
-
_daemon_unavailable("update", use_json)
|
|
1975
|
-
if use_json:
|
|
1976
|
-
from superlocalmemory.cli.json_output import json_print
|
|
1977
|
-
json_print("update", data={
|
|
1978
|
-
"predecessor_fact_id": result.get("predecessor_fact_id", fact_id),
|
|
1979
|
-
"successor_fact_id": result.get("successor_fact_id"),
|
|
1980
|
-
"correction_case": result.get("correction_case"),
|
|
1981
|
-
"review_required": bool(result.get("review_required", False)),
|
|
1982
|
-
}, next_actions=[
|
|
1983
|
-
{
|
|
1984
|
-
"command": "slm brain --json",
|
|
1985
|
-
"description": "Inspect the observation-only brain status",
|
|
1986
|
-
},
|
|
1987
|
-
])
|
|
1988
|
-
else:
|
|
1989
|
-
if result.get("review_required"):
|
|
1990
|
-
case = result.get("correction_case", {})
|
|
1991
|
-
print(f"Correction proposed: {case.get('case_id', 'unknown')}")
|
|
1992
|
-
print(f"Predecessor remains current until review: {fact_id}")
|
|
1993
|
-
else:
|
|
1994
|
-
print(f"Unchanged: {fact_id}")
|
|
1995
|
-
return
|
|
1996
|
-
_daemon_unavailable("update", use_json)
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
def cmd_review_correction(args: Namespace) -> None:
|
|
2000
|
-
"""Apply, reject, or roll back an explicitly reviewed correction case."""
|
|
2001
|
-
from superlocalmemory.core.admission import gate_cli_mutation
|
|
2002
|
-
from superlocalmemory.core.operation_request import OperationKind
|
|
2003
|
-
|
|
2004
|
-
gate_cli_mutation(OperationKind.CORRECT)
|
|
2005
|
-
import urllib.parse
|
|
2006
|
-
|
|
2007
|
-
from superlocalmemory.cli.daemon import daemon_request, ensure_daemon, is_daemon_running
|
|
2008
|
-
|
|
2009
|
-
use_json = getattr(args, "json", False)
|
|
2010
|
-
action = str(args.action).strip().lower()
|
|
2011
|
-
if action not in {"apply", "reject", "rollback"}:
|
|
2012
|
-
raise ValueError("action must be apply, reject, or rollback")
|
|
2013
|
-
if not isinstance(args.expected_version, int) or args.expected_version < 0:
|
|
2014
|
-
raise ValueError("expected_version must be a non-negative integer")
|
|
2015
|
-
if not (is_daemon_running() or ensure_daemon()):
|
|
2016
|
-
_daemon_unavailable("correction review", use_json)
|
|
2017
|
-
return
|
|
2018
|
-
payload: dict[str, object] = {"expected_version": args.expected_version}
|
|
2019
|
-
if getattr(args, "event_valid_until", None):
|
|
2020
|
-
payload["event_valid_until"] = args.event_valid_until
|
|
2021
|
-
path = "/api/corrections/" + urllib.parse.quote(args.case_id, safe="") + "/" + action
|
|
2022
|
-
result = daemon_request("POST", path, payload)
|
|
2023
|
-
if not isinstance(result, dict) or not result.get("success"):
|
|
2024
|
-
_daemon_unavailable("correction review", use_json)
|
|
2025
|
-
return
|
|
2026
|
-
if use_json:
|
|
2027
|
-
from superlocalmemory.cli.json_output import json_print
|
|
2028
|
-
|
|
2029
|
-
json_print("review-correction", data=result)
|
|
2030
|
-
else:
|
|
2031
|
-
case = result.get("correction_case", {})
|
|
2032
|
-
print(f"Correction {action}: {case.get('case_id', args.case_id)}")
|
|
2033
|
-
print(f"State: {case.get('status', 'unknown')}")
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
# -- Diagnostics (all support --json) -------------------------------------
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
def cmd_status(args: Namespace) -> None:
|
|
2040
|
-
"""Show system status."""
|
|
2041
|
-
from superlocalmemory.core.config import SLMConfig
|
|
2042
|
-
# Same source the other two status surfaces read, so all three cannot
|
|
2043
|
-
# disagree about which version answered.
|
|
2044
|
-
from superlocalmemory.server.routes.helpers import SLM_VERSION
|
|
2045
|
-
|
|
2046
|
-
config = SLMConfig.load()
|
|
2047
|
-
daemon_status = None
|
|
2048
|
-
try:
|
|
2049
|
-
from superlocalmemory.cli.daemon import (
|
|
2050
|
-
daemon_request,
|
|
2051
|
-
is_daemon_running,
|
|
2052
|
-
)
|
|
2053
|
-
|
|
2054
|
-
if is_daemon_running():
|
|
2055
|
-
candidate = daemon_request("GET", "/status")
|
|
2056
|
-
if isinstance(candidate, dict) and candidate.get("profile"):
|
|
2057
|
-
daemon_status = candidate
|
|
2058
|
-
except Exception:
|
|
2059
|
-
logger.debug(
|
|
2060
|
-
"cmd_status: daemon runtime status unavailable; using offline view",
|
|
2061
|
-
exc_info=True,
|
|
2062
|
-
)
|
|
2063
|
-
|
|
2064
|
-
if getattr(args, 'json', False):
|
|
2065
|
-
from superlocalmemory.cli.json_output import json_print
|
|
2066
|
-
|
|
2067
|
-
if daemon_status is not None:
|
|
2068
|
-
data = {
|
|
2069
|
-
# Lowercase is what the daemon, the HTTP surface and MCP all
|
|
2070
|
-
# report. Uppercasing it here made one field disagree across
|
|
2071
|
-
# surfaces for anyone comparing the two answers.
|
|
2072
|
-
"mode": str(daemon_status.get("mode", "unknown")),
|
|
2073
|
-
"provider": daemon_status.get("provider", "none"),
|
|
2074
|
-
"profile": daemon_status["profile"],
|
|
2075
|
-
"base_dir": daemon_status.get("base_dir", str(config.base_dir)),
|
|
2076
|
-
"db_path": daemon_status.get("db_path", str(config.db_path)),
|
|
2077
|
-
"db_size_mb": float(daemon_status.get("db_size_mb", 0.0)),
|
|
2078
|
-
"fact_count": int(daemon_status.get("fact_count", 0)),
|
|
2079
|
-
"entity_count": int(daemon_status.get("entity_count", 0)),
|
|
2080
|
-
"edge_count": int(daemon_status.get("edge_count", 0)),
|
|
2081
|
-
"profile_generation": int(
|
|
2082
|
-
daemon_status.get("profile_generation", 0)
|
|
2083
|
-
),
|
|
2084
|
-
"version": SLM_VERSION,
|
|
2085
|
-
"projection_queue_depth": int(
|
|
2086
|
-
daemon_status.get("projection_queue_depth", 0)
|
|
2087
|
-
),
|
|
2088
|
-
}
|
|
2089
|
-
json_print("status", data=data, next_actions=[
|
|
2090
|
-
{"command": "slm health --json", "description": "Check math layer health"},
|
|
2091
|
-
{"command": "slm list --json", "description": "List recent memories"},
|
|
2092
|
-
])
|
|
2093
|
-
return
|
|
2094
|
-
|
|
2095
|
-
# canonical key set — db_size_mb always present (0.0 if absent).
|
|
2096
|
-
db_size_mb = 0.0
|
|
2097
|
-
if config.db_path.exists():
|
|
2098
|
-
db_size_mb = round(config.db_path.stat().st_size / 1024 / 1024, 2)
|
|
2099
|
-
|
|
2100
|
-
# Open engine for counts (json branch only — LLD Decision B).
|
|
2101
|
-
# Fail-open to 0 on any error; status must never crash.
|
|
2102
|
-
# Guard on db existence: `slm status --json` must stay observational —
|
|
2103
|
-
# opening the engine on a fresh install would create + migrate the db
|
|
2104
|
-
# (MemoryEngine.initialize → DatabaseManager mkdir/connect/DDL). A
|
|
2105
|
-
# previously read-only command must not acquire a write side-effect.
|
|
2106
|
-
fact_count = 0
|
|
2107
|
-
entity_count = 0
|
|
2108
|
-
edge_count = 0
|
|
2109
|
-
eng = None
|
|
2110
|
-
if config.db_path.exists():
|
|
2111
|
-
try:
|
|
2112
|
-
from superlocalmemory.core.engine import MemoryEngine
|
|
2113
|
-
from superlocalmemory.core.engine_capabilities import Capabilities
|
|
2114
|
-
eng = MemoryEngine(config, capabilities=Capabilities.LIGHT)
|
|
2115
|
-
eng.initialize()
|
|
2116
|
-
pid = config.active_profile
|
|
2117
|
-
fact_count = eng._db.get_fact_count(pid)
|
|
2118
|
-
rows = eng._db.execute(
|
|
2119
|
-
"SELECT COUNT(*) AS c FROM canonical_entities WHERE profile_id = ?",
|
|
2120
|
-
(pid,),
|
|
2121
|
-
)
|
|
2122
|
-
entity_count = int(dict(rows[0])["c"]) if rows else 0
|
|
2123
|
-
rows2 = eng._db.execute(
|
|
2124
|
-
"SELECT COUNT(*) AS c FROM graph_edges WHERE profile_id = ?",
|
|
2125
|
-
(pid,),
|
|
2126
|
-
)
|
|
2127
|
-
edge_count = int(dict(rows2[0])["c"]) if rows2 else 0
|
|
2128
|
-
except Exception:
|
|
2129
|
-
logger.debug("cmd_status: engine count query failed; using 0s", exc_info=True)
|
|
2130
|
-
finally:
|
|
2131
|
-
if eng is not None:
|
|
2132
|
-
try:
|
|
2133
|
-
eng.close()
|
|
2134
|
-
except Exception:
|
|
2135
|
-
pass
|
|
2136
|
-
|
|
2137
|
-
data = {
|
|
2138
|
-
"mode": config.mode.value,
|
|
2139
|
-
"provider": config.llm.provider or "none",
|
|
2140
|
-
"profile": config.active_profile,
|
|
2141
|
-
"base_dir": str(config.base_dir),
|
|
2142
|
-
"db_path": str(config.db_path),
|
|
2143
|
-
"db_size_mb": db_size_mb,
|
|
2144
|
-
"fact_count": fact_count,
|
|
2145
|
-
"entity_count": entity_count,
|
|
2146
|
-
"edge_count": edge_count,
|
|
2147
|
-
"profile_generation": 0,
|
|
2148
|
-
"version": SLM_VERSION,
|
|
2149
|
-
"projection_queue_depth": _cli_projection_queue_depth(config.db_path),
|
|
2150
|
-
}
|
|
2151
|
-
json_print("status", data=data, next_actions=[
|
|
2152
|
-
{"command": "slm health --json", "description": "Check math layer health"},
|
|
2153
|
-
{"command": "slm list --json", "description": "List recent memories"},
|
|
2154
|
-
])
|
|
2155
|
-
return
|
|
2156
|
-
|
|
2157
|
-
print("SuperLocalMemory V4")
|
|
2158
|
-
print(f" Mode: {config.mode.value.upper()}")
|
|
2159
|
-
print(f" Provider: {config.llm.provider or 'none'}")
|
|
2160
|
-
print(
|
|
2161
|
-
f" Profile: "
|
|
2162
|
-
f"{daemon_status.get('profile') if daemon_status else config.active_profile}"
|
|
2163
|
-
)
|
|
2164
|
-
print(f" Base dir: {config.base_dir}")
|
|
2165
|
-
print(f" Database: {config.db_path}")
|
|
2166
|
-
if config.db_path.exists():
|
|
2167
|
-
size_mb = round(config.db_path.stat().st_size / 1024 / 1024, 2)
|
|
2168
|
-
print(f" DB size: {size_mb} MB")
|
|
2169
|
-
|
|
2170
|
-
# S9-UX-07 / S9-UX-13: --verbose surfaces the disabled marker,
|
|
2171
|
-
# last-version marker, and daemon port so users who are debugging
|
|
2172
|
-
# "slm seems broken" get the signal without opening three files.
|
|
2173
|
-
if getattr(args, "verbose", False):
|
|
2174
|
-
home = config.base_dir
|
|
2175
|
-
disabled_path = home / ".disabled"
|
|
2176
|
-
version_path = home / ".last_version"
|
|
2177
|
-
print()
|
|
2178
|
-
print(" --verbose --")
|
|
2179
|
-
print(
|
|
2180
|
-
f" Disabled marker: "
|
|
2181
|
-
f"{'YES (slm enable to reactivate)' if disabled_path.exists() else 'no'}",
|
|
2182
|
-
)
|
|
2183
|
-
if version_path.exists():
|
|
2184
|
-
try:
|
|
2185
|
-
last_ver = version_path.read_text(encoding="utf-8").strip()
|
|
2186
|
-
except OSError:
|
|
2187
|
-
last_ver = "(unreadable)"
|
|
2188
|
-
print(f" Last booted version: {last_ver}")
|
|
2189
|
-
else:
|
|
2190
|
-
print(" Last booted version: (never booted)")
|
|
2191
|
-
port = os.environ.get("SLM_DAEMON_PORT") or "8765"
|
|
2192
|
-
print(f" Daemon port: {port}")
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
def cmd_health(args: Namespace) -> None:
|
|
2196
|
-
"""Show math layer health status."""
|
|
2197
|
-
from superlocalmemory.core.config import SLMConfig
|
|
2198
|
-
|
|
2199
|
-
use_json = getattr(args, 'json', False)
|
|
2200
|
-
try:
|
|
2201
|
-
config = SLMConfig.load()
|
|
2202
|
-
from superlocalmemory.cli.daemon import is_daemon_running
|
|
2203
|
-
if is_daemon_running():
|
|
2204
|
-
# A running daemon owns the writable SQLite connections. Opening a
|
|
2205
|
-
# second MemoryEngine re-runs schema initialization and can lock
|
|
2206
|
-
# the user's database. Health only needs aggregate counts, so use
|
|
2207
|
-
# a read-only snapshot connection instead.
|
|
2208
|
-
from superlocalmemory.storage.memory_write import memory_read
|
|
2209
|
-
|
|
2210
|
-
db_path = config.db_path
|
|
2211
|
-
with memory_read(db_path) as conn:
|
|
2212
|
-
row = conn.execute(
|
|
2213
|
-
"SELECT COUNT(*), "
|
|
2214
|
-
"SUM(CASE WHEN fisher_mean IS NOT NULL THEN 1 ELSE 0 END), "
|
|
2215
|
-
"SUM(CASE WHEN langevin_position IS NOT NULL THEN 1 ELSE 0 END) "
|
|
2216
|
-
"FROM atomic_facts WHERE profile_id = ?",
|
|
2217
|
-
(config.active_profile,),
|
|
2218
|
-
).fetchone()
|
|
2219
|
-
total_facts, fisher_count, langevin_count = row or (0, 0, 0)
|
|
2220
|
-
facts = [None] * int(total_facts or 0)
|
|
2221
|
-
fisher_count = int(fisher_count or 0)
|
|
2222
|
-
langevin_count = int(langevin_count or 0)
|
|
2223
|
-
else:
|
|
2224
|
-
from superlocalmemory.core.engine import MemoryEngine
|
|
2225
|
-
engine = MemoryEngine(config)
|
|
2226
|
-
engine.initialize()
|
|
2227
|
-
facts = engine._db.get_all_facts(engine.profile_id)
|
|
2228
|
-
fisher_count = sum(1 for f in facts if f.fisher_mean is not None)
|
|
2229
|
-
langevin_count = sum(1 for f in facts if f.langevin_position is not None)
|
|
2230
|
-
except Exception as exc:
|
|
2231
|
-
if use_json:
|
|
2232
|
-
from superlocalmemory.cli.json_output import json_print
|
|
2233
|
-
json_print("health", error={"code": "ENGINE_ERROR", "message": str(exc)})
|
|
2234
|
-
sys.exit(1)
|
|
2235
|
-
raise
|
|
2236
|
-
|
|
2237
|
-
if use_json:
|
|
2238
|
-
from superlocalmemory.cli.json_output import json_print
|
|
2239
|
-
json_print("health", data={
|
|
2240
|
-
"total_facts": len(facts),
|
|
2241
|
-
"similarity_indexed": fisher_count,
|
|
2242
|
-
"lifecycle_positioned": langevin_count,
|
|
2243
|
-
"mode": config.mode.value,
|
|
2244
|
-
}, next_actions=[
|
|
2245
|
-
{"command": "slm status --json", "description": "Check system status"},
|
|
2246
|
-
{"command": "slm recall '<query>' --json", "description": "Test retrieval"},
|
|
2247
|
-
])
|
|
2248
|
-
return
|
|
2249
|
-
|
|
2250
|
-
print("Math Layer Health:")
|
|
2251
|
-
print(f" Total facts: {len(facts)}")
|
|
2252
|
-
print(f" Math layer indexed: {fisher_count}/{len(facts)}")
|
|
2253
|
-
print(f" Lifecycle positioned: {langevin_count}/{len(facts)}")
|
|
2254
|
-
print(f" Mode: {config.mode.value.upper()}")
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
def _gather_optimize_surface_b() -> dict:
|
|
2258
|
-
"""Gather Surface-B health data for slm doctor.
|
|
2259
|
-
|
|
2260
|
-
Pure data-gather — never raises, never prints, never starts the
|
|
2261
|
-
ConfigStore watchdog thread. Reads daemon-persisted metrics only
|
|
2262
|
-
(CacheDB.metrics_load), never the in-process KV counters.
|
|
2263
|
-
|
|
2264
|
-
Returns a dict with keys:
|
|
2265
|
-
enabled, cache_enabled, compress_enabled, proxy_enabled,
|
|
2266
|
-
compress_runs, tokens_saved, cache_hits, cache_misses,
|
|
2267
|
-
db_present, error
|
|
2268
|
-
"""
|
|
2269
|
-
from superlocalmemory.infra.data_root import state_path
|
|
2270
|
-
from superlocalmemory.optimize.storage.db import CacheDB
|
|
2271
|
-
|
|
2272
|
-
result: dict = {
|
|
2273
|
-
"enabled": False,
|
|
2274
|
-
"cache_enabled": False,
|
|
2275
|
-
"compress_enabled": False,
|
|
2276
|
-
"proxy_enabled": False,
|
|
2277
|
-
"compress_runs": 0,
|
|
2278
|
-
"tokens_saved": 0,
|
|
2279
|
-
"cache_hits": 0,
|
|
2280
|
-
"cache_misses": 0,
|
|
2281
|
-
"db_present": False,
|
|
2282
|
-
"error": "",
|
|
2283
|
-
}
|
|
2284
|
-
|
|
2285
|
-
# Step 1: read optimize config — NO watchdog start.
|
|
2286
|
-
try:
|
|
2287
|
-
from superlocalmemory.optimize.config.store import ConfigStore
|
|
2288
|
-
cfg = ConfigStore().get()
|
|
2289
|
-
result["enabled"] = cfg.enabled
|
|
2290
|
-
result["cache_enabled"] = cfg.cache_enabled
|
|
2291
|
-
result["compress_enabled"] = cfg.compress_enabled
|
|
2292
|
-
result["proxy_enabled"] = cfg.proxy_enabled
|
|
2293
|
-
except Exception as exc: # noqa: BLE001
|
|
2294
|
-
result["error"] = str(exc)
|
|
2295
|
-
# enabled stays False — safe default
|
|
2296
|
-
|
|
2297
|
-
# Step 2: read persisted metrics from llmcache.db (daemon-flushed, ≤60s stale).
|
|
2298
|
-
try:
|
|
2299
|
-
db_path = state_path("llmcache.db")
|
|
2300
|
-
result["db_present"] = db_path.exists()
|
|
2301
|
-
if result["db_present"]:
|
|
2302
|
-
snap = CacheDB.get_default().metrics_load()
|
|
2303
|
-
result["compress_runs"] = snap.compress_runs
|
|
2304
|
-
result["tokens_saved"] = snap.tokens_saved_compress
|
|
2305
|
-
result["cache_hits"] = snap.hits
|
|
2306
|
-
result["cache_misses"] = snap.misses
|
|
2307
|
-
except Exception as exc: # noqa: BLE001
|
|
2308
|
-
prior = result["error"]
|
|
2309
|
-
result["error"] = (prior + "; " if prior else "") + "metrics read failed"
|
|
2310
|
-
|
|
2311
|
-
return result
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
def _readline_with_timeout(
|
|
2315
|
-
stream, timeout_sec: float,
|
|
2316
|
-
) -> tuple[str | None, Exception | None]:
|
|
2317
|
-
"""Read one line from a pipe-like stream with a deadline.
|
|
2318
|
-
|
|
2319
|
-
Prefer a selector poll of the stream's file descriptor on POSIX so a
|
|
2320
|
-
timeout never abandons a thread blocked in readline() (thread + FD leak).
|
|
2321
|
-
Windows select() only accepts sockets, not subprocess pipes — fall back
|
|
2322
|
-
to a bounded helper thread there and for streams without a usable fileno.
|
|
2323
|
-
"""
|
|
2324
|
-
import selectors
|
|
2325
|
-
import threading
|
|
2326
|
-
|
|
2327
|
-
timeout_sec = max(0.0, float(timeout_sec))
|
|
2328
|
-
fd: int | None
|
|
2329
|
-
try:
|
|
2330
|
-
raw_fd = stream.fileno()
|
|
2331
|
-
fd = raw_fd if isinstance(raw_fd, int) else None
|
|
2332
|
-
except (AttributeError, OSError, ValueError, TypeError):
|
|
2333
|
-
fd = None
|
|
2334
|
-
|
|
2335
|
-
if fd is not None and sys.platform != "win32":
|
|
2336
|
-
try:
|
|
2337
|
-
with selectors.DefaultSelector() as sel:
|
|
2338
|
-
sel.register(fd, selectors.EVENT_READ)
|
|
2339
|
-
events = sel.select(timeout=timeout_sec)
|
|
2340
|
-
if not events:
|
|
2341
|
-
return None, None
|
|
2342
|
-
line = stream.readline()
|
|
2343
|
-
return (line if isinstance(line, str) else None), None
|
|
2344
|
-
except (OSError, ValueError) as exc:
|
|
2345
|
-
return None, exc
|
|
2346
|
-
|
|
2347
|
-
result: dict[str, object] = {}
|
|
2348
|
-
|
|
2349
|
-
def _read() -> None:
|
|
2350
|
-
try:
|
|
2351
|
-
result["line"] = stream.readline()
|
|
2352
|
-
except Exception as exc: # noqa: BLE001 - returned as probe failure
|
|
2353
|
-
result["exc"] = exc
|
|
2354
|
-
|
|
2355
|
-
reader = threading.Thread(target=_read, daemon=True)
|
|
2356
|
-
reader.start()
|
|
2357
|
-
reader.join(timeout_sec)
|
|
2358
|
-
if reader.is_alive():
|
|
2359
|
-
return None, None
|
|
2360
|
-
line = result.get("line")
|
|
2361
|
-
exc = result.get("exc")
|
|
2362
|
-
return (
|
|
2363
|
-
line if isinstance(line, str) else None,
|
|
2364
|
-
exc if isinstance(exc, Exception) else None,
|
|
2365
|
-
)
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
# ---------------------------------------------------------------------------
|
|
2369
|
-
# Super-help (v3.8.2) — one place a non-technical user sees EVERYTHING.
|
|
2370
|
-
# Grouped so `slm help` is scannable; a drift test asserts every top-level
|
|
2371
|
-
# command in the parser appears here (see test_super_help).
|
|
2372
|
-
# ---------------------------------------------------------------------------
|
|
2373
|
-
|
|
2374
|
-
_COMMAND_GROUPS: list[tuple[str, list[tuple[str, str]]]] = [
|
|
2375
|
-
("Getting started", [
|
|
2376
|
-
("setup", "Guided first-time setup (models, mode, IDEs) — start here"),
|
|
2377
|
-
("init", "Set up + wire Claude Code hooks + connect IDEs"),
|
|
2378
|
-
("reconfigure", "Re-run setup / pick a performance profile"),
|
|
2379
|
-
("mode", "Switch memory mode: a (local) / b (Ollama) / c (cloud)"),
|
|
2380
|
-
("provider", "Configure the cloud LLM provider + API key (Mode C)"),
|
|
2381
|
-
("connect", "Auto-configure detected IDEs (Cursor, VS Code, …)"),
|
|
2382
|
-
("upgrade-hosts", "Preview or explicitly refresh existing SLM host integrations"),
|
|
2383
|
-
("hooks", "Install/inspect Claude Code hooks"),
|
|
2384
|
-
("codex", "Configure the Codex / OpenAI integration"),
|
|
2385
|
-
]),
|
|
2386
|
-
("Store & recall memories", [
|
|
2387
|
-
("remember", "Store a memory (--tags to label)"),
|
|
2388
|
-
("recall", "Semantic + keyword search across memories"),
|
|
2389
|
-
("search", "Exact keyword / full-text search"),
|
|
2390
|
-
("list", "Show recent memories (-n N)"),
|
|
2391
|
-
("update", "Propose an immutable correction by id"),
|
|
2392
|
-
("review-correction", "Apply, reject, or roll back a correction case"),
|
|
2393
|
-
("delete", "Delete a memory by id"),
|
|
2394
|
-
("forget", "Run the decay cycle (preview first)"),
|
|
2395
|
-
("trace", "Recall with a per-channel score breakdown"),
|
|
2396
|
-
("ingest", "Ingest external observations / documents"),
|
|
2397
|
-
("summary", "Readable summaries: session, day, or project"),
|
|
2398
|
-
]),
|
|
2399
|
-
("Privacy & compliance", [
|
|
2400
|
-
# gdpr shipped in 4.0.6 but was never listed here, so `slm help` did not
|
|
2401
|
-
# mention it at all. The drift test caught it; the omission is the thing
|
|
2402
|
-
# that test exists to prevent.
|
|
2403
|
-
("gdpr", "Subject rights: status, export, erase, verify"),
|
|
2404
|
-
]),
|
|
2405
|
-
("Run SLM (daemon & dashboard)", [
|
|
2406
|
-
("serve", "Start/stop the background daemon"),
|
|
2407
|
-
("restart", "Restart the daemon (applies restart-only settings)"),
|
|
2408
|
-
("dashboard", "Open the web dashboard (localhost:8765)"),
|
|
2409
|
-
("mcp", "Start the MCP server (used by IDEs)"),
|
|
2410
|
-
("warmup", "Pre-load models so the first recall is fast"),
|
|
2411
|
-
("status", "Show daemon status + memory counts"),
|
|
2412
|
-
]),
|
|
2413
|
-
("Health & self-healing", [
|
|
2414
|
-
("doctor", "Full pre-flight check (add --fix to auto-repair)"),
|
|
2415
|
-
("health", "Quick math/retrieval layer status"),
|
|
2416
|
-
("ops", "List/resolve stuck, dead-lettered, or degraded operations (admin)"),
|
|
2417
|
-
("diagnostics", "Export a local diagnostics bundle"),
|
|
2418
|
-
("evidence", "Build/inspect evidence bundles"),
|
|
2419
|
-
("rotate-token", "Rotate the local dashboard install token"),
|
|
2420
|
-
]),
|
|
2421
|
-
("Configuration", [
|
|
2422
|
-
("config", "View/set configuration (see: slm help config)"),
|
|
2423
|
-
("enable", "Enable a feature/escape hatch"),
|
|
2424
|
-
("disable", "Disable a feature/escape hatch"),
|
|
2425
|
-
("clear-cache", "Clear caches"),
|
|
2426
|
-
]),
|
|
2427
|
-
("Profiles & sessions", [
|
|
2428
|
-
("profile", "Manage workspaces/profiles (add, switch, list)"),
|
|
2429
|
-
("session", "Open/close a work session"),
|
|
2430
|
-
("session-context", "Pre-stage session context for hooks"),
|
|
2431
|
-
]),
|
|
2432
|
-
("Multi-device mesh", [
|
|
2433
|
-
("mesh", "Inspect/manage the cross-device memory mesh"),
|
|
2434
|
-
]),
|
|
2435
|
-
("Token optimization", [
|
|
2436
|
-
("optimize", "Configure the Optimize surface (cache/compress/proxy)"),
|
|
2437
|
-
("cache", "Manage the LLM response cache"),
|
|
2438
|
-
("compress", "Manage prose compression"),
|
|
2439
|
-
("proxy", "Manage the optimizing LLM proxy"),
|
|
2440
|
-
("wrap", "Run an agent (claude, codex, …) through the proxy"),
|
|
2441
|
-
("help-optimize", "Detailed help for the Optimize surface"),
|
|
2442
|
-
]),
|
|
2443
|
-
("Learning & maintenance", [
|
|
2444
|
-
("evolve", "Skill-evolution controls"),
|
|
2445
|
-
("observe", "External observation / telemetry ingestion"),
|
|
2446
|
-
("brain", "Show profile-scoped Living Brain evidence"),
|
|
2447
|
-
("decay", "Run a forgetting/decay pass"),
|
|
2448
|
-
("consolidate", "Merge/consolidate related memories"),
|
|
2449
|
-
("quantize", "Quantize embeddings to save space"),
|
|
2450
|
-
("soft-prompts", "Manage learned soft prompts"),
|
|
2451
|
-
("reap", "Clean up stale/dead records"),
|
|
2452
|
-
("adapters", "Manage ingestion adapters"),
|
|
2453
|
-
("benchmark", "Run local benchmarks"),
|
|
2454
|
-
]),
|
|
2455
|
-
("Data & migration", [
|
|
2456
|
-
("db", "Low-level database operations"),
|
|
2457
|
-
("migrate", "Migrate the data store to the current version"),
|
|
2458
|
-
]),
|
|
2459
|
-
("Automation", [
|
|
2460
|
-
("loop", "Run gate-verified bounded agent loops"),
|
|
2461
|
-
]),
|
|
2462
|
-
("Help", [
|
|
2463
|
-
("help", "This overview. Try: slm help config | modes | self-heal"),
|
|
2464
|
-
]),
|
|
2465
|
-
]
|
|
2466
|
-
|
|
2467
|
-
_HELP_TOPICS: dict[str, str] = {
|
|
2468
|
-
"modes": """\
|
|
2469
|
-
Operating modes
|
|
2470
|
-
a On-device only — no AI language model runs; all data stays on this
|
|
2471
|
-
device. Fastest and most private. (EU AI Act: full)
|
|
2472
|
-
b On-device + AI — uses a local Ollama model to improve recall quality;
|
|
2473
|
-
all data stays on this device. Requires Ollama running.
|
|
2474
|
-
c Cloud AI — uses a cloud provider (OpenAI, Anthropic, …) for best
|
|
2475
|
-
recall quality; queries leave this device. Needs a key.
|
|
2476
|
-
|
|
2477
|
-
Switch any time: slm mode a (or b / c)
|
|
2478
|
-
""",
|
|
2479
|
-
"config": """\
|
|
2480
|
-
Configuration
|
|
2481
|
-
slm config Show current configuration
|
|
2482
|
-
slm config get <key> Read one setting
|
|
2483
|
-
slm config set <key> <val> Change a setting
|
|
2484
|
-
|
|
2485
|
-
Most day-to-day settings are editable live in the dashboard
|
|
2486
|
-
(Settings tab) and apply without a restart. Settings that need a
|
|
2487
|
-
restart show a one-click "apply & restart" banner.
|
|
2488
|
-
|
|
2489
|
-
Common runtime settings: recall depth (top_k), memory injection on/off,
|
|
2490
|
-
stale-memory suppression, idle consolidation, PII redaction, reranker.
|
|
2491
|
-
""",
|
|
2492
|
-
"self-heal": """\
|
|
2493
|
-
Self-healing (v3.8.2)
|
|
2494
|
-
SLM repairs itself. On every start the daemon checks that all models
|
|
2495
|
-
and dependencies are present and re-downloads/installs the fixable ones
|
|
2496
|
-
in the background — you don't run anything.
|
|
2497
|
-
|
|
2498
|
-
See what's present/missing:
|
|
2499
|
-
slm doctor Full report with fix commands
|
|
2500
|
-
slm doctor --fix Auto-repair everything fixable, then report
|
|
2501
|
-
Dashboard → Help → System Health Live "what's missing" report
|
|
2502
|
-
|
|
2503
|
-
Big or opt-in pieces (the local LLM 'Ollama', the large compression
|
|
2504
|
-
model, PyTorch) are never installed without you — SLM shows the exact
|
|
2505
|
-
command instead.
|
|
2506
|
-
""",
|
|
2507
|
-
}
|
|
2508
|
-
# Aliases so common phrasings resolve to a topic.
|
|
2509
|
-
_HELP_TOPICS["health"] = _HELP_TOPICS["self-heal"]
|
|
2510
|
-
_HELP_TOPICS["selfheal"] = _HELP_TOPICS["self-heal"]
|
|
2511
|
-
_HELP_TOPICS["mode"] = _HELP_TOPICS["modes"]
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
def all_help_commands() -> set[str]:
|
|
2515
|
-
"""Every command name listed in the super-help (drift-test hook)."""
|
|
2516
|
-
return {cmd for _title, rows in _COMMAND_GROUPS for cmd, _desc in rows}
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
def cmd_help(args: Namespace) -> None:
|
|
2520
|
-
"""Super-help: a grouped overview of every command, plus focused topics.
|
|
2521
|
-
|
|
2522
|
-
`slm help` → grouped command overview
|
|
2523
|
-
`slm help <topic>` → modes | config | self-heal
|
|
2524
|
-
"""
|
|
2525
|
-
topic = getattr(args, "topic", None)
|
|
2526
|
-
if topic:
|
|
2527
|
-
key = topic.strip().lower()
|
|
2528
|
-
text = _HELP_TOPICS.get(key)
|
|
2529
|
-
if text:
|
|
2530
|
-
print(text)
|
|
2531
|
-
else:
|
|
2532
|
-
print(f"No help topic '{topic}'. Available topics: "
|
|
2533
|
-
+ ", ".join(sorted(set(_HELP_TOPICS))))
|
|
2534
|
-
return
|
|
2535
|
-
|
|
2536
|
-
from superlocalmemory.cli.json_output import _get_version
|
|
2537
|
-
print(f"SuperLocalMemory V4 ({_get_version()}) — command overview")
|
|
2538
|
-
print("=" * 58)
|
|
2539
|
-
print("Run any command with -h for its options, e.g. slm recall -h\n")
|
|
2540
|
-
for title, rows in _COMMAND_GROUPS:
|
|
2541
|
-
print(f"{title}:")
|
|
2542
|
-
for cmd, desc in rows:
|
|
2543
|
-
print(f" {cmd:<16} {desc}")
|
|
2544
|
-
print()
|
|
2545
|
-
print("Health at a glance: slm doctor (auto-repair: slm doctor --fix)")
|
|
2546
|
-
print("Topics: slm help modes | slm help config | "
|
|
2547
|
-
"slm help self-heal")
|
|
2548
|
-
print("Docs: https://superlocalmemory.com")
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
def _migration_error_logs() -> list:
|
|
2552
|
-
"""Return a sorted list of migration-error log files in the SLM home dir.
|
|
2553
|
-
|
|
2554
|
-
Returns an empty list when none exist or when the directory is not readable.
|
|
2555
|
-
"""
|
|
2556
|
-
try:
|
|
2557
|
-
from superlocalmemory.core.config import SLMConfig
|
|
2558
|
-
from superlocalmemory.infra.data_root import canonical_data_root
|
|
2559
|
-
from superlocalmemory.storage.models import Mode
|
|
2560
|
-
|
|
2561
|
-
# The daemon writes this log beside the memory database it was actually
|
|
2562
|
-
# configured with, which is not necessarily the canonical data root —
|
|
2563
|
-
# any of SLM_DATA_DIR / SL_MEMORY_PATH / SLM_HOME can move it. Globbing
|
|
2564
|
-
# only the canonical root reported "clean" while the log sat unread in
|
|
2565
|
-
# the real directory. Search both, de-duplicated.
|
|
2566
|
-
roots = {canonical_data_root()}
|
|
2567
|
-
try:
|
|
2568
|
-
cfg = SLMConfig.for_mode(Mode.A)
|
|
2569
|
-
db = getattr(cfg, "memory_db_path", None) or getattr(cfg, "db_path", None)
|
|
2570
|
-
if db:
|
|
2571
|
-
roots.add(pathlib.Path(db).parent)
|
|
2572
|
-
except Exception:
|
|
2573
|
-
pass
|
|
2574
|
-
|
|
2575
|
-
found: list = []
|
|
2576
|
-
for root in roots:
|
|
2577
|
-
try:
|
|
2578
|
-
found.extend(root.glob("migration-error-*.log"))
|
|
2579
|
-
except OSError:
|
|
2580
|
-
continue
|
|
2581
|
-
return sorted(set(found))
|
|
2582
|
-
except Exception:
|
|
2583
|
-
return []
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
def _slm_version() -> str:
|
|
2587
|
-
"""The installed package version, or "unknown"."""
|
|
2588
|
-
try:
|
|
2589
|
-
from importlib.metadata import version
|
|
2590
|
-
return version("superlocalmemory")
|
|
2591
|
-
except Exception: # noqa: BLE001
|
|
2592
|
-
return "unknown"
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
def _installed_plugin_versions() -> dict:
|
|
2596
|
-
"""Version of each editor plugin found on this machine, by install name.
|
|
2597
|
-
|
|
2598
|
-
The skills, agents and commands live in the editor's plugin channel rather
|
|
2599
|
-
than in the Python package, so upgrading with pip leaves them exactly where
|
|
2600
|
-
they were. This looks for them where each editor puts them, and returns an
|
|
2601
|
-
empty mapping when none is installed -- which is itself the answer worth
|
|
2602
|
-
reporting, because it means pip is the only thing being upgraded.
|
|
2603
|
-
|
|
2604
|
-
Best effort by design: an editor this does not know about should produce
|
|
2605
|
-
"not detected", never an error.
|
|
2606
|
-
"""
|
|
2607
|
-
import json
|
|
2608
|
-
from pathlib import Path
|
|
2609
|
-
|
|
2610
|
-
found: dict[str, str] = {}
|
|
2611
|
-
roots = (
|
|
2612
|
-
# Claude Code: marketplace installs and directly-added plugins.
|
|
2613
|
-
Path.home() / ".claude" / "plugins",
|
|
2614
|
-
# Codex and VS Code copies, when placed by hand.
|
|
2615
|
-
Path.home() / ".codex" / "plugins",
|
|
2616
|
-
Path.home() / ".vscode" / "extensions",
|
|
2617
|
-
)
|
|
2618
|
-
for root in roots:
|
|
2619
|
-
if not root.is_dir():
|
|
2620
|
-
continue
|
|
2621
|
-
for manifest in list(root.glob("*/.claude-plugin/plugin.json")) + \
|
|
2622
|
-
list(root.glob("*/plugin.json")) + \
|
|
2623
|
-
list(root.glob("*/*/.claude-plugin/plugin.json")):
|
|
2624
|
-
try:
|
|
2625
|
-
data = json.loads(manifest.read_text(encoding="utf-8"))
|
|
2626
|
-
except Exception: # noqa: BLE001 — a sibling plugin's bad json is not ours
|
|
2627
|
-
continue
|
|
2628
|
-
if str(data.get("name", "")) != "superlocalmemory":
|
|
2629
|
-
continue
|
|
2630
|
-
found[str(manifest.parent.parent.name)] = str(
|
|
2631
|
-
data.get("version", "unknown")
|
|
2632
|
-
)
|
|
2633
|
-
return found
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
def _detect_all_installs() -> list:
|
|
2637
|
-
"""Thin shim so cmd_doctor can be tested without importing install_detector."""
|
|
2638
|
-
try:
|
|
2639
|
-
from superlocalmemory.core.install_detector import (
|
|
2640
|
-
_detect_all_installs as _real,
|
|
2641
|
-
)
|
|
2642
|
-
return _real()
|
|
2643
|
-
except Exception:
|
|
2644
|
-
return []
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
def cmd_doctor(args: Namespace) -> None:
|
|
2648
|
-
"""Comprehensive pre-flight check — verify everything works.
|
|
2649
|
-
|
|
2650
|
-
S9-UX-10: ``--quick`` skips the slow probes (embedding worker
|
|
2651
|
-
subprocess, Ollama roundtrip) so first-run installer hooks can
|
|
2652
|
-
surface a sub-second PASS/FAIL line before letting the user go.
|
|
2653
|
-
Full doctor remains the default for ``slm doctor`` with no flag.
|
|
2654
|
-
"""
|
|
2655
|
-
import shutil
|
|
2656
|
-
from pathlib import Path
|
|
2657
|
-
|
|
2658
|
-
use_json = getattr(args, "json", False)
|
|
2659
|
-
quick = getattr(args, "quick", False)
|
|
2660
|
-
checks: list[dict] = []
|
|
2661
|
-
passed = warned = failed = 0
|
|
2662
|
-
|
|
2663
|
-
def _check(name: str, status: str, detail: str, fix: str = ""):
|
|
2664
|
-
nonlocal passed, warned, failed
|
|
2665
|
-
checks.append({"name": name, "status": status, "detail": detail, "fix": fix})
|
|
2666
|
-
if status == "PASS":
|
|
2667
|
-
passed += 1
|
|
2668
|
-
elif status == "WARN":
|
|
2669
|
-
warned += 1
|
|
2670
|
-
else:
|
|
2671
|
-
failed += 1
|
|
2672
|
-
if not use_json:
|
|
2673
|
-
tag = {"PASS": "[PASS]", "WARN": "[WARN]", "FAIL": "[FAIL]"}[status]
|
|
2674
|
-
line = f" {tag} {name}: {detail}"
|
|
2675
|
-
if fix:
|
|
2676
|
-
line += f"\n Fix: {fix}"
|
|
2677
|
-
print(line)
|
|
2678
|
-
|
|
2679
|
-
if not use_json:
|
|
2680
|
-
print("SuperLocalMemory V4 — Doctor (Pre-flight Check)")
|
|
2681
|
-
print("=" * 50)
|
|
2682
|
-
print()
|
|
2683
|
-
|
|
2684
|
-
# v3.8.2 `--fix`: run the component healer (the SAME repair the daemon
|
|
2685
|
-
# self-heal performs) BEFORE the checks, so the report below reflects the
|
|
2686
|
-
# healed state. Re-downloads missing models / installs sqlite-vec; big or
|
|
2687
|
-
# opt-in items are left as manual fixes. Fail-open — never blocks doctor.
|
|
2688
|
-
if getattr(args, "fix", False):
|
|
2689
|
-
try:
|
|
2690
|
-
from superlocalmemory.core import component_healer
|
|
2691
|
-
from superlocalmemory.core.config import SLMConfig as _FixCfg
|
|
2692
|
-
try:
|
|
2693
|
-
_fcfg = _FixCfg.load()
|
|
2694
|
-
except Exception:
|
|
2695
|
-
_fcfg = None
|
|
2696
|
-
if not use_json:
|
|
2697
|
-
print("Auto-repair (--fix): repairing fixable components…")
|
|
2698
|
-
_fix_res = component_healer.heal_missing(
|
|
2699
|
-
_fcfg,
|
|
2700
|
-
on_progress=(None if use_json
|
|
2701
|
-
else (lambda k, m: print(f" [{k}] {m}"))),
|
|
2702
|
-
)
|
|
2703
|
-
if not use_json:
|
|
2704
|
-
print(
|
|
2705
|
-
f" repaired {_fix_res['healed']}/{_fix_res['attempted']} "
|
|
2706
|
-
f"(failed {_fix_res['failed']})\n"
|
|
2707
|
-
)
|
|
2708
|
-
except Exception as _fix_exc:
|
|
2709
|
-
if not use_json:
|
|
2710
|
-
print(f" auto-repair error: {_fix_exc}\n")
|
|
2711
|
-
|
|
2712
|
-
# H5 — stale-artifact reap (additive; runs after component healer).
|
|
2713
|
-
# Removes provably-dead lock/PID files so the doctor report below
|
|
2714
|
-
# reflects the healed state. Fail-open — never blocks doctor.
|
|
2715
|
-
try:
|
|
2716
|
-
from superlocalmemory.infra.self_heal import reap_stale_artifacts
|
|
2717
|
-
from superlocalmemory.infra.data_root import canonical_data_root
|
|
2718
|
-
|
|
2719
|
-
_sh_report = reap_stale_artifacts(canonical_data_root())
|
|
2720
|
-
if not use_json:
|
|
2721
|
-
if _sh_report["removed"]:
|
|
2722
|
-
print(
|
|
2723
|
-
f" Cleaned {len(_sh_report['removed'])} stale"
|
|
2724
|
-
" lock/pid file(s):"
|
|
2725
|
-
)
|
|
2726
|
-
for _item in _sh_report["removed"]:
|
|
2727
|
-
print(
|
|
2728
|
-
f" - {Path(_item['path']).name}"
|
|
2729
|
-
f" ({_item['reason']})"
|
|
2730
|
-
)
|
|
2731
|
-
else:
|
|
2732
|
-
print(" No stale lock/pid files found.")
|
|
2733
|
-
print()
|
|
2734
|
-
except Exception as _sh_exc:
|
|
2735
|
-
if not use_json:
|
|
2736
|
-
print(f" stale-artifact cleanup skipped: {_sh_exc}\n")
|
|
2737
|
-
|
|
2738
|
-
# 1. Python version
|
|
2739
|
-
v = sys.version_info
|
|
2740
|
-
if v >= (3, 11):
|
|
2741
|
-
_check("Python", "PASS", f"{v.major}.{v.minor}.{v.micro} (>= 3.11)")
|
|
2742
|
-
else:
|
|
2743
|
-
_check("Python", "FAIL", f"{v.major}.{v.minor}.{v.micro} (need >= 3.11)",
|
|
2744
|
-
"Install Python 3.11+ from https://python.org/downloads/")
|
|
2745
|
-
|
|
2746
|
-
# 1b. Version integrity (issue #107). Doctor is what a user runs when
|
|
2747
|
-
# something seems wrong, so it is exactly where "the code you are running
|
|
2748
|
-
# is not the code you installed" has to appear. Reported as WARN rather
|
|
2749
|
-
# than FAIL: the installation is sound, it is this *process* that is
|
|
2750
|
-
# behind, and `slm doctor` itself is short-lived so it is rarely the
|
|
2751
|
-
# stale one -- it is reporting on behalf of the long-lived servers.
|
|
2752
|
-
try:
|
|
2753
|
-
from superlocalmemory.infra.version_integrity import check_version_integrity
|
|
2754
|
-
|
|
2755
|
-
_vi = check_version_integrity()
|
|
2756
|
-
if _vi.is_stale:
|
|
2757
|
-
_check("Version integrity", "WARN", _vi.detail, _vi.hint)
|
|
2758
|
-
elif _vi.state == "mismatch":
|
|
2759
|
-
_check("Version integrity", "WARN", _vi.detail, _vi.hint)
|
|
2760
|
-
elif _vi.state == "unknown":
|
|
2761
|
-
_check("Version integrity", "WARN", _vi.detail,
|
|
2762
|
-
"Reinstall so distribution metadata is readable.")
|
|
2763
|
-
else:
|
|
2764
|
-
_check("Version integrity", "PASS", _vi.detail)
|
|
2765
|
-
except Exception as _vi_exc: # noqa: BLE001 - never break doctor
|
|
2766
|
-
_check("Version integrity", "WARN", f"could not verify: {_vi_exc}")
|
|
2767
|
-
|
|
2768
|
-
# 2. Core deps
|
|
2769
|
-
core_modules = {
|
|
2770
|
-
"numpy": "numpy", "scipy": "scipy", "networkx": "networkx",
|
|
2771
|
-
"httpx": "httpx", "dateutil": "python-dateutil",
|
|
2772
|
-
"rank_bm25": "rank-bm25", "vaderSentiment": "vadersentiment",
|
|
2773
|
-
"einops": "einops",
|
|
2774
|
-
}
|
|
2775
|
-
core_ok, core_versions = [], []
|
|
2776
|
-
for mod, pkg in core_modules.items():
|
|
2777
|
-
try:
|
|
2778
|
-
m = __import__(mod)
|
|
2779
|
-
ver = getattr(m, "__version__", "?")
|
|
2780
|
-
core_ok.append(mod)
|
|
2781
|
-
core_versions.append(f"{mod} {ver}")
|
|
2782
|
-
except Exception: # dependency import may fail after module discovery
|
|
2783
|
-
pass
|
|
2784
|
-
if len(core_ok) == len(core_modules):
|
|
2785
|
-
_check("Core deps", "PASS", ", ".join(core_versions[:4]) + "...")
|
|
2786
|
-
else:
|
|
2787
|
-
missing = set(core_modules) - set(core_ok)
|
|
2788
|
-
_check("Core deps", "FAIL", f"Missing: {', '.join(missing)}",
|
|
2789
|
-
"pip install " + " ".join(core_modules[m] for m in missing))
|
|
2790
|
-
|
|
2791
|
-
# 3. Search deps
|
|
2792
|
-
#
|
|
2793
|
-
# Presence only. Importing these executes torch, which cost ~4 s of every
|
|
2794
|
-
# doctor run — a quarter of the whole command — to learn something the
|
|
2795
|
-
# embedding-worker check below already proves by actually running them.
|
|
2796
|
-
# find_spec answers "is it installed" without executing a line of it.
|
|
2797
|
-
# A package that is present but broken on import is caught by check 7,
|
|
2798
|
-
# which spawns the worker and waits for it to answer.
|
|
2799
|
-
search_mods = {"sentence_transformers": "sentence-transformers", "torch": "torch",
|
|
2800
|
-
"sklearn": "scikit-learn"}
|
|
2801
|
-
search_ok = []
|
|
2802
|
-
for mod, pkg in search_mods.items():
|
|
2803
|
-
try:
|
|
2804
|
-
import importlib.util as _ilu
|
|
2805
|
-
if _ilu.find_spec(mod) is not None:
|
|
2806
|
-
search_ok.append(mod)
|
|
2807
|
-
except Exception: # a broken meta-path finder must not fail the check
|
|
2808
|
-
pass
|
|
2809
|
-
if len(search_ok) == len(search_mods):
|
|
2810
|
-
_check("Search deps", "PASS", "sentence-transformers, torch, sklearn")
|
|
2811
|
-
else:
|
|
2812
|
-
missing = set(search_mods) - set(search_ok)
|
|
2813
|
-
_check("Search deps", "WARN", f"Missing: {', '.join(missing)}",
|
|
2814
|
-
"pip install 'superlocalmemory[search]'")
|
|
2815
|
-
|
|
2816
|
-
# 4. Dashboard deps
|
|
2817
|
-
dash_ok = True
|
|
2818
|
-
for mod in ["fastapi", "uvicorn", "websockets"]:
|
|
2819
|
-
try:
|
|
2820
|
-
__import__(mod)
|
|
2821
|
-
except Exception: # dependency import may fail after module discovery
|
|
2822
|
-
dash_ok = False
|
|
2823
|
-
break
|
|
2824
|
-
if dash_ok:
|
|
2825
|
-
_check("Dashboard deps", "PASS", "fastapi, uvicorn, websockets")
|
|
2826
|
-
else:
|
|
2827
|
-
_check("Dashboard deps", "WARN", "Missing dashboard deps",
|
|
2828
|
-
"pip install 'fastapi[all]' uvicorn websockets")
|
|
2829
|
-
|
|
2830
|
-
# 5. Learning deps
|
|
2831
|
-
try:
|
|
2832
|
-
import lightgbm
|
|
2833
|
-
_check("Learning deps", "PASS", f"lightgbm {lightgbm.__version__}")
|
|
2834
|
-
except Exception: # dependency import may fail after module discovery
|
|
2835
|
-
_check("Learning deps", "WARN", "lightgbm not installed",
|
|
2836
|
-
"pip install lightgbm")
|
|
2837
|
-
except OSError as exc:
|
|
2838
|
-
_check("Learning deps", "WARN", f"lightgbm installed but broken: {exc}",
|
|
2839
|
-
"brew install libomp && pip install --force-reinstall lightgbm")
|
|
2840
|
-
|
|
2841
|
-
# 6. Performance deps
|
|
2842
|
-
# v3.4.43: diskcache removed from this check — it was a phantom dependency
|
|
2843
|
-
# (declared in pyproject.toml but never imported anywhere in src/ or tests/).
|
|
2844
|
-
# Dropping it closes CVE-2025-69872 (pickle deserialization RCE) without any
|
|
2845
|
-
# behavior change. orjson remains a real performance dep.
|
|
2846
|
-
perf_ok = []
|
|
2847
|
-
for mod in ["orjson"]:
|
|
2848
|
-
try:
|
|
2849
|
-
__import__(mod)
|
|
2850
|
-
perf_ok.append(mod)
|
|
2851
|
-
except Exception: # dependency import may fail after module discovery
|
|
2852
|
-
pass
|
|
2853
|
-
if perf_ok:
|
|
2854
|
-
_check("Performance deps", "PASS", "orjson")
|
|
2855
|
-
else:
|
|
2856
|
-
_check("Performance deps", "WARN", "Missing: orjson",
|
|
2857
|
-
"pip install orjson")
|
|
2858
|
-
|
|
2859
|
-
# 7. Embedding worker functional test — skipped under --quick.
|
|
2860
|
-
if quick:
|
|
2861
|
-
_check("Embedding worker", "PASS", "skipped (--quick)")
|
|
2862
|
-
else:
|
|
2863
|
-
try:
|
|
2864
|
-
import subprocess as _sp
|
|
2865
|
-
import json as _json
|
|
2866
|
-
|
|
2867
|
-
env = {
|
|
2868
|
-
**__import__("os").environ,
|
|
2869
|
-
"CUDA_VISIBLE_DEVICES": "",
|
|
2870
|
-
"PYTORCH_MPS_HIGH_WATERMARK_RATIO": "0.0",
|
|
2871
|
-
"TOKENIZERS_PARALLELISM": "false",
|
|
2872
|
-
"TORCH_DEVICE": "cpu",
|
|
2873
|
-
}
|
|
2874
|
-
proc = _sp.Popen(
|
|
2875
|
-
[sys.executable, "-m",
|
|
2876
|
-
"superlocalmemory.core.embedding_worker"],
|
|
2877
|
-
stdin=_sp.PIPE, stdout=_sp.PIPE, stderr=_sp.DEVNULL,
|
|
2878
|
-
text=True, bufsize=1, env=env,
|
|
2879
|
-
)
|
|
2880
|
-
proc.stdin.write(_json.dumps({"cmd": "ping"}) + "\n")
|
|
2881
|
-
proc.stdin.flush()
|
|
2882
|
-
|
|
2883
|
-
line, read_exc = _readline_with_timeout(proc.stdout, 30)
|
|
2884
|
-
if read_exc is not None:
|
|
2885
|
-
_check("Embedding worker", "FAIL", str(read_exc), "slm warmup")
|
|
2886
|
-
elif line is not None:
|
|
2887
|
-
resp = _json.loads(line or "{}")
|
|
2888
|
-
if resp.get("ok"):
|
|
2889
|
-
_check(
|
|
2890
|
-
"Embedding worker", "PASS",
|
|
2891
|
-
f"responsive (PID {proc.pid}, "
|
|
2892
|
-
f"Python {sys.executable})",
|
|
2893
|
-
)
|
|
2894
|
-
else:
|
|
2895
|
-
_check(
|
|
2896
|
-
"Embedding worker", "FAIL",
|
|
2897
|
-
f"error: {resp.get('error', 'unknown')}",
|
|
2898
|
-
"pip install sentence-transformers einops torch",
|
|
2899
|
-
)
|
|
2900
|
-
else:
|
|
2901
|
-
_check("Embedding worker", "FAIL", "timed out (30s)",
|
|
2902
|
-
"slm warmup")
|
|
2903
|
-
proc.stdin.write(_json.dumps({"cmd": "quit"}) + "\n")
|
|
2904
|
-
proc.stdin.flush()
|
|
2905
|
-
proc.wait(timeout=5)
|
|
2906
|
-
except FileNotFoundError:
|
|
2907
|
-
_check(
|
|
2908
|
-
"Embedding worker", "FAIL",
|
|
2909
|
-
"embedding_worker module not found",
|
|
2910
|
-
"Reinstall: npm install -g superlocalmemory",
|
|
2911
|
-
)
|
|
2912
|
-
except Exception as exc:
|
|
2913
|
-
_check("Embedding worker", "FAIL", str(exc), "slm warmup")
|
|
2914
|
-
|
|
2915
|
-
# 8. Ollama connectivity (Mode B only) — skipped under --quick.
|
|
2916
|
-
if quick:
|
|
2917
|
-
_check("Ollama / API key", "PASS", "skipped (--quick)")
|
|
2918
|
-
else:
|
|
2919
|
-
try:
|
|
2920
|
-
from superlocalmemory.core.config import SLMConfig
|
|
2921
|
-
config = SLMConfig.load()
|
|
2922
|
-
if config.mode.value == "b":
|
|
2923
|
-
import httpx
|
|
2924
|
-
try:
|
|
2925
|
-
resp = httpx.get(
|
|
2926
|
-
f"{config.llm.api_base}/api/tags", timeout=5.0,
|
|
2927
|
-
)
|
|
2928
|
-
if resp.status_code == 200:
|
|
2929
|
-
models = [
|
|
2930
|
-
m["name"].split(":")[0]
|
|
2931
|
-
for m in resp.json().get("models", [])
|
|
2932
|
-
]
|
|
2933
|
-
has_llm = config.llm.model.split(":")[0] in models
|
|
2934
|
-
if has_llm:
|
|
2935
|
-
_check(
|
|
2936
|
-
"Ollama", "PASS",
|
|
2937
|
-
f"running, {len(models)} models, "
|
|
2938
|
-
f"'{config.llm.model}' available",
|
|
2939
|
-
)
|
|
2940
|
-
else:
|
|
2941
|
-
_check(
|
|
2942
|
-
"Ollama", "WARN",
|
|
2943
|
-
f"running but '{config.llm.model}' "
|
|
2944
|
-
f"not pulled",
|
|
2945
|
-
f"ollama pull {config.llm.model}",
|
|
2946
|
-
)
|
|
2947
|
-
else:
|
|
2948
|
-
_check(
|
|
2949
|
-
"Ollama", "WARN",
|
|
2950
|
-
f"HTTP {resp.status_code}",
|
|
2951
|
-
"brew services start ollama",
|
|
2952
|
-
)
|
|
2953
|
-
except Exception:
|
|
2954
|
-
_check(
|
|
2955
|
-
"Ollama", "WARN",
|
|
2956
|
-
"not reachable at " + config.llm.api_base,
|
|
2957
|
-
"brew services start ollama",
|
|
2958
|
-
)
|
|
2959
|
-
elif config.mode.value == "c":
|
|
2960
|
-
if config.llm.api_key:
|
|
2961
|
-
_check(
|
|
2962
|
-
"API key", "PASS",
|
|
2963
|
-
f"provider={config.llm.provider}, "
|
|
2964
|
-
f"key=***{config.llm.api_key[-4:]}",
|
|
2965
|
-
)
|
|
2966
|
-
else:
|
|
2967
|
-
_check(
|
|
2968
|
-
"API key", "WARN", "no API key configured",
|
|
2969
|
-
"slm provider set",
|
|
2970
|
-
)
|
|
2971
|
-
except Exception:
|
|
2972
|
-
pass # Config load failed — already caught above
|
|
2973
|
-
|
|
2974
|
-
# 9. Disk space
|
|
2975
|
-
from superlocalmemory.infra.data_root import canonical_data_root
|
|
2976
|
-
slm_home = canonical_data_root()
|
|
2977
|
-
try:
|
|
2978
|
-
usage = shutil.disk_usage(slm_home if slm_home.exists() else Path.home())
|
|
2979
|
-
free_gb = usage.free / (1024 ** 3)
|
|
2980
|
-
if free_gb >= 2.0:
|
|
2981
|
-
_check("Disk space", "PASS", f"{free_gb:.1f} GB free")
|
|
2982
|
-
else:
|
|
2983
|
-
_check("Disk space", "WARN", f"{free_gb:.1f} GB free (< 2 GB)",
|
|
2984
|
-
"Free up disk space")
|
|
2985
|
-
except Exception:
|
|
2986
|
-
pass
|
|
2987
|
-
|
|
2988
|
-
# 10. Database integrity
|
|
2989
|
-
db_path = slm_home / "memory.db"
|
|
2990
|
-
if db_path.exists():
|
|
2991
|
-
try:
|
|
2992
|
-
from superlocalmemory.storage.memory_write import memory_read
|
|
2993
|
-
|
|
2994
|
-
# integrity_check reads every page. On a 610 MB store that is
|
|
2995
|
-
# 10-15 s — over half of this command — and it ran on every
|
|
2996
|
-
# invocation, including the ones a user makes twice in a row while
|
|
2997
|
-
# fixing something else. quick_check walks the same B-trees and
|
|
2998
|
-
# catches structural damage; what it skips is page-level checksum
|
|
2999
|
-
# work that only finds hardware bit rot, which surfaces as read
|
|
3000
|
-
# errors long before anyone runs a doctor.
|
|
3001
|
-
#
|
|
3002
|
-
# The trade is named rather than hidden: the output says which
|
|
3003
|
-
# check ran, and --deep runs the exhaustive one.
|
|
3004
|
-
deep = bool(getattr(args, "deep", False))
|
|
3005
|
-
pragma = "integrity_check" if deep else "quick_check"
|
|
3006
|
-
with memory_read(db_path) as conn:
|
|
3007
|
-
result = conn.execute(f"PRAGMA {pragma}").fetchone()
|
|
3008
|
-
if result and result[0] == "ok":
|
|
3009
|
-
size_mb = db_path.stat().st_size / (1024 * 1024)
|
|
3010
|
-
detail = f"OK ({size_mb:.2f} MB, {pragma})"
|
|
3011
|
-
if not deep:
|
|
3012
|
-
detail += " — run `slm doctor --deep` for a full page scan"
|
|
3013
|
-
_check("Database", "PASS", detail)
|
|
3014
|
-
else:
|
|
3015
|
-
_check("Database", "FAIL", f"{pragma}: {result}",
|
|
3016
|
-
"Backup and recreate database")
|
|
3017
|
-
except Exception as exc:
|
|
3018
|
-
_check("Database", "FAIL", str(exc))
|
|
3019
|
-
else:
|
|
3020
|
-
_check("Database", "PASS", "not yet created (will initialize on first use)")
|
|
3021
|
-
|
|
3022
|
-
# 10b. Projection queue. The graph and the vectors live in other storage
|
|
3023
|
-
# engines, and a queue that stops draining is the one failure that produces
|
|
3024
|
-
# no error at all: the memory is safely in SQLite, nothing raises, and
|
|
3025
|
-
# recall quietly stops finding it. Depth alone cannot distinguish a busy
|
|
3026
|
-
# queue from a wedged one, so the check keys on attempts — a row that has
|
|
3027
|
-
# been tried and refused is a defect with a fact id attached.
|
|
3028
|
-
if db_path.exists():
|
|
3029
|
-
try:
|
|
3030
|
-
from superlocalmemory.storage.memory_write import memory_read
|
|
3031
|
-
from superlocalmemory.storage.projection_outbox import DEPTH_SQL
|
|
3032
|
-
|
|
3033
|
-
with memory_read(db_path) as conn:
|
|
3034
|
-
depth = int(conn.execute(DEPTH_SQL).fetchone()[0])
|
|
3035
|
-
stalled = [
|
|
3036
|
-
row[0] for row in conn.execute(
|
|
3037
|
-
"SELECT fact_id FROM projection_outbox "
|
|
3038
|
-
"WHERE attempts >= 3 ORDER BY attempts DESC LIMIT 5"
|
|
3039
|
-
)
|
|
3040
|
-
]
|
|
3041
|
-
if stalled:
|
|
3042
|
-
_check(
|
|
3043
|
-
"Projection queue", "FAIL",
|
|
3044
|
-
f"{len(stalled)}+ memories refused by the graph or vector "
|
|
3045
|
-
f"store (e.g. {', '.join(f[:12] for f in stalled)})",
|
|
3046
|
-
"Check `slm logs` for the projection error, then restart "
|
|
3047
|
-
"the daemon to retry",
|
|
3048
|
-
)
|
|
3049
|
-
elif depth:
|
|
3050
|
-
_check(
|
|
3051
|
-
"Projection queue", "PASS",
|
|
3052
|
-
f"{depth} memory/memories queued — the worker drains these "
|
|
3053
|
-
"in the background",
|
|
3054
|
-
)
|
|
3055
|
-
else:
|
|
3056
|
-
_check("Projection queue", "PASS", "empty (graph is up to date)")
|
|
3057
|
-
except Exception:
|
|
3058
|
-
# No table means a store that predates the queue. Nothing is
|
|
3059
|
-
# pending on it by definition, so this is not a finding.
|
|
3060
|
-
_check("Projection queue", "PASS", "not applicable to this store")
|
|
3061
|
-
|
|
3062
|
-
# 11. PEP 668 advisory: detect EXTERNALLY-MANAGED marker and
|
|
3063
|
-
# recommend pipx when the system Python is managed by the OS package
|
|
3064
|
-
# manager (e.g. Homebrew, Debian/Ubuntu, Fedora 38+).
|
|
3065
|
-
try:
|
|
3066
|
-
import sysconfig as _sc
|
|
3067
|
-
_stdlib = _sc.get_path("stdlib")
|
|
3068
|
-
if _stdlib:
|
|
3069
|
-
_em_marker = Path(_stdlib) / "EXTERNALLY-MANAGED"
|
|
3070
|
-
if _em_marker.exists():
|
|
3071
|
-
_check(
|
|
3072
|
-
"PEP 668 / Install method",
|
|
3073
|
-
"WARN",
|
|
3074
|
-
"System Python is externally managed (EXTERNALLY-MANAGED marker found). "
|
|
3075
|
-
"pip install may fail with PEP 668 error.",
|
|
3076
|
-
"Use an isolated install: pipx install superlocalmemory "
|
|
3077
|
-
"or uv tool install superlocalmemory",
|
|
3078
|
-
)
|
|
3079
|
-
else:
|
|
3080
|
-
_check(
|
|
3081
|
-
"PEP 668 / Install method",
|
|
3082
|
-
"PASS",
|
|
3083
|
-
"No EXTERNALLY-MANAGED marker — standard pip install supported",
|
|
3084
|
-
)
|
|
3085
|
-
except Exception:
|
|
3086
|
-
pass # advisory only — never fail doctor on this check
|
|
3087
|
-
|
|
3088
|
-
# 12a. Component registry (v3.8.2) — models + optional vector/graph/
|
|
3089
|
-
# compression backends the older checks above did NOT cover
|
|
3090
|
-
# (embedder/reranker/compressor cache presence, sqlite-vec, lancedb,
|
|
3091
|
-
# cozo, llmlingua). Single source of truth shared with the daemon
|
|
3092
|
-
# self-heal and the dashboard "what's missing" report. Missing items
|
|
3093
|
-
# are advisory (WARN), never FAIL — the self-heal thread and
|
|
3094
|
-
# `slm doctor --fix` repair the auto-fixable ones. Runs BEFORE the
|
|
3095
|
-
# Optimize check so that check stays last (its ordering is pinned by
|
|
3096
|
-
# test_doctor_optimize).
|
|
3097
|
-
try:
|
|
3098
|
-
from superlocalmemory.core import component_registry as _cr
|
|
3099
|
-
try:
|
|
3100
|
-
from superlocalmemory.core.config import SLMConfig as _Cfg
|
|
3101
|
-
_dcfg = _Cfg.load()
|
|
3102
|
-
except Exception:
|
|
3103
|
-
_dcfg = None
|
|
3104
|
-
_already = {"python", "search_deps", "ollama"}
|
|
3105
|
-
for _c in _cr.probe_all(_dcfg):
|
|
3106
|
-
if _c.key in _already:
|
|
3107
|
-
continue # covered by checks 1/3/8 above — avoid duplicates
|
|
3108
|
-
_status = "PASS" if _c.status == _cr.STATUS_OK else "WARN"
|
|
3109
|
-
_detail = _c.detail or _c.status
|
|
3110
|
-
if _c.auto_fixable:
|
|
3111
|
-
_detail += " (auto-heals on daemon start)"
|
|
3112
|
-
_check(f"Component: {_c.label}", _status, _detail, _c.fix_cmd)
|
|
3113
|
-
except Exception as _cr_exc:
|
|
3114
|
-
_check("Component registry", "WARN", f"probe failed: {_cr_exc}")
|
|
3115
|
-
|
|
3116
|
-
# 13. Install version convergence — detect all copies of SLM on this machine
|
|
3117
|
-
# and warn when they are at different versions. Silent divergence is the
|
|
3118
|
-
# most common source of unexpected behaviour after an upgrade.
|
|
3119
|
-
try:
|
|
3120
|
-
_installs = _detect_all_installs() # shim defined above cmd_doctor
|
|
3121
|
-
_versions = {i["version"] for i in _installs}
|
|
3122
|
-
if not _installs:
|
|
3123
|
-
_check(
|
|
3124
|
-
"install_versions",
|
|
3125
|
-
"PASS",
|
|
3126
|
-
"No additional SLM installs detected on this machine",
|
|
3127
|
-
)
|
|
3128
|
-
elif len(_versions) > 1:
|
|
3129
|
-
_detail = "Version divergence: " + ", ".join(
|
|
3130
|
-
f"{i['type']}={i['version']}" for i in _installs
|
|
3131
|
-
)
|
|
3132
|
-
_check(
|
|
3133
|
-
"install_versions",
|
|
3134
|
-
"WARN",
|
|
3135
|
-
_detail,
|
|
3136
|
-
fix="Upgrade all installs: "
|
|
3137
|
-
"pipx upgrade superlocalmemory | "
|
|
3138
|
-
"pip install -U superlocalmemory (in ~/.slm-venv) | "
|
|
3139
|
-
"npm install -g superlocalmemory@latest",
|
|
3140
|
-
)
|
|
3141
|
-
else:
|
|
3142
|
-
_unified = next(iter(_versions))
|
|
3143
|
-
_types = ", ".join(i["type"] for i in _installs)
|
|
3144
|
-
_check(
|
|
3145
|
-
"install_versions",
|
|
3146
|
-
"PASS",
|
|
3147
|
-
f"All installs at {_unified} ({_types})",
|
|
3148
|
-
)
|
|
3149
|
-
except Exception as _inst_exc: # noqa: BLE001 — never break doctor
|
|
3150
|
-
_check("install_versions", "WARN", f"could not probe installs: {_inst_exc}")
|
|
3151
|
-
|
|
3152
|
-
# 14. The skills, agents and commands are NOT in the Python package.
|
|
3153
|
-
# They ship through the editor's own plugin channel -- `plugin/` in the
|
|
3154
|
-
# repository -- so `pip install --upgrade` cannot move them, and until now
|
|
3155
|
-
# nothing said so. 4.1 changed 76 files across those trees; a user who
|
|
3156
|
-
# upgraded the package and read a clean `slm doctor` had every reason to
|
|
3157
|
-
# believe they had all of it, and no way to find out otherwise.
|
|
3158
|
-
try:
|
|
3159
|
-
_pkg_version = _slm_version()
|
|
3160
|
-
_pl = _installed_plugin_versions()
|
|
3161
|
-
if not _pl:
|
|
3162
|
-
_check(
|
|
3163
|
-
"plugin_skills",
|
|
3164
|
-
"WARN",
|
|
3165
|
-
f"package is {_pkg_version}; no editor plugin detected, so the "
|
|
3166
|
-
f"skills, agents and commands are not installed or updated by "
|
|
3167
|
-
f"pip",
|
|
3168
|
-
fix="Claude Code: claude plugin marketplace add "
|
|
3169
|
-
"qualixar/superlocalmemory && claude plugin install "
|
|
3170
|
-
"superlocalmemory@qualixar "
|
|
3171
|
-
"Codex / VS Code: copy codex-plugin/ or copilot-plugin/ "
|
|
3172
|
-
"from the tag you are on",
|
|
3173
|
-
)
|
|
3174
|
-
else:
|
|
3175
|
-
_stale = {n: v for n, v in _pl.items() if v != _pkg_version}
|
|
3176
|
-
if _stale:
|
|
3177
|
-
_check(
|
|
3178
|
-
"plugin_skills",
|
|
3179
|
-
"WARN",
|
|
3180
|
-
"package is %s; plugin content still at %s" % (
|
|
3181
|
-
_pkg_version,
|
|
3182
|
-
", ".join(f"{n}={v}" for n, v in sorted(_stale.items())),
|
|
3183
|
-
),
|
|
3184
|
-
fix="claude plugin marketplace update qualixar && "
|
|
3185
|
-
"claude plugin update superlocalmemory@qualixar",
|
|
3186
|
-
)
|
|
3187
|
-
else:
|
|
3188
|
-
_check(
|
|
3189
|
-
"plugin_skills",
|
|
3190
|
-
"PASS",
|
|
3191
|
-
f"plugin content matches the package ({_pkg_version})",
|
|
3192
|
-
)
|
|
3193
|
-
except Exception as _pl_exc: # noqa: BLE001 — never break doctor
|
|
3194
|
-
_check("plugin_skills", "WARN", f"could not probe plugins: {_pl_exc}")
|
|
3195
|
-
|
|
3196
|
-
# 14. Migration error logs — surface any unresolved failure from a previous
|
|
3197
|
-
# upgrade attempt. The daemon writes these and they persist until the
|
|
3198
|
-
# user takes action; doctor is the right place to surface them.
|
|
3199
|
-
try:
|
|
3200
|
-
_err_logs = _migration_error_logs()
|
|
3201
|
-
if _err_logs:
|
|
3202
|
-
_latest = _err_logs[-1]
|
|
3203
|
-
_check(
|
|
3204
|
-
"migration_errors",
|
|
3205
|
-
"FAIL",
|
|
3206
|
-
f"Unresolved migration failure: {_latest.name}",
|
|
3207
|
-
# `--fix` heals models, sqlite-vec and stale locks. It does NOT
|
|
3208
|
-
# re-run a migration or restore a snapshot, so pointing here at
|
|
3209
|
-
# `--fix` sent the user round a loop that could never clear this
|
|
3210
|
-
# check. The log itself carries the restore command.
|
|
3211
|
-
fix=(
|
|
3212
|
-
f"Read {_latest} — it names what failed and, if a snapshot "
|
|
3213
|
-
f"was taken, the exact command to restore it. Restart the "
|
|
3214
|
-
f"daemon to retry the upgrade. Delete the log once resolved."
|
|
3215
|
-
),
|
|
3216
|
-
)
|
|
3217
|
-
else:
|
|
3218
|
-
# Reporting nothing when clean made "no failures" and "the check
|
|
3219
|
-
# never ran" look identical in the output.
|
|
3220
|
-
_check("migration_errors", "PASS", "no unresolved migration failures")
|
|
3221
|
-
except Exception as _mel_exc: # noqa: BLE001 — never break doctor
|
|
3222
|
-
_check("migration_errors", "WARN", f"could not check error logs: {_mel_exc}")
|
|
3223
|
-
|
|
3224
|
-
# 12. Optimize (Surface B) — reads daemon-persisted metrics (≤60s stale).
|
|
3225
|
-
info = _gather_optimize_surface_b()
|
|
3226
|
-
_enabled = info["enabled"]
|
|
3227
|
-
_error = info.get("error", "")
|
|
3228
|
-
if not _enabled:
|
|
3229
|
-
_check(
|
|
3230
|
-
"Optimize (Surface B)",
|
|
3231
|
-
"WARN",
|
|
3232
|
-
"disabled (optimize.json enabled=false) — caching/compression not active"
|
|
3233
|
-
+ (f" [{_error}]" if _error else ""),
|
|
3234
|
-
fix="Enable via dashboard Optimize tab or set enabled=true"
|
|
3235
|
-
f" in {slm_home / 'optimize.json'}",
|
|
3236
|
-
)
|
|
3237
|
-
else:
|
|
3238
|
-
_surfaces = []
|
|
3239
|
-
if info["cache_enabled"]:
|
|
3240
|
-
_surfaces.append("cache")
|
|
3241
|
-
if info["compress_enabled"]:
|
|
3242
|
-
_surfaces.append("compress")
|
|
3243
|
-
if info["proxy_enabled"]:
|
|
3244
|
-
_surfaces.append("proxy")
|
|
3245
|
-
_stats = (
|
|
3246
|
-
f"compress_runs={info['compress_runs']}"
|
|
3247
|
-
f" tokens_saved={info['tokens_saved']}"
|
|
3248
|
-
f" cache_hits={info['cache_hits']}"
|
|
3249
|
-
f" cache_misses={info['cache_misses']}"
|
|
3250
|
-
)
|
|
3251
|
-
_surface_str = ",".join(_surfaces) if _surfaces else "(none)"
|
|
3252
|
-
if not _surfaces:
|
|
3253
|
-
_check(
|
|
3254
|
-
"Optimize (Surface B)",
|
|
3255
|
-
"WARN",
|
|
3256
|
-
f"enabled [{_surface_str}] but no surface active"
|
|
3257
|
-
+ (f" [{_error}]" if _error else ""),
|
|
3258
|
-
)
|
|
3259
|
-
elif not info["db_present"]:
|
|
3260
|
-
_check(
|
|
3261
|
-
"Optimize (Surface B)",
|
|
3262
|
-
"WARN",
|
|
3263
|
-
f"enabled [{_surface_str}] {_stats}"
|
|
3264
|
-
" but no metrics yet (llmcache.db not created)"
|
|
3265
|
-
+ (f" [{_error}]" if _error else ""),
|
|
3266
|
-
fix="slm serve start",
|
|
3267
|
-
)
|
|
3268
|
-
elif _error:
|
|
3269
|
-
_check(
|
|
3270
|
-
"Optimize (Surface B)",
|
|
3271
|
-
"WARN",
|
|
3272
|
-
f"enabled [{_surface_str}] {_stats} (partial: {_error})",
|
|
3273
|
-
)
|
|
3274
|
-
else:
|
|
3275
|
-
_check(
|
|
3276
|
-
"Optimize (Surface B)",
|
|
3277
|
-
"PASS",
|
|
3278
|
-
f"enabled [{_surface_str}] {_stats}",
|
|
3279
|
-
)
|
|
3280
|
-
|
|
3281
|
-
# Memory answer-ability.
|
|
3282
|
-
#
|
|
3283
|
-
# Every other check here asks whether a component is installed. None of
|
|
3284
|
-
# them asked the only question that matters to the owner: can my memories
|
|
3285
|
-
# actually be found? One machine ran for months with 43.7% of its store
|
|
3286
|
-
# unreachable while doctor reported everything green, because the reachable
|
|
3287
|
-
# share was never counted.
|
|
3288
|
-
memory_health_data: dict | None = None
|
|
3289
|
-
try:
|
|
3290
|
-
from superlocalmemory.core.config import SLMConfig as _HealthCfg
|
|
3291
|
-
from superlocalmemory.core.memory_health import describe, measure
|
|
3292
|
-
_h = measure(_HealthCfg.load().db_path)
|
|
3293
|
-
memory_health_data = {
|
|
3294
|
-
"live_facts": _h.live_facts,
|
|
3295
|
-
"findable_by_meaning": _h.findable_by_meaning,
|
|
3296
|
-
"missing_vector": _h.missing_vector,
|
|
3297
|
-
"withheld_summaries": _h.withheld_summaries,
|
|
3298
|
-
"display_summaries": _h.display_summaries,
|
|
3299
|
-
"hidden_by_forgetting": _h.hidden_by_forgetting,
|
|
3300
|
-
"inconsistently_hidden": _h.inconsistently_hidden,
|
|
3301
|
-
"reachability": round(_h.reachability, 4),
|
|
3302
|
-
"healthy": _h.healthy,
|
|
3303
|
-
"unavailable": list(_h.unavailable),
|
|
3304
|
-
"summary": describe(_h),
|
|
3305
|
-
}
|
|
3306
|
-
_detail = " ".join(describe(_h))
|
|
3307
|
-
if _h.healthy:
|
|
3308
|
-
_check("Memory answer-ability", "PASS", _detail)
|
|
3309
|
-
elif _h.inconsistently_hidden or _h.reachability < 0.9:
|
|
3310
|
-
_check(
|
|
3311
|
-
"Memory answer-ability", "FAIL", _detail,
|
|
3312
|
-
fix="slm restart",
|
|
3313
|
-
)
|
|
3314
|
-
else:
|
|
3315
|
-
_check("Memory answer-ability", "WARN", _detail,
|
|
3316
|
-
fix="slm db reembed --missing-only")
|
|
3317
|
-
except Exception as _mh_exc: # noqa: BLE001 — a report must not break doctor
|
|
3318
|
-
_check(
|
|
3319
|
-
"Memory answer-ability", "WARN",
|
|
3320
|
-
f"could not be measured: {_mh_exc}",
|
|
3321
|
-
)
|
|
3322
|
-
|
|
3323
|
-
# Summary
|
|
3324
|
-
if use_json:
|
|
3325
|
-
from superlocalmemory.cli.json_output import json_print
|
|
3326
|
-
next_actions = []
|
|
3327
|
-
for c in checks:
|
|
3328
|
-
if c["fix"]:
|
|
3329
|
-
next_actions.append({"command": c["fix"], "description": f"Fix {c['name']}"})
|
|
3330
|
-
json_print("doctor", data={
|
|
3331
|
-
"checks": checks,
|
|
3332
|
-
"summary": {"passed": passed, "warned": warned, "failed": failed},
|
|
3333
|
-
"memory_health": memory_health_data,
|
|
3334
|
-
}, next_actions=next_actions)
|
|
3335
|
-
else:
|
|
3336
|
-
print(f"\nSummary: {passed} passed, {warned} warnings, {failed} failed")
|
|
3337
|
-
if failed > 0:
|
|
3338
|
-
print("Run the suggested fix commands above, then re-run: slm doctor")
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
def cmd_trace(args: Namespace) -> None:
|
|
3342
|
-
"""Recall with per-channel score breakdown."""
|
|
3343
|
-
use_json = getattr(args, 'json', False)
|
|
3344
|
-
limit = getattr(args, 'limit', 10)
|
|
3345
|
-
|
|
3346
|
-
# Trace must use the same daemon-owned engine as recall. A direct CLI
|
|
3347
|
-
# engine cannot attach to the machine-wide embedding worker already owned
|
|
3348
|
-
# by the daemon; it then silently loses semantic, Hopfield, and spreading
|
|
3349
|
-
# activation channels. The daemon trace route keeps the loaded model,
|
|
3350
|
-
# graph, and retrieval state intact while returning the same score detail.
|
|
3351
|
-
try:
|
|
3352
|
-
from superlocalmemory.cli.daemon import (
|
|
3353
|
-
daemon_request, ensure_daemon, is_daemon_running,
|
|
3354
|
-
)
|
|
3355
|
-
if is_daemon_running() or ensure_daemon():
|
|
3356
|
-
result = daemon_request(
|
|
3357
|
-
"POST", "/api/v3/recall/trace",
|
|
3358
|
-
{"query": args.query, "limit": limit},
|
|
3359
|
-
)
|
|
3360
|
-
if result and "results" in result:
|
|
3361
|
-
if use_json:
|
|
3362
|
-
from superlocalmemory.cli.json_output import json_print
|
|
3363
|
-
json_print("trace", data={
|
|
3364
|
-
"query": result.get("query", args.query),
|
|
3365
|
-
"query_type": result.get("query_type", "unknown"),
|
|
3366
|
-
"retrieval_time_ms": round(
|
|
3367
|
-
float(result.get("retrieval_time_ms", 0)), 1,
|
|
3368
|
-
),
|
|
3369
|
-
"results": result["results"],
|
|
3370
|
-
"count": len(result["results"]),
|
|
3371
|
-
"no_confident_match": bool(
|
|
3372
|
-
result.get("no_confident_match", False),
|
|
3373
|
-
),
|
|
3374
|
-
"score_contract_version": result.get(
|
|
3375
|
-
"score_contract_version", "2",
|
|
3376
|
-
),
|
|
3377
|
-
"calibration_status": result.get(
|
|
3378
|
-
"calibration_status", "uncalibrated",
|
|
3379
|
-
),
|
|
3380
|
-
"calibration_id": result.get("calibration_id"),
|
|
3381
|
-
"answer_confidence": result.get("answer_confidence"),
|
|
3382
|
-
"abstained": bool(result.get("abstained", False)),
|
|
3383
|
-
"abstention_reason": result.get("abstention_reason"),
|
|
3384
|
-
}, next_actions=[
|
|
3385
|
-
{
|
|
3386
|
-
"command": "slm recall '<query>' --json",
|
|
3387
|
-
"description": "Standard recall",
|
|
3388
|
-
},
|
|
3389
|
-
])
|
|
3390
|
-
return
|
|
3391
|
-
print(f"Query: {result.get('query', args.query)}")
|
|
3392
|
-
print(
|
|
3393
|
-
f"Type: {result.get('query_type', 'unknown')} | Time: "
|
|
3394
|
-
f"{float(result.get('retrieval_time_ms', 0)):.0f}ms"
|
|
3395
|
-
)
|
|
3396
|
-
print(f"Results: {len(result['results'])}")
|
|
3397
|
-
for i, item in enumerate(result["results"], 1):
|
|
3398
|
-
print(
|
|
3399
|
-
f"\n {i}. [relevance "
|
|
3400
|
-
f"{float(item.get('relevance_score', item.get('score', 0))):.3f}] "
|
|
3401
|
-
f"{str(item.get('content', ''))[:100]}"
|
|
3402
|
-
)
|
|
3403
|
-
if item.get("ranking_score") is not None:
|
|
3404
|
-
print(
|
|
3405
|
-
" ranking utility: "
|
|
3406
|
-
f"{float(item['ranking_score']):.6f}"
|
|
3407
|
-
)
|
|
3408
|
-
for channel, score in (item.get("channel_scores") or {}).items():
|
|
3409
|
-
print(f" {channel}: {float(score):.3f}")
|
|
3410
|
-
return
|
|
3411
|
-
except Exception:
|
|
3412
|
-
# The direct path remains the offline escape hatch when a daemon is
|
|
3413
|
-
# unavailable or a local transport error occurs.
|
|
3414
|
-
pass
|
|
3415
|
-
|
|
3416
|
-
from superlocalmemory.core.engine import MemoryEngine
|
|
3417
|
-
from superlocalmemory.core.config import SLMConfig
|
|
3418
|
-
try:
|
|
3419
|
-
config = SLMConfig.load()
|
|
3420
|
-
engine = MemoryEngine(config)
|
|
3421
|
-
engine.initialize()
|
|
3422
|
-
response = engine.recall(args.query, limit=limit)
|
|
3423
|
-
except Exception as exc:
|
|
3424
|
-
if use_json:
|
|
3425
|
-
from superlocalmemory.cli.json_output import json_print
|
|
3426
|
-
json_print("trace", error={"code": "ENGINE_ERROR", "message": str(exc)})
|
|
3427
|
-
sys.exit(1)
|
|
3428
|
-
raise
|
|
3429
|
-
|
|
3430
|
-
if use_json:
|
|
3431
|
-
from superlocalmemory.cli.json_output import json_print
|
|
3432
|
-
from superlocalmemory.server.recall_serializer import (
|
|
3433
|
-
recall_response_metadata,
|
|
3434
|
-
serialize_recall_response,
|
|
3435
|
-
)
|
|
3436
|
-
items, no_confident_match = serialize_recall_response(
|
|
3437
|
-
response,
|
|
3438
|
-
limit=limit,
|
|
3439
|
-
per_fact_max=200,
|
|
3440
|
-
total_max=max(200, limit * 200),
|
|
3441
|
-
)
|
|
3442
|
-
json_print("trace", data={
|
|
3443
|
-
"query": args.query,
|
|
3444
|
-
"query_type": getattr(response, "query_type", "unknown"),
|
|
3445
|
-
"retrieval_time_ms": round(getattr(response, "retrieval_time_ms", 0), 1),
|
|
3446
|
-
"results": items, "count": len(items),
|
|
3447
|
-
"no_confident_match": no_confident_match,
|
|
3448
|
-
**recall_response_metadata(response),
|
|
3449
|
-
}, next_actions=[
|
|
3450
|
-
{"command": "slm recall '<query>' --json", "description": "Standard recall"},
|
|
3451
|
-
])
|
|
3452
|
-
return
|
|
3453
|
-
|
|
3454
|
-
print(f"Query: {args.query}")
|
|
3455
|
-
print(f"Type: {response.query_type} | Time: {response.retrieval_time_ms:.0f}ms")
|
|
3456
|
-
print(f"Results: {len(response.results)}")
|
|
3457
|
-
for i, r in enumerate(response.results, 1):
|
|
3458
|
-
print(f"\n {i}. [relevance {r.relevance_score:.3f}] {r.fact.content[:100]}")
|
|
3459
|
-
if r.ranking_score is not None:
|
|
3460
|
-
print(f" ranking utility: {r.ranking_score:.6f}")
|
|
3461
|
-
if hasattr(r, "channel_scores") and r.channel_scores:
|
|
3462
|
-
for ch, sc in r.channel_scores.items():
|
|
3463
|
-
print(f" {ch}: {sc:.3f}")
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
# -- Services (no --json — these start long-running processes) -------------
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
def cmd_mcp(_args: Namespace) -> None:
|
|
3470
|
-
"""Start the V3 MCP server (stdio transport for IDE integration)."""
|
|
3471
|
-
# SINGLETON GUARD (v3.5.8): Reap fresh orphans before binding stdio.
|
|
3472
|
-
# Root cause: every IDE session spawns a new `slm mcp`; dead sessions'
|
|
3473
|
-
# processes survive indefinitely (~400 MB each) because the reaper's
|
|
3474
|
-
# 1-hour age gate misses them. Fix: run find_orphans with age_threshold=0
|
|
3475
|
-
# so any `slm mcp` whose parent IDE/Claude process has died is killed
|
|
3476
|
-
# immediately. Only orphans (dead parent) are killed — live sessions safe.
|
|
3477
|
-
# CRITICAL: No stdout anywhere below — MCP stdio transport, any print
|
|
3478
|
-
# corrupts the JSON-RPC protocol.
|
|
3479
|
-
try:
|
|
3480
|
-
from superlocalmemory.infra.process_reaper import (
|
|
3481
|
-
ReaperConfig,
|
|
3482
|
-
find_orphans,
|
|
3483
|
-
is_mcp_server_process,
|
|
3484
|
-
kill_orphan,
|
|
3485
|
-
)
|
|
3486
|
-
_reaper_cfg = ReaperConfig(orphan_age_threshold_hours=0.0)
|
|
3487
|
-
for _orphan in find_orphans(_reaper_cfg):
|
|
3488
|
-
# A unified daemon is expected to be detached from the launching
|
|
3489
|
-
# shell and can therefore have PPID 1. The MCP reaper must never
|
|
3490
|
-
# treat that healthy shared daemon as an orphaned stdio server.
|
|
3491
|
-
if is_mcp_server_process(_orphan):
|
|
3492
|
-
kill_orphan(_orphan.pid, graceful_timeout_seconds=1.0)
|
|
3493
|
-
except Exception:
|
|
3494
|
-
pass # Never block MCP startup on cleanup failure
|
|
3495
|
-
|
|
3496
|
-
# Version integrity (issue #107). The reaper above only kills *orphans* —
|
|
3497
|
-
# servers whose parent died. A server whose IDE is still alive is never an
|
|
3498
|
-
# orphan, so it survives upgrades indefinitely and keeps serving the code
|
|
3499
|
-
# it imported at startup. That is how eighteen `slm mcp` processes spanning
|
|
3500
|
-
# four days and two releases stayed alive on one machine, and how a stale
|
|
3501
|
-
# one made issue #106 look unfixed across two debugging sessions.
|
|
3502
|
-
#
|
|
3503
|
-
# This runs at startup, so a server launched *after* an upgrade is correct
|
|
3504
|
-
# by construction and stays silent. It fires for a server that started
|
|
3505
|
-
# before its own package was upgraded — which is possible when the client
|
|
3506
|
-
# respawns it from an old cached path.
|
|
3507
|
-
#
|
|
3508
|
-
# CRITICAL: logging only, never stdout — MCP speaks JSON-RPC over stdio and
|
|
3509
|
-
# any print corrupts the protocol. The logger writes to stderr.
|
|
3510
|
-
try:
|
|
3511
|
-
from superlocalmemory.infra.version_integrity import check_version_integrity
|
|
3512
|
-
|
|
3513
|
-
_vi = check_version_integrity()
|
|
3514
|
-
if _vi.is_stale:
|
|
3515
|
-
logger.warning("MCP server version drift: %s. %s", _vi.detail, _vi.hint)
|
|
3516
|
-
elif _vi.differs:
|
|
3517
|
-
logger.info("MCP server version note: %s", _vi.detail)
|
|
3518
|
-
except Exception:
|
|
3519
|
-
pass # A diagnostic must never prevent the server from starting.
|
|
3520
|
-
|
|
3521
|
-
from superlocalmemory.mcp.server import server
|
|
3522
|
-
|
|
3523
|
-
server.run(transport="stdio")
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
def cmd_warmup(_args: Namespace) -> None:
|
|
3527
|
-
"""Pre-download the embedding model so first use is instant.
|
|
3528
|
-
|
|
3529
|
-
v3.4.42: daemon-aware. The embedding worker is a machine-wide
|
|
3530
|
-
singleton (`_is_embedding_worker_alive` + PID file), so when the
|
|
3531
|
-
unified daemon is running it OWNS the worker. A fresh
|
|
3532
|
-
`EmbeddingService` started here would see the singleton, set
|
|
3533
|
-
`_available = False`, return None from `_subprocess_embed`, and
|
|
3534
|
-
print "embedding verification failed" — even though the daemon's
|
|
3535
|
-
worker is already happily serving the same model. The fix: detect
|
|
3536
|
-
the daemon, verify via its health endpoint, and skip the local
|
|
3537
|
-
spawn. Only fall through to the original local-worker path when
|
|
3538
|
-
the daemon is genuinely unreachable.
|
|
3539
|
-
"""
|
|
3540
|
-
import superlocalmemory.core.embeddings as _emb_mod
|
|
3541
|
-
|
|
3542
|
-
print("SuperLocalMemory V4 — Embedding Model Warmup")
|
|
3543
|
-
print("=" * 50)
|
|
3544
|
-
print(f" Python: {sys.executable}")
|
|
3545
|
-
print(f" Model: nomic-ai/nomic-embed-text-v1.5 (~500MB)")
|
|
3546
|
-
print()
|
|
3547
|
-
|
|
3548
|
-
# v3.4.42 — daemon-aware fast path. If the daemon is up and reports
|
|
3549
|
-
# engine=initialized, the embedding model is already loaded inside
|
|
3550
|
-
# the daemon's worker subprocess. No need to spawn a redundant one;
|
|
3551
|
-
# in fact, the machine-wide singleton would refuse to do so anyway.
|
|
3552
|
-
try:
|
|
3553
|
-
from superlocalmemory.cli.daemon import (
|
|
3554
|
-
is_daemon_running, daemon_request,
|
|
3555
|
-
)
|
|
3556
|
-
if is_daemon_running():
|
|
3557
|
-
health = daemon_request("GET", "/health")
|
|
3558
|
-
if health and health.get("engine") == "initialized":
|
|
3559
|
-
from superlocalmemory.core.config import EmbeddingConfig
|
|
3560
|
-
cfg = EmbeddingConfig()
|
|
3561
|
-
print("[PASS] Daemon is running with embedding model loaded.")
|
|
3562
|
-
print(f" Model: {cfg.model_name} ({cfg.dimension}-dim)")
|
|
3563
|
-
print("Semantic search is fully operational.")
|
|
3564
|
-
return
|
|
3565
|
-
# Daemon up but engine not yet initialized — warn and return
|
|
3566
|
-
# rather than racing the daemon for the singleton lock.
|
|
3567
|
-
engine_state = (health or {}).get("engine", "unknown")
|
|
3568
|
-
print(f"[INFO] Daemon is up but engine state is '{engine_state}'.")
|
|
3569
|
-
print(" Wait ~30s and retry, or run: slm doctor")
|
|
3570
|
-
return
|
|
3571
|
-
except Exception:
|
|
3572
|
-
# Any failure in the daemon path falls through to local warmup —
|
|
3573
|
-
# better to spawn a local worker than block warmup entirely.
|
|
3574
|
-
pass
|
|
3575
|
-
|
|
3576
|
-
# Local-warmup fallback path: daemon is unreachable, so it's safe
|
|
3577
|
-
# to spawn our own embedding worker (no singleton conflict).
|
|
3578
|
-
# Increase timeout for first-time download.
|
|
3579
|
-
original_timeout = _emb_mod._SUBPROCESS_RESPONSE_TIMEOUT
|
|
3580
|
-
_emb_mod._SUBPROCESS_RESPONSE_TIMEOUT = 180 # 3 min for cold start
|
|
3581
|
-
|
|
3582
|
-
try:
|
|
3583
|
-
from superlocalmemory.core.config import EmbeddingConfig
|
|
3584
|
-
from superlocalmemory.core.embeddings import EmbeddingService
|
|
3585
|
-
|
|
3586
|
-
config = EmbeddingConfig()
|
|
3587
|
-
|
|
3588
|
-
print("Step 1/3: Spawning embedding worker subprocess...")
|
|
3589
|
-
svc = EmbeddingService(config)
|
|
3590
|
-
|
|
3591
|
-
if not svc.is_available:
|
|
3592
|
-
print("\n[FAIL] Embedding service not available.")
|
|
3593
|
-
_warmup_diagnose()
|
|
3594
|
-
return
|
|
3595
|
-
|
|
3596
|
-
print("Step 2/3: Loading model (may download ~500MB on first run)...")
|
|
3597
|
-
emb = svc.embed("warmup test")
|
|
3598
|
-
|
|
3599
|
-
if emb and len(emb) == config.dimension:
|
|
3600
|
-
print("Step 3/3: Verifying embedding output...")
|
|
3601
|
-
print(f"\n[PASS] Model ready: {config.model_name} ({config.dimension}-dim)")
|
|
3602
|
-
print("Semantic search is fully operational.")
|
|
3603
|
-
else:
|
|
3604
|
-
print("\n[FAIL] Model loaded but embedding verification failed.")
|
|
3605
|
-
_warmup_diagnose()
|
|
3606
|
-
|
|
3607
|
-
except ImportError as exc:
|
|
3608
|
-
print(f"\n[FAIL] Missing dependency: {exc}")
|
|
3609
|
-
print("Fix: pip install sentence-transformers einops torch")
|
|
3610
|
-
except Exception as exc:
|
|
3611
|
-
print(f"\n[FAIL] Warmup failed: {exc}")
|
|
3612
|
-
_warmup_diagnose()
|
|
3613
|
-
finally:
|
|
3614
|
-
_emb_mod._SUBPROCESS_RESPONSE_TIMEOUT = original_timeout
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
def _warmup_diagnose() -> None:
|
|
3618
|
-
"""Diagnostic helper when warmup fails."""
|
|
3619
|
-
print("\nDiagnosing...")
|
|
3620
|
-
print(f" Python executable: {sys.executable}")
|
|
3621
|
-
os.environ["ORT_DISABLE_COREML"] = "1"
|
|
3622
|
-
try:
|
|
3623
|
-
from sentence_transformers import SentenceTransformer
|
|
3624
|
-
print(" sentence-transformers: importable")
|
|
3625
|
-
m = SentenceTransformer(
|
|
3626
|
-
"nomic-ai/nomic-embed-text-v1.5", trust_remote_code=True, device="cpu",
|
|
3627
|
-
)
|
|
3628
|
-
v = m.encode(["test"], normalize_embeddings=True)
|
|
3629
|
-
print(f" Direct embed: OK (dim={v.shape[1]})")
|
|
3630
|
-
print("\n Issue: Subprocess worker failed but direct import works.")
|
|
3631
|
-
print(" This is likely a Python path mismatch between Node.js wrapper")
|
|
3632
|
-
print(" and your current shell. Run: slm doctor")
|
|
3633
|
-
except ImportError as ie:
|
|
3634
|
-
print(f" sentence-transformers: NOT importable ({ie})")
|
|
3635
|
-
print(" Fix: pip install sentence-transformers einops torch")
|
|
3636
|
-
except Exception as de:
|
|
3637
|
-
print(f" Direct embed failed: {de}")
|
|
3638
|
-
print(" Run: slm doctor")
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
def cmd_dashboard(args: Namespace) -> None:
|
|
3642
|
-
"""Open the web dashboard in the browser.
|
|
3643
|
-
|
|
3644
|
-
v3.4.3: Dashboard is now served by the unified daemon. This command
|
|
3645
|
-
ensures the daemon is running and opens the browser. It does NOT
|
|
3646
|
-
start a separate server (saves ~500MB RAM from duplicate engine).
|
|
3647
|
-
"""
|
|
3648
|
-
from superlocalmemory.cli.daemon import ensure_daemon, _get_port
|
|
3649
|
-
|
|
3650
|
-
port = getattr(args, "port", None) or _get_port()
|
|
3651
|
-
|
|
3652
|
-
print(" SuperLocalMemory V4 — Web Dashboard")
|
|
3653
|
-
print(f" Starting daemon if needed...")
|
|
3654
|
-
|
|
3655
|
-
if not ensure_daemon(port=port):
|
|
3656
|
-
print(" ✗ Could not start daemon. Run `slm doctor` to diagnose.")
|
|
3657
|
-
sys.exit(1)
|
|
3658
|
-
|
|
3659
|
-
url = f"http://localhost:{port}"
|
|
3660
|
-
print(f" ✓ Daemon running")
|
|
3661
|
-
print(f" Dashboard: {url}")
|
|
3662
|
-
print(f" API Docs: {url}/docs")
|
|
3663
|
-
|
|
3664
|
-
# Open browser
|
|
3665
|
-
import webbrowser
|
|
3666
|
-
webbrowser.open(url)
|
|
3667
|
-
print("\n Dashboard opened in browser. Daemon continues running in background.")
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
# -- Profiles (supports --json) -------------------------------------------
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
def _switch_profile_runtime(config, profile_name: str) -> dict:
|
|
3674
|
-
"""Switch through the resident daemon, or persist an offline fallback."""
|
|
3675
|
-
from superlocalmemory.cli.daemon import daemon_request, is_daemon_running
|
|
3676
|
-
|
|
3677
|
-
if is_daemon_running():
|
|
3678
|
-
result = daemon_request(
|
|
3679
|
-
"POST",
|
|
3680
|
-
f"/api/profiles/{profile_name}/switch",
|
|
3681
|
-
)
|
|
3682
|
-
if not result or not result.get("success"):
|
|
3683
|
-
raise RuntimeError(
|
|
3684
|
-
"resident daemon did not acknowledge the profile switch"
|
|
3685
|
-
)
|
|
3686
|
-
acknowledged = str(result.get("active_profile", ""))
|
|
3687
|
-
if acknowledged != profile_name:
|
|
3688
|
-
raise RuntimeError(
|
|
3689
|
-
"resident daemon acknowledged a different active profile"
|
|
3690
|
-
)
|
|
3691
|
-
return {
|
|
3692
|
-
"action": "switched",
|
|
3693
|
-
"profile": acknowledged,
|
|
3694
|
-
"generation": int(result.get("generation", 0)),
|
|
3695
|
-
"runtime": "daemon",
|
|
3696
|
-
}
|
|
3697
|
-
|
|
3698
|
-
from superlocalmemory.server.profile_runtime import persist_active_profile
|
|
3699
|
-
|
|
3700
|
-
persist_active_profile(profile_name)
|
|
3701
|
-
config.active_profile = profile_name
|
|
3702
|
-
return {
|
|
3703
|
-
"action": "switched",
|
|
3704
|
-
"profile": profile_name,
|
|
3705
|
-
"runtime": "offline",
|
|
3706
|
-
}
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
def cmd_profile(args: Namespace) -> None:
|
|
3710
|
-
"""Profile management (list, switch, create).
|
|
3711
|
-
|
|
3712
|
-
Writes to BOTH SQLite and profiles.json so CLI, Dashboard, and
|
|
3713
|
-
MCP all see the same profiles.
|
|
3714
|
-
"""
|
|
3715
|
-
action = getattr(args, "action", "list")
|
|
3716
|
-
if action in ("switch", "create"):
|
|
3717
|
-
from superlocalmemory.core.admission import gate_cli_mutation
|
|
3718
|
-
from superlocalmemory.core.operation_request import OperationKind
|
|
3719
|
-
# The role that decides this is the one held on the workspace being
|
|
3720
|
-
# switched to or created, not on whichever one happens to be active.
|
|
3721
|
-
gate_cli_mutation(
|
|
3722
|
-
OperationKind.PROFILE_SWITCH,
|
|
3723
|
-
profile=str(getattr(args, "name", "") or ""),
|
|
3724
|
-
)
|
|
3725
|
-
|
|
3726
|
-
from superlocalmemory.core.config import SLMConfig
|
|
3727
|
-
from superlocalmemory.storage.database import DatabaseManager
|
|
3728
|
-
from superlocalmemory.storage import schema
|
|
3729
|
-
from superlocalmemory.server.routes.helpers import (
|
|
3730
|
-
ensure_profile_in_json,
|
|
3731
|
-
)
|
|
3732
|
-
|
|
3733
|
-
config = SLMConfig.load()
|
|
3734
|
-
db = DatabaseManager(config.db_path)
|
|
3735
|
-
db.initialize(schema)
|
|
3736
|
-
|
|
3737
|
-
if getattr(args, 'json', False):
|
|
3738
|
-
from superlocalmemory.cli.json_output import json_print
|
|
3739
|
-
if args.action == "list":
|
|
3740
|
-
rows = db.execute("SELECT profile_id, name FROM profiles")
|
|
3741
|
-
profiles = [
|
|
3742
|
-
{"profile_id": dict(r)["profile_id"], "name": dict(r).get("name", "")}
|
|
3743
|
-
for r in rows
|
|
3744
|
-
]
|
|
3745
|
-
json_print("profile", data={"profiles": profiles, "count": len(profiles)},
|
|
3746
|
-
next_actions=[
|
|
3747
|
-
{"command": "slm profile switch <name> --json", "description": "Switch profile"},
|
|
3748
|
-
])
|
|
3749
|
-
elif args.action == "switch":
|
|
3750
|
-
rows = db.execute(
|
|
3751
|
-
"SELECT 1 FROM profiles WHERE profile_id = ?",
|
|
3752
|
-
(args.name,),
|
|
3753
|
-
)
|
|
3754
|
-
if not rows:
|
|
3755
|
-
json_print("profile", error={
|
|
3756
|
-
"code": "PROFILE_NOT_FOUND",
|
|
3757
|
-
"message": f"Profile '{args.name}' does not exist.",
|
|
3758
|
-
})
|
|
3759
|
-
sys.exit(1)
|
|
3760
|
-
try:
|
|
3761
|
-
result = _switch_profile_runtime(config, args.name)
|
|
3762
|
-
except Exception as exc:
|
|
3763
|
-
json_print("profile", error={
|
|
3764
|
-
"code": "PROFILE_SWITCH_FAILED",
|
|
3765
|
-
"message": str(exc),
|
|
3766
|
-
})
|
|
3767
|
-
sys.exit(1)
|
|
3768
|
-
json_print("profile", data=result)
|
|
3769
|
-
elif args.action == "create":
|
|
3770
|
-
db.execute(
|
|
3771
|
-
"INSERT OR IGNORE INTO profiles (profile_id, name) VALUES (?, ?)",
|
|
3772
|
-
(args.name, args.name),
|
|
3773
|
-
)
|
|
3774
|
-
ensure_profile_in_json(args.name)
|
|
3775
|
-
json_print("profile", data={"action": "created", "profile": args.name},
|
|
3776
|
-
next_actions=[
|
|
3777
|
-
{"command": f"slm profile switch {args.name} --json",
|
|
3778
|
-
"description": "Switch to new profile"},
|
|
3779
|
-
])
|
|
3780
|
-
return
|
|
3781
|
-
|
|
3782
|
-
if args.action == "list":
|
|
3783
|
-
rows = db.execute("SELECT profile_id, name FROM profiles")
|
|
3784
|
-
print("Profiles:")
|
|
3785
|
-
for r in rows:
|
|
3786
|
-
d = dict(r)
|
|
3787
|
-
print(f" - {d['profile_id']}: {d.get('name', '')}")
|
|
3788
|
-
elif args.action == "switch":
|
|
3789
|
-
rows = db.execute(
|
|
3790
|
-
"SELECT 1 FROM profiles WHERE profile_id = ?",
|
|
3791
|
-
(args.name,),
|
|
3792
|
-
)
|
|
3793
|
-
if not rows:
|
|
3794
|
-
print(f"Profile '{args.name}' does not exist.", file=sys.stderr)
|
|
3795
|
-
sys.exit(1)
|
|
3796
|
-
try:
|
|
3797
|
-
result = _switch_profile_runtime(config, args.name)
|
|
3798
|
-
except Exception as exc:
|
|
3799
|
-
print(f"Profile switch failed: {exc}", file=sys.stderr)
|
|
3800
|
-
sys.exit(1)
|
|
3801
|
-
generation = result.get("generation")
|
|
3802
|
-
suffix = f" (generation {generation})" if generation is not None else ""
|
|
3803
|
-
print(f"Switched to profile: {args.name}{suffix}")
|
|
3804
|
-
elif args.action == "create":
|
|
3805
|
-
db.execute(
|
|
3806
|
-
"INSERT OR IGNORE INTO profiles (profile_id, name) VALUES (?, ?)",
|
|
3807
|
-
(args.name, args.name),
|
|
3808
|
-
)
|
|
3809
|
-
ensure_profile_in_json(args.name)
|
|
3810
|
-
print(f"Created profile: {args.name}")
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
# -- Active Memory commands (V3.1) ------------------------------------------
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
def _cmd_init_auto(
|
|
3817
|
-
args: Namespace,
|
|
3818
|
-
slm_data_dir: "Path",
|
|
3819
|
-
config_exists: bool,
|
|
3820
|
-
force: bool,
|
|
3821
|
-
) -> None:
|
|
3822
|
-
"""Non-interactive --auto branch for slm init.
|
|
3823
|
-
|
|
3824
|
-
Best-effort at every step; only exits non-zero when config save fails.
|
|
3825
|
-
No TTY required. Does NOT run IDE connect (AC6).
|
|
3826
|
-
"""
|
|
3827
|
-
from pathlib import Path
|
|
3828
|
-
from superlocalmemory.core.config import SLMConfig
|
|
3829
|
-
from superlocalmemory.storage.models import Mode
|
|
3830
|
-
|
|
3831
|
-
# Step 1: write mode-A config (create-if-absent or --force).
|
|
3832
|
-
# Pass slm_data_dir explicitly so env-overridden paths are respected even
|
|
3833
|
-
# when DEFAULT_BASE_DIR was evaluated before the env var was set (e.g. tests).
|
|
3834
|
-
if force or not config_exists:
|
|
3835
|
-
try:
|
|
3836
|
-
cfg = SLMConfig.for_mode(Mode.A, base_dir=slm_data_dir)
|
|
3837
|
-
cfg.save(mode_change=True)
|
|
3838
|
-
except Exception as exc:
|
|
3839
|
-
print(f"[ERROR] slm init --auto: config save failed: {exc}", file=sys.stderr)
|
|
3840
|
-
sys.exit(1)
|
|
3841
|
-
|
|
3842
|
-
# Step 2: mark complete (write .setup-complete sentinel).
|
|
3843
|
-
# Write the sentinel directly using the already-selected namespace.
|
|
3844
|
-
try:
|
|
3845
|
-
import platform
|
|
3846
|
-
import time as _time
|
|
3847
|
-
sentinel = slm_data_dir / ".setup-complete"
|
|
3848
|
-
slm_data_dir.mkdir(parents=True, exist_ok=True)
|
|
3849
|
-
sentinel.write_text(
|
|
3850
|
-
f"setup_completed={_time.strftime('%Y-%m-%dT%H:%M:%S')}\n"
|
|
3851
|
-
f"python={sys.executable}\n"
|
|
3852
|
-
f"platform={platform.system()}\n"
|
|
3853
|
-
f"version={platform.python_version()}\n"
|
|
3854
|
-
)
|
|
3855
|
-
except Exception:
|
|
3856
|
-
pass # best-effort — sentinel is advisory only
|
|
3857
|
-
|
|
3858
|
-
# Step 3: install hooks if ~/.claude exists.
|
|
3859
|
-
try:
|
|
3860
|
-
claude_dir = Path.home() / ".claude"
|
|
3861
|
-
if claude_dir.exists():
|
|
3862
|
-
from superlocalmemory.hooks.claude_code_hooks import install_hooks
|
|
3863
|
-
install_hooks(include_gate=getattr(args, "gate", False))
|
|
3864
|
-
except Exception:
|
|
3865
|
-
pass # best-effort
|
|
3866
|
-
|
|
3867
|
-
# Step 4: warmup best-effort (don't block if models not present).
|
|
3868
|
-
# Skipped in --auto to keep startup fast for CI; user can run slm warmup.
|
|
3869
|
-
|
|
3870
|
-
print("[OK] slm init --auto: setup complete (mode A, non-interactive)", file=sys.stderr)
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
def cmd_init(args: Namespace) -> None:
|
|
3874
|
-
"""One-command setup: mode + hooks + IDE connect + warmup."""
|
|
3875
|
-
from superlocalmemory.cli._lazy_init import slm_home
|
|
3876
|
-
from superlocalmemory.core.config import SLMConfig
|
|
3877
|
-
|
|
3878
|
-
force = getattr(args, "force", False)
|
|
3879
|
-
auto = getattr(args, "auto", False)
|
|
3880
|
-
|
|
3881
|
-
slm_data_dir = slm_home()
|
|
3882
|
-
config_exists = (slm_data_dir / "config.json").exists()
|
|
3883
|
-
|
|
3884
|
-
# --auto branch — fully non-interactive, no TTY required.
|
|
3885
|
-
if auto:
|
|
3886
|
-
os.environ["SLM_NON_INTERACTIVE"] = "1"
|
|
3887
|
-
_cmd_init_auto(args, slm_data_dir, config_exists, force)
|
|
3888
|
-
return
|
|
3889
|
-
|
|
3890
|
-
print()
|
|
3891
|
-
print("SuperLocalMemory — One-Time Setup")
|
|
3892
|
-
print("=" * 40)
|
|
3893
|
-
|
|
3894
|
-
# Step 1: Mode selection (interactive)
|
|
3895
|
-
if force or not config_exists:
|
|
3896
|
-
print()
|
|
3897
|
-
from superlocalmemory.cli.setup_wizard import run_wizard
|
|
3898
|
-
run_wizard()
|
|
3899
|
-
else:
|
|
3900
|
-
config = SLMConfig.load()
|
|
3901
|
-
print(f"\n Already configured: Mode {config.mode.value.upper()}")
|
|
3902
|
-
print(f" Profile: {config.active_profile}")
|
|
3903
|
-
|
|
3904
|
-
# Step 2: Install hooks (gate always OFF by default)
|
|
3905
|
-
print()
|
|
3906
|
-
print("Installing Claude Code hooks...")
|
|
3907
|
-
from superlocalmemory.hooks.claude_code_hooks import install_hooks, check_status
|
|
3908
|
-
|
|
3909
|
-
status = check_status()
|
|
3910
|
-
|
|
3911
|
-
if status["installed"] and not force:
|
|
3912
|
-
if status["needs_upgrade"]:
|
|
3913
|
-
from superlocalmemory.hooks.claude_code_hooks import upgrade_hooks
|
|
3914
|
-
result = upgrade_hooks()
|
|
3915
|
-
if result.get("upgraded"):
|
|
3916
|
-
print(f" Hooks upgraded: {result['from_version']} -> {result['to_version']}")
|
|
3917
|
-
else:
|
|
3918
|
-
print(f" Upgrade issue: {result.get('reason', result.get('errors', ''))}")
|
|
3919
|
-
else:
|
|
3920
|
-
print(f" Hooks already installed (v{status['version']})")
|
|
3921
|
-
else:
|
|
3922
|
-
result = install_hooks(include_gate=False)
|
|
3923
|
-
if result["success"]:
|
|
3924
|
-
print(f" Hooks installed: {', '.join(result['hooks_added'])}")
|
|
3925
|
-
print(" SLM: Hooks installed into Claude Code (slm hooks remove to undo)")
|
|
3926
|
-
else:
|
|
3927
|
-
print(f" Hook install failed: {result['errors']}")
|
|
3928
|
-
|
|
3929
|
-
# Step 3: IDE connection
|
|
3930
|
-
print()
|
|
3931
|
-
print("Detecting IDEs...")
|
|
3932
|
-
try:
|
|
3933
|
-
from superlocalmemory.hooks.ide_connector import IDEConnector
|
|
3934
|
-
connector = IDEConnector()
|
|
3935
|
-
results = connector.connect_all()
|
|
3936
|
-
for ide_id, ide_status in results.items():
|
|
3937
|
-
print(f" {ide_id}: {ide_status}")
|
|
3938
|
-
except Exception as exc:
|
|
3939
|
-
print(f" IDE detection skipped: {exc}")
|
|
3940
|
-
|
|
3941
|
-
# Step 4: Warmup (embedding model)
|
|
3942
|
-
print()
|
|
3943
|
-
print("Checking embedding model...")
|
|
3944
|
-
try:
|
|
3945
|
-
from superlocalmemory.core.config import SLMConfig as _Cfg
|
|
3946
|
-
cfg = _Cfg.load()
|
|
3947
|
-
model_name = cfg.embedding.model_name
|
|
3948
|
-
print(f" Model: {model_name}")
|
|
3949
|
-
# Quick check: try creating embedding service (auto-downloads if needed)
|
|
3950
|
-
from superlocalmemory.core.embeddings import EmbeddingService
|
|
3951
|
-
svc = EmbeddingService(cfg.embedding)
|
|
3952
|
-
test_result = svc.embed_text("test")
|
|
3953
|
-
if test_result is not None and len(test_result) > 0:
|
|
3954
|
-
print(" Status: ready")
|
|
3955
|
-
else:
|
|
3956
|
-
print(" Status: model not available (run: slm warmup)")
|
|
3957
|
-
except Exception as exc:
|
|
3958
|
-
print(f" Warmup skipped: {exc}")
|
|
3959
|
-
print(" Run 'slm warmup' later to download the embedding model.")
|
|
3960
|
-
|
|
3961
|
-
# Done
|
|
3962
|
-
print()
|
|
3963
|
-
print("=" * 40)
|
|
3964
|
-
print("SLM is active. Your AI now remembers you.")
|
|
3965
|
-
print()
|
|
3966
|
-
print("What happens next:")
|
|
3967
|
-
print(" - Open Claude Code in any project")
|
|
3968
|
-
print(" - SLM auto-injects your memory context")
|
|
3969
|
-
print(" - Decisions, bugs, preferences are captured automatically")
|
|
3970
|
-
print(" - Session summaries saved when you close")
|
|
3971
|
-
print()
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
def cmd_hooks(args: Namespace) -> None:
|
|
3975
|
-
"""Manage additive Claude Code or Codex memory lifecycle hooks."""
|
|
3976
|
-
from superlocalmemory.hooks.claude_code_hooks import (
|
|
3977
|
-
install_hooks, remove_hooks, check_status,
|
|
3978
|
-
)
|
|
3979
|
-
|
|
3980
|
-
action = getattr(args, "action", "status")
|
|
3981
|
-
agent = getattr(args, "agent", "claude")
|
|
3982
|
-
dry_run = getattr(args, "dry_run", False)
|
|
3983
|
-
if agent == "codex":
|
|
3984
|
-
from superlocalmemory.hooks.codex_hooks import (
|
|
3985
|
-
install_hooks as install_codex_hooks,
|
|
3986
|
-
remove_hooks as remove_codex_hooks,
|
|
3987
|
-
check_status as check_codex_hooks,
|
|
3988
|
-
)
|
|
3989
|
-
if action == "install":
|
|
3990
|
-
result = install_codex_hooks(dry_run=dry_run)
|
|
3991
|
-
if result["success"]:
|
|
3992
|
-
prefix = "would be installed" if dry_run else "installed"
|
|
3993
|
-
print(f"SLM hooks {prefix} in Codex: {result['path']}")
|
|
3994
|
-
if result.get("hooks_added"):
|
|
3995
|
-
print(f" Hook types: {', '.join(result['hooks_added'])}")
|
|
3996
|
-
print(" Review and trust the new hooks in Codex with /hooks.")
|
|
3997
|
-
else:
|
|
3998
|
-
print(f"Installation failed: {result['errors']}")
|
|
3999
|
-
return
|
|
4000
|
-
if action == "remove":
|
|
4001
|
-
result = remove_codex_hooks(dry_run=dry_run)
|
|
4002
|
-
if result["success"]:
|
|
4003
|
-
prefix = "would be removed" if dry_run else "removed"
|
|
4004
|
-
print(f"SLM hooks {prefix} from Codex: {result['path']}")
|
|
4005
|
-
else:
|
|
4006
|
-
print(f"Removal failed: {result['errors']}")
|
|
4007
|
-
return
|
|
4008
|
-
result = check_codex_hooks()
|
|
4009
|
-
if result["installed"] is None:
|
|
4010
|
-
print(f"SLM Codex hooks: INDETERMINATE ({result['error']})")
|
|
4011
|
-
elif result["installed"]:
|
|
4012
|
-
print("SLM Codex hooks: INSTALLED")
|
|
4013
|
-
print(f" Hook types: {', '.join(result['hook_types'])}")
|
|
4014
|
-
else:
|
|
4015
|
-
print("SLM Codex hooks: NOT INSTALLED")
|
|
4016
|
-
print(" Run: slm hooks install --agent codex")
|
|
4017
|
-
return
|
|
4018
|
-
# Gate is OFF by default. --gate opts in (for brave users).
|
|
4019
|
-
include_gate = getattr(args, "gate", False)
|
|
4020
|
-
|
|
4021
|
-
if action == "install":
|
|
4022
|
-
result = install_hooks(include_gate=include_gate)
|
|
4023
|
-
if result["success"]:
|
|
4024
|
-
print("SLM hooks installed in Claude Code.")
|
|
4025
|
-
print(f" Hook types: {', '.join(result['hooks_added'])}")
|
|
4026
|
-
if include_gate:
|
|
4027
|
-
print(" Gate: ON (enforces session_init — experimental)")
|
|
4028
|
-
print(" SLM: Hooks installed into Claude Code (slm hooks remove to undo)")
|
|
4029
|
-
|
|
4030
|
-
else:
|
|
4031
|
-
print(f"Installation failed: {result['errors']}")
|
|
4032
|
-
elif action == "remove":
|
|
4033
|
-
result = remove_hooks()
|
|
4034
|
-
if result["success"]:
|
|
4035
|
-
print("SLM hooks removed from Claude Code.")
|
|
4036
|
-
else:
|
|
4037
|
-
print(f"Removal failed: {result['errors']}")
|
|
4038
|
-
else:
|
|
4039
|
-
result = check_status()
|
|
4040
|
-
if result["installed"]:
|
|
4041
|
-
print(f"SLM hooks: INSTALLED (v{result['version']})")
|
|
4042
|
-
print(f" Hook types: {', '.join(result['hook_types'])}")
|
|
4043
|
-
print(f" Gate: {'ON' if result['gate_enabled'] else 'OFF'}")
|
|
4044
|
-
if result["needs_upgrade"]:
|
|
4045
|
-
print(f" Update available: {result['version']} -> {result['latest_version']}")
|
|
4046
|
-
print(" Run: slm hooks install")
|
|
4047
|
-
else:
|
|
4048
|
-
print("SLM hooks: NOT INSTALLED")
|
|
4049
|
-
print(" Run: slm hooks install")
|
|
4050
|
-
print(" Or: slm init (full setup)")
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
def cmd_codex(args: Namespace) -> None:
|
|
4054
|
-
"""Manage explicit, SLM-owned Codex skills, agents, and lifecycle hooks."""
|
|
4055
|
-
from superlocalmemory.hooks.codex_assets import install_assets, remove_assets, status_assets
|
|
4056
|
-
from superlocalmemory.hooks.codex_hooks import install_hooks, remove_hooks, check_status
|
|
4057
|
-
|
|
4058
|
-
action, dry_run = getattr(args, "action", "status"), getattr(args, "dry_run", False)
|
|
4059
|
-
if action == "install":
|
|
4060
|
-
assets, hooks = install_assets(dry_run=dry_run), install_hooks(dry_run=dry_run)
|
|
4061
|
-
if assets.get("success") and hooks.get("success"):
|
|
4062
|
-
print(f"SLM Codex add-ons {'would be installed' if dry_run else 'installed'}: 7 skills, 2 subagents, 4 lifecycle hooks.")
|
|
4063
|
-
print("MCP wiring remains explicit: run `slm connect codex` if it is not already configured.")
|
|
4064
|
-
print("Review and trust newly installed hooks in Codex with /hooks.")
|
|
4065
|
-
else:
|
|
4066
|
-
print(f"Codex integration failed: {assets.get('errors', []) + hooks.get('errors', [])}")
|
|
4067
|
-
return
|
|
4068
|
-
if action == "remove":
|
|
4069
|
-
assets, hooks = remove_assets(dry_run=dry_run), remove_hooks(dry_run=dry_run)
|
|
4070
|
-
if assets.get("success") and hooks.get("success"):
|
|
4071
|
-
print("SLM-owned Codex add-ons removed; your MCP and non-SLM settings were left intact.")
|
|
4072
|
-
else:
|
|
4073
|
-
print(f"Codex removal failed: {assets.get('errors', []) + hooks.get('errors', [])}")
|
|
4074
|
-
return
|
|
4075
|
-
assets, hooks = status_assets(), check_status()
|
|
4076
|
-
print(f"SLM Codex add-ons: {'INSTALLED' if assets['installed'] and hooks['installed'] else 'NOT INSTALLED'}")
|
|
4077
|
-
print(f" Skills: {len(assets['skills'])}/7; subagents: {len(assets['agents'])}/2")
|
|
4078
|
-
print(f" Hooks: {', '.join(hooks.get('hook_types', [])) or 'none'}")
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
def cmd_session_context(args: Namespace) -> None:
|
|
4082
|
-
"""Print session context (for hook scripts and piping).
|
|
4083
|
-
|
|
4084
|
-
Uses a FAST PATH that queries SQLite directly (no engine/Ollama needed).
|
|
4085
|
-
This ensures the SessionStart hook completes within its 15s timeout even
|
|
4086
|
-
when Ollama requires a 60s+ cold start. The fast path returns:
|
|
4087
|
-
- Core Memory blocks (always-on context)
|
|
4088
|
-
- Recent high-importance memories (last N days)
|
|
4089
|
-
- Session summary from last session
|
|
4090
|
-
Falls back to the full engine path only if --full is passed explicitly.
|
|
4091
|
-
|
|
4092
|
-
v3.4.65: uses shared injection formatter (render_context) for identical
|
|
4093
|
-
output across MCP and CLI surfaces. --json flag returns structured JSON.
|
|
4094
|
-
"""
|
|
4095
|
-
import sqlite3
|
|
4096
|
-
from superlocalmemory.core.config import SLMConfig
|
|
4097
|
-
|
|
4098
|
-
use_json = getattr(args, "json", False)
|
|
4099
|
-
use_full = getattr(args, "full", False)
|
|
4100
|
-
|
|
4101
|
-
if use_full:
|
|
4102
|
-
# Full engine path (slow, requires Ollama) — for explicit CLI use
|
|
4103
|
-
try:
|
|
4104
|
-
from superlocalmemory.hooks.auto_recall import AutoRecall
|
|
4105
|
-
from superlocalmemory.core.engine import MemoryEngine
|
|
4106
|
-
config = SLMConfig.load()
|
|
4107
|
-
engine = MemoryEngine(config)
|
|
4108
|
-
engine.initialize()
|
|
4109
|
-
auto = AutoRecall(
|
|
4110
|
-
engine=engine,
|
|
4111
|
-
config={"enabled": True, "max_memories_injected": 10, "relevance_threshold": 0.3},
|
|
4112
|
-
)
|
|
4113
|
-
context = auto.get_session_context(
|
|
4114
|
-
query=getattr(args, "query", "") or "recent decisions and important context",
|
|
4115
|
-
)
|
|
4116
|
-
if context:
|
|
4117
|
-
if use_json:
|
|
4118
|
-
from superlocalmemory.cli.json_output import json_print
|
|
4119
|
-
json_print("session-context", data={"context": context}, next_actions=[
|
|
4120
|
-
{"command": "slm recall --json <query>", "description": "Search memories"},
|
|
4121
|
-
])
|
|
4122
|
-
else:
|
|
4123
|
-
print(context)
|
|
4124
|
-
except Exception as exc:
|
|
4125
|
-
logger.debug("session-context (full) failed: %s", exc)
|
|
4126
|
-
return
|
|
4127
|
-
|
|
4128
|
-
# ── FAST PATH: direct SQLite, no engine, <500ms ──────────────
|
|
4129
|
-
try:
|
|
4130
|
-
from superlocalmemory.core.injection import InjectableMemory, render_context
|
|
4131
|
-
|
|
4132
|
-
config = SLMConfig.load()
|
|
4133
|
-
db_path = config.base_dir / "memory.db"
|
|
4134
|
-
if not db_path.exists():
|
|
4135
|
-
return
|
|
4136
|
-
|
|
4137
|
-
pid = config.active_profile
|
|
4138
|
-
from superlocalmemory.storage.memory_write import memory_read
|
|
4139
|
-
|
|
4140
|
-
def _read_rows(query: str, params: tuple[object, ...]) -> list[sqlite3.Row]:
|
|
4141
|
-
with memory_read(db_path) as conn:
|
|
4142
|
-
return conn.execute(query, params).fetchall()
|
|
4143
|
-
|
|
4144
|
-
# Collect facts for injection — same queries as pre-v3.4.65 but
|
|
4145
|
-
# mapped into InjectableMemory for the shared formatter.
|
|
4146
|
-
inj_mems: list[InjectableMemory] = []
|
|
4147
|
-
|
|
4148
|
-
# Core Memory blocks (compiled high-value context)
|
|
4149
|
-
try:
|
|
4150
|
-
cm_rows = _read_rows(
|
|
4151
|
-
"SELECT block_type, content FROM core_memory_blocks "
|
|
4152
|
-
"WHERE profile_id = ? ORDER BY block_type",
|
|
4153
|
-
(pid,),
|
|
4154
|
-
)
|
|
4155
|
-
for r in cm_rows:
|
|
4156
|
-
content = f"[{r['block_type']}] {r['content']}"
|
|
4157
|
-
inj_mems.append(InjectableMemory(
|
|
4158
|
-
content=content, score=1.0, fact_id="",
|
|
4159
|
-
importance=1.0, access_count=10,
|
|
4160
|
-
pinned=True,
|
|
4161
|
-
))
|
|
4162
|
-
except sqlite3.OperationalError:
|
|
4163
|
-
pass
|
|
4164
|
-
|
|
4165
|
-
# Recent important memories — age gate from --max-age-days (default 30)
|
|
4166
|
-
max_age = getattr(args, "max_age_days", 30)
|
|
4167
|
-
age_clause = (
|
|
4168
|
-
f"AND created_at >= datetime('now', '-{int(max_age)} days') "
|
|
4169
|
-
if max_age > 0 else ""
|
|
4170
|
-
)
|
|
4171
|
-
try:
|
|
4172
|
-
fact_rows = _read_rows(
|
|
4173
|
-
"SELECT fact_id, content, importance, access_count, fact_type FROM atomic_facts "
|
|
4174
|
-
"WHERE profile_id = ? "
|
|
4175
|
-
f"{age_clause}"
|
|
4176
|
-
"AND lifecycle = 'active' "
|
|
4177
|
-
"ORDER BY importance DESC, created_at DESC LIMIT 10",
|
|
4178
|
-
(pid,),
|
|
4179
|
-
)
|
|
4180
|
-
for r in fact_rows:
|
|
4181
|
-
inj_mems.append(InjectableMemory(
|
|
4182
|
-
content=r["content"],
|
|
4183
|
-
score=r["importance"] or 0.5,
|
|
4184
|
-
fact_id=r["fact_id"],
|
|
4185
|
-
importance=r["importance"] or 0.0,
|
|
4186
|
-
access_count=r["access_count"] or 0,
|
|
4187
|
-
))
|
|
4188
|
-
except sqlite3.OperationalError:
|
|
4189
|
-
pass
|
|
4190
|
-
|
|
4191
|
-
# Session markers (last session summary)
|
|
4192
|
-
try:
|
|
4193
|
-
sess_rows = _read_rows(
|
|
4194
|
-
"SELECT fact_id, content, importance, access_count FROM atomic_facts "
|
|
4195
|
-
"WHERE profile_id = ? AND content LIKE 'Session%' "
|
|
4196
|
-
"ORDER BY created_at DESC LIMIT 3",
|
|
4197
|
-
(pid,),
|
|
4198
|
-
)
|
|
4199
|
-
for r in sess_rows:
|
|
4200
|
-
inj_mems.append(InjectableMemory(
|
|
4201
|
-
content=r["content"],
|
|
4202
|
-
score=r["importance"] or 0.3,
|
|
4203
|
-
fact_id=r["fact_id"],
|
|
4204
|
-
importance=r["importance"] or 0.0,
|
|
4205
|
-
access_count=r["access_count"] or 0,
|
|
4206
|
-
))
|
|
4207
|
-
except sqlite3.OperationalError:
|
|
4208
|
-
pass
|
|
4209
|
-
|
|
4210
|
-
if not inj_mems:
|
|
4211
|
-
return
|
|
4212
|
-
|
|
4213
|
-
# V3.3 Soft prompts (auto-learned patterns) — append as high-importance
|
|
4214
|
-
try:
|
|
4215
|
-
sp_rows = _read_rows(
|
|
4216
|
-
"SELECT category, content FROM soft_prompt_templates "
|
|
4217
|
-
"WHERE profile_id = ? AND active = 1 "
|
|
4218
|
-
"ORDER BY confidence DESC LIMIT 5",
|
|
4219
|
-
(pid,),
|
|
4220
|
-
)
|
|
4221
|
-
for r in sp_rows:
|
|
4222
|
-
inj_mems.append(InjectableMemory(
|
|
4223
|
-
content=f"[{r['category']}] {r['content']}",
|
|
4224
|
-
score=0.7, fact_id="",
|
|
4225
|
-
importance=0.7, access_count=5,
|
|
4226
|
-
))
|
|
4227
|
-
except Exception:
|
|
4228
|
-
pass
|
|
4229
|
-
|
|
4230
|
-
cfg_inj = getattr(config, "injection", None)
|
|
4231
|
-
context = render_context(inj_mems, mode=config.mode.value.upper(), cfg=cfg_inj, wrap=True)
|
|
4232
|
-
if context:
|
|
4233
|
-
if use_json:
|
|
4234
|
-
from superlocalmemory.cli.json_output import json_print
|
|
4235
|
-
json_print("session-context", data={
|
|
4236
|
-
"context": context,
|
|
4237
|
-
"memory_count": len(inj_mems),
|
|
4238
|
-
"mode": config.mode.value,
|
|
4239
|
-
}, next_actions=[
|
|
4240
|
-
{"command": "slm recall --json <query>", "description": "Search memories"},
|
|
4241
|
-
])
|
|
4242
|
-
else:
|
|
4243
|
-
print(context)
|
|
4244
|
-
|
|
4245
|
-
except Exception as exc:
|
|
4246
|
-
logger.debug("session-context (fast) failed: %s", exc)
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
def cmd_brain(args: Namespace) -> None:
|
|
4250
|
-
"""Read the portable, profile-scoped Living Brain truth snapshot.
|
|
4251
|
-
|
|
4252
|
-
This command deliberately opens no memory engine and starts no daemon.
|
|
4253
|
-
BrainTruth uses short read-only connections to keep the status view out of
|
|
4254
|
-
recall and writer domains.
|
|
4255
|
-
"""
|
|
4256
|
-
from superlocalmemory.brain.truth import BrainTruthService
|
|
4257
|
-
from superlocalmemory.core.config import SLMConfig
|
|
4258
|
-
from superlocalmemory.infra.data_root import state_path
|
|
4259
|
-
|
|
4260
|
-
config = SLMConfig.load()
|
|
4261
|
-
profile_id = config.active_profile
|
|
4262
|
-
data = BrainTruthService(
|
|
4263
|
-
memory_db_path=state_path("memory.db"),
|
|
4264
|
-
learning_db_path=state_path("learning.db"),
|
|
4265
|
-
).snapshot(profile_id)
|
|
4266
|
-
if getattr(args, "json", False):
|
|
4267
|
-
from superlocalmemory.cli.json_output import json_print
|
|
4268
|
-
|
|
4269
|
-
json_print("brain", data=data, next_actions=[
|
|
4270
|
-
{"command": "slm dashboard", "description": "Open the Living Brain dashboard"},
|
|
4271
|
-
])
|
|
4272
|
-
return
|
|
4273
|
-
memory = data["memory_activity"]
|
|
4274
|
-
feedback = data["feedback"]
|
|
4275
|
-
evidence = data["agent_experience"]
|
|
4276
|
-
external = data["external_evidence"]
|
|
4277
|
-
corrections = data["correction_quality"]
|
|
4278
|
-
print("SuperLocalMemory Living Brain")
|
|
4279
|
-
print(f" Profile: {profile_id}")
|
|
4280
|
-
print(f" Stored facts: {memory['facts_total']}")
|
|
4281
|
-
print(f" Feedback signals: {feedback['signals_total']}")
|
|
4282
|
-
print(f" Claimed agent experiences: {evidence['claimed_experiences_total']}")
|
|
4283
|
-
print(
|
|
4284
|
-
" Independently verified experiences: "
|
|
4285
|
-
f"{evidence['independently_verified_experiences_total']}"
|
|
4286
|
-
)
|
|
4287
|
-
print(f" Cognitive turns: {evidence['cognitive_turns_total']}")
|
|
4288
|
-
if evidence["cognitive_turns_by_state"]:
|
|
4289
|
-
states = ", ".join(
|
|
4290
|
-
f"{state}: {count}"
|
|
4291
|
-
for state, count in sorted(evidence["cognitive_turns_by_state"].items())
|
|
4292
|
-
)
|
|
4293
|
-
print(f" Turn states: {states}")
|
|
4294
|
-
print(f" Bounded Loop observations: {external['receipts_total']}")
|
|
4295
|
-
print(f" Correction cases: {corrections['cases_total']}")
|
|
4296
|
-
print(
|
|
4297
|
-
" Retrieval control: observation only; does not change recall, "
|
|
4298
|
-
"ranking, or model routing"
|
|
4299
|
-
)
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
def cmd_observe(args: Namespace) -> None:
|
|
4303
|
-
"""Evaluate and auto-capture content from stdin or argument.
|
|
4304
|
-
|
|
4305
|
-
V3.3.28: Routes through daemon to prevent embedding worker memory blast.
|
|
4306
|
-
Previously each `slm observe` spawned its own MemoryEngine + embedding
|
|
4307
|
-
worker (~1.4 GB each). With 20 parallel edits = 28+ GB = system crash.
|
|
4308
|
-
Now uses the daemon's singleton engine (1 worker total).
|
|
4309
|
-
"""
|
|
4310
|
-
import sys
|
|
4311
|
-
|
|
4312
|
-
content = getattr(args, "content", "") or ""
|
|
4313
|
-
if not content and not sys.stdin.isatty():
|
|
4314
|
-
content = sys.stdin.read().strip()
|
|
4315
|
-
|
|
4316
|
-
if not content:
|
|
4317
|
-
print("No content to observe.")
|
|
4318
|
-
return
|
|
4319
|
-
|
|
4320
|
-
from superlocalmemory.core.admission import gate_cli_mutation
|
|
4321
|
-
from superlocalmemory.core.operation_request import OperationKind
|
|
4322
|
-
gate_cli_mutation(OperationKind.REMEMBER)
|
|
4323
|
-
|
|
4324
|
-
# V3.3.28: Route through daemon (singleton engine, single embedding worker).
|
|
4325
|
-
# This is the P0 fix for the memory blast incident of April 7, 2026.
|
|
4326
|
-
from superlocalmemory.cli.daemon import DaemonRefused
|
|
4327
|
-
try:
|
|
4328
|
-
from superlocalmemory.cli.daemon import is_daemon_running, daemon_request, ensure_daemon
|
|
4329
|
-
if is_daemon_running() or ensure_daemon():
|
|
4330
|
-
result = daemon_request("POST", "/observe", {"content": content})
|
|
4331
|
-
if result is not None:
|
|
4332
|
-
if result.get("captured"):
|
|
4333
|
-
cat = result.get("category", "unknown")
|
|
4334
|
-
conf = result.get("confidence", 0)
|
|
4335
|
-
print(f"Auto-captured: {cat} (confidence: {conf:.2f}) (via daemon)")
|
|
4336
|
-
else:
|
|
4337
|
-
reason = result.get("reason", "no patterns matched")
|
|
4338
|
-
print(f"Not captured: {reason}")
|
|
4339
|
-
return
|
|
4340
|
-
except DaemonRefused as refusal:
|
|
4341
|
-
# The workspace declined this write. The fallback below exists for a
|
|
4342
|
-
# daemon that is not running -- not for one that answered no.
|
|
4343
|
-
print(
|
|
4344
|
-
f"[slm] Not captured: {refusal}. "
|
|
4345
|
-
"This workspace requires authentication; set SLM_USER_SESSION or "
|
|
4346
|
-
"log in, then try again.",
|
|
4347
|
-
flush=True,
|
|
4348
|
-
)
|
|
4349
|
-
sys.exit(1)
|
|
4350
|
-
except Exception:
|
|
4351
|
-
pass # Fall through to direct engine
|
|
4352
|
-
|
|
4353
|
-
# Fallback: direct engine (only if daemon unavailable).
|
|
4354
|
-
# Acquires a system-wide file lock to prevent concurrent worker spawns.
|
|
4355
|
-
try:
|
|
4356
|
-
from superlocalmemory.hooks.auto_capture import AutoCapture
|
|
4357
|
-
from superlocalmemory.core.config import SLMConfig
|
|
4358
|
-
from superlocalmemory.core.engine import MemoryEngine
|
|
4359
|
-
from superlocalmemory.core.embeddings import acquire_embedding_lock
|
|
4360
|
-
|
|
4361
|
-
if not acquire_embedding_lock():
|
|
4362
|
-
logger.debug("observe: another embedding worker active, skipping")
|
|
4363
|
-
print("Not captured: system busy (another embedding in progress)")
|
|
4364
|
-
return
|
|
4365
|
-
|
|
4366
|
-
try:
|
|
4367
|
-
config = SLMConfig.load()
|
|
4368
|
-
engine = MemoryEngine(config)
|
|
4369
|
-
engine.initialize()
|
|
4370
|
-
|
|
4371
|
-
from superlocalmemory.core.engine_ingestion import (
|
|
4372
|
-
canonical_store_fn,
|
|
4373
|
-
local_trusted_actor_id,
|
|
4374
|
-
)
|
|
4375
|
-
auto = AutoCapture(store_fn=canonical_store_fn(
|
|
4376
|
-
engine,
|
|
4377
|
-
source_type="cli-observe",
|
|
4378
|
-
trusted_actor_id=local_trusted_actor_id("cli"),
|
|
4379
|
-
))
|
|
4380
|
-
decision = auto.evaluate(content)
|
|
4381
|
-
|
|
4382
|
-
if decision.capture:
|
|
4383
|
-
stored = auto.capture(content, category=decision.category)
|
|
4384
|
-
if stored:
|
|
4385
|
-
print(f"Auto-captured: {decision.category} (confidence: {decision.confidence:.2f})")
|
|
4386
|
-
else:
|
|
4387
|
-
print(f"Detected {decision.category} but store failed.")
|
|
4388
|
-
else:
|
|
4389
|
-
print(f"Not captured: {decision.reason}")
|
|
4390
|
-
finally:
|
|
4391
|
-
from superlocalmemory.core.embeddings import release_embedding_lock
|
|
4392
|
-
release_embedding_lock()
|
|
4393
|
-
except Exception as exc:
|
|
4394
|
-
logger.debug("observe failed: %s", exc)
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
# -- V3.3 Commands -----------------------------------------------------------
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
def cmd_decay(args: Namespace) -> None:
|
|
4401
|
-
"""Run Ebbinghaus forgetting decay cycle."""
|
|
4402
|
-
from superlocalmemory.core.admission import gate_cli_mutation
|
|
4403
|
-
from superlocalmemory.core.operation_request import OperationKind
|
|
4404
|
-
gate_cli_mutation(OperationKind.CONSOLIDATE)
|
|
4405
|
-
from superlocalmemory.core.config import SLMConfig
|
|
4406
|
-
from superlocalmemory.core.engine import MemoryEngine
|
|
4407
|
-
|
|
4408
|
-
use_json = getattr(args, "json", False)
|
|
4409
|
-
dry_run = getattr(args, "dry_run", True)
|
|
4410
|
-
profile = getattr(args, "profile", "")
|
|
4411
|
-
|
|
4412
|
-
try:
|
|
4413
|
-
config = SLMConfig.load()
|
|
4414
|
-
engine = MemoryEngine(config)
|
|
4415
|
-
engine.initialize()
|
|
4416
|
-
pid = profile or engine.profile_id
|
|
4417
|
-
|
|
4418
|
-
from superlocalmemory.math.ebbinghaus import EbbinghausCurve
|
|
4419
|
-
from superlocalmemory.learning.forgetting_scheduler import (
|
|
4420
|
-
ForgettingScheduler,
|
|
4421
|
-
)
|
|
4422
|
-
|
|
4423
|
-
ebbinghaus = EbbinghausCurve(config.forgetting)
|
|
4424
|
-
scheduler = ForgettingScheduler(
|
|
4425
|
-
engine._db, ebbinghaus, config.forgetting,
|
|
4426
|
-
)
|
|
4427
|
-
result = scheduler.run_decay_cycle(pid, force=True, dry_run=dry_run)
|
|
4428
|
-
except Exception as exc:
|
|
4429
|
-
if use_json:
|
|
4430
|
-
from superlocalmemory.cli.json_output import json_print
|
|
4431
|
-
json_print("decay", error={"code": "DECAY_ERROR", "message": str(exc)})
|
|
4432
|
-
sys.exit(1)
|
|
4433
|
-
raise
|
|
4434
|
-
|
|
4435
|
-
if use_json:
|
|
4436
|
-
from superlocalmemory.cli.json_output import json_print
|
|
4437
|
-
json_print("decay", data={"dry_run": dry_run, **result},
|
|
4438
|
-
next_actions=[
|
|
4439
|
-
{"command": "slm decay --execute --json", "description": "Apply transitions"},
|
|
4440
|
-
{"command": "slm status --json", "description": "Check system status"},
|
|
4441
|
-
])
|
|
4442
|
-
return
|
|
4443
|
-
|
|
4444
|
-
if result.get("skipped"):
|
|
4445
|
-
print(f"Skipped: {result.get('reason', 'unknown')}")
|
|
4446
|
-
return
|
|
4447
|
-
|
|
4448
|
-
total = result.get("total", 0)
|
|
4449
|
-
print(f"Decay cycle complete (dry_run={dry_run})")
|
|
4450
|
-
print(f" Total facts: {total}")
|
|
4451
|
-
print(f" Active: {result.get('active', 0)}")
|
|
4452
|
-
print(f" Warm: {result.get('warm', 0)}")
|
|
4453
|
-
print(f" Cold: {result.get('cold', 0)}")
|
|
4454
|
-
print(f" Archive: {result.get('archive', 0)}")
|
|
4455
|
-
print(f" Forgotten: {result.get('forgotten', 0)}")
|
|
4456
|
-
print(f" Transitions: {result.get('transitions', 0)}")
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
def cmd_quantize(args: Namespace) -> None:
|
|
4460
|
-
"""Run EAP embedding quantization cycle."""
|
|
4461
|
-
from superlocalmemory.core.admission import gate_cli_mutation
|
|
4462
|
-
from superlocalmemory.core.operation_request import OperationKind
|
|
4463
|
-
gate_cli_mutation(OperationKind.CONSOLIDATE)
|
|
4464
|
-
from superlocalmemory.core.config import SLMConfig
|
|
4465
|
-
from superlocalmemory.core.engine import MemoryEngine
|
|
4466
|
-
|
|
4467
|
-
use_json = getattr(args, "json", False)
|
|
4468
|
-
dry_run = getattr(args, "dry_run", True)
|
|
4469
|
-
profile = getattr(args, "profile", "")
|
|
4470
|
-
|
|
4471
|
-
try:
|
|
4472
|
-
config = SLMConfig.load()
|
|
4473
|
-
engine = MemoryEngine(config)
|
|
4474
|
-
engine.initialize()
|
|
4475
|
-
pid = profile or engine.profile_id
|
|
4476
|
-
|
|
4477
|
-
from superlocalmemory.math.ebbinghaus import EbbinghausCurve
|
|
4478
|
-
from superlocalmemory.dynamics.eap_scheduler import EAPScheduler
|
|
4479
|
-
from superlocalmemory.storage.quantized_store import (
|
|
4480
|
-
QuantizedEmbeddingStore,
|
|
4481
|
-
)
|
|
4482
|
-
|
|
4483
|
-
from superlocalmemory.math.polar_quant import PolarQuantEncoder
|
|
4484
|
-
from superlocalmemory.math.qjl import QJLEncoder
|
|
4485
|
-
|
|
4486
|
-
ebbinghaus = EbbinghausCurve(config.forgetting)
|
|
4487
|
-
polar = PolarQuantEncoder(config.quantization.polar)
|
|
4488
|
-
qjl = QJLEncoder(config.quantization.qjl)
|
|
4489
|
-
qstore = QuantizedEmbeddingStore(
|
|
4490
|
-
engine._db, polar, qjl, config.quantization,
|
|
4491
|
-
)
|
|
4492
|
-
scheduler = EAPScheduler(
|
|
4493
|
-
engine._db, ebbinghaus, qstore, config.quantization,
|
|
4494
|
-
)
|
|
4495
|
-
result = scheduler.run_eap_cycle(pid, dry_run=dry_run)
|
|
4496
|
-
except Exception as exc:
|
|
4497
|
-
if use_json:
|
|
4498
|
-
from superlocalmemory.cli.json_output import json_print
|
|
4499
|
-
json_print("quantize", error={"code": "EAP_ERROR", "message": str(exc)})
|
|
4500
|
-
sys.exit(1)
|
|
4501
|
-
raise
|
|
4502
|
-
|
|
4503
|
-
if use_json:
|
|
4504
|
-
from superlocalmemory.cli.json_output import json_print
|
|
4505
|
-
json_print("quantize", data={"dry_run": dry_run, **result},
|
|
4506
|
-
next_actions=[
|
|
4507
|
-
{"command": "slm quantize --execute --json", "description": "Apply changes"},
|
|
4508
|
-
{"command": "slm status --json", "description": "Check status"},
|
|
4509
|
-
])
|
|
4510
|
-
return
|
|
4511
|
-
|
|
4512
|
-
print(f"EAP quantization cycle (dry_run={dry_run})")
|
|
4513
|
-
print(f" Total: {result.get('total', 0)}")
|
|
4514
|
-
print(f" Downgrades: {result.get('downgrades', 0)}")
|
|
4515
|
-
print(f" Upgrades: {result.get('upgrades', 0)}")
|
|
4516
|
-
print(f" Skipped: {result.get('skipped', 0)}")
|
|
4517
|
-
print(f" Errors: {result.get('errors', 0)}")
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
def cmd_consolidate(args: Namespace) -> None:
|
|
4521
|
-
"""Run cognitive consolidation pipeline."""
|
|
4522
|
-
from superlocalmemory.core.admission import gate_cli_mutation
|
|
4523
|
-
from superlocalmemory.core.operation_request import OperationKind
|
|
4524
|
-
gate_cli_mutation(OperationKind.CONSOLIDATE)
|
|
4525
|
-
|
|
4526
|
-
from superlocalmemory.core.config import SLMConfig
|
|
4527
|
-
from superlocalmemory.core.engine import MemoryEngine
|
|
4528
|
-
|
|
4529
|
-
use_json = getattr(args, "json", False)
|
|
4530
|
-
cognitive = getattr(args, "cognitive", False)
|
|
4531
|
-
dry_run = getattr(args, "dry_run", False)
|
|
4532
|
-
profile = getattr(args, "profile", "")
|
|
4533
|
-
|
|
4534
|
-
if not cognitive:
|
|
4535
|
-
if use_json:
|
|
4536
|
-
from superlocalmemory.cli.json_output import json_print
|
|
4537
|
-
json_print("consolidate", error={
|
|
4538
|
-
"code": "MISSING_FLAG",
|
|
4539
|
-
"message": "Use --cognitive to run CCQ pipeline",
|
|
4540
|
-
})
|
|
4541
|
-
sys.exit(1)
|
|
4542
|
-
print("Use --cognitive to run CCQ consolidation pipeline.")
|
|
4543
|
-
print(" slm consolidate --cognitive")
|
|
4544
|
-
return
|
|
4545
|
-
|
|
4546
|
-
try:
|
|
4547
|
-
config = SLMConfig.load()
|
|
4548
|
-
engine = MemoryEngine(config)
|
|
4549
|
-
engine.initialize()
|
|
4550
|
-
pid = profile or engine.profile_id
|
|
4551
|
-
|
|
4552
|
-
from superlocalmemory.encoding.cognitive_consolidator import (
|
|
4553
|
-
CognitiveConsolidator,
|
|
4554
|
-
)
|
|
4555
|
-
|
|
4556
|
-
consolidator = CognitiveConsolidator(db=engine._db)
|
|
4557
|
-
result = consolidator.run_pipeline(pid, dry_run=dry_run)
|
|
4558
|
-
except Exception as exc:
|
|
4559
|
-
if use_json:
|
|
4560
|
-
from superlocalmemory.cli.json_output import json_print
|
|
4561
|
-
json_print("consolidate", error={
|
|
4562
|
-
"code": "CCQ_ERROR", "message": str(exc),
|
|
4563
|
-
})
|
|
4564
|
-
sys.exit(1)
|
|
4565
|
-
raise
|
|
4566
|
-
|
|
4567
|
-
if use_json:
|
|
4568
|
-
from superlocalmemory.cli.json_output import json_print
|
|
4569
|
-
json_print("consolidate", data={
|
|
4570
|
-
"clusters_processed": result.clusters_processed,
|
|
4571
|
-
"blocks_created": result.blocks_created,
|
|
4572
|
-
"facts_archived": result.facts_archived,
|
|
4573
|
-
"compression_ratio": round(result.compression_ratio, 3),
|
|
4574
|
-
}, next_actions=[
|
|
4575
|
-
{"command": "slm list --json", "description": "List recent memories"},
|
|
4576
|
-
{"command": "slm status --json", "description": "Check status"},
|
|
4577
|
-
])
|
|
4578
|
-
return
|
|
4579
|
-
|
|
4580
|
-
print("CCQ Cognitive Consolidation")
|
|
4581
|
-
print(f" Clusters processed: {result.clusters_processed}")
|
|
4582
|
-
print(f" Blocks created: {result.blocks_created}")
|
|
4583
|
-
print(f" Facts archived: {result.facts_archived}")
|
|
4584
|
-
print(f" Compression ratio: {result.compression_ratio:.3f}")
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
def cmd_soft_prompts(args: Namespace) -> None:
|
|
4588
|
-
"""List active soft prompts (auto-learned user patterns)."""
|
|
4589
|
-
from superlocalmemory.core.config import SLMConfig
|
|
4590
|
-
from superlocalmemory.core.engine import MemoryEngine
|
|
4591
|
-
|
|
4592
|
-
use_json = getattr(args, "json", False)
|
|
4593
|
-
profile = getattr(args, "profile", "")
|
|
4594
|
-
|
|
4595
|
-
try:
|
|
4596
|
-
config = SLMConfig.load()
|
|
4597
|
-
engine = MemoryEngine(config)
|
|
4598
|
-
engine.initialize()
|
|
4599
|
-
pid = profile or engine.profile_id
|
|
4600
|
-
|
|
4601
|
-
rows = engine._db.execute(
|
|
4602
|
-
"SELECT prompt_id, category, content, confidence, "
|
|
4603
|
-
" effectiveness, token_count, version, created_at "
|
|
4604
|
-
"FROM soft_prompt_templates "
|
|
4605
|
-
"WHERE profile_id = ? AND active = 1 "
|
|
4606
|
-
"ORDER BY confidence DESC",
|
|
4607
|
-
(pid,),
|
|
4608
|
-
)
|
|
4609
|
-
prompts = []
|
|
4610
|
-
for row in rows:
|
|
4611
|
-
r = dict(row)
|
|
4612
|
-
prompts.append({
|
|
4613
|
-
"prompt_id": r["prompt_id"],
|
|
4614
|
-
"category": r["category"],
|
|
4615
|
-
"content": r["content"],
|
|
4616
|
-
"confidence": round(float(r["confidence"]), 3),
|
|
4617
|
-
"effectiveness": round(float(r["effectiveness"]), 3),
|
|
4618
|
-
"token_count": int(r["token_count"]),
|
|
4619
|
-
"version": int(r["version"]),
|
|
4620
|
-
"created_at": r["created_at"],
|
|
4621
|
-
})
|
|
4622
|
-
except Exception as exc:
|
|
4623
|
-
if use_json:
|
|
4624
|
-
from superlocalmemory.cli.json_output import json_print
|
|
4625
|
-
json_print("soft-prompts", error={
|
|
4626
|
-
"code": "QUERY_ERROR", "message": str(exc),
|
|
4627
|
-
})
|
|
4628
|
-
sys.exit(1)
|
|
4629
|
-
raise
|
|
4630
|
-
|
|
4631
|
-
if use_json:
|
|
4632
|
-
from superlocalmemory.cli.json_output import json_print
|
|
4633
|
-
json_print("soft-prompts", data={
|
|
4634
|
-
"prompts": prompts, "count": len(prompts), "profile": pid,
|
|
4635
|
-
}, next_actions=[
|
|
4636
|
-
{"command": "slm status --json", "description": "Check status"},
|
|
4637
|
-
])
|
|
4638
|
-
return
|
|
4639
|
-
|
|
4640
|
-
if not prompts:
|
|
4641
|
-
print("No active soft prompts.")
|
|
4642
|
-
return
|
|
4643
|
-
|
|
4644
|
-
print(f"Active soft prompts ({len(prompts)}):\n")
|
|
4645
|
-
for i, p in enumerate(prompts, 1):
|
|
4646
|
-
print(f" {i}. [{p['category']}] (conf={p['confidence']:.2f})")
|
|
4647
|
-
content_preview = p["content"][:100]
|
|
4648
|
-
if len(p["content"]) > 100:
|
|
4649
|
-
content_preview += "..."
|
|
4650
|
-
print(f" {content_preview}")
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
def cmd_reap(args: Namespace) -> None:
|
|
4654
|
-
"""Find and kill orphaned SLM processes."""
|
|
4655
|
-
use_json = getattr(args, "json", False)
|
|
4656
|
-
dry_run = not getattr(args, "force", False)
|
|
4657
|
-
# V3.5.9: --all bypasses orphan detection and kills every slm mcp process
|
|
4658
|
-
# except the current one. Use after switching IDEs to clear stale sessions.
|
|
4659
|
-
use_force_all = getattr(args, "all", False)
|
|
4660
|
-
if use_force_all:
|
|
4661
|
-
dry_run = False # --all always kills; --force is implied
|
|
4662
|
-
|
|
4663
|
-
try:
|
|
4664
|
-
from superlocalmemory.infra.process_reaper import (
|
|
4665
|
-
cleanup_all_orphans,
|
|
4666
|
-
ReaperConfig,
|
|
4667
|
-
)
|
|
4668
|
-
|
|
4669
|
-
config = ReaperConfig()
|
|
4670
|
-
result = cleanup_all_orphans(config, dry_run=dry_run, force=use_force_all)
|
|
4671
|
-
except Exception as exc:
|
|
4672
|
-
if use_json:
|
|
4673
|
-
from superlocalmemory.cli.json_output import json_print
|
|
4674
|
-
json_print("reap", error={
|
|
4675
|
-
"code": "REAP_ERROR", "message": str(exc),
|
|
4676
|
-
})
|
|
4677
|
-
sys.exit(1)
|
|
4678
|
-
raise
|
|
4679
|
-
|
|
4680
|
-
if use_json:
|
|
4681
|
-
from superlocalmemory.cli.json_output import json_print
|
|
4682
|
-
json_print("reap", data={
|
|
4683
|
-
"dry_run": dry_run,
|
|
4684
|
-
"total_found": result.get("total_found", 0),
|
|
4685
|
-
"orphans_found": result.get("orphans_found", 0),
|
|
4686
|
-
"killed": result.get("killed", 0),
|
|
4687
|
-
"skipped": result.get("skipped", 0),
|
|
4688
|
-
}, next_actions=[
|
|
4689
|
-
{"command": "slm reap --force --json", "description": "Kill orphan processes"},
|
|
4690
|
-
{"command": "slm reap --all --json", "description": "Kill ALL slm mcp sessions (IDE switch)"},
|
|
4691
|
-
{"command": "slm status --json", "description": "Check status"},
|
|
4692
|
-
])
|
|
4693
|
-
return
|
|
4694
|
-
|
|
4695
|
-
total = result.get("total_found", 0)
|
|
4696
|
-
orphans = result.get("orphans_found", 0)
|
|
4697
|
-
killed = result.get("killed", 0)
|
|
4698
|
-
skipped = result.get("skipped", 0)
|
|
4699
|
-
|
|
4700
|
-
if dry_run:
|
|
4701
|
-
print(f"Process reaper (dry run)")
|
|
4702
|
-
else:
|
|
4703
|
-
print(f"Process reaper")
|
|
4704
|
-
print(f" Total SLM processes: {total}")
|
|
4705
|
-
print(f" Orphans found: {orphans}")
|
|
4706
|
-
print(f" Killed: {killed}")
|
|
4707
|
-
print(f" Skipped: {skipped}")
|
|
4708
|
-
if dry_run and orphans > 0:
|
|
4709
|
-
print("\n Use --force to kill orphaned processes.")
|
|
4710
|
-
print(" Use --all to kill ALL slm mcp sessions (after IDE switch).")
|