Skip to content

fix(sync): tombstone cascade-deleted tasks, repair FK orphans on pull (#837) - #854

Merged
h4yfans merged 2 commits into
mainfrom
sync-task-fk-orphan-repair
Jul 22, 2026
Merged

fix(sync): tombstone cascade-deleted tasks, repair FK orphans on pull (#837)#854
h4yfans merged 2 commits into
mainfrom
sync-task-fk-orphan-repair

Conversation

@h4yfans

@h4yfans h4yfans commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

Closes #837. Sync pull permanently dropped tasks on FOREIGN KEY constraint failed, and manifest-check then re-pulled the same items forever. A real user was affected for the whole 3-day Loki window (Linux install, plus one darwin).

Root cause: cascade deletes are invisible to sync. deleteProject relies on SQLite ON DELETE cascade to remove its tasks locally, but the publisher only pushes a project delete. The child task rows are never tombstoned, so they live on the server forever. Every device — including the one that did the delete — then sees them as server-only in manifest-check, re-pulls, fails tasks.project_id → projects(id), defers, skips, and loops.

sortByApplyOrder (project rank 0 before task rank 2) and the end-of-run deferred retry already existed. Neither can help here: the parent is not coming.

Second orphan source, same class: reconcileStatuses deletes statuses out from under tasks.status_id.

Changes

Stop new orphans at the source

  • domain-tasks/commands.ts deleteProject lists the project's tasks (including completed and archived — SQLite cascades those just the same) before the delete, then publishes taskDeleted for each so the cascade is tombstoned server-side.

Name the missing parent

  • New MissingSyncParentError(childType, childId, parentType, parentId). SQLite's bare FOREIGN KEY constraint failed names neither the constraint nor the id, which is exactly the gap the issue flagged in the production logs. Pull failure logs now carry parentType and parentId.
  • task-handler guards both FK parents across all three write paths (merge / plain update / insert). A missing project throws the typed error; a dangling status_id is nulled, since the FK is already ON DELETE SET NULL and null is the schema's own answer. An absent projectId still means "unchanged" and is left alone.

Heal installs already stuck in the loop

  • New engine/orphan-repair.ts. Re-fetches the missing parent by id, which is authoritative in a way the pull cursor window is not:
    • server still returns the parent → apply it, then the child lands normally.
    • server no longer returns it → the parent is gone everywhere → the child is a confirmed orphan and gets tombstoned. That is what the cascade should have pushed in the first place, and it ends the loop on every device.
  • Deletion is gated on that second condition alone. A child whose re-apply fails for any other reason is left untouched and retried next cycle.

Release note

Fixed tasks created or edited on another device failing to appear, with sync repeatedly re-downloading the same items, after a project had been deleted.

Test plan

  • apps/desktop main sync suite — PASS (965), FAIL (0)
  • packages/domain-tasksPASS (88), FAIL (0)
  • New regression coverage: task-handler-fk.test.ts (4), orphan-repair.test.ts (5), commands-delete-project.test.ts (3)
  • Pre-fix repro confirmed the FK throw on both parents before any code was changed
  • pnpm typecheck 16/16 · eslint 0 errors · check:architecture · check:contracts · ipc:check · docs:impact --strict covered · docs:build

Reviewer notes

  • Tombstoning is a one-way write to server-side user data. The guard is deliberately narrow: local absence alone is never enough — the server must also decline to return the parent. Worth a second pair of eyes on that condition.
  • orphan-repair lives in its own module because pull-coordinator.ts hit the 800-line max-lines cap. That matches the existing corrupt-item-tracker / quarantine-manager split.
  • No schema, contract, or protocol change. Nothing here requires a server deploy first, and older clients are unaffected — they just keep not tombstoning cascades.
  • Not covered: the issue's third symptom, app_error_seen error_code=SQLITE_CONSTRAINT_FOREIGNKEY action=Failed_to_update_task (darwin + win32), is the local tasks:update IPC path, not the sync applier — likely a renderer holding a stale statusId after an incoming sync reconciled statuses away. Should get its own issue.

h4yfans added 2 commits July 22, 2026 18:03
…#837)

Sync pull dropped tasks on `FOREIGN KEY constraint failed` and then re-pulled
the same items forever. A real user was affected for the whole 3-day Loki
window.

Root cause: cascade deletes are invisible to sync. `deleteProject` relies on
SQLite `ON DELETE cascade` to remove its tasks locally, but the publisher only
pushes a *project* delete. The child task rows are never tombstoned, so they
live on the server forever. Every device -- including the one that did the
delete -- then sees them as server-only in manifest-check, re-pulls, fails
`tasks.project_id -> projects(id)`, defers, skips, and loops.

`sortByApplyOrder` (project rank 0 before task rank 2) and the end-of-run
deferred retry already existed. Neither can help: the parent is not coming.

Second orphan source, same class: `reconcileStatuses` deletes statuses out
from under `tasks.status_id`.

Changes:

- `domain-tasks/commands.ts` deleteProject now lists the project's tasks
  (including completed and archived, which SQLite cascades just the same)
  before the delete and publishes `taskDeleted` for each, so the cascade is
  tombstoned server-side. Stops new orphans at the source.
- New `MissingSyncParentError` naming the missing parent. SQLite's bare
  `FOREIGN KEY constraint failed` names neither the constraint nor the id,
  which is exactly what the production logs were missing.
- `task-handler` guards both FK parents in all three write paths: a missing
  project throws the typed error, a dangling `status_id` is nulled (the FK is
  already `ON DELETE SET NULL`, so null is the schema's own answer). An absent
  `projectId` still means "unchanged" and is left alone.
- New `engine/orphan-repair.ts` heals installs already stuck in the loop. It
  refetches the parent by id, which is authoritative in a way the pull cursor
  window is not: if the server still has it, apply the parent and the child
  lands normally; if the server no longer returns it, the parent is gone
  everywhere, so the child is a confirmed orphan and gets tombstoned. That is
  what the cascade should have pushed in the first place, and it ends the loop
  on every device. Split into its own module because pull-coordinator.ts hit
  the 800-line max-lines cap.
- Pull failure logs now carry `parentType` and `parentId`.

Not covered: the `Failed_to_update_task` FK symptom on darwin/win32 is the
local `tasks:update` IPC path, not the sync applier. Separate fix.
Copilot AI review requested due to automatic review settings July 22, 2026 15:04
@vercel

vercel Bot commented Jul 22, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated (UTC)
memry-docs Ignored Ignored Preview Jul 22, 2026 3:05pm
memrynote-landing Ignored Ignored Jul 22, 2026 3:05pm

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions github-actions Bot added bug Something isn't working documentation Improvements or additions to documentation test labels Jul 22, 2026
@github-actions

Copy link
Copy Markdown

React Doctor found no new issues. 🎉

Reviewed by React Doctor for commit fca90f7.

@h4yfans
h4yfans marked this pull request as ready for review July 22, 2026 15:42
@h4yfans
h4yfans merged commit f6a7b12 into main Jul 22, 2026
18 of 19 checks passed
@h4yfans
h4yfans deleted the sync-task-fk-orphan-repair branch July 22, 2026 15:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working documentation Improvements or additions to documentation test

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sync pull permanently skips tasks on FOREIGN KEY constraint failure → endless re-pull loop (real user affected)

2 participants