brainlayer 1.5.2__tar.gz → 1.5.4__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.2 → brainlayer-1.5.4}/.gitignore +2 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/PKG-INFO +3 -2
- {brainlayer-1.5.2 → brainlayer-1.5.4}/pyproject.toml +3 -2
- {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/hotlane_brainbar_daemon.py +451 -64
- {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/backup-daily.sh +7 -1
- brainlayer-1.5.4/scripts/launchd/com.brainlayer.t3-ingest.plist +53 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/com.brainlayer.wal-checkpoint.plist +3 -4
- {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/install.sh +7 -2
- {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/throughput-watchdog.py +225 -39
- {brainlayer-1.5.2 → brainlayer-1.5.4}/server.json +2 -2
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/__init__.py +1 -1
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/agent_provenance.py +13 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/backup_daily.py +401 -70
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/brainbar_hybrid_helper.py +1 -1
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/cli/__init__.py +85 -11
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/deploy_drift.py +81 -24
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/doctor.py +37 -8
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/drain.py +161 -51
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/embeddings.py +15 -11
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/health_check.py +40 -21
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/index_new.py +31 -25
- brainlayer-1.5.4/src/brainlayer/ingest/__init__.py +27 -0
- brainlayer-1.5.4/src/brainlayer/ingest/t3.py +404 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/launchd_primitive.py +31 -6
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/maintenance.py +108 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/mcp/__init__.py +162 -62
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/mcp/_format.py +11 -2
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/mcp/_shared.py +1 -1
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/mcp/palette.py +1 -1
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/mcp/store_handler.py +106 -10
- brainlayer-1.5.4/src/brainlayer/paths.py +55 -0
- brainlayer-1.5.4/src/brainlayer/pause.py +46 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/queue_io.py +6 -2
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/runtime_store.py +2 -0
- brainlayer-1.5.4/src/brainlayer/setup.py +278 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/store.py +14 -0
- brainlayer-1.5.4/src/brainlayer/t3_provenance.py +97 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/vector_store.py +46 -12
- brainlayer-1.5.4/src/brainlayer/wal_checkpoint.py +175 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/watcher.py +504 -127
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/watcher_bridge.py +127 -14
- brainlayer-1.5.2/src/brainlayer/ingest/__init__.py +0 -1
- brainlayer-1.5.2/src/brainlayer/paths.py +0 -28
- brainlayer-1.5.2/src/brainlayer/setup.py +0 -114
- brainlayer-1.5.2/src/brainlayer/wal_checkpoint.py +0 -72
- {brainlayer-1.5.2 → brainlayer-1.5.4}/LICENSE +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/README.md +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/brainlayer-env-run.sh +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/brainlayer.env.example +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/com.brainlayer.backup-daily.plist +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/com.brainlayer.decay.plist +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/com.brainlayer.drain.plist +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/com.brainlayer.enrichment.plist +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/com.brainlayer.health-check.plist +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/com.brainlayer.hotlane-brainbar.plist +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/com.brainlayer.index.plist +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/com.brainlayer.jsonl-backup.plist +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/com.brainlayer.maintenance-nightly.plist +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/com.brainlayer.maintenance-weekly.plist +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/com.brainlayer.p0-counter.plist +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/com.brainlayer.repair-fts.plist +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/com.brainlayer.throughput-watchdog.plist +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/com.brainlayer.tier0-watchdog.plist +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/com.brainlayer.watch.plist +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/jsonl-backup.sh +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/tier0-watchdog.sh +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/_helpers.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/agent_profiles.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/alarm.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/bitemporal.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/calibrate.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/chunk_origin.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/chunk_origin_backfill.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/classify.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/claude_paths.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/cli/wizard.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/cli_new.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/cloud_backfill.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/clustering.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/config.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/content_class.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/correction_judge.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/data/agents_registry.yaml +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/data/sandbox_seeds/skill-eval-baseline.json +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/db_shrink.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/decay.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/decay_backfill.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/decay_job.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/dedupe.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/drain_liveness.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/engine.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/enrichment_controller.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/eval/__init__.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/eval/abcde_enrich_runner.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/eval/abcde_variants.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/eval/abcde_variants.yaml +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/eval/benchmark.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/eval/enrichment_gold.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/eval/enrichment_graders.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/eval/enrichment_judge.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/eval/enrichment_llm_judge.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/eval/enrichment_quality_benchmark.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/eval/experiment_store.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/eval/fixtures/README.md +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/eval/labeling/enrichment-labeler.html +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/eval/phoenix_gate/__init__.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/eval/phoenix_gate/__main__.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/eval/phoenix_gate/baseline_store.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/eval/phoenix_gate/cli.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/eval/phoenix_gate/models.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/eval/phoenix_gate/phoenix_client.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/eval/phoenix_gate/regression_gate.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/eval/phoenix_gate/triggers.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/fallback_replay.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/git_learning.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/hooks/__init__.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/hooks/indexer.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/ingest/codex.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/ingest_denylist.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/ingest_guard.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/isolation_proof.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/jsonl_backup.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/kg/__init__.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/kg_cleanup.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/kg_judge.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/kg_promotion.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/kg_repo.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/kg_review_session.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/kg_session_finish.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/kg_session_harvest.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/lexical_defense.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/lexical_defense_dictionary.json +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/mcp/enrich_handler.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/mcp/entity_handler.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/mcp/search_handler.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/mcp/tags_handler.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/mcp_stdio_bridge.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/memory_types.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/migrate.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/p0_longitudinal_count.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/parent_death.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/phonetic.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/__init__.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/agent_enrichment.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/analyze_communication.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/batch_extraction.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/brain_graph.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/chat_tags.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/chunk.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/classify.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/cluster_sampling.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/code_intelligence.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/correction_detection.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/correction_mining.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/digest.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/enrichment.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/enrichment_tiers.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/entity_extraction.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/entity_resolution.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/extract.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/extract_claude_desktop.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/extract_corrections.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/extract_markdown.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/extract_whatsapp.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/frustration_mining.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/git_overlay.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/kg_extraction.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/kg_extraction_groq.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/longitudinal_analyzer.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/obsidian_export.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/operation_grouping.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/plan_linking.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/rate_limiter.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/sanitize.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/secret_scrub.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/semantic_style.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/sentiment.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/session_enrichment.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/style_embed.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/style_index.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/tag_entity_promotion.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/temporal_chains.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/time_batcher.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/unified_timeline.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/write_queue.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/provenance.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/provenance_autosupersede.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/provenance_integration.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/queue_merge.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/reembed_backfill.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/sandbox_db.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/scoping.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/search_fanout.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/search_profile.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/search_repo.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/session_repo.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/storage.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/system_prompt_guard.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/tag_normalization.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/taxonomy.json +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/telemetry.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/types.py +0 -0
- {brainlayer-1.5.2 → brainlayer-1.5.4}/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.4
|
|
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
|
|
@@ -23,7 +23,8 @@ Requires-Dist: abydos>=0.5.0
|
|
|
23
23
|
Requires-Dist: apsw>=3.45.0
|
|
24
24
|
Requires-Dist: google-api-python-client>=2.0.0
|
|
25
25
|
Requires-Dist: google-auth>=2.0.0
|
|
26
|
-
Requires-Dist:
|
|
26
|
+
Requires-Dist: jsonschema>=4.20.0
|
|
27
|
+
Requires-Dist: mcp<3.0.0,>=2.0.0
|
|
27
28
|
Requires-Dist: numpy<3.0,>=1.22
|
|
28
29
|
Requires-Dist: orjson>=3.9.0
|
|
29
30
|
Requires-Dist: pydantic>=2.0.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "brainlayer"
|
|
3
|
-
version = "1.5.
|
|
3
|
+
version = "1.5.4"
|
|
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"
|
|
@@ -16,7 +16,8 @@ dependencies = [
|
|
|
16
16
|
"sentence-transformers>=2.2.0",
|
|
17
17
|
|
|
18
18
|
# MCP server
|
|
19
|
-
"mcp>=
|
|
19
|
+
"mcp>=2.0.0,<3.0.0",
|
|
20
|
+
"jsonschema>=4.20.0", # MCP v2 low-level handlers require explicit schema validation
|
|
20
21
|
|
|
21
22
|
# CLI
|
|
22
23
|
"typer>=0.9.0",
|
|
@@ -15,6 +15,7 @@ import logging
|
|
|
15
15
|
import os
|
|
16
16
|
import signal
|
|
17
17
|
import time
|
|
18
|
+
from collections import OrderedDict
|
|
18
19
|
from pathlib import Path
|
|
19
20
|
from typing import Callable, NamedTuple
|
|
20
21
|
|
|
@@ -37,12 +38,94 @@ LOGGER = logging.getLogger("brainlayer.hotlane_brainbar")
|
|
|
37
38
|
STOP = False
|
|
38
39
|
DEFAULT_HOTLANE_ENRICH_LIMIT = 5
|
|
39
40
|
DEFAULT_BACKLOG_BATCH = 4
|
|
41
|
+
DEFAULT_HOTLANE_EMBED_DEVICE = "cpu"
|
|
40
42
|
MAX_BACKLOG_BATCH = 16
|
|
41
43
|
DEFAULT_HOTLANE_WRITE_BUSY_TIMEOUT_MS = 1000
|
|
42
44
|
MAX_APSW_BUSY_TIMEOUT_MS = 2_147_483_647
|
|
43
45
|
VECTOR_WRITE_YIELD_SECONDS = 0.005
|
|
46
|
+
MAX_PENDING_CANDIDATE_SCAN_PAGES = 16
|
|
47
|
+
HOT_CANDIDATE_SCAN_LIMIT = 256
|
|
48
|
+
HOT_CANDIDATE_HEAD_LIMIT = HOT_CANDIDATE_SCAN_LIMIT // 2
|
|
49
|
+
MAX_HOT_CANDIDATE_RETRIES = 256
|
|
44
50
|
_sleep = time.sleep
|
|
45
51
|
|
|
52
|
+
HOT_CANDIDATE_SCAN_SQL = """
|
|
53
|
+
SELECT
|
|
54
|
+
c.id,
|
|
55
|
+
c.content,
|
|
56
|
+
c.source_file,
|
|
57
|
+
c.source,
|
|
58
|
+
c.archived_at,
|
|
59
|
+
c.superseded_by,
|
|
60
|
+
c.aggregated_into,
|
|
61
|
+
COALESCE(c.archived, 0),
|
|
62
|
+
COALESCE(c.status, 'active'),
|
|
63
|
+
r.id
|
|
64
|
+
FROM chunks c
|
|
65
|
+
LEFT JOIN chunk_vectors_rowids r ON r.id = c.id
|
|
66
|
+
ORDER BY c.created_at DESC
|
|
67
|
+
LIMIT ?
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
HOT_CANDIDATE_ROWID_SCAN_SQL = """
|
|
71
|
+
SELECT
|
|
72
|
+
c.rowid,
|
|
73
|
+
c.id,
|
|
74
|
+
c.content,
|
|
75
|
+
c.source_file,
|
|
76
|
+
c.source,
|
|
77
|
+
c.archived_at,
|
|
78
|
+
c.superseded_by,
|
|
79
|
+
c.aggregated_into,
|
|
80
|
+
COALESCE(c.archived, 0),
|
|
81
|
+
COALESCE(c.status, 'active'),
|
|
82
|
+
r.id
|
|
83
|
+
FROM chunks c
|
|
84
|
+
LEFT JOIN chunk_vectors_rowids r ON r.id = c.id
|
|
85
|
+
ORDER BY c.rowid DESC
|
|
86
|
+
LIMIT ?
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
HOT_CANDIDATE_ROWID_PAGE_SQL = """
|
|
90
|
+
SELECT
|
|
91
|
+
c.rowid,
|
|
92
|
+
c.id,
|
|
93
|
+
c.content,
|
|
94
|
+
c.source_file,
|
|
95
|
+
c.source,
|
|
96
|
+
c.archived_at,
|
|
97
|
+
c.superseded_by,
|
|
98
|
+
c.aggregated_into,
|
|
99
|
+
COALESCE(c.archived, 0),
|
|
100
|
+
COALESCE(c.status, 'active'),
|
|
101
|
+
r.id
|
|
102
|
+
FROM chunks c
|
|
103
|
+
LEFT JOIN chunk_vectors_rowids r ON r.id = c.id
|
|
104
|
+
WHERE c.rowid < ?
|
|
105
|
+
ORDER BY c.rowid DESC
|
|
106
|
+
LIMIT ?
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
HOT_CANDIDATE_ROWID_FORWARD_SQL = """
|
|
110
|
+
SELECT
|
|
111
|
+
c.rowid,
|
|
112
|
+
c.id,
|
|
113
|
+
c.content,
|
|
114
|
+
c.source_file,
|
|
115
|
+
c.source,
|
|
116
|
+
c.archived_at,
|
|
117
|
+
c.superseded_by,
|
|
118
|
+
c.aggregated_into,
|
|
119
|
+
COALESCE(c.archived, 0),
|
|
120
|
+
COALESCE(c.status, 'active'),
|
|
121
|
+
r.id
|
|
122
|
+
FROM chunks c
|
|
123
|
+
LEFT JOIN chunk_vectors_rowids r ON r.id = c.id
|
|
124
|
+
WHERE c.rowid > ?
|
|
125
|
+
ORDER BY c.rowid ASC
|
|
126
|
+
LIMIT ?
|
|
127
|
+
"""
|
|
128
|
+
|
|
46
129
|
|
|
47
130
|
class CycleResult(NamedTuple):
|
|
48
131
|
embedded: int = 0
|
|
@@ -58,12 +141,201 @@ class EmbedCandidate(NamedTuple):
|
|
|
58
141
|
content: str
|
|
59
142
|
|
|
60
143
|
|
|
144
|
+
class PendingCandidateScanState:
|
|
145
|
+
def __init__(self) -> None:
|
|
146
|
+
self.active = False
|
|
147
|
+
self.after_created_at: str | None = None
|
|
148
|
+
self.after_rowid = 0
|
|
149
|
+
|
|
150
|
+
def reset(self) -> None:
|
|
151
|
+
self.active = False
|
|
152
|
+
self.after_created_at = None
|
|
153
|
+
self.after_rowid = 0
|
|
154
|
+
|
|
155
|
+
|
|
61
156
|
class EmbeddedVector(NamedTuple):
|
|
62
157
|
chunk_id: str
|
|
63
158
|
content: str
|
|
64
159
|
embedding: list[float]
|
|
65
160
|
|
|
66
161
|
|
|
162
|
+
def _candidates_from_scanned_rows(
|
|
163
|
+
rows: list[tuple], *, limit: int, exclude_ids: set[str] | None = None
|
|
164
|
+
) -> tuple[list[EmbedCandidate], int | None, int | None, bool]:
|
|
165
|
+
if limit <= 0:
|
|
166
|
+
return [], None, None, False
|
|
167
|
+
candidates: list[EmbedCandidate] = []
|
|
168
|
+
exclude_ids = exclude_ids or set()
|
|
169
|
+
first_candidate_rowid: int | None = None
|
|
170
|
+
last_inspected_rowid: int | None = None
|
|
171
|
+
for index, row in enumerate(rows):
|
|
172
|
+
(
|
|
173
|
+
rowid,
|
|
174
|
+
chunk_id,
|
|
175
|
+
content,
|
|
176
|
+
source_file,
|
|
177
|
+
source,
|
|
178
|
+
archived_at,
|
|
179
|
+
superseded_by,
|
|
180
|
+
aggregated_into,
|
|
181
|
+
archived,
|
|
182
|
+
status,
|
|
183
|
+
vector_id,
|
|
184
|
+
) = row
|
|
185
|
+
last_inspected_rowid = int(rowid)
|
|
186
|
+
eligible = (
|
|
187
|
+
vector_id is None
|
|
188
|
+
and source_file == "brainbar-store"
|
|
189
|
+
and source == "mcp"
|
|
190
|
+
and content
|
|
191
|
+
and archived_at is None
|
|
192
|
+
and superseded_by is None
|
|
193
|
+
and aggregated_into is None
|
|
194
|
+
and not archived
|
|
195
|
+
and status == "active"
|
|
196
|
+
)
|
|
197
|
+
if eligible:
|
|
198
|
+
if first_candidate_rowid is None:
|
|
199
|
+
first_candidate_rowid = int(rowid)
|
|
200
|
+
if str(chunk_id) in exclude_ids:
|
|
201
|
+
continue
|
|
202
|
+
candidates.append(EmbedCandidate(str(chunk_id), str(content)))
|
|
203
|
+
if len(candidates) >= limit:
|
|
204
|
+
return candidates, first_candidate_rowid, last_inspected_rowid, index == len(rows) - 1
|
|
205
|
+
return candidates, first_candidate_rowid, last_inspected_rowid, True
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class HotCandidateScanner:
|
|
209
|
+
"""Scan bounded rowid lanes while retaining returned candidates for retry."""
|
|
210
|
+
|
|
211
|
+
def __init__(self) -> None:
|
|
212
|
+
self._before_rowid: int | None = None
|
|
213
|
+
self._newest_seen_rowid: int | None = None
|
|
214
|
+
self._retries: OrderedDict[str, EmbedCandidate] = OrderedDict()
|
|
215
|
+
self._retry_turn = True
|
|
216
|
+
self._retry_buffer_full_logged = False
|
|
217
|
+
|
|
218
|
+
def _refresh_retries(self, cursor: apsw.Cursor) -> None:
|
|
219
|
+
if not self._retries:
|
|
220
|
+
return
|
|
221
|
+
placeholders = ",".join("?" for _ in self._retries)
|
|
222
|
+
rows = cursor.execute(
|
|
223
|
+
f"""
|
|
224
|
+
SELECT c.id, c.content
|
|
225
|
+
FROM chunks c
|
|
226
|
+
LEFT JOIN chunk_vectors_rowids r ON r.id = c.id
|
|
227
|
+
WHERE c.id IN ({placeholders})
|
|
228
|
+
AND r.id IS NULL
|
|
229
|
+
AND c.source_file = 'brainbar-store'
|
|
230
|
+
AND c.source = 'mcp'
|
|
231
|
+
AND c.content IS NOT NULL
|
|
232
|
+
AND c.content != ''
|
|
233
|
+
AND c.archived_at IS NULL
|
|
234
|
+
AND c.superseded_by IS NULL
|
|
235
|
+
AND c.aggregated_into IS NULL
|
|
236
|
+
AND COALESCE(c.archived, 0) = 0
|
|
237
|
+
AND COALESCE(c.status, 'active') = 'active'
|
|
238
|
+
""",
|
|
239
|
+
tuple(self._retries),
|
|
240
|
+
)
|
|
241
|
+
active = {str(chunk_id): EmbedCandidate(str(chunk_id), str(content)) for chunk_id, content in rows}
|
|
242
|
+
for chunk_id in list(self._retries):
|
|
243
|
+
if chunk_id not in active:
|
|
244
|
+
del self._retries[chunk_id]
|
|
245
|
+
else:
|
|
246
|
+
self._retries[chunk_id] = active[chunk_id]
|
|
247
|
+
if len(self._retries) < MAX_HOT_CANDIDATE_RETRIES:
|
|
248
|
+
self._retry_buffer_full_logged = False
|
|
249
|
+
|
|
250
|
+
def _take_retries(self, *, limit: int) -> list[EmbedCandidate]:
|
|
251
|
+
selected: list[EmbedCandidate] = []
|
|
252
|
+
for _ in range(min(limit, len(self._retries))):
|
|
253
|
+
chunk_id, candidate = self._retries.popitem(last=False)
|
|
254
|
+
self._retries[chunk_id] = candidate
|
|
255
|
+
selected.append(candidate)
|
|
256
|
+
return selected
|
|
257
|
+
|
|
258
|
+
def _retain(self, candidates: list[EmbedCandidate]) -> None:
|
|
259
|
+
for candidate in candidates:
|
|
260
|
+
if candidate.chunk_id in self._retries:
|
|
261
|
+
continue
|
|
262
|
+
if len(self._retries) >= MAX_HOT_CANDIDATE_RETRIES:
|
|
263
|
+
if not self._retry_buffer_full_logged:
|
|
264
|
+
LOGGER.warning("hot candidate retry buffer full; pausing new retry retention")
|
|
265
|
+
self._retry_buffer_full_logged = True
|
|
266
|
+
break
|
|
267
|
+
self._retries[candidate.chunk_id] = candidate
|
|
268
|
+
|
|
269
|
+
def __call__(self, store: VectorStore, *, limit: int) -> list[EmbedCandidate]:
|
|
270
|
+
cursor = store.conn.cursor()
|
|
271
|
+
self._refresh_retries(cursor)
|
|
272
|
+
if limit <= 0:
|
|
273
|
+
return []
|
|
274
|
+
if limit == 1 and self._retries:
|
|
275
|
+
retry_limit = 1 if self._retry_turn else 0
|
|
276
|
+
self._retry_turn = not self._retry_turn
|
|
277
|
+
else:
|
|
278
|
+
retry_limit = min(len(self._retries), max(1, limit // 2))
|
|
279
|
+
candidates = self._take_retries(limit=retry_limit)
|
|
280
|
+
retention_slots = MAX_HOT_CANDIDATE_RETRIES - len(self._retries)
|
|
281
|
+
scan_limit = min(limit - len(candidates), retention_slots)
|
|
282
|
+
if scan_limit <= 0:
|
|
283
|
+
return candidates
|
|
284
|
+
retry_ids = set(self._retries)
|
|
285
|
+
head_rows = list(cursor.execute(HOT_CANDIDATE_ROWID_SCAN_SQL, (HOT_CANDIDATE_HEAD_LIMIT,)))
|
|
286
|
+
head_candidates, _, last_inspected_rowid, _ = _candidates_from_scanned_rows(
|
|
287
|
+
head_rows,
|
|
288
|
+
limit=scan_limit,
|
|
289
|
+
exclude_ids=retry_ids,
|
|
290
|
+
)
|
|
291
|
+
candidates.extend(head_candidates)
|
|
292
|
+
retention_slots -= len(head_candidates)
|
|
293
|
+
if not head_rows:
|
|
294
|
+
self._retain(candidates)
|
|
295
|
+
return candidates
|
|
296
|
+
if self._newest_seen_rowid is None:
|
|
297
|
+
self._newest_seen_rowid = last_inspected_rowid
|
|
298
|
+
if self._before_rowid is None and last_inspected_rowid is not None:
|
|
299
|
+
self._before_rowid = last_inspected_rowid
|
|
300
|
+
if len(candidates) >= limit or retention_slots <= 0:
|
|
301
|
+
self._retain(candidates)
|
|
302
|
+
return candidates
|
|
303
|
+
|
|
304
|
+
page_limit = HOT_CANDIDATE_SCAN_LIMIT - HOT_CANDIDATE_HEAD_LIMIT
|
|
305
|
+
forward_rows = list(
|
|
306
|
+
cursor.execute(
|
|
307
|
+
HOT_CANDIDATE_ROWID_FORWARD_SQL,
|
|
308
|
+
(self._newest_seen_rowid, page_limit),
|
|
309
|
+
)
|
|
310
|
+
)
|
|
311
|
+
if forward_rows:
|
|
312
|
+
forward_candidates, _, last_inspected_rowid, _ = _candidates_from_scanned_rows(
|
|
313
|
+
forward_rows,
|
|
314
|
+
limit=min(limit - len(candidates), retention_slots),
|
|
315
|
+
exclude_ids=retry_ids | {candidate.chunk_id for candidate in candidates},
|
|
316
|
+
)
|
|
317
|
+
candidates.extend(forward_candidates)
|
|
318
|
+
retention_slots -= len(forward_candidates)
|
|
319
|
+
self._newest_seen_rowid = last_inspected_rowid
|
|
320
|
+
if len(candidates) >= limit or retention_slots <= 0 or self._before_rowid is None:
|
|
321
|
+
self._retain(candidates)
|
|
322
|
+
return candidates[:limit]
|
|
323
|
+
|
|
324
|
+
page_rows = list(cursor.execute(HOT_CANDIDATE_ROWID_PAGE_SQL, (self._before_rowid, page_limit)))
|
|
325
|
+
if page_rows:
|
|
326
|
+
page_candidates, _, last_inspected_rowid, fully_scanned = _candidates_from_scanned_rows(
|
|
327
|
+
page_rows,
|
|
328
|
+
limit=min(limit - len(candidates), retention_slots),
|
|
329
|
+
exclude_ids=retry_ids | {candidate.chunk_id for candidate in candidates},
|
|
330
|
+
)
|
|
331
|
+
candidates.extend(page_candidates)
|
|
332
|
+
self._before_rowid = last_inspected_rowid
|
|
333
|
+
if fully_scanned and len(page_rows) < page_limit:
|
|
334
|
+
self._before_rowid = None
|
|
335
|
+
self._retain(candidates)
|
|
336
|
+
return candidates[:limit]
|
|
337
|
+
|
|
338
|
+
|
|
67
339
|
def _stop(_signum: int, _frame: object) -> None:
|
|
68
340
|
global STOP
|
|
69
341
|
STOP = True
|
|
@@ -95,48 +367,135 @@ def _candidate_chunk_ids(store: VectorStore, *, limit: int) -> list[str]:
|
|
|
95
367
|
|
|
96
368
|
def _candidate_chunk_rows(store: VectorStore, *, limit: int) -> list[EmbedCandidate]:
|
|
97
369
|
rows = store.conn.cursor().execute(
|
|
98
|
-
|
|
99
|
-
|
|
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,),
|
|
370
|
+
HOT_CANDIDATE_SCAN_SQL,
|
|
371
|
+
(HOT_CANDIDATE_SCAN_LIMIT,),
|
|
116
372
|
)
|
|
117
|
-
|
|
373
|
+
candidates: list[EmbedCandidate] = []
|
|
374
|
+
for row in rows:
|
|
375
|
+
(
|
|
376
|
+
chunk_id,
|
|
377
|
+
content,
|
|
378
|
+
source_file,
|
|
379
|
+
source,
|
|
380
|
+
archived_at,
|
|
381
|
+
superseded_by,
|
|
382
|
+
aggregated_into,
|
|
383
|
+
archived,
|
|
384
|
+
status,
|
|
385
|
+
vector_id,
|
|
386
|
+
) = row
|
|
387
|
+
if (
|
|
388
|
+
vector_id is None
|
|
389
|
+
and source_file == "brainbar-store"
|
|
390
|
+
and source == "mcp"
|
|
391
|
+
and content
|
|
392
|
+
and archived_at is None
|
|
393
|
+
and superseded_by is None
|
|
394
|
+
and aggregated_into is None
|
|
395
|
+
and not archived
|
|
396
|
+
and status == "active"
|
|
397
|
+
):
|
|
398
|
+
candidates.append(EmbedCandidate(str(chunk_id), str(content)))
|
|
399
|
+
if len(candidates) >= limit:
|
|
400
|
+
break
|
|
401
|
+
return candidates
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _pending_chunk_rows(
|
|
405
|
+
store: VectorStore,
|
|
406
|
+
*,
|
|
407
|
+
limit: int,
|
|
408
|
+
scan_state: PendingCandidateScanState | None = None,
|
|
409
|
+
) -> list[EmbedCandidate]:
|
|
410
|
+
if limit <= 0:
|
|
411
|
+
return []
|
|
118
412
|
|
|
413
|
+
scan_limit = limit
|
|
414
|
+
candidates: list[EmbedCandidate] = []
|
|
415
|
+
after_created_at = scan_state.after_created_at if scan_state and scan_state.active else None
|
|
416
|
+
after_rowid = scan_state.after_rowid if scan_state and scan_state.active else 0
|
|
417
|
+
first_page = not scan_state or not scan_state.active
|
|
418
|
+
exhausted = False
|
|
419
|
+
|
|
420
|
+
for _page in range(MAX_PENDING_CANDIDATE_SCAN_PAGES):
|
|
421
|
+
if first_page:
|
|
422
|
+
page_filter = ""
|
|
423
|
+
bindings: tuple[object, ...] = (scan_limit,)
|
|
424
|
+
elif after_created_at is None:
|
|
425
|
+
page_filter = "AND ((c.created_at IS NULL AND c.rowid > ?) OR c.created_at IS NOT NULL)"
|
|
426
|
+
bindings = (after_rowid, scan_limit)
|
|
427
|
+
else:
|
|
428
|
+
page_filter = """
|
|
429
|
+
AND (c.created_at, c.rowid) > (?, ?)
|
|
430
|
+
"""
|
|
431
|
+
bindings = (after_created_at, after_rowid, scan_limit)
|
|
432
|
+
|
|
433
|
+
id_rows = list(
|
|
434
|
+
store.conn.cursor().execute(
|
|
435
|
+
f"""
|
|
436
|
+
SELECT c.id, c.created_at, c.rowid
|
|
437
|
+
FROM chunks c
|
|
438
|
+
LEFT JOIN chunk_vectors_rowids r ON c.id = r.id
|
|
439
|
+
WHERE r.id IS NULL
|
|
440
|
+
{page_filter}
|
|
441
|
+
ORDER BY c.created_at ASC
|
|
442
|
+
LIMIT ?
|
|
443
|
+
""",
|
|
444
|
+
bindings,
|
|
445
|
+
)
|
|
446
|
+
)
|
|
447
|
+
if not id_rows:
|
|
448
|
+
exhausted = True
|
|
449
|
+
break
|
|
119
450
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
451
|
+
candidate_ids = [str(row[0]) for row in id_rows]
|
|
452
|
+
placeholders = ", ".join("?" for _chunk_id in candidate_ids)
|
|
453
|
+
content_rows = store.conn.cursor().execute(
|
|
454
|
+
f"""
|
|
455
|
+
SELECT
|
|
456
|
+
id,
|
|
457
|
+
content,
|
|
458
|
+
archived_at,
|
|
459
|
+
superseded_by,
|
|
460
|
+
aggregated_into,
|
|
461
|
+
COALESCE(archived, 0),
|
|
462
|
+
COALESCE(status, 'active')
|
|
463
|
+
FROM chunks
|
|
464
|
+
WHERE id IN ({placeholders})
|
|
465
|
+
""",
|
|
466
|
+
tuple(candidate_ids),
|
|
467
|
+
)
|
|
468
|
+
content_by_id = {
|
|
469
|
+
str(row[0]): str(row[1])
|
|
470
|
+
for row in content_rows
|
|
471
|
+
if row[1] and row[2] is None and row[3] is None and row[4] is None and not row[5] and row[6] == "active"
|
|
472
|
+
}
|
|
473
|
+
candidates.extend(
|
|
474
|
+
EmbedCandidate(chunk_id, content_by_id[chunk_id]) for chunk_id in candidate_ids if chunk_id in content_by_id
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
after_created_at = id_rows[-1][1]
|
|
478
|
+
after_rowid = int(id_rows[-1][2])
|
|
479
|
+
first_page = False
|
|
480
|
+
if len(candidates) >= limit or len(id_rows) < scan_limit:
|
|
481
|
+
exhausted = len(id_rows) < scan_limit
|
|
482
|
+
break
|
|
483
|
+
|
|
484
|
+
if scan_state is not None:
|
|
485
|
+
if exhausted:
|
|
486
|
+
scan_state.reset()
|
|
487
|
+
else:
|
|
488
|
+
scan_state.active = True
|
|
489
|
+
scan_state.after_created_at = after_created_at
|
|
490
|
+
scan_state.after_rowid = after_rowid
|
|
491
|
+
return candidates[:limit]
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
PENDING_CANDIDATE_SCAN_STATE = PendingCandidateScanState()
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def _pending_chunk_rows_with_resume(store: VectorStore, *, limit: int) -> list[EmbedCandidate]:
|
|
498
|
+
return _pending_chunk_rows(store, limit=limit, scan_state=PENDING_CANDIDATE_SCAN_STATE)
|
|
140
499
|
|
|
141
500
|
|
|
142
501
|
def _embed_candidates(
|
|
@@ -335,9 +694,11 @@ def _callable_accepts_keyword(func: Callable[..., object], keyword: str) -> bool
|
|
|
335
694
|
signature = inspect.signature(func)
|
|
336
695
|
except (TypeError, ValueError):
|
|
337
696
|
return True
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
697
|
+
parameter = signature.parameters.get(keyword)
|
|
698
|
+
return (
|
|
699
|
+
parameter is not None
|
|
700
|
+
and parameter.kind in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY)
|
|
701
|
+
) or any(parameter.kind == inspect.Parameter.VAR_KEYWORD for parameter in signature.parameters.values())
|
|
341
702
|
|
|
342
703
|
|
|
343
704
|
def _type_error_rejects_keyword(exc: TypeError, keyword: str) -> bool:
|
|
@@ -349,6 +710,12 @@ def _type_error_rejects_keyword(exc: TypeError, keyword: str) -> bool:
|
|
|
349
710
|
)
|
|
350
711
|
|
|
351
712
|
|
|
713
|
+
def _create_embedding_model(model_factory: Callable[..., object], *, device: str) -> object:
|
|
714
|
+
if _callable_accepts_keyword(model_factory, "device"):
|
|
715
|
+
return model_factory(device=device)
|
|
716
|
+
return model_factory()
|
|
717
|
+
|
|
718
|
+
|
|
352
719
|
def _run_split_cycle(
|
|
353
720
|
*,
|
|
354
721
|
db_path: Path,
|
|
@@ -361,7 +728,7 @@ def _run_split_cycle(
|
|
|
361
728
|
embed_batch_fn: Callable[[list[str]], list[list[float]]] | None = None,
|
|
362
729
|
enrich_fn: Callable[..., object] = enrich_realtime,
|
|
363
730
|
candidate_rows_fn: Callable[..., list[EmbedCandidate]] = _candidate_chunk_rows,
|
|
364
|
-
pending_rows_fn: Callable[..., list[EmbedCandidate]] =
|
|
731
|
+
pending_rows_fn: Callable[..., list[EmbedCandidate]] = _pending_chunk_rows_with_resume,
|
|
365
732
|
write_vectors_fn: Callable[..., int] = _write_embedded_vectors,
|
|
366
733
|
) -> CycleResult:
|
|
367
734
|
embedded = 0
|
|
@@ -419,9 +786,13 @@ def _queue_depth(queue_dir: Path) -> int:
|
|
|
419
786
|
return 0
|
|
420
787
|
|
|
421
788
|
|
|
789
|
+
def _is_enrichment_queue_file(path: Path) -> bool:
|
|
790
|
+
return path.name.startswith("enrichment-") or path.name.startswith("queue-enrichment")
|
|
791
|
+
|
|
792
|
+
|
|
422
793
|
def _high_priority_queue_depth(queue_dir: Path) -> int:
|
|
423
794
|
try:
|
|
424
|
-
return sum(1 for path in queue_dir.glob("*.jsonl") if not path
|
|
795
|
+
return sum(1 for path in queue_dir.glob("*.jsonl") if not _is_enrichment_queue_file(path))
|
|
425
796
|
except OSError:
|
|
426
797
|
return 0
|
|
427
798
|
|
|
@@ -479,7 +850,8 @@ def run(
|
|
|
479
850
|
enrich_limit: int,
|
|
480
851
|
enrich_since_hours: int,
|
|
481
852
|
vector_store_cls: Callable[[Path], VectorStore] = VectorStore,
|
|
482
|
-
model_factory: Callable[
|
|
853
|
+
model_factory: Callable[..., object] = get_embedding_model,
|
|
854
|
+
embedding_device: str = DEFAULT_HOTLANE_EMBED_DEVICE,
|
|
483
855
|
cycle_fn: Callable[..., CycleResult] = run_cycle,
|
|
484
856
|
time_fn: Callable[[], float] = time.monotonic,
|
|
485
857
|
sleep_fn: Callable[[float], None] = time.sleep,
|
|
@@ -488,7 +860,7 @@ def run(
|
|
|
488
860
|
queue_depth_fn: Callable[[Path], int] = _queue_depth,
|
|
489
861
|
high_priority_queue_depth_fn: Callable[[Path], int] = _high_priority_queue_depth,
|
|
490
862
|
) -> None:
|
|
491
|
-
model = model_factory
|
|
863
|
+
model = _create_embedding_model(model_factory, device=embedding_device)
|
|
492
864
|
embed_batch_fn = getattr(model, "embed_texts", None)
|
|
493
865
|
if embed_batch_fn is not None:
|
|
494
866
|
|
|
@@ -505,6 +877,9 @@ def run(
|
|
|
505
877
|
last_backlog = time_fn() - backlog_interval
|
|
506
878
|
last_enrich = 0.0
|
|
507
879
|
enrich_disabled = False
|
|
880
|
+
queue_backpressure_active = False
|
|
881
|
+
backlog_slice_logged = False
|
|
882
|
+
hot_candidate_scanner = HotCandidateScanner()
|
|
508
883
|
cycles = 0
|
|
509
884
|
backlog_batch = min(max(backlog_batch, 0), MAX_BACKLOG_BATCH)
|
|
510
885
|
LOGGER.info("hotlane adapter started db=%s", db_path)
|
|
@@ -515,23 +890,34 @@ def run(
|
|
|
515
890
|
now = time_fn()
|
|
516
891
|
queue_has_backlog = queue_depth_fn(queue_dir) > 0
|
|
517
892
|
queue_has_high_priority_backlog = queue_has_backlog and high_priority_queue_depth_fn(queue_dir) > 0
|
|
518
|
-
if
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
LOGGER.info("durable queue has backlog;
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
893
|
+
cycle_backlog_batch = backlog_batch if backlog_batch > 0 and now - last_backlog >= backlog_interval else 0
|
|
894
|
+
cycle_recent_limit = recent_limit
|
|
895
|
+
if queue_has_high_priority_backlog:
|
|
896
|
+
if not queue_backpressure_active:
|
|
897
|
+
LOGGER.info("durable high-priority queue has backlog; suppressing hot embedding and enrichment")
|
|
898
|
+
queue_backpressure_active = True
|
|
899
|
+
if cycle_backlog_batch <= 0:
|
|
900
|
+
cycles += 1
|
|
901
|
+
sleep_fn(interval)
|
|
902
|
+
continue
|
|
903
|
+
cycle_recent_limit = 0
|
|
904
|
+
if not backlog_slice_logged:
|
|
905
|
+
LOGGER.info(
|
|
906
|
+
"durable high-priority queue has backlog; reserving backlog embedding slice batch=%d",
|
|
907
|
+
cycle_backlog_batch,
|
|
908
|
+
)
|
|
909
|
+
backlog_slice_logged = True
|
|
526
910
|
else:
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
911
|
+
queue_backpressure_active = False
|
|
912
|
+
backlog_slice_logged = False
|
|
913
|
+
cycle_enrich_limit = (
|
|
914
|
+
enrich_limit
|
|
915
|
+
if not queue_has_backlog
|
|
916
|
+
and not enrich_disabled
|
|
917
|
+
and enrich_limit > 0
|
|
918
|
+
and now - last_enrich >= enrich_interval
|
|
919
|
+
else 0
|
|
920
|
+
)
|
|
535
921
|
if cycle_backlog_batch > 0:
|
|
536
922
|
last_backlog = now
|
|
537
923
|
if cycle_enrich_limit > 0:
|
|
@@ -541,11 +927,12 @@ def run(
|
|
|
541
927
|
db_path=db_path,
|
|
542
928
|
vector_store_cls=vector_store_cls,
|
|
543
929
|
embed_fn=embed_fn,
|
|
544
|
-
recent_limit=
|
|
930
|
+
recent_limit=cycle_recent_limit,
|
|
545
931
|
backlog_batch=cycle_backlog_batch,
|
|
546
932
|
embed_batch_fn=embed_batch_fn,
|
|
547
933
|
enrich_limit=cycle_enrich_limit,
|
|
548
934
|
enrich_since_hours=enrich_since_hours,
|
|
935
|
+
candidate_rows_fn=hot_candidate_scanner,
|
|
549
936
|
)
|
|
550
937
|
else:
|
|
551
938
|
store = vector_store_cls(db_path)
|
|
@@ -553,7 +940,7 @@ def run(
|
|
|
553
940
|
result = cycle_fn(
|
|
554
941
|
store=store,
|
|
555
942
|
embed_fn=embed_fn,
|
|
556
|
-
recent_limit=
|
|
943
|
+
recent_limit=cycle_recent_limit,
|
|
557
944
|
backlog_batch=cycle_backlog_batch,
|
|
558
945
|
embed_batch_fn=embed_batch_fn,
|
|
559
946
|
enrich_limit=cycle_enrich_limit,
|
|
@@ -3,8 +3,14 @@ set -euo pipefail
|
|
|
3
3
|
|
|
4
4
|
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:$HOME/.local/bin"
|
|
5
5
|
export PYTHONUNBUFFERED=1
|
|
6
|
-
: "${
|
|
6
|
+
: "${BRAINLAYER_BACKUP_CLIENT_TIMEOUT_SECONDS:=0}"
|
|
7
|
+
export BRAINLAYER_BACKUP_CLIENT_TIMEOUT_SECONDS
|
|
8
|
+
: "${BRAINLAYER_BACKUP_TIMEOUT_SECONDS:=21600}"
|
|
7
9
|
export BRAINLAYER_BACKUP_TIMEOUT_SECONDS
|
|
10
|
+
: "${BRAINLAYER_BACKUP_SQLITE_CHECK_TIMEOUT_SECONDS:=0}"
|
|
11
|
+
export BRAINLAYER_BACKUP_SQLITE_CHECK_TIMEOUT_SECONDS
|
|
12
|
+
: "${BRAINLAYER_BACKUP_ATTEMPT_MAX_AGE_SECONDS:=86400}"
|
|
13
|
+
export BRAINLAYER_BACKUP_ATTEMPT_MAX_AGE_SECONDS
|
|
8
14
|
: "${BRAINLAYER_BACKUP_LOG_PROVENANCE:=real}"
|
|
9
15
|
export BRAINLAYER_BACKUP_LOG_PROVENANCE
|
|
10
16
|
BRAINLAYER_DIR="${BRAINLAYER_DIR:-__BRAINLAYER_DIR_VALUE__}"
|