HKUDS/Vibe-Trading · error · TypeError

'delivery.attempts' must be a non-negative integer

Error message

'delivery.attempts' must be a non-negative integer

What it means

Raised by delivery from_dict (agent/src/scheduled_research/models.py:289) when the 'attempts' field in the delivery object is a bool, a non-int, or a negative int. The bool check exists because isinstance(True, int) is True in Python, so explicit rejection is needed. Defaults to 0 when absent.

Source

Thrown at agent/src/scheduled_research/models.py:289

        if not data:
            return cls()
        if not isinstance(data, dict):
            raise TypeError("'delivery' must be an object or null")
        raw_status = data.get("status", DeliveryStatus.NONE.value)
        try:
            status = DeliveryStatus(raw_status)
        except ValueError as exc:
            raise ValueError(f"unknown delivery status {raw_status!r}") from exc
        for name in ("session_id", "key", "error", "provider_message_id"):
            value = data.get(name)
            if value is not None and not isinstance(value, str):
                raise TypeError(f"'delivery.{name}' must be a string or null")
        updated_at = data.get("updated_at")
        if updated_at is not None and not isinstance(updated_at, int):
            raise TypeError("'delivery.updated_at' must be an integer (epoch ms) or null")
        attempts = data.get("attempts", 0)
        if isinstance(attempts, bool) or not isinstance(attempts, int) or attempts < 0:
            raise TypeError("'delivery.attempts' must be a non-negative integer")
        return cls(
            status=status,
            session_id=data.get("session_id"),
            key=data.get("key"),
            error=data.get("error"),
            attempts=attempts,
            updated_at=updated_at,
            provider_message_id=data.get("provider_message_id"),
        )


# ---------------------------------------------------------------------------
# Data model
# ---------------------------------------------------------------------------


def _verdict_record_or_none(data: Any, job_id: str) -> Optional[VerdictRecord]:
    """Parse a persisted last_verdict, degrading an unreadable one to None.

View on GitHub (pinned to 80ffdda44c)

Solutions

  1. Clamp retry counters: max(0, int(attempts))
  2. Keep attempts as int through all retry arithmetic
  3. Omit the field to use the default of 0

Example fix

// before
delivery = {"attempts": attempts - 1}  # can reach -1

// after
delivery = {"attempts": max(0, attempts - 1)}
Defensive patterns

Strategy: validation

Validate before calling

def attempts_ok(v):
    return not isinstance(v, bool) and isinstance(v, int) and v >= 0

Type guard

def safe_attempts(v, default=0):
    if isinstance(v, bool) or not isinstance(v, int) or v < 0:
        return default
    return v

Try / catch

try:
    delivery = DeliveryInfo.from_dict(raw)
except TypeError as exc:
    if "attempts" in str(exc):
        raw = dict(raw); raw["attempts"] = 0
        delivery = DeliveryInfo.from_dict(raw)
    else:
        raise

Prevention

When it happens

Trigger: delivery = {"attempts": -1}, {"attempts": "3"}, {"attempts": 2.0}, or {"attempts": True}. All four hit the combined isinstance/bool/negative guard.

Common situations: Decrementing attempts below zero in retry logic; JSON round-tripping ints as strings; flags accidentally stored in the attempts slot; floats after division-based backoff math.

Related errors


AI-assisted analysis of HKUDS/Vibe-Trading@80ffdda44c (2026-08-28). Data as JSON: /api/errors/d782ae66c3697be1. Report an issue: GitHub.