@mulmoclaude/core 5.1.0 → 5.4.0

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 (47) hide show
  1. package/README.md +17 -1
  2. package/assets/helps/error-recovery.md +78 -0
  3. package/assets/helps/google-calendar-collection.md +92 -12
  4. package/dist/collection/core/schemaZ.d.ts +15 -2
  5. package/dist/collection/core/viewChatPolicy.d.ts +15 -3
  6. package/dist/collection/firestore.cjs +2 -1
  7. package/dist/collection/firestore.cjs.map +1 -1
  8. package/dist/collection/firestore.js +3 -2
  9. package/dist/collection/firestore.js.map +1 -1
  10. package/dist/collection/index.cjs +15 -3
  11. package/dist/collection/index.cjs.map +1 -1
  12. package/dist/collection/index.js +15 -3
  13. package/dist/collection/index.js.map +1 -1
  14. package/dist/collection/registry/server/index.cjs +2 -2
  15. package/dist/collection/registry/server/index.js +2 -2
  16. package/dist/collection/server/firestoreDocs.d.ts +10 -0
  17. package/dist/collection/server/index.cjs +2 -2
  18. package/dist/collection/server/index.js +2 -2
  19. package/dist/collection-watchers/index.cjs +2 -2
  20. package/dist/collection-watchers/index.js +2 -2
  21. package/dist/{discovery-DCMW05Bp.cjs → discovery-B79IxEM6.cjs} +15 -5
  22. package/dist/discovery-B79IxEM6.cjs.map +1 -0
  23. package/dist/{discovery-CBjI1lx9.js → discovery-DIGau7M8.js} +15 -5
  24. package/dist/discovery-DIGau7M8.js.map +1 -0
  25. package/dist/feeds/server/index.cjs +2 -2
  26. package/dist/feeds/server/index.js +2 -2
  27. package/dist/google/calendar.d.ts +24 -0
  28. package/dist/google/collectionPush.d.ts +45 -2
  29. package/dist/google/deletePlan.d.ts +32 -0
  30. package/dist/google/eventDerived.d.ts +19 -0
  31. package/dist/google/index.cjs +179 -18
  32. package/dist/google/index.cjs.map +1 -1
  33. package/dist/google/index.d.ts +4 -2
  34. package/dist/google/index.js +174 -19
  35. package/dist/google/index.js.map +1 -1
  36. package/dist/remote-view/index.cjs +9 -7
  37. package/dist/remote-view/index.cjs.map +1 -1
  38. package/dist/remote-view/index.d.ts +3 -3
  39. package/dist/remote-view/index.js +9 -7
  40. package/dist/remote-view/index.js.map +1 -1
  41. package/dist/{server-rA9FSkl2.cjs → server-BUbU9G-J.cjs} +2 -2
  42. package/dist/{server-rA9FSkl2.cjs.map → server-BUbU9G-J.cjs.map} +1 -1
  43. package/dist/{server-BMZ_BcPH.js → server-DZ7Cq7s_.js} +2 -2
  44. package/dist/{server-BMZ_BcPH.js.map → server-DZ7Cq7s_.js.map} +1 -1
  45. package/package.json +1 -1
  46. package/dist/discovery-CBjI1lx9.js.map +0 -1
  47. package/dist/discovery-DCMW05Bp.cjs.map +0 -1
@@ -1,5 +1,7 @@
1
1
  export { configureGoogleHost, type GoogleLogger } from './host.js';
2
2
  export { isIsoDateTimeWithOffset } from './datetime.js';
3
+ export { deleteRefusalMessage, planDelete, type DeleteDecision, type DeleteRefusal } from './deletePlan.js';
4
+ export { conferenceVideoUri, selfResponseStatus } from './eventDerived.js';
3
5
  export { isCalendarDateOnly, resolvePartialSpanInput, resolveSpanInput, toEventTimeInput, eventTimeHint, EVENT_TIME_HINT, ALL_DAY_END_HINT, LONE_ALL_DAY_HINT, MIXED_SPAN_HINT, type PartialSpanResult, type PartialSpanTimes, type SpanResult, type SpanTimes, } from './eventSpanInput.js';
4
6
  export { googleConfigDir, googleSecretsDir, googleTokenPath, legacyGoogleTokenPath } from './paths.js';
5
7
  export { clientSecretPresence, findClientSecretPath, loadClientSecret, type ClientSecretPresence, type InstalledClientSecret } from './clientSecret.js';
