HKUDS/Vibe-Trading · error · TypeError

'last_error' must be a string or null

Error message

'last_error' must be a string or null

What it means

Raised by Job from_dict (agent/src/scheduled_research/models.py:455) when the optional 'last_error' field is present but is not a string and not null. It holds the message of the most recent failure; dicts, lists, or exception objects serialized as structures fail this check.

Source

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

            raise TypeError("'id', 'prompt', and 'schedule' must be strings")
        next_run_at = data["next_run_at"]
        created_at = data["created_at"]
        if not isinstance(next_run_at, int) or not isinstance(created_at, int):
            raise TypeError("'next_run_at' and 'created_at' must be integers (epoch ms)")
        last_run_at = data.get("last_run_at")
        if last_run_at is not None and not isinstance(last_run_at, int):
            raise TypeError("'last_run_at' must be an integer (epoch ms) or null")
        consecutive_failures = data.get("consecutive_failures", 0)
        if (
            isinstance(consecutive_failures, bool)
            or not isinstance(consecutive_failures, int)
            or consecutive_failures < 0
        ):
            raise TypeError("'consecutive_failures' must be a non-negative integer")
        last_error = data.get("last_error")
        failure_kind = data.get("failure_kind")
        if last_error is not None and not isinstance(last_error, str):
            raise TypeError("'last_error' must be a string or null")
        if failure_kind is not None and failure_kind not in {"dispatch", "schedule"}:
            raise ValueError("'failure_kind' must be 'dispatch', 'schedule', or null")
        # Never raises: the store quarantines the whole file when a single
        # record fails to load, so an unusable timezone value degrades that
        # one job to UTC — the semantics it had before the field existed —
        # instead of taking every other job down with it. Absent, blank, and
        # non-string values all normalize to None.
        raw_tz = data.get("timezone")
        tz = raw_tz if isinstance(raw_tz, str) and raw_tz.strip() else None
        if raw_tz is not None and tz is None:
            logger.warning(
                "scheduled research job %s has an unusable timezone %r; "
                "evaluating its schedule in UTC",
                job_id,
                raw_tz,
            )
        status = JobStatus(data["status"])
        title = data.get("title", "")

View on GitHub (pinned to 80ffdda44c)

Solutions

  1. Store str(exc) or exc.__repr__() as a plain string
  2. Flatten structured error info into one message line
  3. Omit/null the field when there is no error

Example fix

// before
record["last_error"] = {"msg": str(exc)}

// after
record["last_error"] = str(exc)
Defensive patterns

Strategy: validation

Validate before calling

def last_error_ok(d):
    v = d.get("last_error")
    return v is None or isinstance(v, str)

Type guard

def safe_last_error(v):
    return v if isinstance(v, str) else (None if v is None else str(v))

Try / catch

try:
    job = Job.from_dict(record)
except TypeError as exc:
    if "last_error" in str(exc):
        record = dict(record); record["last_error"] = None
        job = Job.from_dict(record)
    else:
        raise

Prevention

When it happens

Trigger: {"last_error": {"type": "TimeoutError", "msg": "..."}} or storing repr output of an exception object that isn't a plain string; absent/null is fine.

Common situations: Structured error logging leaking into the field; passing an exception instance where str(exc) was intended.

Related errors


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