bytedance/deer-flow · error · ValueError
fact.createdAt and fact.updatedAt must be strings
Error message
fact.createdAt and fact.updatedAt must be strings
What it means
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.
Source
Thrown at backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py:250
normalized["revision"] = revision
else:
existing_revision = existing.get("revision")
if not isinstance(existing_revision, int) or existing_revision < 1:
raise MemoryStorageCorruption(f"Stored fact {normalized['id']!r} has an invalid revision")
if revision != existing_revision:
raise MemoryFactRevisionConflict(f"Expected fact {normalized['id']!r} revision {revision}, found {existing_revision}")
normalized["createdAt"] = existing.get("createdAt") or normalized.get("createdAt") or now
comparison_keys = {"revision", "updatedAt"}
incoming_material = {key: value for key, value in normalized.items() if key not in comparison_keys}
existing_material = {key: value for key, value in existing.items() if key not in comparison_keys}
if incoming_material == existing_material:
normalized["revision"] = existing_revision
normalized["updatedAt"] = existing.get("updatedAt") or normalized["createdAt"]
else:
normalized["revision"] = existing_revision + 1
normalized["updatedAt"] = now
if not isinstance(normalized.get("createdAt"), str) or not isinstance(normalized.get("updatedAt"), str):
raise ValueError("fact.createdAt and fact.updatedAt must be strings")
if normalized["consolidatedFrom"]:
normalized.setdefault("consolidatedAt", normalized["updatedAt"])
return normalized
def _safe_relative_path(root: Path, relative: str, *, label: str) -> Path:
"""Resolve an untrusted persisted relative path without leaving root."""
candidate = Path(relative)
if candidate.is_absolute():
raise MemoryStorageCorruption(f"{label} path escapes the user memory directory: {relative!r}")
root_resolved = root.resolve()
resolved = (root / candidate).resolve()
try:
resolved.relative_to(root_resolved)
except ValueError as exc:
raise MemoryStorageCorruption(f"{label} path escapes the user memory directory: {relative!r}") from exc
return resolved
View on GitHub (pinned to 1dd6ba1acb)
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.
Example fix
# before
memory.save_fact({"content": "...", "createdAt": 1710000000})
# after
from datetime import datetime, timezone
memory.save_fact({"content": "...", "createdAt": datetime.fromtimestamp(1710000000, tz=timezone.utc).isoformat()}) Defensive patterns
Strategy: validation
Validate before calling
from datetime import datetime, timezone
for key in ("createdAt", "updatedAt"):
v = fact.get(key)
if v is not None and not isinstance(v, str):
fact[key] = datetime.fromtimestamp(v, tz=timezone.utc).isoformat() if isinstance(v, (int, float)) else str(v) Type guard
def has_string_timestamps(fact: dict) -> bool:
return isinstance(fact.get("createdAt", ""), str) and isinstance(fact.get("updatedAt", ""), str) Prevention
- Omit createdAt/updatedAt and let the backend stamp them.
- Serialize datetimes with .isoformat() at the boundary; convert epochs to ISO strings during import.
When it happens
Trigger: 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.
Common situations: Producers serializing datetimes as epoch numbers; ORM-style records leaking datetime objects; hand-built facts copying timestamps from another system's numeric format.
Related errors
- retrieval fact.id must be a non-empty string
- retrieval fact.content must be a non-empty string
- unsupported FTS5 retrieval mode: {mode}
- retrieval category filter must be a string
- fact.category must be a string
AI-assisted analysis of bytedance/deer-flow@1dd6ba1acb (2026-08-14).
Data as JSON: /api/errors/8f44f49962831d97.
Report an issue: GitHub.