{"record":{"id":"66a5742ec71f69ca","repo":"bytedance/deer-flow","slug":"fact-status-must-be-active-deletion-is-physical","errorCode":null,"errorMessage":"fact.status must be 'active'; deletion is physical","messagePattern":"fact\\.status must be 'active'; deletion is physical","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py","lineNumber":205,"sourceCode":"    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\"}:\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):","sourceCodeStart":187,"sourceCodeEnd":223,"githubUrl":"https://github.com/bytedance/deer-flow/blob/1dd6ba1acb03700589994b0366c5d1c7d05e2eff/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py#L187-L223","documentation":"The v2 storage schema has no soft-delete: fact['status'] must be exactly 'active' (the default), and anything else raises ValueError with 'deletion is physical'. Facts are removed by deleting their records; a status field carrying 'deleted'/'archived' indicates an old or wrong producer.","triggerScenarios":"Saving {'status': 'deleted'}, {'status': 'inactive'}, or forwarding v1 facts whose status was set to soft-deleted during an export/migration that skipped the physical delete.","commonSituations":"Porting data from a system with soft deletes; an LLM or script 'deleting' a fact by flipping status instead of calling the delete API.","solutions":["Remove the status key (or set it to 'active') for facts you intend to store; use the backend's delete/remove API to delete, which physically removes the record.","In migration code, filter out facts with non-active status instead of importing them.","If you need archiving, model it with a category or custom field, not status."],"exampleFix":"# before\nmemory.save_fact({\"content\": \"...\", \"status\": \"archived\"})\n# after\nmemory.save_fact({\"content\": \"...\"})  # status defaults to 'active'; use memory.delete_fact(id) to remove","handlingStrategy":"validation","validationCode":"status = fact.get(\"status\", \"active\")\nif status != \"active\":\n    if status in {\"deleted\", \"archived\", \"inactive\"}:\n        return  # drop soft-deleted facts on import; deletion here is physical\n    fact[\"status\"] = \"active\"","typeGuard":"def is_active_status(fact: dict) -> bool:\n    return fact.get(\"status\", \"active\") == \"active\"","tryCatchPattern":null,"preventionTips":["Use the delete API to remove facts; never flip status.","Filter non-active facts out of migration/import streams."],"tags":["deermem","memory","validation","schema"],"backgroundTag":null,"analyzedSha":"1dd6ba1acb03700589994b0366c5d1c7d05e2eff","analyzedAt":"2026-08-14T21:20:34.804Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}