Reminders over CalDAV: a Task with only a scheduled time shows undated in Apple Reminders (DTSTART only, floating) #647

Closed
opened 2026-10-01 18:48:08 +00:00 by kayg · 7 comments
Owner

Summary

A Task created in calternal with a time but no due date ("MDV web task tomorrow 15:00 p2") shows in Apple Reminders with no date at all. The VTODO has only a floating DTSTART and no DUE; Reminders shows dates from DUE, so the Task looks undated on the Mac and gets no notification. The DTSTART also has no TZID although the request had the User's zone (Asia/Kolkata).

Found in the Apple interop run on the macOS 27 VM, 2026-10-01 (lab server from dev at 687ff7031). Not a merge blocker (no data loss); the Task Markdown is correct.

Repro

  1. POST /api/v1/notes/tasks {"text":"MDV web task tomorrow 15:00 p2","standalone":true} with X-Calternal-Timezone: Asia/Kolkata → 201, scheduled: 2026-10-02 15:00:00, due: null.
  2. CalDAV GET of …/reminders/fecb5406-….ics returns:
BEGIN:VTODO
UID:fecb5406-7426-4afa-a70c-f013d572b841
SUMMARY:MDV web task
STATUS:NEEDS-ACTION
DTSTART:20261002T150000
PRIORITY:3
END:VTODO
  1. After a Reminders sync, AppleScript reads due date = missing value; the Reminders UI shows no date. A Task with due 2026-10-08 in the same run shows its date correctly.

Expected

A Task that has a date in calternal also has a visible date in Reminders. Options for the fix: project scheduled as DUE when due is empty (Apple has no separate scheduled date), or have the composer set due for "tomorrow 15:00". Timed values carry the User's TZID (or UTC), not floating time. Owner choice only if the mapping of scheduled vs due to Apple is not already decided; check DESIGN §31 first.

Actual

Undated on Apple, floating time in the VTODO.

## Summary A Task created in calternal with a time but no due date ("MDV web task tomorrow 15:00 p2") shows in Apple Reminders with **no date at all**. The VTODO has only a floating `DTSTART` and no `DUE`; Reminders shows dates from `DUE`, so the Task looks undated on the Mac and gets no notification. The `DTSTART` also has no `TZID` although the request had the User's zone (Asia/Kolkata). Found in the Apple interop run on the macOS 27 VM, 2026-10-01 (lab server from `dev` at `687ff7031`). Not a merge blocker (no data loss); the Task Markdown is correct. ## Repro 1. `POST /api/v1/notes/tasks {"text":"MDV web task tomorrow 15:00 p2","standalone":true}` with `X-Calternal-Timezone: Asia/Kolkata` → 201, `scheduled: 2026-10-02 15:00:00`, `due: null`. 2. CalDAV GET of `…/reminders/fecb5406-….ics` returns: ``` BEGIN:VTODO UID:fecb5406-7426-4afa-a70c-f013d572b841 SUMMARY:MDV web task STATUS:NEEDS-ACTION DTSTART:20261002T150000 PRIORITY:3 END:VTODO ``` 3. After a Reminders sync, AppleScript reads `due date = missing value`; the Reminders UI shows no date. A Task with `due 2026-10-08` in the same run shows its date correctly. ## Expected A Task that has a date in calternal also has a visible date in Reminders. Options for the fix: project `scheduled` as `DUE` when `due` is empty (Apple has no separate scheduled date), or have the composer set `due` for "tomorrow 15:00". Timed values carry the User's `TZID` (or UTC), not floating time. Owner choice only if the mapping of scheduled vs due to Apple is not already decided; check DESIGN §31 first. ## Actual Undated on Apple, floating time in the VTODO.
Author
Owner

Started #643 and #647 on job/reminders-643, base 687ff70313. Read the issue bodies and apple-interop-2026-10-02.md. Tracing preserved DAV dates and PRIORITY against current Markdown fields, source fingerprints, and checked writes. No push, deployment, or Mac account change.

Started #643 and #647 on job/reminders-643, base 687ff703136e71e89f8dfba139e93cd0788b25c1. Read the issue bodies and apple-interop-2026-10-02.md. Tracing preserved DAV dates and PRIORITY against current Markdown fields, source fingerprints, and checked writes. No push, deployment, or Mac account change.
Author
Owner

