@agent-native/core 0.185.0 → 0.186.0-nightly-20260923174435

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/calendar/actions/delete-event.ts +6 -4
  3. package/corpus/templates/calendar/actions/delete-events.ts +10 -11
  4. package/corpus/templates/calendar/actions/event-action-helpers.ts +53 -0
  5. package/corpus/templates/calendar/actions/get-event.ts +59 -14
  6. package/corpus/templates/calendar/actions/rsvp-event.ts +5 -3
  7. package/corpus/templates/calendar/actions/update-event.ts +15 -7
  8. package/corpus/templates/calendar/actions/update-events.ts +29 -11
  9. package/corpus/templates/calendar/changelog/2026-09-21-events-stay-editable-when-their-calendar-is-connected-throug.md +6 -0
  10. package/corpus/templates/calendar/server/lib/google-calendar.ts +70 -18
  11. package/corpus/templates/calendar/shared/google-calendar-sources.ts +47 -0
  12. package/corpus/templates/clips/actions/finalize-recording.ts +35 -1
  13. package/corpus/templates/clips/app/components/player/delete-recording-menu.tsx +5 -1
  14. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +18 -1
  15. package/corpus/templates/clips/app/routes/_app.r.$recordingId.tsx +2 -2
  16. package/corpus/templates/clips/app/routes/record.tsx +1 -0
  17. package/corpus/templates/clips/changelog/2026-09-23-long-recording-menus-scroll-within-the-visible-space-so-ever.md +6 -0
  18. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +14 -4
  19. package/corpus/templates/content/app/blocks/contentBlockRegistry.tsx +9 -1
  20. package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +16 -2
  21. package/corpus/templates/content/app/components/editor/registrySlashItems.ts +61 -40
  22. package/corpus/templates/content/app/i18n-data.ts +12 -0
  23. package/corpus/templates/content/app/routes/_app.settings.tsx +36 -9
  24. package/corpus/templates/content/docs/product/capabilities/content.renderer.custom-block.md +1 -1
  25. package/corpus/templates/content/server/plugins/agent-chat.ts +1 -0
  26. package/corpus/templates/content/shared/labs.ts +35 -1
  27. package/corpus/templates/slides/actions/get-design-system.ts +4 -2
  28. package/dist/agent/engine/openai-compatible-endpoint.d.ts +7 -0
  29. package/dist/agent/engine/openai-compatible-endpoint.js +9 -0
  30. package/dist/agent/engine/provider-endpoint-validation.d.ts +8 -1
  31. package/dist/agent/engine/provider-endpoint-validation.js +47 -10
  32. package/dist/agent/engine/registry.js +8 -5
  33. package/dist/automation/index.d.ts +1 -1
  34. package/dist/cli/skills.d.ts +3 -0
  35. package/dist/cli/skills.js +45 -9
  36. package/dist/client/agent-engine-key.d.ts +7 -0
  37. package/dist/client/agent-engine-key.js +37 -0
  38. package/dist/client/agent-provider-catalog.js +1 -1
  39. package/dist/client/settings/ProviderSetupForm.js +80 -7
  40. package/dist/client/settings/SettingsPanel.js +149 -11
  41. package/dist/client/use-chat-models.js +31 -0
  42. package/dist/collab/awareness.d.ts +2 -2
  43. package/dist/collab/routes.d.ts +1 -1
  44. package/dist/labs/actions/set-lab.js +1 -0
  45. package/dist/labs/registry.js +6 -1
  46. package/dist/localization/default-messages.d.ts +7 -1
  47. package/dist/localization/default-messages.js +7 -1
  48. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  49. package/dist/notifications/routes.d.ts +3 -3
  50. package/dist/observability/routes.d.ts +3 -3
  51. package/dist/org/index.d.ts +1 -1
  52. package/dist/provider-api/actions/custom-provider-registration.d.ts +6 -6
  53. package/dist/provider-api/actions/provider-api.d.ts +3 -3
  54. package/dist/scripts/agent-engines/test-agent-engine.js +9 -7
  55. package/dist/secrets/routes.d.ts +3 -3
  56. package/dist/server/agent-engine-api-key-route.d.ts +3 -3
  57. package/dist/server/agent-engine-api-key-route.js +8 -5
  58. package/dist/server/agent-engine-ollama-models-route.d.ts +15 -0
  59. package/dist/server/agent-engine-ollama-models-route.js +105 -0
  60. package/dist/server/core-routes-plugin.js +5 -0
  61. package/dist/server/credential-provider.d.ts +13 -0
  62. package/dist/server/credential-provider.js +19 -0
  63. package/package.json +3 -3
