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,1661 +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
|
-
"""SuperLocalMemory V3 - Memory Routes (AGPL-3.0-or-later).
|
|
5
|
-
Routes: /api/memories, /api/graph, /api/search, /api/clusters, /api/clusters/{id}
|
|
6
|
-
Uses V3 MemoryEngine for store/recall. Falls back to direct DB for list/graph.
|
|
7
|
-
"""
|
|
8
|
-
import json
|
|
9
|
-
import logging
|
|
10
|
-
import re
|
|
11
|
-
import uuid
|
|
12
|
-
from typing import Optional
|
|
13
|
-
|
|
14
|
-
from fastapi import APIRouter, HTTPException, Query, Request
|
|
15
|
-
|
|
16
|
-
from superlocalmemory.core.config import BROWSE_PAGE_SIZE
|
|
17
|
-
from superlocalmemory.storage.database import (
|
|
18
|
-
visible_fact_clause_for_connection,
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
from .helpers import (
|
|
22
|
-
SearchRequest,
|
|
23
|
-
dict_factory,
|
|
24
|
-
get_active_profile,
|
|
25
|
-
get_db_connection,
|
|
26
|
-
get_engine_lazy,
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
logger = logging.getLogger("superlocalmemory.routes.memories")
|
|
30
|
-
router = APIRouter()
|
|
31
|
-
_IDEMPOTENCY_KEY = re.compile(r"^[A-Za-z0-9._:-]{1,256}$")
|
|
32
|
-
|
|
33
|
-
# v3.8.3: GENEROUS latency budget for recall. SLM's value is quality recall
|
|
34
|
-
# under heavy multi-agent load, so semantic recall is given ample time to
|
|
35
|
-
# finish — the keyword fallback is a LAST-RESORT safety net for a genuine hang
|
|
36
|
-
# (e.g. a wedged embedder), NOT an aggressive speed cutoff. Only if recall
|
|
37
|
-
# exceeds this budget do we serve the fast keyword search so the caller ALWAYS
|
|
38
|
-
# gets a result instead of hanging forever. Tune with SLM_SEARCH_RECALL_TIMEOUT_S.
|
|
39
|
-
# The dashboard's fetch timeout is set ABOVE this so the browser waits for the
|
|
40
|
-
# quality result rather than aborting early.
|
|
41
|
-
_DEFAULT_RECALL_BUDGET_S = 25.0
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def _search_recall_timeout_s() -> float:
|
|
45
|
-
import os
|
|
46
|
-
try:
|
|
47
|
-
v = float(os.environ.get("SLM_SEARCH_RECALL_TIMEOUT_S", ""))
|
|
48
|
-
return v if v > 0 else _DEFAULT_RECALL_BUDGET_S
|
|
49
|
-
except (TypeError, ValueError):
|
|
50
|
-
return _DEFAULT_RECALL_BUDGET_S
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def _internal_error(detail: str = "Internal server error") -> HTTPException:
|
|
54
|
-
"""SEC-H-02: log the full traceback server-side; return a generic message.
|
|
55
|
-
|
|
56
|
-
Returning ``str(e)`` to HTTP clients leaks DB schema (column/constraint
|
|
57
|
-
names), the data-directory filesystem path, and possibly config internals —
|
|
58
|
-
a GDPR Art. 32 gap. Call this only from inside an ``except`` block so
|
|
59
|
-
``logger.exception`` captures the active traceback automatically.
|
|
60
|
-
"""
|
|
61
|
-
logger.exception("memories route error")
|
|
62
|
-
return HTTPException(status_code=500, detail=detail)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def _get_engine(request: Request):
|
|
66
|
-
"""Get V3 engine from app state, initializing lazily on first call."""
|
|
67
|
-
return get_engine_lazy(request.app.state)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def _canonical_mutation_runtime(request: Request):
|
|
71
|
-
"""Return the daemon-owned mutation boundary or fail before queueing work."""
|
|
72
|
-
runtime = getattr(request.app.state, "canonical_remember_runtime", None)
|
|
73
|
-
if runtime is None or not runtime.ready:
|
|
74
|
-
raise HTTPException(503, detail="canonical mutation writer is not ready; retry shortly")
|
|
75
|
-
return runtime
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def _mutation_idempotency_key(request: Request) -> str:
|
|
79
|
-
"""Accept a client retry key without making one mandatory for the dashboard."""
|
|
80
|
-
key = request.headers.get("X-Idempotency-Key", "").strip() or str(uuid.uuid4())
|
|
81
|
-
if not _IDEMPOTENCY_KEY.fullmatch(key):
|
|
82
|
-
raise HTTPException(
|
|
83
|
-
422,
|
|
84
|
-
detail="X-Idempotency-Key must contain 1-256 safe characters",
|
|
85
|
-
)
|
|
86
|
-
return key
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def _canonical_mutation_error(exc: Exception, detail: str) -> HTTPException:
|
|
90
|
-
"""Map typed mutation failures without leaking SQLite or filesystem detail."""
|
|
91
|
-
from superlocalmemory.core.remember_runtime import (
|
|
92
|
-
CanonicalMutationConflict,
|
|
93
|
-
CanonicalRememberUnavailable,
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
if isinstance(exc, CanonicalMutationConflict):
|
|
97
|
-
return HTTPException(409, detail=str(exc))
|
|
98
|
-
if isinstance(exc, CanonicalRememberUnavailable):
|
|
99
|
-
return HTTPException(
|
|
100
|
-
503,
|
|
101
|
-
detail="canonical mutation writer is temporarily unavailable",
|
|
102
|
-
)
|
|
103
|
-
return _internal_error(detail)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def _mutation_runtime_or_missing_fact(
|
|
107
|
-
request: Request, engine, profile_id: str, fact_id: str,
|
|
108
|
-
):
|
|
109
|
-
"""Retain the public 404 for a missing fact without creating a local writer."""
|
|
110
|
-
runtime = getattr(request.app.state, "canonical_remember_runtime", None)
|
|
111
|
-
if runtime is not None and runtime.ready:
|
|
112
|
-
return runtime
|
|
113
|
-
rows = engine._db.execute(
|
|
114
|
-
"SELECT 1 FROM atomic_facts WHERE fact_id = ? AND profile_id = ?",
|
|
115
|
-
(fact_id, profile_id),
|
|
116
|
-
)
|
|
117
|
-
if not rows:
|
|
118
|
-
raise HTTPException(status_code=404, detail="Memory not found")
|
|
119
|
-
raise HTTPException(503, detail="canonical mutation writer is not ready; retry shortly")
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
def _admit_http_mutation(request: Request, operation: str) -> None:
|
|
123
|
-
"""Route a memory HTTP mutation through OperationPolicyRegistry.evaluate().
|
|
124
|
-
|
|
125
|
-
Called from _authorize_memory_mutation after RBAC passes. Raises HTTP 403
|
|
126
|
-
if the policy registry denies the actor. Maps "delete" → FORGET,
|
|
127
|
-
"update" → CORRECT. Uses the server-derived principal and roles.
|
|
128
|
-
"""
|
|
129
|
-
from fastapi import HTTPException as _HTTPException
|
|
130
|
-
|
|
131
|
-
from superlocalmemory.core.actor_context import Transport
|
|
132
|
-
from superlocalmemory.core.admission import AdmissionDenied, admit, resolve_actor
|
|
133
|
-
from superlocalmemory.core.operation_request import OperationKind
|
|
134
|
-
from superlocalmemory.server.rbac_enforce import (
|
|
135
|
-
resolve_actor_roles,
|
|
136
|
-
resolve_principal,
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
from superlocalmemory.core.admission import _company_mode_active
|
|
140
|
-
|
|
141
|
-
# Both switches, the same rule the other entry points use. Reading
|
|
142
|
-
# config.toml alone left this path treating a workspace as personal after
|
|
143
|
-
# the dashboard toggle had turned per-user access on -- so a write refused
|
|
144
|
-
# everywhere else was admitted here.
|
|
145
|
-
deployment = getattr(request.app.state, "deployment", None)
|
|
146
|
-
if deployment is None:
|
|
147
|
-
from superlocalmemory.core.admission import _resolve_deployment
|
|
148
|
-
|
|
149
|
-
deployment = _resolve_deployment()
|
|
150
|
-
company = _company_mode_active(deployment)
|
|
151
|
-
tier = "enterprise" if company else "personal"
|
|
152
|
-
mode = "company" if company else "local"
|
|
153
|
-
|
|
154
|
-
principal_info = resolve_principal(request)
|
|
155
|
-
principal = str(principal_info.get("user_id") or "")
|
|
156
|
-
actor_roles = resolve_actor_roles(request)
|
|
157
|
-
actor = resolve_actor(
|
|
158
|
-
Transport.HTTP,
|
|
159
|
-
tier=tier,
|
|
160
|
-
mode=mode,
|
|
161
|
-
principal=principal,
|
|
162
|
-
roles=actor_roles,
|
|
163
|
-
)
|
|
164
|
-
kind = OperationKind.FORGET if operation == "delete" else OperationKind.CORRECT
|
|
165
|
-
try:
|
|
166
|
-
admit(kind, actor, mode=mode)
|
|
167
|
-
except AdmissionDenied as exc:
|
|
168
|
-
raise _HTTPException(
|
|
169
|
-
status_code=403,
|
|
170
|
-
detail=f"Operation denied: {exc.decision.reason}",
|
|
171
|
-
) from exc
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
def _authorize_memory_mutation(
|
|
175
|
-
request: Request,
|
|
176
|
-
operation: str,
|
|
177
|
-
fact_id: str,
|
|
178
|
-
*,
|
|
179
|
-
content_preview: str = "",
|
|
180
|
-
run_pre_hook: bool = True,
|
|
181
|
-
):
|
|
182
|
-
"""Authenticate a mutation, optionally gating route-owned direct SQL."""
|
|
183
|
-
from superlocalmemory.server.write_identity import require_write_actor
|
|
184
|
-
|
|
185
|
-
actor_id = require_write_actor(
|
|
186
|
-
request,
|
|
187
|
-
getattr(request.app.state, "daemon_descriptor", None),
|
|
188
|
-
actor_kind="dashboard",
|
|
189
|
-
)
|
|
190
|
-
# RBAC (C3): on top of machine auth, enforce the caller's role on the active
|
|
191
|
-
# profile. delete → DELETE; every other mutation → WRITE.
|
|
192
|
-
from superlocalmemory.access.rbac import Permission as _Perm
|
|
193
|
-
from superlocalmemory.server.rbac_enforce import require_permission as _rbac_require
|
|
194
|
-
_rbac_require(
|
|
195
|
-
request,
|
|
196
|
-
_Perm.DELETE if operation == "delete" else _Perm.WRITE,
|
|
197
|
-
)
|
|
198
|
-
# Phase 1: admission gateway — policy registry decision for this route.
|
|
199
|
-
_admit_http_mutation(request, operation)
|
|
200
|
-
engine = _get_engine(request)
|
|
201
|
-
if engine is None:
|
|
202
|
-
raise HTTPException(503, detail="Engine not initialized")
|
|
203
|
-
profile_id = engine.profile_id
|
|
204
|
-
context = {
|
|
205
|
-
"operation": operation,
|
|
206
|
-
"agent_id": actor_id,
|
|
207
|
-
"source_agent_id": "dashboard",
|
|
208
|
-
"profile_id": profile_id,
|
|
209
|
-
"fact_id": fact_id,
|
|
210
|
-
}
|
|
211
|
-
if content_preview:
|
|
212
|
-
context["content_preview"] = content_preview[:100]
|
|
213
|
-
if run_pre_hook:
|
|
214
|
-
try:
|
|
215
|
-
engine._hooks.run_pre(operation, context)
|
|
216
|
-
except Exception as exc:
|
|
217
|
-
logger.warning("Dashboard %s authorization rejected: %s", operation, exc)
|
|
218
|
-
raise HTTPException(403, detail="Write authorization rejected") from exc
|
|
219
|
-
return engine, profile_id, context
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
def _preview(content: str | None) -> str:
|
|
223
|
-
"""Truncate content for preview display."""
|
|
224
|
-
if not content:
|
|
225
|
-
return ""
|
|
226
|
-
return content[:100] + "..." if len(content) > 100 else content
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
def _has_table(cursor, name: str) -> bool:
|
|
230
|
-
"""Check if a table exists in the database."""
|
|
231
|
-
try:
|
|
232
|
-
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (name,))
|
|
233
|
-
return cursor.fetchone() is not None
|
|
234
|
-
except Exception:
|
|
235
|
-
return False
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
def _fetch_graph_data(
|
|
239
|
-
cursor, profile: str, use_v3: bool, min_importance: int, max_nodes: int,
|
|
240
|
-
) -> tuple[list, list, list]:
|
|
241
|
-
"""Fetch graph nodes, links, clusters from V3 or V2 schema."""
|
|
242
|
-
if use_v3:
|
|
243
|
-
# Recency-first: get the most recent nodes, then find their edges.
|
|
244
|
-
# LEFT JOIN fact_importance for graph metrics (v3.4.1 — additive only).
|
|
245
|
-
cursor.execute("""
|
|
246
|
-
SELECT af.fact_id as id, af.content, af.fact_type as category,
|
|
247
|
-
af.confidence as importance, af.session_id as project_name,
|
|
248
|
-
af.created_at,
|
|
249
|
-
fi.pagerank_score, fi.community_id, fi.degree_centrality
|
|
250
|
-
FROM atomic_facts af
|
|
251
|
-
LEFT JOIN fact_importance fi
|
|
252
|
-
ON af.fact_id = fi.fact_id AND fi.profile_id = ?
|
|
253
|
-
WHERE af.profile_id = ? AND af.confidence >= ?
|
|
254
|
-
ORDER BY af.created_at DESC
|
|
255
|
-
LIMIT ?
|
|
256
|
-
""", (profile, profile, min_importance / 10.0, max_nodes))
|
|
257
|
-
nodes = cursor.fetchall()
|
|
258
|
-
|
|
259
|
-
node_ids = {n['id'] for n in nodes}
|
|
260
|
-
|
|
261
|
-
# Fetch edges between these nodes
|
|
262
|
-
if node_ids:
|
|
263
|
-
ph = ','.join('?' * len(node_ids))
|
|
264
|
-
id_list = list(node_ids)
|
|
265
|
-
cursor.execute(f"""
|
|
266
|
-
SELECT source_id as source, target_id as target,
|
|
267
|
-
weight, edge_type as relationship_type
|
|
268
|
-
FROM graph_edges
|
|
269
|
-
WHERE profile_id = ?
|
|
270
|
-
AND source_id IN ({ph}) AND target_id IN ({ph})
|
|
271
|
-
ORDER BY weight DESC
|
|
272
|
-
""", [profile] + id_list + id_list)
|
|
273
|
-
all_links = cursor.fetchall()
|
|
274
|
-
else:
|
|
275
|
-
all_links = []
|
|
276
|
-
|
|
277
|
-
links = all_links
|
|
278
|
-
for n in nodes:
|
|
279
|
-
n['entities'] = []
|
|
280
|
-
n['content_preview'] = _preview(n.get('content'))
|
|
281
|
-
# v3.4.1: Default graph metrics when fact_importance has no data
|
|
282
|
-
if n.get('pagerank_score') is None:
|
|
283
|
-
n['pagerank_score'] = 0.0
|
|
284
|
-
if n.get('community_id') is None:
|
|
285
|
-
n['community_id'] = 0
|
|
286
|
-
if n.get('degree_centrality') is None:
|
|
287
|
-
n['degree_centrality'] = 0.0
|
|
288
|
-
|
|
289
|
-
# Filter edges to only those between displayed nodes
|
|
290
|
-
node_ids = {n['id'] for n in nodes}
|
|
291
|
-
links = [lk for lk in all_links
|
|
292
|
-
if lk['source'] in node_ids and lk['target'] in node_ids]
|
|
293
|
-
|
|
294
|
-
# Compute clusters from memory_scenes
|
|
295
|
-
clusters = []
|
|
296
|
-
try:
|
|
297
|
-
cursor.execute("""
|
|
298
|
-
SELECT scene_id, theme, fact_ids_json
|
|
299
|
-
FROM memory_scenes WHERE profile_id = ?
|
|
300
|
-
""", (profile,))
|
|
301
|
-
for row in cursor.fetchall():
|
|
302
|
-
fact_ids = []
|
|
303
|
-
try:
|
|
304
|
-
fact_ids = json.loads(row.get('fact_ids_json', '[]') or '[]')
|
|
305
|
-
except (json.JSONDecodeError, TypeError):
|
|
306
|
-
pass
|
|
307
|
-
# Only include clusters that overlap with displayed nodes
|
|
308
|
-
overlap = [fid for fid in fact_ids if fid in node_ids]
|
|
309
|
-
if overlap:
|
|
310
|
-
clusters.append({
|
|
311
|
-
'cluster_id': row['scene_id'],
|
|
312
|
-
'size': len(fact_ids),
|
|
313
|
-
'visible_size': len(overlap),
|
|
314
|
-
'theme': row.get('theme', ''),
|
|
315
|
-
})
|
|
316
|
-
except Exception:
|
|
317
|
-
pass
|
|
318
|
-
|
|
319
|
-
return nodes, links, clusters
|
|
320
|
-
|
|
321
|
-
# V2 fallback
|
|
322
|
-
try:
|
|
323
|
-
cursor.execute("""
|
|
324
|
-
SELECT m.id, m.content, m.summary, m.category, m.cluster_id,
|
|
325
|
-
m.importance, m.project_name, m.created_at, m.tags, gn.entities
|
|
326
|
-
FROM memories m LEFT JOIN graph_nodes gn ON m.id = gn.memory_id
|
|
327
|
-
WHERE m.importance >= ? AND m.profile = ?
|
|
328
|
-
ORDER BY m.importance DESC, m.updated_at DESC LIMIT ?
|
|
329
|
-
""", (min_importance, profile, max_nodes))
|
|
330
|
-
except Exception:
|
|
331
|
-
cursor.execute("""
|
|
332
|
-
SELECT id, content, summary, category, cluster_id, importance,
|
|
333
|
-
project_name, created_at, tags, NULL as entities
|
|
334
|
-
FROM memories WHERE importance >= ? AND profile = ?
|
|
335
|
-
ORDER BY importance DESC, updated_at DESC LIMIT ?
|
|
336
|
-
""", (min_importance, profile, max_nodes))
|
|
337
|
-
nodes = cursor.fetchall()
|
|
338
|
-
for n in nodes:
|
|
339
|
-
ent = n.get('entities')
|
|
340
|
-
n['entities'] = json.loads(ent) if ent else []
|
|
341
|
-
n['content_preview'] = _preview(n.get('content'))
|
|
342
|
-
ids = [n['id'] for n in nodes]
|
|
343
|
-
links = _fetch_edges_v2(cursor, ids)
|
|
344
|
-
try:
|
|
345
|
-
cursor.execute("""
|
|
346
|
-
SELECT cluster_id, COUNT(*) as size, AVG(importance) as avg_importance
|
|
347
|
-
FROM memories WHERE cluster_id IS NOT NULL AND profile = ?
|
|
348
|
-
GROUP BY cluster_id
|
|
349
|
-
""", (profile,))
|
|
350
|
-
clusters = cursor.fetchall()
|
|
351
|
-
except Exception:
|
|
352
|
-
clusters = []
|
|
353
|
-
return nodes, links, clusters
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
def _fetch_edges_v3(cursor, profile: str, fact_ids: list) -> list:
|
|
357
|
-
if not fact_ids:
|
|
358
|
-
return []
|
|
359
|
-
ph = ','.join('?' * len(fact_ids))
|
|
360
|
-
try:
|
|
361
|
-
cursor.execute(f"""
|
|
362
|
-
SELECT source_id as source, target_id as target,
|
|
363
|
-
weight, edge_type as relationship_type
|
|
364
|
-
FROM graph_edges WHERE profile_id = ?
|
|
365
|
-
AND source_id IN ({ph}) AND target_id IN ({ph})
|
|
366
|
-
ORDER BY weight DESC
|
|
367
|
-
""", [profile] + fact_ids + fact_ids)
|
|
368
|
-
return cursor.fetchall()
|
|
369
|
-
except Exception:
|
|
370
|
-
return []
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
def _fetch_edges_v2(cursor, memory_ids: list) -> list:
|
|
374
|
-
if not memory_ids:
|
|
375
|
-
return []
|
|
376
|
-
ph = ','.join('?' * len(memory_ids))
|
|
377
|
-
try:
|
|
378
|
-
cursor.execute(f"""
|
|
379
|
-
SELECT source_memory_id as source, target_memory_id as target,
|
|
380
|
-
weight, relationship_type, shared_entities
|
|
381
|
-
FROM graph_edges
|
|
382
|
-
WHERE source_memory_id IN ({ph}) AND target_memory_id IN ({ph})
|
|
383
|
-
ORDER BY weight DESC
|
|
384
|
-
""", memory_ids + memory_ids)
|
|
385
|
-
links = cursor.fetchall()
|
|
386
|
-
for lk in links:
|
|
387
|
-
se = lk.get('shared_entities')
|
|
388
|
-
if se:
|
|
389
|
-
try:
|
|
390
|
-
lk['shared_entities'] = json.loads(se)
|
|
391
|
-
except Exception:
|
|
392
|
-
lk['shared_entities'] = []
|
|
393
|
-
return links
|
|
394
|
-
except Exception:
|
|
395
|
-
return []
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
@router.get("/api/memories")
|
|
399
|
-
async def get_memories(
|
|
400
|
-
request: Request,
|
|
401
|
-
category: Optional[str] = None,
|
|
402
|
-
project_name: Optional[str] = None,
|
|
403
|
-
cluster_id: Optional[int] = None,
|
|
404
|
-
min_importance: Optional[int] = None,
|
|
405
|
-
tags: Optional[str] = None,
|
|
406
|
-
limit: int = Query(BROWSE_PAGE_SIZE, ge=1, le=200),
|
|
407
|
-
offset: int = Query(0, ge=0),
|
|
408
|
-
filter: Optional[str] = Query(
|
|
409
|
-
None,
|
|
410
|
-
description="Named filter: 'high_reward' | 'being_forgotten'",
|
|
411
|
-
),
|
|
412
|
-
scope: Optional[str] = Query(
|
|
413
|
-
None,
|
|
414
|
-
description=(
|
|
415
|
-
"Scope view (v3 only): 'shared' (shared with this profile), "
|
|
416
|
-
"'global' (global memories), 'all' (this profile + global + shared). "
|
|
417
|
-
"Default None = this profile only (unchanged isolation)."
|
|
418
|
-
),
|
|
419
|
-
),
|
|
420
|
-
):
|
|
421
|
-
"""List memories with optional filtering and pagination.
|
|
422
|
-
|
|
423
|
-
S9-DASH-07: ``filter`` enables dashboard "learning-visible" views:
|
|
424
|
-
|
|
425
|
-
* ``high_reward``: facts cited by ``action_outcomes`` with
|
|
426
|
-
``reward >= 0.7`` in the last 30 days. Surfaces what the ranker
|
|
427
|
-
is actually learning from.
|
|
428
|
-
* ``being_forgotten``: facts in ``archive_status='archived'`` OR
|
|
429
|
-
with ``lifecycle='cold'`` AND no positive reward in 60 days.
|
|
430
|
-
Makes "memory decay" tangible to the operator.
|
|
431
|
-
"""
|
|
432
|
-
try:
|
|
433
|
-
conn = get_db_connection()
|
|
434
|
-
conn.row_factory = dict_factory
|
|
435
|
-
cursor = conn.cursor()
|
|
436
|
-
active_profile = get_active_profile()
|
|
437
|
-
|
|
438
|
-
use_v3 = _has_table(cursor, 'atomic_facts')
|
|
439
|
-
|
|
440
|
-
if use_v3:
|
|
441
|
-
# Scope view clause. Default (scope=None) is this-profile-only —
|
|
442
|
-
# identical isolation to before. Other values widen the view to
|
|
443
|
-
# global and/or shared-with-this-profile memories.
|
|
444
|
-
_esc = active_profile.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
|
|
445
|
-
_shared_pat = f'%"{_esc}"%'
|
|
446
|
-
if scope == "global":
|
|
447
|
-
scope_where = "scope = 'global'"
|
|
448
|
-
scope_params = []
|
|
449
|
-
elif scope == "shared":
|
|
450
|
-
scope_where = "scope = 'shared' AND shared_with LIKE ? ESCAPE '\\'"
|
|
451
|
-
scope_params = [_shared_pat]
|
|
452
|
-
elif scope == "all":
|
|
453
|
-
scope_where = (
|
|
454
|
-
"(profile_id = ? OR scope = 'global' "
|
|
455
|
-
"OR (scope = 'shared' AND shared_with LIKE ? ESCAPE '\\'))"
|
|
456
|
-
)
|
|
457
|
-
scope_params = [active_profile, _shared_pat]
|
|
458
|
-
else:
|
|
459
|
-
scope_where = "profile_id = ?"
|
|
460
|
-
scope_params = [active_profile]
|
|
461
|
-
|
|
462
|
-
# Withheld rows are excluded from BOTH the page and the total. On
|
|
463
|
-
# the author's store this list served 24 model-authored summaries
|
|
464
|
-
# in its first 50 rows and reported 5,218 memories against 3,919
|
|
465
|
-
# real ones -- the count a user reads, inflated by exactly the
|
|
466
|
-
# 1,299 rows 4.0.10 withheld.
|
|
467
|
-
visible = visible_fact_clause_for_connection(conn)
|
|
468
|
-
query = (
|
|
469
|
-
"SELECT fact_id as id, memory_id, content, fact_type as category, "
|
|
470
|
-
"confidence as importance, access_count, "
|
|
471
|
-
"created_at, created_at as updated_at, "
|
|
472
|
-
"session_id as project_name, scope, shared_with "
|
|
473
|
-
f"FROM atomic_facts WHERE {scope_where}{visible}"
|
|
474
|
-
)
|
|
475
|
-
params = list(scope_params)
|
|
476
|
-
count_base = (
|
|
477
|
-
"SELECT COUNT(*) as total FROM atomic_facts "
|
|
478
|
-
f"WHERE {scope_where}{visible}"
|
|
479
|
-
)
|
|
480
|
-
else:
|
|
481
|
-
query = """
|
|
482
|
-
SELECT id, content, summary, category, project_name, project_path,
|
|
483
|
-
importance, cluster_id, depth, access_count, parent_id,
|
|
484
|
-
created_at, updated_at, last_accessed, tags, memory_type
|
|
485
|
-
FROM memories WHERE profile = ?
|
|
486
|
-
"""
|
|
487
|
-
params = [active_profile]
|
|
488
|
-
count_base = "SELECT COUNT(*) as total FROM memories WHERE profile = ?"
|
|
489
|
-
|
|
490
|
-
# count params must mirror the base WHERE params (scope-aware for v3).
|
|
491
|
-
count_params = list(params)
|
|
492
|
-
|
|
493
|
-
if category:
|
|
494
|
-
if use_v3:
|
|
495
|
-
query += " AND fact_type = ?"
|
|
496
|
-
else:
|
|
497
|
-
query += " AND category = ?"
|
|
498
|
-
params.append(category)
|
|
499
|
-
count_base += " AND category = ?" if not use_v3 else " AND fact_type = ?"
|
|
500
|
-
count_params.append(category)
|
|
501
|
-
if project_name:
|
|
502
|
-
if use_v3:
|
|
503
|
-
query += " AND session_id = ?"
|
|
504
|
-
else:
|
|
505
|
-
query += " AND project_name = ?"
|
|
506
|
-
params.append(project_name)
|
|
507
|
-
count_base += " AND project_name = ?" if not use_v3 else " AND session_id = ?"
|
|
508
|
-
count_params.append(project_name)
|
|
509
|
-
if cluster_id is not None and not use_v3:
|
|
510
|
-
query += " AND cluster_id = ?"
|
|
511
|
-
params.append(cluster_id)
|
|
512
|
-
count_base += " AND cluster_id = ?"
|
|
513
|
-
count_params.append(cluster_id)
|
|
514
|
-
if min_importance:
|
|
515
|
-
if use_v3:
|
|
516
|
-
query += " AND confidence >= ?"
|
|
517
|
-
params.append(min_importance / 10.0)
|
|
518
|
-
else:
|
|
519
|
-
query += " AND importance >= ?"
|
|
520
|
-
params.append(min_importance)
|
|
521
|
-
if tags and not use_v3:
|
|
522
|
-
tag_list = [t.strip() for t in tags.split(',')]
|
|
523
|
-
for tag in tag_list:
|
|
524
|
-
query += " AND tags LIKE ?"
|
|
525
|
-
params.append(f'%{tag}%')
|
|
526
|
-
|
|
527
|
-
# S9-DASH-07: named filters — "high_reward" and "being_forgotten".
|
|
528
|
-
# Only supported on the v3 (atomic_facts) path — v2 fallback
|
|
529
|
-
# ignores the flag silently.
|
|
530
|
-
if filter and use_v3:
|
|
531
|
-
if filter == "high_reward":
|
|
532
|
-
query += (
|
|
533
|
-
" AND fact_id IN ("
|
|
534
|
-
" SELECT DISTINCT json_each.value"
|
|
535
|
-
" FROM action_outcomes, json_each(action_outcomes.fact_ids_json)"
|
|
536
|
-
" WHERE action_outcomes.reward >= 0.7"
|
|
537
|
-
" AND datetime(action_outcomes.settled_at) >= "
|
|
538
|
-
" datetime('now', '-30 day')"
|
|
539
|
-
")"
|
|
540
|
-
)
|
|
541
|
-
count_base += (
|
|
542
|
-
" AND fact_id IN ("
|
|
543
|
-
" SELECT DISTINCT json_each.value"
|
|
544
|
-
" FROM action_outcomes, json_each(action_outcomes.fact_ids_json)"
|
|
545
|
-
" WHERE action_outcomes.reward >= 0.7"
|
|
546
|
-
" AND datetime(action_outcomes.settled_at) >= "
|
|
547
|
-
" datetime('now', '-30 day')"
|
|
548
|
-
")"
|
|
549
|
-
)
|
|
550
|
-
elif filter == "being_forgotten":
|
|
551
|
-
# Cold / archived + no recent positive reward.
|
|
552
|
-
query += (
|
|
553
|
-
" AND ("
|
|
554
|
-
" archive_status = 'archived' OR "
|
|
555
|
-
" (lifecycle = 'cold' AND fact_id NOT IN ("
|
|
556
|
-
" SELECT DISTINCT json_each.value"
|
|
557
|
-
" FROM action_outcomes, json_each(action_outcomes.fact_ids_json)"
|
|
558
|
-
" WHERE action_outcomes.reward >= 0.5"
|
|
559
|
-
" AND datetime(action_outcomes.settled_at) >= "
|
|
560
|
-
" datetime('now', '-60 day')"
|
|
561
|
-
" ))"
|
|
562
|
-
")"
|
|
563
|
-
)
|
|
564
|
-
count_base += (
|
|
565
|
-
" AND ("
|
|
566
|
-
" archive_status = 'archived' OR "
|
|
567
|
-
" (lifecycle = 'cold' AND fact_id NOT IN ("
|
|
568
|
-
" SELECT DISTINCT json_each.value"
|
|
569
|
-
" FROM action_outcomes, json_each(action_outcomes.fact_ids_json)"
|
|
570
|
-
" WHERE action_outcomes.reward >= 0.5"
|
|
571
|
-
" AND datetime(action_outcomes.settled_at) >= "
|
|
572
|
-
" datetime('now', '-60 day')"
|
|
573
|
-
" ))"
|
|
574
|
-
")"
|
|
575
|
-
)
|
|
576
|
-
|
|
577
|
-
query += " ORDER BY created_at DESC LIMIT ? OFFSET ?"
|
|
578
|
-
params.extend([limit, offset])
|
|
579
|
-
|
|
580
|
-
cursor.execute(query, params)
|
|
581
|
-
memories = cursor.fetchall()
|
|
582
|
-
|
|
583
|
-
cursor.execute(count_base, count_params)
|
|
584
|
-
total = cursor.fetchone()['total']
|
|
585
|
-
|
|
586
|
-
conn.close()
|
|
587
|
-
|
|
588
|
-
return {
|
|
589
|
-
"memories": memories, "total": total,
|
|
590
|
-
"limit": limit, "offset": offset,
|
|
591
|
-
"has_more": (offset + limit) < total,
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
except Exception:
|
|
595
|
-
raise _internal_error("Database error")
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
@router.get("/api/graph")
|
|
599
|
-
async def get_graph(
|
|
600
|
-
request: Request,
|
|
601
|
-
max_nodes: int = Query(100, ge=10, le=10000),
|
|
602
|
-
min_importance: int = Query(1, ge=1, le=10),
|
|
603
|
-
):
|
|
604
|
-
"""Get knowledge graph data for D3.js force-directed visualization."""
|
|
605
|
-
try:
|
|
606
|
-
conn = get_db_connection()
|
|
607
|
-
conn.row_factory = dict_factory
|
|
608
|
-
cursor = conn.cursor()
|
|
609
|
-
active_profile = get_active_profile()
|
|
610
|
-
|
|
611
|
-
use_v3 = _has_table(cursor, 'atomic_facts')
|
|
612
|
-
|
|
613
|
-
nodes, links, clusters = _fetch_graph_data(
|
|
614
|
-
cursor, active_profile, use_v3, min_importance, max_nodes,
|
|
615
|
-
)
|
|
616
|
-
|
|
617
|
-
conn.close()
|
|
618
|
-
|
|
619
|
-
return {
|
|
620
|
-
"nodes": nodes, "links": links, "clusters": clusters,
|
|
621
|
-
"metadata": {
|
|
622
|
-
"node_count": len(nodes), "edge_count": len(links),
|
|
623
|
-
"cluster_count": len(clusters) if clusters else 0,
|
|
624
|
-
"filters_applied": {"max_nodes": max_nodes, "min_importance": min_importance},
|
|
625
|
-
},
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
except Exception:
|
|
629
|
-
raise _internal_error("Graph error")
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
@router.post("/api/search")
|
|
633
|
-
async def search_memories(request: Request, body: SearchRequest):
|
|
634
|
-
"""Semantic search using the daemon's in-process engine.
|
|
635
|
-
|
|
636
|
-
v3.4.61: Replaced WorkerPool.shared() (subprocess-based, cold-starts on
|
|
637
|
-
every request, always >15s) with the daemon's own engine that is already
|
|
638
|
-
loaded and warm. WorkerPool.shared() was legacy from v3.4.32 before the
|
|
639
|
-
unified daemon architecture. Using the daemon engine matches what the /recall
|
|
640
|
-
HTTP endpoint does and shares its warm SQLite page cache, bringing dashboard
|
|
641
|
-
search from >15s timeout to <1s warm.
|
|
642
|
-
|
|
643
|
-
Falls back to direct DB LIKE search if engine is unavailable.
|
|
644
|
-
"""
|
|
645
|
-
from superlocalmemory.core.recall_gate import begin_recall, end_recall
|
|
646
|
-
begin_recall()
|
|
647
|
-
try:
|
|
648
|
-
# Use the daemon engine directly — already loaded, shares warm cache.
|
|
649
|
-
# v3.4.63: engine.recall() is synchronous/blocking (~2-10s). Calling it
|
|
650
|
-
# directly in an async route blocks the ASGI event loop — Chrome detects
|
|
651
|
-
# a stalled connection and aborts with "signal is aborted without reason"
|
|
652
|
-
# before the response arrives. Fix: run in a thread-pool executor so the
|
|
653
|
-
# event loop stays alive to send keepalive frames.
|
|
654
|
-
# v3.8.2: fast=True — the dashboard search BOX is a snappy retrieval
|
|
655
|
-
# list (all six local channels + reranker), never the internal agentic
|
|
656
|
-
# LLM round, which would reintroduce the multi-second hang this endpoint
|
|
657
|
-
# is regression-tested against (test_search_fast_param_and_profile_isolation).
|
|
658
|
-
# The human-facing LLM synthesis lives on separate paths that are NOT the
|
|
659
|
-
# search list: the "ask" memory-chat (/api/v3/chat/stream, Ollama Mode B)
|
|
660
|
-
# and the precomputed knowledge-cluster summaries (core.community_summary,
|
|
661
|
-
# Mode B/C). So search stays fast; synthesis is where the LLM adds value.
|
|
662
|
-
import asyncio
|
|
663
|
-
import time as _time
|
|
664
|
-
engine = _get_engine(request)
|
|
665
|
-
if engine is not None:
|
|
666
|
-
loop = asyncio.get_running_loop()
|
|
667
|
-
t0 = _time.monotonic()
|
|
668
|
-
# T-window: prefer an explicit ``window`` spec; otherwise derive a
|
|
669
|
-
# range from the legacy date_from/date_to pair when both are set.
|
|
670
|
-
_window = getattr(body, "window", None) or ""
|
|
671
|
-
if not _window and getattr(body, "date_from", None) and getattr(body, "date_to", None):
|
|
672
|
-
_window = f"{body.date_from}..{body.date_to}"
|
|
673
|
-
# v3.8.3: bound the synchronous recall. Under a concurrent
|
|
674
|
-
# maintenance pass or a busy embedder it can run tens of seconds
|
|
675
|
-
# and the browser aborts the fetch. If it exceeds the budget we
|
|
676
|
-
# fall through to the fast keyword search below, so the dashboard
|
|
677
|
-
# ALWAYS returns instead of failing with an abort.
|
|
678
|
-
_recall_future = loop.run_in_executor(
|
|
679
|
-
None,
|
|
680
|
-
lambda: engine.recall(
|
|
681
|
-
body.query, limit=body.limit, fast=True,
|
|
682
|
-
window=_window or None,
|
|
683
|
-
# Name the surface. A recall with no name leaves no record
|
|
684
|
-
# an outcome can be matched to, and a search typed into the
|
|
685
|
-
# dashboard is one continuous thread of use, not a
|
|
686
|
-
# conversation turn.
|
|
687
|
-
session_id=f"dashboard:{get_active_profile()}",
|
|
688
|
-
),
|
|
689
|
-
)
|
|
690
|
-
# A run_in_executor thread cannot be cancelled, and wait_for() on it
|
|
691
|
-
# blocks until the thread finishes (defeating the timeout). So poll
|
|
692
|
-
# the future without blocking the event loop and give up at the
|
|
693
|
-
# deadline — the orphaned recall completes in the background and its
|
|
694
|
-
# result is discarded. This is what bounds dashboard-search latency.
|
|
695
|
-
_budget = _search_recall_timeout_s()
|
|
696
|
-
_deadline = loop.time() + _budget
|
|
697
|
-
while not _recall_future.done() and loop.time() < _deadline:
|
|
698
|
-
await asyncio.sleep(0.05)
|
|
699
|
-
if _recall_future.done():
|
|
700
|
-
response = _recall_future.result()
|
|
701
|
-
else:
|
|
702
|
-
# Ensure the orphaned future's eventual result/exception is
|
|
703
|
-
# retrieved so asyncio doesn't log "never retrieved".
|
|
704
|
-
_recall_future.add_done_callback(
|
|
705
|
-
lambda f: (f.cancelled() or f.exception())
|
|
706
|
-
)
|
|
707
|
-
logger.warning(
|
|
708
|
-
"search_memories: semantic recall exceeded %.0fs budget for "
|
|
709
|
-
"%r — serving keyword fallback",
|
|
710
|
-
_budget, (body.query or "")[:80],
|
|
711
|
-
)
|
|
712
|
-
response = None
|
|
713
|
-
if response is not None:
|
|
714
|
-
elapsed_ms = round((_time.monotonic() - t0) * 1000, 1)
|
|
715
|
-
from superlocalmemory.server.recall_serializer import (
|
|
716
|
-
recall_response_metadata,
|
|
717
|
-
serialize_recall_response,
|
|
718
|
-
)
|
|
719
|
-
results, no_confident_match = serialize_recall_response(
|
|
720
|
-
response,
|
|
721
|
-
limit=body.limit,
|
|
722
|
-
per_fact_max=300,
|
|
723
|
-
total_max=max(300, body.limit * 300),
|
|
724
|
-
)
|
|
725
|
-
return {
|
|
726
|
-
"query": body.query,
|
|
727
|
-
"results": results,
|
|
728
|
-
"total": len(results),
|
|
729
|
-
"query_type": getattr(response, "query_type", "semantic"),
|
|
730
|
-
"retrieval_time_ms": elapsed_ms,
|
|
731
|
-
"no_confident_match": no_confident_match,
|
|
732
|
-
**recall_response_metadata(response),
|
|
733
|
-
}
|
|
734
|
-
# recall timed out — fall through to the fast keyword search.
|
|
735
|
-
|
|
736
|
-
# Fallback: direct DB text search (engine not ready OR recall over budget)
|
|
737
|
-
conn = get_db_connection()
|
|
738
|
-
conn.row_factory = dict_factory
|
|
739
|
-
cursor = conn.cursor()
|
|
740
|
-
active_profile = get_active_profile()
|
|
741
|
-
cursor.execute("""
|
|
742
|
-
SELECT fact_id, content, confidence as memory_confidence,
|
|
743
|
-
fact_type as category, created_at
|
|
744
|
-
FROM atomic_facts
|
|
745
|
-
WHERE profile_id = ? AND content LIKE ?
|
|
746
|
-
ORDER BY confidence DESC LIMIT ?
|
|
747
|
-
""", (active_profile, f'%{body.query}%', body.limit))
|
|
748
|
-
rows = cursor.fetchall()
|
|
749
|
-
conn.close()
|
|
750
|
-
|
|
751
|
-
results = [{
|
|
752
|
-
**row,
|
|
753
|
-
"score": None,
|
|
754
|
-
"relevance_score": None,
|
|
755
|
-
"ranking_score": None,
|
|
756
|
-
"confidence": row.get("memory_confidence"),
|
|
757
|
-
"rank_position": position,
|
|
758
|
-
} for position, row in enumerate(rows, start=1)]
|
|
759
|
-
|
|
760
|
-
return {
|
|
761
|
-
"query": body.query, "results": results, "total": len(results),
|
|
762
|
-
"query_type": "text_search", "retrieval_time_ms": 0,
|
|
763
|
-
"retrieval_mode": "degraded_lexical",
|
|
764
|
-
"score_contract_version": "2",
|
|
765
|
-
"calibration_status": "uncalibrated",
|
|
766
|
-
"calibration_id": None,
|
|
767
|
-
"answer_confidence": None,
|
|
768
|
-
"abstained": not bool(results),
|
|
769
|
-
"abstention_reason": None if results else "no_candidates",
|
|
770
|
-
"no_confident_match": False,
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
except Exception:
|
|
774
|
-
raise _internal_error("Search error")
|
|
775
|
-
finally:
|
|
776
|
-
end_recall()
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
@router.get("/api/summary")
|
|
780
|
-
async def get_summary(request: Request, kind: str = "day", target: str = ""):
|
|
781
|
-
"""Readable summary of memories: a day, a project, or one session (#113).
|
|
782
|
-
|
|
783
|
-
The dashboard surface for the summary layer. 4.0.6 shipped the generators
|
|
784
|
-
with no caller, 4.0.7 added the CLI, 4.0.8 adds this and the MCP tool — the
|
|
785
|
-
"no command, tool or endpoint" gap, closed at the third point.
|
|
786
|
-
|
|
787
|
-
Always returns ``coverage`` and ``source_fact_ids``: a summary that hides how
|
|
788
|
-
much it covered is the opaque generic summary issue #113 warned against.
|
|
789
|
-
Reads memory.db directly; never runs during remember or recall.
|
|
790
|
-
"""
|
|
791
|
-
from datetime import date as _date, timedelta as _timedelta
|
|
792
|
-
|
|
793
|
-
kind = (kind or "day").strip().lower()
|
|
794
|
-
if kind not in ("day", "project", "session"):
|
|
795
|
-
raise HTTPException(status_code=400, detail=f"unknown summary kind '{kind}'")
|
|
796
|
-
|
|
797
|
-
profile = get_active_profile()
|
|
798
|
-
from superlocalmemory.infra.data_root import state_path
|
|
799
|
-
db_path = state_path("memory.db")
|
|
800
|
-
if not db_path.exists():
|
|
801
|
-
raise HTTPException(status_code=404, detail="no memory database")
|
|
802
|
-
|
|
803
|
-
# Pass the loaded config so Mode B/C write the summary. Omitting it silently
|
|
804
|
-
# forces the extractive path for every caller regardless of mode.
|
|
805
|
-
try:
|
|
806
|
-
from superlocalmemory.core.config import SLMConfig
|
|
807
|
-
cfg = SLMConfig.load()
|
|
808
|
-
except Exception:
|
|
809
|
-
cfg = None
|
|
810
|
-
|
|
811
|
-
try:
|
|
812
|
-
if kind == "day":
|
|
813
|
-
from superlocalmemory.summaries import generate_daily_reflection
|
|
814
|
-
day = (target or "").strip() or _date.today().isoformat()
|
|
815
|
-
if day == "today":
|
|
816
|
-
day = _date.today().isoformat()
|
|
817
|
-
elif day == "yesterday":
|
|
818
|
-
day = (_date.today() - _timedelta(days=1)).isoformat()
|
|
819
|
-
result = generate_daily_reflection(db_path, day, profile, cfg)
|
|
820
|
-
elif kind == "project":
|
|
821
|
-
if not (target or "").strip():
|
|
822
|
-
raise HTTPException(status_code=400, detail="project requires target")
|
|
823
|
-
from superlocalmemory.summaries import generate_project_work_log
|
|
824
|
-
result = generate_project_work_log(db_path, target.strip(), profile, cfg)
|
|
825
|
-
else:
|
|
826
|
-
if not (target or "").strip():
|
|
827
|
-
raise HTTPException(status_code=400, detail="session requires target")
|
|
828
|
-
from superlocalmemory.summaries import generate_session_summary
|
|
829
|
-
result = generate_session_summary(db_path, target.strip(), profile, cfg)
|
|
830
|
-
except HTTPException:
|
|
831
|
-
raise
|
|
832
|
-
except Exception:
|
|
833
|
-
raise _internal_error("Summary generation error")
|
|
834
|
-
|
|
835
|
-
# Say why, not just what. The response already reported that a summary was
|
|
836
|
-
# assembled rather than written; someone seeing a plainer result than they
|
|
837
|
-
# expected still had no way to learn that a written one needs a model, and
|
|
838
|
-
# would reasonably read the feature as broken.
|
|
839
|
-
from superlocalmemory.core.mode_capability import llm_capability
|
|
840
|
-
|
|
841
|
-
generated = str(getattr(result, "generated_by", "") or "")
|
|
842
|
-
return {
|
|
843
|
-
"kind": result.kind,
|
|
844
|
-
"profile_id": result.profile_id,
|
|
845
|
-
"summary": result.content,
|
|
846
|
-
"coverage": result.coverage,
|
|
847
|
-
"generated_by": result.generated_by,
|
|
848
|
-
"source_fact_ids": result.source_fact_ids,
|
|
849
|
-
"source_count": len(result.source_fact_ids),
|
|
850
|
-
"metadata": result.metadata,
|
|
851
|
-
"capability": llm_capability(
|
|
852
|
-
cfg,
|
|
853
|
-
# This call has just been made, so its outcome is the honest answer
|
|
854
|
-
# about availability -- better than asking the configuration again.
|
|
855
|
-
llm_reachable=generated.startswith("llm"),
|
|
856
|
-
),
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
@router.get("/api/summary/projects")
|
|
861
|
-
async def get_summary_projects(request: Request):
|
|
862
|
-
"""Projects SuperLocalMemory has actually observed, for the summary picker.
|
|
863
|
-
|
|
864
|
-
WHY THIS EXISTS
|
|
865
|
-
---------------
|
|
866
|
-
SLM is installed globally and the dashboard is a browser tab — it has no
|
|
867
|
-
working directory, so there is no such thing as "this project" from the
|
|
868
|
-
server's point of view. 4.0.8 shipped a "This project" button that sent an
|
|
869
|
-
empty target and produced "project requires target" every time. A button
|
|
870
|
-
that cannot know its own answer is the wrong control; a list of the projects
|
|
871
|
-
we have seen is the right one.
|
|
872
|
-
|
|
873
|
-
Scope comes from ``tool_events.project_path`` — the directory an agent was
|
|
874
|
-
working in when it called SLM. Deliberately NOT ``entity_profiles.
|
|
875
|
-
project_name``, which has exactly one distinct value on a real store (see
|
|
876
|
-
the note at the top of summaries/project_work_log.py).
|
|
877
|
-
|
|
878
|
-
``tool_events`` is a bounded ring buffer, so this lists recently active
|
|
879
|
-
projects rather than every project in history. ``truncated`` says so
|
|
880
|
-
honestly instead of implying the list is exhaustive.
|
|
881
|
-
"""
|
|
882
|
-
profile = get_active_profile()
|
|
883
|
-
try:
|
|
884
|
-
conn = get_db_connection()
|
|
885
|
-
# get_db_connection() hands back a SHARED read connection, and other
|
|
886
|
-
# handlers set row_factory on it. Never index these rows positionally —
|
|
887
|
-
# whichever handler ran last decides whether r[0] is a column or a
|
|
888
|
-
# KeyError. Name the columns and read them by name.
|
|
889
|
-
conn.row_factory = dict_factory
|
|
890
|
-
cursor = conn.cursor()
|
|
891
|
-
cursor.execute(
|
|
892
|
-
"""
|
|
893
|
-
SELECT project_path AS path, COUNT(*) AS events
|
|
894
|
-
FROM tool_events
|
|
895
|
-
WHERE project_path IS NOT NULL AND project_path != ''
|
|
896
|
-
AND profile_id = ?
|
|
897
|
-
GROUP BY project_path
|
|
898
|
-
ORDER BY events DESC
|
|
899
|
-
LIMIT 50
|
|
900
|
-
""",
|
|
901
|
-
(profile,),
|
|
902
|
-
)
|
|
903
|
-
rows = cursor.fetchall()
|
|
904
|
-
total = cursor.execute(
|
|
905
|
-
"SELECT COUNT(*) AS n FROM tool_events"
|
|
906
|
-
).fetchone()["n"]
|
|
907
|
-
except Exception:
|
|
908
|
-
raise _internal_error("Project list error")
|
|
909
|
-
|
|
910
|
-
projects = [
|
|
911
|
-
{
|
|
912
|
-
"path": r["path"],
|
|
913
|
-
"events": r["events"],
|
|
914
|
-
"label": _project_label(r["path"]),
|
|
915
|
-
}
|
|
916
|
-
for r in rows
|
|
917
|
-
]
|
|
918
|
-
return {
|
|
919
|
-
"projects": projects,
|
|
920
|
-
"profile_id": profile,
|
|
921
|
-
# Surfaced so the UI can explain an unexpectedly short list rather than
|
|
922
|
-
# leaving the user to assume their project was never recorded.
|
|
923
|
-
"truncated": total >= _TOOL_EVENT_RING_SIZE,
|
|
924
|
-
"event_rows": total,
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
#: tool_events is capped; at the cap the project list is a recent window, not history.
|
|
929
|
-
_TOOL_EVENT_RING_SIZE = 2000
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
def _project_label(path: str) -> str:
|
|
933
|
-
"""Short, human label for a project path.
|
|
934
|
-
|
|
935
|
-
Full paths are long and share prefixes ("/Users/x/Documents/official/..."),
|
|
936
|
-
so a dropdown of raw paths is unreadable. Last two segments keep sibling
|
|
937
|
-
projects distinguishable without the noise.
|
|
938
|
-
"""
|
|
939
|
-
parts = [p for p in str(path).replace("\\", "/").split("/") if p]
|
|
940
|
-
if not parts:
|
|
941
|
-
return str(path)
|
|
942
|
-
return "/".join(parts[-2:]) if len(parts) > 1 else parts[-1]
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
@router.get("/api/clusters")
|
|
946
|
-
async def get_clusters(request: Request):
|
|
947
|
-
"""Get cluster information with member counts and statistics."""
|
|
948
|
-
try:
|
|
949
|
-
conn = get_db_connection()
|
|
950
|
-
conn.row_factory = dict_factory
|
|
951
|
-
cursor = conn.cursor()
|
|
952
|
-
profile = get_active_profile()
|
|
953
|
-
unclustered = 0
|
|
954
|
-
|
|
955
|
-
# V3 schema: memory_scenes stores fact_ids_json (JSON array)
|
|
956
|
-
if _has_table(cursor, 'memory_scenes'):
|
|
957
|
-
cursor.execute("""
|
|
958
|
-
SELECT scene_id as cluster_id, theme, fact_ids_json,
|
|
959
|
-
entity_ids_json, created_at as first_memory
|
|
960
|
-
FROM memory_scenes WHERE profile_id = ?
|
|
961
|
-
ORDER BY created_at DESC
|
|
962
|
-
""", (profile,))
|
|
963
|
-
raw_scenes = cursor.fetchall()
|
|
964
|
-
clusters = []
|
|
965
|
-
for scene in raw_scenes:
|
|
966
|
-
fact_ids = []
|
|
967
|
-
try:
|
|
968
|
-
fact_ids = json.loads(scene.get('fact_ids_json', '[]') or '[]')
|
|
969
|
-
except (json.JSONDecodeError, TypeError):
|
|
970
|
-
pass
|
|
971
|
-
entity_ids = []
|
|
972
|
-
try:
|
|
973
|
-
entity_ids = json.loads(scene.get('entity_ids_json', '[]') or '[]')
|
|
974
|
-
except (json.JSONDecodeError, TypeError):
|
|
975
|
-
pass
|
|
976
|
-
clusters.append({
|
|
977
|
-
'cluster_id': scene['cluster_id'],
|
|
978
|
-
'member_count': len(fact_ids),
|
|
979
|
-
'categories': scene.get('theme', ''),
|
|
980
|
-
'summary': scene.get('theme', ''),
|
|
981
|
-
'first_memory': scene.get('first_memory', ''),
|
|
982
|
-
'top_entities': entity_ids[:5],
|
|
983
|
-
})
|
|
984
|
-
# Filter out empty clusters
|
|
985
|
-
clusters = [c for c in clusters if c['member_count'] > 0]
|
|
986
|
-
clusters.sort(key=lambda c: c['member_count'], reverse=True)
|
|
987
|
-
|
|
988
|
-
# Count facts not in any scene
|
|
989
|
-
all_scene_fact_ids = set()
|
|
990
|
-
for scene in raw_scenes:
|
|
991
|
-
try:
|
|
992
|
-
ids = json.loads(scene.get('fact_ids_json', '[]') or '[]')
|
|
993
|
-
all_scene_fact_ids.update(ids)
|
|
994
|
-
except (json.JSONDecodeError, TypeError):
|
|
995
|
-
pass
|
|
996
|
-
total_facts = cursor.execute(
|
|
997
|
-
"SELECT COUNT(*) as c FROM atomic_facts WHERE profile_id = ?",
|
|
998
|
-
(profile,),
|
|
999
|
-
).fetchone()['c']
|
|
1000
|
-
unclustered = total_facts - len(all_scene_fact_ids)
|
|
1001
|
-
else:
|
|
1002
|
-
# V2 fallback
|
|
1003
|
-
try:
|
|
1004
|
-
cursor.execute("""
|
|
1005
|
-
SELECT cluster_id, COUNT(*) as member_count,
|
|
1006
|
-
AVG(importance) as avg_importance,
|
|
1007
|
-
GROUP_CONCAT(DISTINCT category) as categories
|
|
1008
|
-
FROM memories WHERE cluster_id IS NOT NULL AND profile = ?
|
|
1009
|
-
GROUP BY cluster_id ORDER BY member_count DESC
|
|
1010
|
-
""", (profile,))
|
|
1011
|
-
clusters = [dict(r, top_entities=[]) for r in cursor.fetchall()]
|
|
1012
|
-
except Exception:
|
|
1013
|
-
clusters = []
|
|
1014
|
-
try:
|
|
1015
|
-
cursor.execute(
|
|
1016
|
-
"SELECT COUNT(*) as c FROM memories WHERE cluster_id IS NULL AND profile = ?",
|
|
1017
|
-
(profile,),
|
|
1018
|
-
)
|
|
1019
|
-
unclustered = cursor.fetchone()['c']
|
|
1020
|
-
except Exception:
|
|
1021
|
-
unclustered = 0
|
|
1022
|
-
|
|
1023
|
-
conn.close()
|
|
1024
|
-
return {
|
|
1025
|
-
"clusters": clusters,
|
|
1026
|
-
"total_clusters": len(clusters),
|
|
1027
|
-
"unclustered_count": unclustered,
|
|
1028
|
-
}
|
|
1029
|
-
except Exception:
|
|
1030
|
-
raise _internal_error("Cluster error")
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
@router.get("/api/clusters/{cluster_id}")
|
|
1034
|
-
async def get_cluster_detail(
|
|
1035
|
-
request: Request,
|
|
1036
|
-
cluster_id: str,
|
|
1037
|
-
limit: int = Query(BROWSE_PAGE_SIZE, ge=1, le=200),
|
|
1038
|
-
):
|
|
1039
|
-
"""Get detailed view of a specific cluster (scene)."""
|
|
1040
|
-
try:
|
|
1041
|
-
conn = get_db_connection()
|
|
1042
|
-
conn.row_factory = dict_factory
|
|
1043
|
-
cursor = conn.cursor()
|
|
1044
|
-
profile = get_active_profile()
|
|
1045
|
-
|
|
1046
|
-
if _has_table(cursor, 'memory_scenes'):
|
|
1047
|
-
# Get fact IDs from the scene's JSON array
|
|
1048
|
-
cursor.execute(
|
|
1049
|
-
"SELECT fact_ids_json, theme FROM memory_scenes "
|
|
1050
|
-
"WHERE scene_id = ? AND profile_id = ?",
|
|
1051
|
-
(cluster_id, profile),
|
|
1052
|
-
)
|
|
1053
|
-
scene_row = cursor.fetchone()
|
|
1054
|
-
if scene_row:
|
|
1055
|
-
fact_ids = []
|
|
1056
|
-
try:
|
|
1057
|
-
fact_ids = json.loads(scene_row.get('fact_ids_json', '[]') or '[]')
|
|
1058
|
-
except (json.JSONDecodeError, TypeError):
|
|
1059
|
-
pass
|
|
1060
|
-
if fact_ids:
|
|
1061
|
-
ph = ','.join('?' * min(len(fact_ids), limit))
|
|
1062
|
-
cursor.execute(f"""
|
|
1063
|
-
SELECT fact_id as id, content, fact_type as category,
|
|
1064
|
-
confidence as importance, created_at
|
|
1065
|
-
FROM atomic_facts
|
|
1066
|
-
WHERE profile_id = ? AND fact_id IN ({ph})
|
|
1067
|
-
ORDER BY confidence DESC
|
|
1068
|
-
""", [profile] + fact_ids[:limit])
|
|
1069
|
-
else:
|
|
1070
|
-
cursor.execute("SELECT 1 WHERE 0") # empty result
|
|
1071
|
-
else:
|
|
1072
|
-
cursor.execute("SELECT 1 WHERE 0") # empty result
|
|
1073
|
-
else:
|
|
1074
|
-
cursor.execute("""
|
|
1075
|
-
SELECT id, content, summary, category, project_name, importance, created_at, tags
|
|
1076
|
-
FROM memories WHERE cluster_id = ? AND profile = ?
|
|
1077
|
-
ORDER BY importance DESC, created_at DESC LIMIT ?
|
|
1078
|
-
""", (cluster_id, profile, limit))
|
|
1079
|
-
members = cursor.fetchall()
|
|
1080
|
-
conn.close()
|
|
1081
|
-
if not members:
|
|
1082
|
-
raise HTTPException(status_code=404, detail="Cluster not found")
|
|
1083
|
-
# Generate cluster summary
|
|
1084
|
-
# v3.7.8: previously routed through WorkerPool.shared(), a subprocess
|
|
1085
|
-
# cache never recycled on a profile switch (CRITICAL cross-profile
|
|
1086
|
-
# leak — up to 120s stale). Use the daemon's own resident,
|
|
1087
|
-
# lease-protected engine's config instead; the summarizer only
|
|
1088
|
-
# consumes already profile-filtered `texts` above, so this closes
|
|
1089
|
-
# the stale-engine window without changing behavior.
|
|
1090
|
-
summary = ""
|
|
1091
|
-
try:
|
|
1092
|
-
texts = [m.get("content", "")[:200] for m in members[:10] if m.get("content")]
|
|
1093
|
-
if texts:
|
|
1094
|
-
from superlocalmemory.core.summarizer import Summarizer
|
|
1095
|
-
from superlocalmemory.server.routes.helpers import get_engine_lazy
|
|
1096
|
-
|
|
1097
|
-
# v3.4.64: Do NOT call runtime.operation() synchronously in an
|
|
1098
|
-
# async route — if a transition is pending, acquire_operation()
|
|
1099
|
-
# blocks the event loop thread, deadlocking the drain. The
|
|
1100
|
-
# middleware already holds an operation lease for this request;
|
|
1101
|
-
# the engine is stable for the full duration of the handler.
|
|
1102
|
-
engine = get_engine_lazy(request.app.state)
|
|
1103
|
-
if engine is not None:
|
|
1104
|
-
summarizer = Summarizer(engine._config)
|
|
1105
|
-
summary = summarizer.summarize_cluster(
|
|
1106
|
-
[{"content": t} for t in texts]
|
|
1107
|
-
) or ""
|
|
1108
|
-
except Exception:
|
|
1109
|
-
pass
|
|
1110
|
-
|
|
1111
|
-
return {
|
|
1112
|
-
"cluster_info": {"cluster_id": cluster_id, "total_members": len(members)},
|
|
1113
|
-
"summary": summary,
|
|
1114
|
-
"members": members,
|
|
1115
|
-
"connections": [],
|
|
1116
|
-
}
|
|
1117
|
-
except HTTPException:
|
|
1118
|
-
raise
|
|
1119
|
-
except Exception:
|
|
1120
|
-
raise _internal_error("Cluster detail error")
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
@router.get("/api/memories/{memory_id}/facts")
|
|
1124
|
-
async def get_memory_facts(request: Request, memory_id: str):
|
|
1125
|
-
"""Get original memory text with all its child atomic facts.
|
|
1126
|
-
|
|
1127
|
-
v3.7.8: previously routed through WorkerPool.shared(), a subprocess
|
|
1128
|
-
engine cached at process init and never recycled on a profile switch
|
|
1129
|
-
(CRITICAL cross-profile leak — served the OLD profile's facts for up to
|
|
1130
|
-
120s after a switch). Uses the daemon's own resident, lease-protected
|
|
1131
|
-
engine instead, exactly like the /recall route.
|
|
1132
|
-
"""
|
|
1133
|
-
try:
|
|
1134
|
-
from superlocalmemory.server.routes.helpers import get_engine_lazy
|
|
1135
|
-
|
|
1136
|
-
# v3.4.64: Do NOT call runtime.operation() synchronously in an async
|
|
1137
|
-
# route — blocks the event loop when a transition is pending. The
|
|
1138
|
-
# middleware already holds an operation lease; the engine and its
|
|
1139
|
-
# profile_id are stable for the full duration of this request.
|
|
1140
|
-
engine = get_engine_lazy(request.app.state)
|
|
1141
|
-
if engine is None:
|
|
1142
|
-
raise HTTPException(status_code=503, detail="Engine not initialized")
|
|
1143
|
-
active_profile = engine.profile_id
|
|
1144
|
-
mem_map = engine._db.get_memory_content_batch(
|
|
1145
|
-
[memory_id], active_profile, include_global=True, include_shared=True,
|
|
1146
|
-
)
|
|
1147
|
-
original = mem_map.get(memory_id, "")
|
|
1148
|
-
facts = engine._db.get_facts_by_memory_id(memory_id, active_profile)
|
|
1149
|
-
fact_list = [
|
|
1150
|
-
{
|
|
1151
|
-
"fact_id": f.fact_id,
|
|
1152
|
-
"content": f.content,
|
|
1153
|
-
"fact_type": (
|
|
1154
|
-
f.fact_type.value if hasattr(f.fact_type, "value")
|
|
1155
|
-
else str(f.fact_type)
|
|
1156
|
-
),
|
|
1157
|
-
"confidence": round(f.confidence, 3),
|
|
1158
|
-
"created_at": f.created_at,
|
|
1159
|
-
}
|
|
1160
|
-
for f in facts
|
|
1161
|
-
]
|
|
1162
|
-
return {
|
|
1163
|
-
"ok": True,
|
|
1164
|
-
"memory_id": memory_id,
|
|
1165
|
-
"original_content": original,
|
|
1166
|
-
"facts": fact_list,
|
|
1167
|
-
"fact_count": len(fact_list),
|
|
1168
|
-
}
|
|
1169
|
-
except HTTPException:
|
|
1170
|
-
raise
|
|
1171
|
-
except Exception:
|
|
1172
|
-
raise _internal_error()
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
@router.get("/api/memories/{memory_id}/detail")
|
|
1176
|
-
async def get_memory_detail(request: Request, memory_id: str):
|
|
1177
|
-
"""Full memory row + all child atomic facts (for dashboard modal)."""
|
|
1178
|
-
try:
|
|
1179
|
-
conn = get_db_connection()
|
|
1180
|
-
conn.row_factory = dict_factory
|
|
1181
|
-
cursor = conn.cursor()
|
|
1182
|
-
active_profile = get_active_profile()
|
|
1183
|
-
|
|
1184
|
-
cursor.execute(
|
|
1185
|
-
"SELECT memory_id, content, session_id, speaker, role, "
|
|
1186
|
-
"session_date, created_at, metadata_json "
|
|
1187
|
-
"FROM memories WHERE memory_id = ? AND profile_id = ?",
|
|
1188
|
-
(memory_id, active_profile),
|
|
1189
|
-
)
|
|
1190
|
-
mem = cursor.fetchone()
|
|
1191
|
-
if not mem:
|
|
1192
|
-
conn.close()
|
|
1193
|
-
raise HTTPException(status_code=404, detail="Memory not found")
|
|
1194
|
-
|
|
1195
|
-
cursor.execute(
|
|
1196
|
-
"SELECT fact_id, content, fact_type, confidence, importance, "
|
|
1197
|
-
"access_count, created_at, entities_json "
|
|
1198
|
-
"FROM atomic_facts WHERE memory_id = ? AND profile_id = ? "
|
|
1199
|
-
"ORDER BY created_at ASC",
|
|
1200
|
-
(memory_id, active_profile),
|
|
1201
|
-
)
|
|
1202
|
-
facts = cursor.fetchall()
|
|
1203
|
-
conn.close()
|
|
1204
|
-
|
|
1205
|
-
try:
|
|
1206
|
-
mem["metadata"] = json.loads(mem.pop("metadata_json") or "{}")
|
|
1207
|
-
except Exception:
|
|
1208
|
-
mem["metadata"] = {}
|
|
1209
|
-
for f in facts:
|
|
1210
|
-
try:
|
|
1211
|
-
f["entities"] = json.loads(f.pop("entities_json") or "[]")
|
|
1212
|
-
except Exception:
|
|
1213
|
-
f["entities"] = []
|
|
1214
|
-
|
|
1215
|
-
return {
|
|
1216
|
-
"memory": mem,
|
|
1217
|
-
"facts": facts,
|
|
1218
|
-
"fact_count": len(facts),
|
|
1219
|
-
}
|
|
1220
|
-
except HTTPException:
|
|
1221
|
-
raise
|
|
1222
|
-
except Exception:
|
|
1223
|
-
raise _internal_error("Detail error")
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
@router.get("/api/facts/{fact_id}")
|
|
1227
|
-
async def get_fact_detail(request: Request, fact_id: str):
|
|
1228
|
-
"""Single atomic fact detail (for fact popup)."""
|
|
1229
|
-
try:
|
|
1230
|
-
conn = get_db_connection()
|
|
1231
|
-
conn.row_factory = dict_factory
|
|
1232
|
-
cursor = conn.cursor()
|
|
1233
|
-
active_profile = get_active_profile()
|
|
1234
|
-
|
|
1235
|
-
cursor.execute(
|
|
1236
|
-
"SELECT f.fact_id, f.memory_id, f.content, f.fact_type, "
|
|
1237
|
-
"f.confidence, f.importance, f.access_count, f.created_at, "
|
|
1238
|
-
"f.entities_json, f.canonical_entities_json, f.session_id, "
|
|
1239
|
-
"m.content AS source_memory_content "
|
|
1240
|
-
"FROM atomic_facts f "
|
|
1241
|
-
"LEFT JOIN memories m ON f.memory_id = m.memory_id "
|
|
1242
|
-
"WHERE f.fact_id = ? AND f.profile_id = ?",
|
|
1243
|
-
(fact_id, active_profile),
|
|
1244
|
-
)
|
|
1245
|
-
row = cursor.fetchone()
|
|
1246
|
-
conn.close()
|
|
1247
|
-
if not row:
|
|
1248
|
-
raise HTTPException(status_code=404, detail="Fact not found")
|
|
1249
|
-
try:
|
|
1250
|
-
row["entities"] = json.loads(row.pop("entities_json") or "[]")
|
|
1251
|
-
except Exception:
|
|
1252
|
-
row["entities"] = []
|
|
1253
|
-
try:
|
|
1254
|
-
row["canonical_entities"] = json.loads(
|
|
1255
|
-
row.pop("canonical_entities_json") or "[]"
|
|
1256
|
-
)
|
|
1257
|
-
except Exception:
|
|
1258
|
-
row["canonical_entities"] = []
|
|
1259
|
-
row["code_links"] = _code_links_for_fact(fact_id)
|
|
1260
|
-
return row
|
|
1261
|
-
except HTTPException:
|
|
1262
|
-
raise
|
|
1263
|
-
except Exception:
|
|
1264
|
-
raise _internal_error("Fact detail error")
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
def _code_links_for_fact(fact_id: str) -> list[dict]:
|
|
1268
|
-
"""Code entities this fact mentions, from the code graph.
|
|
1269
|
-
|
|
1270
|
-
Fail-open by design: this is a display extra on a detail popup. No code graph
|
|
1271
|
-
built, bridge switched off, or database missing all mean "no section shown",
|
|
1272
|
-
never an error on the fact itself. A user who has never touched the code
|
|
1273
|
-
graph must not see a failure because of a feature they do not use.
|
|
1274
|
-
|
|
1275
|
-
Reads code_graph.db, which is a separate database from memory.db and is never
|
|
1276
|
-
opened by the recall path — so nothing here can affect recall.
|
|
1277
|
-
"""
|
|
1278
|
-
try:
|
|
1279
|
-
from superlocalmemory.code_graph.config import CodeGraphConfig
|
|
1280
|
-
|
|
1281
|
-
cfg = CodeGraphConfig.load()
|
|
1282
|
-
if not (cfg.enabled and cfg.bridge_enabled):
|
|
1283
|
-
return []
|
|
1284
|
-
db_path = cfg.get_db_path()
|
|
1285
|
-
if not db_path.exists():
|
|
1286
|
-
return []
|
|
1287
|
-
|
|
1288
|
-
import sqlite3
|
|
1289
|
-
|
|
1290
|
-
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
|
|
1291
|
-
try:
|
|
1292
|
-
conn.row_factory = dict_factory
|
|
1293
|
-
rows = conn.execute(
|
|
1294
|
-
"SELECT cml.link_type, cml.confidence, cml.is_stale, "
|
|
1295
|
-
" cml.enriched_description, "
|
|
1296
|
-
" gn.name, gn.qualified_name, gn.kind, gn.file_path "
|
|
1297
|
-
"FROM code_memory_links cml "
|
|
1298
|
-
"LEFT JOIN graph_nodes gn ON gn.node_id = cml.code_node_id "
|
|
1299
|
-
"WHERE cml.slm_fact_id = ? "
|
|
1300
|
-
"ORDER BY cml.confidence DESC, gn.qualified_name",
|
|
1301
|
-
(fact_id,),
|
|
1302
|
-
).fetchall()
|
|
1303
|
-
finally:
|
|
1304
|
-
conn.close()
|
|
1305
|
-
|
|
1306
|
-
return [
|
|
1307
|
-
{
|
|
1308
|
-
"name": r.get("name") or "",
|
|
1309
|
-
"qualified_name": r.get("qualified_name") or "",
|
|
1310
|
-
"kind": r.get("kind") or "",
|
|
1311
|
-
"file_path": r.get("file_path") or "",
|
|
1312
|
-
"link_type": r.get("link_type") or "mentions",
|
|
1313
|
-
"confidence": r.get("confidence"),
|
|
1314
|
-
"is_stale": bool(r.get("is_stale")),
|
|
1315
|
-
"description": r.get("enriched_description") or "",
|
|
1316
|
-
}
|
|
1317
|
-
for r in rows
|
|
1318
|
-
# A link whose node is gone (LEFT JOIN produced no row) is a stale
|
|
1319
|
-
# pointer, not something to render as a blank entry.
|
|
1320
|
-
if r.get("qualified_name")
|
|
1321
|
-
]
|
|
1322
|
-
except Exception:
|
|
1323
|
-
logger.debug("code links unavailable for %s", fact_id, exc_info=True)
|
|
1324
|
-
return []
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
@router.delete("/api/memories/{fact_id}")
|
|
1328
|
-
async def delete_memory(request: Request, fact_id: str):
|
|
1329
|
-
"""Delete a specific memory (atomic fact) by ID."""
|
|
1330
|
-
engine, _active_profile, hook_context = _authorize_memory_mutation(
|
|
1331
|
-
request, "delete", fact_id, run_pre_hook=False,
|
|
1332
|
-
)
|
|
1333
|
-
try:
|
|
1334
|
-
from superlocalmemory.core.mutations import delete_fact_authorized
|
|
1335
|
-
|
|
1336
|
-
result = delete_fact_authorized(
|
|
1337
|
-
engine,
|
|
1338
|
-
fact_id,
|
|
1339
|
-
trusted_actor_id=hook_context["agent_id"],
|
|
1340
|
-
source_agent_id="dashboard",
|
|
1341
|
-
canonical_runtime=_mutation_runtime_or_missing_fact(
|
|
1342
|
-
request, engine, _active_profile, fact_id,
|
|
1343
|
-
),
|
|
1344
|
-
idempotency_key=_mutation_idempotency_key(request),
|
|
1345
|
-
)
|
|
1346
|
-
if not result.get("ok"):
|
|
1347
|
-
if result.get("retryable"):
|
|
1348
|
-
raise HTTPException(
|
|
1349
|
-
status_code=503,
|
|
1350
|
-
detail="Erasure incomplete (projection residue); retry shortly",
|
|
1351
|
-
)
|
|
1352
|
-
raise HTTPException(status_code=404, detail="Memory not found")
|
|
1353
|
-
return {
|
|
1354
|
-
"success": True,
|
|
1355
|
-
"deleted": fact_id,
|
|
1356
|
-
"erasure_verified": bool(result.get("erasure_verified", False)),
|
|
1357
|
-
"erasure_state": result.get("erasure_state", "FAILED"),
|
|
1358
|
-
}
|
|
1359
|
-
except HTTPException:
|
|
1360
|
-
raise
|
|
1361
|
-
except Exception as exc:
|
|
1362
|
-
raise _canonical_mutation_error(exc, "Delete error")
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
@router.post("/api/memories/{fact_id}/forget")
|
|
1366
|
-
async def forget_memory(request: Request, fact_id: str):
|
|
1367
|
-
"""S9-DASH-08: soft-forget a fact — flip archive_status='archived'.
|
|
1368
|
-
|
|
1369
|
-
Non-destructive: the row stays in ``atomic_facts`` for audit and
|
|
1370
|
-
can be un-archived later. Default recall paths filter it out.
|
|
1371
|
-
The fact's payload is ALSO copied into ``memory_archive`` so a
|
|
1372
|
-
future ``slm restore`` can bring it back.
|
|
1373
|
-
"""
|
|
1374
|
-
engine, active_profile, hook_context = _authorize_memory_mutation(
|
|
1375
|
-
request, "delete", fact_id,
|
|
1376
|
-
run_pre_hook=False,
|
|
1377
|
-
)
|
|
1378
|
-
try:
|
|
1379
|
-
engine._hooks.run_pre("delete", hook_context)
|
|
1380
|
-
result = _canonical_mutation_runtime(request).archive_fact(
|
|
1381
|
-
active_profile,
|
|
1382
|
-
fact_id,
|
|
1383
|
-
idempotency_key=_mutation_idempotency_key(request),
|
|
1384
|
-
)
|
|
1385
|
-
if not result.get("ok"):
|
|
1386
|
-
raise HTTPException(status_code=404, detail="Memory not found")
|
|
1387
|
-
engine._hooks.run_post("delete", hook_context)
|
|
1388
|
-
return {"success": True, "fact_id": fact_id, "archived_at": result["archived_at"]}
|
|
1389
|
-
except HTTPException:
|
|
1390
|
-
raise
|
|
1391
|
-
except Exception as exc:
|
|
1392
|
-
raise _canonical_mutation_error(exc, "Forget error")
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
@router.post("/api/memories/{fact_id}/merge")
|
|
1396
|
-
async def merge_memory(request: Request, fact_id: str):
|
|
1397
|
-
"""S9-DASH-08: merge this fact into another (keep the other).
|
|
1398
|
-
|
|
1399
|
-
Body: ``{into: <kept_fact_id>}``.
|
|
1400
|
-
|
|
1401
|
-
Writes a ``memory_merge_log`` row (M011) for provenance and marks
|
|
1402
|
-
the loser's ``merged_into`` column. The loser is archived so it
|
|
1403
|
-
no longer appears in default recall. The winner is untouched.
|
|
1404
|
-
"""
|
|
1405
|
-
engine, active_profile, hook_context = _authorize_memory_mutation(
|
|
1406
|
-
request, "delete", fact_id,
|
|
1407
|
-
run_pre_hook=False,
|
|
1408
|
-
)
|
|
1409
|
-
try:
|
|
1410
|
-
body = await request.json()
|
|
1411
|
-
kept = str((body or {}).get("into", "")).strip()
|
|
1412
|
-
if not kept:
|
|
1413
|
-
raise HTTPException(400, "Body field 'into' is required")
|
|
1414
|
-
# S9-AUDIT: cap length defensively — fact_ids are UUID-v4 36 chars.
|
|
1415
|
-
if len(kept) > 200:
|
|
1416
|
-
raise HTTPException(400, "'into' exceeds 200-char limit")
|
|
1417
|
-
if kept == fact_id:
|
|
1418
|
-
raise HTTPException(400, "Cannot merge a fact into itself")
|
|
1419
|
-
engine._hooks.run_pre("delete", hook_context)
|
|
1420
|
-
result = _canonical_mutation_runtime(request).merge_fact(
|
|
1421
|
-
active_profile,
|
|
1422
|
-
fact_id,
|
|
1423
|
-
kept,
|
|
1424
|
-
idempotency_key=_mutation_idempotency_key(request),
|
|
1425
|
-
)
|
|
1426
|
-
if not result.get("ok"):
|
|
1427
|
-
raise HTTPException(404, "Both fact_ids must exist in the active profile")
|
|
1428
|
-
engine._hooks.run_post("delete", hook_context)
|
|
1429
|
-
return {
|
|
1430
|
-
"success": True,
|
|
1431
|
-
"merged": fact_id,
|
|
1432
|
-
"into": kept,
|
|
1433
|
-
"merged_at": result["merged_at"],
|
|
1434
|
-
}
|
|
1435
|
-
except HTTPException:
|
|
1436
|
-
raise
|
|
1437
|
-
except Exception as exc:
|
|
1438
|
-
raise _canonical_mutation_error(exc, "Merge error")
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
@router.patch("/api/memories/{fact_id}", status_code=202)
|
|
1442
|
-
async def edit_memory(request: Request, fact_id: str):
|
|
1443
|
-
"""Propose an immutable, review-required correction for one memory."""
|
|
1444
|
-
try:
|
|
1445
|
-
body = await request.json()
|
|
1446
|
-
new_content = (body.get("content") or "").strip()
|
|
1447
|
-
if not new_content:
|
|
1448
|
-
raise HTTPException(status_code=400, detail="content is required")
|
|
1449
|
-
engine, _active_profile, hook_context = _authorize_memory_mutation(
|
|
1450
|
-
request,
|
|
1451
|
-
"update",
|
|
1452
|
-
fact_id,
|
|
1453
|
-
content_preview=new_content,
|
|
1454
|
-
run_pre_hook=False,
|
|
1455
|
-
)
|
|
1456
|
-
from superlocalmemory.core.mutations import update_fact_authorized
|
|
1457
|
-
|
|
1458
|
-
result = update_fact_authorized(
|
|
1459
|
-
engine,
|
|
1460
|
-
fact_id,
|
|
1461
|
-
new_content,
|
|
1462
|
-
trusted_actor_id=hook_context["agent_id"],
|
|
1463
|
-
source_agent_id="dashboard",
|
|
1464
|
-
canonical_runtime=_canonical_mutation_runtime(request),
|
|
1465
|
-
idempotency_key=_mutation_idempotency_key(request),
|
|
1466
|
-
)
|
|
1467
|
-
if not result.get("ok"):
|
|
1468
|
-
raise HTTPException(status_code=404, detail="Memory not found")
|
|
1469
|
-
if result.get("unchanged"):
|
|
1470
|
-
return {"success": True, "fact_id": fact_id, "content": new_content, "unchanged": True}
|
|
1471
|
-
correction = result["correction_case"]
|
|
1472
|
-
return {
|
|
1473
|
-
"success": True,
|
|
1474
|
-
"fact_id": fact_id,
|
|
1475
|
-
"predecessor_fact_id": result["predecessor_fact_id"],
|
|
1476
|
-
"successor_fact_id": result["successor_fact_id"],
|
|
1477
|
-
"correction_case": correction,
|
|
1478
|
-
"review_required": True,
|
|
1479
|
-
"status": "proposed",
|
|
1480
|
-
}
|
|
1481
|
-
except HTTPException:
|
|
1482
|
-
raise
|
|
1483
|
-
except Exception as exc:
|
|
1484
|
-
raise _canonical_mutation_error(exc, "Edit error")
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
@router.post("/api/corrections/{case_id}/{action}")
|
|
1488
|
-
async def review_correction(request: Request, case_id: str, action: str):
|
|
1489
|
-
"""Apply, reject, or roll back an active-profile correction case.
|
|
1490
|
-
|
|
1491
|
-
The caller authenticates through the daemon boundary. It cannot select a
|
|
1492
|
-
profile, fact scope, or trust tier; the canonical writer rechecks all of
|
|
1493
|
-
those fields in its one SQLite transaction.
|
|
1494
|
-
"""
|
|
1495
|
-
try:
|
|
1496
|
-
body = await request.json()
|
|
1497
|
-
if action not in {"apply", "reject", "rollback"}:
|
|
1498
|
-
raise HTTPException(422, detail="action must be apply, reject, or rollback")
|
|
1499
|
-
expected_version = body.get("expected_version") if isinstance(body, dict) else None
|
|
1500
|
-
if not isinstance(expected_version, int) or isinstance(expected_version, bool):
|
|
1501
|
-
raise HTTPException(422, detail="expected_version must be a non-negative integer")
|
|
1502
|
-
if expected_version < 0:
|
|
1503
|
-
raise HTTPException(422, detail="expected_version must be a non-negative integer")
|
|
1504
|
-
event_valid_until = body.get("event_valid_until") if isinstance(body, dict) else None
|
|
1505
|
-
if event_valid_until is not None and not isinstance(event_valid_until, str):
|
|
1506
|
-
raise HTTPException(422, detail="event_valid_until must be an RFC3339 timestamp")
|
|
1507
|
-
if event_valid_until is not None and action != "apply":
|
|
1508
|
-
raise HTTPException(422, detail="event_valid_until is permitted only for apply")
|
|
1509
|
-
engine, active_profile, hook_context = _authorize_memory_mutation(
|
|
1510
|
-
request, "update", case_id, run_pre_hook=False
|
|
1511
|
-
)
|
|
1512
|
-
result = _canonical_mutation_runtime(request).transition_correction(
|
|
1513
|
-
active_profile,
|
|
1514
|
-
case_id,
|
|
1515
|
-
action=action,
|
|
1516
|
-
expected_version=expected_version,
|
|
1517
|
-
actor_id=hook_context["agent_id"],
|
|
1518
|
-
event_valid_until=event_valid_until,
|
|
1519
|
-
idempotency_key=_mutation_idempotency_key(request),
|
|
1520
|
-
)
|
|
1521
|
-
if not result.get("ok"):
|
|
1522
|
-
raise HTTPException(404, detail="Correction case not found")
|
|
1523
|
-
if action in {"apply", "rollback"}:
|
|
1524
|
-
from superlocalmemory.core.mutations import purge_profile_context_cache
|
|
1525
|
-
|
|
1526
|
-
purge_profile_context_cache(engine, active_profile)
|
|
1527
|
-
engine._hooks.run_post("update", hook_context)
|
|
1528
|
-
return {"success": True, "correction_case": result}
|
|
1529
|
-
except HTTPException:
|
|
1530
|
-
raise
|
|
1531
|
-
except Exception as exc:
|
|
1532
|
-
raise _canonical_mutation_error(exc, "Correction review error")
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
def _correction_case_response(case) -> dict[str, object]:
|
|
1536
|
-
"""Return review metadata only; correction ledgers never contain fact text."""
|
|
1537
|
-
return {
|
|
1538
|
-
"case_id": case.case_id,
|
|
1539
|
-
"profile_id": case.profile_id,
|
|
1540
|
-
"scope": case.scope,
|
|
1541
|
-
"predecessor_fact_id": case.predecessor_fact_id,
|
|
1542
|
-
"successor_fact_id": case.successor_fact_id,
|
|
1543
|
-
"reason_code": case.reason_code,
|
|
1544
|
-
"status": case.status,
|
|
1545
|
-
"version": case.version,
|
|
1546
|
-
"created_at": case.created_at,
|
|
1547
|
-
"updated_at": case.updated_at,
|
|
1548
|
-
"reviewed_at": case.reviewed_at,
|
|
1549
|
-
"applied_at": case.applied_at,
|
|
1550
|
-
"system_effective_at": case.system_effective_at,
|
|
1551
|
-
"event_valid_from": case.event_valid_from,
|
|
1552
|
-
"event_valid_until": case.event_valid_until,
|
|
1553
|
-
}
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
def _correction_store_for(engine, active_profile: str):
|
|
1557
|
-
from superlocalmemory.storage.correction_cases import CorrectionCaseStore
|
|
1558
|
-
|
|
1559
|
-
return CorrectionCaseStore(
|
|
1560
|
-
engine._db.db_path,
|
|
1561
|
-
is_profile_active=lambda candidate: candidate == active_profile,
|
|
1562
|
-
# Read operations never invoke this callback; writes use the daemon's
|
|
1563
|
-
# canonical runtime, which derives the authenticated actor separately.
|
|
1564
|
-
is_actor_trusted=lambda _actor: False,
|
|
1565
|
-
)
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
@router.get("/api/corrections")
|
|
1569
|
-
async def list_corrections(request: Request, limit: int = 100):
|
|
1570
|
-
"""List bounded review metadata for the active owning profile."""
|
|
1571
|
-
try:
|
|
1572
|
-
engine, active_profile, _context = _authorize_memory_mutation(
|
|
1573
|
-
request, "update", "correction-list", run_pre_hook=False
|
|
1574
|
-
)
|
|
1575
|
-
cases = _correction_store_for(engine, active_profile).list_cases(active_profile, limit=limit)
|
|
1576
|
-
return {"success": True, "corrections": [_correction_case_response(case) for case in cases]}
|
|
1577
|
-
except HTTPException:
|
|
1578
|
-
raise
|
|
1579
|
-
except Exception as exc:
|
|
1580
|
-
raise _canonical_mutation_error(exc, "Correction list error")
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
@router.get("/api/corrections/{case_id}")
|
|
1584
|
-
async def get_correction(request: Request, case_id: str):
|
|
1585
|
-
"""Get one active-profile correction case without exposing raw memory text."""
|
|
1586
|
-
try:
|
|
1587
|
-
engine, active_profile, _context = _authorize_memory_mutation(
|
|
1588
|
-
request, "update", case_id, run_pre_hook=False
|
|
1589
|
-
)
|
|
1590
|
-
case = _correction_store_for(engine, active_profile).get_case(case_id)
|
|
1591
|
-
return {"success": True, "correction": _correction_case_response(case)}
|
|
1592
|
-
except HTTPException:
|
|
1593
|
-
raise
|
|
1594
|
-
except Exception as exc:
|
|
1595
|
-
from superlocalmemory.storage.correction_cases import CorrectionNotFoundError
|
|
1596
|
-
|
|
1597
|
-
if isinstance(exc, CorrectionNotFoundError):
|
|
1598
|
-
raise HTTPException(404, detail="Correction case not found") from exc
|
|
1599
|
-
raise _canonical_mutation_error(exc, "Correction lookup error")
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
_VALID_SCOPES = ("personal", "shared", "global")
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
@router.patch("/api/memories/{fact_id}/scope")
|
|
1606
|
-
async def set_memory_scope(request: Request, fact_id: str):
|
|
1607
|
-
"""Set a memory's scope (personal | shared | global) + shared_with.
|
|
1608
|
-
|
|
1609
|
-
Body: {"scope": "shared", "shared_with": "alice,bob"}. shared_with accepts a
|
|
1610
|
-
comma-separated string or a list; it is stored as a JSON array so the
|
|
1611
|
-
_scope_where LIKE match works. Mutation-authorized, and the fact must
|
|
1612
|
-
belong to the active profile (a caller cannot re-scope another profile's
|
|
1613
|
-
fact). This is the write side of multi-scope sharing from the dashboard.
|
|
1614
|
-
"""
|
|
1615
|
-
try:
|
|
1616
|
-
body = await request.json()
|
|
1617
|
-
scope = (body.get("scope") or "").strip().lower()
|
|
1618
|
-
if scope not in _VALID_SCOPES:
|
|
1619
|
-
raise HTTPException(400, detail=f"scope must be one of {_VALID_SCOPES}")
|
|
1620
|
-
|
|
1621
|
-
raw_shared = body.get("shared_with", [])
|
|
1622
|
-
if isinstance(raw_shared, str):
|
|
1623
|
-
shared_list = [s.strip() for s in raw_shared.split(",") if s.strip()]
|
|
1624
|
-
elif isinstance(raw_shared, list):
|
|
1625
|
-
shared_list = [str(s).strip() for s in raw_shared if str(s).strip()]
|
|
1626
|
-
else:
|
|
1627
|
-
shared_list = []
|
|
1628
|
-
if scope == "shared" and not shared_list:
|
|
1629
|
-
raise HTTPException(
|
|
1630
|
-
400, detail="shared scope requires at least one profile in shared_with")
|
|
1631
|
-
# global/personal never carry a shared_with list.
|
|
1632
|
-
if scope != "shared":
|
|
1633
|
-
shared_list = []
|
|
1634
|
-
|
|
1635
|
-
engine, active_profile, hook_context = _authorize_memory_mutation(
|
|
1636
|
-
request, "update", fact_id, run_pre_hook=False,
|
|
1637
|
-
)
|
|
1638
|
-
if scope in {"shared", "global"}:
|
|
1639
|
-
from superlocalmemory.access.rbac import Permission
|
|
1640
|
-
from superlocalmemory.server.rbac_enforce import require_permission
|
|
1641
|
-
|
|
1642
|
-
require_permission(request, Permission.SHARE, profile=active_profile)
|
|
1643
|
-
engine._hooks.run_pre("update", hook_context)
|
|
1644
|
-
result = _canonical_mutation_runtime(request).set_fact_scope(
|
|
1645
|
-
active_profile,
|
|
1646
|
-
fact_id,
|
|
1647
|
-
scope,
|
|
1648
|
-
shared_list,
|
|
1649
|
-
idempotency_key=_mutation_idempotency_key(request),
|
|
1650
|
-
)
|
|
1651
|
-
if not result.get("ok"):
|
|
1652
|
-
raise HTTPException(404, detail="Memory not found in this profile")
|
|
1653
|
-
engine._hooks.run_post("update", hook_context)
|
|
1654
|
-
return {
|
|
1655
|
-
"success": True, "fact_id": fact_id, "scope": scope,
|
|
1656
|
-
"shared_with": shared_list, "active_profile": active_profile,
|
|
1657
|
-
}
|
|
1658
|
-
except HTTPException:
|
|
1659
|
-
raise
|
|
1660
|
-
except Exception as exc:
|
|
1661
|
-
raise _canonical_mutation_error(exc, "Scope update error")
|