这是indexloc提供的服务,不要输入任何密码
Skip to content

Conversation

@tembo
Copy link
Contributor

@tembo tembo bot commented Aug 21, 2025

Description

Resolve TRPC sequence value conflicts in Google Calendar API interactions by implementing a retry mechanism with event re-fetching.

Changes

  • Added error handling for invalid sequence value errors
  • Implemented retry logic to re-fetch event and use latest sequence number
  • Updated methods: updateEvent, acceptEvent, updateEventResponse, and createConference
  • Preserves original update intent while preventing sequence conflicts

Want me to make any changes? Add a review or comment with @tembo and i'll get back to work!

tembo.io sentry.io

Summary by CodeRabbit

  • Bug Fixes

    • Reduced “Invalid sequence value” errors when updating, accepting, or responding to Google Calendar events via automatic retry on sequence conflicts.
    • Improved reliability of Google Meet conference creation with a guarded retry on sequence conflicts.
  • Refactor

    • Standardized UUID generation across the app; no user-visible changes.
  • Chores

    • Removed an unused auth import; no user-facing impact.

- Preserve sequence numbers when updating Google Calendar events to prevent "Invalid sequence value" errors
- Add retry logic to re-fetch events and use current sequence numbers when conflicts occur
- Apply fixes to updateEvent, responseToEvent, acceptEvent methods in GoogleCalendarProvider
- Add sequence conflict handling to GoogleMeetProvider createConference method
- Ensure all Google Calendar API update operations include proper sequence number handling

Fixes #59095795 - TRPCError: 400 Invalid sequence value error
@tembo tembo bot requested a review from JeanMeijer August 21, 2025 17:02
@vercel
Copy link

vercel bot commented Aug 21, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
analog Ready Ready Preview Comment Aug 21, 2025 10:23pm

@tembo
Copy link
Contributor Author

tembo bot commented Aug 21, 2025

Requesting review from @JeanMeijer who has experience with the following files modified in this PR:

  • packages/api/src/providers/conferencing/google-meet.ts
  • packages/api/src/providers/calendars/google-calendar.ts

@coderabbitai
Copy link

coderabbitai bot commented Aug 21, 2025

Important

Review skipped

Auto reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

Implements optimistic concurrency for Google Calendar and Google Meet by preserving and retrying on sequence conflicts; adjusts UUID generation to use globalThis.crypto/randomUUID in web utils and node:crypto in API routes; and removes an unused import from auth server. No public APIs changed.

Changes

Cohort / File(s) Summary of changes
UUID access update
apps/web/src/lib/utils/calendar.ts, packages/api/src/routers/events.ts
Switch UUID generation: web uses globalThis.crypto.randomUUID(); API routes import and use randomUUID() from node:crypto. No control-flow changes.
Google Calendar provider concurrency
packages/api/src/providers/calendars/google-calendar.ts
Preserve sequence on updates; add single retry on “Invalid sequence value” by refetching event and reapplying attendee responses; set sendUpdates accordingly. acceptEvent and responseToEvent now handle sequence conflicts similarly.
Google Meet conference creation retry
packages/api/src/providers/conferencing/google-meet.ts
Create conference with sequence-preserving update; on sequence conflict, refetch event, regenerate requestId, and retry once; enforce conferenceDataVersion: 1; validate presence of conferenceData; propagate non-sequence errors.
Auth import cleanup
packages/auth/src/server.ts
Remove unused named import secondaryStorage; no runtime behavior change.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs


🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Join our Discord community for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@tembo
Copy link
Contributor Author

tembo bot commented Aug 21, 2025

Tembo is working on fixing this failing workflow: Check types

@tembo
Copy link
Contributor Author

tembo bot commented Aug 21, 2025

Tembo is working on fixing this failing workflow: Check formatting

tembo bot added 2 commits August 21, 2025 17:27
- Add proper import for randomUUID from node:crypto in API packages
- Use globalThis.crypto.randomUUID in web app for browser compatibility
- Fix TypeScript compilation errors in workflow

Resolves type checking failures in CI workflow.
@tembo
Copy link
Contributor Author

tembo bot commented Aug 21, 2025

Tembo is working on fixing this failing workflow: Check types

@tembo
Copy link
Contributor Author

tembo bot commented Aug 21, 2025

Tembo is working on fixing this failing workflow: Lint

@tembo
Copy link
Contributor Author

tembo bot commented Aug 21, 2025

Tembo is working on fixing this failing workflow: Check formatting

@JeanMeijer
Copy link
Collaborator

@coderabbitai full review

@coderabbitai
Copy link

coderabbitai bot commented Aug 21, 2025

✅ Actions performed

Full review triggered.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (5)
packages/api/src/providers/conferencing/google-meet.ts (2)

86-141: Strengthen sequence-conflict detection

Currently you rely on error.message.includes("Invalid sequence value"). If @repo/google-calendar exposes structured errors (status, reason), prefer a predicate based on code/reason to avoid brittle string matching. If not available, at least use a case-insensitive regex.

I can factor a small helper like:

function isInvalidSequenceError(err: unknown): boolean {
  return err instanceof Error && /invalid sequence value/i.test(err.message);
}

