superlocalmemory 4.1.13 → 4.1.14
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 +48 -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,1615 +0,0 @@
|
|
|
1
|
-
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
-
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
-
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
|
-
|
|
5
|
-
"""SuperLocalMemory V3 — retrieval orchestration.
|
|
6
|
-
|
|
7
|
-
Five parallel candidate producers (semantic, BM25, temporal, spreading
|
|
8
|
-
activation, and Hopfield) feed single-pass RRF fusion; optional profile hits
|
|
9
|
-
can join that fusion input. The entity graph may then score and boost fused
|
|
10
|
-
candidates when enabled and within the recall time budget. It is not a sixth
|
|
11
|
-
parallel candidate producer. Optional cross-encoder reranking follows fusion.
|
|
12
|
-
Replaces V1's broken 10-channel triple-re-fusion pipeline.
|
|
13
|
-
|
|
14
|
-
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
15
|
-
License: AGPL-3.0-or-later
|
|
16
|
-
"""
|
|
17
|
-
from __future__ import annotations
|
|
18
|
-
|
|
19
|
-
import concurrent.futures
|
|
20
|
-
import functools
|
|
21
|
-
import logging
|
|
22
|
-
import math
|
|
23
|
-
import os
|
|
24
|
-
import re
|
|
25
|
-
import threading
|
|
26
|
-
import time
|
|
27
|
-
from typing import TYPE_CHECKING, Any, Protocol
|
|
28
|
-
|
|
29
|
-
from superlocalmemory.core.config import (
|
|
30
|
-
CANONICAL_RECALL_LIMIT,
|
|
31
|
-
ChannelWeights,
|
|
32
|
-
RetrievalConfig,
|
|
33
|
-
)
|
|
34
|
-
from superlocalmemory.retrieval import channel_status as chstat
|
|
35
|
-
from superlocalmemory.retrieval.fusion import FusionResult, weighted_rrf
|
|
36
|
-
from superlocalmemory.retrieval.strategy import QueryStrategy, QueryStrategyClassifier
|
|
37
|
-
from superlocalmemory.retrieval.temporal_validity_filter import (
|
|
38
|
-
CorrectionAdmissionCache,
|
|
39
|
-
admit_correction_candidates,
|
|
40
|
-
admit_correction_fusion_results,
|
|
41
|
-
)
|
|
42
|
-
from superlocalmemory.retrieval.time_window import (
|
|
43
|
-
in_window,
|
|
44
|
-
infer_window_from_query,
|
|
45
|
-
parse_window,
|
|
46
|
-
)
|
|
47
|
-
from superlocalmemory.storage.models import (
|
|
48
|
-
AtomicFact,
|
|
49
|
-
Mode,
|
|
50
|
-
RecallResponse,
|
|
51
|
-
RetrievalResult,
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
if TYPE_CHECKING:
|
|
55
|
-
from superlocalmemory.retrieval.bm25_channel import BM25Channel
|
|
56
|
-
from superlocalmemory.retrieval.entity_channel import EntityGraphChannel
|
|
57
|
-
from superlocalmemory.retrieval.hopfield_channel import HopfieldChannel
|
|
58
|
-
from superlocalmemory.retrieval.semantic_channel import SemanticChannel
|
|
59
|
-
from superlocalmemory.retrieval.temporal_channel import TemporalChannel
|
|
60
|
-
from superlocalmemory.storage.database import DatabaseManager
|
|
61
|
-
from superlocalmemory.trust.scorer import TrustScorer
|
|
62
|
-
|
|
63
|
-
logger = logging.getLogger(__name__)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
# How long the parallel channel phase may run before a channel is abandoned.
|
|
67
|
-
#
|
|
68
|
-
# This is a guard against a genuinely wedged channel, NOT a speed cutoff, and
|
|
69
|
-
# the distinction is the whole point. A channel that misses this limit is
|
|
70
|
-
# cancelled and contributes NOTHING to fusion: its candidates are not reordered,
|
|
71
|
-
# they are absent. So whenever this limit binds, the answer is decided partly by
|
|
72
|
-
# what else the machine happened to be doing — the same question returns a
|
|
73
|
-
# different answer under load, which is a correctness failure, not a slow one.
|
|
74
|
-
#
|
|
75
|
-
# It replaced a 1.4 s cutoff that was chosen to keep the recall p95 low. Six
|
|
76
|
-
# runs of identical code against the same 0.95 GB store logged 0, 0, 25, 2, 0
|
|
77
|
-
# and 0 abandoned channels; in the third run `hopfield` was cut off on 13 of
|
|
78
|
-
# 140 queries and `temporal` on 9, while the first run lost nothing on those
|
|
79
|
-
# same queries. That spread was the last remaining source of unrepeatable
|
|
80
|
-
# recall, and it is why fixing tie-breaks everywhere else moved top-10 churn
|
|
81
|
-
# from 40.7% to 22.9% and left rank-1 disagreement sitting at ~15%: a
|
|
82
|
-
# tie-break cannot repair a missing input.
|
|
83
|
-
#
|
|
84
|
-
# The value comes from the measured cost of the channels themselves, on that
|
|
85
|
-
# same store, 140 queries, with the limit raised out of the way so nothing was
|
|
86
|
-
# truncated (p95 / max, ms):
|
|
87
|
-
#
|
|
88
|
-
# temporal 580 / 1983 hopfield 492 / 945 bm25 230 / 1093
|
|
89
|
-
# semantic 264 / 662 spreading_activation 238 / 571
|
|
90
|
-
#
|
|
91
|
-
# The slowest channel's p95 is 580 ms and its worst single run was 1,983 ms, so
|
|
92
|
-
# 8 s is roughly four times the worst observed cost — it should never bind on a
|
|
93
|
-
# machine that is merely busy. It also stays well inside the daemon's own
|
|
94
|
-
# last-resort recall budget (25 s, `_recall_budget_s`), which is the layer that
|
|
95
|
-
# exists to catch a true hang and which already tells the caller when it fires
|
|
96
|
-
# (`retrieval_mode=degraded_lexical`). Before this change the inner 1.4 s cutoff
|
|
97
|
-
# silently overrode that outer promise of "quality recall under load".
|
|
98
|
-
#
|
|
99
|
-
# Lowering this to improve a latency percentile means buying that percentile
|
|
100
|
-
# with missing answers. Per HARD-RULES RULE 6 the ordering is Correct, then
|
|
101
|
-
# Complete, then Repeatable, and only then Fast — so if this needs to move,
|
|
102
|
-
# measure what it costs in answer quality first and record the number.
|
|
103
|
-
CHANNEL_HANG_GUARD_SECONDS = 8.0
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
class CrossEncoderProtocol(Protocol):
|
|
107
|
-
"""Duck-typed cross-encoder interface."""
|
|
108
|
-
def rerank(self, query: str, candidates: list[tuple[str, str]]) -> list[tuple[str, float]]: ...
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
class EmbeddingProvider(Protocol):
|
|
112
|
-
"""Duck-typed embedding provider."""
|
|
113
|
-
def embed(self, text: str) -> list[float]: ...
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
class RetrievalEngine:
|
|
117
|
-
"""Retrieval orchestrator: five candidate producers -> RRF fusion.
|
|
118
|
-
|
|
119
|
-
Five parallel candidate producers (semantic, BM25, temporal,
|
|
120
|
-
spreading_activation, hopfield) feed single-pass RRF fusion, followed by
|
|
121
|
-
optional cross-encoder rerank and an optional entity-graph post-fusion
|
|
122
|
-
score enhancement. Entity graph is not a sixth parallel candidate producer.
|
|
123
|
-
|
|
124
|
-
Usage::
|
|
125
|
-
engine = RetrievalEngine(db, config, channels, embedder)
|
|
126
|
-
response = engine.recall("What did Alice do?", "default", Mode.A)
|
|
127
|
-
"""
|
|
128
|
-
|
|
129
|
-
def __init__(
|
|
130
|
-
self, db: DatabaseManager, config: RetrievalConfig,
|
|
131
|
-
channels: dict[str, Any],
|
|
132
|
-
embedder: EmbeddingProvider | None = None,
|
|
133
|
-
reranker: CrossEncoderProtocol | None = None,
|
|
134
|
-
strategy: QueryStrategyClassifier | None = None,
|
|
135
|
-
base_weights: ChannelWeights | None = None,
|
|
136
|
-
profile_channel: Any | None = None,
|
|
137
|
-
bridge_discovery: Any | None = None,
|
|
138
|
-
trust_scorer: TrustScorer | None = None,
|
|
139
|
-
) -> None:
|
|
140
|
-
self._db = db
|
|
141
|
-
self._config = config
|
|
142
|
-
self._semantic: SemanticChannel | None = channels.get("semantic")
|
|
143
|
-
self._bm25: BM25Channel | None = channels.get("bm25")
|
|
144
|
-
self._entity: EntityGraphChannel | None = channels.get("entity_graph")
|
|
145
|
-
self._temporal: TemporalChannel | None = channels.get("temporal")
|
|
146
|
-
# Phase G: Hopfield channel (6th)
|
|
147
|
-
self._hopfield: HopfieldChannel | None = channels.get("hopfield")
|
|
148
|
-
# Phase 3: Spreading Activation channel
|
|
149
|
-
self._spreading_activation = channels.get("spreading_activation")
|
|
150
|
-
self._embedder = embedder
|
|
151
|
-
self._reranker = reranker
|
|
152
|
-
self._strategy = strategy or QueryStrategyClassifier(config=config)
|
|
153
|
-
self._base_weights = (base_weights or ChannelWeights()).as_dict()
|
|
154
|
-
self._profile_channel = profile_channel
|
|
155
|
-
self._bridge = bridge_discovery
|
|
156
|
-
self._trust_scorer = trust_scorer
|
|
157
|
-
# v3.7.9: scope flags (include_global / include_shared) are now threaded
|
|
158
|
-
# as explicit call parameters into every channel's search() method, so
|
|
159
|
-
# concurrent recalls each carry their own flags — no shared mutable state,
|
|
160
|
-
# no lock needed. The _scope_lock and per-recall attribute-set loop have
|
|
161
|
-
# been removed. See defect S01 in the fix/3.7.9 branch notes.
|
|
162
|
-
# One executor belongs to one retrieval engine. Creating/destroying six
|
|
163
|
-
# worker threads on every recall caused allocator/thread-stack RSS churn
|
|
164
|
-
# under sustained sessions. The scope lock already serializes channel
|
|
165
|
-
# execution, so one six-worker pool preserves the existing concurrency
|
|
166
|
-
# semantics while making ownership and shutdown deterministic.
|
|
167
|
-
self._channel_executor = concurrent.futures.ThreadPoolExecutor(
|
|
168
|
-
max_workers=6,
|
|
169
|
-
thread_name_prefix="slm-recall-channel",
|
|
170
|
-
)
|
|
171
|
-
self._close_lock = threading.Lock()
|
|
172
|
-
self._closed = False
|
|
173
|
-
|
|
174
|
-
# V3.3.4: LRU cache for query embeddings (avoids redundant Ollama API calls)
|
|
175
|
-
# V3.4.40 (2026-05-09): bumped 64 -> 512. Each cached embedding is ~3KB
|
|
176
|
-
# (768 floats × 4 bytes). 512 entries ~1.5MB — trivial memory cost,
|
|
177
|
-
# massive latency win on repeated queries (sub-ms vs 200-2000ms ollama).
|
|
178
|
-
self._query_embedding_cache: dict[str, list[float]] = {}
|
|
179
|
-
self._cache_max_size = 512
|
|
180
|
-
|
|
181
|
-
# V3.2: ChannelRegistry for self-registration (Phase 0.5)
|
|
182
|
-
from superlocalmemory.retrieval.channel_registry import ChannelRegistry
|
|
183
|
-
self._registry = ChannelRegistry()
|
|
184
|
-
if self._semantic is not None:
|
|
185
|
-
self._registry.register_channel("semantic", self._semantic, needs_embedding=True)
|
|
186
|
-
if self._bm25 is not None:
|
|
187
|
-
self._registry.register_channel("bm25", self._bm25)
|
|
188
|
-
if self._entity is not None:
|
|
189
|
-
self._registry.register_channel("entity_graph", self._entity)
|
|
190
|
-
if self._temporal is not None:
|
|
191
|
-
self._registry.register_channel("temporal", self._temporal)
|
|
192
|
-
# Phase G: Hopfield channel (6th) — needs embedding input
|
|
193
|
-
if self._hopfield is not None:
|
|
194
|
-
self._registry.register_channel("hopfield", self._hopfield, needs_embedding=True)
|
|
195
|
-
# Phase 3: Spreading Activation (5th channel) — needs embedding input
|
|
196
|
-
if self._spreading_activation is not None:
|
|
197
|
-
self._registry.register_channel(
|
|
198
|
-
"spreading_activation", self._spreading_activation, needs_embedding=True,
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
def recall(
|
|
202
|
-
self, query: str, profile_id: str,
|
|
203
|
-
mode: Mode = Mode.A, limit: int = CANONICAL_RECALL_LIMIT,
|
|
204
|
-
*,
|
|
205
|
-
extra_disabled_channels: set[str] | None = None,
|
|
206
|
-
include_global: bool = False,
|
|
207
|
-
include_shared: bool = False,
|
|
208
|
-
window: str | tuple[str, str] | None = None,
|
|
209
|
-
as_of: str | None = None,
|
|
210
|
-
known_as_of: str | None = None,
|
|
211
|
-
valid_at: str | None = None,
|
|
212
|
-
include_unknown: bool = False,
|
|
213
|
-
) -> RecallResponse:
|
|
214
|
-
"""Full retrieval pipeline: strategy -> channels -> RRF -> rerank.
|
|
215
|
-
|
|
216
|
-
Multi-scope: ``include_global`` / ``include_shared`` control which
|
|
217
|
-
scopes participate in retrieval. Both default to False so direct
|
|
218
|
-
retrieval-engine callers are private unless they explicitly opt in.
|
|
219
|
-
|
|
220
|
-
V3.4.40 (2026-05-09): ``extra_disabled_channels`` allows callers to
|
|
221
|
-
skip specific channels for a single recall (e.g. SpreadingActivation
|
|
222
|
-
for the ``--fast`` CLI flag) without mutating shared config.
|
|
223
|
-
|
|
224
|
-
``as_of``: Optional ISO 8601 datetime string. When set, the bi-temporal
|
|
225
|
-
validity filter treats facts as seen from that point in time —
|
|
226
|
-
not-yet-valid and already-expired facts are demoted. Default ``None``
|
|
227
|
-
leaves all existing behaviour unchanged.
|
|
228
|
-
"""
|
|
229
|
-
from superlocalmemory.retrieval.temporal_utils import normalize_strict_boundary
|
|
230
|
-
known_as_of = normalize_strict_boundary(known_as_of, "known_as_of")
|
|
231
|
-
valid_at = normalize_strict_boundary(valid_at, "valid_at")
|
|
232
|
-
t0 = time.monotonic()
|
|
233
|
-
# NOTE: extra_disabled_channels is passed as an explicit local argument
|
|
234
|
-
# to _run_channels() — it is NOT stored on self. Storing it as a shared
|
|
235
|
-
# mutable instance attribute (the old self._extra_disabled = ...) caused
|
|
236
|
-
# a race condition where two concurrent recalls could overwrite each
|
|
237
|
-
# other's channel-disable set (v3.4.64 fix).
|
|
238
|
-
|
|
239
|
-
# v3.5.0 diagnostic: stage timing inside retrieval (SLM_RECALL_TIMING=1).
|
|
240
|
-
import os as _os_e
|
|
241
|
-
import time as _time_e
|
|
242
|
-
_et = bool(_os_e.environ.get("SLM_RECALL_TIMING"))
|
|
243
|
-
_e0 = _time_e.monotonic()
|
|
244
|
-
|
|
245
|
-
def _em(_l: str) -> None:
|
|
246
|
-
if _et:
|
|
247
|
-
logger.warning("[RECALL-TIMING] engine.%-16s %.0f ms",
|
|
248
|
-
_l, (_time_e.monotonic() - _e0) * 1000.0)
|
|
249
|
-
|
|
250
|
-
# 1. Classify query, get adaptive weights
|
|
251
|
-
strat = self._strategy.classify(query, self._base_weights)
|
|
252
|
-
_em("classify")
|
|
253
|
-
|
|
254
|
-
# What each channel did, so the caller can tell an answer apart from
|
|
255
|
-
# an outage. Owned by this call for the same reason the dropped set is:
|
|
256
|
-
# a dict on the engine would have two concurrent recalls overwriting
|
|
257
|
-
# each other's report.
|
|
258
|
-
channel_status: dict[str, str] = {}
|
|
259
|
-
|
|
260
|
-
# Profile shortcut (runs before channel search).
|
|
261
|
-
#
|
|
262
|
-
# The ablation flag is honoured here too. It was not, so an operator who
|
|
263
|
-
# switched this channel off still had it searched, still had its weight
|
|
264
|
-
# doubled on a hit, and read a status of "ok" or "empty" — which reports
|
|
265
|
-
# their own configuration as a live channel's finding.
|
|
266
|
-
_profile_disabled = "profile" in set(self._config.disabled_channels) | set(
|
|
267
|
-
extra_disabled_channels or (),
|
|
268
|
-
)
|
|
269
|
-
if _profile_disabled:
|
|
270
|
-
profile_hits = []
|
|
271
|
-
channel_status["profile"] = chstat.DISABLED
|
|
272
|
-
elif self._profile_channel is not None:
|
|
273
|
-
try:
|
|
274
|
-
profile_hits = self._profile_channel.search(
|
|
275
|
-
query, profile_id, top_k=10,
|
|
276
|
-
)
|
|
277
|
-
channel_status["profile"] = (
|
|
278
|
-
chstat.OK if profile_hits else chstat.EMPTY
|
|
279
|
-
)
|
|
280
|
-
if profile_hits:
|
|
281
|
-
strat.weights["profile"] = 2.0
|
|
282
|
-
except Exception as exc:
|
|
283
|
-
logger.warning("Profile channel: %s", exc)
|
|
284
|
-
profile_hits = []
|
|
285
|
-
channel_status["profile"] = chstat.ERROR
|
|
286
|
-
else:
|
|
287
|
-
profile_hits = []
|
|
288
|
-
channel_status["profile"] = chstat.NOT_CONFIGURED
|
|
289
|
-
|
|
290
|
-
# Dynamic top-k for aggregation queries
|
|
291
|
-
effective_limit = 100 if strat.query_type == "aggregation" else limit
|
|
292
|
-
|
|
293
|
-
# 3. Run channels. Both scope flags AND extra_disabled_channels travel as
|
|
294
|
-
# explicit call parameters so concurrent recalls with different flags
|
|
295
|
-
# cannot corrupt each other. No lock needed — no shared mutable state.
|
|
296
|
-
# Owned by this call, so concurrent recalls cannot report each other's
|
|
297
|
-
# losses. Non-empty means this answer is incomplete, not just slow.
|
|
298
|
-
dropped_channels: set[str] = set()
|
|
299
|
-
ch_results = self._run_channels(
|
|
300
|
-
query, profile_id, strat,
|
|
301
|
-
extra_disabled_channels=extra_disabled_channels,
|
|
302
|
-
include_global=include_global, include_shared=include_shared,
|
|
303
|
-
as_of=as_of, known_as_of=known_as_of, valid_at=valid_at,
|
|
304
|
-
include_unknown=include_unknown,
|
|
305
|
-
dropped_channels=dropped_channels,
|
|
306
|
-
channel_status=channel_status,
|
|
307
|
-
)
|
|
308
|
-
_em("run_channels")
|
|
309
|
-
# One request may need admission before fusion and again after optional
|
|
310
|
-
# bridge/scene expansion. Cache only the IDs checked during this one
|
|
311
|
-
# request; every newly expanded candidate remains a hard DB lookup.
|
|
312
|
-
correction_admission = CorrectionAdmissionCache()
|
|
313
|
-
if profile_hits:
|
|
314
|
-
ch_results["profile"] = profile_hits
|
|
315
|
-
# The profile shortcut bypasses _run_channels(), so it needs the same
|
|
316
|
-
# admission before it can influence fusion or seed graph expansion.
|
|
317
|
-
ch_results = admit_correction_candidates(
|
|
318
|
-
ch_results, profile_id, self._db, as_of=as_of,
|
|
319
|
-
known_as_of=known_as_of, valid_at=valid_at,
|
|
320
|
-
include_unknown=include_unknown,
|
|
321
|
-
include_global=include_global, include_shared=include_shared,
|
|
322
|
-
lifecycle_cache=correction_admission,
|
|
323
|
-
)
|
|
324
|
-
total = sum(len(v) for v in ch_results.values())
|
|
325
|
-
|
|
326
|
-
# 3. Single-pass RRF fusion
|
|
327
|
-
ch_results = self._semantic_rank_for_unenriched(ch_results)
|
|
328
|
-
fused = weighted_rrf(ch_results, strat.weights, k=self._config.rrf_k)
|
|
329
|
-
_em("rrf_fusion")
|
|
330
|
-
|
|
331
|
-
# V3.3.21: Cross-channel intersection boost for multi-hop/temporal queries.
|
|
332
|
-
# Problem: channels work in ISOLATION. "When did Caroline go to X?" needs
|
|
333
|
-
# entity(Caroline) ∩ temporal(date). RRF averages scores but doesn't enforce
|
|
334
|
-
# the intersection constraint. Fix: boost facts that appear in 2+ signal-type
|
|
335
|
-
# channels (entity+temporal, entity+semantic, temporal+semantic).
|
|
336
|
-
if strat.query_type == "multi_hop" and len(ch_results) >= 2:
|
|
337
|
-
fused = self._apply_cross_channel_intersection(fused, ch_results, strat)
|
|
338
|
-
|
|
339
|
-
# Bridge discovery for multi-hop queries
|
|
340
|
-
# V3.3.19: Only bridge.discover() (86ms). Removed bridge.spreading_activation()
|
|
341
|
-
# which did per-node SQL queries across 254K edges → 78s latency.
|
|
342
|
-
# The SYNAPSE SA channel already provides proper SA with in-memory caching.
|
|
343
|
-
# recall-retrieval-01: O(1) membership/score lookups instead of repeated
|
|
344
|
-
# O(N) `any(...)`/`next(...)` scans inside the bridge + scene loops
|
|
345
|
-
# (was O(N^2) per recall, ~400 ms on large sessions). Kept in sync as
|
|
346
|
-
# `fused` grows so behaviour is identical.
|
|
347
|
-
fused_ids = {fr.fact_id for fr in fused}
|
|
348
|
-
fused_scores = {fr.fact_id: fr.fused_score for fr in fused}
|
|
349
|
-
|
|
350
|
-
bridge_query_types = ("multi_hop", "entity", "factual", "general")
|
|
351
|
-
if self._bridge is not None and strat.query_type in bridge_query_types:
|
|
352
|
-
try:
|
|
353
|
-
seed_ids = [fr.fact_id for fr in fused[:10]]
|
|
354
|
-
bridges = self._bridge.discover(
|
|
355
|
-
seed_ids,
|
|
356
|
-
profile_id,
|
|
357
|
-
max_bridges=10,
|
|
358
|
-
include_global=include_global,
|
|
359
|
-
include_shared=include_shared,
|
|
360
|
-
)
|
|
361
|
-
for fid, score in bridges:
|
|
362
|
-
if fid not in fused_ids:
|
|
363
|
-
new_score = score * 0.8
|
|
364
|
-
fused.append(FusionResult(
|
|
365
|
-
fact_id=fid, fused_score=new_score,
|
|
366
|
-
channel_ranks={}, channel_scores={},
|
|
367
|
-
))
|
|
368
|
-
fused_ids.add(fid)
|
|
369
|
-
fused_scores[fid] = new_score
|
|
370
|
-
except Exception as exc:
|
|
371
|
-
logger.warning("Bridge discovery: %s", exc)
|
|
372
|
-
|
|
373
|
-
# Scene expansion (v3.5.0: batch).
|
|
374
|
-
#
|
|
375
|
-
# This used to be skipped when more than 0.8 s of the recall had already
|
|
376
|
-
# elapsed, on the reasoning that the scene signal is nice-to-have and
|
|
377
|
-
# never worth delaying a response. The reasoning was wrong, because the
|
|
378
|
-
# stage does not merely decorate the answer — it appends candidates that
|
|
379
|
-
# can outrank what fusion produced. Gating it on a stopwatch therefore
|
|
380
|
-
# made the ANSWER depend on how busy the machine was, and 0.8 s sits on
|
|
381
|
-
# top of recall's own median (~1,044 ms on the 0.95 GB archive), so it
|
|
382
|
-
# was not a rare safety valve: measured over two runs of 60 queries, the
|
|
383
|
-
# two clock gates flipped their decision on 22 of 60, and of the 19
|
|
384
|
-
# queries whose answer changed, every one had a flipped gate.
|
|
385
|
-
#
|
|
386
|
-
# Removing both gates moved rank-1 disagreement between two runs from
|
|
387
|
-
# 20.0% to 3.3% and top-10 from 31.7% to 10.0%, for about 100-180 ms of
|
|
388
|
-
# p95 (1,191-1,230 ms -> 1,256-1,375 ms, ceiling 2,000 ms). Per
|
|
389
|
-
# HARD-RULES RULE 6 that is the correct direction: Correct, Complete,
|
|
390
|
-
# Repeatable, and only then Fast.
|
|
391
|
-
#
|
|
392
|
-
# So do not reintroduce a time condition here. If this stage ever needs
|
|
393
|
-
# bounding, bound it by DATA — a candidate count, a scene cap — so the
|
|
394
|
-
# same input always takes the same path.
|
|
395
|
-
if fused:
|
|
396
|
-
try:
|
|
397
|
-
top_ids = [fr.fact_id for fr in fused[:20]]
|
|
398
|
-
scenes_map = self._db.get_scenes_for_facts_batch(top_ids, profile_id)
|
|
399
|
-
expanded_ids: set[str] = set()
|
|
400
|
-
for fid in top_ids:
|
|
401
|
-
for scene in scenes_map.get(fid, [])[:2]:
|
|
402
|
-
for sfid in scene.fact_ids:
|
|
403
|
-
if sfid not in fused_ids and sfid not in expanded_ids:
|
|
404
|
-
expanded_ids.add(sfid)
|
|
405
|
-
new_score = fused_scores.get(fid, 0.5) * 0.8
|
|
406
|
-
fused.append(FusionResult(
|
|
407
|
-
fact_id=sfid, fused_score=new_score,
|
|
408
|
-
channel_ranks={}, channel_scores={},
|
|
409
|
-
))
|
|
410
|
-
fused_ids.add(sfid)
|
|
411
|
-
fused_scores[sfid] = new_score
|
|
412
|
-
except Exception as exc:
|
|
413
|
-
logger.warning("Scene expansion: %s", exc)
|
|
414
|
-
|
|
415
|
-
# V3.4.11: Entity graph signal enhancement (post-RRF boost)
|
|
416
|
-
# Instead of competing as independent channel, entity_graph SCORES
|
|
417
|
-
# the candidates from other channels by graph proximity to query entities.
|
|
418
|
-
# Research: Microsoft GraphRAG DRIFT, Pistis-RAG cascaded architecture.
|
|
419
|
-
# The 0.9 s clock gate that used to guard this stage is gone for the
|
|
420
|
-
# reason given above the scene expansion, and it mattered more here:
|
|
421
|
-
# this stage re-scores every fused candidate and then re-sorts them, so
|
|
422
|
-
# whether it ran decided the top answer outright rather than adding to
|
|
423
|
-
# it. Bound by data if it ever needs bounding, never by elapsed time.
|
|
424
|
-
if self._entity is None:
|
|
425
|
-
channel_status["entity_graph"] = chstat.NOT_CONFIGURED
|
|
426
|
-
elif "entity_graph" in set(self._config.disabled_channels) | set(
|
|
427
|
-
extra_disabled_channels or (),
|
|
428
|
-
):
|
|
429
|
-
# Both the configured set AND the per-call one, the same way the
|
|
430
|
-
# profile channel resolves it. Reading only the configured set meant
|
|
431
|
-
# a caller that switched this channel off for one recall still had
|
|
432
|
-
# it run, and still saw it reported as though it were the caller's
|
|
433
|
-
# own setting that was being honoured.
|
|
434
|
-
channel_status["entity_graph"] = chstat.DISABLED
|
|
435
|
-
elif not fused:
|
|
436
|
-
# It did not run, and saying "found nothing" would be a different
|
|
437
|
-
# claim. This channel re-scores other channels' candidates rather
|
|
438
|
-
# than producing its own, so with nothing fused there is nothing for
|
|
439
|
-
# it to do — and if the reason nothing fused is that the other five
|
|
440
|
-
# failed, reporting this one as having searched and come back empty
|
|
441
|
-
# hides that.
|
|
442
|
-
channel_status["entity_graph"] = chstat.NO_CANDIDATES
|
|
443
|
-
else:
|
|
444
|
-
# One chain, evaluated once. Repeating the three conditions to guard
|
|
445
|
-
# the work separately is how a status starts describing a decision
|
|
446
|
-
# the code no longer makes.
|
|
447
|
-
try:
|
|
448
|
-
candidate_ids = [fr.fact_id for fr in fused[:100]]
|
|
449
|
-
eg_scores = self._entity.score_candidates(
|
|
450
|
-
query,
|
|
451
|
-
candidate_ids,
|
|
452
|
-
profile_id,
|
|
453
|
-
include_global=include_global,
|
|
454
|
-
include_shared=include_shared,
|
|
455
|
-
)
|
|
456
|
-
channel_status["entity_graph"] = (
|
|
457
|
-
chstat.OK if eg_scores else chstat.EMPTY
|
|
458
|
-
)
|
|
459
|
-
if eg_scores:
|
|
460
|
-
boosted = []
|
|
461
|
-
for fr in fused:
|
|
462
|
-
eg_sc = eg_scores.get(fr.fact_id, 0.0)
|
|
463
|
-
if eg_sc > 0:
|
|
464
|
-
eg_weight = strat.weights.get("entity_graph", 1.0)
|
|
465
|
-
boost = 1.0 + eg_sc * eg_weight * 0.3
|
|
466
|
-
boosted.append(FusionResult(
|
|
467
|
-
fact_id=fr.fact_id,
|
|
468
|
-
fused_score=fr.fused_score * boost,
|
|
469
|
-
channel_ranks=fr.channel_ranks,
|
|
470
|
-
channel_scores={**fr.channel_scores, "entity_graph": eg_sc},
|
|
471
|
-
))
|
|
472
|
-
else:
|
|
473
|
-
boosted.append(fr)
|
|
474
|
-
fused = sorted(boosted, key=lambda r: (-r.fused_score, r.fact_id))
|
|
475
|
-
except Exception as exc:
|
|
476
|
-
logger.warning("Entity graph signal enhancement: %s", exc)
|
|
477
|
-
channel_status["entity_graph"] = chstat.ERROR
|
|
478
|
-
|
|
479
|
-
# Brain Core S402: bridge and scene expansion append candidates after
|
|
480
|
-
# the channel boundary. Reapply the same hard correction-admission rule
|
|
481
|
-
# immediately before any candidate can be materialized or reranked.
|
|
482
|
-
fused = admit_correction_fusion_results(
|
|
483
|
-
fused, profile_id, self._db, as_of=as_of,
|
|
484
|
-
known_as_of=known_as_of, valid_at=valid_at,
|
|
485
|
-
include_unknown=include_unknown,
|
|
486
|
-
include_global=include_global, include_shared=include_shared,
|
|
487
|
-
lifecycle_cache=correction_admission,
|
|
488
|
-
)
|
|
489
|
-
|
|
490
|
-
_em("expand+entity_enh")
|
|
491
|
-
|
|
492
|
-
# T-window: prune candidates to the requested event-time range.
|
|
493
|
-
# Event times are fetched for the bounded candidate set only (indexed),
|
|
494
|
-
# then in-range facts are kept — before fact load, so out-of-window facts
|
|
495
|
-
# are never materialized. T3: when the caller passes no explicit window,
|
|
496
|
-
# infer one from natural-language scope in the query ("last week").
|
|
497
|
-
# Safety: an EXPLICIT window is authoritative (honoured even if it empties
|
|
498
|
-
# the set — the user asked for that scope), but an INFERRED window is
|
|
499
|
-
# additive and never makes recall worse — if it would empty the results,
|
|
500
|
-
# fall back to the unwindowed set.
|
|
501
|
-
_explicit_window = window is not None
|
|
502
|
-
_window = window if _explicit_window else infer_window_from_query(query)
|
|
503
|
-
if _window is not None and fused:
|
|
504
|
-
bounds = parse_window(_window)
|
|
505
|
-
if bounds is not None:
|
|
506
|
-
etimes = self._db.get_fact_event_times(
|
|
507
|
-
[fr.fact_id for fr in fused], profile_id,
|
|
508
|
-
)
|
|
509
|
-
windowed = [
|
|
510
|
-
fr for fr in fused
|
|
511
|
-
if in_window(etimes.get(fr.fact_id), bounds)
|
|
512
|
-
]
|
|
513
|
-
if windowed or _explicit_window:
|
|
514
|
-
fused = windowed
|
|
515
|
-
_em("time_window")
|
|
516
|
-
|
|
517
|
-
# 4. Load facts for rerank pool
|
|
518
|
-
pool = min(len(fused), max(effective_limit * 3, 30))
|
|
519
|
-
top = fused[:pool]
|
|
520
|
-
facts = self._load_facts(
|
|
521
|
-
top,
|
|
522
|
-
profile_id,
|
|
523
|
-
include_global=include_global,
|
|
524
|
-
include_shared=include_shared,
|
|
525
|
-
)
|
|
526
|
-
_em("load_facts")
|
|
527
|
-
|
|
528
|
-
# V3.3.21: Session diversity for aggregation queries.
|
|
529
|
-
if strat.query_type == "aggregation" and facts:
|
|
530
|
-
top = self._enforce_session_diversity(top, facts, min_sessions=3, top_k=20)
|
|
531
|
-
|
|
532
|
-
# v3.6.6: Evidence floor — gate on per-channel scores (NOT fused/RRF score).
|
|
533
|
-
# Nonsense queries fuse at 0.75-0.78 because RRF is rank-derived and
|
|
534
|
-
# uncalibrated. The discriminator is EARNED CHANNEL EVIDENCE:
|
|
535
|
-
# semantic >= min_semantic_evidence (0.60) OR bm25 > 0
|
|
536
|
-
# OR entity_graph > 0 OR temporal > 0 OR fact is pinned.
|
|
537
|
-
# spreading_activation and hopfield do NOT count — they are associative
|
|
538
|
-
# amplifiers that fabricated the nonsense results in calibration tests.
|
|
539
|
-
# Kill-switch: SLM_RECALL_NO_FLOOR=1 bypasses the floor.
|
|
540
|
-
# Runs BEFORE the cross-encoder so the CE batch contains only
|
|
541
|
-
# evidence-qualified candidates. The floor gates on channel_scores
|
|
542
|
-
# (semantic, bm25, entity_graph, temporal) which are assigned during
|
|
543
|
-
# channel execution and are not affected by CE reranking. Moving the
|
|
544
|
-
# floor here does not change which queries abstain; it reduces the CE
|
|
545
|
-
# batch from ~180 candidates to the qualified subset (~30–60).
|
|
546
|
-
import os as _os_floor
|
|
547
|
-
floor_enabled = (
|
|
548
|
-
getattr(self._config, "evidence_floor_enabled", True)
|
|
549
|
-
and _os_floor.environ.get("SLM_RECALL_NO_FLOOR", "0") != "1"
|
|
550
|
-
)
|
|
551
|
-
if floor_enabled:
|
|
552
|
-
min_sem = getattr(self._config, "min_semantic_evidence", 0.60)
|
|
553
|
-
# Qualify the rerank pool BEFORE applying the caller's limit. RRF
|
|
554
|
-
# can rank associative-only hits above an exact BM25 match; slicing
|
|
555
|
-
# first allowed those hits to occupy every output slot and then be
|
|
556
|
-
# removed by the floor, producing a false abstention even though a
|
|
557
|
-
# qualified candidate was immediately below the slice.
|
|
558
|
-
top = self._apply_evidence_floor(top, facts, min_sem)
|
|
559
|
-
|
|
560
|
-
# 5. Cross-encoder rerank (optional, on the evidence-qualified pool)
|
|
561
|
-
# Bug 4 fix: reduced alpha for multi-hop/temporal to preserve diversity
|
|
562
|
-
# V3.3.21: Skip reranker if worker isn't ready yet (cold start).
|
|
563
|
-
# Returns results without CE reranking (~5-10pp lower quality) but instant
|
|
564
|
-
# instead of blocking 15-19s on first recall. Worker warms up in background.
|
|
565
|
-
reranker_ready = (
|
|
566
|
-
self._reranker is not None
|
|
567
|
-
and getattr(self._reranker, '_worker_ready', False)
|
|
568
|
-
)
|
|
569
|
-
reranker_applied = False
|
|
570
|
-
reranker_status = (
|
|
571
|
-
"fallback_not_ready" if self._reranker is not None
|
|
572
|
-
else "not_configured"
|
|
573
|
-
)
|
|
574
|
-
if reranker_ready and facts:
|
|
575
|
-
ce_alpha = 0.5 if strat.query_type in ("multi_hop", "temporal") else 0.75
|
|
576
|
-
top, reranker_applied, reranker_status = self._apply_reranker(
|
|
577
|
-
query, top, facts, alpha=ce_alpha,
|
|
578
|
-
)
|
|
579
|
-
elif reranker_ready:
|
|
580
|
-
reranker_status = "no_candidates"
|
|
581
|
-
_em(f"rerank(ready={reranker_ready})")
|
|
582
|
-
|
|
583
|
-
# V3.4.11: Channel diversity — guarantee entity_graph results appear in
|
|
584
|
-
# the final output. Applied AFTER reranking and evidence qualification
|
|
585
|
-
# so an associative-only candidate cannot be reintroduced after the gate.
|
|
586
|
-
final_top = top[:effective_limit]
|
|
587
|
-
final_top = self._enforce_channel_diversity(
|
|
588
|
-
final_top, fused, ch_results, effective_limit,
|
|
589
|
-
)
|
|
590
|
-
|
|
591
|
-
# A channel-diversity promotion may come from outside the rerank pool.
|
|
592
|
-
# Load only when that happens; ordinary recalls reuse the existing map.
|
|
593
|
-
if any(fr.fact_id not in facts for fr in final_top):
|
|
594
|
-
facts.update(self._load_facts(
|
|
595
|
-
final_top,
|
|
596
|
-
profile_id,
|
|
597
|
-
include_global=include_global,
|
|
598
|
-
include_shared=include_shared,
|
|
599
|
-
))
|
|
600
|
-
|
|
601
|
-
# Trim facts to the selected, qualified result set.
|
|
602
|
-
selected_ids = {fr.fact_id for fr in final_top}
|
|
603
|
-
facts = {fid: f for fid, f in facts.items() if fid in selected_ids}
|
|
604
|
-
|
|
605
|
-
# 6. Build response
|
|
606
|
-
results = self._build_results(final_top, facts, strat)
|
|
607
|
-
ms = (time.monotonic() - t0) * 1000.0
|
|
608
|
-
no_match = floor_enabled and len(results) == 0
|
|
609
|
-
return RecallResponse(
|
|
610
|
-
query=query, mode=mode, results=results,
|
|
611
|
-
query_type=strat.query_type, channel_weights=strat.weights,
|
|
612
|
-
total_candidates=total, retrieval_time_ms=ms,
|
|
613
|
-
no_confident_match=no_match,
|
|
614
|
-
reranker_applied=reranker_applied,
|
|
615
|
-
reranker_status=reranker_status,
|
|
616
|
-
# Q2b: thematic context when the top results cluster in one
|
|
617
|
-
# community. Precomputed summary lookup only — no per-query LLM.
|
|
618
|
-
community_context=self._community_context(results, profile_id),
|
|
619
|
-
incomplete_channels=tuple(sorted(dropped_channels)),
|
|
620
|
-
channel_status=dict(channel_status),
|
|
621
|
-
)
|
|
622
|
-
|
|
623
|
-
# -- Community context (Wave Q2b) --------------------------------------
|
|
624
|
-
|
|
625
|
-
def _community_context(
|
|
626
|
-
self, results: list[Any], profile_id: str, top_k: int = 8,
|
|
627
|
-
) -> dict | None:
|
|
628
|
-
"""Attach the precomputed community summary the top results fall into.
|
|
629
|
-
|
|
630
|
-
On-device-safe (market CRIT-1): a single read of the ≤N precomputed
|
|
631
|
-
community_summaries rows + a membership tally — never a per-query LLM
|
|
632
|
-
fan-out. Gated: fires only when >=2 of the top results AND >=40% of
|
|
633
|
-
them belong to one community, so precise factual queries are untouched.
|
|
634
|
-
Fail-open: any error returns None (recall is never affected).
|
|
635
|
-
"""
|
|
636
|
-
if not results or not getattr(
|
|
637
|
-
self._config, "enable_community_context", True,
|
|
638
|
-
):
|
|
639
|
-
return None
|
|
640
|
-
try:
|
|
641
|
-
import json
|
|
642
|
-
from collections import Counter
|
|
643
|
-
|
|
644
|
-
rows = [
|
|
645
|
-
dict(r) for r in self._db.execute(
|
|
646
|
-
"SELECT community_id, summary, keywords, fact_ids_json, "
|
|
647
|
-
"fact_count FROM community_summaries WHERE profile_id = ?",
|
|
648
|
-
(profile_id,),
|
|
649
|
-
)
|
|
650
|
-
]
|
|
651
|
-
if not rows:
|
|
652
|
-
return None
|
|
653
|
-
|
|
654
|
-
fact_to_cid: dict[str, int] = {}
|
|
655
|
-
summ_by_cid: dict[int, dict] = {}
|
|
656
|
-
for r in rows:
|
|
657
|
-
cid = int(r["community_id"])
|
|
658
|
-
summ_by_cid[cid] = r
|
|
659
|
-
try:
|
|
660
|
-
for fid in json.loads(r.get("fact_ids_json") or "[]"):
|
|
661
|
-
fact_to_cid[str(fid)] = cid
|
|
662
|
-
except (ValueError, TypeError):
|
|
663
|
-
continue
|
|
664
|
-
|
|
665
|
-
top_ids = [
|
|
666
|
-
res.fact.fact_id
|
|
667
|
-
for res in results[:top_k]
|
|
668
|
-
if getattr(res, "fact", None) is not None
|
|
669
|
-
]
|
|
670
|
-
tally = Counter(
|
|
671
|
-
fact_to_cid[fid] for fid in top_ids if fid in fact_to_cid
|
|
672
|
-
)
|
|
673
|
-
if not tally:
|
|
674
|
-
return None
|
|
675
|
-
best_cid, count = tally.most_common(1)[0]
|
|
676
|
-
coverage = count / len(top_ids) if top_ids else 0.0
|
|
677
|
-
if count < 2 or coverage < 0.4:
|
|
678
|
-
return None
|
|
679
|
-
|
|
680
|
-
row = summ_by_cid[best_cid]
|
|
681
|
-
try:
|
|
682
|
-
members = json.loads(row.get("fact_ids_json") or "[]")
|
|
683
|
-
except (ValueError, TypeError):
|
|
684
|
-
members = []
|
|
685
|
-
return {
|
|
686
|
-
"community_id": best_cid,
|
|
687
|
-
"summary": row.get("summary", ""),
|
|
688
|
-
"keywords": row.get("keywords", ""),
|
|
689
|
-
"member_fact_ids": members,
|
|
690
|
-
"coverage": round(coverage, 3),
|
|
691
|
-
"matched_results": count,
|
|
692
|
-
}
|
|
693
|
-
except Exception as exc:
|
|
694
|
-
logger.debug("community context skipped (fail-open): %s", exc)
|
|
695
|
-
return None
|
|
696
|
-
|
|
697
|
-
# -- Evidence floor (v3.6.6) -------------------------------------------
|
|
698
|
-
|
|
699
|
-
@staticmethod
|
|
700
|
-
def _apply_evidence_floor(
|
|
701
|
-
final_top: list[FusionResult],
|
|
702
|
-
facts: dict[str, AtomicFact],
|
|
703
|
-
min_semantic: float,
|
|
704
|
-
) -> list[FusionResult]:
|
|
705
|
-
"""Filter results that earned no channel evidence.
|
|
706
|
-
|
|
707
|
-
Keep a result only if it earned:
|
|
708
|
-
- semantic cosine >= min_semantic (default 0.60), OR
|
|
709
|
-
- bm25 > 0, OR entity_graph > 0, OR temporal > 0, OR
|
|
710
|
-
- the underlying fact is pinned.
|
|
711
|
-
|
|
712
|
-
spreading_activation and hopfield do NOT count as primary evidence.
|
|
713
|
-
Empty result after filtering is a success (no_confident_match=True).
|
|
714
|
-
"""
|
|
715
|
-
kept: list[FusionResult] = []
|
|
716
|
-
for fr in final_top:
|
|
717
|
-
cs = fr.channel_scores or {}
|
|
718
|
-
# Primary channel evidence check
|
|
719
|
-
if (
|
|
720
|
-
cs.get("semantic", 0.0) >= min_semantic
|
|
721
|
-
or cs.get("bm25", 0.0) > 0.0
|
|
722
|
-
or cs.get("entity_graph", 0.0) > 0.0
|
|
723
|
-
or cs.get("temporal", 0.0) > 0.0
|
|
724
|
-
):
|
|
725
|
-
kept.append(fr)
|
|
726
|
-
continue
|
|
727
|
-
# Pinned fact bypass — always pass regardless of channel scores
|
|
728
|
-
fact = facts.get(fr.fact_id)
|
|
729
|
-
if fact is not None and getattr(fact, "pinned", False):
|
|
730
|
-
kept.append(fr)
|
|
731
|
-
return kept
|
|
732
|
-
|
|
733
|
-
# -- Cross-channel intersection boost -----------------------------------
|
|
734
|
-
|
|
735
|
-
@staticmethod
|
|
736
|
-
def _apply_cross_channel_intersection(
|
|
737
|
-
fused: list[FusionResult],
|
|
738
|
-
ch_results: dict[str, list[tuple[str, float]]],
|
|
739
|
-
strat: QueryStrategy,
|
|
740
|
-
) -> list[FusionResult]:
|
|
741
|
-
"""Boost facts that appear across multiple signal-type channels.
|
|
742
|
-
|
|
743
|
-
V3.3.21: Solves the channel isolation problem. When a query has both
|
|
744
|
-
entity and temporal signals (e.g., "When did Caroline go to X?"), facts
|
|
745
|
-
matching BOTH dimensions should rank higher than facts matching only one.
|
|
746
|
-
|
|
747
|
-
Channel groups:
|
|
748
|
-
- content: semantic, bm25 (text similarity)
|
|
749
|
-
- structure: entity_graph, spreading_activation (graph structure)
|
|
750
|
-
- temporal: temporal (date proximity)
|
|
751
|
-
- associative: hopfield (pattern completion)
|
|
752
|
-
|
|
753
|
-
Boost: facts in 2+ groups get 1.5x, facts in 3+ groups get 2.0x.
|
|
754
|
-
"""
|
|
755
|
-
# Map channels to signal groups
|
|
756
|
-
_CHANNEL_GROUPS = {
|
|
757
|
-
"semantic": "content", "bm25": "content",
|
|
758
|
-
"entity_graph": "structure", "spreading_activation": "structure",
|
|
759
|
-
"temporal": "temporal",
|
|
760
|
-
"hopfield": "associative",
|
|
761
|
-
"profile": "content",
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
# Build fact_id -> set of signal groups it appears in
|
|
765
|
-
fact_groups: dict[str, set[str]] = {}
|
|
766
|
-
for ch_name, results in ch_results.items():
|
|
767
|
-
group = _CHANNEL_GROUPS.get(ch_name, ch_name)
|
|
768
|
-
for fid, _score in results:
|
|
769
|
-
if fid not in fact_groups:
|
|
770
|
-
fact_groups[fid] = set()
|
|
771
|
-
fact_groups[fid].add(group)
|
|
772
|
-
|
|
773
|
-
# Apply boost based on cross-group coverage
|
|
774
|
-
boosted: list[FusionResult] = []
|
|
775
|
-
for fr in fused:
|
|
776
|
-
groups = fact_groups.get(fr.fact_id, set())
|
|
777
|
-
n_groups = len(groups)
|
|
778
|
-
if n_groups >= 3:
|
|
779
|
-
boost = 2.0
|
|
780
|
-
elif n_groups >= 2:
|
|
781
|
-
# Extra boost for temporal+structure intersection (the exact gap)
|
|
782
|
-
if "temporal" in groups and "structure" in groups:
|
|
783
|
-
boost = 1.8
|
|
784
|
-
else:
|
|
785
|
-
boost = 1.5
|
|
786
|
-
else:
|
|
787
|
-
boost = 1.0
|
|
788
|
-
boosted.append(FusionResult(
|
|
789
|
-
fact_id=fr.fact_id,
|
|
790
|
-
fused_score=fr.fused_score * boost,
|
|
791
|
-
channel_ranks=fr.channel_ranks,
|
|
792
|
-
channel_scores=fr.channel_scores,
|
|
793
|
-
))
|
|
794
|
-
boosted.sort(key=lambda r: (-r.fused_score, r.fact_id))
|
|
795
|
-
return boosted
|
|
796
|
-
|
|
797
|
-
# -- Session diversity enforcement ----------------------------------------
|
|
798
|
-
|
|
799
|
-
@staticmethod
|
|
800
|
-
def _enforce_session_diversity(
|
|
801
|
-
fused: list[FusionResult],
|
|
802
|
-
fact_map: dict[str, AtomicFact],
|
|
803
|
-
min_sessions: int = 3,
|
|
804
|
-
top_k: int = 20,
|
|
805
|
-
) -> list[FusionResult]:
|
|
806
|
-
"""Ensure top-k results span at least min_sessions different session_ids.
|
|
807
|
-
|
|
808
|
-
V3.3.21: Category 1 (aggregation) needs facts from MULTIPLE sessions —
|
|
809
|
-
95.7% of cat 1 questions require cross-session evidence. Without this,
|
|
810
|
-
top-20 may cluster around 1-2 sessions, missing scattered mentions.
|
|
811
|
-
|
|
812
|
-
Algorithm: if top-k has < min_sessions, promote the highest-scored facts
|
|
813
|
-
from underrepresented sessions into the top-k window.
|
|
814
|
-
"""
|
|
815
|
-
if len(fused) <= top_k:
|
|
816
|
-
return fused
|
|
817
|
-
|
|
818
|
-
top = fused[:top_k]
|
|
819
|
-
rest = fused[top_k:]
|
|
820
|
-
|
|
821
|
-
sessions_in_top: set[str] = set()
|
|
822
|
-
for fr in top:
|
|
823
|
-
fact = fact_map.get(fr.fact_id)
|
|
824
|
-
if fact and fact.session_id:
|
|
825
|
-
sessions_in_top.add(fact.session_id)
|
|
826
|
-
|
|
827
|
-
if len(sessions_in_top) >= min_sessions:
|
|
828
|
-
return fused
|
|
829
|
-
|
|
830
|
-
promoted: list[FusionResult] = []
|
|
831
|
-
for fr in rest:
|
|
832
|
-
fact = fact_map.get(fr.fact_id)
|
|
833
|
-
if fact and fact.session_id and fact.session_id not in sessions_in_top:
|
|
834
|
-
sessions_in_top.add(fact.session_id)
|
|
835
|
-
promoted.append(fr)
|
|
836
|
-
if len(sessions_in_top) >= min_sessions:
|
|
837
|
-
break
|
|
838
|
-
|
|
839
|
-
if not promoted:
|
|
840
|
-
return fused
|
|
841
|
-
|
|
842
|
-
promoted_ids = {fr.fact_id for fr in promoted}
|
|
843
|
-
remaining = [fr for fr in rest if fr.fact_id not in promoted_ids]
|
|
844
|
-
return top + promoted + remaining
|
|
845
|
-
|
|
846
|
-
# -- Channel diversity enforcement ----------------------------------------
|
|
847
|
-
|
|
848
|
-
@staticmethod
|
|
849
|
-
def _enforce_channel_diversity(
|
|
850
|
-
top: list,
|
|
851
|
-
fused: list,
|
|
852
|
-
ch_results: dict[str, list[tuple[str, float]]],
|
|
853
|
-
effective_limit: int,
|
|
854
|
-
) -> list:
|
|
855
|
-
"""Keep strong lexical and structure evidence visible in the result cap.
|
|
856
|
-
|
|
857
|
-
A semantic channel with a larger weight can fill a small result limit
|
|
858
|
-
even when BM25 has an exact, high-signal hit. That broke the
|
|
859
|
-
``queryable now`` ingestion contract: a freshly inserted FTS row could
|
|
860
|
-
exist durably but remain invisible to immediate recall. Reserve one
|
|
861
|
-
capped slot for a strong BM25 hit and two for a structure channel when
|
|
862
|
-
such candidates exist, without returning more than ``effective_limit``.
|
|
863
|
-
"""
|
|
864
|
-
channel_minimums = (
|
|
865
|
-
("bm25", 1, 0.0),
|
|
866
|
-
("entity_graph", 2, 0.0),
|
|
867
|
-
)
|
|
868
|
-
top_ids = {fr.fact_id for fr in top}
|
|
869
|
-
|
|
870
|
-
promoted = []
|
|
871
|
-
for ch_name, minimum, score_floor in channel_minimums:
|
|
872
|
-
ch_items = ch_results.get(ch_name, [])
|
|
873
|
-
if not ch_items:
|
|
874
|
-
continue
|
|
875
|
-
|
|
876
|
-
eligible_ids = {
|
|
877
|
-
fid
|
|
878
|
-
for fid, score in ch_items
|
|
879
|
-
if (
|
|
880
|
-
float(score) > score_floor
|
|
881
|
-
if ch_name == "bm25"
|
|
882
|
-
else float(score) >= score_floor
|
|
883
|
-
)
|
|
884
|
-
}
|
|
885
|
-
if not eligible_ids:
|
|
886
|
-
continue
|
|
887
|
-
|
|
888
|
-
present = sum(1 for fid in eligible_ids if fid in top_ids)
|
|
889
|
-
if present >= minimum:
|
|
890
|
-
continue
|
|
891
|
-
|
|
892
|
-
needed = minimum - present
|
|
893
|
-
for fr in fused:
|
|
894
|
-
if fr.fact_id in eligible_ids and fr.fact_id not in top_ids:
|
|
895
|
-
promoted.append(fr)
|
|
896
|
-
top_ids.add(fr.fact_id)
|
|
897
|
-
needed -= 1
|
|
898
|
-
if needed <= 0:
|
|
899
|
-
break
|
|
900
|
-
|
|
901
|
-
if not promoted:
|
|
902
|
-
return top
|
|
903
|
-
|
|
904
|
-
selected = promoted[:effective_limit]
|
|
905
|
-
result = list(top[:effective_limit])
|
|
906
|
-
free_slots = max(0, effective_limit - len(result))
|
|
907
|
-
result.extend(selected[:free_slots])
|
|
908
|
-
remaining = selected[free_slots:]
|
|
909
|
-
if remaining:
|
|
910
|
-
keep = max(0, effective_limit - len(remaining))
|
|
911
|
-
result = result[:keep] + remaining
|
|
912
|
-
return result[:effective_limit]
|
|
913
|
-
|
|
914
|
-
# -- Channel execution --------------------------------------------------
|
|
915
|
-
|
|
916
|
-
def _embed_query(self, query: str) -> list[float] | None:
|
|
917
|
-
"""Embed query with LRU cache. Avoids redundant Ollama/API calls."""
|
|
918
|
-
if self._embedder is None:
|
|
919
|
-
return None
|
|
920
|
-
cached = self._query_embedding_cache.get(query)
|
|
921
|
-
if cached is not None:
|
|
922
|
-
return cached
|
|
923
|
-
emb = self._embedder.embed(query)
|
|
924
|
-
# Evict oldest if cache full
|
|
925
|
-
if len(self._query_embedding_cache) >= self._cache_max_size:
|
|
926
|
-
oldest = next(iter(self._query_embedding_cache))
|
|
927
|
-
del self._query_embedding_cache[oldest]
|
|
928
|
-
self._query_embedding_cache[query] = emb
|
|
929
|
-
return emb
|
|
930
|
-
|
|
931
|
-
def _semantic_rank_for_unenriched(
|
|
932
|
-
self, ch_results: dict[str, list[tuple[str, float]]],
|
|
933
|
-
) -> dict[str, list[tuple[str, float]]]:
|
|
934
|
-
"""Give a candidate whose vector does not exist yet a fair semantic rank.
|
|
935
|
-
|
|
936
|
-
Fusion here is rank-based, so a fact the semantic channel did not return
|
|
937
|
-
forfeits that channel's entire contribution — the most heavily weighted
|
|
938
|
-
one. When the reason for that absence is simply that the vector has not
|
|
939
|
-
been computed yet, the absence describes the ingest pipeline and says
|
|
940
|
-
nothing about the fact. Left alone, a memory written seconds ago is the
|
|
941
|
-
hardest thing in the store to find, which is the worst possible failure
|
|
942
|
-
for this product.
|
|
943
|
-
|
|
944
|
-
Such candidates are placed at the MEDIAN of the semantic ranking, never
|
|
945
|
-
near the top: enough to compete on their other evidence, not enough to
|
|
946
|
-
win on freshness alone. A candidate that HAS a vector and still was not
|
|
947
|
-
returned is left exactly as it is — that absence is real evidence of
|
|
948
|
-
irrelevance, and the two must not be confused.
|
|
949
|
-
|
|
950
|
-
Returns a new mapping; the input is not modified.
|
|
951
|
-
"""
|
|
952
|
-
sem = ch_results.get("semantic") or []
|
|
953
|
-
if not sem:
|
|
954
|
-
return ch_results
|
|
955
|
-
if not getattr(self._config, "write_recency_floor_enabled", True):
|
|
956
|
-
return ch_results
|
|
957
|
-
if os.environ.get("SLM_WRITE_RECENCY_NO_FLOOR", "0") == "1":
|
|
958
|
-
return ch_results
|
|
959
|
-
|
|
960
|
-
have = {fid for fid, _ in sem}
|
|
961
|
-
elsewhere = {
|
|
962
|
-
fid
|
|
963
|
-
for name, rows in ch_results.items()
|
|
964
|
-
if name != "semantic"
|
|
965
|
-
for fid, _ in rows
|
|
966
|
-
}
|
|
967
|
-
candidates = sorted(elsewhere - have)
|
|
968
|
-
if not candidates:
|
|
969
|
-
return ch_results
|
|
970
|
-
|
|
971
|
-
from datetime import UTC, datetime, timedelta
|
|
972
|
-
|
|
973
|
-
minutes = float(getattr(self._config, "write_recency_floor_minutes", 60.0))
|
|
974
|
-
cutoff = (datetime.now(UTC) - timedelta(minutes=minutes)).isoformat()
|
|
975
|
-
placeholders = ",".join("?" for _ in candidates)
|
|
976
|
-
try:
|
|
977
|
-
# A missing embedding_metadata row means no vector projection exists,
|
|
978
|
-
# which is what makes the semantic channel's silence uninformative.
|
|
979
|
-
unenriched = [
|
|
980
|
-
dict(r)["fact_id"]
|
|
981
|
-
for r in self._db.execute(
|
|
982
|
-
f"SELECT af.fact_id FROM atomic_facts AS af "
|
|
983
|
-
f"LEFT JOIN embedding_metadata AS em ON em.fact_id = af.fact_id "
|
|
984
|
-
f"WHERE af.fact_id IN ({placeholders}) "
|
|
985
|
-
f" AND em.fact_id IS NULL "
|
|
986
|
-
f" AND af.created_at >= ?",
|
|
987
|
-
(*candidates, cutoff),
|
|
988
|
-
)
|
|
989
|
-
]
|
|
990
|
-
except (NameError, AttributeError, TypeError):
|
|
991
|
-
# These mean this code is wrong, not that the data is unusual. A bare
|
|
992
|
-
# `except Exception` here hid a missing import and left the whole
|
|
993
|
-
# feature silently inert while every test still passed.
|
|
994
|
-
raise
|
|
995
|
-
except Exception as exc:
|
|
996
|
-
# A store without this table, or a locked database: ranking must still
|
|
997
|
-
# return. Logged at warning, because "silently did nothing" is the
|
|
998
|
-
# failure mode this task exists to fix.
|
|
999
|
-
logger.warning("recent-unenriched admission skipped: %s: %s",
|
|
1000
|
-
type(exc).__name__, exc)
|
|
1001
|
-
return ch_results
|
|
1002
|
-
if not unenriched:
|
|
1003
|
-
return ch_results
|
|
1004
|
-
|
|
1005
|
-
scores = sorted(s for _, s in sem)
|
|
1006
|
-
mid = len(scores) // 2
|
|
1007
|
-
median = (
|
|
1008
|
-
scores[mid] if len(scores) % 2 == 1
|
|
1009
|
-
else (scores[mid - 1] + scores[mid]) / 2.0
|
|
1010
|
-
)
|
|
1011
|
-
insert_at = len(sem) // 2
|
|
1012
|
-
merged = list(sem[:insert_at]) + [(fid, median) for fid in unenriched] + list(sem[insert_at:])
|
|
1013
|
-
logger.debug(
|
|
1014
|
-
"admitted %d recent un-enriched candidate(s) at semantic rank %d of %d",
|
|
1015
|
-
len(unenriched), insert_at + 1, len(merged),
|
|
1016
|
-
)
|
|
1017
|
-
return {**ch_results, "semantic": merged}
|
|
1018
|
-
|
|
1019
|
-
def _run_channels(
|
|
1020
|
-
self,
|
|
1021
|
-
query: str,
|
|
1022
|
-
profile_id: str,
|
|
1023
|
-
strat: QueryStrategy,
|
|
1024
|
-
*,
|
|
1025
|
-
extra_disabled_channels: set[str] | None = None,
|
|
1026
|
-
include_global: bool = False,
|
|
1027
|
-
include_shared: bool = False,
|
|
1028
|
-
as_of: str | None = None,
|
|
1029
|
-
known_as_of: str | None = None,
|
|
1030
|
-
valid_at: str | None = None,
|
|
1031
|
-
include_unknown: bool = False,
|
|
1032
|
-
dropped_channels: set[str] | None = None,
|
|
1033
|
-
channel_status: dict[str, str] | None = None,
|
|
1034
|
-
) -> dict[str, list[tuple[str, float]]]:
|
|
1035
|
-
"""Run active retrieval channels.
|
|
1036
|
-
|
|
1037
|
-
v3.4.53: channels run in PARALLEL via ThreadPoolExecutor. Industry
|
|
1038
|
-
standard (EverMemOS, szl-recall, ContentPilot 2026): all channels
|
|
1039
|
-
are independent after embedding; running them serially wastes time
|
|
1040
|
-
equal to the sum of all producer latencies. When multiple producers are
|
|
1041
|
-
enabled and healthy, parallel dispatch generally bounds the producer
|
|
1042
|
-
phase by the slowest submitted producer, plus serial embedding and
|
|
1043
|
-
result-collection overhead.
|
|
1044
|
-
|
|
1045
|
-
``dropped_channels``, when given, receives the name of every channel
|
|
1046
|
-
abandoned at ``CHANNEL_HANG_GUARD_SECONDS``. Those channels contributed
|
|
1047
|
-
nothing, so the caller needs to know the answer is incomplete rather
|
|
1048
|
-
than merely late. It is a caller-owned set passed down per recall and
|
|
1049
|
-
deliberately not an attribute of self — two concurrent recalls sharing
|
|
1050
|
-
one would report each other's losses (the v3.4.64 race).
|
|
1051
|
-
|
|
1052
|
-
``channel_status``, likewise caller-owned, receives one entry per
|
|
1053
|
-
channel saying what became of it. Every channel gets exactly one:
|
|
1054
|
-
those that cannot run are recorded before dispatch with the reason, and
|
|
1055
|
-
every dispatched channel is recorded by the collection loop below —
|
|
1056
|
-
which iterates the futures, so it cannot skip one.
|
|
1057
|
-
"""
|
|
1058
|
-
import os as _os_e
|
|
1059
|
-
import time as _time_e
|
|
1060
|
-
_et = bool(_os_e.environ.get("SLM_RECALL_TIMING"))
|
|
1061
|
-
out: dict[str, list[tuple[str, float]]] = {}
|
|
1062
|
-
# Skip channels listed in disabled_channels (ablation support)
|
|
1063
|
-
# V3.4.40: union with per-recall extra_disabled set (e.g. --fast skip)
|
|
1064
|
-
# V3.4.64: extra_disabled is now a local parameter, not a shared instance
|
|
1065
|
-
# attribute — eliminates the concurrent-recall race condition.
|
|
1066
|
-
disabled = set(self._config.disabled_channels) | set(extra_disabled_channels or ())
|
|
1067
|
-
|
|
1068
|
-
# V3.3.4: Embed query ONCE, reuse for semantic + hopfield channels
|
|
1069
|
-
q_emb: list[float] | None = None
|
|
1070
|
-
needs_embedding = (
|
|
1071
|
-
(self._semantic is not None and "semantic" not in disabled)
|
|
1072
|
-
or (self._hopfield is not None and "hopfield" not in disabled)
|
|
1073
|
-
or (self._spreading_activation is not None and "spreading_activation" not in disabled)
|
|
1074
|
-
)
|
|
1075
|
-
if needs_embedding:
|
|
1076
|
-
try:
|
|
1077
|
-
q_emb = self._embed_query(query)
|
|
1078
|
-
if q_emb is None:
|
|
1079
|
-
logger.warning(
|
|
1080
|
-
"Query embedding returned None — semantic, hopfield, "
|
|
1081
|
-
"spreading_activation channels will be skipped this recall"
|
|
1082
|
-
)
|
|
1083
|
-
except Exception as exc:
|
|
1084
|
-
logger.warning("Query embedding failed: %s", exc)
|
|
1085
|
-
|
|
1086
|
-
# Why a channel will not run, recorded BEFORE dispatch. An embedding
|
|
1087
|
-
# failure silently takes three of the five channels down together, and
|
|
1088
|
-
# the answer never said so: it looked exactly like a store with nothing
|
|
1089
|
-
# relevant in it. Configuration and ablation are recorded too, so an
|
|
1090
|
-
# operator reading a list of absent channels can tell their own choices
|
|
1091
|
-
# apart from a fault.
|
|
1092
|
-
if channel_status is not None:
|
|
1093
|
-
for _name, _obj, _needs_emb in (
|
|
1094
|
-
("semantic", self._semantic, True),
|
|
1095
|
-
("bm25", self._bm25, False),
|
|
1096
|
-
("temporal", self._temporal, False),
|
|
1097
|
-
("hopfield", self._hopfield, True),
|
|
1098
|
-
("spreading_activation", self._spreading_activation, True),
|
|
1099
|
-
):
|
|
1100
|
-
if _obj is None:
|
|
1101
|
-
channel_status[_name] = chstat.NOT_CONFIGURED
|
|
1102
|
-
elif _name in disabled:
|
|
1103
|
-
channel_status[_name] = chstat.DISABLED
|
|
1104
|
-
elif _needs_emb and q_emb is None:
|
|
1105
|
-
channel_status[_name] = chstat.NO_EMBEDDING
|
|
1106
|
-
|
|
1107
|
-
# v3.4.53: collect channel callables and run in parallel.
|
|
1108
|
-
# Each channel is a standalone search — no shared mutable state,
|
|
1109
|
-
# no ordering dependencies. SQLite WAL mode permits concurrent reads.
|
|
1110
|
-
futures: dict[str, concurrent.futures.Future] = {}
|
|
1111
|
-
|
|
1112
|
-
def _safe_channel(name: str, fn, *args):
|
|
1113
|
-
"""Run a single channel, returning (name, result_or_None, status).
|
|
1114
|
-
|
|
1115
|
-
Returning the status alongside the result is what separates "found
|
|
1116
|
-
nothing" from "raised": both used to come back as ``None``.
|
|
1117
|
-
"""
|
|
1118
|
-
_cs = _time_e.monotonic() if _et else 0.0
|
|
1119
|
-
try:
|
|
1120
|
-
res = fn(*args)
|
|
1121
|
-
if _et:
|
|
1122
|
-
logger.warning("[RECALL-TIMING] channel.%-16s %.0f ms",
|
|
1123
|
-
name, (_time_e.monotonic() - _cs) * 1000.0)
|
|
1124
|
-
if res:
|
|
1125
|
-
return (name, res, chstat.OK)
|
|
1126
|
-
return (name, None, chstat.EMPTY)
|
|
1127
|
-
except Exception as exc:
|
|
1128
|
-
logger.warning("%s channel: %s", name, exc)
|
|
1129
|
-
return (name, None, chstat.ERROR)
|
|
1130
|
-
|
|
1131
|
-
executor = self._channel_executor
|
|
1132
|
-
if self._semantic is not None and q_emb is not None and "semantic" not in disabled:
|
|
1133
|
-
futures["semantic"] = executor.submit(
|
|
1134
|
-
_safe_channel, "semantic",
|
|
1135
|
-
functools.partial(
|
|
1136
|
-
self._semantic.search,
|
|
1137
|
-
include_global=include_global, include_shared=include_shared,
|
|
1138
|
-
),
|
|
1139
|
-
q_emb, profile_id, self._config.semantic_top_k,
|
|
1140
|
-
)
|
|
1141
|
-
if self._bm25 is not None and "bm25" not in disabled:
|
|
1142
|
-
futures["bm25"] = executor.submit(
|
|
1143
|
-
_safe_channel, "bm25",
|
|
1144
|
-
functools.partial(
|
|
1145
|
-
self._bm25.search,
|
|
1146
|
-
include_global=include_global, include_shared=include_shared,
|
|
1147
|
-
),
|
|
1148
|
-
query, profile_id, self._config.bm25_top_k,
|
|
1149
|
-
)
|
|
1150
|
-
if self._temporal is not None and "temporal" not in disabled:
|
|
1151
|
-
futures["temporal"] = executor.submit(
|
|
1152
|
-
_safe_channel, "temporal",
|
|
1153
|
-
functools.partial(
|
|
1154
|
-
self._temporal.search,
|
|
1155
|
-
include_global=include_global, include_shared=include_shared,
|
|
1156
|
-
query_type=strat.query_type,
|
|
1157
|
-
),
|
|
1158
|
-
query, profile_id, self._config.bm25_top_k,
|
|
1159
|
-
)
|
|
1160
|
-
if self._hopfield is not None and q_emb is not None and "hopfield" not in disabled:
|
|
1161
|
-
futures["hopfield"] = executor.submit(
|
|
1162
|
-
_safe_channel, "hopfield",
|
|
1163
|
-
functools.partial(
|
|
1164
|
-
self._hopfield.search,
|
|
1165
|
-
include_global=include_global, include_shared=include_shared,
|
|
1166
|
-
),
|
|
1167
|
-
q_emb, profile_id, self._config.hopfield_top_k,
|
|
1168
|
-
)
|
|
1169
|
-
if (
|
|
1170
|
-
self._spreading_activation is not None
|
|
1171
|
-
and q_emb is not None
|
|
1172
|
-
and "spreading_activation" not in disabled
|
|
1173
|
-
):
|
|
1174
|
-
futures["spreading_activation"] = executor.submit(
|
|
1175
|
-
_safe_channel, "spreading_activation",
|
|
1176
|
-
functools.partial(
|
|
1177
|
-
self._spreading_activation.search,
|
|
1178
|
-
include_global=include_global, include_shared=include_shared,
|
|
1179
|
-
),
|
|
1180
|
-
q_emb, profile_id, self._config.bm25_top_k,
|
|
1181
|
-
)
|
|
1182
|
-
|
|
1183
|
-
# One shared limit keeps parallel dispatch genuinely bounded. A
|
|
1184
|
-
# per-future timeout here would serialise the wait and turn five slow
|
|
1185
|
-
# channels into five seconds of UI latency.
|
|
1186
|
-
done, pending = concurrent.futures.wait(
|
|
1187
|
-
futures.values(), timeout=CHANNEL_HANG_GUARD_SECONDS,
|
|
1188
|
-
)
|
|
1189
|
-
for name, fut in futures.items():
|
|
1190
|
-
if fut in pending:
|
|
1191
|
-
# Not a latency notice: this answer is missing whatever this
|
|
1192
|
-
# channel alone could see, so it is logged at the level that
|
|
1193
|
-
# says so and recorded for the caller.
|
|
1194
|
-
logger.error(
|
|
1195
|
-
"Channel %s did not finish within %.1fs; this recall is "
|
|
1196
|
-
"answering without it",
|
|
1197
|
-
name, CHANNEL_HANG_GUARD_SECONDS,
|
|
1198
|
-
)
|
|
1199
|
-
if dropped_channels is not None:
|
|
1200
|
-
dropped_channels.add(name)
|
|
1201
|
-
# Same branch as the dropped set on purpose: two writes in one
|
|
1202
|
-
# place cannot disagree about which channels timed out.
|
|
1203
|
-
if channel_status is not None:
|
|
1204
|
-
channel_status[name] = chstat.TIMEOUT
|
|
1205
|
-
fut.cancel() # no-op if already running; prevents queued jobs from starting
|
|
1206
|
-
continue
|
|
1207
|
-
try:
|
|
1208
|
-
ch_name, result, status = fut.result()
|
|
1209
|
-
if channel_status is not None:
|
|
1210
|
-
channel_status[ch_name] = status
|
|
1211
|
-
if result:
|
|
1212
|
-
out[ch_name] = result
|
|
1213
|
-
except Exception as exc:
|
|
1214
|
-
logger.warning("Channel %s failed: %s", name, exc)
|
|
1215
|
-
if channel_status is not None:
|
|
1216
|
-
channel_status[name] = chstat.ERROR
|
|
1217
|
-
|
|
1218
|
-
# Apply registered post-retrieval filters (forgetting filter, etc.).
|
|
1219
|
-
# Pass as_of in context dict when set so the bi-temporal validity filter
|
|
1220
|
-
# can perform point-in-time demotion. None context preserves the existing
|
|
1221
|
-
# behaviour for all callers that don't use time-travel recall.
|
|
1222
|
-
_filter_context = {"as_of": as_of} if as_of is not None else None
|
|
1223
|
-
if hasattr(self, '_registry') and self._registry._filters:
|
|
1224
|
-
for fn in self._registry._filters:
|
|
1225
|
-
try:
|
|
1226
|
-
out = fn(out, profile_id, _filter_context)
|
|
1227
|
-
except Exception as exc:
|
|
1228
|
-
logger.warning("Post-retrieval filter failed: %s", exc)
|
|
1229
|
-
|
|
1230
|
-
return out
|
|
1231
|
-
|
|
1232
|
-
def close(self, *, wait: bool = False) -> None:
|
|
1233
|
-
"""Release owned channel workers without blocking daemon shutdown.
|
|
1234
|
-
|
|
1235
|
-
Active channel calls have their own response deadline. Waiting here
|
|
1236
|
-
can still deadlock shutdown when an extension ignores that deadline,
|
|
1237
|
-
so the daemon uses the executor's non-blocking cancellation path.
|
|
1238
|
-
"""
|
|
1239
|
-
with self._close_lock:
|
|
1240
|
-
if self._closed:
|
|
1241
|
-
return
|
|
1242
|
-
self._closed = True
|
|
1243
|
-
self._channel_executor.shutdown(wait=wait, cancel_futures=True)
|
|
1244
|
-
|
|
1245
|
-
# -- Fact loading -------------------------------------------------------
|
|
1246
|
-
|
|
1247
|
-
def _load_facts(
|
|
1248
|
-
self,
|
|
1249
|
-
fused: list[FusionResult],
|
|
1250
|
-
profile_id: str,
|
|
1251
|
-
*,
|
|
1252
|
-
include_global: bool = False,
|
|
1253
|
-
include_shared: bool = False,
|
|
1254
|
-
) -> dict[str, AtomicFact]:
|
|
1255
|
-
"""Load facts by ID — targeted query, not full-table scan.
|
|
1256
|
-
|
|
1257
|
-
V3.3.13: Was loading ALL facts (O(n) memory) then filtering.
|
|
1258
|
-
Now uses get_facts_by_ids() for O(k) where k = pool size (~60).
|
|
1259
|
-
"""
|
|
1260
|
-
needed = [fr.fact_id for fr in fused]
|
|
1261
|
-
if not needed:
|
|
1262
|
-
return {}
|
|
1263
|
-
facts = self._db.get_facts_by_ids(
|
|
1264
|
-
needed, profile_id,
|
|
1265
|
-
include_global=include_global,
|
|
1266
|
-
include_shared=include_shared,
|
|
1267
|
-
)
|
|
1268
|
-
return {f.fact_id: f for f in facts}
|
|
1269
|
-
|
|
1270
|
-
# -- Cross-encoder rerank -----------------------------------------------
|
|
1271
|
-
|
|
1272
|
-
@staticmethod
|
|
1273
|
-
def _sigmoid(x: float) -> float:
|
|
1274
|
-
"""Numerically stable sigmoid."""
|
|
1275
|
-
x = max(-500.0, min(500.0, x))
|
|
1276
|
-
return 1.0 / (1.0 + math.exp(-x))
|
|
1277
|
-
|
|
1278
|
-
def _apply_reranker(
|
|
1279
|
-
self, query: str, fused: list[FusionResult],
|
|
1280
|
-
fact_map: dict[str, AtomicFact],
|
|
1281
|
-
alpha: float = 0.75,
|
|
1282
|
-
) -> tuple[list[FusionResult], bool, str]:
|
|
1283
|
-
"""Rerank with blended CE + RRF scores (Bug 1 fix).
|
|
1284
|
-
|
|
1285
|
-
Blended: alpha * sigmoid(CE_score) + (1 - alpha) * rrf_score.
|
|
1286
|
-
Speaker tags stripped before scoring (Bug 3 fix).
|
|
1287
|
-
"""
|
|
1288
|
-
# Bug 2 fix: score ALL candidates, not just top_k. v3.8.5: verified on
|
|
1289
|
-
# the real DB that bounding the CE to the top-N fusion candidates both
|
|
1290
|
-
# (a) gave NO latency win (the cross-encoder batches all pairs in one
|
|
1291
|
-
# forward pass, so 60 vs 184 pairs is within noise) and (b) CHANGED the
|
|
1292
|
-
# top-5 on 4/8 queries — the CE legitimately promotes items ranked below
|
|
1293
|
-
# the fusion top-N into the answer. So exhaustive reranking stays: it is
|
|
1294
|
-
# a quality feature, not the latency bottleneck.
|
|
1295
|
-
candidates = [
|
|
1296
|
-
(fact_map[fr.fact_id], fr.fused_score)
|
|
1297
|
-
for fr in fused if fr.fact_id in fact_map
|
|
1298
|
-
]
|
|
1299
|
-
if not candidates:
|
|
1300
|
-
return fused, False, "no_candidates"
|
|
1301
|
-
|
|
1302
|
-
# V3.3.16: Strip speaker tags WITHOUT copying full AtomicFact objects.
|
|
1303
|
-
# Previously created full copies including 768-dim embeddings (~6KB each),
|
|
1304
|
-
# which over 304 recalls caused pymalloc arena fragmentation → 25GB.
|
|
1305
|
-
# Now: temporarily patch .content on originals, rerank, then restore.
|
|
1306
|
-
originals: list[tuple[AtomicFact, str]] = [] # (fact, original_content)
|
|
1307
|
-
for fact, _ in candidates:
|
|
1308
|
-
orig = fact.content
|
|
1309
|
-
fact.content = re.sub(r'^\[[A-Za-z]+\]:\s*', '', orig)
|
|
1310
|
-
originals.append((fact, orig))
|
|
1311
|
-
|
|
1312
|
-
try:
|
|
1313
|
-
rerank_with_status = getattr(
|
|
1314
|
-
self._reranker, "rerank_with_status", None,
|
|
1315
|
-
)
|
|
1316
|
-
# MagicMock fabricates arbitrary attributes; only use the richer
|
|
1317
|
-
# contract when it is defined by the reranker type itself.
|
|
1318
|
-
if callable(rerank_with_status) and hasattr(
|
|
1319
|
-
type(self._reranker), "rerank_with_status",
|
|
1320
|
-
):
|
|
1321
|
-
scored, applied, status = rerank_with_status(
|
|
1322
|
-
query, candidates, top_k=len(candidates),
|
|
1323
|
-
)
|
|
1324
|
-
else:
|
|
1325
|
-
scored = self._reranker.rerank( # type: ignore[union-attr]
|
|
1326
|
-
query, candidates, top_k=len(candidates),
|
|
1327
|
-
)
|
|
1328
|
-
applied, status = True, "applied"
|
|
1329
|
-
except Exception as exc:
|
|
1330
|
-
logger.warning("Cross-encoder rerank failed: %s", exc)
|
|
1331
|
-
return fused, False, "error"
|
|
1332
|
-
finally:
|
|
1333
|
-
# Restore original content (with speaker tags)
|
|
1334
|
-
for fact, orig_content in originals:
|
|
1335
|
-
fact.content = orig_content
|
|
1336
|
-
|
|
1337
|
-
if not applied:
|
|
1338
|
-
return fused, False, status
|
|
1339
|
-
|
|
1340
|
-
# The worker can report applied=True while returning scores=null — the
|
|
1341
|
-
# subprocess answers, so the call "succeeded", but there is nothing to
|
|
1342
|
-
# score with. Iterating None here raised TypeError from OUTSIDE the
|
|
1343
|
-
# try/except above (which only wraps the rerank call itself), so the
|
|
1344
|
-
# error escaped into the recall path rather than degrading to the fused
|
|
1345
|
-
# ordering. Fail soft: reranking is a quality improvement on top of a
|
|
1346
|
-
# correct result set, never a correctness requirement.
|
|
1347
|
-
# `not scored` covers None AND an empty sequence. An empty list is the
|
|
1348
|
-
# same defect wearing different clothes: the worker says applied=True but
|
|
1349
|
-
# supplied nothing to rank with. Guarding only None would let [] through
|
|
1350
|
-
# to build an empty score_map, and every candidate would then be scored
|
|
1351
|
-
# against a degenerate min/max — silently shrinking the fused component
|
|
1352
|
-
# by (1 - alpha) while still reporting the rerank as applied.
|
|
1353
|
-
if not scored:
|
|
1354
|
-
logger.warning(
|
|
1355
|
-
"Cross-encoder worker reported applied=True with %s scores; "
|
|
1356
|
-
"falling back to fused ranking for this query.",
|
|
1357
|
-
"null" if scored is None else "empty",
|
|
1358
|
-
)
|
|
1359
|
-
return fused, False, "worker_null_scores"
|
|
1360
|
-
|
|
1361
|
-
score_map = {fact.fact_id: score for fact, score in scored}
|
|
1362
|
-
|
|
1363
|
-
# Min-max normalize CE scores to [0, 1] within the batch instead of
|
|
1364
|
-
# sigmoid (which compresses the useful discrimination range).
|
|
1365
|
-
ce_values = list(score_map.values())
|
|
1366
|
-
ce_min = min(ce_values) if ce_values else 0.0
|
|
1367
|
-
ce_max = max(ce_values) if ce_values else 1.0
|
|
1368
|
-
ce_range = ce_max - ce_min if ce_max > ce_min else 1.0
|
|
1369
|
-
|
|
1370
|
-
# Also normalize RRF scores so both terms contribute meaningfully
|
|
1371
|
-
rrf_values = [fr.fused_score for fr in fused]
|
|
1372
|
-
rrf_max = max(rrf_values) if rrf_values else 1.0
|
|
1373
|
-
rrf_max = rrf_max if rrf_max > 0 else 1.0
|
|
1374
|
-
|
|
1375
|
-
updated = [
|
|
1376
|
-
FusionResult(
|
|
1377
|
-
fact_id=fr.fact_id,
|
|
1378
|
-
fused_score=(
|
|
1379
|
-
alpha * ((score_map.get(fr.fact_id, ce_min) - ce_min) / ce_range)
|
|
1380
|
-
+ (1.0 - alpha) * (fr.fused_score / rrf_max)
|
|
1381
|
-
),
|
|
1382
|
-
channel_ranks=fr.channel_ranks,
|
|
1383
|
-
channel_scores=fr.channel_scores,
|
|
1384
|
-
)
|
|
1385
|
-
for fr in fused
|
|
1386
|
-
]
|
|
1387
|
-
updated.sort(key=lambda r: (-r.fused_score, r.fact_id))
|
|
1388
|
-
return updated, True, "applied"
|
|
1389
|
-
|
|
1390
|
-
# -- Agentic adapter -----------------------------------
|
|
1391
|
-
|
|
1392
|
-
def recall_facts(
|
|
1393
|
-
self, query: str, profile_id: str,
|
|
1394
|
-
top_k: int = 20, skip_agentic: bool = True,
|
|
1395
|
-
) -> list[tuple[AtomicFact, float]]:
|
|
1396
|
-
"""Simplified recall returning (fact, score) tuples.
|
|
1397
|
-
|
|
1398
|
-
Used by AgenticRetriever for round-2 re-retrieval.
|
|
1399
|
-
skip_agentic is always True here to prevent infinite recursion.
|
|
1400
|
-
"""
|
|
1401
|
-
response = self.recall(query, profile_id, limit=top_k)
|
|
1402
|
-
return [(r.fact, r.score) for r in response.results]
|
|
1403
|
-
|
|
1404
|
-
# -- Trust weighting ----------------------------------------------------
|
|
1405
|
-
|
|
1406
|
-
def _get_trust_weight(self, fact: AtomicFact, profile_id: str) -> tuple[float, float]:
|
|
1407
|
-
"""Look up Bayesian trust score and convert to a multiplicative weight.
|
|
1408
|
-
|
|
1409
|
-
Returns (trust_weight, raw_trust_score).
|
|
1410
|
-
trust_weight is clamped to [0.5, 1.5]:
|
|
1411
|
-
- trust=0.0 -> weight=0.5 (demote untrusted facts)
|
|
1412
|
-
- trust=0.5 -> weight=1.0 (neutral, default prior)
|
|
1413
|
-
- trust=1.0 -> weight=1.5 (promote highly trusted facts)
|
|
1414
|
-
If trust scoring is disabled or unavailable, returns (1.0, 0.5).
|
|
1415
|
-
"""
|
|
1416
|
-
if not self._config.use_trust_weighting or self._trust_scorer is None:
|
|
1417
|
-
return 1.0, 0.5
|
|
1418
|
-
|
|
1419
|
-
try:
|
|
1420
|
-
raw = self._trust_scorer.get_fact_trust(fact.fact_id, profile_id)
|
|
1421
|
-
except Exception:
|
|
1422
|
-
return 1.0, 0.5
|
|
1423
|
-
|
|
1424
|
-
# Linear map: trust 0.0->0.5, 0.5->1.0, 1.0->1.5
|
|
1425
|
-
weight = 0.5 + raw # raw in [0, 1] -> weight in [0.5, 1.5]
|
|
1426
|
-
return weight, raw
|
|
1427
|
-
|
|
1428
|
-
# -- Response building --------------------------------------------------
|
|
1429
|
-
|
|
1430
|
-
def _build_results(
|
|
1431
|
-
self, fused: list[FusionResult], fact_map: dict[str, AtomicFact],
|
|
1432
|
-
strat: QueryStrategy,
|
|
1433
|
-
) -> list[RetrievalResult]:
|
|
1434
|
-
from datetime import UTC, datetime
|
|
1435
|
-
now = datetime.now(UTC)
|
|
1436
|
-
results: list[RetrievalResult] = []
|
|
1437
|
-
profile_id = next(
|
|
1438
|
-
(f.profile_id for f in fact_map.values()), "default",
|
|
1439
|
-
)
|
|
1440
|
-
for fr in fused:
|
|
1441
|
-
fact = fact_map.get(fr.fact_id)
|
|
1442
|
-
if fact is None:
|
|
1443
|
-
continue
|
|
1444
|
-
evidence = [
|
|
1445
|
-
f"{ch}(rank={rk}, score={fr.channel_scores.get(ch, 0.0):.4f})"
|
|
1446
|
-
# Channel name breaks a tie, so the evidence string a caller
|
|
1447
|
-
# sees is the same on two runs when two channels agree on rank.
|
|
1448
|
-
for ch, rk in sorted(fr.channel_ranks.items(), key=lambda x: (x[1], x[0]))
|
|
1449
|
-
if rk < 1000
|
|
1450
|
-
]
|
|
1451
|
-
# Recency decay: Ebbinghaus exponential + FSRS stability strengthening (v3.4.51).
|
|
1452
|
-
#
|
|
1453
|
-
# Base: R = e^(-λt), λ = ln(2)/S, S = effective half-life in days.
|
|
1454
|
-
# FSRS v5 (Dae & Jarrett 2024): S grows with successful recall frequency.
|
|
1455
|
-
# S_effective = S_base × min(2.0, 1 + 0.1 × access_count)
|
|
1456
|
-
# → 0 recalls: S=30d 5 recalls: S=45d 10+ recalls: S=60d (max)
|
|
1457
|
-
# Effect: frequently-recalled architectural decisions resist decay naturally;
|
|
1458
|
-
# one-off session handoffs and debug notes decay at full rate.
|
|
1459
|
-
#
|
|
1460
|
-
# Boost range: [0.80×, 1.10×]
|
|
1461
|
-
# 0d, 0acc → 1.10× 45d, 0acc → 0.91× 90d, 0acc → 0.84×
|
|
1462
|
-
# 45d, 5acc → 0.95× 90d, 10acc → 0.90× (frequently used memories stay relevant)
|
|
1463
|
-
age_days = 0.0
|
|
1464
|
-
age_known = False
|
|
1465
|
-
if fact.created_at:
|
|
1466
|
-
try:
|
|
1467
|
-
created = datetime.fromisoformat(fact.created_at.replace("Z", "+00:00"))
|
|
1468
|
-
age_days = max(0.0, (now - created).total_seconds() / 86400.0)
|
|
1469
|
-
age_known = True
|
|
1470
|
-
except (ValueError, TypeError):
|
|
1471
|
-
pass
|
|
1472
|
-
_access = max(0, getattr(fact, "access_count", 0) or 0)
|
|
1473
|
-
_S = 30.0 * min(2.0, 1.0 + 0.1 * _access)
|
|
1474
|
-
recency_boost = 0.8 + 0.3 * math.exp(-(math.log(2) / _S) * age_days)
|
|
1475
|
-
|
|
1476
|
-
# Content quality: penalize short/low-info facts that rank high
|
|
1477
|
-
# due to BM25 name-matching (greetings like "Hey Caroline!" score high
|
|
1478
|
-
# on BM25 but have zero retrieval value)
|
|
1479
|
-
content_len = len(fact.content.strip())
|
|
1480
|
-
if content_len < 10:
|
|
1481
|
-
quality = 0.3
|
|
1482
|
-
elif content_len < 25:
|
|
1483
|
-
quality = 0.7
|
|
1484
|
-
else:
|
|
1485
|
-
quality = 1.0
|
|
1486
|
-
|
|
1487
|
-
# Trust weighting: Bayesian trust modulates final ranking
|
|
1488
|
-
trust_weight, raw_trust = self._get_trust_weight(fact, profile_id)
|
|
1489
|
-
|
|
1490
|
-
boosted_score = fr.fused_score * recency_boost * quality * trust_weight
|
|
1491
|
-
|
|
1492
|
-
# Query-type-conditioned recency amplifier.
|
|
1493
|
-
# Applied only to "recency" and "temporal" queries; factual, entity,
|
|
1494
|
-
# and all other types receive a factor of exactly 1.0 (no change).
|
|
1495
|
-
# The amplitude scalar is read from RetrievalConfig so it can be tuned
|
|
1496
|
-
# or zeroed at runtime. strength=0.0 is a strict no-op — the if-guard
|
|
1497
|
-
# ensures the previous ranking is reproduced byte-for-byte.
|
|
1498
|
-
#
|
|
1499
|
-
# recency — 7-day half-life, 1.5× maximum (present-activity queries)
|
|
1500
|
-
# temporal — 30-day half-life, 1.2× maximum (past-event queries)
|
|
1501
|
-
#
|
|
1502
|
-
# Hook for the follow-on embedding-lag adjustment (task 2.6): that
|
|
1503
|
-
# adjustment also multiplies boosted_score and belongs immediately after
|
|
1504
|
-
# this block, conditioned on channel_scores["semantic"] == 0.0 AND
|
|
1505
|
-
# age_days < 1.0. Add it as an independent if-block here so the two
|
|
1506
|
-
# factors compose cleanly without restructuring what is above or below.
|
|
1507
|
-
_prior_strength = getattr(self._config, "recency_prior_strength", 0.5)
|
|
1508
|
-
# age_known matters here: the fallback above leaves age_days at 0.0
|
|
1509
|
-
# when a fact carries no usable timestamp, which reads as "written
|
|
1510
|
-
# moments ago" and would hand an undated fact the largest possible
|
|
1511
|
-
# boost for being new. Not knowing when something was written is not
|
|
1512
|
-
# evidence that it is fresh.
|
|
1513
|
-
if (_prior_strength > 0.0 and age_known
|
|
1514
|
-
and strat.query_type in ("recency", "temporal")):
|
|
1515
|
-
_half_life = 7.0 if strat.query_type == "recency" else 30.0
|
|
1516
|
-
# Both query types use max_amp=1.5 so the decay is visible.
|
|
1517
|
-
# With max_amp=1.2 and half_life=30, the raw value at age 0d
|
|
1518
|
-
# is 1.5 and at age 30d is 1.25 — both clamp to 1.2. The prior
|
|
1519
|
-
# was inert over the first ~39 days, which is the range it was
|
|
1520
|
-
# built to discriminate. Raising the cap to 1.5 lets the
|
|
1521
|
-
# formula vary from 1.5 (fresh) through 1.25 (30d) toward 1.0
|
|
1522
|
-
# (old). This changes ranking: facts from 2 days ago and 30
|
|
1523
|
-
# days ago now receive different boosts. The change is a
|
|
1524
|
-
# correction to a clamp that made the prior inert, not a
|
|
1525
|
-
# measured gain.
|
|
1526
|
-
_max_amp = 1.5
|
|
1527
|
-
_cond_boost = 1.0 + _prior_strength * math.exp(
|
|
1528
|
-
-(math.log(2) / _half_life) * age_days
|
|
1529
|
-
)
|
|
1530
|
-
_cond_boost = min(_cond_boost, _max_amp)
|
|
1531
|
-
boosted_score = boosted_score * _cond_boost
|
|
1532
|
-
|
|
1533
|
-
# v3.5.0 (M2): soft-normalize to [0,1]. RRF weights + scene/entity
|
|
1534
|
-
# boosts push raw scores well above 1 (observed: 27.97). A sigmoid
|
|
1535
|
-
# preserves rank (monotonic) while giving users a readable 0-1 range.
|
|
1536
|
-
normalized_score = 1.0 / (1.0 + math.exp(-boosted_score * 0.5))
|
|
1537
|
-
results.append(RetrievalResult(
|
|
1538
|
-
fact=fact, score=round(normalized_score, 4),
|
|
1539
|
-
channel_scores=fr.channel_scores,
|
|
1540
|
-
confidence=fact.confidence,
|
|
1541
|
-
relevance_score=round(normalized_score, 4),
|
|
1542
|
-
ranking_score=boosted_score,
|
|
1543
|
-
memory_confidence=fact.confidence,
|
|
1544
|
-
evidence_chain=evidence,
|
|
1545
|
-
trust_score=raw_trust,
|
|
1546
|
-
))
|
|
1547
|
-
# ranking_score incorporates every modifier computed in this loop
|
|
1548
|
-
# (Ebbinghaus decay, quality, trust, and the query-type-conditioned
|
|
1549
|
-
# recency amplifier). Sort here so RecallResponse.results[0] is
|
|
1550
|
-
# always the highest-ranked fact — callers that rely on the returned
|
|
1551
|
-
# order get the amplified ranking, not the pre-amplifier fused order.
|
|
1552
|
-
# Tie-break on fact_id keeps two runs over an unchanged store stable.
|
|
1553
|
-
results.sort(key=lambda r: (-(r.ranking_score or 0.0), r.fact.fact_id))
|
|
1554
|
-
return results
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
# ---------------------------------------------------------------------------
|
|
1558
|
-
# apply_channel_weights (LLD-03 §5.5 — module-level pure helper)
|
|
1559
|
-
# ---------------------------------------------------------------------------
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
_CHANNEL_KEYS: tuple[str, ...] = (
|
|
1563
|
-
"semantic", "bm25", "entity_graph", "temporal",
|
|
1564
|
-
# hopfield + spreading_activation are real retrieval channels (score
|
|
1565
|
-
# contract v2) with bandit-chosen weights; omitting them here silently
|
|
1566
|
-
# discarded adaptive reranking for multi-hop relational recall.
|
|
1567
|
-
"spreading_activation", "hopfield",
|
|
1568
|
-
)
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
def apply_channel_weights(
|
|
1572
|
-
candidates: list[RetrievalResult],
|
|
1573
|
-
weights: dict[str, float] | None,
|
|
1574
|
-
) -> list[RetrievalResult]:
|
|
1575
|
-
"""Re-score candidates under a bandit-chosen weight bundle.
|
|
1576
|
-
|
|
1577
|
-
Multiplies each candidate's ``channel_scores[ch]`` by ``weights[ch]``
|
|
1578
|
-
and applies ``cross_encoder_bias`` to the final score. Preserves order;
|
|
1579
|
-
callers reorder via ensemble_rerank.
|
|
1580
|
-
|
|
1581
|
-
Returns a NEW list with new ``RetrievalResult`` instances — never mutates
|
|
1582
|
-
input. Unknown / missing weights default to 1.0.
|
|
1583
|
-
|
|
1584
|
-
Safe against ``weights=None`` (returns input unchanged) and empty lists.
|
|
1585
|
-
"""
|
|
1586
|
-
if not candidates or not weights:
|
|
1587
|
-
return list(candidates)
|
|
1588
|
-
|
|
1589
|
-
ce_bias = float(weights.get("cross_encoder_bias", 1.0))
|
|
1590
|
-
out: list[RetrievalResult] = []
|
|
1591
|
-
for c in candidates:
|
|
1592
|
-
original_cs = c.channel_scores or {}
|
|
1593
|
-
new_cs: dict[str, float] = dict(original_cs)
|
|
1594
|
-
base = 0.0
|
|
1595
|
-
for ch in _CHANNEL_KEYS:
|
|
1596
|
-
raw = float(original_cs.get(ch, 0.0))
|
|
1597
|
-
w = float(weights.get(ch, 1.0))
|
|
1598
|
-
scaled = raw * w
|
|
1599
|
-
new_cs[ch] = scaled
|
|
1600
|
-
base += scaled
|
|
1601
|
-
new_score = (base if base > 0.0 else float(c.score)) * ce_bias
|
|
1602
|
-
out.append(RetrievalResult(
|
|
1603
|
-
fact=c.fact,
|
|
1604
|
-
score=c.score,
|
|
1605
|
-
channel_scores=new_cs,
|
|
1606
|
-
confidence=c.confidence,
|
|
1607
|
-
relevance_score=c.relevance_score,
|
|
1608
|
-
ranking_score=new_score,
|
|
1609
|
-
memory_confidence=c.memory_confidence,
|
|
1610
|
-
rank_position=c.rank_position,
|
|
1611
|
-
evidence_chain=c.evidence_chain,
|
|
1612
|
-
trust_score=c.trust_score,
|
|
1613
|
-
marker=c.marker,
|
|
1614
|
-
))
|
|
1615
|
-
return out
|