abi/screenshot-to-code · error · HTTPException

max_age_days must be >= 1

Error message

max_age_days must be >= 1

What it means

Raised by POST /agent-runs/prune (400) when the request body's max_age_days is less than 1. The endpoint refuses to prune runs younger than one day; zero or negative values are rejected up front rather than clamped. It is pure request validation — no state is touched.

Source

Thrown at backend/routes/agent_runs.py:231

@router.get("/agent-runs/{run_id}/assets/{filename}")
async def get_agent_run_asset(run_id: str, filename: str) -> FileResponse:
    _fetch_run(run_id)
    assets_dir = os.path.join(get_agent_runs_directory(), run_id, "assets")
    # basename() strips any traversal components before the containment check.
    safe_name = os.path.basename(filename)
    path = os.path.realpath(os.path.join(assets_dir, safe_name))
    if not path.startswith(os.path.realpath(assets_dir) + os.sep):
        raise HTTPException(status_code=400, detail="Invalid asset path")
    if not os.path.isfile(path):
        raise HTTPException(status_code=404, detail="Asset not found")
    return FileResponse(path)


@router.post("/agent-runs/prune", response_model=PruneAgentRunsResponse)
async def prune_agent_runs(request: PruneAgentRunsRequest) -> PruneAgentRunsResponse:
    if request.max_age_days < 1:
        raise HTTPException(status_code=400, detail="max_age_days must be >= 1")

    runs_directory = get_agent_runs_directory()
    if not os.path.isdir(runs_directory):
        return PruneAgentRunsResponse(deleted_count=0, freed_bytes=0)

    cutoff_timestamp = (
        datetime.now() - timedelta(days=request.max_age_days)
    ).timestamp()

    deleted_count = 0
    freed_bytes = 0
    deleted_run_ids: list[str] = []

    for entry in os.scandir(runs_directory):
        if not entry.is_dir() or not RUN_ID_PATTERN.match(entry.name):
            continue
        if entry.stat().st_mtime >= cutoff_timestamp:
            continue

View on GitHub (pinned to d026163f58)

Solutions

  1. Pass an integer >= 1, e.g. {"max_age_days": 7}.
  2. Clamp computed values in the caller: max(1, int(days)).
  3. To delete everything regardless of age, compute a large max_age_days (e.g. 36500) instead of 0.
  4. Check the PruneAgentRunsRequest model if unsure of the expected field name/type.

Example fix

# before
requests.post(url + "/agent-runs/prune", json={"max_age_days": 0})  # 400

# after
requests.post(url + "/agent-runs/prune", json={"max_age_days": max(1, days)})
Defensive patterns

Strategy: validation

Validate before calling

max_age_days = max(1, int(max_age_days))
if max_age_days < 1:
    raise ValueError("max_age_days must be >= 1")
resp = requests.post(url + "/agent-runs/prune", json={"max_age_days": max_age_days})

Type guard

def is_valid_prune_days(value: object) -> bool:
    return isinstance(value, int) and not isinstance(value, bool) and value >= 1

Try / catch

try:
    resp = requests.post(url + "/agent-runs/prune", json={"max_age_days": days})
    resp.raise_for_status()
except requests.HTTPError as e:
    if e.response is not None and e.response.status_code == 400:
        raise ValueError("prune rejected: max_age_days must be >= 1") from e
    raise

Prevention

When it happens

Trigger: Calling POST /agent-runs/prune with {"max_age_days": 0} or a negative number, or omitting coercion so a string like "0" is parsed to 0 by the Pydantic request model.

Common situations: Automation scripts that compute max_age_days from a delta that can go negative (e.g. cutoff in the future); UIs defaulting the field to 0; attempts to 'prune everything' with 0.

Related errors


AI-assisted analysis of abi/screenshot-to-code@d026163f58 (2026-08-14). Data as JSON: /api/errors/d83d54ccb6f9f1ed. Report an issue: GitHub.