Skip to content

fix(caldav): honor time range in list_events via server-side calendar… - #1147

Open
thoscut wants to merge 2 commits into
moltis-org:mainfrom
thoscut:claude/caldav-time-range-filter-0lrbbu
Open

fix(caldav): honor time range in list_events via server-side calendar…#1147
thoscut wants to merge 2 commits into
moltis-org:mainfrom
thoscut:claude/caldav-time-range-filter-0lrbbu

Conversation

@thoscut

@thoscut thoscut commented Jul 11, 2026

Copy link
Copy Markdown

…-query

The range parameter of CalDavClient::list_events was bound as _range and never used: the client always fetched every resource in the calendar, so the start/end parameters of the list_events tool had no effect on any server, contradicting the docs.

list_events now issues a CalDAV calendar-query REPORT (RFC 4791) with a time-range filter nested inside comp-filter VCALENDAR > VEVENT — the only placement Nextcloud and other servers honor — and then fetches only the matching resources via calendar-multiget. Range bounds are parsed from ISO 8601 (offset, naive, or date-only) and serialized as UTC with a trailing Z (e.g. 20260101T000000Z) as the spec requires; naive inputs are treated as UTC. When range is None, behavior is unchanged.

Date/time handling uses the time crate through a new re-export from moltis-agents (which already depends on it), so no new direct dependency or lockfile change is needed.

The mock client applies equivalent client-side overlap filtering (new time_filter module) so tests exercise the range semantics: events fully outside the range are dropped, boundary-overlapping events are kept, all-day events span a full day, and events without DTEND are treated as instantaneous. A unit test also asserts the generated REPORT XML nests time-range under VCALENDAR > VEVENT with UTC Z timestamps.

Claude-Session: https://claude.ai/code/session_012UDpqLhTib2oMkPj2roqaB

…-query

The range parameter of CalDavClient::list_events was bound as _range and
never used: the client always fetched every resource in the calendar, so
the start/end parameters of the list_events tool had no effect on any
server, contradicting the docs.

list_events now issues a CalDAV calendar-query REPORT (RFC 4791) with a
time-range filter nested inside comp-filter VCALENDAR > VEVENT — the only
placement Nextcloud and other servers honor — and then fetches only the
matching resources via calendar-multiget. Range bounds are parsed from
ISO 8601 (offset, naive, or date-only) and serialized as UTC with a
trailing Z (e.g. 20260101T000000Z) as the spec requires; naive inputs are
treated as UTC. When range is None, behavior is unchanged.

Date/time handling uses the time crate through a new re-export from
moltis-agents (which already depends on it), so no new direct dependency
or lockfile change is needed.

The mock client applies equivalent client-side overlap filtering (new
time_filter module) so tests exercise the range semantics: events fully
outside the range are dropped, boundary-overlapping events are kept,
all-day events span a full day, and events without DTEND are treated as
instantaneous. A unit test also asserts the generated REPORT XML nests
time-range under VCALENDAR > VEVENT with UTC Z timestamps.

Claude-Session: https://claude.ai/code/session_012UDpqLhTib2oMkPj2roqaB
@greptile-apps

greptile-apps Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a long-standing bug where list_events's start/end parameters were silently ignored — the underlying CalDAV client always fetched every resource in the collection. The fix adds a proper two-phase server-side filter: a CalDAV calendar-query REPORT (RFC 4791) with a VCALENDAR > VEVENT > time-range nesting is issued first, and only the matching hrefs are fetched via calendar-multiget.

  • time_filter.rs is a new module that converts ISO 8601 (offset, naive, or date-only) inputs to the YYYYMMDDTHHMMSSZ iCal UTC format required by the wire protocol, and provides test-only client-side overlap helpers that mirror the server-side semantics for the mock client.
  • moltis_agents re-exports the time crate so the caldav crate can share the same dependency without a new lockfile entry.
  • The mock client now applies equivalent client-side filtering so tests exercise the range semantics end-to-end.

Confidence Score: 4/5

The core fix is correct — the two-phase REPORT/multiget approach is the right CalDAV pattern and the UTC conversion is well-tested. The only concerns are a subtle boundary-semantics gap in the mock and a missing hint in the agent tool description.

The implementation correctly issues a server-side calendar-query REPORT and only fetches matching resources. The to_ical_utc path is covered by unit tests including offset conversion. Two issues are worth noting: the mock's overlap predicate is slightly more inclusive than RFC 4791 (inclusive vs strict inequalities at exact boundaries), and the in-process agent description string doesn't warn that both start and end are needed for filtering to activate.

crates/caldav/src/time_filter.rs — the event_overlaps function's boundary semantics vs RFC 4791

Important Files Changed

