mobster-cli 0.4.3__py3-none-any.whl
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.
- mobile_agent/__init__.py +4 -0
- mobile_agent/__main__.py +598 -0
- mobile_agent/accounts/__init__.py +1 -0
- mobile_agent/accounts/context.py +80 -0
- mobile_agent/accounts/mcp.py +55 -0
- mobile_agent/accounts/pacing.py +318 -0
- mobile_agent/accounts/register.py +78 -0
- mobile_agent/accounts/routes.py +212 -0
- mobile_agent/accounts/routing.py +69 -0
- mobile_agent/accounts/screen.py +273 -0
- mobile_agent/accounts/stats.py +54 -0
- mobile_agent/accounts/store.py +394 -0
- mobile_agent/agent_hooks.py +123 -0
- mobile_agent/ai_plans/__init__.py +7 -0
- mobile_agent/ai_plans/agents.py +96 -0
- mobile_agent/ai_plans/claude_code.py +208 -0
- mobile_agent/ai_plans/cli.py +400 -0
- mobile_agent/ai_plans/codex.py +258 -0
- mobile_agent/ai_plans/compat_chat.py +307 -0
- mobile_agent/ai_plans/device_login.py +164 -0
- mobile_agent/ai_plans/draft_client.py +68 -0
- mobile_agent/ai_plans/gemini.py +137 -0
- mobile_agent/ai_plans/instructions.py +65 -0
- mobile_agent/ai_plans/mcp_http.py +160 -0
- mobile_agent/ai_plans/phone_tools.py +892 -0
- mobile_agent/ai_plans/providers.py +235 -0
- mobile_agent/ai_plans/routes.py +160 -0
- mobile_agent/ai_plans/runner.py +400 -0
- mobile_agent/ai_plans/vault.py +114 -0
- mobile_agent/alerts.py +337 -0
- mobile_agent/android/__init__.py +18 -0
- mobile_agent/android/adb.py +268 -0
- mobile_agent/android/conditions.py +128 -0
- mobile_agent/android/doctor.py +54 -0
- mobile_agent/android/driver.py +137 -0
- mobile_agent/android/emulator.py +216 -0
- mobile_agent/android/logs.py +85 -0
- mobile_agent/android/manager.py +225 -0
- mobile_agent/android/records.py +31 -0
- mobile_agent/android/tree.py +96 -0
- mobile_agent/answer_shapes.py +230 -0
- mobile_agent/api_devices.py +333 -0
- mobile_agent/api_errors.py +17 -0
- mobile_agent/api_get.py +249 -0
- mobile_agent/api_http.py +467 -0
- mobile_agent/api_post.py +453 -0
- mobile_agent/api_routes.py +173 -0
- mobile_agent/app_api/__init__.py +4 -0
- mobile_agent/app_api/cli.py +151 -0
- mobile_agent/app_api/endpoints.py +193 -0
- mobile_agent/app_api/mcp.py +54 -0
- mobile_agent/app_api/openapi.py +146 -0
- mobile_agent/app_api/reading.py +172 -0
- mobile_agent/app_api/register.py +10 -0
- mobile_agent/app_api/screen.py +216 -0
- mobile_agent/app_api/service.py +382 -0
- mobile_agent/app_api/sessions.py +404 -0
- mobile_agent/app_api/tokens.py +167 -0
- mobile_agent/app_choice.py +131 -0
- mobile_agent/app_icons.py +183 -0
- mobile_agent/app_killer.py +69 -0
- mobile_agent/artifacts.py +606 -0
- mobile_agent/attach/__init__.py +13 -0
- mobile_agent/attach/cli.py +46 -0
- mobile_agent/attach/flutter.py +28 -0
- mobile_agent/attach/score.py +119 -0
- mobile_agent/attach/web.py +13 -0
- mobile_agent/attachments/__init__.py +1 -0
- mobile_agent/attachments/agent.py +418 -0
- mobile_agent/attachments/extract.py +217 -0
- mobile_agent/attachments/mcp.py +104 -0
- mobile_agent/attachments/register.py +73 -0
- mobile_agent/attachments/routes.py +326 -0
- mobile_agent/attachments/service.py +170 -0
- mobile_agent/attachments/sniff.py +152 -0
- mobile_agent/attachments/store.py +441 -0
- mobile_agent/audit_verify.py +436 -0
- mobile_agent/bench/README.md +410 -0
- mobile_agent/bench/__init__.py +0 -0
- mobile_agent/bench/__main__.py +394 -0
- mobile_agent/bench/ab.py +168 -0
- mobile_agent/bench/actuation.py +368 -0
- mobile_agent/bench/agents/__init__.py +9 -0
- mobile_agent/bench/agents/base.py +105 -0
- mobile_agent/bench/agents/plan_clients.py +245 -0
- mobile_agent/bench/agents/smart.py +533 -0
- mobile_agent/bench/checks.py +358 -0
- mobile_agent/bench/decide_gate.py +214 -0
- mobile_agent/bench/diagnose.py +181 -0
- mobile_agent/bench/evals/__init__.py +14 -0
- mobile_agent/bench/evals/build.py +565 -0
- mobile_agent/bench/evals/checkpoint.py +214 -0
- mobile_agent/bench/evals/irt.py +219 -0
- mobile_agent/bench/evals/l0.py +647 -0
- mobile_agent/bench/evals/l1.py +866 -0
- mobile_agent/bench/evals/models.py +124 -0
- mobile_agent/bench/evals/pairwise.py +189 -0
- mobile_agent/bench/evals/proxy.py +90 -0
- mobile_agent/bench/evals/sets.py +149 -0
- mobile_agent/bench/evals/traces.py +446 -0
- mobile_agent/bench/fixtures.py +176 -0
- mobile_agent/bench/guard_replay.py +129 -0
- mobile_agent/bench/h2-manifest.json +20 -0
- mobile_agent/bench/h3-manifest.json +20 -0
- mobile_agent/bench/heldout-manifest.json +32 -0
- mobile_agent/bench/heldout.py +375 -0
- mobile_agent/bench/iosworld.py +901 -0
- mobile_agent/bench/iosworld_stats.py +510 -0
- mobile_agent/bench/llm_units.py +116 -0
- mobile_agent/bench/manifest.json +93 -0
- mobile_agent/bench/metrics.py +66 -0
- mobile_agent/bench/phone.py +44 -0
- mobile_agent/bench/replay.py +172 -0
- mobile_agent/bench/report.py +207 -0
- mobile_agent/bench/runner.py +456 -0
- mobile_agent/bench/safety.py +310 -0
- mobile_agent/bench/stream_timing.py +213 -0
- mobile_agent/bench/suite.py +444 -0
- mobile_agent/bench/sweep.py +176 -0
- mobile_agent/bench/tape.py +798 -0
- mobile_agent/bench/tape_replay.py +88 -0
- mobile_agent/bench/trace_evals.py +128 -0
- mobile_agent/bench/trace_view.py +81 -0
- mobile_agent/bench/truth/iphone15pro.json +138 -0
- mobile_agent/bench/truth.py +340 -0
- mobile_agent/bench/tuning.py +543 -0
- mobile_agent/bench/tuning_analysis.py +656 -0
- mobile_agent/bench/vague-heldout-manifest.json +12 -0
- mobile_agent/bench/vague_rubric.py +135 -0
- mobile_agent/bench/vague_run.py +243 -0
- mobile_agent/brand.py +454 -0
- mobile_agent/brand_fonts.py +756 -0
- mobile_agent/build_identity.py +21 -0
- mobile_agent/cable_bt.py +623 -0
- mobile_agent/cable_bt_runtime.py +128 -0
- mobile_agent/catalog.py +69 -0
- mobile_agent/chart_svg.py +415 -0
- mobile_agent/chat_here.py +189 -0
- mobile_agent/cli_constants.py +5 -0
- mobile_agent/cli_parser.py +611 -0
- mobile_agent/cli_run.py +115 -0
- mobile_agent/compiled_read/__init__.py +21 -0
- mobile_agent/compiled_read/delta.py +89 -0
- mobile_agent/compiled_read/extractors.py +202 -0
- mobile_agent/compiled_read/routes.py +56 -0
- mobile_agent/completion.py +267 -0
- mobile_agent/compose.py +48 -0
- mobile_agent/config.py +114 -0
- mobile_agent/connectors/__init__.py +1 -0
- mobile_agent/connectors/api_session.py +76 -0
- mobile_agent/connectors/callback_page.py +146 -0
- mobile_agent/connectors/client.py +582 -0
- mobile_agent/connectors/github_app.py +55 -0
- mobile_agent/connectors/graph_api.py +396 -0
- mobile_agent/connectors/imports.py +170 -0
- mobile_agent/connectors/issue.py +66 -0
- mobile_agent/connectors/keystore.py +85 -0
- mobile_agent/connectors/marks.py +222 -0
- mobile_agent/connectors/oauth.py +476 -0
- mobile_agent/connectors/picker.py +228 -0
- mobile_agent/connectors/presets.py +591 -0
- mobile_agent/connectors/register.py +48 -0
- mobile_agent/connectors/routes.py +153 -0
- mobile_agent/connectors/service.py +966 -0
- mobile_agent/connectors/shared.py +136 -0
- mobile_agent/connectors/skill.py +446 -0
- mobile_agent/connectors/slack_api.py +269 -0
- mobile_agent/connectors/steps.py +92 -0
- mobile_agent/connectors/store.py +144 -0
- mobile_agent/connectors/vault.py +72 -0
- mobile_agent/console.py +178 -0
- mobile_agent/content_queue/__init__.py +1 -0
- mobile_agent/content_queue/client.py +263 -0
- mobile_agent/content_queue/importing.py +153 -0
- mobile_agent/content_queue/media_check.py +224 -0
- mobile_agent/content_queue/prompt.py +64 -0
- mobile_agent/content_queue/register.py +91 -0
- mobile_agent/content_queue/routes.py +370 -0
- mobile_agent/content_queue/runner.py +475 -0
- mobile_agent/content_queue/store.py +521 -0
- mobile_agent/contract.py +996 -0
- mobile_agent/contract_asks.py +248 -0
- mobile_agent/contract_prompt.py +45 -0
- mobile_agent/contract_screen.py +981 -0
- mobile_agent/contract_view.py +84 -0
- mobile_agent/corpus/__init__.py +26 -0
- mobile_agent/corpus/cli.py +63 -0
- mobile_agent/corpus/consent.py +55 -0
- mobile_agent/corpus/listener.py +76 -0
- mobile_agent/corpus/register.py +9 -0
- mobile_agent/corpus/skeleton.py +72 -0
- mobile_agent/corpus/store.py +143 -0
- mobile_agent/costs.py +161 -0
- mobile_agent/decider_scope.py +40 -0
- mobile_agent/decision_provider.py +189 -0
- mobile_agent/deeplinks.py +352 -0
- mobile_agent/device_info.py +116 -0
- mobile_agent/device_manager.py +978 -0
- mobile_agent/device_status.py +195 -0
- mobile_agent/device_targets.py +217 -0
- mobile_agent/devices.py +676 -0
- mobile_agent/devkit/__init__.py +64 -0
- mobile_agent/devkit/account_cli.py +90 -0
- mobile_agent/devkit/asc.py +562 -0
- mobile_agent/devkit/asc_cli.py +214 -0
- mobile_agent/devkit/ci.py +737 -0
- mobile_agent/devkit/ci_cli.py +196 -0
- mobile_agent/devkit/ci_hosts.py +194 -0
- mobile_agent/devkit/es256.py +197 -0
- mobile_agent/devkit/expo.py +440 -0
- mobile_agent/devkit/expo_cli.py +405 -0
- mobile_agent/devkit/expo_dev.py +203 -0
- mobile_agent/devkit/expo_routes.py +90 -0
- mobile_agent/devkit/fixtures_cli.py +133 -0
- mobile_agent/devkit/fixtures_routes.py +82 -0
- mobile_agent/devkit/links.py +375 -0
- mobile_agent/devkit/links_cli.py +123 -0
- mobile_agent/devkit/links_routes.py +83 -0
- mobile_agent/devkit/nightly.py +152 -0
- mobile_agent/devkit/shots.py +203 -0
- mobile_agent/devkit/testflight.py +155 -0
- mobile_agent/devkit/testflight_cli.py +127 -0
- mobile_agent/devkit/testflight_routes.py +111 -0
- mobile_agent/devkit/xcode.py +236 -0
- mobile_agent/devkit/xcode_cli.py +101 -0
- mobile_agent/devtools.py +419 -0
- mobile_agent/doctor.py +509 -0
- mobile_agent/doctor_fix.py +467 -0
- mobile_agent/downloads.py +55 -0
- mobile_agent/drivers.py +793 -0
- mobile_agent/dry_run.py +102 -0
- mobile_agent/effect_ledger.py +83 -0
- mobile_agent/engine_health.py +378 -0
- mobile_agent/engine_hooks.py +96 -0
- mobile_agent/engines.py +408 -0
- mobile_agent/env_file.py +63 -0
- mobile_agent/env_secrets.py +149 -0
- mobile_agent/errors.py +15 -0
- mobile_agent/evals/__init__.py +3 -0
- mobile_agent/evals/app_survey.py +149 -0
- mobile_agent/evals/development/__init__.py +1 -0
- mobile_agent/evals/development/score.py +276 -0
- mobile_agent/evals/latency_report.py +312 -0
- mobile_agent/evals/offline_report.py +374 -0
- mobile_agent/evals/oracles.py +298 -0
- mobile_agent/evals/redteam/__init__.py +15 -0
- mobile_agent/evals/redteam/__main__.py +72 -0
- mobile_agent/evals/redteam/cases.py +730 -0
- mobile_agent/evals/redteam/harness.py +604 -0
- mobile_agent/evals/redteam/live.py +139 -0
- mobile_agent/evals/redteam/plain.py +85 -0
- mobile_agent/evals/vision/__init__.py +0 -0
- mobile_agent/evals/vision/images/dog_001.jpg +0 -0
- mobile_agent/evals/vision/images/dog_002.jpg +0 -0
- mobile_agent/evals/vision/images/dog_003.jpg +0 -0
- mobile_agent/evals/vision/images/dog_004.jpg +0 -0
- mobile_agent/evals/vision/images/dog_005.jpg +0 -0
- mobile_agent/evals/vision/images/dog_006.jpg +0 -0
- mobile_agent/evals/vision/images/dog_007.jpg +0 -0
- mobile_agent/evals/vision/images/dog_010.jpg +0 -0
- mobile_agent/evals/vision/images/dog_011.jpg +0 -0
- mobile_agent/evals/vision/images/dog_013.jpg +0 -0
- mobile_agent/evals/vision/images/dog_014.jpg +0 -0
- mobile_agent/evals/vision/images/dog_015.jpg +0 -0
- mobile_agent/evals/vision/images/dog_016.jpg +0 -0
- mobile_agent/evals/vision/images/dog_017.jpg +0 -0
- mobile_agent/evals/vision/images/dog_019.jpg +0 -0
- mobile_agent/evals/vision/images/dog_020.jpg +0 -0
- mobile_agent/evals/vision/images/dog_021.jpg +0 -0
- mobile_agent/evals/vision/images/dog_022.jpg +0 -0
- mobile_agent/evals/vision/images/dog_023.jpg +0 -0
- mobile_agent/evals/vision/images/dog_024.jpg +0 -0
- mobile_agent/evals/vision/images/dog_026.jpg +0 -0
- mobile_agent/evals/vision/images/dog_027.jpg +0 -0
- mobile_agent/evals/vision/images/dog_034.jpg +0 -0
- mobile_agent/evals/vision/images/dog_035.jpg +0 -0
- mobile_agent/evals/vision/images/dog_038.jpg +0 -0
- mobile_agent/evals/vision/images/dog_042.jpg +0 -0
- mobile_agent/evals/vision/images/dog_047.jpg +0 -0
- mobile_agent/evals/vision/images/dog_052.jpg +0 -0
- mobile_agent/evals/vision/images/dog_058.jpg +0 -0
- mobile_agent/evals/vision/images/dog_062.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_000.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_010.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_011.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_015.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_016.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_017.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_019.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_021.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_025.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_026.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_027.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_030.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_032.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_033.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_034.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_035.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_036.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_037.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_039.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_040.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_041.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_042.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_043.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_044.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_046.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_047.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_048.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_049.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_051.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_053.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_054.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_055.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_058.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_059.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_002.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_003.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_004.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_007.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_009.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_010.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_011.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_012.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_013.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_014.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_016.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_017.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_023.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_027.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_029.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_032.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_033.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_036.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_041.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_043.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_047.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_049.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_051.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_053.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_056.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_057.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_058.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_059.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_060.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_061.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_062.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_063.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_064.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_065.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_066.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_067.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_068.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_069.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_070.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_071.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_000.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_001.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_002.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_004.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_005.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_008.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_009.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_010.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_012.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_013.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_015.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_016.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_017.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_018.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_021.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_022.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_023.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_024.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_030.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_031.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_032.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_033.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_034.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_036.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_038.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_039.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_040.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_041.jpg +0 -0
- mobile_agent/evals/vision/manifest.json +2355 -0
- mobile_agent/eventbus.py +110 -0
- mobile_agent/explain.py +34 -0
- mobile_agent/failure_line.py +197 -0
- mobile_agent/fanout.py +663 -0
- mobile_agent/feedback/__init__.py +2 -0
- mobile_agent/feedback/bundle.py +204 -0
- mobile_agent/feedback/outbox.py +196 -0
- mobile_agent/feedback/register.py +9 -0
- mobile_agent/feedback/routes.py +177 -0
- mobile_agent/fleet.py +841 -0
- mobile_agent/fleet_health.py +729 -0
- mobile_agent/fleet_phone.py +186 -0
- mobile_agent/frontier/__init__.py +79 -0
- mobile_agent/frontier/act.py +380 -0
- mobile_agent/frontier/actions.py +70 -0
- mobile_agent/frontier/agent.py +256 -0
- mobile_agent/frontier/approvals.py +366 -0
- mobile_agent/frontier/hooks.py +199 -0
- mobile_agent/frontier/interrupts.py +125 -0
- mobile_agent/frontier/observe.py +331 -0
- mobile_agent/frontier/phases.py +381 -0
- mobile_agent/frontier/pipeline.py +61 -0
- mobile_agent/frontier/prompt.py +384 -0
- mobile_agent/frontier/protocol.py +91 -0
- mobile_agent/frontier/safety.py +467 -0
- mobile_agent/frontier/saved.py +375 -0
- mobile_agent/frontier/screen.py +419 -0
- mobile_agent/frontier/skills.py +147 -0
- mobile_agent/frontier/stages/__init__.py +1 -0
- mobile_agent/frontier/stages/after.py +192 -0
- mobile_agent/frontier/stages/approve.py +138 -0
- mobile_agent/frontier/stages/controls.py +115 -0
- mobile_agent/frontier/stages/ends.py +102 -0
- mobile_agent/frontier/stages/gate.py +92 -0
- mobile_agent/frontier/stages/guards.py +132 -0
- mobile_agent/frontier/stages/points.py +155 -0
- mobile_agent/frontier/stages/send.py +177 -0
- mobile_agent/frontier/turn.py +151 -0
- mobile_agent/full_disk_access.py +83 -0
- mobile_agent/gemini.py +69 -0
- mobile_agent/genuine.py +120 -0
- mobile_agent/graph/__init__.py +4 -0
- mobile_agent/graph/catalog.py +789 -0
- mobile_agent/graph/chain.py +306 -0
- mobile_agent/graph/dispatch.py +98 -0
- mobile_agent/graph/draft.py +373 -0
- mobile_agent/graph/edit.py +76 -0
- mobile_agent/graph/estimate.py +189 -0
- mobile_agent/graph/expr.py +562 -0
- mobile_agent/graph/functions.py +271 -0
- mobile_agent/graph/infer.py +144 -0
- mobile_agent/graph/nodes.py +936 -0
- mobile_agent/graph/phone_steps.py +178 -0
- mobile_agent/graph/rerun.py +327 -0
- mobile_agent/graph/run.py +899 -0
- mobile_agent/graph/schema.py +842 -0
- mobile_agent/graph/state.py +161 -0
- mobile_agent/graph/values.py +198 -0
- mobile_agent/harness/__init__.py +2 -0
- mobile_agent/harness/checkpoints.py +468 -0
- mobile_agent/harness/control.py +101 -0
- mobile_agent/harness/prewarm.py +213 -0
- mobile_agent/harness/register.py +43 -0
- mobile_agent/harness/routes.py +114 -0
- mobile_agent/harness/settings.py +66 -0
- mobile_agent/harness/tools/__init__.py +67 -0
- mobile_agent/harness/tools/ask_user.py +102 -0
- mobile_agent/harness/tools/watch.py +120 -0
- mobile_agent/harness_api.py +815 -0
- mobile_agent/hedge.py +203 -0
- mobile_agent/help_topics.py +305 -0
- mobile_agent/hints.py +248 -0
- mobile_agent/history.py +69 -0
- mobile_agent/home.py +214 -0
- mobile_agent/http_pool.py +175 -0
- mobile_agent/image_policy.py +139 -0
- mobile_agent/inbox.py +148 -0
- mobile_agent/installed_apps.py +344 -0
- mobile_agent/integrations/__init__.py +1 -0
- mobile_agent/integrations/cli.py +472 -0
- mobile_agent/integrations/client_files.py +65 -0
- mobile_agent/integrations/clients.py +998 -0
- mobile_agent/integrations/doctor.py +185 -0
- mobile_agent/integrations/jsonc.py +383 -0
- mobile_agent/integrations/routes.py +135 -0
- mobile_agent/integrations/rules.py +112 -0
- mobile_agent/integrations/server_options.py +46 -0
- mobile_agent/integrations/skill.py +152 -0
- mobile_agent/integrations/skill_files.py +10 -0
- mobile_agent/integrations/textedit.py +309 -0
- mobile_agent/intents/__init__.py +17 -0
- mobile_agent/intents/atlas.py +216 -0
- mobile_agent/intents/callback.py +44 -0
- mobile_agent/intents/cli.py +55 -0
- mobile_agent/intents/harvest.py +91 -0
- mobile_agent/intents/plan.py +93 -0
- mobile_agent/ios/__init__.py +1 -0
- mobile_agent/ios/guide.py +16 -0
- mobile_agent/ios/kb.py +212 -0
- mobile_agent/ios/kb_data.py +1588 -0
- mobile_agent/ios/mcp.py +44 -0
- mobile_agent/ios/register.py +8 -0
- mobile_agent/ios/system.py +369 -0
- mobile_agent/journal.py +531 -0
- mobile_agent/keychain_acl.py +374 -0
- mobile_agent/keys.py +395 -0
- mobile_agent/latency_overlap.py +36 -0
- mobile_agent/latency_trace.py +170 -0
- mobile_agent/learned/__init__.py +15 -0
- mobile_agent/learned/app_path.py +160 -0
- mobile_agent/learned/code_read.py +38 -0
- mobile_agent/learned/parts.py +546 -0
- mobile_agent/learned/programs.py +691 -0
- mobile_agent/learned/register.py +171 -0
- mobile_agent/learned/routes.py +61 -0
- mobile_agent/learned/taught.py +329 -0
- mobile_agent/learned/teach.py +538 -0
- mobile_agent/learned/workflows.py +647 -0
- mobile_agent/learned/zero.py +41 -0
- mobile_agent/links.py +37 -0
- mobile_agent/listen.py +607 -0
- mobile_agent/lock_spike.py +278 -0
- mobile_agent/lockscreen.py +526 -0
- mobile_agent/login.py +354 -0
- mobile_agent/longform/__init__.py +15 -0
- mobile_agent/longform/ads.py +191 -0
- mobile_agent/longform/blocks.py +117 -0
- mobile_agent/longform/budget.py +265 -0
- mobile_agent/longform/context.py +285 -0
- mobile_agent/longform/costs.py +93 -0
- mobile_agent/longform/digest.py +145 -0
- mobile_agent/longform/extract.py +508 -0
- mobile_agent/longform/feed.py +505 -0
- mobile_agent/longform/followup.py +168 -0
- mobile_agent/longform/frames.py +82 -0
- mobile_agent/longform/identity.py +87 -0
- mobile_agent/longform/inbox.py +221 -0
- mobile_agent/longform/items.py +425 -0
- mobile_agent/longform/models.py +115 -0
- mobile_agent/longform/notebook.py +329 -0
- mobile_agent/longform/plan.py +614 -0
- mobile_agent/longform/recall.py +195 -0
- mobile_agent/longform/receipts.py +81 -0
- mobile_agent/longform/register.py +124 -0
- mobile_agent/longform/report.py +343 -0
- mobile_agent/longform/routes.py +102 -0
- mobile_agent/longform/session.py +644 -0
- mobile_agent/longform/store.py +146 -0
- mobile_agent/longform/synthesis.py +347 -0
- mobile_agent/mac_tools.py +223 -0
- mobile_agent/manual_control.py +143 -0
- mobile_agent/maps.py +149 -0
- mobile_agent/marks.py +92 -0
- mobile_agent/mcp_server/__init__.py +3 -0
- mobile_agent/mcp_server/__main__.py +29 -0
- mobile_agent/mcp_server/action_journal.py +24 -0
- mobile_agent/mcp_server/action_tools.py +638 -0
- mobile_agent/mcp_server/approvals.py +219 -0
- mobile_agent/mcp_server/catalog.py +92 -0
- mobile_agent/mcp_server/cli.py +194 -0
- mobile_agent/mcp_server/codes.py +147 -0
- mobile_agent/mcp_server/device_tools.py +306 -0
- mobile_agent/mcp_server/direct.py +272 -0
- mobile_agent/mcp_server/expo_tools.py +105 -0
- mobile_agent/mcp_server/phone_tools.py +510 -0
- mobile_agent/mcp_server/protocol.py +364 -0
- mobile_agent/mcp_server/registry.py +55 -0
- mobile_agent/mcp_server/run_tools.py +531 -0
- mobile_agent/mcp_server/schemas.py +425 -0
- mobile_agent/mcp_server/screen_tools.py +168 -0
- mobile_agent/mcp_server/session.py +469 -0
- mobile_agent/mcp_server/support.py +145 -0
- mobile_agent/mcp_server/tools.py +493 -0
- mobile_agent/memory/__init__.py +9 -0
- mobile_agent/memory/cli.py +207 -0
- mobile_agent/memory/extract.py +191 -0
- mobile_agent/memory/listener.py +100 -0
- mobile_agent/memory/model.py +144 -0
- mobile_agent/memory/project.py +96 -0
- mobile_agent/memory/provider.py +127 -0
- mobile_agent/memory/register.py +21 -0
- mobile_agent/memory/retrieval.py +172 -0
- mobile_agent/memory/routes.py +216 -0
- mobile_agent/memory/runs.py +124 -0
- mobile_agent/memory/store.py +806 -0
- mobile_agent/memory/writer.py +278 -0
- mobile_agent/mirroring.py +585 -0
- mobile_agent/mirroring_runtime.py +221 -0
- mobile_agent/model_clients.py +604 -0
- mobile_agent/narrate.py +813 -0
- mobile_agent/native_helpers.py +84 -0
- mobile_agent/netxray/__init__.py +1 -0
- mobile_agent/netxray/analyze.py +322 -0
- mobile_agent/netxray/attribution.py +62 -0
- mobile_agent/netxray/capture.py +88 -0
- mobile_agent/netxray/cli.py +142 -0
- mobile_agent/netxray/job.py +219 -0
- mobile_agent/netxray/mobster-pcap.c +560 -0
- mobile_agent/netxray/packets.py +410 -0
- mobile_agent/netxray/pcapng.py +223 -0
- mobile_agent/netxray/register.py +93 -0
- mobile_agent/netxray/report.py +214 -0
- mobile_agent/netxray/store.py +48 -0
- mobile_agent/netxray/summary.py +157 -0
- mobile_agent/netxray/target.py +67 -0
- mobile_agent/netxray/tool.py +66 -0
- mobile_agent/netxray/trackers.py +194 -0
- mobile_agent/notify_triggers/__init__.py +1 -0
- mobile_agent/notify_triggers/rules.py +39 -0
- mobile_agent/notify_triggers/sources.py +315 -0
- mobile_agent/notify_triggers/watch.py +135 -0
- mobile_agent/offers.py +86 -0
- mobile_agent/orchestrate.py +215 -0
- mobile_agent/outlets/__init__.py +1 -0
- mobile_agent/outlets/connect.py +249 -0
- mobile_agent/outlets/folder.py +180 -0
- mobile_agent/outlets/hooks.py +453 -0
- mobile_agent/outlets/inbound.py +472 -0
- mobile_agent/outlets/inbound_reply.py +57 -0
- mobile_agent/outlets/inbound_routes.py +81 -0
- mobile_agent/outlets/kinds.py +308 -0
- mobile_agent/outlets/mail.py +110 -0
- mobile_agent/outlets/net.py +86 -0
- mobile_agent/outlets/notion.py +244 -0
- mobile_agent/outlets/register.py +56 -0
- mobile_agent/outlets/routes.py +135 -0
- mobile_agent/outlets/service.py +580 -0
- mobile_agent/outlets/sheets.py +169 -0
- mobile_agent/outlets/slack.py +177 -0
- mobile_agent/outlets/store.py +194 -0
- mobile_agent/outlets/vault.py +80 -0
- mobile_agent/outlets/webhook.py +316 -0
- mobile_agent/output_contract.py +141 -0
- mobile_agent/owner_presence.py +211 -0
- mobile_agent/ownership.py +219 -0
- mobile_agent/packs/__init__.py +1 -0
- mobile_agent/packs/calendar.py +243 -0
- mobile_agent/packs/context.py +68 -0
- mobile_agent/packs/core.py +757 -0
- mobile_agent/packs/front_door.py +316 -0
- mobile_agent/packs/heal.py +214 -0
- mobile_agent/packs/health.py +122 -0
- mobile_agent/packs/insights.py +76 -0
- mobile_agent/packs/instagram.py +640 -0
- mobile_agent/packs/linkedin.py +106 -0
- mobile_agent/packs/maps.py +196 -0
- mobile_agent/packs/mcp.py +149 -0
- mobile_agent/packs/messages.py +270 -0
- mobile_agent/packs/notes.py +226 -0
- mobile_agent/packs/photos.py +166 -0
- mobile_agent/packs/pins.py +47 -0
- mobile_agent/packs/reddit.py +110 -0
- mobile_agent/packs/register.py +276 -0
- mobile_agent/packs/rows.py +108 -0
- mobile_agent/packs/safari.py +143 -0
- mobile_agent/packs/settings.py +253 -0
- mobile_agent/packs/social.py +300 -0
- mobile_agent/packs/threads.py +97 -0
- mobile_agent/packs/tiktok.py +455 -0
- mobile_agent/packs/tools.py +290 -0
- mobile_agent/packs/x.py +127 -0
- mobile_agent/packs/youtube.py +199 -0
- mobile_agent/parity.py +270 -0
- mobile_agent/paths.py +99 -0
- mobile_agent/perception.py +470 -0
- mobile_agent/permissions/__init__.py +32 -0
- mobile_agent/permissions/consequence.py +535 -0
- mobile_agent/permissions/exceptions.py +99 -0
- mobile_agent/permissions/gate.py +316 -0
- mobile_agent/permissions/outside.py +316 -0
- mobile_agent/permissions/policy.py +288 -0
- mobile_agent/permissions/project_policy.py +411 -0
- mobile_agent/permissions/register.py +148 -0
- mobile_agent/permissions/rules.py +291 -0
- mobile_agent/phone_io/__init__.py +146 -0
- mobile_agent/phone_io/cli.py +427 -0
- mobile_agent/phone_io/clipboard.py +78 -0
- mobile_agent/phone_io/controls.py +78 -0
- mobile_agent/phone_io/files.py +289 -0
- mobile_agent/phone_io/handoff.py +123 -0
- mobile_agent/phone_io/icloud.py +118 -0
- mobile_agent/phone_io/install.py +188 -0
- mobile_agent/phone_io/network_capture.py +188 -0
- mobile_agent/phone_io/network_cli.py +138 -0
- mobile_agent/phone_io/network_profiles.py +138 -0
- mobile_agent/phone_io/photos.py +512 -0
- mobile_agent/phone_io/recording.py +115 -0
- mobile_agent/phone_io/secrets.py +146 -0
- mobile_agent/phone_io/transfer.py +895 -0
- mobile_agent/phone_io/tunnel.py +129 -0
- mobile_agent/phone_ready.py +91 -0
- mobile_agent/pinning.py +158 -0
- mobile_agent/plan_gate.py +807 -0
- mobile_agent/plugins/__init__.py +3 -0
- mobile_agent/plugins/install.py +163 -0
- mobile_agent/plugins/manifest.py +242 -0
- mobile_agent/plugins/register.py +15 -0
- mobile_agent/plugins/routes.py +82 -0
- mobile_agent/plugins/service.py +322 -0
- mobile_agent/plugins/skills.py +76 -0
- mobile_agent/plugins/store.py +79 -0
- mobile_agent/pool.py +285 -0
- mobile_agent/power_watch.py +275 -0
- mobile_agent/premium_model.py +22 -0
- mobile_agent/project_env.py +291 -0
- mobile_agent/project_env_cli.py +74 -0
- mobile_agent/project_env_routes.py +55 -0
- mobile_agent/projects/__init__.py +27 -0
- mobile_agent/projects/ci.py +332 -0
- mobile_agent/projects/clone.py +339 -0
- mobile_agent/projects/drafts.py +158 -0
- mobile_agent/projects/folder.py +436 -0
- mobile_agent/projects/github.py +131 -0
- mobile_agent/projects/register.py +31 -0
- mobile_agent/projects/revisions.py +253 -0
- mobile_agent/projects/routes.py +234 -0
- mobile_agent/projects/runs.py +297 -0
- mobile_agent/projects/service.py +695 -0
- mobile_agent/projects/setup.py +84 -0
- mobile_agent/projects/store.py +379 -0
- mobile_agent/projects/sync.py +380 -0
- mobile_agent/projects/workflow_files.py +270 -0
- mobile_agent/replay_core/__init__.py +26 -0
- mobile_agent/replay_core/checks.py +594 -0
- mobile_agent/replay_core/engine.py +596 -0
- mobile_agent/replay_core/heal.py +364 -0
- mobile_agent/replay_core/path.py +240 -0
- mobile_agent/replay_core/recorder.py +206 -0
- mobile_agent/replay_core/store.py +171 -0
- mobile_agent/result_blocks.py +941 -0
- mobile_agent/run.py +642 -0
- mobile_agent/run_delivery.py +123 -0
- mobile_agent/run_executor.py +200 -0
- mobile_agent/run_export.py +284 -0
- mobile_agent/run_gif.py +891 -0
- mobile_agent/runner_hints.py +86 -0
- mobile_agent/runner_renewal.py +205 -0
- mobile_agent/runtime_create.py +549 -0
- mobile_agent/runtime_driver.py +458 -0
- mobile_agent/runtime_limits.py +319 -0
- mobile_agent/runtime_queue.py +394 -0
- mobile_agent/runtime_runs.py +383 -0
- mobile_agent/runtime_status.py +181 -0
- mobile_agent/runtime_teach.py +295 -0
- mobile_agent/runtime_work.py +599 -0
- mobile_agent/screen_text.py +101 -0
- mobile_agent/secret_filter.py +113 -0
- mobile_agent/serve.py +160 -0
- mobile_agent/server.py +363 -0
- mobile_agent/service_support.py +42 -0
- mobile_agent/settle/__init__.py +1 -0
- mobile_agent/settle/predict.py +102 -0
- mobile_agent/settle/replay.py +92 -0
- mobile_agent/settle/store.py +93 -0
- mobile_agent/settle/stream.py +43 -0
- mobile_agent/setup_cli.py +225 -0
- mobile_agent/setup_errors.py +87 -0
- mobile_agent/setup_service.py +527 -0
- mobile_agent/share.py +104 -0
- mobile_agent/signing.py +184 -0
- mobile_agent/sim/__init__.py +6 -0
- mobile_agent/sim/__main__.py +26 -0
- mobile_agent/sim/api.py +952 -0
- mobile_agent/sim/apps.py +172 -0
- mobile_agent/sim/biometrics.py +84 -0
- mobile_agent/sim/cli.py +271 -0
- mobile_agent/sim/doctor.py +293 -0
- mobile_agent/sim/governor.py +90 -0
- mobile_agent/sim/holders.py +88 -0
- mobile_agent/sim/models.py +62 -0
- mobile_agent/sim/registry.py +161 -0
- mobile_agent/sim/simctl.py +269 -0
- mobile_agent/sim/wda.py +618 -0
- mobile_agent/sim/xcode.py +74 -0
- mobile_agent/skills/__init__.py +12 -0
- mobile_agent/skills/codes.py +597 -0
- mobile_agent/skills/notifications.py +281 -0
- mobile_agent/skills/open_url.py +127 -0
- mobile_agent/smart_events.py +588 -0
- mobile_agent/smart_result.py +490 -0
- mobile_agent/smart_run.py +413 -0
- mobile_agent/state.py +662 -0
- mobile_agent/states.py +217 -0
- mobile_agent/steering.py +70 -0
- mobile_agent/style.py +178 -0
- mobile_agent/supervised.py +234 -0
- mobile_agent/swift/helper_bt_hid.swift +534 -0
- mobile_agent/swift/helper_doc_text.swift +184 -0
- mobile_agent/swift/helper_draft_reply.swift +170 -0
- mobile_agent/swift/helper_map.swift +110 -0
- mobile_agent/swift/helper_mirroring.swift +1099 -0
- mobile_agent/swift/helper_mjpeg_mp4.swift +103 -0
- mobile_agent/swift/helper_pdf.swift +100 -0
- mobile_agent/swift/helper_phone_audio.swift +993 -0
- mobile_agent/swift/helper_phone_screen.swift +416 -0
- mobile_agent/swift/helper_screen_text.swift +24 -0
- mobile_agent/swift/helper_transfer.swift +246 -0
- mobile_agent/swift/ocr.swift +22 -0
- mobile_agent/task_policy.py +337 -0
- mobile_agent/team_signing/__init__.py +34 -0
- mobile_agent/team_signing/apple_ca.py +37 -0
- mobile_agent/team_signing/bundle.py +136 -0
- mobile_agent/team_signing/cli.py +208 -0
- mobile_agent/team_signing/identity.py +248 -0
- mobile_agent/team_signing/ledger.py +105 -0
- mobile_agent/team_signing/prebuilt.py +61 -0
- mobile_agent/team_signing/resign.py +256 -0
- mobile_agent/team_signing/service.py +382 -0
- mobile_agent/team_signing/tooling.py +243 -0
- mobile_agent/team_signing/writes.py +268 -0
- mobile_agent/terminal_image.py +81 -0
- mobile_agent/testkit/__init__.py +1 -0
- mobile_agent/testkit/a11y_baseline.py +132 -0
- mobile_agent/testkit/api.py +403 -0
- mobile_agent/testkit/capsule.py +400 -0
- mobile_agent/testkit/capsule_cli.py +57 -0
- mobile_agent/testkit/cli.py +948 -0
- mobile_agent/testkit/cli_arguments.py +193 -0
- mobile_agent/testkit/comparison.py +288 -0
- mobile_agent/testkit/comparison_cli.py +41 -0
- mobile_agent/testkit/discover.py +257 -0
- mobile_agent/testkit/editor.py +243 -0
- mobile_agent/testkit/execute.py +515 -0
- mobile_agent/testkit/github_checks.py +248 -0
- mobile_agent/testkit/gitref.py +66 -0
- mobile_agent/testkit/heal_review.py +176 -0
- mobile_agent/testkit/health.py +480 -0
- mobile_agent/testkit/history.py +255 -0
- mobile_agent/testkit/hosted_results.py +321 -0
- mobile_agent/testkit/input_files.py +173 -0
- mobile_agent/testkit/jobs.py +285 -0
- mobile_agent/testkit/junit_verdict.py +33 -0
- mobile_agent/testkit/maestro.py +802 -0
- mobile_agent/testkit/mcp.py +562 -0
- mobile_agent/testkit/mcp_comparison.py +88 -0
- mobile_agent/testkit/minimize.py +471 -0
- mobile_agent/testkit/minimizer_cli.py +48 -0
- mobile_agent/testkit/notify.py +212 -0
- mobile_agent/testkit/phone_video.py +191 -0
- mobile_agent/testkit/pr.py +491 -0
- mobile_agent/testkit/pr_report.py +562 -0
- mobile_agent/testkit/prbuild.py +148 -0
- mobile_agent/testkit/proof_report.py +55 -0
- mobile_agent/testkit/provenance.py +119 -0
- mobile_agent/testkit/prrun.py +244 -0
- mobile_agent/testkit/recent.py +166 -0
- mobile_agent/testkit/recipes.py +146 -0
- mobile_agent/testkit/record.py +204 -0
- mobile_agent/testkit/register.py +288 -0
- mobile_agent/testkit/reports.py +998 -0
- mobile_agent/testkit/repro.py +432 -0
- mobile_agent/testkit/scenario.py +440 -0
- mobile_agent/testkit/scenario_cli.py +37 -0
- mobile_agent/testkit/snapshots_cli.py +60 -0
- mobile_agent/testkit/suite.py +670 -0
- mobile_agent/testkit/targets.py +497 -0
- mobile_agent/testkit/xctest.py +378 -0
- mobile_agent/threads/__init__.py +1 -0
- mobile_agent/threads/chat_once.py +264 -0
- mobile_agent/threads/cli.py +740 -0
- mobile_agent/threads/client.py +326 -0
- mobile_agent/threads/context.py +246 -0
- mobile_agent/threads/mcp.py +739 -0
- mobile_agent/threads/recent.py +34 -0
- mobile_agent/threads/register.py +54 -0
- mobile_agent/threads/routes.py +188 -0
- mobile_agent/threads/service.py +975 -0
- mobile_agent/threads/store.py +361 -0
- mobile_agent/threads/summary.py +117 -0
- mobile_agent/threads/term.py +101 -0
- mobile_agent/threads/workflow_cli.py +104 -0
- mobile_agent/tls.py +36 -0
- mobile_agent/traces.py +271 -0
- mobile_agent/tracks.py +235 -0
- mobile_agent/transport.py +271 -0
- mobile_agent/triggers/__init__.py +1 -0
- mobile_agent/triggers/actions.py +204 -0
- mobile_agent/triggers/model.py +146 -0
- mobile_agent/triggers/register.py +29 -0
- mobile_agent/triggers/routes.py +101 -0
- mobile_agent/triggers/service.py +387 -0
- mobile_agent/triggers/store.py +142 -0
- mobile_agent/tui/__init__.py +89 -0
- mobile_agent/tui/app.py +582 -0
- mobile_agent/tui/chrome.py +321 -0
- mobile_agent/tui/commands.py +375 -0
- mobile_agent/tui/demo_phone.py +369 -0
- mobile_agent/tui/images.py +29 -0
- mobile_agent/tui/keys.py +64 -0
- mobile_agent/tui/prompt.py +195 -0
- mobile_agent/tui/render.py +395 -0
- mobile_agent/tui/screens.py +180 -0
- mobile_agent/tui/session.py +308 -0
- mobile_agent/tui/tasks.py +287 -0
- mobile_agent/tui/theme.py +118 -0
- mobile_agent/tui/widgets.py +272 -0
- mobile_agent/update_check.py +213 -0
- mobile_agent/usage.py +184 -0
- mobile_agent/verify/__init__.py +0 -0
- mobile_agent/verify/__main__.py +41 -0
- mobile_agent/verify/a11y.py +189 -0
- mobile_agent/verify/actions.py +290 -0
- mobile_agent/verify/assertions.py +570 -0
- mobile_agent/verify/audit.py +312 -0
- mobile_agent/verify/business.py +221 -0
- mobile_agent/verify/business_run.py +131 -0
- mobile_agent/verify/business_state.py +168 -0
- mobile_agent/verify/check_schema.py +398 -0
- mobile_agent/verify/checks.py +614 -0
- mobile_agent/verify/cli.py +423 -0
- mobile_agent/verify/conditions.py +483 -0
- mobile_agent/verify/crashes.py +321 -0
- mobile_agent/verify/device_audit.py +246 -0
- mobile_agent/verify/dialogs.py +287 -0
- mobile_agent/verify/evidence.py +705 -0
- mobile_agent/verify/fixtures.py +305 -0
- mobile_agent/verify/flow.py +213 -0
- mobile_agent/verify/l10n.py +141 -0
- mobile_agent/verify/netcerts.py +67 -0
- mobile_agent/verify/netproxy.py +356 -0
- mobile_agent/verify/netrun.py +81 -0
- mobile_agent/verify/netshim.m +28 -0
- mobile_agent/verify/netshim.py +48 -0
- mobile_agent/verify/netstubs.py +132 -0
- mobile_agent/verify/overlay.py +174 -0
- mobile_agent/verify/perf.py +239 -0
- mobile_agent/verify/proof.py +192 -0
- mobile_agent/verify/replay_admission.py +48 -0
- mobile_agent/verify/report.py +952 -0
- mobile_agent/verify/run_cleanup.py +67 -0
- mobile_agent/verify/run_links.py +48 -0
- mobile_agent/verify/run_perf.py +44 -0
- mobile_agent/verify/run_secrets.py +39 -0
- mobile_agent/verify/runner.py +976 -0
- mobile_agent/verify/runs.py +57 -0
- mobile_agent/verify/screen_coverage.py +181 -0
- mobile_agent/verify/smart.py +106 -0
- mobile_agent/verify/snapshot_run.py +114 -0
- mobile_agent/verify/snapshots.py +306 -0
- mobile_agent/verify/sound.py +335 -0
- mobile_agent/verify/source_observation.py +130 -0
- mobile_agent/verify/system_surfaces/__init__.py +7 -0
- mobile_agent/verify/system_surfaces/play.py +106 -0
- mobile_agent/verify/system_surfaces/spec.py +139 -0
- mobile_agent/verify/system_surfaces/stage.py +138 -0
- mobile_agent/verify/system_surfaces/surfaces.py +121 -0
- mobile_agent/verify/tree.py +223 -0
- mobile_agent/verify/verdict.py +377 -0
- mobile_agent/verify/wire.py +392 -0
- mobile_agent/verify/wirestore.py +176 -0
- mobile_agent/waiting/__init__.py +1 -0
- mobile_agent/waiting/contacts.py +73 -0
- mobile_agent/waiting/drafts.py +136 -0
- mobile_agent/waiting/job.py +145 -0
- mobile_agent/waiting/messages.py +174 -0
- mobile_agent/waiting/rank.py +115 -0
- mobile_agent/waiting/register.py +26 -0
- mobile_agent/watch.py +502 -0
- mobile_agent/wda/__init__.py +2 -0
- mobile_agent/wda/frame_clock.py +646 -0
- mobile_agent/wda/frame_source.py +325 -0
- mobile_agent/wda/frame_store.py +115 -0
- mobile_agent/wda/gate.py +163 -0
- mobile_agent/wda/gestures.py +291 -0
- mobile_agent/wda/guards.py +94 -0
- mobile_agent/wda/identity.py +55 -0
- mobile_agent/wda/observe.py +884 -0
- mobile_agent/wda/patches.py +856 -0
- mobile_agent/wda/point_gestures.py +150 -0
- mobile_agent/wda/source.py +32 -0
- mobile_agent/wda/system_dialogs.py +571 -0
- mobile_agent/wda/text.py +694 -0
- mobile_agent/wda/timeouts.py +77 -0
- mobile_agent/wda/video.py +505 -0
- mobile_agent/wda/watchdog.py +243 -0
- mobile_agent/wireless/__init__.py +15 -0
- mobile_agent/wireless/api.py +354 -0
- mobile_agent/wireless/cli.py +287 -0
- mobile_agent/wireless/devicectl.py +229 -0
- mobile_agent/wireless/exposure.py +33 -0
- mobile_agent/wireless/mobster-wifi-pair.c +122 -0
- mobile_agent/wireless/monitor.py +332 -0
- mobile_agent/wireless/network.py +334 -0
- mobile_agent/wireless/onboarding.py +293 -0
- mobile_agent/wireless/pair.py +124 -0
- mobile_agent/wireless/probe.py +322 -0
- mobile_agent/wireless/register.py +8 -0
- mobile_agent/wireless/relay.py +179 -0
- mobile_agent/wireless/result.py +87 -0
- mobile_agent/wireless/store.py +85 -0
- mobile_agent/wireless/switch.py +77 -0
- mobile_agent/wireless/transport.py +118 -0
- mobile_agent/wireless/words.py +81 -0
- mobile_agent/wireless/xctestrun.py +83 -0
- mobile_agent/workflow_tools.py +59 -0
- mobile_agent/workflows/__init__.py +2 -0
- mobile_agent/workflows/brief.py +201 -0
- mobile_agent/workflows/cron.py +110 -0
- mobile_agent/workflows/health.py +257 -0
- mobile_agent/workflows/history.py +126 -0
- mobile_agent/workflows/records.py +442 -0
- mobile_agent/workflows/runs.py +1000 -0
- mobile_agent/workflows/scheduler.py +134 -0
- mobile_agent/workflows/service.py +973 -0
- mobile_agent/workflows/starters.py +285 -0
- mobile_agent/workflows/triggers.py +248 -0
- mobile_agent/xray/__init__.py +1 -0
- mobile_agent/xray/app_cli.py +116 -0
- mobile_agent/xray/cli.py +21 -0
- mobile_agent/xray/crawl.py +754 -0
- mobile_agent/xray/diff.py +171 -0
- mobile_agent/xray/diff_cli.py +43 -0
- mobile_agent/xray/job.py +184 -0
- mobile_agent/xray/paywall.py +188 -0
- mobile_agent/xray/policy.py +305 -0
- mobile_agent/xray/register.py +43 -0
- mobile_agent/xray/report.py +283 -0
- mobile_agent/xray/screens.py +117 -0
- mobster_cli-0.4.3.dist-info/METADATA +93 -0
- mobster_cli-0.4.3.dist-info/RECORD +1009 -0
- mobster_cli-0.4.3.dist-info/WHEEL +5 -0
- mobster_cli-0.4.3.dist-info/entry_points.txt +3 -0
- mobster_cli-0.4.3.dist-info/licenses/LICENSE +21 -0
- mobster_cli-0.4.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,884 @@
|
|
|
1
|
+
"""WebDriverAgent screen reads."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import os
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
from urllib.parse import quote
|
|
8
|
+
|
|
9
|
+
from ..state import PIXEL_SOURCES, _parse_wda, from_wda_root, pane_key, validate_bundle_id
|
|
10
|
+
from ..transport import Deadline, TransportError
|
|
11
|
+
from .gestures import WDA_GLIDE_MOMENTUM
|
|
12
|
+
from .guards import DriverRejection, last_change, unchanged_since
|
|
13
|
+
|
|
14
|
+
# Measured on the same phone (Settings, n=3 each): excluding `visible` and
|
|
15
|
+
# `accessible` cuts a source read from ~950 ms to ~120 ms. Visibility is then
|
|
16
|
+
# reconstructed geometrically by `state.wda_occlusion`.
|
|
17
|
+
WDA_SOURCE_PATH = "/source?format=xml&excluded_attributes=visible,accessible,index,traits"
|
|
18
|
+
# A snapshot this recent is the dispatch grounding; older ones are re-read and
|
|
19
|
+
# compared before a coordinate tap. The agent refreshes immediately before
|
|
20
|
+
# dispatch, so its taps never pay the extra read.
|
|
21
|
+
WDA_FRESH_SECONDS = 1.0
|
|
22
|
+
# How long WDA's answer about which stacked list pane is hidden is reused.
|
|
23
|
+
WDA_PANE_VISIBILITY_SECONDS = 20
|
|
24
|
+
# Which stacked tab page shows is reused this long at most (it is re-asked after a tab-bar tap).
|
|
25
|
+
WDA_PAGE_VISIBILITY_SECONDS = 120
|
|
26
|
+
# Layers change with every presentation and dismissal: their answer is reused only briefly.
|
|
27
|
+
WDA_LAYER_VISIBILITY_SECONDS = 3
|
|
28
|
+
# A settled read taken this recently is reused as the next step's observation.
|
|
29
|
+
WDA_STABLE_REUSE_SECONDS = .5
|
|
30
|
+
# Settle polling issues reads back to back. A settle read is started only with this much deadline left: reads take
|
|
31
|
+
# 130-210 ms on device, and a read cut off by its deadline surfaced as a
|
|
32
|
+
# TransportError that ended two live runs right after a swipe.
|
|
33
|
+
WDA_READ_MARGIN_SECONDS = .35
|
|
34
|
+
# A tree read that runs this long is too slow for a step to wait on. The step reads its screen from a screenshot and
|
|
35
|
+
# Apple Vision instead (WDA._observe_pixels), and the app is remembered as slow for WDA_SLOW_TREE_SECONDS, so its
|
|
36
|
+
# next reads send no /source at all. Measured 2026-10-14 and 15 (iPhone 15 Pro, iOS 26.0.1, TikTok's feed in front):
|
|
37
|
+
# /source took 29.8 s for 240 KB and /wda/activeAppInfo 17-50 s, then WDA died; a screenshot took 0.3 s and
|
|
38
|
+
# /wda/apps/state 7 ms. WDA runs one request at a time and goes on with one the client gave up on, so a read dropped
|
|
39
|
+
# at 5 s kept it busy for 25 s more, and the state, screenshot and Home requests behind it waited; every further
|
|
40
|
+
# /source added another 30 s. That is why the app is marked at the first read that runs out, and why a slow app's
|
|
41
|
+
# screens never fall back to the tree. Normal reads take 0.13-0.21 s, Safari's up to 2.8 s.
|
|
42
|
+
WDA_TREE_BUDGET_SECONDS = 4.0
|
|
43
|
+
# A read of a caller whose own deadline is shorter than this proves nothing: only a read that was given at least this
|
|
44
|
+
# long and used all of it counts as too slow.
|
|
45
|
+
WDA_TREE_MIN_SECONDS = 3.5
|
|
46
|
+
# The hint retry after a failed read (the Siri overlay; see _source_xml) reads in 0.6 s when it works.
|
|
47
|
+
WDA_TREE_RETRY_SECONDS = 2.0
|
|
48
|
+
WDA_SLOW_TREE_SECONDS = 3600
|
|
49
|
+
# POST /wda/apps/state answers in milliseconds when anything does.
|
|
50
|
+
WDA_STATE_SECONDS = 2.0
|
|
51
|
+
# A screen read from pixels: the screenshot takes 0.3 s and Vision's own limit is 3 s (screen_text.RUN_SECONDS).
|
|
52
|
+
WDA_PIXEL_READ_SECONDS = 8.0
|
|
53
|
+
# The source of a screen read from pixels. It joins state.PIXEL_SOURCES, so the safety rules for such screens apply.
|
|
54
|
+
WDA_PIXEL_SOURCE = "wda_pixels"
|
|
55
|
+
PIXEL_SOURCES.add(WDA_PIXEL_SOURCE)
|
|
56
|
+
# Screens proven at rest (two agreeing reads) remembered by content identity.
|
|
57
|
+
# After a tap, a read identical to one of them is at rest again, which saves
|
|
58
|
+
# the confirming read on revisits (Back, repeated tasks). Mid-transition frames
|
|
59
|
+
# carry both screens or shifted geometry and cannot match. Never used within
|
|
60
|
+
# this long after a swipe: a decelerating list can pass through a remembered
|
|
61
|
+
# offset without having stopped there.
|
|
62
|
+
WDA_SETTLED_MEMORY = 64
|
|
63
|
+
WDA_SWIPE_QUARANTINE_SECONDS = 3.0
|
|
64
|
+
# Upper bound on proving a screen settled; animated content never does.
|
|
65
|
+
WDA_SETTLE_CAP_SECONDS = 1.5
|
|
66
|
+
# Two agreeing reads are not enough on their own: iOS 26 Settings' General
|
|
67
|
+
# screen showed 24 elements for ~450 ms, identical across two reads, before
|
|
68
|
+
# loading to 29. A new screen must also stay unchanged this long.
|
|
69
|
+
WDA_SETTLE_QUIET_SECONDS = .5
|
|
70
|
+
# FrameClock settle (frame_clock.py): see WDA.wait_for_change.
|
|
71
|
+
# The pixel rule's "at rest": nothing moved for this long (frame_clock.STABLE_SECONDS).
|
|
72
|
+
WDA_FRAME_STABLE_SECONDS = .18
|
|
73
|
+
# With the pixels at rest, AX must still agree with itself this long (instead
|
|
74
|
+
# of WDA_SETTLE_QUIET_SECONDS). Measured: after a pop to the Settings root, the
|
|
75
|
+
# glass search bar keeps morphing by ~1 pt for ~0.5-0.8 s with no tile-level
|
|
76
|
+
# pixel change, and AX passes through states that last up to ~270 ms (two
|
|
77
|
+
# agreeing reads); accepting two agreeing reads alone was premature 1 in 20.
|
|
78
|
+
WDA_FRAME_AX_QUIET_SECONDS = .3
|
|
79
|
+
# MOBSTER_TREE_READS=still (under measurement, mobile_agent/docs/running.md): observe_ready takes its first read as the
|
|
80
|
+
# screen at rest,
|
|
81
|
+
# with no agreeing read and no quiet period, when the stream shows not one changed byte from this long before the read
|
|
82
|
+
# began and the last change came after the last dispatch (an effect that has not reached the stream yet is no rest).
|
|
83
|
+
# The AX flicker measured after pushes (research/frameclock-2026-09-23.md) happens while the bytes still change.
|
|
84
|
+
WDA_AT_REST_LEAD_SECONDS = WDA_FRAME_AX_QUIET_SECONDS
|
|
85
|
+
# wait_for_change(single_read=True): how long to wait for the action's first pixel change before
|
|
86
|
+
# settling from AX reads alone. The watch is marked before dispatch, so a tap's or a keystroke's
|
|
87
|
+
# effect has usually arrived by the time the dispatch returns.
|
|
88
|
+
WDA_ONE_READ_EFFECT_SECONDS = .35
|
|
89
|
+
# MOBSTER_SETTLE=launch (under measurement, mobile_agent/docs/running.md): an app launch settles on one read when the
|
|
90
|
+
# effect is on the stream and the pixels have rested for WDA_FRAME_STABLE_SECONDS as the launch returns (458 of 1,441
|
|
91
|
+
# recorded launch settles, on simulators). Where the zoom is still running as activate returns, nothing changes.
|
|
92
|
+
|
|
93
|
+
# The frontier policy's options (WDA.tune): Jev and the plain driver keep every default above.
|
|
94
|
+
# Its source read keeps the traits ("Selected"): no measurable cost on the simulator (90 ms with or
|
|
95
|
+
# without, 26 Sep). ``accessible`` stays out: it tripled the read (0.14 -> 0.35 s).
|
|
96
|
+
WDA_RICH_SOURCE_PATH = "/source?format=xml&excluded_attributes=visible,accessible,index"
|
|
97
|
+
# Settle reads are spaced at least this far apart (start to start). Back-to-back reads every 80-90 ms during
|
|
98
|
+
# settle ("Find the Application 'com.apple.Preferences'" bursts) preceded runner restarts on the phone
|
|
99
|
+
# (quality baseline B4, 5 Oct); a read takes 130-210 ms there, so this mostly spaces the fast ones.
|
|
100
|
+
WDA_SETTLE_POLL_SECONDS = .25
|
|
101
|
+
# Screenshot timeouts (a /screenshot that times out, or the MJPEG stream stalling this long) after which the
|
|
102
|
+
# driver settles from AX reads alone for the next AX_ONLY_STEPS steps: XCTest screenshots block WDA's main
|
|
103
|
+
# queue in GPU-heavy apps (Maps, Spotify: "Cannot take a screenshot within 20000 ms", B4).
|
|
104
|
+
WDA_SCREENSHOT_TIMEOUTS_FOR_AX_ONLY = 2
|
|
105
|
+
WDA_AX_ONLY_STEPS = 5
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class TreeTooSlow(TransportError):
|
|
109
|
+
"""A tree read that used its whole WDA_TREE_BUDGET_SECONDS; it changed nothing, so the step reads pixels instead."""
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
W3C_ELEMENT_KEY = "element-6066-11e4-a52e-4f735466cecf"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def element_id(element):
|
|
116
|
+
"""A WDA element reference's id (JSONWP or W3C key), or None."""
|
|
117
|
+
identifier = element.get("ELEMENT") or element.get(W3C_ELEMENT_KEY)
|
|
118
|
+
return identifier if isinstance(identifier, str) and identifier else None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class WDAObserve:
|
|
122
|
+
"""The reading part of WDA."""
|
|
123
|
+
# Apps whose tree read used the whole WDA_TREE_BUDGET_SECONDS, per WDA URL: {bundle: when (monotonic)}. Their
|
|
124
|
+
# screens come from pixels (_observe_pixels) until WDA_SLOW_TREE_SECONDS pass, then the tree is tried once more.
|
|
125
|
+
_slow_trees = {}
|
|
126
|
+
# bytes of an image -> rows of text Vision read ({"text", "confidence", "rect"}) or None; None here means
|
|
127
|
+
# screen_text.read_image (the screen-text helper on this Mac). Tests set their own.
|
|
128
|
+
ocr = None
|
|
129
|
+
# Optional FrameClock (frame_clock.attach_frame_clock).
|
|
130
|
+
frame_clock = None
|
|
131
|
+
# Source reads and their seconds, for callers' timing reports.
|
|
132
|
+
source_reads, source_seconds = 0, 0.0
|
|
133
|
+
# wait_for_change accepts single_read; settled_by says how the last settle ended ("one_read":
|
|
134
|
+
# a single read, not yet proven by a second one; "agreeing_reads": two agreeing reads, before the
|
|
135
|
+
# quiet period that proves them).
|
|
136
|
+
settles_on_one_read = True
|
|
137
|
+
settles_early = True
|
|
138
|
+
settled_by = None
|
|
139
|
+
predicted_settle = None # MOBSTER_EXPERIMENTAL_SETTLE: the last settle's prediction and outcome (settle/predict.py)
|
|
140
|
+
# Steps left of AX-only settling after repeated screenshot timeouts (see note_screenshot_timeout).
|
|
141
|
+
ax_only_steps = 0
|
|
142
|
+
screenshot_timeouts = 0
|
|
143
|
+
|
|
144
|
+
def observe(self, timeout=10):
|
|
145
|
+
started = time.monotonic()
|
|
146
|
+
try:
|
|
147
|
+
result = self._observe(timeout)
|
|
148
|
+
result.read_started = started
|
|
149
|
+
return result
|
|
150
|
+
finally:
|
|
151
|
+
self.source_reads += 1
|
|
152
|
+
self.source_seconds += time.monotonic() - started
|
|
153
|
+
if self.call_trace is not None:
|
|
154
|
+
self.call_trace.append(("observe", started, (time.monotonic() - started) * 1000))
|
|
155
|
+
|
|
156
|
+
def _observe(self, timeout):
|
|
157
|
+
deadline = Deadline(timeout)
|
|
158
|
+
source = WDA_RICH_SOURCE_PATH if self.rich_rows else WDA_SOURCE_PATH
|
|
159
|
+
pixels = self._slow_screen(deadline)
|
|
160
|
+
if pixels is not None:
|
|
161
|
+
return pixels
|
|
162
|
+
try:
|
|
163
|
+
xml = self._source_xml(source, deadline)
|
|
164
|
+
except TreeTooSlow as slow:
|
|
165
|
+
# One budget, one fallback: the app is slow from now on, this screen comes from the screenshot, and no
|
|
166
|
+
# later read of the app sends /source.
|
|
167
|
+
candidates = self._front_candidates()
|
|
168
|
+
if candidates and self._pixel_reader() is not None:
|
|
169
|
+
self.mark_tree_slow(candidates[0])
|
|
170
|
+
pixels = self._slow_screen(deadline)
|
|
171
|
+
if pixels is not None:
|
|
172
|
+
return pixels
|
|
173
|
+
raise TransportError(str(slow)) from None
|
|
174
|
+
if not isinstance(xml, str):
|
|
175
|
+
raise ValueError("WDA did not return XML")
|
|
176
|
+
root = _parse_wda(xml)
|
|
177
|
+
rich = self.rich_rows
|
|
178
|
+
result = from_wda_root(root, rich=rich)
|
|
179
|
+
if result.stacked_panes or result.stacked_layers or result.stacked_pages:
|
|
180
|
+
hidden = self._hidden_panes(result.stacked_panes, deadline) if result.stacked_panes else frozenset()
|
|
181
|
+
layers = self._hidden_layers(result.stacked_layers, deadline) if result.stacked_layers else frozenset()
|
|
182
|
+
sheets = frozenset(layers) # screens a presented sheet covers (hidden pages are other tabs)
|
|
183
|
+
if result.stacked_pages:
|
|
184
|
+
layers |= self._hidden_pages(result.stacked_pages, deadline, result.bundle_id)
|
|
185
|
+
if hidden or layers:
|
|
186
|
+
full = result
|
|
187
|
+
result = from_wda_root(root, hidden, layers, rich=rich) # the same tree, not parsed again
|
|
188
|
+
if sheets:
|
|
189
|
+
result.behind = tuple(e for e in full.elements
|
|
190
|
+
if any(e.locator == p or e.locator.startswith(p + "/") for p in sheets))
|
|
191
|
+
self._last_bundle = result.bundle_id or self._last_bundle
|
|
192
|
+
if self._detection_point_pending:
|
|
193
|
+
# configure() could not read the window size; the app frame is the screen.
|
|
194
|
+
try:
|
|
195
|
+
self._set_detection_point(result.width, result.height, min(2, deadline.remaining()))
|
|
196
|
+
except (TransportError, TimeoutError, ValueError):
|
|
197
|
+
pass # Best effort; the next observation tries again.
|
|
198
|
+
deadline.remaining()
|
|
199
|
+
if self._frame_followup is not None:
|
|
200
|
+
self._frame_follow_up(result)
|
|
201
|
+
return result
|
|
202
|
+
|
|
203
|
+
def _front_candidates(self):
|
|
204
|
+
"""The apps that may be in front, most likely first: the one a caller brought forward,
|
|
205
|
+
then the last one read."""
|
|
206
|
+
found = []
|
|
207
|
+
for bundle in (self._foreground_hint, self._last_bundle):
|
|
208
|
+
if bundle and bundle not in found:
|
|
209
|
+
found.append(bundle)
|
|
210
|
+
return found
|
|
211
|
+
|
|
212
|
+
def tree_is_slow(self, bundle):
|
|
213
|
+
marked = self._slow_trees.get(self.url, {}).get(bundle)
|
|
214
|
+
return marked is not None and time.monotonic() - marked < WDA_SLOW_TREE_SECONDS
|
|
215
|
+
|
|
216
|
+
def mark_tree_slow(self, bundle):
|
|
217
|
+
self._slow_trees.setdefault(self.url, {})[bundle] = time.monotonic()
|
|
218
|
+
|
|
219
|
+
def _source_xml(self, source, deadline):
|
|
220
|
+
"""The tree's XML within WDA_TREE_BUDGET_SECONDS: only a read that used the whole budget raises TreeTooSlow."""
|
|
221
|
+
started = time.monotonic()
|
|
222
|
+
limit = min(WDA_TREE_BUDGET_SECONDS, deadline.remaining())
|
|
223
|
+
try:
|
|
224
|
+
return self.call("GET", source, timeout=limit)
|
|
225
|
+
except TransportError as error:
|
|
226
|
+
failure = error
|
|
227
|
+
spent = time.monotonic() - started
|
|
228
|
+
out_of_budget = limit >= WDA_TREE_MIN_SECONDS and spent >= limit - .1
|
|
229
|
+
# Measured 2026-09-23 (iOS 26.0.1): with Safari in front, WDA resolved
|
|
230
|
+
# the "active application" to an invisible system overlay
|
|
231
|
+
# (com.apple.siri.IntelligentLight) and every source read failed
|
|
232
|
+
# after ~8 s ("stale element", kAXErrorServerNotFound). Naming the
|
|
233
|
+
# app we brought forward as WDA's default active application
|
|
234
|
+
# read the same screen in 0.58 s. A read changes nothing, so one
|
|
235
|
+
# retry is safe. The hint then stays for this session: while it is
|
|
236
|
+
# set WDA prefers that app when several are active.
|
|
237
|
+
hint = self._foreground_hint or self._last_bundle
|
|
238
|
+
left = deadline.end - time.monotonic()
|
|
239
|
+
# A read that ran out of the budget with the hint already in force is a slow app, not the wrong app.
|
|
240
|
+
if not hint or left < WDA_READ_MARGIN_SECONDS or (out_of_budget and self._hinted == hint):
|
|
241
|
+
if out_of_budget:
|
|
242
|
+
raise TreeTooSlow(f"the tree read used the whole {WDA_TREE_BUDGET_SECONDS:g} s budget") from None
|
|
243
|
+
raise failure
|
|
244
|
+
try:
|
|
245
|
+
# Re-applied on every failed read: another client of the same session (a
|
|
246
|
+
# harness probe, the app's video) can put detection back on "auto".
|
|
247
|
+
self._hint_app(hint, min(2, deadline.remaining()))
|
|
248
|
+
xml = self.call("GET", source, timeout=min(WDA_TREE_RETRY_SECONDS, deadline.remaining()))
|
|
249
|
+
except (TransportError, TimeoutError):
|
|
250
|
+
if out_of_budget:
|
|
251
|
+
raise TreeTooSlow(f"the tree read used the whole {WDA_TREE_BUDGET_SECONDS:g} s budget") from None
|
|
252
|
+
raise
|
|
253
|
+
self._overlay_apps.setdefault(self.url, set()).add(hint)
|
|
254
|
+
self._save_overlay_hints()
|
|
255
|
+
return xml
|
|
256
|
+
|
|
257
|
+
def in_front(self, bundle, timeout=WDA_STATE_SECONDS):
|
|
258
|
+
"""Whether ``bundle`` runs in the foreground, from POST /wda/apps/state (state 4): True or False, or None when
|
|
259
|
+
WDA does not answer in ``timeout``. Measured 2026-10-14 with TikTok's feed in front: 7 ms, where
|
|
260
|
+
/wda/activeAppInfo took 17-50 s. Sheets over the app (a permission prompt) do not change it: it says the app
|
|
261
|
+
is in front, not that nothing covers it."""
|
|
262
|
+
try:
|
|
263
|
+
state = self.call("POST", "/wda/apps/state", {"bundleId": validate_bundle_id(bundle)}, timeout)
|
|
264
|
+
except (TransportError, TimeoutError, ValueError):
|
|
265
|
+
return None
|
|
266
|
+
return state == 4 if type(state) is int else None
|
|
267
|
+
|
|
268
|
+
def slow_front(self, timeout=WDA_STATE_SECONDS):
|
|
269
|
+
"""The app in front when its tree is remembered as slow (confirmed by in_front), else None: for those apps
|
|
270
|
+
/wda/activeAppInfo takes as long as the tree, so the question is never put to it. Raises TransportError when
|
|
271
|
+
WDA does not say: a request sent to a busy WDA only lengthens its queue."""
|
|
272
|
+
for bundle in self._front_candidates():
|
|
273
|
+
if self.tree_is_slow(bundle):
|
|
274
|
+
front = self.in_front(bundle, timeout)
|
|
275
|
+
if front is None:
|
|
276
|
+
raise TransportError(f"WDA did not say whether {bundle} is in front; it may still be busy with "
|
|
277
|
+
"that app's tree")
|
|
278
|
+
if front:
|
|
279
|
+
return bundle
|
|
280
|
+
return None
|
|
281
|
+
|
|
282
|
+
@staticmethod
|
|
283
|
+
def _pixel_reader():
|
|
284
|
+
"""The harness's ``pixel_screen`` reader, or None: the engine alone has none and keeps the tree's error."""
|
|
285
|
+
from .. import harness_api
|
|
286
|
+
return harness_api.hooks().pixel_screen()
|
|
287
|
+
|
|
288
|
+
def _slow_screen(self, deadline):
|
|
289
|
+
"""The screen in front read from pixels when it is an app remembered as slow, else None."""
|
|
290
|
+
for bundle in self._front_candidates():
|
|
291
|
+
if self.tree_is_slow(bundle):
|
|
292
|
+
snapshot = self._observe_pixels(bundle, deadline)
|
|
293
|
+
if snapshot is not None:
|
|
294
|
+
return snapshot
|
|
295
|
+
return None
|
|
296
|
+
|
|
297
|
+
def _observe_pixels(self, bundle, deadline):
|
|
298
|
+
"""The screen in front read from a screenshot and Apple Vision: a Snapshot with source ``wda_pixels`` (the
|
|
299
|
+
safety rules for screens read from pixels apply). None when ``bundle`` is not in front (POST
|
|
300
|
+
/wda/apps/state), the phone's size is unknown or the harness has no pixel reader. Raises TransportError when
|
|
301
|
+
WDA does not answer or the picture can not be read: the caller never falls back to /source for an app that
|
|
302
|
+
is too slow, because each such read keeps WDA busy for another 30 s. Reads only: it never taps."""
|
|
303
|
+
pixels = self._pixel_reader()
|
|
304
|
+
if pixels is None:
|
|
305
|
+
return None
|
|
306
|
+
started = time.monotonic()
|
|
307
|
+
front = self.in_front(bundle, min(deadline.remaining(), WDA_PIXEL_READ_SECONDS))
|
|
308
|
+
if front is None:
|
|
309
|
+
raise TransportError(f"WDA did not say whether {bundle} is in front; it may still be busy with that "
|
|
310
|
+
"app's tree")
|
|
311
|
+
size = self._screen_sizes.get(self.url)
|
|
312
|
+
if not front or not size:
|
|
313
|
+
return None
|
|
314
|
+
image = self.call("GET", "/screenshot", timeout=deadline.remaining())
|
|
315
|
+
try:
|
|
316
|
+
snapshot = pixels.wda_snapshot(base64.b64decode(image), size=size, bundle=bundle, source=WDA_PIXEL_SOURCE,
|
|
317
|
+
started=started, ocr=self.ocr) if isinstance(image, str) and image else None
|
|
318
|
+
except ValueError:
|
|
319
|
+
snapshot = None
|
|
320
|
+
if snapshot is None:
|
|
321
|
+
raise TransportError(f"the screen of {bundle} could not be read from a screenshot")
|
|
322
|
+
self._last_bundle = bundle
|
|
323
|
+
return snapshot
|
|
324
|
+
|
|
325
|
+
def pixel_snapshot(self, mark=False):
|
|
326
|
+
"""The screen in front read from pixels, for an evidence read of an app whose tree is too slow."""
|
|
327
|
+
candidates = self._front_candidates()
|
|
328
|
+
if mark and candidates and self._pixel_reader() is not None:
|
|
329
|
+
self.mark_tree_slow(candidates[0])
|
|
330
|
+
return self._slow_screen(Deadline(WDA_PIXEL_READ_SECONDS))
|
|
331
|
+
|
|
332
|
+
def _hidden_panes(self, stacked, deadline):
|
|
333
|
+
"""Which stacked list panes WDA reports not visible; a failure leaves the order rule in charge."""
|
|
334
|
+
key = frozenset(stacked)
|
|
335
|
+
cached = self._pane_visibility.get(key)
|
|
336
|
+
if cached is not None and time.monotonic() - cached[1] < WDA_PANE_VISIBILITY_SECONDS:
|
|
337
|
+
return cached[0]
|
|
338
|
+
hidden = set()
|
|
339
|
+
try:
|
|
340
|
+
for role in sorted({pane[0] for pane in stacked}):
|
|
341
|
+
found = self.call("POST", "/elements", {"using": "predicate string", "value":
|
|
342
|
+
f"type == 'XCUIElementType{role}' AND visible == 0"},
|
|
343
|
+
min(3, deadline.remaining()))
|
|
344
|
+
for element in (found if isinstance(found, list) else [])[:4]:
|
|
345
|
+
identifier = element_id(element)
|
|
346
|
+
if identifier is None:
|
|
347
|
+
continue
|
|
348
|
+
path = f"/element/{quote(identifier, safe='')}"
|
|
349
|
+
rect = self.call("GET", path + "/rect", timeout=min(3, deadline.remaining()))
|
|
350
|
+
label = self.call("GET", path + "/attribute/label", timeout=min(3, deadline.remaining()))
|
|
351
|
+
if isinstance(rect, dict):
|
|
352
|
+
pane = pane_key(role, label if isinstance(label, str) else "", *(
|
|
353
|
+
float(rect.get(k, 0)) for k in ("x", "y", "width", "height")))
|
|
354
|
+
if pane in key:
|
|
355
|
+
hidden.add(pane)
|
|
356
|
+
except (TransportError, TypeError, ValueError):
|
|
357
|
+
return frozenset()
|
|
358
|
+
hidden = frozenset(hidden) if len(hidden) < len(key) else frozenset() # never hide every pane
|
|
359
|
+
return self._remember_visibility(key, hidden)
|
|
360
|
+
|
|
361
|
+
def _hidden_layers(self, paths, deadline):
|
|
362
|
+
"""Which window layers behind the front one WDA reports not visible (~0.3 s); a failure hides nothing."""
|
|
363
|
+
key = ("layers", frozenset(paths))
|
|
364
|
+
cached = self._pane_visibility.get(key)
|
|
365
|
+
# Not across an action: SplitPay's Confirm Payment sheet hid the feed behind it, and the first read after its
|
|
366
|
+
# Pay button reused that answer while the closing sheet's container still stacked the same layers. Only the
|
|
367
|
+
# keyboard was left on screen, and the payment's feed row was never read (iOSWorld splitpay-001, 11 Oct).
|
|
368
|
+
if (cached is not None and cached[1] > self._dispatched_at
|
|
369
|
+
and time.monotonic() - cached[1] < WDA_LAYER_VISIBILITY_SECONDS):
|
|
370
|
+
return cached[0]
|
|
371
|
+
hidden = set()
|
|
372
|
+
try:
|
|
373
|
+
for path in paths:
|
|
374
|
+
chain = "/".join(path.strip("/").split("/")[1:])
|
|
375
|
+
found = self.call("POST", "/elements", {"using": "class chain", "value": chain},
|
|
376
|
+
min(3, deadline.remaining()))
|
|
377
|
+
if not isinstance(found, list) or len(found) != 1:
|
|
378
|
+
continue
|
|
379
|
+
identifier = element_id(found[0])
|
|
380
|
+
if identifier is None:
|
|
381
|
+
return frozenset() # as when WDA refused the request for a missing id
|
|
382
|
+
visible = self.call("GET", f"/element/{quote(identifier, safe='')}/attribute/visible",
|
|
383
|
+
timeout=min(3, deadline.remaining()))
|
|
384
|
+
if visible is False or visible == "false":
|
|
385
|
+
hidden.add(path)
|
|
386
|
+
except (TransportError, TypeError, ValueError):
|
|
387
|
+
return frozenset()
|
|
388
|
+
return self._remember_visibility(key, frozenset(hidden))
|
|
389
|
+
|
|
390
|
+
def _hidden_pages(self, groups, deadline, bundle=None):
|
|
391
|
+
"""Source paths of same-frame sibling pages WDA reports not visible; any failure hides nothing.
|
|
392
|
+
SwiftUI keeps every TabView tab in the tree: multi-087 tapped a row of a hidden tab for 16 turns (24 Sep)."""
|
|
393
|
+
key = ("pages", bundle, frozenset(groups))
|
|
394
|
+
cached = self._pane_visibility.get(key)
|
|
395
|
+
# The question costs ~0.8 s a page on QuickChat (5 s a read when asked after every action: settles
|
|
396
|
+
# went 0.75 -> 1.9 s a turn, 25 Sep), and only a tap outside the pages (the tab bar), an app switch
|
|
397
|
+
# or HOME changes the answer: it is kept until one of those, or WDA_PAGE_VISIBILITY_SECONDS.
|
|
398
|
+
if (cached is not None and not self._pages_stale
|
|
399
|
+
and time.monotonic() - cached[1] < WDA_PAGE_VISIBILITY_SECONDS):
|
|
400
|
+
return cached[0]
|
|
401
|
+
self._pages_stale = False
|
|
402
|
+
self._page_groups = tuple(groups)
|
|
403
|
+
hidden = set()
|
|
404
|
+
try:
|
|
405
|
+
for parent, children in groups:
|
|
406
|
+
chain = "/".join(parent.strip("/").split("/")[1:])
|
|
407
|
+
found = self.call("POST", "/elements",
|
|
408
|
+
{"using": "class chain",
|
|
409
|
+
"value": (chain + "/" if chain else "") + "XCUIElementTypeOther[`visible == 1`]"},
|
|
410
|
+
min(3, deadline.remaining()))
|
|
411
|
+
shown = set()
|
|
412
|
+
for element in (found if isinstance(found, list) else [])[:8]:
|
|
413
|
+
identifier = element_id(element)
|
|
414
|
+
index = identifier and self.call("GET", f"/element/{quote(identifier, safe='')}/attribute/index",
|
|
415
|
+
timeout=min(3, deadline.remaining()))
|
|
416
|
+
if isinstance(index, (int, str)) and str(index).isdigit():
|
|
417
|
+
shown.add(int(index))
|
|
418
|
+
positions = {position for position, _ in children}
|
|
419
|
+
if shown & positions: # else unknown: hide nothing
|
|
420
|
+
hidden.update(path for position, path in children if position not in shown)
|
|
421
|
+
except (TransportError, TypeError, ValueError):
|
|
422
|
+
return frozenset()
|
|
423
|
+
return self._remember_visibility(key, frozenset(hidden))
|
|
424
|
+
|
|
425
|
+
def _note_tap(self, locator):
|
|
426
|
+
"""A tap outside every stacked page (a tab bar, chrome) may switch the page shown."""
|
|
427
|
+
groups = self._page_groups
|
|
428
|
+
if not groups:
|
|
429
|
+
return
|
|
430
|
+
if not locator or any(locator.startswith(parent + "/") and not any(
|
|
431
|
+
locator == page or locator.startswith(page + "/") for _, page in pages)
|
|
432
|
+
for parent, pages in groups):
|
|
433
|
+
self._pages_stale = True
|
|
434
|
+
|
|
435
|
+
def _dispatching(self):
|
|
436
|
+
"""An action is about to be sent: the screen is no longer the settled one, and its window layers may change."""
|
|
437
|
+
self._stable = None
|
|
438
|
+
self._dispatched_at = time.monotonic()
|
|
439
|
+
|
|
440
|
+
def _remember_visibility(self, key, hidden):
|
|
441
|
+
"""Cache a pane or layer visibility answer; panes and layers share one 16-entry bound."""
|
|
442
|
+
self._pane_visibility[key] = (hidden, time.monotonic())
|
|
443
|
+
while len(self._pane_visibility) > 16:
|
|
444
|
+
self._pane_visibility.pop(next(iter(self._pane_visibility)))
|
|
445
|
+
return hidden
|
|
446
|
+
|
|
447
|
+
def _prove_settled(self, snapshot):
|
|
448
|
+
fingerprint = snapshot.content_fingerprint
|
|
449
|
+
self._settled.pop(fingerprint, None)
|
|
450
|
+
self._settled[fingerprint] = True
|
|
451
|
+
while len(self._settled) > WDA_SETTLED_MEMORY:
|
|
452
|
+
self._settled.pop(next(iter(self._settled)))
|
|
453
|
+
self._stable = snapshot
|
|
454
|
+
return snapshot
|
|
455
|
+
|
|
456
|
+
def _known_at_rest(self, snapshot):
|
|
457
|
+
return (time.monotonic() - self._swiped_at > WDA_SWIPE_QUARANTINE_SECONDS
|
|
458
|
+
and snapshot.content_fingerprint in self._settled)
|
|
459
|
+
|
|
460
|
+
def observe_ready(self, timeout=10):
|
|
461
|
+
"""A settled observation: reuse the read that just proved stability, else
|
|
462
|
+
read until two consecutive reads agree (or one matches a screen already
|
|
463
|
+
proven at rest). A screen that never stops moving (a running timer,
|
|
464
|
+
video) returns its latest read after the settle cap. Each call that reads
|
|
465
|
+
is logged to the FrameClock ("ready": seconds, reads, and which rule ended it)."""
|
|
466
|
+
started, reads = time.monotonic(), self.source_reads
|
|
467
|
+
snapshot, rule, first, rest = self._ready_read(timeout)
|
|
468
|
+
self._ready_report(started, reads, snapshot, rule, first, rest)
|
|
469
|
+
return snapshot
|
|
470
|
+
|
|
471
|
+
def _ready_read(self, timeout):
|
|
472
|
+
"""(the settled read, the rule that accepted it, the first read, whether the stream showed that read at
|
|
473
|
+
rest). The rule is reuse, memory, rest, agreement, pixels or cap."""
|
|
474
|
+
stable = self._stable
|
|
475
|
+
if stable is not None and time.monotonic() - stable.captured_at <= WDA_STABLE_REUSE_SECONDS:
|
|
476
|
+
return stable, "reuse", None, False
|
|
477
|
+
deadline = Deadline(timeout)
|
|
478
|
+
cap = time.monotonic() + min(WDA_SETTLE_CAP_SECONDS, timeout)
|
|
479
|
+
since = time.monotonic()
|
|
480
|
+
previous = first = self.observe(timeout=deadline.remaining())
|
|
481
|
+
if self._known_at_rest(previous):
|
|
482
|
+
return self._prove_settled(previous), "memory", first, False
|
|
483
|
+
rest = self._at_rest(previous)
|
|
484
|
+
if rest and os.environ.get("MOBSTER_TREE_READS", "").strip().casefold() == "still":
|
|
485
|
+
return self._prove_settled(previous), "rest", first, rest
|
|
486
|
+
clock = self._frame_clock()
|
|
487
|
+
previous_started = since
|
|
488
|
+
while time.monotonic() < cap:
|
|
489
|
+
self._poll_pause(previous_started, cap)
|
|
490
|
+
if time.monotonic() >= cap:
|
|
491
|
+
break
|
|
492
|
+
started = time.monotonic()
|
|
493
|
+
current = self.observe(timeout=deadline.remaining())
|
|
494
|
+
if current.content_fingerprint != previous.content_fingerprint:
|
|
495
|
+
since = started
|
|
496
|
+
elif time.monotonic() - since >= WDA_SETTLE_QUIET_SECONDS:
|
|
497
|
+
return self._prove_settled(current), "agreement", first, rest
|
|
498
|
+
elif (clock is not None and current.elements
|
|
499
|
+
and time.monotonic() - since >= WDA_FRAME_AX_QUIET_SECONDS):
|
|
500
|
+
# Agreeing reads for WDA_FRAME_AX_QUIET_SECONDS, and the whole screen
|
|
501
|
+
# (status bar aside, no masked regions) pixel-still from
|
|
502
|
+
# WDA_FRAME_STABLE_SECONDS before the last two reads until now.
|
|
503
|
+
still = clock.still_for(exact=False) # AX agreement (above) covers sub-tile changes
|
|
504
|
+
if still is not None and time.monotonic() - still <= previous_started - WDA_FRAME_STABLE_SECONDS:
|
|
505
|
+
return self._prove_settled(current), "pixels", first, rest
|
|
506
|
+
previous, previous_started = current, started
|
|
507
|
+
return previous, "cap", first, rest
|
|
508
|
+
|
|
509
|
+
def _at_rest(self, read):
|
|
510
|
+
"""Whether the stream shows ``read`` at rest: not one changed byte from WDA_AT_REST_LEAD_SECONDS before the
|
|
511
|
+
read began until now, and that stillness began after the last dispatch. An empty screen never settles on
|
|
512
|
+
pixels."""
|
|
513
|
+
if not read.elements:
|
|
514
|
+
return False
|
|
515
|
+
changed = last_change(self._frame_clock())
|
|
516
|
+
return changed is not None and self._dispatched_at <= changed <= read.read_started - WDA_AT_REST_LEAD_SECONDS
|
|
517
|
+
|
|
518
|
+
def _ready_report(self, started, reads, snapshot, rule, first, rest):
|
|
519
|
+
"""One "ready" row: the rule, seconds and reads, and, with or without the switch, ``would_rest`` (the stream
|
|
520
|
+
showed the first read at rest) and ``first_held`` (the screen accepted is the first read): their
|
|
521
|
+
agreement in a run without the switch is the rule's false-rest rate."""
|
|
522
|
+
clock = self.frame_clock
|
|
523
|
+
if clock is None:
|
|
524
|
+
return
|
|
525
|
+
row = {"kind": "ready", "rule": rule, "ms": round((time.monotonic() - started) * 1000, 1),
|
|
526
|
+
"reads": self.source_reads - reads}
|
|
527
|
+
if first is not None:
|
|
528
|
+
row["would_rest"], row["first_held"] = rest, snapshot.content_fingerprint == first.content_fingerprint
|
|
529
|
+
try:
|
|
530
|
+
clock.record(row)
|
|
531
|
+
except Exception:
|
|
532
|
+
pass
|
|
533
|
+
|
|
534
|
+
@staticmethod
|
|
535
|
+
def _poll_pause(last_read, cap):
|
|
536
|
+
"""Wait until WDA_SETTLE_POLL_SECONDS after the last settle read began (never past ``cap``)."""
|
|
537
|
+
pause = min(last_read + WDA_SETTLE_POLL_SECONDS, cap) - time.monotonic()
|
|
538
|
+
if pause > 0:
|
|
539
|
+
time.sleep(pause)
|
|
540
|
+
|
|
541
|
+
def wait_for_change(self, snapshot, timeout=2, wait_seconds=.6, on_settling=None, single_read=False,
|
|
542
|
+
early=False):
|
|
543
|
+
"""Read back to back until the screen differs from ``snapshot`` and is at
|
|
544
|
+
rest: two consecutive agreeing reads, or one read identical to a screen
|
|
545
|
+
already proven at rest. A mid-transition frame is never returned while it
|
|
546
|
+
is still moving. Unchanged after ``wait_seconds`` returns the unchanged
|
|
547
|
+
read; changing past the settle cap returns the latest read.
|
|
548
|
+
|
|
549
|
+
``on_settling(read)`` is called once per new screen the first time two
|
|
550
|
+
consecutive reads agree, before its quiet period has elapsed: the
|
|
551
|
+
caller may start work for that screen while it is being proven. It
|
|
552
|
+
never influences what this method returns.
|
|
553
|
+
|
|
554
|
+
With a FrameClock in "on" mode, the pixels shorten the quiet period:
|
|
555
|
+
a changed read that agrees with the reads of the last
|
|
556
|
+
WDA_FRAME_AX_QUIET_SECONDS (0.3 s instead of 0.5 s) is accepted when,
|
|
557
|
+
after a pixel change was seen, no tile moved from
|
|
558
|
+
WDA_FRAME_STABLE_SECONDS before it until it returned. AX is never
|
|
559
|
+
skipped: after a push, Settings briefly re-exposed the previous
|
|
560
|
+
screen's search field in AX with nothing moving on screen, and after a
|
|
561
|
+
pop the glass search bar's 1 pt morph changes AX for ~0.5-0.8 s below
|
|
562
|
+
tile resolution (both measured). Every other path is the AX loop
|
|
563
|
+
above, unchanged.
|
|
564
|
+
|
|
565
|
+
``single_read`` (FrameClock "on"): wait for the pixels to change and
|
|
566
|
+
come to rest, then read once; a changed read during which nothing moved
|
|
567
|
+
is returned without a second, agreeing read (``settled_by`` becomes
|
|
568
|
+
"one_read"). On a simulator a read of a long list takes 0.7-1.7 s, and
|
|
569
|
+
the agreeing read costs as much again. The caller proves the read later
|
|
570
|
+
(frontier: a second read while the model thinks). Anything else falls
|
|
571
|
+
back to the loop above.
|
|
572
|
+
|
|
573
|
+
``early``: the AX loop returns the first changed read that agrees with
|
|
574
|
+
the read before it, without waiting out the quiet period
|
|
575
|
+
(``settled_by`` "agreeing_reads"). The caller proves it later, as for
|
|
576
|
+
``single_read``: the frontier starts its next model call on it and
|
|
577
|
+
reads again meanwhile (0.5 s a turn when the pixels cannot settle it).
|
|
578
|
+
|
|
579
|
+
``single_read="rest"`` (MOBSTER_SETTLE=launch): as ``single_read``, but only when the effect was seen
|
|
580
|
+
and the pixels already rest, without waiting for either; else the AX loop runs as for no
|
|
581
|
+
``single_read``. Every settle's log row says ``miss``: why the one-read settle was not taken.
|
|
582
|
+
"""
|
|
583
|
+
watch, self._frame_watch = self._frame_watch, None
|
|
584
|
+
action, self._frame_action = self._frame_action, None
|
|
585
|
+
clock = self._frame_clock()
|
|
586
|
+
reported = set()
|
|
587
|
+
|
|
588
|
+
def settling(read):
|
|
589
|
+
if on_settling is None or read.content_fingerprint in reported:
|
|
590
|
+
return
|
|
591
|
+
reported.add(read.content_fingerprint)
|
|
592
|
+
try:
|
|
593
|
+
on_settling(read)
|
|
594
|
+
except Exception:
|
|
595
|
+
pass # Speculation is optional; settling is not.
|
|
596
|
+
|
|
597
|
+
deadline = Deadline(timeout)
|
|
598
|
+
started = time.monotonic()
|
|
599
|
+
cap = started + min(WDA_SETTLE_CAP_SECONDS, timeout)
|
|
600
|
+
reads = []
|
|
601
|
+
info = None
|
|
602
|
+
self.settled_by = self.predicted_settle = None
|
|
603
|
+
if single_read == "rest" and os.environ.get("MOBSTER_SETTLE", "").strip().casefold() != "launch":
|
|
604
|
+
single_read = False
|
|
605
|
+
try:
|
|
606
|
+
accept = None
|
|
607
|
+
if clock is not None and watch is not None and watch.usable:
|
|
608
|
+
info = {}
|
|
609
|
+
read, info["miss"] = (self._one_read_settle(snapshot, clock, watch, deadline, cap, reads,
|
|
610
|
+
rest_only=single_read == "rest")
|
|
611
|
+
if single_read else (None, "not_requested"))
|
|
612
|
+
if read is not None:
|
|
613
|
+
info["accepted_by"], self.settled_by = "one_read", "one_read"
|
|
614
|
+
self._frame_report(clock, watch, action, snapshot, read, started, reads, info)
|
|
615
|
+
return read
|
|
616
|
+
|
|
617
|
+
def accept(since, read_started, read_ended):
|
|
618
|
+
# A changed read agreeing with the read before it, unchanged since ``since``.
|
|
619
|
+
if (read_ended - since >= WDA_FRAME_AX_QUIET_SECONDS
|
|
620
|
+
and clock.quiet_between(watch, read_started - WDA_FRAME_STABLE_SECONDS, read_ended)):
|
|
621
|
+
info["accepted_by"] = "pixels"
|
|
622
|
+
return True
|
|
623
|
+
return False
|
|
624
|
+
elif clock is not None and watch is not None:
|
|
625
|
+
info = {"fallback": watch.reason or "unusable"}
|
|
626
|
+
if (single_read or early) and clock is not None and watch is not None:
|
|
627
|
+
info = {} if info is None else info
|
|
628
|
+
from ..settle.predict import predicted_settle # here: a start never loads the dark settle package
|
|
629
|
+
read = predicted_settle(self, snapshot, action, watch, clock, deadline, started, cap, reads, info)
|
|
630
|
+
if read is not None: # unproven, like one_read: the caller's verify read proves it
|
|
631
|
+
info["accepted_by"], self.settled_by = "predicted", "predicted"
|
|
632
|
+
self._frame_report(clock, watch, action, snapshot, read, started, reads, info)
|
|
633
|
+
return read
|
|
634
|
+
result = self._ax_settle(snapshot, deadline, started, started + wait_seconds, cap, settling, reads,
|
|
635
|
+
accept, early=early)
|
|
636
|
+
if clock is not None and watch is not None:
|
|
637
|
+
self._frame_report(clock, watch, action, snapshot, result, started, reads, info)
|
|
638
|
+
return result
|
|
639
|
+
finally:
|
|
640
|
+
if clock is not None and watch is not None:
|
|
641
|
+
clock.release(watch)
|
|
642
|
+
|
|
643
|
+
def _one_read_settle(self, snapshot, clock, watch, deadline, cap, reads, rest_only=False):
|
|
644
|
+
"""(a changed read taken after the pixels moved and came to rest, during which nothing moved, None), or
|
|
645
|
+
(None, why not): no_effect (the pixels never moved), not_stable (never rested), read_failed, unchanged
|
|
646
|
+
(the read shows ``snapshot``) or moved (the pixels moved during the read). ``rest_only``: the effect
|
|
647
|
+
and the rest must already be there, with no waiting for either."""
|
|
648
|
+
effect_wait = 0.0 if rest_only else max(0.0, min(WDA_ONE_READ_EFFECT_SECONDS, cap - time.monotonic()))
|
|
649
|
+
if clock.wait_effect(watch, effect_wait) is None:
|
|
650
|
+
return None, "no_effect"
|
|
651
|
+
if clock.wait_stable(watch, 0.0 if rest_only else max(0.0, cap - time.monotonic())) is None:
|
|
652
|
+
return None, "not_stable"
|
|
653
|
+
read_started = time.monotonic()
|
|
654
|
+
try:
|
|
655
|
+
current = self.observe(timeout=deadline.remaining())
|
|
656
|
+
except (TransportError, TimeoutError):
|
|
657
|
+
return None, "read_failed"
|
|
658
|
+
now = time.monotonic()
|
|
659
|
+
reads.append((read_started, now, current.content_fingerprint))
|
|
660
|
+
if current.content_fingerprint == snapshot.content_fingerprint:
|
|
661
|
+
return None, "unchanged"
|
|
662
|
+
if not clock.quiet_between(watch, read_started - WDA_FRAME_STABLE_SECONDS, now):
|
|
663
|
+
return None, "moved"
|
|
664
|
+
return current, None
|
|
665
|
+
|
|
666
|
+
def _ax_settle(self, snapshot, deadline, started, quiet_end, cap, settling, reads, accept=None, early=False):
|
|
667
|
+
"""The AX-only settle loop (see wait_for_change)."""
|
|
668
|
+
before = snapshot.content_fingerprint
|
|
669
|
+
previous, since = None, started
|
|
670
|
+
current = snapshot
|
|
671
|
+
last_read = None
|
|
672
|
+
while True:
|
|
673
|
+
if last_read is not None:
|
|
674
|
+
self._poll_pause(last_read, cap)
|
|
675
|
+
now = time.monotonic()
|
|
676
|
+
if now >= cap or deadline.end - now <= WDA_READ_MARGIN_SECONDS:
|
|
677
|
+
return current
|
|
678
|
+
read_started = last_read = time.monotonic()
|
|
679
|
+
try:
|
|
680
|
+
current = self.observe(timeout=deadline.remaining())
|
|
681
|
+
except (TransportError, TimeoutError):
|
|
682
|
+
# A read changes nothing on the phone; the action's outcome is
|
|
683
|
+
# decided by the caller from the last good observation.
|
|
684
|
+
if current is snapshot:
|
|
685
|
+
raise
|
|
686
|
+
return current
|
|
687
|
+
fingerprint = current.content_fingerprint
|
|
688
|
+
now = time.monotonic()
|
|
689
|
+
reads.append((read_started, now, fingerprint))
|
|
690
|
+
if fingerprint != before:
|
|
691
|
+
if previous is None or previous.content_fingerprint != fingerprint:
|
|
692
|
+
since = read_started
|
|
693
|
+
if (accept is not None and previous is not None and previous.content_fingerprint == fingerprint
|
|
694
|
+
and accept(since, read_started, now)):
|
|
695
|
+
return self._prove_settled(current)
|
|
696
|
+
if early and previous is not None and previous.content_fingerprint == fingerprint \
|
|
697
|
+
and not self._known_at_rest(current):
|
|
698
|
+
self.settled_by = "agreeing_reads" # unproven: the caller reads again
|
|
699
|
+
return current
|
|
700
|
+
if (self._known_at_rest(current) or previous is not None
|
|
701
|
+
and previous.content_fingerprint == fingerprint
|
|
702
|
+
and now - since >= WDA_SETTLE_QUIET_SECONDS):
|
|
703
|
+
return self._prove_settled(current)
|
|
704
|
+
if previous is not None and previous.content_fingerprint == fingerprint:
|
|
705
|
+
settling(current)
|
|
706
|
+
previous = current
|
|
707
|
+
else:
|
|
708
|
+
previous = None
|
|
709
|
+
if now >= quiet_end:
|
|
710
|
+
return current
|
|
711
|
+
if now >= cap or deadline.end - now <= WDA_READ_MARGIN_SECONDS:
|
|
712
|
+
return current
|
|
713
|
+
|
|
714
|
+
def _frame_clock(self):
|
|
715
|
+
"""The attached FrameClock when active, else None."""
|
|
716
|
+
clock = self.frame_clock
|
|
717
|
+
if clock is None or self.ax_only_steps > 0:
|
|
718
|
+
return None
|
|
719
|
+
return clock
|
|
720
|
+
|
|
721
|
+
def note_screenshot_timeout(self):
|
|
722
|
+
"""A screenshot timed out or the stream stalled; two in a row settle on AX reads for WDA_AX_ONLY_STEPS."""
|
|
723
|
+
self.screenshot_timeouts += 1
|
|
724
|
+
if self.screenshot_timeouts >= WDA_SCREENSHOT_TIMEOUTS_FOR_AX_ONLY:
|
|
725
|
+
self.screenshot_timeouts = 0
|
|
726
|
+
started = self.ax_only_steps == 0
|
|
727
|
+
self.ax_only_steps = WDA_AX_ONLY_STEPS
|
|
728
|
+
return started
|
|
729
|
+
return False
|
|
730
|
+
|
|
731
|
+
def next_step(self):
|
|
732
|
+
"""A new agent step: counts down AX-only settling and counts the FrameClock's stalls as screenshot timeouts."""
|
|
733
|
+
clock = self.frame_clock
|
|
734
|
+
stalls = getattr(clock, "stalls", 0) if clock is not None else 0
|
|
735
|
+
seen = self._stalls_seen
|
|
736
|
+
if isinstance(stalls, int) and stalls > seen:
|
|
737
|
+
self._stalls_seen = stalls
|
|
738
|
+
for _ in range(stalls - seen):
|
|
739
|
+
self.note_screenshot_timeout()
|
|
740
|
+
elif self.ax_only_steps > 0:
|
|
741
|
+
self.ax_only_steps -= 1
|
|
742
|
+
return self.ax_only_steps
|
|
743
|
+
|
|
744
|
+
def _frame_mark(self, operation, **extra):
|
|
745
|
+
"""Remember the pre-dispatch frame; called immediately before dispatch."""
|
|
746
|
+
clock = self._frame_clock()
|
|
747
|
+
if clock is None:
|
|
748
|
+
return
|
|
749
|
+
if self._frame_watch is not None:
|
|
750
|
+
clock.release(self._frame_watch)
|
|
751
|
+
try:
|
|
752
|
+
self._frame_watch = clock.mark()
|
|
753
|
+
except Exception:
|
|
754
|
+
self._frame_watch = self._frame_action = None
|
|
755
|
+
return
|
|
756
|
+
self._frame_action = {"op": operation, "dispatch_started": time.monotonic(), **extra}
|
|
757
|
+
|
|
758
|
+
def _frame_dispatched(self):
|
|
759
|
+
if self._frame_action is not None:
|
|
760
|
+
self._frame_action["dispatch_ended"] = time.monotonic()
|
|
761
|
+
if self._frame_watch is not None and self.frame_clock is not None:
|
|
762
|
+
try:
|
|
763
|
+
self.frame_clock.set_release(self._frame_watch, self._frame_action["dispatch_ended"])
|
|
764
|
+
except Exception:
|
|
765
|
+
pass
|
|
766
|
+
|
|
767
|
+
def _frame_report(self, clock, watch, action, snapshot, result, started, reads, info):
|
|
768
|
+
"""Log one settle: what happened, and which rule settled it."""
|
|
769
|
+
try:
|
|
770
|
+
before = snapshot.content_fingerprint
|
|
771
|
+
ended = time.monotonic()
|
|
772
|
+
entry = {"kind": "settle",
|
|
773
|
+
"op": (action or {}).get("op"), "bundle": snapshot.bundle_id,
|
|
774
|
+
"settle_ms": round((ended - started) * 1000, 1), "reads": len(reads),
|
|
775
|
+
"changed": result.content_fingerprint != before,
|
|
776
|
+
"usable": watch.usable, "reason": watch.reason or None,
|
|
777
|
+
"fps": clock.stats().get("fps")}
|
|
778
|
+
if action and action.get("dispatch_ended") is not None:
|
|
779
|
+
entry["dispatch_ms"] = round((action["dispatch_ended"] - action["dispatch_started"]) * 1000, 1)
|
|
780
|
+
if watch.effect is not None and action:
|
|
781
|
+
entry["effect_after_dispatch_start_ms"] = round((watch.effect.t - action["dispatch_started"]) * 1000, 1)
|
|
782
|
+
entry["effect_after_dispatch_end_ms"] = round((watch.effect.t - started) * 1000, 1)
|
|
783
|
+
entry["effect_tiles"] = watch.effect_tiles
|
|
784
|
+
from ..settle.predict import annotate as annotate_settle
|
|
785
|
+
annotate_settle(self, entry, action, snapshot, result, started, reads, info)
|
|
786
|
+
if watch.stable_at is not None:
|
|
787
|
+
entry["stable_ms"] = round((watch.stable_at - started) * 1000, 1)
|
|
788
|
+
if info is not None and info.get("miss"):
|
|
789
|
+
entry["miss"] = info["miss"]
|
|
790
|
+
if info is not None and info.get("accepted_by"):
|
|
791
|
+
entry["path"] = info["accepted_by"] if info["accepted_by"] in ("one_read", "predicted") else "frame"
|
|
792
|
+
elif info is not None and info.get("fallback"):
|
|
793
|
+
entry["path"], entry["fallback"] = "frame_fallback", info["fallback"]
|
|
794
|
+
else:
|
|
795
|
+
entry["path"] = "ax"
|
|
796
|
+
fc_fingerprint = result.content_fingerprint if entry["path"] in ("frame", "one_read") else None
|
|
797
|
+
last_motion = clock.last_motion(watch)
|
|
798
|
+
if action and last_motion is not None and action.get("dispatch_ended") is not None:
|
|
799
|
+
entry["motion_after_dispatch_ms"] = round((last_motion - action["dispatch_ended"]) * 1000, 1)
|
|
800
|
+
odometry = None
|
|
801
|
+
if action and str(action.get("op", "")).startswith("SWIPE_"):
|
|
802
|
+
entry["glide"] = bool(action.get("glide"))
|
|
803
|
+
latest = clock.latest_frame()
|
|
804
|
+
if latest is not None and watch.ref is not None:
|
|
805
|
+
odometry = (watch.ref, getattr(watch, "release_frame", None), latest,
|
|
806
|
+
"y" if action["op"] in {"SWIPE_UP", "SWIPE_DOWN"} else "x")
|
|
807
|
+
if os.environ.get("MOBSTER_FRAME_CLOCK_TRACE"):
|
|
808
|
+
# Offline threshold tuning: every frame's motion flag and every read.
|
|
809
|
+
entry["trace"] = {
|
|
810
|
+
"moves": [[round((t - started) * 1000), int(moved)] for t, moved in list(watch.moves)],
|
|
811
|
+
"exact": [[round((t - started) * 1000), int(moved)] for t, moved in list(watch.exact)],
|
|
812
|
+
"reads": [[round((a - started) * 1000), round((b - started) * 1000), fingerprint[:12]]
|
|
813
|
+
for a, b, fingerprint in reads],
|
|
814
|
+
"before": before[:12], "result": result.content_fingerprint[:12],
|
|
815
|
+
"effect_ms": None if watch.effect is None else round((watch.effect.t - started) * 1000)}
|
|
816
|
+
if odometry is None:
|
|
817
|
+
clock.record(entry)
|
|
818
|
+
else:
|
|
819
|
+
# Scroll odometry costs ~10 ms per comparison: measured off the settle
|
|
820
|
+
# path. A glide that kept moving after release demotes its app.
|
|
821
|
+
bundle, url = snapshot.bundle_id, self.url
|
|
822
|
+
|
|
823
|
+
def measure():
|
|
824
|
+
try:
|
|
825
|
+
from .. import harness_api
|
|
826
|
+
frame_clock = harness_api.hooks().frame_clock() # only a run with a frame clock gets here
|
|
827
|
+
changed_tiles, displacement, tile_grid = (frame_clock.changed_tiles, frame_clock.displacement,
|
|
828
|
+
frame_clock.tile_grid)
|
|
829
|
+
ref, released, final, axes = odometry
|
|
830
|
+
entry["displacement"] = displacement(ref, final, axes)
|
|
831
|
+
if released is not None:
|
|
832
|
+
after = displacement(released, final, axes)
|
|
833
|
+
entry["after_release"] = after
|
|
834
|
+
shift = abs((after or {}).get("dy" if axes == "y" else "dx", 0))
|
|
835
|
+
columns, rows = tile_grid(final.image.size)
|
|
836
|
+
churn = len(changed_tiles(released.image, final.image)) / (columns * rows)
|
|
837
|
+
if entry["glide"] and (shift > WDA_GLIDE_MOMENTUM or (
|
|
838
|
+
churn > .4 and (after or {}).get("confidence", 0) < .5)):
|
|
839
|
+
self._momentum_apps.setdefault(url, set()).add(bundle)
|
|
840
|
+
entry["demoted_glide"] = True
|
|
841
|
+
except Exception:
|
|
842
|
+
pass
|
|
843
|
+
clock.record(entry)
|
|
844
|
+
threading.Thread(target=measure, name="mobster-odometry", daemon=True).start()
|
|
845
|
+
self._frame_followup = {"at": ended, "result": result.content_fingerprint, "fc": fc_fingerprint,
|
|
846
|
+
"op": entry["op"], "path": entry["path"], "read_started": result.read_started}
|
|
847
|
+
except Exception as error: # Logging must never affect a run.
|
|
848
|
+
self._frame_followup = None
|
|
849
|
+
try:
|
|
850
|
+
clock.record({"kind": "error", "error": type(error).__name__})
|
|
851
|
+
except Exception:
|
|
852
|
+
pass
|
|
853
|
+
|
|
854
|
+
def _frame_follow_up(self, read):
|
|
855
|
+
"""The next read after a settle: did the accepted screen hold?"""
|
|
856
|
+
followup, self._frame_followup = self._frame_followup, None
|
|
857
|
+
clock = self.frame_clock
|
|
858
|
+
if clock is None:
|
|
859
|
+
return
|
|
860
|
+
try:
|
|
861
|
+
# stream_still: not one byte changed from the accepted read's start to this read's end, so this read
|
|
862
|
+
# would have been redundant (the share of next reads that disagree,
|
|
863
|
+
# split by it, decides MOBSTER_TREE_READS).
|
|
864
|
+
entry = {"kind": "next_read", "op": followup["op"],
|
|
865
|
+
"path": followup["path"],
|
|
866
|
+
"age_ms": round((time.monotonic() - followup["at"]) * 1000, 1),
|
|
867
|
+
"matches_result": read.content_fingerprint == followup["result"],
|
|
868
|
+
"stream_still": unchanged_since(clock, followup["read_started"])}
|
|
869
|
+
if followup["fc"] is not None:
|
|
870
|
+
entry["matches_fc"] = read.content_fingerprint == followup["fc"]
|
|
871
|
+
clock.record(entry)
|
|
872
|
+
except Exception:
|
|
873
|
+
pass
|
|
874
|
+
|
|
875
|
+
def _check_current(self, snapshot, deadline):
|
|
876
|
+
"""Re-ground an aging snapshot before a coordinate tap; fresh ones pass.
|
|
877
|
+
|
|
878
|
+
A change is refused before dispatch, which the agent treats as an
|
|
879
|
+
ordinary re-observe rather than an action of unknown outcome.
|
|
880
|
+
"""
|
|
881
|
+
if time.monotonic() - snapshot.captured_at <= WDA_FRESH_SECONDS:
|
|
882
|
+
return
|
|
883
|
+
if self.observe(timeout=deadline.remaining()).fingerprint != snapshot.fingerprint:
|
|
884
|
+
raise DriverRejection("stale_revision")
|