• v0.9.0 562f66eed3

    v0.9.0
    All checks were successful
    CI / gate (push) Successful in 1m18s
    CI / image (push) Successful in 1m21s
    Stable

    joeyr released this 2026-08-09 22:10:26 -05:00 | 91 commits to main since this release

    Repeating events. Creating them, and changing or deleting this occurrence, this and
    every later one
    , or all of them — including the this-and-following split that had
    been held back for 0.9.1.

    Added

    • create_event can repeat, from a constrained vocabulary rather than a raw RRULE:
      repeat (daily, weekly, monthly, yearly), repeat_interval, repeat_days for a weekly
      rule, and either repeat_count or repeat_until. Give neither and it repeats
      indefinitely, which is the right answer for a standing weekly meeting.

      Not a raw rule string, deliberately. FREQ=SECONDLY with no COUNT is legal
      iCalendar, expands to millions of occurrences, and turns every later read of that
      calendar into a timeout. A vocabulary that cannot express it beats a validator that has
      to catch it.

    • scope on update_event and delete_eventthis, following, or all.
      Required for a repeating event, with no default. "Cancel Tuesday's standup" means
      one occurrence to a person; under a default of all it ends the standup permanently.
      There is no safe default, only a convenient one, so a missing scope is refused with the
      three choices named and scope_required: true in the envelope. A scope on a one-off
      event is accepted and ignored — it has one occurrence, so every scope names the same
      write.

    • Every occurrence of a series now carries an event_id. Before this, repeating
      events had none, because every write against them was refused. The handle gained a
      fourth field naming which occurrence it is — the value a RECURRENCE-ID carries, in
      UTC so it survives the server's TZ changing between being issued and being used.
      Shorter handles still decode, so a one-off event's handle is byte-identical to the one
      0.8.1 issued.

      What the absence of a handle means has narrowed accordingly: a subscription, or a
      calendar that is readable but not writable.

    The three scopes are three different operations

    Not three flags on one operation. Removing one occurrence of a series is a write, not a
    deletion
    — and that is the whole safety argument here:

    Scope Update Delete
    this add a RECURRENCE-ID override to the same resource add an EXDATE to the master
    following truncate the original with UNTIL, create a new resource for the remainder set UNTIL — one write, no second resource
    all edit the master DELETE the resource

    Only all, and following from the very first occurrence, are a DELETE. An
    implementation that reaches for obj.delete() because the caller said "delete" takes the
    entire series and gets exactly the same 204 back as taking one occurrence, so nothing
    downstream can tell.

    Two more consequences worth stating:

    • An override is built by copying the master, properties and VALARM reminders and all,
      never composed from the fields this server models. An occurrence becoming an override is
      the one moment a reminder could silently disappear, and a lost reminder is invisible in
      every response until an appointment does not ring.
    • following from the first occurrence, and truncation generally, degenerate. An
      UNTIL earlier than DTSTART writes a rule that generates nothing — a resource that
      exists and renders as no events at all. That case is detected and treated as the whole
      series instead.

    The split creates before it truncates

    Editing following is the only operation here that writes two resources, and CalDAV
    offers no transaction across them. One of the two writes can therefore succeed alone, and
    the order decides which failure the user gets:

    • remainder first, then truncate — a failure between them leaves a visible duplicate
      series from the split date onward, recoverable by hand;
    • truncate first — a failure leaves every future occurrence silently gone, behind a
      plausible-looking success.

    So the remainder is created first, and if the truncation then fails the error says
    precisely that both are on the calendar and nothing was lost, rather than "nothing was
    modified", which would be a lie.

    A series bounded by COUNT refuses a following edit. Splitting it correctly means
    knowing how many occurrences fall before the split, which needs the series expanded — and
    a count wrong by one is a missing or extra occurrence months away with nothing to indicate
    it. Deleting following on a counted series is fine: every occurrence being kept was
    inside the original count either way.

    Changed

    • recurring is read from the resource, not from the component. A RECURRENCE-ID
      override carries no RRULE, so asking the expanded copy reported a moved occurrence of
      a weekly meeting as a one-off. All of a series' components live in one resource (RFC
      4791 §4.1), so asking the resource cannot get it wrong. The flag is now correct for an
      override rather than merely not trusted.

    • The write instructions and both tool descriptions now carry the scope and its lack of a
      default. A model told only that series are "supported" picks a scope itself.

    Testing

    417 tests, and a twelve-mutation pass over the safety properties — each one
    individually reverted, each one caught. The three that were not caught on the first run
    became tests before this shipped, and all three were real gaps: a reminder surviving an
    occurrence becoming an override, an override not inheriting the series rule, and a second
    edit of the same occurrence editing that override rather than adding another.

    Recurrence has not run against the live account. docs/plan.md carries the eight
    checks, which want a scratch calendar — one of them deliberately tries to take a whole
    series.

    Downloads