Uh oh!
There was an error while loading. Please reload this page.
Fold first-time calendar invitations onto the event's thread - #379
Merged
Conversation
The Google Calendar sync now records an event-uid:<iCalUID> marker for every upcoming event with attendees it saves. The Gmail sync gains an optional readCalendarState() bridge to check for that marker (mirroring the existing readMailState() bridge calendar already reads on the Google connector). Nothing consumes the new bridge yet - it lays the groundwork for the mail sync to recognize when an arriving invitation email already has a corresponding event thread.
A Google Calendar invitation email previously imported as its own email thread even when the event had already synced, duplicating a thread that already renders the schedule, guest list and RSVP affordance. First-time invitations (ICS METHOD:REQUEST at SEQUENCE 0) are now folded onto the event's own thread instead: the organizer's ICS COMMENT (if any) is attached there as a note, and no separate email thread is created. If the event hasn't synced yet, the invitation keeps its email thread and records a pending marker so it can be reconciled once the event arrives. Invitation updates (SEQUENCE > 0) are unaffected and continue to bundle onto the event thread via the existing calendar-thread bundling path.
When a Google Calendar invitation email arrives before its event has synced, the connector previously had no way to know a matching event was coming, so it kept the invitation as its own email thread rather than risk silently dropping it. Once the event does sync, that email thread is now redundant. The calendar sync now checks, for every event it saves, whether there's a pending invitation waiting on it, and if so archives that invitation's email link so the event's own thread is the single source of truth. A pending invitation older than 7 days is treated as stale and cleared without being retracted, rather than lingering indefinitely.
Both sides appended to connectors/google/src/mail/gmail-api.ts: main tagged inline images with their Content-ID, this branch added invitation extraction. The changes are disjoint — union the imports and keep both test describe blocks.
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A first-time calendar invitation currently imports as its own email thread even when the connector has already synced the event it refers to. The result is two threads for one meeting: the event, which renders the schedule, the guest list and the RSVP; and a mail thread carrying the calendar system's notification, which only describes those things in prose.
Attendee responses were folded onto the event thread in earlier work. Invitations were not:
classifyCalendarThreadbundles updates (METHOD:REQUESTwithSEQUENCE > 0), cancellations and reply chains, and a bare invite falls through to a standalone thread.What changes
A bare invitation (
METHOD:REQUEST,SEQUENCE 0) is folded onto the event's thread and its note dropped, so an invitation-only conversation never becomes an email thread. A conversation that also carries real correspondence keeps its thread, minus the folded message, with its preview and classification recomputed from a message whose note survived.This is a fold, not a bundle. Adding
icaluid:<uid>to the mail link'ssourceswould be smaller, but a bundled mail link is the event's thread: it would rewrite the event's title toInvitation: … @ …and re-author it.The organizer's note survives. Extra content comes from the ICS
COMMENTproperty, which RFC 5545 scopes to the iCalendar transmission rather than the event body — so it is exactly "what the invitation adds beyond the event", and the event's ownDESCRIPTIONcan never double-post because it is never read. A stock invitation carries noCOMMENTand no note is written.An invitation is never silently lost. The fold only happens when the connector knows the event exists. The calendar sync records
event-uid:<uid>for events that have guests and have not ended; the mail sync reads it through a newreadCalendarStatebridge, the mirror of the existingreadMailState. With no marker — no calendar channel enabled, or the event lives on another provider — the email thread is kept as before.When the mail arrives first, the email thread is kept and
invite-wait:<uid>recorded; when the calendar later saves that event it archives the redundant mail link and clears the key, within a 7-day window enforced in code since the store has no native expiry.Notes for reviewers
unescapeIcsTextis now exported from@plotday/rsvp-fold. Connectors read escaped text out of properties that module does not parse for them, and a second copy of the RFC 5545 un-escaping rule would undo an earlier deduplication.addLinkdoes not mergesources. If an exception instance is added before its master on an incremental page, the surviving entry keeps the instance-scoped UID, so that pass marks under it and an invitation carrying the master UID will not fold. It self-heals on the next page where the master lands alone, and backfills are unaffected because instances are buffered.recurrenceCount-bounded series are marked as upcoming even after they finish, because the API sets a count rather than an until. Over-inclusive in the safe direction; it only inflates the key space.Testing
libs/rsvp-fold46 passing,connectors/google388 passing (up from 365),tsc --noEmitclean. Roughly half the diff is tests.Calendar fixtures use the shape the Gmail API really produces — an attachment with a synthesized
invite.icsfilename and abody.attachmentId, fetched through a stubbedmessages.attachments.get— rather than an inline part. Both new guards were verified by breaking them and confirming the intended test fails.🤖 Generated with Claude Code