2222
2323_LANE_ENV_KEYS = ("KORU_AUTOPILOT_IDE" , "KORU_AUTOPILOT_INSTANCE" , "KORU_AUTOPILOT_SOCKET" )
2424_ORIGINAL_SUBPROCESS_RUN = subprocess .run
25+ _LANE_ENV_PAYLOAD_TIMEOUT_S = float (os .environ .get ("CORU_LANE_ENV_PAYLOAD_TIMEOUT_S" , "5" ))
26+ _KORU_SUBPROCESS_TIMEOUT_S = float (os .environ .get ("CORU_KORU_SUBPROCESS_TIMEOUT_S" , "20" ))
27+
28+
29+ def _koru_subprocess_timeout (koru_args : Sequence [str ]) -> float | None :
30+ """Return timeout for koru subprocess calls; ``None`` for long-running commands."""
31+ if not koru_args :
32+ return _KORU_SUBPROCESS_TIMEOUT_S
33+ head = str (koru_args [0 ]).lower ()
34+ if head in {"auto" , "autonomous" , "serve" }:
35+ return None
36+ if head == "autopilot" and len (koru_args ) > 1 and str (koru_args [1 ]).lower () == "daemon" :
37+ return None
38+ return _KORU_SUBPROCESS_TIMEOUT_S
2539
2640
2741
@@ -414,9 +428,25 @@ def _extract_global_flags(argv: Sequence[str]) -> tuple[list[str], bool, bool, s
414428 return rest , verbose , show_version , log_format , require_plugin
415429
416430
417- def _run (command : Sequence [str ], * , passthrough : bool = True ) -> int :
431+ def _run (
432+ command : Sequence [str ],
433+ * ,
434+ passthrough : bool = True ,
435+ timeout : float | None = None ,
436+ ) -> int :
418437 try :
419- proc = subprocess .run (list (command ), check = False )
438+ kwargs : dict [str , Any ] = {"check" : False }
439+ if timeout is not None :
440+ kwargs ["timeout" ] = timeout
441+ proc = subprocess .run (list (command ), ** kwargs )
442+ except subprocess .TimeoutExpired :
443+ preview = " " .join (str (part ) for part in command [:4 ])
444+ limit = timeout if timeout is not None else 0.0
445+ print (
446+ f"[coru] command timed out after { limit :.0f} s: { preview } " ,
447+ file = sys .stderr ,
448+ )
449+ return 124
420450 except KeyboardInterrupt :
421451 return 130
422452 if passthrough :
@@ -1224,13 +1254,19 @@ def _bind_lane_session(ide: str, instance: str):
12241254 os .environ .pop (key , None )
12251255
12261256
1227- def _run_with_lane_environment (command : Sequence [str ], * , ide : str , instance : str ) -> int :
1257+ def _run_with_lane_environment (
1258+ command : Sequence [str ],
1259+ * ,
1260+ ide : str ,
1261+ instance : str ,
1262+ timeout : float | None = _KORU_SUBPROCESS_TIMEOUT_S ,
1263+ ) -> int :
12281264 previous = {key : os .environ [key ] for key in _LANE_ENV_KEYS if key in os .environ }
12291265 try :
12301266 os .environ ["KORU_AUTOPILOT_IDE" ] = ide
12311267 os .environ ["KORU_AUTOPILOT_INSTANCE" ] = instance
12321268 os .environ .pop ("KORU_AUTOPILOT_SOCKET" , None )
1233- return _run (command )
1269+ return _run (command , timeout = timeout )
12341270 finally :
12351271 for key in _LANE_ENV_KEYS :
12361272 if key in previous :
@@ -1268,9 +1304,16 @@ def _koru_autopilot_env_payload(ide: str, instance: str) -> dict[str, Any] | Non
12681304 text = True ,
12691305 check = False ,
12701306 env = _lane_subprocess_env (ide , instance ),
1271- timeout = 5.0 ,
1307+ timeout = _LANE_ENV_PAYLOAD_TIMEOUT_S ,
12721308 close_fds = True ,
12731309 )
1310+ except subprocess .TimeoutExpired :
1311+ print (
1312+ f"[coru] koru autopilot env timed out after { _LANE_ENV_PAYLOAD_TIMEOUT_S :.0f} s "
1313+ f"(ide={ ide } instance={ instance } )" ,
1314+ file = sys .stderr ,
1315+ )
1316+ return None
12741317 except Exception :
12751318 return None
12761319 if proc .returncode != 0 :
@@ -1284,10 +1327,18 @@ def _koru_autopilot_env_payload(ide: str, instance: str) -> dict[str, Any] | Non
12841327 return payload
12851328
12861329
1287- def _run_with_resolved_lane_env (command : Sequence [str ], * , ide : str , instance : str ) -> int :
1330+ def _run_with_resolved_lane_env (
1331+ command : Sequence [str ],
1332+ * ,
1333+ ide : str ,
1334+ instance : str ,
1335+ timeout : float | None = _KORU_SUBPROCESS_TIMEOUT_S ,
1336+ ) -> int :
12881337 payload = _koru_autopilot_env_payload (ide , instance )
12891338 if not payload :
1290- return _run_with_lane_environment (command , ide = ide , instance = instance )
1339+ if timeout is None :
1340+ return _run (list (command ))
1341+ return _run_with_lane_environment (command , ide = ide , instance = instance , timeout = timeout )
12911342
12921343 lane_keys = ("KORU_AUTOPILOT_IDE" , "KORU_AUTOPILOT_INSTANCE" , "KORU_AUTOPILOT_SOCKET" )
12931344 previous = {key : os .environ [key ] for key in lane_keys if key in os .environ }
@@ -1299,7 +1350,7 @@ def _run_with_resolved_lane_env(command: Sequence[str], *, ide: str, instance: s
12991350 os .environ ["KORU_AUTOPILOT_INSTANCE" ] = str (payload ["instance" ])
13001351 if payload .get ("ide" ):
13011352 os .environ ["KORU_AUTOPILOT_IDE" ] = str (payload ["ide" ])
1302- return _run (command )
1353+ return _run (command , timeout = timeout )
13031354 finally :
13041355 for key in lane_keys :
13051356 if key in previous :
@@ -1313,7 +1364,12 @@ def _run_koru_lane(ide: str, instance: str, koru_args: Sequence[str]) -> int:
13131364 if koru_exec is None :
13141365 print ("error: koru is not available; run 'coru ensure --install'" , file = sys .stderr )
13151366 return 127
1316- return _run_with_resolved_lane_env ([* koru_exec , * koru_args ], ide = ide , instance = instance )
1367+ return _run_with_resolved_lane_env (
1368+ [* koru_exec , * koru_args ],
1369+ ide = ide ,
1370+ instance = instance ,
1371+ timeout = _koru_subprocess_timeout (koru_args ),
1372+ )
13171373
13181374
13191375def _koruenv_run_fallback (ide : str , instance : str , run_payload : Sequence [str ]) -> int :
@@ -1384,7 +1440,9 @@ def _lane_status_payload(
13841440 if resolved .get ("socket" ):
13851441 env ["KORU_AUTOPILOT_SOCKET" ] = str (resolved ["socket" ])
13861442 try :
1387- proc = subprocess .run (cmd , stdout = subprocess .PIPE , stderr = subprocess .DEVNULL , text = True , check = False , env = env , timeout = 5.0 , close_fds = True )
1443+ proc = subprocess .run (cmd , stdout = subprocess .PIPE , stderr = subprocess .DEVNULL , text = True , check = False , env = env , timeout = _LANE_ENV_PAYLOAD_TIMEOUT_S , close_fds = True )
1444+ except subprocess .TimeoutExpired :
1445+ return None
13881446 except Exception :
13891447 return None
13901448 if proc .returncode != 0 :
@@ -1406,7 +1464,9 @@ def _fetch_manage_report(ide: str, instance: str) -> dict[str, Any] | None:
14061464 cmd = [* koru_exec , "autopilot" , "manage" , "--ide" , ide , "--format" , "json" ]
14071465 env = _lane_subprocess_env (ide , instance )
14081466 try :
1409- proc = subprocess .run (cmd , stdout = subprocess .PIPE , stderr = subprocess .DEVNULL , text = True , check = False , env = env , timeout = 5.0 , close_fds = True )
1467+ proc = subprocess .run (cmd , stdout = subprocess .PIPE , stderr = subprocess .DEVNULL , text = True , check = False , env = env , timeout = _LANE_ENV_PAYLOAD_TIMEOUT_S , close_fds = True )
1468+ except subprocess .TimeoutExpired :
1469+ return None
14101470 except Exception :
14111471 return None
14121472 if proc .returncode != 0 and not proc .stdout .strip ():
@@ -2660,7 +2720,14 @@ def _lane_manage_fix(ide: str, instance: str) -> int:
26602720 if koru_exec is None :
26612721 print ("error: koru is not available; run 'coru ensure --install'" , file = sys .stderr )
26622722 return 127
2663- return _run_koru_lane (ide , instance , ["autopilot" , "manage" , "--ide" , ide , "--fix" ])
2723+ rc = _run_koru_lane (ide , instance , ["autopilot" , "manage" , "--ide" , ide , "--fix" ])
2724+ if rc != 0 :
2725+ _run_koru_lane (
2726+ ide ,
2727+ instance ,
2728+ ["ide" , "doctor" , "--ide" , ide , "--fix" , "--gc-sockets" ],
2729+ )
2730+ return rc
26642731
26652732
26662733def _lane_daemon_foreground (ide : str , instance : str ) -> int :
0 commit comments