@mulmoclaude/core 3.2.0 → 3.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 (34) hide show
  1. package/assets/helps/error-recovery.md +71 -0
  2. package/assets/helps/google-calendar-collection.md +10 -0
  3. package/dist/collection/registry/server/index.cjs +3 -3
  4. package/dist/collection/registry/server/index.cjs.map +1 -1
  5. package/dist/collection/registry/server/index.js +3 -3
  6. package/dist/collection/registry/server/index.js.map +1 -1
  7. package/dist/collection/server/discovery.d.ts +10 -2
  8. package/dist/collection/server/host.d.ts +27 -3
  9. package/dist/collection/server/index.cjs +2 -2
  10. package/dist/collection/server/index.js +2 -2
  11. package/dist/collection-watchers/index.cjs +2 -2
  12. package/dist/collection-watchers/index.js +2 -2
  13. package/dist/{discovery-CDOjfctD.cjs → discovery-CrA_EqUJ.cjs} +19 -7
  14. package/dist/discovery-CrA_EqUJ.cjs.map +1 -0
  15. package/dist/{discovery-CC7kuwHf.js → discovery-D7LCN8gJ.js} +19 -7
  16. package/dist/discovery-D7LCN8gJ.js.map +1 -0
  17. package/dist/feeds/server/index.cjs +2 -2
  18. package/dist/feeds/server/index.js +2 -2
  19. package/dist/google/calendar.d.ts +15 -0
  20. package/dist/google/calendarBackfillState.d.ts +19 -0
  21. package/dist/google/calendarSyncStore.d.ts +15 -0
  22. package/dist/google/collectionSync.d.ts +78 -11
  23. package/dist/google/index.cjs +207 -35
  24. package/dist/google/index.cjs.map +1 -1
  25. package/dist/google/index.d.ts +4 -3
  26. package/dist/google/index.js +197 -35
  27. package/dist/google/index.js.map +1 -1
  28. package/dist/{server-WU35UYlF.cjs → server-CorfPQVQ.cjs} +2 -2
  29. package/dist/{server-WU35UYlF.cjs.map → server-CorfPQVQ.cjs.map} +1 -1
  30. package/dist/{server-AA2kfDfk.js → server-D6QY853r.js} +2 -2
  31. package/dist/{server-AA2kfDfk.js.map → server-D6QY853r.js.map} +1 -1
  32. package/package.json +1 -1
  33. package/dist/discovery-CC7kuwHf.js.map +0 -1
  34. package/dist/discovery-CDOjfctD.cjs.map +0 -1
@@ -7,9 +7,10 @@ export { brokerBaseUrl, brokerExchange, brokerRefresh, brokerStart, type BrokerS
7
7
  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';
8
8
  export { createGoogleAuthFlow, googleAuthFlow, type GoogleAuthFlow, type GoogleAuthFlowStatus } from './authFlow.js';
9
9
  export { googleApiError, isGoogleApiError, GoogleApiError, DEFAULT_LIST_MAX_RESULTS, HTTP_FORBIDDEN, MAX_LIST_RESULTS } from './apiClient.js';
10
- 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, } from './calendar.js';
11
- export { calendarSyncStatePath, claimCalendarSyncIfDue, clearCalendarLastSyncedAt, clearCalendarSyncToken, loadCalendarLastSyncedAt, loadCalendarSyncToken, saveCalendarSyncToken, } from './calendarSyncStore.js';
10
+ 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';
11
+ export { calendarSyncStatePath, claimCalendarSyncIfDue, clearCalendarLastSyncedAt, clearCalendarSyncToken, loadCalendarLastSyncedAt, loadCalendarSyncToken, saveCalendarSyncToken, toolCalendarSyncKey, } from './calendarSyncStore.js';
12
12
  export { calendarSyncDueWindowMs, isCalendarSyncDue } from './calendarSyncDue.js';
13
+ export { calendarBackfillPath, markCalendarBackfilled, needsCalendarBackfill } from './calendarBackfillState.js';
13
14
  export { stateLockPath, withCalendarStateLock, liveLockClock, type LockClock } from './calendarStateLock.js';
