brainlayer 1.5.43__tar.gz → 1.5.45__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 (237) hide show
  1. {brainlayer-1.5.43 → brainlayer-1.5.45}/.gitignore +2 -0
  2. {brainlayer-1.5.43 → brainlayer-1.5.45}/PKG-INFO +10 -1
  3. {brainlayer-1.5.43 → brainlayer-1.5.45}/README.md +6 -0
  4. {brainlayer-1.5.43 → brainlayer-1.5.45}/pyproject.toml +5 -1
  5. {brainlayer-1.5.43 → brainlayer-1.5.45}/scripts/launchd/install.sh +1 -1
  6. {brainlayer-1.5.43 → brainlayer-1.5.45}/server.json +2 -2
  7. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/__init__.py +1 -1
  8. brainlayer-1.5.45/src/brainlayer/_build.py +1 -0
  9. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/backup_daily.py +68 -62
  10. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/brainbar_hybrid_helper.py +14 -2
  11. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/cli/__init__.py +41 -8
  12. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/content_class.py +9 -7
  13. brainlayer-1.5.45/src/brainlayer/drive_credentials.py +313 -0
  14. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/eval/abcde_variants.yaml +26 -26
  15. brainlayer-1.5.45/src/brainlayer/eval/benchmark.py +406 -0
  16. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/eval/phoenix_gate/phoenix_client.py +3 -2
  17. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/health_check.py +6 -0
  18. brainlayer-1.5.45/src/brainlayer/job_alerts.py +91 -0
  19. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/jsonl_backup.py +40 -1
  20. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/kg_promotion.py +29 -3
  21. brainlayer-1.5.45/src/brainlayer/lexical_defense_dictionary.json +321 -0
  22. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/maintenance.py +272 -6
  23. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/mcp/__init__.py +1 -1
  24. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/mcp/search_handler.py +2 -2
  25. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/observability_backup.py +9 -0
  26. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/batch_extraction.py +21 -13
  27. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/enrichment.py +2 -2
  28. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/entity_extraction.py +5 -5
  29. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/kg_extraction_groq.py +2 -2
  30. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/tag_entity_promotion.py +8 -12
  31. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/runtime_store.py +21 -0
  32. brainlayer-1.5.43/src/brainlayer/_build.py +0 -1
  33. brainlayer-1.5.43/src/brainlayer/eval/benchmark.py +0 -469
  34. brainlayer-1.5.43/src/brainlayer/lexical_defense_dictionary.json +0 -246
  35. {brainlayer-1.5.43 → brainlayer-1.5.45}/LICENSE +0 -0
  36. {brainlayer-1.5.43 → brainlayer-1.5.45}/scripts/hotlane_brainbar_daemon.py +0 -0
  37. {brainlayer-1.5.43 → brainlayer-1.5.45}/scripts/launchd/backup-daily.sh +0 -0
  38. {brainlayer-1.5.43 → brainlayer-1.5.45}/scripts/launchd/brainlayer-env-run.sh +0 -0
  39. {brainlayer-1.5.43 → brainlayer-1.5.45}/scripts/launchd/brainlayer.env.example +0 -0
  40. {brainlayer-1.5.43 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.backup-daily.plist +0 -0
  41. {brainlayer-1.5.43 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.decay.plist +0 -0
  42. {brainlayer-1.5.43 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.drain.plist +0 -0
  43. {brainlayer-1.5.43 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.enrichment.plist +0 -0
  44. {brainlayer-1.5.43 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.health-check.plist +0 -0
  45. {brainlayer-1.5.43 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.hotlane-brainbar.plist +0 -0
  46. {brainlayer-1.5.43 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.index.plist +0 -0
  47. {brainlayer-1.5.43 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.jsonl-backup.plist +0 -0
  48. {brainlayer-1.5.43 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.log-cap.plist +0 -0
  49. {brainlayer-1.5.43 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.maintenance-nightly.plist +0 -0
  50. {brainlayer-1.5.43 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.maintenance-weekly.plist +0 -0
  51. {brainlayer-1.5.43 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.observability.plist +0 -0
  52. {brainlayer-1.5.43 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.p0-counter.plist +0 -0
  53. {brainlayer-1.5.43 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.repair-fts.plist +0 -0
  54. {brainlayer-1.5.43 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.t3-ingest.plist +0 -0
  55. {brainlayer-1.5.43 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.throughput-watchdog.plist +0 -0
  56. {brainlayer-1.5.43 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.tier0-watchdog.plist +0 -0
  57. {brainlayer-1.5.43 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.wal-checkpoint.plist +0 -0
  58. {brainlayer-1.5.43 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.watch.plist +0 -0
  59. {brainlayer-1.5.43 → brainlayer-1.5.45}/scripts/launchd/com.etanhey.brainlayer-fleet-watchdog.plist +0 -0
  60. {brainlayer-1.5.43 → brainlayer-1.5.45}/scripts/launchd/fleet-watchdog.sh +0 -0
  61. {brainlayer-1.5.43 → brainlayer-1.5.45}/scripts/launchd/jsonl-backup.sh +0 -0
  62. {brainlayer-1.5.43 → brainlayer-1.5.45}/scripts/launchd/throughput-watchdog.py +0 -0
  63. {brainlayer-1.5.43 → brainlayer-1.5.45}/scripts/release-verify-signatures.sh +0 -0
  64. {brainlayer-1.5.43 → brainlayer-1.5.45}/scripts/tier0-watchdog.sh +0 -0
  65. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/__main__.py +0 -0
  66. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/_helpers.py +0 -0
  67. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/agent_profiles.py +0 -0
  68. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/agent_provenance.py +0 -0
  69. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/alarm.py +0 -0
  70. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/archive_collapse.py +0 -0
  71. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/archive_lifecycle.py +0 -0
  72. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/backup_retention_invariant.py +0 -0
  73. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/badge_state.py +0 -0
  74. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/bitemporal.py +0 -0
  75. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/cachedelete_purge.swift +0 -0
  76. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/calibrate.py +0 -0
  77. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/chunk_origin.py +0 -0
  78. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/chunk_origin_backfill.py +0 -0
  79. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/chunk_origin_wipe.py +0 -0
  80. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/chunk_write.py +0 -0
  81. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/classify.py +0 -0
  82. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/claude_paths.py +0 -0
  83. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/cli/wizard.py +0 -0
  84. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/cli_new.py +0 -0
  85. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/cloud_backfill.py +0 -0
  86. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/clustering.py +0 -0
  87. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/config.py +0 -0
  88. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/correction_judge.py +0 -0
  89. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/data/agents_registry.yaml +0 -0
  90. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/data/sandbox_seeds/skill-eval-baseline.json +0 -0
  91. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/db_shrink.py +0 -0
  92. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/decay.py +0 -0
  93. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/decay_backfill.py +0 -0
  94. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/decay_job.py +0 -0
  95. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/dedupe.py +0 -0
  96. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/dedupe_merge.py +0 -0
  97. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/deploy_drift.py +0 -0
  98. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/doctor.py +0 -0
  99. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/drain.py +0 -0
  100. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/drain_liveness.py +0 -0
  101. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/embeddings.py +0 -0
  102. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/engine.py +0 -0
  103. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/enrichment_controller.py +0 -0
  104. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/eval/__init__.py +0 -0
  105. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/eval/abcde_enrich_runner.py +0 -0
  106. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/eval/abcde_variants.py +0 -0
  107. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/eval/enrichment_gold.py +0 -0
  108. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/eval/enrichment_graders.py +0 -0
  109. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/eval/enrichment_judge.py +0 -0
  110. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/eval/enrichment_llm_judge.py +0 -0
  111. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/eval/enrichment_quality_benchmark.py +0 -0
  112. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/eval/experiment_store.py +0 -0
  113. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/eval/fixtures/README.md +0 -0
  114. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/eval/labeling/enrichment-labeler.html +0 -0
  115. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/eval/phoenix_gate/__init__.py +0 -0
  116. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/eval/phoenix_gate/__main__.py +0 -0
  117. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/eval/phoenix_gate/baseline_store.py +0 -0
  118. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/eval/phoenix_gate/cli.py +0 -0
  119. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/eval/phoenix_gate/models.py +0 -0
  120. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/eval/phoenix_gate/regression_gate.py +0 -0
  121. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/eval/phoenix_gate/triggers.py +0 -0
  122. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/fallback_replay.py +0 -0
  123. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/git_learning.py +0 -0
  124. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/hook_python.py +0 -0
  125. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/hooks/__init__.py +0 -0
  126. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/hooks/indexer.py +0 -0
  127. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/import_sweep.py +0 -0
  128. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/index_completeness.py +0 -0
  129. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/index_new.py +0 -0
  130. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/index_watchdog.py +0 -0
  131. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/ingest/__init__.py +0 -0
  132. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/ingest/codex.py +0 -0
  133. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/ingest/t3.py +0 -0
  134. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/ingest_denylist.py +0 -0
  135. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/ingest_guard.py +0 -0
  136. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/isolation_proof.py +0 -0
  137. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/job_lifecycle_health.py +0 -0
  138. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/jobs.py +0 -0
  139. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/kg/__init__.py +0 -0
  140. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/kg_cleanup.py +0 -0
  141. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/kg_judge.py +0 -0
  142. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/kg_repo.py +0 -0
  143. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/kg_review_session.py +0 -0
  144. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/kg_session_finish.py +0 -0
  145. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/kg_session_harvest.py +0 -0
  146. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/launchd_primitive.py +0 -0
  147. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/lexical_defense.py +0 -0
  148. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/log_cap.py +0 -0
  149. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/mcp/_format.py +0 -0
  150. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/mcp/_shared.py +0 -0
  151. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/mcp/enrich_handler.py +0 -0
  152. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/mcp/entity_handler.py +0 -0
  153. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/mcp/palette.py +0 -0
  154. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/mcp/store_handler.py +0 -0
  155. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/mcp/tags_handler.py +0 -0
  156. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/mcp_socket_config.py +0 -0
  157. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/mcp_stdio_bridge.py +0 -0
  158. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/memory_types.py +0 -0
  159. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/migrate.py +0 -0
  160. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/notification_policy.py +0 -0
  161. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/observability_surface.py +0 -0
  162. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/p0_longitudinal_count.py +0 -0
  163. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/parent_death.py +0 -0
  164. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/paths.py +0 -0
  165. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pause.py +0 -0
  166. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/phonetic.py +0 -0
  167. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/__init__.py +0 -0
  168. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/agent_enrichment.py +0 -0
  169. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/analyze_communication.py +0 -0
  170. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/brain_graph.py +0 -0
  171. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/chat_tags.py +0 -0
  172. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/chunk.py +0 -0
  173. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/classify.py +0 -0
  174. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/cloud_scrub.py +0 -0
  175. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/cluster_sampling.py +0 -0
  176. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/code_intelligence.py +0 -0
  177. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/correction_detection.py +0 -0
  178. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/correction_mining.py +0 -0
  179. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/digest.py +0 -0
  180. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/enrichment_tiers.py +0 -0
  181. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/entity_resolution.py +0 -0
  182. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/extract.py +0 -0
  183. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/extract_claude_desktop.py +0 -0
  184. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/extract_corrections.py +0 -0
  185. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/extract_markdown.py +0 -0
  186. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/extract_whatsapp.py +0 -0
  187. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/frustration_mining.py +0 -0
  188. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/git_overlay.py +0 -0
  189. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/groq.py +0 -0
  190. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/kg_extraction.py +0 -0
  191. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/longitudinal_analyzer.py +0 -0
  192. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/obsidian_export.py +0 -0
  193. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/operation_grouping.py +0 -0
  194. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/plan_linking.py +0 -0
  195. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/rate_limiter.py +0 -0
  196. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/relation_backfill.py +0 -0
  197. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/sanitize.py +0 -0
  198. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/secret_scrub.py +0 -0
  199. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/semantic_style.py +0 -0
  200. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/sentiment.py +0 -0
  201. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/session_enrichment.py +0 -0
  202. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/style_embed.py +0 -0
  203. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/style_index.py +0 -0
  204. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/temporal_chains.py +0 -0
  205. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/time_batcher.py +0 -0
  206. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/unified_timeline.py +0 -0
  207. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/pipeline/write_queue.py +0 -0
  208. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/provenance.py +0 -0
  209. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/provenance_autosupersede.py +0 -0
  210. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/provenance_integration.py +0 -0
  211. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/queue_io.py +0 -0
  212. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/queue_merge.py +0 -0
  213. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/reembed_backfill.py +0 -0
  214. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/sandbox_db.py +0 -0
  215. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/scoping.py +0 -0
  216. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/search_fanout.py +0 -0
  217. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/search_profile.py +0 -0
  218. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/search_repo.py +0 -0
  219. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/session_repo.py +0 -0
  220. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/setup.py +0 -0
  221. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/socket_hygiene.py +0 -0
  222. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/source_class_migration.py +0 -0
  223. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/spotlight.py +0 -0
  224. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/storage.py +0 -0
  225. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/store.py +0 -0
  226. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/system_prompt_guard.py +0 -0
  227. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/t3_provenance.py +0 -0
  228. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/tag_normalization.py +0 -0
  229. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/taxonomy.json +0 -0
  230. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/telemetry.py +0 -0
  231. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/timestamp_iso.py +0 -0
  232. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/types.py +0 -0
  233. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/vector_store.py +0 -0
  234. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/wal_checkpoint.py +0 -0
  235. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/watcher.py +0 -0
  236. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/watcher_bridge.py +0 -0
  237. {brainlayer-1.5.43 → brainlayer-1.5.45}/src/brainlayer/writer_telemetry.py +0 -0
@@ -86,6 +86,8 @@ eval_results/*.jsonl
86
86
 
87
87
  # Generated local eval/graph outputs
88
88
  eval_results/
89
+ results/
90
+ artifacts/
89
91
  graphify-out/
90
92
  *.bak-*
91
93
  *.json.lock
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: brainlayer
3
- Version: 1.5.43
3
+ Version: 1.5.45
4
4
  Summary: Persistent memory MCP server for AI agents — 17 tools, semantic search, knowledge graph, on-device SQLite
5
5
  Project-URL: Homepage, https://brainlayer.etanheyman.com
6
6
  Project-URL: Repository, https://github.com/EtanHey/brainlayer
@@ -23,6 +23,7 @@ Requires-Dist: abydos>=0.5.0
23
23
  Requires-Dist: apsw>=3.45.0
24
24
  Requires-Dist: deprecation>=2.1.0
25
25
  Requires-Dist: google-api-python-client>=2.0.0
26
+ Requires-Dist: google-auth-oauthlib>=1.1.0
26
27
  Requires-Dist: google-auth>=2.0.0
27
28
  Requires-Dist: idna<4,>=2.5
28
29
  Requires-Dist: jsonschema>=4.20.0
@@ -53,10 +54,12 @@ Requires-Dist: deepchecks>=0.19.1; extra == 'dev'
53
54
  Requires-Dist: google-genai>=1.0.0; extra == 'dev'
54
55
  Requires-Dist: numpy<2; extra == 'dev'
55
56
  Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
57
+ Requires-Dist: pytest-timeout>=2.3.1; extra == 'dev'
56
58
  Requires-Dist: pytest>=7.0.0; extra == 'dev'
57
59
  Requires-Dist: ruff>=0.1.0; extra == 'dev'
58
60
  Requires-Dist: scikit-learn<1.6; extra == 'dev'
59
61
  Requires-Dist: setuptools<81,>=70.0.0; extra == 'dev'
62
+ Requires-Dist: threadpoolctl>=3.0.0; extra == 'dev'
60
63
  Requires-Dist: tiktoken>=0.7.0; extra == 'dev'
61
64
  Requires-Dist: umap-learn>=0.5.0; extra == 'dev'
62
65
  Provides-Extra: docs
@@ -368,6 +371,12 @@ BRAINLAYER_ENRICH_BACKEND=groq brainlayer enrich # Force Groq
368
371
 
369
372
  ## CLI Reference
370
373
 
374
+ ### Backups
375
+
376
+ Run `brainlayer backup auth` once to authorize BrainLayer's Drive backups in your browser.
377
+ The private token lives at `~/.config/brainlayer/drive-tokens.json`; `BRAINLAYER_DRIVE_TOKEN_PATH` overrides it.
378
+ BrainBar uses `brainlayer backup auth --status --json` to check and `brainlayer backup auth --json` to authorize.
379
+
371
380
  ```bash
372
381
  brainlayer setup # Create ~/.config/brainlayer/brainlayer.env
373
382
  brainlayer setup --launchd # Create config and install launchd agents
@@ -293,6 +293,12 @@ BRAINLAYER_ENRICH_BACKEND=groq brainlayer enrich # Force Groq
293
293
 
294
294
  ## CLI Reference
295
295
 
296
+ ### Backups
297
+
298
+ Run `brainlayer backup auth` once to authorize BrainLayer's Drive backups in your browser.
299
+ The private token lives at `~/.config/brainlayer/drive-tokens.json`; `BRAINLAYER_DRIVE_TOKEN_PATH` overrides it.
300
+ BrainBar uses `brainlayer backup auth --status --json` to check and `brainlayer backup auth --json` to authorize.
301
+
296
302
  ```bash
297
303
  brainlayer setup # Create ~/.config/brainlayer/brainlayer.env
298
304
  brainlayer setup --launchd # Create config and install launchd agents
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "brainlayer"
3
- version = "1.5.43"
3
+ version = "1.5.45"
4
4
  description = "Persistent memory MCP server for AI agents — 17 tools, semantic search, knowledge graph, on-device SQLite"
5
5
  license = {text = "Apache-2.0"}
6
6
  readme = "README.md"
@@ -34,6 +34,7 @@ dependencies = [
34
34
  "idna>=2.5,<4", # Imported through requests by backup and hook entrypoints
35
35
  "google-api-python-client>=2.0.0", # Daily DB backup upload to Google Drive
36
36
  "google-auth>=2.0.0", # OAuth refresh for Google Drive backups
37
+ "google-auth-oauthlib>=1.1.0", # Installed-app Drive consent for BrainLayer-owned backup tokens
37
38
  "ranx>=0.3.20", # IR evaluation metrics + significance testing
38
39
  "abydos>=0.5.0", # Beider-Morse phonetic matching for cross-script entity aliases
39
40
  "deprecation>=2.1.0", # Imported by abydos phonetic modules used by prompt hooks
@@ -82,10 +83,12 @@ dev = [
82
83
  "numpy<2",
83
84
  "pytest>=7.0.0",
84
85
  "pytest-asyncio>=0.21.0",
86
+ "pytest-timeout>=2.3.1",
85
87
  "ruff>=0.1.0",
86
88
  "scikit-learn<1.6",
87
89
  "setuptools>=70.0.0,<81",
88
90
  "tiktoken>=0.7.0",
91
+ "threadpoolctl>=3.0.0",
89
92
  "umap-learn>=0.5.0",
90
93
  ]
91
94
  docs = [
@@ -121,6 +124,7 @@ pythonpath = ["src"]
121
124
  markers = [
122
125
  "engine: pure-library engine tests (excludes CLI, dashboard, BrainBar, launchd, and root orchestration surfaces)",
123
126
  "embedding_model: tests that deliberately load a real embedding model (deselected by scripts/run_tests.sh)",
127
+ "timeout: maximum duration for an individual test",
124
128
  "integration: tests that need the real production DB (268K+ chunks)",
125
129
  "live: tests that need the live production DB or mutable local hooks state",
126
130
  "slow: tests that load ML models or take >30s",
@@ -11,7 +11,7 @@
11
11
  # ./scripts/launchd/install.sh load enrichment
12
12
  # ./scripts/launchd/install.sh unload enrichment
13
13
  # ./scripts/launchd/install.sh checkpoint # Install WAL checkpoint only
14
- # ./scripts/launchd/install.sh repair-fts # Install weekly explicit FTS repair
14
+ # ./scripts/launchd/install.sh repair-fts # Install weekly read-only FTS check
15
15
  # ./scripts/launchd/install.sh backup # Install daily DB backup only
16
16
  # ./scripts/launchd/install.sh jsonl-backup # Install daily JSONL backup only
17
17
  # ./scripts/launchd/install.sh maintenance # Install recurring maintenance jobs
@@ -3,7 +3,7 @@
3
3
  "name": "io.github.etanhey/brainlayer",
4
4
  "title": "BrainLayer",
5
5
  "description": "Persistent memory for AI agents \u2014 local-first semantic search, knowledge graph, and 17 MCP tools with ToolAnnotations. One SQLite file, no cloud, no API keys.",
6
- "version": "1.5.43",
6
+ "version": "1.5.45",
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.43",
17
+ "version": "1.5.45",
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.43"
5
+ __version__ = "1.5.45"
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 = "d3d00b64768a53e7c534ee82dc9f9938aab65f81"
@@ -36,13 +36,14 @@ from typing import Any
36
36
 
37
37
  import requests
38
38
 
39
+ from . import drive_credentials
39
40
  from .paths import get_db_path
40
41
  from .socket_hygiene import refuse_production_brainbar_socket
41
42
 
42
43
  _sleep = time.sleep
43
44
 
44
- DEFAULT_TOKEN_PATH = Path.home() / ".config" / "google-drive-mcp" / "tokens.json"
45
- DEFAULT_CLIENT_PATH = Path.home() / ".config" / "google-drive-mcp" / "gcp-oauth.keys.json"
45
+ DEFAULT_TOKEN_PATH = drive_credentials.DEFAULT_TOKEN_PATH
46
+ DEFAULT_CLIENT_PATH = drive_credentials.DEFAULT_CLIENT_PATH
46
47
  CANONICAL_MACHINE_ID = "MacBook-Pro"
47
48
  CANONICAL_FOLDER_PARTS = ["Brain Drive", "06_ARCHIVE", "backups", "brainlayer-db"]
48
49
  # Compatibility alias for callers that explicitly target the legacy M4 folder.
@@ -61,6 +62,7 @@ BACKUP_ATTEMPT_MAX_AGE_ENV = "BRAINLAYER_BACKUP_ATTEMPT_MAX_AGE_SECONDS"
61
62
  BACKUP_FULL_VERIFY_ENV = "BRAINLAYER_BACKUP_FULL_VERIFY"
62
63
  BACKUP_LOG_PATH_ENV = "BRAINLAYER_BACKUP_LOG_PATH"
63
64
  BACKUP_LOG_PROVENANCE_ENV = "BRAINLAYER_BACKUP_LOG_PROVENANCE"
65
+ BACKUP_REUSE_VERIFIED_MAX_AGE_HOURS_ENV = "BRAINLAYER_BACKUP_REUSE_VERIFIED_MAX_AGE_HOURS"
64
66
  BACKUP_SUPERVISED_CHILD_ENV = "BRAINLAYER_BACKUP_SUPERVISED_CHILD"
65
67
  BACKUP_SQLITE_CHECK_TIMEOUT_ENV = "BRAINLAYER_BACKUP_SQLITE_CHECK_TIMEOUT_SECONDS"
66
68
  BACKUP_DRIVE_RETENTION_ENV = "BRAINLAYER_BACKUP_DRIVE_RETENTION"
@@ -68,7 +70,7 @@ DRIVE_UPLOAD_DEADLINE_FLOOR_ENV = "BRAINLAYER_DRIVE_UPLOAD_DEADLINE_FLOOR_SECOND
68
70
  DRIVE_UPLOAD_MIN_BYTES_PER_SECOND_ENV = "BRAINLAYER_DRIVE_UPLOAD_MIN_BYTES_PER_SECOND"
69
71
  DRIVE_UPLOAD_STALL_MAX_ATTEMPTS_ENV = "BRAINLAYER_DRIVE_UPLOAD_STALL_MAX_ATTEMPTS"
70
72
  DRIVE_FOLDER_MIME = "application/vnd.google-apps.folder"
71
- DRIVE_SCOPES = ["https://www.googleapis.com/auth/drive"]
73
+ DRIVE_SCOPES = drive_credentials.DRIVE_SCOPES
72
74
  DEFAULT_LOCAL_COMPRESSED_KEEP = 3
73
75
  DEFAULT_LOCAL_UNCOMPRESSED_KEEP = 1
74
76
  DEFAULT_DRIVE_KEEP = 7
@@ -101,6 +103,10 @@ DAILY_RETENTION = DriveRetentionPolicy(keep_latest=DEFAULT_DRIVE_KEEP)
101
103
  WEEKLY_RETENTION = DriveRetentionPolicy(keep_latest=DEFAULT_DRIVE_KEEP)
102
104
 
103
105
 
106
+ class BackupAlreadyRunningError(RuntimeError):
107
+ """The shared backup staging lock is held by another backup."""
108
+
109
+
104
110
  @dataclass(frozen=True)
105
111
  class SQLiteBackupArtifact:
106
112
  gzip_path: Path
@@ -981,65 +987,12 @@ def _send_brainbar_json_request(
981
987
  return json.loads(data.decode("utf-8"))
982
988
 
983
989
 
984
- def _atomic_write_text(path: Path, content: str) -> None:
985
- temp_path = path.with_name(f".{path.name}.{os.getpid()}.tmp")
986
- try:
987
- temp_path.write_text(content)
988
- os.replace(temp_path, path)
989
- finally:
990
- temp_path.unlink(missing_ok=True)
991
-
992
-
993
- def get_drive_credentials(token_path: Path = DEFAULT_TOKEN_PATH, client_path: Path = DEFAULT_CLIENT_PATH):
994
- """Load and refresh Google Drive OAuth credentials from the existing MCP auth files."""
995
- from google.auth.transport.requests import Request
996
- from google.oauth2.credentials import Credentials
997
-
998
- token_path = Path(token_path).expanduser()
999
- client_path = Path(client_path).expanduser()
1000
- if not token_path.exists():
1001
- raise FileNotFoundError(f"Google Drive token file not found: {token_path}")
1002
- if not client_path.exists():
1003
- raise FileNotFoundError(f"Google OAuth client file not found: {client_path}")
1004
-
1005
- lock_path = token_path.with_suffix(token_path.suffix + ".lock")
1006
- with lock_path.open("w") as lock_file:
1007
- fcntl.flock(lock_file, fcntl.LOCK_EX)
1008
- token_data = json.loads(token_path.read_text())
1009
- client_data = json.loads(client_path.read_text())["installed"]
1010
-
1011
- expiry = token_data.get("expiry")
1012
- if not expiry and token_data.get("expiry_date"):
1013
- expiry = dt.datetime.fromtimestamp(int(token_data["expiry_date"]) / 1000, tz=dt.UTC).isoformat()
1014
-
1015
- parsed_expiry = dt.datetime.fromisoformat(expiry.replace("Z", "+00:00")) if expiry else None
1016
- if parsed_expiry and parsed_expiry.tzinfo:
1017
- parsed_expiry = parsed_expiry.astimezone(dt.UTC).replace(tzinfo=None)
1018
- elif parsed_expiry:
1019
- parsed_expiry = parsed_expiry.replace(tzinfo=None)
1020
-
1021
- creds = Credentials(
1022
- token=token_data.get("access_token"),
1023
- refresh_token=token_data.get("refresh_token"),
1024
- token_uri=client_data["token_uri"],
1025
- client_id=client_data["client_id"],
1026
- client_secret=client_data["client_secret"],
1027
- scopes=token_data.get("scope", " ".join(DRIVE_SCOPES)).split(),
1028
- expiry=parsed_expiry,
1029
- )
1030
-
1031
- # google-auth Credentials.expired compares against a naive UTC helper, so keep expiry comparisons naive UTC.
1032
- refresh_before = dt.datetime.now(dt.UTC).replace(tzinfo=None) + dt.timedelta(hours=2)
1033
- if creds.expired or not creds.valid or (creds.expiry and creds.expiry < refresh_before):
1034
- creds.refresh(Request())
1035
- token_data["access_token"] = creds.token
1036
- token_data["expiry"] = creds.expiry.isoformat() if creds.expiry else None
1037
- _atomic_write_text(token_path, json.dumps(token_data, indent=2, sort_keys=True) + "\n")
1038
-
1039
- return creds
990
+ def get_drive_credentials(token_path: Path | None = None, client_path: Path | None = None):
991
+ """Use BrainLayer-owned credentials for both database and JSONL backups."""
992
+ return drive_credentials.load_credentials(token_path=token_path, client_path=client_path)
1040
993
 
1041
994
 
1042
- def build_drive_service(token_path: Path = DEFAULT_TOKEN_PATH, client_path: Path = DEFAULT_CLIENT_PATH):
995
+ def build_drive_service(token_path: Path | None = None, client_path: Path | None = None):
1043
996
  from googleapiclient.discovery import build
1044
997
 
1045
998
  return build("drive", "v3", credentials=get_drive_credentials(token_path, client_path))
@@ -1568,6 +1521,37 @@ def prune_drive_backups(
1568
1521
  return trashed
1569
1522
 
1570
1523
 
1524
+ def _recent_verified_backup_for_reuse(
1525
+ log_path: Path, db_path: Path, *, now: dt.datetime, max_age_hours: float
1526
+ ) -> dict[str, Any] | None:
1527
+ try:
1528
+ lines = log_path.read_text(encoding="utf-8").splitlines()
1529
+ except FileNotFoundError:
1530
+ return None
1531
+ now = now.astimezone(dt.UTC)
1532
+ for line in reversed(lines):
1533
+ try:
1534
+ receipt = json.loads(line)
1535
+ attempted = dt.datetime.fromisoformat(receipt["attempted_at"])
1536
+ except (ValueError, KeyError, TypeError):
1537
+ continue
1538
+ if attempted.tzinfo is None:
1539
+ continue
1540
+ age = now - attempted.astimezone(dt.UTC)
1541
+ drive_file = receipt.get("drive_file")
1542
+ if (
1543
+ receipt.get("db") == str(db_path)
1544
+ and receipt.get("uploaded") is True
1545
+ and receipt.get("verified") is True
1546
+ and "error_type" not in receipt
1547
+ and isinstance(drive_file, dict)
1548
+ and isinstance(drive_file.get("id"), str)
1549
+ and dt.timedelta(0) <= age <= dt.timedelta(hours=max_age_hours)
1550
+ ):
1551
+ return receipt
1552
+ return None
1553
+
1554
+
1571
1555
  def _serialized_backup_run(func: Callable[..., dict[str, Any]]) -> Callable[..., dict[str, Any]]:
1572
1556
  @functools.wraps(func)
1573
1557
  def wrapped(*args: Any, **kwargs: Any) -> dict[str, Any]:
@@ -1592,8 +1576,22 @@ def _serialized_backup_run(func: Callable[..., dict[str, Any]]) -> Callable[...,
1592
1576
  "error": error,
1593
1577
  },
1594
1578
  )
1595
- raise RuntimeError(error) from exc
1579
+ raise BackupAlreadyRunningError(error) from exc
1596
1580
  try:
1581
+ # Weekly maintenance's first receipt check precedes this lock. A daily
1582
+ # backup can finish in that gap, so recheck before copying the DB.
1583
+ reuse_age = os.environ.get(BACKUP_REUSE_VERIFIED_MAX_AGE_HOURS_ENV)
1584
+ if reuse_age is not None:
1585
+ hours = float(reuse_age)
1586
+ if not math.isfinite(hours) or hours < 0:
1587
+ raise ValueError(f"{BACKUP_REUSE_VERIFIED_MAX_AGE_HOURS_ENV} must be finite and nonnegative")
1588
+ db_path = Path(bound.arguments.get("db_path") or get_db_path())
1589
+ log_path = _backup_log_path(bound.arguments.get("log_path"), db_path=db_path)
1590
+ receipt = _recent_verified_backup_for_reuse(
1591
+ log_path, db_path, now=dt.datetime.now(dt.UTC), max_age_hours=hours
1592
+ )
1593
+ if receipt is not None:
1594
+ return receipt
1597
1595
  return func(*args, **kwargs)
1598
1596
  finally:
1599
1597
  fcntl.flock(lock, fcntl.LOCK_UN)
@@ -1904,7 +1902,15 @@ def main() -> int:
1904
1902
  timeout_seconds = _configured_backup_timeout_seconds()
1905
1903
  if _env_flag_enabled(BACKUP_SUPERVISED_CHILD_ENV):
1906
1904
  return _run_backup_process(timeout_seconds)
1907
- return _supervise_backup_process(timeout_seconds)
1905
+ from .job_alerts import report
1906
+
1907
+ try:
1908
+ code = _supervise_backup_process(timeout_seconds)
1909
+ except Exception:
1910
+ report("backup-daily", "Database backup failed; check the backup log")
1911
+ raise
1912
+ report("backup-daily", None if code == 0 else "Database backup failed; check the backup log")
1913
+ return code
1908
1914
 
1909
1915
 
1910
1916
  if __name__ == "__main__":
@@ -13,6 +13,7 @@ import hashlib
13
13
  import json
14
14
  import logging
15
15
  import os
16
+ import re
16
17
  import signal
17
18
  import socket
18
19
  import sys
@@ -23,6 +24,9 @@ from typing import Any, Callable
23
24
 
24
25
  from . import search_profile
25
26
 
27
+ # A profile id is only ever the shape search_profile.new_query_id() (and BrainBar's
28
+ # SearchProfileLogger.newQueryID()) generates; anything else is client-chosen text.
29
+ _PROFILE_QUERY_ID_RE = re.compile(r"q-[0-9A-Fa-f]{12}")
26
30
  _ACCEPT_TIMEOUT_SECONDS = 0.25
27
31
  _CONNECTION_TIMEOUT_SECONDS = 5.0
28
32
 
@@ -217,7 +221,9 @@ class HybridSearchHelper:
217
221
  request = json.loads(raw.decode("utf-8"))
218
222
  response = self._dispatch_with_deadline(request)
219
223
  except Exception as exc:
220
- response = {"ok": False, "error": str(exc)}
224
+ # The class name only: BrainBar logs this field, and an exception message
225
+ # can carry the query or file contents (PR #993 review, B3).
226
+ response = {"ok": False, "error": type(exc).__name__}
221
227
 
222
228
  payload = json.dumps(_json_safe(response), separators=(",", ":")).encode("utf-8") + b"\n"
223
229
  try:
@@ -225,6 +231,12 @@ class HybridSearchHelper:
225
231
  except OSError:
226
232
  return
227
233
 
234
+ @staticmethod
235
+ def _accepted_profile_query_id(raw: object) -> str | None:
236
+ if isinstance(raw, str) and _PROFILE_QUERY_ID_RE.fullmatch(raw):
237
+ return raw
238
+ return None
239
+
228
240
  @staticmethod
229
241
  def _read_line(conn: socket.socket) -> bytes:
230
242
  chunks: list[bytes] = []
@@ -269,7 +281,7 @@ class HybridSearchHelper:
269
281
  async def _search(self, arguments: dict[str, Any]) -> tuple[str, dict[str, Any] | None, bool]:
270
282
  from brainlayer.mcp.search_handler import _brain_search
271
283
 
272
- query_id = str(arguments.get("_profile_query_id") or "") or None
284
+ query_id = self._accepted_profile_query_id(arguments.get("_profile_query_id"))
273
285
  if search_profile.enabled() and query_id is None:
274
286
  query_id = search_profile.new_query_id()
275
287
  source = arguments.get("source")
@@ -50,6 +50,25 @@ sandbox_app = typer.Typer(help="Manage isolated sandbox BrainLayer databases")
50
50
  app.add_typer(sandbox_app, name="sandbox")
51
51
  jobs_app = typer.Typer(help="Inspect and restart installed BrainLayer LaunchAgents")
52
52
  app.add_typer(jobs_app, name="jobs")
53
+ backup_app = typer.Typer(help="Manage BrainLayer backup authorization")
54
+ app.add_typer(backup_app, name="backup")
55
+
56
+
57
+ @backup_app.command("auth")
58
+ def backup_auth_command(
59
+ status: bool = typer.Option(False, "--status", help="Check authorization without changing the token."),
60
+ json_output: bool = typer.Option(False, "--json", help="Print one JSON result for BrainBar."),
61
+ ) -> None:
62
+ """Authorize the BrainLayer-owned Google Drive token in the system browser."""
63
+ from .. import drive_credentials
64
+
65
+ result = drive_credentials.credential_status() if status else drive_credentials.authorize()
66
+ if json_output:
67
+ typer.echo(json.dumps(result, sort_keys=True))
68
+ else:
69
+ typer.echo(result["reason"])
70
+ if not status and result["status"] != "ok":
71
+ raise typer.Exit(1)
53
72
 
54
73
 
55
74
  @jobs_app.command("restart")
@@ -2804,6 +2823,7 @@ def wal_checkpoint(
2804
2823
  quiet: bool = typer.Option(False, "--quiet", help="Silent unless error"),
2805
2824
  ) -> None:
2806
2825
  """Run a SQLite WAL checkpoint through the BrainLayer CLI."""
2826
+ from ..job_alerts import report as report_job_alert
2807
2827
  from ..wal_checkpoint import run_wal_checkpoint
2808
2828
 
2809
2829
  normalized_mode = mode.upper()
@@ -2815,12 +2835,14 @@ def wal_checkpoint(
2815
2835
  run_wal_checkpoint(normalized_mode, retry_busy=True) if retry_busy else run_wal_checkpoint(normalized_mode)
2816
2836
  )
2817
2837
  except FileNotFoundError as e:
2838
+ report_job_alert("wal-checkpoint", "BrainLayer WAL checkpoint failed; check the job log")
2818
2839
  if json_output:
2819
2840
  console.print_json(data={"error": str(e)})
2820
2841
  elif not quiet:
2821
2842
  rprint(f"[bold red]Error:[/] {e}")
2822
2843
  raise typer.Exit(1)
2823
2844
  except Exception as e:
2845
+ report_job_alert("wal-checkpoint", "BrainLayer WAL checkpoint failed; check the job log")
2824
2846
  if json_output:
2825
2847
  console.print_json(data={"error": str(e)})
2826
2848
  elif not quiet:
@@ -2838,25 +2860,36 @@ def wal_checkpoint(
2838
2860
  )
2839
2861
 
2840
2862
  if result["busy"]:
2863
+ report_job_alert("wal-checkpoint", "BrainLayer WAL checkpoint was busy; check the job log")
2841
2864
  raise typer.Exit(1)
2865
+ report_job_alert("wal-checkpoint", None)
2842
2866
 
2843
2867
 
2844
2868
  @app.command("repair-fts")
2845
2869
  def repair_fts(
2846
- db_path: Path = typer.Argument(..., help="Explicit offline database copy to repair."),
2870
+ db_path: Path | None = typer.Argument(None, help="Offline database copy; omitted means read-only FTS count check."),
2847
2871
  ) -> None:
2848
- """Rebuild FTS maintenance tables on an offline copy only."""
2849
- from ..runtime_store import OfflineMigrator
2872
+ """Check live FTS counts read-only, or rebuild an explicit offline copy."""
2873
+ from ..job_alerts import report as report_job_alert
2874
+ from ..runtime_store import OfflineMigrator, check_fts_counts
2850
2875
 
2851
2876
  try:
2852
- store = OfflineMigrator(db_path)
2853
- try:
2854
- result = store.repair_fts(rebuild_trigram=True)
2855
- finally:
2856
- store.close()
2877
+ if db_path is None:
2878
+ result = check_fts_counts(get_db_path())
2879
+ else:
2880
+ store = OfflineMigrator(db_path)
2881
+ try:
2882
+ result = store.repair_fts(rebuild_trigram=True)
2883
+ finally:
2884
+ store.close()
2857
2885
  except PermissionError as exc:
2886
+ report_job_alert("repair-fts", "BrainLayer FTS repair failed; check the job log")
2858
2887
  rprint(f"[bold red]Repair refused:[/] {exc}")
2859
2888
  raise typer.Exit(1) from exc
2889
+ except Exception:
2890
+ report_job_alert("repair-fts", "BrainLayer FTS repair failed; check the job log")
2891
+ raise
2892
+ report_job_alert("repair-fts", None)
2860
2893
  console.print_json(data=result)
2861
2894
 
2862
2895
 
@@ -68,12 +68,14 @@ _PERSONAL_RE = re.compile(
68
68
  r")",
69
69
  re.IGNORECASE,
70
70
  )
71
- _PERSON_NAME_RE = re.compile(
72
- r"(\betan\b|etan@heyman\.net|heyman\.net|\bYuval Rapoport\b|"
73
- r"\bGal Rava\b|\bSamantha Cerqueira\b|\bDaniel Munk\b|\bOren Efraim\b|"
74
- r"\bOren Ephraim\b|\bAvi Simon\b|\bMichal Cohen\b|\bChase\b)",
75
- re.IGNORECASE,
71
+ _NAME_WORD = r"[A-Z][a-z]{1,30}(?:[-'][A-Z][a-z]{1,30})?"
72
+ _CONTEXTUAL_PERSON_NAME_RE = re.compile(
73
+ rf"\b(?i:met|spoke with|talked to|called|contacted|introduced|client|friend|person named)\s+"
74
+ rf"{_NAME_WORD}\s+{_NAME_WORD}\b"
76
75
  )
76
+ _EMAIL_ADDRESS_RE = re.compile(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b")
77
+
78
+
77
79
  _PERSONAL_RISK_RE = re.compile(
78
80
  r"("
79
81
  r"\betan\b|etan@heyman\.net|heyman\.net|\bjournal\b|\bpersonal\b|\bmy life\b|\bfamily\b|"
@@ -145,8 +147,8 @@ def has_personal_risk_signal(content: str | None) -> bool:
145
147
 
146
148
 
147
149
  def has_person_name_signal(content: str | None) -> bool:
148
- """Return true for known personal-name signals in Etan's corpus."""
149
- return bool(content and _PERSON_NAME_RE.search(content))
150
+ """Recognize a contextual full name or email without bundled identities."""
151
+ return bool(content and (_CONTEXTUAL_PERSON_NAME_RE.search(content) or _EMAIL_ADDRESS_RE.search(content)))
150
152
 
151
153
 
152
154
  def has_hebrew_signal(content: str | None) -> bool: