Skip to content

Commit 450cb4c

Browse files
committed
fix(screenpipe): do not write a chosen frame onto a discarded observation
Discard and duplicate $unset media_data, media_filename, media_content_type and content_hash on a closed observation. Applying the agent's frame choice to the same update made Mongo reject the whole write — "Updating the path 'media_data' would create a conflict at 'media_data'" — so every discard of a closed observation carrying a shortlist failed. The choice is still applied in memory, because the promotion path reads item.media_data, but it is only written on the branches that keep an image.
1 parent a041882 commit 450cb4c

2 files changed

Lines changed: 52 additions & 11 deletions

File tree

‎backends/advanced/src/advanced_omi_backend/services/observation_curation.py‎

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -598,25 +598,30 @@ async def apply_curation_decision(
598598
action = decision["decision"]
599599

600600
updates: dict[str, Any] = {"agent_reason": agent_reason}
601-
# Promote the frame the agent chose to the observation's representative image, so
601+
# The frame the agent chose becomes the observation's representative image, so
602602
# every downstream consumer — vault promotion, the 1280px source-media fetch, the
603603
# timeline thumbnail — uses the frame that was actually judged to depict this
604604
# session, not the one that scored highest before anyone looked at it.
605+
#
606+
# Applied in memory here because the promotion path below reads `item.media_data`,
607+
# but written only on the branches that keep an image: discard and duplicate
608+
# `$unset` these same paths, and Mongo rejects a `$set` and `$unset` of one path
609+
# in a single update.
605610
selected = _selected_preview(item, decision)
611+
selection_updates: dict[str, Any] = {}
606612
if selected is not None:
607613
item.media_data = selected["data"]
608614
item.media_content_type = selected["content_type"]
609615
item.media_filename = f"frame-{selected['frame_id']}.jpg"
610-
updates.update(
611-
{
612-
"media_data": selected["data"],
613-
"media_filename": item.media_filename,
614-
"media_content_type": selected["content_type"],
615-
"content_hash": hashlib.sha256(selected["data"]).hexdigest(),
616-
"metadata.preview_frame_id": selected["frame_id"],
617-
"metadata.thumbnail_available": True,
618-
}
619-
)
616+
item.content_hash = hashlib.sha256(selected["data"]).hexdigest()
617+
selection_updates = {
618+
"media_data": item.media_data,
619+
"media_filename": item.media_filename,
620+
"media_content_type": item.media_content_type,
621+
"content_hash": item.content_hash,
622+
"metadata.preview_frame_id": selected["frame_id"],
623+
"metadata.thumbnail_available": True,
624+
}
620625
unset: tuple[str, ...] = ()
621626
if action == "duplicate":
622627
target_id = decision.get("duplicate_observation_id")
@@ -650,6 +655,7 @@ async def apply_curation_decision(
650655
"content_hash",
651656
)
652657
else:
658+
updates.update(selection_updates)
653659
retain_image = bool(decision.get("retain_image")) or action == "promote_image"
654660
immich_item_id = decision.get("immich_item_id") if retain_image else None
655661
if (

‎backends/advanced/tests/test_observation_curation.py‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,3 +323,38 @@ def test_only_a_shortlisted_frame_can_be_selected():
323323
observation_curation._selected_preview(item, {"selected_frame_id": None})
324324
is None
325325
)
326+
327+
328+
@pytest.mark.asyncio
329+
async def test_discarding_a_selected_frame_does_not_set_and_unset_one_path(monkeypatch):
330+
"""A discarded observation drops its image; it must not also write one.
331+
332+
Mongo rejects a `$set` and `$unset` of the same path in one update, so applying
333+
the agent's frame choice unconditionally made every discard of a closed
334+
observation fail with "Updating the path 'media_data' would create a conflict".
335+
"""
336+
337+
item = observation(
338+
lifecycle="closed",
339+
media_previews=[
340+
{"frame_id": 7, "data": b"jpeg", "content_type": "image/jpeg"},
341+
],
342+
curation="curating",
343+
)
344+
seen: list[tuple[dict, tuple]] = []
345+
346+
async def capture(target, fields, *, unset=()):
347+
seen.append((fields, unset))
348+
return True
349+
350+
monkeypatch.setattr(observation_curation, "_apply_curation_fields", capture)
351+
352+
await apply_curation_decision(
353+
item,
354+
{"decision": "discard", "reason": "Routine", "selected_frame_id": 7},
355+
observation_revision(item),
356+
)
357+
358+
fields, unset = seen[-1]
359+
assert "media_data" in unset
360+
assert not set(fields) & set(unset)

0 commit comments

Comments
 (0)