HKUDS/Vibe-Trading · error · ValueError

end_at must include an explicit timezone offset

Error message

end_at must include an explicit timezone offset

What it means

The end_at timestamp parsed successfully but carried no timezone information (no 'Z' suffix or ±HH:MM offset). Because the value is converted to epoch milliseconds, an implicit local-time interpretation would be ambiguous, so an explicit offset is required.

Source

Thrown at agent/src/scheduled_research/service.py:91

        running = bool(executor.is_running)
    return {"enabled": enabled, "running": running, "executable": enabled and running}


def _parse_end_at(value: Any) -> int | None:
    if value in (None, ""):
        return None
    if isinstance(value, bool):
        raise ValueError("end_at must be RFC3339 text or epoch milliseconds")
    if isinstance(value, int):
        return value
    if not isinstance(value, str):
        raise ValueError("end_at must be RFC3339 text or epoch milliseconds")
    try:
        parsed = datetime.fromisoformat(value.strip().replace("Z", "+00:00"))
    except ValueError as exc:
        raise ValueError("end_at must be a valid RFC3339 timestamp") from exc
    if parsed.tzinfo is None:
        raise ValueError("end_at must include an explicit timezone offset")
    return int(parsed.timestamp() * 1000)


def _origin_target(session_id: str | None) -> tuple[str, str, str | None, str]:
    if not session_id:
        raise ValueError("origin delivery requires the originating session")
    host = sys.modules.get("api_server") or sys.modules.get("agent.api_server")
    service = (
        host._get_session_service()
        if host and hasattr(host, "_get_session_service")
        else None
    )
    session = service.get_session(session_id) if service else None
    config = getattr(session, "config", None) or {}
    channel = config.get("channel")
    target = config.get("channel_chat_id")
    if not isinstance(channel, str) or not isinstance(target, str):
        raise ValueError("the originating session is not an IM conversation")

View on GitHub (pinned to 80ffdda44c)

Solutions

  1. Append the offset: use datetime.now(timezone.utc).isoformat() or add 'Z'
  2. Configure serializers to always include the zone
  3. Or pass epoch milliseconds, which are inherently UTC

Example fix

# before
datetime(2026,12,31).isoformat()  # naive
# after
datetime(2026,12,31,tzinfo=timezone.utc).isoformat()  # '2026-12-31T00:00:00+00:00'
Defensive patterns

Strategy: validation

Validate before calling

from datetime import datetime

def has_tz(s: str) -> bool:
    try:
        return datetime.fromisoformat(s.strip().replace("Z", "+00:00")).tzinfo is not None
    except ValueError:
        return False
assert has_tz(draft["end_at"])

Type guard

def is_timezone_aware_rfc3339(s: str) -> bool:
    try:
        return datetime.fromisoformat(s.strip().replace("Z", "+00:00")).tzinfo is not None
    except ValueError:
        return False

Try / catch

try:
    build_job_from_draft(draft)
except ValueError as e:
    if "explicit timezone" in str(e):
        draft["end_at"] += "+00:00"  # or re-format as UTC

Prevention

When it happens

Trigger: Passing '2026-12-31T00:00:00' (naive ISO string) without offset; clients stripping the timezone during serialization; date libraries defaulting to naive datetimes.

Common situations: Python datetime.isoformat() on a naive datetime; JavaScript date formatting that drops the zone; users in non-UTC zones getting silently shifted times, hence the strict rule.

Related errors


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