brainlayer 1.5.76__tar.gz → 1.5.77__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 (265) hide show
  1. {brainlayer-1.5.76 → brainlayer-1.5.77}/PKG-INFO +1 -1
  2. {brainlayer-1.5.76 → brainlayer-1.5.77}/pyproject.toml +1 -1
  3. {brainlayer-1.5.76 → brainlayer-1.5.77}/server.json +2 -2
  4. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/__init__.py +1 -1
  5. brainlayer-1.5.77/src/brainlayer/_build.py +1 -0
  6. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/backup_daily.py +135 -20
  7. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/health_check.py +18 -17
  8. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/observability_backup.py +29 -0
  9. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/supervisor_adapters.py +81 -4
  10. brainlayer-1.5.76/src/brainlayer/_build.py +0 -1
  11. {brainlayer-1.5.76 → brainlayer-1.5.77}/.gitignore +0 -0
  12. {brainlayer-1.5.76 → brainlayer-1.5.77}/LICENSE +0 -0
  13. {brainlayer-1.5.76 → brainlayer-1.5.77}/README.md +0 -0
  14. {brainlayer-1.5.76 → brainlayer-1.5.77}/scripts/hotlane_brainbar_daemon.py +0 -0
  15. {brainlayer-1.5.76 → brainlayer-1.5.77}/scripts/launchd/backup-daily.sh +0 -0
  16. {brainlayer-1.5.76 → brainlayer-1.5.77}/scripts/launchd/brainlayer-env-run.sh +0 -0
  17. {brainlayer-1.5.76 → brainlayer-1.5.77}/scripts/launchd/brainlayer.env.example +0 -0
  18. {brainlayer-1.5.76 → brainlayer-1.5.77}/scripts/launchd/com.brainlayer.backup-daily.plist +0 -0
  19. {brainlayer-1.5.76 → brainlayer-1.5.77}/scripts/launchd/com.brainlayer.daemon.plist +0 -0
  20. {brainlayer-1.5.76 → brainlayer-1.5.77}/scripts/launchd/com.brainlayer.decay.plist +0 -0
  21. {brainlayer-1.5.76 → brainlayer-1.5.77}/scripts/launchd/com.brainlayer.drain.plist +0 -0
  22. {brainlayer-1.5.76 → brainlayer-1.5.77}/scripts/launchd/com.brainlayer.health-check.plist +0 -0
  23. {brainlayer-1.5.76 → brainlayer-1.5.77}/scripts/launchd/com.brainlayer.hotlane-brainbar.plist +0 -0
  24. {brainlayer-1.5.76 → brainlayer-1.5.77}/scripts/launchd/com.brainlayer.index.plist +0 -0
  25. {brainlayer-1.5.76 → brainlayer-1.5.77}/scripts/launchd/com.brainlayer.jsonl-backup.plist +0 -0
  26. {brainlayer-1.5.76 → brainlayer-1.5.77}/scripts/launchd/com.brainlayer.log-cap.plist +0 -0
  27. {brainlayer-1.5.76 → brainlayer-1.5.77}/scripts/launchd/com.brainlayer.maintenance-nightly.plist +0 -0
  28. {brainlayer-1.5.76 → brainlayer-1.5.77}/scripts/launchd/com.brainlayer.maintenance-weekly.plist +0 -0
  29. {brainlayer-1.5.76 → brainlayer-1.5.77}/scripts/launchd/com.brainlayer.maintenance.plist +0 -0
  30. {brainlayer-1.5.76 → brainlayer-1.5.77}/scripts/launchd/com.brainlayer.observability.plist +0 -0
  31. {brainlayer-1.5.76 → brainlayer-1.5.77}/scripts/launchd/com.brainlayer.p0-counter.plist +0 -0
  32. {brainlayer-1.5.76 → brainlayer-1.5.77}/scripts/launchd/com.brainlayer.repair-fts.plist +0 -0
  33. {brainlayer-1.5.76 → brainlayer-1.5.77}/scripts/launchd/com.brainlayer.t3-ingest.plist +0 -0
  34. {brainlayer-1.5.76 → brainlayer-1.5.77}/scripts/launchd/com.brainlayer.throughput-watchdog.plist +0 -0
  35. {brainlayer-1.5.76 → brainlayer-1.5.77}/scripts/launchd/com.brainlayer.tier0-watchdog.plist +0 -0
  36. {brainlayer-1.5.76 → brainlayer-1.5.77}/scripts/launchd/com.brainlayer.wal-checkpoint.plist +0 -0
  37. {brainlayer-1.5.76 → brainlayer-1.5.77}/scripts/launchd/com.brainlayer.watch.plist +0 -0
  38. {brainlayer-1.5.76 → brainlayer-1.5.77}/scripts/launchd/com.etanhey.brainlayer-fleet-watchdog.plist +0 -0
  39. {brainlayer-1.5.76 → brainlayer-1.5.77}/scripts/launchd/fleet-watchdog.sh +0 -0
  40. {brainlayer-1.5.76 → brainlayer-1.5.77}/scripts/launchd/install.sh +0 -0
  41. {brainlayer-1.5.76 → brainlayer-1.5.77}/scripts/launchd/jsonl-backup.sh +0 -0
  42. {brainlayer-1.5.76 → brainlayer-1.5.77}/scripts/launchd/throughput-watchdog.py +0 -0
  43. {brainlayer-1.5.76 → brainlayer-1.5.77}/scripts/release-verify-signatures.sh +0 -0
  44. {brainlayer-1.5.76 → brainlayer-1.5.77}/scripts/tier0-watchdog.sh +0 -0
  45. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/__main__.py +0 -0
  46. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/_helpers.py +0 -0
  47. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/agent_profiles.py +0 -0
  48. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/agent_provenance.py +0 -0
  49. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/alarm.py +0 -0
  50. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/archive_collapse.py +0 -0
  51. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/archive_lifecycle.py +0 -0
  52. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/backup_retention_invariant.py +0 -0
  53. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/badge_state.py +0 -0
  54. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/bitemporal.py +0 -0
  55. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/brainbar_activation.py +0 -0
  56. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/brainbar_auto_activation.py +0 -0
  57. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/brainbar_hybrid_helper.py +0 -0
  58. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/brainbar_identity.py +0 -0
  59. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/cachedelete_purge.swift +0 -0
  60. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/calibrate.py +0 -0
  61. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/chunk_origin.py +0 -0
  62. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/chunk_origin_backfill.py +0 -0
  63. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/chunk_origin_wipe.py +0 -0
  64. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/chunk_write.py +0 -0
  65. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/classify.py +0 -0
  66. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/claude_paths.py +0 -0
  67. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/cli/__init__.py +0 -0
  68. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/cli/wizard.py +0 -0
  69. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/cli_jobs.py +0 -0
  70. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/cli_new.py +0 -0
  71. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/cloud_backfill.py +0 -0
  72. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/clustering.py +0 -0
  73. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/component_jobs.py +0 -0
  74. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/config.py +0 -0
  75. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/consolidation_plan.py +0 -0
  76. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/content_class.py +0 -0
  77. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/correction_judge.py +0 -0
  78. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/data/agents_registry.yaml +0 -0
  79. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/data/sandbox_seeds/skill-eval-baseline.json +0 -0
  80. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/db_shrink.py +0 -0
  81. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/decay.py +0 -0
  82. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/decay_backfill.py +0 -0
  83. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/decay_job.py +0 -0
  84. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/dedupe.py +0 -0
  85. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/dedupe_merge.py +0 -0
  86. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/deploy_drift.py +0 -0
  87. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/disk_space.py +0 -0
  88. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/doctor.py +0 -0
  89. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/drain.py +0 -0
  90. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/drain_liveness.py +0 -0
  91. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/drive_credentials.py +0 -0
  92. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/embeddings.py +0 -0
  93. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/engine.py +0 -0
  94. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/enrichment_replay.py +0 -0
  95. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/eval/__init__.py +0 -0
  96. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/eval/abcde_enrich_runner.py +0 -0
  97. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/eval/abcde_variants.py +0 -0
  98. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/eval/abcde_variants.yaml +0 -0
  99. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/eval/benchmark.py +0 -0
  100. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/eval/enrichment_gold.py +0 -0
  101. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/eval/enrichment_graders.py +0 -0
  102. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/eval/enrichment_judge.py +0 -0
  103. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/eval/enrichment_llm_judge.py +0 -0
  104. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/eval/enrichment_quality_benchmark.py +0 -0
  105. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/eval/experiment_store.py +0 -0
  106. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/eval/fixtures/README.md +0 -0
  107. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/eval/labeling/enrichment-labeler.html +0 -0
  108. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/eval/phoenix_gate/__init__.py +0 -0
  109. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/eval/phoenix_gate/__main__.py +0 -0
  110. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/eval/phoenix_gate/baseline_store.py +0 -0
  111. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/eval/phoenix_gate/cli.py +0 -0
  112. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/eval/phoenix_gate/models.py +0 -0
  113. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/eval/phoenix_gate/phoenix_client.py +0 -0
  114. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/eval/phoenix_gate/regression_gate.py +0 -0
  115. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/eval/phoenix_gate/triggers.py +0 -0
  116. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/eval/pilot_artifact_report.py +0 -0
  117. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/fallback_replay.py +0 -0
  118. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/git_learning.py +0 -0
  119. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/hook_python.py +0 -0
  120. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/hooks/__init__.py +0 -0
  121. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/hooks/indexer.py +0 -0
  122. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/import_sweep.py +0 -0
  123. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/index_admission.py +0 -0
  124. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/index_budget.py +0 -0
  125. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/index_completeness.py +0 -0
  126. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/index_new.py +0 -0
  127. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/index_watchdog.py +0 -0
  128. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/ingest/__init__.py +0 -0
  129. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/ingest/codex.py +0 -0
  130. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/ingest/t3.py +0 -0
  131. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/ingest_denylist.py +0 -0
  132. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/ingest_guard.py +0 -0
  133. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/isolation_proof.py +0 -0
  134. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/job_alerts.py +0 -0
  135. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/job_lifecycle_health.py +0 -0
  136. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/jobs.py +0 -0
  137. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/jsonl_backup.py +0 -0
  138. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/kg/__init__.py +0 -0
  139. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/kg_cleanup.py +0 -0
  140. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/kg_judge.py +0 -0
  141. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/kg_promotion.py +0 -0
  142. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/kg_repo.py +0 -0
  143. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/kg_review_session.py +0 -0
  144. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/kg_session_finish.py +0 -0
  145. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/kg_session_harvest.py +0 -0
  146. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/launchd_consolidation.py +0 -0
  147. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/launchd_install.py +0 -0
  148. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/launchd_primitive.py +0 -0
  149. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/launchd_revert.py +0 -0
  150. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/lexical_defense.py +0 -0
  151. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/lexical_defense_dictionary.json +0 -0
  152. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/log_cap.py +0 -0
  153. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/maintenance.py +0 -0
  154. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/maintenance_calendar.py +0 -0
  155. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/maintenance_control.py +0 -0
  156. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/mcp/__init__.py +0 -0
  157. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/mcp/_format.py +0 -0
  158. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/mcp/_shared.py +0 -0
  159. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/mcp/enrich_handler.py +0 -0
  160. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/mcp/entity_handler.py +0 -0
  161. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/mcp/palette.py +0 -0
  162. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/mcp/search_handler.py +0 -0
  163. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/mcp/store_handler.py +0 -0
  164. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/mcp/tags_handler.py +0 -0
  165. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/mcp_socket_config.py +0 -0
  166. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/mcp_stdio_bridge.py +0 -0
  167. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/memory_types.py +0 -0
  168. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/migrate.py +0 -0
  169. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/migration_custody.py +0 -0
  170. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/migration_journal.py +0 -0
  171. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/migration_runtime.py +0 -0
  172. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/notification_policy.py +0 -0
  173. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/observability_surface.py +0 -0
  174. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/observability_transport.py +0 -0
  175. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/p0_longitudinal_count.py +0 -0
  176. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/parent_death.py +0 -0
  177. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/paths.py +0 -0
  178. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pause.py +0 -0
  179. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/phonetic.py +0 -0
  180. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/__init__.py +0 -0
  181. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/agent_enrichment.py +0 -0
  182. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/analyze_communication.py +0 -0
  183. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/batch_extraction.py +0 -0
  184. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/brain_graph.py +0 -0
  185. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/chat_tags.py +0 -0
  186. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/chunk.py +0 -0
  187. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/classify.py +0 -0
  188. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/cloud_scrub.py +0 -0
  189. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/cluster_sampling.py +0 -0
  190. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/code_intelligence.py +0 -0
  191. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/correction_detection.py +0 -0
  192. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/correction_mining.py +0 -0
  193. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/digest.py +0 -0
  194. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/enrichment_prompts.py +0 -0
  195. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/enrichment_results.py +0 -0
  196. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/enrichment_tiers.py +0 -0
  197. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/entity_extraction.py +0 -0
  198. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/entity_resolution.py +0 -0
  199. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/extract.py +0 -0
  200. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/extract_claude_desktop.py +0 -0
  201. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/extract_corrections.py +0 -0
  202. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/extract_markdown.py +0 -0
  203. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/extract_whatsapp.py +0 -0
  204. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/frustration_mining.py +0 -0
  205. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/git_overlay.py +0 -0
  206. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/kg_extraction.py +0 -0
  207. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/kg_extraction_groq.py +0 -0
  208. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/longitudinal_analyzer.py +0 -0
  209. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/obsidian_export.py +0 -0
  210. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/operation_grouping.py +0 -0
  211. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/plan_linking.py +0 -0
  212. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/rate_limiter.py +0 -0
  213. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/relation_backfill.py +0 -0
  214. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/relation_verification.py +0 -0
  215. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/sanitize.py +0 -0
  216. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/secret_scrub.py +0 -0
  217. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/semantic_style.py +0 -0
  218. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/sentiment.py +0 -0
  219. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/session_history.py +0 -0
  220. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/style_embed.py +0 -0
  221. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/style_index.py +0 -0
  222. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/tag_entity_promotion.py +0 -0
  223. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/temporal_chains.py +0 -0
  224. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/time_batcher.py +0 -0
  225. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/unified_timeline.py +0 -0
  226. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/pipeline/write_queue.py +0 -0
  227. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/provenance.py +0 -0
  228. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/provenance_autosupersede.py +0 -0
  229. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/provenance_integration.py +0 -0
  230. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/queue_io.py +0 -0
  231. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/queue_merge.py +0 -0
  232. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/reembed_backfill.py +0 -0
  233. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/retired_services.py +0 -0
  234. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/runtime_store.py +0 -0
  235. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/sandbox_db.py +0 -0
  236. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/scoping.py +0 -0
  237. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/scrub_at_rest.py +0 -0
  238. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/search_fanout.py +0 -0
  239. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/search_profile.py +0 -0
  240. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/search_repo.py +0 -0
  241. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/service_supervisor.py +0 -0
  242. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/session_repo.py +0 -0
  243. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/setup.py +0 -0
  244. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/socket_hygiene.py +0 -0
  245. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/source_class_migration.py +0 -0
  246. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/spotlight.py +0 -0
  247. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/storage.py +0 -0
  248. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/store.py +0 -0
  249. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/supervisor_child.py +0 -0
  250. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/supervisor_guard.py +0 -0
  251. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/supervisor_jobs.py +0 -0
  252. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/system_prompt_guard.py +0 -0
  253. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/t3_linkage_alarm.py +0 -0
  254. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/t3_provenance.py +0 -0
  255. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/t3_sources.py +0 -0
  256. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/tag_normalization.py +0 -0
  257. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/taxonomy.json +0 -0
  258. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/telemetry.py +0 -0
  259. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/timestamp_iso.py +0 -0
  260. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/types.py +0 -0
  261. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/vector_store.py +0 -0
  262. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/wal_checkpoint.py +0 -0
  263. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/watcher.py +0 -0
  264. {brainlayer-1.5.76 → brainlayer-1.5.77}/src/brainlayer/watcher_bridge.py +0 -0
  265. {brainlayer-1.5.76 → brainlayer-1.5.77}/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.76
