@mulmoclaude/core 1.12.0 → 1.14.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 (117) hide show
  1. package/assets/helps/error-recovery.md +110 -1
  2. package/dist/{calendarGrid-CaS9er8i.cjs → calendarGrid-BQYqOCnF.cjs} +8 -9
  3. package/dist/{calendarGrid-CaS9er8i.cjs.map → calendarGrid-BQYqOCnF.cjs.map} +1 -1
  4. package/dist/{calendarGrid-CrGxeCOG.js → calendarGrid-DivcNKkX.js} +8 -9
  5. package/dist/{calendarGrid-CrGxeCOG.js.map → calendarGrid-DivcNKkX.js.map} +1 -1
  6. package/dist/collection/core/project.d.ts +11 -0
  7. package/dist/collection/core/schemaZ.d.ts +4 -4
  8. package/dist/collection/index.cjs +5 -4
  9. package/dist/collection/index.js +5 -5
  10. package/dist/collection/registry/server/index.cjs +3 -3
  11. package/dist/collection/registry/server/index.cjs.map +1 -1
  12. package/dist/collection/registry/server/index.js +3 -3
  13. package/dist/collection/registry/server/index.js.map +1 -1
  14. package/dist/collection/server/csvQuery.d.ts +7 -2
  15. package/dist/collection/server/csvStore.d.ts +2 -1
  16. package/dist/collection/server/index.cjs +3 -3
  17. package/dist/collection/server/index.js +3 -3
  18. package/dist/collection-watchers/index.cjs +10 -25
  19. package/dist/collection-watchers/index.cjs.map +1 -1
  20. package/dist/collection-watchers/index.js +10 -25
  21. package/dist/collection-watchers/index.js.map +1 -1
  22. package/dist/{discovery-NRy3tyUA.cjs → discovery-CRDx6TS4.cjs} +88 -47
  23. package/dist/discovery-CRDx6TS4.cjs.map +1 -0
  24. package/dist/{discovery-B4CZvrXR.js → discovery-D6p4ITNR.js} +88 -47
  25. package/dist/discovery-D6p4ITNR.js.map +1 -0
  26. package/dist/{dist-pWpC-b04.cjs → dist-CsgSfWwR.cjs} +66 -1
  27. package/dist/{dist-pWpC-b04.cjs.map → dist-CsgSfWwR.cjs.map} +1 -1
  28. package/dist/{dist-Cwk0e12G.js → dist-D8zokgGo.js} +25 -2
  29. package/dist/{dist-Cwk0e12G.js.map → dist-D8zokgGo.js.map} +1 -1
  30. package/dist/feeds/index.cjs +2 -2
  31. package/dist/feeds/index.js +2 -2
  32. package/dist/feeds/server/engine.d.ts +27 -0
  33. package/dist/feeds/server/index.cjs +97 -26
  34. package/dist/feeds/server/index.cjs.map +1 -1
  35. package/dist/feeds/server/index.d.ts +1 -1
  36. package/dist/feeds/server/index.js +96 -27
  37. package/dist/feeds/server/index.js.map +1 -1
  38. package/dist/google/calendar.d.ts +23 -9
  39. package/dist/google/calendarStateLock.d.ts +23 -0
  40. package/dist/google/collectionProjection.d.ts +0 -8
  41. package/dist/google/collectionPush.d.ts +13 -8
  42. package/dist/google/collectionSync.d.ts +80 -10
  43. package/dist/google/index.cjs +366 -98
  44. package/dist/google/index.cjs.map +1 -1
  45. package/dist/google/index.d.ts +6 -4
  46. package/dist/google/index.js +357 -99
  47. package/dist/google/index.js.map +1 -1
  48. package/dist/{ingestTypes-D1GQdG8e.cjs → ingestTypes-C7IJSqv4.cjs} +2 -2
  49. package/dist/{ingestTypes-D1GQdG8e.cjs.map → ingestTypes-C7IJSqv4.cjs.map} +1 -1
  50. package/dist/{ingestTypes-XReG-li7.js → ingestTypes-DmtDLRmt.js} +2 -2
  51. package/dist/{ingestTypes-XReG-li7.js.map → ingestTypes-DmtDLRmt.js.map} +1 -1
  52. package/dist/notifier/index.cjs +1 -1
  53. package/dist/notifier/index.js +1 -1
  54. package/dist/notifier/store.d.ts +4 -4
  55. package/dist/{notifier-BdA5qzhe.js → notifier-Dxh8mEFh.js} +39 -25
  56. package/dist/notifier-Dxh8mEFh.js.map +1 -0
  57. package/dist/{notifier-tMsAXyXp.cjs → notifier-Wc4M24Bi.cjs} +39 -25
  58. package/dist/notifier-Wc4M24Bi.cjs.map +1 -0
  59. package/dist/plugin-vue/index.cjs +4 -2
  60. package/dist/plugin-vue/index.cjs.map +1 -1
  61. package/dist/plugin-vue/index.js +4 -2
  62. package/dist/plugin-vue/index.js.map +1 -1
  63. package/dist/project-B0j_Vf_Q.js +33 -0
  64. package/dist/project-B0j_Vf_Q.js.map +1 -0
  65. package/dist/project-C1ep9pvo.cjs +44 -0
  66. package/dist/project-C1ep9pvo.cjs.map +1 -0
  67. package/dist/{promptSafety-FD8cQn3e.cjs → promptSafety-B52GvaJc.cjs} +99 -85
  68. package/dist/promptSafety-B52GvaJc.cjs.map +1 -0
  69. package/dist/{promptSafety-C--op_6a.js → promptSafety-DJgaDjM4.js} +99 -85
  70. package/dist/promptSafety-DJgaDjM4.js.map +1 -0
  71. package/dist/remote-host/index.cjs +2 -1
  72. package/dist/remote-host/index.d.ts +10 -0
  73. package/dist/remote-host/index.js +2 -2
  74. package/dist/remote-host/server/index.cjs +2 -2
  75. package/dist/remote-host/server/index.cjs.map +1 -1
  76. package/dist/remote-host/server/index.js +2 -2
  77. package/dist/remote-host/server/index.js.map +1 -1
  78. package/dist/{remote-host-DkDVxNim.cjs → remote-host-CtQjagPt.cjs} +53 -1
  79. package/dist/remote-host-CtQjagPt.cjs.map +1 -0
  80. package/dist/remote-host-Dba4lF3Z.js +82 -0
  81. package/dist/remote-host-Dba4lF3Z.js.map +1 -0
  82. package/dist/remote-view/index.cjs +6 -4
  83. package/dist/remote-view/index.cjs.map +1 -1
  84. package/dist/remote-view/index.js +6 -4
  85. package/dist/remote-view/index.js.map +1 -1
  86. package/dist/scheduler/index.cjs +1 -1
  87. package/dist/scheduler/index.js +1 -1
  88. package/dist/{server-DdyIvFhz.js → server-D6EUf3BF.js} +46 -27
  89. package/dist/server-D6EUf3BF.js.map +1 -0
  90. package/dist/{server-7U-3DE2e.cjs → server-D7_oRbTJ.cjs} +46 -27
  91. package/dist/server-D7_oRbTJ.cjs.map +1 -0
  92. package/dist/utils/index.cjs +1 -1
  93. package/dist/utils/index.js +1 -1
  94. package/dist/whisper/index.cjs +2 -6
  95. package/dist/whisper/index.cjs.map +1 -1
  96. package/dist/whisper/index.js +2 -6
  97. package/dist/whisper/index.js.map +1 -1
  98. package/dist/wiki/index.cjs +2 -2
  99. package/dist/wiki/index.cjs.map +1 -1
  100. package/dist/wiki/index.js +2 -2
  101. package/dist/wiki/index.js.map +1 -1
  102. package/package.json +2 -2
  103. package/dist/discovery-B4CZvrXR.js.map +0 -1
  104. package/dist/discovery-NRy3tyUA.cjs.map +0 -1
  105. package/dist/notifier-BdA5qzhe.js.map +0 -1
  106. package/dist/notifier-tMsAXyXp.cjs.map +0 -1
  107. package/dist/project-BWI5w_BT.cjs +0 -17
  108. package/dist/project-BWI5w_BT.cjs.map +0 -1
  109. package/dist/project-bU98ycsy.js +0 -12
  110. package/dist/project-bU98ycsy.js.map +0 -1
  111. package/dist/promptSafety-C--op_6a.js.map +0 -1
  112. package/dist/promptSafety-FD8cQn3e.cjs.map +0 -1
  113. package/dist/remote-host-D_BRFHcI.js +0 -36
  114. package/dist/remote-host-D_BRFHcI.js.map +0 -1
  115. package/dist/remote-host-DkDVxNim.cjs.map +0 -1
  116. package/dist/server-7U-3DE2e.cjs.map +0 -1
  117. package/dist/server-DdyIvFhz.js.map +0 -1