Finding: the scheduled-only composer replay failed with DTSTART:20261003T150000, no DUE and no TZID, despite X-Calternal-Timezone: Asia/Kolkata. DESIGN §31 requires VTODO but does not choose this Apple mapping. Decision: project scheduled as DUE when due is absent, retain DTSTART, and use X-CALTERNAL-DUE-ROLE:SCHEDULED for lossless write-back. If Apple omits the marker, the existing scheduled-only Task keeps its role. Persist the request zone with the existing per-UID DAV frontmatter map; date-only values need no zone. Native legacy timed Tasks with no recorded zone use UTC. A Reminder feed epoch rotation will force existing clients to refetch changed projections on upgrade. Serializer and Notes ETag slices are committed as 2678f92db and 73fcee8d9; the ETag slice passed 166 Notes unit tests and the Apple replay test.

Finding: the scheduled-only composer replay failed with DTSTART:20261003T150000, no DUE and no TZID, despite X-Calternal-Timezone: Asia/Kolkata. DESIGN §31 requires VTODO but does not choose this Apple mapping. Decision: project scheduled as DUE when due is absent, retain DTSTART, and use X-CALTERNAL-DUE-ROLE:SCHEDULED for lossless write-back. If Apple omits the marker, the existing scheduled-only Task keeps its role. Persist the request zone with the existing per-UID DAV frontmatter map; date-only values need no zone. Native legacy timed Tasks with no recorded zone use UTC. A Reminder feed epoch rotation will force existing clients to refetch changed projections on upgrade. Serializer and Notes ETag slices are committed as 2678f92db and 73fcee8d9; the ETag slice passed 166 Notes unit tests and the Apple replay test.
Author
Owner

Commit a0115a3bf fixes a real-writer regression: a marker-free Apple PUT with DUE removed and the old DTSTART still present restored the removed date. The failing test observed scheduled=2026-09-30 instead of None. The parser now carries its role provenance, and the provider treats missing DUE as a date removal for an existing scheduled-only Task. The regression passes. The same commit preserves the baseline for later alarm invalidation after a no-op date PATCH and parses folded/case-insensitive absolute alarm triggers. DAV and Notes clippy/tests passed; server gates and the executable build are running.

Commit a0115a3bf fixes a real-writer regression: a marker-free Apple PUT with DUE removed and the old DTSTART still present restored the removed date. The failing test observed scheduled=2026-09-30 instead of None. The parser now carries its role provenance, and the provider treats missing DUE as a date removal for an existing scheduled-only Task. The regression passes. The same commit preserves the baseline for later alarm invalidation after a no-op date PATCH and parses folded/case-insensitive absolute alarm triggers. DAV and Notes clippy/tests passed; server gates and the executable build are running.
Author
Owner

The earlier UTC fallback decision is withdrawn after a real-Mac check. Reminders displays a floating DUE at 15:00, and a Mac notes edit keeps both API clocks at 15:00. Legacy Tasks with no stored request zone now remain floating; the server does not guess a zone. New Tasks still retain the request zone. A new native-legacy projection regression failed on DUE:20260928T150000Z and passes on floating DUE:20260928T150000. DAV clippy and 46 unit / 36 replay tests pass. The final dependent Notes and server gates are running. The original lab server is detached, remains ready after the launcher exits, and does not retain macvm.lock.

The earlier UTC fallback decision is withdrawn after a real-Mac check. Reminders displays a floating DUE at 15:00, and a Mac notes edit keeps both API clocks at 15:00. Legacy Tasks with no stored request zone now remain floating; the server does not guess a zone. New Tasks still retain the request zone. A new native-legacy projection regression failed on `DUE:20260928T150000Z` and passes on floating `DUE:20260928T150000`. DAV clippy and 46 unit / 36 replay tests pass. The final dependent Notes and server gates are running. The original lab server is detached, remains ready after the launcher exits, and does not retain macvm.lock.
Author
Owner

Completed #643 and #647 on job/reminders-643. Head: bc66ed31d62bb020115b27cfe3f5bb2e1f38b743. Base: 687ff703136e71e89f8dfba139e93cd0788b25c1. Fetched origin and merged origin/dev once before final gates; it was already up to date. No push or permanent deployment. The original Mac lab server is restored and ready.

