@mulmoclaude/core 1.10.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.
Files changed (34) hide show
  1. package/assets/helps/error-recovery.md +85 -0
  2. package/assets/helps/google-calendar-collection.md +41 -18
  3. package/dist/collection/core/schemaZ.d.ts +10 -1
  4. package/dist/collection/registry/server/index.cjs +2 -2
  5. package/dist/collection/registry/server/index.js +2 -2
  6. package/dist/collection/server/index.cjs +2 -2
  7. package/dist/collection/server/index.js +2 -2
  8. package/dist/collection-watchers/index.cjs +2 -2
  9. package/dist/collection-watchers/index.js +2 -2
  10. package/dist/{discovery-_fjsluLx.js → discovery-B4CZvrXR.js} +11 -3
  11. package/dist/{discovery-_fjsluLx.js.map → discovery-B4CZvrXR.js.map} +1 -1
  12. package/dist/{discovery-DYcR83qx.cjs → discovery-NRy3tyUA.cjs} +11 -3
  13. package/dist/{discovery-DYcR83qx.cjs.map → discovery-NRy3tyUA.cjs.map} +1 -1
  14. package/dist/feeds/server/index.cjs +2 -2
  15. package/dist/feeds/server/index.js +2 -2
  16. package/dist/google/calendar.d.ts +9 -0
  17. package/dist/google/calendarLock.d.ts +16 -0
  18. package/dist/google/calendarPushState.d.ts +1 -1
  19. package/dist/google/calendarSyncDue.d.ts +10 -0
  20. package/dist/google/calendarSyncStore.d.ts +20 -1
  21. package/dist/google/collectionProjection.d.ts +18 -0
  22. package/dist/google/collectionPush.d.ts +62 -0
  23. package/dist/google/collectionSync.d.ts +75 -28
  24. package/dist/google/index.cjs +1217 -771
  25. package/dist/google/index.cjs.map +1 -1
  26. package/dist/google/index.d.ts +6 -3
  27. package/dist/google/index.js +1204 -772
  28. package/dist/google/index.js.map +1 -1
  29. package/dist/google/pushPlan.d.ts +1 -1
  30. package/dist/{server-l4TZ5Lbf.cjs → server-7U-3DE2e.cjs} +2 -2
  31. package/dist/{server-l4TZ5Lbf.cjs.map → server-7U-3DE2e.cjs.map} +1 -1
  32. package/dist/{server-BlIrtGKl.js → server-DdyIvFhz.js} +2 -2
  33. package/dist/{server-BlIrtGKl.js.map → server-DdyIvFhz.js.map} +1 -1
  34. package/package.json +4 -4
@@ -1,8 +1,8 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_rolldown_runtime = require("../../rolldown-runtime-D6vf50IK.cjs");
3
3
  const require_calendarGrid = require("../../calendarGrid-CaS9er8i.cjs");
4
- const require_discovery = require("../../discovery-DYcR83qx.cjs");
5
- const require_server = require("../../server-l4TZ5Lbf.cjs");
4
+ const require_discovery = require("../../discovery-NRy3tyUA.cjs");
5
+ const require_server = require("../../server-7U-3DE2e.cjs");
6
6
  const require_feeds_paths = require("../paths.cjs");
7
7
  const require_ingestTypes = require("../../ingestTypes-D1GQdG8e.cjs");
8
8
  const require_notifier = require("../../notifier-tMsAXyXp.cjs");
@@ -1,6 +1,6 @@
1
1
  import { C as FEED_SCHEDULES, w as INGEST_KINDS } from "../../calendarGrid-CrGxeCOG.js";