14
15
  export { calendarPushStatePath, clearCalendarShadow, loadCalendarShadow, mergeShadow, saveCalendarShadow, toShadowEvent, type ShadowEvent, } from './calendarPushState.js';
15
16
  export { isDeniedAccessRole, isUnpushed, locallyEditedIds, pushCalendarForCollection, pushCollectionNow, reportedAccessRole, unsentLocalEdits, type PushOutcomeKind, type CalendarCollectionPushResult, type CalendarPushDeps, type CalendarPushOutcome, type CalendarWriteTarget, } from './collectionPush.js';
@@ -19,6 +20,6 @@ export { toCollectionDateTime } from './collectionDateTime.js';
19
20
  export { withCalendarLock, withKeyedLock } from './calendarLock.js';
20
21
  export { toCollectionRecord, type GoogleCalendarSourceField } from './collectionProjection.js';
21
22
  export { mergeIntoExisting } from '../collection/core/project.js';
22
- export { googleCalendarSyncTaskDef, classifyDelete, classifyWrite, anySyncedCollectionSurvives, applyPlanFor, groupByCalendar, allUnpushed, unsentEditGuard, heldBack, orphanedCalendarId, pullableEvents, pullProtectionFor, pushAndProtect, releaseOrphanedCalendarToken, unpushedFor, PROTECTION_UNKNOWN, type ClaimGuard, type PullProtectionDeps, type UnpushedBySlug, syncCalendarForCollection, syncCalendarGroup, syncDueCalendarCollections, syncNewCalendarCollections, shadowUpdates, unsyncedGroups, GOOGLE_CALENDAR_SYNC_TASK_ID, type CalendarCollectionSyncResult, type CalendarDeclaring, type ManualCalendarSyncDeps, type ManualCalendarSyncOutcome, } from './collectionSync.js';
23
+ export { googleCalendarSyncTaskDef, classifyDelete, classifyWrite, anySyncedCollectionSurvives, applyPlanFor, groupByCalendar, allUnpushed, unsentEditGuard, heldBack, orphanedCalendarId, pullableEvents, pullProtectionFor, pushAndProtect, releaseOrphanedCalendarToken, unpushedFor, PROTECTION_UNKNOWN, type ClaimGuard, type PullProtectionDeps, type UnpushedBySlug, syncCalendarForCollection, syncCalendarGroup, syncDueCalendarCollections, syncNewCalendarCollections, shadowUpdates, collectionsNeedingBackfill, resumableToken, windowAdvance, type WindowAdvance, groupsNeedingBackfill, withPartialWindowError, PARTIAL_CALENDAR_WINDOW, GOOGLE_CALENDAR_SYNC_TASK_ID, type CalendarCollectionSyncResult, type CalendarDeclaring, type ManualCalendarSyncDeps, type ManualCalendarSyncOutcome, } from './collectionSync.js';
23
24
  export { buildTaskPatch, canonicalTaskListId, completeTask, createTask, deleteTask, listTaskLists, listTasks, toTaskListSummary, toTaskSummary, uncompleteTask, updateTask, type CompleteTaskInput, type CreateTaskInput, type ListTasksInput, type TaskListSummary, type TaskSummary, type UpdateTaskInput, } from './tasks.js';
24
25
  export { assertSafeMimeType, buildMultipartBody, createDriveFile, deleteDriveFile, isTextMimeType, listDriveFiles, pickBoundary, readDriveFile, toDriveFileSummary, type CreateDriveFileInput, type DriveFileSummary, type ListDriveFilesInput, type ReadDriveFileInput, } from './driveFile.js';
@@ -4,7 +4,7 @@ import { n as writeFileAtomic } from "../root-BMroU_mB.js";
4
4
  import { fetchWithTimeout } from "../utils/fetch.js";
5
5
  import { l as parseIsoDate, u as parseIsoDateTime } from "../calendarGrid-Csy2rpjp.js";
6
6
  import { t as mergeIntoExisting } from "../project-B0j_Vf_Q.js";
