brainlayer 1.5.78__tar.gz → 1.5.80__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 (269) hide show
  1. {brainlayer-1.5.78 → brainlayer-1.5.80}/PKG-INFO +1 -1
  2. {brainlayer-1.5.78 → brainlayer-1.5.80}/pyproject.toml +1 -1
  3. {brainlayer-1.5.78 → brainlayer-1.5.80}/server.json +2 -2
  4. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/__init__.py +1 -1
  5. brainlayer-1.5.80/src/brainlayer/_build.py +1 -0
  6. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/backup_daily.py +20 -7
  7. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/brainbar_auto_activation.py +13 -4
  8. brainlayer-1.5.80/src/brainlayer/cli_jobs.py +108 -0
  9. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/decay_job.py +3 -0
  10. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/health_check.py +28 -11
  11. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/job_alerts.py +10 -1
  12. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/jsonl_backup.py +39 -22
  13. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/maintenance.py +64 -5
  14. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/maintenance_calendar.py +352 -32
  15. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/observability_backup.py +3 -5
  16. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/service_supervisor.py +3 -1
  17. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/supervisor_adapters.py +1 -0
  18. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/supervisor_guard.py +72 -5
  19. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/supervisor_jobs.py +27 -0
  20. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/vector_store.py +3 -24
  21. brainlayer-1.5.80/src/brainlayer/wal_checkpoint.py +338 -0
  22. brainlayer-1.5.80/src/brainlayer/writer_policy.py +31 -0
  23. brainlayer-1.5.78/src/brainlayer/_build.py +0 -1
  24. brainlayer-1.5.78/src/brainlayer/cli_jobs.py +0 -66
  25. brainlayer-1.5.78/src/brainlayer/wal_checkpoint.py +0 -175
  26. {brainlayer-1.5.78 → brainlayer-1.5.80}/.gitignore +0 -0
  27. {brainlayer-1.5.78 → brainlayer-1.5.80}/LICENSE +0 -0
  28. {brainlayer-1.5.78 → brainlayer-1.5.80}/README.md +0 -0
  29. {brainlayer-1.5.78 → brainlayer-1.5.80}/scripts/hotlane_brainbar_daemon.py +0 -0
  30. {brainlayer-1.5.78 → brainlayer-1.5.80}/scripts/launchd/backup-daily.sh +0 -0
  31. {brainlayer-1.5.78 → brainlayer-1.5.80}/scripts/launchd/brainlayer-env-run.sh +0 -0
  32. {brainlayer-1.5.78 → brainlayer-1.5.80}/scripts/launchd/brainlayer.env.example +0 -0
  33. {brainlayer-1.5.78 → brainlayer-1.5.80}/scripts/launchd/com.brainlayer.backup-daily.plist +0 -0
  34. {brainlayer-1.5.78 → brainlayer-1.5.80}/scripts/launchd/com.brainlayer.daemon.plist +0 -0
  35. {brainlayer-1.5.78 → brainlayer-1.5.80}/scripts/launchd/com.brainlayer.decay.plist +0 -0
  36. {brainlayer-1.5.78 → brainlayer-1.5.80}/scripts/launchd/com.brainlayer.drain.plist +0 -0
  37. {brainlayer-1.5.78 → brainlayer-1.5.80}/scripts/launchd/com.brainlayer.health-check.plist +0 -0
  38. {brainlayer-1.5.78 → brainlayer-1.5.80}/scripts/launchd/com.brainlayer.hotlane-brainbar.plist +0 -0
  39. {brainlayer-1.5.78 → brainlayer-1.5.80}/scripts/launchd/com.brainlayer.index.plist +0 -0
  40. {brainlayer-1.5.78 → brainlayer-1.5.80}/scripts/launchd/com.brainlayer.jsonl-backup.plist +0 -0
  41. {brainlayer-1.5.78 → brainlayer-1.5.80}/scripts/launchd/com.brainlayer.log-cap.plist +0 -0
  42. {brainlayer-1.5.78 → brainlayer-1.5.80}/scripts/launchd/com.brainlayer.maintenance-nightly.plist +0 -0
  43. {brainlayer-1.5.78 → brainlayer-1.5.80}/scripts/launchd/com.brainlayer.maintenance-weekly.plist +0 -0
  44. {brainlayer-1.5.78 → brainlayer-1.5.80}/scripts/launchd/com.brainlayer.maintenance.plist +0 -0
  45. {brainlayer-1.5.78 → brainlayer-1.5.80}/scripts/launchd/com.brainlayer.observability.plist +0 -0
  46. {brainlayer-1.5.78 → brainlayer-1.5.80}/scripts/launchd/com.brainlayer.p0-counter.plist +0 -0
  47. {brainlayer-1.5.78 → brainlayer-1.5.80}/scripts/launchd/com.brainlayer.repair-fts.plist +0 -0
  48. {brainlayer-1.5.78 → brainlayer-1.5.80}/scripts/launchd/com.brainlayer.t3-ingest.plist +0 -0
  49. {brainlayer-1.5.78 → brainlayer-1.5.80}/scripts/launchd/com.brainlayer.throughput-watchdog.plist +0 -0
  50. {brainlayer-1.5.78 → brainlayer-1.5.80}/scripts/launchd/com.brainlayer.tier0-watchdog.plist +0 -0
  51. {brainlayer-1.5.78 → brainlayer-1.5.80}/scripts/launchd/com.brainlayer.wal-checkpoint.plist +0 -0
  52. {brainlayer-1.5.78 → brainlayer-1.5.80}/scripts/launchd/com.brainlayer.watch.plist +0 -0
  53. {brainlayer-1.5.78 → brainlayer-1.5.80}/scripts/launchd/com.etanhey.brainlayer-fleet-watchdog.plist +0 -0
  54. {brainlayer-1.5.78 → brainlayer-1.5.80}/scripts/launchd/fleet-watchdog.sh +0 -0
  55. {brainlayer-1.5.78 → brainlayer-1.5.80}/scripts/launchd/install.sh +0 -0
  56. {brainlayer-1.5.78 → brainlayer-1.5.80}/scripts/launchd/jsonl-backup.sh +0 -0
  57. {brainlayer-1.5.78 → brainlayer-1.5.80}/scripts/launchd/throughput-watchdog.py +0 -0
  58. {brainlayer-1.5.78 → brainlayer-1.5.80}/scripts/release-verify-signatures.sh +0 -0
  59. {brainlayer-1.5.78 → brainlayer-1.5.80}/scripts/tier0-watchdog.sh +0 -0
  60. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/__main__.py +0 -0
  61. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/_helpers.py +0 -0
  62. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/agent_profiles.py +0 -0
  63. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/agent_provenance.py +0 -0
  64. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/alarm.py +0 -0
  65. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/archive_collapse.py +0 -0
  66. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/archive_lifecycle.py +0 -0
  67. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/backup_retention_invariant.py +0 -0
  68. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/badge_state.py +0 -0
  69. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/bitemporal.py +0 -0
  70. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/brainbar_activation.py +0 -0
  71. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/brainbar_hybrid_helper.py +0 -0
  72. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/brainbar_identity.py +0 -0
  73. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/cachedelete_purge.swift +0 -0
  74. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/calibrate.py +0 -0
  75. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/chunk_origin.py +0 -0
  76. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/chunk_origin_backfill.py +0 -0
  77. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/chunk_origin_wipe.py +0 -0
  78. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/chunk_write.py +0 -0
  79. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/classify.py +0 -0
  80. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/claude_paths.py +0 -0
  81. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/cli/__init__.py +0 -0
  82. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/cli/wizard.py +0 -0
  83. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/cli_new.py +0 -0
  84. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/cloud_backfill.py +0 -0
  85. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/clustering.py +0 -0
  86. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/component_jobs.py +0 -0
  87. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/config.py +0 -0
  88. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/consolidation_plan.py +0 -0
  89. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/content_class.py +0 -0
  90. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/correction_judge.py +0 -0
  91. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/data/agents_registry.yaml +0 -0
  92. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/data/sandbox_seeds/skill-eval-baseline.json +0 -0
  93. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/db_shrink.py +0 -0
  94. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/decay.py +0 -0
  95. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/decay_backfill.py +0 -0
  96. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/dedupe.py +0 -0
  97. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/dedupe_merge.py +0 -0
  98. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/deploy_drift.py +0 -0
  99. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/disk_space.py +0 -0
  100. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/doctor.py +0 -0
  101. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/drain.py +0 -0
  102. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/drain_liveness.py +0 -0
  103. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/drive_credentials.py +0 -0
  104. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/embeddings.py +0 -0
  105. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/engine.py +0 -0
  106. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/enrichment_replay.py +0 -0
  107. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/eval/__init__.py +0 -0
  108. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/eval/abcde_enrich_runner.py +0 -0
  109. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/eval/abcde_variants.py +0 -0
  110. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/eval/abcde_variants.yaml +0 -0
  111. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/eval/benchmark.py +0 -0
  112. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/eval/enrichment_gold.py +0 -0
  113. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/eval/enrichment_graders.py +0 -0
  114. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/eval/enrichment_judge.py +0 -0
  115. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/eval/enrichment_llm_judge.py +0 -0
  116. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/eval/enrichment_quality_benchmark.py +0 -0
  117. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/eval/experiment_store.py +0 -0
  118. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/eval/fixtures/README.md +0 -0
  119. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/eval/labeling/enrichment-labeler.html +0 -0
  120. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/eval/phoenix_gate/__init__.py +0 -0
  121. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/eval/phoenix_gate/__main__.py +0 -0
  122. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/eval/phoenix_gate/baseline_store.py +0 -0
  123. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/eval/phoenix_gate/cli.py +0 -0
  124. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/eval/phoenix_gate/models.py +0 -0
  125. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/eval/phoenix_gate/phoenix_client.py +0 -0
  126. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/eval/phoenix_gate/regression_gate.py +0 -0
  127. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/eval/phoenix_gate/triggers.py +0 -0
  128. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/eval/pilot_artifact_report.py +0 -0
  129. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/fallback_replay.py +0 -0
  130. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/git_learning.py +0 -0
  131. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/hook_python.py +0 -0
  132. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/hooks/__init__.py +0 -0
  133. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/hooks/indexer.py +0 -0
  134. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/import_sweep.py +0 -0
  135. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/index_admission.py +0 -0
  136. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/index_budget.py +0 -0
  137. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/index_completeness.py +0 -0
  138. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/index_new.py +0 -0
  139. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/index_watchdog.py +0 -0
  140. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/ingest/__init__.py +0 -0
  141. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/ingest/codex.py +0 -0
  142. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/ingest/t3.py +0 -0
  143. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/ingest_denylist.py +0 -0
  144. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/ingest_guard.py +0 -0
  145. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/isolation_proof.py +0 -0
  146. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/job_lifecycle_health.py +0 -0
  147. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/jobs.py +0 -0
  148. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/kg/__init__.py +0 -0
  149. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/kg_cleanup.py +0 -0
  150. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/kg_judge.py +0 -0
  151. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/kg_promotion.py +0 -0
  152. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/kg_repo.py +0 -0
  153. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/kg_review_session.py +0 -0
  154. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/kg_session_finish.py +0 -0
  155. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/kg_session_harvest.py +0 -0
  156. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/launchd_consolidation.py +0 -0
  157. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/launchd_install.py +0 -0
  158. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/launchd_primitive.py +0 -0
  159. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/launchd_revert.py +0 -0
  160. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/lexical_defense.py +0 -0
  161. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/lexical_defense_dictionary.json +0 -0
  162. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/log_cap.py +0 -0
  163. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/maintenance_control.py +0 -0
  164. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/mcp/__init__.py +0 -0
  165. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/mcp/_format.py +0 -0
  166. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/mcp/_shared.py +0 -0
  167. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/mcp/enrich_handler.py +0 -0
  168. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/mcp/entity_handler.py +0 -0
  169. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/mcp/palette.py +0 -0
  170. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/mcp/search_handler.py +0 -0
  171. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/mcp/store_handler.py +0 -0
  172. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/mcp/tags_handler.py +0 -0
  173. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/mcp_socket_config.py +0 -0
  174. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/mcp_stdio_bridge.py +0 -0
  175. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/memory_types.py +0 -0
  176. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/migrate.py +0 -0
  177. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/migration_custody.py +0 -0
  178. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/migration_journal.py +0 -0
  179. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/migration_runtime.py +0 -0
  180. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/notification_policy.py +0 -0
  181. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/observability_surface.py +0 -0
  182. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/observability_transport.py +0 -0
  183. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/p0_longitudinal_count.py +0 -0
  184. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/parent_death.py +0 -0
  185. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/paths.py +0 -0
  186. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pause.py +0 -0
  187. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/phonetic.py +0 -0
  188. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/__init__.py +0 -0
  189. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/agent_enrichment.py +0 -0
  190. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/analyze_communication.py +0 -0
  191. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/batch_extraction.py +0 -0
  192. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/brain_graph.py +0 -0
  193. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/chat_tags.py +0 -0
  194. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/chunk.py +0 -0
  195. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/classify.py +0 -0
  196. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/cloud_scrub.py +0 -0
  197. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/cluster_sampling.py +0 -0
  198. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/code_intelligence.py +0 -0
  199. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/correction_detection.py +0 -0
  200. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/correction_mining.py +0 -0
  201. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/digest.py +0 -0
  202. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/enrichment_prompts.py +0 -0
  203. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/enrichment_results.py +0 -0
  204. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/enrichment_tiers.py +0 -0
  205. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/entity_extraction.py +0 -0
  206. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/entity_resolution.py +0 -0
  207. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/extract.py +0 -0
  208. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/extract_claude_desktop.py +0 -0
  209. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/extract_corrections.py +0 -0
  210. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/extract_markdown.py +0 -0
  211. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/extract_whatsapp.py +0 -0
  212. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/frustration_mining.py +0 -0
  213. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/git_overlay.py +0 -0
  214. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/kg_extraction.py +0 -0
  215. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/kg_extraction_groq.py +0 -0
  216. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/longitudinal_analyzer.py +0 -0
  217. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/obsidian_export.py +0 -0
  218. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/operation_grouping.py +0 -0
  219. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/plan_linking.py +0 -0
  220. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/rate_limiter.py +0 -0
  221. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/relation_backfill.py +0 -0
  222. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/relation_verification.py +0 -0
  223. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/sanitize.py +0 -0
  224. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/secret_scrub.py +0 -0
  225. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/semantic_style.py +0 -0
  226. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/sentiment.py +0 -0
  227. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/session_history.py +0 -0
  228. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/style_embed.py +0 -0
  229. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/style_index.py +0 -0
  230. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/tag_entity_promotion.py +0 -0
  231. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/temporal_chains.py +0 -0
  232. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/time_batcher.py +0 -0
  233. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/unified_timeline.py +0 -0
  234. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/pipeline/write_queue.py +0 -0
  235. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/provenance.py +0 -0
  236. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/provenance_autosupersede.py +0 -0
  237. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/provenance_integration.py +0 -0
  238. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/queue_io.py +0 -0
  239. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/queue_merge.py +0 -0
  240. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/reembed_backfill.py +0 -0
  241. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/retired_services.py +0 -0
  242. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/runtime_store.py +0 -0
  243. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/sandbox_db.py +0 -0
  244. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/scoping.py +0 -0
  245. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/scrub_at_rest.py +0 -0
  246. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/search_fanout.py +0 -0
  247. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/search_profile.py +0 -0
  248. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/search_repo.py +0 -0
  249. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/session_repo.py +0 -0
  250. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/setup.py +0 -0
  251. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/socket_hygiene.py +0 -0
  252. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/source_class_migration.py +0 -0
  253. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/spotlight.py +0 -0
  254. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/storage.py +0 -0
  255. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/store.py +0 -0
  256. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/supervisor_child.py +0 -0
  257. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/supervisor_diagnostics.py +0 -0
  258. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/system_prompt_guard.py +0 -0
  259. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/t3_linkage_alarm.py +0 -0
  260. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/t3_provenance.py +0 -0
  261. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/t3_sources.py +0 -0
  262. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/tag_normalization.py +0 -0
  263. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/taxonomy.json +0 -0
  264. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/telemetry.py +0 -0
  265. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/timestamp_iso.py +0 -0
  266. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/types.py +0 -0
  267. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/watcher.py +0 -0
  268. {brainlayer-1.5.78 → brainlayer-1.5.80}/src/brainlayer/watcher_bridge.py +0 -0
  269. {brainlayer-1.5.78 → brainlayer-1.5.80}/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.78
