{"record":{"id":"10030e2f175cceef","repo":"bytedance/deer-flow","slug":"retrieval-fact-id-must-be-a-non-empty-string","errorCode":null,"errorMessage":"retrieval fact.id must be a non-empty string","messagePattern":"retrieval fact\\.id 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":574,"sourceCode":"    The index is derived data. Canonical facts remain in Markdown and storage\n    notifications update only the addressed row. A deterministic composite\n    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","sourceCodeStart":556,"sourceCodeEnd":592,"githubUrl":"https://github.com/bytedance/deer-flow/blob/1dd6ba1acb03700589994b0366c5d1c7d05e2eff/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/retrieval.py#L556-L592","documentation":"ValueError from FTS5RetrievalAdapter._document: a fact dict passed for indexing has an 'id' that is not a non-empty string (missing, None, empty, or non-str). The id becomes part of the JSON composite document key (scope_user, scope_agent, fact_id), so it must be a stable non-empty string.","triggerScenarios":"Calling the adapter's indexing/upsert path with facts like {\"content\": \"...\"} (no id), {\"id\": \"\", ...}, or {\"id\": 42, ...}.","commonSituations":"Facts extracted by the memory pipeline whose id field was never populated; id named differently (fact_id, key) and not mapped; None id from a failed upstream generation; numeric ids from a database row passed raw.","solutions":["Ensure every fact carries a unique non-empty string id before indexing (generate one if missing: uuid4 hex).","Map upstream field names to 'id' explicitly when converting rows to fact dicts.","Validate the fact batch before submission: reject/log entries failing the id check."],"exampleFix":"# before\nadapter.upsert({\"content\": \"user lives in Berlin\"}, scope)  # no id\n\n# after\nimport uuid\nadapter.upsert({\"id\": uuid.uuid4().hex, \"content\": \"user lives in Berlin\"}, scope)","handlingStrategy":"validation","validationCode":"def is_indexable_fact(fact) -> bool:\n    return isinstance(fact, dict) and isinstance(fact.get('id'), str) and bool(fact['id'].strip())","typeGuard":"def is_indexable_fact(fact: dict) -> bool:\n    fid = fact.get('id') if isinstance(fact, dict) else None\n    return isinstance(fid, str) and len(fid) > 0","tryCatchPattern":"try:\n    adapter.upsert(fact, scope)\nexcept ValueError as e:\n    if 'fact.id' in str(e):\n        logger.warning('skipping fact without usable id: %r', fact)\n        continue  # bad fact must not kill the whole indexing batch\n    raise","preventionTips":["Guarantee id at extraction time: every fact gets a uuid before leaving the extractor.","Map DB rows to {'id': str(row_pk), ...} explicitly.","Batch-validate facts and quarantine failures instead of aborting the run."],"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"}