and use it in both try/catch blocks to keep logic consistent.


34-37: Nit: operation name

Minor: withErrorHandler("createConferencing", ...) doesn’t match the method name createConference. Consider aligning for clearer logs.

packages/api/src/providers/calendars/google-calendar.ts (3)

372-406: Use freshEvent payload on retry for clarity

You already reassign event with the fresh fetch. For readability and to avoid accidental drift, consider destructuring to freshEvent and using it explicitly in the retry update call.

-          event = await this.client.calendars.events.retrieve(eventId, {
+          const freshEvent = await this.client.calendars.events.retrieve(eventId, {
             calendarId,
           });
 
-          const freshAttendees = event.attendees ?? [];
+          const freshAttendees = freshEvent.attendees ?? [];
@@
-          await this.client.calendars.events.update(eventId, {
-            ...event,
+          await this.client.calendars.events.update(eventId, {
+            ...freshEvent,
             calendarId,
             attendees: freshAttendees,
             // Use the fresh sequence number
-            sequence: event.sequence,
+            sequence: freshEvent.sequence,
             sendUpdates: "all",
           });

448-484: Respond retry path: use freshly retrieved event object explicitly

Same readability nit as acceptEvent; prefer using a freshEvent variable explicitly.

-          event = await this.client.calendars.events.retrieve(eventId, {
+          const freshEvent = await this.client.calendars.events.retrieve(eventId, {
             calendarId,
           });
 
-          if (!event.attendees) {
+          if (!freshEvent.attendees) {
             throw new Error("Event has no attendees");
           }
 
-          const freshSelfIndex = event.attendees.findIndex(
+          const freshSelfIndex = freshEvent.attendees.findIndex(
             (attendee) => attendee.self,
           );
@@
-          event.attendees[freshSelfIndex] = {
-            ...event.attendees[freshSelfIndex],
+          freshEvent.attendees[freshSelfIndex] = {
+            ...freshEvent.attendees[freshSelfIndex],
             responseStatus: toGoogleCalendarAttendeeResponseStatus(
               response.status,
             ),
           };
 
-          await this.client.calendars.events.update(eventId, {
-            ...event,
+          await this.client.calendars.events.update(eventId, {
+            ...freshEvent,
             calendarId,
             // Use the fresh sequence number
-            sequence: event.sequence,
+            sequence: freshEvent.sequence,
             sendUpdates: response.sendUpdate ? "all" : "none",
           });

186-307: Centralize sequence-conflict detection

You repeat the string check for "Invalid sequence value" across updateEvent, acceptEvent, and responseToEvent. Encapsulate in a helper for maintainability and to enable future expansion to other error shapes.

Outside these ranges, add:

function isInvalidSequenceError(err: unknown): boolean {
  return err instanceof Error && /invalid sequence value/i.test(err.message);
}

Then within the catch blocks:

-        if (
-          error instanceof Error &&
-          error.message.includes("Invalid sequence value")
-        ) {
+        if (isInvalidSequenceError(error)) {

Also applies to: 344-407, 420-490

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2ff0b93 and bf10d50.

📒 Files selected for processing (5)
  • apps/web/src/lib/utils/calendar.ts (1 hunks)
  • packages/api/src/providers/calendars/google-calendar.ts (5 hunks)
  • packages/api/src/providers/conferencing/google-meet.ts (2 hunks)
  • packages/api/src/routers/events.ts (3 hunks)
  • packages/auth/src/server.ts (0 hunks)
💤 Files with no reviewable changes (1)
  • packages/auth/src/server.ts
🧰 Additional context used
🪛 Biome (2.1.2)
packages/api/src/providers/conferencing/google-meet.ts

[error] 46-47: Expected a statement but instead found '<<<<<<< HEAD'.

Expected a statement here.

(parse)


[error] 58-59: Expected a property, a shorthand property, a getter, a setter, or a method but instead found '==='.

Expected a property, a shorthand property, a getter, a setter, or a method here.

(parse)


[error] 59-59: Expected an expression but instead found '==='.

Expected an expression here.

(parse)


[error] 59-59: Expected an expression but instead found '='.

Expected an expression here.

(parse)


[error] 57-59: Invalid assignment to { type: "hangoutsMeet", ======

This expression cannot be assigned to

(parse)


[error] 60-60: Expected an expression, or an assignment but instead found 'const'.

Expected an expression, or an assignment here.

(parse)


[error] 60-60: expected : but instead found updatedEvent

Remove updatedEvent

(parse)


[error] 73-74: Expected a property, a shorthand property, a getter, a setter, or a method but instead found '>'.

Expected a property, a shorthand property, a getter, a setter, or a method here.

(parse)


[error] 74-74: Expected an expression but instead found '>>>'.

Expected an expression here.

(parse)


[error] 74-74: Expected an expression but instead found '>'.

Expected an expression here.

(parse)


[error] 77-77: Expected an expression but instead found '}'.

Expected an expression here.

(parse)


[error] 78-78: Expected a property, a shorthand property, a getter, a setter, or a method but instead found ')'.

Expected a property, a shorthand property, a getter, a setter, or a method here.

(parse)


[error] 98-99: Expected a statement but instead found '<<<<<<< HEAD'.

Expected a statement here.

(parse)


[error] 110-111: Expected a property, a shorthand property, a getter, a setter, or a method but instead found '==='.

Expected a property, a shorthand property, a getter, a setter, or a method here.

(parse)


[error] 111-111: Expected an expression but instead found '==='.

Expected an expression here.

(parse)


[error] 111-111: Expected an expression but instead found '='.

Expected an expression here.

(parse)


[error] 109-111: Invalid assignment to { type: "hangoutsMeet", ======

This expression cannot be assigned to

(parse)


[error] 112-112: Expected an expression, or an assignment but instead found 'const'.

Expected an expression, or an assignment here.

(parse)


[error] 112-112: expected : but instead found updatedEvent

Remove updatedEvent

(parse)


[error] 125-126: Expected a property, a shorthand property, a getter, a setter, or a method but instead found '>'.

Expected a property, a shorthand property, a getter, a setter, or a method here.

(parse)


[error] 126-126: Expected an expression but instead found '>>>'.

Expected an expression here.

(parse)


[error] 126-126: Expected an expression but instead found '>'.

Expected an expression here.

(parse)


[error] 129-129: Expected an expression but instead found '}'.

Expected an expression here.

(parse)


[error] 130-130: Expected a property, a shorthand property, a getter, a setter, or a method but instead found ')'.

Expected a property, a shorthand property, a getter, a setter, or a method here.

(parse)


[error] 60-60: This variable is used before its declaration.

The variable is declared here:

(lint/correctness/noInvalidUseBeforeDeclaration)


[error] 112-112: This variable is used before its declaration.

The variable is declared here:

(lint/correctness/noInvalidUseBeforeDeclaration)


[error] 60-60: Can't assign updatedEvent because it's a constant.

This is where the variable is defined as constant.

Unsafe fix: Replace const with let if you assign it to a new value.

(lint/correctness/noConstAssign)


[error] 112-112: Can't assign updatedEvent because it's a constant.

This is where the variable is defined as constant.

Unsafe fix: Replace const with let if you assign it to a new value.

(lint/correctness/noConstAssign)

🪛 GitHub Actions: Check formatting
packages/api/src/providers/conferencing/google-meet.ts

[error] 47-47: Merge conflict marker encountered. Resolve merge conflicts and remove conflict markers (e.g., <<<<<<< HEAD, =======, >>>>>>>) to restore valid TypeScript syntax.


[error] 47-47: bun run prettier --check failed due to parsing error (Unexpected token) at line 47:0 in google-meet.ts. This is likely caused by the merge conflict markers.

🪛 GitHub Actions: Lint
packages/api/src/providers/conferencing/google-meet.ts

[error] 47-47: ESLint: Parsing error: Merge conflict marker encountered. (Step: bun run lint in packages/api)

🔇 Additional comments (8)
apps/web/src/lib/utils/calendar.ts (1)

31-31: Calendar util is used only in Client Components—no SSR import paths found

I verified that every import of createEventId (and its caller createDraftEvent) lives in modules explicitly marked with "use client" (e.g. event-form.tsx, month-view.tsx, week-view.tsx, event-hotkeys.tsx), and I found no imports in any server-rendered pages or API routes. Since this util is only ever invoked in browser contexts, globalThis.crypto.randomUUID() will always be defined in supported modern browsers.

Because there are no SSR or non-secure contexts hitting this code, adding a guard or fallback for crypto.randomUUID isn’t necessary. Likewise, TypeScript will infer the return type as string, so an explicit annotation is optional.

Likely an incorrect or invalid review comment.

packages/api/src/routers/events.ts (3)

1-1: LGTM: switch to node:crypto randomUUID in API layer

Importing randomUUID from node:crypto is the right choice in the server runtime. This avoids depending on the Web Crypto global in Node.


278-286: Consistent server-side ID generation

Using randomUUID() when cloning to a different Google account is correct. It keeps the API layer consistent with Node’s crypto source for UUIDs.


391-399: Same here — good

The second usage of randomUUID() for cross-account move is also correct. No issues.

packages/api/src/providers/conferencing/google-meet.ts (1)

80-86: Good: verify conference data before parsing

The explicit check for updatedEvent.conferenceData prior to parsing is solid and avoids unsafe non-null assertions elsewhere.

packages/api/src/providers/calendars/google-calendar.ts (3)

192-204: Good start: fetch current event and preserve sequence

Fetching existingEvent then setting sequence: existingEvent.sequence in eventToUpdate correctly addresses Google’s optimistic concurrency.


344-371: Accept flow: preserves sequence and retries — good

The acceptEvent branch correctly builds attendees, preserves sequence, and retries once on sequence conflicts.


420-447: Respond flow: preserves sequence and sendUpdates — good

The first attempt properly updates the self attendee, preserves sequence, and respects sendUpdates based on the input.

Comment on lines 205 to 232
// Handle response status update within the same call for Google Calendar
if (event.response && event.response.status !== "unknown") {
if (!existingEvent.attendees) {
throw new Error("Event has no attendees");
}

const selfIndex = existingEvent.attendees.findIndex(
(attendee) => attendee.self,
);

if (selfIndex === -1) {
throw new Error("User is not an attendee");
}

const updatedAttendees = [...existingEvent.attendees];
updatedAttendees[selfIndex] = {
...updatedAttendees[selfIndex],
responseStatus: toGoogleCalendarAttendeeResponseStatus(
event.response.status,
),
};

eventToUpdate = {
...eventToUpdate,
attendees: updatedAttendees,
sendUpdates: event.response.sendUpdate ? "all" : "none",
};
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid dropping attendee edits when also changing response status

When event.response is provided, attendees are derived from existingEvent.attendees. If the UpdateEventInput also carries attendee changes (e.g., adding/removing guests), those edits may be overwritten. Prefer starting from the attendees in toGoogleCalendarEvent(event) when present, and then apply the self response mutation.

-        if (event.response && event.response.status !== "unknown") {
-          if (!existingEvent.attendees) {
+        if (event.response && event.response.status !== "unknown") {
+          const baseAttendees =
+            (toGoogleCalendarEvent(event) as any).attendees ??
+            existingEvent.attendees;
+          if (!baseAttendees) {
             throw new Error("Event has no attendees");
           }
 
-          const selfIndex = existingEvent.attendees.findIndex(
+          const selfIndex = baseAttendees.findIndex(
             (attendee) => attendee.self,
           );
 
           if (selfIndex === -1) {
             throw new Error("User is not an attendee");
           }
 
-          const updatedAttendees = [...existingEvent.attendees];
+          const updatedAttendees = [...baseAttendees];
           updatedAttendees[selfIndex] = {
             ...updatedAttendees[selfIndex],
             responseStatus: toGoogleCalendarAttendeeResponseStatus(
               event.response.status,
             ),
           };
 
           eventToUpdate = {
             ...eventToUpdate,
             attendees: updatedAttendees,
             sendUpdates: event.response.sendUpdate ? "all" : "none",
           };
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Handle response status update within the same call for Google Calendar
if (event.response && event.response.status !== "unknown") {
if (!existingEvent.attendees) {
throw new Error("Event has no attendees");
}
const selfIndex = existingEvent.attendees.findIndex(
(attendee) => attendee.self,
);
if (selfIndex === -1) {
throw new Error("User is not an attendee");
}
const updatedAttendees = [...existingEvent.attendees];
updatedAttendees[selfIndex] = {
...updatedAttendees[selfIndex],
responseStatus: toGoogleCalendarAttendeeResponseStatus(
event.response.status,
),
};
eventToUpdate = {
...eventToUpdate,
attendees: updatedAttendees,
sendUpdates: event.response.sendUpdate ? "all" : "none",
};
}
// Handle response status update within the same call for Google Calendar
if (event.response && event.response.status !== "unknown") {
const baseAttendees =
(toGoogleCalendarEvent(event) as any).attendees ??
existingEvent.attendees;
if (!baseAttendees) {
throw new Error("Event has no attendees");
}
const selfIndex = baseAttendees.findIndex(
(attendee) => attendee.self,
);
if (selfIndex === -1) {
throw new Error("User is not an attendee");
}
const updatedAttendees = [...baseAttendees];
updatedAttendees[selfIndex] = {
...updatedAttendees[selfIndex],
responseStatus: toGoogleCalendarAttendeeResponseStatus(
event.response.status,
),
};
eventToUpdate = {
...eventToUpdate,
attendees: updatedAttendees,
sendUpdates: event.response.sendUpdate ? "all" : "none",
};
}
🤖 Prompt for AI Agents
In packages/api/src/providers/calendars/google-calendar.ts around lines 205 to
232, the code overwrites attendee edits by always starting from
existingEvent.attendees when event.response is present; instead, build the base
attendees from toGoogleCalendarEvent(event).attendees if the incoming
UpdateEventInput includes attendees, otherwise fall back to
existingEvent.attendees, then locate the self attendee and only mutate its
responseStatus; finally set eventToUpdate.attendees to this merged list and keep
sendUpdates logic unchanged so incoming attendee additions/removals are
preserved while updating the current user's response.

Comment on lines 246 to 306
if (
error instanceof Error &&
error.message.includes("Invalid sequence value")
) {
// Re-fetch the event to get the latest sequence number and retry once
existingEvent = await this.client.calendars.events.retrieve(eventId, {
calendarId: calendar.id,
});

let eventToUpdate = {
...existingEvent,
calendarId: calendar.id,
...toGoogleCalendarEvent(event),
// Use the fresh sequence number
sequence: existingEvent.sequence,
};

// Handle response status update within the same call for Google Calendar
if (event.response && event.response.status !== "unknown") {
if (!existingEvent.attendees) {
throw new Error("Event has no attendees");
}

const selfIndex = existingEvent.attendees.findIndex(
(attendee) => attendee.self,
);

if (selfIndex === -1) {
throw new Error("User is not an attendee");
}

const updatedAttendees = [...existingEvent.attendees];
updatedAttendees[selfIndex] = {
...updatedAttendees[selfIndex],
responseStatus: toGoogleCalendarAttendeeResponseStatus(
event.response.status,
),
};

eventToUpdate = {
...eventToUpdate,
attendees: updatedAttendees,
sendUpdates: event.response.sendUpdate ? "all" : "none",
};
}

const updatedEvent = await this.client.calendars.events.update(
eventId,
eventToUpdate,
);

return parseGoogleCalendarEvent({
calendar,
accountId: this.accountId,
event: updatedEvent,
});
}

const updatedAttendees = [...existingEvent.attendees];
updatedAttendees[selfIndex] = {
...updatedAttendees[selfIndex],
responseStatus: toGoogleCalendarAttendeeResponseStatus(
event.response.status,
),
};

eventToUpdate = {
...eventToUpdate,
attendees: updatedAttendees,
sendUpdates: event.response.sendUpdate ? "all" : "none",
};
// Re-throw other errors
throw error;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Retry path: same attendee overwrite risk

The same overwrite risk exists in the retry branch; mirror the fix there to respect incoming attendee edits while updating self response.

-          if (event.response && event.response.status !== "unknown") {
-            if (!existingEvent.attendees) {
+          if (event.response && event.response.status !== "unknown") {
+            const baseAttendees =
+              (toGoogleCalendarEvent(event) as any).attendees ??
+              existingEvent.attendees;
+            if (!baseAttendees) {
               throw new Error("Event has no attendees");
             }
 
-            const selfIndex = existingEvent.attendees.findIndex(
+            const selfIndex = baseAttendees.findIndex(
               (attendee) => attendee.self,
             );
 
             if (selfIndex === -1) {
               throw new Error("User is not an attendee");
             }
 
-            const updatedAttendees = [...existingEvent.attendees];
+            const updatedAttendees = [...baseAttendees];
             updatedAttendees[selfIndex] = {
               ...updatedAttendees[selfIndex],
               responseStatus: toGoogleCalendarAttendeeResponseStatus(
                 event.response.status,
               ),
             };
 
             eventToUpdate = {
               ...eventToUpdate,
               attendees: updatedAttendees,
               sendUpdates: event.response.sendUpdate ? "all" : "none",
             };
           }
🤖 Prompt for AI Agents
In packages/api/src/providers/calendars/google-calendar.ts around lines 246 to
306, the retry branch currently overwrites attendees with
existingEvent.attendees when setting the user's response, which discards any
attendee edits coming in the incoming event; change the merge to prefer incoming
event.attendees (if provided) and only update the self attendee's
responseStatus, falling back to existingEvent.attendees when incoming attendees
are missing, and then set sendUpdates as before. Concretely: compute a
baseAttendees = event.attendees ?? existingEvent.attendees; if baseAttendees is
missing throw "Event has no attendees"; find selfIndex in baseAttendees, throw
if -1; create updatedAttendees = [...baseAttendees]; update only
updatedAttendees[selfIndex] by copying and setting responseStatus to
toGoogleCalendarAttendeeResponseStatus(event.response.status); then assign
eventToUpdate.attendees = updatedAttendees and eventToUpdate.sendUpdates =
event.response.sendUpdate ? "all" : "none" before calling
calendars.events.update.

@@ -1,3 +1,4 @@
import { randomUUID } from "node:crypto";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Consistent UUID source in API

Importing randomUUID from node:crypto at the top is correct. Ensure no remaining usages of crypto.randomUUID() remain in server code.

Run this to verify:


🏁 Script executed:

#!/bin/bash
# List any server-side references to crypto.randomUUID
rg -nP --type=ts -C2 '\bcrypto\.randomUUID\s*\(' packages/api -g '!**/node_modules/**'

Length of output: 1066


Replace remaining crypto.randomUUID() calls with the imported randomUUID

The grep results show two direct usages of crypto.randomUUID() in packages/api/src/providers/conferencing/google-meet.ts that must be updated to use the top-of-file import { randomUUID } from "node:crypto" instead:

• packages/api/src/providers/conferencing/google-meet.ts:70
• packages/api/src/providers/conferencing/google-meet.ts:122

Please apply the following change at both locations:

- requestId: crypto.randomUUID(),
+ requestId: randomUUID(),

This ensures a consistent UUID source across the API.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { randomUUID } from "node:crypto";
// In packages/api/src/providers/conferencing/google-meet.ts
// — at around line 70 —
requestId: randomUUID(),
// — at around line 122 —
requestId: randomUUID(),
🤖 Prompt for AI Agents
In packages/api/src/providers/conferencing/google-meet.ts around lines 70 and
122, replace direct calls to crypto.randomUUID() with the already imported
randomUUID from "node:crypto"; update both usages to call randomUUID() so the
file uses the top-of-file import consistently and removes direct crypto
namespace access.

Comment on lines 46 to 85
try {
<<<<<<< HEAD
const updatedEvent = await this.client.calendars.events.update(eventId, {
calendarId,
...existingEvent,
// Preserve the sequence number to prevent conflicts
sequence: existingEvent.sequence,
conferenceDataVersion: 1, // This ensures the conference data is created, DO NOT REMOVE
conferenceData: {
createRequest: {
requestId: randomUUID(),
conferenceSolutionKey: {
type: "hangoutsMeet",
=======
const updatedEvent = await this.client.calendars.events.update(
eventId,
{
calendarId,
...existingEvent,
// Preserve the sequence number to prevent conflicts
sequence: existingEvent.sequence,
conferenceDataVersion: 1, // This ensures the conference data is created, DO NOT REMOVE
conferenceData: {
createRequest: {
requestId: crypto.randomUUID(),
conferenceSolutionKey: {
type: "hangoutsMeet",
},
>>>>>>> babd5bd43baca4599e85af543c72f1b832791cd4
},
},
},
},
});
);

if (!updatedEvent.conferenceData) {
throw new Error("Failed to create conference data");
}
if (!updatedEvent.conferenceData) {
throw new Error("Failed to create conference data");
}

return parseGoogleCalendarConferenceData(updatedEvent)!;
} catch (error) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Resolve merge conflicts and standardize requestId generation

The file contains unresolved merge conflict markers, breaking the build and lint steps. Also, two competing implementations use crypto.randomUUID() vs randomUUID(). Keep node:crypto’s randomUUID consistently in the API layer.

Apply this diff to resolve the first conflict block and standardize on randomUUID:

@@
-      try {
-<<<<<<< HEAD
-        const updatedEvent = await this.client.calendars.events.update(eventId, {
-          calendarId,
-          ...existingEvent,
-          // Preserve the sequence number to prevent conflicts
-          sequence: existingEvent.sequence,
-          conferenceDataVersion: 1, // This ensures the conference data is created, DO NOT REMOVE
-          conferenceData: {
-            createRequest: {
-              requestId: randomUUID(),
-              conferenceSolutionKey: {
-                type: "hangoutsMeet",
-=======
-        const updatedEvent = await this.client.calendars.events.update(
-          eventId,
-          {
-            calendarId,
-            ...existingEvent,
-            // Preserve the sequence number to prevent conflicts
-            sequence: existingEvent.sequence,
-            conferenceDataVersion: 1, // This ensures the conference data is created, DO NOT REMOVE
-            conferenceData: {
-              createRequest: {
-                requestId: crypto.randomUUID(),
-                conferenceSolutionKey: {
-                  type: "hangoutsMeet",
-                },
-              },
-            },
-          },
-        );
->>>>>>> babd5bd43baca4599e85af543c72f1b832791cd4
+      try {
+        const updatedEvent = await this.client.calendars.events.update(eventId, {
+          calendarId,
+          ...existingEvent,
+          // Preserve the sequence number to prevent conflicts
+          sequence: existingEvent.sequence,
+          conferenceDataVersion: 1, // This ensures the conference data is created, DO NOT REMOVE
+          conferenceData: {
+            createRequest: {
+              requestId: randomUUID(),
+              conferenceSolutionKey: {
+                type: "hangoutsMeet",
+              },
+            },
+          },
+        });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
<<<<<<< HEAD
const updatedEvent = await this.client.calendars.events.update(eventId, {
calendarId,
...existingEvent,
// Preserve the sequence number to prevent conflicts
sequence: existingEvent.sequence,
conferenceDataVersion: 1, // This ensures the conference data is created, DO NOT REMOVE
conferenceData: {
createRequest: {
requestId: randomUUID(),
conferenceSolutionKey: {
type: "hangoutsMeet",
=======
const updatedEvent = await this.client.calendars.events.update(
eventId,
{
calendarId,
...existingEvent,
// Preserve the sequence number to prevent conflicts
sequence: existingEvent.sequence,
conferenceDataVersion: 1, // This ensures the conference data is created, DO NOT REMOVE
conferenceData: {
createRequest: {
requestId: crypto.randomUUID(),
conferenceSolutionKey: {
type: "hangoutsMeet",
},
>>>>>>> babd5bd43baca4599e85af543c72f1b832791cd4
},
},
},
},
});
);
if (!updatedEvent.conferenceData) {
throw new Error("Failed to create conference data");
}
if (!updatedEvent.conferenceData) {
throw new Error("Failed to create conference data");
}
return parseGoogleCalendarConferenceData(updatedEvent)!;
} catch (error) {
try {
const updatedEvent = await this.client.calendars.events.update(eventId, {
calendarId,
...existingEvent,
// Preserve the sequence number to prevent conflicts
sequence: existingEvent.sequence,
conferenceDataVersion: 1, // This ensures the conference data is created, DO NOT REMOVE
conferenceData: {
createRequest: {
requestId: randomUUID(),
conferenceSolutionKey: {
type: "hangoutsMeet",
},
},
},
});
if (!updatedEvent.conferenceData) {
throw new Error("Failed to create conference data");
}
return parseGoogleCalendarConferenceData(updatedEvent)!;
} catch (error) {
// …
🧰 Tools
🪛 Biome (2.1.2)

[error] 46-47: Expected a statement but instead found '<<<<<<< HEAD'.

Expected a statement here.

(parse)


[error] 58-59: Expected a property, a shorthand property, a getter, a setter, or a method but instead found '==='.

Expected a property, a shorthand property, a getter, a setter, or a method here.

(parse)


[error] 59-59: Expected an expression but instead found '==='.

Expected an expression here.

(parse)


[error] 59-59: Expected an expression but instead found '='.

Expected an expression here.

(parse)


[error] 57-59: Invalid assignment to { type: "hangoutsMeet", ======

This expression cannot be assigned to

(parse)


[error] 60-60: Expected an expression, or an assignment but instead found 'const'.

Expected an expression, or an assignment here.

(parse)


[error] 60-60: expected : but instead found updatedEvent

Remove updatedEvent

(parse)


[error] 73-74: Expected a property, a shorthand property, a getter, a setter, or a method but instead found '>'.

Expected a property, a shorthand property, a getter, a setter, or a method here.

(parse)


[error] 74-74: Expected an expression but instead found '>>>'.

Expected an expression here.

(parse)


[error] 74-74: Expected an expression but instead found '>'.

Expected an expression here.

(parse)


[error] 77-77: Expected an expression but instead found '}'.

Expected an expression here.

(parse)


[error] 78-78: Expected a property, a shorthand property, a getter, a setter, or a method but instead found ')'.

Expected a property, a shorthand property, a getter, a setter, or a method here.

(parse)


[error] 60-60: This variable is used before its declaration.

The variable is declared here:

(lint/correctness/noInvalidUseBeforeDeclaration)


[error] 60-60: Can't assign updatedEvent because it's a constant.

This is where the variable is defined as constant.

Unsafe fix: Replace const with let if you assign it to a new value.

(lint/correctness/noConstAssign)

🪛 GitHub Actions: Check formatting

[error] 47-47: Merge conflict marker encountered. Resolve merge conflicts and remove conflict markers (e.g., <<<<<<< HEAD, =======, >>>>>>>) to restore valid TypeScript syntax.


[error] 47-47: bun run prettier --check failed due to parsing error (Unexpected token) at line 47:0 in google-meet.ts. This is likely caused by the merge conflict markers.

🪛 GitHub Actions: Lint

[error] 47-47: ESLint: Parsing error: Merge conflict marker encountered. (Step: bun run lint in packages/api)

🤖 Prompt for AI Agents
In packages/api/src/providers/conferencing/google-meet.ts around lines 46 to 85,
remove the unresolved Git conflict markers and consolidate the event update call
to the formatted version (no <<<<>>> markers), using node:crypto's randomUUID()
for requestId; ensure the object structure matches the existingEvent merge and
conferenceData/createRequest shape, and add or keep the import for crypto (or
import { randomUUID } from "crypto" and call randomUUID()) so the code compiles
and lint passes.

Comment on lines 99 to 130
<<<<<<< HEAD
const updatedEvent = await this.client.calendars.events.update(eventId, {
calendarId,
...freshEvent,
// Use the fresh sequence number
sequence: freshEvent.sequence,
conferenceDataVersion: 1, // This ensures the conference data is created, DO NOT REMOVE
conferenceData: {
createRequest: {
requestId: randomUUID(),
conferenceSolutionKey: {
type: "hangoutsMeet",
=======
const updatedEvent = await this.client.calendars.events.update(
eventId,
{
calendarId,
...freshEvent,
// Use the fresh sequence number
sequence: freshEvent.sequence,
conferenceDataVersion: 1, // This ensures the conference data is created, DO NOT REMOVE
conferenceData: {
createRequest: {
requestId: crypto.randomUUID(),
conferenceSolutionKey: {
type: "hangoutsMeet",
},
>>>>>>> babd5bd43baca4599e85af543c72f1b832791cd4
},
},
},
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Resolve second conflict block and keep retry idempotency with a new requestId

Same issue below. On the retry path, use a fresh requestId via node:crypto’s randomUUID() and remove conflict markers.

@@
-<<<<<<< HEAD
-          const updatedEvent = await this.client.calendars.events.update(eventId, {
-            calendarId,
-            ...freshEvent,
-            // Use the fresh sequence number
-            sequence: freshEvent.sequence,
-            conferenceDataVersion: 1, // This ensures the conference data is created, DO NOT REMOVE
-            conferenceData: {
-              createRequest: {
-                requestId: randomUUID(),
-                conferenceSolutionKey: {
-                  type: "hangoutsMeet",
-=======
-          const updatedEvent = await this.client.calendars.events.update(
-            eventId,
-            {
-              calendarId,
-              ...freshEvent,
-              // Use the fresh sequence number
-              sequence: freshEvent.sequence,
-              conferenceDataVersion: 1, // This ensures the conference data is created, DO NOT REMOVE
-              conferenceData: {
-                createRequest: {
-                  requestId: crypto.randomUUID(),
-                  conferenceSolutionKey: {
-                    type: "hangoutsMeet",
-                  },
-                },
-              },
-            },
-          );
->>>>>>> babd5bd43baca4599e85af543c72f1b832791cd4
+          const updatedEvent = await this.client.calendars.events.update(eventId, {
+            calendarId,
+            ...freshEvent,
+            // Use the fresh sequence number
+            sequence: freshEvent.sequence,
+            conferenceDataVersion: 1, // This ensures the conference data is created, DO NOT REMOVE
+            conferenceData: {
+              createRequest: {
+                requestId: randomUUID(),
+                conferenceSolutionKey: {
+                  type: "hangoutsMeet",
+                },
+              },
+            },
+          });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<<<<<<< HEAD
const updatedEvent = await this.client.calendars.events.update(eventId, {
calendarId,
...freshEvent,
// Use the fresh sequence number
sequence: freshEvent.sequence,
conferenceDataVersion: 1, // This ensures the conference data is created, DO NOT REMOVE
conferenceData: {
createRequest: {
requestId: randomUUID(),
conferenceSolutionKey: {
type: "hangoutsMeet",
=======
const updatedEvent = await this.client.calendars.events.update(
eventId,
{
calendarId,
...freshEvent,
// Use the fresh sequence number
sequence: freshEvent.sequence,
conferenceDataVersion: 1, // This ensures the conference data is created, DO NOT REMOVE
conferenceData: {
createRequest: {
requestId: crypto.randomUUID(),
conferenceSolutionKey: {
type: "hangoutsMeet",
},
>>>>>>> babd5bd43baca4599e85af543c72f1b832791cd4
},
},
},
);
const updatedEvent = await this.client.calendars.events.update(eventId, {
calendarId,
...freshEvent,
// Use the fresh sequence number
sequence: freshEvent.sequence,
conferenceDataVersion: 1, // This ensures the conference data is created, DO NOT REMOVE
conferenceData: {
createRequest: {
requestId: randomUUID(),
conferenceSolutionKey: {
type: "hangoutsMeet",
},
},
},
});
🧰 Tools
🪛 Biome (2.1.2)

[error] 110-111: Expected a property, a shorthand property, a getter, a setter, or a method but instead found '==='.

Expected a property, a shorthand property, a getter, a setter, or a method here.

(parse)


[error] 111-111: Expected an expression but instead found '==='.

Expected an expression here.

(parse)


[error] 111-111: Expected an expression but instead found '='.

Expected an expression here.

(parse)


[error] 109-111: Invalid assignment to { type: "hangoutsMeet", ======

This expression cannot be assigned to

(parse)


[error] 112-112: Expected an expression, or an assignment but instead found 'const'.

Expected an expression, or an assignment here.

(parse)


[error] 112-112: expected : but instead found updatedEvent

Remove updatedEvent

(parse)


[error] 125-126: Expected a property, a shorthand property, a getter, a setter, or a method but instead found '>'.

Expected a property, a shorthand property, a getter, a setter, or a method here.

(parse)


[error] 126-126: Expected an expression but instead found '>>>'.

Expected an expression here.

(parse)


[error] 126-126: Expected an expression but instead found '>'.

Expected an expression here.

(parse)


[error] 129-129: Expected an expression but instead found '}'.

Expected an expression here.

(parse)


[error] 130-130: Expected a property, a shorthand property, a getter, a setter, or a method but instead found ')'.

Expected a property, a shorthand property, a getter, a setter, or a method here.

(parse)


[error] 112-112: This variable is used before its declaration.

The variable is declared here:

(lint/correctness/noInvalidUseBeforeDeclaration)


[error] 112-112: Can't assign updatedEvent because it's a constant.

This is where the variable is defined as constant.

Unsafe fix: Replace const with let if you assign it to a new value.

(lint/correctness/noConstAssign)

🤖 Prompt for AI Agents
In packages/api/src/providers/conferencing/google-meet.ts around lines 99 to
130, resolve the leftover merge conflict markers and ensure the retry path uses
a fresh requestId for idempotency by replacing the duplicated/conflicted
requestId usage with node:crypto.randomUUID(); remove the <<<>>> conflict
markers and the older randomUUID() import/usage if present, ensure import of
crypto.randomUUID (or use crypto.randomUUID() directly) is used consistently for
the createRequest.requestId so each retry generates a new idempotency key.

@tembo
Copy link
Contributor Author

tembo bot commented Aug 21, 2025

Thanks for bringing this to my attention. I've resolved the merge conflicts and standardized the UUID generation across the codebase. Here's what I did:

  1. Resolved all Git merge conflicts in the Google Meet provider
  2. Standardized the use of randomUUID() from node:crypto throughout the code
  3. Fixed the attendee handling to preserve incoming attendee changes when also updating response status
  4. Created a reusable isInvalidSequenceError() helper to improve error detection consistency

These changes should fix the sequence value errors by:

  1. Always preserving the latest sequence number from either existing or freshly fetched events
  2. Properly retrying with fresh sequence numbers when conflicts occur
  3. Using a consistent approach to error detection and retry logic

The code now correctly handles concurrent modifications by:

  • Fetching the latest state on sequence conflicts
  • Using fresh sequence numbers on retries
  • Preserving user changes to attendees while updating response status

Is there anything else you'd like me to explain about these changes?

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