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
30 changes: 30 additions & 0 deletions migrations/versions/009_add_cleanup_at.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""Add cleanup_at column for delayed sandbox cleanup.

Revision ID: 009
Revises: 008
Create Date: 2026-04-01
"""

from collections.abc import Sequence

import sqlalchemy as sa
from alembic import op


revision: str = "009"
down_revision: str = "008"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
op.add_column(
"automation_runs",
sa.Column("cleanup_at", sa.DateTime(timezone=True), nullable=True),
)
op.create_index("ix_automation_runs_cleanup_at", "automation_runs", ["cleanup_at"])


def downgrade() -> None:
op.drop_index("ix_automation_runs_cleanup_at", table_name="automation_runs")
op.drop_column("automation_runs", "cleanup_at")
3 changes: 2 additions & 1 deletion openhands/automation/backends/cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,8 @@ async def _do_verify() -> VerificationResult:
api_url=self.api_url,
api_key=await self._ensure_api_key(),
sandbox_id=sandbox_id,
keep_alive=self._run.keep_alive,
# Watchdog decides when to clean up (immediate or delayed).
keep_alive=True,
run_id=run_id,
bash_command_id=self._run.bash_command_id,
)
Expand Down
3 changes: 3 additions & 0 deletions openhands/automation/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,9 @@ class ServiceSettings(BaseSettings):
dispatcher_interval_seconds: int = 10
dispatcher_batch_size: int = 10
watchdog_interval_seconds: int = 60
# Delay after terminal run status before cloud sandbox cleanup, in minutes.
# Set to 0 for immediate cleanup.
sandbox_cleanup_delay_mins: int = 60

# API pagination
api_default_page_size: int = 50
Expand Down
5 changes: 5 additions & 0 deletions openhands/automation/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,11 @@ class AutomationRun(Base):
DateTime(timezone=True), nullable=True
)

# Scheduled sandbox cleanup time for delayed cleanup after terminal status.
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
24 changes: 17 additions & 7 deletions openhands/automation/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import logging
import re
import uuid
from datetime import timedelta

from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
from fastapi.responses import RedirectResponse
Expand Down Expand Up @@ -338,7 +339,8 @@ async def complete_run(
(by ``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.
If keep_alive is False, cleanup is either scheduled for later via
``cleanup_at`` or performed immediately when the cleanup delay is 0.
"""
result = await session.execute(
select(AutomationRun)
Expand Down Expand Up @@ -371,6 +373,14 @@ async def complete_run(
if body.status == "FAILED" and body.error:
values["error_detail"] = body.error

from openhands.automation.config import get_settings

settings = get_settings()
if not run.keep_alive and run.sandbox_id and settings.sandbox_cleanup_delay_mins > 0:
values["cleanup_at"] = now + timedelta(
minutes=settings.sandbox_cleanup_delay_mins
)

stmt = (
update(AutomationRun)
.where(
Expand All @@ -390,12 +400,12 @@ 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 openhands.automation.config import get_settings

settings = get_settings()
# Clean up sandbox immediately only when delayed cleanup is disabled.
if (
not run.keep_alive
and run.sandbox_id
and settings.sandbox_cleanup_delay_mins <= 0
):
api_key = user.api_key
if api_key is None:
# Cookie-authenticated users don't carry an API key;
Expand Down
1 change: 1 addition & 0 deletions openhands/automation/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,7 @@ class AutomationRunResponse(BaseModel):
created_at: UtcDatetime
started_at: UtcDatetime | None
completed_at: UtcDatetime | None
cleanup_at: UtcDatetime | None

model_config = {"from_attributes": True}

Expand Down
Loading
Loading