"""Multi-step Avatar plan executor (Phase C)."""

from __future__ import annotations

import logging
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FuturesTimeoutError
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Callable

logger = logging.getLogger(__name__)

from iuve_connect_agents.desktop.active.audit_logger import CommandAuditLogger
from iuve_connect_agents.desktop.active.evidence_collector import EvidenceCollector
from iuve_connect_agents.desktop.active.avatar_local_executor import AvatarExecutionOutcome, AvatarLocalExecutor
from iuve_connect_agents.desktop.active.execution_plan import ExecutionPlan, ExecutionPlanStep
from iuve_connect_agents.desktop.active.plan_step_context import PlanStepContext
from iuve_connect_agents.desktop.active.plan_execution_control import DEFAULT_PLAN_STEP_MAX_SEC
from iuve_connect_agents.desktop.active.double_confirm_flow import wrap_confirmation_callback
from iuve_connect_agents.desktop.active.vision_recovery import (
    VISION_RECOVERY_MAX_ATTEMPTS,
    VisionRecoveryCallback,
    VisionRecoveryResult,
    build_recovery_capture_payload,
    build_recovery_verify_payload,
    recovery_capture_step_index,
    recovery_verify_step_index,
    resolve_fallback_capture_evidence,
    vision_recovery_enabled,
    vision_recovery_max_attempts_for_plan,
)
from iuve_connect_agents.shared.feature_flags import (
    autonomous_replan_enabled,
    avatar_desktop_vision_enabled,
    avatar_evidence_collector_enabled,
    avatar_plan_executor_enabled,
    avatar_plan_max_steps,
    avatar_plan_stop_on_failure,
    confidence_execution_enabled,
    replanning_enabled,
)


ConfirmationCallback = Callable[..., Any]


PLAN_STEP_DEFAULTS: dict[str, tuple[str, str, dict[str, Any]]] = {
    "browser_open_url": ("agent.browser.open_url", "medium", {}),
    "open_url": ("agent.browser.open_url", "medium", {}),
    "web_fetch_bounded": ("agent.network.fetch", "medium", {}),
    "summarize_content": ("agent.content.summarize", "medium", {}),
    "generate_content": ("agent.content.summarize", "medium", {}),
    "insert_exact_text": ("agent.content.summarize", "low", {}),
    "verify_workspace_file": ("agent.workspace.file.read", "low", {"needs_workspace": True}),
    "open_document_edit": ("agent.desktop.app", "medium", {"needs_workspace": True}),
    "write_file_in_workspace": ("agent.workspace.file.write", "medium", {"needs_workspace": True}),
    "read_file_in_workspace": ("agent.workspace.file.read", "medium", {"needs_workspace": True}),
    "open_folder": ("agent.workspace.file.read", "low", {"needs_workspace": True}),
    "mass_mail_send": ("agent.desktop.app", "high", {}),
    "public_accusation_post": ("agent.browser.open_url", "high", {}),
    "social_post_draft": ("agent.desktop.app", "medium", {}),
    "email_compose": ("agent.desktop.app", "medium", {}),
    "social_post_publish": ("agent.browser.open_url", "high", {}),
    "browser_click_publish": ("agent.browser.open_url", "high", {}),
    "email_send": ("agent.desktop.app", "high", {}),
    "get_diagnostics": ("agent.diagnostics.read", "low", {}),
    "empty_trash": ("agent.workspace.file.write", "medium", {}),
    "application_launch": ("agent.desktop.app", "medium", {}),
    "keyboard_type_text": ("agent.desktop.input", "high", {}),
    "capture_screenshot": ("agent.screen.capture", "medium", {}),
    "verify_screen_contains_text": ("agent.screen.verify", "low", {}),
    "verify_web_draft": ("agent.screen.verify", "low", {}),
    "verify_published": ("agent.screen.verify", "low", {}),
    "open_web_composer": ("agent.screen.verify", "medium", {}),
    "describe_active_window": ("agent.screen.verify", "low", {}),
    "list_files_in_folder": ("agent.workspace.file.read", "low", {}),
    "plan_step": ("agent.status.read", "low", {}),
    "apply_update": ("agent.update.apply", "high", {}),
}


_PUBLISH_COMMAND_TYPES = frozenset(
    {
        "social_post_publish",
        "browser_click_publish",
        "email_send",
        "mass_mail_send",
        "public_accusation_post",
    }
)


def _confidence_context_from_plan(plan: ExecutionPlan | dict[str, Any]) -> dict[str, Any]:
    trace: dict[str, Any] = {}
    if isinstance(plan, ExecutionPlan):
        trace = dict(plan.planner_trace or {})
    elif isinstance(plan, dict):
        trace = dict(plan.get("planner_trace") or {})
    contract = trace.get("orchestra_execution_contract")
    if not isinstance(contract, dict):
        contract = {}
    policy = trace.get("orchestra_policy") if isinstance(trace.get("orchestra_policy"), dict) else {}
    return {
        "execution_confidence": float(
            contract.get("execution_confidence")
            or trace.get("execution_confidence")
            or policy.get("execution_confidence")
            or 0.0
        ),
        "execution_posture": str(
            contract.get("execution_posture")
            or trace.get("execution_posture")
            or policy.get("execution_posture")
            or ""
        ).strip().upper(),
        "decision_mode": str(
            policy.get("decision_mode") or trace.get("decision_mode") or "balanced"
        ).strip().lower(),
        "min_confidence_for_autonomous": float(
            policy.get("min_confidence_for_autonomous")
            or trace.get("min_confidence_for_autonomous")
            or 0.75
        ),
        "draft_only": bool(contract.get("draft_only") or trace.get("confidence_draft_only")),
        "stop_zone_required": bool(
            contract.get("stop_zone_required") or trace.get("confidence_stop_zone_required")
        ),
    }


def _should_defer_step_for_confidence(
    *,
    command_type: str,
    confidence_ctx: dict[str, Any],
) -> bool:
    if not confidence_execution_enabled():
        return False
    posture = str(confidence_ctx.get("execution_posture") or "").upper()
    normalized = str(command_type or "").strip().lower()
    if posture == "LOW":
        return normalized in _PUBLISH_COMMAND_TYPES
    if posture == "MEDIUM" or confidence_ctx.get("draft_only"):
        return normalized in _PUBLISH_COMMAND_TYPES
    return False