2
- import { N as listItems, V as resolveDataDir, W as safeSlugName, d as storeFor, et as createForwardingLogger, n as discoverCollections, tt as createHostSlot } from "../../discovery-_fjsluLx.js";
3
- import { A as buildCollectionActionSeedPrompt, P as readSkillTemplate, j as promptPathsFor } from "../../server-BlIrtGKl.js";
2
+ import { N as listItems, V as resolveDataDir, W as safeSlugName, d as storeFor, et as createForwardingLogger, n as discoverCollections, tt as createHostSlot } from "../../discovery-B4CZvrXR.js";
3
+ import { A as buildCollectionActionSeedPrompt, P as readSkillTemplate, j as promptPathsFor } from "../../server-DdyIvFhz.js";
4
4
  import { FEEDS_DIR, feedDir, feedStatePath, feedsRoot, ingestStateDir, ingestStatePath } from "../paths.js";
5
5
  import { n as DEFAULT_FEED_MAX_ITEMS, r as isFeedSchedule, t as AGENT_INGEST_KIND } from "../../ingestTypes-XReG-li7.js";
6
6
  import { d as publish, n as clear } from "../../notifier-BdA5qzhe.js";
@@ -40,6 +40,7 @@ export type CalendarEventSpan = {
40
40
  export type CalendarEventInput = {
41
41
  summary: string;
42
42
  description?: string;
43
+ location?: string;
43
44
  /** Calendar to create the event on; defaults to the user's primary. */
44
45
  calendarId?: string;
45
46
  /** Event colour (Google event palette id "1".."11"); omit to inherit the calendar's colour. */
@@ -61,6 +62,8 @@ export interface UpdateCalendarEventInput {
61
62
  end?: CalendarEventTime;
62
63
  /** `""` clears the description; omit to leave it untouched. */
63
64
  description?: string;
65
+ /** `""` clears the location; omit to leave it untouched. */
66
+ location?: string;
64
67
  calendarId?: string;
65
68
  colorId?: string;
66
69
  /** Etag of the version this edit was computed against. Sent as `If-Match`, so
@@ -87,6 +90,12 @@ export interface CalendarEventSummary {
87
90
  status: string;
88
91
  /** Google event palette id ("1".."11"), or "" when the event inherits the calendar's colour. */
89
92
  colorId: string;
93
+ /** The event body, RAW. Google stores limited HTML here, and normalising it on
94
+ * the way in would silently strip the user's formatting on the way back out —
95
+ * a mirrored calendar has to survive the round trip, so any prettifying
96
+ * belongs to whatever displays it. */
97
+ description: string;
98
+ location: string;
90
99
  }
91
100
  export interface CalendarSummary {
92
101
  id: string;
@@ -0,0 +1,16 @@
1
+ /** Serialise `run` against whatever is already running for `key`.
2
+ *
3
+ * `locks` is passed in so the queuing rule is testable without module state;
4
+ * the key is dropped once nothing is queued behind it, so the map cannot grow
5
+ * an entry per calendar forever. A failed predecessor still releases the
6
+ * queue — `then(run, run)`. */
7
+ export declare function withKeyedLock<T>(locks: Map<string, Promise<unknown>>, key: string, run: () => Promise<T>): Promise<T>;
8
+ /** Serialise anything that touches ONE calendar's sync state.
9
+ *
10
+ * Shared with the push path (#2598), not only the pull doors: a push that
11
+ * overtakes an in-flight pull would record a baseline for events the pull is
12
+ * still writing, so the next push would diff against a future it never saw.
13
+ *
14
+ * NOT reentrant — a holder must call the lock-free form of whatever it drives,
15
+ * or it waits on itself forever. */
16
+ export declare function withCalendarLock<T>(calendarId: string | undefined, run: () => Promise<T>): Promise<T>;
@@ -1,7 +1,7 @@
1
1
  import { CalendarEventSummary } from './calendar.js';
2
2
  /** The pushable subset of an event. `htmlLink` and `status` are read-only on
3
3
  * Google's side, so a baseline for them could never be acted on. */
4
- export type ShadowEvent = Pick<CalendarEventSummary, "summary" | "start" | "end" | "colorId">;
4
+ export type ShadowEvent = Pick<CalendarEventSummary, "summary" | "start" | "end" | "colorId" | "description" | "location">;
5
5
  /** `<workspace>/data/calendar/.push-state.json` */
6
6
  export declare function calendarPushStatePath(workspaceRoot?: string): string;
7
7
  export declare const toShadowEvent: (event: CalendarEventSummary) => ShadowEvent;
@@ -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>;
@@ -0,0 +1,18 @@
1
+ import { CollectionFieldSpec, CollectionItem } from '../collection/core/schema.js';
2
+ import { GOOGLE_CALENDAR_SOURCE_FIELDS } from '../collection/core/schemaZ.js';
3
+ import { CalendarEventSummary } from './calendar.js';
4
+ /** The event fields a schema may map from — narrowed to real keys of
5
+ * `CalendarEventSummary` so the projection below needs no cast. */
6
+ export type GoogleCalendarSourceField = (typeof GOOGLE_CALENDAR_SOURCE_FIELDS)[number];
7
+ /** Project one Google event onto the collection's own field names. The
8
+ * primary field always takes the event id — upsert-by-id is what keeps the
9
+ * sync idempotent, so it is deliberately not remappable. */
10
+ export declare function toCollectionRecord(event: CalendarEventSummary, map: Record<string, GoogleCalendarSourceField>, primaryKey: string, fields: Record<string, CollectionFieldSpec>): CollectionItem;
11
+ /** Google's mapped values laid over whatever the record already holds.
12
+ *
13
+ * A record file is written whole (`writeItem`), so projecting alone would drop
14
+ * every column the map does not name — a collection cannot then carry a local
15
+ * note next to a mirrored event, because the next pull that touches that event
16
+ * silently deletes it. The projection still wins on the fields it covers: those
17
+ * are Google's to own. */
18
+ export declare function mergeIntoExisting(existing: CollectionItem | null, projected: CollectionItem): CollectionItem;
@@ -1,4 +1,8 @@
1
1
  import { LoadedCollection } from '../collection/server/discoveredCollection.js';
2
+ import { CollectionFieldSpec, CollectionItem } from '../collection/core/schema.js';
3
+ import { CalendarEventSummary } from './calendar.js';
4
+ import { ShadowEvent } from './calendarPushState.js';
5
+ import { PushableSourceField } from './pushPlan.js';
2
6
  /** What the up-front writability gate knows about the target calendar. */
3
7
  export interface CalendarWriteTarget {
4
8
  /** The role from the user's calendar LIST, or null when the calendar is not in
@@ -27,6 +31,15 @@ export interface CalendarCollectionPushResult {
27
31
  /** Records that cannot be pushed as they stand, each with the reason. */
28
32
  skipped: string[];
29
33
  errors: string[];
34
+ /** Records whose local edit did NOT reach Google and would be destroyed by
35
+ * the pull that follows an automatic push (#2620).
36
+ *
37
+ * Only the two states where overwriting loses work with nowhere to recover
38
+ * it from: a both-sides conflict, and an unexpected failure. A `skipped`
39
+ * record is deliberately absent — several of its reasons (an id already
40
+ * taken in Google, an unmappable span) are recovered BY letting the pull
41
+ * write, so protecting them would stall the collection instead. */
42
+ unpushedIds: string[];
30
43
  }
31
44
  export type CalendarPushOutcome = {
32
45
  kind: "pushed";
@@ -55,6 +68,54 @@ export interface CalendarPushDeps {
55
68
  accessToken: () => Promise<string>;
56
69
  calendarMeta: (accessToken: string, calendarId: string | undefined) => Promise<CalendarWriteTarget>;
57
70
  }
71
+ type PushOutcome = {
72
+ kind: "created";
73
+ event: CalendarEventSummary;
74
+ } | {
75
+ kind: "updated";
76
+ event: CalendarEventSummary;
77
+ } | {
78
+ kind: "conflict";
79
+ }
80
+ /** `event` present only when this record's baseline had to be repaired (an
81
+ * event that already existed in Google), so the write path can persist it. */
82
+ | {
83
+ kind: "unchanged";
84
+ event?: CalendarEventSummary;
85
+ } | {
86
+ kind: "skipped";
87
+ message: string;
88
+ } | {
89
+ kind: "error";
90
+ message: string;
91
+ };
92
+ export type PushOutcomeKind = PushOutcome["kind"];
93
+ /** Whether this outcome leaves a local edit that only exists locally, so the
94
+ * pull must not overwrite the record and the baseline must not advance. */
95
+ export declare const isUnpushed: (kind: PushOutcomeKind) => boolean;
96
+ /** Record ids whose local value no longer matches the baseline — edits that have
97
+ * not reached Google.
98
+ *
99
+ * Pure, and deliberately built from `planRecord`, so it answers with exactly the
100
+ * records a push WOULD have acted on. A private re-implementation would drift
101
+ * from the push and protect the wrong set. */
102
+ export declare function locallyEditedIds(records: readonly CollectionItem[], shadow: Record<string, ShadowEvent>, map: Record<string, PushableSourceField>, primaryKey: string, fields: Record<string, CollectionFieldSpec>): string[];
103
+ /** The same set, read from the workspace.
104
+ *
105
+ * Needed when a push cannot run AT ALL — a calendar whose role degraded to
106
+ * reader, a revoked grant, an IO failure. The pull still runs, since reading
107
+ * needs no write access, and would overwrite exactly these records while
108
+ * advancing their baseline past the edit (CodeRabbit review #2666). Computed
109
+ * from the stored baseline alone, so it works precisely when Google is the
110
+ * thing that is unreachable. */
111
+ export declare function unsentLocalEdits(collection: LoadedCollection, workspaceRoot: string): Promise<string[]>;
112
+ /** Push one collection WITHOUT taking the calendar lock.
113
+ *
114
+ * The caller must already hold it. The scheduled push→pull cycle
115
+ * (`collectionSync.ts`) runs inside the lock it took for the pull, and the lock
116
+ * is not reentrant — going through `pushCalendarForCollection` there would make
117
+ * the sync wait on itself forever. */
118
+ export declare function pushCollectionNow(collection: LoadedCollection, workspaceRoot: string, deps?: CalendarPushDeps): Promise<CalendarPushOutcome>;
58
119
  /** Push ONE collection's records to the calendar it declares.
59
120
  *
60
121
  * "Does this collection sync at all" is answered before "is Google linked", for
@@ -62,3 +123,4 @@ export interface CalendarPushDeps {
62
123
  * account for a collection that never declared a calendar sends them fixing the
63
124
  * wrong thing. */
64
125
  export declare function pushCalendarForCollection(slug: string, workspaceRoot: string, deps?: CalendarPushDeps): Promise<CalendarPushOutcome>;
126
+ export {};
@@ -1,8 +1,6 @@
1
1
  import { SystemTaskDef } from '../scheduler/adapter.js';
2
2
  import { LoadedCollection } from '../collection/server/discoveredCollection.js';
3
3
  import { DeleteItemResult, WriteItemResult } from '../collection/server/io.js';
4
- import { CollectionFieldSpec, CollectionItem } from '../collection/core/schema.js';
5
- import { GOOGLE_CALENDAR_SOURCE_FIELDS } from '../collection/core/schemaZ.js';
6
4
  import { CalendarEventSummary } from './calendar.js';
7
5
  import { ShadowEvent } from './calendarPushState.js';
8
6
  export declare const GOOGLE_CALENDAR_SYNC_TASK_ID = "system:google-calendar-sync";
@@ -16,13 +14,6 @@ export interface CalendarCollectionSyncResult {
16
14
  /** Retryable failures. Any of these hold the sync token back. */
17
15
  errors: string[];
18
16
  }
19
- /** The event fields a schema may map from — narrowed to real keys of
20
- * `CalendarEventSummary` so the projection below needs no cast. */
21
- type GoogleCalendarSourceField = (typeof GOOGLE_CALENDAR_SOURCE_FIELDS)[number];
22
- /** Project one Google event onto the collection's own field names. The
23
- * primary field always takes the event id — upsert-by-id is what keeps the
24
- * sync idempotent, so it is deliberately not remappable. */
25
- export declare function toCollectionRecord(event: CalendarEventSummary, map: Record<string, GoogleCalendarSourceField>, primaryKey: string, fields: Record<string, CollectionFieldSpec>): CollectionItem;
26
17
  /** `skipped` is a benign no-op; `unwritable` can never succeed so it must NOT
27
18
  * hold the token; `error` is retryable and does hold it. */
28
19
  type ApplyOutcome = {
@@ -40,13 +31,55 @@ type ApplyOutcome = {
40
31
  };
41
32
  export declare function classifyWrite(eventId: string, kind: WriteItemResult["kind"]): ApplyOutcome;
42
33
  export declare function classifyDelete(eventId: string, kind: DeleteItemResult["kind"]): ApplyOutcome;
43
- /** Serialise `run` against whatever is already running for `key`.
34
+ /** The events of a window a pull may act on.
44
35
  *
45
- * `locks` is passed in so the queuing rule is testable without module state;
46
- * the key is dropped once nothing is queued behind it, so the map cannot grow
47
- * an entry per calendar forever. A failed predecessor still releases the
48
- * queue — `then(run, run)`. */
49
- export declare function withKeyedLock<T>(locks: Map<string, Promise<unknown>>, key: string, run: () => Promise<T>): Promise<T>;
36
+ * A record the push just refused to send is edited on BOTH sides. Writing
37
+ * Google's value over it destroys the local edit the push declined to resolve,
38
+ * so the local one stands. Single-sourced because the record write and the
39
+ * baseline write must agree exactly on which events they skip — disagreeing is
40
+ * what would silently overwrite Google on the next push (#2620). */
41
+ export declare function pullableEvents(events: readonly CalendarEventSummary[], unpushed: ReadonlySet<string>): CalendarEventSummary[];
42
+ /** What an automatic push protected, per collection slug. `null` means it could
43
+ * not be worked out at all — see `PROTECTION_UNKNOWN`. */
44
+ export type UnpushedBySlug = ReadonlyMap<string, ReadonlySet<string> | null>;
45
+ /** A collection whose protection is unknown must not be pulled this run.
46
+ *
47
+ * Reported through the retryable `errors` channel rather than as a special case:
48
+ * that already holds the sync token AND skips the baseline save, so the window
49
+ * simply replays next run with nothing lost. Failing OPEN here — pulling with no
50
+ * protection — would overwrite the very edits this exists to protect; the read
51
+ * that failed is no evidence that the pull's own writes would fail too, so they
52
+ * would land (CodeRabbit review #2666). */
53
+ export declare const PROTECTION_UNKNOWN = "could not work out which records to protect after a failed push";
54
+ /** What ONE collection's pull must leave alone: only what ITS OWN push failed to
55
+ * send.
56
+ *
57
+ * Scoped per collection because a calendar can back several of them, and a
58
+ * conflict in one says nothing about the others. Sharing one set across the
59
+ * group starved a collection that never even declares `autoPush`: it cannot
60
+ * conflict, yet a neighbour's conflict froze its records — and the sync token
61
+ * still advanced, so Google never resent them (Codex review #2666). */
62
+ export declare const unpushedFor: (unpushed: UnpushedBySlug, slug: string) => ReadonlySet<string> | null;
63
+ /** What the calendar's BASELINE must leave alone: the union over every
64
+ * collection.
65
+ *
66
+ * Deliberately not per collection, unlike the records above. `.push-state.json`
67
+ * holds ONE baseline per calendar, shared by every collection on it, so there is
68
+ * no per-collection baseline to hold back. Advancing it while any collection
69
+ * still has an unresolved conflict is the failure that silently overwrites
70
+ * Google on the next push, and holding it back only ever means "keep reporting
71
+ * the conflict" — so the union is the safe side of an asymmetry the shared
72
+ * storage forces.
73
+ *
74
+ * A `null` (unknown) entry contributes nothing, because that collection reports
75
+ * a retryable error instead — which stops the baseline being saved at all. */
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;
50
83
  /** Sync ONE calendar and fan its events out to every collection bound to it.
51
84
  *
52
85
  * The fan-out is not an optimisation, it is correctness: the sync token is
@@ -61,17 +94,19 @@ export declare function withKeyedLock<T>(locks: Map<string, Promise<unknown>>, k
61
94
  * load the SAME stored token and walk the same window. That is idempotent —
62
95
  * writes are upserts by event id — but it is a wasted full walk. Queued, the
63
96
  * second pass resumes from the token the first just stored and fetches only
64
- * what is genuinely new. */
65
- /** Serialise anything that touches ONE calendar's sync state.
66
- *
67
- * Shared with the push path (#2598), not only the pull doors: a push that
68
- * overtakes an in-flight pull would record a baseline for events the pull is
69
- * still writing, so the next push would diff against a future it never saw. */
70
- export declare function withCalendarLock<T>(calendarId: string | undefined, run: () => Promise<T>): Promise<T>;
71
- 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[]>;
72
100
  /** The baseline this window establishes: what Google now says per event, and
73
- * `null` for a cancelled one so a recreate cannot resume from a dead baseline. */
74
- export declare function shadowUpdates(events: readonly CalendarEventSummary[]): Record<string, ShadowEvent | null>;
101
+ * `null` for a cancelled one so a recreate cannot resume from a dead baseline.
102
+ *
103
+ * An event whose record the push could not send is left OUT, and that omission
104
+ * is load-bearing. Advancing its baseline to Google's new value while the record
105
+ * keeps the local one would make the next push read a plain one-sided edit —
106
+ * no conflict to detect any more — and quietly overwrite Google. Held back, the
107
+ * baseline stays older than both sides, so the conflict keeps being reported
108
+ * until someone resolves it (#2620). */
109
+ export declare function shadowUpdates(events: readonly CalendarEventSummary[], unpushed?: ReadonlySet<string>): Record<string, ShadowEvent | null>;
75
110
  /** Liveness of the collections a sync just wrote to, checked against the skill
76
111
  * dir `deleteCollection` removes. `exists` is injected so the rule is testable
77
112
  * without a filesystem. An empty group has no survivor by definition. */
@@ -111,8 +146,15 @@ export declare function orphanedCalendarId(deleted: CalendarDeclaring, remaining
111
146
  * Returns the cleared calendar id, or null when nothing was cleared. Never
112
147
  * throws: a failed cleanup must not fail the delete it follows. */
113
148
  export declare function releaseOrphanedCalendarToken(deleted: CalendarDeclaring, workspaceRoot: string): Promise<string | null>;
114
- /** Sync every collection that declares `googleCalendar`. */
115
- 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[]>;
116
158
  /** The groups whose calendar has never synced. A missing token IS the "created
117
159
  * since the last sync" signal — nothing else distinguishes a new collection
118
160
  * from an edited one on the write path this feeds (#2427).
@@ -156,7 +198,12 @@ export interface ManualCalendarSyncDeps {
156
198
  * calendar sends them fixing the wrong thing. */
157
199
  export declare function syncCalendarForCollection(slug: string, workspaceRoot: string, deps?: ManualCalendarSyncDeps): Promise<ManualCalendarSyncOutcome>;
158
200
  /** Scheduler registration, shaped like `feedRefreshTaskDef` so hosts wire it
159
- * 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. */
160
207
  export declare function googleCalendarSyncTaskDef(opts?: {
161
208
  workspaceRoot?: string;
162
209
  intervalMs?: number;