@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.
- package/assets/helps/error-recovery.md +71 -0
- package/assets/helps/google-calendar-collection.md +10 -0
- package/dist/collection/registry/server/index.cjs +3 -3
- package/dist/collection/registry/server/index.cjs.map +1 -1
- package/dist/collection/registry/server/index.js +3 -3
- package/dist/collection/registry/server/index.js.map +1 -1
- package/dist/collection/server/discovery.d.ts +10 -2
- package/dist/collection/server/host.d.ts +27 -3
- package/dist/collection/server/index.cjs +2 -2
- package/dist/collection/server/index.js +2 -2
- package/dist/collection-watchers/index.cjs +2 -2
- package/dist/collection-watchers/index.js +2 -2
- package/dist/{discovery-CDOjfctD.cjs → discovery-CrA_EqUJ.cjs} +19 -7
- package/dist/discovery-CrA_EqUJ.cjs.map +1 -0
- package/dist/{discovery-CC7kuwHf.js → discovery-D7LCN8gJ.js} +19 -7
- package/dist/discovery-D7LCN8gJ.js.map +1 -0
- package/dist/feeds/server/index.cjs +2 -2
- package/dist/feeds/server/index.js +2 -2
- package/dist/google/calendar.d.ts +15 -0
- package/dist/google/calendarBackfillState.d.ts +19 -0
- package/dist/google/calendarSyncStore.d.ts +15 -0
- package/dist/google/collectionSync.d.ts +78 -11
- package/dist/google/index.cjs +207 -35
- package/dist/google/index.cjs.map +1 -1
- package/dist/google/index.d.ts +4 -3
- package/dist/google/index.js +197 -35
- package/dist/google/index.js.map +1 -1
- package/dist/{server-WU35UYlF.cjs → server-CorfPQVQ.cjs} +2 -2
- package/dist/{server-WU35UYlF.cjs.map → server-CorfPQVQ.cjs.map} +1 -1
- package/dist/{server-AA2kfDfk.js → server-D6QY853r.js} +2 -2
- package/dist/{server-AA2kfDfk.js.map → server-D6QY853r.js.map} +1 -1
- package/package.json +1 -1
- package/dist/discovery-CC7kuwHf.js.map +0 -1
- package/dist/discovery-CDOjfctD.cjs.map +0 -1
|
@@ -4,8 +4,8 @@ const require_dist = require("../../dist-CsgSfWwR.cjs");
|
|
|
4
4
|
const require_root = require("../../root-rPH6FGDT.cjs");
|
|
5
5
|
const require_calendarGrid = require("../../calendarGrid-p8K-7cuB.cjs");
|
|
6
6
|
const require_project = require("../../project-C1ep9pvo.cjs");
|
|
7
|
-
const require_discovery = require("../../discovery-
|
|
8
|
-
const require_server = require("../../server-
|
|
7
|
+
const require_discovery = require("../../discovery-CrA_EqUJ.cjs");
|
|
8
|
+
const require_server = require("../../server-CorfPQVQ.cjs");
|
|
9
9
|
const require_feeds_paths = require("../paths.cjs");
|
|
10
10
|
const require_ingestTypes = require("../../ingestTypes-CPR-h-OZ.cjs");
|
|
11
11
|
const require_notifier = require("../../notifier-DZJj0sOh.cjs");
|
|
@@ -2,8 +2,8 @@ import { a as isErrorWithCode, l as isUnknownArray, s as isRecord$1 } from "../.
|
|
|
2
2
|
import { t as canonicalRoot } from "../../root-BMroU_mB.js";
|
|
3
3
|
import { C as FEED_SCHEDULES, w as INGEST_KINDS } from "../../calendarGrid-Csy2rpjp.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-
|
|
6
|
-
import { A as buildCollectionActionSeedPrompt, P as readSkillTemplate, j as promptPathsFor } from "../../server-
|
|
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";
|
|
7
7
|
import { FEEDS_DIR, feedDir, feedStatePath, feedsRoot, ingestStateDir, ingestStatePath } from "../paths.js";
|
|
8
8
|
import { n as DEFAULT_FEED_MAX_ITEMS, r as isFeedSchedule, t as AGENT_INGEST_KIND } from "../../ingestTypes-F2IJhOLJ.js";
|
|
9
9
|
import { d as publish, n as clear } from "../../notifier-DhnwR82U.js";
|
|
@@ -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
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
*
|
|
238
|
-
*
|
|
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
|
package/dist/google/index.cjs
CHANGED
|
@@ -6,7 +6,7 @@ const require_root = require("../root-rPH6FGDT.cjs");
|
|
|
6
6
|
const require_utils_fetch = require("../utils/fetch.cjs");
|
|
7
7
|
const require_calendarGrid = require("../calendarGrid-p8K-7cuB.cjs");
|
|
8
8
|
const require_project = require("../project-C1ep9pvo.cjs");
|
|
9
|
-
const require_discovery = require("../discovery-
|
|
9
|
+
const require_discovery = require("../discovery-CrA_EqUJ.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
|
|
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
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
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:
|
|
2335
|
+
syncToken: resumeFrom
|
|
2197
2336
|
});
|
|
2198
2337
|
const result = first.fullResyncRequired ? await restartFullSync(accessToken, calendarId, workspaceRoot) : first;
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
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
|
|
2441
|
-
*
|
|
2442
|
-
*
|
|
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
|
-
*
|
|
2445
|
-
*
|
|
2446
|
-
|
|
2447
|
-
|
|
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
|
|
2452
|
-
* just-created collection, which otherwise stays empty until the
|
|
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
|
|
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
|
|
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
|