P1: stale Task refresh can revert an acknowledged status through collaboration #936

Open
opened 2026-10-02 17:41:24 +00:00 by kayg · 1 comment
Owner

P1 — An older refresh can undo an acknowledged status write

Evidence: apps/web/src/lib/notes/TaskHeader.svelte:185 starts a refresh and
checks only its refresh sequence at line 186. Local writes at lines 67–71 do
not invalidate that sequence. apps/web/src/lib/tasks/TaskSection.svelte:479
and :483 have the same gap. Start a refresh that reads To do, then complete
the Task. If the old response arrives after the completion response, it passes
the sequence check and applies To do through onchange.
apps/web/src/lib/notes/NoteView.svelte:425 then calls syncTaskEditor, which
changes the live checkbox back at line 320. The server's
crates/plugins/notes/src/tasks_api.rs:194 accepts a changed body checkbox
when the previous marker matched frontmatter. Thus an old read can become a
new persisted status write. Deferring refreshes that start during a save does
not cancel a refresh that already started.

Fix: invalidate in-flight reads when a write starts, and check the write epoch
again before applying a read result or calling the parent. Re-read once after
the write. Do not publish stale read projections as editable collaboration
transactions. Use one shared guard for TaskHeader and TaskSection.

Test idea: hold a To do refresh response, complete the Task and receive its
acknowledgement, then release the old response. Assert that the visible state,
Markdown checkbox and stored Task status stay Done after the room flushes.
Repeat with an Inspector status edit and Undo.

Rule: DESIGN §§2, 9, 41; state-changing actions must not lose later edits.

Source: independent read-only review of #659 at 013f6785a. Line numbers refer to that commit. No build or runtime test was run.

## P1 — An older refresh can undo an acknowledged status write Evidence: `apps/web/src/lib/notes/TaskHeader.svelte:185` starts a refresh and checks only its refresh sequence at line 186. Local writes at lines 67–71 do not invalidate that sequence. `apps/web/src/lib/tasks/TaskSection.svelte:479` and `:483` have the same gap. Start a refresh that reads To do, then complete the Task. If the old response arrives after the completion response, it passes the sequence check and applies To do through `onchange`. `apps/web/src/lib/notes/NoteView.svelte:425` then calls `syncTaskEditor`, which changes the live checkbox back at line 320. The server's `crates/plugins/notes/src/tasks_api.rs:194` accepts a changed body checkbox when the previous marker matched frontmatter. Thus an old read can become a new persisted status write. Deferring refreshes that start during a save does not cancel a refresh that already started. Fix: invalidate in-flight reads when a write starts, and check the write epoch again before applying a read result or calling the parent. Re-read once after the write. Do not publish stale read projections as editable collaboration transactions. Use one shared guard for TaskHeader and TaskSection. Test idea: hold a To do refresh response, complete the Task and receive its acknowledgement, then release the old response. Assert that the visible state, Markdown checkbox and stored Task status stay Done after the room flushes. Repeat with an Inspector status edit and Undo. Rule: DESIGN §§2, 9, 41; state-changing actions must not lose later edits. Source: independent read-only review of #659 at 013f6785a. Line numbers refer to that commit. No build or runtime test was run.
Author
Owner

P1 — An older refresh can persist a stale root checkbox

Evidence: apps/web/src/lib/notes/TaskHeader.svelte:185 starts a refresh and
checks only its refresh sequence at line 186. Local writes at lines 67–71 do
not invalidate that sequence. apps/web/src/lib/tasks/TaskSection.svelte:479
and :483 have the same gap. Start a refresh that reads To do, then complete
the Task. If the old response arrives after the completion response, it passes
the sequence check and applies To do through onchange.
apps/web/src/lib/notes/NoteView.svelte:425 then calls syncTaskEditor, which
changes the live checkbox back at line 320. The live writer in
crates/plugins/notes/src/lib.rs:2855 preserves frontmatter and writes the
changed body at line 2864. Thus an old read can become a new persisted checkbox
that disagrees with the acknowledged Done frontmatter. Deferring refreshes
that start during a save does not cancel a refresh that already started.

Fix: invalidate in-flight reads when a write starts, and check the write epoch
again before applying a read result or calling the parent. Re-read once after
the write. Do not publish stale read projections as editable collaboration
transactions. Use one shared guard for TaskHeader and TaskSection.

Test idea: hold a To do refresh response, complete the Task and receive its
acknowledgement, then release the old response. Assert that the visible state,
Markdown checkbox and stored Task status stay Done after the room flushes.
Repeat with an Inspector status edit and Undo.

Rule: DESIGN §§2, 9, 41; state-changing actions must not lose later edits.

## P1 — An older refresh can persist a stale root checkbox Evidence: `apps/web/src/lib/notes/TaskHeader.svelte:185` starts a refresh and checks only its refresh sequence at line 186. Local writes at lines 67–71 do not invalidate that sequence. `apps/web/src/lib/tasks/TaskSection.svelte:479` and `:483` have the same gap. Start a refresh that reads To do, then complete the Task. If the old response arrives after the completion response, it passes the sequence check and applies To do through `onchange`. `apps/web/src/lib/notes/NoteView.svelte:425` then calls `syncTaskEditor`, which changes the live checkbox back at line 320. The live writer in `crates/plugins/notes/src/lib.rs:2855` preserves frontmatter and writes the changed body at line 2864. Thus an old read can become a new persisted checkbox that disagrees with the acknowledged Done frontmatter. Deferring refreshes that start during a save does not cancel a refresh that already started. Fix: invalidate in-flight reads when a write starts, and check the write epoch again before applying a read result or calling the parent. Re-read once after the write. Do not publish stale read projections as editable collaboration transactions. Use one shared guard for TaskHeader and TaskSection. Test idea: hold a To do refresh response, complete the Task and receive its acknowledgement, then release the old response. Assert that the visible state, Markdown checkbox and stored Task status stay Done after the room flushes. Repeat with an Inspector status edit and Undo. Rule: DESIGN §§2, 9, 41; state-changing actions must not lose later edits.
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#936
No description provided.