fix(soft-delete): filter trashed rows across read/write paths
Drift-audit Group 3 (soft-delete lifecycle gaps). Trashed rows were leaking into reads and being mutated/resurrected by writes: - update SELECTs now exclude trashed rows: update_milestone, update_project, update_event, and get_milestone_in_project (the latter backs all four milestone routes). Mutating a trashed row silently persisted and reappeared on restore. - MCP get_recent (notes/projects/events) and list_tags now filter deleted_at IS NULL, so trashed items stop surfacing in the agent's bootstrap context and tag counts. - convert_task_to_note clears recurrence_rule + recurrence_next_spawn_at so a demoted note can't spawn children via the (now-live) sweep. - caldav pull skips locally-trashed events (by caldav_uid) instead of resurrecting them via update or creating a duplicate live copy. - trash _cascade now stamps the FULL sub-task subtree (iterative descent), not just direct children, so deeply nested sub-tasks restore as one batch. Test updated for the new descent query. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -125,7 +125,7 @@ async def sync_user_events(user_id: int) -> dict:
|
||||
logger.warning("CalDAV pull sync failed for user %d", user_id, exc_info=True)
|
||||
return {"error": "CalDAV fetch failed"}
|
||||
|
||||
created = updated = unchanged = 0
|
||||
created = updated = unchanged = skipped = 0
|
||||
|
||||
async with async_session() as session:
|
||||
for ev in remote_events:
|
||||
@@ -149,6 +149,14 @@ async def sync_user_events(user_id: int) -> dict:
|
||||
)
|
||||
existing = result.scalar_one_or_none()
|
||||
|
||||
if existing is not None and existing.deleted_at is not None:
|
||||
# The user trashed this event locally. Don't resurrect it by
|
||||
# updating, and don't create a duplicate live copy — leave it
|
||||
# in the trash. (Propagating the delete to the remote server is
|
||||
# tracked separately.)
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
if existing is None:
|
||||
# Create new event
|
||||
new_ev = Event(
|
||||
@@ -180,10 +188,10 @@ async def sync_user_events(user_id: int) -> dict:
|
||||
await session.commit()
|
||||
|
||||
logger.info(
|
||||
"CalDAV sync user %d: %d created, %d updated, %d unchanged",
|
||||
user_id, created, updated, unchanged,
|
||||
"CalDAV sync user %d: %d created, %d updated, %d unchanged, %d skipped (trashed)",
|
||||
user_id, created, updated, unchanged, skipped,
|
||||
)
|
||||
return {"created": created, "updated": updated, "unchanged": unchanged}
|
||||
return {"created": created, "updated": updated, "unchanged": unchanged, "skipped": skipped}
|
||||
|
||||
|
||||
async def sync_all_users() -> None:
|
||||
|
||||
Reference in New Issue
Block a user