brainlayer 1.5.83__tar.gz → 1.5.85__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 (274) hide show
  1. {brainlayer-1.5.83 → brainlayer-1.5.85}/PKG-INFO +1 -1
  2. {brainlayer-1.5.83 → brainlayer-1.5.85}/pyproject.toml +1 -1
  3. {brainlayer-1.5.83 → brainlayer-1.5.85}/scripts/hotlane_brainbar_daemon.py +287 -24
  4. {brainlayer-1.5.83 → brainlayer-1.5.85}/scripts/launchd/com.brainlayer.hotlane-brainbar.plist +1 -1
  5. {brainlayer-1.5.83 → brainlayer-1.5.85}/server.json +2 -2
  6. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/__init__.py +1 -1
  7. brainlayer-1.5.85/src/brainlayer/_build.py +1 -0
  8. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/backup_daily.py +70 -6
  9. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/badge_state.py +34 -13
  10. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/dedupe.py +113 -15
  11. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/doctor.py +110 -5
  12. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/embeddings.py +20 -7
  13. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/health_check.py +32 -5
  14. brainlayer-1.5.85/src/brainlayer/health_facts.py +540 -0
  15. brainlayer-1.5.85/src/brainlayer/health_verdict.py +742 -0
  16. brainlayer-1.5.85/src/brainlayer/hotlane_budget.py +91 -0
  17. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/launchd_consolidation.py +29 -3
  18. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/maintenance.py +149 -35
  19. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/migrate.py +2 -2
  20. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/semantic_style.py +6 -4
  21. brainlayer-1.5.85/src/brainlayer/policy_reconcile.py +84 -0
  22. brainlayer-1.5.85/src/brainlayer/readonly_snapshot.py +217 -0
  23. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/reembed_backfill.py +2 -2
  24. brainlayer-1.5.85/src/brainlayer/supervisor_child.py +76 -0
  25. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/supervisor_jobs.py +2 -1
  26. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/vector_store.py +48 -6
  27. brainlayer-1.5.83/src/brainlayer/_build.py +0 -1
  28. brainlayer-1.5.83/src/brainlayer/supervisor_child.py +0 -38
  29. {brainlayer-1.5.83 → brainlayer-1.5.85}/.gitignore +0 -0
  30. {brainlayer-1.5.83 → brainlayer-1.5.85}/LICENSE +0 -0
  31. {brainlayer-1.5.83 → brainlayer-1.5.85}/README.md +0 -0
  32. {brainlayer-1.5.83 → brainlayer-1.5.85}/scripts/launchd/backup-daily.sh +0 -0
  33. {brainlayer-1.5.83 → brainlayer-1.5.85}/scripts/launchd/brainlayer-env-run.sh +0 -0
  34. {brainlayer-1.5.83 → brainlayer-1.5.85}/scripts/launchd/brainlayer.env.example +0 -0
  35. {brainlayer-1.5.83 → brainlayer-1.5.85}/scripts/launchd/com.brainlayer.backup-daily.plist +0 -0
  36. {brainlayer-1.5.83 → brainlayer-1.5.85}/scripts/launchd/com.brainlayer.daemon.plist +0 -0
  37. {brainlayer-1.5.83 → brainlayer-1.5.85}/scripts/launchd/com.brainlayer.decay.plist +0 -0
  38. {brainlayer-1.5.83 → brainlayer-1.5.85}/scripts/launchd/com.brainlayer.drain.plist +0 -0
  39. {brainlayer-1.5.83 → brainlayer-1.5.85}/scripts/launchd/com.brainlayer.health-check.plist +0 -0
  40. {brainlayer-1.5.83 → brainlayer-1.5.85}/scripts/launchd/com.brainlayer.index.plist +0 -0
  41. {brainlayer-1.5.83 → brainlayer-1.5.85}/scripts/launchd/com.brainlayer.jsonl-backup.plist +0 -0
  42. {brainlayer-1.5.83 → brainlayer-1.5.85}/scripts/launchd/com.brainlayer.log-cap.plist +0 -0
  43. {brainlayer-1.5.83 → brainlayer-1.5.85}/scripts/launchd/com.brainlayer.maintenance-nightly.plist +0 -0
  44. {brainlayer-1.5.83 → brainlayer-1.5.85}/scripts/launchd/com.brainlayer.maintenance-weekly.plist +0 -0
  45. {brainlayer-1.5.83 → brainlayer-1.5.85}/scripts/launchd/com.brainlayer.maintenance.plist +0 -0
  46. {brainlayer-1.5.83 → brainlayer-1.5.85}/scripts/launchd/com.brainlayer.observability.plist +0 -0
  47. {brainlayer-1.5.83 → brainlayer-1.5.85}/scripts/launchd/com.brainlayer.p0-counter.plist +0 -0
  48. {brainlayer-1.5.83 → brainlayer-1.5.85}/scripts/launchd/com.brainlayer.repair-fts.plist +0 -0
  49. {brainlayer-1.5.83 → brainlayer-1.5.85}/scripts/launchd/com.brainlayer.t3-ingest.plist +0 -0
  50. {brainlayer-1.5.83 → brainlayer-1.5.85}/scripts/launchd/com.brainlayer.throughput-watchdog.plist +0 -0
  51. {brainlayer-1.5.83 → brainlayer-1.5.85}/scripts/launchd/com.brainlayer.tier0-watchdog.plist +0 -0
  52. {brainlayer-1.5.83 → brainlayer-1.5.85}/scripts/launchd/com.brainlayer.wal-checkpoint.plist +0 -0
  53. {brainlayer-1.5.83 → brainlayer-1.5.85}/scripts/launchd/com.brainlayer.watch.plist +0 -0
  54. {brainlayer-1.5.83 → brainlayer-1.5.85}/scripts/launchd/com.etanhey.brainlayer-fleet-watchdog.plist +0 -0
  55. {brainlayer-1.5.83 → brainlayer-1.5.85}/scripts/launchd/fleet-watchdog.sh +0 -0
  56. {brainlayer-1.5.83 → brainlayer-1.5.85}/scripts/launchd/install.sh +0 -0
  57. {brainlayer-1.5.83 → brainlayer-1.5.85}/scripts/launchd/jsonl-backup.sh +0 -0
  58. {brainlayer-1.5.83 → brainlayer-1.5.85}/scripts/launchd/throughput-watchdog.py +0 -0
  59. {brainlayer-1.5.83 → brainlayer-1.5.85}/scripts/release-verify-signatures.sh +0 -0
  60. {brainlayer-1.5.83 → brainlayer-1.5.85}/scripts/tier0-watchdog.sh +0 -0
  61. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/__main__.py +0 -0
  62. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/_helpers.py +0 -0
  63. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/agent_profiles.py +0 -0
  64. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/agent_provenance.py +0 -0
  65. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/alarm.py +0 -0
  66. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/archive_collapse.py +0 -0
  67. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/archive_lifecycle.py +0 -0
  68. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/backup_retention_invariant.py +0 -0
  69. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/bitemporal.py +0 -0
  70. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/brainbar_activation.py +0 -0
  71. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/brainbar_auto_activation.py +0 -0
  72. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/brainbar_hybrid_helper.py +0 -0
  73. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/brainbar_identity.py +0 -0
  74. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/cachedelete_purge.swift +0 -0
  75. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/calibrate.py +0 -0
  76. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/chunk_origin.py +0 -0
  77. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/chunk_origin_backfill.py +0 -0
  78. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/chunk_origin_wipe.py +0 -0
  79. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/chunk_write.py +0 -0
  80. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/classify.py +0 -0
  81. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/claude_paths.py +0 -0
  82. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/cli/__init__.py +0 -0
  83. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/cli/wizard.py +0 -0
  84. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/cli_jobs.py +0 -0
  85. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/cli_new.py +0 -0
  86. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/cloud_backfill.py +0 -0
  87. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/clustering.py +0 -0
  88. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/component_jobs.py +0 -0
  89. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/config.py +0 -0
  90. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/consolidation_plan.py +0 -0
  91. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/content_class.py +0 -0
  92. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/correction_judge.py +0 -0
  93. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/data/agents_registry.yaml +0 -0
  94. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/data/sandbox_seeds/skill-eval-baseline.json +0 -0
  95. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/db_shrink.py +0 -0
  96. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/decay.py +0 -0
  97. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/decay_backfill.py +0 -0
  98. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/decay_job.py +0 -0
  99. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/dedupe_merge.py +0 -0
  100. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/deploy_drift.py +0 -0
  101. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/disk_space.py +0 -0
  102. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/drain.py +0 -0
  103. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/drain_liveness.py +0 -0
  104. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/drive_credentials.py +0 -0
  105. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/engine.py +0 -0
  106. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/enrichment_replay.py +0 -0
  107. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/eval/__init__.py +0 -0
  108. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/eval/abcde_enrich_runner.py +0 -0
  109. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/eval/abcde_variants.py +0 -0
  110. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/eval/abcde_variants.yaml +0 -0
  111. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/eval/benchmark.py +0 -0
  112. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/eval/enrichment_gold.py +0 -0
  113. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/eval/enrichment_graders.py +0 -0
  114. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/eval/enrichment_judge.py +0 -0
  115. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/eval/enrichment_llm_judge.py +0 -0
  116. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/eval/enrichment_quality_benchmark.py +0 -0
  117. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/eval/experiment_store.py +0 -0
  118. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/eval/fixtures/README.md +0 -0
  119. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/eval/labeling/enrichment-labeler.html +0 -0
  120. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/eval/phoenix_gate/__init__.py +0 -0
  121. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/eval/phoenix_gate/__main__.py +0 -0
  122. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/eval/phoenix_gate/baseline_store.py +0 -0
  123. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/eval/phoenix_gate/cli.py +0 -0
  124. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/eval/phoenix_gate/models.py +0 -0
  125. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/eval/phoenix_gate/phoenix_client.py +0 -0
  126. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/eval/phoenix_gate/regression_gate.py +0 -0
  127. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/eval/phoenix_gate/triggers.py +0 -0
  128. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/eval/pilot_artifact_report.py +0 -0
  129. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/fallback_replay.py +0 -0
  130. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/git_learning.py +0 -0
  131. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/hook_python.py +0 -0
  132. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/hooks/__init__.py +0 -0
  133. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/hooks/indexer.py +0 -0
  134. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/import_sweep.py +0 -0
  135. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/index_admission.py +0 -0
  136. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/index_budget.py +0 -0
  137. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/index_completeness.py +0 -0
  138. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/index_new.py +0 -0
  139. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/index_watchdog.py +0 -0
  140. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/ingest/__init__.py +0 -0
  141. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/ingest/codex.py +0 -0
  142. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/ingest/t3.py +0 -0
  143. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/ingest_denylist.py +0 -0
  144. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/ingest_guard.py +0 -0
  145. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/isolation_proof.py +0 -0
  146. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/job_alerts.py +0 -0
  147. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/job_lifecycle_health.py +0 -0
  148. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/jobs.py +0 -0
  149. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/jsonl_backup.py +0 -0
  150. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/kg/__init__.py +0 -0
  151. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/kg_cleanup.py +0 -0
  152. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/kg_judge.py +0 -0
  153. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/kg_promotion.py +0 -0
  154. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/kg_repo.py +0 -0
  155. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/kg_review_session.py +0 -0
  156. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/kg_session_finish.py +0 -0
  157. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/kg_session_harvest.py +0 -0
  158. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/launchd_install.py +0 -0
  159. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/launchd_primitive.py +0 -0
  160. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/launchd_revert.py +0 -0
  161. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/lexical_defense.py +0 -0
  162. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/lexical_defense_dictionary.json +0 -0
  163. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/linkedin_import.py +0 -0
  164. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/log_cap.py +0 -0
  165. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/maintenance_calendar.py +0 -0
  166. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/maintenance_control.py +0 -0
  167. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/mcp/__init__.py +0 -0
  168. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/mcp/_format.py +0 -0
  169. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/mcp/_shared.py +0 -0
  170. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/mcp/enrich_handler.py +0 -0
  171. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/mcp/entity_handler.py +0 -0
  172. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/mcp/palette.py +0 -0
  173. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/mcp/search_handler.py +0 -0
  174. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/mcp/store_handler.py +0 -0
  175. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/mcp/tags_handler.py +0 -0
  176. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/mcp_socket_config.py +0 -0
  177. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/mcp_stdio_bridge.py +0 -0
  178. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/memory_types.py +0 -0
  179. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/migration_custody.py +0 -0
  180. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/migration_journal.py +0 -0
  181. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/migration_runtime.py +0 -0
  182. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/notification_policy.py +0 -0
  183. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/observability_backup.py +0 -0
  184. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/observability_surface.py +0 -0
  185. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/observability_transport.py +0 -0
  186. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/p0_longitudinal_count.py +0 -0
  187. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/parent_death.py +0 -0
  188. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/paths.py +0 -0
  189. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pause.py +0 -0
  190. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/phonetic.py +0 -0
  191. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/__init__.py +0 -0
  192. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/agent_enrichment.py +0 -0
  193. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/analyze_communication.py +0 -0
  194. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/batch_extraction.py +0 -0
  195. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/brain_graph.py +0 -0
  196. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/chat_tags.py +0 -0
  197. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/chunk.py +0 -0
  198. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/classify.py +0 -0
  199. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/cloud_scrub.py +0 -0
  200. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/cluster_sampling.py +0 -0
  201. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/code_intelligence.py +0 -0
  202. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/correction_detection.py +0 -0
  203. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/correction_mining.py +0 -0
  204. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/digest.py +0 -0
  205. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/enrichment_prompts.py +0 -0
  206. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/enrichment_results.py +0 -0
  207. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/enrichment_tiers.py +0 -0
  208. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/entity_extraction.py +0 -0
  209. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/entity_resolution.py +0 -0
  210. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/extract.py +0 -0
  211. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/extract_claude_desktop.py +0 -0
  212. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/extract_corrections.py +0 -0
  213. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/extract_markdown.py +0 -0
  214. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/extract_whatsapp.py +0 -0
  215. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/frustration_mining.py +0 -0
  216. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/git_overlay.py +0 -0
  217. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/kg_extraction.py +0 -0
  218. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/kg_extraction_groq.py +0 -0
  219. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/longitudinal_analyzer.py +0 -0
  220. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/obsidian_export.py +0 -0
  221. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/operation_grouping.py +0 -0
  222. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/plan_linking.py +0 -0
  223. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/rate_limiter.py +0 -0
  224. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/relation_backfill.py +0 -0
  225. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/relation_verification.py +0 -0
  226. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/sanitize.py +0 -0
  227. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/secret_scrub.py +0 -0
  228. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/sentiment.py +0 -0
  229. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/session_history.py +0 -0
  230. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/style_embed.py +0 -0
  231. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/style_index.py +0 -0
  232. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/tag_entity_promotion.py +0 -0
  233. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/temporal_chains.py +0 -0
  234. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/time_batcher.py +0 -0
  235. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/unified_timeline.py +0 -0
  236. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/pipeline/write_queue.py +0 -0
  237. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/provenance.py +0 -0
  238. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/provenance_autosupersede.py +0 -0
  239. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/provenance_integration.py +0 -0
  240. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/queue_io.py +0 -0
  241. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/queue_merge.py +0 -0
  242. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/retired_services.py +0 -0
  243. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/runtime_store.py +0 -0
  244. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/sandbox_db.py +0 -0
  245. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/scoping.py +0 -0
  246. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/scrub_at_rest.py +0 -0
  247. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/search_fanout.py +0 -0
  248. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/search_profile.py +0 -0
  249. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/search_repo.py +0 -0
  250. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/service_supervisor.py +0 -0
  251. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/session_repo.py +0 -0
  252. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/setup.py +0 -0
  253. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/socket_hygiene.py +0 -0
  254. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/source_class_migration.py +0 -0
  255. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/spotlight.py +0 -0
  256. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/storage.py +0 -0
  257. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/store.py +0 -0
  258. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/supervisor_adapters.py +0 -0
  259. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/supervisor_diagnostics.py +0 -0
  260. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/supervisor_guard.py +0 -0
  261. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/system_prompt_guard.py +0 -0
  262. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/t3_linkage_alarm.py +0 -0
  263. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/t3_provenance.py +0 -0
  264. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/t3_sources.py +0 -0
  265. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/tag_normalization.py +0 -0
  266. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/taxonomy.json +0 -0
  267. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/telemetry.py +0 -0
  268. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/timestamp_iso.py +0 -0
  269. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/types.py +0 -0
  270. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/wal_checkpoint.py +0 -0
  271. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/watcher.py +0 -0
  272. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/watcher_bridge.py +0 -0
  273. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/writer_policy.py +0 -0
  274. {brainlayer-1.5.83 → brainlayer-1.5.85}/src/brainlayer/writer_telemetry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: brainlayer