3
+ Version: 1.5.77
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.76"
3
+ version = "1.5.77"
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.76",
6
+ "version": "1.5.77",
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.76",
17
+ "version": "1.5.77",
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.76"
5
+ __version__ = "1.5.77"
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 = "54c580ca6849c2d32628f9ad38d924c54109f055"
@@ -2763,6 +2763,8 @@ def _verified_drive_file_ids(log_path: Path) -> set[str] | None:
2763
2763
  return None
2764
2764
  latest: dict[str, Mapping[str, Any]] = {}
2765
2765
  for receipt in receipts:
2766
+ if receipt.get("event") == "local_sweep":
2767
+ continue
2766
2768
  drive_file = receipt.get("drive_file")
2767
2769
  file_id = drive_file.get("id") if isinstance(drive_file, dict) else None
2768
2770
  if isinstance(file_id, str) and file_id:
@@ -2876,17 +2878,116 @@ def _recent_local_verified_backup(
2876
2878
  return None
2877
2879
 
2878
2880
 
2879
- def _protected_snapshot_names(staging_dir: Path) -> set[str]:
2881
+ def _weekly_pin_protection(staging_dir: Path) -> tuple[set[str], str | None]:
2882
+ """No snapshot-bound completion receipt exists; weekly custody expires the next day."""
2880
2883
  pin = staging_dir / ".weekly-backup-pin.json"
2881
2884
  try:
2882
2885
  payload = json.loads(pin.read_text())
2883
- if payload["date"] == local_backup_date().isoformat():
2884
- return {Path(payload["snapshot"]).name}
2886
+ if dt.date.fromisoformat(payload["date"]) == local_backup_date():
2887
+ return {Path(payload["snapshot"]).name}, "weekly_pin_pending"
2885
2888
  except FileNotFoundError:
2886
2889
  pass
2887
2890
  except (OSError, ValueError, KeyError, TypeError):
2888
- return {p.name for p in staging_dir.iterdir() if _snapshot_suffix(p.name)} # unreadable custody fails closed
2889
- return set()
2891
+ return {
2892
+ p.name for p in staging_dir.iterdir() if _parse_snapshot_date(p.name) == local_backup_date()
2893
+ }, "pin_unreadable"
2894
+ return set(), None
2895
+
2896
+
2897
+ def _protected_snapshot_names(staging_dir: Path) -> set[str]:
2898
+ return _weekly_pin_protection(staging_dir)[0]
2899
+
2900
+
2901
+ def _archive_drive_md5_matches(receipt: Mapping[str, Any]) -> bool:
2902
+ """Local deletion needs both recorded checksums, including Drive's stored-byte MD5."""
2903
+ drive = receipt.get("drive_file")
2904
+ local_md5 = receipt.get("local_md5")
2905
+ return (
2906
+ isinstance(local_md5, str)
2907
+ and bool(local_md5)
2908
+ and isinstance(drive, dict)
2909
+ and drive.get("md5Checksum") == local_md5
2910
+ )
2911
+
2912
+
2913
+ def _sweep_verified_local_archives(
2914
+ log_path: Path, staging_dir: Path, context: Mapping[str, Any], *, remove_local: bool
2915
+ ) -> list[str]:
2916
+ """Remove older archives only with latest, matching Drive proof, under the job's lock."""
2917
+ if not log_path.exists():
2918
+ return []
2919
+ try:
2920
+ receipts = _read_backup_receipts(log_path, strict=True)
2921
+ except (OSError, ValueError):
2922
+ print("Cannot validate Drive receipts; retaining local archives", flush=True)
2923
+ return []
2924
+ latest = {
2925
+ r["snapshot"]: r for r in receipts if r.get("event") != "local_sweep" and isinstance(r.get("snapshot"), str)
2926
+ }
2927
+ last_sweep = {
2928
+ r["archive"]: r for r in receipts if r.get("event") == "local_sweep" and isinstance(r.get("archive"), str)
2929
+ }
2930
+ verified_ids = _verified_drive_file_ids(log_path) or set()
2931
+ protected, pin_reason = _weekly_pin_protection(staging_dir)
2932
+ removed = []
2933
+ for receipt in latest.values():
2934
+ path = Path(receipt["snapshot"])
2935
+ day = _parse_snapshot_date(path.name)
2936
+ if (
2937
+ receipt.get("db") != context.get("db")
2938
+ or path.parent.resolve() != staging_dir.resolve()
2939
+ or day is None
2940
+ or path.name != f"{day.isoformat()}{_snapshot_suffix(path.name)}"
2941
+ or day >= local_backup_date()
2942
+ or path.is_symlink()
2943
+ or not path.is_file()
2944
+ or path.resolve() == Path(context["db"]).resolve()
2945
+ ):
2946
+ continue
2947
+ drive = receipt.get("drive_file")
2948
+ verified = (
2949
+ receipt.get("uploaded") is True
2950
+ and receipt.get("verified") is True
2951
+ and isinstance(drive, dict)
2952
+ and isinstance(drive.get("id"), str)
2953
+ and drive.get("id") in verified_ids
2954
+ and drive.get("name") == path.name
2955
+ and str(drive.get("size")) == str(path.stat().st_size)
2956
+ )
2957
+ reason = (
2958
+ "not_verified"
2959
+ if not verified
2960
+ else "md5_unverified"
2961
+ if not _archive_drive_md5_matches(receipt)
2962
+ else "opt_out"
2963
+ if not remove_local
2964
+ else pin_reason
2965
+ if path.name in protected
2966
+ else None
2967
+ )
2968
+ if reason is None:
2969
+ try:
2970
+ if _md5_file(path) != drive["md5Checksum"]:
2971
+ reason = "md5_current_mismatch"
2972
+ else:
2973
+ path.unlink()
2974
+ except OSError as exc:
2975
+ print(f"Cannot delete local archive {path}: {exc}", flush=True)
2976
+ reason = "delete_failed"
2977
+ else:
2978
+ if reason is None:
2979
+ removed.append(path.name)
2980
+ # Local housekeeping cannot grant/revoke Drive verification or refresh last-backup status.
2981
+ update = {
2982
+ "event": "local_sweep",
2983
+ "archive": str(path),
2984
+ "at": dt.datetime.now(dt.UTC).isoformat(),
2985
+ "local_removed": reason is None,
2986
+ "local_removed_skip_reason": reason,
2987
+ }
2988
+ if reason is None or last_sweep.get(str(path), {}).get("local_removed_skip_reason") != reason:
2989
+ _append_json_log(log_path, update)
2990
+ return removed
2890
2991
 
2891
2992
 
2892
2993
  def _reclaim_superseded_local(log_path: Path, result: dict[str, Any], staging_dir: Path) -> None:
@@ -3191,6 +3292,7 @@ def run_backup(
3191
3292
  "db": str(resolved_db_path),
3192
3293
  "uploaded": False,
3193
3294
  "local_removed": False,
3295
+ "local_removed_skip_reason": "not_verified",
3194
3296
  "verified": False,
3195
3297
  "backup_log_provenance": _backup_log_provenance(),
3196
3298
  "attempt_reclamation": "unknown",
@@ -3232,6 +3334,9 @@ def run_backup(
3232
3334
  "machine_id": resolved_machine_id,
3233
3335
  }
3234
3336
  )
3337
+ result["local_gzip_retention_deleted"] = _sweep_verified_local_archives(
3338
+ resolved_log_path, Path(staging_dir), result, remove_local=remove_local_after_upload
3339
+ )
3235
3340
  result["consumed_raw_crash_leftovers_deleted"] = _cleanup_consumed_raw_remnants(Path(staging_dir))
3236
3341
  if resolved_log_path.exists():
3237
3342
  try:
@@ -3533,6 +3638,16 @@ def run_backup(
3533
3638
  )
3534
3639
  result["upload_state"] = "verified" if result["verified"] else "failed"
3535
3640
  if result["verified"]:
3641
+ protected, pin_reason = _weekly_pin_protection(snapshot.parent)
3642
+ result["local_removed_skip_reason"] = (
3643
+ "md5_unverified"
3644
+ if not _archive_drive_md5_matches(result)
3645
+ else "opt_out"
3646
+ if not remove_local_after_upload
3647
+ else pin_reason
3648
+ if snapshot.name in protected
3649
+ else None
3650
+ )
3536
3651
  _append_json_log(resolved_log_path, result) # remote proof precedes reclamation
3537
3652
  _reclaim_superseded_local(resolved_log_path, result, snapshot.parent)
3538
3653
  from . import job_alerts
@@ -3543,23 +3658,16 @@ def run_backup(
3543
3658
  result["temp_dirs_removed_after_verification"] = _prune_owned_temps_after_verified(
3544
3659
  snapshot.parent, snapshot
3545
3660
  )
3546
- if remove_local_after_upload and snapshot.name not in _protected_snapshot_names(snapshot.parent):
3547
- snapshot.unlink()
3548
- result["local_removed"] = True
3661
+ if result["local_removed_skip_reason"] is None:
3662
+ try:
3663
+ snapshot.unlink()
3664
+ except OSError as exc:
3665
+ print(f"Cannot delete local archive {snapshot}: {exc}", flush=True)
3666
+ result["local_removed_skip_reason"] = "delete_failed"
3667
+ else:
3668
+ result["local_removed"] = True
3549
3669
  if resume is None:
3550
3670
  _remove_superseded_stalls(stalled, result, snapshot.name[:10])
3551
- verified_drive_names = _verified_snapshot_names_from_log(
3552
- resolved_log_path, db_path=Path(resolved_db_path)
3553
- )
3554
- verified_drive_names.add(snapshot.name)
3555
- result["local_gzip_retention_deleted"] = prune_local_compressed_snapshots(
3556
- snapshot.parent,
3557
- verified_drive_names=verified_drive_names,
3558
- protected_names=_protected_snapshot_names(snapshot.parent)
3559
- | {
3560
- name for name in _compressed_snapshot_names(snapshot.parent) if name not in verified_drive_names
3561
- },
3562
- )
3563
3671
  if retention_enabled:
3564
3672
  try:
3565
3673
  result["retention_deleted"] = prune_drive_backups(
@@ -3620,6 +3728,13 @@ def run_backup(
3620
3728
  result.setdefault("error_code", "drive_upload_interrupted")
3621
3729
  if not nothing_pending:
3622
3730
  _append_json_log(resolved_log_path, result)
3731
+ swept = _sweep_verified_local_archives(
3732
+ resolved_log_path, Path(staging_dir), result, remove_local=remove_local_after_upload
3733
+ )
3734
+ if swept:
3735
+ result["local_gzip_retention_deleted"].extend(swept)
3736
+ if not nothing_pending:
3737
+ _append_json_log(resolved_log_path, result)
3623
3738
  return result
3624
3739
 
3625
3740
 
@@ -1415,23 +1415,24 @@ def _run_health_check_locked(
1415
1415
  }
1416
1416
  )
1417
1417
 
1418
- if config.heal:
1419
- from .supervisor_adapters import adapt_commands
1420
-
1421
- command_runner = adapt_commands(
1422
- command_runner,
1423
- config.pause_sentinel_path,
1424
- required=(
1425
- config.watch_label,
1426
- config.drain_label,
1427
- config.hotlane_label,
1428
- config.health_check_label,
1429
- config.observability_label,
1430
- ),
1431
- tolerate_unavailable=True,
1432
- )
1433
- if error := getattr(command_runner, "control_error", ""):
1434
- add_issue("supervisor_control_unavailable", "critical", f"supervisor control unavailable: {error}")
1418
+ from .supervisor_adapters import adapt_commands
1419
+
1420
+ command_runner = adapt_commands(
1421
+ command_runner,
1422
+ config.pause_sentinel_path,
1423
+ required=(
1424
+ config.watch_label,
1425
+ config.drain_label,
1426
+ config.hotlane_label,
1427
+ config.health_check_label,
1428
+ config.observability_label,
1429
+ ),
1430
+ tolerate_unavailable=True,
1431
+ discover_ownership=True,
1432
+ )
1433
+ if error := getattr(command_runner, "control_error", ""):
1434
+ add_issue("supervisor_control_unavailable", "critical", f"supervisor control unavailable: {error}")
1435
+ if config.heal:
1435
1436
  result.actions.append("heal_failed:supervisor_control_unavailable")
1436
1437
 
1437
1438
  if state.get("_state_corrupt_now"):
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import json
4
4
  import math
5
5
  import os
6
+ import plistlib
6
7
  import re
7
8
  import subprocess
8
9
  from datetime import UTC, datetime
@@ -301,6 +302,27 @@ def _unmeasurable(reason: str, inputs: list[dict[str, Any]]) -> dict[str, Any]:
301
302
  return {"state": "unmeasurable", "reason": reason, "inputs": inputs}
302
303
 
303
304
 
305
+ def _maintenance_schedule_error(path: Path) -> str | None:
306
+ """A loaded parent is usable schedule evidence only with a readable Hour/Minute calendar."""
307
+ try:
308
+ with path.open("rb") as handle:
309
+ definition = plistlib.load(handle)
310
+ except Exception as exc:
311
+ reason = f"cannot read plist ({type(exc).__name__})"
312
+ else:
313
+ interval = definition.get("StartCalendarInterval") if isinstance(definition, dict) else None
314
+ if (
315
+ isinstance(interval, dict)
316
+ and type(interval.get("Hour")) is int
317
+ and 0 <= interval["Hour"] <= 23
318
+ and type(interval.get("Minute")) is int
319
+ and 0 <= interval["Minute"] <= 59
320
+ ):
321
+ return None
322
+ reason = "expected plist dictionary with StartCalendarInterval Hour (0-23) and Minute (0-59)"
323
+ return f"maintenance_schedule_unavailable: {path}: {reason}"
324
+
325
+
304
326
  def build_backups_section(
305
327
  *,
306
328
  env: Mapping[str, str],
@@ -321,6 +343,7 @@ def build_backups_section(
321
343
  launchd_override = env.get("BRAINLAYER_OBSERVABILITY_LAUNCHD_OUTPUT")
322
344
  observed_label, extra_inputs = LABEL, []
323
345
  launchd_not_loaded = False
346
+ schedule_error: str | None = None
324
347
  command_failure: str | None = None
325
348
  if launchd_override:
326
349
  launchd_path = Path(launchd_override).expanduser()
@@ -368,6 +391,7 @@ def build_backups_section(
368
391
  )
369
392
  )
370
393
  observed_label = "com.brainlayer.maintenance"
394
+ schedule_error = _maintenance_schedule_error(agents / f"{observed_label}.plist")
371
395
  argv = ["launchctl", "print", f"gui/{os.getuid()}/{observed_label}"]
372
396
  completed = subprocess.run(argv, capture_output=True, text=False, timeout=5, check=False, shell=False)
373
397
  exit_code = completed.returncode
@@ -491,6 +515,11 @@ def build_backups_section(
491
515
  if error_type is None and db_reason is not None:
492
516
  error_type = db_reason
493
517
  bootstrapped = not launchd_not_loaded and observed_label in launchd_text
518
+ if schedule_error is not None:
519
+ bootstrapped = False
520
+ # Schedule uncertainty must not discard dated receipts or mask an existing failure.
521
+ if not error_type:
522
+ error_type = schedule_error
494
523
 
495
524
  # Optional evidence, never synthesized from a missing upload or a zero archive count.
496
525
  evidence: dict[str, Any] = {}
@@ -50,6 +50,48 @@ ARCHIVED = {
50
50
  }
51
51
 
52
52
 
53
+ def supervisor_owns_jobs() -> bool:
54
+ """Read the canonical migration owner with BrainBarJobOwnership's custody rules."""
55
+ path = Path.home() / ".local/share/brainlayer/supervisor/migration.json"
56
+ limit = 4 * 1024 * 1024
57
+ fd = os.open("/", os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC)
58
+ try:
59
+ parts = path.parts[1:]
60
+ for index, part in enumerate(parts):
61
+ if part in {".", ".."}:
62
+ raise ValueError("unsafe migration journal path")
63
+ flags = os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK | os.O_CLOEXEC
64
+ if index < len(parts) - 1:
65
+ flags |= os.O_DIRECTORY
66
+ next_fd = os.open(part, flags, dir_fd=fd)
67
+ os.close(fd)
68
+ fd = next_fd
69
+ info = os.fstat(fd)
70
+ if (
71
+ not stat.S_ISREG(info.st_mode)
72
+ or info.st_uid != os.getuid()
73
+ or info.st_nlink != 1
74
+ or info.st_mode & 0o077
75
+ or info.st_size > limit
76
+ ):
77
+ raise PermissionError("migration journal is untrusted")
78
+ with os.fdopen(os.dup(fd), "rb") as stream:
79
+ raw = stream.read(limit + 1)
80
+ if len(raw) > limit:
81
+ raise ValueError("migration journal exceeds read limit")
82
+ row = json.loads(raw)
83
+ phase = row.get("phase") if isinstance(row, dict) else None
84
+ if isinstance(phase, str) and phase in {"migrating", "migrated", "reverting"}:
85
+ return True
86
+ if isinstance(phase, str) and phase in {"reverted", "rolled_back"}:
87
+ return False
88
+ raise ValueError("migration journal has unknown phase")
89
+ except FileNotFoundError:
90
+ return False
91
+ finally:
92
+ os.close(fd)
93
+
94
+
53
95
  class SupervisorCommands:
54
96
  """Adapt legacy *policy*, never launch archived component plists.
55
97
 
@@ -99,6 +141,15 @@ class SupervisorCommands:
99
141
  or type(value["exit_timeout"]) not in {int, float}
100
142
  or not math.isfinite(value["exit_timeout"])
101
143
  or value["exit_timeout"] < 0
144
+ or type(value.get("interval", 0)) not in {int, float}
145
+ or not math.isfinite(value.get("interval", 0))
146
+ or value.get("interval", 0) < 0
147
+ or value.get("next_run_at") is not None
148
+ and (
149
+ type(value["next_run_at"]) not in {int, float}
150
+ or not math.isfinite(value["next_run_at"])
151
+ or value["next_run_at"] < 0
152
+ )
102
153
  or value["pid"] is not None
103
154
  and (type(value["pid"]) is not int or value["pid"] <= 1)
104
155
  ):
@@ -168,15 +219,30 @@ class SupervisorCommands:
168
219
  raise PermissionError("wrong launchd target")
169
220
  if verb == "print":
170
221
  state = states[name]
222
+ # An interval job remains loaded between runs, including a health
223
+ # check that exits 1 to report issues. A dead worker with no next
224
+ # scheduled run must remain unhealthy.
225
+ down = (
226
+ state["configured"]
227
+ and not state["paused"]
228
+ and not state["pid"]
229
+ and not (state.get("interval") and state.get("next_run_at") is not None)
230
+ )
171
231
  return result(
232
+ code=113 if down else 0,
172
233
  out=f"state = {'running' if state['pid'] else 'waiting'}\nruns = {state['starts']}\n"
173
- + (f"pid = {state['pid']}\n" if state["pid"] else "")
234
+ + (f"pid = {state['pid']}\n" if state["pid"] else ""),
174
235
  )
175
236
  if verb == "bootstrap":
176
237
  if args[2] != self.domain:
177
238
  raise PermissionError("wrong launchd domain")
178
- action = "resume"
239
+ state = states[name]
240
+ # Resume only selects paused children. Heal an already-unpaused
241
+ # dead worker directly, without creating an owner-less pause.
242
+ action = "restart" if state["configured"] and not state["paused"] and not state["pid"] else "resume"
179
243
  elif verb == "bootout":
244
+ if not states[name]["pid"]:
245
+ return result()
180
246
  action = "pause"
181
247
  elif verb == "kickstart":
182
248
  action = "restart"
@@ -201,9 +267,18 @@ class SupervisorCommands:
201
267
  return result(127, error=f"supervisor control failed: {exc}")
202
268
 
203
269
 
204
- def adapt_commands(runner, pause_path=None, *, required=(), tolerate_unavailable=False):
270
+ def adapt_commands(runner, pause_path=None, *, required=(), tolerate_unavailable=False, discover_ownership=False):
205
271
  path = os.environ.get("BRAINLAYER_SUPERVISOR_SOCKET")
206
- if not path or isinstance(runner, SupervisorCommands):
272
+ if isinstance(runner, SupervisorCommands):
273
+ return runner
274
+ ownership_error = ""
275
+ if discover_ownership:
276
+ try:
277
+ if not supervisor_owns_jobs():
278
+ return runner
279
+ except (OSError, ValueError) as exc:
280
+ ownership_error = f"can't determine job owner: {exc}"
281
+ elif not path:
207
282
  return runner
208
283
  backend = SupervisorCommands(
209
284
  supervisor_socket_path(),
@@ -211,6 +286,8 @@ def adapt_commands(runner, pause_path=None, *, required=(), tolerate_unavailable
211
286
  pause_path=pause_path or Path(os.environ.get("BRAINLAYER_PAUSE_SENTINEL", str(DEFAULT_PAUSE_SENTINEL_PATH))),
212
287
  )
213
288
  try:
289
+ if ownership_error:
290
+ raise RuntimeError(ownership_error)
214
291
  states = backend.request()
215
292
  if any(backend.aliases[label] not in states for label in required if label in backend.aliases):
216
293
  raise RuntimeError("required supervisor component is unavailable")
@@ -1 +0,0 @@
1
- BUILD_SHA = "184525f889a9552f6f08a5f3fe499b41f3fbc6dc"
File without changes
File without changes
File without changes