@@ -191,10 +191,10 @@ def _send_step_webhook(
191191 # pushed to the existing PR. The first hook fire (right after the agent
192192 # finishes) has unsynced changes; _maybe_continue_pipeline triggers the
193193 # push, which re-fires this hook once the PR is updated. Only emit the
194- # completion webhook on that synced pass so the payload reflects the
195- # updated PR and we don't double-fire .
194+ # completion webhook once changes are synced, or when push failed
195+ # terminally for errored repos so we don't wait forever .
196196 _ , is_synced = state .has_code_changes ()
197- if not is_synced :
197+ if not is_synced and not cls . _iteration_terminal_errored_repos ( state ) :
198198 return
199199 webhook_action_type = SeerActionType .ITERATION_COMPLETED
200200 iteration_index = get_latest_iteration_index (state )
@@ -512,6 +512,28 @@ def run_introspection(
512512 elif step == AutofixStep .PR_ITERATION :
513513 return introspect_iteration (organization , run_id , state , group )
514514
515+ @classmethod
516+ def _iteration_terminal_errored_repos (cls , state : SeerRunState ) -> list [str ]:
517+ """
518+ Return the errored repos when unsynced repos have terminal push failures.
519+
520+ Returns an empty list when there are no errored repos, or when some
521+ non-errored repo is still unsynced (i.e. a push can still make progress).
522+ Used to stop waiting for a synced PR after push errors without retrying.
523+ """
524+ diffs_by_repo = state .get_diffs_by_repo ()
525+ errored_repos = [
526+ repo
527+ for repo in diffs_by_repo
528+ if (pr_state := state .repo_pr_states .get (repo )) is not None
529+ and pr_state .pr_creation_status == "error"
530+ ]
531+ if not errored_repos :
532+ return []
533+ if all (state ._is_repo_synced (repo ) or repo in errored_repos for repo in diffs_by_repo ):
534+ return errored_repos
535+ return []
536+
515537 @classmethod
516538 def _push_changes (cls , group : Group , run_id : int , state : SeerRunState ) -> None :
517539 """Push code changes to create PRs."""
@@ -530,16 +552,8 @@ def _push_changes(cls, group: Group, run_id: int, state: SeerRunState) -> None:
530552 return
531553
532554 # Errored repos are terminal — re-pushing would re-fire this hook in a loop.
533- diffs_by_repo = state .get_diffs_by_repo ()
534- errored_repos = [
535- repo
536- for repo in diffs_by_repo
537- if (pr_state := state .repo_pr_states .get (repo )) is not None
538- and pr_state .pr_creation_status == "error"
539- ]
540- if errored_repos and all (
541- state ._is_repo_synced (repo ) or repo in errored_repos for repo in diffs_by_repo
542- ):
555+ errored_repos = cls ._iteration_terminal_errored_repos (state )
556+ if errored_repos :
543557 logger .info (
544558 "autofix.on_completion_hook.skip_no_pushable_repos" ,
545559 extra = {
0 commit comments