brainlayer 1.5.80__tar.gz → 1.5.82__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.80 → brainlayer-1.5.82}/PKG-INFO +1 -1
- {brainlayer-1.5.80 → brainlayer-1.5.82}/pyproject.toml +1 -1
- {brainlayer-1.5.80 → brainlayer-1.5.82}/server.json +2 -2
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/__init__.py +1 -1
- brainlayer-1.5.82/src/brainlayer/_build.py +1 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/backup_daily.py +124 -7
- brainlayer-1.5.82/src/brainlayer/linkedin_import.py +365 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/observability_backup.py +13 -2
- brainlayer-1.5.80/src/brainlayer/_build.py +0 -1
- {brainlayer-1.5.80 → brainlayer-1.5.82}/.gitignore +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/LICENSE +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/README.md +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/scripts/hotlane_brainbar_daemon.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/scripts/launchd/backup-daily.sh +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/scripts/launchd/brainlayer-env-run.sh +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/scripts/launchd/brainlayer.env.example +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/scripts/launchd/com.brainlayer.backup-daily.plist +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/scripts/launchd/com.brainlayer.daemon.plist +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/scripts/launchd/com.brainlayer.decay.plist +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/scripts/launchd/com.brainlayer.drain.plist +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/scripts/launchd/com.brainlayer.health-check.plist +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/scripts/launchd/com.brainlayer.hotlane-brainbar.plist +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/scripts/launchd/com.brainlayer.index.plist +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/scripts/launchd/com.brainlayer.jsonl-backup.plist +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/scripts/launchd/com.brainlayer.log-cap.plist +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/scripts/launchd/com.brainlayer.maintenance-nightly.plist +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/scripts/launchd/com.brainlayer.maintenance-weekly.plist +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/scripts/launchd/com.brainlayer.maintenance.plist +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/scripts/launchd/com.brainlayer.observability.plist +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/scripts/launchd/com.brainlayer.p0-counter.plist +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/scripts/launchd/com.brainlayer.repair-fts.plist +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/scripts/launchd/com.brainlayer.t3-ingest.plist +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/scripts/launchd/com.brainlayer.throughput-watchdog.plist +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/scripts/launchd/com.brainlayer.tier0-watchdog.plist +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/scripts/launchd/com.brainlayer.wal-checkpoint.plist +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/scripts/launchd/com.brainlayer.watch.plist +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/scripts/launchd/com.etanhey.brainlayer-fleet-watchdog.plist +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/scripts/launchd/fleet-watchdog.sh +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/scripts/launchd/install.sh +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/scripts/launchd/jsonl-backup.sh +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/scripts/launchd/throughput-watchdog.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/scripts/release-verify-signatures.sh +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/scripts/tier0-watchdog.sh +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/__main__.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/_helpers.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/agent_profiles.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/agent_provenance.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/alarm.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/archive_collapse.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/archive_lifecycle.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/backup_retention_invariant.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/badge_state.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/bitemporal.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/brainbar_activation.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/brainbar_auto_activation.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/brainbar_hybrid_helper.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/brainbar_identity.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/cachedelete_purge.swift +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/calibrate.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/chunk_origin.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/chunk_origin_backfill.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/chunk_origin_wipe.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/chunk_write.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/classify.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/claude_paths.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/cli/__init__.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/cli/wizard.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/cli_jobs.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/cli_new.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/cloud_backfill.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/clustering.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/component_jobs.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/config.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/consolidation_plan.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/content_class.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/correction_judge.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/data/agents_registry.yaml +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/data/sandbox_seeds/skill-eval-baseline.json +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/db_shrink.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/decay.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/decay_backfill.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/decay_job.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/dedupe.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/dedupe_merge.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/deploy_drift.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/disk_space.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/doctor.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/drain.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/drain_liveness.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/drive_credentials.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/embeddings.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/engine.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/enrichment_replay.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/eval/__init__.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/eval/abcde_enrich_runner.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/eval/abcde_variants.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/eval/abcde_variants.yaml +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/eval/benchmark.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/eval/enrichment_gold.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/eval/enrichment_graders.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/eval/enrichment_judge.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/eval/enrichment_llm_judge.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/eval/enrichment_quality_benchmark.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/eval/experiment_store.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/eval/fixtures/README.md +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/eval/labeling/enrichment-labeler.html +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/eval/phoenix_gate/__init__.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/eval/phoenix_gate/__main__.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/eval/phoenix_gate/baseline_store.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/eval/phoenix_gate/cli.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/eval/phoenix_gate/models.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/eval/phoenix_gate/phoenix_client.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/eval/phoenix_gate/regression_gate.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/eval/phoenix_gate/triggers.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/eval/pilot_artifact_report.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/fallback_replay.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/git_learning.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/health_check.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/hook_python.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/hooks/__init__.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/hooks/indexer.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/import_sweep.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/index_admission.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/index_budget.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/index_completeness.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/index_new.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/index_watchdog.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/ingest/__init__.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/ingest/codex.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/ingest/t3.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/ingest_denylist.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/ingest_guard.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/isolation_proof.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/job_alerts.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/job_lifecycle_health.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/jobs.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/jsonl_backup.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/kg/__init__.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/kg_cleanup.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/kg_judge.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/kg_promotion.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/kg_repo.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/kg_review_session.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/kg_session_finish.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/kg_session_harvest.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/launchd_consolidation.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/launchd_install.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/launchd_primitive.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/launchd_revert.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/lexical_defense.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/lexical_defense_dictionary.json +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/log_cap.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/maintenance.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/maintenance_calendar.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/maintenance_control.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/mcp/__init__.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/mcp/_format.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/mcp/_shared.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/mcp/enrich_handler.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/mcp/entity_handler.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/mcp/palette.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/mcp/search_handler.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/mcp/store_handler.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/mcp/tags_handler.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/mcp_socket_config.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/mcp_stdio_bridge.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/memory_types.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/migrate.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/migration_custody.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/migration_journal.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/migration_runtime.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/notification_policy.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/observability_surface.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/observability_transport.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/p0_longitudinal_count.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/parent_death.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/paths.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pause.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/phonetic.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/__init__.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/agent_enrichment.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/analyze_communication.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/batch_extraction.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/brain_graph.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/chat_tags.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/chunk.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/classify.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/cloud_scrub.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/cluster_sampling.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/code_intelligence.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/correction_detection.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/correction_mining.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/digest.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/enrichment_prompts.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/enrichment_results.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/enrichment_tiers.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/entity_extraction.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/entity_resolution.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/extract.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/extract_claude_desktop.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/extract_corrections.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/extract_markdown.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/extract_whatsapp.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/frustration_mining.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/git_overlay.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/kg_extraction.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/kg_extraction_groq.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/longitudinal_analyzer.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/obsidian_export.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/operation_grouping.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/plan_linking.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/rate_limiter.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/relation_backfill.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/relation_verification.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/sanitize.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/secret_scrub.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/semantic_style.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/sentiment.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/session_history.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/style_embed.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/style_index.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/tag_entity_promotion.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/temporal_chains.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/time_batcher.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/unified_timeline.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/pipeline/write_queue.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/provenance.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/provenance_autosupersede.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/provenance_integration.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/queue_io.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/queue_merge.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/reembed_backfill.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/retired_services.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/runtime_store.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/sandbox_db.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/scoping.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/scrub_at_rest.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/search_fanout.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/search_profile.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/search_repo.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/service_supervisor.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/session_repo.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/setup.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/socket_hygiene.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/source_class_migration.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/spotlight.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/storage.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/store.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/supervisor_adapters.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/supervisor_child.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/supervisor_diagnostics.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/supervisor_guard.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/supervisor_jobs.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/system_prompt_guard.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/t3_linkage_alarm.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/t3_provenance.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/t3_sources.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/tag_normalization.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/taxonomy.json +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/telemetry.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/timestamp_iso.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/types.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/vector_store.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/wal_checkpoint.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/watcher.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/watcher_bridge.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/src/brainlayer/writer_policy.py +0 -0
- {brainlayer-1.5.80 → brainlayer-1.5.82}/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.82
|
|
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.82",
|
|
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.82",
|
|
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.82"
|
|
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 = "9b6e1c75b4bed7440161989df15eb49ccabca0db"
|
|
@@ -16,6 +16,7 @@ import inspect
|
|
|
16
16
|
import json
|
|
17
17
|
import math
|
|
18
18
|
import os
|
|
19
|
+
import plistlib
|
|
19
20
|
import queue
|
|
20
21
|
import re
|
|
21
22
|
import shutil
|
|
@@ -37,6 +38,7 @@ from dataclasses import dataclass
|
|
|
37
38
|
from pathlib import Path
|
|
38
39
|
from typing import Any
|
|
39
40
|
from urllib.parse import urlsplit
|
|
41
|
+
from xml.parsers.expat import ExpatError
|
|
40
42
|
|
|
41
43
|
import requests
|
|
42
44
|
|
|
@@ -45,6 +47,7 @@ from .paths import get_db_path, resolve_db_path
|
|
|
45
47
|
from .socket_hygiene import refuse_production_brainbar_socket
|
|
46
48
|
|
|
47
49
|
_sleep = time.sleep
|
|
50
|
+
_reclaim_clock = time.monotonic
|
|
48
51
|
|
|
49
52
|
DEFAULT_TOKEN_PATH = drive_credentials.DEFAULT_TOKEN_PATH
|
|
50
53
|
DEFAULT_CLIENT_PATH = drive_credentials.DEFAULT_CLIENT_PATH
|
|
@@ -61,6 +64,8 @@ DEFAULT_STAGING_DIR = Path.home() / ".local" / "share" / "brainlayer" / "backups
|
|
|
61
64
|
MIN_RAW_FREE_BYTES = 2 * 1024 * 1024 * 1024
|
|
62
65
|
COPY_CHUNK_BYTES = 1024 * 1024
|
|
63
66
|
RECLAIM_CHUNK_BYTES = 64 * 1024 * 1024
|
|
67
|
+
RECLAIM_WINDOW_SECONDS = 2.0
|
|
68
|
+
RECLAIM_MAX_SAMPLES = 5
|
|
64
69
|
FRAME_OVERHEAD_BYTES = 64 * 1024
|
|
65
70
|
POST_PURGE_POLL_SECONDS = 0.5 # 60 polls give APFS up to 30s to release reclaimed bytes.
|
|
66
71
|
DEFAULT_LOG_PATH = Path.home() / ".local" / "share" / "brainlayer" / "logs" / "backup-daily.log"
|
|
@@ -170,6 +175,7 @@ class SQLiteBackupArtifact:
|
|
|
170
175
|
compressor_fallback: str | None = None
|
|
171
176
|
compress_duration_seconds: float | None = None
|
|
172
177
|
raw_sha256: str | None = None
|
|
178
|
+
reclaim_basis: str | None = None
|
|
173
179
|
|
|
174
180
|
@property
|
|
175
181
|
def archive_path(self) -> Path:
|
|
@@ -1442,7 +1448,102 @@ def _archive_stream_sha256(source: Any, *, zstd: bool) -> str:
|
|
|
1442
1448
|
return _stream_sha256(restored).hex()
|
|
1443
1449
|
|
|
1444
1450
|
|
|
1445
|
-
def
|
|
1451
|
+
def _staging_snapshots_may_exist(path: Path) -> bool | None:
|
|
1452
|
+
"""Read the staging volume's APFS snapshot inventory; uncertainty keeps the volume gate."""
|
|
1453
|
+
if sys.platform != "darwin":
|
|
1454
|
+
return None
|
|
1455
|
+
try:
|
|
1456
|
+
# df resolves the device even through macOS firmlinks (/Users is on Data).
|
|
1457
|
+
# Use only its device identity, never its free/purgeable space accounting.
|
|
1458
|
+
rows = (
|
|
1459
|
+
subprocess.run(["/bin/df", "-P", str(path)], capture_output=True, check=True, timeout=5)
|
|
1460
|
+
.stdout.decode()
|
|
1461
|
+
.splitlines()
|
|
1462
|
+
)
|
|
1463
|
+
device = rows[1].split()[0] if len(rows) == 2 else ""
|
|
1464
|
+
if not re.fullmatch(r"/dev/disk\d+s\d+", device):
|
|
1465
|
+
raise ValueError("staging device is ambiguous")
|
|
1466
|
+
result = subprocess.run(
|
|
1467
|
+
["/usr/sbin/diskutil", "apfs", "listSnapshots", device.removeprefix("/dev/"), "-plist"],
|
|
1468
|
+
capture_output=True,
|
|
1469
|
+
check=True,
|
|
1470
|
+
timeout=5,
|
|
1471
|
+
)
|
|
1472
|
+
inventory = plistlib.loads(result.stdout)
|
|
1473
|
+
if not isinstance(inventory, dict) or inventory.get("Error") or inventory.get("ExitCode", 0) != 0:
|
|
1474
|
+
raise ValueError("snapshot inventory reported an error")
|
|
1475
|
+
snapshots = inventory.get("Snapshots")
|
|
1476
|
+
if not isinstance(snapshots, list):
|
|
1477
|
+
raise ValueError("snapshot inventory is ambiguous")
|
|
1478
|
+
return bool(snapshots)
|
|
1479
|
+
except BackupTimeoutError:
|
|
1480
|
+
raise # Preserve the enclosing backup deadline, rather than treating it as probe uncertainty.
|
|
1481
|
+
except (OSError, subprocess.SubprocessError, ValueError, IndexError, ExpatError) as exc:
|
|
1482
|
+
print(f"Backup snapshot probe unknown: {type(exc).__name__}; retaining volume reclaim check", flush=True)
|
|
1483
|
+
return None
|
|
1484
|
+
|
|
1485
|
+
|
|
1486
|
+
class RawReclaimUnconfirmedError(RuntimeError):
|
|
1487
|
+
error_type = "raw_reclaim_unconfirmed"
|
|
1488
|
+
|
|
1489
|
+
def __init__(self, measurements: dict[str, Any]):
|
|
1490
|
+
self.measurements = measurements
|
|
1491
|
+
best_delta = max(
|
|
1492
|
+
(
|
|
1493
|
+
sample["delta_bytes"]
|
|
1494
|
+
for sample in measurements["samples"]
|
|
1495
|
+
if sample["elapsed_seconds"] <= measurements["window_seconds"]
|
|
1496
|
+
),
|
|
1497
|
+
default=0,
|
|
1498
|
+
)
|
|
1499
|
+
mib = 1024 * 1024
|
|
1500
|
+
super().__init__(
|
|
1501
|
+
"Database backup stopped: raw file blocks were released, but free space rose only "
|
|
1502
|
+
f"{best_delta / mib:g} MiB of the required {measurements['threshold_bytes'] / mib:g} MiB "
|
|
1503
|
+
f"within {measurements['window_seconds']:g} s "
|
|
1504
|
+
"(another process may be writing, or macOS is slow to report freed space)."
|
|
1505
|
+
)
|
|
1506
|
+
|
|
1507
|
+
|
|
1508
|
+
def _confirm_raw_reclaim(
|
|
1509
|
+
path: Path, *, offset: int, length: int, before_blocks: int, after_blocks: int, before_free: int
|
|
1510
|
+
) -> None:
|
|
1511
|
+
"""Keep one pre-punch baseline; bound both sample count and elapsed waiting."""
|
|
1512
|
+
started = _reclaim_clock()
|
|
1513
|
+
samples = []
|
|
1514
|
+
threshold = length - length // 2
|
|
1515
|
+
for index in range(RECLAIM_MAX_SAMPLES):
|
|
1516
|
+
elapsed = _reclaim_clock() - started
|
|
1517
|
+
if index and elapsed > RECLAIM_WINDOW_SECONDS:
|
|
1518
|
+
break
|
|
1519
|
+
free = shutil.disk_usage(path).free
|
|
1520
|
+
elapsed = _reclaim_clock() - started
|
|
1521
|
+
samples.append({"elapsed_seconds": elapsed, "free_bytes": free, "delta_bytes": free - before_free})
|
|
1522
|
+
if free - before_free >= threshold and elapsed <= RECLAIM_WINDOW_SECONDS:
|
|
1523
|
+
return
|
|
1524
|
+
remaining = RECLAIM_WINDOW_SECONDS - elapsed
|
|
1525
|
+
if remaining <= 0 or index == RECLAIM_MAX_SAMPLES - 1:
|
|
1526
|
+
break
|
|
1527
|
+
_sleep(min(RECLAIM_WINDOW_SECONDS / (RECLAIM_MAX_SAMPLES - 1), remaining))
|
|
1528
|
+
raise RawReclaimUnconfirmedError(
|
|
1529
|
+
{
|
|
1530
|
+
"reclaim_basis": "volume_delta",
|
|
1531
|
+
"chunk_offset": offset,
|
|
1532
|
+
"chunk_length": length,
|
|
1533
|
+
"blocks_before": before_blocks,
|
|
1534
|
+
"blocks_after": after_blocks,
|
|
1535
|
+
"free_before": before_free,
|
|
1536
|
+
"samples": samples,
|
|
1537
|
+
"threshold_bytes": threshold,
|
|
1538
|
+
"window_seconds": RECLAIM_WINDOW_SECONDS,
|
|
1539
|
+
"elapsed_seconds": _reclaim_clock() - started,
|
|
1540
|
+
}
|
|
1541
|
+
)
|
|
1542
|
+
|
|
1543
|
+
|
|
1544
|
+
def _compress_reclaiming_raw(
|
|
1545
|
+
raw: Path, archive: Path, *, zstd_binary: str | None, reclaim_basis: str = "volume_delta"
|
|
1546
|
+
) -> str:
|
|
1446
1547
|
"""Write durable independent frames, then release their raw extents; hash before punching.
|
|
1447
1548
|
|
|
1448
1549
|
Concatenated gzip members and zstd frames decode to the original byte stream. A failed
|
|
@@ -1480,7 +1581,6 @@ def _compress_reclaiming_raw(raw: Path, archive: Path, *, zstd_binary: str | Non
|
|
|
1480
1581
|
# because no unread bytes follow it. APFS may retain its final partial block.
|
|
1481
1582
|
before_free = shutil.disk_usage(archive.parent).free
|
|
1482
1583
|
_punch_raw_hole(source.fileno(), offset, len(chunk))
|
|
1483
|
-
freed = shutil.disk_usage(archive.parent).free - before_free
|
|
1484
1584
|
after_blocks = os.fstat(source.fileno()).st_blocks
|
|
1485
1585
|
if after_blocks >= before_blocks:
|
|
1486
1586
|
raise RuntimeError("punched raw extents not reclaimed (filesystem allocation pin)")
|
|
@@ -1488,10 +1588,15 @@ def _compress_reclaiming_raw(raw: Path, archive: Path, *, zstd_binary: str | Non
|
|
|
1488
1588
|
# accumulate against reclaim. Allow half a chunk for concurrent volume activity.
|
|
1489
1589
|
# Short tails can free less than this tolerance due to APFS accounting; they
|
|
1490
1590
|
# retain the per-file block check above and the raw free-space floor below.
|
|
1491
|
-
|
|
1492
|
-
if len(chunk) == RECLAIM_CHUNK_BYTES and
|
|
1493
|
-
|
|
1494
|
-
|
|
1591
|
+
# With no snapshots, the block drop proves reclaim despite unrelated writers.
|
|
1592
|
+
if len(chunk) == RECLAIM_CHUNK_BYTES and reclaim_basis != "block_drop_no_snapshot":
|
|
1593
|
+
_confirm_raw_reclaim(
|
|
1594
|
+
archive.parent,
|
|
1595
|
+
offset=offset,
|
|
1596
|
+
length=len(chunk),
|
|
1597
|
+
before_blocks=before_blocks,
|
|
1598
|
+
after_blocks=after_blocks,
|
|
1599
|
+
before_free=before_free,
|
|
1495
1600
|
)
|
|
1496
1601
|
offset += len(chunk)
|
|
1497
1602
|
_require_raw_floor(archive.parent)
|
|
@@ -1707,11 +1812,18 @@ def create_sqlite_backup_artifact(
|
|
|
1707
1812
|
temp_archive = Path(tmp) / final_archive.name
|
|
1708
1813
|
compress_started = time.monotonic()
|
|
1709
1814
|
raw_sha256 = None
|
|
1815
|
+
reclaim_basis = None
|
|
1710
1816
|
if reclaim_raw:
|
|
1817
|
+
# Cache once for this backup, before compression starts, never per chunk.
|
|
1818
|
+
snapshots = _staging_snapshots_may_exist(output_dir)
|
|
1819
|
+
reclaim_basis = "block_drop_no_snapshot" if snapshots is False else "volume_delta"
|
|
1820
|
+
print(f"Backup raw reclaim_basis={reclaim_basis}; snapshots_may_exist={snapshots}", flush=True)
|
|
1711
1821
|
# Keep consumed extents inside the owned attempt, never at the published raw path.
|
|
1712
1822
|
consumed_raw = _consume_raw_in_attempt(raw_snapshot, Path(tmp))
|
|
1713
1823
|
print("Backup consuming verified raw snapshot; a failed attempt requires a fresh snapshot", flush=True)
|
|
1714
|
-
raw_sha256 = _compress_reclaiming_raw(
|
|
1824
|
+
raw_sha256 = _compress_reclaiming_raw(
|
|
1825
|
+
consumed_raw, temp_archive, zstd_binary=zstd_binary, reclaim_basis=reclaim_basis
|
|
1826
|
+
)
|
|
1715
1827
|
elif zstd_binary is not None:
|
|
1716
1828
|
_compress_zstd(zstd_binary, raw_snapshot, temp_archive, output_dir)
|
|
1717
1829
|
else:
|
|
@@ -1755,6 +1867,7 @@ def create_sqlite_backup_artifact(
|
|
|
1755
1867
|
compressor_fallback=compressor_fallback,
|
|
1756
1868
|
compress_duration_seconds=compress_duration,
|
|
1757
1869
|
raw_sha256=raw_sha256,
|
|
1870
|
+
reclaim_basis=reclaim_basis,
|
|
1758
1871
|
)
|
|
1759
1872
|
|
|
1760
1873
|
|
|
@@ -3425,6 +3538,7 @@ def run_backup(
|
|
|
3425
3538
|
snapshot_duration_seconds=resume.get("snapshot_duration_seconds"),
|
|
3426
3539
|
integrity_duration_seconds=resume.get("integrity_duration_seconds"),
|
|
3427
3540
|
raw_sha256=resume.get("raw_sha256"),
|
|
3541
|
+
reclaim_basis=resume.get("reclaim_basis"),
|
|
3428
3542
|
)
|
|
3429
3543
|
for suffix in DB_SNAPSHOT_SUFFIXES:
|
|
3430
3544
|
target = Path(staging_dir) / f"{resolved_date_stamp}{suffix}"
|
|
@@ -3488,6 +3602,7 @@ def run_backup(
|
|
|
3488
3602
|
"snapshot_duration_seconds": getattr(artifact, "snapshot_duration_seconds", None),
|
|
3489
3603
|
"integrity_duration_seconds": getattr(artifact, "integrity_duration_seconds", None),
|
|
3490
3604
|
"raw_sha256": getattr(artifact, "raw_sha256", None),
|
|
3605
|
+
"reclaim_basis": getattr(artifact, "reclaim_basis", None),
|
|
3491
3606
|
"compressor": getattr(artifact, "compressor", None)
|
|
3492
3607
|
or ("zstd" if snapshot.name.endswith(ZSTD_SNAPSHOT_SUFFIX) else "gzip"),
|
|
3493
3608
|
"compress_duration_seconds": getattr(artifact, "compress_duration_seconds", None),
|
|
@@ -3715,6 +3830,8 @@ def run_backup(
|
|
|
3715
3830
|
{"error_type": getattr(exc, "error_type", type(exc).__name__), "error": _redact_upload_secrets(str(exc))}
|
|
3716
3831
|
)
|
|
3717
3832
|
error_code = getattr(exc, "error_code", None)
|
|
3833
|
+
if isinstance(exc, RawReclaimUnconfirmedError):
|
|
3834
|
+
result["raw_reclaim"] = exc.measurements
|
|
3718
3835
|
if isinstance(error_code, str):
|
|
3719
3836
|
result["error_code"] = error_code
|
|
3720
3837
|
if isinstance(exc, DriveUploadStalledError):
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
"""LinkedIn KG import with an explicit copy or SHA-confirmed live mode. Output contains counts, never contact records.
|
|
2
|
+
|
|
3
|
+
Run: python -m brainlayer.linkedin_import EXPORT.json --db-copy COPY.db
|
|
4
|
+
Identity matching is exact: LinkedIn URL, email, then normalized full name.
|
|
5
|
+
First/last names stay in metadata; name aliases must contain multiple tokens.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import time
|
|
15
|
+
from collections import Counter, defaultdict
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from urllib.parse import urlsplit
|
|
18
|
+
from uuid import uuid4
|
|
19
|
+
|
|
20
|
+
from .disk_space import measure_disk_space
|
|
21
|
+
from .kg_repo import KGMixin
|
|
22
|
+
from .paths import resolve_db_path
|
|
23
|
+
from .pipeline.entity_resolution import _normalize_name
|
|
24
|
+
from .runtime_store import RuntimeStoreModeError, open_writer_store
|
|
25
|
+
|
|
26
|
+
SOURCE = "linkedin-export 2026-10-31 (portfolio scrub)"
|
|
27
|
+
ATTRIBUTES = (
|
|
28
|
+
"first_name",
|
|
29
|
+
"last_name",
|
|
30
|
+
"linkedin_url",
|
|
31
|
+
"company",
|
|
32
|
+
"position",
|
|
33
|
+
"connected_on",
|
|
34
|
+
"relationship_strength",
|
|
35
|
+
"has_messages",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ImportRefusal(ValueError):
|
|
40
|
+
"""An importer-owned static reason; never carries source values."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, reason):
|
|
43
|
+
if reason not in {
|
|
44
|
+
"copy_refusal",
|
|
45
|
+
"invalid_export",
|
|
46
|
+
"disk_floor",
|
|
47
|
+
"confirmation_required",
|
|
48
|
+
"confirmation_sha256_mismatch",
|
|
49
|
+
}:
|
|
50
|
+
raise ValueError("unknown_import_reason")
|
|
51
|
+
self.reason = reason
|
|
52
|
+
super().__init__(reason)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def normalized(value):
|
|
56
|
+
return " ".join(_normalize_name(value or "").split())
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def email_key(value):
|
|
60
|
+
return (value or "").strip().casefold()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def linkedin_key(value):
|
|
64
|
+
if not value:
|
|
65
|
+
return ""
|
|
66
|
+
parsed = urlsplit(value if "://" in value else "https://" + value)
|
|
67
|
+
if not re.fullmatch(r"(?:www\.|[a-z]{2}\.)?linkedin\.com", parsed.hostname or "") or not parsed.path.startswith(
|
|
68
|
+
"/in/"
|
|
69
|
+
):
|
|
70
|
+
raise ValueError("invalid_linkedin_url")
|
|
71
|
+
return "https://linkedin.com" + parsed.path.rstrip("/").casefold()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def require_copy(path, live=None):
|
|
75
|
+
"""Refuse canonical, configured-live, symlink and hardlink aliases before opening."""
|
|
76
|
+
from .paths import get_db_path
|
|
77
|
+
|
|
78
|
+
path = Path(path).expanduser().resolve(strict=True)
|
|
79
|
+
protected = [Path.home() / ".local/share/brainlayer/brainlayer.db", get_db_path()]
|
|
80
|
+
if live is not None:
|
|
81
|
+
protected.append(Path(live))
|
|
82
|
+
if any(path == p.expanduser().resolve() or (p.exists() and os.path.samefile(path, p)) for p in protected):
|
|
83
|
+
raise ImportRefusal("copy_refusal")
|
|
84
|
+
return path
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class _ImportGraph(KGMixin):
|
|
88
|
+
"""Use the supported KG writers on one connection, including read-your-writes."""
|
|
89
|
+
|
|
90
|
+
def __init__(self, store):
|
|
91
|
+
self.conn, self.db_path = store.conn, store.db_path
|
|
92
|
+
|
|
93
|
+
def _read_cursor(self):
|
|
94
|
+
return self.conn.cursor()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def import_connections(store, export):
|
|
98
|
+
"""Import atomically on the caller's supported writer; never opens a database."""
|
|
99
|
+
started = time.monotonic()
|
|
100
|
+
raw = export if isinstance(export, bytes) else Path(export).read_bytes()
|
|
101
|
+
rows = json.loads(raw)
|
|
102
|
+
if not isinstance(rows, list):
|
|
103
|
+
raise ImportRefusal("invalid_export")
|
|
104
|
+
store.conn.execute("BEGIN IMMEDIATE")
|
|
105
|
+
try:
|
|
106
|
+
result = _import_rows(_ImportGraph(store), rows, hashlib.sha256(raw).hexdigest(), started)
|
|
107
|
+
store.conn.execute("COMMIT")
|
|
108
|
+
return result
|
|
109
|
+
except BaseException:
|
|
110
|
+
store.conn.execute("ROLLBACK")
|
|
111
|
+
raise
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _import_rows(store, rows, sha256, started):
|
|
115
|
+
provenance = {"source": SOURCE, "sha256": sha256}
|
|
116
|
+
counts = {key: {"new": 0, "merged": 0} for key in ("persons", "companies", "relations")}
|
|
117
|
+
counts["persons"].update(merged_by=dict.fromkeys(("linkedin_url", "email", "name"), 0), new_name_conflict=0)
|
|
118
|
+
counts["companies"]["merged_by"] = {"name": 0}
|
|
119
|
+
skipped = Counter()
|
|
120
|
+
invalid_existing_alias = 0
|
|
121
|
+
identities = defaultdict(lambda: defaultdict(set))
|
|
122
|
+
|
|
123
|
+
def index_identity(kind, key, eid):
|
|
124
|
+
if key:
|
|
125
|
+
indexes[kind][key].add(eid)
|
|
126
|
+
identities[eid][kind].add(key)
|
|
127
|
+
|
|
128
|
+
indexes = {kind: defaultdict(set) for kind in ("person", "company", "linkedin_url", "email")}
|
|
129
|
+
for eid, kind, name, meta in store.conn.execute(
|
|
130
|
+
"SELECT id, entity_type, name, metadata FROM kg_entities WHERE entity_type IN ('person','company')"
|
|
131
|
+
):
|
|
132
|
+
indexes[kind][normalized(name)].add(eid)
|
|
133
|
+
if kind == "person":
|
|
134
|
+
metadata = json.loads(meta or "{}")
|
|
135
|
+
if metadata.get("linkedin_full_name"):
|
|
136
|
+
indexes[kind][normalized(metadata["linkedin_full_name"])].add(eid)
|
|
137
|
+
try:
|
|
138
|
+
index_identity("linkedin_url", linkedin_key(metadata.get("linkedin_url")), eid)
|
|
139
|
+
except ValueError:
|
|
140
|
+
pass
|
|
141
|
+
contact = metadata.get("contact_info") or {}
|
|
142
|
+
for value in (metadata.get("email"), contact.get("email")):
|
|
143
|
+
index_identity("email", email_key(value), eid)
|
|
144
|
+
for alias, eid, kind in store.conn.execute(
|
|
145
|
+
"SELECT a.alias, a.entity_id, a.alias_type FROM kg_entity_aliases a "
|
|
146
|
+
"JOIN kg_entities e ON e.id=a.entity_id WHERE e.entity_type='person' "
|
|
147
|
+
"AND a.alias_type IN ('name','linkedin_url','email')"
|
|
148
|
+
):
|
|
149
|
+
if kind == "name":
|
|
150
|
+
indexes["person"][normalized(alias)].add(eid)
|
|
151
|
+
continue
|
|
152
|
+
try:
|
|
153
|
+
key = linkedin_key(alias) if kind == "linkedin_url" else email_key(alias)
|
|
154
|
+
except ValueError:
|
|
155
|
+
invalid_existing_alias += 1
|
|
156
|
+
continue
|
|
157
|
+
index_identity(kind, key, eid)
|
|
158
|
+
owner_ids = indexes["person"].get(normalized("Etan Heyman"), set())
|
|
159
|
+
owner = next(iter(owner_ids)) if len(owner_ids) == 1 else None
|
|
160
|
+
seen_people, seen_companies, seen_relations = set(), set(), set()
|
|
161
|
+
|
|
162
|
+
def upsert(kind, name, eid, attributes, row_number=None, name_conflict=False, identity_key=None):
|
|
163
|
+
old = store.get_entity(eid) if eid else None
|
|
164
|
+
metadata = dict(old["metadata"]) if old else {}
|
|
165
|
+
metadata.update({k: v for k, v in attributes.items() if v is not None and v != ""})
|
|
166
|
+
evidence = dict(provenance)
|
|
167
|
+
if row_number is not None:
|
|
168
|
+
evidence["row_number"] = row_number
|
|
169
|
+
sources = list(metadata.get("linkedin_imports", []))
|
|
170
|
+
if evidence not in sources:
|
|
171
|
+
sources.append(evidence)
|
|
172
|
+
metadata["linkedin_imports"] = sources
|
|
173
|
+
storage_name = old["name"] if old else name
|
|
174
|
+
if name_conflict:
|
|
175
|
+
# The KG enforces UNIQUE(type, name). Keep the original as a full-name alias.
|
|
176
|
+
suffix = hashlib.sha256(identity_key.encode()).hexdigest()[:16]
|
|
177
|
+
storage_name = f"{name} [linkedin-{suffix}]"
|
|
178
|
+
metadata["linkedin_full_name"] = name
|
|
179
|
+
stored = store.upsert_entity(eid or f"{kind}-{uuid4().hex}", kind, storage_name, metadata=metadata)
|
|
180
|
+
indexes[kind][normalized(name)].add(stored)
|
|
181
|
+
if kind == "person" and len(name.split()) > 1:
|
|
182
|
+
store.add_entity_alias(name, stored)
|
|
183
|
+
return stored
|
|
184
|
+
|
|
185
|
+
def relation(source, target, kind, properties):
|
|
186
|
+
key = (source, target, kind)
|
|
187
|
+
if key in seen_relations:
|
|
188
|
+
return
|
|
189
|
+
seen_relations.add(key)
|
|
190
|
+
existing = next(
|
|
191
|
+
(
|
|
192
|
+
r
|
|
193
|
+
for r in store.get_entity_relations(source, direction="outgoing")
|
|
194
|
+
if r["target_id"] == target and r["relation_type"] == kind
|
|
195
|
+
),
|
|
196
|
+
None,
|
|
197
|
+
)
|
|
198
|
+
props = dict(existing["properties"]) if existing else {}
|
|
199
|
+
props.update({k: v for k, v in properties.items() if v is not None and v != ""})
|
|
200
|
+
sources = list(props.get("linkedin_imports", []))
|
|
201
|
+
if provenance not in sources:
|
|
202
|
+
sources.append(provenance)
|
|
203
|
+
props["linkedin_imports"] = sources
|
|
204
|
+
store.add_relation(
|
|
205
|
+
f"rel-{uuid4().hex}",
|
|
206
|
+
source,
|
|
207
|
+
target,
|
|
208
|
+
kind,
|
|
209
|
+
properties=props,
|
|
210
|
+
confidence=existing["confidence"] if existing else 1.0,
|
|
211
|
+
importance=existing["importance"] if existing else 0.5,
|
|
212
|
+
)
|
|
213
|
+
counts["relations"]["merged" if existing else "new"] += 1
|
|
214
|
+
|
|
215
|
+
for number, row in enumerate(rows, 1):
|
|
216
|
+
if measure_disk_space(store.db_path.parent, include_available=False).free_now_bytes < 2 * 1024**3:
|
|
217
|
+
raise ImportRefusal("disk_floor")
|
|
218
|
+
if (
|
|
219
|
+
not isinstance(row, dict)
|
|
220
|
+
or any(
|
|
221
|
+
value is not None and not isinstance(value, str) for key, value in row.items() if key != "has_messages"
|
|
222
|
+
)
|
|
223
|
+
or row.get("has_messages") not in (None, True, False)
|
|
224
|
+
):
|
|
225
|
+
skipped["invalid_row"] += 1
|
|
226
|
+
continue
|
|
227
|
+
name = " ".join((row.get("full_name") or "").split())
|
|
228
|
+
if not name:
|
|
229
|
+
skipped["missing_name"] += 1
|
|
230
|
+
continue
|
|
231
|
+
try:
|
|
232
|
+
url = linkedin_key(row.get("linkedin_url"))
|
|
233
|
+
except ValueError:
|
|
234
|
+
skipped["invalid_linkedin_url"] += 1
|
|
235
|
+
continue
|
|
236
|
+
email = email_key(row.get("email"))
|
|
237
|
+
url_owners = indexes["linkedin_url"].get(url, set()) if url else set()
|
|
238
|
+
email_owners = indexes["email"].get(email, set()) if email else set()
|
|
239
|
+
if url_owners and email_owners and url_owners != email_owners:
|
|
240
|
+
# The URL and email already belong to different people: never join them.
|
|
241
|
+
skipped["ambiguous_identity"] += 1
|
|
242
|
+
continue
|
|
243
|
+
match_kind, matches = next(
|
|
244
|
+
(
|
|
245
|
+
(kind, indexes[kind][key])
|
|
246
|
+
for kind, key in (("linkedin_url", url), ("email", email))
|
|
247
|
+
if key and indexes[kind].get(key)
|
|
248
|
+
),
|
|
249
|
+
("name", set()),
|
|
250
|
+
)
|
|
251
|
+
# One person has one LinkedIn profile: a candidate holding a different URL is a different person.
|
|
252
|
+
if matches and url and any(identities[eid]["linkedin_url"] - {url} for eid in matches):
|
|
253
|
+
skipped["ambiguous_identity"] += 1
|
|
254
|
+
continue
|
|
255
|
+
name_candidates = indexes["person"].get(normalized(name), set())
|
|
256
|
+
name_conflict = False
|
|
257
|
+
if not matches:
|
|
258
|
+
matches = {
|
|
259
|
+
eid
|
|
260
|
+
for eid in name_candidates
|
|
261
|
+
if not any(
|
|
262
|
+
key and identities[eid][kind] and identities[eid][kind] != {key}
|
|
263
|
+
for kind, key in (("linkedin_url", url), ("email", email))
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
name_conflict = bool(name_candidates and not matches)
|
|
267
|
+
if matches and name_candidates - matches:
|
|
268
|
+
skipped["ambiguous_identity"] += 1
|
|
269
|
+
continue
|
|
270
|
+
if len(matches) > 1:
|
|
271
|
+
skipped["ambiguous_identity"] += 1
|
|
272
|
+
continue
|
|
273
|
+
eid = next(iter(matches), None)
|
|
274
|
+
if eid and indexes["person"].get(normalized(store.get_entity(eid)["name"]), set()) - {eid}:
|
|
275
|
+
skipped["ambiguous_identity"] += 1
|
|
276
|
+
continue
|
|
277
|
+
if eid in seen_people:
|
|
278
|
+
skipped["duplicate_in_file"] += 1
|
|
279
|
+
continue
|
|
280
|
+
eid = upsert("person", name, eid, {k: row.get(k) for k in ATTRIBUTES}, number, name_conflict, url or email)
|
|
281
|
+
counts["persons"]["merged" if matches else "new"] += 1
|
|
282
|
+
counts["persons"]["new_name_conflict"] += int(name_conflict)
|
|
283
|
+
if matches:
|
|
284
|
+
counts["persons"]["merged_by"][match_kind] += 1
|
|
285
|
+
seen_people.add(eid)
|
|
286
|
+
for kind, key in (("linkedin_url", url), ("email", email)):
|
|
287
|
+
if key:
|
|
288
|
+
index_identity(kind, key, eid)
|
|
289
|
+
store.add_entity_alias(key, eid, alias_type=kind)
|
|
290
|
+
if owner and owner != eid:
|
|
291
|
+
relation(
|
|
292
|
+
eid,
|
|
293
|
+
owner,
|
|
294
|
+
"linkedin_connection",
|
|
295
|
+
{k: row.get(k) for k in ("connected_on", "relationship_strength", "has_messages")},
|
|
296
|
+
)
|
|
297
|
+
company = " ".join((row.get("company_normalized") or row.get("company") or "").split())
|
|
298
|
+
if not company:
|
|
299
|
+
continue
|
|
300
|
+
key = normalized(company)
|
|
301
|
+
matches = indexes["company"].get(key, set())
|
|
302
|
+
if len(matches) > 1:
|
|
303
|
+
skipped["ambiguous_company"] += 1
|
|
304
|
+
continue
|
|
305
|
+
cid = next(iter(matches), None)
|
|
306
|
+
if key not in seen_companies:
|
|
307
|
+
cid = upsert("company", company, cid, {})
|
|
308
|
+
counts["companies"]["merged" if matches else "new"] += 1
|
|
309
|
+
counts["companies"]["merged_by"]["name"] += int(bool(matches))
|
|
310
|
+
seen_companies.add(key)
|
|
311
|
+
relation(eid, cid, "works_at", {"position": row.get("position")})
|
|
312
|
+
return {
|
|
313
|
+
**counts,
|
|
314
|
+
"rows": len(rows),
|
|
315
|
+
"invalid_existing_alias": invalid_existing_alias,
|
|
316
|
+
"skipped": dict(skipped),
|
|
317
|
+
"owner_found": owner is not None,
|
|
318
|
+
"duplicates_within_file": skipped["duplicate_in_file"],
|
|
319
|
+
"rows_missing_name": skipped["missing_name"],
|
|
320
|
+
"seconds": round(time.monotonic() - started, 3),
|
|
321
|
+
"sha256": provenance["sha256"],
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def main():
|
|
326
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
327
|
+
parser.add_argument("export", type=Path)
|
|
328
|
+
mode = parser.add_mutually_exclusive_group(required=True)
|
|
329
|
+
mode.add_argument("--db-copy", type=Path)
|
|
330
|
+
mode.add_argument("--live", action="store_true")
|
|
331
|
+
parser.add_argument("--confirm-sha256")
|
|
332
|
+
args = parser.parse_args()
|
|
333
|
+
previous_logging_level = logging.root.manager.disable
|
|
334
|
+
logging.disable(logging.CRITICAL) # Library diagnostics can contain personal data.
|
|
335
|
+
store = None
|
|
336
|
+
try:
|
|
337
|
+
if args.live and not args.confirm_sha256:
|
|
338
|
+
raise ImportRefusal("confirmation_required")
|
|
339
|
+
raw = args.export.read_bytes() # Hash and import these same bytes; never reopen the export.
|
|
340
|
+
if args.live and hashlib.sha256(raw).hexdigest() != args.confirm_sha256:
|
|
341
|
+
raise ImportRefusal("confirmation_sha256_mismatch")
|
|
342
|
+
path = resolve_db_path() if args.live else require_copy(args.db_copy)
|
|
343
|
+
store = open_writer_store(path)
|
|
344
|
+
print(json.dumps(import_connections(store, raw), sort_keys=True))
|
|
345
|
+
except Exception as exc:
|
|
346
|
+
reason = (
|
|
347
|
+
exc.reason
|
|
348
|
+
if isinstance(exc, ImportRefusal)
|
|
349
|
+
else "invalid_export"
|
|
350
|
+
if isinstance(exc, json.JSONDecodeError)
|
|
351
|
+
else "invalid_store_mode"
|
|
352
|
+
if isinstance(exc, RuntimeStoreModeError)
|
|
353
|
+
else "import_failed"
|
|
354
|
+
)
|
|
355
|
+
print(json.dumps({"error_type": type(exc).__name__, "reason": reason}))
|
|
356
|
+
return 1
|
|
357
|
+
finally:
|
|
358
|
+
if store is not None:
|
|
359
|
+
store.close()
|
|
360
|
+
logging.disable(previous_logging_level)
|
|
361
|
+
return 0
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
if __name__ == "__main__":
|
|
365
|
+
raise SystemExit(main())
|
|
@@ -131,7 +131,11 @@ def _daily_snapshot(records: list[dict[str, Any]]) -> tuple[dict[str, Any] | Non
|
|
|
131
131
|
error_type = str(pending.get("error_type") or pending.get("error_code") or "backup_error")
|
|
132
132
|
error_reason = pending.get("error") or pending.get("verification_error")
|
|
133
133
|
break
|
|
134
|
-
if
|
|
134
|
+
if (
|
|
135
|
+
error_type
|
|
136
|
+
in {"BrainBarDatabaseIdentityError", "BrainBarDatabaseIdentityUnavailableError", "raw_reclaim_unconfirmed"}
|
|
137
|
+
and error_reason
|
|
138
|
+
):
|
|
135
139
|
error_type = f"{error_type}: {error_reason}"
|
|
136
140
|
if "google-drive-mcp/tokens.json" in str(latest.get("error", "")):
|
|
137
141
|
error_type = "drive_credentials_missing"
|
|
@@ -489,7 +493,14 @@ def build_backups_section(
|
|
|
489
493
|
resolved_alert_path = alert_path(env=env, db_path=db_path)
|
|
490
494
|
job_alerts = active_alerts(resolved_alert_path)
|
|
491
495
|
if job_alerts:
|
|
492
|
-
|
|
496
|
+
# The generic DB job alert must not replace this attempt's measured diagnostic.
|
|
497
|
+
reclaim_failure = (error_type or "").startswith("raw_reclaim_unconfirmed:")
|
|
498
|
+
generic_db_alerts = all(
|
|
499
|
+
key in {"backup-daily", "backup-snapshot"} and reason == "Database backup failed; check the backup log"
|
|
500
|
+
for key, reason in job_alerts.items()
|
|
501
|
+
)
|
|
502
|
+
if not (reclaim_failure and generic_db_alerts):
|
|
503
|
+
error_type = "job_alert:" + next(iter(job_alerts.values()))
|
|
493
504
|
alert_input = record_input(resolved_alert_path, status="read", rows_or_bytes=None)
|
|
494
505
|
inputs.append(alert_input)
|
|
495
506
|
if alert_input.get("status") == "future":
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
BUILD_SHA = "cc301a9f19b679f75278f9cca43c48fc49fe8d7e"
|
|
File without changes
|
|
File without changes
|