{"record":{"id":"ebc1514d7d9d1da5","repo":"bytedance/deer-flow","slug":"agent-name-is-required-to-persist-facts","errorCode":null,"errorMessage":"agent_name is required to persist facts","messagePattern":"agent_name is required to persist facts","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py","lineNumber":1087,"sourceCode":"        Repository callers should prefer ``apply_changes`` to avoid even that\n        full comparison scan.\n        \"\"\"\n        path = self._get_memory_file_path(agent_name, user_id=user_id)\n        key = self._cache_key(agent_name, user_id=user_id)\n        lock_path = path.parent / \".memory.lock\"\n        notifications: list[RetrievalNotification] = []\n        try:\n            if not isinstance(memory_data, dict):\n                raise ValueError(\"memory_data must be an object\")\n            if agent_name is not None and \"facts\" not in memory_data:\n                raise ValueError(\"memory_data.facts is required for an agent full save\")\n            facts_raw = memory_data.get(\"facts\", [])\n            if not isinstance(facts_raw, list):\n                raise ValueError(\"memory_data.facts must be a list\")\n            if any(not isinstance(fact, dict) for fact in facts_raw):\n                raise ValueError(\"memory_data.facts must contain only fact objects\")\n            if agent_name is None and facts_raw:\n                raise ValueError(\"agent_name is required to persist facts\")\n            with self._scope_lock(key), _process_file_lock(lock_path, float(getattr(self._config, \"file_lock_timeout_seconds\", 10))):\n                self._recover_if_needed(path)\n                ids = [str(fact.get(\"id\") or \"\") for fact in facts_raw]\n                if len(ids) != len(set(ids)):\n                    raise ValueError(\"Duplicate fact ids are not allowed\")\n                old_ids = set(self._agent_entries(path, agent_name, user_id=user_id)) if agent_name is not None else set()\n                summaries = None\n                if agent_name is None:\n                    summaries = {\"user\": memory_data.get(\"user\", {}), \"history\": memory_data.get(\"history\", {})}\n                _, notifications = self._commit_changes_locked(\n                    path,\n                    user_id=user_id,\n                    agent_name=agent_name,\n                    upserts=copy.deepcopy(facts_raw),\n                    deletes=sorted(old_ids - set(ids)),\n                    summaries=summaries,\n                    expected_revision=expected_revision,\n                )","sourceCodeStart":1069,"sourceCodeEnd":1105,"githubUrl":"https://github.com/bytedance/deer-flow/blob/1dd6ba1acb03700589994b0366c5d1c7d05e2eff/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py#L1069-L1105","documentation":"Raised by FileMemoryStorage.save() when the payload contains a non-empty facts list but agent_name is None. Facts in this backend are always scoped to an agent (per-agent memory files keyed by agent_name and user_id); there is no global fact repository. The guard runs before any locking or I/O, so nothing is written when it fires.","triggerScenarios":"storage.save({'facts': [...fact dicts...]}) with agent_name omitted/None (a user-global summary-only save), while still including facts in the payload.","commonSituations":"Copy-pasting an agent-scoped save call into a user-summary sync path; a caller that loads a full document (which includes facts) and re-saves it without the agent_name it loaded it with; refactoring that drops the agent_name parameter.","solutions":["Pass the agent_name you originally loaded the facts with: storage.save(memory_data, agent_name='my-agent', user_id=user_id).","If the save is intentionally user-global (summaries only), strip facts: save({'user': ..., 'history': ...}, user_id=...) with no 'facts' key.","Add a call-site assertion that agent_name is not None whenever facts are present."],"exampleFix":"# before\nstorage.save(memory_data, user_id=user_id)  # memory_data contains facts\n\n# after\nstorage.save(memory_data, agent_name=agent_name, user_id=user_id)","handlingStrategy":"validation","validationCode":"facts = memory_data.get(\"facts\") or []\nif facts and agent_name is None:\n    raise HTTPException(400, \"agent_name required when facts are present\")\nstorage.save(memory_data, agent_name=agent_name, user_id=user_id)","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Make agent_name a required keyword in wrappers that save fact-bearing documents.","Load and save with the same scope parameters in the same function.","Strip 'facts' from payloads destined for user-global summary saves."],"tags":["memory","validation","scoping","deermem"],"backgroundTag":null,"analyzedSha":"1dd6ba1acb03700589994b0366c5d1c7d05e2eff","analyzedAt":"2026-08-14T21:20:34.804Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}