From dd06f923cd13fbdcf8c4acfd86987a9db2c52bd6 Mon Sep 17 00:00:00 2001 From: Robert Helewka Date: Wed, 17 Jun 2026 20:26:43 -0400 Subject: [PATCH] feat(workspaces): return 409 name_conflict instead of 500 on Library name clash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A recreate of a workspace whose Mnemosyne Library was orphaned (left behind by a failed Daedalus delete-propagate) collides on the global Library.name unique constraint. neomodel raised UniqueProperty unguarded, so workspace_create 500'd and ingest then 404'd forever — the queue froze silently. Guard lib.save() and return a structured 409 with a machine code so Daedalus can classify the failure without string-matching: - name_conflict — the new name-collision case - owner_conflict, library_type_immutable — codes added to the two existing 409s Cypher-touching paths stay covered by the manual end-to-end plan, per the test module's stated convention. Co-Authored-By: Claude Opus 4.8 --- mnemosyne/library/api/workspaces.py | 33 ++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/mnemosyne/library/api/workspaces.py b/mnemosyne/library/api/workspaces.py index 1d91d2b..c7ee4c5 100644 --- a/mnemosyne/library/api/workspaces.py +++ b/mnemosyne/library/api/workspaces.py @@ -17,6 +17,7 @@ across users. import logging from neomodel import db +from neomodel.exceptions import UniqueProperty from rest_framework import status from rest_framework.decorators import api_view, permission_classes from rest_framework.permissions import IsAuthenticated @@ -85,7 +86,10 @@ def workspace_create(request): data["workspace_id"], request.user.username, ) return Response( - {"detail": "Workspace id is already in use."}, + { + "detail": "Workspace id is already in use.", + "code": "owner_conflict", + }, status=status.HTTP_409_CONFLICT, ) if existing.library_type != data["library_type"]: @@ -95,7 +99,8 @@ def workspace_create(request): "library_type is immutable for an existing workspace " f"(have '{existing.library_type}', " f"got '{data['library_type']}')." - ) + ), + "code": "library_type_immutable", }, status=status.HTTP_409_CONFLICT, ) @@ -120,7 +125,29 @@ def workspace_create(request): reranker_instruction=defaults["reranker_instruction"], llm_context_prompt=defaults["llm_context_prompt"], ) - lib.save() + try: + lib.save() + except UniqueProperty: + # Library.name is globally unique. A name collision here almost always + # means an orphaned Library survived a failed Daedalus workspace delete + # (the old node kept the name), and the recreate under a new + # workspace_id now clashes. Surface a clean 409 instead of a 500 so + # Daedalus can record + report it; the operator clears the orphan + # (admin delete) or renames the workspace. + logger.warning( + "workspace_create name_conflict workspace_id=%s name=%s", + data["workspace_id"], data["name"], + ) + return Response( + { + "detail": ( + f"A library named '{data['name']}' already exists in " + "Mnemosyne." + ), + "code": "name_conflict", + }, + status=status.HTTP_409_CONFLICT, + ) logger.info( "Workspace created workspace_id=%s library_uid=%s library_type=%s", data["workspace_id"], lib.uid, lib.library_type,