{"record":{"id":"8f44f49962831d97","repo":"bytedance/deer-flow","slug":"fact-createdat-and-fact-updatedat-must-be-strings","errorCode":null,"errorMessage":"fact.createdAt and fact.updatedAt must be strings","messagePattern":"fact\\.createdAt and fact\\.updatedAt must be strings","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py","lineNumber":250,"sourceCode":"        normalized[\"revision\"] = revision\n    else:\n        existing_revision = existing.get(\"revision\")\n        if not isinstance(existing_revision, int) or existing_revision < 1:\n            raise MemoryStorageCorruption(f\"Stored fact {normalized['id']!r} has an invalid revision\")\n        if revision != existing_revision:\n            raise MemoryFactRevisionConflict(f\"Expected fact {normalized['id']!r} revision {revision}, found {existing_revision}\")\n        normalized[\"createdAt\"] = existing.get(\"createdAt\") or normalized.get(\"createdAt\") or now\n        comparison_keys = {\"revision\", \"updatedAt\"}\n        incoming_material = {key: value for key, value in normalized.items() if key not in comparison_keys}\n        existing_material = {key: value for key, value in existing.items() if key not in comparison_keys}\n        if incoming_material == existing_material:\n            normalized[\"revision\"] = existing_revision\n            normalized[\"updatedAt\"] = existing.get(\"updatedAt\") or normalized[\"createdAt\"]\n        else:\n            normalized[\"revision\"] = existing_revision + 1\n            normalized[\"updatedAt\"] = now\n    if not isinstance(normalized.get(\"createdAt\"), str) or not isinstance(normalized.get(\"updatedAt\"), str):\n        raise ValueError(\"fact.createdAt and fact.updatedAt must be strings\")\n    if normalized[\"consolidatedFrom\"]:\n        normalized.setdefault(\"consolidatedAt\", normalized[\"updatedAt\"])\n    return normalized\n\n\ndef _safe_relative_path(root: Path, relative: str, *, label: str) -> Path:\n    \"\"\"Resolve an untrusted persisted relative path without leaving root.\"\"\"\n    candidate = Path(relative)\n    if candidate.is_absolute():\n        raise MemoryStorageCorruption(f\"{label} path escapes the user memory directory: {relative!r}\")\n    root_resolved = root.resolve()\n    resolved = (root / candidate).resolve()\n    try:\n        resolved.relative_to(root_resolved)\n    except ValueError as exc:\n        raise MemoryStorageCorruption(f\"{label} path escapes the user memory directory: {relative!r}\") from exc\n    return resolved\n","sourceCodeStart":232,"sourceCodeEnd":268,"githubUrl":"https://github.com/bytedance/deer-flow/blob/1dd6ba1acb03700589994b0366c5d1c7d05e2eff/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py#L232-L268","documentation":"After normalization and merge, fact['createdAt'] and fact['updatedAt'] must both be strings. The backend fills them with ISO-8601 UTC 'Z' timestamps when absent, so this fires when the caller supplies non-string values (ints/None/objects) that survive the defaults, or a stored record contributed non-string timestamps during a rebase.","triggerScenarios":"Saving {'createdAt': 1710000000} (epoch int) or {'createdAt': None} on the incoming dict while an existing record also lacks valid timestamps; imports using datetime objects rather than their ISO strings.","commonSituations":"Producers serializing datetimes as epoch numbers; ORM-style records leaking datetime objects; hand-built facts copying timestamps from another system's numeric format.","solutions":["Format timestamps as ISO strings via dt.isoformat(), or omit both keys to let the backend stamp them.","During import, convert epochs: datetime.fromtimestamp(ts, tz=timezone.utc).isoformat().","Leave timestamp management to the backend unless you specifically need to preserve source times."],"exampleFix":"# before\nmemory.save_fact({\"content\": \"...\", \"createdAt\": 1710000000})\n# after\nfrom datetime import datetime, timezone\nmemory.save_fact({\"content\": \"...\", \"createdAt\": datetime.fromtimestamp(1710000000, tz=timezone.utc).isoformat()})","handlingStrategy":"validation","validationCode":"from datetime import datetime, timezone\nfor key in (\"createdAt\", \"updatedAt\"):\n    v = fact.get(key)\n    if v is not None and not isinstance(v, str):\n        fact[key] = datetime.fromtimestamp(v, tz=timezone.utc).isoformat() if isinstance(v, (int, float)) else str(v)","typeGuard":"def has_string_timestamps(fact: dict) -> bool:\n    return isinstance(fact.get(\"createdAt\", \"\"), str) and isinstance(fact.get(\"updatedAt\", \"\"), str)","tryCatchPattern":null,"preventionTips":["Omit createdAt/updatedAt and let the backend stamp them.","Serialize datetimes with .isoformat() at the boundary; convert epochs to ISO strings during import."],"tags":["deermem","memory","validation","timestamps"],"backgroundTag":null,"analyzedSha":"1dd6ba1acb03700589994b0366c5d1c7d05e2eff","analyzedAt":"2026-08-14T21:20:34.804Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}