Skip to content

Add robust recurring event deletion with timezone-aware occurrence matching#18

Open
tamm wants to merge 14 commits intoOmar-V2:mainfrom
tamm:main
Open

Add robust recurring event deletion with timezone-aware occurrence matching#18
tamm wants to merge 14 commits intoOmar-V2:mainfrom
tamm:main

Conversation

@tamm
Copy link
Copy Markdown

@tamm tamm commented Nov 12, 2025

This PR addresses the issues identified in #1 by implementing a more robust solution for deleting recurring event occurrences with proper timezone handling.

Key Improvements

1. Timezone-aware occurrence matching

2. Flexible deletion modes

  • Delete single occurrence: occurrence_date only (default)
  • Delete from occurrence forward: occurrence_date + delete_entire_series=True
  • Delete entire series: delete_entire_series=True only
  • Delete non-recurring event: event_id only (unchanged behavior)

3. Comprehensive test coverage (as requested in #1)

Added 4 integration tests in tests/test_calendar_manager_integration.py:

  • test_delete_non_recurring_event: Basic deletion case
  • test_delete_recurring_event_entire_series: Delete all occurrences
  • test_delete_recurring_event_single_occurrence: Delete one specific occurrence
  • test_delete_recurring_event_from_occurrence_forward: Delete from occurrence onward
  • All tests verify expected behavior with occurrence counts

4. Enhanced MCP tool documentation

  • CRITICAL warnings about using exact datetimes from list_events
  • Clear usage examples for all deletion scenarios
  • Explanation of timezone importance to prevent user errors

What This Fixes from #1

The original PR had two main issues mentioned in the review:

  1. Wrong occurrence being deleted → Fixed by exact datetime matching with timezone awareness
  2. Events appearing deleted but not actually removed → Fixed by proper occurrence lookup before deletion

Implementation Details

The solution uses EventKit's equality operator for datetime matching after narrowing down candidates with a predicate search. When a naive datetime is provided, it automatically tries UTC interpretation as a fallback (common when Claude constructs times).

Example flow for deleting 3rd occurrence of a daily recurring event:

# 1. User lists events and sees exact datetime
list_events() # Shows: "2025-11-15T14:00:00+00:00"

# 2. Delete using EXACT datetime from list_events
delete_event(
    event_id="ABC123",
    occurrence_date=datetime.fromisoformat("2025-11-15T14:00:00+00:00")
)

# 3. System finds exact match using:
#    - Predicate search: ±1 minute window
#    - Exact match: ekevent.startDate() == target_datetime

Testing

All existing tests pass, plus 4 new integration tests specifically for recurring event deletion scenarios.

🤖 Generated with Claude Code

Co-Authored-By: Claude noreply@anthropic.com

tamm and others added 14 commits November 12, 2025 18:44
…tching

This PR addresses the issues identified in PR #1 by implementing a more robust
solution for deleting recurring event occurrences with proper timezone handling.

Key improvements:

1. **Timezone-aware occurrence matching**
   - New find_event_occurrence() method with exact datetime matching
   - Automatic UTC fallback for naive datetimes (common in LLM-constructed dates)
   - Uses ±1 minute search window followed by exact datetime equality check
   - Prevents wrong occurrence deletion issues from PR #1

2. **Flexible deletion modes**
   - Delete single occurrence: occurrence_date only (default)
   - Delete from occurrence forward: occurrence_date + delete_entire_series=True
   - Delete entire series: delete_entire_series=True only
   - Delete non-recurring event: event_id only (unchanged behavior)

3. **Comprehensive test coverage**
   - test_delete_non_recurring_event: Basic deletion case
   - test_delete_recurring_event_entire_series: Delete all occurrences
   - test_delete_recurring_event_single_occurrence: Delete one specific occurrence
   - test_delete_recurring_event_from_occurrence_forward: Delete from occurrence onward
   - All tests verify expected behavior with occurrence counts

4. **Enhanced MCP tool documentation**
   - CRITICAL warnings about using exact datetimes from list_events
   - Clear usage examples for all deletion scenarios
   - Explanation of timezone importance to prevent user errors

Fixes the two main issues from PR #1:
- Wrong occurrence being deleted (timezone matching problem)
- Events appearing deleted but not actually being removed (matching failure)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…-timezone-handling

Add robust recurring event deletion with timezone-aware occurrence matching
Previously, there was no way to update just one occurrence of a recurring
event - updates always affected the entire series or all future occurrences.

This adds occurrence-specific update support with clear datetime handling.

Changes:
- Add occurrence_date parameter to target specific occurrences
- Add update_future_events parameter to control update scope
- When occurrence_date is provided with update_future_events=False (default),
  only that specific occurrence is updated using EKSpanThisEvent
- When occurrence_date is provided with update_future_events=True,
  that occurrence and all future ones are updated (creates a new series fork)
- When no occurrence_date is provided, the entire series is updated
- Document that occurrence_date should use naive datetime format (local time
  without timezone offset), matching EventKit's requirements
- Update both update_event and delete_event docs for datetime format consistency

Test coverage:
- Update single occurrence only (new capability) ✓
- Update from specific occurrence forward (new capability) ✓
- Update entire series (existing behavior verified) ✓

All 19 tests pass including 3 new comprehensive tests for occurrence-specific updates.

EventKit behavior note: When update_future_events=True creates a series fork,
subsequent updates to the master event only affect the original series, not
the forked occurrences. This is correct EventKit behavior.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Add support for updating single occurrences of recurring events
This is the foundational fix for timezone handling. All datetime fields in
Event objects are now timezone-aware, preventing ambiguity and enabling
proper cross-timezone operations.

Changes to models.py:
- Import timezone from datetime
- Update convert_datetime() to always return timezone-aware datetimes:
  * NSDate from EventKit → UTC then convert to local timezone with tzinfo
  * ISO strings → parse and add local timezone if naive
  * datetime objects → add local timezone if naive
- Apply convert_datetime() to all datetime fields in from_ekevent():
  * start_time, end_time, last_modified
  * recurrence_rule.end_date
- Update __str__() to format datetimes in ISO 8601 format with timezone
  (e.g., "2025-11-14T14:00:00+11:00" instead of "2025-11-14 14:00:00")

Impact:
- Fixes Issue Omar-V2#17 root cause: Event times now include timezone information
- Addresses Issue Omar-V2#5 root cause: Timezone info is preserved internally
- Uses standard ISO 8601 format for universal compatibility
- Breaking change: Tests expecting naive datetimes will need updates
- Next layers will handle EventKit boundary conversions

Note: This commit intentionally breaks some tests - they will be fixed in
subsequent layers as we add proper timezone handling throughout the stack.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Adds to_eventkit_datetime() helper function that converts timezone-aware
datetimes to naive local time for EventKit APIs, while passing through
naive datetimes unchanged.

Changes to ical.py:
- Add to_eventkit_datetime() helper function:
  * Converts timezone-aware datetimes to local timezone, strips tzinfo
  * Passes naive datetimes through unchanged (assumes local time)
  * Well-documented with examples for UTC, PST, and naive inputs

- Use to_eventkit_datetime() at all EventKit boundaries:
  * list_events(): Convert start_time and end_time before predicate
  * create_event(): Convert start_time and end_time before setStartDate/setEndDate
  * update_event(): Convert start_time and end_time if provided

Impact:
- Claude can now provide datetimes in ANY timezone (UTC, PST, AEDT, etc.)
- All conversions happen automatically and correctly
- EventKit gets what it expects (naive local time)
- Internal Event objects remain timezone-aware (from Layer 1)
- Tests still fail (expected) - will be fixed in Layer 5

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Updates occurrence matching to properly handle timezone-aware datetimes
from Claude. Now correctly matches occurrences regardless of which timezone
Claude provides the datetime in (PST, UTC, AEDT, etc.).

Changes to ical.py:
- Update _search_occurrence_by_datetime():
  * Convert search window to naive local time using to_eventkit_datetime()
  * Convert target datetime to naive local time for comparison
  * Match against EventKit's naive datetimes correctly
  * Add documentation explaining timezone-aware matching approach

- find_event_occurrence() unchanged (already has UTC fallback for naive times)

Impact:
- Cross-timezone occurrence matching now works correctly
- Claude can provide occurrence_date in any timezone (PST, UTC, AEDT, etc.)
- System automatically converts to local time for EventKit matching
- Naive datetime fallback (try local, then UTC) still works
- Fixes the core issue preventing occurrence updates/deletes across timezones

Example scenarios that now work:
- Event created in AEDT, Claude provides PST datetime → matches correctly
- Event in UTC, Claude provides local time → matches correctly
- Naive datetime provided → tries local first, then UTC (existing behavior)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Updates all tool documentation to reflect the new timezone-aware capabilities.
Removes outdated guidance about stripping timezone offsets and clarifies that
the system now fully supports timezone-aware datetimes.

Changes to server.py:
- update_event() docstring:
  * Remove guidance about stripping timezone offsets (no longer needed)
  * Add clear statement about timezone-aware datetime support
  * Update examples to show timezone-aware format
  * Document automatic timezone conversion
  * Clarify both formats work: timezone-aware (preferred) and naive (local assumed)

- delete_event() docstring:
  * Same updates as update_event for consistency
  * Remove outdated "remove timezone suffix" instructions
  * Add timezone handling note explaining automatic conversion

Impact:
- Claude will now see correct guidance about timezone handling
- Encourages use of timezone-aware datetimes (more explicit, less ambiguous)
- Still supports naive datetimes for backward compatibility
- Addresses confusion from Issues Omar-V2#5 and Omar-V2#17 about timezone handling

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Updates test fixtures to create timezone-aware datetimes, ensuring tests
work correctly with the new timezone-aware Event model.

Changes to test_calendar_manager_integration.py:
- Import timezone from datetime
- Update test_event_base fixture:
  * Use datetime.now(timezone.utc).astimezone() for timezone-aware local time
  * Add comment explaining timezone-aware approach
  * Ensures test datetimes match Event object datetimes (both timezone-aware)

Impact:
- All 19 tests now pass ✓
- Tests verify timezone-aware functionality works end-to-end
- Fixture creates realistic test data (timezone-aware like real usage)
- No behavioral changes to tests, just fixes comparison mismatches

Test results:
- test_create_and_get_event: PASSED (was failing before)
- All other tests: PASSED (still passing)
- Total: 19/19 tests passing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Streamlined documentation to be clearer and less verbose
- Removed overly prescriptive language that could confuse the AI
- Maintained support for both timezone-aware and naive datetime formats
- All tests passing with full timezone support

This completes the comprehensive timezone handling implementation:
- ISO 8601 datetimes with timezone offsets throughout
- Cross-timezone operation support (UTC, PST, AEDT, etc.)
- Single occurrence updates/deletes with timezone-aware matching
- Backward compatibility with naive datetimes maintained
…pport

- Reject naive datetimes in list_events with a clear error guiding AI
  to include an explicit timezone (Z or +HH:MM offset)
- Show pre-converted local time alongside ISO timestamps in event output
  so AI consumers don't misread UTC offsets and assign wrong calendar days
- Add attendee support via AppleScript (EventKit attendees are read-only)
- Update all tool docstrings to require explicit timezone in examples
…try logic

- Match events by calendar + title + start date (not just title)
- Fix NSDate crash when passing EventKit dates to AppleScript
- Rate-limit Calendar.app launches (30s cooldown) to prevent crashes
- Split AppleScript into find/add/verify phases (no duplicate writes)
- Enrich attendee output to include emails via EKParticipant.URL()
- Refresh EventKit store after AppleScript modifications
- Strengthen test assertions to verify specific attendee emails
list_events was returning full event details (~33k chars for a week query),
exceeding MCP token limits. Now returns compact plaintext summaries with only
essential fields: title, local time, id, calendar, truncated location/notes,
attendee count, and human-readable recurrence. Full details remain available
via __str__ for single-event inspection.

Also deduplicates all-day events by title+date (fixes duplicate holidays from
multiple subscribed calendars), flags cancelled events, and includes date on
all-day events so they get assigned to the correct calendar day.
Location: URLs (Teams/Meet/Zoom links) replaced with "(online meeting
link)" — only short venue names shown. Notes: replaced truncated content
with a has_notes flag — notes often contain forwarded emails, URLs, and
legal boilerplate that bloat output and leak PII.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant