@mulmoclaude/core 1.2.1 → 1.3.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.
@@ -902,13 +902,47 @@ async function syncCalendarGroup(calendarId, collections, workspaceRoot) {
902
902
  unwritable
903
903
  });
904
904
  const failed = results.flatMap((entry) => entry.errors);
905
- if (result.nextSyncToken && failed.length === 0) await saveCalendarSyncToken(calendarId, result.nextSyncToken, workspaceRoot);
906
- else if (failed.length > 0) log.warn("google", "holding back calendar sync token after failed writes", {
907
- calendarId,
908
- failed: failed.length
909
- });
905
+ if (failed.length > 0) {
906
+ log.warn("google", "holding back calendar sync token after failed writes", {
907
+ calendarId,
908
+ failed: failed.length
909
+ });
910
+ return results;
911
+ }
912
+ if (result.nextSyncToken) await advanceToken(calendarId, result.nextSyncToken, collections, workspaceRoot);
910
913
  return results;
911
914
  }
915
+ /** Save the window's token unless every collection that consumed it was deleted
916
+ * while the sync was in flight.
917
+ *
918
+ * The sync opens with a `discoverCollections()` snapshot, so a delete landing
919
+ * mid-run has already cleared this calendar's token by the time we get here
920
+ * (`releaseOrphanedCalendarToken`). Saving anyway would resurrect exactly the
921
+ * orphan the delete removed, and the next collection on this calendar would
922
+ * resume from a token describing records it never received (#2428).
923
+ *
924
+ * One survivor is enough: it still needs the incremental position. */
925
+ async function advanceToken(calendarId, nextSyncToken, collections, workspaceRoot) {
926
+ if (!await anySyncedCollectionSurvives(collections)) {
927
+ log.info("google", "not advancing the sync token — every collection on this calendar was deleted mid-sync", { calendarId });
928
+ return;
929
+ }
930
+ await saveCalendarSyncToken(calendarId, nextSyncToken, workspaceRoot);
931
+ }
932
+ async function pathExists(absPath) {
933
+ try {
934
+ await stat(absPath);
935
+ return true;
936
+ } catch {
937
+ return false;
938
+ }
939
+ }
940
+ /** Liveness of the collections a sync just wrote to, checked against the skill
941
+ * dir `deleteCollection` removes. `exists` is injected so the rule is testable
942
+ * without a filesystem. An empty group has no survivor by definition. */
943
+ async function anySyncedCollectionSurvives(collections, exists = pathExists) {
944
+ return (await Promise.all(collections.map((collection) => exists(collection.skillDir)))).some(Boolean);
945
+ }
912
946
  async function applyEventsToCollection(collection, events, workspaceRoot) {
913
947
  const outcomes = [];
914
948
  for (const event of events) outcomes.push(await applyEvent(collection, event, workspaceRoot));
@@ -941,6 +975,41 @@ function groupByCalendar(collections) {
941
975
  }
942
976
  return groups;
943
977
  }
978
+ /** The canonical calendar whose sync token nothing needs any more, or null.
979
+ *
980
+ * Sync tokens are keyed by calendar, NOT by collection, so a deleted
981
+ * collection's token outlives it. Recreating a collection on the same calendar
982
+ * then resumes from that token and receives only the delta — the new
983
+ * collection never gets the history (#2428).
984
+ *
985
+ * Returns null while ANY remaining collection still reads that calendar:
986
+ * clearing a live calendar's token costs a full re-walk on the next sync. The
987
+ * comparison is on the CANONICAL id for the same reason `groupByCalendar` is —
988
+ * an omitted `calendarId` and an explicit `"primary"` are one calendar. */
989
+ function orphanedCalendarId(deleted, remaining) {
990
+ if (!deleted.googleCalendar) return null;
991
+ const key = canonicalCalendarId(deleted.googleCalendar.calendarId);
992
+ return remaining.some((other) => other.googleCalendar && canonicalCalendarId(other.googleCalendar.calendarId) === key) ? null : key;
993
+ }
994
+ /** Drop the sync token of a just-deleted collection's calendar, unless another
995
+ * collection still reads it. Call AFTER the delete lands — the check reads the
996
+ * collections that survive it.
997
+ *
998
+ * Returns the cleared calendar id, or null when nothing was cleared. Never
999
+ * throws: a failed cleanup must not fail the delete it follows. */
1000
+ async function releaseOrphanedCalendarToken(deleted, workspaceRoot) {
1001
+ try {
1002
+ if (!deleted.googleCalendar) return null;
1003
+ const orphaned = orphanedCalendarId(deleted, (await discoverCollections({ workspaceRoot })).map((collection) => collection.schema));
1004
+ if (orphaned === null) return null;
1005
+ await clearCalendarSyncToken(orphaned, workspaceRoot);
1006
+ log.info("google", "cleared the sync token of a calendar no collection reads any more", { calendarId: orphaned });
1007
+ return orphaned;
1008
+ } catch (error) {
1009
+ log.warn("google", "could not release the deleted collection's calendar sync token", { error: String(error) });
1010
+ return null;
1011
+ }
1012
+ }
944
1013
  /** Sync every collection that declares `googleCalendar`. Failures are isolated
945
1014
  * per calendar — one unreachable calendar (or a revoked grant) must not stop
946
1015
  * the others. */
