brainlayer 1.5.0__tar.gz → 1.5.2__tar.gz
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.
- {brainlayer-1.5.0 → brainlayer-1.5.2}/PKG-INFO +1 -1
- {brainlayer-1.5.0 → brainlayer-1.5.2}/pyproject.toml +3 -1
- brainlayer-1.5.2/scripts/hotlane_brainbar_daemon.py +611 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/scripts/launchd/com.brainlayer.hotlane-brainbar.plist +1 -1
- {brainlayer-1.5.0 → brainlayer-1.5.2}/scripts/launchd/install.sh +225 -5
- {brainlayer-1.5.0 → brainlayer-1.5.2}/server.json +2 -2
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/__init__.py +1 -1
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/setup.py +4 -2
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/watcher.py +210 -43
- {brainlayer-1.5.0 → brainlayer-1.5.2}/.gitignore +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/LICENSE +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/README.md +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/scripts/launchd/backup-daily.sh +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/scripts/launchd/brainlayer-env-run.sh +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/scripts/launchd/brainlayer.env.example +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/scripts/launchd/com.brainlayer.backup-daily.plist +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/scripts/launchd/com.brainlayer.decay.plist +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/scripts/launchd/com.brainlayer.drain.plist +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/scripts/launchd/com.brainlayer.enrichment.plist +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/scripts/launchd/com.brainlayer.health-check.plist +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/scripts/launchd/com.brainlayer.index.plist +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/scripts/launchd/com.brainlayer.jsonl-backup.plist +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/scripts/launchd/com.brainlayer.maintenance-nightly.plist +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/scripts/launchd/com.brainlayer.maintenance-weekly.plist +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/scripts/launchd/com.brainlayer.p0-counter.plist +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/scripts/launchd/com.brainlayer.repair-fts.plist +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/scripts/launchd/com.brainlayer.throughput-watchdog.plist +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/scripts/launchd/com.brainlayer.tier0-watchdog.plist +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/scripts/launchd/com.brainlayer.wal-checkpoint.plist +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/scripts/launchd/com.brainlayer.watch.plist +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/scripts/launchd/jsonl-backup.sh +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/scripts/launchd/throughput-watchdog.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/scripts/tier0-watchdog.sh +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/_helpers.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/agent_profiles.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/agent_provenance.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/alarm.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/backup_daily.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/bitemporal.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/brainbar_hybrid_helper.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/calibrate.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/chunk_origin.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/chunk_origin_backfill.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/classify.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/claude_paths.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/cli/__init__.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/cli/wizard.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/cli_new.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/cloud_backfill.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/clustering.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/config.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/content_class.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/correction_judge.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/data/agents_registry.yaml +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/data/sandbox_seeds/skill-eval-baseline.json +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/db_shrink.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/decay.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/decay_backfill.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/decay_job.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/dedupe.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/deploy_drift.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/doctor.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/drain.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/drain_liveness.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/embeddings.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/engine.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/enrichment_controller.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/eval/__init__.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/eval/abcde_enrich_runner.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/eval/abcde_variants.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/eval/abcde_variants.yaml +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/eval/benchmark.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/eval/enrichment_gold.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/eval/enrichment_graders.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/eval/enrichment_judge.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/eval/enrichment_llm_judge.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/eval/enrichment_quality_benchmark.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/eval/experiment_store.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/eval/fixtures/README.md +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/eval/labeling/enrichment-labeler.html +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/eval/phoenix_gate/__init__.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/eval/phoenix_gate/__main__.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/eval/phoenix_gate/baseline_store.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/eval/phoenix_gate/cli.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/eval/phoenix_gate/models.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/eval/phoenix_gate/phoenix_client.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/eval/phoenix_gate/regression_gate.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/eval/phoenix_gate/triggers.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/fallback_replay.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/git_learning.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/health_check.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/hooks/__init__.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/hooks/indexer.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/index_new.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/ingest/__init__.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/ingest/codex.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/ingest_denylist.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/ingest_guard.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/isolation_proof.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/jsonl_backup.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/kg/__init__.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/kg_cleanup.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/kg_judge.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/kg_promotion.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/kg_repo.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/kg_review_session.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/kg_session_finish.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/kg_session_harvest.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/launchd_primitive.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/lexical_defense.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/lexical_defense_dictionary.json +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/maintenance.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/mcp/__init__.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/mcp/_format.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/mcp/_shared.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/mcp/enrich_handler.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/mcp/entity_handler.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/mcp/palette.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/mcp/search_handler.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/mcp/store_handler.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/mcp/tags_handler.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/mcp_stdio_bridge.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/memory_types.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/migrate.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/p0_longitudinal_count.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/parent_death.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/paths.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/phonetic.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/__init__.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/agent_enrichment.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/analyze_communication.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/batch_extraction.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/brain_graph.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/chat_tags.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/chunk.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/classify.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/cluster_sampling.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/code_intelligence.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/correction_detection.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/correction_mining.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/digest.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/enrichment.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/enrichment_tiers.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/entity_extraction.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/entity_resolution.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/extract.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/extract_claude_desktop.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/extract_corrections.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/extract_markdown.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/extract_whatsapp.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/frustration_mining.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/git_overlay.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/kg_extraction.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/kg_extraction_groq.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/longitudinal_analyzer.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/obsidian_export.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/operation_grouping.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/plan_linking.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/rate_limiter.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/sanitize.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/secret_scrub.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/semantic_style.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/sentiment.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/session_enrichment.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/style_embed.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/style_index.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/tag_entity_promotion.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/temporal_chains.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/time_batcher.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/unified_timeline.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/pipeline/write_queue.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/provenance.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/provenance_autosupersede.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/provenance_integration.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/queue_io.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/queue_merge.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/reembed_backfill.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/runtime_store.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/sandbox_db.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/scoping.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/search_fanout.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/search_profile.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/search_repo.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/session_repo.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/storage.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/store.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/system_prompt_guard.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/tag_normalization.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/taxonomy.json +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/telemetry.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/types.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/vector_store.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/wal_checkpoint.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/watcher_bridge.py +0 -0
- {brainlayer-1.5.0 → brainlayer-1.5.2}/src/brainlayer/writer_telemetry.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: brainlayer
|
|
3
|
-
Version: 1.5.
|
|
3
|
+
Version: 1.5.2
|
|
4
4
|
Summary: Persistent memory MCP server for AI agents — 13 tools, semantic search, knowledge graph, on-device SQLite
|
|
5
5
|
Project-URL: Homepage, https://brainlayer.etanheyman.com
|
|
6
6
|
Project-URL: Repository, https://github.com/EtanHey/brainlayer
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "brainlayer"
|
|
3
|
-
version = "1.5.
|
|
3
|
+
version = "1.5.2"
|
|
4
4
|
description = "Persistent memory MCP server for AI agents — 13 tools, semantic search, knowledge graph, on-device SQLite"
|
|
5
5
|
license = {text = "Apache-2.0"}
|
|
6
6
|
readme = "README.md"
|
|
@@ -131,12 +131,14 @@ exclude = [
|
|
|
131
131
|
|
|
132
132
|
[tool.hatch.build.targets.wheel.force-include]
|
|
133
133
|
"scripts/launchd" = "brainlayer/launchd"
|
|
134
|
+
"scripts/hotlane_brainbar_daemon.py" = "brainlayer/launchd/hotlane_brainbar_daemon.py"
|
|
134
135
|
"scripts/tier0-watchdog.sh" = "brainlayer/launchd/tier0-watchdog.sh"
|
|
135
136
|
|
|
136
137
|
[tool.hatch.build.targets.sdist]
|
|
137
138
|
only-include = [
|
|
138
139
|
"src/brainlayer",
|
|
139
140
|
"scripts/launchd",
|
|
141
|
+
"scripts/hotlane_brainbar_daemon.py",
|
|
140
142
|
"scripts/tier0-watchdog.sh",
|
|
141
143
|
"README.md",
|
|
142
144
|
"LICENSE",
|
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Hot-embed recent BrainBar MCP writes and interleave realtime enrichment.
|
|
3
|
+
|
|
4
|
+
BrainBar owns the live MCP socket and stores ``brain_store`` rows directly in
|
|
5
|
+
SQLite. This adapter owns the single BrainLayer writer slot and must therefore
|
|
6
|
+
perform all write-side background work that needs steady progress: hot embedding,
|
|
7
|
+
optional embedding backlog, and enrichment backlog.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import inspect
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
16
|
+
import signal
|
|
17
|
+
import time
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Callable, NamedTuple
|
|
20
|
+
|
|
21
|
+
import apsw
|
|
22
|
+
import sqlite_vec
|
|
23
|
+
|
|
24
|
+
from brainlayer._helpers import serialize_f32
|
|
25
|
+
from brainlayer.embeddings import get_embedding_model
|
|
26
|
+
from brainlayer.enrichment_controller import (
|
|
27
|
+
DEFAULT_ENRICH_SUPERVISOR_SINCE_HOURS,
|
|
28
|
+
_result_hit_daily_cap,
|
|
29
|
+
enrich_realtime,
|
|
30
|
+
)
|
|
31
|
+
from brainlayer.paths import get_db_path
|
|
32
|
+
from brainlayer.store import embed_hot_chunk, embed_pending_chunks
|
|
33
|
+
from brainlayer.vector_store import VectorStore
|
|
34
|
+
from brainlayer.writer_telemetry import start_writer_span
|
|
35
|
+
|
|
36
|
+
LOGGER = logging.getLogger("brainlayer.hotlane_brainbar")
|
|
37
|
+
STOP = False
|
|
38
|
+
DEFAULT_HOTLANE_ENRICH_LIMIT = 5
|
|
39
|
+
DEFAULT_BACKLOG_BATCH = 4
|
|
40
|
+
MAX_BACKLOG_BATCH = 16
|
|
41
|
+
DEFAULT_HOTLANE_WRITE_BUSY_TIMEOUT_MS = 1000
|
|
42
|
+
MAX_APSW_BUSY_TIMEOUT_MS = 2_147_483_647
|
|
43
|
+
VECTOR_WRITE_YIELD_SECONDS = 0.005
|
|
44
|
+
_sleep = time.sleep
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class CycleResult(NamedTuple):
|
|
48
|
+
embedded: int = 0
|
|
49
|
+
enrich_attempted: int = 0
|
|
50
|
+
enriched: int = 0
|
|
51
|
+
enrich_skipped: int = 0
|
|
52
|
+
enrich_failed: int = 0
|
|
53
|
+
enrich_daily_cap_reached: bool = False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class EmbedCandidate(NamedTuple):
|
|
57
|
+
chunk_id: str
|
|
58
|
+
content: str
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class EmbeddedVector(NamedTuple):
|
|
62
|
+
chunk_id: str
|
|
63
|
+
content: str
|
|
64
|
+
embedding: list[float]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _stop(_signum: int, _frame: object) -> None:
|
|
68
|
+
global STOP
|
|
69
|
+
STOP = True
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _candidate_chunk_ids(store: VectorStore, *, limit: int) -> list[str]:
|
|
73
|
+
rows = store.conn.cursor().execute(
|
|
74
|
+
"""
|
|
75
|
+
SELECT c.id
|
|
76
|
+
FROM chunks c
|
|
77
|
+
LEFT JOIN chunk_vectors_rowids r ON r.id = c.id
|
|
78
|
+
WHERE r.id IS NULL
|
|
79
|
+
AND c.source_file = 'brainbar-store'
|
|
80
|
+
AND c.source = 'mcp'
|
|
81
|
+
AND c.content IS NOT NULL
|
|
82
|
+
AND c.content != ''
|
|
83
|
+
AND c.archived_at IS NULL
|
|
84
|
+
AND c.superseded_by IS NULL
|
|
85
|
+
AND c.aggregated_into IS NULL
|
|
86
|
+
AND COALESCE(c.archived, 0) = 0
|
|
87
|
+
AND COALESCE(c.status, 'active') = 'active'
|
|
88
|
+
ORDER BY c.created_at DESC
|
|
89
|
+
LIMIT ?
|
|
90
|
+
""",
|
|
91
|
+
(limit,),
|
|
92
|
+
)
|
|
93
|
+
return [str(row[0]) for row in rows]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _candidate_chunk_rows(store: VectorStore, *, limit: int) -> list[EmbedCandidate]:
|
|
97
|
+
rows = store.conn.cursor().execute(
|
|
98
|
+
"""
|
|
99
|
+
SELECT c.id, c.content
|
|
100
|
+
FROM chunks c
|
|
101
|
+
LEFT JOIN chunk_vectors_rowids r ON r.id = c.id
|
|
102
|
+
WHERE r.id IS NULL
|
|
103
|
+
AND c.source_file = 'brainbar-store'
|
|
104
|
+
AND c.source = 'mcp'
|
|
105
|
+
AND c.content IS NOT NULL
|
|
106
|
+
AND c.content != ''
|
|
107
|
+
AND c.archived_at IS NULL
|
|
108
|
+
AND c.superseded_by IS NULL
|
|
109
|
+
AND c.aggregated_into IS NULL
|
|
110
|
+
AND COALESCE(c.archived, 0) = 0
|
|
111
|
+
AND COALESCE(c.status, 'active') = 'active'
|
|
112
|
+
ORDER BY c.created_at DESC
|
|
113
|
+
LIMIT ?
|
|
114
|
+
""",
|
|
115
|
+
(limit,),
|
|
116
|
+
)
|
|
117
|
+
return [EmbedCandidate(str(row[0]), str(row[1])) for row in rows]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _pending_chunk_rows(store: VectorStore, *, limit: int) -> list[EmbedCandidate]:
|
|
121
|
+
rows = store.conn.cursor().execute(
|
|
122
|
+
"""
|
|
123
|
+
SELECT c.id, c.content
|
|
124
|
+
FROM chunks c
|
|
125
|
+
LEFT JOIN chunk_vectors_rowids r ON c.id = r.id
|
|
126
|
+
WHERE r.id IS NULL
|
|
127
|
+
AND c.content IS NOT NULL
|
|
128
|
+
AND c.content != ''
|
|
129
|
+
AND c.archived_at IS NULL
|
|
130
|
+
AND c.superseded_by IS NULL
|
|
131
|
+
AND c.aggregated_into IS NULL
|
|
132
|
+
AND COALESCE(c.archived, 0) = 0
|
|
133
|
+
AND COALESCE(c.status, 'active') = 'active'
|
|
134
|
+
ORDER BY c.created_at ASC
|
|
135
|
+
LIMIT ?
|
|
136
|
+
""",
|
|
137
|
+
(limit,),
|
|
138
|
+
)
|
|
139
|
+
return [EmbedCandidate(str(row[0]), str(row[1])) for row in rows]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _embed_candidates(
|
|
143
|
+
candidates: list[EmbedCandidate],
|
|
144
|
+
*,
|
|
145
|
+
embed_fn: Callable[[str], list[float]],
|
|
146
|
+
embed_batch_fn: Callable[[list[str]], list[list[float]]] | None = None,
|
|
147
|
+
) -> list[EmbeddedVector]:
|
|
148
|
+
if not candidates:
|
|
149
|
+
return []
|
|
150
|
+
|
|
151
|
+
if embed_batch_fn is not None and len(candidates) > 1:
|
|
152
|
+
try:
|
|
153
|
+
embeddings = embed_batch_fn([candidate.content for candidate in candidates])
|
|
154
|
+
if len(embeddings) != len(candidates):
|
|
155
|
+
raise ValueError(f"batch embedder returned {len(embeddings)} embeddings for {len(candidates)} chunks")
|
|
156
|
+
return [
|
|
157
|
+
EmbeddedVector(candidate.chunk_id, candidate.content, embedding)
|
|
158
|
+
for candidate, embedding in zip(candidates, embeddings)
|
|
159
|
+
]
|
|
160
|
+
except Exception as exc:
|
|
161
|
+
LOGGER.warning("Failed to embed hotlane batch: %s", exc)
|
|
162
|
+
|
|
163
|
+
embedded: list[EmbeddedVector] = []
|
|
164
|
+
for candidate in candidates:
|
|
165
|
+
try:
|
|
166
|
+
embedded.append(EmbeddedVector(candidate.chunk_id, candidate.content, embed_fn(candidate.content)))
|
|
167
|
+
except Exception as exc:
|
|
168
|
+
LOGGER.warning("Failed to embed chunk %s: %s", candidate.chunk_id, exc)
|
|
169
|
+
return embedded
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _hotlane_write_busy_timeout_ms() -> int:
|
|
173
|
+
raw_value = os.environ.get("BRAINLAYER_HOTLANE_WRITE_BUSY_TIMEOUT_MS")
|
|
174
|
+
if raw_value is None:
|
|
175
|
+
return DEFAULT_HOTLANE_WRITE_BUSY_TIMEOUT_MS
|
|
176
|
+
try:
|
|
177
|
+
return max(1, min(int(raw_value), MAX_APSW_BUSY_TIMEOUT_MS))
|
|
178
|
+
except ValueError:
|
|
179
|
+
LOGGER.warning(
|
|
180
|
+
"Invalid BRAINLAYER_HOTLANE_WRITE_BUSY_TIMEOUT_MS=%r; using %dms",
|
|
181
|
+
raw_value,
|
|
182
|
+
DEFAULT_HOTLANE_WRITE_BUSY_TIMEOUT_MS,
|
|
183
|
+
)
|
|
184
|
+
return DEFAULT_HOTLANE_WRITE_BUSY_TIMEOUT_MS
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _open_hotlane_write_connection(db_path: Path) -> apsw.Connection:
|
|
188
|
+
conn = apsw.Connection(str(db_path), flags=apsw.SQLITE_OPEN_READWRITE)
|
|
189
|
+
conn.setbusytimeout(_hotlane_write_busy_timeout_ms())
|
|
190
|
+
conn.enableloadextension(True)
|
|
191
|
+
conn.loadextension(sqlite_vec.loadable_path())
|
|
192
|
+
conn.enableloadextension(False)
|
|
193
|
+
return conn
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _table_exists(conn: apsw.Connection, table_name: str) -> bool:
|
|
197
|
+
return bool(
|
|
198
|
+
conn.cursor()
|
|
199
|
+
.execute(
|
|
200
|
+
"SELECT 1 FROM sqlite_master WHERE type IN ('table', 'view') AND name = ?",
|
|
201
|
+
(table_name,),
|
|
202
|
+
)
|
|
203
|
+
.fetchone()
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _upsert_chunk_vector_raw(
|
|
208
|
+
cursor,
|
|
209
|
+
chunk_id: str,
|
|
210
|
+
embedding: list[float],
|
|
211
|
+
*,
|
|
212
|
+
binary_index_available: bool,
|
|
213
|
+
) -> None:
|
|
214
|
+
embedding_bytes = serialize_f32(embedding)
|
|
215
|
+
cursor.execute("DELETE FROM chunk_vectors WHERE chunk_id = ?", (chunk_id,))
|
|
216
|
+
cursor.execute(
|
|
217
|
+
"""
|
|
218
|
+
INSERT INTO chunk_vectors (chunk_id, embedding)
|
|
219
|
+
VALUES (?, ?)
|
|
220
|
+
""",
|
|
221
|
+
(chunk_id, embedding_bytes),
|
|
222
|
+
)
|
|
223
|
+
if binary_index_available:
|
|
224
|
+
cursor.execute("DELETE FROM chunk_vectors_binary WHERE chunk_id = ?", (chunk_id,))
|
|
225
|
+
cursor.execute(
|
|
226
|
+
"""
|
|
227
|
+
INSERT INTO chunk_vectors_binary (chunk_id, embedding)
|
|
228
|
+
VALUES (?, vec_quantize_binary(?))
|
|
229
|
+
""",
|
|
230
|
+
(chunk_id, embedding_bytes),
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _write_embedded_vectors(store_or_path: VectorStore | Path | str, vectors: list[EmbeddedVector]) -> int:
|
|
235
|
+
if not vectors:
|
|
236
|
+
return 0
|
|
237
|
+
|
|
238
|
+
conn: apsw.Connection | None = None
|
|
239
|
+
db_path: Path | None
|
|
240
|
+
if hasattr(store_or_path, "conn"):
|
|
241
|
+
store = store_or_path
|
|
242
|
+
cursor = store.conn.cursor()
|
|
243
|
+
db_path = getattr(store, "db_path", None)
|
|
244
|
+
upsert_chunk_vector = store._upsert_chunk_vector
|
|
245
|
+
else:
|
|
246
|
+
db_path = Path(store_or_path)
|
|
247
|
+
conn = _open_hotlane_write_connection(db_path)
|
|
248
|
+
cursor = conn.cursor()
|
|
249
|
+
binary_index_available = _table_exists(conn, "chunk_vectors_binary")
|
|
250
|
+
|
|
251
|
+
def upsert_chunk_vector(cursor, chunk_id, embedding):
|
|
252
|
+
return _upsert_chunk_vector_raw(
|
|
253
|
+
cursor,
|
|
254
|
+
chunk_id,
|
|
255
|
+
embedding,
|
|
256
|
+
binary_index_available=binary_index_available,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
count = 0
|
|
260
|
+
try:
|
|
261
|
+
for vector_index, (chunk_id, content, embedding) in enumerate(vectors):
|
|
262
|
+
transaction_started = False
|
|
263
|
+
telemetry_span = start_writer_span(
|
|
264
|
+
cursor.connection if hasattr(cursor, "connection") else (conn or store.conn),
|
|
265
|
+
db_path=db_path,
|
|
266
|
+
producer="hotlane",
|
|
267
|
+
lane="hotlane",
|
|
268
|
+
operation="write_vectors",
|
|
269
|
+
rows_planned=1,
|
|
270
|
+
)
|
|
271
|
+
try:
|
|
272
|
+
cursor.execute("BEGIN IMMEDIATE")
|
|
273
|
+
transaction_started = True
|
|
274
|
+
still_eligible = cursor.execute(
|
|
275
|
+
"""
|
|
276
|
+
SELECT 1
|
|
277
|
+
FROM chunks c
|
|
278
|
+
LEFT JOIN chunk_vectors_rowids r ON c.id = r.id
|
|
279
|
+
WHERE c.id = ?
|
|
280
|
+
AND c.content = ?
|
|
281
|
+
AND r.id IS NULL
|
|
282
|
+
AND c.content IS NOT NULL
|
|
283
|
+
AND c.content != ''
|
|
284
|
+
AND c.archived_at IS NULL
|
|
285
|
+
AND c.superseded_by IS NULL
|
|
286
|
+
AND c.aggregated_into IS NULL
|
|
287
|
+
AND COALESCE(c.archived, 0) = 0
|
|
288
|
+
AND COALESCE(c.status, 'active') = 'active'
|
|
289
|
+
""",
|
|
290
|
+
(chunk_id, content),
|
|
291
|
+
).fetchone()
|
|
292
|
+
rows_touched = 0
|
|
293
|
+
if still_eligible:
|
|
294
|
+
upsert_chunk_vector(cursor, chunk_id, embedding)
|
|
295
|
+
rows_touched = 1
|
|
296
|
+
cursor.execute("COMMIT")
|
|
297
|
+
transaction_started = False
|
|
298
|
+
count += rows_touched
|
|
299
|
+
telemetry_span.finish("commit", rows_touched=rows_touched)
|
|
300
|
+
if vector_index < len(vectors) - 1:
|
|
301
|
+
_sleep(VECTOR_WRITE_YIELD_SECONDS)
|
|
302
|
+
except Exception as exc:
|
|
303
|
+
if transaction_started:
|
|
304
|
+
cursor.execute("ROLLBACK")
|
|
305
|
+
telemetry_span.finish("rollback", error=f"{type(exc).__name__}: {exc}")
|
|
306
|
+
raise
|
|
307
|
+
finally:
|
|
308
|
+
if conn is not None:
|
|
309
|
+
conn.close()
|
|
310
|
+
if count:
|
|
311
|
+
from brainlayer.search_repo import clear_hybrid_search_cache
|
|
312
|
+
|
|
313
|
+
clear_hybrid_search_cache(db_path)
|
|
314
|
+
return count
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _open_store(
|
|
318
|
+
vector_store_cls: Callable[..., VectorStore],
|
|
319
|
+
db_path: Path,
|
|
320
|
+
*,
|
|
321
|
+
readonly: bool,
|
|
322
|
+
) -> VectorStore:
|
|
323
|
+
if not _callable_accepts_keyword(vector_store_cls, "readonly"):
|
|
324
|
+
return vector_store_cls(db_path)
|
|
325
|
+
try:
|
|
326
|
+
return vector_store_cls(db_path, readonly=readonly)
|
|
327
|
+
except TypeError as exc:
|
|
328
|
+
if not _type_error_rejects_keyword(exc, "readonly"):
|
|
329
|
+
raise
|
|
330
|
+
return vector_store_cls(db_path)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _callable_accepts_keyword(func: Callable[..., object], keyword: str) -> bool:
|
|
334
|
+
try:
|
|
335
|
+
signature = inspect.signature(func)
|
|
336
|
+
except (TypeError, ValueError):
|
|
337
|
+
return True
|
|
338
|
+
return keyword in signature.parameters or any(
|
|
339
|
+
parameter.kind == inspect.Parameter.VAR_KEYWORD for parameter in signature.parameters.values()
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _type_error_rejects_keyword(exc: TypeError, keyword: str) -> bool:
|
|
344
|
+
message = str(exc)
|
|
345
|
+
return keyword in message and (
|
|
346
|
+
"unexpected keyword argument" in message
|
|
347
|
+
or "got an unexpected keyword" in message
|
|
348
|
+
or "invalid keyword argument" in message
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _run_split_cycle(
|
|
353
|
+
*,
|
|
354
|
+
db_path: Path,
|
|
355
|
+
vector_store_cls: Callable[..., VectorStore],
|
|
356
|
+
embed_fn: Callable[[str], list[float]],
|
|
357
|
+
recent_limit: int,
|
|
358
|
+
backlog_batch: int,
|
|
359
|
+
enrich_limit: int,
|
|
360
|
+
enrich_since_hours: int,
|
|
361
|
+
embed_batch_fn: Callable[[list[str]], list[list[float]]] | None = None,
|
|
362
|
+
enrich_fn: Callable[..., object] = enrich_realtime,
|
|
363
|
+
candidate_rows_fn: Callable[..., list[EmbedCandidate]] = _candidate_chunk_rows,
|
|
364
|
+
pending_rows_fn: Callable[..., list[EmbedCandidate]] = _pending_chunk_rows,
|
|
365
|
+
write_vectors_fn: Callable[..., int] = _write_embedded_vectors,
|
|
366
|
+
) -> CycleResult:
|
|
367
|
+
embedded = 0
|
|
368
|
+
hot_rows: list[EmbedCandidate] = []
|
|
369
|
+
pending_rows: list[EmbedCandidate] = []
|
|
370
|
+
|
|
371
|
+
if recent_limit > 0 or backlog_batch > 0:
|
|
372
|
+
if not db_path.exists():
|
|
373
|
+
bootstrap_store = _open_store(vector_store_cls, db_path, readonly=False)
|
|
374
|
+
bootstrap_store.close()
|
|
375
|
+
read_store = _open_store(vector_store_cls, db_path, readonly=True)
|
|
376
|
+
try:
|
|
377
|
+
if recent_limit > 0:
|
|
378
|
+
hot_rows = candidate_rows_fn(read_store, limit=recent_limit)
|
|
379
|
+
if backlog_batch > 0:
|
|
380
|
+
pending_rows = pending_rows_fn(read_store, limit=backlog_batch)
|
|
381
|
+
finally:
|
|
382
|
+
read_store.close()
|
|
383
|
+
|
|
384
|
+
seen_hot_ids = {candidate.chunk_id for candidate in hot_rows}
|
|
385
|
+
pending_rows = [candidate for candidate in pending_rows if candidate.chunk_id not in seen_hot_ids]
|
|
386
|
+
vectors = _embed_candidates(hot_rows, embed_fn=embed_fn, embed_batch_fn=embed_batch_fn)
|
|
387
|
+
vectors.extend(_embed_candidates(pending_rows, embed_fn=embed_fn, embed_batch_fn=embed_batch_fn))
|
|
388
|
+
if vectors:
|
|
389
|
+
embedded += write_vectors_fn(db_path, vectors)
|
|
390
|
+
|
|
391
|
+
if enrich_limit <= 0:
|
|
392
|
+
return CycleResult(embedded=embedded)
|
|
393
|
+
|
|
394
|
+
enrich_store = _open_store(vector_store_cls, db_path, readonly=False)
|
|
395
|
+
try:
|
|
396
|
+
enrich_result = enrich_fn(store=enrich_store, limit=enrich_limit, since_hours=enrich_since_hours)
|
|
397
|
+
finally:
|
|
398
|
+
enrich_store.close()
|
|
399
|
+
return CycleResult(
|
|
400
|
+
embedded=embedded,
|
|
401
|
+
enrich_attempted=int(getattr(enrich_result, "attempted", 0) or 0),
|
|
402
|
+
enriched=int(getattr(enrich_result, "enriched", 0) or 0),
|
|
403
|
+
enrich_skipped=int(getattr(enrich_result, "skipped", 0) or 0),
|
|
404
|
+
enrich_failed=int(getattr(enrich_result, "failed", 0) or 0),
|
|
405
|
+
enrich_daily_cap_reached=hasattr(enrich_result, "errors") and _result_hit_daily_cap(enrich_result),
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _default_queue_dir() -> Path:
|
|
410
|
+
import os
|
|
411
|
+
|
|
412
|
+
return Path(os.environ.get("BRAINLAYER_QUEUE_DIR", str(Path.home() / ".brainlayer" / "queue"))).expanduser()
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _queue_depth(queue_dir: Path) -> int:
|
|
416
|
+
try:
|
|
417
|
+
return sum(1 for _path in queue_dir.glob("*.jsonl"))
|
|
418
|
+
except OSError:
|
|
419
|
+
return 0
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _high_priority_queue_depth(queue_dir: Path) -> int:
|
|
423
|
+
try:
|
|
424
|
+
return sum(1 for path in queue_dir.glob("*.jsonl") if not path.name.startswith("enrichment-"))
|
|
425
|
+
except OSError:
|
|
426
|
+
return 0
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def run_cycle(
|
|
430
|
+
*,
|
|
431
|
+
store: VectorStore,
|
|
432
|
+
embed_fn: Callable[[str], list[float]],
|
|
433
|
+
recent_limit: int,
|
|
434
|
+
backlog_batch: int,
|
|
435
|
+
enrich_limit: int,
|
|
436
|
+
enrich_since_hours: int,
|
|
437
|
+
candidate_chunk_ids_fn: Callable[..., list[str]] = _candidate_chunk_ids,
|
|
438
|
+
hot_embed_fn: Callable[..., bool] = embed_hot_chunk,
|
|
439
|
+
pending_embed_fn: Callable[..., int] = embed_pending_chunks,
|
|
440
|
+
embed_batch_fn: Callable[[list[str]], list[list[float]]] | None = None,
|
|
441
|
+
enrich_fn: Callable[..., object] = enrich_realtime,
|
|
442
|
+
) -> CycleResult:
|
|
443
|
+
embedded = 0
|
|
444
|
+
for chunk_id in candidate_chunk_ids_fn(store, limit=recent_limit):
|
|
445
|
+
if hot_embed_fn(store=store, embed_fn=embed_fn, chunk_id=chunk_id):
|
|
446
|
+
embedded += 1
|
|
447
|
+
break
|
|
448
|
+
|
|
449
|
+
if backlog_batch > 0:
|
|
450
|
+
embedded += pending_embed_fn(
|
|
451
|
+
store=store,
|
|
452
|
+
embed_fn=embed_fn,
|
|
453
|
+
batch_size=backlog_batch,
|
|
454
|
+
embed_batch_fn=embed_batch_fn,
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
if enrich_limit <= 0:
|
|
458
|
+
return CycleResult(embedded=embedded)
|
|
459
|
+
|
|
460
|
+
enrich_result = enrich_fn(store, limit=enrich_limit, since_hours=enrich_since_hours)
|
|
461
|
+
return CycleResult(
|
|
462
|
+
embedded=embedded,
|
|
463
|
+
enrich_attempted=int(getattr(enrich_result, "attempted", 0) or 0),
|
|
464
|
+
enriched=int(getattr(enrich_result, "enriched", 0) or 0),
|
|
465
|
+
enrich_skipped=int(getattr(enrich_result, "skipped", 0) or 0),
|
|
466
|
+
enrich_failed=int(getattr(enrich_result, "failed", 0) or 0),
|
|
467
|
+
enrich_daily_cap_reached=hasattr(enrich_result, "errors") and _result_hit_daily_cap(enrich_result),
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def run(
|
|
472
|
+
*,
|
|
473
|
+
db_path: Path,
|
|
474
|
+
interval: float,
|
|
475
|
+
recent_limit: int,
|
|
476
|
+
backlog_interval: float,
|
|
477
|
+
backlog_batch: int,
|
|
478
|
+
enrich_interval: float,
|
|
479
|
+
enrich_limit: int,
|
|
480
|
+
enrich_since_hours: int,
|
|
481
|
+
vector_store_cls: Callable[[Path], VectorStore] = VectorStore,
|
|
482
|
+
model_factory: Callable[[], object] = get_embedding_model,
|
|
483
|
+
cycle_fn: Callable[..., CycleResult] = run_cycle,
|
|
484
|
+
time_fn: Callable[[], float] = time.monotonic,
|
|
485
|
+
sleep_fn: Callable[[float], None] = time.sleep,
|
|
486
|
+
max_cycles: int | None = None,
|
|
487
|
+
queue_dir: Path | None = None,
|
|
488
|
+
queue_depth_fn: Callable[[Path], int] = _queue_depth,
|
|
489
|
+
high_priority_queue_depth_fn: Callable[[Path], int] = _high_priority_queue_depth,
|
|
490
|
+
) -> None:
|
|
491
|
+
model = model_factory()
|
|
492
|
+
embed_batch_fn = getattr(model, "embed_texts", None)
|
|
493
|
+
if embed_batch_fn is not None:
|
|
494
|
+
|
|
495
|
+
def embed_fn(text: str) -> list[float]:
|
|
496
|
+
embeddings = embed_batch_fn([text])
|
|
497
|
+
if len(embeddings) != 1:
|
|
498
|
+
raise RuntimeError(f"single text embedder returned {len(embeddings)} embeddings")
|
|
499
|
+
return embeddings[0]
|
|
500
|
+
|
|
501
|
+
else:
|
|
502
|
+
embed_fn = model.embed_query
|
|
503
|
+
queue_dir_was_explicit = queue_dir is not None
|
|
504
|
+
queue_dir = queue_dir or _default_queue_dir()
|
|
505
|
+
last_backlog = time_fn() - backlog_interval
|
|
506
|
+
last_enrich = 0.0
|
|
507
|
+
enrich_disabled = False
|
|
508
|
+
cycles = 0
|
|
509
|
+
backlog_batch = min(max(backlog_batch, 0), MAX_BACKLOG_BATCH)
|
|
510
|
+
LOGGER.info("hotlane adapter started db=%s", db_path)
|
|
511
|
+
while not STOP:
|
|
512
|
+
if max_cycles is not None and cycles >= max_cycles:
|
|
513
|
+
break
|
|
514
|
+
try:
|
|
515
|
+
now = time_fn()
|
|
516
|
+
queue_has_backlog = queue_depth_fn(queue_dir) > 0
|
|
517
|
+
queue_has_high_priority_backlog = queue_has_backlog and high_priority_queue_depth_fn(queue_dir) > 0
|
|
518
|
+
if queue_has_backlog:
|
|
519
|
+
if queue_has_high_priority_backlog:
|
|
520
|
+
LOGGER.info("durable high-priority queue has backlog; yielding all hotlane writer work")
|
|
521
|
+
else:
|
|
522
|
+
LOGGER.info("durable queue has backlog; yielding all hotlane writer work")
|
|
523
|
+
cycles += 1
|
|
524
|
+
sleep_fn(interval)
|
|
525
|
+
continue
|
|
526
|
+
else:
|
|
527
|
+
cycle_backlog_batch = (
|
|
528
|
+
backlog_batch if backlog_batch > 0 and now - last_backlog >= backlog_interval else 0
|
|
529
|
+
)
|
|
530
|
+
cycle_enrich_limit = (
|
|
531
|
+
enrich_limit
|
|
532
|
+
if not enrich_disabled and enrich_limit > 0 and now - last_enrich >= enrich_interval
|
|
533
|
+
else 0
|
|
534
|
+
)
|
|
535
|
+
if cycle_backlog_batch > 0:
|
|
536
|
+
last_backlog = now
|
|
537
|
+
if cycle_enrich_limit > 0:
|
|
538
|
+
last_enrich = now
|
|
539
|
+
if cycle_fn is run_cycle:
|
|
540
|
+
result = _run_split_cycle(
|
|
541
|
+
db_path=db_path,
|
|
542
|
+
vector_store_cls=vector_store_cls,
|
|
543
|
+
embed_fn=embed_fn,
|
|
544
|
+
recent_limit=recent_limit,
|
|
545
|
+
backlog_batch=cycle_backlog_batch,
|
|
546
|
+
embed_batch_fn=embed_batch_fn,
|
|
547
|
+
enrich_limit=cycle_enrich_limit,
|
|
548
|
+
enrich_since_hours=enrich_since_hours,
|
|
549
|
+
)
|
|
550
|
+
else:
|
|
551
|
+
store = vector_store_cls(db_path)
|
|
552
|
+
try:
|
|
553
|
+
result = cycle_fn(
|
|
554
|
+
store=store,
|
|
555
|
+
embed_fn=embed_fn,
|
|
556
|
+
recent_limit=recent_limit,
|
|
557
|
+
backlog_batch=cycle_backlog_batch,
|
|
558
|
+
embed_batch_fn=embed_batch_fn,
|
|
559
|
+
enrich_limit=cycle_enrich_limit,
|
|
560
|
+
enrich_since_hours=enrich_since_hours,
|
|
561
|
+
)
|
|
562
|
+
finally:
|
|
563
|
+
store.close()
|
|
564
|
+
if result.enrich_daily_cap_reached:
|
|
565
|
+
enrich_disabled = True
|
|
566
|
+
LOGGER.warning("enrichment daily cap reached; disabling hotlane enrichment until restart")
|
|
567
|
+
if result.embedded or result.enrich_attempted:
|
|
568
|
+
LOGGER.info(
|
|
569
|
+
"embedded=%d enrich_attempted=%d enriched=%d skipped=%d failed=%d",
|
|
570
|
+
result.embedded,
|
|
571
|
+
result.enrich_attempted,
|
|
572
|
+
result.enriched,
|
|
573
|
+
result.enrich_skipped,
|
|
574
|
+
result.enrich_failed,
|
|
575
|
+
)
|
|
576
|
+
except Exception:
|
|
577
|
+
LOGGER.exception("hotlane adapter cycle failed")
|
|
578
|
+
sleep_fn(min(interval * 2, 5.0))
|
|
579
|
+
cycles += 1
|
|
580
|
+
sleep_fn(interval)
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def main() -> None:
|
|
584
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
585
|
+
parser.add_argument("--db", type=Path, default=get_db_path())
|
|
586
|
+
parser.add_argument("--interval", type=float, default=1.0)
|
|
587
|
+
parser.add_argument("--recent-limit", type=int, default=5)
|
|
588
|
+
parser.add_argument("--backlog-interval", type=float, default=10.0)
|
|
589
|
+
parser.add_argument("--backlog-batch", type=int, default=DEFAULT_BACKLOG_BATCH)
|
|
590
|
+
parser.add_argument("--enrich-interval", type=float, default=10.0)
|
|
591
|
+
parser.add_argument("--enrich-limit", type=int, default=DEFAULT_HOTLANE_ENRICH_LIMIT)
|
|
592
|
+
parser.add_argument("--enrich-since-hours", type=int, default=DEFAULT_ENRICH_SUPERVISOR_SINCE_HOURS)
|
|
593
|
+
args = parser.parse_args()
|
|
594
|
+
|
|
595
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
|
596
|
+
signal.signal(signal.SIGTERM, _stop)
|
|
597
|
+
signal.signal(signal.SIGINT, _stop)
|
|
598
|
+
run(
|
|
599
|
+
db_path=args.db,
|
|
600
|
+
interval=max(args.interval, 0.25),
|
|
601
|
+
recent_limit=max(args.recent_limit, 1),
|
|
602
|
+
backlog_interval=max(args.backlog_interval, 1.0),
|
|
603
|
+
backlog_batch=min(max(args.backlog_batch, 0), MAX_BACKLOG_BATCH),
|
|
604
|
+
enrich_interval=max(args.enrich_interval, 1.0),
|
|
605
|
+
enrich_limit=max(args.enrich_limit, 0),
|
|
606
|
+
enrich_since_hours=max(args.enrich_since_hours, 0),
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
if __name__ == "__main__":
|
|
611
|
+
main()
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<array>
|
|
9
9
|
<string>__BRAINLAYER_ENV_RUN__</string>
|
|
10
10
|
<string>__BRAINLAYER_PYTHON__</string>
|
|
11
|
-
<string>
|
|
11
|
+
<string>__HOTLANE_BRAINBAR_DAEMON__</string>
|
|
12
12
|
<string>--interval</string>
|
|
13
13
|
<string>1.0</string>
|
|
14
14
|
<string>--recent-limit</string>
|