@mulmoclaude/core 1.11.0 → 1.13.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 (70) hide show
  1. package/assets/helps/error-recovery.md +98 -0
  2. package/dist/collection/core/project.d.ts +11 -0
  3. package/dist/collection/index.cjs +3 -2
  4. package/dist/collection/index.js +3 -3
  5. package/dist/collection/registry/server/index.cjs +3 -3
  6. package/dist/collection/registry/server/index.js +3 -3
  7. package/dist/collection/server/index.cjs +2 -2
  8. package/dist/collection/server/index.js +2 -2
  9. package/dist/collection-watchers/index.cjs +3 -3
  10. package/dist/collection-watchers/index.js +3 -3
  11. package/dist/{discovery-B4CZvrXR.js → discovery-D-ZJ6a6z.js} +39 -18
  12. package/dist/discovery-D-ZJ6a6z.js.map +1 -0
  13. package/dist/{discovery-NRy3tyUA.cjs → discovery-DAoMnBYM.cjs} +39 -18
  14. package/dist/discovery-DAoMnBYM.cjs.map +1 -0
  15. package/dist/{dist-pWpC-b04.cjs → dist-21pMsI1a.cjs} +45 -1
  16. package/dist/{dist-pWpC-b04.cjs.map → dist-21pMsI1a.cjs.map} +1 -1
  17. package/dist/{dist-Cwk0e12G.js → dist-DP1FTGnO.js} +16 -2
  18. package/dist/{dist-Cwk0e12G.js.map → dist-DP1FTGnO.js.map} +1 -1
  19. package/dist/feeds/server/engine.d.ts +27 -0
  20. package/dist/feeds/server/index.cjs +72 -7
  21. package/dist/feeds/server/index.cjs.map +1 -1
  22. package/dist/feeds/server/index.d.ts +1 -1
  23. package/dist/feeds/server/index.js +71 -8
  24. package/dist/feeds/server/index.js.map +1 -1
  25. package/dist/google/calendarStateLock.d.ts +23 -0
  26. package/dist/google/calendarSyncDue.d.ts +10 -0
  27. package/dist/google/calendarSyncStore.d.ts +20 -1
  28. package/dist/google/collectionProjection.d.ts +0 -8
  29. package/dist/google/collectionSync.d.ts +104 -15
  30. package/dist/google/index.cjs +512 -89
  31. package/dist/google/index.cjs.map +1 -1
  32. package/dist/google/index.d.ts +6 -3
  33. package/dist/google/index.js +501 -91
  34. package/dist/google/index.js.map +1 -1
  35. package/dist/notifier/index.cjs +1 -1
  36. package/dist/notifier/index.js +1 -1
  37. package/dist/notifier/store.d.ts +4 -4
  38. package/dist/{notifier-tMsAXyXp.cjs → notifier-C3IhYxqh.cjs} +39 -25
  39. package/dist/notifier-C3IhYxqh.cjs.map +1 -0
  40. package/dist/{notifier-BdA5qzhe.js → notifier-CMFlw74b.js} +39 -25
  41. package/dist/notifier-CMFlw74b.js.map +1 -0
  42. package/dist/project-B9NSyr6L.js +28 -0
  43. package/dist/project-B9NSyr6L.js.map +1 -0
  44. package/dist/project-CCtT_fQr.cjs +39 -0
  45. package/dist/project-CCtT_fQr.cjs.map +1 -0
  46. package/dist/remote-host/server/index.cjs +1 -1
  47. package/dist/remote-host/server/index.js +1 -1
  48. package/dist/remote-view/index.cjs +1 -1
  49. package/dist/remote-view/index.js +1 -1
  50. package/dist/scheduler/index.cjs +1 -1
  51. package/dist/scheduler/index.js +1 -1
  52. package/dist/{server-7U-3DE2e.cjs → server-B_b0bLSW.cjs} +3 -3
  53. package/dist/{server-7U-3DE2e.cjs.map → server-B_b0bLSW.cjs.map} +1 -1
  54. package/dist/{server-DdyIvFhz.js → server-Ci_A3chZ.js} +3 -3
  55. package/dist/{server-DdyIvFhz.js.map → server-Ci_A3chZ.js.map} +1 -1
  56. package/dist/utils/index.cjs +1 -1
  57. package/dist/utils/index.js +1 -1
  58. package/dist/whisper/index.cjs +1 -1
  59. package/dist/whisper/index.js +1 -1
  60. package/dist/wiki/index.cjs +1 -1
  61. package/dist/wiki/index.js +1 -1
  62. package/package.json +1 -1
  63. package/dist/discovery-B4CZvrXR.js.map +0 -1
  64. package/dist/discovery-NRy3tyUA.cjs.map +0 -1
  65. package/dist/notifier-BdA5qzhe.js.map +0 -1
  66. package/dist/notifier-tMsAXyXp.cjs.map +0 -1
  67. package/dist/project-BWI5w_BT.cjs +0 -17
  68. package/dist/project-BWI5w_BT.cjs.map +0 -1
  69. package/dist/project-bU98ycsy.js +0 -12
  70. package/dist/project-bU98ycsy.js.map +0 -1
