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.
Files changed (203) hide show
  1. {brainlayer-1.5.2 → brainlayer-1.5.4}/.gitignore +2 -0
  2. {brainlayer-1.5.2 → brainlayer-1.5.4}/PKG-INFO +3 -2
  3. {brainlayer-1.5.2 → brainlayer-1.5.4}/pyproject.toml +3 -2
  4. {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/hotlane_brainbar_daemon.py +451 -64
  5. {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/backup-daily.sh +7 -1
  6. brainlayer-1.5.4/scripts/launchd/com.brainlayer.t3-ingest.plist +53 -0
  7. {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/com.brainlayer.wal-checkpoint.plist +3 -4
  8. {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/install.sh +7 -2
  9. {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/throughput-watchdog.py +225 -39
  10. {brainlayer-1.5.2 → brainlayer-1.5.4}/server.json +2 -2
  11. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/__init__.py +1 -1
  12. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/agent_provenance.py +13 -0
  13. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/backup_daily.py +401 -70
  14. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/brainbar_hybrid_helper.py +1 -1
  15. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/cli/__init__.py +85 -11
  16. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/deploy_drift.py +81 -24
  17. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/doctor.py +37 -8
  18. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/drain.py +161 -51
  19. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/embeddings.py +15 -11
  20. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/health_check.py +40 -21
  21. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/index_new.py +31 -25
  22. brainlayer-1.5.4/src/brainlayer/ingest/__init__.py +27 -0
  23. brainlayer-1.5.4/src/brainlayer/ingest/t3.py +404 -0
  24. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/launchd_primitive.py +31 -6
  25. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/maintenance.py +108 -0
  26. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/mcp/__init__.py +162 -62
  27. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/mcp/_format.py +11 -2
  28. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/mcp/_shared.py +1 -1
  29. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/mcp/palette.py +1 -1
  30. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/mcp/store_handler.py +106 -10
  31. brainlayer-1.5.4/src/brainlayer/paths.py +55 -0
  32. brainlayer-1.5.4/src/brainlayer/pause.py +46 -0
  33. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/queue_io.py +6 -2
  34. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/runtime_store.py +2 -0
  35. brainlayer-1.5.4/src/brainlayer/setup.py +278 -0
  36. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/store.py +14 -0
  37. brainlayer-1.5.4/src/brainlayer/t3_provenance.py +97 -0
  38. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/vector_store.py +46 -12
  39. brainlayer-1.5.4/src/brainlayer/wal_checkpoint.py +175 -0
  40. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/watcher.py +504 -127
  41. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/watcher_bridge.py +127 -14
  42. brainlayer-1.5.2/src/brainlayer/ingest/__init__.py +0 -1
  43. brainlayer-1.5.2/src/brainlayer/paths.py +0 -28
  44. brainlayer-1.5.2/src/brainlayer/setup.py +0 -114
  45. brainlayer-1.5.2/src/brainlayer/wal_checkpoint.py +0 -72
  46. {brainlayer-1.5.2 → brainlayer-1.5.4}/LICENSE +0 -0
  47. {brainlayer-1.5.2 → brainlayer-1.5.4}/README.md +0 -0
  48. {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/brainlayer-env-run.sh +0 -0
  49. {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/brainlayer.env.example +0 -0
  50. {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/com.brainlayer.backup-daily.plist +0 -0
  51. {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/com.brainlayer.decay.plist +0 -0
  52. {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/com.brainlayer.drain.plist +0 -0
  53. {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/com.brainlayer.enrichment.plist +0 -0
  54. {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/com.brainlayer.health-check.plist +0 -0
  55. {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/com.brainlayer.hotlane-brainbar.plist +0 -0
  56. {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/com.brainlayer.index.plist +0 -0
  57. {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/com.brainlayer.jsonl-backup.plist +0 -0
  58. {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/com.brainlayer.maintenance-nightly.plist +0 -0
  59. {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/com.brainlayer.maintenance-weekly.plist +0 -0
  60. {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/com.brainlayer.p0-counter.plist +0 -0
  61. {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/com.brainlayer.repair-fts.plist +0 -0
  62. {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/com.brainlayer.throughput-watchdog.plist +0 -0
  63. {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/com.brainlayer.tier0-watchdog.plist +0 -0
  64. {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/com.brainlayer.watch.plist +0 -0
  65. {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/launchd/jsonl-backup.sh +0 -0
  66. {brainlayer-1.5.2 → brainlayer-1.5.4}/scripts/tier0-watchdog.sh +0 -0
  67. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/_helpers.py +0 -0
  68. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/agent_profiles.py +0 -0
  69. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/alarm.py +0 -0
  70. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/bitemporal.py +0 -0
  71. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/calibrate.py +0 -0
  72. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/chunk_origin.py +0 -0
  73. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/chunk_origin_backfill.py +0 -0
  74. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/classify.py +0 -0
  75. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/claude_paths.py +0 -0
  76. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/cli/wizard.py +0 -0
  77. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/cli_new.py +0 -0
  78. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/cloud_backfill.py +0 -0
  79. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/clustering.py +0 -0
  80. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/config.py +0 -0
  81. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/content_class.py +0 -0
  82. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/correction_judge.py +0 -0
  83. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/data/agents_registry.yaml +0 -0
  84. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/data/sandbox_seeds/skill-eval-baseline.json +0 -0
  85. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/db_shrink.py +0 -0
  86. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/decay.py +0 -0
  87. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/decay_backfill.py +0 -0
  88. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/decay_job.py +0 -0
  89. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/dedupe.py +0 -0
  90. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/drain_liveness.py +0 -0
  91. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/engine.py +0 -0
  92. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/enrichment_controller.py +0 -0
  93. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/eval/__init__.py +0 -0
  94. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/eval/abcde_enrich_runner.py +0 -0
  95. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/eval/abcde_variants.py +0 -0
  96. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/eval/abcde_variants.yaml +0 -0
  97. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/eval/benchmark.py +0 -0
  98. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/eval/enrichment_gold.py +0 -0
  99. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/eval/enrichment_graders.py +0 -0
  100. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/eval/enrichment_judge.py +0 -0
  101. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/eval/enrichment_llm_judge.py +0 -0
  102. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/eval/enrichment_quality_benchmark.py +0 -0
  103. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/eval/experiment_store.py +0 -0
  104. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/eval/fixtures/README.md +0 -0
  105. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/eval/labeling/enrichment-labeler.html +0 -0
  106. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/eval/phoenix_gate/__init__.py +0 -0
  107. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/eval/phoenix_gate/__main__.py +0 -0
  108. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/eval/phoenix_gate/baseline_store.py +0 -0
  109. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/eval/phoenix_gate/cli.py +0 -0
  110. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/eval/phoenix_gate/models.py +0 -0
  111. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/eval/phoenix_gate/phoenix_client.py +0 -0
  112. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/eval/phoenix_gate/regression_gate.py +0 -0
  113. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/eval/phoenix_gate/triggers.py +0 -0
  114. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/fallback_replay.py +0 -0
  115. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/git_learning.py +0 -0
  116. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/hooks/__init__.py +0 -0
  117. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/hooks/indexer.py +0 -0
  118. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/ingest/codex.py +0 -0
  119. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/ingest_denylist.py +0 -0
  120. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/ingest_guard.py +0 -0
  121. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/isolation_proof.py +0 -0
  122. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/jsonl_backup.py +0 -0
  123. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/kg/__init__.py +0 -0
  124. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/kg_cleanup.py +0 -0
  125. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/kg_judge.py +0 -0
  126. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/kg_promotion.py +0 -0
  127. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/kg_repo.py +0 -0
  128. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/kg_review_session.py +0 -0
  129. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/kg_session_finish.py +0 -0
  130. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/kg_session_harvest.py +0 -0
  131. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/lexical_defense.py +0 -0
  132. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/lexical_defense_dictionary.json +0 -0
  133. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/mcp/enrich_handler.py +0 -0
  134. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/mcp/entity_handler.py +0 -0
  135. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/mcp/search_handler.py +0 -0
  136. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/mcp/tags_handler.py +0 -0
  137. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/mcp_stdio_bridge.py +0 -0
  138. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/memory_types.py +0 -0
  139. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/migrate.py +0 -0
  140. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/p0_longitudinal_count.py +0 -0
  141. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/parent_death.py +0 -0
  142. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/phonetic.py +0 -0
  143. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/__init__.py +0 -0
  144. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/agent_enrichment.py +0 -0
  145. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/analyze_communication.py +0 -0
  146. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/batch_extraction.py +0 -0
  147. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/brain_graph.py +0 -0
  148. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/chat_tags.py +0 -0
  149. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/chunk.py +0 -0
  150. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/classify.py +0 -0
  151. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/cluster_sampling.py +0 -0
  152. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/code_intelligence.py +0 -0
  153. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/correction_detection.py +0 -0
  154. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/correction_mining.py +0 -0
  155. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/digest.py +0 -0
  156. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/enrichment.py +0 -0
  157. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/enrichment_tiers.py +0 -0
  158. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/entity_extraction.py +0 -0
  159. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/entity_resolution.py +0 -0
  160. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/extract.py +0 -0
  161. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/extract_claude_desktop.py +0 -0
  162. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/extract_corrections.py +0 -0
  163. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/extract_markdown.py +0 -0
  164. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/extract_whatsapp.py +0 -0
  165. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/frustration_mining.py +0 -0
  166. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/git_overlay.py +0 -0
  167. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/kg_extraction.py +0 -0
  168. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/kg_extraction_groq.py +0 -0
  169. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/longitudinal_analyzer.py +0 -0
  170. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/obsidian_export.py +0 -0
  171. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/operation_grouping.py +0 -0
  172. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/plan_linking.py +0 -0
  173. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/rate_limiter.py +0 -0
  174. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/sanitize.py +0 -0
  175. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/secret_scrub.py +0 -0
  176. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/semantic_style.py +0 -0
  177. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/sentiment.py +0 -0
  178. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/session_enrichment.py +0 -0
  179. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/style_embed.py +0 -0
  180. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/style_index.py +0 -0
  181. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/tag_entity_promotion.py +0 -0
  182. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/temporal_chains.py +0 -0
  183. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/time_batcher.py +0 -0
  184. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/unified_timeline.py +0 -0
  185. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/pipeline/write_queue.py +0 -0
  186. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/provenance.py +0 -0
  187. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/provenance_autosupersede.py +0 -0
  188. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/provenance_integration.py +0 -0
  189. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/queue_merge.py +0 -0
  190. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/reembed_backfill.py +0 -0
  191. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/sandbox_db.py +0 -0
  192. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/scoping.py +0 -0
  193. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/search_fanout.py +0 -0
  194. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/search_profile.py +0 -0
  195. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/search_repo.py +0 -0
  196. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/session_repo.py +0 -0
  197. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/storage.py +0 -0
  198. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/system_prompt_guard.py +0 -0
  199. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/tag_normalization.py +0 -0
  200. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/taxonomy.json +0 -0
  201. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/telemetry.py +0 -0
  202. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/types.py +0 -0
  203. {brainlayer-1.5.2 → brainlayer-1.5.4}/src/brainlayer/writer_telemetry.py +0 -0
@@ -98,3 +98,5 @@ site/
98
98
 
99
99
  # render-verification ephemeral logs
100
100
  brain-bar/.render-logs/
101
+ .worktrees/
102
+ .cmux/worktrees/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: brainlayer
3
- Version: 1.5.2
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: mcp>=1.0.0
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.2"
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>=1.0.0",
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
- 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,),
370
+ HOT_CANDIDATE_SCAN_SQL,
371
+ (HOT_CANDIDATE_SCAN_LIMIT,),
116
372
  )
117
- return [EmbedCandidate(str(row[0]), str(row[1])) for row in rows]
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
- 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]
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
- return keyword in signature.parameters or any(
339
- parameter.kind == inspect.Parameter.VAR_KEYWORD for parameter in signature.parameters.values()
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]] = _pending_chunk_rows,
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.name.startswith("enrichment-"))
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[[], object] = get_embedding_model,
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 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
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
- 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
- )
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=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=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
- : "${BRAINLAYER_BACKUP_TIMEOUT_SECONDS:=300}"
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__}"