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

  1. Format timestamps as ISO strings via dt.isoformat(), or omit both keys to let the backend stamp them.
  2. During import, convert epochs: datetime.fromtimestamp(ts, tz=timezone.utc).isoformat().
  3. 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

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


AI-assisted analysis of bytedance/deer-flow@1dd6ba1acb (2026-08-14). Data as JSON: /api/errors/8f44f49962831d97. Report an issue: GitHub.