bytedance/deer-flow · error · ValueError

fact.revision must be an integer >= 1

Error message

fact.revision must be an integer >= 1

What it means

fact['revision'] (default 1) must be an int >= 1, with bool explicitly rejected. The revision implements optimistic concurrency per fact: callers echo the revision they read, and the backend verifies it against the stored copy. Fractional, zero, negative, or string revisions break that protocol.

Solutions

  1. Omit revision for new facts (defaults to 1); echo the exact integer you read from the stored fact for updates.
  2. Coerce before save: fact['revision'] = int(fact['revision']) and verify >= 1.
  3. Do not invent revisions client-side - they are compared for equality against stored state, so guessed values cause conflicts (see error 496).

Example fix

# before
memory.save_fact({"content": "...", "revision": "2"})
# after
memory.save_fact({"content": "...", "revision": 2})
Defensive patterns

Strategy: validation

Validate before calling

rev = fact.get("revision", 1)
if isinstance(rev, bool) or not isinstance(rev, int) or rev < 1:
    fact["revision"] = max(1, int(rev)) if not isinstance(rev, bool) and isinstance(rev, (int, float)) else 1

Type guard

def is_valid_revision(v: object) -> bool:
    return not isinstance(v, bool) and isinstance(v, int) and v >= 1

Prevention

When it happens

Trigger: Saving {'revision': 0} or {'revision': '3'}; computing revision as a float; passing True (bool) after arithmetic on flags.

Common situations: Clients that treat revision as optional metadata and send 0 for 'new'; spreadsheets/CSV exports turning ints into strings; producers copying updatedAt timestamps into revision.

Related errors


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

Appendix: source

Thrown at backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py:212

        raise ValueError("fact.content must be a string")
    normalized["content"] = normalized["content"].strip()
    if not normalized["content"]:
        raise ValueError("fact.content must not be empty")
    _normalize_category(normalized)
    confidence = normalized.get("confidence", 0.5)
    if isinstance(confidence, bool) or not isinstance(confidence, (int, float)) or not 0 <= confidence <= 1:
        raise ValueError("fact.confidence must be a number between 0 and 1")
    normalized["confidence"] = float(confidence)
    status = normalized.get("status", "active")
    if status != "active":
        raise ValueError("fact.status must be 'active'; deletion is physical")
    normalized["status"] = "active"
    normalized["scope"] = copy.deepcopy(scope)
    _require_string_list(normalized, "topics")
    _require_string_list(normalized, "consolidatedFrom")
    revision = normalized.get("revision", 1)
    if isinstance(revision, bool) or not isinstance(revision, int) or revision < 1:
        raise ValueError("fact.revision must be an integer >= 1")
    source = normalized.get("source")
    if isinstance(source, str):
        if source in {"manual", "consolidation", "import", "unknown"}:
            normalized["source"] = {"type": source, "threadId": None}
        else:
            normalized["source"] = {"type": "conversation", "threadId": source}
    elif not isinstance(source, dict):
        normalized["source"] = {"type": "unknown", "threadId": None}
    else:
        normalized["source"].setdefault("type", "unknown")
        if not isinstance(normalized["source"].get("type"), str):
            raise ValueError("fact.source.type must be a string")
        if normalized["source"].get("threadId") is not None and not isinstance(normalized["source"].get("threadId"), str):
            raise ValueError("fact.source.threadId must be a string or null")
    normalized["title"] = _fact_title(normalized)
    now = utc_now_iso_z()
    if existing is None:
        normalized.setdefault("createdAt", now)

View on GitHub (pinned to 1dd6ba1acb)