Built:

  • Current Task priority and dates override stale Apple wire metadata. Precise DAV values survive only while they agree with Markdown. Obsolete absolute alarms are cleared with the shared parser; relative alarms remain.
  • ETags cover the Markdown source fingerprint and VTODO bytes. The existing shared writer lock enforces If-Match against that current version. Stale PUT returns 412. The existing source journal advances the collection cursor; migration 0024 rotates the feed epoch once for the wire upgrade.
  • Scheduled-only Tasks expose DUE and DTSTART. The role marker and existing Task role keep writes scheduled. Removing DUE clears the schedule even when Apple drops the marker. New date writes retain the request zone. Legacy clocks with no stored zone stay floating.
  • Captured-request replay, real-writer concurrency and property tests, a bounded live replay, and a GET/sync performance profile. No existing dev test assertion was changed; direct edit constructors gain the new provenance field.

Files:

  • bench/reminders-sync.py
  • crates/calternal-dav/src/reminders.rs
  • crates/calternal-dav/tests/apple_replay.rs
  • crates/plugins/notes/migrations/0024_reminder_authoritative_wire_epoch.sql
  • crates/plugins/notes/src/lib.rs
  • crates/plugins/notes/src/reminders_tests.rs
  • crates/plugins/notes/src/store.rs
  • crates/plugins/notes/src/tasks_api.rs
  • crates/plugins/notes/src/tasks_dav.rs
  • crates/plugins/notes/src/tests/unicode_titles.rs
  • docs/perf/reminders-643.local.json
  • tests/adversarial/reminders_sync.py

Real Mac verification on the final branch build:

  • API priority high and due 2026-10-07 11:45 appeared in Reminders as priority 3 and 7 October 11:45. A Mac notes edit kept high priority and the same due date/time. A stale Apple-shaped PUT returned 412.
  • A scheduled-only Task appeared on 3 October at 15:00. Changing the Mac date to 16:00 kept it scheduled and did not add a deadline.
  • A legacy Task was staged through Files DAV with no zone metadata. It appeared at 15:00; a Mac notes edit kept 15:00 and no deadline. The earlier UTC fallback decision is withdrawn.
  • All three fixtures were deleted (204). The lab daemon runs in a detached session, does not inherit macvm.lock, and remains ready after the launcher exits. Evidence: artifacts/mac-reminders-proof.log, artifacts/reminders-live-replay.log, artifacts/mac-floating-due.log.

Gates, verbatim completion output (full logs: artifacts/final-*.log):

cargo fmt --check: exit 0, no output.

cargo clippy -p calternal-dav --all-targets -- -D warnings

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 16.97s

cargo test -p calternal-dav

test result: ok. 46 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.21s
test result: ok. 36 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.10s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

cargo clippy -p calternal-plugin-notes --all-targets -- -D warnings

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1m 30s

cargo test -p calternal-plugin-notes

test result: ok. 170 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 96.20s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.21s
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 1m 41s

cargo test -p calternal-server

test result: ok. 107 passed; 0 failed; 3 ignored; 0 measured; 0 filtered out; finished in 33.82s

The three server entry points marked ignored are run by the passing live_apps_run_in_separate_processes test. cargo build -p calternal-server also passed. No frontend file changed, so web gates and UI screenshot gates do not apply.

DAV adversarial round:

  • One bounded existing DAV round completed on a private local Instance. One non-SLOW fixture finding is filed as #650: the 100-href Daily note setup PUT returned 412 instead of 201, so its batch checks did not run. Its existing expectation is unchanged.
  • Other reported findings were SLOW observations under host load. The scripted WebDAV probes completed. The separate #643/#647 live replay passed. No crash or 5xx was reported by this round.

Performance (local shared host, debug; result: docs/perf/reminders-643.local.json):

Phase Before p50/p95 ms After p50/p95 ms CPU seconds before/after RSS KiB before/after
average_get 520.9 / 699.8 1317.9 / 1859.3 16.17 / 17.24 777604 / 358136
average_sync 471.3 / 547.9 1548.1 / 2298.7 13.72 / 15.68 777604 / 358236
worst_sync 5621.3 / 10704.5 10374.3 / 22174.2 15.77 / 15.01 895072 / 498340

The worst case uses 2,000 Tasks and eight concurrent sync requests. The perf VM was unreachable. The official baseline has no matching Reminder profile. Local load rose from 20.6 to 29.9 and server CPU share fell. #651 records p95 above the local comparison and requests a controlled perf-VM follow-up. These runs do not establish a code regression. The measured commit is recorded in the JSON; the later legacy-clock correction does not change this date-only profile input.

