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.
Files changed (1009) hide show
  1. mobile_agent/__init__.py +4 -0
  2. mobile_agent/__main__.py +598 -0
  3. mobile_agent/accounts/__init__.py +1 -0
  4. mobile_agent/accounts/context.py +80 -0
  5. mobile_agent/accounts/mcp.py +55 -0
  6. mobile_agent/accounts/pacing.py +318 -0
  7. mobile_agent/accounts/register.py +78 -0
  8. mobile_agent/accounts/routes.py +212 -0
  9. mobile_agent/accounts/routing.py +69 -0
  10. mobile_agent/accounts/screen.py +273 -0
  11. mobile_agent/accounts/stats.py +54 -0
  12. mobile_agent/accounts/store.py +394 -0
  13. mobile_agent/agent_hooks.py +123 -0
  14. mobile_agent/ai_plans/__init__.py +7 -0
  15. mobile_agent/ai_plans/agents.py +96 -0
  16. mobile_agent/ai_plans/claude_code.py +208 -0
  17. mobile_agent/ai_plans/cli.py +400 -0
  18. mobile_agent/ai_plans/codex.py +258 -0
  19. mobile_agent/ai_plans/compat_chat.py +307 -0
  20. mobile_agent/ai_plans/device_login.py +164 -0
  21. mobile_agent/ai_plans/draft_client.py +68 -0
  22. mobile_agent/ai_plans/gemini.py +137 -0
  23. mobile_agent/ai_plans/instructions.py +65 -0
  24. mobile_agent/ai_plans/mcp_http.py +160 -0
  25. mobile_agent/ai_plans/phone_tools.py +892 -0
  26. mobile_agent/ai_plans/providers.py +235 -0
  27. mobile_agent/ai_plans/routes.py +160 -0
  28. mobile_agent/ai_plans/runner.py +400 -0
  29. mobile_agent/ai_plans/vault.py +114 -0
  30. mobile_agent/alerts.py +337 -0
  31. mobile_agent/android/__init__.py +18 -0
  32. mobile_agent/android/adb.py +268 -0
  33. mobile_agent/android/conditions.py +128 -0
  34. mobile_agent/android/doctor.py +54 -0
  35. mobile_agent/android/driver.py +137 -0
  36. mobile_agent/android/emulator.py +216 -0
  37. mobile_agent/android/logs.py +85 -0
  38. mobile_agent/android/manager.py +225 -0
  39. mobile_agent/android/records.py +31 -0
  40. mobile_agent/android/tree.py +96 -0
  41. mobile_agent/answer_shapes.py +230 -0
  42. mobile_agent/api_devices.py +333 -0
  43. mobile_agent/api_errors.py +17 -0
  44. mobile_agent/api_get.py +249 -0
  45. mobile_agent/api_http.py +467 -0
  46. mobile_agent/api_post.py +453 -0
  47. mobile_agent/api_routes.py +173 -0
  48. mobile_agent/app_api/__init__.py +4 -0
  49. mobile_agent/app_api/cli.py +151 -0
  50. mobile_agent/app_api/endpoints.py +193 -0
  51. mobile_agent/app_api/mcp.py +54 -0
  52. mobile_agent/app_api/openapi.py +146 -0
  53. mobile_agent/app_api/reading.py +172 -0
  54. mobile_agent/app_api/register.py +10 -0
  55. mobile_agent/app_api/screen.py +216 -0
  56. mobile_agent/app_api/service.py +382 -0
  57. mobile_agent/app_api/sessions.py +404 -0
  58. mobile_agent/app_api/tokens.py +167 -0
  59. mobile_agent/app_choice.py +131 -0
  60. mobile_agent/app_icons.py +183 -0
  61. mobile_agent/app_killer.py +69 -0
  62. mobile_agent/artifacts.py +606 -0
  63. mobile_agent/attach/__init__.py +13 -0
  64. mobile_agent/attach/cli.py +46 -0
  65. mobile_agent/attach/flutter.py +28 -0
  66. mobile_agent/attach/score.py +119 -0
  67. mobile_agent/attach/web.py +13 -0
  68. mobile_agent/attachments/__init__.py +1 -0
  69. mobile_agent/attachments/agent.py +418 -0
  70. mobile_agent/attachments/extract.py +217 -0
  71. mobile_agent/attachments/mcp.py +104 -0
  72. mobile_agent/attachments/register.py +73 -0
  73. mobile_agent/attachments/routes.py +326 -0
  74. mobile_agent/attachments/service.py +170 -0
  75. mobile_agent/attachments/sniff.py +152 -0
  76. mobile_agent/attachments/store.py +441 -0
  77. mobile_agent/audit_verify.py +436 -0
  78. mobile_agent/bench/README.md +410 -0
  79. mobile_agent/bench/__init__.py +0 -0
  80. mobile_agent/bench/__main__.py +394 -0
  81. mobile_agent/bench/ab.py +168 -0
  82. mobile_agent/bench/actuation.py +368 -0
  83. mobile_agent/bench/agents/__init__.py +9 -0
  84. mobile_agent/bench/agents/base.py +105 -0
  85. mobile_agent/bench/agents/plan_clients.py +245 -0
  86. mobile_agent/bench/agents/smart.py +533 -0
  87. mobile_agent/bench/checks.py +358 -0
  88. mobile_agent/bench/decide_gate.py +214 -0
  89. mobile_agent/bench/diagnose.py +181 -0
  90. mobile_agent/bench/evals/__init__.py +14 -0
  91. mobile_agent/bench/evals/build.py +565 -0
  92. mobile_agent/bench/evals/checkpoint.py +214 -0
  93. mobile_agent/bench/evals/irt.py +219 -0
  94. mobile_agent/bench/evals/l0.py +647 -0
  95. mobile_agent/bench/evals/l1.py +866 -0
  96. mobile_agent/bench/evals/models.py +124 -0
  97. mobile_agent/bench/evals/pairwise.py +189 -0
  98. mobile_agent/bench/evals/proxy.py +90 -0
  99. mobile_agent/bench/evals/sets.py +149 -0
  100. mobile_agent/bench/evals/traces.py +446 -0
  101. mobile_agent/bench/fixtures.py +176 -0
  102. mobile_agent/bench/guard_replay.py +129 -0
  103. mobile_agent/bench/h2-manifest.json +20 -0
  104. mobile_agent/bench/h3-manifest.json +20 -0
  105. mobile_agent/bench/heldout-manifest.json +32 -0
  106. mobile_agent/bench/heldout.py +375 -0
  107. mobile_agent/bench/iosworld.py +901 -0
  108. mobile_agent/bench/iosworld_stats.py +510 -0
  109. mobile_agent/bench/llm_units.py +116 -0
  110. mobile_agent/bench/manifest.json +93 -0
  111. mobile_agent/bench/metrics.py +66 -0
  112. mobile_agent/bench/phone.py +44 -0
  113. mobile_agent/bench/replay.py +172 -0
  114. mobile_agent/bench/report.py +207 -0
  115. mobile_agent/bench/runner.py +456 -0
  116. mobile_agent/bench/safety.py +310 -0
  117. mobile_agent/bench/stream_timing.py +213 -0
  118. mobile_agent/bench/suite.py +444 -0
  119. mobile_agent/bench/sweep.py +176 -0
  120. mobile_agent/bench/tape.py +798 -0
  121. mobile_agent/bench/tape_replay.py +88 -0
  122. mobile_agent/bench/trace_evals.py +128 -0
  123. mobile_agent/bench/trace_view.py +81 -0
  124. mobile_agent/bench/truth/iphone15pro.json +138 -0
  125. mobile_agent/bench/truth.py +340 -0
  126. mobile_agent/bench/tuning.py +543 -0
  127. mobile_agent/bench/tuning_analysis.py +656 -0
  128. mobile_agent/bench/vague-heldout-manifest.json +12 -0
  129. mobile_agent/bench/vague_rubric.py +135 -0
  130. mobile_agent/bench/vague_run.py +243 -0
  131. mobile_agent/brand.py +454 -0
  132. mobile_agent/brand_fonts.py +756 -0
  133. mobile_agent/build_identity.py +21 -0
  134. mobile_agent/cable_bt.py +623 -0
  135. mobile_agent/cable_bt_runtime.py +128 -0
  136. mobile_agent/catalog.py +69 -0
  137. mobile_agent/chart_svg.py +415 -0
  138. mobile_agent/chat_here.py +189 -0
  139. mobile_agent/cli_constants.py +5 -0
  140. mobile_agent/cli_parser.py +611 -0
  141. mobile_agent/cli_run.py +115 -0
  142. mobile_agent/compiled_read/__init__.py +21 -0
  143. mobile_agent/compiled_read/delta.py +89 -0
  144. mobile_agent/compiled_read/extractors.py +202 -0
  145. mobile_agent/compiled_read/routes.py +56 -0
  146. mobile_agent/completion.py +267 -0
  147. mobile_agent/compose.py +48 -0
  148. mobile_agent/config.py +114 -0
  149. mobile_agent/connectors/__init__.py +1 -0
  150. mobile_agent/connectors/api_session.py +76 -0
  151. mobile_agent/connectors/callback_page.py +146 -0
  152. mobile_agent/connectors/client.py +582 -0
  153. mobile_agent/connectors/github_app.py +55 -0
  154. mobile_agent/connectors/graph_api.py +396 -0
  155. mobile_agent/connectors/imports.py +170 -0
  156. mobile_agent/connectors/issue.py +66 -0
  157. mobile_agent/connectors/keystore.py +85 -0
  158. mobile_agent/connectors/marks.py +222 -0
  159. mobile_agent/connectors/oauth.py +476 -0
  160. mobile_agent/connectors/picker.py +228 -0
  161. mobile_agent/connectors/presets.py +591 -0
  162. mobile_agent/connectors/register.py +48 -0
  163. mobile_agent/connectors/routes.py +153 -0
  164. mobile_agent/connectors/service.py +966 -0
  165. mobile_agent/connectors/shared.py +136 -0
  166. mobile_agent/connectors/skill.py +446 -0
  167. mobile_agent/connectors/slack_api.py +269 -0
  168. mobile_agent/connectors/steps.py +92 -0
  169. mobile_agent/connectors/store.py +144 -0
  170. mobile_agent/connectors/vault.py +72 -0
  171. mobile_agent/console.py +178 -0
  172. mobile_agent/content_queue/__init__.py +1 -0
  173. mobile_agent/content_queue/client.py +263 -0
  174. mobile_agent/content_queue/importing.py +153 -0
  175. mobile_agent/content_queue/media_check.py +224 -0
  176. mobile_agent/content_queue/prompt.py +64 -0
  177. mobile_agent/content_queue/register.py +91 -0
  178. mobile_agent/content_queue/routes.py +370 -0
  179. mobile_agent/content_queue/runner.py +475 -0
  180. mobile_agent/content_queue/store.py +521 -0
  181. mobile_agent/contract.py +996 -0
  182. mobile_agent/contract_asks.py +248 -0
  183. mobile_agent/contract_prompt.py +45 -0
  184. mobile_agent/contract_screen.py +981 -0
  185. mobile_agent/contract_view.py +84 -0
  186. mobile_agent/corpus/__init__.py +26 -0
  187. mobile_agent/corpus/cli.py +63 -0
  188. mobile_agent/corpus/consent.py +55 -0
  189. mobile_agent/corpus/listener.py +76 -0
  190. mobile_agent/corpus/register.py +9 -0
  191. mobile_agent/corpus/skeleton.py +72 -0
  192. mobile_agent/corpus/store.py +143 -0
  193. mobile_agent/costs.py +161 -0
  194. mobile_agent/decider_scope.py +40 -0
  195. mobile_agent/decision_provider.py +189 -0
  196. mobile_agent/deeplinks.py +352 -0
  197. mobile_agent/device_info.py +116 -0
  198. mobile_agent/device_manager.py +978 -0
  199. mobile_agent/device_status.py +195 -0
  200. mobile_agent/device_targets.py +217 -0
  201. mobile_agent/devices.py +676 -0
  202. mobile_agent/devkit/__init__.py +64 -0
  203. mobile_agent/devkit/account_cli.py +90 -0
  204. mobile_agent/devkit/asc.py +562 -0
  205. mobile_agent/devkit/asc_cli.py +214 -0
  206. mobile_agent/devkit/ci.py +737 -0
  207. mobile_agent/devkit/ci_cli.py +196 -0
  208. mobile_agent/devkit/ci_hosts.py +194 -0
  209. mobile_agent/devkit/es256.py +197 -0
  210. mobile_agent/devkit/expo.py +440 -0
  211. mobile_agent/devkit/expo_cli.py +405 -0
  212. mobile_agent/devkit/expo_dev.py +203 -0
  213. mobile_agent/devkit/expo_routes.py +90 -0
  214. mobile_agent/devkit/fixtures_cli.py +133 -0
  215. mobile_agent/devkit/fixtures_routes.py +82 -0
  216. mobile_agent/devkit/links.py +375 -0
  217. mobile_agent/devkit/links_cli.py +123 -0
  218. mobile_agent/devkit/links_routes.py +83 -0
  219. mobile_agent/devkit/nightly.py +152 -0
  220. mobile_agent/devkit/shots.py +203 -0
  221. mobile_agent/devkit/testflight.py +155 -0
  222. mobile_agent/devkit/testflight_cli.py +127 -0
  223. mobile_agent/devkit/testflight_routes.py +111 -0
  224. mobile_agent/devkit/xcode.py +236 -0
  225. mobile_agent/devkit/xcode_cli.py +101 -0
  226. mobile_agent/devtools.py +419 -0
  227. mobile_agent/doctor.py +509 -0
  228. mobile_agent/doctor_fix.py +467 -0
  229. mobile_agent/downloads.py +55 -0
  230. mobile_agent/drivers.py +793 -0
  231. mobile_agent/dry_run.py +102 -0
  232. mobile_agent/effect_ledger.py +83 -0
  233. mobile_agent/engine_health.py +378 -0
  234. mobile_agent/engine_hooks.py +96 -0
  235. mobile_agent/engines.py +408 -0
  236. mobile_agent/env_file.py +63 -0
  237. mobile_agent/env_secrets.py +149 -0
  238. mobile_agent/errors.py +15 -0
  239. mobile_agent/evals/__init__.py +3 -0
  240. mobile_agent/evals/app_survey.py +149 -0
  241. mobile_agent/evals/development/__init__.py +1 -0
  242. mobile_agent/evals/development/score.py +276 -0
  243. mobile_agent/evals/latency_report.py +312 -0
  244. mobile_agent/evals/offline_report.py +374 -0
  245. mobile_agent/evals/oracles.py +298 -0
  246. mobile_agent/evals/redteam/__init__.py +15 -0
  247. mobile_agent/evals/redteam/__main__.py +72 -0
  248. mobile_agent/evals/redteam/cases.py +730 -0
  249. mobile_agent/evals/redteam/harness.py +604 -0
  250. mobile_agent/evals/redteam/live.py +139 -0
  251. mobile_agent/evals/redteam/plain.py +85 -0
  252. mobile_agent/evals/vision/__init__.py +0 -0
  253. mobile_agent/evals/vision/images/dog_001.jpg +0 -0
  254. mobile_agent/evals/vision/images/dog_002.jpg +0 -0
  255. mobile_agent/evals/vision/images/dog_003.jpg +0 -0
  256. mobile_agent/evals/vision/images/dog_004.jpg +0 -0
  257. mobile_agent/evals/vision/images/dog_005.jpg +0 -0
  258. mobile_agent/evals/vision/images/dog_006.jpg +0 -0
  259. mobile_agent/evals/vision/images/dog_007.jpg +0 -0
  260. mobile_agent/evals/vision/images/dog_010.jpg +0 -0
  261. mobile_agent/evals/vision/images/dog_011.jpg +0 -0
  262. mobile_agent/evals/vision/images/dog_013.jpg +0 -0
  263. mobile_agent/evals/vision/images/dog_014.jpg +0 -0
  264. mobile_agent/evals/vision/images/dog_015.jpg +0 -0
  265. mobile_agent/evals/vision/images/dog_016.jpg +0 -0
  266. mobile_agent/evals/vision/images/dog_017.jpg +0 -0
  267. mobile_agent/evals/vision/images/dog_019.jpg +0 -0
  268. mobile_agent/evals/vision/images/dog_020.jpg +0 -0
  269. mobile_agent/evals/vision/images/dog_021.jpg +0 -0
  270. mobile_agent/evals/vision/images/dog_022.jpg +0 -0
  271. mobile_agent/evals/vision/images/dog_023.jpg +0 -0
  272. mobile_agent/evals/vision/images/dog_024.jpg +0 -0
  273. mobile_agent/evals/vision/images/dog_026.jpg +0 -0
  274. mobile_agent/evals/vision/images/dog_027.jpg +0 -0
  275. mobile_agent/evals/vision/images/dog_034.jpg +0 -0
  276. mobile_agent/evals/vision/images/dog_035.jpg +0 -0
  277. mobile_agent/evals/vision/images/dog_038.jpg +0 -0
  278. mobile_agent/evals/vision/images/dog_042.jpg +0 -0
  279. mobile_agent/evals/vision/images/dog_047.jpg +0 -0
  280. mobile_agent/evals/vision/images/dog_052.jpg +0 -0
  281. mobile_agent/evals/vision/images/dog_058.jpg +0 -0
  282. mobile_agent/evals/vision/images/dog_062.jpg +0 -0
  283. mobile_agent/evals/vision/images/eyes_000.jpg +0 -0
  284. mobile_agent/evals/vision/images/eyes_010.jpg +0 -0
  285. mobile_agent/evals/vision/images/eyes_011.jpg +0 -0
  286. mobile_agent/evals/vision/images/eyes_015.jpg +0 -0
  287. mobile_agent/evals/vision/images/eyes_016.jpg +0 -0
  288. mobile_agent/evals/vision/images/eyes_017.jpg +0 -0
  289. mobile_agent/evals/vision/images/eyes_019.jpg +0 -0
  290. mobile_agent/evals/vision/images/eyes_021.jpg +0 -0
  291. mobile_agent/evals/vision/images/eyes_025.jpg +0 -0
  292. mobile_agent/evals/vision/images/eyes_026.jpg +0 -0
  293. mobile_agent/evals/vision/images/eyes_027.jpg +0 -0
  294. mobile_agent/evals/vision/images/eyes_030.jpg +0 -0
  295. mobile_agent/evals/vision/images/eyes_032.jpg +0 -0
  296. mobile_agent/evals/vision/images/eyes_033.jpg +0 -0
  297. mobile_agent/evals/vision/images/eyes_034.jpg +0 -0
  298. mobile_agent/evals/vision/images/eyes_035.jpg +0 -0
  299. mobile_agent/evals/vision/images/eyes_036.jpg +0 -0
  300. mobile_agent/evals/vision/images/eyes_037.jpg +0 -0
  301. mobile_agent/evals/vision/images/eyes_039.jpg +0 -0
  302. mobile_agent/evals/vision/images/eyes_040.jpg +0 -0
  303. mobile_agent/evals/vision/images/eyes_041.jpg +0 -0
  304. mobile_agent/evals/vision/images/eyes_042.jpg +0 -0
  305. mobile_agent/evals/vision/images/eyes_043.jpg +0 -0
  306. mobile_agent/evals/vision/images/eyes_044.jpg +0 -0
  307. mobile_agent/evals/vision/images/eyes_046.jpg +0 -0
  308. mobile_agent/evals/vision/images/eyes_047.jpg +0 -0
  309. mobile_agent/evals/vision/images/eyes_048.jpg +0 -0
  310. mobile_agent/evals/vision/images/eyes_049.jpg +0 -0
  311. mobile_agent/evals/vision/images/eyes_051.jpg +0 -0
  312. mobile_agent/evals/vision/images/eyes_053.jpg +0 -0
  313. mobile_agent/evals/vision/images/eyes_054.jpg +0 -0
  314. mobile_agent/evals/vision/images/eyes_055.jpg +0 -0
  315. mobile_agent/evals/vision/images/eyes_058.jpg +0 -0
  316. mobile_agent/evals/vision/images/eyes_059.jpg +0 -0
  317. mobile_agent/evals/vision/images/receipt_002.jpg +0 -0
  318. mobile_agent/evals/vision/images/receipt_003.jpg +0 -0
  319. mobile_agent/evals/vision/images/receipt_004.jpg +0 -0
  320. mobile_agent/evals/vision/images/receipt_007.jpg +0 -0
  321. mobile_agent/evals/vision/images/receipt_009.jpg +0 -0
  322. mobile_agent/evals/vision/images/receipt_010.jpg +0 -0
  323. mobile_agent/evals/vision/images/receipt_011.jpg +0 -0
  324. mobile_agent/evals/vision/images/receipt_012.jpg +0 -0
  325. mobile_agent/evals/vision/images/receipt_013.jpg +0 -0
  326. mobile_agent/evals/vision/images/receipt_014.jpg +0 -0
  327. mobile_agent/evals/vision/images/receipt_016.jpg +0 -0
  328. mobile_agent/evals/vision/images/receipt_017.jpg +0 -0
  329. mobile_agent/evals/vision/images/receipt_023.jpg +0 -0
  330. mobile_agent/evals/vision/images/receipt_027.jpg +0 -0
  331. mobile_agent/evals/vision/images/receipt_029.jpg +0 -0
  332. mobile_agent/evals/vision/images/receipt_032.jpg +0 -0
  333. mobile_agent/evals/vision/images/receipt_033.jpg +0 -0
  334. mobile_agent/evals/vision/images/receipt_036.jpg +0 -0
  335. mobile_agent/evals/vision/images/receipt_041.jpg +0 -0
  336. mobile_agent/evals/vision/images/receipt_043.jpg +0 -0
  337. mobile_agent/evals/vision/images/receipt_047.jpg +0 -0
  338. mobile_agent/evals/vision/images/receipt_049.jpg +0 -0
  339. mobile_agent/evals/vision/images/receipt_051.jpg +0 -0
  340. mobile_agent/evals/vision/images/receipt_053.jpg +0 -0
  341. mobile_agent/evals/vision/images/receipt_056.jpg +0 -0
  342. mobile_agent/evals/vision/images/receipt_057.jpg +0 -0
  343. mobile_agent/evals/vision/images/receipt_058.jpg +0 -0
  344. mobile_agent/evals/vision/images/receipt_059.jpg +0 -0
  345. mobile_agent/evals/vision/images/receipt_060.jpg +0 -0
  346. mobile_agent/evals/vision/images/receipt_061.jpg +0 -0
  347. mobile_agent/evals/vision/images/receipt_062.jpg +0 -0
  348. mobile_agent/evals/vision/images/receipt_063.jpg +0 -0
  349. mobile_agent/evals/vision/images/receipt_064.jpg +0 -0
  350. mobile_agent/evals/vision/images/receipt_065.jpg +0 -0
  351. mobile_agent/evals/vision/images/receipt_066.jpg +0 -0
  352. mobile_agent/evals/vision/images/receipt_067.jpg +0 -0
  353. mobile_agent/evals/vision/images/receipt_068.jpg +0 -0
  354. mobile_agent/evals/vision/images/receipt_069.jpg +0 -0
  355. mobile_agent/evals/vision/images/receipt_070.jpg +0 -0
  356. mobile_agent/evals/vision/images/receipt_071.jpg +0 -0
  357. mobile_agent/evals/vision/images/sunglasses_000.jpg +0 -0
  358. mobile_agent/evals/vision/images/sunglasses_001.jpg +0 -0
  359. mobile_agent/evals/vision/images/sunglasses_002.jpg +0 -0
  360. mobile_agent/evals/vision/images/sunglasses_004.jpg +0 -0
  361. mobile_agent/evals/vision/images/sunglasses_005.jpg +0 -0
  362. mobile_agent/evals/vision/images/sunglasses_008.jpg +0 -0
  363. mobile_agent/evals/vision/images/sunglasses_009.jpg +0 -0
  364. mobile_agent/evals/vision/images/sunglasses_010.jpg +0 -0
  365. mobile_agent/evals/vision/images/sunglasses_012.jpg +0 -0
  366. mobile_agent/evals/vision/images/sunglasses_013.jpg +0 -0
  367. mobile_agent/evals/vision/images/sunglasses_015.jpg +0 -0
  368. mobile_agent/evals/vision/images/sunglasses_016.jpg +0 -0
  369. mobile_agent/evals/vision/images/sunglasses_017.jpg +0 -0
  370. mobile_agent/evals/vision/images/sunglasses_018.jpg +0 -0
  371. mobile_agent/evals/vision/images/sunglasses_021.jpg +0 -0
  372. mobile_agent/evals/vision/images/sunglasses_022.jpg +0 -0
  373. mobile_agent/evals/vision/images/sunglasses_023.jpg +0 -0
  374. mobile_agent/evals/vision/images/sunglasses_024.jpg +0 -0
  375. mobile_agent/evals/vision/images/sunglasses_030.jpg +0 -0
  376. mobile_agent/evals/vision/images/sunglasses_031.jpg +0 -0
  377. mobile_agent/evals/vision/images/sunglasses_032.jpg +0 -0
  378. mobile_agent/evals/vision/images/sunglasses_033.jpg +0 -0
  379. mobile_agent/evals/vision/images/sunglasses_034.jpg +0 -0
  380. mobile_agent/evals/vision/images/sunglasses_036.jpg +0 -0
  381. mobile_agent/evals/vision/images/sunglasses_038.jpg +0 -0
  382. mobile_agent/evals/vision/images/sunglasses_039.jpg +0 -0
  383. mobile_agent/evals/vision/images/sunglasses_040.jpg +0 -0
  384. mobile_agent/evals/vision/images/sunglasses_041.jpg +0 -0
  385. mobile_agent/evals/vision/manifest.json +2355 -0
  386. mobile_agent/eventbus.py +110 -0
  387. mobile_agent/explain.py +34 -0
  388. mobile_agent/failure_line.py +197 -0
  389. mobile_agent/fanout.py +663 -0
  390. mobile_agent/feedback/__init__.py +2 -0
  391. mobile_agent/feedback/bundle.py +204 -0
  392. mobile_agent/feedback/outbox.py +196 -0
  393. mobile_agent/feedback/register.py +9 -0
  394. mobile_agent/feedback/routes.py +177 -0
  395. mobile_agent/fleet.py +841 -0
  396. mobile_agent/fleet_health.py +729 -0
  397. mobile_agent/fleet_phone.py +186 -0
  398. mobile_agent/frontier/__init__.py +79 -0
  399. mobile_agent/frontier/act.py +380 -0
  400. mobile_agent/frontier/actions.py +70 -0
  401. mobile_agent/frontier/agent.py +256 -0
  402. mobile_agent/frontier/approvals.py +366 -0
  403. mobile_agent/frontier/hooks.py +199 -0
  404. mobile_agent/frontier/interrupts.py +125 -0
  405. mobile_agent/frontier/observe.py +331 -0
  406. mobile_agent/frontier/phases.py +381 -0
  407. mobile_agent/frontier/pipeline.py +61 -0
  408. mobile_agent/frontier/prompt.py +384 -0
  409. mobile_agent/frontier/protocol.py +91 -0
  410. mobile_agent/frontier/safety.py +467 -0
  411. mobile_agent/frontier/saved.py +375 -0
  412. mobile_agent/frontier/screen.py +419 -0
  413. mobile_agent/frontier/skills.py +147 -0
  414. mobile_agent/frontier/stages/__init__.py +1 -0
  415. mobile_agent/frontier/stages/after.py +192 -0
  416. mobile_agent/frontier/stages/approve.py +138 -0
  417. mobile_agent/frontier/stages/controls.py +115 -0
  418. mobile_agent/frontier/stages/ends.py +102 -0
  419. mobile_agent/frontier/stages/gate.py +92 -0
  420. mobile_agent/frontier/stages/guards.py +132 -0
  421. mobile_agent/frontier/stages/points.py +155 -0
  422. mobile_agent/frontier/stages/send.py +177 -0
  423. mobile_agent/frontier/turn.py +151 -0
  424. mobile_agent/full_disk_access.py +83 -0
  425. mobile_agent/gemini.py +69 -0
  426. mobile_agent/genuine.py +120 -0
  427. mobile_agent/graph/__init__.py +4 -0
  428. mobile_agent/graph/catalog.py +789 -0
  429. mobile_agent/graph/chain.py +306 -0
  430. mobile_agent/graph/dispatch.py +98 -0
  431. mobile_agent/graph/draft.py +373 -0
  432. mobile_agent/graph/edit.py +76 -0
  433. mobile_agent/graph/estimate.py +189 -0
  434. mobile_agent/graph/expr.py +562 -0
  435. mobile_agent/graph/functions.py +271 -0
  436. mobile_agent/graph/infer.py +144 -0
  437. mobile_agent/graph/nodes.py +936 -0
  438. mobile_agent/graph/phone_steps.py +178 -0
  439. mobile_agent/graph/rerun.py +327 -0
  440. mobile_agent/graph/run.py +899 -0
  441. mobile_agent/graph/schema.py +842 -0
  442. mobile_agent/graph/state.py +161 -0
  443. mobile_agent/graph/values.py +198 -0
  444. mobile_agent/harness/__init__.py +2 -0
  445. mobile_agent/harness/checkpoints.py +468 -0
  446. mobile_agent/harness/control.py +101 -0
  447. mobile_agent/harness/prewarm.py +213 -0
  448. mobile_agent/harness/register.py +43 -0
  449. mobile_agent/harness/routes.py +114 -0
  450. mobile_agent/harness/settings.py +66 -0
  451. mobile_agent/harness/tools/__init__.py +67 -0
  452. mobile_agent/harness/tools/ask_user.py +102 -0
  453. mobile_agent/harness/tools/watch.py +120 -0
  454. mobile_agent/harness_api.py +815 -0
  455. mobile_agent/hedge.py +203 -0
  456. mobile_agent/help_topics.py +305 -0
  457. mobile_agent/hints.py +248 -0
  458. mobile_agent/history.py +69 -0
  459. mobile_agent/home.py +214 -0
  460. mobile_agent/http_pool.py +175 -0
  461. mobile_agent/image_policy.py +139 -0
  462. mobile_agent/inbox.py +148 -0
  463. mobile_agent/installed_apps.py +344 -0
  464. mobile_agent/integrations/__init__.py +1 -0
  465. mobile_agent/integrations/cli.py +472 -0
  466. mobile_agent/integrations/client_files.py +65 -0
  467. mobile_agent/integrations/clients.py +998 -0
  468. mobile_agent/integrations/doctor.py +185 -0
  469. mobile_agent/integrations/jsonc.py +383 -0
  470. mobile_agent/integrations/routes.py +135 -0
  471. mobile_agent/integrations/rules.py +112 -0
  472. mobile_agent/integrations/server_options.py +46 -0
  473. mobile_agent/integrations/skill.py +152 -0
  474. mobile_agent/integrations/skill_files.py +10 -0
  475. mobile_agent/integrations/textedit.py +309 -0
  476. mobile_agent/intents/__init__.py +17 -0
  477. mobile_agent/intents/atlas.py +216 -0
  478. mobile_agent/intents/callback.py +44 -0
  479. mobile_agent/intents/cli.py +55 -0
  480. mobile_agent/intents/harvest.py +91 -0
  481. mobile_agent/intents/plan.py +93 -0
  482. mobile_agent/ios/__init__.py +1 -0
  483. mobile_agent/ios/guide.py +16 -0
  484. mobile_agent/ios/kb.py +212 -0
  485. mobile_agent/ios/kb_data.py +1588 -0
  486. mobile_agent/ios/mcp.py +44 -0
  487. mobile_agent/ios/register.py +8 -0
  488. mobile_agent/ios/system.py +369 -0
  489. mobile_agent/journal.py +531 -0
  490. mobile_agent/keychain_acl.py +374 -0
  491. mobile_agent/keys.py +395 -0
  492. mobile_agent/latency_overlap.py +36 -0
  493. mobile_agent/latency_trace.py +170 -0
  494. mobile_agent/learned/__init__.py +15 -0
  495. mobile_agent/learned/app_path.py +160 -0
  496. mobile_agent/learned/code_read.py +38 -0
  497. mobile_agent/learned/parts.py +546 -0
  498. mobile_agent/learned/programs.py +691 -0
  499. mobile_agent/learned/register.py +171 -0
  500. mobile_agent/learned/routes.py +61 -0
  501. mobile_agent/learned/taught.py +329 -0
  502. mobile_agent/learned/teach.py +538 -0
  503. mobile_agent/learned/workflows.py +647 -0
  504. mobile_agent/learned/zero.py +41 -0
  505. mobile_agent/links.py +37 -0
  506. mobile_agent/listen.py +607 -0
  507. mobile_agent/lock_spike.py +278 -0
  508. mobile_agent/lockscreen.py +526 -0
  509. mobile_agent/login.py +354 -0
  510. mobile_agent/longform/__init__.py +15 -0
  511. mobile_agent/longform/ads.py +191 -0
  512. mobile_agent/longform/blocks.py +117 -0
  513. mobile_agent/longform/budget.py +265 -0
  514. mobile_agent/longform/context.py +285 -0
  515. mobile_agent/longform/costs.py +93 -0
  516. mobile_agent/longform/digest.py +145 -0
  517. mobile_agent/longform/extract.py +508 -0
  518. mobile_agent/longform/feed.py +505 -0
  519. mobile_agent/longform/followup.py +168 -0
  520. mobile_agent/longform/frames.py +82 -0
  521. mobile_agent/longform/identity.py +87 -0
  522. mobile_agent/longform/inbox.py +221 -0
  523. mobile_agent/longform/items.py +425 -0
  524. mobile_agent/longform/models.py +115 -0
  525. mobile_agent/longform/notebook.py +329 -0
  526. mobile_agent/longform/plan.py +614 -0
  527. mobile_agent/longform/recall.py +195 -0
  528. mobile_agent/longform/receipts.py +81 -0
  529. mobile_agent/longform/register.py +124 -0
  530. mobile_agent/longform/report.py +343 -0
  531. mobile_agent/longform/routes.py +102 -0
  532. mobile_agent/longform/session.py +644 -0
  533. mobile_agent/longform/store.py +146 -0
  534. mobile_agent/longform/synthesis.py +347 -0
  535. mobile_agent/mac_tools.py +223 -0
  536. mobile_agent/manual_control.py +143 -0
  537. mobile_agent/maps.py +149 -0
  538. mobile_agent/marks.py +92 -0
  539. mobile_agent/mcp_server/__init__.py +3 -0
  540. mobile_agent/mcp_server/__main__.py +29 -0
  541. mobile_agent/mcp_server/action_journal.py +24 -0
  542. mobile_agent/mcp_server/action_tools.py +638 -0
  543. mobile_agent/mcp_server/approvals.py +219 -0
  544. mobile_agent/mcp_server/catalog.py +92 -0
  545. mobile_agent/mcp_server/cli.py +194 -0
  546. mobile_agent/mcp_server/codes.py +147 -0
  547. mobile_agent/mcp_server/device_tools.py +306 -0
  548. mobile_agent/mcp_server/direct.py +272 -0
  549. mobile_agent/mcp_server/expo_tools.py +105 -0
  550. mobile_agent/mcp_server/phone_tools.py +510 -0
  551. mobile_agent/mcp_server/protocol.py +364 -0
  552. mobile_agent/mcp_server/registry.py +55 -0
  553. mobile_agent/mcp_server/run_tools.py +531 -0
  554. mobile_agent/mcp_server/schemas.py +425 -0
  555. mobile_agent/mcp_server/screen_tools.py +168 -0
  556. mobile_agent/mcp_server/session.py +469 -0
  557. mobile_agent/mcp_server/support.py +145 -0
  558. mobile_agent/mcp_server/tools.py +493 -0
  559. mobile_agent/memory/__init__.py +9 -0
  560. mobile_agent/memory/cli.py +207 -0
  561. mobile_agent/memory/extract.py +191 -0
  562. mobile_agent/memory/listener.py +100 -0
  563. mobile_agent/memory/model.py +144 -0
  564. mobile_agent/memory/project.py +96 -0
  565. mobile_agent/memory/provider.py +127 -0
  566. mobile_agent/memory/register.py +21 -0
  567. mobile_agent/memory/retrieval.py +172 -0
  568. mobile_agent/memory/routes.py +216 -0
  569. mobile_agent/memory/runs.py +124 -0
  570. mobile_agent/memory/store.py +806 -0
  571. mobile_agent/memory/writer.py +278 -0
  572. mobile_agent/mirroring.py +585 -0
  573. mobile_agent/mirroring_runtime.py +221 -0
  574. mobile_agent/model_clients.py +604 -0
  575. mobile_agent/narrate.py +813 -0
  576. mobile_agent/native_helpers.py +84 -0
  577. mobile_agent/netxray/__init__.py +1 -0
  578. mobile_agent/netxray/analyze.py +322 -0
  579. mobile_agent/netxray/attribution.py +62 -0
  580. mobile_agent/netxray/capture.py +88 -0
  581. mobile_agent/netxray/cli.py +142 -0
  582. mobile_agent/netxray/job.py +219 -0
  583. mobile_agent/netxray/mobster-pcap.c +560 -0
  584. mobile_agent/netxray/packets.py +410 -0
  585. mobile_agent/netxray/pcapng.py +223 -0
  586. mobile_agent/netxray/register.py +93 -0
  587. mobile_agent/netxray/report.py +214 -0
  588. mobile_agent/netxray/store.py +48 -0
  589. mobile_agent/netxray/summary.py +157 -0
  590. mobile_agent/netxray/target.py +67 -0
  591. mobile_agent/netxray/tool.py +66 -0
  592. mobile_agent/netxray/trackers.py +194 -0
  593. mobile_agent/notify_triggers/__init__.py +1 -0
  594. mobile_agent/notify_triggers/rules.py +39 -0
  595. mobile_agent/notify_triggers/sources.py +315 -0
  596. mobile_agent/notify_triggers/watch.py +135 -0
  597. mobile_agent/offers.py +86 -0
  598. mobile_agent/orchestrate.py +215 -0
  599. mobile_agent/outlets/__init__.py +1 -0
  600. mobile_agent/outlets/connect.py +249 -0
  601. mobile_agent/outlets/folder.py +180 -0
  602. mobile_agent/outlets/hooks.py +453 -0
  603. mobile_agent/outlets/inbound.py +472 -0
  604. mobile_agent/outlets/inbound_reply.py +57 -0
  605. mobile_agent/outlets/inbound_routes.py +81 -0
  606. mobile_agent/outlets/kinds.py +308 -0
  607. mobile_agent/outlets/mail.py +110 -0
  608. mobile_agent/outlets/net.py +86 -0
  609. mobile_agent/outlets/notion.py +244 -0
  610. mobile_agent/outlets/register.py +56 -0
  611. mobile_agent/outlets/routes.py +135 -0
  612. mobile_agent/outlets/service.py +580 -0
  613. mobile_agent/outlets/sheets.py +169 -0
  614. mobile_agent/outlets/slack.py +177 -0
  615. mobile_agent/outlets/store.py +194 -0
  616. mobile_agent/outlets/vault.py +80 -0
  617. mobile_agent/outlets/webhook.py +316 -0
  618. mobile_agent/output_contract.py +141 -0
  619. mobile_agent/owner_presence.py +211 -0
  620. mobile_agent/ownership.py +219 -0
  621. mobile_agent/packs/__init__.py +1 -0
  622. mobile_agent/packs/calendar.py +243 -0
  623. mobile_agent/packs/context.py +68 -0
  624. mobile_agent/packs/core.py +757 -0
  625. mobile_agent/packs/front_door.py +316 -0
  626. mobile_agent/packs/heal.py +214 -0
  627. mobile_agent/packs/health.py +122 -0
  628. mobile_agent/packs/insights.py +76 -0
  629. mobile_agent/packs/instagram.py +640 -0
  630. mobile_agent/packs/linkedin.py +106 -0
  631. mobile_agent/packs/maps.py +196 -0
  632. mobile_agent/packs/mcp.py +149 -0
  633. mobile_agent/packs/messages.py +270 -0
  634. mobile_agent/packs/notes.py +226 -0
  635. mobile_agent/packs/photos.py +166 -0
  636. mobile_agent/packs/pins.py +47 -0
  637. mobile_agent/packs/reddit.py +110 -0
  638. mobile_agent/packs/register.py +276 -0
  639. mobile_agent/packs/rows.py +108 -0
  640. mobile_agent/packs/safari.py +143 -0
  641. mobile_agent/packs/settings.py +253 -0
  642. mobile_agent/packs/social.py +300 -0
  643. mobile_agent/packs/threads.py +97 -0
  644. mobile_agent/packs/tiktok.py +455 -0
  645. mobile_agent/packs/tools.py +290 -0
  646. mobile_agent/packs/x.py +127 -0
  647. mobile_agent/packs/youtube.py +199 -0
  648. mobile_agent/parity.py +270 -0
  649. mobile_agent/paths.py +99 -0
  650. mobile_agent/perception.py +470 -0
  651. mobile_agent/permissions/__init__.py +32 -0
  652. mobile_agent/permissions/consequence.py +535 -0
  653. mobile_agent/permissions/exceptions.py +99 -0
  654. mobile_agent/permissions/gate.py +316 -0
  655. mobile_agent/permissions/outside.py +316 -0
  656. mobile_agent/permissions/policy.py +288 -0
  657. mobile_agent/permissions/project_policy.py +411 -0
  658. mobile_agent/permissions/register.py +148 -0
  659. mobile_agent/permissions/rules.py +291 -0
  660. mobile_agent/phone_io/__init__.py +146 -0
  661. mobile_agent/phone_io/cli.py +427 -0
  662. mobile_agent/phone_io/clipboard.py +78 -0
  663. mobile_agent/phone_io/controls.py +78 -0
  664. mobile_agent/phone_io/files.py +289 -0
  665. mobile_agent/phone_io/handoff.py +123 -0
  666. mobile_agent/phone_io/icloud.py +118 -0
  667. mobile_agent/phone_io/install.py +188 -0
  668. mobile_agent/phone_io/network_capture.py +188 -0
  669. mobile_agent/phone_io/network_cli.py +138 -0
  670. mobile_agent/phone_io/network_profiles.py +138 -0
  671. mobile_agent/phone_io/photos.py +512 -0
  672. mobile_agent/phone_io/recording.py +115 -0
  673. mobile_agent/phone_io/secrets.py +146 -0
  674. mobile_agent/phone_io/transfer.py +895 -0
  675. mobile_agent/phone_io/tunnel.py +129 -0
  676. mobile_agent/phone_ready.py +91 -0
  677. mobile_agent/pinning.py +158 -0
  678. mobile_agent/plan_gate.py +807 -0
  679. mobile_agent/plugins/__init__.py +3 -0
  680. mobile_agent/plugins/install.py +163 -0
  681. mobile_agent/plugins/manifest.py +242 -0
  682. mobile_agent/plugins/register.py +15 -0
  683. mobile_agent/plugins/routes.py +82 -0
  684. mobile_agent/plugins/service.py +322 -0
  685. mobile_agent/plugins/skills.py +76 -0
  686. mobile_agent/plugins/store.py +79 -0
  687. mobile_agent/pool.py +285 -0
  688. mobile_agent/power_watch.py +275 -0
  689. mobile_agent/premium_model.py +22 -0
  690. mobile_agent/project_env.py +291 -0
  691. mobile_agent/project_env_cli.py +74 -0
  692. mobile_agent/project_env_routes.py +55 -0
  693. mobile_agent/projects/__init__.py +27 -0
  694. mobile_agent/projects/ci.py +332 -0
  695. mobile_agent/projects/clone.py +339 -0
  696. mobile_agent/projects/drafts.py +158 -0
  697. mobile_agent/projects/folder.py +436 -0
  698. mobile_agent/projects/github.py +131 -0
  699. mobile_agent/projects/register.py +31 -0
  700. mobile_agent/projects/revisions.py +253 -0
  701. mobile_agent/projects/routes.py +234 -0
  702. mobile_agent/projects/runs.py +297 -0
  703. mobile_agent/projects/service.py +695 -0
  704. mobile_agent/projects/setup.py +84 -0
  705. mobile_agent/projects/store.py +379 -0
  706. mobile_agent/projects/sync.py +380 -0
  707. mobile_agent/projects/workflow_files.py +270 -0
  708. mobile_agent/replay_core/__init__.py +26 -0
  709. mobile_agent/replay_core/checks.py +594 -0
  710. mobile_agent/replay_core/engine.py +596 -0
  711. mobile_agent/replay_core/heal.py +364 -0
  712. mobile_agent/replay_core/path.py +240 -0
  713. mobile_agent/replay_core/recorder.py +206 -0
  714. mobile_agent/replay_core/store.py +171 -0
  715. mobile_agent/result_blocks.py +941 -0
  716. mobile_agent/run.py +642 -0
  717. mobile_agent/run_delivery.py +123 -0
  718. mobile_agent/run_executor.py +200 -0
  719. mobile_agent/run_export.py +284 -0
  720. mobile_agent/run_gif.py +891 -0
  721. mobile_agent/runner_hints.py +86 -0
  722. mobile_agent/runner_renewal.py +205 -0
  723. mobile_agent/runtime_create.py +549 -0
  724. mobile_agent/runtime_driver.py +458 -0
  725. mobile_agent/runtime_limits.py +319 -0
  726. mobile_agent/runtime_queue.py +394 -0
  727. mobile_agent/runtime_runs.py +383 -0
  728. mobile_agent/runtime_status.py +181 -0
  729. mobile_agent/runtime_teach.py +295 -0
  730. mobile_agent/runtime_work.py +599 -0
  731. mobile_agent/screen_text.py +101 -0
  732. mobile_agent/secret_filter.py +113 -0
  733. mobile_agent/serve.py +160 -0
  734. mobile_agent/server.py +363 -0
  735. mobile_agent/service_support.py +42 -0
  736. mobile_agent/settle/__init__.py +1 -0
  737. mobile_agent/settle/predict.py +102 -0
  738. mobile_agent/settle/replay.py +92 -0
  739. mobile_agent/settle/store.py +93 -0
  740. mobile_agent/settle/stream.py +43 -0
  741. mobile_agent/setup_cli.py +225 -0
  742. mobile_agent/setup_errors.py +87 -0
  743. mobile_agent/setup_service.py +527 -0
  744. mobile_agent/share.py +104 -0
  745. mobile_agent/signing.py +184 -0
  746. mobile_agent/sim/__init__.py +6 -0
  747. mobile_agent/sim/__main__.py +26 -0
  748. mobile_agent/sim/api.py +952 -0
  749. mobile_agent/sim/apps.py +172 -0
  750. mobile_agent/sim/biometrics.py +84 -0
  751. mobile_agent/sim/cli.py +271 -0
  752. mobile_agent/sim/doctor.py +293 -0
  753. mobile_agent/sim/governor.py +90 -0
  754. mobile_agent/sim/holders.py +88 -0
  755. mobile_agent/sim/models.py +62 -0
  756. mobile_agent/sim/registry.py +161 -0
  757. mobile_agent/sim/simctl.py +269 -0
  758. mobile_agent/sim/wda.py +618 -0
  759. mobile_agent/sim/xcode.py +74 -0
  760. mobile_agent/skills/__init__.py +12 -0
  761. mobile_agent/skills/codes.py +597 -0
  762. mobile_agent/skills/notifications.py +281 -0
  763. mobile_agent/skills/open_url.py +127 -0
  764. mobile_agent/smart_events.py +588 -0
  765. mobile_agent/smart_result.py +490 -0
  766. mobile_agent/smart_run.py +413 -0
  767. mobile_agent/state.py +662 -0
  768. mobile_agent/states.py +217 -0
  769. mobile_agent/steering.py +70 -0
  770. mobile_agent/style.py +178 -0
  771. mobile_agent/supervised.py +234 -0
  772. mobile_agent/swift/helper_bt_hid.swift +534 -0
  773. mobile_agent/swift/helper_doc_text.swift +184 -0
  774. mobile_agent/swift/helper_draft_reply.swift +170 -0
  775. mobile_agent/swift/helper_map.swift +110 -0
  776. mobile_agent/swift/helper_mirroring.swift +1099 -0
  777. mobile_agent/swift/helper_mjpeg_mp4.swift +103 -0
  778. mobile_agent/swift/helper_pdf.swift +100 -0
  779. mobile_agent/swift/helper_phone_audio.swift +993 -0
  780. mobile_agent/swift/helper_phone_screen.swift +416 -0
  781. mobile_agent/swift/helper_screen_text.swift +24 -0
  782. mobile_agent/swift/helper_transfer.swift +246 -0
  783. mobile_agent/swift/ocr.swift +22 -0
  784. mobile_agent/task_policy.py +337 -0
  785. mobile_agent/team_signing/__init__.py +34 -0
  786. mobile_agent/team_signing/apple_ca.py +37 -0
  787. mobile_agent/team_signing/bundle.py +136 -0
  788. mobile_agent/team_signing/cli.py +208 -0
  789. mobile_agent/team_signing/identity.py +248 -0
  790. mobile_agent/team_signing/ledger.py +105 -0
  791. mobile_agent/team_signing/prebuilt.py +61 -0
  792. mobile_agent/team_signing/resign.py +256 -0
  793. mobile_agent/team_signing/service.py +382 -0
  794. mobile_agent/team_signing/tooling.py +243 -0
  795. mobile_agent/team_signing/writes.py +268 -0
  796. mobile_agent/terminal_image.py +81 -0
  797. mobile_agent/testkit/__init__.py +1 -0
  798. mobile_agent/testkit/a11y_baseline.py +132 -0
  799. mobile_agent/testkit/api.py +403 -0
  800. mobile_agent/testkit/capsule.py +400 -0
  801. mobile_agent/testkit/capsule_cli.py +57 -0
  802. mobile_agent/testkit/cli.py +948 -0
  803. mobile_agent/testkit/cli_arguments.py +193 -0
  804. mobile_agent/testkit/comparison.py +288 -0
  805. mobile_agent/testkit/comparison_cli.py +41 -0
  806. mobile_agent/testkit/discover.py +257 -0
  807. mobile_agent/testkit/editor.py +243 -0
  808. mobile_agent/testkit/execute.py +515 -0
  809. mobile_agent/testkit/github_checks.py +248 -0
  810. mobile_agent/testkit/gitref.py +66 -0
  811. mobile_agent/testkit/heal_review.py +176 -0
  812. mobile_agent/testkit/health.py +480 -0
  813. mobile_agent/testkit/history.py +255 -0
  814. mobile_agent/testkit/hosted_results.py +321 -0
  815. mobile_agent/testkit/input_files.py +173 -0
  816. mobile_agent/testkit/jobs.py +285 -0
  817. mobile_agent/testkit/junit_verdict.py +33 -0
  818. mobile_agent/testkit/maestro.py +802 -0
  819. mobile_agent/testkit/mcp.py +562 -0
  820. mobile_agent/testkit/mcp_comparison.py +88 -0
  821. mobile_agent/testkit/minimize.py +471 -0
  822. mobile_agent/testkit/minimizer_cli.py +48 -0
  823. mobile_agent/testkit/notify.py +212 -0
  824. mobile_agent/testkit/phone_video.py +191 -0
  825. mobile_agent/testkit/pr.py +491 -0
  826. mobile_agent/testkit/pr_report.py +562 -0
  827. mobile_agent/testkit/prbuild.py +148 -0
  828. mobile_agent/testkit/proof_report.py +55 -0
  829. mobile_agent/testkit/provenance.py +119 -0
  830. mobile_agent/testkit/prrun.py +244 -0
  831. mobile_agent/testkit/recent.py +166 -0
  832. mobile_agent/testkit/recipes.py +146 -0
  833. mobile_agent/testkit/record.py +204 -0
  834. mobile_agent/testkit/register.py +288 -0
  835. mobile_agent/testkit/reports.py +998 -0
  836. mobile_agent/testkit/repro.py +432 -0
  837. mobile_agent/testkit/scenario.py +440 -0
  838. mobile_agent/testkit/scenario_cli.py +37 -0
  839. mobile_agent/testkit/snapshots_cli.py +60 -0
  840. mobile_agent/testkit/suite.py +670 -0
  841. mobile_agent/testkit/targets.py +497 -0
  842. mobile_agent/testkit/xctest.py +378 -0
  843. mobile_agent/threads/__init__.py +1 -0
  844. mobile_agent/threads/chat_once.py +264 -0
  845. mobile_agent/threads/cli.py +740 -0
  846. mobile_agent/threads/client.py +326 -0
  847. mobile_agent/threads/context.py +246 -0
  848. mobile_agent/threads/mcp.py +739 -0
  849. mobile_agent/threads/recent.py +34 -0
  850. mobile_agent/threads/register.py +54 -0
  851. mobile_agent/threads/routes.py +188 -0
  852. mobile_agent/threads/service.py +975 -0
  853. mobile_agent/threads/store.py +361 -0
  854. mobile_agent/threads/summary.py +117 -0
  855. mobile_agent/threads/term.py +101 -0
  856. mobile_agent/threads/workflow_cli.py +104 -0
  857. mobile_agent/tls.py +36 -0
  858. mobile_agent/traces.py +271 -0
  859. mobile_agent/tracks.py +235 -0
  860. mobile_agent/transport.py +271 -0
  861. mobile_agent/triggers/__init__.py +1 -0
  862. mobile_agent/triggers/actions.py +204 -0
  863. mobile_agent/triggers/model.py +146 -0
  864. mobile_agent/triggers/register.py +29 -0
  865. mobile_agent/triggers/routes.py +101 -0
  866. mobile_agent/triggers/service.py +387 -0
  867. mobile_agent/triggers/store.py +142 -0
  868. mobile_agent/tui/__init__.py +89 -0
  869. mobile_agent/tui/app.py +582 -0
  870. mobile_agent/tui/chrome.py +321 -0
  871. mobile_agent/tui/commands.py +375 -0
  872. mobile_agent/tui/demo_phone.py +369 -0
  873. mobile_agent/tui/images.py +29 -0
  874. mobile_agent/tui/keys.py +64 -0
  875. mobile_agent/tui/prompt.py +195 -0
  876. mobile_agent/tui/render.py +395 -0
  877. mobile_agent/tui/screens.py +180 -0
  878. mobile_agent/tui/session.py +308 -0
  879. mobile_agent/tui/tasks.py +287 -0
  880. mobile_agent/tui/theme.py +118 -0
  881. mobile_agent/tui/widgets.py +272 -0
  882. mobile_agent/update_check.py +213 -0
  883. mobile_agent/usage.py +184 -0
  884. mobile_agent/verify/__init__.py +0 -0
  885. mobile_agent/verify/__main__.py +41 -0
  886. mobile_agent/verify/a11y.py +189 -0
  887. mobile_agent/verify/actions.py +290 -0
  888. mobile_agent/verify/assertions.py +570 -0
  889. mobile_agent/verify/audit.py +312 -0
  890. mobile_agent/verify/business.py +221 -0
  891. mobile_agent/verify/business_run.py +131 -0
  892. mobile_agent/verify/business_state.py +168 -0
  893. mobile_agent/verify/check_schema.py +398 -0
  894. mobile_agent/verify/checks.py +614 -0
  895. mobile_agent/verify/cli.py +423 -0
  896. mobile_agent/verify/conditions.py +483 -0
  897. mobile_agent/verify/crashes.py +321 -0
  898. mobile_agent/verify/device_audit.py +246 -0
  899. mobile_agent/verify/dialogs.py +287 -0
  900. mobile_agent/verify/evidence.py +705 -0
  901. mobile_agent/verify/fixtures.py +305 -0
  902. mobile_agent/verify/flow.py +213 -0
  903. mobile_agent/verify/l10n.py +141 -0
  904. mobile_agent/verify/netcerts.py +67 -0
  905. mobile_agent/verify/netproxy.py +356 -0
  906. mobile_agent/verify/netrun.py +81 -0
  907. mobile_agent/verify/netshim.m +28 -0
  908. mobile_agent/verify/netshim.py +48 -0
  909. mobile_agent/verify/netstubs.py +132 -0
  910. mobile_agent/verify/overlay.py +174 -0
  911. mobile_agent/verify/perf.py +239 -0
  912. mobile_agent/verify/proof.py +192 -0
  913. mobile_agent/verify/replay_admission.py +48 -0
  914. mobile_agent/verify/report.py +952 -0
  915. mobile_agent/verify/run_cleanup.py +67 -0
  916. mobile_agent/verify/run_links.py +48 -0
  917. mobile_agent/verify/run_perf.py +44 -0
  918. mobile_agent/verify/run_secrets.py +39 -0
  919. mobile_agent/verify/runner.py +976 -0
  920. mobile_agent/verify/runs.py +57 -0
  921. mobile_agent/verify/screen_coverage.py +181 -0
  922. mobile_agent/verify/smart.py +106 -0
  923. mobile_agent/verify/snapshot_run.py +114 -0
  924. mobile_agent/verify/snapshots.py +306 -0
  925. mobile_agent/verify/sound.py +335 -0
  926. mobile_agent/verify/source_observation.py +130 -0
  927. mobile_agent/verify/system_surfaces/__init__.py +7 -0
  928. mobile_agent/verify/system_surfaces/play.py +106 -0
  929. mobile_agent/verify/system_surfaces/spec.py +139 -0
  930. mobile_agent/verify/system_surfaces/stage.py +138 -0
  931. mobile_agent/verify/system_surfaces/surfaces.py +121 -0
  932. mobile_agent/verify/tree.py +223 -0
  933. mobile_agent/verify/verdict.py +377 -0
  934. mobile_agent/verify/wire.py +392 -0
  935. mobile_agent/verify/wirestore.py +176 -0
  936. mobile_agent/waiting/__init__.py +1 -0
  937. mobile_agent/waiting/contacts.py +73 -0
  938. mobile_agent/waiting/drafts.py +136 -0
  939. mobile_agent/waiting/job.py +145 -0
  940. mobile_agent/waiting/messages.py +174 -0
  941. mobile_agent/waiting/rank.py +115 -0
  942. mobile_agent/waiting/register.py +26 -0
  943. mobile_agent/watch.py +502 -0
  944. mobile_agent/wda/__init__.py +2 -0
  945. mobile_agent/wda/frame_clock.py +646 -0
  946. mobile_agent/wda/frame_source.py +325 -0
  947. mobile_agent/wda/frame_store.py +115 -0
  948. mobile_agent/wda/gate.py +163 -0
  949. mobile_agent/wda/gestures.py +291 -0
  950. mobile_agent/wda/guards.py +94 -0
  951. mobile_agent/wda/identity.py +55 -0
  952. mobile_agent/wda/observe.py +884 -0
  953. mobile_agent/wda/patches.py +856 -0
  954. mobile_agent/wda/point_gestures.py +150 -0
  955. mobile_agent/wda/source.py +32 -0
  956. mobile_agent/wda/system_dialogs.py +571 -0
  957. mobile_agent/wda/text.py +694 -0
  958. mobile_agent/wda/timeouts.py +77 -0
  959. mobile_agent/wda/video.py +505 -0
  960. mobile_agent/wda/watchdog.py +243 -0
  961. mobile_agent/wireless/__init__.py +15 -0
  962. mobile_agent/wireless/api.py +354 -0
  963. mobile_agent/wireless/cli.py +287 -0
  964. mobile_agent/wireless/devicectl.py +229 -0
  965. mobile_agent/wireless/exposure.py +33 -0
  966. mobile_agent/wireless/mobster-wifi-pair.c +122 -0
  967. mobile_agent/wireless/monitor.py +332 -0
  968. mobile_agent/wireless/network.py +334 -0
  969. mobile_agent/wireless/onboarding.py +293 -0
  970. mobile_agent/wireless/pair.py +124 -0
  971. mobile_agent/wireless/probe.py +322 -0
  972. mobile_agent/wireless/register.py +8 -0
  973. mobile_agent/wireless/relay.py +179 -0
  974. mobile_agent/wireless/result.py +87 -0
  975. mobile_agent/wireless/store.py +85 -0
  976. mobile_agent/wireless/switch.py +77 -0
  977. mobile_agent/wireless/transport.py +118 -0
  978. mobile_agent/wireless/words.py +81 -0
  979. mobile_agent/wireless/xctestrun.py +83 -0
  980. mobile_agent/workflow_tools.py +59 -0
  981. mobile_agent/workflows/__init__.py +2 -0
  982. mobile_agent/workflows/brief.py +201 -0
  983. mobile_agent/workflows/cron.py +110 -0
  984. mobile_agent/workflows/health.py +257 -0
  985. mobile_agent/workflows/history.py +126 -0
  986. mobile_agent/workflows/records.py +442 -0
  987. mobile_agent/workflows/runs.py +1000 -0
  988. mobile_agent/workflows/scheduler.py +134 -0
  989. mobile_agent/workflows/service.py +973 -0
  990. mobile_agent/workflows/starters.py +285 -0
  991. mobile_agent/workflows/triggers.py +248 -0
  992. mobile_agent/xray/__init__.py +1 -0
  993. mobile_agent/xray/app_cli.py +116 -0
  994. mobile_agent/xray/cli.py +21 -0
  995. mobile_agent/xray/crawl.py +754 -0
  996. mobile_agent/xray/diff.py +171 -0
  997. mobile_agent/xray/diff_cli.py +43 -0
  998. mobile_agent/xray/job.py +184 -0
  999. mobile_agent/xray/paywall.py +188 -0
  1000. mobile_agent/xray/policy.py +305 -0
  1001. mobile_agent/xray/register.py +43 -0
  1002. mobile_agent/xray/report.py +283 -0
  1003. mobile_agent/xray/screens.py +117 -0
  1004. mobster_cli-0.4.3.dist-info/METADATA +93 -0
  1005. mobster_cli-0.4.3.dist-info/RECORD +1009 -0
  1006. mobster_cli-0.4.3.dist-info/WHEEL +5 -0
  1007. mobster_cli-0.4.3.dist-info/entry_points.txt +3 -0
  1008. mobster_cli-0.4.3.dist-info/licenses/LICENSE +21 -0
  1009. 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")