Tasks: Inspector status Undo fails after concurrent property update #1430

Closed
opened 2026-10-08 03:01:25 +00:00 by kayg · 13 comments
Owner

Steps

  1. Open a file Task Note and its Task Inspector.
  2. Hold a Task projection read, then update a different Task property (the existing production E2E uses the Task area).
  3. Change Status from To do to Done, release the stale read, then use the visible “Task status updated” Undo action.

Expected

Undo restores To do and keeps the unrelated property update.

Actual

The UI shows “The Task change could not be undone. Try again.” The task projection remains Done. Reproduced by the existing production flow in apps/web/e2e/task-header-659.mjs.

Evidence

artifacts/bash3-tasks/task-undo-revision-evidence.txt

Severity: minor. The ordinary status update succeeds; its Undo fails in the interleaved property update path. The current evidence points to a revision conflict across Task property writes. This bash will trace ownership before deciding whether it can be fixed in the Tasks area.

## Steps 1. Open a file Task Note and its Task Inspector. 2. Hold a Task projection read, then update a different Task property (the existing production E2E uses the Task area). 3. Change Status from To do to Done, release the stale read, then use the visible “Task status updated” Undo action. ## Expected Undo restores To do and keeps the unrelated property update. ## Actual The UI shows “The Task change could not be undone. Try again.” The task projection remains Done. Reproduced by the existing production flow in `apps/web/e2e/task-header-659.mjs`. ## Evidence `artifacts/bash3-tasks/task-undo-revision-evidence.txt` Severity: minor. The ordinary status update succeeds; its Undo fails in the interleaved property update path. The current evidence points to a revision conflict across Task property writes. This bash will trace ownership before deciding whether it can be fixed in the Tasks area.
Author
Owner

Follow-up evidence for #1430: the status PATCH returned HTTP 200 with revision "35753d4ee07d4146d4fad4d7e780d1f9968f24f4f4f151ab7d58d8c36a12ddee"; its Undo PATCH received HTTP 412 (Task changed). The Task remained done and the UI showed “The Task change could not be undone. Try again.” Full trace: artifacts/bash3-tasks/task-undo-revision-evidence.txt. This appears to cross the shared Notes revision/write boundary, so I am keeping it filed only under the shared-core ownership rule.

Follow-up evidence for #1430: the status PATCH returned HTTP 200 with revision `"35753d4ee07d4146d4fad4d7e780d1f9968f24f4f4f151ab7d58d8c36a12ddee"`; its Undo PATCH received HTTP 412 (`Task changed`). The Task remained `done` and the UI showed “The Task change could not be undone. Try again.” Full trace: `artifacts/bash3-tasks/task-undo-revision-evidence.txt`. This appears to cross the shared Notes revision/write boundary, so I am keeping it filed only under the shared-core ownership rule.
Author
Owner

Additional evidence: Undo conflict reproduction details (artifacts/bash3-tasks/task-undo-revision-evidence.txt).