3
- Version: 1.5.83
3
+ Version: 1.5.85
4
4
  Summary: Persistent local memory for AI agents — 16 BrainBar MCP tools, semantic search, knowledge graph, 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.83"
3
+ version = "1.5.85"
4
4
  description = "Persistent local memory for AI agents — 16 BrainBar MCP tools, semantic search, knowledge graph, SQLite"
5
5
  license = {text = "Apache-2.0"}
6
6
  readme = "README.md"
@@ -13,8 +13,11 @@ import argparse
13
13
  import inspect
14
14
  import json
15
15
  import logging
16
+ import math
16
17
  import os
17
18
  import signal
19
+ import subprocess
20
+ import sys
18
21
  import time
19
22
  from collections import OrderedDict
20
23
  from pathlib import Path
@@ -25,9 +28,10 @@ import sqlite_vec
25
28
 
26
29
  from brainlayer._helpers import serialize_f32
27
30
  from brainlayer.embeddings import get_embedding_model
31
+ from brainlayer.hotlane_budget import HotlaneCpuSampler
28
32
  from brainlayer.paths import get_db_path
29
33
  from brainlayer.store import embed_hot_chunk, embed_pending_chunks
30
- from brainlayer.vector_store import VectorStore
34
+ from brainlayer.vector_store import VectorStore, skip_connection_hooks, temporary_write_busy_timeout_ms
31
35
  from brainlayer.writer_telemetry import IngestMetrics, start_writer_span
