{"record":{"id":"076f210622c6a69a","repo":"bytedance/deer-flow","slug":"fact-content-must-not-be-empty","errorCode":null,"errorMessage":"fact.content must not be empty","messagePattern":"fact\\.content must not be empty","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py","lineNumber":197,"sourceCode":"    \"\"\"Validate one fact and derive its per-item revision.\n\n    The shared JSON revision protects the multi-file transaction.  The fact's\n    own revision protects one Markdown object when a disjoint transaction is\n    safely rebased after that shared revision changed.\n    \"\"\"\n    if not isinstance(fact, dict):\n        raise ValueError(\"fact must be an object\")\n    normalized = copy.deepcopy(fact)\n    normalized[\"id\"] = str(normalized.get(\"id\") or f\"fact_{uuid.uuid4().hex}\")\n    # Validate the id through the canonical path builder's public contract.\n    if not normalized[\"id\"] or any(character not in \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-\" for character in normalized[\"id\"]):\n        raise ValueError(\"fact.id may contain only letters, numbers, '_' and '-'\")\n    normalized[\"schemaVersion\"] = 2\n    if not isinstance(normalized.get(\"content\"), str):\n        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\"}:","sourceCodeStart":179,"sourceCodeEnd":215,"githubUrl":"https://github.com/bytedance/deer-flow/blob/1dd6ba1acb03700589994b0366c5d1c7d05e2eff/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py#L179-L215","documentation":"After stripping whitespace, fact['content'] must be non-empty; a whitespace-only or empty string raises ValueError. Empty facts would corrupt dedup, titles (which derive from the first content line), and retrieval scoring, so they are rejected rather than stored as no-ops.","triggerScenarios":"Saving {'content': ''}, {'content': '   \\n  '}, or content assembled by joining an empty list of extracted sentences.","commonSituations":"Extraction pipelines that emit empty strings when nothing memorable was found; template rendering producing only whitespace; users submitting blank form fields.","solutions":["Guard at the call site: if not content.strip(), skip the save (this is a no-op case, not an error to surface).","Fix the extractor to omit empty extraction results instead of materializing them as facts.","Trim user input earlier and validate non-empty at the form/API boundary."],"exampleFix":"# before\nmemory.save_fact({\"content\": extracted_text})  # extracted_text == \"\"\n# after\nif extracted_text and extracted_text.strip():\n    memory.save_fact({\"content\": extracted_text})","handlingStrategy":"validation","validationCode":"content = fact.get(\"content\")\nif not isinstance(content, str) or not content.strip():\n    return  # nothing memorable - skip the save entirely","typeGuard":"def has_nonempty_content(fact: dict) -> bool:\n    return isinstance(fact.get(\"content\"), str) and bool(fact[\"content\"].strip())","tryCatchPattern":null,"preventionTips":["Skip empty extraction results instead of materializing empty facts.","Validate non-empty trimmed input at the form/API boundary."],"tags":["deermem","memory","validation","empty-input"],"backgroundTag":null,"analyzedSha":"1dd6ba1acb03700589994b0366c5d1c7d05e2eff","analyzedAt":"2026-08-14T21:20:34.804Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}