gemini-cli-extensions / gemini-cli-extensions/workspace
calendar.updateEvent: `attendees` replaces the guest list, so "add a guest" silently removes the others
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 638
- Forks
- 107
- PR merge metrics
- No merged PRs in 30d
Description
Summary
calendar.updateEvent treats attendees as a full replacement of the guest list. So the most common phrasing a user gives an agent — "add Ana to that meeting" — silently removes everyone else who was already invited.
This is not the events.update vs events.patch bug (that one is fixed; updateEvent correctly uses patch today). patch merges at the field level, but attendees is a single field holding an array, and Google replaces the array wholesale. So a partial body still wipes the other guests, and the API returns 200 with the stripped guest list.
Reproduction
- Create an event with attendees
a@example.comandb@example.com. calendar.updateEvent({ eventId, attendees: ["c@example.com"] }).- The event now has exactly one attendee,
c@example.com.a@andb@were removed, and they receive cancellation notices.
Proposal
Make attendees mean add, which is what a caller asking to add a guest means, and add an explicit removeAttendees for the opposite intent — a merge alone can never remove anybody, so the removal case needs its own door.
One deliberate exception preserves existing behaviour: an empty attendees array still writes straight through as "clear the guest list". Merging there would make the one intent that cannot be expressed any other way impossible, and it keeps the current semantics (and its test) intact.
Patch
CalendarService.ts, in updateEvent, replacing:
if (attendees !== undefined)
requestBody.attendees = attendees.map((email) => ({ email }));
with:
// Attendees need read-modify-write even under patch: Google replaces the
// whole array rather than merging into it, so "add one guest" has to
// resend everyone.
if (attendees?.length === 0 && !removeAttendees?.length) {
requestBody.attendees = [];
} else if (attendees?.length || removeAttendees?.length) {
const current = await calendar.events.get({
calendarId: finalCalendarId,
eventId,
});
const dropped = new Set((removeAttendees ?? []).map((e) => e.toLowerCase()));
const merged = new Map<string, calendar_v3.Schema$EventAttendee>();
for (const a of current.data.attendees ?? []) {
const email = a.email?.toLowerCase();
if (!email || dropped.has(email)) continue;
merged.set(email, a);
}
for (const email of attendees ?? []) {
const key = email.toLowerCase();
if (dropped.has(key) || merged.has(key)) continue;
merged.set(key, { email });
}
requestBody.attendees = [...merged.values()];
}
Plus removeAttendees?: string[] on UpdateEventInput, and on the tool schema in index.ts:
attendees: z
.array(z.string())
.optional()
.describe(
'Email addresses to ADD as attendees. Merged with the guests already on the event, so existing guests are kept. Use removeAttendees to drop someone.',
),
removeAttendees: z
.array(z.string())
.optional()
.describe('Email addresses to remove from the event.'),
Notes on the details that matter:
- Existing attendee objects are re-sent whole, not rebuilt from the email, so each guest keeps their
responseStatus. Rebuilding them would reset everyone toneedsActionand re-notify the whole list. - Comparison is case-insensitive, since Google is.
- The read is skipped entirely when neither list is given, so the common "just change the title" path still costs one API call.
Verification
- Full suite green. The merge assertions were watched fail first by reverting the code to replacement semantics, before being trusted.
- Live against the Google API: adding a second guest to an event that already had one leaves both on the event, with the first guest's response status preserved.
Trade-off, stated plainly
This changes the meaning of attendees on update, so it is a behaviour change, not a pure bug fix. The argument for it is that the current meaning is the one nobody asks for: an agent told "add someone" has no way to comply without first reading the event itself, and every client that does not know to do that silently destroys data. If you would rather keep replacement and add addAttendees/removeAttendees as new fields, the same merge logic applies unchanged.
Happy to open a PR, though the CLA is a gate on our side — the patch above is complete either way.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in CalendarService.ts at updateEvent and inspect the UpdateEventInput definition and tool schema in index.ts. Run the full suite, including the attendee merge assertions, and verify that adding guests preserves existing attendee objects, removal works, an empty list clears guests, and updates without attendee fields avoid the extra read.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 58/100