32
36
 
33
37
  LOGGER = logging.getLogger("brainlayer.hotlane_brainbar")
@@ -40,6 +44,16 @@ MAX_BACKLOG_BATCH = 16
40
44
  DEFAULT_HOTLANE_WRITE_BUSY_TIMEOUT_MS = 1000
41
45
  MAX_APSW_BUSY_TIMEOUT_MS = 2_147_483_647
42
46
  VECTOR_WRITE_YIELD_SECONDS = 0.005
47
+ # S17 (2026-11-02): a busy writer slot must never cost an embedded batch. The write retries
48
+ # inside the cycle (each try waits up to the hotlane busy timeout at BEGIN IMMEDIATE), then
49
+ # whatever is still unwritten is carried to the next cycle instead of being discarded.
50
+ HOTLANE_WRITE_ATTEMPTS = 3
51
+ HOTLANE_WRITE_RETRY_BACKOFF_SECONDS = 0.25
52
+ MAX_CARRIED_VECTORS = 64
53
+ # Hooks the hotlane writer never runs in its Connection() constructor. `connection_optimize`
54
+ # runs PRAGMA optimize (a write lock) on every open; `connection_busy_timeout` would reset the
55
+ # constructor's busy timeout to APSW's 100 ms. connection_wal and the rest still run.
56
+ HOTLANE_SKIPPED_CONNECTION_HOOKS = ("connection_optimize", "connection_busy_timeout")
43
57
 
