@@ -1310,6 +1310,7 @@ async def eventually(
13101310 min_confidence : float | None = None ,
13111311 max_snapshot_attempts : int = 3 ,
13121312 snapshot_kwargs : dict [str , Any ] | None = None ,
1313+ snapshot_limit_growth : dict [str , Any ] | None = None ,
13131314 vision_provider : Any | None = None ,
13141315 vision_system_prompt : str | None = None ,
13151316 vision_user_prompt : str | None = None ,
@@ -1325,9 +1326,103 @@ async def eventually(
13251326 snapshot_attempt = 0
13261327 last_outcome = None
13271328
1329+ # Optional: increase SnapshotOptions.limit across retries to widen element coverage.
1330+ #
1331+ # This is useful on long / virtualized pages where an initial small limit may miss
1332+ # a target element, but taking a "bigger" snapshot is enough to make a deterministic
1333+ # predicate pass.
1334+ #
1335+ # Additive schedule (requested):
1336+ # limit(attempt) = min(max_limit, start_limit + step*(attempt-1))
1337+ #
1338+ # Notes:
1339+ # - We clamp to SnapshotOptions Field constraints (1..500).
1340+ # - If both snapshot_kwargs["limit"] and snapshot_limit_growth are provided,
1341+ # snapshot_limit_growth controls the per-attempt limit (callers can set
1342+ # start_limit explicitly if desired).
1343+ growth = snapshot_limit_growth or None
1344+ growth_apply_on = "only_on_fail"
1345+ growth_start : int | None = None
1346+ growth_step : int | None = None
1347+ growth_max : int | None = None
1348+ if isinstance (growth , dict ) and growth :
1349+ try :
1350+ growth_apply_on = str (growth .get ("apply_on" ) or "only_on_fail" )
1351+ except Exception :
1352+ growth_apply_on = "only_on_fail"
1353+ try :
1354+ v = growth .get ("start_limit" , None )
1355+ growth_start = int (v ) if v is not None else None
1356+ except Exception :
1357+ growth_start = None
1358+ try :
1359+ v = growth .get ("step" , None )
1360+ growth_step = int (v ) if v is not None else None
1361+ except Exception :
1362+ growth_step = None
1363+ try :
1364+ v = growth .get ("max_limit" , None )
1365+ growth_max = int (v ) if v is not None else None
1366+ except Exception :
1367+ growth_max = None
1368+
1369+ # Resolve defaults from runtime + snapshot_kwargs.
1370+ if growth and growth_start is None :
1371+ try :
1372+ if snapshot_kwargs and snapshot_kwargs .get ("limit" ) is not None :
1373+ growth_start = int (snapshot_kwargs ["limit" ])
1374+ except Exception :
1375+ growth_start = None
1376+ if growth and growth_start is None :
1377+ try :
1378+ growth_start = int (getattr (self .runtime , "_snapshot_options" , None ).limit ) # type: ignore[attr-defined]
1379+ except Exception :
1380+ growth_start = None
1381+ if growth and growth_start is None :
1382+ growth_start = 50 # SnapshotOptions default
1383+
1384+ if growth and growth_step is None :
1385+ growth_step = max (1 , int (growth_start ))
1386+ if growth and growth_max is None :
1387+ growth_max = 500
1388+
1389+ def _clamp_limit (n : int ) -> int :
1390+ if n < 1 :
1391+ return 1
1392+ if n > 500 :
1393+ return 500
1394+ return n
1395+
1396+ def _limit_for_attempt (attempt_idx_1based : int ) -> int :
1397+ assert growth_start is not None and growth_step is not None and growth_max is not None
1398+ base = int (growth_start ) + int (growth_step ) * max (0 , int (attempt_idx_1based ) - 1 )
1399+ return _clamp_limit (min (int (growth_max ), base ))
1400+
13281401 while True :
13291402 attempt += 1
1330- await self .runtime .snapshot (** (snapshot_kwargs or {}))
1403+
1404+ per_attempt_kwargs = dict (snapshot_kwargs or {})
1405+ snapshot_limit : int | None = None
1406+ if growth :
1407+ # Only grow if requested; otherwise fixed start_limit.
1408+ apply = growth_apply_on == "all"
1409+ if growth_apply_on == "only_on_fail" :
1410+ # attempt==1 always uses the start_limit; attempt>1 grows (since we'd have
1411+ # returned already if the previous attempt passed).
1412+ apply = attempt == 1 or (last_outcome is not None and not bool (last_outcome .passed ))
1413+ if apply :
1414+ snapshot_limit = _limit_for_attempt (attempt )
1415+ else :
1416+ snapshot_limit = _clamp_limit (int (growth_start or 50 ))
1417+ per_attempt_kwargs ["limit" ] = snapshot_limit
1418+ else :
1419+ try :
1420+ if per_attempt_kwargs .get ("limit" ) is not None :
1421+ snapshot_limit = int (per_attempt_kwargs ["limit" ])
1422+ except Exception :
1423+ snapshot_limit = None
1424+
1425+ await self .runtime .snapshot (** per_attempt_kwargs )
13311426 snapshot_attempt += 1
13321427
13331428 # Optional: gate predicate evaluation on snapshot confidence.
@@ -1372,6 +1467,7 @@ async def eventually(
13721467 "eventually" : True ,
13731468 "attempt" : attempt ,
13741469 "snapshot_attempt" : snapshot_attempt ,
1470+ "snapshot_limit" : snapshot_limit ,
13751471 },
13761472 )
13771473
@@ -1481,6 +1577,7 @@ async def eventually(
14811577 "eventually" : True ,
14821578 "attempt" : attempt ,
14831579 "snapshot_attempt" : snapshot_attempt ,
1580+ "snapshot_limit" : snapshot_limit ,
14841581 "final" : True ,
14851582 "timeout" : True ,
14861583 },
@@ -1503,7 +1600,12 @@ async def eventually(
15031600 required = self .required ,
15041601 kind = "assert" ,
15051602 record_in_step = False ,
1506- extra = {"eventually" : True , "attempt" : attempt },
1603+ extra = {
1604+ "eventually" : True ,
1605+ "attempt" : attempt ,
1606+ "snapshot_attempt" : snapshot_attempt ,
1607+ "snapshot_limit" : snapshot_limit ,
1608+ },
15071609 )
15081610
15091611 if last_outcome .passed :
0 commit comments