@@ -194,18 +194,32 @@ export interface CalendarListPage {
194
194
  /** Pagination loop for CalendarList.list, extracted so it can be tested without
195
195
  * network. Stops at the last page (no token) or the runaway page cap. */
196
196
  export declare function collectCalendarPages(fetchPage: (pageToken?: string) => Promise<CalendarListPage>, maxPages?: number): Promise<CalendarSummary[]>;
197
+ /** What a calendar absent from the user's list can still tell about itself. */
198
+ export interface CalendarMeta {
199
+ /** IANA zone (`Asia/Tokyo`), `""` when Google omits it. */
200
+ timeZone: string;
201
+ /** `owner` / `writer` / `reader` / `freeBusyReader`, `""` when unreported. */
202
+ accessRole: string;
203
+ }
204
+ export declare const toCalendarMeta: (value: unknown) => CalendarMeta;
205
+ /** One calendar's zone and the caller's role on it, read by id rather than
206
+ * looked up in the user's list.
207
+ *
208
+ * `calendarList` only holds calendars the user has ADDED; a calendar shared
209
+ * with them can be readable and writable by id without appearing there, and a
210
+ * push still needs its zone to send an offset-less `dateTime`.
211
+ *
212
+ * Deliberately `events.list` and NOT `calendars.get`: the latter takes only
213
+ * full-calendar scopes (`calendar`, `calendar.readonly`, `calendar.calendars*`)
214
+ * and this app asks for none of them, so it answers 403 for every account it
215
+ * links (#2735). The events list carries the same `timeZone` — plus the
216
+ * `accessRole` the calendar resource does not have — under `calendar.events`,
217
+ * which the push already holds because it writes with it. Only the envelope is
218
+ * read; the one event asked for is the smallest page Google will serve. */
219
+ export declare function getCalendarMeta(accessToken: string, calendarId: string | undefined): Promise<CalendarMeta>;
197
220
  /** The calendars the user has added/subscribed to (primary + secondary +
198
221
  * shared), each with its id, name and colour, following pagination. Needs the
199
222
  * calendar-list read scope (GOOGLE_CALENDARLIST_SCOPE). */
200
- /** One calendar's own resource, addressed by id rather than looked up in the
201
- * user's list.
202
- *
203
- * `calendarList` only holds calendars the user has ADDED; a calendar shared
204
- * with them can be readable and writable by id without appearing there. This is
205
- * how a push learns such a calendar's `timeZone`. Note the resource carries no
206
- * `accessRole` — that is a calendarList property — so reachability here says
207
- * nothing about writability. */
208
- export declare function getCalendar(accessToken: string, calendarId: string | undefined): Promise<CalendarSummary>;
209
223
  export declare function listCalendars(accessToken: string): Promise<CalendarSummary[]>;
210
224
  /** Resolve a `colorId` (on an event or calendar) to its hex background/foreground. */
211
225
  export declare function getCalendarColors(accessToken: string): Promise<CalendarColors>;
@@ -0,0 +1,23 @@
1
+ /** The clock and the sleep, injected so the timing rules can be exercised
2
+ * without real waits. */
3
+ export interface LockClock {
4
+ now: () => number;
5
+ sleep: (delay_ms: number) => Promise<void>;
6
+ }
7
+ export declare const liveLockClock: LockClock;
8
+ /** Run `mutate` with no other process mutating the same file.
9
+ *
10
+ * Fails OPEN. A workspace that cannot create the lock file — a read-only
11
+ * mount, a missing directory, an exhausted disk — still syncs, the way it did
12
+ * before #2679; the lock removes a race, it is not a precondition for
13
+ * correctness. The same applies to waiting the timeout out: proceeding
14
+ * unlocked risks the lost update this exists to prevent, whereas refusing
15
+ * would stop the sync outright.
16
+ *
17
+ * Note what this cannot promise. `O_EXCL` is atomic on a local filesystem and
18
+ * on NFSv3+, but a workspace living in a consumer sync folder (Dropbox,
19
+ * iCloud, Drive) has no such guarantee — there the file is replicated after
20
+ * the fact and both hosts believe they hold it. */
21
+ export declare function withCalendarStateLock<T>(lockPath: string, mutate: () => Promise<T>, clock?: LockClock): Promise<T>;
22
+ /** `<state file>.lock`, so each state file is serialised on its own. */
23
+ export declare const stateLockPath: (statePath: string) => string;
@@ -8,11 +8,3 @@ export type GoogleCalendarSourceField = (typeof GOOGLE_CALENDAR_SOURCE_FIELDS)[n
8
8
  * primary field always takes the event id — upsert-by-id is what keeps the
9
9
  * sync idempotent, so it is deliberately not remappable. */
10
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;
@@ -5,21 +5,26 @@ import { ShadowEvent } from './calendarPushState.js';
5
5
  import { PushableSourceField } from './pushPlan.js';
6
6
  /** What the up-front writability gate knows about the target calendar. */
7
7
  export interface CalendarWriteTarget {
8
- /** The role from the user's calendar LIST, or null when the calendar is not in
9
- * it. Null means unknown, never read-only: `calendarList` holds only the
10
- * calendars the user has added, so one shared with write access can be absent
11
- * from it (Codex review). */
8
+ /** The caller's role on the calendar, or null when neither source reported one.
9
+ * Null means unknown, never read-only: hard-denying an unknown role would
10
+ * refuse a calendar the user can in fact write to. */
12
11
  accessRole: string | null;
13
12
  /** IANA zone for rebuilding a zone-less stored clock, `""` when unreported. */
14
13
  timeZone: string;
15
14
  }
16
15
  /** Whether to refuse the whole push before touching a single event.
17
16
  *
18
- * Only on POSITIVE evidence of a non-writable role. An unlisted calendar is
19
- * unknown and must fall through: hard-denying it would block the feature
20
- * outright for a calendar the user can in fact write to. Those attempts surface
21
- * Google's own 403 per record if the write really is not allowed. */
17
+ * Only on POSITIVE evidence of a non-writable role. A calendar whose role
18
+ * neither source reported is unknown and must fall through: hard-denying it
19
+ * would block the feature outright for a calendar the user can in fact write
20
+ * to. Those attempts surface Google's own 403 per record if the write really is
21
+ * not allowed. */
22
22
  export declare const isDeniedAccessRole: (accessRole: string | null) => boolean;
23
+ /** A role Google left blank is UNKNOWN, not a denial. `isDeniedAccessRole`
24
+ * refuses every role it does not recognise — `""` included — so passing an
25
+ * unreported one straight through would refuse a calendar Google simply said
26
+ * nothing about. */
27
+ export declare const reportedAccessRole: (accessRole: string) => string | null;
23
28
  export interface CalendarCollectionPushResult {
24
29
  slug: string;
25
30
  created: number;
@@ -1,7 +1,9 @@
1
1
  import { SystemTaskDef } from '../scheduler/adapter.js';
2
+ import { CollectionItem } from '../collection/core/schema.js';
2
3
  import { LoadedCollection } from '../collection/server/discoveredCollection.js';
3
4
  import { DeleteItemResult, WriteItemResult } from '../collection/server/io.js';
4
5
  import { CalendarEventSummary } from './calendar.js';
6
+ import { CalendarPushOutcome } from './collectionPush.js';
5
7
  import { ShadowEvent } from './calendarPushState.js';
6
8
  export declare const GOOGLE_CALENDAR_SYNC_TASK_ID = "system:google-calendar-sync";
7
9
  export interface CalendarCollectionSyncResult {
@@ -11,17 +13,24 @@ export interface CalendarCollectionSyncResult {
11
13
  /** Events that can NEVER be stored — e.g. an id the record-file sanitiser
12
14
  * rejects. Reported and skipped rather than retried; see `classifyWrite`. */
13
15
  unwritable: string[];
16
+ /** Events left alone because the record they would overwrite holds an edit
17
+ * Google has not seen. NOT an error — the token still advances past them; only
18
+ * the baseline is held back, so the next push reports the conflict (#2684). */
19
+ withheld: string[];
14
20
  /** Retryable failures. Any of these hold the sync token back. */
15
21
  errors: string[];
16
22
  }
17
23
  /** `skipped` is a benign no-op; `unwritable` can never succeed so it must NOT
18
- * hold the token; `error` is retryable and does hold it. */
24
+ * hold the token; `error` is retryable and does hold it; `withheld` is a
25
+ * deliberate refusal to overwrite a local edit. */
19
26
  type ApplyOutcome = {
20
27
  kind: "written";
21
28
  } | {
22
29
  kind: "removed";
23
30
  } | {
24
31
  kind: "skipped";
32
+ } | {
33
+ kind: "withheld";
25
34
  } | {
26
35
  kind: "unwritable";
27
36
  message: string;
@@ -31,6 +40,24 @@ type ApplyOutcome = {
31
40
  };
32
41
  export declare function classifyWrite(eventId: string, kind: WriteItemResult["kind"]): ApplyOutcome;
33
42
  export declare function classifyDelete(eventId: string, kind: DeleteItemResult["kind"]): ApplyOutcome;
43
+ /** Whether this record still says what the workspace last saw Google say.
44
+ *
45
+ * Compared against the baseline as it stood when this run STARTED READING, not
46
+ * as it stands now: a full re-walk clears the baseline before writing a new one
47
+ * (`restartFullSync`), so reading it live would answer "no baseline" for every
48
+ * event and protect nothing exactly when the window is widest (#2684).
49
+ *
50
+ * A record with no baseline at all is NOT withheld — that is an event this
51
+ * workspace has never held, so there is no local edit to lose. */
52
+ export declare function unsentEditGuard(schema: LoadedCollection["schema"], baseline: Record<string, ShadowEvent>): (existing: CollectionItem, eventId: string) => boolean;
53
+ /** What the pull may do to the record behind one event.
54
+ *
55
+ * The guard runs BEFORE the status is consulted, and that order is the whole
56
+ * fix: "is there something local to lose here?" outranks "what did Google do
57
+ * to it?". Asking about the status first is how a cancellation kept deleting
58
+ * records that held an edit Google had never seen (#2688), long after the
59
+ * same guard had been put in front of the overwrite (#2684). */
60
+ export declare function applyPlanFor(existing: CollectionItem | null, event: CalendarEventSummary, hasUnsentEdit: (existing: CollectionItem, eventId: string) => boolean): "withhold" | "delete" | "write";
34
61
  /** The events of a window a pull may act on.
35
62
  *
36
63
  * A record the push just refused to send is edited on BOTH sides. Writing
@@ -50,15 +77,15 @@ export type UnpushedBySlug = ReadonlyMap<string, ReadonlySet<string> | null>;
50
77
  * protection — would overwrite the very edits this exists to protect; the read
51
78
  * that failed is no evidence that the pull's own writes would fail too, so they
52
79
  * 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
80
+ export declare const PROTECTION_UNKNOWN = "could not work out which records to protect from the pull";
81
+ /** What ONE collection's pull must leave alone: only what ITS OWN push did not
55
82
  * send.
56
83
  *
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). */
84
+ * Scoped per collection because a calendar can back several of them, and one
85
+ * collection's unsent edit says nothing about the others. Sharing one set across
86
+ * the group starved a collection that never even declares `autoPush`: a
87
+ * neighbour's conflict froze its records — and the sync token still advanced, so
88
+ * Google never resent them (Codex review #2666). */
62
89
  export declare const unpushedFor: (unpushed: UnpushedBySlug, slug: string) => ReadonlySet<string> | null;
63
90
  /** What the calendar's BASELINE must leave alone: the union over every
64
91
  * collection.
@@ -74,6 +101,32 @@ export declare const unpushedFor: (unpushed: UnpushedBySlug, slug: string) => Re
74
101
  * A `null` (unknown) entry contributes nothing, because that collection reports
75
102
  * a retryable error instead — which stops the baseline being saved at all. */
76
103
  export declare const allUnpushed: (unpushed: UnpushedBySlug) => ReadonlySet<string>;
104
+ /** The I/O the protection rule crosses, injected so every branch of it can be
105
+ * exercised with fakes instead of a workspace on disk and a live Google grant.
106
+ * The rule is what #2666 and #2683 both got wrong, so it is worth pinning. */
107
+ export interface PullProtectionDeps {
108
+ pushNow: (collection: LoadedCollection, workspaceRoot: string) => Promise<CalendarPushOutcome>;
109
+ unsentEdits: (collection: LoadedCollection, workspaceRoot: string) => Promise<string[]>;
110
+ }
111
+ /** What ONE collection's pull must leave alone, pushing it first if it asked to
112
+ * be pushed.
113
+ *
114
+ * A collection WITHOUT `autoPush` never pushes, so its local edits are unsent by
115
+ * definition — the same state a failed push leaves behind, and it needs the same
116
+ * protection. Pulling over them destroys the edit AND advances the baseline past
117
+ * it, after which no conflict can be detected any more (#2683). "The push did not
118
+ * run" is the condition that matters here; why it did not run is not.
119
+ *
120
+ * MUST run inside the calendar lock the caller already holds — hence
121
+ * `pushCollectionNow` rather than `pushCalendarForCollection`, which would take
122
+ * the same non-reentrant lock and wait on itself forever.
123
+ *
124
+ * A failed push must not stop the pull: the pull is what keeps the collection
125
+ * fresh, and a revoked write grant is no reason to freeze reading. */
126
+ export declare function pullProtectionFor(collection: LoadedCollection, workspaceRoot: string, deps?: PullProtectionDeps): Promise<ReadonlySet<string> | null>;
127
+ /** Push the `autoPush` collections in this group, and answer with what every
128
+ * collection's pull must leave alone, keyed by collection. */
129
+ export declare function pushAndProtect(collections: readonly LoadedCollection[], workspaceRoot: string, deps?: PullProtectionDeps): Promise<UnpushedBySlug>;
77
130
  /** Whether a run may take the calendar, given what the shared marker says.
78
131
  *
79
132
  * The scheduled door defers to it; every user-facing door claims regardless,
@@ -97,6 +150,14 @@ export type ClaimGuard = (lastSyncedAt: string | null) => boolean;
97
150
  * what is genuinely new. That queue is module state, so it orders the doors
98
151
  * into THIS process only; `claimThenSync` is what other hosts can see. */
99
152
  export declare function syncCalendarGroup(calendarId: string | undefined, collections: readonly LoadedCollection[], workspaceRoot: string, mayClaim?: ClaimGuard): Promise<CalendarCollectionSyncResult[]>;
153
+ /** Every event whose baseline must NOT advance: what the push could not send,
154
+ * plus what the apply refused to overwrite.
155
+ *
156
+ * The two must agree exactly. The apply's refusals only became visible after it
157
+ * ran, so they join the union here rather than at push time — leaving them out
158
+ * would advance the baseline past a record the pull deliberately left holding a
159
+ * local edit, which is the silent overwrite this whole path exists to stop. */
160
+ export declare const heldBack: (unpushed: UnpushedBySlug, results: readonly CalendarCollectionSyncResult[]) => ReadonlySet<string>;
100
161
  /** The baseline this window establishes: what Google now says per event, and
101
162
  * `null` for a cancelled one so a recreate cannot resume from a dead baseline.
102
163
  *
@@ -105,8 +166,17 @@ export declare function syncCalendarGroup(calendarId: string | undefined, collec
105
166
  * keeps the local one would make the next push read a plain one-sided edit —
106
167
  * no conflict to detect any more — and quietly overwrite Google. Held back, the
107
168
  * 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>;
169
+ * until someone resolves it (#2620).
170
+ *
171
+ * `held` carries what those events must KEEP. Omitting them is enough on an
172
+ * incremental run, where the file is merged rather than replaced — but a full
173
+ * re-walk CLEARS the baseline first (`restartFullSync`), and there omission
174
+ * drops the entry for good. The next push would then read a conflicted record
175
+ * as a brand-new create, hit Google's duplicate-id 409 and refuse it, instead
176
+ * of reporting the conflict it actually is. Re-stating the pre-run value makes
177
+ * a held-back event behave the same either way (observed during Claude review;
178
+ * no bot flagged it). */
179
+ export declare function shadowUpdates(events: readonly CalendarEventSummary[], unpushed?: ReadonlySet<string>, held?: Record<string, ShadowEvent>): Record<string, ShadowEvent | null>;
110
180
  /** Liveness of the collections a sync just wrote to, checked against the skill
111
181
  * dir `deleteCollection` removes. `exists` is injected so the rule is testable
112
182
  * without a filesystem. An empty group has no survivor by definition. */