def _should_auto_confirm_for_confidence(
    *,
    confidence_ctx: dict[str, Any],
    require_confirmation: bool,
) -> bool:
    if not confidence_execution_enabled() or not require_confirmation:
        return False
    posture = str(confidence_ctx.get("execution_posture") or "").upper()
    if posture in {"CRITICAL_RISK", "LOW", "MEDIUM"}:
        return False
    if confidence_ctx.get("stop_zone_required"):
        return False
    confidence = float(confidence_ctx.get("execution_confidence") or 0.0)
    min_conf = float(confidence_ctx.get("min_confidence_for_autonomous") or 0.75)
    return posture == "HIGH" and confidence >= min_conf


def default_workspace_scope() -> dict[str, Any]:
    home = str(Path.home())
    return {"workspace_paths": [home]}


def enrich_plan_step_payload(
    step: ExecutionPlanStep,
    *,
    plan: ExecutionPlan,
    context: PlanStepContext,
    workspace_scope: dict[str, Any] | None = None,
) -> tuple[dict[str, Any], dict[str, Any]]:
    defaults = PLAN_STEP_DEFAULTS.get(step.command_type, ("agent.status.read", "low", {}))
    required_capability, risk_level, hints = defaults

    payload = context.resolve_payload(dict(step.payload), command_type=step.command_type)

    if step.command_type == "keyboard_type_text":
        if not str(payload.get("text") or "").strip():
            for dep_id in step.depends_on or []:
                dep_text = context.get_text(dep_id)
                if dep_text:
                    payload["text"] = dep_text
                    payload["generated_text"] = dep_text
                    payload.setdefault("content", dep_text)
                    break
        for dep_id in step.depends_on or []:
            dep_evidence = context.get_evidence(dep_id)
            dep_type = str(dep_evidence.get("handler") or dep_evidence.get("command_type") or "").strip().lower()
            editor = str(dep_evidence.get("editor") or "").strip().lower()
            if dep_evidence.get("launch_pid") and not payload.get("launch_pid"):
                payload["launch_pid"] = dep_evidence["launch_pid"]
            if editor == "libreoffice" or dep_type == "open_document_edit":
                payload.setdefault("target_app", "libreoffice")
                payload.setdefault("window_name", "LibreOffice")
                payload.setdefault("focus_recovery", True)
                if not payload.get("delay_sec"):
                    payload["delay_sec"] = 6.0
        generated_text = str(payload.get("text") or payload.get("generated_text") or "").strip()
        logger.info(
            "keyboard_type_text_input generated_text_length=%s generated_text_preview=%r text_parameter_present=%s",
            len(generated_text),
            generated_text[:120],
            bool(generated_text),
        )
    payload.setdefault("required_capability", required_capability)
    payload.setdefault("capability_risk_level", risk_level)
    payload.setdefault("user_consent_state", "consented_via_local_avatar")
    payload.setdefault("source_agent", "iuve_avatar_plan_executor")
    payload.setdefault("model_source", "avatar_plan_executor_v0.1.24.0")
    payload.setdefault("plan_id", plan.plan_id)
    payload.setdefault(
        "reasoning_summary",
        f"Plan step {step.step_id}: {step.command_type} for {plan.goal_summary[:120]}",
    )
    payload.setdefault("intended_action", f"{step.command_type}:{step.step_id}")

    if risk_level in {"high", "critical"}:
        payload.setdefault("rollback_hint", "Review and undo the action if unintended")
        payload.setdefault(
            "rollback_instruction",
            "Follow platform-specific recall steps to reverse the action if needed",
        )

    scope: dict[str, Any] = {}
    if hints.get("needs_workspace"):
        scope = dict(workspace_scope or default_workspace_scope())

    if step.command_type == "capture_screenshot":
        payload.setdefault("plan_id", plan.plan_id)
        payload.setdefault("step_id", step.step_id)
        payload.setdefault("region", "full")
        payload.setdefault("delay_ms", 500)

    if step.command_type == "verify_screen_contains_text":
        payload.setdefault("plan_id", plan.plan_id)
        payload.setdefault("step_id", step.step_id)
        payload.setdefault("match_mode", "contains")
        payload.setdefault("case_sensitive", False)
        payload.setdefault("min_confidence", 0.7)
        payload.setdefault("capture_if_missing", True)
        payload.setdefault("screen_vision_granted", True)
        payload.setdefault("verify_mode", str(payload.get("verify_mode") or "contains"))
        depends_on_step = str(payload.get("depends_on_step") or "").strip()
        if not depends_on_step and step.depends_on:
            depends_on_step = step.depends_on[0]
            payload.setdefault("depends_on_step", depends_on_step)
        if depends_on_step:
            dep_evidence = context.get_evidence(depends_on_step)
            if dep_evidence.get("path"):
                payload.setdefault("screenshot_path", dep_evidence["path"])
                payload.setdefault("screenshot_sha256", dep_evidence.get("sha256", ""))

    if step.command_type == "verify_web_draft":
        payload.setdefault("plan_id", plan.plan_id)
        payload.setdefault("step_id", step.step_id)
        payload.setdefault("capture_if_missing", True)
        payload.setdefault("screen_vision_granted", True)
        payload.setdefault("enable_recovery", True)
        payload.setdefault("min_confidence", 0.55)
        payload.setdefault("delay_ms", 500)
        type_step_id = str(payload.get("type_step_id") or payload.get("depends_on_step") or "").strip()
        if not type_step_id and step.depends_on:
            type_step_id = step.depends_on[0]
            payload.setdefault("type_step_id", type_step_id)
            payload.setdefault("depends_on_step", type_step_id)
        text_step_id = str(payload.get("expected_text_from_step") or payload.get("content_from_step") or "").strip()
        if not text_step_id:
            for dep_id in step.depends_on or []:
                dep_evidence = context.get_evidence(dep_id)
                if str(dep_evidence.get("content") or dep_evidence.get("text") or "").strip():
                    text_step_id = dep_id
                    break
        if text_step_id and not str(payload.get("expected_text") or "").strip():
            dep_text = context.get_text(text_step_id) or str(
                context.get_evidence(text_step_id).get("content")
                or context.get_evidence(text_step_id).get("text")
                or ""
            ).strip()
            if dep_text:
                payload["expected_text"] = dep_text
                payload.setdefault("generated_text", dep_text)
        if type_step_id:
            type_evidence = context.get_evidence(type_step_id)
            type_payload = dict(payload.get("type_payload") or {})
            for key in (
                "target_app",
                "window_name",
                "delay_sec",
                "active_site",
                "source_request_text",
                "composer_mode",
                "require_draft_verify",
                "launch_pid",
            ):
                if key in type_evidence and key not in type_payload:
                    type_payload[key] = type_evidence[key]
            for key in (
                "target_app",
                "window_name",
                "delay_sec",
                "active_site",
                "source_request_text",
                "composer_mode",
                "require_draft_verify",
                "launch_pid",
            ):
                if key in payload and key not in type_payload:
                    type_payload[key] = payload[key]
            if str(payload.get("expected_text") or "").strip():
                type_payload.setdefault("text", payload["expected_text"])
                type_payload.setdefault("generated_text", payload["expected_text"])
            type_payload.setdefault("submit_after", False)
            payload["type_payload"] = type_payload

    if step.command_type == "open_web_composer":
        payload.setdefault("plan_id", plan.plan_id)
        payload.setdefault("step_id", step.step_id)
        payload.setdefault("target_app", "browser")
        payload.setdefault("screen_vision_granted", True)
        payload.setdefault("capture_if_missing", True)
        payload.setdefault("delay_ms", 500)

    if step.command_type == "describe_active_window":
        payload.setdefault("plan_id", plan.plan_id)
        payload.setdefault("step_id", step.step_id)
        payload.setdefault("capture_if_missing", True)
        payload.setdefault("screen_vision_granted", True)
        depends_on_step = str(payload.get("depends_on_step") or "").strip()
        if not depends_on_step and step.depends_on:
            depends_on_step = step.depends_on[0]
            payload.setdefault("depends_on_step", depends_on_step)
        if depends_on_step:
            dep_evidence = context.get_evidence(depends_on_step)
            if dep_evidence.get("path"):
                payload.setdefault("screenshot_path", dep_evidence["path"])
                payload.setdefault("screenshot_sha256", dep_evidence.get("sha256", ""))

    if step.command_type == "list_files_in_folder":
        scope = {}

    return payload, scope