44
58
  # Incident 2026-08-24: the loop slept a fixed `interval` (1.0s) whether or not a cycle
45
59
  # did any work, so an idle hotlane kept probing once per second forever. Idle cycles now
@@ -144,6 +158,8 @@ class CycleResult(NamedTuple):
144
158
  enrich_skipped: int = 0
145
159
  enrich_failed: int = 0
146
160
  enrich_daily_cap_reached: bool = False
161
+ deferred: int = 0
162
+ backlog_encoded: int = 0
147
163
 
148
164
 
149
165
  class EmbedCandidate(NamedTuple):
@@ -151,6 +167,59 @@ class EmbedCandidate(NamedTuple):
151
167
  content: str
152
168
 
153
169
 
170
+ def _sysctl(name: str) -> str:
171
+ return subprocess.check_output(["/usr/sbin/sysctl", "-n", name], text=True, timeout=5).strip()
172
+
173
+
174
+ def _performance_cores() -> int:
175
+ if sys.platform == "darwin":
176
+ return max(1, int(_sysctl("hw.perflevel0.physicalcpu")))
177
+ return os.cpu_count() or 1
178
+
179
+
180
+ def _backlog_max_load() -> float:
181
+ value = float(os.environ.get("BRAINLAYER_HOTLANE_BACKLOG_MAX_LOAD", "") or _performance_cores())
182
+ if not math.isfinite(value) or value < 0:
183
+ raise ValueError("BRAINLAYER_HOTLANE_BACKLOG_MAX_LOAD must be finite and nonnegative")
184
+ return value
185
+
186
+
187
+ def setup_cpu_budget(db_path: Path) -> dict:
188
+ """CPU-only priority: never request Darwin background or a disk I/O policy."""
189
+ policy = {"policy": "not applicable", "platform": sys.platform}
190
+ if sys.platform == "darwin":
191
+ os.setpriority(os.PRIO_PROCESS, 0, 20)
192
+ policy.update(policy="nice", nice=os.getpriority(os.PRIO_PROCESS, 0))
193
+ default_threads = 1 if sys.platform == "darwin" and "Apple M1" in _sysctl("machdep.cpu.brand_string") else 2
194
+ threads = int(os.environ.get("BRAINLAYER_HOTLANE_THREADS", str(default_threads)))
195
+ if threads < 1:
196
+ raise ValueError("BRAINLAYER_HOTLANE_THREADS must be positive")
197
+ for name in ("OMP_NUM_THREADS", "MKL_NUM_THREADS", "VECLIB_MAXIMUM_THREADS", "OPENBLAS_NUM_THREADS"):
198
+ os.environ[name] = str(threads)
199
+ # embeddings.py imports sentence_transformers/torch only when the model first encodes.
200
+ import torch
201
+
202
+ torch.set_num_threads(threads)
203
+ policy.update(threads=torch.get_num_threads(), backlog_max_load=_backlog_max_load(), pid=os.getpid())
204
+ receipt = db_path.parent / "hotlane-budget.json"
205
+ receipt.parent.mkdir(parents=True, exist_ok=True)
206
+ temporary = receipt.with_suffix(".tmp")
207
+ temporary.write_text(json.dumps(policy, sort_keys=True) + "\n")
208
+ temporary.replace(receipt)
209
+ LOGGER.info("hotlane CPU budget %s receipt=%s", json.dumps(policy, sort_keys=True), receipt)
210
+ return policy
211
+
212
+
213
+ def _backlog_yield_reason(db_path: Path, max_load: float) -> str:
214
+ if (db_path.parent / "hotlane.pause").exists():
215
+ return "pause marker"
216
+ try:
217
+ load = os.getloadavg()[0]
218
+ except OSError:
219
+ return "load unavailable"
220
+ return "high load" if load > max_load else ""
221
+
222
+
154
223
  class PendingCandidateScanState:
