@mulmoclaude/core 3.3.0 → 3.5.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 (57) hide show
  1. package/assets/helps/error-recovery.md +71 -0
  2. package/assets/helps/google-calendar-collection.md +10 -0
  3. package/dist/{calendarGrid-Csy2rpjp.js → calendarGrid-CQ8MVSRb.js} +141 -2
  4. package/dist/calendarGrid-CQ8MVSRb.js.map +1 -0
  5. package/dist/{calendarGrid-p8K-7cuB.cjs → calendarGrid-DGILaVxI.cjs} +194 -1
  6. package/dist/calendarGrid-DGILaVxI.cjs.map +1 -0
  7. package/dist/collection/core/collectionKey.d.ts +67 -0
  8. package/dist/collection/index.cjs +11 -2
  9. package/dist/collection/index.d.ts +1 -0
  10. package/dist/collection/index.js +3 -3
  11. package/dist/collection/registry/server/index.cjs +3 -3
  12. package/dist/collection/registry/server/index.cjs.map +1 -1
  13. package/dist/collection/registry/server/index.js +3 -3
  14. package/dist/collection/registry/server/index.js.map +1 -1
  15. package/dist/collection/server/host.d.ts +49 -8
  16. package/dist/collection/server/index.cjs +6 -3
  17. package/dist/collection/server/index.d.ts +1 -1
  18. package/dist/collection/server/index.js +4 -4
  19. package/dist/collection-watchers/index.cjs +33 -12
  20. package/dist/collection-watchers/index.cjs.map +1 -1
  21. package/dist/collection-watchers/index.js +33 -12
  22. package/dist/collection-watchers/index.js.map +1 -1
  23. package/dist/collection-watchers/reconciler.d.ts +35 -0
  24. package/dist/{discovery-CrA_EqUJ.cjs → discovery-Ck4AqikY.cjs} +63 -2
  25. package/dist/discovery-Ck4AqikY.cjs.map +1 -0
  26. package/dist/{discovery-D7LCN8gJ.js → discovery-DH9wweuj.js} +46 -3
  27. package/dist/discovery-DH9wweuj.js.map +1 -0
  28. package/dist/feeds/index.cjs +2 -2
  29. package/dist/feeds/index.js +2 -2
  30. package/dist/feeds/server/index.cjs +4 -4
  31. package/dist/feeds/server/index.js +4 -4
  32. package/dist/google/calendar.d.ts +15 -0
  33. package/dist/google/calendarBackfillState.d.ts +19 -0
  34. package/dist/google/calendarSyncStore.d.ts +15 -0
  35. package/dist/google/collectionSync.d.ts +78 -11
  36. package/dist/google/index.cjs +208 -36
  37. package/dist/google/index.cjs.map +1 -1
  38. package/dist/google/index.d.ts +4 -3
  39. package/dist/google/index.js +198 -36
  40. package/dist/google/index.js.map +1 -1
  41. package/dist/{ingestTypes-F2IJhOLJ.js → ingestTypes-CEi-Ot7F.js} +2 -2
  42. package/dist/{ingestTypes-F2IJhOLJ.js.map → ingestTypes-CEi-Ot7F.js.map} +1 -1
  43. package/dist/{ingestTypes-CPR-h-OZ.cjs → ingestTypes-iaiq33Ph.cjs} +2 -2
  44. package/dist/{ingestTypes-CPR-h-OZ.cjs.map → ingestTypes-iaiq33Ph.cjs.map} +1 -1
  45. package/dist/{promptSafety-ACXw_2Yu.js → promptSafety-CpiME8pj.js} +2 -2
  46. package/dist/{promptSafety-ACXw_2Yu.js.map → promptSafety-CpiME8pj.js.map} +1 -1
  47. package/dist/{promptSafety-CNEW3_qS.cjs → promptSafety-NNGViiCr.cjs} +2 -2
  48. package/dist/{promptSafety-CNEW3_qS.cjs.map → promptSafety-NNGViiCr.cjs.map} +1 -1
  49. package/dist/{server-CorfPQVQ.cjs → server-5EMj3naj.cjs} +4 -4
  50. package/dist/{server-CorfPQVQ.cjs.map → server-5EMj3naj.cjs.map} +1 -1
  51. package/dist/{server-D6QY853r.js → server-BiRLLMpW.js} +4 -4
  52. package/dist/{server-D6QY853r.js.map → server-BiRLLMpW.js.map} +1 -1
  53. package/package.json +1 -1
  54. package/dist/calendarGrid-Csy2rpjp.js.map +0 -1
  55. package/dist/calendarGrid-p8K-7cuB.cjs.map +0 -1
  56. package/dist/discovery-CrA_EqUJ.cjs.map +0 -1
  57. package/dist/discovery-D7LCN8gJ.js.map +0 -1
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_calendarGrid = require("../calendarGrid-p8K-7cuB.cjs");
3
- const require_ingestTypes = require("../ingestTypes-CPR-h-OZ.cjs");
2
+ const require_calendarGrid = require("../calendarGrid-DGILaVxI.cjs");
3
+ const require_ingestTypes = require("../ingestTypes-iaiq33Ph.cjs");
4
4
  exports.AGENT_INGEST_KIND = require_ingestTypes.AGENT_INGEST_KIND;
5
5
  exports.DEFAULT_FEED_MAX_ITEMS = require_ingestTypes.DEFAULT_FEED_MAX_ITEMS;
6
6
  exports.FEED_SCHEDULES = require_calendarGrid.FEED_SCHEDULES;