package/corpus/README.md CHANGED
@@ -31,4 +31,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
31
31
 
32
32
  ## Generated Counts
33
33
 
34
- - template files: 9786
34
+ - template files: 9788
@@ -9,9 +9,11 @@ import { isGoogleEventAbsentError } from "../server/lib/google-api.js";
9
9
  import * as googleCalendar from "../server/lib/google-calendar.js";
10
10
  import {
11
11
  cliBoolean,
12
+ googleEventResultId,
12
13
  normalizeWritableGoogleEventId,
13
14
  rawCliBoolean,
14
15
  requireActionUserEmail,
16
+ resolveGoogleEventAccountEmail,
15
17
  resolveOwnedAccountEmail,
16
18
  } from "./event-action-helpers.js";
17
19
 
@@ -66,11 +68,11 @@ export default defineAction({
66
68
  );
67
69
  }
68
70
 
69
- const googleEventId = normalizeWritableGoogleEventId(args.id);
70
71
  const accountEmail = await resolveOwnedAccountEmail(
71
- args.accountEmail,
72
+ resolveGoogleEventAccountEmail(args.id, args.accountEmail),
72
73
  ownerEmail,
73
74
  );
75
+ const googleEventId = normalizeWritableGoogleEventId(args.id);
74
76
  const guestNotificationMessage = normalizeGuestNotificationMessage(
75
77
  args.notificationMessage,
76
78
  );