155
224
  def __init__(self) -> None:
156
225
  self.active = False
@@ -169,6 +238,70 @@ class EmbeddedVector(NamedTuple):
169
238
  embedding: list[float]
170
239
 
171
240
 
241
+ class HotlaneVectorWriteDeferred(apsw.BusyError):
242
+ """A busy write with progress for cycle carry-over.
243
+
244
+ Remains a BusyError so callers using the writer's existing lock-error contract
245
+ can retry after contention; the split cycle also retains ``remaining``.
246
+ """
247
+
248
+ def __init__(self, *, written: int, remaining: list[EmbeddedVector], attempts: int, cause: BaseException):
249
+ super().__init__(f"{type(cause).__name__}: {cause} (after {attempts} attempts, {len(remaining)} unwritten)")
250
+ self.written = written
251
+ self.remaining = remaining
252
+ self.attempts = attempts
253
+ self.cause = cause
254
+
255
+
256
+ class VectorCarryover:
257
+ """Computed vectors whose write was deferred by a busy writer slot (S17).
258
+
259
+ Held in memory for the next cycle so a lock never costs the embedding work. The
260
+ writer re-checks each chunk's content and vector state at write time, so a stale
261
+ entry (content edited, or vectorized meanwhile) is skipped, never written wrongly.
262
+ Bounded: past ``capacity`` the oldest entries are dropped, loudly and counted. A
263
+ dropped vector loses only compute; its chunk stays unvectored and is re-embedded.
264
+ """
265
+
266
+ def __init__(self, capacity: int = MAX_CARRIED_VECTORS) -> None:
267
+ self.capacity = capacity
268
+ self._vectors: OrderedDict[str, EmbeddedVector] = OrderedDict()
269
+ self.carried_total = 0
270
+ self.dropped_total = 0
271
+
272
+ def __len__(self) -> int:
273
+ return len(self._vectors)
274
+
275
+ def put(self, vectors: list[EmbeddedVector]) -> int:
276
+ for vector in vectors:
277
+ self._vectors.pop(vector.chunk_id, None)
278
+ self._vectors[vector.chunk_id] = vector
279
+ self.carried_total += len(vectors)
280
+ dropped = 0
281
+ while len(self._vectors) > self.capacity:
282
+ self._vectors.popitem(last=False)
283
+ dropped += 1
284
+ if dropped:
285
+ self.dropped_total += dropped
286
+ LOGGER.warning(
287
+ "hotlane carry-over full; dropped %d oldest computed vectors (chunks stay unvectored and "
288
+ "will be re-embedded) capacity=%d dropped_total=%d",
289
+ dropped,
290
+ self.capacity,
291
+ self.dropped_total,
292
+ )
293
+ return dropped
294
+
295
+ def peek(self) -> list[EmbeddedVector]:
296
+ return list(self._vectors.values())
297
+
298
+ def clear(self) -> None:
299
+ self._vectors.clear()
300
+
301
+
302
+ VECTOR_CARRYOVER = VectorCarryover()
303
+
304
+
172
305
  def _candidates_from_scanned_rows(
173
306
  rows: list[tuple], *, limit: int, exclude_ids: set[str] | None = None
174
307
  ) -> tuple[list[EmbedCandidate], int | None, int | None, bool]:
@@ -546,11 +679,20 @@ def _hotlane_write_busy_timeout_ms() -> int:
546
679
 
547
680
 
548
681
  def _open_hotlane_write_connection(db_path: Path) -> apsw.Connection:
549
- conn = apsw.Connection(str(db_path), flags=apsw.SQLITE_OPEN_READWRITE)
550
- conn.setbusytimeout(_hotlane_write_busy_timeout_ms())
551
- conn.enableloadextension(True)
552
- conn.loadextension(sqlite_vec.loadable_path())
553
- conn.enableloadextension(False)
682
+ timeout_ms = _hotlane_write_busy_timeout_ms()
683
+ # S17: the constructor runs the connection hooks. Without this, PRAGMA optimize took the
684
+ # write lock on every cycle at APSW's 100 ms timeout and threw away the embedded batch
685
+ # (1,264 of 1,430 logged cycle failures). The hotlane timeout covers the hooks that do run.
686
+ with skip_connection_hooks(*HOTLANE_SKIPPED_CONNECTION_HOOKS), temporary_write_busy_timeout_ms(timeout_ms):
687
+ conn = apsw.Connection(str(db_path), flags=apsw.SQLITE_OPEN_READWRITE)
688
+ try:
689
+ conn.setbusytimeout(timeout_ms)
690
+ conn.enableloadextension(True)
691
+ conn.loadextension(sqlite_vec.loadable_path())
692
+ conn.enableloadextension(False)
693
+ except BaseException:
694
+ conn.close()
695
+ raise
554
696
  return conn
555
697
 
556
698
 
@@ -592,9 +734,56 @@ def _upsert_chunk_vector_raw(
592
734
  )
593
735
 
594
736
 
737
+ class _WriteProgress:
738
+ def __init__(self) -> None:
739
+ self.written = 0
740
+ self.processed = 0
741
+
742
+
595
743
  def _write_embedded_vectors(store_or_path: VectorStore | Path | str, vectors: list[EmbeddedVector]) -> int:
