fix(soft-delete): filter trashed rows across read/write paths
CI & Build / TypeScript typecheck (push) Has been cancelled
CI & Build / Python lint (push) Has been cancelled
CI & Build / Python tests (push) Has been cancelled
CI & Build / Build & push image (push) Has been cancelled

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:
2026-06-02 18:51:40 -04:00
co-authored by Claude Opus 4.8
parent 8b49ea896a
commit 5fe0fd126d
9 changed files with 57 additions and 16 deletions
+12 -4
View File
@@ -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: