Deleted task note still shows as a title-less task in Calendar (stale projection/cache); delete must remove it everywhere #623

Open
opened 2026-10-01 10:17:58 +00:00 by kayg · 7 comments
Owner

Owner report (2026-10-01, Calendar Week screenshot)

"This is the Buy milk task note that I deleted manually, but it still shows up?" The all-day lane on Mon 28 still shows a task chip. Its hover card shows only "Task · Monday, September 28 · Open", with no title: a ghost of a deleted task.
Investigate and fix (data-consistency class):

  1. Where does the ghost come from: the Tasks/Calendar index not removing a task whose note file moved to the Trash (a file delete event not projected), the client cache (#423 warm cache / userStorage) showing a stale snapshot without revalidating, or a Task row whose source note is gone but whose row is kept?
  2. Fix the source: deleting a task's note (Files → Trash, WebDAV DELETE, CalDAV DELETE, API) removes the Task from every projection (Calendar, Tasks, Search, CalDAV) immediately, and live views update without reload. Restoring from the Trash brings it back.
  3. A consistency check: no Task row may exist whose source note is missing or in the Trash (a repair job + a test).
  4. The hover card must never render a task without a title. If the data is inconsistent, hide the item and log it.
    Test: e2e: create a task → delete its note in Files → the Calendar and Tasks views drop it within one revalidation (no reload); restore it → it returns. A Rust test for the projection on delete/restore.
## Owner report (2026-10-01, Calendar Week screenshot) "This is the Buy milk task note that I deleted manually, but it still shows up?" The all-day lane on Mon 28 still shows a task chip. Its hover card shows only "Task · Monday, September 28 · Open", with **no title**: a ghost of a deleted task. **Investigate and fix (data-consistency class):** 1. Where does the ghost come from: the Tasks/Calendar index not removing a task whose note file moved to the Trash (a file delete event not projected), the client cache (#423 warm cache / userStorage) showing a stale snapshot without revalidating, or a Task row whose source note is gone but whose row is kept? 2. Fix the source: deleting a task's note (Files → Trash, WebDAV DELETE, CalDAV DELETE, API) removes the Task from every projection (Calendar, Tasks, Search, CalDAV) **immediately**, and live views update without reload. Restoring from the Trash brings it back. 3. A consistency check: no Task row may exist whose source note is missing or in the Trash (a repair job + a test). 4. The hover card must never render a task without a title. If the data is inconsistent, hide the item and log it. **Test:** e2e: create a task → delete its note in Files → the Calendar and Tasks views drop it within one revalidation (no reload); restore it → it returns. A Rust test for the projection on delete/restore.
Author
Owner

Started on job/ghosttask-623, base 9a6c38c3b7. Tracing Note removal, Task projections, restore, and Calendar invalidation before changing code.

Started on job/ghosttask-623, base 9a6c38c3b7c8bbbdad9919cd2419f773d9c34b6e. Tracing Note removal, Task projections, restore, and Calendar invalidation before changing code.
Author
Owner

Finding: task line parsing strips reference-link labels; tasks_store::index_source suppresses these rows only while the target is an indexed Task. After deletion and reconciliation, a surviving reference-only checkbox becomes an empty-title inline Task. remove_index already deletes Task and Reminder rows, so the ghost is recreated rather than merely retained. Files also publishes its SSE hint before asynchronous Note adoption; directory events bypass Markdown-only adoption. Fixing these paths, testing repair and stable restore, and guarding Calendar rendering. Decision: suppress every empty-title derived Task, reuse the existing notes.reconcile repair job, and complete folder adoption under the existing Notes lock. WebDAV DELETE already delegates to Files Trash.

Finding: task line parsing strips reference-link labels; tasks_store::index_source suppresses these rows only while the target is an indexed Task. After deletion and reconciliation, a surviving reference-only checkbox becomes an empty-title inline Task. remove_index already deletes Task and Reminder rows, so the ghost is recreated rather than merely retained. Files also publishes its SSE hint before asynchronous Note adoption; directory events bypass Markdown-only adoption. Fixing these paths, testing repair and stable restore, and guarding Calendar rendering. Decision: suppress every empty-title derived Task, reuse the existing notes.reconcile repair job, and complete folder adoption under the existing Notes lock. WebDAV DELETE already delegates to Files Trash.
Author
Owner

Further findings: the warm Calendar Map outlives its mounted view, but its Files subscription did not. A return from Files could therefore reuse a stale snapshot for 30 seconds. Added a cache-owned listener on the shared Files stream, released on invalidation, with a passing regression test. Folder adoption now scans only the affected subtree and uses indexed prefix ranges for old descendants; it does not rescan unrelated Notes. Normal-stack Rust runs exposed oversized cross-plugin futures in Notes and Files; the request boundary now boxes those futures. Notes delete/restore/repair and directory tests pass. UI and cache fixes are committed separately; full Files validation and production browser evidence are next.

Further findings: the warm Calendar Map outlives its mounted view, but its Files subscription did not. A return from Files could therefore reuse a stale snapshot for 30 seconds. Added a cache-owned listener on the shared Files stream, released on invalidation, with a passing regression test. Folder adoption now scans only the affected subtree and uses indexed prefix ranges for old descendants; it does not rescan unrelated Notes. Normal-stack Rust runs exposed oversized cross-plugin futures in Notes and Files; the request boundary now boxes those futures. Notes delete/restore/repair and directory tests pass. UI and cache fixes are committed separately; full Files validation and production browser evidence are next.
Author
Owner

Implementation commits: 7a9edfd82 (hide/log empty Calendar Tasks), ab3917caa (projection repair and orphan references), 631cadbc7 (warm cache live invalidation), ce4bbb246 (subtree repair), 64266169e (synchronous Files lifecycle and boxed adoption). Merged origin/dev once at cd0bf5d0b. Normal-stack Notes Task slice: 27 passed; Files stack regression now passes. Final per-crate Rust gates and web gates are running. Existing hourly notes.reconcile also repairs Task-only orphans; the API reconcile now rebuilds Tasks before Notes. No separate repair worker or migration is needed.

Implementation commits: 7a9edfd82 (hide/log empty Calendar Tasks), ab3917caa (projection repair and orphan references), 631cadbc7 (warm cache live invalidation), ce4bbb246 (subtree repair), 64266169e (synchronous Files lifecycle and boxed adoption). Merged origin/dev once at cd0bf5d0b. Normal-stack Notes Task slice: 27 passed; Files stack regression now passes. Final per-crate Rust gates and web gates are running. Existing hourly notes.reconcile also repairs Task-only orphans; the API reconcile now rebuilds Tasks before Notes. No separate repair worker or migration is needed.
Author
Owner

All six production Calendar delete/restore checks passed (390, 820 and 1440 px, light and dark). The first Task revalidation removes the chip and closes its card; restore returns the same Task identity without reload. Returning from Files to a warm Calendar Tab also revalidates. Tasks list/day/standalone APIs agree.

The bounded local probe passed invalid Trash/restore input, Unicode Tasks, eight concurrent delete/restore cycles and repair consistency. It also found an existing non-UTF-8 Markdown restore 500 in Tags reconciliation, now filed as #631. The probe keeps the expected 200 assertion and exits nonzero for that finding. Tags behavior is outside this job's owned files.

Final per-crate Rust and web gates passed. Production evidence capture and the local 1,000-Note performance profile are finishing. The perf VM lock was occupied, so measurements use a local debug server and are labeled as such.

All six production Calendar delete/restore checks passed (390, 820 and 1440 px, light and dark). The first Task revalidation removes the chip and closes its card; restore returns the same Task identity without reload. Returning from Files to a warm Calendar Tab also revalidates. Tasks list/day/standalone APIs agree. The bounded local probe passed invalid Trash/restore input, Unicode Tasks, eight concurrent delete/restore cycles and repair consistency. It also found an existing non-UTF-8 Markdown restore 500 in Tags reconciliation, now filed as #631. The probe keeps the expected 200 assertion and exits nonzero for that finding. Tags behavior is outside this job's owned files. Final per-crate Rust and web gates passed. Production evidence capture and the local 1,000-Note performance profile are finishing. The perf VM lock was occupied, so measurements use a local debug server and are labeled as such.
Author
Owner

Built the #623 fix on job/ghosttask-623. Head: 84320d42e095c25af2475dd3a94466a09493d301. Merged origin/dev once before final gates. No push or deploy. The worktree is clean.

The ghost came from a surviving reference-only Daily note checkbox. Once its target Task left the Index, repair could create an inline Task with an empty title. Files Trash/restore also published its live hint before Notes adoption completed. Calendar could retain a warm snapshot while its Tab was unmounted.

Task projection now rejects and logs empty titles. Files Trash/restore completes Notes, Tasks and Reminder adoption before its response and live hint. Folder changes repair only the affected subtree. CalDAV Task DELETE removes the complete source Note projection, including Search. The existing repair job and manual repair endpoint remove Task-only orphans and preserve the Task identity on restore. Calendar invalidates unmounted warm snapshots, refreshes undated Tasks and closes deleted Task cards. The data adapter and card hide and log inconsistent empty titles.

Rust regressions cover delete/restore with a surviving reference, lost delete events, Task-only orphans, folder changes (including a folder ending in .md), stable Reminder restore and invalid UTF-8 projection removal. Unit regressions cover blank cards and warm-cache lifetime.

Production e2e passed at 390, 820 and 1440 px in light and dark. Files Trash removes the Calendar chip and card on the first revalidation without reload. Restore returns the same identity. Tasks list, standalone and day APIs agree. A same-page Files-to-Calendar Tab check also passed. Screenshot icon/text pairs were checked at original resolution. Overall visual review remains with the orchestrator.

Files

  • crates/plugins/notes/src/lib.rs
  • crates/plugins/notes/src/store.rs
  • crates/plugins/notes/src/tasks_store.rs
  • crates/plugins/notes/src/tasks_dav.rs
  • crates/plugins/files/src/lib.rs
  • apps/web/src/lib/calendar/data.ts
  • apps/web/src/lib/calendar/data.test.ts
  • apps/web/src/lib/calendar/ItemPreview.svelte.test.ts
  • apps/web/src/routes/calendar/[view]/[date]/+page.svelte
  • packages/ui/src/components/calendar/ItemPreview.svelte
  • apps/web/e2e/task-trash-623.mjs
  • tests/adversarial/task-trash-623.mjs
  • bench/task-trash-623.py
  • docs/perf/task-trash-623.md

Gate output (result lines verbatim)

cargo fmt --check: exit 0 (no output)
cargo clippy -p calternal-plugin-notes --all-targets -- -D warnings
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.58s
cargo test -p calternal-plugin-notes
test result: ok. 165 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 57.55s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.52s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
cargo clippy -p calternal-plugin-files --all-targets -- -D warnings
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 32.30s
cargo test -p calternal-plugin-files
test result: ok. 146 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 155.69s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
cargo clippy -p calternal-server --all-targets -- -D warnings
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 4m 45s
cargo test -p calternal-server
test result: ok. 107 passed; 0 failed; 3 ignored; 0 measured; 0 filtered out; finished in 22.81s
bun run check
svelte-check found 0 errors and 0 warnings
bun run test
 Test Files  148 passed (148)
      Tests  1015 passed (1015)

The Files gate has one existing ignored 10,000-file/rclone probe. The server gate has three existing process-global live-app tests marked ignored; its separate-process runner passed. The production web and embedded server builds also passed.

Bounded adversarial round

Invalid Trash/restore input, Unicode Tasks, eight concurrent delete/restore cycles and repair consistency passed. The probe exited 1 for the separate non-SLOW finding #631: existing Tags reconciliation makes Files restore return 500 for non-UTF-8 Markdown after its bytes have been restored. The expected-200 assertion remains in the probe. Tags behavior is outside this job's owned files.

FINDING: non-UTF-8 Markdown restore returns 500 in Tags reconciliation
PASS #623 local bounded round: invalid Trash/restore input, Unicode Tasks, eight concurrent delete/restore cycles, repair consistency
AssertionError: non-UTF-8 Markdown is a File, not a failed restore

500 !== 200

Performance and known gaps

The new profile covers one-Note requests, a 1,000-Note folder and an eight-Note burst. The perf VM lock was busy. The local debug run indexed 1,000 Task Notes, then hit the 120-second Files restore timeout in the average phase. It produced no complete p50/p95, average CPU or peak RSS result. Folder and burst timings are unavailable. A spot check showed 269.45 MiB RSS and 67.3% lifetime CPU; these are not phase averages or peaks. Host load after the failed run was 8.27 / 10.97 / 13.24. The baseline has no matching Task Trash/restore metric, so no regression ratio can be calculated. The timeout is recorded as a SLOW finding. Release-build measurement remains open.

There is no registered Tasks web route in this branch. The e2e uses the real Tasks APIs and Calendar Task surfaces. The separate #631 restore failure remains open.

Decisions

  • Reuse the existing hourly Notes repair job instead of adding another job or migration.
  • Treat a reference-only empty checkbox as an attachment even when its target is absent.
  • Keep a shared Files listener while a warm Calendar cache exists. Release it when the cache is empty.
  • Seed benchmark Notes while the test server is stopped. Measure mutations through the real Files API.

Production screenshots

Width/theme Calendar deleted Calendar restored Files deleted Files restored
390 light deleted restored files-deleted files-restored
390 dark deleted restored files-deleted files-restored
820 light deleted restored files-deleted files-restored
820 dark deleted restored files-deleted files-restored
1440 light deleted restored files-deleted files-restored
1440 dark deleted restored files-deleted files-restored

Cleanup completed. Web build output was removed. Cargo cleanup output:

     Removed 18105 files, 10.0GiB total
Built the #623 fix on `job/ghosttask-623`. Head: `84320d42e095c25af2475dd3a94466a09493d301`. Merged `origin/dev` once before final gates. No push or deploy. The worktree is clean. The ghost came from a surviving reference-only Daily note checkbox. Once its target Task left the Index, repair could create an inline Task with an empty title. Files Trash/restore also published its live hint before Notes adoption completed. Calendar could retain a warm snapshot while its Tab was unmounted. Task projection now rejects and logs empty titles. Files Trash/restore completes Notes, Tasks and Reminder adoption before its response and live hint. Folder changes repair only the affected subtree. CalDAV Task DELETE removes the complete source Note projection, including Search. The existing repair job and manual repair endpoint remove Task-only orphans and preserve the Task identity on restore. Calendar invalidates unmounted warm snapshots, refreshes undated Tasks and closes deleted Task cards. The data adapter and card hide and log inconsistent empty titles. Rust regressions cover delete/restore with a surviving reference, lost delete events, Task-only orphans, folder changes (including a folder ending in .md), stable Reminder restore and invalid UTF-8 projection removal. Unit regressions cover blank cards and warm-cache lifetime. Production e2e passed at 390, 820 and 1440 px in light and dark. Files Trash removes the Calendar chip and card on the first revalidation without reload. Restore returns the same identity. Tasks list, standalone and day APIs agree. A same-page Files-to-Calendar Tab check also passed. Screenshot icon/text pairs were checked at original resolution. Overall visual review remains with the orchestrator. **Files** - `crates/plugins/notes/src/lib.rs` - `crates/plugins/notes/src/store.rs` - `crates/plugins/notes/src/tasks_store.rs` - `crates/plugins/notes/src/tasks_dav.rs` - `crates/plugins/files/src/lib.rs` - `apps/web/src/lib/calendar/data.ts` - `apps/web/src/lib/calendar/data.test.ts` - `apps/web/src/lib/calendar/ItemPreview.svelte.test.ts` - `apps/web/src/routes/calendar/[view]/[date]/+page.svelte` - `packages/ui/src/components/calendar/ItemPreview.svelte` - `apps/web/e2e/task-trash-623.mjs` - `tests/adversarial/task-trash-623.mjs` - `bench/task-trash-623.py` - `docs/perf/task-trash-623.md` **Gate output (result lines verbatim)** ```text cargo fmt --check: exit 0 (no output) cargo clippy -p calternal-plugin-notes --all-targets -- -D warnings Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.58s cargo test -p calternal-plugin-notes test result: ok. 165 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 57.55s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.52s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s cargo clippy -p calternal-plugin-files --all-targets -- -D warnings Finished `dev` profile [unoptimized + debuginfo] target(s) in 32.30s cargo test -p calternal-plugin-files test result: ok. 146 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 155.69s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s cargo clippy -p calternal-server --all-targets -- -D warnings Finished `dev` profile [unoptimized + debuginfo] target(s) in 4m 45s cargo test -p calternal-server test result: ok. 107 passed; 0 failed; 3 ignored; 0 measured; 0 filtered out; finished in 22.81s bun run check svelte-check found 0 errors and 0 warnings bun run test Test Files 148 passed (148) Tests 1015 passed (1015) ``` The Files gate has one existing ignored 10,000-file/rclone probe. The server gate has three existing process-global live-app tests marked ignored; its separate-process runner passed. The production web and embedded server builds also passed. **Bounded adversarial round** Invalid Trash/restore input, Unicode Tasks, eight concurrent delete/restore cycles and repair consistency passed. The probe exited 1 for the separate non-SLOW finding [#631](https://git.kayg.org/kayg/calternal/issues/631): existing Tags reconciliation makes Files restore return 500 for non-UTF-8 Markdown after its bytes have been restored. The expected-200 assertion remains in the probe. Tags behavior is outside this job's owned files. ```text FINDING: non-UTF-8 Markdown restore returns 500 in Tags reconciliation PASS #623 local bounded round: invalid Trash/restore input, Unicode Tasks, eight concurrent delete/restore cycles, repair consistency AssertionError: non-UTF-8 Markdown is a File, not a failed restore 500 !== 200 ``` **Performance and known gaps** The new profile covers one-Note requests, a 1,000-Note folder and an eight-Note burst. The perf VM lock was busy. The local debug run indexed 1,000 Task Notes, then hit the 120-second Files restore timeout in the average phase. It produced no complete p50/p95, average CPU or peak RSS result. Folder and burst timings are unavailable. A spot check showed 269.45 MiB RSS and 67.3% lifetime CPU; these are not phase averages or peaks. Host load after the failed run was 8.27 / 10.97 / 13.24. The baseline has no matching Task Trash/restore metric, so no regression ratio can be calculated. The timeout is recorded as a SLOW finding. Release-build measurement remains open. There is no registered Tasks web route in this branch. The e2e uses the real Tasks APIs and Calendar Task surfaces. The separate #631 restore failure remains open. **Decisions** - Reuse the existing hourly Notes repair job instead of adding another job or migration. - Treat a reference-only empty checkbox as an attachment even when its target is absent. - Keep a shared Files listener while a warm Calendar cache exists. Release it when the cache is empty. - Seed benchmark Notes while the test server is stopped. Measure mutations through the real Files API. **Production screenshots** | Width/theme | Calendar deleted | Calendar restored | Files deleted | Files restored | |---|---|---|---|---| | 390 light | [deleted](https://git.kayg.org/attachments/58228873-9790-4de4-bc06-c26aaa6d75a8) | [restored](https://git.kayg.org/attachments/403ab37d-8fe7-447f-9c70-bb887477b21a) | [files-deleted](https://git.kayg.org/attachments/8b357152-6c0c-40c8-8720-d987d76ba716) | [files-restored](https://git.kayg.org/attachments/8c8bf49d-c97a-443d-91f6-9a7978b49ea7) | | 390 dark | [deleted](https://git.kayg.org/attachments/43472aa0-4630-4960-9ad4-310e6a918466) | [restored](https://git.kayg.org/attachments/7fbfcbb0-e451-4bf0-89a3-df3f3ff3d576) | [files-deleted](https://git.kayg.org/attachments/66e9b5a7-e0f0-4039-b1dd-9fffb9e37e05) | [files-restored](https://git.kayg.org/attachments/db88376d-3385-46d1-8675-7aefea94fe48) | | 820 light | [deleted](https://git.kayg.org/attachments/8b647830-5d00-4b5b-ad68-5f1117025375) | [restored](https://git.kayg.org/attachments/0e60f636-efe0-4e04-b4ab-f963f2a58cce) | [files-deleted](https://git.kayg.org/attachments/2169d192-8178-4970-9fb1-89a483544aaf) | [files-restored](https://git.kayg.org/attachments/ed8c0313-8a77-48ac-ab01-96241abbf739) | | 820 dark | [deleted](https://git.kayg.org/attachments/665bd60f-5397-490f-b644-96bf76c6dbb7) | [restored](https://git.kayg.org/attachments/eaa7756a-1faf-4e53-a741-e384b0ea7bf5) | [files-deleted](https://git.kayg.org/attachments/7665e658-882e-4043-9746-88635e3f0885) | [files-restored](https://git.kayg.org/attachments/467fc1a1-62ba-4336-ade8-53cea8653e97) | | 1440 light | [deleted](https://git.kayg.org/attachments/9d3a1a97-f390-4985-a5e8-f92d62aa4515) | [restored](https://git.kayg.org/attachments/2bd33ff5-0c2b-4080-90e1-94ae541b3010) | [files-deleted](https://git.kayg.org/attachments/81502452-e8fb-4a4e-99d0-4a14eb4bf319) | [files-restored](https://git.kayg.org/attachments/60555c38-561a-4d20-b407-33cb17552f84) | | 1440 dark | [deleted](https://git.kayg.org/attachments/1339ccc2-a98a-4529-a61a-2704cf4b9b5c) | [restored](https://git.kayg.org/attachments/111ebb82-3702-4d84-89a1-cff98e7a58c8) | [files-deleted](https://git.kayg.org/attachments/fbcba517-023e-423c-87f5-a11651ddeaa8) | [files-restored](https://git.kayg.org/attachments/b933adf5-3570-47ba-bedc-45fa13267ae4) | Cleanup completed. Web build output was removed. Cargo cleanup output: ```text Removed 18105 files, 10.0GiB total ```
Author
Owner

Real-server probe evidence (worktree job/files-631, local throwaway Home): creating a standalone Task and POSTing /api/v1/files/trash returned 200, but GET /api/v1/notes/tasks/by-id?task_id= still returned 200 where the #623 probe expects 404. This is the stale Task projection described by this issue. The #631 invalid-UTF-8 Markdown restore case ran first and returned 200. I preserved the #623 expectation and did not change Notes behavior in this job; this finding remains tracked here.

Real-server probe evidence (worktree job/files-631, local throwaway Home): creating a standalone Task and POSTing /api/v1/files/trash returned 200, but GET /api/v1/notes/tasks/by-id?task_id=<created task ID> still returned 200 where the #623 probe expects 404. This is the stale Task projection described by this issue. The #631 invalid-UTF-8 Markdown restore case ran first and returned 200. I preserved the #623 expectation and did not change Notes behavior in this job; this finding remains tracked here.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#623
No description provided.