@mulmoclaude/core 1.11.0 → 1.12.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.
@@ -0,0 +1,10 @@
1
+ /** How long a fresh marker suppresses a sync: one interval, less the slack.
2
+ * The slack is halved rather than subtracted flat for a very short configured
3
+ * interval, where a fixed five minutes would swallow the window whole. */
4
+ export declare function calendarSyncDueWindowMs(intervalMs: number): number;
5
+ /** True iff no recent enough run stands in for this one.
6
+ *
7
+ * A marker that cannot be read as a time is treated as due: a stuck calendar is
8
+ * worse than a duplicate run, which the sync is built to tolerate anyway
9
+ * (writes are upserts by event id). */
10
+ export declare function isCalendarSyncDue(lastSyncedAt: string | null, windowMs: number, now?: number): boolean;
@@ -3,5 +3,24 @@ export declare function calendarSyncStatePath(workspaceRoot?: string): string;
3
3
  export declare function loadCalendarSyncToken(calendarId?: string, workspaceRoot?: string): Promise<string | null>;
4
4
  export declare function saveCalendarSyncToken(calendarId: string | undefined, syncToken: string, workspaceRoot?: string): Promise<void>;
5
5
  /** Drop one calendar's token — used when Google answers 410, so the next run
6
- * starts a clean full sync. */
6
+ * starts a clean full sync. Leaves the sync marker alone: "restart the walk"
7
+ * says nothing about "nobody is running". */
7
8
  export declare function clearCalendarSyncToken(calendarId?: string, workspaceRoot?: string): Promise<void>;
9
+ export declare function loadCalendarLastSyncedAt(calendarId?: string, workspaceRoot?: string): Promise<string | null>;
10
+ /** Stamp when a sync of this calendar started, but only if `mayClaim` accepts
11
+ * what the marker currently says. Answers whether THIS call is the one that
12
+ * claimed it.
13
+ *
14
+ * The decision and the stamp share one pass through the queue on purpose.
15
+ * Deciding from a snapshot read earlier — as a caller filtering a whole map of
16
+ * calendars up front must — leaves both hosts believing the calendar is free,
17
+ * which is the race this exists to close (Codex review #2680). What remains is
18
+ * the read→write gap BETWEEN processes; closing that needs a real file lock
19
+ * (#2679), and this stays a soft dedup until then.
20
+ *
21
+ * The time is passed in rather than read here so the rule stays testable
22
+ * without freezing the clock. */
23
+ export declare function claimCalendarSyncIfDue(calendarId: string | undefined, startedAt: string, mayClaim: (lastSyncedAt: string | null) => boolean, workspaceRoot?: string): Promise<boolean>;
24
+ /** Drop one calendar's marker, so the next tick — on either host — may sync it
25
+ * immediately instead of waiting the window out. */
26
+ export declare function clearCalendarLastSyncedAt(calendarId?: string, workspaceRoot?: string): Promise<void>;
@@ -74,6 +74,12 @@ export declare const unpushedFor: (unpushed: UnpushedBySlug, slug: string) => Re
74
74
  * A `null` (unknown) entry contributes nothing, because that collection reports
75
75
  * a retryable error instead — which stops the baseline being saved at all. */
76
76
  export declare const allUnpushed: (unpushed: UnpushedBySlug) => ReadonlySet<string>;