Additional evidence: [Undo conflict reproduction details](https://git.kayg.org/attachments/95d4e71c-42d6-4edd-aed7-de5738817d6b) (`artifacts/bash3-tasks/task-undo-revision-evidence.txt`).
Author
Owner

Starting work on job/undo412-1430, based on origin/dev at d395b6e25d. I am tracing the shared Task write and Undo paths and will add a real-browser regression for the unrelated-property race.

Starting work on job/undo412-1430, based on origin/dev at d395b6e25da20d8c67b8b9fc1a593b813939c8a7. I am tracing the shared Task write and Undo paths and will add a real-browser regression for the unrelated-property race.
Author
Owner

Finding: the focused production-browser regression fails before the fix. It holds a real Task projection response, accepts the Status PATCH, commits a concurrent List edit, releases the stale projection, and clicks the visible Undo action. The inverse PATCH returns HTTP 412 with {"error":{"code":"conflict","message":"Task changed"}} instead of 200.

The broader existing task-header-659.mjs also stops later on its unchanged inline Task assertion: it expects only a Status control row, while the current build renders Status and Tags. I did not change that expectation because it is unrelated to #1430.

Finding: the focused production-browser regression fails before the fix. It holds a real Task projection response, accepts the Status PATCH, commits a concurrent List edit, releases the stale projection, and clicks the visible Undo action. The inverse PATCH returns HTTP 412 with `{"error":{"code":"conflict","message":"Task changed"}}` instead of 200. The broader existing `task-header-659.mjs` also stops later on its unchanged inline Task assertion: it expects only a Status control row, while the current build renders Status and Tags. I did not change that expectation because it is unrelated to #1430.
Author
Owner

Finding: node --check bench/task-header-659.mjs found const completed and let completed declared in the same main() scope. I renamed those locals so the Task Notes performance profile parses and can include the #1430 guarded Undo measurements.

Finding: `node --check bench/task-header-659.mjs` found `const completed` and `let completed` declared in the same `main()` scope. I renamed those locals so the Task Notes performance profile parses and can include the #1430 guarded Undo measurements.
Author
Owner

Decision for #1430: reuse the existing Task properties PATCH with optional expected_fields and expected_tags; compare them with the source read under the per-User writer lock, then apply only the requested inverse properties. A mismatch returns the existing 412 conflict shape. I added the projected List value to TaskView so clients can update that display from the write response without another API read.

Decision for #1430: reuse the existing Task properties PATCH with optional `expected_fields` and `expected_tags`; compare them with the source read under the per-User writer lock, then apply only the requested inverse properties. A mismatch returns the existing 412 conflict shape. I added the projected List value to TaskView so clients can update that display from the write response without another API read.
Author
Owner

Gate finding — Notes test timing under shared load

cargo test -p calternal-plugin-notes completed with 316 passed, 1 failed, and 4 ignored. The sole failure was tests::journal_read_does_not_wait_for_reconciliation; its assertion reported Journal returns within 100 ms while reconciliation is held: Elapsed(()) at crates/plugins/notes/src/lib.rs:20730. The host load average was about 35 during the run. This test does not exercise Task Undo. I kept its expectation unchanged and did not rerun the gate.

Gate finding — Notes test timing under shared load `cargo test -p calternal-plugin-notes` completed with 316 passed, 1 failed, and 4 ignored. The sole failure was `tests::journal_read_does_not_wait_for_reconciliation`; its assertion reported `Journal returns within 100 ms while reconciliation is held: Elapsed(())` at `crates/plugins/notes/src/lib.rs:20730`. The host load average was about 35 during the run. This test does not exercise Task Undo. I kept its expectation unchanged and did not rerun the gate.
Author
Owner

Browser finding — Notes Info shortcut was not wired

The real-browser regression passed the different-property Undo assertions, then the keyboard capture timed out after pressing Meta+I: the Notes Info Inspector stayed closed. The Notes header already labels Info with app.inspector, but NoteView had no handler for that shortcut. I added a NoteView handler that toggles the same shared Inspector at the existing Info toolbar anchor, and the browser regression will cover it with the Mac platform emulation.

Browser finding — Notes Info shortcut was not wired The real-browser regression passed the different-property Undo assertions, then the keyboard capture timed out after pressing `Meta+I`: the Notes Info Inspector stayed closed. The Notes header already labels Info with `app.inspector`, but `NoteView` had no handler for that shortcut. I added a NoteView handler that toggles the same shared Inspector at the existing Info toolbar anchor, and the browser regression will cover it with the Mac platform emulation.
Author
Owner

Real-browser evidence: after the Notes Info shortcut opened the Inspector, the shared surface's programmatic initial focus left matching (task-undo-1430.mjs:95,284). This violates the owner's shortcut focus-ring rule. I am correcting the shared focus path and retaining the assertion.

Real-browser evidence: after the Notes Info shortcut opened the Inspector, the shared surface's programmatic initial focus left matching (task-undo-1430.mjs:95,284). This violates the owner's shortcut focus-ring rule. I am correcting the shared focus path and retaining the assertion.
Author
Owner

Clarification to the last evidence note: the focused element is the shared Inspector Close button, and document.activeElement.matches(':focus-visible') is true after Cmd+I. The shell omitted the code-style names from my previous comment; the exact assertion and selector are in apps/web/e2e/task-undo-1430.mjs.

Clarification to the last evidence note: the focused element is the shared Inspector Close button, and document.activeElement.matches(':focus-visible') is true after Cmd+I. The shell omitted the code-style names from my previous comment; the exact assertion and selector are in apps/web/e2e/task-undo-1430.mjs.
Author
Owner

Benchmark finding: bench/task-header-659.mjs stopped at its existing Inspector check. After loading the real Task Inspector, it found 10 metadata rows where the unchanged profile expects 11 (line 166); no Undo metrics were emitted. I left that expectation unchanged and am adding an Undo-only mode to the same profile so the #1430 hot path can be measured independently.

Benchmark finding: bench/task-header-659.mjs stopped at its existing Inspector check. After loading the real Task Inspector, it found 10 metadata rows where the unchanged profile expects 11 (line 166); no Undo metrics were emitted. I left that expectation unchanged and am adding an Undo-only mode to the same profile so the #1430 hot path can be measured independently.
Author
Owner

Final report

Implemented and committed on job/undo412-1430.

What changed

Undo now re-reads the current Task revision and reapplies only the property being undone. A concurrent edit to another property survives; if that same property changed, the API returns 412 and the UI shows the existing conflict toast. Added a real browser regression that exercises both cases.

Also wired the Notes Info shortcut to open the existing Notes Inspector and routed programmatic focus through the shared quiet-focus behavior, so clicking or using Cmd+I does not leave a focus ring.

Commits:

  • 3f1b0c76f fix(ui): keep Notes Inspector shortcut ringless
  • 9d2c9dd27 fix(tasks): guard Undo by changed properties

Head SHA: 9d2c9dd27f09f6193573d217a40081153487950c

Files

  • Rust contract and regression coverage: crates/plugins/notes/src/tasks_api.rs, crates/plugins/notes/src/reminders_tests.rs
  • API contract/client: contracts/openapi.json, packages/api-client/src/generated.ts
  • Task Undo and callers: apps/web/src/lib/tasks/writes.ts, writes.test.ts, TaskHeader.svelte, TaskSection.svelte, TasksWorkspace.svelte, index.svelte.ts, apps/web/src/routes/calendar/[view]/[date]/+page.svelte
  • Inspector shortcut and shared focus surfaces: apps/web/src/lib/notes/NoteView.svelte, apps/web/src/lib/a11y/focusTrap.ts, focusTrap.test.ts, inputModality.ts, packages/ui/src/components/PopoverSurface.svelte, packages/ui/src/components/menu/FloatingSurface.svelte
  • Browser regression and performance profile: apps/web/e2e/task-undo-1430.mjs, apps/web/package.json, bench/task-header-659.mjs
  • Exact perf fingerprints: contracts/perf/adoption-1058.json, contracts/perf/exceptions.json

Gates and evidence

  • 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 51.09s
  • cargo test -p calternal-plugin-notes: 316 passed, 1 failed, 4 ignored. Sole failure under host load (load average about 35): Journal returns within 100 ms while reconciliation is held: Elapsed(()). Existing timing expectation was left unchanged.
  • cargo clippy -p calternal-server --all-targets -- -D warnings:
    Finished dev profile [unoptimized + debuginfo] target(s) in 2m 07s
  • cargo test -p calternal-server:
    test result: ok. 272 passed; 0 failed; 12 ignored; 0 measured; 0 filtered out; finished in 292.01s
    test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 62.06s
    test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.68s
  • Focused Vitest, bunx vitest run src/lib/a11y/focusTrap.test.ts src/lib/tasks/writes.test.ts --maxWorkers=2:
    Test Files 2 passed (2)
    Tests 18 passed (18)
  • cd apps/web && bun run check:
    perf-lint: PASS; 0 violations; 22544 scoped exceptions
    svelte-check found 0 errors and 4 warnings in 2 files
    The four warnings are existing Mail unused CSS selectors and Money .ready.
  • bun run build: exit 0.
  • Production real-browser regression:
    PASS #1430: property-level Task Undo, same-property conflict, and 12 macOS production captures (/home/kayg/Developer/calternal-wt/undo412-1430/artifacts/task-undo-1430)
    Captures cover pointer and Cmd+I, light and dark, at 390, 820 and 1440 px. They are attached below.
  • Full bun run test and full adversarial matrices are deferred to the merge round under the shared verification policy.

UX gaps closed / left

Closed: different-property edits survive Undo; same-property conflicts are visible; the Notes Info shortcut now opens the Inspector; programmatic opening and focus restoration do not show a ring. No additional UX gap was found in the exercised flow.

Performance

Expected Undo work: read and project the current Task, then write only the targeted property and update its index under the existing per-User lock. The local profile used a Task with 100 tags and 50 property-write/guarded-Undo pairs; it also ran a 32-Task Undo burst. On the shared, loaded local host: write p50/p95 489.5/1258.8 ms; Undo p50/p95 498.4/2108.5 ms; burst elapsed 11,904.5 ms, API p50/p95 5,715.8/11,469.3 ms. No directly comparable property-level Undo baseline exists.

The full existing task-header-659 profile stopped at its unchanged Inspector-row expectation (11); the live 100-tag fixture had 10 rows. The added TASK_UNDO_ONLY=1 profile completed. No existing expectation was changed.

Decisions not specified by DESIGN

The PATCH contract uses optional expected_fields and expected_tags guards to carry the property's prior value. A mismatch returns 412; the server applies the requested property to the current Markdown under the existing per-User lock. This keeps the conflict check and targeted write atomic without replacing unrelated Task properties.

Screenshots

## Final report Implemented and committed on `job/undo412-1430`. ### What changed Undo now re-reads the current Task revision and reapplies only the property being undone. A concurrent edit to another property survives; if that same property changed, the API returns 412 and the UI shows the existing conflict toast. Added a real browser regression that exercises both cases. Also wired the Notes Info shortcut to open the existing Notes Inspector and routed programmatic focus through the shared quiet-focus behavior, so clicking or using Cmd+I does not leave a focus ring. Commits: - `3f1b0c76f fix(ui): keep Notes Inspector shortcut ringless` - `9d2c9dd27 fix(tasks): guard Undo by changed properties` Head SHA: `9d2c9dd27f09f6193573d217a40081153487950c` ### Files - Rust contract and regression coverage: `crates/plugins/notes/src/tasks_api.rs`, `crates/plugins/notes/src/reminders_tests.rs` - API contract/client: `contracts/openapi.json`, `packages/api-client/src/generated.ts` - Task Undo and callers: `apps/web/src/lib/tasks/writes.ts`, `writes.test.ts`, `TaskHeader.svelte`, `TaskSection.svelte`, `TasksWorkspace.svelte`, `index.svelte.ts`, `apps/web/src/routes/calendar/[view]/[date]/+page.svelte` - Inspector shortcut and shared focus surfaces: `apps/web/src/lib/notes/NoteView.svelte`, `apps/web/src/lib/a11y/focusTrap.ts`, `focusTrap.test.ts`, `inputModality.ts`, `packages/ui/src/components/PopoverSurface.svelte`, `packages/ui/src/components/menu/FloatingSurface.svelte` - Browser regression and performance profile: `apps/web/e2e/task-undo-1430.mjs`, `apps/web/package.json`, `bench/task-header-659.mjs` - Exact perf fingerprints: `contracts/perf/adoption-1058.json`, `contracts/perf/exceptions.json` ### Gates and evidence - `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 51.09s` - `cargo test -p calternal-plugin-notes`: 316 passed, 1 failed, 4 ignored. Sole failure under host load (load average about 35): `Journal returns within 100 ms while reconciliation is held: Elapsed(())`. Existing timing expectation was left unchanged. - `cargo clippy -p calternal-server --all-targets -- -D warnings`: `Finished dev profile [unoptimized + debuginfo] target(s) in 2m 07s` - `cargo test -p calternal-server`: `test result: ok. 272 passed; 0 failed; 12 ignored; 0 measured; 0 filtered out; finished in 292.01s` `test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 62.06s` `test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.68s` - Focused Vitest, `bunx vitest run src/lib/a11y/focusTrap.test.ts src/lib/tasks/writes.test.ts --maxWorkers=2`: `Test Files 2 passed (2)` `Tests 18 passed (18)` - `cd apps/web && bun run check`: `perf-lint: PASS; 0 violations; 22544 scoped exceptions` `svelte-check found 0 errors and 4 warnings in 2 files` The four warnings are existing Mail unused CSS selectors and Money `.ready`. - `bun run build`: exit 0. - Production real-browser regression: `PASS #1430: property-level Task Undo, same-property conflict, and 12 macOS production captures (/home/kayg/Developer/calternal-wt/undo412-1430/artifacts/task-undo-1430)` Captures cover pointer and Cmd+I, light and dark, at 390, 820 and 1440 px. They are attached below. - Full `bun run test` and full adversarial matrices are deferred to the merge round under the shared verification policy. ### UX gaps closed / left Closed: different-property edits survive Undo; same-property conflicts are visible; the Notes Info shortcut now opens the Inspector; programmatic opening and focus restoration do not show a ring. No additional UX gap was found in the exercised flow. ### Performance Expected Undo work: read and project the current Task, then write only the targeted property and update its index under the existing per-User lock. The local profile used a Task with 100 tags and 50 property-write/guarded-Undo pairs; it also ran a 32-Task Undo burst. On the shared, loaded local host: write p50/p95 489.5/1258.8 ms; Undo p50/p95 498.4/2108.5 ms; burst elapsed 11,904.5 ms, API p50/p95 5,715.8/11,469.3 ms. No directly comparable property-level Undo baseline exists. The full existing `task-header-659` profile stopped at its unchanged Inspector-row expectation (11); the live 100-tag fixture had 10 rows. The added `TASK_UNDO_ONLY=1` profile completed. No existing expectation was changed. ### Decisions not specified by DESIGN The PATCH contract uses optional `expected_fields` and `expected_tags` guards to carry the property's prior value. A mismatch returns 412; the server applies the requested property to the current Markdown under the existing per-User lock. This keeps the conflict check and targeted write atomic without replacing unrelated Task properties. ### Screenshots - Pointer, 390 light: https://git.kayg.org/attachments/d800eb96-ad7e-4e55-b8c3-6e8928c454ae - Pointer, 390 dark: https://git.kayg.org/attachments/14993ead-b532-4e21-8f6e-b607d442101f - Pointer, 820 light: https://git.kayg.org/attachments/79f9f69d-d00c-4011-9718-fc726ed0e291 - Pointer, 820 dark: https://git.kayg.org/attachments/c4c1a630-65ab-4849-8483-ecdfbab681ff - Pointer, 1440 light: https://git.kayg.org/attachments/bd2b8533-5810-4656-94e6-40077a43cdd3 - Pointer, 1440 dark: https://git.kayg.org/attachments/03fceff1-90f9-4188-815d-b3c7e23adae2 - Cmd+I, 390 light: https://git.kayg.org/attachments/3c9c98c7-06ad-436c-af23-a5ab4e342fbb - Cmd+I, 390 dark: https://git.kayg.org/attachments/1856cd34-6b12-4a27-a867-11f34efc21ab - Cmd+I, 820 light: https://git.kayg.org/attachments/0ec174dc-b1ef-4496-9bda-5c6e89fd77b7 - Cmd+I, 820 dark: https://git.kayg.org/attachments/026825f5-a3f4-4a74-a4ca-f95cec724158 - Cmd+I, 1440 light: https://git.kayg.org/attachments/7db3f459-2dba-43f1-833b-8d6c332bc664 - Cmd+I, 1440 dark: https://git.kayg.org/attachments/29bdf7c8-0c7f-449c-95ab-4848e0f15126
kayg referenced this issue from a commit 2026-10-08 19:43:32 +00:00
Author
Owner

Deployed to calternal.cloud in merge round 35b (build 9dc65437e, live 2026-10-09 ~11:07 IST). Closing.

Deployed to calternal.cloud in merge round 35b (build 9dc65437e, live 2026-10-09 ~11:07 IST). Closing.
kayg closed this issue 2026-10-09 05:36:44 +00:00
Commenting is not possible because the repository is archived.
No labels
No milestone
No project
No assignees
1 participant
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-v0#1430
No description provided.