{"record":{"id":"e14481cc7f65a367","repo":"HKUDS/Vibe-Trading","slug":"last-run-at-must-be-an-integer-epoch-ms-or-nul","errorCode":null,"errorMessage":"'last_run_at' must be an integer (epoch ms) or null","messagePattern":"'last_run_at' must be an integer \\(epoch ms\\) or null","errorType":"validation","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"agent/src/scheduled_research/models.py","lineNumber":444,"sourceCode":"            The reconstructed ``ScheduledResearchJob``.\n\n        Raises:\n            KeyError: If a required field is missing.\n            TypeError: If a field has the wrong type.\n            ValueError: If ``status`` is not a recognized ``JobStatus`` value.\n        \"\"\"\n        job_id = data[\"id\"]\n        prompt = data[\"prompt\"]\n        schedule = data[\"schedule\"]\n        if not isinstance(job_id, str) or not isinstance(prompt, str) or not isinstance(schedule, str):\n            raise TypeError(\"'id', 'prompt', and 'schedule' must be strings\")\n        next_run_at = data[\"next_run_at\"]\n        created_at = data[\"created_at\"]\n        if not isinstance(next_run_at, int) or not isinstance(created_at, int):\n            raise TypeError(\"'next_run_at' and 'created_at' must be integers (epoch ms)\")\n        last_run_at = data.get(\"last_run_at\")\n        if last_run_at is not None and not isinstance(last_run_at, int):\n            raise TypeError(\"'last_run_at' must be an integer (epoch ms) or null\")\n        consecutive_failures = data.get(\"consecutive_failures\", 0)\n        if (\n            isinstance(consecutive_failures, bool)\n            or not isinstance(consecutive_failures, int)\n            or consecutive_failures < 0\n        ):\n            raise TypeError(\"'consecutive_failures' must be a non-negative integer\")\n        last_error = data.get(\"last_error\")\n        failure_kind = data.get(\"failure_kind\")\n        if last_error is not None and not isinstance(last_error, str):\n            raise TypeError(\"'last_error' must be a string or null\")\n        if failure_kind is not None and failure_kind not in {\"dispatch\", \"schedule\"}:\n            raise ValueError(\"'failure_kind' must be 'dispatch', 'schedule', or null\")\n        # Never raises: the store quarantines the whole file when a single\n        # record fails to load, so an unusable timezone value degrades that\n        # one job to UTC — the semantics it had before the field existed —\n        # instead of taking every other job down with it. Absent, blank, and\n        # non-string values all normalize to None.","sourceCodeStart":426,"sourceCodeEnd":462,"githubUrl":"https://github.com/HKUDS/Vibe-Trading/blob/80ffdda44c5c4db0dd84d70e051cca591cea67df/agent/src/scheduled_research/models.py#L426-L462","documentation":"Raised by Job from_dict (agent/src/scheduled_research/models.py:444) when the optional 'last_run_at' field is present but is neither an int (epoch ms) nor null. Unlike the required timestamps, absence is fine (None); only a present wrong-typed value raises.","triggerScenarios":"{\"last_run_at\": \"1767225600000\"} or a float/ISO string in the last-run slot after a job has run at least once; absent key or explicit null is accepted.","commonSituations":"Same timestamp format drift as other fields but only appearing after the first run, so it surfaces late; retry writers formatting timestamps differently than the creation path.","solutions":["Use the same int-epoch-ms helper for all timestamp fields including last_run_at","Convert on write: int(dt.timestamp() * 1000)","Set it to null rather than 0 or \"\" when unknown"],"exampleFix":"// before\njob[\"last_run_at\"] = datetime.now(timezone.utc).isoformat()\n\n// after\njob[\"last_run_at\"] = int(datetime.now(timezone.utc).timestamp() * 1000)","handlingStrategy":"validation","validationCode":"def last_run_ok(d):\n    v = d.get(\"last_run_at\")\n    return v is None or (isinstance(v, int) and not isinstance(v, bool))","typeGuard":"def safe_last_run(v):\n    if v is None or isinstance(v, bool) or not isinstance(v, int):\n        return None\n    return v","tryCatchPattern":"try:\n    job = Job.from_dict(record)\nexcept TypeError as exc:\n    if \"last_run_at\" in str(exc):\n        record = dict(record); record[\"last_run_at\"] = None\n        job = Job.from_dict(record)\n    else:\n        raise","preventionTips":["Null out unknown timestamps instead of writing 0 or ''","Reuse the same timestamp helper for optional fields","Test deserialization after the first completed run"],"tags":["deserialization","timestamp","epoch-milliseconds","scheduled-research"],"backgroundTag":"timestamp-format-mismatch","analyzedSha":"80ffdda44c5c4db0dd84d70e051cca591cea67df","analyzedAt":"2026-08-28T12:46:38.989Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}