77
+ /** Whether a run may take the calendar, given what the shared marker says.
78
+ *
79
+ * The scheduled door defers to it; every user-facing door claims regardless,
80
+ * because a Refresh click that silently returns nothing reads as an empty
81
+ * calendar, not as "another host has this". */
82
+ export type ClaimGuard = (lastSyncedAt: string | null) => boolean;
77
83
  /** Sync ONE calendar and fan its events out to every collection bound to it.
78
84
  *
79
85
  * The fan-out is not an optimisation, it is correctness: the sync token is
@@ -88,8 +94,9 @@ export declare const allUnpushed: (unpushed: UnpushedBySlug) => ReadonlySet<stri
88
94
  * load the SAME stored token and walk the same window. That is idempotent —
89
95
  * writes are upserts by event id — but it is a wasted full walk. Queued, the
90
96
  * second pass resumes from the token the first just stored and fetches only
91
- * what is genuinely new. */
92
- export declare function syncCalendarGroup(calendarId: string | undefined, collections: readonly LoadedCollection[], workspaceRoot: string): Promise<CalendarCollectionSyncResult[]>;
97
+ * what is genuinely new. That queue is module state, so it orders the doors
98
+ * into THIS process only; `claimThenSync` is what other hosts can see. */
99
+ export declare function syncCalendarGroup(calendarId: string | undefined, collections: readonly LoadedCollection[], workspaceRoot: string, mayClaim?: ClaimGuard): Promise<CalendarCollectionSyncResult[]>;
93
100
  /** The baseline this window establishes: what Google now says per event, and
94
101
  * `null` for a cancelled one so a recreate cannot resume from a dead baseline.
95
102
  *
@@ -139,8 +146,15 @@ export declare function orphanedCalendarId(deleted: CalendarDeclaring, remaining
139
146
  * Returns the cleared calendar id, or null when nothing was cleared. Never
140
147
  * throws: a failed cleanup must not fail the delete it follows. */
141
148
  export declare function releaseOrphanedCalendarToken(deleted: CalendarDeclaring, workspaceRoot: string): Promise<string | null>;
142
- /** Sync every collection that declares `googleCalendar`. */
143
- export declare function syncDueCalendarCollections(workspaceRoot: string): Promise<CalendarCollectionSyncResult[]>;
149
+ /** Sync every collection whose calendar is due — no host in this workspace has
150
+ * started one within `intervalMs` (#2678). Without that gate this walked every
151
+ * declaring group on every tick, so a second host registering the same task
152
+ * simply doubled the runs, concurrently.
153
+ *
154
+ * Dueness is not decided here, only described: the guard is handed down and
155
+ * evaluated where the marker is written, so the answer cannot go stale between
156
+ * deciding and claiming (Codex review #2680). */
157
+ export declare function syncDueCalendarCollections(workspaceRoot: string, intervalMs?: number): Promise<CalendarCollectionSyncResult[]>;
144
158
  /** The groups whose calendar has never synced. A missing token IS the "created
145
159
  * since the last sync" signal — nothing else distinguishes a new collection
146
160
  * from an edited one on the write path this feeds (#2427).
@@ -184,7 +198,12 @@ export interface ManualCalendarSyncDeps {
184
198
  * calendar sends them fixing the wrong thing. */
185
199
  export declare function syncCalendarForCollection(slug: string, workspaceRoot: string, deps?: ManualCalendarSyncDeps): Promise<ManualCalendarSyncOutcome>;
186
200
  /** Scheduler registration, shaped like `feedRefreshTaskDef` so hosts wire it
187
- * with a single line. */
201
+ * with a single line.
202
+ *
203
+ * `run` reads the interval back off the definition instead of closing over the
204
+ * option: a host rewrites `schedule` from its own overrides file AFTER this
205
+ * returns, and the due window has to follow it. Frozen at the default, a
206
+ * shortened interval would tick often and skip nearly every tick. */
188
207
  export declare function googleCalendarSyncTaskDef(opts?: {
189
208
  workspaceRoot?: string;
190
209
  intervalMs?: number;
@@ -845,34 +845,128 @@ function calendarSyncStatePath(workspaceRoot) {
845
845
  return node_path.default.join(workspaceRoot ?? require_discovery.getWorkspaceRoot(), "data", "calendar", ".sync-state.json");
846
846
  }
847
847
  var calendarKey = canonicalCalendarId;
848
+ /** Read one map out of whatever is on disk. Tolerant on purpose: a file written
849
+ * before `lastSyncedAt` existed simply lacks the key, and a hand-edited one may
850
+ * hold anything. A dropped entry costs a full re-walk or a duplicate run, never
851
+ * data — throwing here would stop the sync entirely. */
852
+ function stringRecord(value) {
853
+ if (typeof value !== "object" || value === null) return {};
854
+ return Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === "string"));
855
+ }
848
856
  async function readState$1(workspaceRoot) {
849
857
  const stored = await readJsonOrNull(calendarSyncStatePath(workspaceRoot));
850
- return stored?.tokens ? stored : { tokens: {} };
858
+ return {
859
+ tokens: stringRecord(stored?.tokens),
860
+ lastSyncedAt: stringRecord(stored?.lastSyncedAt)
861
+ };
851
862
  }