@@ -109,7 +111,7 @@ export default defineAction({
109
111
  return {
110
112
  success: true,
111
113
  alreadyAbsent: true,
112
- id: `google-${googleEventId}`,
114
+ id: googleEventResultId(args.id, googleEventId, accountEmail),
113
115
  accountEmail,
114
116
  scope: args.scope,
115
117
  removedOnly: args.removeOnly ?? false,
@@ -130,7 +132,7 @@ export default defineAction({
130
132
  return {
131
133
  success: true,
132
134
  alreadyAbsent: false,
133
- id: `google-${googleEventId}`,
135
+ id: googleEventResultId(args.id, googleEventId, accountEmail),
134
136
  accountEmail,
135
137
  scope: args.scope,
136
138
  removedOnly: args.removeOnly ?? false,
@@ -15,12 +15,14 @@ import {
15
15
  BULK_EVENT_CONCURRENCY,
16
16
  MAX_MATCHED_EVENTS,
17
17
  cliBoolean,
18
+ googleEventResultId,
18
19
  isBookedOnAccount,
19
20
  mapWithConcurrency,
20
21
  normalizeWritableGoogleEventId,
21
22
  rawCliBoolean,
22
23
  requireActionUserEmail,
23
24
  requireExplicitBound,
25
+ resolveBulkGoogleEventAccountEmail,
24
26
  resolveOwnedAccountEmail,
25
27
  startsWithinRange,
26
28
  undeletableEventReason,
@@ -218,22 +220,24 @@ export default defineAction({
218
220
 
219
221
  if (hasIds) {
220
222
  const accountEmail = await resolveOwnedAccountEmail(
221
- args.accountEmail,
223
+ resolveBulkGoogleEventAccountEmail(args.ids!, args.accountEmail),
222
224
  ownerEmail,
223
225
  );
224
226
  // Two spellings of one id ("google-a" and "a") would otherwise enqueue two
225
227
  // writes for the same event: one succeeds, the other 404s, and the report
226
228
  // claims a failure that never happened.
227
229
  const requested = Array.from(
228
- new Set(args.ids!.map(normalizeWritableGoogleEventId)),
230
+ new Map(
231
+ args.ids!.map((id) => [normalizeWritableGoogleEventId(id), id]),
232
+ ).entries(),
229
233
  );
230
234
  // Explicit ids get the same booking protection as a filtered selection:
231
235
  // naming the event directly does not make leaving its booking confirmed
232
236
  // any less of a silent inconsistency.
233
- const booked = await findBookedGoogleEvents(requested);
234
- for (const googleEventId of requested) {
237
+ const booked = await findBookedGoogleEvents(requested.map(([id]) => id));
238
+ for (const [googleEventId, displayId] of requested) {
235
239
  const display: BulkEventResult = {
236
- id: `google-${googleEventId}`,
240
+ id: googleEventResultId(displayId, googleEventId, accountEmail),
237
241
  accountEmail,
238
242
  outcome: "matched",
239
243
  };
@@ -290,12 +294,7 @@ export default defineAction({
290
294
 
291
295
  for (const event of matched) {
292
296
  const display: BulkEventResult = {
293
- id:
294
- event.overlayEmail || event.calendarReadOnly
295
- ? event.id
296
- : event.googleEventId
297
- ? `google-${event.googleEventId}`
298
- : event.id,
297
+ id: event.id,
299
298
  title: event.title,
300
299
  start: event.start,
301
300
  weekday: eventWeekday(event.start, range.timezone),
@@ -12,6 +12,10 @@ import {
12
12
  } from "../server/lib/find-time.js";
13
13
  import * as googleCalendar from "../server/lib/google-calendar.js";
14
14
  import type { CalendarEvent } from "../shared/api.js";
15
+ import {
16
+ createGoogleAccountEventId,
17
+ parseGoogleAccountEventId,
18
+ } from "../shared/google-calendar-sources.js";
15
19
 
16
20
  export const cliBoolean = z
17
21
  .union([z.boolean(), z.enum(["true", "false"])])
@@ -190,9 +194,58 @@ export function requireActionUserEmail(): string {
190
194
  }
191
195
 
192
196
  export function normalizeGoogleEventId(id: string): string {
197
+ const accountEvent = parseGoogleAccountEventId(id);
198
+ if (accountEvent) return accountEvent.googleEventId;
193
199
  return id.startsWith("google-") ? id.slice("google-".length) : id;
194
200
  }
195
201
 
202
+ export function googleEventResultId(
203
+ inputId: string,
204
+ googleEventId: string,
205
+ accountEmail: string,
206
+ ): string {
207
+ return parseGoogleAccountEventId(inputId)
208
+ ? createGoogleAccountEventId({ accountEmail, googleEventId })
209
+ : `google-${googleEventId}`;
210
+ }
211
+
212
+ export function resolveGoogleEventAccountEmail(
213
+ id: string,
214
+ accountEmail: string | undefined,
215
+ ): string | undefined {
216
+ const accountEvent = parseGoogleAccountEventId(id);
217
+ if (!accountEvent) return accountEmail;
218
+ if (
219
+ accountEmail &&
220
+ accountEmail.trim().toLowerCase() !== accountEvent.accountEmail
221
+ ) {
222
+ throw new Error("Google event account does not match the selected account");
223
+ }
224
+ return accountEvent.accountEmail;
225
+ }
226
+
227
+ export function resolveBulkGoogleEventAccountEmail(
228
+ ids: string[],
229
+ accountEmail: string | undefined,
230
+ ): string | undefined {
231
+ const scopedCount = ids.filter((id) => parseGoogleAccountEventId(id)).length;
232
+ if (scopedCount > 0 && scopedCount !== ids.length) {
233
+ throw new Error(
234
+ "Bulk event ids cannot mix account-scoped and legacy Google ids",
235
+ );
236
+ }
237
+ const accounts = new Set(
238
+ ids
239
+ .map((id) => resolveGoogleEventAccountEmail(id, accountEmail))
240
+ .filter((email): email is string => !!email)
241
+ .map((email) => email.trim().toLowerCase()),
242
+ );
243
+ if (accounts.size > 1) {
244
+ throw new Error("Bulk event ids must belong to one Google account");
245
+ }
246
+ return accounts.values().next().value ?? accountEmail;
247
+ }
248
+
196
249
  export function normalizeWritableGoogleEventId(id: string): string {
197
250
  if (id.startsWith("overlay-") && id.slice("overlay-".length).includes("@")) {
198
251
  throw new Error("Overlay Google calendar events are read-only");
@@ -5,7 +5,11 @@ import { z } from "zod";
5
5
  import { calendarGetEvent } from "../server/lib/google-api.js";
6
6
  import * as googleCalendar from "../server/lib/google-calendar.js";
7
7
  import type { CalendarEvent } from "../shared/api.js";
8
- import { parseGoogleCalendarSourceKey } from "../shared/google-calendar-sources.js";
8
+ import {
9
+ createGoogleAccountEventId,
10
+ parseGoogleAccountEventId,
11
+ parseGoogleCalendarSourceKey,
12
+ } from "../shared/google-calendar-sources.js";
9
13
  import { getGoogleEventColorHex } from "../shared/google-event-colors.js";
10
14
 
11
15
  export default defineAction({
@@ -61,26 +65,37 @@ export default defineAction({
61
65
  run: async (args) => {
62
66
  const email = getRequestUserEmail();
63
67
  if (!email) throw new Error("no authenticated user");
68
+ const accountEvent = parseGoogleAccountEventId(args.id);
64
69
 
65
70
  if (args.calendarSourceKey) {
66
71
  const source = parseGoogleCalendarSourceKey(args.calendarSourceKey);
67
72
  if (!source) throw new Error("Invalid Google Calendar source key");
73
+ if (accountEvent && accountEvent.accountEmail !== source.accountEmail) {
74
+ throw new Error(
75
+ "Google event account does not match the selected source",
76
+ );
77
+ }
68
78
  const namespacedPrefix = `google-${args.calendarSourceKey}-`;
69
- const rawId = args.id.startsWith(namespacedPrefix)
70
- ? args.id.slice(namespacedPrefix.length)
71
- : args.id.startsWith("google-")
72
- ? args.id.slice("google-".length)
73
- : args.id;
74
- return googleCalendar.getEvent(
79
+ const rawId = accountEvent
80
+ ? accountEvent.googleEventId
81
+ : args.id.startsWith(namespacedPrefix)
82
+ ? args.id.slice(namespacedPrefix.length)
83
+ : args.id.startsWith("google-")
84
+ ? args.id.slice("google-".length)
85
+ : args.id;
86
+ const event = await googleCalendar.getEvent(
75
87
  rawId,
76
88
  { ownerEmail: email, accountEmail: source.accountEmail },
77
89
  { calendarSourceKey: args.calendarSourceKey },
78
90
  );
91
+ return accountEvent ? { ...event, id: args.id } : event;
79
92
  }
80
93
 
81
- const rawId = args.id.startsWith("google-")
82
- ? args.id.slice("google-".length)
83
- : args.id;
94
+ const rawId = accountEvent
95
+ ? accountEvent.googleEventId
96
+ : args.id.startsWith("google-")
97
+ ? args.id.slice("google-".length)
98
+ : args.id;
84
99
  const calendarId = args.calendarId ?? "primary";
85
100
  if (calendarId !== "primary") {
86
101
  throw new Error(
@@ -88,20 +103,49 @@ export default defineAction({
88
103
  );
89
104
  }
90
105
 
91
- const clients = await googleCalendar.getClients(email);
106
+ const { clients, errors } = accountEvent
107
+ ? await googleCalendar.getClientsWithErrors(email)
108
+ : { clients: await googleCalendar.getClients(email), errors: [] };
109
+ const accountError = accountEvent
110
+ ? errors.find(
111
+ ({ email: accountEmail }) =>
112
+ accountEmail.trim().toLowerCase() === accountEvent.accountEmail,
113
+ )
114
+ : undefined;
115
+ if (accountError) {
116
+ throw new Error(
117
+ `Google Calendar connection for ${accountEvent!.accountEmail} failed: ${accountError.error}`,
118
+ );
119
+ }
92
120
  if (clients.length === 0) {
93
121
  return {
94
122
  error: "Google Calendar not connected. Connect via Settings first.",
95
123
  };
96
124
  }
97
125
 
98
- for (const { email: acctEmail, accessToken } of clients) {
126
+ const selectedClients = accountEvent
127
+ ? clients.filter(
128
+ ({ email: accountEmail }) =>
129
+ accountEmail.trim().toLowerCase() === accountEvent.accountEmail,
130
+ )
131
+ : clients;
132
+ if (accountEvent && selectedClients.length === 0) {
133
+ throw new Error(
134
+ `Google Calendar account is not connected: ${accountEvent.accountEmail}`,
135
+ );
136
+ }
137
+ for (const { email: acctEmail, accessToken } of selectedClients) {
99
138
  try {
100
139
  const evt = await calendarGetEvent(accessToken, calendarId, rawId);
101
140
  const selfAttendee = evt.attendees?.find((a: any) => a.self === true);
102
141
 
103
142
  const calEvent: CalendarEvent = {
104
- id: `google-${evt.id}`,
143
+ id: accountEvent
144
+ ? createGoogleAccountEventId({
145
+ accountEmail: acctEmail,
146
+ googleEventId: evt.id,
147
+ })
148
+ : `google-${evt.id}`,
105
149
  title: evt.summary || "Untitled",
106
150
  titleIsGenerated: !evt.summary,
107
151
  description: evt.description || "",
@@ -185,7 +229,8 @@ export default defineAction({
185
229
  };
186
230
 
187
231
  return calEvent;
188
- } catch {
232
+ } catch (error) {
233
+ if (accountEvent) throw error;
189
234
  // Try next account
190
235
  continue;
191
236
  }
@@ -3,8 +3,10 @@ import { z } from "zod";
3
3
 
4
4
  import * as googleCalendar from "../server/lib/google-calendar.js";
5
5
  import {
6
+ googleEventResultId,
6
7
  normalizeWritableGoogleEventId,
7
8
  requireActionUserEmail,
9
+ resolveGoogleEventAccountEmail,
8
10
  resolveOwnedAccountEmail,
9
11
  } from "./event-action-helpers.js";
10
12
 
@@ -49,11 +51,11 @@ export default defineAction({
49
51
  );
50
52
  }
51
53
 
52
- const googleEventId = normalizeWritableGoogleEventId(args.id);
53
54
  const accountEmail = await resolveOwnedAccountEmail(
54
- args.accountEmail,
55
+ resolveGoogleEventAccountEmail(args.id, args.accountEmail),
55
56
  ownerEmail,
56
57
  );
58
+ const googleEventId = normalizeWritableGoogleEventId(args.id);
57
59
 
58
60
  await googleCalendar.rsvpEvent(
59
61
  googleEventId,
@@ -66,7 +68,7 @@ export default defineAction({
66
68
 
67
69
  return {
68
70
  success: true,
69
- id: `google-${googleEventId}`,
71
+ id: googleEventResultId(args.id, googleEventId, accountEmail),
70
72
  accountEmail,
71
73
  status: args.status,
72
74
  note: args.note?.trim() ?? args.note,
@@ -20,12 +20,14 @@ import {
20
20
  cliBoolean,
21
21
  googleColorIdInput,
22
22
  normalizeAttendees,
23
+ googleEventResultId,
23
24
  normalizeWritableGoogleEventId,
24
25
  normalizeRecurrence,
25
26
  reminderMethodInput,
26
27
  reminderMinutesInput,
27
28
  remindersInput,
28
29
  requireActionUserEmail,
30
+ resolveGoogleEventAccountEmail,
29
31
  resolveOwnedAccountEmail,
30
32
  validateEventTimeOrder,
31
33
  validateStatusEventTiming,
@@ -256,11 +258,11 @@ export default defineAction({
256
258
  );
257
259
  }
258
260
 
259
- const googleEventId = normalizeWritableGoogleEventId(args.id);
260
261
  const accountEmail = await resolveOwnedAccountEmail(
261
- args.accountEmail,
262
+ resolveGoogleEventAccountEmail(args.id, args.accountEmail),
262
263
  ownerEmail,
263
264
  );
265
+ const googleEventId = normalizeWritableGoogleEventId(args.id);
264
266
  const targetAccountEmail =
265
267
  args.targetAccountEmail !== undefined
266
268
  ? await resolveOwnedAccountEmail(args.targetAccountEmail, ownerEmail)
@@ -445,8 +447,8 @@ export default defineAction({
445
447
  );
446
448
  return {
447
449
  success: true,
448
- id: `google-${result.id}`,
449
- replacedId: `google-${googleEventId}`,
450
+ id: googleEventResultId(args.id, result.id, targetAccountEmail!),
451
+ replacedId: googleEventResultId(args.id, googleEventId, accountEmail),
450
452
  accountEmail: targetAccountEmail,
451
453
  updated: ["accountEmail"],
452
454
  htmlLink: result.htmlLink,
@@ -585,7 +587,7 @@ export default defineAction({
585
587
  if (updatedKeys.length === 0 && zoomAlreadyPresent) {
586
588
  return {
587
589
  success: true,
588
- id: `google-${googleEventId}`,
590
+ id: googleEventResultId(args.id, googleEventId, accountEmail),
589
591
  accountEmail,
590
592
  updated: [],
591
593
  meetingLink: zoomMeetingLink,
@@ -746,9 +748,15 @@ export default defineAction({
746
748
 
747
749
  return {
748
750
  success: true,
749
- id: `google-${returnedGoogleEventId}`,
751
+ id: googleEventResultId(args.id, returnedGoogleEventId, accountEmail),
750
752
  ...(returnedGoogleEventId !== googleEventId
751
- ? { replacedId: `google-${googleEventId}` }
753
+ ? {
754
+ replacedId: googleEventResultId(
755
+ args.id,
756
+ googleEventId,
757
+ accountEmail,
758
+ ),
759
+ }
752
760
  : {}),
753
761
  accountEmail,
754
762
  updated: updatedKeys,
@@ -13,11 +13,13 @@ import {
13
13
  BULK_EVENT_CONCURRENCY,
14
14
  MAX_MATCHED_EVENTS,
15
15
  cliBoolean,
16
+ googleEventResultId,
16
17
  isBookedOnAccount,
17
18
  mapWithConcurrency,
18
19
  normalizeWritableGoogleEventId,
19
20
  requireActionUserEmail,
20
21
  requireExplicitBound,
22
+ resolveBulkGoogleEventAccountEmail,
21
23
  validateEventTimeOrder,
22
24
  resolveOwnedAccountEmail,
23
25
  startsWithinRange,
@@ -188,22 +190,24 @@ export default defineAction({
188
190
 
189
191
  if (hasIds) {
190
192
  const accountEmail = await resolveOwnedAccountEmail(
191
- args.accountEmail,
193
+ resolveBulkGoogleEventAccountEmail(args.ids!, args.accountEmail),
192
194
  ownerEmail,
193
195
  );
194
196
  const requested = Array.from(
195
- new Set(args.ids!.map(normalizeWritableGoogleEventId)),
197
+ new Map(
198
+ args.ids!.map((id) => [normalizeWritableGoogleEventId(id), id]),
199
+ ).entries(),
196
200
  );
197
- for (const id of requested) {
201
+ for (const [id, displayId] of requested) {
198
202
  try {
199
203
  const event = await googleCalendar.getEvent(id, {
200
204
  ownerEmail,
201
205
  accountEmail,
202
206
  });
203
- events.push({ event, accountEmail });
207
+ events.push({ event: { ...event, id: displayId }, accountEmail });
204
208
  } catch (error) {
205
209
  skipped.push({
206
- id: `google-${id}`,
210
+ id: googleEventResultId(displayId, id, accountEmail),
207
211
  accountEmail,
208
212
  outcome: "failed",
209
213
  reason: isGoogleNotFoundError(error)
@@ -243,7 +247,13 @@ export default defineAction({
243
247
  );
244
248
  for (const event of matched) {
245
249
  const result: BulkEventResult = {
246
- id: event.googleEventId ? `google-${event.googleEventId}` : event.id,
250
+ id: event.googleEventId
251
+ ? googleEventResultId(
252
+ event.id,
253
+ event.googleEventId,
254
+ event.accountEmail ?? ownerEmail,
255
+ )
256
+ : event.id,
247
257
  title: event.title,
248
258
  start: event.start,
249
259
  end: event.end,
@@ -291,7 +301,7 @@ export default defineAction({
291
301
  }
292
302
  if (isBookedOnAccount(booked, event.googleEventId, accountEmail)) {
293
303
  skipped.push({
294
- id: `google-${event.googleEventId}`,
304
+ id: googleEventResultId(event.id, event.googleEventId, accountEmail),
295
305
  title: event.title,
296
306
  start: event.start,
297
307
  end: event.end,
@@ -303,7 +313,7 @@ export default defineAction({
303
313
  }
304
314
  if (args.shiftMinutes !== undefined && event.allDay) {
305
315
  skipped.push({
306
- id: `google-${event.googleEventId}`,
316
+ id: googleEventResultId(event.id, event.googleEventId, accountEmail),
307
317
  title: event.title,
308
318
  start: event.start,
309
319
  end: event.end,
@@ -321,7 +331,7 @@ export default defineAction({
321
331
  const range = effectiveRange(event, args);
322
332
  assertEffectiveRangeOrdered(event, range);
323
333
  return {
324
- id: `google-${event.googleEventId}`,
334
+ id: googleEventResultId(event.id, event.googleEventId!, accountEmail),
325
335
  title: event.title,
326
336
  start: range.start,
327
337
  end: range.end,
@@ -360,7 +370,11 @@ export default defineAction({
360
370
  },
361
371
  );
362
372
  return {
363
- id: `google-${event.googleEventId}`,
373
+ id: googleEventResultId(
374
+ event.id,
375
+ event.googleEventId!,
376
+ accountEmail,
377
+ ),
364
378
  title: event.title,
365
379
  start,
366
380
  end,
@@ -369,7 +383,11 @@ export default defineAction({
369
383
  };
370
384
  } catch (error) {
371
385
  return {
372
- id: `google-${event.googleEventId}`,
386
+ id: googleEventResultId(
387
+ event.id,
388
+ event.googleEventId!,
389
+ accountEmail,
390
+ ),
373
391
  title: event.title,
374
392
  start,
375
393
  end,
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-09-21
4
+ ---
5
+
6
+ Events stay editable when their calendar is connected through multiple Google accounts
@@ -22,6 +22,7 @@ import type {
22
22
  UpdateEventScope,
23
23
  } from "../../shared/api.js";
24
24
  import {
25
+ createGoogleAccountEventId,
25
26
  createGoogleCalendarCanonicalKey,
26
27
  createGoogleCalendarSourceKey,
27
28
  } from "../../shared/google-calendar-sources.js";
@@ -1087,6 +1088,26 @@ function compareCalendarSourcePaths(
1087
1088
  return a.accountEmail.localeCompare(b.accountEmail);
1088
1089
  }
1089
1090
 
1091
+ function compareCalendarEventSources(
1092
+ a: CalendarEvent,
1093
+ b: CalendarEvent,
1094
+ ): number {
1095
+ const writable =
1096
+ Number(b.calendarReadOnly === false) - Number(a.calendarReadOnly === false);
1097
+ if (writable !== 0) return writable;
1098
+
1099
+ const primary =
1100
+ Number(b.calendarPrimary === true) - Number(a.calendarPrimary === true);
1101
+ if (primary !== 0) return primary;
1102
+
1103
+ const access =
1104
+ (CALENDAR_ACCESS_RANK[b.calendarAccessRole ?? "freeBusyReader"] ?? -1) -
1105
+ (CALENDAR_ACCESS_RANK[a.calendarAccessRole ?? "freeBusyReader"] ?? -1);
1106
+ if (access !== 0) return access;
1107
+
1108
+ return (a.accountEmail ?? "").localeCompare(b.accountEmail ?? "");
1109
+ }
1110
+
1090
1111
  /**
1091
1112
  * Resolve a client-supplied canonical source identity against the user's live
1092
1113
  * CalendarList. Provider paths stay server-selected, so a stale or forged
@@ -1097,9 +1118,17 @@ export async function resolveGoogleCalendarSource(
1097
1118
  sourceKey: string,
1098
1119
  ): Promise<GoogleCalendarSource> {
1099
1120
  const discovered = await listGoogleCalendars(ownerEmail);
1100
- const source = discovered.calendars.find(
1101
- (candidate) => candidate.sourceKey === sourceKey,
1102
- );
1121
+ const source = discovered.calendars
1122
+ .flatMap((candidate) =>
1123
+ (candidate.sourcePaths ?? [candidate]).map((path) => ({
1124
+ ...candidate,
1125
+ ...path,
1126
+ readOnly:
1127
+ path.primary !== true ||
1128
+ (path.accessRole !== "owner" && path.accessRole !== "writer"),
1129
+ })),
1130
+ )
1131
+ .find((candidate) => candidate.sourceKey === sourceKey);
1103
1132
  if (!source) {
1104
1133
  throw new Error(
1105
1134
  "Google Calendar source is not connected or no longer available",
@@ -1331,6 +1360,8 @@ export async function listEvents(
1331
1360
  // "calendar is empty" and the user sees no error.
1332
1361
  const errors: Array<{ email: string; error: string }> = [...refreshErrors];
1333
1362
  if (clients.length === 0) return { events: [], errors };
1363
+ const hasMultipleOwnedAccounts =
1364
+ clients.length > 1 || (await getOwnedAccountEmails(forEmail)).length > 1;
1334
1365
 
1335
1366
  const requestedSourceKeys = Array.from(
1336
1367
  new Set((options.calendarSourceKeys ?? []).filter(Boolean)),
@@ -1339,9 +1370,27 @@ export async function listEvents(
1339
1370
  if (requestedSourceKeys.length > 0) {
1340
1371
  const discovered = await listGoogleCalendars(forEmail);
1341
1372
  errors.push(...discovered.errors);
1342
- const discoveredByKey = new Map(
1343
- discovered.calendars.map((source) => [source.sourceKey, source]),
1344
- );
1373
+ const discoveredByKey = new Map<
1374
+ string,
1375
+ {
1376
+ source: GoogleCalendarSource;
1377
+ paths: Array<
1378
+ Pick<
1379
+ GoogleCalendarSource,
1380
+ "sourceKey" | "accountEmail" | "accessRole" | "primary"
1381
+ >
1382
+ >;
1383
+ }
1384
+ >();
1385
+ for (const source of discovered.calendars) {
1386
+ const paths = source.sourcePaths ?? [source];
1387
+ discoveredByKey.set(source.sourceKey, { source, paths });
1388
+ for (const path of paths) {
1389
+ if (path.sourceKey !== source.sourceKey) {
1390
+ discoveredByKey.set(path.sourceKey, { source, paths: [path] });
1391
+ }
1392
+ }
1393
+ }
1345
1394
  const invalid = requestedSourceKeys.filter(
1346
1395
  (sourceKey) => !discoveredByKey.has(sourceKey),
1347
1396
  );
@@ -1351,21 +1400,14 @@ export async function listEvents(
1351
1400
  );
1352
1401
  }
1353
1402
  for (const sourceKey of requestedSourceKeys) {
1354
- const source = discoveredByKey.get(sourceKey)!;
1355
- const paths = source.sourcePaths ?? [
1356
- {
1357
- sourceKey: source.sourceKey,
1358
- accountEmail: source.accountEmail,
1359
- accessRole: source.accessRole,
1360
- primary: source.primary,
1361
- },
1362
- ];
1403
+ const { source, paths } = discoveredByKey.get(sourceKey)!;
1363
1404
  for (const path of paths) {
1364
1405
  const accountKey = path.accountEmail.trim().toLowerCase();
1365
1406
  selectedSourcesByAccount.set(accountKey, [
1366
1407
  ...(selectedSourcesByAccount.get(accountKey) ?? []),
1367
1408
  {
1368
1409
  ...source,
1410
+ sourceKey: path.sourceKey,
1369
1411
  accountEmail: path.accountEmail,
1370
1412
  accessRole: path.accessRole,
1371
1413
  primary: path.primary,
@@ -1457,7 +1499,12 @@ export async function listEvents(
1457
1499
  id:
1458
1500
  calendarSource && !calendarSource.primary
1459
1501
  ? `google-${calendarSource.sourceKey}-${event.id}`
1460
- : `google-${event.id}`,
1502
+ : hasMultipleOwnedAccounts
1503
+ ? createGoogleAccountEventId({
1504
+ accountEmail: email,
1505
+ googleEventId: event.id,
1506
+ })
1507
+ : `google-${event.id}`,
1461
1508
  title: event.summary || "Untitled",
1462
1509
  titleIsGenerated: !event.summary,
1463
1510
  description: event.description || "",
@@ -1538,8 +1585,13 @@ export async function listEvents(
1538
1585
  const key =
1539
1586
  event.canonicalKey && event.googleEventId
1540
1587
  ? `${event.canonicalKey}:${event.googleEventId}`
1541
- : event.id;
1542
- if (!dedupedEvents.has(key)) dedupedEvents.set(key, event);
1588
+ : event.googleEventId && event.accountEmail
1589
+ ? `google-account:${event.accountEmail.toLowerCase()}:${event.googleEventId}`
1590
+ : event.id;
1591
+ const existing = dedupedEvents.get(key);
1592
+ if (!existing || compareCalendarEventSources(event, existing) < 0) {
1593
+ dedupedEvents.set(key, event);
1594
+ }
1543
1595
  }
1544
1596
  return { events: Array.from(dedupedEvents.values()), errors };
1545
1597
  }