@@ -8,13 +10,13 @@ export { brokerBaseUrl, brokerExchange, brokerRefresh, brokerStart, type BrokerS
8
10
  export { authorizeGoogle, commitLinkedTokens, getGoogleAccessToken, unlinkGoogle, waitForAuthCode, GOOGLE_AUTH_CANCELLED, GOOGLE_CALENDAR_SCOPE, GOOGLE_CALENDARLIST_SCOPE, GOOGLE_TASKS_SCOPE, GOOGLE_DRIVE_FILE_SCOPE, GOOGLE_SCOPES, type AuthorizeGoogleOptions, type RevokeFetch, } from './auth.js';
9
11
  export { createGoogleAuthFlow, googleAuthFlow, type GoogleAuthFlow, type GoogleAuthFlowStatus } from './authFlow.js';
10
12
  export { googleApiError, isGoogleApiError, GoogleApiError, DEFAULT_LIST_MAX_RESULTS, HTTP_FORBIDDEN, MAX_LIST_RESULTS } from './apiClient.js';
11
- export { buildEventPatch, calendarApiError, collectCalendarPages, createCalendarEvent, deleteCalendarEvent, getCalendarColors, canonicalCalendarId, listCalendarEvents, listCalendars, syncCalendarEvents, toCalendarMeta, toCalendarSummary, toEventSummary, updateCalendarEvent, getCalendarEvent, getCalendarMeta, resolveEventSpan, CANCELLED_EVENT_STATUS, HTTP_CONFLICT, HTTP_PRECONDITION_FAILED, type CalendarColorEntry, type CalendarColors, type CalendarEventInput, type CalendarEventSpan, type CalendarEventSummary, type CalendarEventTime, type CalendarListPage, type CalendarMeta, type CalendarSummary, type CalendarSyncResult, type DeleteCalendarEventInput, type FetchedCalendarEvent, type ListEventsInput, type SyncEventsInput, type UpdateCalendarEventInput, syncPageParams, } from './calendar.js';
13
+ export { buildEventPatch, calendarApiError, collectCalendarPages, createCalendarEvent, deleteCalendarEvent, getCalendarColors, canonicalCalendarId, listCalendarEvents, listCalendars, syncCalendarEvents, toCalendarMeta, toCalendarSummary, toEventSummary, updateCalendarEvent, getCalendarEvent, getCalendarMeta, resolveEventSpan, CANCELLED_EVENT_STATUS, EVENT_ABSENT_STATUSES, HTTP_CONFLICT, HTTP_PRECONDITION_FAILED, type CalendarColorEntry, type CalendarColors, type CalendarEventInput, type CalendarEventSpan, type CalendarEventSummary, type CalendarEventTime, type CalendarListPage, type CalendarMeta, type CalendarSummary, type CalendarSyncResult, type DeleteCalendarEventInput, type FetchedCalendarEvent, type ListEventsInput, type SyncEventsInput, type UpdateCalendarEventInput, syncPageParams, } from './calendar.js';
12
14
  export { calendarSyncStatePath, claimCalendarSyncIfDue, clearCalendarLastSyncedAt, clearCalendarSyncToken, loadCalendarLastSyncedAt, loadCalendarSyncToken, saveCalendarSyncToken, toolCalendarSyncKey, } from './calendarSyncStore.js';
13
15
  export { calendarSyncDueWindowMs, isCalendarSyncDue } from './calendarSyncDue.js';
14
16
  export { calendarBackfillPath, markCalendarBackfilled, needsCalendarBackfill } from './calendarBackfillState.js';
15
17
  export { stateLockPath, withCalendarStateLock, liveLockClock, type LockClock } from './calendarStateLock.js';
16
18
  export { calendarPushStatePath, clearCalendarShadow, loadCalendarShadow, mergeShadow, saveCalendarShadow, toShadowEvent, type ShadowEvent, } from './calendarPushState.js';
17
- export { isDeniedAccessRole, isUnpushed, locallyEditedIds, pushCalendarForCollection, pushCollectionNow, reportedAccessRole, unsentLocalEdits, type PushOutcomeKind, type CalendarCollectionPushResult, type CalendarPushDeps, type CalendarPushOutcome, type CalendarWriteTarget, } from './collectionPush.js';
19
+ export { isDeniedAccessRole, isUnpushed, locallyEditedIds, pushCalendarForCollection, pushCollectionNow, reportedAccessRole, sweepDeletes, unsentLocalEdits, type DeleteSweep, type DeleteSweepDeps, type PushOutcomeKind, type CalendarCollectionPushResult, type CalendarPushDeps, type CalendarPushOutcome, type CalendarWriteTarget, } from './collectionPush.js';
18
20
  export { toGoogleEventTime, zoneSuffixOf } from './pushDateTime.js';
19
21
  export { baselineRecord, bySourceField, conflictingFields, fieldText, isClientSettableEventId, locallyChangedFields, locallyDeletedIds, mayAdoptExisting, planRecord, pushableMap, PUSHABLE_SOURCE_FIELDS, type PushableSourceField, type RecordPlan, } from './pushPlan.js';
20
22
  export { toCollectionDateTime } from './collectionDateTime.js';
@@ -5,7 +5,7 @@ import { n as writeFileAtomic } from "../root-BcS4-Kge.js";
5
5
  import { fetchWithTimeout } from "../utils/fetch.js";
6
6
  import { d as parseIsoDate, f as parseIsoDateTime } from "../itemId-4P9S_ehz.js";
7
7
  import { t as mergeIntoExisting } from "../project-B0j_Vf_Q.js";
8
- import { _t as createForwardingLogger, d as storeFor, n as discoverCollections, rt as getWorkspaceRoot, vt as createHostSlot } from "../discovery-CBjI1lx9.js";
8
+ import { _t as createForwardingLogger, d as storeFor, n as discoverCollections, rt as getWorkspaceRoot, vt as createHostSlot } from "../discovery-DIGau7M8.js";
9
9
  import { t as mulmoConfigDir } from "../paths-CkJxNZ7T.js";
10
10
  import { promises } from "node:fs";
11
11
  import path, { join } from "node:path";
@@ -41,6 +41,74 @@ var isIsoDateTimeWithOffset = (value) => {
41
41
  return isRealCalendarDate(year, month, day) && timeInRange && offsetInRange;
42
42
  };
43
43
  //#endregion
44
+ //#region src/google/deletePlan.ts
45
+ var ATTENDEES_REASON = "it carries attendees — deleting an invited event withdraws it from their calendars too";
46
+ var ALREADY_GONE_REASON = "it is no longer in Google";
47
+ /** The message the push reports for a refusal, with the event named. */
48
+ var deleteRefusalMessage = (eventId, refusal) => `${eventId}: left in Google because ${refusal.kind === "has-attendees" ? ATTENDEES_REASON : ALREADY_GONE_REASON}`;
49
+ /** Decide one deletion against what Google currently holds.
50
+ *
51
+ * `null` means the event is already absent — a 404/410 on the read. Nothing to
52
+ * delete, and reported apart from a refusal so "we chose not to" never reads
53
+ * as "we could not".
54
+ *
55
+ * ANY attendee refuses, including the entry Google adds for the organiser. A
56
+ * rule that tried to exclude "only me" would have to work out which entry is
57
+ * the user from a payload that may not say, and being wrong there withdraws a
58
+ * real invitation. Solo events — the ones this feature exists for — carry no
59
+ * attendees at all, so the blunt rule costs them nothing. */
60
+ function planDelete(fetched) {
61
+ if (fetched === null) return {
62
+ ok: false,
63
+ kind: "already-gone"
64
+ };
65
+ return fetched.attendeeCount > 0 ? {
66
+ ok: false,
67
+ kind: "has-attendees"
68
+ } : {
69
+ ok: true,
70
+ etag: fetched.etag
71
+ };
72
+ }
73
+ //#endregion
74
+ //#region src/google/util.ts
75
+ var ONE_SECOND_MS = 1e3;
76
+ var ONE_MINUTE_MS = 6e4;
77
+ function isRecord(value) {
78
+ return typeof value === "object" && value !== null && !Array.isArray(value);
79
+ }
80
+ //#endregion
81
+ //#region src/google/eventDerived.ts
82
+ var stringAt = (record, key) => typeof record[key] === "string" ? record[key] : "";
83
+ var recordsIn = (value) => Array.isArray(value) ? value.filter(isRecord) : [];
84
+ /** The signed-in user's own `responseStatus` on this event, `""` when Google
85
+ * reported none.
86
+ *
87
+ * `""` is the common case, not an edge one: an event with no attendees has no
88
+ * entry to mark `self`, which is most of a personal calendar. So it means
89
+ * "Google said nothing", never "not going" — filter with `!= "declined"`, not
90
+ * with `== "accepted"`, or every solo event disappears too.
91
+ *
92
+ * Google's values: `needsAction`, `declined`, `tentative`, `accepted`. */
93
+ function selfResponseStatus(attendees) {
94
+ const self = recordsIn(attendees).find((attendee) => attendee.self === true);
95
+ return self === void 0 ? "" : stringAt(self, "responseStatus");
96
+ }
97
+ var VIDEO_ENTRY_POINT = "video";
98
+ /** The URL that joins this event's meeting, `""` when it has none.
99
+ *
100
+ * Only the `video` entry point. The others are a phone number and a dial-in
101
+ * PIN page, and a column named for joining that sometimes holds `tel:` would
102
+ * be worse than one that is empty — the caller can see empty and fall back.
103
+ *
104
+ * `hangoutLink` (#3229) already carries this for Google Meet; this is what
105
+ * reaches the events whose conference is Zoom or Teams. */
106
+ function conferenceVideoUri(conferenceData) {
107
+ if (!isRecord(conferenceData)) return "";
108
+ const video = recordsIn(conferenceData.entryPoints).find((entry) => entry.entryPointType === VIDEO_ENTRY_POINT);
109
+ return video === void 0 ? "" : stringAt(video, "uri");
110
+ }
111
+ //#endregion
44
112
  //#region src/google/eventSpanInput.ts
45
113
  var DATE_ONLY_RE$1 = /^\d{4}-\d{2}-\d{2}$/;
46
114
  /** Whether `value` is a bare calendar day — the all-day spelling of an event
@@ -156,13 +224,6 @@ function googleSecretsDir(home) {
156
224
  return join(home ?? homedir(), ".secrets");
157
225
  }
158
226
  //#endregion
159
- //#region src/google/util.ts
160
- var ONE_SECOND_MS = 1e3;
161
- var ONE_MINUTE_MS = 6e4;
162
- function isRecord(value) {
163
- return typeof value === "object" && value !== null && !Array.isArray(value);
164
- }
165
- //#endregion
166
227
  //#region src/google/clientSecret.ts
167
228
  var isNonEmptyString = (value) => typeof value === "string" && value !== "";
168
229
  var isInstalledClientSecret = (value) => {
@@ -693,6 +754,10 @@ var EVENT_SYNC_PAGE_SIZE = 2500;
693
754
  var MAX_EVENT_SYNC_PAGES = 200;
694
755
  var CALENDAR_META_PAGE_SIZE = 1;
695
756
  var HTTP_GONE = 410;
757
+ /** A GET or DELETE on an event that is not there. Exported because the push's
758
+ * delete sweep has to read "already gone" as success, not as a failure to
759
+ * retry — a retained baseline would announce the same phantom deletion on
760
+ * every run. */
696
761
  var EVENT_ABSENT_STATUSES = [404, HTTP_GONE];
697
762
  /** An `If-Match` write whose etag no longer matches — someone else changed the
698
763
  * event since it was read. */
@@ -710,6 +775,10 @@ var CANCELLED_EVENT_STATUS = "cancelled";
710
775
  * apart while they share a sync token silently loses events (#2184). */
711
776
  var canonicalCalendarId = (calendarId) => calendarId || DEFAULT_CALENDAR_ID;
712
777
  var eventsUrl = (calendarId) => `${CALENDAR_BASE_URL}/calendars/${encodeURIComponent(canonicalCalendarId(calendarId))}/events`;
778
+ /** People Google lists on the event, `0` when it carries none. Counts EVERY
779
+ * entry, including the one Google adds for the organiser: see `deletePlan.ts`
780
+ * for why the guard is not cleverer than that. */
781
+ var attendeeCount = (record) => Array.isArray(record.attendees) ? record.attendees.length : 0;
713
782
  var eventTime$1 = (value) => {
714
783
  if (!isRecord(value)) return "";
715
784
  if (typeof value.dateTime === "string") return value.dateTime;
@@ -733,7 +802,9 @@ var toEventSummary = (value) => {
733
802
  updated: stringField(record, "updated"),
734
803
  transparency: stringField(record, "transparency"),
735
804
  eventType: stringField(record, "eventType"),
736
- hangoutLink: stringField(record, "hangoutLink")
805
+ hangoutLink: stringField(record, "hangoutLink"),
806
+ selfResponseStatus: selfResponseStatus(record.attendees),
807
+ conferenceVideoUri: conferenceVideoUri(record.conferenceData)
737
808
  };
738
809
  };
739
810
  var toCalendarSummary = (value) => {
@@ -824,9 +895,11 @@ async function updateCalendarEvent(accessToken, input) {
824
895
  async function getCalendarEvent(accessToken, input) {
825
896
  try {
826
897
  const fetched = await googleRequest(CALENDAR_API_LABEL, accessToken, eventUrl(input.calendarId, input.eventId));
898
+ const record = asRecord(fetched);
827
899
  return {
828
900
  event: toEventSummary(fetched),
829
- etag: stringField(asRecord(fetched), "etag")
901
+ etag: stringField(record, "etag"),
902
+ attendeeCount: attendeeCount(record)
830
903
  };
831
904
  } catch (error) {
832
905
  if (isGoogleApiError(error) && EVENT_ABSENT_STATUSES.includes(error.status)) return null;
@@ -837,7 +910,10 @@ async function getCalendarEvent(accessToken, input) {
837
910
  * return; a second delete of the same id answers 410, which surfaces as a
838
911
  * GoogleApiError rather than being swallowed. */
839
912
  async function deleteCalendarEvent(accessToken, input) {
840
- await googleRequest(CALENDAR_API_LABEL, accessToken, eventUrl(input.calendarId, input.eventId), { method: "DELETE" });
913
+ await googleRequest(CALENDAR_API_LABEL, accessToken, eventUrl(input.calendarId, input.eventId), {
914
+ method: "DELETE",
915
+ ...input.ifMatch ? { extraHeaders: { "If-Match": input.ifMatch } } : {}
916
+ });
841
917
  }
842
918
  async function listCalendarEvents(accessToken, input = {}) {
843
919
  const params = new URLSearchParams({
@@ -1619,7 +1695,9 @@ var UNPUSHABLE_EVENT_FIELDS = {
1619
1695
  updated: "",
1620
1696
  transparency: "",
1621
1697
  eventType: "",
1622
- hangoutLink: ""
1698
+ hangoutLink: "",
1699
+ selfResponseStatus: "",
1700
+ conferenceVideoUri: ""
1623
1701
  };
1624
1702
  /** A baseline shaped as the event it came from, so the comparison can run
1625
1703
  * through the very projection that wrote the record. */
@@ -1740,7 +1818,9 @@ var liveDeps = {
1740
1818
  findCollection: findCalendarCollection,
1741
1819
  isLinked: async () => Boolean((await loadGoogleTokens())?.refresh_token),
1742
1820
  accessToken: getGoogleAccessToken,
1743
- calendarMeta: liveCalendarMeta
1821
+ calendarMeta: liveCalendarMeta,
1822
+ fetchEvent: getCalendarEvent,
1823
+ deleteEvent: deleteCalendarEvent
1744
1824
  };
1745
1825
  var localValue = (ctx, record, source) => {
1746
1826
  const field = ctx.bySource[source];
@@ -1950,7 +2030,7 @@ var UNPUSHED_KINDS = ["conflict", "error"];
1950
2030
  /** Whether this outcome leaves a local edit that only exists locally, so the
1951
2031
  * pull must not overwrite the record and the baseline must not advance. */
1952
2032
  var isUnpushed = (kind) => UNPUSHED_KINDS.includes(kind);
1953
- function tally(slug, attempts, localDeletes) {
2033
+ function tally(slug, attempts, deletes) {
1954
2034
  const outcomes = attempts.map((attempt) => attempt.outcome);
1955
2035
  const count = (kind) => outcomes.filter((outcome) => outcome.kind === kind).length;
1956
2036
  return {
@@ -1958,9 +2038,10 @@ function tally(slug, attempts, localDeletes) {
1958
2038
  created: count("created"),
1959
2039
  updated: count("updated"),
1960
2040
  conflicts: count("conflict"),
1961
- localDeletes,
1962
- skipped: outcomes.flatMap((outcome) => outcome.kind === "skipped" ? [outcome.message] : []),
1963
- errors: outcomes.flatMap((outcome) => outcome.kind === "error" ? [outcome.message] : []),
2041
+ localDeletes: deletes.seen,
2042
+ deletedInGoogle: deletes.deleted.length,
2043
+ skipped: [...outcomes.flatMap((outcome) => outcome.kind === "skipped" ? [outcome.message] : []), ...deletes.skipped],
2044
+ errors: [...outcomes.flatMap((outcome) => outcome.kind === "error" ? [outcome.message] : []), ...deletes.errors],
1964
2045
  unpushedIds: attempts.filter((attempt) => isUnpushed(attempt.outcome.kind)).map((attempt) => attempt.eventId)
1965
2046
  };
1966
2047
  }
@@ -1989,6 +2070,69 @@ async function unsentLocalEdits(collection, workspaceRoot) {
1989
2070
  const shadow = await loadCalendarShadow(schema.googleCalendar?.calendarId, workspaceRoot);
1990
2071
  return locallyEditedIds(await storeFor(collection, { workspaceRoot }).list(), shadow, pushableMap(schema.googleCalendar?.map ?? {}), schema.primaryKey, schema.fields);
1991
2072
  }
2073
+ var noDeletes = (seen) => ({
2074
+ seen,
2075
+ deleted: [],
2076
+ skipped: [],
2077
+ errors: []
2078
+ });
2079
+ /** A delete whose event changed in Google between the guard reading it and the
2080
+ * delete landing. Reported rather than retried: what changed may be the very
2081
+ * thing the guard refuses, and re-reading in a loop would race the same way. */
2082
+ var staleDeleteMessage = (eventId) => `${eventId}: left in Google because it changed there while this push was checking it — press Push again to re-check`;
2083
+ /** Delete ONE event and forget its baseline, or say why it was left alone.
2084
+ *
2085
+ * The baseline entry goes only after Google confirms, and it must go: the
2086
+ * baseline is what `locallyDeletedIds` reads, so a kept entry would re-report
2087
+ * the same deletion on every run forever. A refusal keeps its entry on
2088
+ * purpose — the event IS still standing in Google, and the report is the only
2089
+ * thing saying so.
2090
+ *
2091
+ * The delete carries the etag the guard read. Without it, an attendee added
2092
+ * between the two requests would be withdrawn by a decision taken before they
2093
+ * existed — the same race `updateCalendarEvent` already guards, and it costs
2094
+ * more here because a delete cannot be undone from this side. */
2095
+ async function propagateOneDelete(eventId, deps) {
2096
+ const decision = planDelete(await deps.fetchEvent(eventId));
2097
+ if (!decision.ok) {
2098
+ if (decision.kind === "already-gone") {
2099
+ await deps.forget(eventId);
2100
+ return {};
2101
+ }
2102
+ return { skipped: [deleteRefusalMessage(eventId, decision)] };
2103
+ }
2104
+ try {
2105
+ await deps.deleteEvent(eventId, decision.etag);
2106
+ } catch (error) {
2107
+ if (!isGoogleApiError(error)) throw error;
2108
+ if (EVENT_ABSENT_STATUSES.includes(error.status)) {
2109
+ await deps.forget(eventId);
2110
+ return {};
2111
+ }
2112
+ if (error.status !== 412) throw error;
2113
+ return { skipped: [staleDeleteMessage(eventId)] };
2114
+ }
2115
+ await deps.forget(eventId);
2116
+ log.info("google", "calendar event deleted by push", { id: eventId });
2117
+ return { deleted: [eventId] };
2118
+ }
2119
+ /** Carry every local deletion, when the collection opted in.
2120
+ *
2121
+ * Sequential rather than concurrent: each one writes the shared baseline file,
2122
+ * and an interrupted run has to leave a state the next run resumes from rather
2123
+ * than a partially applied batch. */
2124
+ async function sweepDeletes(eventIds, propagate, deps) {
2125
+ if (!propagate || eventIds.length === 0) return noDeletes(eventIds.length);
2126
+ const sweep = noDeletes(eventIds.length);
2127
+ for (const eventId of eventIds) try {
2128
+ const one = await propagateOneDelete(eventId, deps);
2129
+ sweep.deleted.push(...one.deleted ?? []);
2130
+ sweep.skipped.push(...one.skipped ?? []);
2131
+ } catch (error) {
2132
+ sweep.errors.push(`${eventId}: could not be deleted in Google — ${errorMessage(error)}`);
2133
+ }
2134
+ return sweep;
2135
+ }
1992
2136
  /** Push one collection WITHOUT taking the calendar lock.
1993
2137
  *
1994
2138
  * The caller must already hold it. The scheduled push→pull cycle
@@ -2027,7 +2171,18 @@ async function pushCollectionNow(collection, workspaceRoot, deps = liveDeps) {
2027
2171
  });
2028
2172
  await saveCalendarShadow(calendarId, pushedShadow([outcome]), workspaceRoot);
2029
2173
  }
2030
- const result = tally(slug, attempts, locallyDeletedIds(shadow, records.map((record) => fieldText(record[schema.primaryKey]))).length);
2174
+ const result = tally(slug, attempts, await sweepDeletes(locallyDeletedIds(shadow, records.map((record) => fieldText(record[schema.primaryKey]))), schema.googleCalendar?.propagateDeletes === true, {
2175
+ fetchEvent: (eventId) => (deps.fetchEvent ?? getCalendarEvent)(accessToken, {
2176
+ calendarId,
2177
+ eventId
2178
+ }),
2179
+ deleteEvent: (eventId, ifMatch) => (deps.deleteEvent ?? deleteCalendarEvent)(accessToken, {
2180
+ calendarId,
2181
+ eventId,
2182
+ ifMatch
2183
+ }),
2184
+ forget: (eventId) => saveCalendarShadow(calendarId, { [eventId]: null }, workspaceRoot)
2185
+ }));
2031
2186
  if (result.errors.length > 0) log.warn("google", "calendar push finished with errors", {
2032
2187
  slug,
2033
2188
  errors: result.errors.length
@@ -2985,6 +3140,6 @@ async function deleteDriveFile(accessToken, input) {
2985
3140
  await googleRequest(DRIVE_API_LABEL, accessToken, `${DRIVE_FILES_URL}/${encodeURIComponent(input.fileId)}`, { method: "DELETE" });
2986
3141
  }
2987
3142
  //#endregion
2988
- export { ALL_DAY_END_HINT, CANCELLED_EVENT_STATUS, DEFAULT_LIST_MAX_RESULTS, EVENT_TIME_HINT, GOOGLE_AUTH_CANCELLED, GOOGLE_CALENDARLIST_SCOPE, GOOGLE_CALENDAR_SCOPE, GOOGLE_CALENDAR_SYNC_TASK_ID, GOOGLE_DRIVE_FILE_SCOPE, GOOGLE_SCOPES, GOOGLE_TASKS_SCOPE, GoogleApiError, HTTP_CONFLICT, HTTP_FORBIDDEN, HTTP_PRECONDITION_FAILED, LONE_ALL_DAY_HINT, MAX_LIST_RESULTS, MIXED_SPAN_HINT, PARTIAL_CALENDAR_WINDOW, PROTECTION_UNKNOWN, PUSHABLE_SOURCE_FIELDS, allUnpushed, anySyncedCollectionSurvives, applyPlanFor, assertSafeMimeType, authorizeGoogle, baselineRecord, brokerBaseUrl, brokerExchange, brokerRefresh, brokerStart, buildEventPatch, buildMultipartBody, buildTaskPatch, bySourceField, calendarApiError, calendarBackfillPath, calendarPushStatePath, calendarSyncDueWindowMs, calendarSyncStatePath, canonicalCalendarId, canonicalTaskListId, claimCalendarSyncIfDue, classifyDelete, classifyWrite, clearCalendarLastSyncedAt, clearCalendarShadow, clearCalendarSyncToken, clientSecretPresence, collectCalendarPages, collectionsNeedingBackfill, commitLinkedTokens, completeTask, configureGoogleHost, conflictingFields, createCalendarEvent, createDriveFile, createGoogleAuthFlow, createTask, deleteCalendarEvent, deleteDriveFile, deleteGoogleTokens, deleteTask, eventTimeHint, fieldText, findClientSecretPath, getCalendarColors, getCalendarEvent, getCalendarMeta, getGoogleAccessToken, googleApiError, googleAuthFlow, googleCalendarSyncTaskDef, googleConfigDir, googleSecretsDir, googleTokenPath, groupByCalendar, groupsNeedingBackfill, heldBack, isCalendarDateOnly, isCalendarSyncDue, isClientSettableEventId, isDeniedAccessRole, isGoogleApiError, isIsoDateTimeWithOffset, isTextMimeType, isUnpushed, legacyGoogleTokenPath, listCalendarEvents, listCalendars, listDriveFiles, listTaskLists, listTasks, liveLockClock, loadCalendarLastSyncedAt, loadCalendarShadow, loadCalendarSyncToken, loadClientSecret, loadGoogleTokens, locallyChangedFields, locallyDeletedIds, locallyEditedIds, markCalendarBackfilled, mayAdoptExisting, mergeGoogleTokens, mergeIntoExisting, mergeShadow, needsCalendarBackfill, orphanedCalendarId, pickBoundary, planRecord, pullProtectionFor, pullableEvents, pushAndProtect, pushCalendarForCollection, pushCollectionNow, pushableMap, readDriveFile, releaseOrphanedCalendarToken, reportedAccessRole, resolveEventSpan, resolvePartialSpanInput, resolveSpanInput, resumableToken, saveCalendarShadow, saveCalendarSyncToken, saveGoogleTokens, shadowUpdates, stateLockPath, syncCalendarEvents, syncCalendarForCollection, syncCalendarGroup, syncDueCalendarCollections, syncNewCalendarCollections, syncPageParams, toCalendarMeta, toCalendarSummary, toCollectionDateTime, toCollectionRecord, toDriveFileSummary, toEventSummary, toEventTimeInput, toGoogleEventTime, toShadowEvent, toTaskListSummary, toTaskSummary, toolCalendarSyncKey, uncompleteTask, unlinkGoogle, unpushedFor, unsentEditGuard, unsentLocalEdits, updateCalendarEvent, updateTask, waitForAuthCode, windowAdvance, withCalendarLock, withCalendarStateLock, withKeyedLock, withPartialWindowError, zoneSuffixOf };
3143
+ export { ALL_DAY_END_HINT, CANCELLED_EVENT_STATUS, DEFAULT_LIST_MAX_RESULTS, EVENT_ABSENT_STATUSES, EVENT_TIME_HINT, GOOGLE_AUTH_CANCELLED, GOOGLE_CALENDARLIST_SCOPE, GOOGLE_CALENDAR_SCOPE, GOOGLE_CALENDAR_SYNC_TASK_ID, GOOGLE_DRIVE_FILE_SCOPE, GOOGLE_SCOPES, GOOGLE_TASKS_SCOPE, GoogleApiError, HTTP_CONFLICT, HTTP_FORBIDDEN, HTTP_PRECONDITION_FAILED, LONE_ALL_DAY_HINT, MAX_LIST_RESULTS, MIXED_SPAN_HINT, PARTIAL_CALENDAR_WINDOW, PROTECTION_UNKNOWN, PUSHABLE_SOURCE_FIELDS, allUnpushed, anySyncedCollectionSurvives, applyPlanFor, assertSafeMimeType, authorizeGoogle, baselineRecord, brokerBaseUrl, brokerExchange, brokerRefresh, brokerStart, buildEventPatch, buildMultipartBody, buildTaskPatch, bySourceField, calendarApiError, calendarBackfillPath, calendarPushStatePath, calendarSyncDueWindowMs, calendarSyncStatePath, canonicalCalendarId, canonicalTaskListId, claimCalendarSyncIfDue, classifyDelete, classifyWrite, clearCalendarLastSyncedAt, clearCalendarShadow, clearCalendarSyncToken, clientSecretPresence, collectCalendarPages, collectionsNeedingBackfill, commitLinkedTokens, completeTask, conferenceVideoUri, configureGoogleHost, conflictingFields, createCalendarEvent, createDriveFile, createGoogleAuthFlow, createTask, deleteCalendarEvent, deleteDriveFile, deleteGoogleTokens, deleteRefusalMessage, deleteTask, eventTimeHint, fieldText, findClientSecretPath, getCalendarColors, getCalendarEvent, getCalendarMeta, getGoogleAccessToken, googleApiError, googleAuthFlow, googleCalendarSyncTaskDef, googleConfigDir, googleSecretsDir, googleTokenPath, groupByCalendar, groupsNeedingBackfill, heldBack, isCalendarDateOnly, isCalendarSyncDue, isClientSettableEventId, isDeniedAccessRole, isGoogleApiError, isIsoDateTimeWithOffset, isTextMimeType, isUnpushed, legacyGoogleTokenPath, listCalendarEvents, listCalendars, listDriveFiles, listTaskLists, listTasks, liveLockClock, loadCalendarLastSyncedAt, loadCalendarShadow, loadCalendarSyncToken, loadClientSecret, loadGoogleTokens, locallyChangedFields, locallyDeletedIds, locallyEditedIds, markCalendarBackfilled, mayAdoptExisting, mergeGoogleTokens, mergeIntoExisting, mergeShadow, needsCalendarBackfill, orphanedCalendarId, pickBoundary, planDelete, planRecord, pullProtectionFor, pullableEvents, pushAndProtect, pushCalendarForCollection, pushCollectionNow, pushableMap, readDriveFile, releaseOrphanedCalendarToken, reportedAccessRole, resolveEventSpan, resolvePartialSpanInput, resolveSpanInput, resumableToken, saveCalendarShadow, saveCalendarSyncToken, saveGoogleTokens, selfResponseStatus, shadowUpdates, stateLockPath, sweepDeletes, syncCalendarEvents, syncCalendarForCollection, syncCalendarGroup, syncDueCalendarCollections, syncNewCalendarCollections, syncPageParams, toCalendarMeta, toCalendarSummary, toCollectionDateTime, toCollectionRecord, toDriveFileSummary, toEventSummary, toEventTimeInput, toGoogleEventTime, toShadowEvent, toTaskListSummary, toTaskSummary, toolCalendarSyncKey, uncompleteTask, unlinkGoogle, unpushedFor, unsentEditGuard, unsentLocalEdits, updateCalendarEvent, updateTask, waitForAuthCode, windowAdvance, withCalendarLock, withCalendarStateLock, withKeyedLock, withPartialWindowError, zoneSuffixOf };
2989
3144
 
2990
3145
  //# sourceMappingURL=index.js.map