@@ -1,3 +1,3 @@
1
- import { C as FEED_SCHEDULES, w as INGEST_KINDS } from "../calendarGrid-Csy2rpjp.js";
2
- import { n as DEFAULT_FEED_MAX_ITEMS, r as isFeedSchedule, t as AGENT_INGEST_KIND } from "../ingestTypes-F2IJhOLJ.js";
1
+ import { M as FEED_SCHEDULES, N as INGEST_KINDS } from "../calendarGrid-CQ8MVSRb.js";
2
+ import { n as DEFAULT_FEED_MAX_ITEMS, r as isFeedSchedule, t as AGENT_INGEST_KIND } from "../ingestTypes-CEi-Ot7F.js";
3
3
  export { AGENT_INGEST_KIND, DEFAULT_FEED_MAX_ITEMS, FEED_SCHEDULES, INGEST_KINDS, isFeedSchedule };
@@ -2,12 +2,12 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_rolldown_runtime = require("../../rolldown-runtime-VH7oDXx4.cjs");
3
3
  const require_dist = require("../../dist-CsgSfWwR.cjs");
4
4
  const require_root = require("../../root-rPH6FGDT.cjs");
5
- const require_calendarGrid = require("../../calendarGrid-p8K-7cuB.cjs");
5
+ const require_calendarGrid = require("../../calendarGrid-DGILaVxI.cjs");
6
6
  const require_project = require("../../project-C1ep9pvo.cjs");
7
- const require_discovery = require("../../discovery-CrA_EqUJ.cjs");
8
- const require_server = require("../../server-CorfPQVQ.cjs");
7
+ const require_discovery = require("../../discovery-Ck4AqikY.cjs");
8
+ const require_server = require("../../server-5EMj3naj.cjs");
9
9
  const require_feeds_paths = require("../paths.cjs");
10
- const require_ingestTypes = require("../../ingestTypes-CPR-h-OZ.cjs");
10
+ const require_ingestTypes = require("../../ingestTypes-iaiq33Ph.cjs");
11
11
  const require_notifier = require("../../notifier-DZJj0sOh.cjs");
12
12
  let node_path = require("node:path");
13
13
  node_path = require_rolldown_runtime.__toESM(node_path, 1);
@@ -1,11 +1,11 @@
1
1
  import { a as isErrorWithCode, l as isUnknownArray, s as isRecord$1 } from "../../dist-D8zokgGo.js";
2
2
  import { t as canonicalRoot } from "../../root-BMroU_mB.js";
3
- import { C as FEED_SCHEDULES, w as INGEST_KINDS } from "../../calendarGrid-Csy2rpjp.js";
3
+ import { M as FEED_SCHEDULES, N as INGEST_KINDS } from "../../calendarGrid-CQ8MVSRb.js";
4
4
  import { t as mergeIntoExisting } from "../../project-B0j_Vf_Q.js";
5
- import { B as resolveDataDir, N as listItems, U as safeSlugName, at as createHostSlot, d as storeFor, it as createForwardingLogger, n as discoverCollections } from "../../discovery-D7LCN8gJ.js";
6
- import { A as buildCollectionActionSeedPrompt, P as readSkillTemplate, j as promptPathsFor } from "../../server-D6QY853r.js";
5
+ import { B as resolveDataDir, N as listItems, U as safeSlugName, ct as createHostSlot, d as storeFor, n as discoverCollections, st as createForwardingLogger } from "../../discovery-DH9wweuj.js";
6
+ import { A as buildCollectionActionSeedPrompt, P as readSkillTemplate, j as promptPathsFor } from "../../server-BiRLLMpW.js";
7
7
  import { FEEDS_DIR, feedDir, feedStatePath, feedsRoot, ingestStateDir, ingestStatePath } from "../paths.js";
8
- import { n as DEFAULT_FEED_MAX_ITEMS, r as isFeedSchedule, t as AGENT_INGEST_KIND } from "../../ingestTypes-F2IJhOLJ.js";
8
+ import { n as DEFAULT_FEED_MAX_ITEMS, r as isFeedSchedule, t as AGENT_INGEST_KIND } from "../../ingestTypes-CEi-Ot7F.js";
9
9
  import { d as publish, n as clear } from "../../notifier-DhnwR82U.js";
10
10
  import path from "node:path";
11
11
  import { createHash } from "node:crypto";
@@ -175,6 +175,17 @@ export interface CalendarSyncResult {
175
175
  /** The stored token had expired (410) — the caller must drop it and re-sync
176
176
  * from scratch; no events are returned in that case. */
177
177
  fullResyncRequired: boolean;
178
+ /** The page guard fired with pages still pending, so `events` is a PARTIAL
179
+ * window and `nextSyncToken` is absent.
180
+ *
181
+ * Reported rather than left implicit because a truncated walk is otherwise
182
+ * byte-identical to a completed one: the caller would apply the partial set,
183
+ * report success, and — since only Google's last page carries the token —
184
+ * repeat the same truncated walk on every later run, silently (#2850).
185
+ * Google states a page "may be less than this value, or none at all, even if
186
+ * there are more events matching the query", so this is reachable on a real
187
+ * calendar, the more so with `singleEvents` expanding unbounded recurrences. */
188
+ pagesExhausted: boolean;
178
189
  }
179
190
  /** Incremental sync over the events of one calendar (#2095).
180
191
  *
@@ -186,6 +197,10 @@ export interface CalendarSyncResult {
186
197
  *
187
198
  * `nextSyncToken` is only present on the LAST page, so every page must be
188
199
  * walked before the token is worth storing. */
200
+ /** The query for one page of the walk. `showDeleted` must stay true or
201
+ * deletions would be invisible; `pageToken` is sent ALONGSIDE `syncToken`,
202
+ * which is what Google requires to page an incremental sync. */
203
+ export declare function syncPageParams(input: SyncEventsInput, pageToken: string | undefined): URLSearchParams;
189
204
  export declare function syncCalendarEvents(accessToken: string, input?: SyncEventsInput): Promise<CalendarSyncResult>;