744
+ """Write vectors one transaction each; retry busy/locked failures, never drop the batch.
745
+
746
+ Each vector is its own transaction, so a lock error part-way leaves earlier vectors
747
+ committed; the retry resumes at the first unprocessed vector. After
748
+ ``HOTLANE_WRITE_ATTEMPTS`` busy failures it raises ``HotlaneVectorWriteDeferred``
749
+ carrying what is still unwritten, for the caller to keep for the next cycle.
750
+ """
751
+ remaining = list(vectors)
752
+ written = 0
753
+ for attempt in range(1, HOTLANE_WRITE_ATTEMPTS + 1):
754
+ if not remaining:
755
+ break
756
+ progress = _WriteProgress()
757
+ try:
758
+ _write_embedded_vectors_once(store_or_path, remaining, progress)
759
+ except (apsw.BusyError, apsw.LockedError) as exc:
760
+ written += progress.written
761
+ remaining = remaining[progress.processed :]
762
+ if attempt == HOTLANE_WRITE_ATTEMPTS:
763
+ raise HotlaneVectorWriteDeferred(
764
+ written=written, remaining=remaining, attempts=attempt, cause=exc
765
+ ) from exc
766
+ LOGGER.info(
767
+ "hotlane vector write busy; retrying attempt=%d/%d unwritten=%d error_type=%s",
768
+ attempt + 1,
769
+ HOTLANE_WRITE_ATTEMPTS,
770
+ len(remaining),
771
+ type(exc).__name__,
772
+ )
773
+ _sleep(HOTLANE_WRITE_RETRY_BACKOFF_SECONDS * (2 ** (attempt - 1)))
774
+ continue
775
+ written += progress.written
776
+ remaining = []
777
+ return written
778
+
779
+
780
+ def _write_embedded_vectors_once(
781
+ store_or_path: VectorStore | Path | str,
782
+ vectors: list[EmbeddedVector],
783
+ progress: _WriteProgress,
784
+ ) -> None:
596
785
  if not vectors:
597
- return 0
786
+ return
598
787
 
599
788
  conn: apsw.Connection | None = None
600
789
  db_path: Path | None