Filename Overview
crates/caldav/src/client.rs Implements two-phase server-side filtering: calendar-query REPORT to get matching hrefs, then calendar-multiget. Logic is correct; the build_time_range_query helper and UTC conversion path are well tested.
crates/caldav/src/time_filter.rs New module providing ISO 8601 → iCal UTC conversion (production) and test-only event overlap helpers. The overlap predicate uses inclusive boundaries rather than the strict inequalities defined in RFC 4791 §9.9.1, creating a latent divergence from real-server behaviour at exact boundaries.
crates/caldav/src/tool.rs Wires the new range path into the agent tool and the mock client. The single-boundary silent fallthrough is documented in docs but the in-process tool description string doesn't warn about it.
crates/agents/src/lib.rs Adds pub use time; re-export so the caldav crate can use the same time dependency without duplicating it. Change is minimal and correct.
crates/caldav/src/lib.rs Adds mod time_filter; declaration (private, crate-internal). No issues.
docs/src/caldav.md Updates list_events docs to describe server-side RFC 4791 filtering and the naive-as-UTC convention. Accurate and consistent with the implementation.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Tool as CalDavTool (tool.rs)
    participant Client as LibDavCalDavClient
    participant TF as time_filter
    participant Server as CalDAV Server

    Tool->>Tool: extract start + end from params
    alt both start and end provided
        Tool->>Client: list_events(calendar_href, Some(TimeRange))
        Client->>TF: to_ical_utc(start), to_ical_utc(end)
        TF-->>Client: YYYYMMDDTHHMMSSZ strings
        Client->>Server: "REPORT calendar-query (VCALENDAR > VEVENT > time-range)"
        Server-->>Client: matching resource hrefs
        Client->>Server: REPORT calendar-multiget (hrefs only)
        Server-->>Client: full iCal data for matched events
        Client-->>Tool: Vec EventSummary
    else only one or neither provided
        Tool->>Client: list_events(calendar_href, None)
        Client->>Server: REPORT calendar-multiget (entire collection)
        Server-->>Client: all iCal data
        Client-->>Tool: Vec EventSummary
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Tool as CalDavTool (tool.rs)
    participant Client as LibDavCalDavClient
    participant TF as time_filter
    participant Server as CalDAV Server

    Tool->>Tool: extract start + end from params
    alt both start and end provided
        Tool->>Client: list_events(calendar_href, Some(TimeRange))
        Client->>TF: to_ical_utc(start), to_ical_utc(end)
        TF-->>Client: YYYYMMDDTHHMMSSZ strings
        Client->>Server: "REPORT calendar-query (VCALENDAR > VEVENT > time-range)"
        Server-->>Client: matching resource hrefs
        Client->>Server: REPORT calendar-multiget (hrefs only)
        Server-->>Client: full iCal data for matched events
        Client-->>Tool: Vec EventSummary
    else only one or neither provided
        Tool->>Client: list_events(calendar_href, None)
        Client->>Server: REPORT calendar-multiget (entire collection)
        Server-->>Client: all iCal data
        Client-->>Tool: Vec EventSummary
    end
Loading

Comments Outside Diff (1)

  1. crates/caldav/src/tool.rs, line 211-220 (link)

    P2 Single-boundary filter silently falls through to full fetch

    When a caller passes only start or only end, the _ => None arm discards the supplied value and fetches all events without any filtering. The markdown docs mention this ("If either is omitted, all events are returned"), but the description() string exposed to the AI agent model only says start/end (ISO 8601, optional) with no hint that both are required together. An agent that provides just start to bound a range will silently receive every event in the calendar.

Reviews (1): Last reviewed commit: "fix(caldav): honor time range in list_ev..." | Re-trigger Greptile

Comment thread crates/caldav/src/time_filter.rs
The mock client's event_overlaps used inclusive inequalities
(end >= range.start && start <= range.end), making it slightly more
permissive at exact boundaries than a compliant CalDAV server. Per
RFC 4791 §9.9 a time-range match is strict for events with a DTEND
(range.start < event_end AND event_start < range.end) and only the
lower bound is inclusive for instantaneous DATE-TIME events
(range.start <= event_start < range.end). The looser mock could keep
an event that a real server drops, letting tests pass against behavior
production would not reproduce.

Reimplement event_overlaps to branch on the RFC cases (explicit DTEND,
all-day spanning one day, instantaneous) and drop the now-unused
event_interval helper. Add tests pinning the boundary cases: an event
ending exactly at range.start and one starting exactly at range.end are
excluded, while an instantaneous event at range.start is included.

Also clarify the caldav tool description and start/end parameter docs
that server-side filtering only activates when both start and end are
provided.
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.

2 participants