852
863
  var writeQueue$1 = { tail: Promise.resolve() };
853
- async function updateState$1(mutate, workspaceRoot) {
864
+ /** Decide from the stored state and write the result, both inside the queue, so
865
+ * nothing in this process can read between the two. A `null` state skips the
866
+ * write; `result` is what the caller learns about the decision. */
867
+ async function updateStateWith(decide, workspaceRoot) {
854
868
  const run = writeQueue$1.tail.then(async () => {
855
- const state = await readState$1(workspaceRoot);
856
- await writeJsonAtomicWithMode(calendarSyncStatePath(workspaceRoot), { tokens: mutate(state.tokens) }, SYNC_STATE_MODE);
869
+ const { state, result } = decide(await readState$1(workspaceRoot));
870
+ if (state) await writeJsonAtomicWithMode(calendarSyncStatePath(workspaceRoot), state, SYNC_STATE_MODE);
871
+ return result;
857
872
  });
858
873
  writeQueue$1.tail = run.catch(() => void 0);
859
874
  return await run;
860
875
  }
876
+ async function updateState$1(mutate, workspaceRoot) {
877
+ await updateStateWith((state) => ({
878
+ state: mutate(state),
879
+ result: void 0
880
+ }), workspaceRoot);
881
+ }
882
+ var withoutKey = (record, dropped) => Object.fromEntries(Object.entries(record).filter(([key]) => key !== dropped));
861
883
  async function loadCalendarSyncToken(calendarId, workspaceRoot) {
862
884
  return (await readState$1(workspaceRoot)).tokens[calendarKey(calendarId)] ?? null;
863
885
  }
864
886
  async function saveCalendarSyncToken(calendarId, syncToken, workspaceRoot) {
865
887
  const stored = calendarKey(calendarId);
866
- await updateState$1((tokens) => ({
867
- ...tokens,
868
- [stored]: syncToken
888
+ await updateState$1((state) => ({
889
+ ...state,
890
+ tokens: {
891
+ ...state.tokens,
892
+ [stored]: syncToken
893
+ }
869
894
  }), workspaceRoot);
870
895
  }
871
896
  /** Drop one calendar's token — used when Google answers 410, so the next run
872
- * starts a clean full sync. */
897
+ * starts a clean full sync. Leaves the sync marker alone: "restart the walk"
898
+ * says nothing about "nobody is running". */
873
899
  async function clearCalendarSyncToken(calendarId, workspaceRoot) {
874
900
  const dropped = calendarKey(calendarId);
875
- await updateState$1((tokens) => Object.fromEntries(Object.entries(tokens).filter(([key]) => key !== dropped)), workspaceRoot);
901
+ await updateState$1((state) => ({
902
+ ...state,
903
+ tokens: withoutKey(state.tokens, dropped)
904
+ }), workspaceRoot);
905
+ }
906
+ async function loadCalendarLastSyncedAt(calendarId, workspaceRoot) {
907
+ return (await readState$1(workspaceRoot)).lastSyncedAt[calendarKey(calendarId)] ?? null;
908
+ }
909
+ /** Stamp when a sync of this calendar started, but only if `mayClaim` accepts
910
+ * what the marker currently says. Answers whether THIS call is the one that
911
+ * claimed it.
912
+ *
913
+ * The decision and the stamp share one pass through the queue on purpose.
914
+ * Deciding from a snapshot read earlier — as a caller filtering a whole map of
915
+ * calendars up front must — leaves both hosts believing the calendar is free,
916
+ * which is the race this exists to close (Codex review #2680). What remains is
917
+ * the read→write gap BETWEEN processes; closing that needs a real file lock
918
+ * (#2679), and this stays a soft dedup until then.
919
+ *
920
+ * The time is passed in rather than read here so the rule stays testable
921
+ * without freezing the clock. */
922
+ async function claimCalendarSyncIfDue(calendarId, startedAt, mayClaim, workspaceRoot) {
923
+ const stored = calendarKey(calendarId);
924
+ return await updateStateWith((state) => {
925
+ const claimed = mayClaim(state.lastSyncedAt[stored] ?? null);
926
+ return {
927
+ state: claimed ? {
928
+ ...state,
929
+ lastSyncedAt: {
930
+ ...state.lastSyncedAt,
931
+ [stored]: startedAt
932
+ }
933
+ } : null,
934
+ result: claimed
935
+ };
936
+ }, workspaceRoot);
937
+ }
938
+ /** Drop one calendar's marker, so the next tick — on either host — may sync it
939
+ * immediately instead of waiting the window out. */
940
+ async function clearCalendarLastSyncedAt(calendarId, workspaceRoot) {
941
+ const dropped = calendarKey(calendarId);
942
+ await updateState$1((state) => ({
943
+ ...state,
944
+ lastSyncedAt: withoutKey(state.lastSyncedAt, dropped)
945
+ }), workspaceRoot);
946
+ }
947
+ //#endregion
948
+ //#region src/google/calendarSyncDue.ts
949
+ /** Slack between a tick and the marker it writes. Without it a lone host reads
950
+ * its OWN marker as "not due yet" — elapsed lands a hair under the interval
951
+ * every time — and syncs every other hour. */
952
+ var TICK_JITTER_TOLERANCE_MS = 300 * 1e3;
953
+ /** How long a fresh marker suppresses a sync: one interval, less the slack.
954
+ * The slack is halved rather than subtracted flat for a very short configured
955
+ * interval, where a fixed five minutes would swallow the window whole. */
956
+ function calendarSyncDueWindowMs(intervalMs) {
957
+ return intervalMs - Math.min(TICK_JITTER_TOLERANCE_MS, intervalMs / 2);
958
+ }
959
+ /** True iff no recent enough run stands in for this one.
960
+ *
961
+ * A marker that cannot be read as a time is treated as due: a stuck calendar is
962
+ * worse than a duplicate run, which the sync is built to tolerate anyway
963
+ * (writes are upserts by event id). */
964
+ function isCalendarSyncDue(lastSyncedAt, windowMs, now = Date.now()) {
965
+ if (!lastSyncedAt) return true;
966
+ const elapsed = now - Date.parse(lastSyncedAt);
967
+ if (!Number.isFinite(elapsed)) return true;
968
+ if (elapsed < -windowMs) return true;
969
+ return elapsed >= windowMs;
876
970
  }
877
971
  //#endregion
878
972
  //#region src/google/calendarPushState.ts
@@ -1786,6 +1880,7 @@ async function restartFullSync(accessToken, calendarId, workspaceRoot) {
1786
1880
  await clearCalendarShadow(calendarId, workspaceRoot);
1787
1881
  return await syncCalendarEvents(accessToken, { calendarId });
1788
1882
  }
1883
+ var ALWAYS_CLAIM = () => true;
1789
1884
  /** Sync ONE calendar and fan its events out to every collection bound to it.
1790
1885
  *
1791
1886
  * The fan-out is not an optimisation, it is correctness: the sync token is
@@ -1800,9 +1895,59 @@ async function restartFullSync(accessToken, calendarId, workspaceRoot) {
1800
1895
  * load the SAME stored token and walk the same window. That is idempotent —
1801
1896
  * writes are upserts by event id — but it is a wasted full walk. Queued, the
1802
1897
  * second pass resumes from the token the first just stored and fetches only
1803
- * what is genuinely new. */
1804
- async function syncCalendarGroup(calendarId, collections, workspaceRoot) {
1805
- return await withCalendarLock(calendarId, () => syncCalendarGroupNow(calendarId, collections, workspaceRoot));
1898
+ * what is genuinely new. That queue is module state, so it orders the doors
1899
+ * into THIS process only; `claimThenSync` is what other hosts can see. */
1900
+ async function syncCalendarGroup(calendarId, collections, workspaceRoot, mayClaim = ALWAYS_CLAIM) {
1901
+ return await withCalendarLock(calendarId, () => claimThenSync(calendarId, collections, workspaceRoot, mayClaim));
1902
+ }
1903
+ /** Take the workspace-shared marker BEFORE the calendar is touched, sync only if
1904
+ * this run got it, and drop it again if the run could not happen at all.
1905
+ *
1906
+ * Claiming first is the whole point: hosts tick in the same minute (interval
1907
+ * schedules align to wall-clock boundaries), so a marker written on completion
1908
+ * would leave the entire run — minutes, for a first full walk — open for a
1909
+ * second host to start alongside this one and lose the push baseline it writes.
1910
+ *
1911
+ * Released only when the sync could not run at all. A run that finished with
1912
+ * retryable per-collection errors KEEPS the marker: it already held the token
1913
+ * and the baseline back, so it replays on the next tick either way, and
1914
+ * releasing would hand a permanently-failing calendar back to both hosts at
1915
+ * once — the very overlap this guards (observed during Claude review). */
1916
+ async function claimThenSync(calendarId, collections, workspaceRoot, mayClaim) {
1917
+ if (!await claimCalendarSync(calendarId, workspaceRoot, mayClaim)) {
1918
+ log.info("google", "skipping a calendar this workspace synced recently — another host may be on it", { calendarId });
1919
+ return [];
1920
+ }
1921
+ try {
1922
+ return await syncCalendarGroupNow(calendarId, collections, workspaceRoot);
1923
+ } catch (error) {
1924
+ await releaseCalendarSyncClaim(calendarId, workspaceRoot);
1925
+ throw error;
1926
+ }
1927
+ }
1928
+ /** Never throws, and fails OPEN: the marker guards against duplicate work, it is
1929
+ * not a precondition for syncing correctly. A workspace that cannot store it
1930
+ * syncs the way it did before #2678. */
1931
+ async function claimCalendarSync(calendarId, workspaceRoot, mayClaim) {
1932
+ try {
1933
+ return await claimCalendarSyncIfDue(calendarId, (/* @__PURE__ */ new Date()).toISOString(), mayClaim, workspaceRoot);
1934
+ } catch (error) {
1935
+ log.warn("google", "could not stamp the calendar sync marker — another host may sync this calendar in parallel", {
1936
+ calendarId,
1937
+ error: String(error)
1938
+ });
1939
+ return true;
1940
+ }
1941
+ }
1942
+ async function releaseCalendarSyncClaim(calendarId, workspaceRoot) {
1943
+ try {
1944
+ await clearCalendarLastSyncedAt(calendarId, workspaceRoot);
1945
+ } catch (error) {
1946
+ log.warn("google", "could not release the calendar sync marker after a failed run", {
1947
+ calendarId,
1948
+ error: String(error)
1949
+ });
1950
+ }
1806
1951
  }
1807
1952
  async function syncCalendarGroupNow(calendarId, collections, workspaceRoot) {
1808
1953
  const unpushed = await pushAutoCollections(collections, workspaceRoot);
@@ -1963,6 +2108,7 @@ async function releaseOrphanedCalendarToken(deleted, workspaceRoot) {
1963
2108
  if (orphaned === null) return null;
1964
2109
  await clearCalendarSyncToken(orphaned, workspaceRoot);
1965
2110
  await clearCalendarShadow(orphaned, workspaceRoot);
2111
+ await clearCalendarLastSyncedAt(orphaned, workspaceRoot);
1966
2112
  log.info("google", "cleared the sync token of a calendar no collection reads any more", { calendarId: orphaned });
1967
2113
  return orphaned;
1968
2114
  } catch (error) {
@@ -1979,19 +2125,27 @@ async function declaringGroups(workspaceRoot) {
1979
2125
  * Authoring the collection before linking the account is an expected state,
1980
2126
  * not a failure. Checking once here keeps it a quiet skip instead of an
1981
2127
  * access-token throw per calendar, every hour, until the user links (#2188).
1982
- * A user-triggered sync answers differently — it says so out loud. */
2128
+ * A user-triggered sync answers differently — it says so out loud.
2129
+ *
2130
+ * Deliberately not phrased as a per-host state: the link lives in
2131
+ * `~/.config/mulmo`, which carries no app name, so every host on the machine
2132
+ * shares one answer. Reading it as "linked on this host only" is what made the
2133
+ * calendar look implicitly exclusive when it never was (#2678). */
1983
2134
  async function backgroundSyncAllowed(groups) {
1984
2135
  if (groups.size === 0) return false;
1985
2136
  if (await isGoogleLinked()) return true;
1986
- log.info("google", "skipping calendar sync — no Google account linked on this host", { calendars: groups.size });
2137
+ log.info("google", "skipping calendar sync — no Google account linked", { calendars: groups.size });
1987
2138
  return false;
1988
2139
  }
1989
2140
  /** Run each group, isolating failures per calendar — one unreachable calendar
1990
- * (or a revoked grant) must not stop the others. */
1991
- async function runCalendarGroups(groups, workspaceRoot) {
2141
+ * (or a revoked grant) must not stop the others. `mayClaim` is evaluated per
2142
+ * calendar as it comes up, never once for the whole map: an earlier calendar's
2143
+ * full walk takes minutes, and a decision made before it started says nothing
2144
+ * about who holds this one now. */
2145
+ async function runCalendarGroups(groups, workspaceRoot, mayClaim = ALWAYS_CLAIM) {
1992
2146
  const results = [];
1993
2147
  for (const [calendarId, collections] of groups) try {
1994
- results.push(...await syncCalendarGroup(calendarId, collections, workspaceRoot));
2148
+ results.push(...await syncCalendarGroup(calendarId, collections, workspaceRoot, mayClaim));
1995
2149
  } catch (error) {
1996
2150
  log.warn("google", "calendar sync failed", {
1997
2151
  calendarId,
@@ -2007,10 +2161,19 @@ async function runCalendarGroups(groups, workspaceRoot) {
2007
2161
  }
2008
2162
  return results;
2009
2163
  }
2010
- /** Sync every collection that declares `googleCalendar`. */
2011
- async function syncDueCalendarCollections(workspaceRoot) {
2164
+ /** Sync every collection whose calendar is due — no host in this workspace has
2165
+ * started one within `intervalMs` (#2678). Without that gate this walked every
2166
+ * declaring group on every tick, so a second host registering the same task
2167
+ * simply doubled the runs, concurrently.
2168
+ *
2169
+ * Dueness is not decided here, only described: the guard is handed down and
2170
+ * evaluated where the marker is written, so the answer cannot go stale between
2171
+ * deciding and claiming (Codex review #2680). */
2172
+ async function syncDueCalendarCollections(workspaceRoot, intervalMs = DEFAULT_SYNC_INTERVAL_MS) {
2012
2173
  const groups = await declaringGroups(workspaceRoot);
2013
- return await backgroundSyncAllowed(groups) ? await runCalendarGroups(groups, workspaceRoot) : [];
2174
+ if (!await backgroundSyncAllowed(groups)) return [];
2175
+ const windowMs = calendarSyncDueWindowMs(intervalMs);
2176
+ return await runCalendarGroups(groups, workspaceRoot, (lastSyncedAt) => isCalendarSyncDue(lastSyncedAt, windowMs));
2014
2177
  }
2015
2178
  /** The groups whose calendar has never synced. A missing token IS the "created
2016
2179
  * since the last sync" signal — nothing else distinguishes a new collection
@@ -2056,10 +2219,21 @@ async function syncCalendarForCollection(slug, workspaceRoot, deps = liveManualS
2056
2219
  results: await deps.runGroups(new Map(owning), workspaceRoot)
2057
2220
  };
2058
2221
  }
2222
+ /** The interval this task is scheduled on right now. A non-interval schedule
2223
+ * cannot be reached — the factory below builds an interval one — but the type
2224
+ * allows it, so it falls back rather than asserting. */
2225
+ function scheduledIntervalMs(task) {
2226
+ return task.schedule.type === _receptron_task_scheduler.SCHEDULE_TYPES.interval ? task.schedule.intervalMs : DEFAULT_SYNC_INTERVAL_MS;
2227
+ }
2059
2228
  /** Scheduler registration, shaped like `feedRefreshTaskDef` so hosts wire it
2060
- * with a single line. */
2229
+ * with a single line.
2230
+ *
2231
+ * `run` reads the interval back off the definition instead of closing over the
2232
+ * option: a host rewrites `schedule` from its own overrides file AFTER this
2233
+ * returns, and the due window has to follow it. Frozen at the default, a
2234
+ * shortened interval would tick often and skip nearly every tick. */
2061
2235
  function googleCalendarSyncTaskDef(opts) {
2062
- return {
2236
+ const def = {
2063
2237
  id: GOOGLE_CALENDAR_SYNC_TASK_ID,
2064
2238
  name: "Google Calendar sync",
2065
2239
  description: "Pulls changed Google Calendar events into any collection declaring `googleCalendar`, without invoking the LLM.",
@@ -2068,8 +2242,9 @@ function googleCalendarSyncTaskDef(opts) {
2068
2242
  intervalMs: opts?.intervalMs ?? DEFAULT_SYNC_INTERVAL_MS
2069
2243
  },
2070
2244
  missedRunPolicy: _receptron_task_scheduler.MISSED_RUN_POLICIES.runOnce,
2071
- run: () => syncDueCalendarCollections(opts?.workspaceRoot ?? require_discovery.getWorkspaceRoot()).then(() => {})
2245
+ run: () => syncDueCalendarCollections(opts?.workspaceRoot ?? require_discovery.getWorkspaceRoot(), scheduledIntervalMs(def)).then(() => {})
2072
2246
  };
2247
+ return def;
2073
2248
  }
2074
2249
  //#endregion
2075
2250
  //#region src/google/tasks.ts
@@ -2297,11 +2472,14 @@ exports.buildTaskPatch = buildTaskPatch;
2297
2472
  exports.bySourceField = bySourceField;
2298
2473
  exports.calendarApiError = calendarApiError;
2299
2474
  exports.calendarPushStatePath = calendarPushStatePath;
2475
+ exports.calendarSyncDueWindowMs = calendarSyncDueWindowMs;
2300
2476
  exports.calendarSyncStatePath = calendarSyncStatePath;
2301
2477
  exports.canonicalCalendarId = canonicalCalendarId;
2302
2478
  exports.canonicalTaskListId = canonicalTaskListId;
2479
+ exports.claimCalendarSyncIfDue = claimCalendarSyncIfDue;
2303
2480
  exports.classifyDelete = classifyDelete;
2304
2481
  exports.classifyWrite = classifyWrite;
2482
+ exports.clearCalendarLastSyncedAt = clearCalendarLastSyncedAt;
2305
2483
  exports.clearCalendarShadow = clearCalendarShadow;
2306
2484
  exports.clearCalendarSyncToken = clearCalendarSyncToken;
2307
2485
  exports.clientSecretPresence = clientSecretPresence;
@@ -2331,6 +2509,7 @@ exports.googleConfigDir = googleConfigDir;
2331
2509
  exports.googleSecretsDir = googleSecretsDir;
2332
2510
  exports.googleTokenPath = googleTokenPath;
2333
2511
  exports.groupByCalendar = groupByCalendar;
2512
+ exports.isCalendarSyncDue = isCalendarSyncDue;
2334
2513
  exports.isClientSettableEventId = isClientSettableEventId;
2335
2514
  exports.isDeniedAccessRole = isDeniedAccessRole;
2336
2515
  exports.isGoogleApiError = isGoogleApiError;
@@ -2343,6 +2522,7 @@ exports.listCalendars = listCalendars;
2343
2522
  exports.listDriveFiles = listDriveFiles;
2344
2523
  exports.listTaskLists = listTaskLists;
2345
2524
  exports.listTasks = listTasks;
2525
+ exports.loadCalendarLastSyncedAt = loadCalendarLastSyncedAt;
2346
2526
  exports.loadCalendarShadow = loadCalendarShadow;
2347
2527
  exports.loadCalendarSyncToken = loadCalendarSyncToken;
2348
2528
  exports.loadClientSecret = loadClientSecret;