7
- import { Y as getWorkspaceRoot, at as createHostSlot, d as storeFor, it as createForwardingLogger, n as discoverCollections } from "../discovery-CC7kuwHf.js";
7
+ import { Y as getWorkspaceRoot, at as createHostSlot, d as storeFor, it as createForwardingLogger, n as discoverCollections } from "../discovery-D7LCN8gJ.js";
8
8
  import { t as mulmoConfigDir } from "../paths-CkJxNZ7T.js";
9
9
  import { promises } from "node:fs";
10
10
  import path, { join } from "node:path";
@@ -762,33 +762,42 @@ async function fetchSyncPage(accessToken, calendarId, params) {
762
762
  *
763
763
  * `nextSyncToken` is only present on the LAST page, so every page must be
764
764
  * walked before the token is worth storing. */
765
+ /** The query for one page of the walk. `showDeleted` must stay true or
766
+ * deletions would be invisible; `pageToken` is sent ALONGSIDE `syncToken`,
767
+ * which is what Google requires to page an incremental sync. */
768
+ function syncPageParams(input, pageToken) {
769
+ const params = new URLSearchParams({
770
+ singleEvents: "true",
771
+ showDeleted: "true",
772
+ maxResults: String(input.maxResults ?? EVENT_SYNC_PAGE_SIZE)
773
+ });
774
+ if (input.syncToken) params.set("syncToken", input.syncToken);
775
+ if (pageToken) params.set("pageToken", pageToken);
776
+ return params;
777
+ }
765
778
  async function syncCalendarEvents(accessToken, input = {}) {
766
779
  const events = [];
767
780
  let pageToken;
768
781
  let nextSyncToken;
769
782
  for (let page = 0; page < MAX_EVENT_SYNC_PAGES; page += 1) {
770
- const params = new URLSearchParams({
771
- singleEvents: "true",
772
- showDeleted: "true",
773
- maxResults: String(input.maxResults ?? EVENT_SYNC_PAGE_SIZE)
774
- });
775
- if (input.syncToken) params.set("syncToken", input.syncToken);
776
- if (pageToken) params.set("pageToken", pageToken);
777
- const payload = await fetchSyncPage(accessToken, input.calendarId, params);
783
+ const payload = await fetchSyncPage(accessToken, input.calendarId, syncPageParams(input, pageToken));
778
784
  if (payload === GONE) return {
779
785
  events: [],
780
- fullResyncRequired: true
786
+ fullResyncRequired: true,
787
+ pagesExhausted: false
781
788
  };
782
789
  const record = asRecord(payload);
783
790
  events.push(...itemsOf(payload).map(toEventSummary));
784
791
  nextSyncToken = stringField(record, "nextSyncToken") || void 0;
785
792
  pageToken = stringField(record, "nextPageToken") || void 0;
786
793
  if (!pageToken) break;
794
+ nextSyncToken = void 0;
787
795
  }
788
796
  return {
789
797
  events,
790
798
  nextSyncToken,
791
- fullResyncRequired: false
799
+ fullResyncRequired: false,
800
+ pagesExhausted: pageToken !== void 0
792
801
  };
793
802
  }
794
803
  /** Pagination loop for CalendarList.list, extracted so it can be tested without
@@ -1013,6 +1022,21 @@ function calendarSyncStatePath(workspaceRoot) {
1013
1022
  return path.join(workspaceRoot ?? getWorkspaceRoot(), "data", "calendar", ".sync-state.json");
1014
1023
  }
1015
1024
  var calendarKey = canonicalCalendarId;
1025
+ /** The key for a consumer that reads a calendar without KEEPING what it reads.
1026
+ *
1027
+ * A sync token is a claim about which events the consumer already holds, but
1028
+ * the key above names only the calendar — so every consumer of one calendar
1029
+ * shares one cursor. That is right for the collections bound to it (one fetch
1030
+ * fans out to all of them in the same pass) and wrong for the standalone
1031
+ * `google` tool's `calendarSync`, which walks the calendar, reports a summary
1032
+ * to the LLM and discards the events.
1033
+ *
1034
+ * Sharing it lost data both ways (#2850): the tool consumed windows the
1035
+ * collections then never received, and a collection created afterwards resumed
1036
+ * from the tool's cursor — so its "first sync" was a delta of a window it had
1037
+ * never held, and the calendar's history never arrived. Same file, separate
1038
+ * keyspace. Idempotent through `calendarKey`, since the result is non-empty. */
1039
+ var toolCalendarSyncKey = (calendarId) => `tool:${canonicalCalendarId(calendarId)}`;
1016
1040
  /** Read one map out of whatever is on disk. Tolerant on purpose: a file written
1017
1041
  * before `lastSyncedAt` existed simply lacks the key, and a hand-edited one may
1018
1042
  * hold anything. A dropped entry costs a full re-walk or a duplicate run, never
@@ -1138,6 +1162,46 @@ function isCalendarSyncDue(lastSyncedAt, windowMs, now = Date.now()) {
1138
1162
  return elapsed >= windowMs;
1139
1163
  }
1140
1164
  //#endregion
1165
+ //#region src/google/calendarBackfillState.ts
1166
+ var BACKFILL_STATE_MODE = 384;
1167
+ var BACKFILL_FILE = ".calendar-sync.json";
1168
+ /** `<dataDir>/.calendar-sync.json` */
1169
+ var calendarBackfillPath = (dataDir) => path.join(dataDir, BACKFILL_FILE);
1170
+ /** Whether these records still need the whole calendar walked into them.
1171
+ *
1172
+ * Compared against the calendar the marker NAMES, not merely its presence: a
1173
+ * schema repointed at another calendar holds a backfill of the wrong one, and
1174
+ * resuming that calendar's cursor would leave the same silent gap this exists
1175
+ * to close.
1176
+ *
1177
+ * Unreadable, absent or INCOMPLETE all answer "yes" — every field must be
1178
+ * there, not just the one being compared. A redundant full walk costs API
1179
+ * calls and rewrites records that already match; skipping a needed one loses
1180
+ * the history silently, which is the failure being fixed, so the bias only
1181
+ * ever goes one way (CodeRabbit review #2853). */
1182
+ async function needsCalendarBackfill(dataDir, calendarId) {
1183
+ const stored = await readJsonOrNull(calendarBackfillPath(dataDir));
1184
+ if (typeof stored?.walkedAt !== "string" || stored.walkedAt === "") return true;
1185
+ return stored.calendarId !== canonicalCalendarId(calendarId);
1186
+ }
1187
+ /** Record that these records now cover the whole calendar. Never throws: the
1188
+ * marker is an optimisation over re-walking, so a workspace that cannot write
1189
+ * it syncs correctly and simply walks in full again next time. */
1190
+ async function markCalendarBackfilled(dataDir, calendarId, walkedAt) {
1191
+ const state = {
1192
+ calendarId: canonicalCalendarId(calendarId),
1193
+ walkedAt
1194
+ };
1195
+ try {
1196
+ await writeJsonAtomicWithMode(calendarBackfillPath(dataDir), state, BACKFILL_STATE_MODE);
1197
+ } catch (error) {
1198
+ log.warn("google", "could not record the calendar backfill marker — the next sync will walk the calendar again", {
1199
+ dataDir,
1200
+ error: String(error)
1201
+ });
1202
+ }
1203
+ }
1204
+ //#endregion
1141
1205
  //#region src/google/calendarPushState.ts
1142
1206
  var PUSH_STATE_MODE = 384;
1143
1207
  /** `<workspace>/data/calendar/.push-state.json` */
@@ -2183,21 +2247,114 @@ async function releaseCalendarSyncClaim(calendarId, workspaceRoot) {
2183
2247
  * would advance the baseline past a record the pull deliberately left holding a
2184
2248
  * local edit, which is the silent overwrite this whole path exists to stop. */
2185
2249
  var heldBack = (unpushed, results) => /* @__PURE__ */ new Set([...allUnpushed(unpushed), ...results.flatMap((result) => result.withheld)]);
2186
- async function syncCalendarGroupNow(calendarId, collections, workspaceRoot) {
2187
- const unpushed = await pushAndProtect(collections, workspaceRoot);
2188
- const baseline = await loadCalendarShadow(calendarId, workspaceRoot);
2250
+ /** The collections in this group that have never received the whole calendar,
2251
+ * by slug. Asked of each collection's OWN records (`calendarBackfillState.ts`),
2252
+ * because the sync token cannot answer it — it is keyed by calendar and shared
2253
+ * by every consumer of that calendar (#2850). */
2254
+ async function collectionsNeedingBackfill(collections) {
2255
+ return (await Promise.all(collections.map(async (collection) => {
2256
+ return await needsCalendarBackfill(collection.dataDir, collection.schema.googleCalendar?.calendarId) ? collection.slug : null;
2257
+ }))).filter((slug) => slug !== null);
2258
+ }
2259
+ /** The token this run may resume from — none while any collection in the group
2260
+ * still needs the whole calendar.
2261
+ *
2262
+ * A stored token says how far THE CALENDAR has been read, by whoever read it:
2263
+ * the standalone `google` tool's `calendarSync`, or a sibling collection that
2264
+ * synced before this one existed. Resuming from it hands a brand-new collection
2265
+ * a delta of a window it never received — a handful of records, reported as a
2266
+ * success with no error, and never the history the docs promise (#2850).
2267
+ *
2268
+ * The full walk fans out to the whole group, which is free: writes are upserts,
2269
+ * so the collections that were already current simply rewrite what they hold. */
2270
+ async function resumableToken(calendarId, collections, workspaceRoot) {
2271
+ const pending = await collectionsNeedingBackfill(collections);
2272
+ if (pending.length === 0) return await loadCalendarSyncToken(calendarId, workspaceRoot) ?? void 0;
2273
+ log.info("google", "walking the whole calendar — these collections have never received it", {
2274
+ calendarId,
2275
+ collections: pending
2276
+ });
2277
+ }
2278
+ /** Record the backfill for every collection the window landed in. Called only
2279
+ * after a FULL walk that fully landed: marking one after an incremental window
2280
+ * would claim a history the records do not hold. */
2281
+ async function markGroupBackfilled(calendarId, collections) {
2282
+ const walkedAt = (/* @__PURE__ */ new Date()).toISOString();
2283
+ await Promise.all(collections.map((collection) => markCalendarBackfilled(collection.dataDir, collection.schema.googleCalendar?.calendarId, walkedAt)));
2284
+ }
2285
+ /** What a partial window reports. Names the cause the user can actually act on:
2286
+ * a page guard is reached when Google has to expand an enormous number of
2287
+ * recurring instances, and one series with no end date is enough. */
2288
+ var PARTIAL_CALENDAR_WINDOW = "Google returned more pages of events than one sync pass walks, so only part of the calendar was copied. Give any recurring event with no end date a finite end date, then sync again.";
2289
+ /** A truncated walk must not read as a completed one (#2850).
2290
+ *
2291
+ * Added AFTER the token/baseline gate, not through it. A short fetch is not a
2292
+ * failed write: the events that did arrive are Google's own and landed
2293
+ * correctly, so their baseline is true and holding it back would freeze them —
2294
+ * a record with no baseline reads as an unsent local edit, which the next pull
2295
+ * then refuses to touch (#2683). What must not happen is the walk claiming
2296
+ * completeness, and that is covered without the gate: `nextSyncToken` only
2297
+ * appears on Google's last page, so a truncated walk has no token to advance,
2298
+ * and the backfill marker is withheld separately. */
2299
+ var withPartialWindowError = (results, pagesExhausted) => pagesExhausted ? results.map((result) => ({
2300
+ ...result,
2301
+ errors: [...result.errors, PARTIAL_CALENDAR_WINDOW]
2302
+ })) : [...results];
2303
+ /** `landed` is the write-side verdict (`windowFullyLanded`); the rest describe
2304
+ * the window Google returned.
2305
+ *
2306
+ * A page-capped walk (`pagesExhausted`) keeps its baseline — the events that
2307
+ * arrived are Google's own and were written correctly — but must NOT claim the
2308
+ * backfill, or the collection would stop asking for the rest of its calendar.
2309
+ * Its token is refused too, though Google makes that moot by sending
2310
+ * `nextSyncToken` only on the last page. */
2311
+ function windowAdvance(window) {
2312
+ if (!window.landed) return {
2313
+ baseline: false,
2314
+ backfill: false,
2315
+ token: false
2316
+ };
2317
+ const complete = !window.pagesExhausted;
2318
+ return {
2319
+ baseline: true,
2320
+ backfill: window.walkedInFull && complete,
2321
+ token: complete && window.nextSyncToken !== void 0
2322
+ };
2323
+ }
2324
+ /** The window this run should read, and whether it was a full walk. A 410
2325
+ * restart is a full walk too, so it backfills just as well as a forced one. */
2326
+ async function readWindow(calendarId, collections, workspaceRoot) {
2189
2327
  const accessToken = await getGoogleAccessToken();
2328
+ const resumeFrom = await resumableToken(calendarId, collections, workspaceRoot);
2190
2329
  const first = await syncCalendarEvents(accessToken, {
2191
2330
  calendarId,
2192
- syncToken: await loadCalendarSyncToken(calendarId, workspaceRoot) ?? void 0
2331
+ syncToken: resumeFrom
2193
2332
  });
2194
2333
  const result = first.fullResyncRequired ? await restartFullSync(accessToken, calendarId, workspaceRoot) : first;
2195
- const results = await applyWindowToGroup(collections, result.events, workspaceRoot, unpushed, baseline);
2196
- if (windowFullyLanded(calendarId, results)) {
2197
- await saveCalendarShadow(calendarId, shadowUpdates(result.events, heldBack(unpushed, results), baseline), workspaceRoot);
2198
- if (result.nextSyncToken) await advanceToken(calendarId, result.nextSyncToken, collections, workspaceRoot);
2199
- }
2200
- return results;
2334
+ if (result.pagesExhausted) log.warn("google", "the calendar walk ran out of pages — only part of it was copied", {
2335
+ calendarId,
2336
+ events: result.events.length
2337
+ });
2338
+ return {
2339
+ result,
2340
+ walkedInFull: resumeFrom === void 0 || first.fullResyncRequired
2341
+ };
2342
+ }
2343
+ async function syncCalendarGroupNow(calendarId, collections, workspaceRoot) {
2344
+ const unpushed = await pushAndProtect(collections, workspaceRoot);
2345
+ const baseline = await loadCalendarShadow(calendarId, workspaceRoot);
2346
+ const { result, walkedInFull } = await readWindow(calendarId, collections, workspaceRoot);
2347
+ const applied = await applyWindowToGroup(collections, result.events, workspaceRoot, unpushed, baseline);
2348
+ const advance = windowAdvance({
2349
+ landed: windowFullyLanded(calendarId, applied),
2350
+ walkedInFull,
2351
+ pagesExhausted: result.pagesExhausted,
2352
+ nextSyncToken: result.nextSyncToken
2353
+ });
2354
+ if (advance.baseline) await saveCalendarShadow(calendarId, shadowUpdates(result.events, heldBack(unpushed, applied), baseline), workspaceRoot);
2355
+ if (advance.backfill) await markGroupBackfilled(calendarId, collections);
2356
+ if (advance.token && result.nextSyncToken) await advanceToken(calendarId, result.nextSyncToken, collections, workspaceRoot);
2357
+ return withPartialWindowError(applied, result.pagesExhausted);
2201
2358
  }
2202
2359
  /** Apply one window to every collection on the calendar, honouring what each
2203
2360
  * one's own push protected. A collection whose protection could not be worked
@@ -2433,24 +2590,29 @@ async function syncDueCalendarCollections(workspaceRoot, intervalMs = DEFAULT_SY
2433
2590
  const windowMs = calendarSyncDueWindowMs(intervalMs);
2434
2591
  return await runCalendarGroups(groups, workspaceRoot, (lastSyncedAt) => isCalendarSyncDue(lastSyncedAt, windowMs));
2435
2592
  }
2436
- /** The groups whose calendar has never synced. A missing token IS the "created
2437
- * since the last sync" signal — nothing else distinguishes a new collection
2438
- * from an edited one on the write path this feeds (#2427).
2593
+ /** The groups holding a collection that still needs the whole calendar.
2594
+ *
2595
+ * This used to ask whether the CALENDAR had a stored token, which is not the
2596
+ * same question and is why the trigger silently did nothing for the #2850
2597
+ * reporter: their calendar already had a token — from the standalone `google`
2598
+ * tool, and on later attempts from the collection they had just deleted by
2599
+ * hand — so the brand-new collection matched nothing and its first sync never
2600
+ * ran at all. `pending` is injected so the rule is testable without a
2601
+ * workspace on disk.
2439
2602
  *
2440
- * Self-silencing by construction: the first sync stores a token, so a calendar
2441
- * matches at most once. `loadToken` is injected so the rule is testable without
2442
- * a workspace on disk. */
2443
- async function unsyncedGroups(groups, loadToken) {
2444
- const checked = await Promise.all([...groups].map(async (entry) => await loadToken(entry[0]) === null ? entry : null));
2603
+ * Still self-silencing: a landed full walk marks its collections, so a group
2604
+ * stops matching once every collection in it holds the history. */
2605
+ async function groupsNeedingBackfill(groups, pending) {
2606
+ const checked = await Promise.all([...groups].map(async (entry) => await pending(entry[1]) ? entry : null));
2445
2607
  return new Map(checked.filter((entry) => entry !== null));
2446
2608
  }
2447
- /** Sync only the calendars that have never synced — the first sync for a
2448
- * just-created collection, which otherwise stays empty until the hourly
2449
- * scheduler run (#2427). Cheap and safe to call on every config write. */
2609
+ /** Sync only the calendars a collection has never received in full — the first
2610
+ * sync for a just-created collection, which otherwise stays empty until the
2611
+ * hourly scheduler run (#2427). Cheap and safe to call on every config write. */
2450
2612
  async function syncNewCalendarCollections(workspaceRoot) {
2451
- const pending = await unsyncedGroups(await declaringGroups(workspaceRoot), (calendarId) => loadCalendarSyncToken(calendarId, workspaceRoot));
2613
+ const pending = await groupsNeedingBackfill(await declaringGroups(workspaceRoot), async (collections) => (await collectionsNeedingBackfill(collections)).length > 0);
2452
2614
  if (!await backgroundSyncAllowed(pending)) return [];
2453
- log.info("google", "running the first sync for newly declared calendars", { calendars: [...pending.keys()] });
2615
+ log.info("google", "running the first sync for calendars a collection has never received in full", { calendars: [...pending.keys()] });
2454
2616
  return await runCalendarGroups(pending, workspaceRoot);
2455
2617
  }
2456
2618
  var liveManualSyncDeps = {
@@ -2699,6 +2861,6 @@ async function deleteDriveFile(accessToken, input) {
2699
2861
  await googleRequest(DRIVE_API_LABEL, accessToken, `${DRIVE_FILES_URL}/${encodeURIComponent(input.fileId)}`, { method: "DELETE" });
2700
2862
  }
2701
2863
  //#endregion
2702
- export { CANCELLED_EVENT_STATUS, 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, HTTP_CONFLICT, HTTP_FORBIDDEN, HTTP_PRECONDITION_FAILED, MAX_LIST_RESULTS, PROTECTION_UNKNOWN, PUSHABLE_SOURCE_FIELDS, allUnpushed, anySyncedCollectionSurvives, applyPlanFor, assertSafeMimeType, authorizeGoogle, baselineRecord, brokerBaseUrl, brokerExchange, brokerRefresh, brokerStart, buildEventPatch, buildMultipartBody, buildTaskPatch, bySourceField, calendarApiError, calendarPushStatePath, calendarSyncDueWindowMs, calendarSyncStatePath, canonicalCalendarId, canonicalTaskListId, claimCalendarSyncIfDue, classifyDelete, classifyWrite, clearCalendarLastSyncedAt, clearCalendarShadow, clearCalendarSyncToken, clientSecretPresence, collectCalendarPages, commitLinkedTokens, completeTask, configureGoogleHost, conflictingFields, createCalendarEvent, createDriveFile, createGoogleAuthFlow, createTask, deleteCalendarEvent, deleteDriveFile, deleteGoogleTokens, deleteTask, fieldText, findClientSecretPath, getCalendarColors, getCalendarEvent, getCalendarMeta, getGoogleAccessToken, googleApiError, googleAuthFlow, googleCalendarSyncTaskDef, googleConfigDir, googleSecretsDir, googleTokenPath, groupByCalendar, heldBack, isCalendarSyncDue, isClientSettableEventId, isDeniedAccessRole, isGoogleApiError, isIsoDateTimeWithOffset, isTextMimeType, isUnpushed, legacyGoogleTokenPath, listCalendarEvents, listCalendars, listDriveFiles, listTaskLists, listTasks, liveLockClock, loadCalendarLastSyncedAt, loadCalendarShadow, loadCalendarSyncToken, loadClientSecret, loadGoogleTokens, locallyChangedFields, locallyDeletedIds, locallyEditedIds, mayAdoptExisting, mergeGoogleTokens, mergeIntoExisting, mergeShadow, orphanedCalendarId, pickBoundary, planRecord, pullProtectionFor, pullableEvents, pushAndProtect, pushCalendarForCollection, pushCollectionNow, pushableMap, readDriveFile, releaseOrphanedCalendarToken, reportedAccessRole, resolveEventSpan, saveCalendarShadow, saveCalendarSyncToken, saveGoogleTokens, shadowUpdates, stateLockPath, syncCalendarEvents, syncCalendarForCollection, syncCalendarGroup, syncDueCalendarCollections, syncNewCalendarCollections, toCalendarMeta, toCalendarSummary, toCollectionDateTime, toCollectionRecord, toDriveFileSummary, toEventSummary, toGoogleEventTime, toShadowEvent, toTaskListSummary, toTaskSummary, uncompleteTask, unlinkGoogle, unpushedFor, unsentEditGuard, unsentLocalEdits, unsyncedGroups, updateCalendarEvent, updateTask, waitForAuthCode, withCalendarLock, withCalendarStateLock, withKeyedLock, zoneSuffixOf };
2864
+ export { CANCELLED_EVENT_STATUS, 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, HTTP_CONFLICT, HTTP_FORBIDDEN, HTTP_PRECONDITION_FAILED, MAX_LIST_RESULTS, 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, fieldText, findClientSecretPath, getCalendarColors, getCalendarEvent, getCalendarMeta, getGoogleAccessToken, googleApiError, googleAuthFlow, googleCalendarSyncTaskDef, googleConfigDir, googleSecretsDir, googleTokenPath, groupByCalendar, groupsNeedingBackfill, heldBack, 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, resumableToken, saveCalendarShadow, saveCalendarSyncToken, saveGoogleTokens, shadowUpdates, stateLockPath, syncCalendarEvents, syncCalendarForCollection, syncCalendarGroup, syncDueCalendarCollections, syncNewCalendarCollections, syncPageParams, toCalendarMeta, toCalendarSummary, toCollectionDateTime, toCollectionRecord, toDriveFileSummary, toEventSummary, toGoogleEventTime, toShadowEvent, toTaskListSummary, toTaskSummary, toolCalendarSyncKey, uncompleteTask, unlinkGoogle, unpushedFor, unsentEditGuard, unsentLocalEdits, updateCalendarEvent, updateTask, waitForAuthCode, windowAdvance, withCalendarLock, withCalendarStateLock, withKeyedLock, withPartialWindowError, zoneSuffixOf };
2703
2865
 
2704
2866
  //# sourceMappingURL=index.js.map