Skip to content

Commit 5373db8

Browse files
phernandezclaude
andcommitted
fix(core): apply create storage check to PUT-as-create branch
The PUT create-or-replace path creates a new entity when its external_id is not yet in the DB, via prepare_create_or_reject, which also disabled the storage existence check. Like POST create, this could commit DB/search rows that diverge from an existing unindexed file on disk, which the next watcher pass would overwrite. Thread verify_storage_absent_on_create through prepare_create_or_reject and map the resulting EntityAlreadyExistsError to a 409 conflict so local PUT-as-create matches POST create. The replace branch (entity already exists) is unchanged. Follow-up to the #1002 review on accepted_note_mutation_runner create. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: phernandez <paul@basicmachines.co>
1 parent cae7a4b commit 5373db8

2 files changed

Lines changed: 37 additions & 1 deletion

File tree

src/basic_memory/indexing/accepted_note_mutation_runner.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,7 @@ async def _run_accepted_note_update(
541541
prepared_write = await prepare_create_or_reject(
542542
preparer,
543543
request.data,
544+
check_storage_exists=dependencies.verify_storage_absent_on_create,
544545
session=session,
545546
)
546547
entity = await create_accepted_pending_entity(
@@ -869,16 +870,21 @@ async def prepare_create_or_reject(
869870
preparer: AcceptedNoteCreatePreparer,
870871
data: EntitySchema,
871872
*,
873+
check_storage_exists: bool,
872874
session: AsyncSession,
873875
) -> AcceptedPreparedNoteWrite:
874876
"""Prepare a new accepted note or raise a typed mutation rejection."""
875877
try:
876878
return await prepare_accepted_note_create(
877879
preparer,
878880
data,
879-
check_storage_exists=False,
881+
check_storage_exists=check_storage_exists,
880882
session=session,
881883
)
884+
except EntityAlreadyExistsError as error:
885+
# PUT-as-create over an unindexed on-disk file (local source-of-truth
886+
# runtimes). Reject rather than committing divergent DB state.
887+
reject_accepted_note_mutation(AcceptedNoteMutationRejectKind.conflict, str(error))
882888
except (ParseError, ValueError) as error:
883889
reject_accepted_note_mutation(AcceptedNoteMutationRejectKind.bad_request, str(error))
884890

tests/api/v2/test_knowledge_router.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,36 @@ async def test_create_entity_rejects_existing_unindexed_file(
419419
assert file_path.read_text(encoding="utf-8") == original
420420

421421

422+
@pytest.mark.asyncio
423+
async def test_put_create_rejects_existing_unindexed_file(
424+
client: AsyncClient, v2_project_url, project_config
425+
):
426+
"""PUT-as-create over an unindexed on-disk file returns 409 (#1002 review).
427+
428+
The create branch of PUT (entity_id not in the DB) must apply the same
429+
storage check as POST so it cannot commit DB/search state that diverges
430+
from the file on disk.
431+
"""
432+
file_path = project_config.home / "conflict" / "PutUnindexed.md"
433+
file_path.parent.mkdir(parents=True, exist_ok=True)
434+
original = "---\ntitle: PutUnindexed\ntype: note\n---\n\nPre-existing on-disk content\n"
435+
file_path.write_text(original, encoding="utf-8")
436+
437+
new_external_id = "abcdef00-0000-4000-8000-000000000000"
438+
response = await client.put(
439+
f"{v2_project_url}/knowledge/entities/{new_external_id}",
440+
json={
441+
"title": "PutUnindexed",
442+
"directory": "conflict",
443+
"content": "New content via PUT",
444+
},
445+
params={"fast": False},
446+
)
447+
448+
assert response.status_code == 409
449+
assert file_path.read_text(encoding="utf-8") == original
450+
451+
422452
@pytest.mark.asyncio
423453
async def test_create_entity_returns_content(client: AsyncClient, file_service, v2_project_url):
424454
"""Test creating an entity always returns file content with frontmatter."""

0 commit comments

Comments
 (0)