Conversation
There was a problem hiding this comment.
1 issue found across 13 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/desktop/src/services/calendar/process/events/sync.ts">
<violation number="1" location="apps/desktop/src/services/calendar/process/events/sync.ts:28">
P1: When a legacy Apple occurrence was tombstoned before its replacement arrives, this key cannot reconcile the old row because `existing` excludes tombstones with different tracking IDs. Load tombstoned legacy candidates by stable occurrence identity before applying this match, or the replacement creates a second durable row and leaves linked sessions on the tombstone.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| for (const storeEvent of existing) { | ||
| const trackingId = storeEvent.tracking_id_event; | ||
| const key = eventKey(storeEvent.calendar_id, trackingId); | ||
| const key = calendarEventKey( |
There was a problem hiding this comment.
P1: When a legacy Apple occurrence was tombstoned before its replacement arrives, this key cannot reconcile the old row because existing excludes tombstones with different tracking IDs. Load tombstoned legacy candidates by stable occurrence identity before applying this match, or the replacement creates a second durable row and leaves linked sessions on the tombstone.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/services/calendar/process/events/sync.ts, line 28:
<comment>When a legacy Apple occurrence was tombstoned before its replacement arrives, this key cannot reconcile the old row because `existing` excludes tombstones with different tracking IDs. Load tombstoned legacy candidates by stable occurrence identity before applying this match, or the replacement creates a second durable row and leaves linked sessions on the tombstone.</comment>
<file context>
@@ -17,27 +18,31 @@ export function syncEvents(
for (const storeEvent of existing) {
- const trackingId = storeEvent.tracking_id_event;
- const key = eventKey(storeEvent.calendar_id, trackingId);
+ const key = calendarEventKey(
+ ctx.provider,
+ storeEvent.calendar_id,
</file context>
There was a problem hiding this comment.
All reported issues were addressed across 6 files (changes from recent commits).
Requires human review: Auto-approval blocked because this review re-detected 1 unresolved issue already reported by Cubic.
Re-trigger cubic
There was a problem hiding this comment.
All reported issues were addressed across 10 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
There was a problem hiding this comment.
2 issues found across 14 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/desktop/src/services/calendar/process/events/sync.test.ts">
<violation number="1" location="apps/desktop/src/services/calendar/process/events/sync.test.ts:494">
P3: This test does not exercise the alias-candidate behavior named in the test. Use a legacy or detached Apple `tracking_id_event` and an input that reaches alias lookup, so the test fails if alias candidates are incorrectly deleted.</violation>
</file>
<file name="apps/desktop/src/services/calendar/fetch/existing.ts">
<violation number="1" location="apps/desktop/src/services/calendar/fetch/existing.ts:12">
P2: When Apple provides no `external_id`, legacy occurrence aliases outside the sync window are never loaded. Pass the derived Apple identity aliases to `loadEventsForSync`, otherwise rescheduled occurrences can create a second durable row.</violation>
</file>
Requires human review: Auto-approval blocked because this review re-detected 1 unresolved issue already reported by Cubic.
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| return loadEventsForSync( | ||
| ctx, | ||
| incoming.map((event) => event.tracking_id_event), | ||
| incoming.flatMap((event) => (event.external_id ? [event.external_id] : [])), |
There was a problem hiding this comment.
P2: When Apple provides no external_id, legacy occurrence aliases outside the sync window are never loaded. Pass the derived Apple identity aliases to loadEventsForSync, otherwise rescheduled occurrences can create a second durable row.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/services/calendar/fetch/existing.ts, line 12:
<comment>When Apple provides no `external_id`, legacy occurrence aliases outside the sync window are never loaded. Pass the derived Apple identity aliases to `loadEventsForSync`, otherwise rescheduled occurrences can create a second durable row.</comment>
<file context>
@@ -9,5 +9,6 @@ export function fetchExistingEvents(
return loadEventsForSync(
ctx,
incoming.map((event) => event.tracking_id_event),
+ incoming.flatMap((event) => (event.external_id ? [event.external_id] : [])),
);
}
</file context>
| }); | ||
| }); | ||
|
|
||
| test("does not delete an unmatched alias candidate outside the sync window", () => { |
There was a problem hiding this comment.
P3: This test does not exercise the alias-candidate behavior named in the test. Use a legacy or detached Apple tracking_id_event and an input that reaches alias lookup, so the test fails if alias candidates are incorrectly deleted.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/services/calendar/process/events/sync.test.ts, line 494:
<comment>This test does not exercise the alias-candidate behavior named in the test. Use a legacy or detached Apple `tracking_id_event` and an input that reaches alias lookup, so the test fails if alias candidates are incorrectly deleted.</comment>
<file context>
@@ -491,4 +490,22 @@ describe("syncEvents", () => {
});
});
+
+ test("does not delete an unmatched alias candidate outside the sync window", () => {
+ const result = syncEvents(
+ createMockCtx(),
</file context>
There was a problem hiding this comment.
All reported issues were addressed across 15 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
There was a problem hiding this comment.
1 existing issue remains and 1 new issue found across 14 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/desktop/src/services/calendar/storage.ts">
<violation number="1" location="apps/desktop/src/services/calendar/storage.ts:484">
P2: When a legacy session has only `external_event_id` or JSON tracking metadata, this join leaves its title and occurrence times empty. Apple visible-key matching then cannot migrate that session to a replacement occurrence; retain a same-calendar fallback for unlinked legacy sessions.</violation>
</file>
Requires human review: Auto-approval blocked because this review re-detected 1 unresolved issue already reported by Cubic.
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| FROM sessions AS session | ||
| LEFT JOIN events AS event | ||
| ON event.id = session.event_id AND event.deleted_at IS NULL | ||
| ON event.id = session.event_id |
There was a problem hiding this comment.
P2: When a legacy session has only external_event_id or JSON tracking metadata, this join leaves its title and occurrence times empty. Apple visible-key matching then cannot migrate that session to a replacement occurrence; retain a same-calendar fallback for unlinked legacy sessions.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/services/calendar/storage.ts, line 484:
<comment>When a legacy session has only `external_event_id` or JSON tracking metadata, this join leaves its title and occurrence times empty. Apple visible-key matching then cannot migrate that session to a replacement occurrence; retain a same-calendar fallback for unlinked legacy sessions.</comment>
<file context>
@@ -430,98 +435,53 @@ export async function loadSessionsForTrackingIds(
- )
- )
- AND event.deleted_at IS NULL
+ ON event.id = session.event_id
WHERE session.deleted_at IS NULL
) AS session_with_event
</file context>
There was a problem hiding this comment.
2 existing issues remain and 1 new issue found across 14 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/desktop/src/services/calendar/storage.test.ts">
<violation number="1" location="apps/desktop/src/services/calendar/storage.test.ts:187">
P2: This test never exercises the directly linked-event path it claims to cover: the mock returns the visible fields regardless of the join, and the fixture omits `session.event_id`. Use a SQLite-backed fixture with a nonempty direct event link and distinct session/event values, or otherwise make the mock validate which event the query selects.</violation>
</file>
Requires human review: Auto-approval blocked because this review re-detected 2 unresolved issues already reported by Cubic.
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| ); | ||
| }); | ||
|
|
||
| test("loads visible identity fields from the directly linked event row", async () => { |
There was a problem hiding this comment.
P2: This test never exercises the directly linked-event path it claims to cover: the mock returns the visible fields regardless of the join, and the fixture omits session.event_id. Use a SQLite-backed fixture with a nonempty direct event link and distinct session/event values, or otherwise make the mock validate which event the query selects.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/services/calendar/storage.test.ts, line 187:
<comment>This test never exercises the directly linked-event path it claims to cover: the mock returns the visible fields regardless of the join, and the fixture omits `session.event_id`. Use a SQLite-backed fixture with a nonempty direct event link and distinct session/event values, or otherwise make the mock validate which event the query selects.</comment>
<file context>
@@ -184,17 +184,18 @@ describe("calendar SQLite storage", () => {
});
- test("loads recurring identity fields for linked session migration", async () => {
+ test("loads visible identity fields from the directly linked event row", async () => {
mocks.execute.mockResolvedValue([
{
</file context>
There was a problem hiding this comment.
2 existing issues remain and 1 new issue found across 14 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/desktop/src/services/calendar/process/events/sync.test.ts">
<violation number="1" location="apps/desktop/src/services/calendar/process/events/sync.test.ts:284">
P2: This replay test never loads duplicate rows into `existing`: it converts the already-coalesced `first.toAdd` result into storage. A regression that leaves pre-existing Apple aliases undeleted would still pass, so build `existing` from a fixture containing every duplicate variant and assert the duplicate IDs are tombstoned.</violation>
</file>
Requires human review: Auto-approval blocked because this review re-detected 2 unresolved issues already reported by Cubic.
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| const first = syncEvents(createMockCtx(), syncInput({ incoming })); | ||
| expect(first.toAdd).toHaveLength(9); | ||
|
|
||
| const existing = first.toAdd.map((event, index) => |
There was a problem hiding this comment.
P2: This replay test never loads duplicate rows into existing: it converts the already-coalesced first.toAdd result into storage. A regression that leaves pre-existing Apple aliases undeleted would still pass, so build existing from a fixture containing every duplicate variant and assert the duplicate IDs are tombstoned.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/services/calendar/process/events/sync.test.ts, line 284:
<comment>This replay test never loads duplicate rows into `existing`: it converts the already-coalesced `first.toAdd` result into storage. A regression that leaves pre-existing Apple aliases undeleted would still pass, so build `existing` from a fixture containing every duplicate variant and assert the duplicate IDs are tombstoned.</comment>
<file context>
@@ -155,159 +155,257 @@ describe("syncEvents", () => {
+ const first = syncEvents(createMockCtx(), syncInput({ incoming }));
+ expect(first.toAdd).toHaveLength(9);
+
+ const existing = first.toAdd.map((event, index) =>
+ createExistingEvent({
+ ...event,
</file context>
There was a problem hiding this comment.
3 existing issues remain and 1 new issue found across 14 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="crates/calendar/src/convert.rs">
<violation number="1" location="crates/calendar/src/convert.rs:287">
P1: When EventKit returns a detached occurrence with `has_recurrence_rules == false`, this branch uses only `event_identifier` instead of synthesizing the occurrence tracking ID. Handle `is_detached` here and derive the ID from the occurrence date plus the stable external/calendar-item identifier so existing sessions and rows continue to match.</violation>
</file>
Requires human review: Auto-approval blocked because this review re-detected 3 unresolved issues already reported by Cubic.
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| let id = if event.has_recurrence_rules { | ||
| let date = event.occurrence_date.as_ref().unwrap_or(&event.start_date); | ||
| let day = local_date_string(date, event.time_zone.as_deref()); | ||
| format!("{}:{}", event.event_identifier, day) | ||
| } else { | ||
| event.event_identifier.clone() | ||
| }; |
There was a problem hiding this comment.
P1: When EventKit returns a detached occurrence with has_recurrence_rules == false, this branch uses only event_identifier instead of synthesizing the occurrence tracking ID. Handle is_detached here and derive the ID from the occurrence date plus the stable external/calendar-item identifier so existing sessions and rows continue to match.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/calendar/src/convert.rs, line 287:
<comment>When EventKit returns a detached occurrence with `has_recurrence_rules == false`, this branch uses only `event_identifier` instead of synthesizing the occurrence tracking ID. Handle `is_detached` here and derive the ID from the occurrence date plus the stable external/calendar-item identifier so existing sessions and rows continue to match.</comment>
<file context>
@@ -272,7 +284,18 @@ fn convert_outlook_event(event: OutlookEvent, calendar_id: &str) -> CalendarEven
fn convert_apple_event(event: AppleEvent) -> CalendarEvent {
let raw = serde_json::to_string(&event).unwrap_or_default();
- let id = apple_event_id(&event);
+ let id = if event.has_recurrence_rules {
+ let date = event.occurrence_date.as_ref().unwrap_or(&event.start_date);
+ let day = local_date_string(date, event.time_zone.as_deref());
</file context>
| let id = if event.has_recurrence_rules { | |
| let date = event.occurrence_date.as_ref().unwrap_or(&event.start_date); | |
| let day = local_date_string(date, event.time_zone.as_deref()); | |
| format!("{}:{}", event.event_identifier, day) | |
| } else { | |
| event.event_identifier.clone() | |
| }; | |
| let id = if event.has_recurrence_rules { | |
| let date = event.occurrence_date.as_ref().unwrap_or(&event.start_date); | |
| let day = local_date_string(date, event.time_zone.as_deref()); | |
| format!("{}:{}", event.event_identifier, day) | |
| } else if event.is_detached { | |
| let date = event.occurrence_date.as_ref().unwrap_or(&event.start_date); | |
| let day = local_date_string(date, event.time_zone.as_deref()); | |
| let stable_id = [ | |
| event.external_identifier.as_str(), | |
| event.calendar_item_identifier.as_str(), | |
| event.event_identifier.as_str(), | |
| ] | |
| .into_iter() | |
| .find(|value| !value.is_empty()) | |
| .unwrap_or_default(); | |
| format!("{stable_id}:{day}") | |
| } else { | |
| event.event_identifier.clone() | |
| }; |
|
The detached-ID finding is intentionally resolved by preserving EventKit’s native identifier. Live EventKit records already encode the occurrence as |
ComputelessComputer
left a comment
There was a problem hiding this comment.
Thanks for digging into this. The Rust-side direction (identify an Apple occurrence by iCal UID + original occurrence date) is sound, but the TypeScript reconciliation for rows that already exist in SQLite is built on an incorrect assumption about how EventKit composes eventIdentifier, and that turns the upgrade path into the bug this PR is fixing.
Blocking
1. Legacy reconciliation does not match real EventKit identifiers
identity.ts and the new tests assume legacy IDs look like {external_id}:{series_id}:{date}. Real rows written by the current converter look like this (checked against a production app.db, 52 recurring + 7 detached Apple rows, all the same layout):
recurring: {storeUUID}:{externalIdentifier}:{YYYY-MM-DD}
detached : {storeUUID}:{externalIdentifier}/RID={secondsSince2001}
The second segment is calendarItemExternalIdentifier, and that is also exactly what recurrence_series_id holds: series_id() in crates/apple-calendar/src/apple/recurrence.rs returns the external identifier (falling back to calendarItemIdentifier, same as external_identifier). So for Apple, recurrence_series_id === external_id is an invariant. The series-marker branch therefore strips the external ID and keeps the store UUID, which is the opposite of what is needed. Feeding real shapes through calendarEventKey:
legacy recurring key : cal-1\0{storeUUID}:2026-06-01
incoming recurring : cal-1\0{externalId}:2026-06-01 -> no match
legacy detached key : cal-1\0{storeUUID}:2026-06-03 (started_at day of the rescheduled instance)
incoming detached : cal-1\0{externalId}:2026-06-01 -> no match
two different series on the same day -> identical legacy key
Effect on the first sync after upgrade: every legacy Apple recurring/detached row lands in toDelete, a fresh row with a new events.id is inserted, and syncSessionEmbeddedEvents misses for the same reason, so sessions.event_id keeps pointing at a tombstone. useSessionCalendarEvent joins on deleted_at IS NULL, so those notes lose their event card and the new event row shows no linked note. That is the duplicate-note experience this PR is trying to remove, applied to every existing Apple user at once.
2. Per-occurrence ignores break
ignored_events stores tracking_id (apps/desktop/src/calendar/ignored-events.ts) and checkEventNotifications checks ignoredIds.has(event.tracking_id_event). Changing the ID format un-mutes every Apple occurrence a user has ignored. Series-level ignores survive because they key on recurrence_series_id.
Suggested direction
Either option removes the string-surgery heuristics:
- Minimal, no migration: keep recurring IDs as
{eventIdentifier}:{day}and, foris_detached, strip the/RID=...suffix fromevent_identifierbefore appending the occurrence day. A detachedeventIdentifieris{master eventIdentifier}/RID=nandoccurrence_dateis the original date, so the detached instance resolves to the exact ID the master occurrence already had. Existing rows, sessions, and ignores stay valid. - UID identity with an explicit bridge: if UID-based identity is wanted (it does cover a store-UUID change), have Rust emit the legacy ID alongside the new one (e.g.
CalendarEvent.legacy_ids) and match existing rows, sessions, and ignored IDs by exacttracking_id_event IN (id, legacy_ids...), then rewrite them. Exact match, testable from fixtures, and removable after a few releases.
Non-blocking
- Please add tests that use real ID shapes (
{UUID}:{uid}:{date},{UUID}:{uid}/RID=n) insync.test.ts,execute.test.ts, and the Rustapple_identity_tests; the current synthetic shapes are what let this slip through. crates/calendar-interface/src/lib.rs:48still documentseventIdentifier:YYYY-MM-DD.plugins/calendar/js/bindings.gen.tscarries the same doc comment and needs regenerating through codegen rather than by hand.- The
status === "cancelled"filter applies to every provider. Outlook cancelled meetings currently sync and will disappear after this change. Probably desirable, but please call it out in the description so it lands in the changelog deliberately. - Detached occurrences still get
recurrence_series_id = ''(pre-existing). With series-based identity it would be natural to setrecurring_event_idforis_detachedtoo, so series-level ignore covers rescheduled occurrences. include_str!("../../apple-calendar/src/fixture/...")couples the two crates' directory layout. Acceptable, but a fixture insidecrates/calendarwould be sturdier.
Cubic triage
#4 (started_at is not the original occurrence date) is valid but subsumed by the layout issue above. #1 and #3 are minor and mostly pre-existing. #2 describes behavior already on main (the tracking-id fallback was never calendar-scoped). #5 does not apply to real ID shapes.
Process
The CLA is still unsigned so no CI has run. Note that cargo test -p calendar only runs in the scheduled macOS native job, so PR CI will not exercise the Rust change even after workflows are approved.
| !/:\d{4}-\d{2}-\d{2}$/.test(event.tracking_id_event)) | ||
| ) { | ||
| return event.tracking_id_event; | ||
| } | ||
|
|
There was a problem hiding this comment.
This assumes the legacy ID is {external_id}:{series_id}:{date}. Real rows are {storeUUID}:{externalIdentifier}:{date}, and for Apple recurrence_series_id is the external identifier (series_id() in crates/apple-calendar/src/apple/recurrence.rs). So this branch returns {storeUUID}:{date}, which never equals the incoming {externalId}:{date} key, and every recurring series in the same store collapses to one key per day.
If legacy rows must be re-keyed in TS, ${event.recurrence_series_id}:${occurrence} is the correct stable ID for a legacy recurring row and needs no string surgery on the UUID. See the review body for the alternative of not re-keying at all.
| if (!occurrence) return event.tracking_id_event; | ||
|
|
||
| const externalId = event.external_id?.trim(); |
There was a problem hiding this comment.
Same layout problem for detached rows: {storeUUID}:{externalIdentifier}/RID=n sliced at the first : keeps the store UUID and drops the external identifier. On top of that occurrence came from started_at, which is the rescheduled date, while the incoming key uses the original occurrence_date. Neither half of the derived key can match the incoming one.
| started_at: session.startedAt, | ||
| }), | ||
| ) | ||
| : undefined) ?? incomingByTrackingId.get(session.trackingId); |
There was a problem hiding this comment.
Because the derived key for a legacy session ({storeUUID}:{date}) never equals an incoming key, this lookup misses for every existing Apple recurring/detached session and falls through to the raw tracking-id match, which also misses since the format changed. Net result: sessions.event_id stays on the row that syncEvents just tombstoned, and useSessionCalendarEvent (joins on deleted_at IS NULL) stops showing the event for that note.
| .find(|value| !value.is_empty()) | ||
| .unwrap_or_default(); | ||
|
|
||
| format!("{stable_id}:{day}") |
There was a problem hiding this comment.
The UID + occurrence-date identity is the right model, but it re-keys every existing Apple recurring row and there is no exact-match bridge for the old IDs (see review body).
A lower-risk alternative that still fixes the reschedule duplicate: keep {event_identifier}:{day} for recurring events and, when is_detached, strip the /RID=... suffix from event_identifier before appending the day. A detached eventIdentifier is {master eventIdentifier}/RID=n and occurrence_date is the original date, so the detached instance resolves to the same ID the master occurrence already had, and nothing in SQLite, sessions, or ignored_events needs migrating.
If you do keep UID identity, please also emit the legacy ID from here (e.g. a legacy_ids field) so TS can match by exact string instead of parsing.
| expect(result.toAdd).toEqual([]); | ||
| }); | ||
|
|
||
| test("keeps one durable row when EventKit replaces a recurring series id", () => { |
There was a problem hiding this comment.
This scenario (same external_id, different recurrence_series_id) cannot occur for Apple: recurring_event_id is recurrence.series_identifier, which series_id() derives from calendarItemExternalIdentifier, the same value as external_id. Could you replace these fixtures with real-shaped IDs ({UUID}:{uid}:{date} for recurring, {UUID}:{uid}/RID=n for detached, recurrence_series_id === uid)? With those inputs the current implementation fails to reconcile, which is the case that matters for the upgrade path.
| const participants: IncomingParticipants = new Map(); | ||
|
|
||
| for (const calendarEvent of calendarEvents) { | ||
| if (calendarEvent.status === "cancelled") continue; |
There was a problem hiding this comment.
This is provider-wide: Outlook events with isCancelled currently sync and will disappear from Anarlog after this change, and any session linked to one gets its event tombstoned. That is probably the behavior we want, but please mention it in the PR description so it is a deliberate, changelog-visible change rather than a side effect of the Apple fix.
ComputelessComputer
left a comment
There was a problem hiding this comment.
Request changes: the current Apple visible-occurrence identity can delete legitimate independent meetings. Reproduced against head 1bbfc15 with two distinct provider IDs in one calendar sharing the title Interview and the same start/end. buildIncomingEventIndex returns one canonical event; syncEvents then tombstones row-1 and rewrites row-0 to the other provider ID. Current main retains both identities. Please build on the provider-identity work already merged in #7625 and add a regression preserving independent overlapping meetings. The original duplicate-event symptom is real, but matching display fields is not sufficient evidence that two meetings are the same.
| const endedAt = normalizedInstant(event.ended_at); | ||
| if (title !== undefined && startedAt && endedAt) { | ||
| return [ | ||
| calendarId, |
There was a problem hiding this comment.
[P1] Preserve distinct provider identities when display fields match. This key ignores tracking_id_event whenever title and dates are present. Two independently created Interview events in the same calendar at the same time produce one canonical entry. Running the current syncEvents with both existing rows deletes the second and rewrites the first to the selected provider ID. Only coalesce when provider occurrence identity or verified aliases prove equivalence; identical display fields alone must not delete a meeting.
Summary
Problem: The SQLite calendar pipeline no longer coalesced rescheduled Apple events the way the previous pipeline did. EventKit can return several active recurrence-series records that render as the same meeting occurrence, so each record became a separate SQLite row and notification.
Solution:
No schema or notification-only workaround is introduced.
Verification
pnpm -F desktop test— 4,408 tests passedpnpm -F desktop typecheckpnpm -F desktop i18n:checkpnpm exec oxlint --quiet --format=github apps/desktop/src/— 0 errorscargo test --locked -p calendarcargo check --locked -p calendar-interface -p tauri-plugin-calendar