{"record":{"id":"65407a651d6f2280","repo":"bytedance/deer-flow","slug":"fact-revision-must-be-an-integer-1","errorCode":null,"errorMessage":"fact.revision must be an integer >= 1","messagePattern":"fact\\.revision must be an integer >= 1","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py","lineNumber":212,"sourceCode":"        raise ValueError(\"fact.content must be a string\")\n    normalized[\"content\"] = normalized[\"content\"].strip()\n    if not normalized[\"content\"]:\n        raise ValueError(\"fact.content must not be empty\")\n    _normalize_category(normalized)\n    confidence = normalized.get(\"confidence\", 0.5)\n    if isinstance(confidence, bool) or not isinstance(confidence, (int, float)) or not 0 <= confidence <= 1:\n        raise ValueError(\"fact.confidence must be a number between 0 and 1\")\n    normalized[\"confidence\"] = float(confidence)\n    status = normalized.get(\"status\", \"active\")\n    if status != \"active\":\n        raise ValueError(\"fact.status must be 'active'; deletion is physical\")\n    normalized[\"status\"] = \"active\"\n    normalized[\"scope\"] = copy.deepcopy(scope)\n    _require_string_list(normalized, \"topics\")\n    _require_string_list(normalized, \"consolidatedFrom\")\n    revision = normalized.get(\"revision\", 1)\n    if isinstance(revision, bool) or not isinstance(revision, int) or revision < 1:\n        raise ValueError(\"fact.revision must be an integer >= 1\")\n    source = normalized.get(\"source\")\n    if isinstance(source, str):\n        if source in {\"manual\", \"consolidation\", \"import\", \"unknown\"}:\n            normalized[\"source\"] = {\"type\": source, \"threadId\": None}\n        else:\n            normalized[\"source\"] = {\"type\": \"conversation\", \"threadId\": source}\n    elif not isinstance(source, dict):\n        normalized[\"source\"] = {\"type\": \"unknown\", \"threadId\": None}\n    else:\n        normalized[\"source\"].setdefault(\"type\", \"unknown\")\n        if not isinstance(normalized[\"source\"].get(\"type\"), str):\n            raise ValueError(\"fact.source.type must be a string\")\n        if normalized[\"source\"].get(\"threadId\") is not None and not isinstance(normalized[\"source\"].get(\"threadId\"), str):\n            raise ValueError(\"fact.source.threadId must be a string or null\")\n    normalized[\"title\"] = _fact_title(normalized)\n    now = utc_now_iso_z()\n    if existing is None:\n        normalized.setdefault(\"createdAt\", now)","sourceCodeStart":194,"sourceCodeEnd":230,"githubUrl":"https://github.com/bytedance/deer-flow/blob/1dd6ba1acb03700589994b0366c5d1c7d05e2eff/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py#L194-L230","documentation":"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.","triggerScenarios":"Saving {'revision': 0} or {'revision': '3'}; computing revision as a float; passing True (bool) after arithmetic on flags.","commonSituations":"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.","solutions":["Omit revision for new facts (defaults to 1); echo the exact integer you read from the stored fact for updates.","Coerce before save: fact['revision'] = int(fact['revision']) and verify >= 1.","Do not invent revisions client-side - they are compared for equality against stored state, so guessed values cause conflicts (see error 496)."],"exampleFix":"# before\nmemory.save_fact({\"content\": \"...\", \"revision\": \"2\"})\n# after\nmemory.save_fact({\"content\": \"...\", \"revision\": 2})","handlingStrategy":"validation","validationCode":"rev = fact.get(\"revision\", 1)\nif isinstance(rev, bool) or not isinstance(rev, int) or rev < 1:\n    fact[\"revision\"] = max(1, int(rev)) if not isinstance(rev, bool) and isinstance(rev, (int, float)) else 1","typeGuard":"def is_valid_revision(v: object) -> bool:\n    return not isinstance(v, bool) and isinstance(v, int) and v >= 1","tryCatchPattern":null,"preventionTips":["Omit revision for creates; echo the exact integer read from storage for updates.","Never compute or guess revisions client-side."],"tags":["deermem","memory","validation","optimistic-concurrency"],"backgroundTag":null,"analyzedSha":"1dd6ba1acb03700589994b0366c5d1c7d05e2eff","analyzedAt":"2026-08-14T21:20:34.804Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}