190
205
  export interface CalendarListPage {
191
206
  items: unknown[];
@@ -0,0 +1,19 @@
1
+ /** `<dataDir>/.calendar-sync.json` */
2
+ export declare const calendarBackfillPath: (dataDir: string) => string;
3
+ /** Whether these records still need the whole calendar walked into them.
4
+ *
5
+ * Compared against the calendar the marker NAMES, not merely its presence: a
6
+ * schema repointed at another calendar holds a backfill of the wrong one, and
7
+ * resuming that calendar's cursor would leave the same silent gap this exists
8
+ * to close.
9
+ *
10
+ * Unreadable, absent or INCOMPLETE all answer "yes" — every field must be
11
+ * there, not just the one being compared. A redundant full walk costs API
12
+ * calls and rewrites records that already match; skipping a needed one loses
13
+ * the history silently, which is the failure being fixed, so the bias only
14
+ * ever goes one way (CodeRabbit review #2853). */
15
+ export declare function needsCalendarBackfill(dataDir: string, calendarId: string | undefined): Promise<boolean>;
16
+ /** Record that these records now cover the whole calendar. Never throws: the
17
+ * marker is an optimisation over re-walking, so a workspace that cannot write
18
+ * it syncs correctly and simply walks in full again next time. */
19
+ export declare function markCalendarBackfilled(dataDir: string, calendarId: string | undefined, walkedAt: string): Promise<void>;
@@ -1,5 +1,20 @@
1
1
  /** `<workspace>/data/calendar/.sync-state.json` */
2
2
  export declare function calendarSyncStatePath(workspaceRoot?: string): string;
3
+ /** The key for a consumer that reads a calendar without KEEPING what it reads.
4
+ *
5
+ * A sync token is a claim about which events the consumer already holds, but
6
+ * the key above names only the calendar — so every consumer of one calendar
7
+ * shares one cursor. That is right for the collections bound to it (one fetch
8
+ * fans out to all of them in the same pass) and wrong for the standalone
9
+ * `google` tool's `calendarSync`, which walks the calendar, reports a summary
10
+ * to the LLM and discards the events.
11
+ *
12
+ * Sharing it lost data both ways (#2850): the tool consumed windows the
13
+ * collections then never received, and a collection created afterwards resumed
14
+ * from the tool's cursor — so its "first sync" was a delta of a window it had
15
+ * never held, and the calendar's history never arrived. Same file, separate
16
+ * keyspace. Idempotent through `calendarKey`, since the result is non-empty. */
17
+ export declare const toolCalendarSyncKey: (calendarId: string | undefined) => string;
3
18
  export declare function loadCalendarSyncToken(calendarId?: string, workspaceRoot?: string): Promise<string | null>;
4
19
  export declare function saveCalendarSyncToken(calendarId: string | undefined, syncToken: string, workspaceRoot?: string): Promise<void>;
5
20
  /** Drop one calendar's token — used when Google answers 410, so the next run
@@ -158,6 +158,68 @@ export declare function syncCalendarGroup(calendarId: string | undefined, collec
158
158
  * would advance the baseline past a record the pull deliberately left holding a
159
159
  * local edit, which is the silent overwrite this whole path exists to stop. */
160
160
  export declare const heldBack: (unpushed: UnpushedBySlug, results: readonly CalendarCollectionSyncResult[]) => ReadonlySet<string>;
161
+ /** The collections in this group that have never received the whole calendar,
162
+ * by slug. Asked of each collection's OWN records (`calendarBackfillState.ts`),
163
+ * because the sync token cannot answer it — it is keyed by calendar and shared
164
+ * by every consumer of that calendar (#2850). */
165
+ export declare function collectionsNeedingBackfill(collections: readonly LoadedCollection[]): Promise<string[]>;
166
+ /** The token this run may resume from — none while any collection in the group
167
+ * still needs the whole calendar.
168
+ *
169
+ * A stored token says how far THE CALENDAR has been read, by whoever read it:
170
+ * the standalone `google` tool's `calendarSync`, or a sibling collection that
171
+ * synced before this one existed. Resuming from it hands a brand-new collection
172
+ * a delta of a window it never received — a handful of records, reported as a
173
+ * success with no error, and never the history the docs promise (#2850).
174
+ *
175
+ * The full walk fans out to the whole group, which is free: writes are upserts,
176
+ * so the collections that were already current simply rewrite what they hold. */
177
+ export declare function resumableToken(calendarId: string | undefined, collections: readonly LoadedCollection[], workspaceRoot: string): Promise<string | undefined>;
178
+ /** What a partial window reports. Names the cause the user can actually act on:
179
+ * a page guard is reached when Google has to expand an enormous number of
180
+ * recurring instances, and one series with no end date is enough. */
181
+ export declare const PARTIAL_CALENDAR_WINDOW: string;
182
+ /** A truncated walk must not read as a completed one (#2850).
183
+ *
184
+ * Added AFTER the token/baseline gate, not through it. A short fetch is not a
185
+ * failed write: the events that did arrive are Google's own and landed
186
+ * correctly, so their baseline is true and holding it back would freeze them —
187
+ * a record with no baseline reads as an unsent local edit, which the next pull
188
+ * then refuses to touch (#2683). What must not happen is the walk claiming
189
+ * completeness, and that is covered without the gate: `nextSyncToken` only
190
+ * appears on Google's last page, so a truncated walk has no token to advance,
191
+ * and the backfill marker is withheld separately. */
192
+ export declare const withPartialWindowError: (results: readonly CalendarCollectionSyncResult[], pagesExhausted: boolean) => CalendarCollectionSyncResult[];
193
+ /** What a finished window is allowed to advance.
194
+ *
195
+ * One pure rule rather than three conditions spread through the write-back,
196
+ * because the three answers genuinely differ and each wrong one is a SILENT
197
+ * data bug rather than a visible failure: a baseline held back freezes the
198
+ * records it describes (#2683), a baseline advanced past a failed write hides
199
+ * a conflict (#2620), a backfill marker set on a partial walk re-creates the
200
+ * #2850 gap, and a token advanced past either loses the window for good. */
201
+ export interface WindowAdvance {
202
+ /** Record what Google now says, so the next push can tell a local edit from a pull. */
203
+ baseline: boolean;
204
+ /** Claim these records now hold the WHOLE calendar. */
205
+ backfill: boolean;
206
+ /** Move the shared cursor past this window. */
207
+ token: boolean;
208
+ }
209
+ /** `landed` is the write-side verdict (`windowFullyLanded`); the rest describe
210
+ * the window Google returned.
211
+ *
212
+ * A page-capped walk (`pagesExhausted`) keeps its baseline — the events that
213
+ * arrived are Google's own and were written correctly — but must NOT claim the
214
+ * backfill, or the collection would stop asking for the rest of its calendar.
215
+ * Its token is refused too, though Google makes that moot by sending
216
+ * `nextSyncToken` only on the last page. */
217
+ export declare function windowAdvance(window: {
218
+ landed: boolean;
219
+ walkedInFull: boolean;
220
+ pagesExhausted: boolean;
221
+ nextSyncToken?: string | undefined;
222
+ }): WindowAdvance;
161
223
  /** The baseline this window establishes: what Google now says per event, and
162
224
  * `null` for a cancelled one so a recreate cannot resume from a dead baseline.
163
225
  *
@@ -225,17 +287,22 @@ export declare function releaseOrphanedCalendarToken(deleted: CalendarDeclaring,
225
287
  * evaluated where the marker is written, so the answer cannot go stale between
226
288
  * deciding and claiming (Codex review #2680). */
227
289
  export declare function syncDueCalendarCollections(workspaceRoot: string, intervalMs?: number): Promise<CalendarCollectionSyncResult[]>;
228
- /** The groups whose calendar has never synced. A missing token IS the "created
229
- * since the last sync" signal — nothing else distinguishes a new collection
230
- * from an edited one on the write path this feeds (#2427).
231
- *
232
- * Self-silencing by construction: the first sync stores a token, so a calendar
233
- * matches at most once. `loadToken` is injected so the rule is testable without
234
- * a workspace on disk. */
235
- export declare function unsyncedGroups<T>(groups: Map<string, T>, loadToken: (calendarId: string) => Promise<string | null>): Promise<Map<string, T>>;
236
- /** Sync only the calendars that have never synced — the first sync for a
237
- * just-created collection, which otherwise stays empty until the hourly
238
- * scheduler run (#2427). Cheap and safe to call on every config write. */
290
+ /** The groups holding a collection that still needs the whole calendar.
291
+ *
292
+ * This used to ask whether the CALENDAR had a stored token, which is not the
293
+ * same question and is why the trigger silently did nothing for the #2850
294
+ * reporter: their calendar already had a token — from the standalone `google`
295
+ * tool, and on later attempts from the collection they had just deleted by
296
+ * hand — so the brand-new collection matched nothing and its first sync never
297
+ * ran at all. `pending` is injected so the rule is testable without a
298
+ * workspace on disk.
299
+ *
300
+ * Still self-silencing: a landed full walk marks its collections, so a group
301
+ * stops matching once every collection in it holds the history. */
302
+ export declare function groupsNeedingBackfill<T>(groups: Map<string, T>, pending: (value: T) => Promise<boolean>): Promise<Map<string, T>>;
303
+ /** Sync only the calendars a collection has never received in full — the first
304
+ * sync for a just-created collection, which otherwise stays empty until the
305
+ * hourly scheduler run (#2427). Cheap and safe to call on every config write. */
239
306
  export declare function syncNewCalendarCollections(workspaceRoot: string): Promise<CalendarCollectionSyncResult[]>;
240
307
  /** A user-triggered sync's outcome. `not-a-calendar` and `not-linked` are
241
308
  * states the caller must report rather than swallow: a Refresh click that
@@ -4,9 +4,9 @@ const require_dist = require("../dist-CsgSfWwR.cjs");
4
4
  const require_text = require("../text-CaAh9ZSO.cjs");
5
5
  const require_root = require("../root-rPH6FGDT.cjs");
6
6
  const require_utils_fetch = require("../utils/fetch.cjs");
7
- const require_calendarGrid = require("../calendarGrid-p8K-7cuB.cjs");
7
+ const require_calendarGrid = require("../calendarGrid-DGILaVxI.cjs");
8
8
  const require_project = require("../project-C1ep9pvo.cjs");
9
- const require_discovery = require("../discovery-CrA_EqUJ.cjs");
9
+ const require_discovery = require("../discovery-Ck4AqikY.cjs");
10
10
  const require_paths = require("../paths-ClV946_f.cjs");
11
11
  let node_fs = require("node:fs");
12
12
  let node_path = require("node:path");
@@ -766,33 +766,42 @@ async function fetchSyncPage(accessToken, calendarId, params) {
766
766
  *
767
767
  * `nextSyncToken` is only present on the LAST page, so every page must be
768
768
  * walked before the token is worth storing. */
769
+ /** The query for one page of the walk. `showDeleted` must stay true or
770
+ * deletions would be invisible; `pageToken` is sent ALONGSIDE `syncToken`,
771
+ * which is what Google requires to page an incremental sync. */
772
+ function syncPageParams(input, pageToken) {
773
+ const params = new URLSearchParams({
774
+ singleEvents: "true",
775
+ showDeleted: "true",
776
+ maxResults: String(input.maxResults ?? EVENT_SYNC_PAGE_SIZE)
777
+ });
778
+ if (input.syncToken) params.set("syncToken", input.syncToken);
779
+ if (pageToken) params.set("pageToken", pageToken);
780
+ return params;
781
+ }
769
782
  async function syncCalendarEvents(accessToken, input = {}) {
770
783
  const events = [];
771
784
  let pageToken;
772
785
  let nextSyncToken;
773
786
  for (let page = 0; page < MAX_EVENT_SYNC_PAGES; page += 1) {
774
- const params = new URLSearchParams({
775
- singleEvents: "true",
776
- showDeleted: "true",
777
- maxResults: String(input.maxResults ?? EVENT_SYNC_PAGE_SIZE)
778
- });
779
- if (input.syncToken) params.set("syncToken", input.syncToken);
780
- if (pageToken) params.set("pageToken", pageToken);
781
- const payload = await fetchSyncPage(accessToken, input.calendarId, params);
787
+ const payload = await fetchSyncPage(accessToken, input.calendarId, syncPageParams(input, pageToken));
782
788
  if (payload === GONE) return {
783
789
  events: [],
784
- fullResyncRequired: true
790
+ fullResyncRequired: true,
791
+ pagesExhausted: false
785
792
  };
786
793
  const record = asRecord(payload);
787
794
  events.push(...itemsOf(payload).map(toEventSummary));
788
795
  nextSyncToken = stringField(record, "nextSyncToken") || void 0;
789
796
  pageToken = stringField(record, "nextPageToken") || void 0;
790
797
  if (!pageToken) break;
798
+ nextSyncToken = void 0;
791
799
  }
792
800
  return {
793
801
  events,
794
802
  nextSyncToken,
795
- fullResyncRequired: false
803
+ fullResyncRequired: false,
804
+ pagesExhausted: pageToken !== void 0
796
805
  };
797
806
  }
798
807
  /** Pagination loop for CalendarList.list, extracted so it can be tested without
@@ -1017,6 +1026,21 @@ function calendarSyncStatePath(workspaceRoot) {
1017
1026
  return node_path.default.join(workspaceRoot ?? require_discovery.getWorkspaceRoot(), "data", "calendar", ".sync-state.json");
1018
1027
  }
1019
1028
  var calendarKey = canonicalCalendarId;
1029
+ /** The key for a consumer that reads a calendar without KEEPING what it reads.
1030
+ *
1031
+ * A sync token is a claim about which events the consumer already holds, but
1032
+ * the key above names only the calendar — so every consumer of one calendar
1033
+ * shares one cursor. That is right for the collections bound to it (one fetch
1034
+ * fans out to all of them in the same pass) and wrong for the standalone
1035
+ * `google` tool's `calendarSync`, which walks the calendar, reports a summary
1036
+ * to the LLM and discards the events.
1037
+ *
1038
+ * Sharing it lost data both ways (#2850): the tool consumed windows the
1039
+ * collections then never received, and a collection created afterwards resumed
1040
+ * from the tool's cursor — so its "first sync" was a delta of a window it had
1041
+ * never held, and the calendar's history never arrived. Same file, separate
1042
+ * keyspace. Idempotent through `calendarKey`, since the result is non-empty. */
1043
+ var toolCalendarSyncKey = (calendarId) => `tool:${canonicalCalendarId(calendarId)}`;
1020
1044
  /** Read one map out of whatever is on disk. Tolerant on purpose: a file written
1021
1045
  * before `lastSyncedAt` existed simply lacks the key, and a hand-edited one may
1022
1046
  * hold anything. A dropped entry costs a full re-walk or a duplicate run, never
@@ -1142,6 +1166,46 @@ function isCalendarSyncDue(lastSyncedAt, windowMs, now = Date.now()) {
1142
1166
  return elapsed >= windowMs;
1143
1167
  }
1144
1168
  //#endregion
1169
+ //#region src/google/calendarBackfillState.ts
1170
+ var BACKFILL_STATE_MODE = 384;
1171
+ var BACKFILL_FILE = ".calendar-sync.json";
1172
+ /** `<dataDir>/.calendar-sync.json` */
1173
+ var calendarBackfillPath = (dataDir) => node_path.default.join(dataDir, BACKFILL_FILE);
1174
+ /** Whether these records still need the whole calendar walked into them.
1175
+ *
1176
+ * Compared against the calendar the marker NAMES, not merely its presence: a
1177
+ * schema repointed at another calendar holds a backfill of the wrong one, and
1178
+ * resuming that calendar's cursor would leave the same silent gap this exists
1179
+ * to close.
1180
+ *
1181
+ * Unreadable, absent or INCOMPLETE all answer "yes" — every field must be
1182
+ * there, not just the one being compared. A redundant full walk costs API
1183
+ * calls and rewrites records that already match; skipping a needed one loses
1184
+ * the history silently, which is the failure being fixed, so the bias only
1185
+ * ever goes one way (CodeRabbit review #2853). */
1186
+ async function needsCalendarBackfill(dataDir, calendarId) {
1187
+ const stored = await readJsonOrNull(calendarBackfillPath(dataDir));
1188
+ if (typeof stored?.walkedAt !== "string" || stored.walkedAt === "") return true;
1189
+ return stored.calendarId !== canonicalCalendarId(calendarId);
1190
+ }
1191
+ /** Record that these records now cover the whole calendar. Never throws: the
1192
+ * marker is an optimisation over re-walking, so a workspace that cannot write
1193
+ * it syncs correctly and simply walks in full again next time. */
1194
+ async function markCalendarBackfilled(dataDir, calendarId, walkedAt) {
1195
+ const state = {
1196
+ calendarId: canonicalCalendarId(calendarId),
1197
+ walkedAt
1198
+ };
1199
+ try {
1200
+ await writeJsonAtomicWithMode(calendarBackfillPath(dataDir), state, BACKFILL_STATE_MODE);
1201
+ } catch (error) {
1202
+ log.warn("google", "could not record the calendar backfill marker — the next sync will walk the calendar again", {
1203
+ dataDir,
1204
+ error: String(error)
1205
+ });
1206
+ }
1207
+ }
1208
+ //#endregion
1145
1209
  //#region src/google/calendarPushState.ts
1146
1210
  var PUSH_STATE_MODE = 384;
1147
1211
  /** `<workspace>/data/calendar/.push-state.json` */
@@ -2187,21 +2251,114 @@ async function releaseCalendarSyncClaim(calendarId, workspaceRoot) {
2187
2251
  * would advance the baseline past a record the pull deliberately left holding a
2188
2252
  * local edit, which is the silent overwrite this whole path exists to stop. */
2189
2253
  var heldBack = (unpushed, results) => /* @__PURE__ */ new Set([...allUnpushed(unpushed), ...results.flatMap((result) => result.withheld)]);
2190
- async function syncCalendarGroupNow(calendarId, collections, workspaceRoot) {
2191
- const unpushed = await pushAndProtect(collections, workspaceRoot);
2192
- const baseline = await loadCalendarShadow(calendarId, workspaceRoot);
2254
+ /** The collections in this group that have never received the whole calendar,
2255
+ * by slug. Asked of each collection's OWN records (`calendarBackfillState.ts`),
2256
+ * because the sync token cannot answer it — it is keyed by calendar and shared
2257
+ * by every consumer of that calendar (#2850). */
2258
+ async function collectionsNeedingBackfill(collections) {
2259
+ return (await Promise.all(collections.map(async (collection) => {
2260
+ return await needsCalendarBackfill(collection.dataDir, collection.schema.googleCalendar?.calendarId) ? collection.slug : null;
2261
+ }))).filter((slug) => slug !== null);
2262
+ }
2263
+ /** The token this run may resume from — none while any collection in the group
2264
+ * still needs the whole calendar.
2265
+ *
2266
+ * A stored token says how far THE CALENDAR has been read, by whoever read it:
2267
+ * the standalone `google` tool's `calendarSync`, or a sibling collection that
2268
+ * synced before this one existed. Resuming from it hands a brand-new collection
2269
+ * a delta of a window it never received — a handful of records, reported as a
2270
+ * success with no error, and never the history the docs promise (#2850).
2271
+ *
2272
+ * The full walk fans out to the whole group, which is free: writes are upserts,
2273
+ * so the collections that were already current simply rewrite what they hold. */
2274
+ async function resumableToken(calendarId, collections, workspaceRoot) {
2275
+ const pending = await collectionsNeedingBackfill(collections);
2276
+ if (pending.length === 0) return await loadCalendarSyncToken(calendarId, workspaceRoot) ?? void 0;
2277
+ log.info("google", "walking the whole calendar — these collections have never received it", {
2278
+ calendarId,
2279
+ collections: pending
2280
+ });
2281
+ }
2282
+ /** Record the backfill for every collection the window landed in. Called only
2283
+ * after a FULL walk that fully landed: marking one after an incremental window
2284
+ * would claim a history the records do not hold. */
2285
+ async function markGroupBackfilled(calendarId, collections) {
2286
+ const walkedAt = (/* @__PURE__ */ new Date()).toISOString();
2287
+ await Promise.all(collections.map((collection) => markCalendarBackfilled(collection.dataDir, collection.schema.googleCalendar?.calendarId, walkedAt)));
2288
+ }
2289
+ /** What a partial window reports. Names the cause the user can actually act on:
2290
+ * a page guard is reached when Google has to expand an enormous number of
2291
+ * recurring instances, and one series with no end date is enough. */
2292
+ 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.";
2293
+ /** A truncated walk must not read as a completed one (#2850).
2294
+ *
2295
+ * Added AFTER the token/baseline gate, not through it. A short fetch is not a
2296
+ * failed write: the events that did arrive are Google's own and landed
2297
+ * correctly, so their baseline is true and holding it back would freeze them —
2298
+ * a record with no baseline reads as an unsent local edit, which the next pull
2299
+ * then refuses to touch (#2683). What must not happen is the walk claiming
2300
+ * completeness, and that is covered without the gate: `nextSyncToken` only
2301
+ * appears on Google's last page, so a truncated walk has no token to advance,
2302
+ * and the backfill marker is withheld separately. */
2303
+ var withPartialWindowError = (results, pagesExhausted) => pagesExhausted ? results.map((result) => ({
2304
+ ...result,
2305
+ errors: [...result.errors, PARTIAL_CALENDAR_WINDOW]
2306
+ })) : [...results];
2307
+ /** `landed` is the write-side verdict (`windowFullyLanded`); the rest describe
2308
+ * the window Google returned.
2309
+ *
2310
+ * A page-capped walk (`pagesExhausted`) keeps its baseline — the events that
2311
+ * arrived are Google's own and were written correctly — but must NOT claim the
2312
+ * backfill, or the collection would stop asking for the rest of its calendar.
2313
+ * Its token is refused too, though Google makes that moot by sending
2314
+ * `nextSyncToken` only on the last page. */
2315
+ function windowAdvance(window) {
2316
+ if (!window.landed) return {
2317
+ baseline: false,
2318
+ backfill: false,
2319
+ token: false
2320
+ };
2321
+ const complete = !window.pagesExhausted;
2322
+ return {
2323
+ baseline: true,
2324
+ backfill: window.walkedInFull && complete,
2325
+ token: complete && window.nextSyncToken !== void 0
2326
+ };
2327
+ }
2328
+ /** The window this run should read, and whether it was a full walk. A 410
2329
+ * restart is a full walk too, so it backfills just as well as a forced one. */
2330
+ async function readWindow(calendarId, collections, workspaceRoot) {
2193
2331
  const accessToken = await getGoogleAccessToken();
2332
+ const resumeFrom = await resumableToken(calendarId, collections, workspaceRoot);
2194
2333
  const first = await syncCalendarEvents(accessToken, {
2195
2334
  calendarId,
2196
- syncToken: await loadCalendarSyncToken(calendarId, workspaceRoot) ?? void 0
2335
+ syncToken: resumeFrom
2197
2336
  });
2198
2337
  const result = first.fullResyncRequired ? await restartFullSync(accessToken, calendarId, workspaceRoot) : first;
2199
- const results = await applyWindowToGroup(collections, result.events, workspaceRoot, unpushed, baseline);
2200
- if (windowFullyLanded(calendarId, results)) {
2201
- await saveCalendarShadow(calendarId, shadowUpdates(result.events, heldBack(unpushed, results), baseline), workspaceRoot);
2202
- if (result.nextSyncToken) await advanceToken(calendarId, result.nextSyncToken, collections, workspaceRoot);
2203
- }
2204
- return results;
2338
+ if (result.pagesExhausted) log.warn("google", "the calendar walk ran out of pages — only part of it was copied", {
2339
+ calendarId,
2340
+ events: result.events.length
2341
+ });
2342
+ return {
2343
+ result,
2344
+ walkedInFull: resumeFrom === void 0 || first.fullResyncRequired
2345
+ };
2346
+ }
2347
+ async function syncCalendarGroupNow(calendarId, collections, workspaceRoot) {
2348
+ const unpushed = await pushAndProtect(collections, workspaceRoot);
2349
+ const baseline = await loadCalendarShadow(calendarId, workspaceRoot);
2350
+ const { result, walkedInFull } = await readWindow(calendarId, collections, workspaceRoot);
2351
+ const applied = await applyWindowToGroup(collections, result.events, workspaceRoot, unpushed, baseline);
2352
+ const advance = windowAdvance({
2353
+ landed: windowFullyLanded(calendarId, applied),
2354
+ walkedInFull,
2355
+ pagesExhausted: result.pagesExhausted,
2356
+ nextSyncToken: result.nextSyncToken
2357
+ });
2358
+ if (advance.baseline) await saveCalendarShadow(calendarId, shadowUpdates(result.events, heldBack(unpushed, applied), baseline), workspaceRoot);
2359
+ if (advance.backfill) await markGroupBackfilled(calendarId, collections);
2360
+ if (advance.token && result.nextSyncToken) await advanceToken(calendarId, result.nextSyncToken, collections, workspaceRoot);
2361
+ return withPartialWindowError(applied, result.pagesExhausted);
2205
2362
  }
2206
2363
  /** Apply one window to every collection on the calendar, honouring what each
2207
2364
  * one's own push protected. A collection whose protection could not be worked
@@ -2437,24 +2594,29 @@ async function syncDueCalendarCollections(workspaceRoot, intervalMs = DEFAULT_SY
2437
2594
  const windowMs = calendarSyncDueWindowMs(intervalMs);
2438
2595
  return await runCalendarGroups(groups, workspaceRoot, (lastSyncedAt) => isCalendarSyncDue(lastSyncedAt, windowMs));
2439
2596
  }
2440
- /** The groups whose calendar has never synced. A missing token IS the "created
2441
- * since the last sync" signal — nothing else distinguishes a new collection
2442
- * from an edited one on the write path this feeds (#2427).
2597
+ /** The groups holding a collection that still needs the whole calendar.
2598
+ *
2599
+ * This used to ask whether the CALENDAR had a stored token, which is not the
2600
+ * same question and is why the trigger silently did nothing for the #2850
2601
+ * reporter: their calendar already had a token — from the standalone `google`
2602
+ * tool, and on later attempts from the collection they had just deleted by
2603
+ * hand — so the brand-new collection matched nothing and its first sync never
2604
+ * ran at all. `pending` is injected so the rule is testable without a
2605
+ * workspace on disk.
2443
2606
  *
2444
- * Self-silencing by construction: the first sync stores a token, so a calendar
2445
- * matches at most once. `loadToken` is injected so the rule is testable without
2446
- * a workspace on disk. */
2447
- async function unsyncedGroups(groups, loadToken) {
2448
- const checked = await Promise.all([...groups].map(async (entry) => await loadToken(entry[0]) === null ? entry : null));
2607
+ * Still self-silencing: a landed full walk marks its collections, so a group
2608
+ * stops matching once every collection in it holds the history. */
2609
+ async function groupsNeedingBackfill(groups, pending) {
2610
+ const checked = await Promise.all([...groups].map(async (entry) => await pending(entry[1]) ? entry : null));
2449
2611
  return new Map(checked.filter((entry) => entry !== null));
2450
2612
  }
2451
- /** Sync only the calendars that have never synced — the first sync for a
2452
- * just-created collection, which otherwise stays empty until the hourly
2453
- * scheduler run (#2427). Cheap and safe to call on every config write. */
2613
+ /** Sync only the calendars a collection has never received in full — the first
2614
+ * sync for a just-created collection, which otherwise stays empty until the
2615
+ * hourly scheduler run (#2427). Cheap and safe to call on every config write. */
2454
2616
  async function syncNewCalendarCollections(workspaceRoot) {
2455
- const pending = await unsyncedGroups(await declaringGroups(workspaceRoot), (calendarId) => loadCalendarSyncToken(calendarId, workspaceRoot));
2617
+ const pending = await groupsNeedingBackfill(await declaringGroups(workspaceRoot), async (collections) => (await collectionsNeedingBackfill(collections)).length > 0);
2456
2618
  if (!await backgroundSyncAllowed(pending)) return [];
2457
- log.info("google", "running the first sync for newly declared calendars", { calendars: [...pending.keys()] });
2619
+ log.info("google", "running the first sync for calendars a collection has never received in full", { calendars: [...pending.keys()] });
2458
2620
  return await runCalendarGroups(pending, workspaceRoot);
2459
2621
  }
2460
2622
  var liveManualSyncDeps = {
@@ -2717,6 +2879,7 @@ exports.HTTP_CONFLICT = HTTP_CONFLICT;
2717
2879
  exports.HTTP_FORBIDDEN = HTTP_FORBIDDEN;
2718
2880
  exports.HTTP_PRECONDITION_FAILED = HTTP_PRECONDITION_FAILED;
2719
2881
  exports.MAX_LIST_RESULTS = MAX_LIST_RESULTS;
2882
+ exports.PARTIAL_CALENDAR_WINDOW = PARTIAL_CALENDAR_WINDOW;
2720
2883
  exports.PROTECTION_UNKNOWN = PROTECTION_UNKNOWN;
2721
2884
  exports.PUSHABLE_SOURCE_FIELDS = PUSHABLE_SOURCE_FIELDS;
2722
2885
  exports.allUnpushed = allUnpushed;
@@ -2734,6 +2897,7 @@ exports.buildMultipartBody = buildMultipartBody;
2734
2897
  exports.buildTaskPatch = buildTaskPatch;
2735
2898
  exports.bySourceField = bySourceField;
2736
2899
  exports.calendarApiError = calendarApiError;
2900
+ exports.calendarBackfillPath = calendarBackfillPath;
2737
2901
  exports.calendarPushStatePath = calendarPushStatePath;
2738
2902
  exports.calendarSyncDueWindowMs = calendarSyncDueWindowMs;
2739
2903
  exports.calendarSyncStatePath = calendarSyncStatePath;
@@ -2747,6 +2911,7 @@ exports.clearCalendarShadow = clearCalendarShadow;
2747
2911
  exports.clearCalendarSyncToken = clearCalendarSyncToken;
2748
2912
  exports.clientSecretPresence = clientSecretPresence;
2749
2913
  exports.collectCalendarPages = collectCalendarPages;
2914
+ exports.collectionsNeedingBackfill = collectionsNeedingBackfill;
2750
2915
  exports.commitLinkedTokens = commitLinkedTokens;
2751
2916
  exports.completeTask = completeTask;
2752
2917
  exports.configureGoogleHost = configureGoogleHost;
@@ -2772,6 +2937,7 @@ exports.googleConfigDir = googleConfigDir;
2772
2937
  exports.googleSecretsDir = googleSecretsDir;
2773
2938
  exports.googleTokenPath = googleTokenPath;
2774
2939
  exports.groupByCalendar = groupByCalendar;
2940
+ exports.groupsNeedingBackfill = groupsNeedingBackfill;
2775
2941
  exports.heldBack = heldBack;
2776
2942
  exports.isCalendarSyncDue = isCalendarSyncDue;
2777
2943
  exports.isClientSettableEventId = isClientSettableEventId;
@@ -2795,10 +2961,12 @@ exports.loadGoogleTokens = loadGoogleTokens;
2795
2961
  exports.locallyChangedFields = locallyChangedFields;
2796
2962
  exports.locallyDeletedIds = locallyDeletedIds;
2797
2963
  exports.locallyEditedIds = locallyEditedIds;
2964
+ exports.markCalendarBackfilled = markCalendarBackfilled;
2798
2965
  exports.mayAdoptExisting = mayAdoptExisting;
2799
2966
  exports.mergeGoogleTokens = mergeGoogleTokens;
2800
2967
  exports.mergeIntoExisting = require_project.mergeIntoExisting;
2801
2968
  exports.mergeShadow = mergeShadow;
2969
+ exports.needsCalendarBackfill = needsCalendarBackfill;
2802
2970
  exports.orphanedCalendarId = orphanedCalendarId;
2803
2971
  exports.pickBoundary = pickBoundary;
2804
2972
  exports.planRecord = planRecord;
@@ -2812,6 +2980,7 @@ exports.readDriveFile = readDriveFile;
2812
2980
  exports.releaseOrphanedCalendarToken = releaseOrphanedCalendarToken;
2813
2981
  exports.reportedAccessRole = reportedAccessRole;
2814
2982
  exports.resolveEventSpan = resolveEventSpan;
2983
+ exports.resumableToken = resumableToken;
2815
2984
  exports.saveCalendarShadow = saveCalendarShadow;
2816
2985
  exports.saveCalendarSyncToken = saveCalendarSyncToken;
2817
2986
  exports.saveGoogleTokens = saveGoogleTokens;
@@ -2822,6 +2991,7 @@ exports.syncCalendarForCollection = syncCalendarForCollection;
2822
2991
  exports.syncCalendarGroup = syncCalendarGroup;
2823
2992
  exports.syncDueCalendarCollections = syncDueCalendarCollections;
2824
2993
  exports.syncNewCalendarCollections = syncNewCalendarCollections;
2994
+ exports.syncPageParams = syncPageParams;
2825
2995
  exports.toCalendarMeta = toCalendarMeta;
2826
2996
  exports.toCalendarSummary = toCalendarSummary;
2827
2997
  exports.toCollectionDateTime = toCollectionDateTime;
@@ -2832,18 +3002,20 @@ exports.toGoogleEventTime = toGoogleEventTime;
2832
3002
  exports.toShadowEvent = toShadowEvent;
2833
3003
  exports.toTaskListSummary = toTaskListSummary;
2834
3004
  exports.toTaskSummary = toTaskSummary;
3005
+ exports.toolCalendarSyncKey = toolCalendarSyncKey;
2835
3006
  exports.uncompleteTask = uncompleteTask;
2836
3007
  exports.unlinkGoogle = unlinkGoogle;
2837
3008
  exports.unpushedFor = unpushedFor;
2838
3009
  exports.unsentEditGuard = unsentEditGuard;
2839
3010
  exports.unsentLocalEdits = unsentLocalEdits;
2840
- exports.unsyncedGroups = unsyncedGroups;
2841
3011
  exports.updateCalendarEvent = updateCalendarEvent;
2842
3012
  exports.updateTask = updateTask;
2843
3013
  exports.waitForAuthCode = waitForAuthCode;
3014
+ exports.windowAdvance = windowAdvance;
2844
3015
  exports.withCalendarLock = withCalendarLock;
2845
3016
  exports.withCalendarStateLock = withCalendarStateLock;
2846
3017
  exports.withKeyedLock = withKeyedLock;
3018
+ exports.withPartialWindowError = withPartialWindowError;
2847
3019
  exports.zoneSuffixOf = zoneSuffixOf;
2848
3020
 
2849
3021
  //# sourceMappingURL=index.cjs.map