Known gaps:

  • #650 prevents the existing 100-href adversarial fixture from reaching its batch checks.
  • Performance requires the controlled follow-up in #651; local wall time is affected by shared-host load.

Decisions not specified by the design:

  • Use X-CALTERNAL-DUE-ROLE:SCHEDULED to retain the scheduled role while exposing Apple DUE. If Apple drops the marker, the existing scheduled-only Task owns that visible date.
  • Keep missing-zone legacy clocks floating, based on real Mac evidence. Do not guess UTC or a historical request zone.
  • Invalidate absolute alarms when their retained date/zone changes; keep relative alarms.
  • Rotate the Reminder feed epoch once so existing clients refetch the changed projection without a Markdown mutation.

Cleanup: cargo clean and web build-output cleanup completed. Review artifacts remain uncommitted in artifacts/.

Completed #643 and #647 on `job/reminders-643`. Head: `bc66ed31d62bb020115b27cfe3f5bb2e1f38b743`. Base: `687ff703136e71e89f8dfba139e93cd0788b25c1`. Fetched origin and merged origin/dev once before final gates; it was already up to date. No push or permanent deployment. The original Mac lab server is restored and ready. Built: - Current Task priority and dates override stale Apple wire metadata. Precise DAV values survive only while they agree with Markdown. Obsolete absolute alarms are cleared with the shared parser; relative alarms remain. - ETags cover the Markdown source fingerprint and VTODO bytes. The existing shared writer lock enforces If-Match against that current version. Stale PUT returns 412. The existing source journal advances the collection cursor; migration 0024 rotates the feed epoch once for the wire upgrade. - Scheduled-only Tasks expose DUE and DTSTART. The role marker and existing Task role keep writes scheduled. Removing DUE clears the schedule even when Apple drops the marker. New date writes retain the request zone. Legacy clocks with no stored zone stay floating. - Captured-request replay, real-writer concurrency and property tests, a bounded live replay, and a GET/sync performance profile. No existing dev test assertion was changed; direct edit constructors gain the new provenance field. Files: - `bench/reminders-sync.py` - `crates/calternal-dav/src/reminders.rs` - `crates/calternal-dav/tests/apple_replay.rs` - `crates/plugins/notes/migrations/0024_reminder_authoritative_wire_epoch.sql` - `crates/plugins/notes/src/lib.rs` - `crates/plugins/notes/src/reminders_tests.rs` - `crates/plugins/notes/src/store.rs` - `crates/plugins/notes/src/tasks_api.rs` - `crates/plugins/notes/src/tasks_dav.rs` - `crates/plugins/notes/src/tests/unicode_titles.rs` - `docs/perf/reminders-643.local.json` - `tests/adversarial/reminders_sync.py` Real Mac verification on the final branch build: - API priority high and due 2026-10-07 11:45 appeared in Reminders as priority 3 and 7 October 11:45. A Mac notes edit kept high priority and the same due date/time. A stale Apple-shaped PUT returned 412. - A scheduled-only Task appeared on 3 October at 15:00. Changing the Mac date to 16:00 kept it scheduled and did not add a deadline. - A legacy Task was staged through Files DAV with no zone metadata. It appeared at 15:00; a Mac notes edit kept 15:00 and no deadline. The earlier UTC fallback decision is withdrawn. - All three fixtures were deleted (204). The lab daemon runs in a detached session, does not inherit macvm.lock, and remains ready after the launcher exits. Evidence: `artifacts/mac-reminders-proof.log`, `artifacts/reminders-live-replay.log`, `artifacts/mac-floating-due.log`. Gates, verbatim completion output (full logs: artifacts/final-*.log): `cargo fmt --check`: exit 0, no output. `cargo clippy -p calternal-dav --all-targets -- -D warnings` ```text Finished `dev` profile [unoptimized + debuginfo] target(s) in 16.97s ``` `cargo test -p calternal-dav` ```text test result: ok. 46 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.21s test result: ok. 36 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.10s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s ``` `cargo clippy -p calternal-plugin-notes --all-targets -- -D warnings` ```text Finished `dev` profile [unoptimized + debuginfo] target(s) in 1m 30s ``` `cargo test -p calternal-plugin-notes` ```text test result: ok. 170 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 96.20s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.21s 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` ```text Finished `dev` profile [unoptimized + debuginfo] target(s) in 1m 41s ``` `cargo test -p calternal-server` ```text test result: ok. 107 passed; 0 failed; 3 ignored; 0 measured; 0 filtered out; finished in 33.82s ``` The three server entry points marked ignored are run by the passing `live_apps_run_in_separate_processes` test. `cargo build -p calternal-server` also passed. No frontend file changed, so web gates and UI screenshot gates do not apply. DAV adversarial round: - One bounded existing DAV round completed on a private local Instance. One non-SLOW fixture finding is filed as #650: the 100-href Daily note setup PUT returned 412 instead of 201, so its batch checks did not run. Its existing expectation is unchanged. - Other reported findings were SLOW observations under host load. The scripted WebDAV probes completed. The separate #643/#647 live replay passed. No crash or 5xx was reported by this round. Performance (local shared host, debug; result: docs/perf/reminders-643.local.json): | Phase | Before p50/p95 ms | After p50/p95 ms | CPU seconds before/after | RSS KiB before/after | |---|---:|---:|---:|---:| | average_get | 520.9 / 699.8 | 1317.9 / 1859.3 | 16.17 / 17.24 | 777604 / 358136 | | average_sync | 471.3 / 547.9 | 1548.1 / 2298.7 | 13.72 / 15.68 | 777604 / 358236 | | worst_sync | 5621.3 / 10704.5 | 10374.3 / 22174.2 | 15.77 / 15.01 | 895072 / 498340 | The worst case uses 2,000 Tasks and eight concurrent sync requests. The perf VM was unreachable. The official baseline has no matching Reminder profile. Local load rose from 20.6 to 29.9 and server CPU share fell. #651 records p95 above the local comparison and requests a controlled perf-VM follow-up. These runs do not establish a code regression. The measured commit is recorded in the JSON; the later legacy-clock correction does not change this date-only profile input. Known gaps: - #650 prevents the existing 100-href adversarial fixture from reaching its batch checks. - Performance requires the controlled follow-up in #651; local wall time is affected by shared-host load. Decisions not specified by the design: - Use `X-CALTERNAL-DUE-ROLE:SCHEDULED` to retain the scheduled role while exposing Apple DUE. If Apple drops the marker, the existing scheduled-only Task owns that visible date. - Keep missing-zone legacy clocks floating, based on real Mac evidence. Do not guess UTC or a historical request zone. - Invalidate absolute alarms when their retained date/zone changes; keep relative alarms. - Rotate the Reminder feed epoch once so existing clients refetch the changed projection without a Markdown mutation. Cleanup: cargo clean and web build-output cleanup completed. Review artifacts remain uncommitted in artifacts/.
Author
Owner