@@ -1133,6 +1202,6 @@ async function deleteDriveFile(accessToken, input) {
1133
1202
  await googleRequest(DRIVE_API_LABEL, accessToken, `${DRIVE_FILES_URL}/${encodeURIComponent(input.fileId)}`, { method: "DELETE" });
1134
1203
  }
1135
1204
  //#endregion
1136
- export { DEFAULT_LIST_MAX_RESULTS, GOOGLE_AUTH_CANCELLED, GOOGLE_CALENDARLIST_SCOPE, GOOGLE_CALENDAR_SCOPE, GOOGLE_CALENDAR_SYNC_TASK_ID, GOOGLE_DRIVE_FILE_SCOPE, GOOGLE_SCOPES, GOOGLE_TASKS_SCOPE, GoogleApiError, MAX_LIST_RESULTS, assertSafeMimeType, authorizeGoogle, brokerBaseUrl, brokerExchange, brokerRefresh, brokerStart, buildMultipartBody, calendarApiError, calendarSyncStatePath, canonicalCalendarId, classifyDelete, classifyWrite, clearCalendarSyncToken, clientSecretPresence, collectCalendarPages, commitLinkedTokens, completeTask, configureGoogleHost, createCalendarEvent, createDriveFile, createGoogleAuthFlow, createTask, deleteDriveFile, deleteGoogleTokens, deleteTask, findClientSecretPath, getCalendarColors, getGoogleAccessToken, googleApiError, googleAuthFlow, googleCalendarSyncTaskDef, googleConfigDir, googleSecretsDir, googleTokenPath, groupByCalendar, isGoogleApiError, isIsoDateTimeWithOffset, isTextMimeType, legacyGoogleTokenPath, listCalendarEvents, listCalendars, listDriveFiles, listTaskLists, listTasks, loadCalendarSyncToken, loadClientSecret, loadGoogleTokens, mergeGoogleTokens, pickBoundary, readDriveFile, saveCalendarSyncToken, saveGoogleTokens, syncCalendarEvents, syncCalendarGroup, syncDueCalendarCollections, toCalendarSummary, toCollectionDateTime, toCollectionRecord, toDriveFileSummary, toEventSummary, toTaskListSummary, toTaskSummary, unlinkGoogle, waitForAuthCode };
1205
+ export { DEFAULT_LIST_MAX_RESULTS, GOOGLE_AUTH_CANCELLED, GOOGLE_CALENDARLIST_SCOPE, GOOGLE_CALENDAR_SCOPE, GOOGLE_CALENDAR_SYNC_TASK_ID, GOOGLE_DRIVE_FILE_SCOPE, GOOGLE_SCOPES, GOOGLE_TASKS_SCOPE, GoogleApiError, MAX_LIST_RESULTS, anySyncedCollectionSurvives, assertSafeMimeType, authorizeGoogle, brokerBaseUrl, brokerExchange, brokerRefresh, brokerStart, buildMultipartBody, calendarApiError, calendarSyncStatePath, canonicalCalendarId, classifyDelete, classifyWrite, clearCalendarSyncToken, clientSecretPresence, collectCalendarPages, commitLinkedTokens, completeTask, configureGoogleHost, createCalendarEvent, createDriveFile, createGoogleAuthFlow, createTask, deleteDriveFile, deleteGoogleTokens, deleteTask, findClientSecretPath, getCalendarColors, getGoogleAccessToken, googleApiError, googleAuthFlow, googleCalendarSyncTaskDef, googleConfigDir, googleSecretsDir, googleTokenPath, groupByCalendar, isGoogleApiError, isIsoDateTimeWithOffset, isTextMimeType, legacyGoogleTokenPath, listCalendarEvents, listCalendars, listDriveFiles, listTaskLists, listTasks, loadCalendarSyncToken, loadClientSecret, loadGoogleTokens, mergeGoogleTokens, orphanedCalendarId, pickBoundary, readDriveFile, releaseOrphanedCalendarToken, saveCalendarSyncToken, saveGoogleTokens, syncCalendarEvents, syncCalendarGroup, syncDueCalendarCollections, toCalendarSummary, toCollectionDateTime, toCollectionRecord, toDriveFileSummary, toEventSummary, toTaskListSummary, toTaskSummary, unlinkGoogle, waitForAuthCode };
1137
1206
 
1138
1207
  //# sourceMappingURL=index.js.map