brainlayer 1.5.45__tar.gz → 1.5.47__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.45 → brainlayer-1.5.47}/PKG-INFO +1 -1
- {brainlayer-1.5.45 → brainlayer-1.5.47}/pyproject.toml +1 -1
- {brainlayer-1.5.45 → brainlayer-1.5.47}/server.json +2 -2
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/__init__.py +1 -1
- brainlayer-1.5.47/src/brainlayer/_build.py +1 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/chunk_write.py +63 -1
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/cli/__init__.py +46 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/doctor.py +87 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/hooks/indexer.py +4 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/jsonl_backup.py +17 -15
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/maintenance.py +102 -21
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/cloud_scrub.py +44 -5
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/secret_scrub.py +33 -10
- brainlayer-1.5.47/src/brainlayer/scrub_at_rest.py +432 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/store.py +8 -2
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/vector_store.py +77 -7
- brainlayer-1.5.45/src/brainlayer/_build.py +0 -1
- {brainlayer-1.5.45 → brainlayer-1.5.47}/.gitignore +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/LICENSE +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/README.md +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/scripts/hotlane_brainbar_daemon.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/scripts/launchd/backup-daily.sh +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/scripts/launchd/brainlayer-env-run.sh +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/scripts/launchd/brainlayer.env.example +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/scripts/launchd/com.brainlayer.backup-daily.plist +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/scripts/launchd/com.brainlayer.decay.plist +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/scripts/launchd/com.brainlayer.drain.plist +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/scripts/launchd/com.brainlayer.enrichment.plist +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/scripts/launchd/com.brainlayer.health-check.plist +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/scripts/launchd/com.brainlayer.hotlane-brainbar.plist +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/scripts/launchd/com.brainlayer.index.plist +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/scripts/launchd/com.brainlayer.jsonl-backup.plist +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/scripts/launchd/com.brainlayer.log-cap.plist +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/scripts/launchd/com.brainlayer.maintenance-nightly.plist +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/scripts/launchd/com.brainlayer.maintenance-weekly.plist +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/scripts/launchd/com.brainlayer.observability.plist +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/scripts/launchd/com.brainlayer.p0-counter.plist +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/scripts/launchd/com.brainlayer.repair-fts.plist +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/scripts/launchd/com.brainlayer.t3-ingest.plist +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/scripts/launchd/com.brainlayer.throughput-watchdog.plist +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/scripts/launchd/com.brainlayer.tier0-watchdog.plist +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/scripts/launchd/com.brainlayer.wal-checkpoint.plist +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/scripts/launchd/com.brainlayer.watch.plist +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/scripts/launchd/com.etanhey.brainlayer-fleet-watchdog.plist +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/scripts/launchd/fleet-watchdog.sh +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/scripts/launchd/install.sh +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/scripts/launchd/jsonl-backup.sh +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/scripts/launchd/throughput-watchdog.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/scripts/release-verify-signatures.sh +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/scripts/tier0-watchdog.sh +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/__main__.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/_helpers.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/agent_profiles.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/agent_provenance.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/alarm.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/archive_collapse.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/archive_lifecycle.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/backup_daily.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/backup_retention_invariant.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/badge_state.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/bitemporal.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/brainbar_hybrid_helper.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/cachedelete_purge.swift +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/calibrate.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/chunk_origin.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/chunk_origin_backfill.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/chunk_origin_wipe.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/classify.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/claude_paths.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/cli/wizard.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/cli_new.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/cloud_backfill.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/clustering.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/config.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/content_class.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/correction_judge.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/data/agents_registry.yaml +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/data/sandbox_seeds/skill-eval-baseline.json +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/db_shrink.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/decay.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/decay_backfill.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/decay_job.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/dedupe.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/dedupe_merge.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/deploy_drift.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/drain.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/drain_liveness.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/drive_credentials.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/embeddings.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/engine.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/enrichment_controller.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/eval/__init__.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/eval/abcde_enrich_runner.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/eval/abcde_variants.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/eval/abcde_variants.yaml +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/eval/benchmark.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/eval/enrichment_gold.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/eval/enrichment_graders.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/eval/enrichment_judge.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/eval/enrichment_llm_judge.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/eval/enrichment_quality_benchmark.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/eval/experiment_store.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/eval/fixtures/README.md +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/eval/labeling/enrichment-labeler.html +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/eval/phoenix_gate/__init__.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/eval/phoenix_gate/__main__.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/eval/phoenix_gate/baseline_store.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/eval/phoenix_gate/cli.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/eval/phoenix_gate/models.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/eval/phoenix_gate/phoenix_client.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/eval/phoenix_gate/regression_gate.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/eval/phoenix_gate/triggers.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/fallback_replay.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/git_learning.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/health_check.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/hook_python.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/hooks/__init__.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/import_sweep.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/index_completeness.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/index_new.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/index_watchdog.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/ingest/__init__.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/ingest/codex.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/ingest/t3.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/ingest_denylist.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/ingest_guard.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/isolation_proof.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/job_alerts.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/job_lifecycle_health.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/jobs.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/kg/__init__.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/kg_cleanup.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/kg_judge.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/kg_promotion.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/kg_repo.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/kg_review_session.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/kg_session_finish.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/kg_session_harvest.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/launchd_primitive.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/lexical_defense.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/lexical_defense_dictionary.json +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/log_cap.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/mcp/__init__.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/mcp/_format.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/mcp/_shared.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/mcp/enrich_handler.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/mcp/entity_handler.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/mcp/palette.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/mcp/search_handler.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/mcp/store_handler.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/mcp/tags_handler.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/mcp_socket_config.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/mcp_stdio_bridge.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/memory_types.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/migrate.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/notification_policy.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/observability_backup.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/observability_surface.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/p0_longitudinal_count.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/parent_death.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/paths.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pause.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/phonetic.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/__init__.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/agent_enrichment.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/analyze_communication.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/batch_extraction.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/brain_graph.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/chat_tags.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/chunk.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/classify.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/cluster_sampling.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/code_intelligence.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/correction_detection.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/correction_mining.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/digest.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/enrichment.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/enrichment_tiers.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/entity_extraction.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/entity_resolution.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/extract.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/extract_claude_desktop.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/extract_corrections.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/extract_markdown.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/extract_whatsapp.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/frustration_mining.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/git_overlay.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/groq.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/kg_extraction.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/kg_extraction_groq.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/longitudinal_analyzer.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/obsidian_export.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/operation_grouping.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/plan_linking.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/rate_limiter.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/relation_backfill.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/sanitize.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/semantic_style.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/sentiment.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/session_enrichment.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/style_embed.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/style_index.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/tag_entity_promotion.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/temporal_chains.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/time_batcher.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/unified_timeline.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/pipeline/write_queue.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/provenance.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/provenance_autosupersede.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/provenance_integration.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/queue_io.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/queue_merge.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/reembed_backfill.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/runtime_store.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/sandbox_db.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/scoping.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/search_fanout.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/search_profile.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/search_repo.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/session_repo.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/setup.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/socket_hygiene.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/source_class_migration.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/spotlight.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/storage.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/system_prompt_guard.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/t3_provenance.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/tag_normalization.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/taxonomy.json +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/telemetry.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/timestamp_iso.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/types.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/wal_checkpoint.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/watcher.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/src/brainlayer/watcher_bridge.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.47}/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.47
|
|
4
4
|
Summary: Persistent memory MCP server for AI agents — 17 tools, semantic search, knowledge graph, on-device SQLite
|
|
5
5
|
Project-URL: Homepage, https://brainlayer.etanheyman.com
|
|
6
6
|
Project-URL: Repository, https://github.com/EtanHey/brainlayer
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "io.github.etanhey/brainlayer",
|
|
4
4
|
"title": "BrainLayer",
|
|
5
5
|
"description": "Persistent memory for AI agents \u2014 local-first semantic search, knowledge graph, and 17 MCP tools with ToolAnnotations. One SQLite file, no cloud, no API keys.",
|
|
6
|
-
"version": "1.5.
|
|
6
|
+
"version": "1.5.47",
|
|
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.47",
|
|
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.47"
|
|
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 = "38521b5296eddaaa7d2cdc722119f2622f3456b2"
|
|
@@ -148,6 +148,62 @@ def _table_columns(conn: Any, table: str) -> set[str]:
|
|
|
148
148
|
return {row[1] for row in rows}
|
|
149
149
|
|
|
150
150
|
|
|
151
|
+
def scrub_chunk_values(values: Mapping[str, Any]) -> dict[str, Any]:
|
|
152
|
+
"""Scrub stored text before deriving hashes, previews or merge inputs.
|
|
153
|
+
|
|
154
|
+
JSON fields are decoded/normalized before scanning so escaped credentials
|
|
155
|
+
and mapping keys cannot bypass the at-rest policy. Quarantine remains
|
|
156
|
+
metadata only. Already-scrubbed callers retain their findings.
|
|
157
|
+
"""
|
|
158
|
+
from .pipeline.cloud_scrub import normalize_json_strings
|
|
159
|
+
from .pipeline.secret_scrub import merge_scrub_metadata, scrub_for_storage
|
|
160
|
+
|
|
161
|
+
clean = dict(values)
|
|
162
|
+
identity_fields = {"id", "brick_id", "conversation_id", "superseded_by", "aggregated_into"}
|
|
163
|
+
findings: dict[str, Any] = {}
|
|
164
|
+
body_findings: dict[str, Any] = {}
|
|
165
|
+
for key, value in clean.items():
|
|
166
|
+
structured = isinstance(value, (dict, list, tuple))
|
|
167
|
+
if structured:
|
|
168
|
+
value = json.dumps(value, ensure_ascii=False)
|
|
169
|
+
if not isinstance(value, str):
|
|
170
|
+
continue
|
|
171
|
+
if key != "content" and key not in identity_fields:
|
|
172
|
+
value = normalize_json_strings(value)
|
|
173
|
+
text, found = scrub_for_storage(value)
|
|
174
|
+
if key in identity_fields and text != value:
|
|
175
|
+
raise ValueError("chunk identifier requires redaction; refusing to change identity")
|
|
176
|
+
clean[key] = json.loads(text) if structured else text
|
|
177
|
+
# Quarantine count describes the body, not keys/derived placeholders.
|
|
178
|
+
findings = merge_scrub_metadata(
|
|
179
|
+
findings, {k: v for k, v in found.items() if k != "secret_scrub_quarantine_count"}
|
|
180
|
+
)
|
|
181
|
+
if key == "content":
|
|
182
|
+
body_findings = found
|
|
183
|
+
if findings or body_findings:
|
|
184
|
+
metadata = clean.get("metadata")
|
|
185
|
+
if metadata is None:
|
|
186
|
+
metadata = {}
|
|
187
|
+
if isinstance(metadata, str):
|
|
188
|
+
try:
|
|
189
|
+
metadata = json.loads(metadata)
|
|
190
|
+
except ValueError:
|
|
191
|
+
metadata = {"value": metadata}
|
|
192
|
+
if not isinstance(metadata, dict):
|
|
193
|
+
metadata = {"value": metadata}
|
|
194
|
+
if clean.get("content") == values.get("content") and (
|
|
195
|
+
"secret_scrub_quarantine_count" in metadata or "secret_scrub_redactions" in metadata
|
|
196
|
+
):
|
|
197
|
+
body_findings.pop("secret_scrub_quarantine_count", None)
|
|
198
|
+
clean["metadata"] = merge_scrub_metadata(merge_scrub_metadata(metadata, findings), body_findings)
|
|
199
|
+
if clean.get("content") != values.get("content"):
|
|
200
|
+
clean["char_count"] = len(str(clean.get("content") or ""))
|
|
201
|
+
# Caller-supplied dedupe fields describe the pre-scrub text. Recompute.
|
|
202
|
+
for key in ("dedupe_hash", "simhash", *(f"simhash_band_{i}" for i in range(4))):
|
|
203
|
+
clean.pop(key, None)
|
|
204
|
+
return clean
|
|
205
|
+
|
|
206
|
+
|
|
151
207
|
def prepare_canonical_insert(
|
|
152
208
|
values: Mapping[str, Any],
|
|
153
209
|
*,
|
|
@@ -158,6 +214,7 @@ def prepare_canonical_insert(
|
|
|
158
214
|
"""Fill required insert columns. Does not write."""
|
|
159
215
|
from .timestamp_iso import normalize_timestamp
|
|
160
216
|
|
|
217
|
+
values = scrub_chunk_values(values)
|
|
161
218
|
content = str(values.get("content") or "")
|
|
162
219
|
if not content.strip():
|
|
163
220
|
raise ValueError("content must be non-empty")
|
|
@@ -271,7 +328,12 @@ def insert_canonical_chunk(
|
|
|
271
328
|
on_conflict: str = "ignore",
|
|
272
329
|
now: datetime | None = None,
|
|
273
330
|
) -> dict[str, Any]:
|
|
274
|
-
"""Insert
|
|
331
|
+
"""Insert a scrubbed chunk; hashes and dedupe fields describe stored text.
|
|
332
|
+
|
|
333
|
+
Tokens of the same provider collapse to the same placeholder, so dedupe
|
|
334
|
+
treats otherwise identical scrubbed memories as the same content. Existing
|
|
335
|
+
unsanitized rows are not rewritten by this go-forward boundary.
|
|
336
|
+
"""
|
|
275
337
|
columns = _table_columns(conn, "chunks")
|
|
276
338
|
row = prepare_canonical_insert(values, now=now, columns=columns)
|
|
277
339
|
names = [key for key in row if key in columns]
|
|
@@ -1285,6 +1285,8 @@ def doctor_command(
|
|
|
1285
1285
|
f"[bold]{status}[/] chunks={result.chunk_count} "
|
|
1286
1286
|
f"recent_unvectored={result.recent_unvectored_chunks} queue={result.queue_count}"
|
|
1287
1287
|
)
|
|
1288
|
+
if result.cli_path_shadow and result.cli_path_shadow["state"] == "skipped":
|
|
1289
|
+
rprint(f"CLI PATH check skipped: {result.cli_path_shadow['reason']}")
|
|
1288
1290
|
for issue in result.issues:
|
|
1289
1291
|
color = "red" if issue.severity == "fatal" else "yellow"
|
|
1290
1292
|
rprint(f"[{color}]{issue.severity.upper()}[/] {issue.code}: {issue.message}")
|
|
@@ -2865,6 +2867,50 @@ def wal_checkpoint(
|
|
|
2865
2867
|
report_job_alert("wal-checkpoint", None)
|
|
2866
2868
|
|
|
2867
2869
|
|
|
2870
|
+
@app.command("scrub-at-rest")
|
|
2871
|
+
def scrub_at_rest_command(
|
|
2872
|
+
db_path: Path = typer.Option(..., "--db", help="Explicit database path; live apply requires guarded opt-in."),
|
|
2873
|
+
providers: str = typer.Option(
|
|
2874
|
+
"google_oauth",
|
|
2875
|
+
"--providers",
|
|
2876
|
+
help="Provider mode: google_oauth, context7, or exa_labeled (EXA key labels only).",
|
|
2877
|
+
),
|
|
2878
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Count matches without writing."),
|
|
2879
|
+
batch_size: int = typer.Option(100, "--batch-size", min=1, max=1000),
|
|
2880
|
+
allow_live_db: bool = typer.Option(
|
|
2881
|
+
False, "--allow-live-db", help="Apply with maintenance safety gates and writer quiescence."
|
|
2882
|
+
),
|
|
2883
|
+
expect_rows: int | None = typer.Option(
|
|
2884
|
+
None, "--expect-rows", min=0, help="Required guarded apply total from the preceding dry run."
|
|
2885
|
+
),
|
|
2886
|
+
) -> None:
|
|
2887
|
+
"""Redact provider credentials on a copy or a guarded runtime DB; print counts only."""
|
|
2888
|
+
from ..scrub_at_rest import scrub_at_rest
|
|
2889
|
+
|
|
2890
|
+
try:
|
|
2891
|
+
result = scrub_at_rest(
|
|
2892
|
+
db_path,
|
|
2893
|
+
providers=providers,
|
|
2894
|
+
dry_run=dry_run,
|
|
2895
|
+
batch_size=batch_size,
|
|
2896
|
+
allow_live_db=allow_live_db,
|
|
2897
|
+
expect_rows=expect_rows,
|
|
2898
|
+
)
|
|
2899
|
+
except Exception as exc:
|
|
2900
|
+
typer.echo(
|
|
2901
|
+
json.dumps(
|
|
2902
|
+
{
|
|
2903
|
+
"error": "at-rest scrub refused or failed",
|
|
2904
|
+
"error_type": type(exc).__name__,
|
|
2905
|
+
"reason": getattr(exc, "reason", "scrub-failed"),
|
|
2906
|
+
"detail": getattr(exc, "detail", None),
|
|
2907
|
+
}
|
|
2908
|
+
)
|
|
2909
|
+
)
|
|
2910
|
+
raise typer.Exit(1) from None
|
|
2911
|
+
typer.echo(json.dumps(result, sort_keys=True))
|
|
2912
|
+
|
|
2913
|
+
|
|
2868
2914
|
@app.command("repair-fts")
|
|
2869
2915
|
def repair_fts(
|
|
2870
2916
|
db_path: Path | None = typer.Argument(None, help="Offline database copy; omitted means read-only FTS count check."),
|
|
@@ -8,6 +8,9 @@ indexes.
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import json
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import shlex
|
|
11
14
|
import sqlite3
|
|
12
15
|
import sys
|
|
13
16
|
import time
|
|
@@ -250,11 +253,93 @@ class DoctorResult:
|
|
|
250
253
|
hotlane_running: bool = False
|
|
251
254
|
roundtrip_latency_seconds: float | None = None
|
|
252
255
|
fts5_health: dict[str, Any] | None = None
|
|
256
|
+
cli_path_shadow: dict[str, Any] | None = None
|
|
253
257
|
|
|
254
258
|
def to_dict(self) -> dict[str, Any]:
|
|
255
259
|
return asdict(self)
|
|
256
260
|
|
|
257
261
|
|
|
262
|
+
def _check_cli_path_shadow(result: DoctorResult) -> None:
|
|
263
|
+
"""Keep an advisory PATH probe from aborting the other doctor checks."""
|
|
264
|
+
try:
|
|
265
|
+
_probe_cli_path_shadow(result)
|
|
266
|
+
except Exception as exc:
|
|
267
|
+
reason = f"{type(exc).__name__}: {exc}"
|
|
268
|
+
result.cli_path_shadow = {"state": "warning", "reason": reason}
|
|
269
|
+
result.issues.append(DoctorIssue("cli_path_shadow_check_failed", "warning", reason))
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _probe_cli_path_shadow(result: DoctorResult) -> None:
|
|
273
|
+
prefix = Path(sys.prefix).resolve()
|
|
274
|
+
keg = next(
|
|
275
|
+
(p for p in (prefix, *prefix.parents) if p.parent.name == "brainlayer" and p.parent.parent.name == "Cellar"),
|
|
276
|
+
None,
|
|
277
|
+
)
|
|
278
|
+
result.cli_path_shadow = {"state": "skipped", "reason": "running interpreter is not a BrainLayer keg install"}
|
|
279
|
+
if keg is None:
|
|
280
|
+
return
|
|
281
|
+
paths: list[Path] = []
|
|
282
|
+
seen: set[Path] = set()
|
|
283
|
+
probe_failed = False
|
|
284
|
+
for directory in os.environ.get("PATH", "").split(os.pathsep):
|
|
285
|
+
candidate = Path(directory) / "brainlayer"
|
|
286
|
+
try:
|
|
287
|
+
if candidate.is_file() and os.access(candidate, os.X_OK):
|
|
288
|
+
resolved = candidate.resolve()
|
|
289
|
+
if resolved not in seen:
|
|
290
|
+
paths.append(candidate.absolute())
|
|
291
|
+
seen.add(resolved)
|
|
292
|
+
except OSError as exc:
|
|
293
|
+
probe_failed = True
|
|
294
|
+
result.issues.append(DoctorIssue("cli_path_shadow_probe_failed", "warning", f"Skipped {candidate}: {exc}"))
|
|
295
|
+
severity = "fatal" if paths and not paths[0].resolve().is_relative_to(keg) else "warning"
|
|
296
|
+
duplicates = [p for p in paths if not p.resolve().is_relative_to(keg)]
|
|
297
|
+
result.cli_path_shadow = {
|
|
298
|
+
"state": severity if duplicates or not paths or probe_failed else "pass",
|
|
299
|
+
"paths": [str(p) for p in paths],
|
|
300
|
+
"prefix": str(keg),
|
|
301
|
+
}
|
|
302
|
+
if not paths:
|
|
303
|
+
result.issues.append(DoctorIssue("cli_path_shadow", "warning", "No brainlayer executable on PATH"))
|
|
304
|
+
for path in duplicates:
|
|
305
|
+
severity = "fatal" if path == paths[0] else "warning"
|
|
306
|
+
kind, remedy = "plain script", "put the running BrainLayer keg bin directory first on PATH"
|
|
307
|
+
try:
|
|
308
|
+
with path.open() as stream:
|
|
309
|
+
shebang = stream.readline(4096).strip()
|
|
310
|
+
interpreter = Path(shebang[2:].split()[0]) if shebang.startswith("#!/") else None
|
|
311
|
+
if interpreter and interpreter.name.startswith("python"):
|
|
312
|
+
install = interpreter.parent.parent
|
|
313
|
+
metadata = list(install.glob("lib/python*/site-packages/brainlayer-*.dist-info/direct_url.json"))
|
|
314
|
+
version_match = re.search(r"python(\d+\.\d+)$", interpreter.resolve().name)
|
|
315
|
+
version = version_match.group(1) if version_match else None
|
|
316
|
+
user_metadata = []
|
|
317
|
+
if version:
|
|
318
|
+
user_metadata = list(
|
|
319
|
+
(Path.home() / "Library/Python" / version / "lib/python/site-packages").glob(
|
|
320
|
+
"brainlayer-*.dist-info"
|
|
321
|
+
)
|
|
322
|
+
)
|
|
323
|
+
user_metadata += list(
|
|
324
|
+
(Path.home() / ".local/lib" / f"python{version}/site-packages").glob("brainlayer-*.dist-info")
|
|
325
|
+
)
|
|
326
|
+
if any(json.loads(p.read_text()).get("dir_info", {}).get("editable") is True for p in metadata):
|
|
327
|
+
kind = "editable install"
|
|
328
|
+
env = "PYTHONNOUSERSITE=1 " if user_metadata else ""
|
|
329
|
+
remedy = f"sudo {env}{shlex.quote(str(interpreter.parent / 'pip3'))} uninstall brainlayer"
|
|
330
|
+
if path.is_relative_to(Path.home() / ".local/bin") or path.is_relative_to(
|
|
331
|
+
Path.home() / "Library/Python"
|
|
332
|
+
):
|
|
333
|
+
remedy = f"{shlex.quote(str(interpreter))} -m pip uninstall brainlayer"
|
|
334
|
+
except (OSError, ValueError, TypeError, AttributeError):
|
|
335
|
+
pass
|
|
336
|
+
result.issues.append(
|
|
337
|
+
DoctorIssue(
|
|
338
|
+
"cli_path_shadow", severity, f"{path}: {kind}; {remedy}", {"path": str(path), "remediation": remedy}
|
|
339
|
+
)
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
|
|
258
343
|
def _ro_conn(db_path: Path) -> sqlite3.Connection:
|
|
259
344
|
uri = f"file:{db_path.expanduser()}?mode=ro"
|
|
260
345
|
conn = sqlite3.connect(uri, uri=True, timeout=5)
|
|
@@ -581,6 +666,8 @@ def run_doctor(
|
|
|
581
666
|
def warning(code: str, message: str, **details: Any) -> None:
|
|
582
667
|
result.issues.append(DoctorIssue(code, "warning", message, details))
|
|
583
668
|
|
|
669
|
+
_check_cli_path_shadow(result)
|
|
670
|
+
|
|
584
671
|
db_config_error = get_brainlayer_db_config_error()
|
|
585
672
|
if db_config_error is not None:
|
|
586
673
|
fatal(
|
|
@@ -181,6 +181,10 @@ class RealtimeIndexer:
|
|
|
181
181
|
next_index = row[0] if row else 0
|
|
182
182
|
|
|
183
183
|
try:
|
|
184
|
+
from ..pipeline.secret_scrub import scrub_secrets
|
|
185
|
+
|
|
186
|
+
compact_summary = scrub_secrets(compact_summary).text
|
|
187
|
+
trigger = scrub_secrets(trigger).text
|
|
184
188
|
cursor = self._db.execute(
|
|
185
189
|
"""INSERT INTO chapters (session_id, chapter_index, compact_summary, trigger)
|
|
186
190
|
VALUES (?, ?, ?, ?)""",
|
|
@@ -50,6 +50,8 @@ import traceback
|
|
|
50
50
|
import uuid
|
|
51
51
|
from dataclasses import dataclass
|
|
52
52
|
from pathlib import Path
|
|
53
|
+
from time import monotonic as _monotonic
|
|
54
|
+
from time import sleep as _sleep
|
|
53
55
|
from typing import Any
|
|
54
56
|
|
|
55
57
|
from . import backup_daily
|
|
@@ -190,7 +192,7 @@ def _icloud_deadline_for_archive(
|
|
|
190
192
|
"""Set an archive-sized budget when the iCloud copy operation begins."""
|
|
191
193
|
archive_size = Path(archive_path).expanduser().stat().st_size
|
|
192
194
|
configured_timeout = _configured_icloud_timeout_seconds() if timeout_seconds is None else timeout_seconds
|
|
193
|
-
return
|
|
195
|
+
return _monotonic() + max(configured_timeout, archive_size / ICLOUD_MIN_BYTES_PER_SECOND)
|
|
194
196
|
|
|
195
197
|
|
|
196
198
|
def _load_state(path: Path) -> dict[str, Any]:
|
|
@@ -454,7 +456,7 @@ class ICloudProbeError(RuntimeError):
|
|
|
454
456
|
|
|
455
457
|
|
|
456
458
|
def _check_icloud_deadline(deadline: float | None, phase: str) -> None:
|
|
457
|
-
if deadline is not None and
|
|
459
|
+
if deadline is not None and _monotonic() >= deadline:
|
|
458
460
|
raise ICloudDeadlineExceeded(f"iCloud operation deadline exceeded during {phase}")
|
|
459
461
|
|
|
460
462
|
|
|
@@ -663,7 +665,7 @@ def copy_archive_to_icloud(
|
|
|
663
665
|
)
|
|
664
666
|
|
|
665
667
|
def remaining_seconds() -> float:
|
|
666
|
-
return max(deadline -
|
|
668
|
+
return max(deadline - _monotonic(), 0.001)
|
|
667
669
|
|
|
668
670
|
icloud_dir.mkdir(parents=True, exist_ok=True)
|
|
669
671
|
_check_icloud_deadline(deadline, "preparing the iCloud directory")
|
|
@@ -738,14 +740,14 @@ def copy_archive_to_icloud(
|
|
|
738
740
|
result = receipt(reused=True)
|
|
739
741
|
_trash_stale_unverified_icloud_items(icloud_dir, logical_sha256)
|
|
740
742
|
return result
|
|
741
|
-
if
|
|
743
|
+
if _monotonic() >= deadline:
|
|
742
744
|
if state.get("is_uploading") is True and destination.is_file():
|
|
743
745
|
return pending_receipt()
|
|
744
746
|
raise ICloudDeadlineExceeded(
|
|
745
747
|
"existing iCloud copy was not uploaded and materialized before its deadline: "
|
|
746
748
|
f"path={destination} state={state!r}"
|
|
747
749
|
)
|
|
748
|
-
|
|
750
|
+
_sleep(min(poll_interval_seconds, remaining_seconds()))
|
|
749
751
|
status_path = placeholder if not destination.exists() and placeholder.exists() else destination
|
|
750
752
|
state = _icloud_item_state(
|
|
751
753
|
status_path,
|
|
@@ -767,7 +769,7 @@ def copy_archive_to_icloud(
|
|
|
767
769
|
raise
|
|
768
770
|
_quarantine_unverified_icloud_item(destination)
|
|
769
771
|
_quarantine_unverified_icloud_item(placeholder)
|
|
770
|
-
if
|
|
772
|
+
if _monotonic() >= deadline:
|
|
771
773
|
raise
|
|
772
774
|
|
|
773
775
|
temp_path = icloud_dir / f".{destination.name}.{os.getpid()}.partial"
|
|
@@ -800,10 +802,10 @@ def copy_archive_to_icloud(
|
|
|
800
802
|
# The local copy/hash budget is complete. Network status gets a fresh phase
|
|
801
803
|
# budget, so an expired disk-scaled deadline cannot become a 0.001-second
|
|
802
804
|
# osascript timeout on the upload probe.
|
|
803
|
-
network_deadline =
|
|
805
|
+
network_deadline = _monotonic() + configured_timeout
|
|
804
806
|
|
|
805
807
|
def network_remaining_seconds() -> float:
|
|
806
|
-
return max(network_deadline -
|
|
808
|
+
return max(network_deadline - _monotonic(), 0.001)
|
|
807
809
|
|
|
808
810
|
verified = False
|
|
809
811
|
pending = False
|
|
@@ -841,10 +843,10 @@ def copy_archive_to_icloud(
|
|
|
841
843
|
)
|
|
842
844
|
_trash_stale_unverified_icloud_items(icloud_dir, logical_sha256)
|
|
843
845
|
return result
|
|
844
|
-
if
|
|
846
|
+
if _monotonic() >= deadline and state.get("is_uploading") is True and destination.is_file():
|
|
845
847
|
pending = True
|
|
846
848
|
return pending_receipt()
|
|
847
|
-
if
|
|
849
|
+
if _monotonic() >= network_deadline:
|
|
848
850
|
if state.get("is_uploading") is True and destination.is_file():
|
|
849
851
|
pending = True
|
|
850
852
|
return pending_receipt()
|
|
@@ -854,7 +856,7 @@ def copy_archive_to_icloud(
|
|
|
854
856
|
"iCloud copy was not uploaded and materialized before its deadline: "
|
|
855
857
|
f"path={destination} materialization={materialization} state={state!r}"
|
|
856
858
|
)
|
|
857
|
-
|
|
859
|
+
_sleep(min(poll_interval_seconds, network_remaining_seconds()))
|
|
858
860
|
placeholder = destination.with_name(f".{destination.name}.icloud")
|
|
859
861
|
status_path = placeholder if not destination.exists() and placeholder.exists() else destination
|
|
860
862
|
state = _icloud_item_state(status_path, request_download=True, timeout_seconds=network_remaining_seconds())
|
|
@@ -1042,7 +1044,7 @@ def _icloud_inventory_is_verified(
|
|
|
1042
1044
|
return False
|
|
1043
1045
|
|
|
1044
1046
|
configured_timeout = _configured_icloud_timeout_seconds() if timeout_seconds is None else timeout_seconds
|
|
1045
|
-
deadline = deadline if deadline is not None else
|
|
1047
|
+
deadline = deadline if deadline is not None else _monotonic() + configured_timeout
|
|
1046
1048
|
for archive_name in sorted(referenced_by_sources):
|
|
1047
1049
|
try:
|
|
1048
1050
|
_check_icloud_deadline(deadline, f"validating iCloud inventory archive {archive_name}")
|
|
@@ -1086,7 +1088,7 @@ def _icloud_inventory_is_verified(
|
|
|
1086
1088
|
archive_valid = True
|
|
1087
1089
|
while True:
|
|
1088
1090
|
_check_icloud_deadline(deadline, f"probing iCloud inventory archive {archive_name}")
|
|
1089
|
-
remaining = max(deadline -
|
|
1091
|
+
remaining = max(deadline - _monotonic(), 0.001)
|
|
1090
1092
|
status_path = placeholder if not destination.exists() and placeholder.exists() else destination
|
|
1091
1093
|
item_state = _icloud_item_state(
|
|
1092
1094
|
status_path,
|
|
@@ -1117,11 +1119,11 @@ def _icloud_inventory_is_verified(
|
|
|
1117
1119
|
elif validated_sources is not None:
|
|
1118
1120
|
validated_sources.update(referenced_by_sources[archive_name])
|
|
1119
1121
|
break
|
|
1120
|
-
if
|
|
1122
|
+
if _monotonic() >= deadline:
|
|
1121
1123
|
raise ICloudDeadlineExceeded(
|
|
1122
1124
|
f"iCloud operation deadline exceeded while materializing inventory archive {archive_name}"
|
|
1123
1125
|
)
|
|
1124
|
-
|
|
1126
|
+
_sleep(min(poll_interval_seconds, remaining))
|
|
1125
1127
|
if not archive_valid:
|
|
1126
1128
|
complete = False
|
|
1127
1129
|
except (OSError, RuntimeError, subprocess.TimeoutExpired) as exc:
|