{"record":{"id":"a8b1a97a8166fbe9","repo":"bytedance/deer-flow","slug":"agent-name-only-exists-in-the-legacy-shared-la","errorCode":null,"errorMessage":"Agent '{name}' only exists in the legacy shared layout and is not scoped to a user. Run scripts/migrate_user_isolation.py to move legacy agents into the per-user layout before updating.","messagePattern":"Agent '(.+?)' only exists in the legacy shared layout and is not scoped to a user\\. Run scripts/migrate_user_isolation\\.py to move legacy agents into the per-user layout before updating\\.","errorType":"http","errorClass":"HTTPException","httpStatus":409,"severity":"error","filePath":"backend/app/gateway/routers/agents.py","lineNumber":395,"sourceCode":"        raise HTTPException(status_code=404, detail=f\"Agent '{name}' not found\")\n\n    def _is_legacy_only_layout() -> bool:\n        # Require config.yaml, not bare directory existence — a per-user agent\n        # directory can exist containing only memory.json (written the first\n        # time this user chats with a legacy shared agent, before this route\n        # is ever called). Bare .exists() would miss that case and let this\n        # fall through to a silent fork of a brand-new config.yaml/SOUL.md\n        # into the memory-only directory instead of blocking (mirrors\n        # resolve_agent_dir's guard, see #3390). The db backend has no legacy\n        # shared layout, so this file-only guard is a no-op there. The .exists()\n        # probes are filesystem IO, so they run off the event loop.\n        paths = get_paths()\n        agent_dir = paths.user_agent_dir(user_id, name)\n        legacy_dir = paths.agent_dir(name)\n        return not (agent_dir / \"config.yaml\").exists() and (legacy_dir / \"config.yaml\").exists()\n\n    if await asyncio.to_thread(_is_legacy_only_layout):\n        raise HTTPException(\n            status_code=409,\n            detail=(f\"Agent '{name}' only exists in the legacy shared layout and is not scoped to a user. Run scripts/migrate_user_isolation.py to move legacy agents into the per-user layout before updating.\"),\n        )\n\n    if \"model\" in request.model_fields_set:\n        _validate_model_exists(request.model)\n\n    try:\n        # Update config if any config fields changed\n        # Use model_fields_set to distinguish \"field omitted\" from \"explicitly set to null\".\n        # This is critical for skills where None means \"inherit all\" (not \"don't change\").\n        fields_set = request.model_fields_set\n        config_changed = bool(fields_set & ({\"description\", \"tool_groups\", \"skills\"} | set(_MODEL_BEHAVIOR_FIELDS)))\n\n        updated: dict | None = None\n        if config_changed:\n            updated = {\n                \"name\": agent_cfg.name,","sourceCodeStart":377,"sourceCodeEnd":413,"githubUrl":"https://github.com/bytedance/deer-flow/blob/1dd6ba1acb03700589994b0366c5d1c7d05e2eff/backend/app/gateway/routers/agents.py#L377-L413","documentation":"409 from PUT `/agents/{name}`: the agent exists only in the legacy shared layout — `user_agent_dir/user/config.yaml` is missing while the shared `agent_dir/config.yaml` exists. Note the guard deliberately checks the config file, not directory existence, because per-user dirs can hold only `memory.json` from chatting with a legacy agent; a bare `.exists()` would silently fork a fresh config into a memory-only dir (issue #3390). Updates are blocked until migration; the db backend has no legacy layout so this is file-backend-only.","triggerScenarios":"Upgrading a deployment that predates per-user agent isolation, then editing one of the old shared agents; a user who has chatted with a legacy agent (memory.json written) attempting to update it.","commonSituations":"Post-upgrade administrations hitting 409 on all pre-existing agents; mixed fleets where only some nodes ran the migration script.","solutions":["Run `scripts/migrate_user_isolation.py` to move legacy agents into the per-user layout, then retry the PUT","Verify migration succeeded by confirming `user_agent_dir/<user>/<name>/config.yaml` now exists","Do not hand-copy config.yaml into the memory-only dir — that recreates the silent-fork bug the guard prevents"],"exampleFix":"# before\nPUT /agents/old-shared-agent  # 409 legacy layout\n# after\npython scripts/migrate_user_isolation.py\nPUT /agents/old-shared-agent  # 200","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try { return await api.updateAgent(name, body); }\ncatch (e) {\n  if (e.status === 409 && /legacy shared layout/.test(e.detail)) {\n    showAdminNotice('Run scripts/migrate_user_isolation.py, then retry');\n    return;\n  }\n  throw e;\n}","preventionTips":["Run migrate_user_isolation.py as a mandatory step of any upgrade that introduces per-user agents","Never hand-copy config.yaml into a memory-only per-user agent dir — it triggers the silent-fork bug","Test update flows against the oldest deployment data you support"],"tags":["http-409","agents","migration","legacy-layout","user-isolation"],"backgroundTag":null,"analyzedSha":"1dd6ba1acb03700589994b0366c5d1c7d05e2eff","analyzedAt":"2026-08-14T21:20:34.804Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}