brainlayer 1.5.87__tar.gz → 1.5.89__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 (275) hide show
  1. {brainlayer-1.5.87 → brainlayer-1.5.89}/PKG-INFO +1 -1
  2. {brainlayer-1.5.87 → brainlayer-1.5.89}/pyproject.toml +1 -1
  3. {brainlayer-1.5.87 → brainlayer-1.5.89}/server.json +2 -2
  4. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/__init__.py +1 -1
  5. brainlayer-1.5.89/src/brainlayer/_build.py +1 -0
  6. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/backup_daily.py +18 -0
  7. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/cli/__init__.py +8 -0
  8. brainlayer-1.5.89/src/brainlayer/job_alerts.py +156 -0
  9. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/launchd_consolidation.py +119 -0
  10. brainlayer-1.5.89/src/brainlayer/launcher_policy_reconcile.py +75 -0
  11. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/mcp_stdio_bridge.py +11 -1
  12. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/wal_checkpoint.py +23 -1
  13. brainlayer-1.5.87/src/brainlayer/_build.py +0 -1
  14. brainlayer-1.5.87/src/brainlayer/job_alerts.py +0 -100
  15. {brainlayer-1.5.87 → brainlayer-1.5.89}/.gitignore +0 -0
  16. {brainlayer-1.5.87 → brainlayer-1.5.89}/LICENSE +0 -0
  17. {brainlayer-1.5.87 → brainlayer-1.5.89}/README.md +0 -0
  18. {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/hotlane_brainbar_daemon.py +0 -0
  19. {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/backup-daily.sh +0 -0
  20. {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/brainlayer-env-run.sh +0 -0
  21. {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/brainlayer.env.example +0 -0
  22. {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/com.brainlayer.backup-daily.plist +0 -0
  23. {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/com.brainlayer.daemon.plist +0 -0
  24. {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/com.brainlayer.decay.plist +0 -0
  25. {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/com.brainlayer.drain.plist +0 -0
  26. {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/com.brainlayer.health-check.plist +0 -0
  27. {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/com.brainlayer.hotlane-brainbar.plist +0 -0
  28. {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/com.brainlayer.index.plist +0 -0
  29. {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/com.brainlayer.jsonl-backup.plist +0 -0
  30. {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/com.brainlayer.log-cap.plist +0 -0
  31. {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/com.brainlayer.maintenance-nightly.plist +0 -0
  32. {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/com.brainlayer.maintenance-weekly.plist +0 -0
  33. {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/com.brainlayer.maintenance.plist +0 -0
  34. {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/com.brainlayer.observability.plist +0 -0
  35. {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/com.brainlayer.p0-counter.plist +0 -0
  36. {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/com.brainlayer.repair-fts.plist +0 -0
  37. {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/com.brainlayer.t3-ingest.plist +0 -0
  38. {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/com.brainlayer.throughput-watchdog.plist +0 -0
  39. {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/com.brainlayer.tier0-watchdog.plist +0 -0
  40. {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/com.brainlayer.wal-checkpoint.plist +0 -0
  41. {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/com.brainlayer.watch.plist +0 -0
  42. {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/com.etanhey.brainlayer-fleet-watchdog.plist +0 -0
  43. {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/fleet-watchdog.sh +0 -0
  44. {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/install.sh +0 -0
  45. {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/jsonl-backup.sh +0 -0
  46. {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/throughput-watchdog.py +0 -0
  47. {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/release-verify-signatures.sh +0 -0
  48. {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/tier0-watchdog.sh +0 -0
  49. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/__main__.py +0 -0
  50. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/_helpers.py +0 -0
  51. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/agent_profiles.py +0 -0
  52. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/agent_provenance.py +0 -0
  53. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/alarm.py +0 -0
  54. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/archive_collapse.py +0 -0
  55. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/archive_lifecycle.py +0 -0
  56. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/backup_retention_invariant.py +0 -0
  57. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/badge_state.py +0 -0
  58. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/bitemporal.py +0 -0
  59. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/brainbar_activation.py +0 -0
  60. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/brainbar_auto_activation.py +0 -0
  61. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/brainbar_hybrid_helper.py +0 -0
  62. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/brainbar_identity.py +0 -0
  63. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/cachedelete_purge.swift +0 -0
  64. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/calibrate.py +0 -0
  65. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/chunk_origin.py +0 -0
  66. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/chunk_origin_backfill.py +0 -0
  67. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/chunk_origin_wipe.py +0 -0
  68. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/chunk_write.py +0 -0
  69. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/classify.py +0 -0
  70. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/claude_paths.py +0 -0
  71. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/cli/wizard.py +0 -0
  72. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/cli_jobs.py +0 -0
  73. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/cli_new.py +0 -0
  74. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/cloud_backfill.py +0 -0
  75. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/clustering.py +0 -0
  76. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/component_jobs.py +0 -0
  77. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/config.py +0 -0
  78. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/consolidation_plan.py +0 -0
  79. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/content_class.py +0 -0
  80. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/correction_judge.py +0 -0
  81. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/data/agents_registry.yaml +0 -0
  82. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/data/sandbox_seeds/skill-eval-baseline.json +0 -0
  83. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/db_shrink.py +0 -0
  84. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/decay.py +0 -0
  85. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/decay_backfill.py +0 -0
  86. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/decay_job.py +0 -0
  87. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/dedupe.py +0 -0
  88. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/dedupe_merge.py +0 -0
  89. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/deploy_drift.py +0 -0
  90. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/disk_space.py +0 -0
  91. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/doctor.py +0 -0
  92. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/drain.py +0 -0
  93. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/drain_liveness.py +0 -0
  94. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/drive_credentials.py +0 -0
  95. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/embeddings.py +0 -0
  96. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/engine.py +0 -0
  97. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/enrichment_replay.py +0 -0
  98. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/__init__.py +0 -0
  99. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/abcde_enrich_runner.py +0 -0
  100. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/abcde_variants.py +0 -0
  101. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/abcde_variants.yaml +0 -0
  102. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/benchmark.py +0 -0
  103. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/enrichment_gold.py +0 -0
  104. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/enrichment_graders.py +0 -0
  105. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/enrichment_judge.py +0 -0
  106. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/enrichment_llm_judge.py +0 -0
  107. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/enrichment_quality_benchmark.py +0 -0
  108. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/experiment_store.py +0 -0
  109. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/fixtures/README.md +0 -0
  110. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/labeling/enrichment-labeler.html +0 -0
  111. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/phoenix_gate/__init__.py +0 -0
  112. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/phoenix_gate/__main__.py +0 -0
  113. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/phoenix_gate/baseline_store.py +0 -0
  114. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/phoenix_gate/cli.py +0 -0
  115. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/phoenix_gate/models.py +0 -0
  116. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/phoenix_gate/phoenix_client.py +0 -0
  117. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/phoenix_gate/regression_gate.py +0 -0
  118. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/phoenix_gate/triggers.py +0 -0
  119. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/pilot_artifact_report.py +0 -0
  120. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/fallback_replay.py +0 -0
  121. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/git_learning.py +0 -0
  122. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/health_check.py +0 -0
  123. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/health_facts.py +0 -0
  124. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/health_verdict.py +0 -0
  125. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/hook_python.py +0 -0
  126. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/hooks/__init__.py +0 -0
  127. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/hooks/indexer.py +0 -0
  128. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/hotlane_budget.py +0 -0
  129. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/import_sweep.py +0 -0
  130. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/index_admission.py +0 -0
  131. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/index_budget.py +0 -0
  132. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/index_completeness.py +0 -0
  133. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/index_new.py +0 -0
  134. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/index_watchdog.py +0 -0
  135. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/ingest/__init__.py +0 -0
  136. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/ingest/codex.py +0 -0
  137. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/ingest/t3.py +0 -0
  138. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/ingest_denylist.py +0 -0
  139. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/ingest_guard.py +0 -0
  140. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/isolation_proof.py +0 -0
  141. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/job_lifecycle_health.py +0 -0
  142. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/jobs.py +0 -0
  143. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/jsonl_backup.py +0 -0
  144. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/kg/__init__.py +0 -0
  145. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/kg_cleanup.py +0 -0
  146. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/kg_judge.py +0 -0
  147. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/kg_promotion.py +0 -0
  148. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/kg_repo.py +0 -0
  149. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/kg_review_session.py +0 -0
  150. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/kg_session_finish.py +0 -0
  151. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/kg_session_harvest.py +0 -0
  152. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/launchd_install.py +0 -0
  153. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/launchd_primitive.py +0 -0
  154. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/launchd_revert.py +0 -0
  155. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/lexical_defense.py +0 -0
  156. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/lexical_defense_dictionary.json +0 -0
  157. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/linkedin_import.py +0 -0
  158. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/log_cap.py +0 -0
  159. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/maintenance.py +0 -0
  160. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/maintenance_calendar.py +0 -0
  161. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/maintenance_control.py +0 -0
  162. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/mcp/__init__.py +0 -0
  163. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/mcp/_format.py +0 -0
  164. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/mcp/_shared.py +0 -0
  165. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/mcp/enrich_handler.py +0 -0
  166. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/mcp/entity_handler.py +0 -0
  167. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/mcp/palette.py +0 -0
  168. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/mcp/search_handler.py +0 -0
  169. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/mcp/store_handler.py +0 -0
  170. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/mcp/tags_handler.py +0 -0
  171. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/mcp_socket_config.py +0 -0
  172. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/memory_types.py +0 -0
  173. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/migrate.py +0 -0
  174. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/migration_custody.py +0 -0
  175. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/migration_journal.py +0 -0
  176. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/migration_runtime.py +0 -0
  177. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/notification_policy.py +0 -0
  178. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/observability_backup.py +0 -0
  179. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/observability_surface.py +0 -0
  180. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/observability_transport.py +0 -0
  181. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/p0_longitudinal_count.py +0 -0
  182. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/parent_death.py +0 -0
  183. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/paths.py +0 -0
  184. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pause.py +0 -0
  185. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/phonetic.py +0 -0
  186. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/__init__.py +0 -0
  187. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/agent_enrichment.py +0 -0
  188. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/analyze_communication.py +0 -0
  189. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/batch_extraction.py +0 -0
  190. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/brain_graph.py +0 -0
  191. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/chat_tags.py +0 -0
  192. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/chunk.py +0 -0
  193. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/classify.py +0 -0
  194. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/cloud_scrub.py +0 -0
  195. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/cluster_sampling.py +0 -0
  196. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/code_intelligence.py +0 -0
  197. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/correction_detection.py +0 -0
  198. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/correction_mining.py +0 -0
  199. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/digest.py +0 -0
  200. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/enrichment_prompts.py +0 -0
  201. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/enrichment_results.py +0 -0
  202. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/enrichment_tiers.py +0 -0
  203. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/entity_extraction.py +0 -0
  204. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/entity_resolution.py +0 -0
  205. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/extract.py +0 -0
  206. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/extract_claude_desktop.py +0 -0
  207. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/extract_corrections.py +0 -0
  208. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/extract_markdown.py +0 -0
  209. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/extract_whatsapp.py +0 -0
  210. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/frustration_mining.py +0 -0
  211. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/git_overlay.py +0 -0
  212. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/kg_extraction.py +0 -0
  213. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/kg_extraction_groq.py +0 -0
  214. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/longitudinal_analyzer.py +0 -0
  215. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/obsidian_export.py +0 -0
  216. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/operation_grouping.py +0 -0
  217. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/plan_linking.py +0 -0
  218. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/rate_limiter.py +0 -0
  219. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/relation_backfill.py +0 -0
  220. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/relation_verification.py +0 -0
  221. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/sanitize.py +0 -0
  222. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/secret_scrub.py +0 -0
  223. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/semantic_style.py +0 -0
  224. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/sentiment.py +0 -0
  225. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/session_history.py +0 -0
  226. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/style_embed.py +0 -0
  227. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/style_index.py +0 -0
  228. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/tag_entity_promotion.py +0 -0
  229. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/temporal_chains.py +0 -0
  230. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/time_batcher.py +0 -0
  231. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/unified_timeline.py +0 -0
  232. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/write_queue.py +0 -0
  233. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/policy_reconcile.py +0 -0
  234. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/provenance.py +0 -0
  235. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/provenance_autosupersede.py +0 -0
  236. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/provenance_integration.py +0 -0
  237. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/queue_io.py +0 -0
  238. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/queue_merge.py +0 -0
  239. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/readonly_snapshot.py +0 -0
  240. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/reembed_backfill.py +0 -0
  241. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/retired_services.py +0 -0
  242. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/runtime_store.py +0 -0
  243. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/sandbox_db.py +0 -0
  244. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/scoping.py +0 -0
  245. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/scrub_at_rest.py +0 -0
  246. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/search_fanout.py +0 -0
  247. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/search_profile.py +0 -0
  248. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/search_repo.py +0 -0
  249. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/service_supervisor.py +0 -0
  250. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/session_repo.py +0 -0
  251. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/setup.py +0 -0
  252. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/socket_hygiene.py +0 -0
  253. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/source_class_migration.py +0 -0
  254. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/spotlight.py +0 -0
  255. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/storage.py +0 -0
  256. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/store.py +0 -0
  257. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/supervisor_adapters.py +0 -0
  258. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/supervisor_child.py +0 -0
  259. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/supervisor_diagnostics.py +0 -0
  260. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/supervisor_guard.py +0 -0
  261. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/supervisor_jobs.py +0 -0
  262. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/system_prompt_guard.py +0 -0
  263. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/t3_linkage_alarm.py +0 -0
  264. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/t3_provenance.py +0 -0
  265. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/t3_sources.py +0 -0
  266. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/tag_normalization.py +0 -0
  267. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/taxonomy.json +0 -0
  268. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/telemetry.py +0 -0
  269. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/timestamp_iso.py +0 -0
  270. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/types.py +0 -0
  271. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/vector_store.py +0 -0
  272. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/watcher.py +0 -0
  273. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/watcher_bridge.py +0 -0
  274. {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/writer_policy.py +0 -0
  275. {brainlayer-1.5.87 → brainlayer-1.5.89}/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.87
3
+ Version: 1.5.89
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.87"
3
+ version = "1.5.89"
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.87",
6
+ "version": "1.5.89",
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.87",
17
+ "version": "1.5.89",
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.87"
5
+ __version__ = "1.5.89"
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 = "4e5c275f01698d443a6727f30d54638b22ac7797"
@@ -3373,6 +3373,7 @@ def _serialized_backup_run(func: Callable[..., dict[str, Any]]) -> Callable[...,
3373
3373
  },
3374
3374
  )
3375
3375
  raise BackupAlreadyRunningError(error) from exc
3376
+ _record_backup_lock_time(staging_dir)
3376
3377
  try:
3377
3378
  # Weekly maintenance's first receipt check precedes this lock. A daily
3378
3379
  # backup can finish in that gap, so recheck before copying the DB.
@@ -3395,6 +3396,22 @@ def _serialized_backup_run(func: Callable[..., dict[str, Any]]) -> Callable[...,
3395
3396
  return wrapped
3396
3397
 
3397
3398
 
3399
+ def _record_backup_lock_time(staging_dir: Path) -> None:
3400
+ """Write the current local/upload phase start beside the held backup flock."""
3401
+ descriptor = -1
3402
+ try:
3403
+ descriptor = os.open(staging_dir / ".backup.lock", os.O_WRONLY | os.O_NOFOLLOW)
3404
+ value = dt.datetime.now(dt.UTC).isoformat(timespec="seconds").encode("ascii") + b"\n"
3405
+ os.ftruncate(descriptor, 0)
3406
+ os.write(descriptor, value)
3407
+ except OSError:
3408
+ # No reader may treat this lock-file content as a lock signal; only the held flock is authoritative.
3409
+ pass # The display timestamp is best effort.
3410
+ finally:
3411
+ if descriptor >= 0:
3412
+ os.close(descriptor)
3413
+
3414
+
3398
3415
  def _upload_identity(path: Path) -> dict[str, Any]:
3399
3416
  with path.open("rb") as handle:
3400
3417
  digest = hashlib.file_digest(handle, "sha256").hexdigest()
@@ -3809,6 +3826,7 @@ def run_backup(
3809
3826
  folder_id = ensure_drive_folder_chain(service, resolved_folder_parts)
3810
3827
  break # Completed session: retry verification, never POST another upload.
3811
3828
  result["upload_state"] = "in_progress"
3829
+ _record_backup_lock_time(snapshot.parent)
3812
3830
  credentials = get_drive_credentials()
3813
3831
  service = build_drive_service()
3814
3832
  folder_id = ensure_drive_folder_chain(service, resolved_folder_parts)
@@ -3142,7 +3142,15 @@ def wal_checkpoint(
3142
3142
  console.print(
3143
3143
  f"Checkpoint ({result['mode']}): {result['checkpointed_pages']}/{result['log_pages']} pages{suffix}"
3144
3144
  )
3145
+ if result.get("deferred"):
3146
+ console.print(
3147
+ "TRUNCATE deferred after bounded retries; PASSIVE checkpoint attempted; "
3148
+ "the WAL remains available for the next checkpoint."
3149
+ )
3145
3150
 
3151
+ if result.get("deferred"):
3152
+ report_job_alert("wal-checkpoint", None)
3153
+ return
3146
3154
  if result["busy"]:
3147
3155
  report_job_alert("wal-checkpoint", "BrainLayer WAL checkpoint was busy; check the job log")
3148
3156
  raise typer.Exit(1)
@@ -0,0 +1,156 @@
1
+ """Durable, deduplicated backup/maintenance failure notices."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import fcntl
6
+ import json
7
+ import logging
8
+ import os
9
+ import subprocess
10
+ from contextlib import contextmanager
11
+ from datetime import UTC, datetime
12
+ from pathlib import Path
13
+ from typing import Callable, Mapping
14
+
15
+ from brainlayer.paths import get_db_path
16
+
17
+ LOG = logging.getLogger(__name__)
18
+ ALERT_PATH_ENV = "BRAINLAYER_JOB_ALERT_PATH"
19
+
20
+
21
+ def alert_path(*, env: Mapping[str, str] | None = None, db_path: Path | None = None) -> Path:
22
+ env = os.environ if env is None else env
23
+ return Path(env.get(ALERT_PATH_ENV) or (db_path or get_db_path()).with_name("job-alerts.json")).expanduser()
24
+
25
+
26
+ def _times_path(path: Path) -> Path:
27
+ return path.with_name(f"{path.stem}.times.json")
28
+
29
+
30
+ def _notify(message: str) -> None:
31
+ if os.environ.get("BRAINLAYER_TEST_PATH_PROVENANCE") == "pytest":
32
+ return
33
+ subprocess.run(
34
+ [
35
+ "osascript",
36
+ "-e",
37
+ 'on run argv\n display notification (item 1 of argv) with title "BrainLayer"\nend run',
38
+ message,
39
+ ],
40
+ timeout=8,
41
+ check=True,
42
+ capture_output=True,
43
+ )
44
+
45
+
46
+ @contextmanager
47
+ def _locked(path: Path):
48
+ path.parent.mkdir(parents=True, exist_ok=True)
49
+ fd = os.open(path.with_suffix(".lock"), os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600)
50
+ try:
51
+ fcntl.flock(fd, fcntl.LOCK_EX)
52
+ yield
53
+ finally:
54
+ fcntl.flock(fd, fcntl.LOCK_UN)
55
+ os.close(fd)
56
+
57
+
58
+ def _alert_records(path: Path | None = None) -> dict[str, dict[str, str | None]]:
59
+ try:
60
+ value = json.loads((path or alert_path()).read_text(encoding="utf-8"))
61
+ if not isinstance(value, dict):
62
+ return {}
63
+ records = {}
64
+ for key, item in value.items():
65
+ if isinstance(item, str): # Read pre-S32 files without losing their live alerts.
66
+ records[str(key)] = {"reason": item, "reported_at": None}
67
+ elif isinstance(item, dict) and isinstance(item.get("reason"), str):
68
+ records[str(key)] = {
69
+ "reason": item["reason"],
70
+ "reported_at": None,
71
+ }
72
+ times = _alert_times(_times_path(path or alert_path()))
73
+ for key, reported_at in times.items():
74
+ if key in records:
75
+ records[key]["reported_at"] = reported_at
76
+ return records
77
+ except (FileNotFoundError, OSError, ValueError):
78
+ return {}
79
+
80
+
81
+ def _alert_times(path: Path) -> dict[str, str]:
82
+ """Read optional timestamps; absent or malformed metadata never hides an alert."""
83
+ try:
84
+ value = json.loads(path.read_text(encoding="utf-8"))
85
+ if not isinstance(value, dict) or not all(isinstance(item, str) for item in value.values()):
86
+ return {}
87
+ return {str(key): item for key, item in value.items()}
88
+ except (FileNotFoundError, OSError, ValueError):
89
+ return {}
90
+
91
+
92
+ def active_alerts(path: Path | None = None) -> dict[str, str]:
93
+ """Return current reasons, accepting both legacy strings and timestamped records."""
94
+ return {key: record["reason"] for key, record in _alert_records(path).items()}
95
+
96
+
97
+ def alert_reported_at(path: Path | None = None) -> dict[str, str]:
98
+ """Return alert occurrence times where available; legacy records have no timestamp."""
99
+ return {key: at for key, record in _alert_records(path).items() if (at := record["reported_at"]) is not None}
100
+
101
+
102
+ def report(
103
+ key: str,
104
+ reason: str | None,
105
+ *,
106
+ path: Path | None = None,
107
+ notify: Callable[[str], None] = _notify,
108
+ ) -> bool:
109
+ """Set or clear one episode. Return true only when the visible state changes.
110
+
111
+ Only a new failure posts a notification, and only when no other active key already
112
+ carries the identical message (one incident fanned out across keys posts once).
113
+ Recovery updates the alert file and the health log but never notifies.
114
+ """
115
+ destination = path or alert_path()
116
+ with _locked(destination):
117
+ records = _alert_records(destination)
118
+ before = records.get(key, {}).get("reason")
119
+ if reason is None:
120
+ if before is None:
121
+ return False
122
+ records.pop(key)
123
+ message = f"BrainLayer {key} recovered"
124
+ post = False
125
+ else:
126
+ if before is not None and before == reason:
127
+ records[key]["reported_at"] = datetime.now(UTC).isoformat(timespec="microseconds")
128
+ _write_records(destination, records)
129
+ return False
130
+ if before is not None and key != "drive-consent":
131
+ return False
132
+ post = reason not in (record["reason"] for other, record in records.items() if other != key)
133
+ records[key] = {"reason": reason, "reported_at": datetime.now(UTC).isoformat(timespec="microseconds")}
134
+ message = reason
135
+ _write_records(destination, records)
136
+ LOG.warning("health event job=%s state=%s reason=%s", key, "recovered" if reason is None else "failed", message)
137
+ if not post:
138
+ return True
139
+ try:
140
+ notify(message)
141
+ except (OSError, subprocess.SubprocessError) as exc:
142
+ LOG.warning("macOS notification failed for %s: %s", key, type(exc).__name__)
143
+ return True
144
+
145
+
146
+ def _write_records(path: Path, records: dict[str, dict[str, str | None]]) -> None:
147
+ """Atomically replace both files under the caller's shared alert lock.
148
+
149
+ The public alerts file intentionally stays a map of strings for older BrainBar readers.
150
+ """
151
+ from brainlayer.drive_credentials import _atomic_write
152
+
153
+ alerts = {key: record["reason"] for key, record in records.items()}
154
+ times = {key: record["reported_at"] for key, record in records.items() if record["reported_at"]}
155
+ _atomic_write(_times_path(path), times)
156
+ _atomic_write(path, alerts)
@@ -1,11 +1,13 @@
1
1
  """Explicit launchd migration with durable archive custody and checked rollback."""
2
2
 
3
+ import fcntl
3
4
  import hashlib
4
5
  import json
5
6
  import os
6
7
  import plistlib
7
8
  import re
8
9
  import stat
10
+ import sys
9
11
  import time
10
12
  import uuid
11
13
  from pathlib import Path
@@ -21,6 +23,8 @@ from .consolidation_plan import (
21
23
  rendered_launchers,
22
24
  snapshot_legacy,
23
25
  )
26
+ from .launcher_policy_reconcile import transition as launcher_policy_transition
27
+ from .launcher_policy_reconcile import validate as validate_launcher_policy
24
28
  from .migration_custody import migration_lock, write_launch_agent
25
29
  from .migration_custody import write_new_file as write_file
26
30
  from .migration_journal import archive_replace as update
@@ -80,6 +84,41 @@ def wait_custody(directory, *, clock=time, grace=30):
80
84
  clock.sleep(0.1)
81
85
 
82
86
 
87
+ def _acquire_maintenance_idle(home):
88
+ """Hold nonblocking maintenance leases through a policy-only launcher restart."""
89
+ paths = (
90
+ home / ".local/share/brainlayer/supervisor/maintenance.lock",
91
+ home / ".local/share/brainlayer/.maintenance.lock",
92
+ home / ".local/share/brainlayer/backups/.backup.lock",
93
+ )
94
+ held = []
95
+ try:
96
+ for path in paths:
97
+ if not os.path.lexists(path):
98
+ continue
99
+ fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
100
+ held.append(fd)
101
+ info = os.fstat(fd)
102
+ if not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid() or info.st_nlink != 1:
103
+ raise RuntimeError("maintenance lock identity refused; retry later")
104
+ try:
105
+ fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
106
+ except BlockingIOError:
107
+ raise RuntimeError("maintenance is busy; retry later") from None
108
+ return held
109
+ except BaseException:
110
+ _release_maintenance_locks(held)
111
+ raise
112
+
113
+
114
+ def _release_maintenance_locks(held):
115
+ for fd in held:
116
+ try:
117
+ fcntl.flock(fd, fcntl.LOCK_UN)
118
+ finally:
119
+ os.close(fd)
120
+
121
+
83
122
  def migrate(*, home, **options):
84
123
  with migration_lock(home) as control:
85
124
  receipt, *destinations = publication_paths(home)
@@ -198,6 +237,65 @@ def _migrate(
198
237
  if label in NEW:
199
238
  remember_started(label, current)
200
239
 
240
+ def reconcile_launchers(journal, expected_receipt, pending, *, recovering, held_locks=None):
241
+ validate_launcher_policy(pending)
242
+ changed = set(pending["diff"])
243
+ own_locks = held_locks is None and "com.brainlayer.maintenance" in changed
244
+ locks = _acquire_maintenance_idle(home) if own_locks else (held_locks or [])
245
+ try:
246
+ if "com.brainlayer.maintenance" in changed:
247
+ _, calendar_pid = observe(NEW[1])
248
+ if calendar_pid is not None and alive(calendar_pid):
249
+ raise RuntimeError("calendar is active; maintenance busy, retry later")
250
+ if not recovering:
251
+ journal_update(journal, expected_receipt)
252
+ expected_receipt = json.dumps(journal).encode()
253
+ except BaseException:
254
+ if own_locks:
255
+ _release_maintenance_locks(locks)
256
+ raise
257
+ publications = {}
258
+ try:
259
+ for name, item in pending["files"].items():
260
+ path = agents / name
261
+ old, new = bytes.fromhex(item["old_bytes"]), bytes.fromhex(item["new_bytes"])
262
+ current = read_owned(path)
263
+ if current not in {old, new}:
264
+ raise RuntimeError(f"launcher policy recovery found foreign plist: {name}")
265
+ label = name.removesuffix(".plist")
266
+ state = runtime.observe(label)
267
+ if current == old and state["loaded"]:
268
+ stop(label, state)
269
+ if current == old:
270
+ update(path, old, new, Path(journal["archive"]), write_launch_agent)
271
+ if not runtime.observe(label)["loaded"]:
272
+ bootstrap(label, path)
273
+ publications[name] = new.hex()
274
+
275
+ result = journal.get("restart_preimage", journal).copy()
276
+ result.pop("launcher_policy_reconcile", None)
277
+ files = dict(result["published_files"])
278
+ files.update(publications)
279
+ result.update(
280
+ phase="migrated",
281
+ published_files=files,
282
+ published_sha256={name: hashlib.sha256(bytes.fromhex(raw)).hexdigest() for name, raw in files.items()},
283
+ configuration_sha256={
284
+ **result["configuration_sha256"],
285
+ **{name.removesuffix(".plist"): item["new_sha256"] for name, item in pending["files"].items()},
286
+ },
287
+ launcher_policy_reconciliations=result.get("launcher_policy_reconciliations", [])
288
+ + [{"diff": pending["diff"], "files": pending["files"]}],
289
+ )
290
+ journal_update(result, expected_receipt)
291
+ for label, changes in pending["diff"].items():
292
+ for key, values in changes.items():
293
+ print(f"{label}: {key} {values['old']} → {values['new']}", file=sys.stderr)
294
+ return result
295
+ finally:
296
+ if own_locks:
297
+ _release_maintenance_locks(locks)
298
+
201
299
  def verify_daemon():
202
300
  if NEW[0] not in disabled:
203
301
  if verify is not None:
@@ -542,17 +640,38 @@ def _migrate(
542
640
  )
543
641
  if pending:
544
642
  try:
643
+ if "launcher_policy_reconcile" in previous:
644
+ return reconcile_launchers(
645
+ previous, previous_bytes, previous["launcher_policy_reconcile"], recovering=True
646
+ )
545
647
  if "requested_keg" in previous:
546
648
  return restart_daemon(previous, previous_bytes, recovering=True)
547
649
  return restore_first(previous, previous_bytes, recovering=True)
548
650
  except BaseException as failure:
549
651
  if not isinstance(failure, Exception):
550
652
  raise
653
+ if "busy, retry later" in str(failure) or "busy; retry later" in str(failure):
654
+ raise
551
655
  raise RuntimeError(
552
656
  f"interrupted migration; rollback incomplete; operator reconciliation: {failure}"
553
657
  ) from failure
554
658
  if previous.get("phase") != "migrated":
555
659
  raise RuntimeError("unknown migration phase; operator reconciliation required")
660
+ try:
661
+ launcher_policy = launcher_policy_transition(previous, prepared)
662
+ except ValueError as failure:
663
+ raise RuntimeError(str(failure)) from failure
664
+ if launcher_policy:
665
+ restarting = previous | dict(phase="migrating", launcher_policy_reconcile=launcher_policy)
666
+ changes_maintenance = "com.brainlayer.maintenance" in launcher_policy["diff"]
667
+ locks = _acquire_maintenance_idle(home) if changes_maintenance else []
668
+ try:
669
+ previous = reconcile_launchers(
670
+ restarting, previous_bytes, launcher_policy, recovering=False, held_locks=locks
671
+ )
672
+ previous_bytes = json.dumps(previous).encode()
673
+ finally:
674
+ _release_maintenance_locks(locks)
556
675
  if any(os.path.lexists(agents / name) for name in previous["files"]):
557
676
  raise RuntimeError("legacy files reappeared after consolidation")
558
677
  for name, original in previous["files"].items():
@@ -0,0 +1,75 @@
1
+ """Journal launcher plist transitions that change policy keys only."""
2
+
3
+ import hashlib
4
+ import plistlib
5
+
6
+ POLICY_KEYS = frozenset({"ProcessType", "Nice", "LowPriorityIO", "SoftResourceLimits"})
7
+
8
+
9
+ def _transition(previous, prepared):
10
+ """Return a verifiable policy-only transition, or raise for any other edit."""
11
+ files = {}
12
+ diff = {}
13
+ for label, new in prepared.items():
14
+ name = f"{label}.plist"
15
+ old = bytes.fromhex(previous["published_files"][name])
16
+ before, after = plistlib.loads(old), plistlib.loads(new)
17
+ if before.get("Label") != label or after.get("Label") != label:
18
+ raise ValueError(f"launcher identity changed: {label}")
19
+ old_nonpolicy = {key: value for key, value in before.items() if key not in POLICY_KEYS}
20
+ new_nonpolicy = {key: value for key, value in after.items() if key not in POLICY_KEYS}
21
+ if old_nonpolicy != new_nonpolicy:
22
+ raise ValueError("launcher configuration changed; explicit replan required")
23
+ changes = {
24
+ key: {"old": before.get(key), "new": after.get(key)}
25
+ for key in sorted(POLICY_KEYS)
26
+ if before.get(key) != after.get(key)
27
+ }
28
+ if changes:
29
+ files[name] = {
30
+ "old_bytes": old.hex(),
31
+ "new_bytes": new.hex(),
32
+ "old_sha256": hashlib.sha256(old).hexdigest(),
33
+ "new_sha256": hashlib.sha256(new).hexdigest(),
34
+ }
35
+ diff[label] = changes
36
+ if not diff:
37
+ return None
38
+ return {"files": files, "diff": diff}
39
+
40
+
41
+ def transition(previous, prepared):
42
+ """Collect policy-only changes per launcher.
43
+
44
+ A launcher whose non-policy keys differ from its published bytes is left out, not refused:
45
+ the existing journal checks own that case (a restart fallback pins a different daemon than
46
+ the requested configuration, and a real configuration change must still refuse there).
47
+ """
48
+ files, diff = {}, {}
49
+ for label, new in prepared.items():
50
+ try:
51
+ one = _transition(previous, {label: new})
52
+ except ValueError as failure:
53
+ if "explicit replan required" in str(failure):
54
+ continue
55
+ raise
56
+ if one:
57
+ files.update(one["files"])
58
+ diff.update(one["diff"])
59
+ return {"files": files, "diff": diff} if diff else None
60
+
61
+
62
+ def validate(pending):
63
+ """Validate journal custody without consulting mutable templates."""
64
+ if set(pending) != {"files", "diff"} or set(pending["files"]) != {f"{label}.plist" for label in pending["diff"]}:
65
+ raise ValueError("invalid launcher policy journal")
66
+ for name, item in pending["files"].items():
67
+ old, new = bytes.fromhex(item["old_bytes"]), bytes.fromhex(item["new_bytes"])
68
+ if hashlib.sha256(old).hexdigest() != item["old_sha256"]:
69
+ raise ValueError("invalid old launcher policy hash")
70
+ if hashlib.sha256(new).hexdigest() != item["new_sha256"]:
71
+ raise ValueError("invalid new launcher policy hash")
72
+ label = name.removesuffix(".plist")
73
+ candidate = _transition({"published_files": {name: old.hex()}}, {label: new})
74
+ if candidate != {"files": {name: item}, "diff": {label: pending["diff"][label]}}:
75
+ raise ValueError("invalid launcher policy transition")
@@ -159,7 +159,7 @@ def _jsonrpc_error_for_frame(frame: InFlightFrame) -> bytes:
159
159
  "id": frame.request_id,
160
160
  "error": {
161
161
  "code": -32000,
162
- "message": "BrainLayer backend disconnected before responding; request was not acknowledged",
162
+ "message": "BrainLayer backend disconnected before responding; request outcome is unknown",
163
163
  },
164
164
  },
165
165
  separators=(",", ":"),
@@ -263,6 +263,14 @@ def run_bridge(
263
263
  eof_response_deadline: float | None = None
264
264
  pending_responses = 0
265
265
  stdout_failed = False
266
+ has_connected_once = False
267
+
268
+ def log_reconnect() -> None:
269
+ nonlocal has_connected_once
270
+ if has_connected_once:
271
+ stderr.write(f"brainlayer-mcp-stdio-bridge: reconnected to socket {config.socket_path!r}\n".encode("utf-8"))
272
+ stderr.flush()
273
+ has_connected_once = True
266
274
 
267
275
  def fail_in_flight_requests() -> None:
268
276
  nonlocal pending_responses, stdout_failed
@@ -316,6 +324,7 @@ def run_bridge(
316
324
  connecting = False
317
325
  connect_started_at = None
318
326
  reconnect_delay = config.reconnect_ms / 1000
327
+ log_reconnect()
319
328
 
320
329
  def mark_connected() -> None:
321
330
  nonlocal connected, connecting, connect_started_at, reconnect_delay
@@ -323,6 +332,7 @@ def run_bridge(
323
332
  connecting = False
324
333
  connect_started_at = None
325
334
  reconnect_delay = config.reconnect_ms / 1000
335
+ log_reconnect()
326
336
 
327
337
  try:
328
338
  while True:
@@ -20,6 +20,7 @@ from .paths import get_db_path, supervisor_socket_path
20
20
  _VALID_CHECKPOINT_MODES = {"PASSIVE", "FULL", "RESTART", "TRUNCATE"}
21
21
  _DEFAULT_GUARD_TIMEOUT_SECONDS = 10.0
22
22
  _GUARD_POLL_SECONDS = 0.1
23
+ _WAL_CHECKPOINT_HARD_CEILING_BYTES = 4 * 1024**3
23
24
 
24
25
 
25
26
  class CheckpointGuardTimeout(TimeoutError):
@@ -155,6 +156,11 @@ def run_wal_checkpoint(
155
156
 
156
157
  mode = mode.upper()
157
158
  wal_before = get_wal_size(db_path)
159
+ if retry_busy and wal_before > _WAL_CHECKPOINT_HARD_CEILING_BYTES:
160
+ raise RuntimeError(
161
+ f"WAL is above the checkpoint safety ceiling ({format_size(wal_before)} > "
162
+ f"{format_size(_WAL_CHECKPOINT_HARD_CEILING_BYTES)})"
163
+ )
158
164
  attempts = 0
159
165
  max_attempts = max(1, int(max_attempts))
160
166
  delay = max(0.0, retry_base_seconds)
@@ -165,9 +171,16 @@ def run_wal_checkpoint(
165
171
  break
166
172
  sleep_fn(delay)
167
173
  delay = min(max(delay * 2, retry_base_seconds), max(retry_max_seconds, retry_base_seconds))
174
+ deferred = bool(busy and mode == "TRUNCATE" and retry_busy)
175
+ passive_result = None
176
+ if deferred:
177
+ # A held reader can prevent TRUNCATE from resetting the WAL. Make any safe
178
+ # progress without waiting for exclusive access, then leave truncation to
179
+ # the periodic fallback after that reader closes.
180
+ passive_result = checkpoint(db_path, "PASSIVE")
168
181
  wal_after = get_wal_size(db_path)
169
182
 
170
- return {
183
+ result = {
171
184
  "db": db_path,
172
185
  "mode": mode,
173
186
  "wal_before_bytes": wal_before,
@@ -178,7 +191,12 @@ def run_wal_checkpoint(
178
191
  "log_pages": log_pages,
179
192
  "checkpointed_pages": checkpointed_pages,
180
193
  "attempts": attempts,
194
+ "deferred": deferred,
181
195
  }
196
+ if passive_result is not None:
197
+ result["passive_log_pages"] = passive_result[1]
198
+ result["passive_checkpointed_pages"] = passive_result[2]
199
+ return result
182
200
 
183
201
 
184
202
  def _fallback_defer_reason(directory):
@@ -331,6 +349,10 @@ def periodic_checkpoint(*, high_water_bytes=256_000_000, busy_timeout_seconds=0.
331
349
  f"{detail}. See wal-fallback.json for progress and diagnostics."
332
350
  )
333
351
  job_alerts.report("wal-checkpoint-fallback", reason, path=job_alerts.alert_path(db_path=Path(db)))
352
+ if reason is None:
353
+ # The 15-minute fallback is an equivalent successful checkpoint: retire a busy
354
+ # scheduled-checkpoint alert durably before BrainBar reads its next status snapshot.
355
+ job_alerts.report("wal-checkpoint", None, path=job_alerts.alert_path(db_path=Path(db)))
334
356
  return result
335
357
 
336
358
 
@@ -1 +0,0 @@
1
- BUILD_SHA = "5fcad968fe3ef58c7d5309909b23c6d4d3ad572c"