Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions automation/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ class Settings(BaseSettings):
# Watchdog (scans for stale RUNNING runs past their timeout)
watchdog_interval_seconds: int = 60

# Sandbox cleanup delay after automation runs complete (in minutes).
# Sandboxes are kept available for inspection within this window.
# Set to 0 for immediate cleanup (legacy behavior).
sandbox_cleanup_delay_mins: int = 60

# Service key for authenticating with the SaaS API to fetch per-user
# API keys (called by the dispatcher before each automation run).
service_key: str = ""
Expand Down
7 changes: 7 additions & 0 deletions automation/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,13 @@ class AutomationRun(Base):
DateTime(timezone=True), nullable=True
)

# Scheduled time for sandbox cleanup. Set when run transitions to terminal
# state (COMPLETED/FAILED). NULL means immediate cleanup or already cleaned.
# Used by the cleanup scanner to delay sandbox deletion for debugging.
cleanup_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True, index=True
)

# Relationship back to automation
automation: Mapped["Automation"] = relationship("Automation", back_populates="runs")

Expand Down
29 changes: 22 additions & 7 deletions automation/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import asyncio
import logging
import uuid
from datetime import timedelta

from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import func, select, update
Expand Down Expand Up @@ -229,8 +230,12 @@ async def complete_run(
``authenticate_request``) and the resulting user must own the run's
parent automation.

If keep_alive is False, deletes the sandbox after updating the run status.
Sandbox cleanup behavior depends on the ``sandbox_cleanup_delay_mins`` setting:
- If 0: immediate cleanup (legacy behavior)
- If > 0: sets ``cleanup_at`` timestamp for delayed cleanup by watchdog
"""
from automation.config import get_settings

result = await session.execute(
select(AutomationRun)
.where(AutomationRun.id == run_id)
Expand Down Expand Up @@ -262,6 +267,13 @@ async def complete_run(
if body.status == "FAILED" and body.error:
values["error_detail"] = body.error

# Schedule sandbox cleanup if not keeping alive
settings = get_settings()
if not run.keep_alive and run.sandbox_id:
delay_mins = settings.sandbox_cleanup_delay_mins
if delay_mins > 0:
values["cleanup_at"] = now + timedelta(minutes=delay_mins)

stmt = (
update(AutomationRun)
.where(
Expand All @@ -281,12 +293,15 @@ async def complete_run(
await session.refresh(run)
logger.info("Run %s → %s", run_id, new_status.value)

# Clean up sandbox if not keeping alive
if not run.keep_alive and run.sandbox_id:
# Fire-and-forget sandbox deletion in background
from automation.config import get_settings

settings = get_settings()
# If delay is 0, clean up sandbox immediately (legacy behavior)
should_cleanup_now = (
not run.keep_alive
and run.sandbox_id
and settings.sandbox_cleanup_delay_mins == 0
)
if should_cleanup_now:
# sandbox_id is guaranteed non-None by the condition above
assert run.sandbox_id is not None
asyncio.create_task(
cleanup_sandbox(
api_url=settings.openhands_api_base_url,
Expand Down
1 change: 1 addition & 0 deletions automation/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ class AutomationRunResponse(BaseModel):
created_at: datetime
started_at: datetime | None
completed_at: datetime | None
cleanup_at: datetime | None # When sandbox cleanup is scheduled

model_config = {"from_attributes": True}

Expand Down
Loading
Loading