Round 2 on staging image c4a61e8cf090170f35b1bed3350d9de20c83ecd5: Task 4a0fc2d9-67f1-472e-82c2-a3b6a368c8a0, R2 scheduled-only task, created by the API with scheduled: 2026-10-02, scheduled_time: 15:00:00, no due date. DAV GET returned floating DTSTART:20261002T150000, no DUE. Reminders read both due date and allday due date as missing value.

New consequence: after native Reminders activity/sync, without explicitly editing this Task, API readback returned scheduled: null, scheduled_time: null. The native client writes a projection without the scheduled value. This is now observed data loss, not only a hidden date. Please treat it as a release blocker. The matrix and raw non-secret evidence are retained under #476.

Round 2 on staging image `c4a61e8cf090170f35b1bed3350d9de20c83ecd5`: Task `4a0fc2d9-67f1-472e-82c2-a3b6a368c8a0`, `R2 scheduled-only task`, created by the API with `scheduled: 2026-10-02`, `scheduled_time: 15:00:00`, no due date. DAV GET returned floating `DTSTART:20261002T150000`, no DUE. Reminders read both due date and allday due date as missing value. New consequence: after native Reminders activity/sync, without explicitly editing this Task, API readback returned `scheduled: null`, `scheduled_time: null`. The native client writes a projection without the scheduled value. This is now observed data loss, not only a hidden date. Please treat it as a release blocker. The matrix and raw non-secret evidence are retained under #476.
Author
Owner

Superseded by #940: the queued job/fix-940 branch includes the #647 fix and is the merge-round replacement for job/reminders-643. #940 stays open until it lands on origin/dev.

Superseded by #940: the queued `job/fix-940` branch includes the #647 fix and is the merge-round replacement for `job/reminders-643`. #940 stays open until it lands on origin/dev.
kayg closed this issue 2026-10-03 11:55:55 +00:00
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#647
No description provided.