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.
- {brainlayer-1.5.87 → brainlayer-1.5.89}/PKG-INFO +1 -1
- {brainlayer-1.5.87 → brainlayer-1.5.89}/pyproject.toml +1 -1
- {brainlayer-1.5.87 → brainlayer-1.5.89}/server.json +2 -2
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/__init__.py +1 -1
- brainlayer-1.5.89/src/brainlayer/_build.py +1 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/backup_daily.py +18 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/cli/__init__.py +8 -0
- brainlayer-1.5.89/src/brainlayer/job_alerts.py +156 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/launchd_consolidation.py +119 -0
- brainlayer-1.5.89/src/brainlayer/launcher_policy_reconcile.py +75 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/mcp_stdio_bridge.py +11 -1
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/wal_checkpoint.py +23 -1
- brainlayer-1.5.87/src/brainlayer/_build.py +0 -1
- brainlayer-1.5.87/src/brainlayer/job_alerts.py +0 -100
- {brainlayer-1.5.87 → brainlayer-1.5.89}/.gitignore +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/LICENSE +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/README.md +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/hotlane_brainbar_daemon.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/backup-daily.sh +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/brainlayer-env-run.sh +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/brainlayer.env.example +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/com.brainlayer.backup-daily.plist +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/com.brainlayer.daemon.plist +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/com.brainlayer.decay.plist +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/com.brainlayer.drain.plist +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/com.brainlayer.health-check.plist +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/com.brainlayer.hotlane-brainbar.plist +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/com.brainlayer.index.plist +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/com.brainlayer.jsonl-backup.plist +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/com.brainlayer.log-cap.plist +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/com.brainlayer.maintenance-nightly.plist +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/com.brainlayer.maintenance-weekly.plist +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/com.brainlayer.maintenance.plist +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/com.brainlayer.observability.plist +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/com.brainlayer.p0-counter.plist +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/com.brainlayer.repair-fts.plist +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/com.brainlayer.t3-ingest.plist +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/com.brainlayer.throughput-watchdog.plist +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/com.brainlayer.tier0-watchdog.plist +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/com.brainlayer.wal-checkpoint.plist +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/com.brainlayer.watch.plist +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/com.etanhey.brainlayer-fleet-watchdog.plist +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/fleet-watchdog.sh +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/install.sh +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/jsonl-backup.sh +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/launchd/throughput-watchdog.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/release-verify-signatures.sh +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/scripts/tier0-watchdog.sh +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/__main__.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/_helpers.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/agent_profiles.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/agent_provenance.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/alarm.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/archive_collapse.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/archive_lifecycle.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/backup_retention_invariant.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/badge_state.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/bitemporal.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/brainbar_activation.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/brainbar_auto_activation.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/brainbar_hybrid_helper.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/brainbar_identity.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/cachedelete_purge.swift +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/calibrate.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/chunk_origin.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/chunk_origin_backfill.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/chunk_origin_wipe.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/chunk_write.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/classify.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/claude_paths.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/cli/wizard.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/cli_jobs.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/cli_new.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/cloud_backfill.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/clustering.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/component_jobs.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/config.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/consolidation_plan.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/content_class.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/correction_judge.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/data/agents_registry.yaml +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/data/sandbox_seeds/skill-eval-baseline.json +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/db_shrink.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/decay.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/decay_backfill.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/decay_job.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/dedupe.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/dedupe_merge.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/deploy_drift.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/disk_space.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/doctor.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/drain.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/drain_liveness.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/drive_credentials.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/embeddings.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/engine.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/enrichment_replay.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/__init__.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/abcde_enrich_runner.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/abcde_variants.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/abcde_variants.yaml +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/benchmark.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/enrichment_gold.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/enrichment_graders.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/enrichment_judge.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/enrichment_llm_judge.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/enrichment_quality_benchmark.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/experiment_store.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/fixtures/README.md +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/labeling/enrichment-labeler.html +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/phoenix_gate/__init__.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/phoenix_gate/__main__.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/phoenix_gate/baseline_store.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/phoenix_gate/cli.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/phoenix_gate/models.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/phoenix_gate/phoenix_client.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/phoenix_gate/regression_gate.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/phoenix_gate/triggers.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/eval/pilot_artifact_report.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/fallback_replay.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/git_learning.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/health_check.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/health_facts.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/health_verdict.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/hook_python.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/hooks/__init__.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/hooks/indexer.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/hotlane_budget.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/import_sweep.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/index_admission.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/index_budget.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/index_completeness.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/index_new.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/index_watchdog.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/ingest/__init__.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/ingest/codex.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/ingest/t3.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/ingest_denylist.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/ingest_guard.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/isolation_proof.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/job_lifecycle_health.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/jobs.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/jsonl_backup.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/kg/__init__.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/kg_cleanup.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/kg_judge.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/kg_promotion.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/kg_repo.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/kg_review_session.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/kg_session_finish.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/kg_session_harvest.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/launchd_install.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/launchd_primitive.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/launchd_revert.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/lexical_defense.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/lexical_defense_dictionary.json +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/linkedin_import.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/log_cap.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/maintenance.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/maintenance_calendar.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/maintenance_control.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/mcp/__init__.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/mcp/_format.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/mcp/_shared.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/mcp/enrich_handler.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/mcp/entity_handler.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/mcp/palette.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/mcp/search_handler.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/mcp/store_handler.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/mcp/tags_handler.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/mcp_socket_config.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/memory_types.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/migrate.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/migration_custody.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/migration_journal.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/migration_runtime.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/notification_policy.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/observability_backup.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/observability_surface.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/observability_transport.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/p0_longitudinal_count.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/parent_death.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/paths.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pause.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/phonetic.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/__init__.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/agent_enrichment.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/analyze_communication.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/batch_extraction.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/brain_graph.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/chat_tags.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/chunk.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/classify.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/cloud_scrub.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/cluster_sampling.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/code_intelligence.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/correction_detection.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/correction_mining.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/digest.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/enrichment_prompts.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/enrichment_results.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/enrichment_tiers.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/entity_extraction.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/entity_resolution.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/extract.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/extract_claude_desktop.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/extract_corrections.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/extract_markdown.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/extract_whatsapp.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/frustration_mining.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/git_overlay.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/kg_extraction.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/kg_extraction_groq.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/longitudinal_analyzer.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/obsidian_export.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/operation_grouping.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/plan_linking.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/rate_limiter.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/relation_backfill.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/relation_verification.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/sanitize.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/secret_scrub.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/semantic_style.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/sentiment.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/session_history.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/style_embed.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/style_index.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/tag_entity_promotion.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/temporal_chains.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/time_batcher.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/unified_timeline.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/pipeline/write_queue.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/policy_reconcile.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/provenance.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/provenance_autosupersede.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/provenance_integration.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/queue_io.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/queue_merge.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/readonly_snapshot.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/reembed_backfill.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/retired_services.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/runtime_store.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/sandbox_db.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/scoping.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/scrub_at_rest.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/search_fanout.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/search_profile.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/search_repo.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/service_supervisor.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/session_repo.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/setup.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/socket_hygiene.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/source_class_migration.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/spotlight.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/storage.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/store.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/supervisor_adapters.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/supervisor_child.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/supervisor_diagnostics.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/supervisor_guard.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/supervisor_jobs.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/system_prompt_guard.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/t3_linkage_alarm.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/t3_provenance.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/t3_sources.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/tag_normalization.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/taxonomy.json +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/telemetry.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/timestamp_iso.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/types.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/vector_store.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/watcher.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/watcher_bridge.py +0 -0
- {brainlayer-1.5.87 → brainlayer-1.5.89}/src/brainlayer/writer_policy.py +0 -0
- {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.
|
|
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
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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"
|