fix(sync): tombstone cascade-deleted tasks, repair FK orphans on pull (#837) - #854
Merged
Conversation
…#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.
|
The latest updates on your projects. Learn more about Vercel for GitHub. 2 Skipped Deployments
|
|
React Doctor found no new issues. 🎉 Reviewed by React Doctor for commit |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
deleteProjectrelies on SQLiteON DELETE cascadeto 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, failstasks.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:
reconcileStatusesdeletes statuses out from undertasks.status_id.Changes
Stop new orphans at the source
domain-tasks/commands.tsdeleteProjectlists the project's tasks (including completed and archived — SQLite cascades those just the same) before the delete, then publishestaskDeletedfor each so the cascade is tombstoned server-side.Name the missing parent
MissingSyncParentError(childType, childId, parentType, parentId). SQLite's bareFOREIGN KEY constraint failednames neither the constraint nor the id, which is exactly the gap the issue flagged in the production logs. Pull failure logs now carryparentTypeandparentId.task-handlerguards both FK parents across all three write paths (merge / plain update / insert). A missing project throws the typed error; a danglingstatus_idis nulled, since the FK is alreadyON DELETE SET NULLand null is the schema's own answer. An absentprojectIdstill means "unchanged" and is left alone.Heal installs already stuck in the loop
engine/orphan-repair.ts. Re-fetches the missing parent by id, which is authoritative in a way the pull cursor window is not: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/desktopmain sync suite — PASS (965), FAIL (0)packages/domain-tasks— PASS (88), FAIL (0)task-handler-fk.test.ts(4),orphan-repair.test.ts(5),commands-delete-project.test.ts(3)pnpm typecheck16/16 ·eslint0 errors ·check:architecture·check:contracts·ipc:check·docs:impact --strictcovered ·docs:buildReviewer notes
orphan-repairlives in its own module becausepull-coordinator.tshit the 800-linemax-linescap. That matches the existingcorrupt-item-tracker/quarantine-managersplit.app_error_seen error_code=SQLITE_CONSTRAINT_FOREIGNKEY action=Failed_to_update_task(darwin + win32), is the localtasks:updateIPC path, not the sync applier — likely a renderer holding a stalestatusIdafter an incoming sync reconciled statuses away. Should get its own issue.