{"record":{"id":"3d45a473341d1a75","repo":"bytedance/deer-flow","slug":"retrieval-fact-content-must-be-a-non-empty-string","errorCode":null,"errorMessage":"retrieval fact.content must be a non-empty string","messagePattern":"retrieval fact\\.content must be a non-empty string","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/retrieval.py","lineNumber":576,"sourceCode":"    document id prevents equal fact ids in two user/agent scopes from\n    overwriting each other.\n    \"\"\"\n\n    def __init__(self, db_path: str | Path = \":memory:\") -> None:\n        self._engine = FTS5Retrieval(db_path)\n\n    @staticmethod\n    def _document_id(fact_id: str, scope: dict[str, str | None]) -> str:\n        scope_user, scope_agent = _scope_key(scope)\n        return json.dumps([scope_user, scope_agent, fact_id], ensure_ascii=False, separators=(\",\", \":\"))\n\n    def _document(self, fact: dict[str, Any], scope: dict[str, str | None]) -> dict[str, Any]:\n        fact_id = fact.get(\"id\")\n        content = fact.get(\"content\")\n        if not isinstance(fact_id, str) or not fact_id:\n            raise ValueError(\"retrieval fact.id must be a non-empty string\")\n        if not isinstance(content, str) or not content.strip():\n            raise ValueError(\"retrieval fact.content must be a non-empty string\")\n        scope_user, scope_agent = _scope_key(scope)\n        payload = dict(fact)\n        payload[\"scope\"] = {\"userId\": scope.get(\"userId\"), \"agentName\": scope.get(\"agentName\")}\n        source = payload.get(\"source\")\n        return {\n            \"fact_id\": self._document_id(fact_id, scope),\n            \"content\": content,\n            \"category\": str(payload.get(\"category\") or \"context\"),\n            \"confidence\": float(payload.get(\"confidence\") or 0.5),\n            \"created_at\": payload.get(\"createdAt\") if isinstance(payload.get(\"createdAt\"), str) else None,\n            \"scope_user\": scope_user,\n            \"scope_agent\": scope_agent,\n            \"source\": source if isinstance(source, str) else json.dumps(source, ensure_ascii=False, default=str),\n            \"fact_data\": payload,\n        }\n\n    def upsert(self, fact: dict[str, Any], *, scope: dict[str, str | None], path: str) -> None:\n        del path  # Canonical location belongs to storage; the index is rebuildable.","sourceCodeStart":558,"sourceCodeEnd":594,"githubUrl":"https://github.com/bytedance/deer-flow/blob/1dd6ba1acb03700589994b0366c5d1c7d05e2eff/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/retrieval.py#L558-L594","documentation":"ValueError from FTS5RetrievalAdapter._document: a fact's 'content' is missing, not a string, empty, or whitespace-only. Content is what FTS5 indexes, so a contentless fact is unindexable and rejected rather than stored as a dead row.","triggerScenarios":"Indexing a fact dict whose content is None, '', '   ', or a non-str value (e.g. a dict/list payload put under 'content').","commonSituations":"Fact extraction emitting empty content for structured events; content field holding structured data instead of text; upstream summarization produced an empty string; whitespace-only content from a template render that substituted nothing.","solutions":["Skip facts with blank content before enqueueing for indexing (they carry nothing searchable).","If content is structured, serialize it to a meaningful text representation first.","Fix the extractor so it never emits empty-content facts (guard at the source)."],"exampleFix":"# before\nfor fact in facts:\n    adapter.upsert(fact, scope)  # raises on blank content\n\n# after\nfor fact in facts:\n    if isinstance(fact.get(\"content\"), str) and fact[\"content\"].strip():\n        adapter.upsert(fact, scope)","handlingStrategy":"validation","validationCode":"def has_searchable_content(fact) -> bool:\n    c = fact.get('content') if isinstance(fact, dict) else None\n    return isinstance(c, str) and bool(c.strip())","typeGuard":"def has_searchable_content(fact: dict) -> bool:\n    c = fact.get('content')\n    return isinstance(c, str) and c.strip() != ''","tryCatchPattern":"try:\n    adapter.upsert(fact, scope)\nexcept ValueError as e:\n    if 'fact.content' in str(e):\n        logger.warning('skipping contentless fact id=%s', fact.get('id'))\n        continue\n    raise","preventionTips":["Skip blank-content facts at the pipeline stage, before the retrieval adapter sees them.","Serialize structured fact payloads into text rather than storing raw objects under content.","Log skip counts — a spike means the extractor is degrading."],"tags":["validation","retrieval","deermem","memory","typing"],"backgroundTag":null,"analyzedSha":"1dd6ba1acb03700589994b0366c5d1c7d05e2eff","analyzedAt":"2026-08-14T21:20:34.804Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}