3
+ Version: 1.5.80
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.78"
3
+ version = "1.5.80"
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"
@@ -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.78",
6
+ "version": "1.5.80",
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.78",
17
+ "version": "1.5.80",
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.78"
5
+ __version__ = "1.5.80"
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 = "cc301a9f19b679f75278f9cca43c48fc49fe8d7e"
@@ -406,6 +406,10 @@ class BrainBarDatabaseIdentityError(RuntimeError):
406
406
  """BrainBar serves a different database than the one this backup was aimed at."""
407
407
 
408
408
 
409
+ class BrainBarDatabaseIdentityUnavailableError(BrainBarDatabaseIdentityError):
410
+ """BrainBar did not confirm its open database; no difference has been established."""
411
+
412
+
409
413
  # MCPRouter.handleBrainBackupVacuumInto refuses with this before any VACUUM INTO when the
410
414
  # request's expected_db_path does not identify the database BrainBar serves.
411
415
  BRAINBAR_DB_IDENTITY_MISMATCH = "BrainBar database identity mismatch"
@@ -1230,7 +1234,9 @@ def _refuse_foreign_db_on_production_socket(db_path: Path, socket_path: Path | s
1230
1234
  configured_db = resolve_db_path()
1231
1235
  if not _same_database_file(db_path, configured_db):
1232
1236
  raise BrainBarDatabaseIdentityError(
1233
- f"{BRAINBAR_DB_IDENTITY_MISMATCH}: refusing to snapshot {db_path} through the production BrainBar "
1237
+ "Backup blocked: BrainBar has a different database open. "
1238
+ "Check the backup database path and BrainBar's configured database. "
1239
+ f"Details: refusing to snapshot {db_path} through the production BrainBar "
1234
1240
  f"socket {resolved_socket}, which serves the configured database {configured_db}; "
1235
1241
  "pass an isolated socket_path to back up any other database"
1236
1242
  )
@@ -1897,15 +1903,22 @@ def request_brainbar_vacuum_into(
1897
1903
  echo = echo if isinstance(echo, dict) else {}
1898
1904
  served_identity = (echo.get("db_dev"), echo.get("db_ino"))
1899
1905
  expected_identity = _file_identity(resolved_expected_db)
1900
- if not all(isinstance(v, int) for v in served_identity) or served_identity != expected_identity:
1901
- # Terminal response: BrainBar has stopped writing this attempt. Whatever it
1902
- # vacuumed is not the configured database, so it must not survive here.
1906
+ identity_valid = all(type(v) is int for v in served_identity) and served_identity[1] > 0
1907
+ if not identity_valid or served_identity != expected_identity:
1908
+ # A terminal response ends this writer. Unconfirmed and foreign snapshots
1909
+ # both fail closed, but absence of support proves no database difference.
1903
1910
  attempt_path.unlink(missing_ok=True)
1904
1911
  _backup_attempt_completion_marker(attempt_path).unlink(missing_ok=True)
1912
+ if not identity_valid:
1913
+ raise BrainBarDatabaseIdentityUnavailableError(
1914
+ "Backup blocked: this BrainBar version cannot confirm which database it has open. "
1915
+ "Upgrade BrainBar."
1916
+ )
1905
1917
  raise BrainBarDatabaseIdentityError(
1906
- f"{BRAINBAR_DB_IDENTITY_MISMATCH}: expected {resolved_expected_db} {expected_identity}, "
1907
- f"BrainBar reported {echo.get('db_path')!r} {served_identity}; deleted the unconfirmed "
1908
- "snapshot. A BrainBar without open-database identity support must be upgraded."
1918
+ "Backup blocked: BrainBar has a different database open. "
1919
+ "Check the backup database path and BrainBar's configured database. "
1920
+ f"Details: expected {resolved_expected_db} {expected_identity}, "
1921
+ f"BrainBar reported {echo.get('db_path')!r} {served_identity}; deleted the unconfirmed snapshot."
1909
1922
  )
1910
1923
  if not attempt_path.exists():
1911
1924
  raise RuntimeError(f"BrainBar backup did not create snapshot: {attempt_path}")
@@ -31,12 +31,15 @@ def brew_processes():
31
31
  # Include brew's Ruby entrypoint and Homebrew-owned child executables, even
32
32
  # after reparenting. Unknown/unreadable process tables defer activation.
33
33
  rows = process_table().splitlines()
34
- matches = []
34
+ matches, readable = [], 0
35
35
  for row in rows:
36
36
  fields = row.split(None, 3)
37
- if len(fields) != 4 or not fields[0].isascii() or not fields[0].isdecimal():
38
- raise ValueError("unreadable process table")
39
- tokens = fields[3].split()
37
+ # A process exiting or exec-ing mid-scan can tear one row: empty args
38
+ # are still checked by comm, a row without a pid and comm is skipped.
39
+ if len(fields) < 3 or not fields[0].isascii() or not fields[0].isdecimal():
40
+ continue
41
+ readable += 1
42
+ tokens = fields[3].split() if len(fields) == 4 else []
40
43
  if Path(fields[2]).name == "brew" or any(
41
44
  Path(token).name in {"brew", "brew.rb"} or "/Homebrew/" in token or "/homebrew/Library/Homebrew/" in token
42
45
  for token in tokens
@@ -44,6 +47,8 @@ def brew_processes():
44
47
  # argv is detection-only: Homebrew curl headers can carry tokens.
45
48
  executable = scrub_secrets(Path(fields[2]).name).text[:128]
46
49
  matches.append(f"{fields[0]} {executable}")
50
+ if not readable:
51
+ raise ValueError("unreadable process table")
47
52
  return matches
48
53
 
49
54
 
@@ -85,6 +90,9 @@ class AutoActivator:
85
90
  processes = brew_processes()
86
91
  except (OSError, subprocess.SubprocessError) as exc:
87
92
  raise ProcessScanUnavailable(type(exc).__name__) from None
93
+ except ValueError:
94
+ # Counted and alerted only after 12 ticks, never an immediate refusal.
95
+ raise ProcessScanUnavailable("unreadable process table") from None
88
96
  # Recover before activation can report a new failure on the same alert key.
89
97
  self.state.with_suffix(".scan.json").unlink(missing_ok=True)
90
98
  reason = active_alerts().get("brainbar-activation", "")
@@ -97,6 +105,7 @@ class AutoActivator:
97
105
  (
98
106
  "BrainBar activation process scan failed for >=12 ticks:",
99
107
  "BrainBar activation monitor refused: Command '['/bin/ps',",
108
+ "BrainBar activation monitor refused: unreadable process table",
100
109
  )
101
110
  ):
102
111
  report("brainbar-activation", None)
@@ -0,0 +1,108 @@
1
+ """Explicit consolidated launcher and component CLI commands."""
2
+
3
+ import asyncio
4
+ import datetime as dt
5
+ import json
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ import typer
10
+
11
+
12
+ def register(app, jobs_app):
13
+ @app.command("maintenance-calendar")
14
+ def maintenance_calendar(
15
+ lib_dir: Path = typer.Option(Path(__file__).parent / "launchd", "--lib-dir"),
16
+ scheduled_date: str | None = typer.Option(None, "--date"),
17
+ state_dir: Path | None = typer.Option(None, "--state-dir"),
18
+ logs_dir: Path = typer.Option(Path.home() / "Library/Logs/brainlayer", "--logs-dir"),
19
+ recover_transcripts: bool = typer.Option(False, "--recover-transcripts"),
20
+ recover_failed: bool = typer.Option(False, "--recover-failed"),
21
+ status: bool = typer.Option(
22
+ False, "--status", help="Check failed-step recovery admission without running steps"
23
+ ),
24
+ ):
25
+ """Run BrainLayer Maintenance for one scheduled date."""
26
+ from .maintenance_calendar import calendar_steps, run_calendar
27
+ from .paths import supervisor_socket_path
28
+
29
+ try:
30
+ date = dt.date.fromisoformat(scheduled_date) if scheduled_date else dt.date.today()
31
+ except ValueError as exc:
32
+ raise typer.BadParameter("date must be ISO YYYY-MM-DD") from exc
33
+ if recover_failed and recover_transcripts:
34
+ raise typer.BadParameter("Choose --recover-failed or --recover-transcripts")
35
+ if status and not recover_failed:
36
+ raise typer.BadParameter("--status requires --recover-failed")
37
+ if recover_failed and scheduled_date:
38
+ raise typer.BadParameter("Failed-step recovery uses the latest scheduled night; omit --date")
39
+ result = {}
40
+ directory = state_dir or supervisor_socket_path().parent / "calendar"
41
+ if directory.resolve() == supervisor_socket_path().parent.resolve():
42
+ raise typer.BadParameter("calendar custody directory must differ from resident supervisor")
43
+ try:
44
+ plan = calendar_steps(sys.executable, str(Path(sys.executable).with_name("brainlayer")), lib_dir, date)
45
+ code = asyncio.run(
46
+ run_calendar(
47
+ plan,
48
+ directory,
49
+ logs_dir,
50
+ install_signals=True,
51
+ transcript_recovery=recover_transcripts,
52
+ **(
53
+ dict(failed_recovery=True, recovery_status=status, recovery_result=result)
54
+ if recover_failed
55
+ else {}
56
+ ),
57
+ )
58
+ )
59
+ except (OSError, ValueError, RuntimeError, KeyError, TypeError) as exc:
60
+ if not recover_failed:
61
+ raise
62
+ from .maintenance_calendar import recovery_refusal_reason
63
+
64
+ results = result.get("results") or {}
65
+ if result.get("status") == "accepted" and results and all(v == "ok" for v in results.values()):
66
+ # The retried steps finished and were journaled; only cleanup after them failed.
67
+ result.update(
68
+ reason="Retried successfully; maintenance cleanup afterwards had a problem (see the log)."
69
+ )
70
+ code = 0
71
+ elif results:
72
+ # The steps ran and did not all succeed: keep their outcome and reason, not an
73
+ # availability error. A cleanup failure afterwards does not change what ran.
74
+ if result.get("status") != "failed":
75
+ result.update(reason="Retry did not finish successfully; see the step results and logs")
76
+ result.update(status="failed")
77
+ code = 1
78
+ else:
79
+ status = "failed" if result.get("status") in {"accepted", "failed"} else "refused"
80
+ result.update(status=status, reason=recovery_refusal_reason(exc), steps=[])
81
+ code = 1
82
+ if recover_failed:
83
+ typer.echo(json.dumps(result, sort_keys=True))
84
+ raise typer.Exit(code)
85
+
86
+ @app.command("daemon")
87
+ def daemon(manifest: Path = typer.Option(..., "--manifest"), socket: Path | None = typer.Option(None, "--socket")):
88
+ """Run BrainLayer Daemon from a private command manifest."""
89
+ from .supervisor_jobs import run_daemon
90
+
91
+ run_daemon(manifest, socket)
92
+
93
+ @jobs_app.command("component")
94
+ def component(
95
+ name: str,
96
+ action: str,
97
+ manifest: Path | None = typer.Option(None, "--manifest"),
98
+ socket: Path | None = typer.Option(None, "--socket"),
99
+ ):
100
+ """Status/restart or configured env/log paths for one component."""
101
+ from .component_jobs import component_action
102
+
103
+ try:
104
+ result = component_action(name, action, manifest=manifest, path=socket)
105
+ except (OSError, ValueError, RuntimeError, KeyError, TypeError) as exc:
106
+ typer.echo(json.dumps({"error": str(exc)}))
107
+ raise typer.Exit(1) from exc
108
+ typer.echo(json.dumps(result, sort_keys=True))
@@ -5,6 +5,8 @@ from typing import Any
5
5
 
6
6
  import apsw
7
7
 
8
+ from .writer_policy import configure_writer_wal_policy
9
+
8
10
  ARCHIVE_THRESHOLD = 0.1
9
11
 
10
12
 
@@ -35,6 +37,7 @@ def run_decay_job(
35
37
  current_time = time.time() if now is None else now
36
38
  connection = apsw.Connection(str(db_path))
37
39
  connection.setbusytimeout(30_000)
40
+ configure_writer_wal_policy(connection)
38
41
  cursor = connection.cursor()
39
42
 
40
43
  last_rowid = 0
@@ -1572,14 +1572,15 @@ def _run_health_check_locked(
1572
1572
  if result.missing_embedding_scan.completeness == "complete":
1573
1573
  result.missing_vectors = result.missing_embedding_scan.missing
1574
1574
  except sqlite3.OperationalError as exc:
1575
- result.missing_embedding_scan = MissingEmbeddingScan(reason=str(exc))
1575
+ # Stable codes for BrainBar's vector notice; the exception text stays in the issue message.
1576
+ result.missing_embedding_scan = MissingEmbeddingScan(reason="sql_error")
1576
1577
  add_issue(
1577
1578
  "missing_embeddings_count_failed",
1578
1579
  "warning" if _text_has_sqlite_busy_signal(exc) else "critical",
1579
1580
  f"could not count missing embeddings: {exc}",
1580
1581
  )
1581
1582
  except Exception as exc:
1582
- result.missing_embedding_scan = MissingEmbeddingScan(reason=str(exc))
1583
+ result.missing_embedding_scan = MissingEmbeddingScan(reason="unexpected")
1583
1584
  add_issue(
1584
1585
  "missing_embeddings_count_failed",
1585
1586
  "critical",
@@ -1587,18 +1588,26 @@ def _run_health_check_locked(
1587
1588
  )
1588
1589
  scan = result.missing_embedding_scan
1589
1590
  state["missing_embedding_scan"] = asdict(scan)
1590
- if scan.completeness != "complete":
1591
+ # Keep budget reasons aligned with BrainBarVectorScan.routinePartial in
1592
+ # brain-bar/Sources/BrainBar/Dashboard/ObservabilityView.swift.
1593
+ routine_partial = (
1594
+ scan.completeness == "partial"
1595
+ and scan.scanned > 0
1596
+ and scan.reason in {"instruction_limit", "time_limit", "page_limit", "window_end"}
1597
+ )
1598
+ if scan.completeness != "complete" and not routine_partial:
1591
1599
  add_issue(
1592
1600
  "missing_embeddings_scan_incomplete",
1593
1601
  "warning",
1594
- f"missing embeddings {scan.completeness}: scanned {scan.scanned} rows ({scan.reason}); not an exact count",
1602
+ "The vector check could not finish. The current backlog is not verified by this check.",
1603
+ )
1604
+ if scan.completeness != "complete" and scan.missing:
1605
+ add_issue(
1606
+ "missing_embeddings_backlog",
1607
+ "warning",
1608
+ f"At least {scan.missing:,} checked {'chunk is' if scan.missing == 1 else 'chunks are'} waiting for vectors. "
1609
+ "See Vector backlog for the latest full count.",
1595
1610
  )
1596
- if scan.missing:
1597
- add_issue(
1598
- "missing_embeddings_backlog",
1599
- "warning",
1600
- f"at least {scan.missing} missing embeddings in {scan.scanned} sampled rows",
1601
- )
1602
1611
 
1603
1612
  hotlane_sample_advanced, hotlane_progressed = _hotlane_progress_sample(state, result.missing_vectors, now)
1604
1613
  if hotlane_progressed:
@@ -1978,7 +1987,15 @@ def _run_health_check_locked(
1978
1987
  state_payload["heal_failures"] = heal_failures
1979
1988
  if job_tick is not None:
1980
1989
  state_payload["job_lifecycle"] = job_tick.state
1981
- state_payload["heal_tripped"] = sorted(heal_tripped) if result.issues else []
1990
+ state_payload["heal_tripped"] = (
1991
+ sorted(heal_tripped)
1992
+ if result.issues
1993
+ else [
1994
+ key
1995
+ for key in sorted(heal_tripped)
1996
+ if not hotlane_sample_advanced and key == _heal_key(config.hotlane_label, "missing_embeddings_not_draining")
1997
+ ]
1998
+ )
1982
1999
  _record_queue_notice(state_payload, queue_notice_signature)
1983
2000
  state_payload["ts"] = now.isoformat()
1984
2001
  if result.missing_vectors is not None:
@@ -65,7 +65,12 @@ def report(
65
65
  path: Path | None = None,
66
66
  notify: Callable[[str], None] = _notify,
67
67
  ) -> bool:
68
- """Set or clear one episode. Return true only when the visible state changes."""
68
+ """Set or clear one episode. Return true only when the visible state changes.
69
+
70
+ Only a new failure posts a notification, and only when no other active key already
71
+ carries the identical message (one incident fanned out across keys posts once).
72
+ Recovery updates the alert file and the health log but never notifies.
73
+ """
69
74
  destination = path or alert_path()
70
75
  with _locked(destination):
71
76
  current = active_alerts(destination)
@@ -75,15 +80,19 @@ def report(
75
80
  return False
76
81
  current.pop(key)
77
82
  message = f"BrainLayer {key} recovered"
83
+ post = False
78
84
  else:
79
85
  if before is not None and (before == reason or key != "drive-consent"):
80
86
  return False
87
+ post = reason not in (value for other, value in current.items() if other != key)
81
88
  current[key] = reason
82
89
  message = reason
83
90
  from brainlayer.drive_credentials import _atomic_write
84
91
 
85
92
  _atomic_write(destination, current)
86
93
  LOG.warning("health event job=%s state=%s reason=%s", key, "recovered" if reason is None else "failed", message)
94
+ if not post:
95
+ return True
87
96
  try:
88
97
  notify(message)
89
98
  except (OSError, subprocess.SubprocessError) as exc:
@@ -30,6 +30,7 @@ opaque `.pb` implicit records, and per-session `.system_generated` /
30
30
  from __future__ import annotations
31
31
 
32
32
  import datetime as dt
33
+ import errno
33
34
  import fcntl
34
35
  import functools
35
36
  import gzip
@@ -1226,7 +1227,8 @@ class _HashingReader:
1226
1227
 
1227
1228
  def create_jsonl_bundle_with_digests(
1228
1229
  candidates: list[JsonlCandidate], staging_dir: Path, *, date_stamp: str
1229
- ) -> tuple[Path, dict[str, str], dict[str, int]]:
1230
+ ) -> tuple[Path | None, dict[str, str], dict[str, int]]:
1231
+ """Bundle completed source snapshots; return no archive when every source vanished."""
1230
1232
  if not candidates:
1231
1233
  raise ValueError("create_jsonl_bundle requires at least one candidate")
1232
1234
  staging_dir = Path(staging_dir).expanduser()
@@ -1240,30 +1242,37 @@ def create_jsonl_bundle_with_digests(
1240
1242
  sizes: dict[str, int] = {}
1241
1243
  try:
1242
1244
  with tarfile.open(temp_path, "w:gz") as tar:
1245
+ tar.dereference = True
1243
1246
  for candidate in candidates:
1244
- # Hash and archive the SAME bytes, without holding the file in memory.
1245
- # Re-reading the source afterwards could record a digest for content the
1246
- # archive does not contain -- a file that changed while keeping its mtime
1247
- # and size would then read as covered. Sources reach ~375MB, so the digest
1248
- # is taken from the very stream tarfile consumes rather than from a copy.
1249
- # Discovery follows file symlinks, so archive their resolved target as a
1250
- # regular member too; preserving the link would make verification reject
1251
- # the whole bundle as non-regular.
1252
- source_path = candidate.path.resolve()
1253
- info = tar.gettarinfo(str(source_path), arcname=_archive_name(candidate))
1254
- with source_path.open("rb") as handle:
1255
- reader = _HashingReader(handle)
1256
- tar.addfile(info, reader)
1247
+ # Open before fstat so unlink/rotation cannot invalidate the metadata.
1248
+ # Snapshot one member on disk before writing its tar header: an ENOENT
1249
+ # even partway through a read must leave no partial member in the gzip.
1250
+ # Hash exactly the snapshot bytes, at O(buffer) memory. Following links
1251
+ # and dereferencing hardlinks keeps every source a regular member.
1252
+ with tempfile.TemporaryFile(dir=staging_dir) as snapshot:
1253
+ try:
1254
+ with candidate.path.open("rb") as handle:
1255
+ info = tar.gettarinfo(fileobj=handle, arcname=_archive_name(candidate))
1256
+ reader = _HashingReader(handle)
1257
+ tarfile.copyfileobj(reader, snapshot, info.size)
1258
+ except OSError as exc:
1259
+ if isinstance(exc, FileNotFoundError) or exc.errno == errno.ENOENT:
1260
+ continue
1261
+ raise
1262
+ snapshot.seek(0)
1263
+ tar.addfile(info, snapshot)
1257
1264
  digests[candidate.path.as_posix()] = reader.hexdigest()
1258
1265
  sizes[candidate.path.as_posix()] = info.size
1266
+ if not digests:
1267
+ return None, digests, sizes
1259
1268
  os.replace(temp_path, archive_path)
1260
1269
  finally:
1261
1270
  temp_path.unlink(missing_ok=True)
1262
1271
  return archive_path, digests, sizes
1263
1272
 
1264
1273
 
1265
- def create_jsonl_bundle(candidates: list[JsonlCandidate], staging_dir: Path, *, date_stamp: str) -> Path:
1266
- """Backwards-compatible wrapper returning only the archive path."""
1274
+ def create_jsonl_bundle(candidates: list[JsonlCandidate], staging_dir: Path, *, date_stamp: str) -> Path | None:
1275
+ """Return only the archive path, or None when every candidate vanished."""
1267
1276
  archive_path, _, _ = create_jsonl_bundle_with_digests(candidates, staging_dir, date_stamp=date_stamp)
1268
1277
  return archive_path
1269
1278
 
@@ -1626,7 +1635,18 @@ def run_backup(
1626
1635
  if pending_candidates:
1627
1636
  changed = [candidate for candidate in changed if candidate.path.as_posix() not in pending_icloud_sources]
1628
1637
 
1629
- if not changed and pending_candidates:
1638
+ archive_path = None
1639
+ bundle_digests: dict[str, str] = {}
1640
+ bundle_sizes: dict[str, int] = {}
1641
+ if changed:
1642
+ archive_path, bundle_digests, bundle_sizes = create_jsonl_bundle_with_digests(
1643
+ changed, staging_dir, date_stamp=date_stamp
1644
+ )
1645
+ bundled = [candidate for candidate in changed if candidate.path.as_posix() in bundle_digests]
1646
+ vanished += len(changed) - len(bundled)
1647
+ changed = bundled
1648
+
1649
+ if archive_path is None and pending_candidates:
1630
1650
  error = (
1631
1651
  f"iCloud coverage is pending; repair deferred while {len(pending_candidates)} source(s) finish uploading"
1632
1652
  )
@@ -1649,7 +1669,7 @@ def run_backup(
1649
1669
  _enqueue_run_summary(result, queue_dir=queue_dir)
1650
1670
  return result
1651
1671
 
1652
- if not changed and icloud_bootstrap_pending and active:
1672
+ if archive_path is None and icloud_bootstrap_pending and active:
1653
1673
  error = (
1654
1674
  "iCloud coverage is not verified; repair deferred because "
1655
1675
  f"{len(active)} discovered source(s) are still active"
@@ -1672,7 +1692,7 @@ def run_backup(
1672
1692
  _enqueue_run_summary(result, queue_dir=queue_dir)
1673
1693
  return result
1674
1694
 
1675
- if not changed:
1695
+ if archive_path is None:
1676
1696
  result: dict[str, Any] = {
1677
1697
  "attempted_at": attempted_at,
1678
1698
  "status": "no-op",
@@ -1689,9 +1709,6 @@ def run_backup(
1689
1709
  _enqueue_run_summary(result, queue_dir=queue_dir)
1690
1710
  return result
1691
1711
 
1692
- archive_path, bundle_digests, bundle_sizes = create_jsonl_bundle_with_digests(
1693
- changed, staging_dir, date_stamp=date_stamp
1694
- )
1695
1712
  archive_size = archive_path.stat().st_size
1696
1713
  result = {
1697
1714
  "attempted_at": attempted_at,
@@ -43,7 +43,8 @@ from .paths import get_db_path
43
43
  from .pause import DEFAULT_PAUSE_SENTINEL_PATH, pause_applies_to_label, pause_sentinel_state
44
44
  from .pipeline.code_intelligence import _pid_is_alive, _pid_start_time
45
45
  from .queue_io import get_queue_dir
46
- from .wal_checkpoint import checkpoint
46
+ from .wal_checkpoint import CheckpointGuardTimeout, checkpoint, get_wal_size
47
+ from .writer_policy import configure_writer_wal_policy
47
48
 
48
49
  PAUSE_SENTINEL_PATH = DEFAULT_PAUSE_SENTINEL_PATH
49
50
 
@@ -64,6 +65,7 @@ _WATCHDOG_DISABLED_BEFORE = "_fleet_watchdog_disabled_before"
64
65
  MAINTENANCE_LOCK_TIMEOUT_SECONDS = 4 * 60 * 60
65
66
  # VACUUM is interrupted this long before its hold expires, so restore wins the calendar's cut.
66
67
  HOLD_UNWIND_S = 30.0
68
+ POST_VACUUM_BUSY_S = 0.25
67
69
  VACUUM_PROGRESS_OPS = 100_000 # SQLite VM steps between deadline polls during VACUUM.
68
70
  # VACUUM's final copy-back into the main DB runs no VM steps, so the progress handler cannot stop
69
71
  # it: a 20.7 GB copy (MBP, 2026-10-19) overran a 60 s cutoff and completed at 328 s, 15.9 s/GB.
@@ -906,6 +908,55 @@ def _checkpoint_full(db_path: Path) -> tuple[int, int, int]:
906
908
  return checkpoint(str(db_path), "FULL")
907
909
 
908
910
 
911
+ def _reclaim_vacuum_wal(
912
+ config: MaintenanceConfig, *, deadline: float | None, phase: str
913
+ ) -> tuple[int, int, int] | None:
914
+ """Try once before releasing custody, preserving the VACUUM outcome.
915
+
916
+ The restore reserve is only checked before starting: ``remaining`` bounds SQLite lock waiting,
917
+ not the checkpoint's WAL-copy I/O once it runs. The replaced FULL checkpoint had the same limit.
918
+ """
919
+ from .maintenance_control import hold_clock
920
+
921
+ started = time.monotonic()
922
+ event = dict(phase=phase, mode="TRUNCATE", wal_before_bytes=get_wal_size(str(config.db_path)))
923
+ result = None
924
+ busy = None
925
+ reason = ""
926
+ try:
927
+ remaining = POST_VACUUM_BUSY_S if deadline is None else deadline - HOLD_UNWIND_S - hold_clock()
928
+ if remaining <= 0:
929
+ reason = "hold-unwind-reserve"
930
+ else:
931
+ result = checkpoint(
932
+ str(config.db_path),
933
+ "TRUNCATE",
934
+ guard_timeout_seconds=0,
935
+ busy_timeout_seconds=min(POST_VACUUM_BUSY_S, remaining),
936
+ )
937
+ busy = result[0]
938
+ if busy:
939
+ reason = "reader-or-writer-busy"
940
+ except CheckpointGuardTimeout:
941
+ reason = "checkpoint-guard-busy"
942
+ except Exception as exc:
943
+ reason = f"{type(exc).__name__}: {exc}"
944
+ event.update(
945
+ wal_after_bytes=get_wal_size(str(config.db_path)),
946
+ duration_ms=(time.monotonic() - started) * 1000,
947
+ busy=busy,
948
+ deferred=bool(reason),
949
+ reason=reason,
950
+ checkpoint=result,
951
+ )
952
+ try:
953
+ _write_log(config.log_path, event)
954
+ except Exception as exc:
955
+ # A failed cleanup receipt must not replace the VACUUM failure or delay restoration.
956
+ print(f"maintenance WAL reclaim: {json.dumps(event)}; log failed: {exc}", file=sys.stderr)
957
+ return result
958
+
959
+
909
960
  def _file_size(path: Path) -> int:
910
961
  try:
911
962
  return path.stat().st_size
@@ -986,6 +1037,7 @@ def _vacuum(db_path: Path, *, deadline: float | None = None) -> tuple[int, int]:
986
1037
  before = db_path.stat().st_size
987
1038
  conn = sqlite3.connect(db_path)
988
1039
  try:
1040
+ configure_writer_wal_policy(conn)
989
1041
  if cutoff is not None:
990
1042
  # Polled on the VACUUM's own thread: no timer race, and an expiry before the statement
991
1043
  # starts still interrupts it (sqlite3_interrupt with no running statement is a no-op).
@@ -1345,10 +1397,17 @@ def run_maintenance(mode: str, *, config: MaintenanceConfig | None = None, dry_r
1345
1397
  if mode == "full" and backup is not None:
1346
1398
  result.actions.append(f"verified_local_backup={backup.get('snapshot')}")
1347
1399
  hold = booted_out.get("_supervisor_hold")
1348
- result.vacuum_before_bytes, result.vacuum_after_bytes = _vacuum(
1349
- config.db_path, deadline=getattr(hold, "deadline", None)
1350
- )
1351
- result.checkpoint = _checkpoint_full(config.db_path)
1400
+ deadline = getattr(hold, "deadline", None)
1401
+ # _vacuum closes its connection before returning or raising. Never reclaim
1402
+ # inside its transaction, and never renew custody just to reclaim disk space.
1403
+ # SystemExit/KeyboardInterrupt (SIGTERM) skip the reclaim and unwind straight
1404
+ # to service restore: a full-size TRUNCATE there would delay it by seconds.
1405
+ try:
1406
+ result.vacuum_before_bytes, result.vacuum_after_bytes = _vacuum(config.db_path, deadline=deadline)
1407
+ except Exception:
1408
+ result.checkpoint = _reclaim_vacuum_wal(config, deadline=deadline, phase="vacuum-failure")
1409
+ raise
1410
+ result.checkpoint = _reclaim_vacuum_wal(config, deadline=deadline, phase="post-vacuum")
1352
1411
  if mode == "burn":
1353
1412
  result.burn = _run_burn_until_empty(config)
1354
1413
  if result.burn.failed_files: