1 Commits

Author SHA1 Message Date
dd06f923cd feat(workspaces): return 409 name_conflict instead of 500 on Library name clash
Some checks failed
CVE Scan & Docker Build / security-scan (pull_request) Successful in 3m49s
CVE Scan & Docker Build / build-and-push (pull_request) Has been cancelled
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 <noreply@anthropic.com>
2026-06-17 20:26:43 -04:00

View File

@@ -17,6 +17,7 @@ across users.
import logging import logging
from neomodel import db from neomodel import db
from neomodel.exceptions import UniqueProperty
from rest_framework import status from rest_framework import status
from rest_framework.decorators import api_view, permission_classes from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
@@ -85,7 +86,10 @@ def workspace_create(request):
data["workspace_id"], request.user.username, data["workspace_id"], request.user.username,
) )
return Response( return Response(
{"detail": "Workspace id is already in use."}, {
"detail": "Workspace id is already in use.",
"code": "owner_conflict",
},
status=status.HTTP_409_CONFLICT, status=status.HTTP_409_CONFLICT,
) )
if existing.library_type != data["library_type"]: if existing.library_type != data["library_type"]:
@@ -95,7 +99,8 @@ def workspace_create(request):
"library_type is immutable for an existing workspace " "library_type is immutable for an existing workspace "
f"(have '{existing.library_type}', " f"(have '{existing.library_type}', "
f"got '{data['library_type']}')." f"got '{data['library_type']}')."
) ),
"code": "library_type_immutable",
}, },
status=status.HTTP_409_CONFLICT, status=status.HTTP_409_CONFLICT,
) )
@@ -120,7 +125,29 @@ def workspace_create(request):
reranker_instruction=defaults["reranker_instruction"], reranker_instruction=defaults["reranker_instruction"],
llm_context_prompt=defaults["llm_context_prompt"], 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( logger.info(
"Workspace created workspace_id=%s library_uid=%s library_type=%s", "Workspace created workspace_id=%s library_uid=%s library_type=%s",
data["workspace_id"], lib.uid, lib.library_type, data["workspace_id"], lib.uid, lib.library_type,