diff --git a/src/cleveragents/cli/commands/plan.py b/src/cleveragents/cli/commands/plan.py index a53a2df72..15c7e2528 100644 --- a/src/cleveragents/cli/commands/plan.py +++ b/src/cleveragents/cli/commands/plan.py @@ -31,6 +31,7 @@ from datetime import datetime from pathlib import Path from typing import TYPE_CHECKING, Annotated, Any, Literal, cast +import structlog import typer from rich.console import Console from rich.panel import Panel @@ -39,13 +40,18 @@ from rich.table import Table from sqlalchemy.exc import SQLAlchemyError from cleveragents.a2a.models import A2aRequest +from cleveragents.application.container import get_container from cleveragents.cli.formatting import OutputFormat, format_output from cleveragents.core.exceptions import ( CleverAgentsError, + NotFoundError, PlanError, ValidationError, ) from cleveragents.domain.models.core.plan import PlanPhase, ProcessingState +from cleveragents.infrastructure.sandbox.git_worktree import ( + GitWorktreeSandbox, +) # Regex for validating namespaced actor names (namespace/name format) _NAMESPACED_ACTOR_RE = re.compile(r"^[a-z][a-z0-9-]*/[a-z][a-z0-9._-]*$") @@ -1349,6 +1355,9 @@ def _get_lifecycle_service(): return container.plan_lifecycle_service() +_cleanup_logger = structlog.get_logger("cleveragents.cli.commands.plan.cleanup") + + def _cleanup_sandbox_for_plan( plan_id: str, service: PlanLifecycleService, @@ -1361,19 +1370,33 @@ def _cleanup_sandbox_for_plan( Used by ``plan cancel`` to prevent resource leaks. """ - from cleveragents.application.container import get_container - from cleveragents.infrastructure.sandbox.git_worktree import ( - GitWorktreeSandbox, - ) + if not plan_id or not plan_id.strip(): + _cleanup_logger.warning("cleanup_sandbox.skipped", reason="empty plan_id") + return container = get_container() - plan = service.get_plan(plan_id) + + try: + plan = service.get_plan(plan_id) + except (NotFoundError, CleverAgentsError) as exc: + _cleanup_logger.warning( + "cleanup_sandbox.plan_not_found", + plan_id=plan_id, + error=str(exc), + ) + return + project_names = [pl.project_name for pl in getattr(plan, "project_links", [])] for project_name in project_names: try: project = container.namespaced_project_repo().get(project_name) - except Exception: + except (NotFoundError, CleverAgentsError, SQLAlchemyError) as exc: + _cleanup_logger.debug( + "cleanup_sandbox.project_lookup_failed", + project_name=project_name, + error=str(exc), + ) continue if project is None: continue @@ -1382,7 +1405,12 @@ def _cleanup_sandbox_for_plan( resource = container.resource_registry_service().show_resource( lr.resource_id, ) - except Exception: + except (NotFoundError, CleverAgentsError, SQLAlchemyError) as exc: + _cleanup_logger.debug( + "cleanup_sandbox.resource_lookup_failed", + resource_id=lr.resource_id, + error=str(exc), + ) continue if ( resource.resource_type_name not in ("git-checkout", "git") diff --git a/src/cleveragents/infrastructure/sandbox/git_worktree.py b/src/cleveragents/infrastructure/sandbox/git_worktree.py index b5651d849..7a61779af 100644 --- a/src/cleveragents/infrastructure/sandbox/git_worktree.py +++ b/src/cleveragents/infrastructure/sandbox/git_worktree.py @@ -226,9 +226,11 @@ class GitWorktreeSandbox: branch_name, ) + branch_deleted = True try: _run_git(["branch", "-D", branch_name], cwd=repo_path) except (subprocess.CalledProcessError, subprocess.TimeoutExpired): + branch_deleted = False logger.warning( "Failed to delete stale branch %s", branch_name, @@ -240,8 +242,14 @@ class GitWorktreeSandbox: ): _run_git(["worktree", "prune"], cwd=repo_path) - logger.info("Stale sandbox branch cleaned up: branch=%s", branch_name) - return True + if branch_deleted: + logger.info("Stale sandbox branch cleaned up: branch=%s", branch_name) + else: + logger.warning( + "Partial cleanup: worktree removed but branch persists: branch=%s", + branch_name, + ) + return True # Branch was found; cleanup attempted (even if partial) # -- protocol methods ----------------------------------------------------