@@ -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,38 @@ 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>;
130
+ /** Whether a run may take the calendar, given what the shared marker says.
131
+ *
132
+ * The scheduled door defers to it; every user-facing door claims regardless,
133
+ * because a Refresh click that silently returns nothing reads as an empty
134
+ * calendar, not as "another host has this". */
135
+ export type ClaimGuard = (lastSyncedAt: string | null) => boolean;
77
136
  /** Sync ONE calendar and fan its events out to every collection bound to it.
78
137
  *
79
138
  * The fan-out is not an optimisation, it is correctness: the sync token is
@@ -88,8 +147,17 @@ export declare const allUnpushed: (unpushed: UnpushedBySlug) => ReadonlySet<stri
88
147
  * load the SAME stored token and walk the same window. That is idempotent —
89
148
  * writes are upserts by event id — but it is a wasted full walk. Queued, the
90
149
  * 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[]>;
150
+ * what is genuinely new. That queue is module state, so it orders the doors
151
+ * into THIS process only; `claimThenSync` is what other hosts can see. */
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>;
93
161
  /** The baseline this window establishes: what Google now says per event, and
94
162
  * `null` for a cancelled one so a recreate cannot resume from a dead baseline.
95
163
  *
@@ -98,8 +166,17 @@ export declare function syncCalendarGroup(calendarId: string | undefined, collec
98
166
  * keeps the local one would make the next push read a plain one-sided edit —
99
167
  * no conflict to detect any more — and quietly overwrite Google. Held back, the
100
168
  * baseline stays older than both sides, so the conflict keeps being reported
101
- * until someone resolves it (#2620). */
102
- 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>;
103
180
  /** Liveness of the collections a sync just wrote to, checked against the skill
104
181
  * dir `deleteCollection` removes. `exists` is injected so the rule is testable
105
182
  * without a filesystem. An empty group has no survivor by definition. */
@@ -139,8 +216,15 @@ export declare function orphanedCalendarId(deleted: CalendarDeclaring, remaining
139
216
  * Returns the cleared calendar id, or null when nothing was cleared. Never
140
217
  * throws: a failed cleanup must not fail the delete it follows. */
141
218
  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[]>;
219
+ /** Sync every collection whose calendar is due — no host in this workspace has
220
+ * started one within `intervalMs` (#2678). Without that gate this walked every
221
+ * declaring group on every tick, so a second host registering the same task
222
+ * simply doubled the runs, concurrently.
223
+ *
224
+ * Dueness is not decided here, only described: the guard is handed down and
225
+ * evaluated where the marker is written, so the answer cannot go stale between
226
+ * deciding and claiming (Codex review #2680). */
227
+ export declare function syncDueCalendarCollections(workspaceRoot: string, intervalMs?: number): Promise<CalendarCollectionSyncResult[]>;
144
228
  /** The groups whose calendar has never synced. A missing token IS the "created
145
229
  * since the last sync" signal — nothing else distinguishes a new collection
146
230
  * from an edited one on the write path this feeds (#2427).
@@ -184,7 +268,12 @@ export interface ManualCalendarSyncDeps {
184
268
  * calendar sends them fixing the wrong thing. */
185
269
  export declare function syncCalendarForCollection(slug: string, workspaceRoot: string, deps?: ManualCalendarSyncDeps): Promise<ManualCalendarSyncOutcome>;
186
270
  /** Scheduler registration, shaped like `feedRefreshTaskDef` so hosts wire it
187
- * with a single line. */
271
+ * with a single line.
272
+ *
273
+ * `run` reads the interval back off the definition instead of closing over the
274
+ * option: a host rewrites `schedule` from its own overrides file AFTER this
275
+ * returns, and the due window has to follow it. Frozen at the default, a
276
+ * shortened interval would tick often and skip nearly every tick. */
188
277
  export declare function googleCalendarSyncTaskDef(opts?: {
189
278
  workspaceRoot?: string;
190
279
  intervalMs?: number;