@dataclass
class PlanStepOutcome:
    step_id: str
    command_type: str
    ok: bool
    status: str
    message: str = ""
    evidence: dict[str, Any] = field(default_factory=dict)


@dataclass
class PlanExecutionOutcome:
    ok: bool
    status: str
    message: str = ""
    plan_id: str = ""
    goal_summary: str = ""
    completed_steps: list[PlanStepOutcome] = field(default_factory=list)
    failed_step_id: str = ""
    replan_recommended: bool = False
    revised_plan: dict[str, Any] | None = None


StepProgressCallback = Callable[[PlanStepOutcome, int, int], None]
CancelCheck = Callable[[], bool]
ReplanCallback = Callable[[dict[str, Any]], dict[str, Any] | None]


class AvatarPlanExecutor:
    def __init__(
        self,
        *,
        local_executor: AvatarLocalExecutor,
        audit_logger: CommandAuditLogger,
        confirmation_callback: ConfirmationCallback | None = None,
        vision_recovery_callback: VisionRecoveryCallback | None = None,
        replan_callback: ReplanCallback | None = None,
    ) -> None:
        self._local_executor = local_executor
        self._audit_logger = audit_logger
        self._confirmation_callback = confirmation_callback
        self._vision_recovery_callback = vision_recovery_callback
        self._replan_callback = replan_callback

    def _finalize_outcome(
        self,
        plan: ExecutionPlan | dict[str, Any],
        outcome: PlanExecutionOutcome | None = None,
        **kwargs: Any,
    ) -> PlanExecutionOutcome:
        if outcome is None:
            outcome = PlanExecutionOutcome(**kwargs)
        try:
            from .outcome_recording import try_record_plan_outcome

            try_record_plan_outcome(plan=plan, outcome=outcome)
        except Exception:
            pass
        try:
            from .runtime_context import finalize_plan_execution

            plan_id = ""
            goal_summary = ""
            if isinstance(plan, ExecutionPlan):
                plan_id = plan.plan_id
                goal_summary = plan.goal_summary
            elif isinstance(plan, dict):
                plan_id = str(plan.get("plan_id") or "")
                goal_summary = str(plan.get("goal_summary") or "")
            status = "completed" if outcome.ok else "failed"
            if str(outcome.status or "").strip().lower() in {"plan_cancelled", "step_declined"}:
                status = "cancelled"
            finalize_plan_execution(
                status=status,
                plan_id=plan_id,
                message=goal_summary,
            )
        except Exception:
            pass
        return outcome

    def execute_plan(
        self,
        plan: ExecutionPlan | dict[str, Any],
        *,
        workspace_scope: dict[str, Any] | None = None,
        step_callback: StepProgressCallback | None = None,
        start_step_index: int = 0,
        cancel_check: CancelCheck | None = None,
        step_timeout_sec: float = DEFAULT_PLAN_STEP_MAX_SEC,
    ) -> PlanExecutionOutcome:
        raw_plan = plan if isinstance(plan, dict) else {}
        if str(raw_plan.get("execution_mode") or "").strip().lower() == "plan_only" or raw_plan.get(
            "no_execution_required"
        ):
            return self._finalize_outcome(
                raw_plan,
                ok=False,
                status="plan_only_no_execution",
                message="plan_only plans must not run in the executor",
                plan_id=str(raw_plan.get("plan_id") or ""),
                goal_summary=str(raw_plan.get("goal_summary") or ""),
            )

        if not avatar_plan_executor_enabled():
            return self._finalize_outcome(
                raw_plan,
                ok=False,
                status="plan_executor_disabled",
                message="AVATAR_PLAN_EXECUTOR_ENABLED=false",
            )

        parsed = plan if isinstance(plan, ExecutionPlan) else ExecutionPlan.from_dict(plan)
        if len(parsed.steps) > avatar_plan_max_steps():
            return self._finalize_outcome(
                parsed,
                ok=False,
                status="plan_too_large",
                message=f"step_count>{avatar_plan_max_steps()}",
                plan_id=parsed.plan_id,
                goal_summary=parsed.goal_summary,
            )

        try:
            ordered = self._topological_order(parsed.steps)
        except ValueError as exc:
            return self._finalize_outcome(
                parsed,
                ok=False,
                status="invalid_plan",
                message=str(exc),
                plan_id=parsed.plan_id,
                goal_summary=parsed.goal_summary,
            )

        resume_index = max(0, min(int(start_step_index), len(ordered)))
        if resume_index >= len(ordered):
            return self._finalize_outcome(
                parsed,
                ok=True,
                status="completed",
                message="plan_already_completed",
                plan_id=parsed.plan_id,
                goal_summary=parsed.goal_summary,
                completed_steps=[],
            )
        ordered = ordered[resume_index:]

        confidence_ctx = _confidence_context_from_plan(parsed)

        goal_route_data: dict[str, Any] = {}
        orchestrator_name = ""
        if isinstance(parsed.planner_trace, dict):
            raw_route = parsed.planner_trace.get("goal_route")
            if isinstance(raw_route, dict):
                goal_route_data = dict(raw_route)
                orchestrator_name = str(raw_route.get("orchestrator") or "")
        if not goal_route_data and parsed.goal_summary.strip():
            try:
                from iuve_connect_agents.shared.goal_router import route_goal

                route = route_goal(parsed.goal_summary)
                goal_route_data = route.to_audit_fields()
                orchestrator_name = route.primary_orchestrator.value
                self._audit_logger.log_goal_routed(
                    goal_text=parsed.goal_summary,
                    goal_category=route.primary_category.value,
                    orchestrator=orchestrator_name,
                    secondary_orchestrators=[o.value for o in route.secondary_orchestrators],
                    confidence=route.confidence,
                    planner_type=route.planner_type,
                    session_preset=route.session_preset,
                    needs_clarification=route.needs_clarification,
                    decision_id=parsed.plan_id,
                )
            except Exception:
                pass

        self._audit_logger.log_plan_created(
            plan_id=parsed.plan_id,
            goal_summary=parsed.goal_summary,
            step_count=len(parsed.steps),
            confirm_gate_count=len(parsed.confirm_gates),
            planner_trace=parsed.planner_trace,
            goal_route=goal_route_data or None,
            orchestrator=orchestrator_name,
        )

        context = PlanStepContext()
        outcomes: list[PlanStepOutcome] = []
        total_steps = len(parsed.steps)
        base_index = resume_index
        collector: EvidenceCollector | None = None
        if avatar_evidence_collector_enabled():
            collector = EvidenceCollector(plan_id=parsed.plan_id, goal_summary=parsed.goal_summary)

        for index, step in enumerate(ordered, start=1):
            absolute_index = base_index + index
            if cancel_check is not None and cancel_check():
                return self._finalize_outcome(
                    parsed,
                    ok=False,
                    status="plan_cancelled",
                    message="plan_cancelled",
                    plan_id=parsed.plan_id,
                    goal_summary=parsed.goal_summary,
                    completed_steps=outcomes,
                    failed_step_id=step.step_id,
                )
            if step.command_type == "plan_step":
                description = str((step.payload or {}).get("description") or "plan step")
                outcome = PlanStepOutcome(
                    step_id=step.step_id,
                    command_type=step.command_type,
                    ok=True,
                    status="plan_step",
                    message=description,
                    evidence={"description": description, "handler": "plan_step"},
                )
                outcomes.append(outcome)
                if step_callback is not None:
                    step_callback(outcome, absolute_index, total_steps)
                continue
            payload, scope = enrich_plan_step_payload(
                step,
                plan=parsed,
                context=context,
                workspace_scope=workspace_scope,
            )
            try:
                from .runtime_context import update_from_plan

                update_from_plan(
                    plan_id=parsed.plan_id,
                    goal_summary=parsed.goal_summary,
                    step_id=step.step_id,
                    command_type=step.command_type,
                )
            except Exception:
                pass
            if step.command_type == "summarize_content":
                payload.setdefault("goal_summary", parsed.goal_summary)

            if step.command_type == "keyboard_type_text" and not str(payload.get("text") or "").strip():
                expects_generated = bool(
                    (step.payload or {}).get("content_from_step")
                    or (step.payload or {}).get("text_from_step")
                )
                if expects_generated:
                    outcome = PlanStepOutcome(
                        step_id=step.step_id,
                        command_type=step.command_type,
                        ok=False,
                        status="no_generated_content",
                        message="No generated content",
                    )
                    outcomes.append(outcome)
                    if step_callback is not None:
                        step_callback(outcome, absolute_index, total_steps)
                    if avatar_plan_stop_on_failure():
                        return self._finalize_outcome(
                            parsed,
                            ok=False,
                            status="no_generated_content",
                            message="No generated content",
                            plan_id=parsed.plan_id,
                            goal_summary=parsed.goal_summary,
                            completed_steps=outcomes,
                            failed_step_id=step.step_id,
                        )
                    continue

            if step.stop_zone is not None:
                payload["stop_zone"] = step.stop_zone.value
            if confidence_ctx.get("stop_zone_required"):
                payload.setdefault("requires_user_confirm", True)

            if _should_defer_step_for_confidence(
                command_type=step.command_type,
                confidence_ctx=confidence_ctx,
            ):
                outcome = PlanStepOutcome(
                    step_id=step.step_id,
                    command_type=step.command_type,
                    ok=True,
                    status="deferred_confidence",
                    message="deferred_by_execution_posture",
                    evidence={
                        "execution_posture": confidence_ctx.get("execution_posture"),
                        "execution_confidence": confidence_ctx.get("execution_confidence"),
                    },
                )
                outcomes.append(outcome)
                if step_callback is not None:
                    step_callback(outcome, absolute_index, total_steps)
                continue

            prepared = self._local_executor.prepare(
                command_type=step.command_type,
                payload=payload,
                scope=scope,
            )
            if not prepared.ok or prepared.envelope is None or prepared.decision is None:
                outcome = PlanStepOutcome(
                    step_id=step.step_id,
                    command_type=step.command_type,
                    ok=False,
                    status=str(prepared.status or "denied"),
                    message=str(prepared.message or prepared.status),
                )
                outcomes.append(outcome)
                if step_callback is not None:
                    step_callback(outcome, absolute_index, total_steps)
                if avatar_plan_stop_on_failure():
                    return self._finalize_outcome(
                        parsed,
                        ok=False,
                        status="step_denied",
                        message=outcome.message,
                        plan_id=parsed.plan_id,
                        goal_summary=parsed.goal_summary,
                        completed_steps=outcomes,
                        failed_step_id=step.step_id,
                    )
                continue

            if prepared.require_confirmation and self._confirmation_callback is not None:
                if _should_auto_confirm_for_confidence(
                    confidence_ctx=confidence_ctx,
                    require_confirmation=bool(prepared.require_confirmation),
                ):
                    confirm = "approve"
                else:
                    confirm_handler = wrap_confirmation_callback(self._confirmation_callback)
                    try:
                        confirm = confirm_handler(prepared.envelope, prepared.decision)
                    except Exception:
                        confirm = "decline"
                if confirm != "approve":
                    outcome = PlanStepOutcome(
                        step_id=step.step_id,
                        command_type=step.command_type,
                        ok=False,
                        status="declined",
                        message="user_declined_confirmation",
                    )
                    outcomes.append(outcome)
                    if step_callback is not None:
                        step_callback(outcome, absolute_index, total_steps)
                    return self._finalize_outcome(
                        parsed,
                        ok=False,
                        status="step_declined",
                        message=outcome.message,
                        plan_id=parsed.plan_id,
                        goal_summary=parsed.goal_summary,
                        completed_steps=outcomes,
                        failed_step_id=step.step_id,
                    )
                executed = self._execute_step_with_watchdog(
                    lambda: self._local_executor.execute(
                        prepared.envelope,
                        prepared.decision,
                        confirmation="approve",
                    ),
                    timeout_sec=step_timeout_sec,
                )
            else:
                executed = self._execute_step_with_watchdog(
                    lambda: self._local_executor.execute(prepared.envelope, prepared.decision),
                    timeout_sec=step_timeout_sec,
                )

            step_outcome = PlanStepOutcome(
                step_id=step.step_id,
                command_type=step.command_type,
                ok=bool(executed.ok),
                status=str(executed.status or ("ok" if executed.ok else "failed")),
                message=str(executed.message or ""),
                evidence=dict(executed.evidence or {}),
            )
            outcomes.append(step_outcome)
            if step_callback is not None:
                step_callback(step_outcome, absolute_index, total_steps)

            try:
                from .orchestra_runtime_sync import emit_action_journal_event

                trace = getattr(parsed, "planner_trace", None) or {}
                emit_action_journal_event(
                    {
                        "plan_id": parsed.plan_id,
                        "intent": str(trace.get("intent") if isinstance(trace, dict) else ""),
                        "step_id": step.step_id,
                        "command_type": step.command_type,
                        "policy": {
                            "require_confirmation": bool(prepared.require_confirmation),
                            "confirm_type": str(getattr(prepared.decision, "confirm_type", "") or ""),
                            "stop_zone": str(getattr(prepared.decision, "stop_zone", "") or ""),
                        },
                        "verifier": {"ok": step_outcome.ok, "status": step_outcome.status},
                        "timestamp": "",
                    }
                )
            except Exception:
                pass

            if executed.evidence:
                context.store(step.step_id, dict(executed.evidence))

            if collector is not None:
                if step.command_type == "verify_screen_contains_text" and executed.evidence:
                    collector.record_verify_outcome(
                        step_id=step.step_id,
                        command_type=step.command_type,
                        status="success" if executed.ok else "failed",
                        evidence=dict(executed.evidence),
                        capability_id=str(payload.get("required_capability") or "agent.screen.verify"),
                    )
                else:
                    collector.record_step(
                        step_id=step.step_id,
                        command_type=step.command_type,
                        status="success" if executed.ok else "failed",
                        artifacts=[dict(executed.evidence)] if executed.evidence else [],
                        capability_id=str(payload.get("required_capability") or ""),
                    )

            step_succeeded = bool(executed.ok)
            if (
                step.command_type == "verify_screen_contains_text"
                and not step_succeeded
                and vision_recovery_enabled(self._vision_recovery_callback)
            ):
                recovery = self._attempt_vision_recovery(
                    plan=parsed,
                    verify_step=step,
                    verify_payload=payload,
                    initial_outcome=step_outcome,
                    workspace_scope=workspace_scope,
                    context=context,
                    collector=collector,
                    step_callback=step_callback,
                    absolute_step_index=absolute_index,
                    total_steps=total_steps,
                )
                if recovery.retry_outcomes:
                    outcomes.extend(recovery.retry_outcomes)
                step_outcome = recovery.verify_outcome
                outcomes[-1] = step_outcome
                if recovery.recovered:
                    step_succeeded = True

            if step_succeeded and step.command_type == "keyboard_type_text":
                if not self._plan_has_explicit_capture_after(parsed, step.step_id):
                    vision_outcome = self._maybe_post_keyboard_capture(
                        parsed,
                        parent_step=step,
                        parent_payload=payload,
                        workspace_scope=workspace_scope,
                        step_index=absolute_index,
                        total_steps=total_steps,
                        step_callback=step_callback,
                        collector=collector,
                    )
                    if vision_outcome is not None:
                        outcomes.append(vision_outcome)
                        if step_callback is not None:
                            step_callback(vision_outcome, absolute_index, total_steps)
                        if vision_outcome.ok and not self._plan_has_explicit_verify_after(parsed, step.step_id):
                            verify_outcome = self._maybe_post_keyboard_verify(
                                parsed,
                                parent_step=step,
                                parent_payload=payload,
                                capture_step_id=vision_outcome.step_id,
                                capture_evidence=vision_outcome.evidence,
                                workspace_scope=workspace_scope,
                                collector=collector,
                            )
                            if verify_outcome is not None:
                                outcomes.append(verify_outcome)
                                if step_callback is not None:
                                    step_callback(verify_outcome, absolute_index, total_steps)
                                if not verify_outcome.ok:
                                    from iuve_connect_agents.desktop.ui.avatar_intent_router import (
                                        extract_vision_verify_expected_text,
                                    )

                                    typed_text = str(
                                        parent_payload.get("text") or parent_payload.get("expected_text") or ""
                                    ).strip()
                                    expected_text = extract_vision_verify_expected_text(
                                        request_text=str(parent_payload.get("source_request_text") or ""),
                                        typed_text=typed_text,
                                    )
                                    keyboard_verify_payload = {
                                        "expected_text": expected_text,
                                        "depends_on_step": vision_outcome.step_id,
                                        "match_mode": "contains",
                                        "verify_mode": "chat_response",
                                        "min_confidence": 0.7,
                                        "screen_vision_granted": True,
                                    }
                                    recovery = self._attempt_vision_recovery(
                                        plan=parsed,
                                        verify_step=ExecutionPlanStep(
                                            step_id=verify_outcome.step_id,
                                            command_type="verify_screen_contains_text",
                                            payload=keyboard_verify_payload,
                                        ),
                                        verify_payload=keyboard_verify_payload,
                                        initial_outcome=verify_outcome,
                                        workspace_scope=workspace_scope,
                                        context=context,
                                        collector=collector,
                                        step_callback=step_callback,
                                        absolute_step_index=absolute_index + 1,
                                        total_steps=total_steps,
                                    )
                                    if recovery.retry_outcomes:
                                        outcomes.extend(recovery.retry_outcomes)
                                    verify_outcome = recovery.verify_outcome
                                    outcomes[-1] = verify_outcome
                                    if not recovery.recovered and avatar_plan_stop_on_failure():
                                        return self._finalize_outcome(
                                            parsed,
                                            ok=False,
                                            status="step_failed",
                                            message=verify_outcome.message,
                                            plan_id=parsed.plan_id,
                                            goal_summary=parsed.goal_summary,
                                            completed_steps=outcomes,
                                            failed_step_id=verify_outcome.step_id,
                                        )

            if not step_succeeded and avatar_plan_stop_on_failure():
                replan_outcome = self._attempt_orchestra_replan(
                    plan=parsed,
                    step=step,
                    step_outcome=step_outcome,
                    completed_steps=outcomes,
                    resume_index=absolute_index - 1,
                )
                if replan_outcome is not None:
                    return replan_outcome
                return self._finalize_outcome(
                    parsed,
                    ok=False,
                    status="step_failed",
                    message=step_outcome.message,
                    plan_id=parsed.plan_id,
                    goal_summary=parsed.goal_summary,
                    completed_steps=outcomes,
                    failed_step_id=step.step_id,
                )

        if collector is not None:
            try:
                collector.persist()
            except Exception:
                pass

        return self._finalize_outcome(
            parsed,
            ok=True,
            status="completed",
            message="plan_completed",
            plan_id=parsed.plan_id,
            goal_summary=parsed.goal_summary,
            completed_steps=outcomes,
        )

    @staticmethod
    def _recovery_hints_from_plan(plan: ExecutionPlan) -> dict[str, Any]:
        trace = dict(plan.planner_trace or {})
        hints = trace.get("recovery_hints")
        if isinstance(hints, dict):
            return dict(hints)
        contract = trace.get("orchestra_execution_contract")
        if isinstance(contract, dict) and isinstance(contract.get("recovery_hints"), dict):
            return dict(contract["recovery_hints"])
        return {}

    def _build_step_failure_payload(
        self,
        *,
        plan: ExecutionPlan,
        step: ExecutionPlanStep,
        step_outcome: PlanStepOutcome,
    ) -> dict[str, Any]:
        hints = self._recovery_hints_from_plan(plan)
        focus_target = str(hints.get("focus_target") or "")
        return {
            "step_id": step.step_id,
            "command_type": step.command_type,
            "status": step_outcome.status,
            "message": step_outcome.message,
            "plan_id": plan.plan_id,
            "goal_summary": plan.goal_summary,
            "evidence": dict(step_outcome.evidence or {}),
            "focus_target": focus_target,
        }

    def _attempt_orchestra_replan(
        self,
        *,
        plan: ExecutionPlan,
        step: ExecutionPlanStep,
        step_outcome: PlanStepOutcome,
        completed_steps: list[PlanStepOutcome],
        resume_index: int,
    ) -> PlanExecutionOutcome | None:
        if not replanning_enabled() or self._replan_callback is None:
            return None
        hints = self._recovery_hints_from_plan(plan)
        step_failure = self._build_step_failure_payload(plan=plan, step=step, step_outcome=step_outcome)
        if not hints.get("replan_recommended") and not hints.get("replan_signal"):
            try:
                from app.services.orchestra.replanning import build_replan_signal_from_step_failure

                signal = build_replan_signal_from_step_failure(
                    step_id=step.step_id,
                    command_type=step.command_type,
                    status=step_outcome.status,
                    message=step_outcome.message,
                    evidence=step_outcome.evidence,
                    focus_target=str(hints.get("focus_target") or ""),
                )
                if signal is None or not signal.replan_recommended:
                    return None
            except Exception:
                return None
        try:
            revise = self._replan_callback(step_failure)
        except Exception:
            logger.debug("orchestra_replan_callback_failed", exc_info=True)
            return None
        if not isinstance(revise, dict):
            return None
        autonomous = bool(revise.get("autonomous_replan")) and autonomous_replan_enabled()
        if not revise.get("replan_recommended") and not autonomous:
            return None
        revised_plan = revise.get("execution_plan")
        if not isinstance(revised_plan, dict) or not revised_plan.get("steps"):
            if autonomous and str(revise.get("decision") or "") == "revise_plan":
                return None
            return None
        trace = dict(revised_plan.get("planner_trace") or {})
        trace.setdefault("recovery_hints", revise.get("recovery_hints") or {})
        if autonomous:
            trace["autonomous_replan"] = True
        trace["replan_source"] = {
            "failed_step_id": step.step_id,
            "resume_index": resume_index,
            "prior_plan_id": plan.plan_id,
        }
        revised_plan["planner_trace"] = trace
        continued = self.execute_plan(
            revised_plan,
            start_step_index=0,
        )
        return self._finalize_outcome(
            revised_plan,
            ok=continued.ok,
            status=continued.status if continued.ok else "replan_step_failed",
            message=continued.message,
            plan_id=str(revised_plan.get("plan_id") or plan.plan_id),
            goal_summary=str(revised_plan.get("goal_summary") or plan.goal_summary),
            completed_steps=completed_steps + continued.completed_steps,
            failed_step_id=continued.failed_step_id,
            replan_recommended=True,
            revised_plan=revised_plan,
        )

    def _maybe_post_keyboard_capture(
        self,
        plan: ExecutionPlan,
        *,
        parent_step: ExecutionPlanStep,
        parent_payload: dict[str, Any],
        workspace_scope: dict[str, Any] | None,
        step_index: int,
        total_steps: int,
        step_callback: StepProgressCallback | None,
        collector: EvidenceCollector | None,
    ) -> PlanStepOutcome | None:
        del step_callback, step_index, total_steps
        if not avatar_desktop_vision_enabled():
            return None
        submit_after = bool(parent_payload.get("submit_after") or parent_payload.get("submit_message"))
        if not submit_after:
            return None

        capture_step_id = f"{parent_step.step_id}_vision_capture"
        capture_payload = {
            "plan_id": plan.plan_id,
            "step_id": capture_step_id,
            "region": "full",
            "delay_ms": 500,
            "required_capability": "agent.screen.capture",
            "capability_risk_level": "medium",
            "user_consent_state": "consented_via_local_avatar",
            "source_agent": "iuve_avatar_plan_executor",
            "intended_action": f"capture_screenshot:{capture_step_id}",
        }
        prepared = self._local_executor.prepare(
            command_type="capture_screenshot",
            payload=capture_payload,
            scope=workspace_scope or {},
        )
        if not prepared.ok or prepared.envelope is None or prepared.decision is None:
            return None

        executed = self._local_executor.execute(prepared.envelope, prepared.decision)
        outcome = PlanStepOutcome(
            step_id=capture_step_id,
            command_type="capture_screenshot",
            ok=bool(executed.ok),
            status=str(executed.status or ("ok" if executed.ok else "failed")),
            message=str(executed.message or ""),
            evidence=dict(executed.evidence or {}),
        )
        if collector is not None:
            collector.record_step(
                step_id=capture_step_id,
                command_type="capture_screenshot",
                status="success" if executed.ok else "failed",
                artifacts=[dict(executed.evidence)] if executed.evidence else [],
                capability_id="agent.screen.capture",
            )
        return outcome

    def _maybe_post_keyboard_verify(
        self,
        plan: ExecutionPlan,
        *,
        parent_step: ExecutionPlanStep,
        parent_payload: dict[str, Any],
        capture_step_id: str,
        capture_evidence: dict[str, Any] | None,
        workspace_scope: dict[str, Any] | None,
        collector: EvidenceCollector | None,
    ) -> PlanStepOutcome | None:
        from iuve_connect_agents.desktop.ui.avatar_intent_router import extract_vision_verify_expected_text
        from iuve_connect_agents.shared.feature_flags import avatar_desktop_vision_local_enabled

        if not avatar_desktop_vision_enabled() or not avatar_desktop_vision_local_enabled():
            return None
        typed_text = str(parent_payload.get("text") or parent_payload.get("expected_text") or "").strip()
        expected_text = extract_vision_verify_expected_text(
            request_text=str(parent_payload.get("source_request_text") or ""),
            typed_text=typed_text,
        )
        if not expected_text:
            return None

        verify_step_id = f"{parent_step.step_id}_vision_verify"
        verify_payload = {
            "plan_id": plan.plan_id,
            "step_id": verify_step_id,
            "expected_text": expected_text,
            "depends_on_step": capture_step_id,
            "match_mode": "contains",
            "verify_mode": "chat_response",
            "min_confidence": 0.55,
            "capture_if_missing": False,
            "required_capability": "agent.screen.verify",
            "capability_risk_level": "low",
            "user_consent_state": "consented_via_local_avatar",
            "source_agent": "iuve_avatar_plan_executor",
            "screen_vision_granted": True,
            "intended_action": f"verify_screen_contains_text:{verify_step_id}",
        }
        if capture_evidence:
            if capture_evidence.get("path"):
                verify_payload["screenshot_path"] = capture_evidence["path"]
                verify_payload["screenshot_sha256"] = capture_evidence.get("sha256", "")
        prepared = self._local_executor.prepare(
            command_type="verify_screen_contains_text",
            payload=verify_payload,
            scope=workspace_scope or {},
        )
        if not prepared.ok or prepared.envelope is None or prepared.decision is None:
            return None

        executed = self._local_executor.execute(prepared.envelope, prepared.decision)
        outcome = PlanStepOutcome(
            step_id=verify_step_id,
            command_type="verify_screen_contains_text",
            ok=bool(executed.ok),
            status=str(executed.status or ("ok" if executed.ok else "failed")),
            message=str(executed.message or ""),
            evidence=dict(executed.evidence or {}),
        )
        if collector is not None and executed.evidence:
            collector.record_verify_outcome(
                step_id=verify_step_id,
                command_type="verify_screen_contains_text",
                status="success" if executed.ok else "failed",
                evidence=dict(executed.evidence),
            )
        return outcome

    def _attempt_vision_recovery(
        self,
        *,
        plan: ExecutionPlan,
        verify_step: ExecutionPlanStep,
        verify_payload: dict[str, Any],
        initial_outcome: PlanStepOutcome,
        workspace_scope: dict[str, Any] | None,
        context: PlanStepContext,
        collector: EvidenceCollector | None,
        step_callback: StepProgressCallback | None,
        absolute_step_index: int,
        total_steps: int,
    ) -> VisionRecoveryResult:
        callback = self._vision_recovery_callback
        if callback is None:
            return VisionRecoveryResult(
                recovered=False,
                verify_outcome=initial_outcome,
                retry_outcomes=[],
            )

        expected_text = str(verify_payload.get("expected_text") or "").strip()
        findings = ""
        if initial_outcome.evidence:
            verifier = initial_outcome.evidence.get("verifier_result") or {}
            findings = str(verifier.get("findings") or initial_outcome.message or "")

        retry_outcomes: list[PlanStepOutcome] = []
        current_outcome = initial_outcome
        fallback_evidence = resolve_fallback_capture_evidence(
            context=context,
            verify_payload=verify_payload,
            verify_step=verify_step,
            initial_outcome=initial_outcome,
        )
        capture_progress_index = recovery_capture_step_index(
            plan=plan,
            verify_step=verify_step,
            verify_payload=verify_payload,
        )
        verify_progress_index = recovery_verify_step_index(plan=plan, verify_step=verify_step)

        for attempt in range(1, vision_recovery_max_attempts_for_plan(plan) + 1):
            decision = str(
                callback(
                    {
                        "attempt": attempt,
                        "max_attempts": vision_recovery_max_attempts_for_plan(plan) or VISION_RECOVERY_MAX_ATTEMPTS,
                        "expected_text": expected_text,
                        "step_id": verify_step.step_id,
                        "findings": findings,
                    }
                )
            ).strip().lower()
            if decision != "retry":
                break

            capture_step_id, capture_payload = build_recovery_capture_payload(
                plan=plan,
                verify_step=verify_step,
                attempt=attempt,
            )
            capture_outcome = self._run_recovery_capture(
                capture_step_id=capture_step_id,
                capture_payload=capture_payload,
                workspace_scope=workspace_scope,
                context=context,
                collector=collector,
            )
            retry_outcomes.append(capture_outcome)
            if step_callback is not None:
                step_callback(capture_outcome, capture_progress_index, total_steps)
            active_capture_evidence = dict(capture_outcome.evidence or {})
            if not active_capture_evidence.get("path") and fallback_evidence.get("path"):
                active_capture_evidence = dict(fallback_evidence)
            if not capture_outcome.ok and not active_capture_evidence.get("path"):
                current_outcome = PlanStepOutcome(
                    step_id=verify_step.step_id,
                    command_type="verify_screen_contains_text",
                    ok=False,
                    status="recovery_capture_failed",
                    message=capture_outcome.message,
                    evidence=dict(capture_outcome.evidence),
                )
                findings = capture_outcome.message
                continue

            verify_retry_payload = build_recovery_verify_payload(
                plan=plan,
                verify_step=verify_step,
                verify_payload=verify_payload,
                capture_step_id=capture_step_id,
                capture_evidence=active_capture_evidence,
                attempt=attempt,
                fallback_evidence=fallback_evidence,
            )
            verify_retry_outcome = self._run_recovery_verify(
                verify_step=verify_step,
                verify_payload=verify_retry_payload,
                workspace_scope=workspace_scope,
                context=context,
                collector=collector,
            )
            retry_outcomes.append(verify_retry_outcome)
            if step_callback is not None:
                step_callback(verify_retry_outcome, verify_progress_index, total_steps)
            current_outcome = verify_retry_outcome
            if active_capture_evidence.get("path"):
                fallback_evidence = dict(active_capture_evidence)
            if verify_retry_outcome.ok:
                return VisionRecoveryResult(
                    recovered=True,
                    verify_outcome=current_outcome,
                    retry_outcomes=retry_outcomes,
                )
            if verify_retry_outcome.evidence:
                verifier = verify_retry_outcome.evidence.get("verifier_result") or {}
                findings = str(verifier.get("findings") or verify_retry_outcome.message or "")

        return VisionRecoveryResult(
            recovered=False,
            verify_outcome=current_outcome,
            retry_outcomes=retry_outcomes,
        )

    def _run_recovery_capture(
        self,
        *,
        capture_step_id: str,
        capture_payload: dict[str, Any],
        workspace_scope: dict[str, Any] | None,
        context: PlanStepContext,
        collector: EvidenceCollector | None,
    ) -> PlanStepOutcome:
        prepared = self._local_executor.prepare(
            command_type="capture_screenshot",
            payload=capture_payload,
            scope=workspace_scope or {},
        )
        if not prepared.ok or prepared.envelope is None or prepared.decision is None:
            return PlanStepOutcome(
                step_id=capture_step_id,
                command_type="capture_screenshot",
                ok=False,
                status=str(prepared.status or "denied"),
                message=str(prepared.message or prepared.status),
            )

        executed = self._local_executor.execute(prepared.envelope, prepared.decision)
        outcome = PlanStepOutcome(
            step_id=capture_step_id,
            command_type="capture_screenshot",
            ok=bool(executed.ok),
            status=str(executed.status or ("ok" if executed.ok else "failed")),
            message=str(executed.message or ""),
            evidence=dict(executed.evidence or {}),
        )
        if executed.evidence:
            context.store(capture_step_id, dict(executed.evidence))
        if collector is not None:
            collector.record_step(
                step_id=capture_step_id,
                command_type="capture_screenshot",
                status="success" if executed.ok else "failed",
                artifacts=[dict(executed.evidence)] if executed.evidence else [],
                capability_id="agent.screen.capture",
            )
        return outcome

    def _run_recovery_verify(
        self,
        *,
        verify_step: ExecutionPlanStep,
        verify_payload: dict[str, Any],
        workspace_scope: dict[str, Any] | None,
        context: PlanStepContext,
        collector: EvidenceCollector | None,
    ) -> PlanStepOutcome:
        prepared = self._local_executor.prepare(
            command_type="verify_screen_contains_text",
            payload=verify_payload,
            scope=workspace_scope or {},
        )
        if not prepared.ok or prepared.envelope is None or prepared.decision is None:
            return PlanStepOutcome(
                step_id=verify_step.step_id,
                command_type="verify_screen_contains_text",
                ok=False,
                status=str(prepared.status or "denied"),
                message=str(prepared.message or prepared.status),
            )

        executed = self._local_executor.execute(prepared.envelope, prepared.decision)
        outcome = PlanStepOutcome(
            step_id=verify_step.step_id,
            command_type="verify_screen_contains_text",
            ok=bool(executed.ok),
            status=str(executed.status or ("ok" if executed.ok else "failed")),
            message=str(executed.message or ""),
            evidence=dict(executed.evidence or {}),
        )
        if executed.evidence:
            context.store(verify_step.step_id, dict(executed.evidence))
        if collector is not None and executed.evidence:
            collector.record_verify_outcome(
                step_id=verify_step.step_id,
                command_type="verify_screen_contains_text",
                status="success" if executed.ok else "failed",
                evidence=dict(executed.evidence),
                capability_id="agent.screen.verify",
            )
        return outcome

    @staticmethod
    def _plan_has_explicit_verify_after(plan: ExecutionPlan, parent_step_id: str) -> bool:
        capture_ids = {parent_step_id, f"{parent_step_id}_capture", f"{parent_step_id}_vision_capture"}
        for step in plan.steps:
            if step.command_type not in {"verify_screen_contains_text", "verify_web_draft"}:
                continue
            if any(dep in capture_ids or dep == parent_step_id for dep in step.depends_on):
                return True
        return False

    @staticmethod
    def _plan_has_explicit_capture_after(plan: ExecutionPlan, parent_step_id: str) -> bool:
        for step in plan.steps:
            if step.command_type != "capture_screenshot":
                continue
            if parent_step_id in step.depends_on:
                return True
        return False

    @staticmethod
    def _execute_step_with_watchdog(fn, *, timeout_sec: float):
        from iuve_connect_agents.desktop.active.avatar_local_executor import AvatarExecutionOutcome

        if timeout_sec <= 0:
            return fn()
        try:
            with ThreadPoolExecutor(max_workers=1) as pool:
                future = pool.submit(fn)
                return future.result(timeout=timeout_sec)
        except FuturesTimeoutError:
            try:
                from iuve_connect_agents.desktop.active.command_executor import kill_orphan_child_processes

                kill_orphan_child_processes()
            except Exception:
                pass
            return AvatarExecutionOutcome(
                ok=False,
                status="step_timeout",
                message=f"step_exceeded_{int(timeout_sec)}s",
            )
        except Exception as exc:
            return AvatarExecutionOutcome(
                ok=False,
                status="step_failed",
                message=str(exc) or "step_failed",
            )

    @staticmethod
    def _topological_order(steps: list[ExecutionPlanStep]) -> list[ExecutionPlanStep]:
        by_id = {step.step_id: step for step in steps}
        indegree = {step.step_id: 0 for step in steps}
        children: dict[str, list[str]] = {step.step_id: [] for step in steps}

        for step in steps:
            for dep in step.depends_on:
                if dep not in by_id:
                    raise ValueError(f"missing_depends_on:{dep}")
                indegree[step.step_id] += 1
                children[dep].append(step.step_id)

        queue = [step_id for step_id, degree in indegree.items() if degree == 0]
        ordered_ids: list[str] = []
        while queue:
            current = queue.pop(0)
            ordered_ids.append(current)
            for child in children[current]:
                indegree[child] -= 1
                if indegree[child] == 0:
                    queue.append(child)

        if len(ordered_ids) != len(steps):
            raise ValueError("cyclic_depends_on")

        return [by_id[step_id] for step_id in ordered_ids]