@@ -666,6 +855,8 @@ def _write_embedded_vectors(store_or_path: VectorStore | Path | str, vectors: li
666
855
  metrics.committed([{"queued_at": metadata.get("ingest_queued_at"), "metadata": metadata}])
667
856
  metrics.finish("commit")
668
857
  count += rows_touched
858
+ progress.written = count
859
+ progress.processed = vector_index + 1
669
860
  telemetry_span.finish("commit", rows_touched=rows_touched)
670
861
  if vector_index < len(vectors) - 1:
671
862
  _sleep(VECTOR_WRITE_YIELD_SECONDS)
@@ -676,6 +867,7 @@ def _write_embedded_vectors(store_or_path: VectorStore | Path | str, vectors: li
676
867
  telemetry_span.finish("rollback", error=f"{type(exc).__name__}: {exc}")
677
868
  if isinstance(exc, (apsw.BusyError, apsw.LockedError)):
678
869
  raise
870
+ progress.processed = vector_index + 1
679
871
  LOGGER.error(
680
872
  "hotlane vector write skipped chunk_id=%s error_type=%s error=%s",
681
873
  chunk_id,
@@ -691,7 +883,6 @@ def _write_embedded_vectors(store_or_path: VectorStore | Path | str, vectors: li
691
883
  from brainlayer.search_repo import clear_hybrid_search_cache
692
884
 
693
885
  clear_hybrid_search_cache(db_path)
694
- return count
695
886
 
696
887
 
697
888
  def _open_store(
@@ -751,9 +942,14 @@ def _run_split_cycle(
751
942
  candidate_rows_fn: Callable[..., list[EmbedCandidate]] = _candidate_chunk_rows,
752
943
  pending_rows_fn: Callable[..., list[EmbedCandidate]] = _pending_chunk_rows_with_resume,
753
944
  write_vectors_fn: Callable[..., int] = _write_embedded_vectors,
945
+ carryover: VectorCarryover | None = None,
754
946
  ) -> CycleResult:
947
+ carryover = VECTOR_CARRYOVER if carryover is None else carryover
755
948
  metrics = IngestMetrics("hotlane", "hotlane")
756
949
  embedded = 0
950
+ # Peek, not take: a scan or encode failure later in this cycle must not lose them.
951
+ carried = carryover.peek()
952
+ carried_by_id = {vector.chunk_id: vector for vector in carried}
757
953
  hot_rows: list[EmbedCandidate] = []
758
954
  pending_rows: list[EmbedCandidate] = []
759
955
  pending_scan_state = PENDING_CANDIDATE_SCAN_STATE if pending_rows_fn is _pending_chunk_rows_with_resume else None
@@ -778,15 +974,28 @@ def _run_split_cycle(
778
974
 
779
975
  seen_hot_ids = {candidate.chunk_id for candidate in hot_rows}
780
976
  pending_rows = [candidate for candidate in pending_rows if candidate.chunk_id not in seen_hot_ids]
977
+
978
+ def needs_embedding(candidate: EmbedCandidate) -> bool:
979
+ # A carried vector for the same content is reused, not recomputed.
980
+ carried_vector = carried_by_id.get(candidate.chunk_id)
981
+ return carried_vector is None or carried_vector.content != candidate.content
982
+
983
+ hot_rows = [candidate for candidate in hot_rows if needs_embedding(candidate)]
984
+ pending_rows = [candidate for candidate in pending_rows if needs_embedding(candidate)]
781
985
  vectors = metrics.run("encode", _embed_candidates, hot_rows, embed_fn=embed_fn, embed_batch_fn=embed_batch_fn)
782
- vectors.extend(
783
- metrics.run("encode", _embed_candidates, pending_rows, embed_fn=embed_fn, embed_batch_fn=embed_batch_fn)
986
+ backlog_vectors = metrics.run(
987
+ "encode", _embed_candidates, pending_rows, embed_fn=embed_fn, embed_batch_fn=embed_batch_fn
784
988
  )
989
+ vectors.extend(backlog_vectors)
990
+ fresh_ids = {vector.chunk_id for vector in vectors}
991
+ vectors = [vector for vector in carried if vector.chunk_id not in fresh_ids] + vectors
785
992
  if vectors:
786
993
  try:
787
994
  embedded += metrics.run("write", write_vectors_fn, db_path, vectors)
995
+ carryover.clear()
788
996
  except Exception as exc:
789
- is_lock_error = isinstance(exc, (apsw.BusyError, apsw.LockedError))
997
+ deferred = exc if isinstance(exc, HotlaneVectorWriteDeferred) else None
998
+ is_lock_error = deferred is not None or isinstance(exc, (apsw.BusyError, apsw.LockedError))
790
999
  if is_lock_error and pending_scan_checkpoint is not None and pending_scan_state is not None:
791
1000
  # Earlier one-row commits stay indexed; retry only rows still missing vectors.
792
1001
  pending_scan_state.active, pending_scan_state.after_created_at, pending_scan_state.after_rowid = (
@@ -798,12 +1007,33 @@ def _run_split_cycle(
798
1007
  [candidate.chunk_id for candidate in pending_rows],
799
1008
  type(exc).__name__,
800
1009
  )
801
- metrics.finish("error")
802
- raise
1010
+ if not is_lock_error:
1011
+ metrics.finish("error")
1012
+ raise
1013
+ # S17: a busy writer slot never costs the embedded batch. A writer that does not
1014
+ # report progress gets all of its vectors carried; its writes are idempotent
1015
+ # because each one re-checks that the chunk still lacks a vector.
1016
+ unwritten = deferred.remaining if deferred is not None else vectors
1017
+ embedded += deferred.written if deferred is not None else 0
1018
+ carryover.clear()
1019
+ carryover.put(unwritten)
1020
+ LOGGER.warning(
1021
+ "hotlane vector write deferred: writer slot busy; carrying %d computed vectors to the next "
1022
+ "cycle written=%d carried_now=%d carried_total=%d dropped_total=%d error=%s",
1023
+ len(unwritten),
1024
+ embedded,
1025
+ len(carryover),
1026
+ carryover.carried_total,
1027
+ carryover.dropped_total,
1028
+ exc,
1029
+ )
1030
+ metrics.finish("deferred")
1031
+ _warn_retired_enrichment(enrich_limit, enrich_since_hours, enrich_fn)
1032
+ return CycleResult(embedded=embedded, deferred=len(unwritten), backlog_encoded=len(backlog_vectors))
803
1033
 
804
1034
  _warn_retired_enrichment(enrich_limit, enrich_since_hours, enrich_fn)
805
1035
  metrics.finish()
806
- return CycleResult(embedded=embedded)
1036
+ return CycleResult(embedded=embedded, backlog_encoded=len(backlog_vectors))
807
1037
 
808
1038
 
809
1039
  def _warn_retired_enrichment(limit: int, since_hours: int, callback: object) -> None:
@@ -891,6 +1121,7 @@ def run(
891
1121
  queue_dir: Path | None = None,
892
1122
  queue_depth_fn: Callable[[Path], int] = _queue_depth,
893
1123
  high_priority_queue_depth_fn: Callable[[Path], int] = _high_priority_queue_depth,
1124
+ cpu_sampler: HotlaneCpuSampler | None = None,
894
1125
  ) -> None:
895
1126
  model = _create_embedding_model(model_factory, device=embedding_device)
896
1127
  embed_batch_fn = getattr(model, "embed_texts", None)
@@ -907,10 +1138,16 @@ def run(
907
1138
  queue_dir_was_explicit = queue_dir is not None
908
1139
  queue_dir = queue_dir or _default_queue_dir()
909
1140
  last_backlog = time_fn() - backlog_interval
1141
+ last_backlog_duty = last_backlog + backlog_interval
1142
+ max_backlog_wait = float(os.environ.get("BRAINLAYER_HOTLANE_BACKLOG_MAX_WAIT_SECONDS", "300"))
1143
+ if not math.isfinite(max_backlog_wait) or max_backlog_wait <= 0:
1144
+ raise ValueError("BRAINLAYER_HOTLANE_BACKLOG_MAX_WAIT_SECONDS must be finite and positive")
910
1145
  _warn_retired_enrichment(enrich_limit, enrich_since_hours, None)
911
1146
  if enrich_limit > 0:
912
1147
  LOGGER.warning("Ignoring retired hotlane enrichment interval=%s", enrich_interval)
913
1148
  queue_backpressure_active = False
1149
+ backlog_max_load = _backlog_max_load()
1150
+ backlog_yield_reason = ""
914
1151
  backlog_slice_logged = False
915
1152
  hot_candidate_scanner = HotCandidateScanner()
916
1153
  cycles = 0
@@ -927,11 +1164,23 @@ def run(
927
1164
  queue_has_high_priority_backlog = queue_has_backlog and high_priority_queue_depth_fn(queue_dir) > 0
928
1165
  cycle_backlog_batch = backlog_batch if backlog_batch > 0 and now - last_backlog >= backlog_interval else 0
929
1166
  cycle_recent_limit = recent_limit
1167
+ reason = _backlog_yield_reason(db_path, backlog_max_load) if backlog_batch else ""
1168
+ if reason == "high load" and now - last_backlog_duty >= max_backlog_wait:
1169
+ # Reserve the existing backlog lane; never lift store-queue backpressure.
1170
+ reason = ""
1171
+ cycle_backlog_batch = backlog_batch
1172
+ if reason != backlog_yield_reason:
1173
+ LOGGER.info("backlog embedding %s", f"yielding: {reason}" if reason else "resumed")
1174
+ backlog_yield_reason = reason
1175
+ if reason:
1176
+ cycle_backlog_batch = 0
930
1177
  if queue_has_high_priority_backlog:
931
1178
  if not queue_backpressure_active:
932
1179
  LOGGER.info("durable high-priority queue has backlog; suppressing hot embedding")
933
1180
  queue_backpressure_active = True
934
1181
  if cycle_backlog_batch <= 0:
1182
+ if cpu_sampler is not None:
1183
+ cpu_sampler.observe("store queue")
935
1184
  cycles += 1
936
1185
  # Backpressure is not idleness: hold the configured cadence so the
937
1186
  # reserved backlog slice resumes on time.
@@ -950,6 +1199,8 @@ def run(
950
1199
  backlog_slice_logged = False
951
1200
  if cycle_backlog_batch > 0:
952
1201
  last_backlog = now
1202
+ last_backlog_duty = now
1203
+ cycle_start = cpu_sampler.begin_cycle() if cpu_sampler is not None else None
953
1204
  if cycle_fn is run_cycle:
954
1205
  result = _run_split_cycle(
955
1206
  db_path=db_path,
@@ -980,10 +1231,15 @@ def run(
980
1231
  # nothing (the hot slice is deliberately suppressed to yield to the drain),
981
1232
  # so it must not be treated as idle -- backing off there would slow recovery.
982
1233
  did_work = bool(result.embedded) or queue_has_backlog
1234
+ if cpu_sampler is not None:
1235
+ state = reason or ("running" if result.backlog_encoded else "waiting")
1236
+ cpu_sampler.observe(state, result.backlog_encoded, cycle_start=cycle_start)
983
1237
  if result.embedded:
984
1238
  LOGGER.info("embedded=%d", result.embedded)
985
1239
  except Exception:
986
1240
  LOGGER.exception("hotlane adapter cycle failed")
1241
+ if cpu_sampler is not None:
1242
+ cpu_sampler.observe("error")
987
1243
  cycles += 1
988
1244
  # A raised cycle is a RETRY, not an idle cycle (PR #735 review, macroscope):
989
1245
  # it already slept its own backoff here, and letting it fall through to the
@@ -1021,16 +1277,23 @@ def main() -> None:
1021
1277
  LOGGER.warning("Hotlane enrichment options are retired and ignored; continuing local embedding")
1022
1278
  signal.signal(signal.SIGTERM, _stop)
1023
1279
  signal.signal(signal.SIGINT, _stop)
1024
- run(
1025
- db_path=args.db,
1026
- interval=max(args.interval, 0.25),
1027
- recent_limit=max(args.recent_limit, 1),
1028
- backlog_interval=max(args.backlog_interval, 1.0),
1029
- backlog_batch=min(max(args.backlog_batch, 0), MAX_BACKLOG_BATCH),
1030
- enrich_interval=0.0,
1031
- enrich_limit=0,
1032
- enrich_since_hours=0,
1033
- )
1280
+ setup_cpu_budget(args.db)
1281
+ sampler = HotlaneCpuSampler(args.db)
1282
+ sampler.start()
1283
+ try:
1284
+ run(
1285
+ db_path=args.db,
1286
+ interval=max(args.interval, 0.25),
1287
+ recent_limit=max(args.recent_limit, 1),
1288
+ backlog_interval=max(args.backlog_interval, 1.0),
1289
+ backlog_batch=min(max(args.backlog_batch, 0), MAX_BACKLOG_BATCH),
1290
+ enrich_interval=0.0,
1291
+ enrich_limit=0,
1292
+ enrich_since_hours=0,
1293
+ cpu_sampler=sampler,
1294
+ )
1295
+ finally:
1296
+ sampler.close()
1034
1297
 
1035
1298
 
1036
1299
  if __name__ == "__main__":
@@ -54,7 +54,7 @@
54
54
  <key>ExitTimeOut</key>
55
55
  <integer>30</integer>
56
56
  <key>LowPriorityIO</key>
57
- <true/>
57
+ <false/>
58
58
  <key>SoftResourceLimits</key>
59
59
  <dict>
60
60
  <key>NumberOfFiles</key>
@@ -3,7 +3,7 @@
3
3
  "name": "io.github.etanhey/brainlayer",
4
4
  "title": "BrainLayer",
5
5
  "description": "Persistent local memory for AI agents \u2014 semantic search, knowledge graph, and 16 BrainBar MCP tools with ToolAnnotations over SQLite.",
6
- "version": "1.5.83",
6
+ "version": "1.5.85",
7
7
  "websiteUrl": "https://brainlayer.etanheyman.com",
8
8
  "repository": {
9
9
  "url": "https://github.com/EtanHey/brainlayer",
@@ -14,7 +14,7 @@
14
14
  "registryType": "pypi",
15
15
  "registryBaseUrl": "https://pypi.org",
16
16
  "identifier": "brainlayer",
17
- "version": "1.5.83",
17
+ "version": "1.5.85",
18
18
  "runtimeHint": "pipx",
19
19
  "transport": {
20
20
  "type": "stdio"
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "1.5.83"
5
+ __version__ = "1.5.85"
6
6
  # Git sha the distributed package was built from; stamped by the release build, None in a source tree.
7
7
  # scripts/sprint_gate.py reads it in "keg" mode to prove the served package is the code under test.
8
8
  # No runtime `str | None` annotation: launchd/install.sh imports this under /usr/bin/python3 (3.9).
@@ -0,0 +1 @@
1
+ BUILD_SHA = "48d9df608550988faa3c8a67a505f3c9751f9adc"