@@ -348,13 +348,26 @@ def run_tb2(
348348 task_prompt = task_prompt ,
349349 model = persona_result .get ("model" , "sonnet" ),
350350 allowed_tools = allowed_tools ,
351+ timeout_seconds = persona_result .get ("timeout_seconds" , 300 ),
351352 )
352353
353354 agent_exit = agent_result .get ("exit_code" , - 1 )
354355 agent_span .set_attribute ("tb2.agent_exit_code" , agent_exit )
355356 attempt_span_ids .append (_span_id_hex (agent_span ))
356357
357- if agent_exit != 0 :
358+ # If the agent timed out, feed the timeout into the retry
359+ # loop instead of returning early. This lets TB-2 retry
360+ # after a slow spawn (the 303s timeout seen in stress tests).
361+ initial_timed_out = agent_result .get ("timed_out" , False )
362+ if initial_timed_out :
363+ duration = agent_result .get ("duration_seconds" , 0 )
364+ agent_span .set_attribute ("tb2.agent_timed_out" , True )
365+ agent_span .set_attribute ("tb2.agent_duration" , duration )
366+ logger .warning (
367+ "TB-2: Initial agent timed out after %.0fs — entering retry loop" ,
368+ duration ,
369+ )
370+ elif agent_exit != 0 :
358371 elapsed = time .monotonic () - pipeline_start
359372 return TB2Result (
360373 issue_id = issue_id ,
@@ -372,59 +385,95 @@ def run_tb2(
372385 ).model_dump ()
373386
374387 # ----------------------------------------------------------
375- # Phase 8: Run quality gates (or force-fail on first attempt )
388+ # Phase 8: Run quality gates (or force-fail, or skip if timed out )
376389 # ----------------------------------------------------------
377- with tracer_tb2 .start_as_current_span (
378- "tb2.phase.gates" ,
379- attributes = {
380- "tb2.phase" : "gates" ,
381- "tb2.attempt" : 0 ,
382- "tb2.force_fail" : force_gate_fail ,
383- },
384- ) as gates_span :
385- if force_gate_fail :
386- logger .info ("TB-2: FORCED FAILURE on initial gate run" )
387- gate_raw = _make_forced_failure ()
388- else :
389- gate_raw = run_all_gates (
390- worktree_path = worktree_path ,
391- issue_title = issue_title ,
392- issue_description = issue_description ,
393- )
394-
395- try :
396- gate_suite = GateSuiteResult (** gate_raw )
397- except Exception as exc :
398- elapsed = time .monotonic () - pipeline_start
399- error_msg = f"Malformed gate result: { exc } "
400- return TB2Result (
401- issue_id = issue_id ,
402- repo_path = repo_path ,
403- success = False ,
404- phase = "gates" ,
405- worktree_path = worktree_path ,
406- persona = persona_name ,
407- error = error_msg ,
408- duration_seconds = round (elapsed , 2 ),
409- trace_id = root_trace_id ,
410- attempt_span_ids = attempt_span_ids ,
411- force_gate_fail_used = force_gate_fail ,
412- ).model_dump ()
413-
414- gates_span .set_attribute ("tb2.gates_passed" , gate_suite .overall_passed )
415- if gate_suite .first_failure :
416- gates_span .set_attribute ("tb2.first_failure" , gate_suite .first_failure )
417-
418- # Record initial attempt
390+ gate_suite : GateSuiteResult | None = None
391+ gate_raw : dict | None = None
392+ gates_span_context = None
393+
394+ if initial_timed_out :
395+ # Agent timed out — no output to gate. Seed a timeout
396+ # failure record and jump straight to the retry loop.
397+ duration = agent_result .get ("duration_seconds" , 0 )
398+ gate_raw = GateSuiteResult (
399+ overall_passed = False ,
400+ first_failure = "agent_timeout" ,
401+ gate_results = [
402+ GateResult (
403+ gate_name = "agent_timeout" ,
404+ passed = False ,
405+ findings = [
406+ Finding (
407+ severity = "critical" ,
408+ message = f"Agent timed out after { duration :.0f} s (no output to gate)" ,
409+ ),
410+ ],
411+ ),
412+ ],
413+ ).model_dump ()
414+ gate_suite = GateSuiteResult (** gate_raw )
419415 retry_history .append (
420416 RetryAttempt (
421417 attempt = 0 ,
422418 agent_exit_code = agent_exit ,
423- gates_passed = gate_suite . overall_passed ,
424- first_failure = gate_suite . first_failure ,
419+ gates_passed = False ,
420+ first_failure = "agent_timeout" ,
425421 span_id = attempt_span_ids [0 ] if attempt_span_ids else None ,
426422 )
427423 )
424+ else :
425+ with tracer_tb2 .start_as_current_span (
426+ "tb2.phase.gates" ,
427+ attributes = {
428+ "tb2.phase" : "gates" ,
429+ "tb2.attempt" : 0 ,
430+ "tb2.force_fail" : force_gate_fail ,
431+ },
432+ ) as gates_span :
433+ if force_gate_fail :
434+ logger .info ("TB-2: FORCED FAILURE on initial gate run" )
435+ gate_raw = _make_forced_failure ()
436+ else :
437+ gate_raw = run_all_gates (
438+ worktree_path = worktree_path ,
439+ issue_title = issue_title ,
440+ issue_description = issue_description ,
441+ )
442+
443+ try :
444+ gate_suite = GateSuiteResult (** gate_raw )
445+ except Exception as exc :
446+ elapsed = time .monotonic () - pipeline_start
447+ error_msg = f"Malformed gate result: { exc } "
448+ return TB2Result (
449+ issue_id = issue_id ,
450+ repo_path = repo_path ,
451+ success = False ,
452+ phase = "gates" ,
453+ worktree_path = worktree_path ,
454+ persona = persona_name ,
455+ error = error_msg ,
456+ duration_seconds = round (elapsed , 2 ),
457+ trace_id = root_trace_id ,
458+ attempt_span_ids = attempt_span_ids ,
459+ force_gate_fail_used = force_gate_fail ,
460+ ).model_dump ()
461+
462+ gates_span .set_attribute ("tb2.gates_passed" , gate_suite .overall_passed )
463+ if gate_suite .first_failure :
464+ gates_span .set_attribute ("tb2.first_failure" , gate_suite .first_failure )
465+ gates_span_context = gates_span .get_span_context ()
466+
467+ # Record initial attempt
468+ retry_history .append (
469+ RetryAttempt (
470+ attempt = 0 ,
471+ agent_exit_code = agent_exit ,
472+ gates_passed = gate_suite .overall_passed ,
473+ first_failure = gate_suite .first_failure ,
474+ span_id = attempt_span_ids [0 ] if attempt_span_ids else None ,
475+ )
476+ )
428477
429478 # ----------------------------------------------------------
430479 # Phase 9: Gates passed on first try -> success
@@ -485,7 +534,7 @@ def run_tb2(
485534 # Phase 10: Gates failed -> retry loop with span linking
486535 # ----------------------------------------------------------
487536 all_gate_failures : list [dict ] = [gate_raw ]
488- previous_span_context = gates_span . get_span_context ()
537+ previous_span_context = gates_span_context
489538
490539 for attempt in range (1 , max_retries + 1 ):
491540 retries_used = attempt
@@ -526,6 +575,7 @@ def run_tb2(
526575 gate_failures = all_gate_failures ,
527576 attempt = attempt ,
528577 max_retries = max_retries ,
578+ timeout_seconds = persona_result .get ("timeout_seconds" , 300 ),
529579 )
530580
531581 retry_success = retry_raw .get ("success" , False )
0 commit comments