@mulmoclaude/core 3.5.0 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/helps/collection-skills.md +49 -1
- package/assets/helps/error-recovery.md +53 -0
- package/dist/calendarGrid-CQ8MVSRb.js.map +1 -1
- package/dist/calendarGrid-DGILaVxI.cjs.map +1 -1
- package/dist/collection/core/schema.d.ts +8 -1
- package/dist/collection/core/schemaZ.d.ts +36 -26
- package/dist/collection/firestore.cjs +51 -0
- package/dist/collection/firestore.cjs.map +1 -0
- package/dist/collection/firestore.d.ts +1 -0
- package/dist/collection/firestore.js +50 -0
- package/dist/collection/firestore.js.map +1 -0
- package/dist/collection/registry/server/index.cjs +2 -2
- package/dist/collection/registry/server/index.js +2 -2
- package/dist/collection/server/appManifest.d.ts +53 -0
- package/dist/collection/server/delete.d.ts +10 -0
- package/dist/collection/server/discoveredCollection.d.ts +10 -0
- package/dist/collection/server/discovery.d.ts +1 -0
- package/dist/collection/server/firestoreDocs.d.ts +39 -0
- package/dist/collection/server/firestoreStore.d.ts +14 -0
- package/dist/collection/server/host.d.ts +41 -0
- package/dist/collection/server/index.cjs +9 -2
- package/dist/collection/server/index.d.ts +4 -1
- package/dist/collection/server/index.js +3 -3
- package/dist/collection-watchers/index.cjs +132 -41
- package/dist/collection-watchers/index.cjs.map +1 -1
- package/dist/collection-watchers/index.js +132 -41
- package/dist/collection-watchers/index.js.map +1 -1
- package/dist/collection-watchers/reconciler.d.ts +1 -1
- package/dist/{discovery-DH9wweuj.js → discovery-CdkaURVY.js} +368 -57
- package/dist/discovery-CdkaURVY.js.map +1 -0
- package/dist/{discovery-Ck4AqikY.cjs → discovery-Cw5xWzZD.cjs} +407 -54
- package/dist/discovery-Cw5xWzZD.cjs.map +1 -0
- package/dist/feeds/server/index.cjs +2 -2
- package/dist/feeds/server/index.js +2 -2
- package/dist/google/index.cjs +1 -1
- package/dist/google/index.js +1 -1
- package/dist/{server-5EMj3naj.cjs → server-9mFaGLdq.cjs} +23 -4
- package/dist/server-9mFaGLdq.cjs.map +1 -0
- package/dist/{server-BiRLLMpW.js → server-DdHaX_Uu.js} +23 -4
- package/dist/server-DdHaX_Uu.js.map +1 -0
- package/package.json +7 -1
- package/dist/discovery-Ck4AqikY.cjs.map +0 -1
- package/dist/discovery-DH9wweuj.js.map +0 -1
- package/dist/server-5EMj3naj.cjs.map +0 -1
- package/dist/server-BiRLLMpW.js.map +0 -1
|
@@ -2,8 +2,8 @@ import { t as canonicalRoot } from "../root-BMroU_mB.js";
|
|
|
2
2
|
import { R as fieldText, y as isValidCollectionName } from "../calendarGrid-CQ8MVSRb.js";
|
|
3
3
|
import { E as whenMatches, f as itemIsDone } from "../promptSafety-CpiME8pj.js";
|
|
4
4
|
import "../collection/index.js";
|
|
5
|
-
import {
|
|
6
|
-
import { d as isTriggerDue, f as maybeSpawnSuccessor } from "../server-
|
|
5
|
+
import { H as itemFilePath, Q as collectionChangePayload, ct as publishCollectionChange, d as storeFor, n as discoverCollections, ot as peekWorkspaceRoot, r as loadCollection, tt as firestoreHandle } from "../discovery-CdkaURVY.js";
|
|
6
|
+
import { d as isTriggerDue, f as maybeSpawnSuccessor } from "../server-DdHaX_Uu.js";
|
|
7
7
|
import { d as publish, m as updateForPlugin, n as clear, s as listAll } from "../notifier-DhnwR82U.js";
|
|
8
8
|
import { access } from "node:fs/promises";
|
|
9
9
|
//#region src/collection-watchers/config.ts
|
|
@@ -132,16 +132,28 @@ function resolveDisplayLabel(schema, item, itemId) {
|
|
|
132
132
|
const label = fieldText(item[displayField]).trim();
|
|
133
133
|
return label.length > 0 ? label : itemId;
|
|
134
134
|
}
|
|
135
|
+
/** The scope a collection's bells belong to.
|
|
136
|
+
*
|
|
137
|
+
* A shared collection's `appId` wins over the call's `workspaceRoot`: the
|
|
138
|
+
* root is where this host happens to have the repository checked out, which
|
|
139
|
+
* is exactly the fact that must NOT reach the identity. A local collection is
|
|
140
|
+
* unchanged — its root is present only when the call carried an explicit one,
|
|
141
|
+
* so a single-workspace host's ids stay byte-identical to what it has already
|
|
142
|
+
* written to `active.json`. */
|
|
143
|
+
function bellScopeOf(collection, ioOpts) {
|
|
144
|
+
return collection.appId === void 0 ? { root: ioOpts.workspaceRoot } : { aid: collection.appId };
|
|
145
|
+
}
|
|
146
|
+
var scopedLegacyId = (slug, itemId, scope) => completionLegacyId(slug, itemId, scope.root, scope.aid);
|
|
135
147
|
/** Every active bell entry whose key matches this (slug, itemId).
|
|
136
148
|
* Returns multiple when defensive cleanup is needed. Scans `listAll()`
|
|
137
149
|
* — cheap because the active set is bounded. */
|
|
138
|
-
async function findActiveEntries(slug, itemId,
|
|
150
|
+
async function findActiveEntries(slug, itemId, scope) {
|
|
139
151
|
const adapter = requireAdapter();
|
|
140
|
-
const legacyId =
|
|
152
|
+
const legacyId = scopedLegacyId(slug, itemId, scope);
|
|
141
153
|
return (await listAll()).filter((entry) => adapter.readEntry(entry.pluginData)?.legacyId === legacyId);
|
|
142
154
|
}
|
|
143
|
-
async function findActiveEntryIds(slug, itemId,
|
|
144
|
-
return (await findActiveEntries(slug, itemId,
|
|
155
|
+
async function findActiveEntryIds(slug, itemId, scope) {
|
|
156
|
+
return (await findActiveEntries(slug, itemId, scope)).map((entry) => entry.id);
|
|
145
157
|
}
|
|
146
158
|
var ensureLocks = /* @__PURE__ */ new Map();
|
|
147
159
|
/** Bell priority for a record: the FIRST flagged value in `notifyWhen.in`
|
|
@@ -154,8 +166,8 @@ function notifyPriorityForItem(schema, item) {
|
|
|
154
166
|
const value = item[spec.field] === void 0 || item[spec.field] === null ? "" : String(item[spec.field]);
|
|
155
167
|
return spec.in.indexOf(value) === 0 ? "high" : "normal";
|
|
156
168
|
}
|
|
157
|
-
async function ensureItemNotification(slug, schema, itemId, displayLabel, priority,
|
|
158
|
-
const legacyId =
|
|
169
|
+
async function ensureItemNotification(slug, schema, itemId, displayLabel, priority, scope) {
|
|
170
|
+
const legacyId = scopedLegacyId(slug, itemId, scope);
|
|
159
171
|
while (true) {
|
|
160
172
|
const inflight = ensureLocks.get(legacyId);
|
|
161
173
|
if (!inflight) break;
|
|
@@ -168,7 +180,7 @@ async function ensureItemNotification(slug, schema, itemId, displayLabel, priori
|
|
|
168
180
|
legacyId,
|
|
169
181
|
displayLabel,
|
|
170
182
|
priority,
|
|
171
|
-
|
|
183
|
+
scope
|
|
172
184
|
}) };
|
|
173
185
|
ensureLocks.set(legacyId, lock);
|
|
174
186
|
try {
|
|
@@ -181,8 +193,9 @@ async function ensureItemNotification(slug, schema, itemId, displayLabel, priori
|
|
|
181
193
|
* place (preserving id / position / createdAt) so a record whose flagged
|
|
182
194
|
* value changed while it stayed pending re-colours the bell without a
|
|
183
195
|
* clear+republish flicker. No-op when the stored priority already matches. */
|
|
184
|
-
async function reconcileEntrySeverity(slug, itemId, entries, priority,
|
|
196
|
+
async function reconcileEntrySeverity(slug, itemId, entries, priority, scope) {
|
|
185
197
|
const adapter = requireAdapter();
|
|
198
|
+
const { root } = scope;
|
|
186
199
|
for (const entry of entries) {
|
|
187
200
|
const parsed = adapter.readEntry(entry.pluginData);
|
|
188
201
|
if (!parsed || parsed.priority === priority) continue;
|
|
@@ -199,12 +212,13 @@ async function reconcileEntrySeverity(slug, itemId, entries, priority, root) {
|
|
|
199
212
|
});
|
|
200
213
|
}
|
|
201
214
|
}
|
|
202
|
-
async function doEnsureItemNotification({ slug, schema, itemId, legacyId, displayLabel, priority,
|
|
215
|
+
async function doEnsureItemNotification({ slug, schema, itemId, legacyId, displayLabel, priority, scope }) {
|
|
203
216
|
const adapter = requireAdapter();
|
|
217
|
+
const { root } = scope;
|
|
204
218
|
try {
|
|
205
|
-
const existing = await findActiveEntries(slug, itemId,
|
|
219
|
+
const existing = await findActiveEntries(slug, itemId, scope);
|
|
206
220
|
if (existing.length > 0) {
|
|
207
|
-
await reconcileEntrySeverity(slug, itemId, existing, priority,
|
|
221
|
+
await reconcileEntrySeverity(slug, itemId, existing, priority, scope);
|
|
208
222
|
return;
|
|
209
223
|
}
|
|
210
224
|
const navigateTarget = adapter.buildNavigateTarget(slug, itemId, root);
|
|
@@ -234,9 +248,18 @@ async function doEnsureItemNotification({ slug, schema, itemId, legacyId, displa
|
|
|
234
248
|
/** Idempotently clear EVERY bell entry that matches this (slug, itemId).
|
|
235
249
|
* Silent no-op when nothing matches. The "every" is defensive: if a
|
|
236
250
|
* duplicate ever slips through, this drains the lot. */
|
|
237
|
-
async function clearItemNotification(slug, itemId, root) {
|
|
251
|
+
async function clearItemNotification(slug, itemId, root, aid) {
|
|
252
|
+
await clearScoped(slug, itemId, {
|
|
253
|
+
root,
|
|
254
|
+
aid
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
/** The scoped form every internal caller uses. `clearItemNotification` keeps
|
|
258
|
+
* its positional `(slug, itemId, root?)` shape because it is public API
|
|
259
|
+
* (MulmoTerminal calls it); `aid` is additive. */
|
|
260
|
+
async function clearScoped(slug, itemId, scope) {
|
|
238
261
|
try {
|
|
239
|
-
const ids = await findActiveEntryIds(slug, itemId,
|
|
262
|
+
const ids = await findActiveEntryIds(slug, itemId, scope);
|
|
240
263
|
for (const entryId of ids) await clear(entryId);
|
|
241
264
|
} catch (err) {
|
|
242
265
|
log().warn("notify clear failed", {
|
|
@@ -256,19 +279,19 @@ async function clearItemNotification(slug, itemId, root) {
|
|
|
256
279
|
* `{ workspaceRoot: <tmpdir> }` so the check accepts a fixture dataDir. */
|
|
257
280
|
async function reconcileItem(collection, itemId, ioOpts = {}, now = evalNow()) {
|
|
258
281
|
const { slug, schema } = collection;
|
|
259
|
-
const
|
|
282
|
+
const scope = bellScopeOf(collection, ioOpts);
|
|
260
283
|
if (!schema.completionField) {
|
|
261
|
-
await
|
|
284
|
+
await clearScoped(slug, itemId, scope);
|
|
262
285
|
return;
|
|
263
286
|
}
|
|
264
287
|
const item = await storeFor(collection, ioOpts).read(itemId);
|
|
265
288
|
if (item === null) {
|
|
266
|
-
await
|
|
289
|
+
await clearScoped(slug, itemId, scope);
|
|
267
290
|
return;
|
|
268
291
|
}
|
|
269
292
|
await maybeSpawnSuccessor(collection, item, itemId, ioOpts);
|
|
270
293
|
if (itemIsDone(schema, item)) {
|
|
271
|
-
await
|
|
294
|
+
await clearScoped(slug, itemId, scope);
|
|
272
295
|
return;
|
|
273
296
|
}
|
|
274
297
|
if (schema.triggerField) {
|
|
@@ -280,15 +303,15 @@ async function reconcileItem(collection, itemId, ioOpts = {}, now = evalNow()) {
|
|
|
280
303
|
triggerField: schema.triggerField
|
|
281
304
|
});
|
|
282
305
|
if (due !== true) {
|
|
283
|
-
await
|
|
306
|
+
await clearScoped(slug, itemId, scope);
|
|
284
307
|
return;
|
|
285
308
|
}
|
|
286
309
|
}
|
|
287
310
|
if (!whenMatches(schema.notifyWhen, item)) {
|
|
288
|
-
await
|
|
311
|
+
await clearScoped(slug, itemId, scope);
|
|
289
312
|
return;
|
|
290
313
|
}
|
|
291
|
-
await ensureItemNotification(slug, schema, itemId, resolveDisplayLabel(schema, item, itemId), notifyPriorityForItem(schema, item),
|
|
314
|
+
await ensureItemNotification(slug, schema, itemId, resolveDisplayLabel(schema, item, itemId), notifyPriorityForItem(schema, item), scope);
|
|
292
315
|
}
|
|
293
316
|
/** Boot-time reconcile: walk every record of the collection once (through
|
|
294
317
|
* its store) and reconcile it. Catches up changes that happened while the
|
|
@@ -334,7 +357,7 @@ async function reconcileAllItems(collection, ioOpts = {}, now = evalNow()) {
|
|
|
334
357
|
* ever clear, while the reconcile publishes a second rooted one beside it.
|
|
335
358
|
* Clearing converges — the record republishes rooted if still pending. */
|
|
336
359
|
function sweepVerdict(entry, ownRoot) {
|
|
337
|
-
if (entry.aid !== void 0) return "
|
|
360
|
+
if (entry.aid !== void 0) return "shared";
|
|
338
361
|
if (entry.root === ownRoot) return "mine";
|
|
339
362
|
if (entry.root === void 0) return "drop-legacy";
|
|
340
363
|
return "skip";
|
|
@@ -344,12 +367,40 @@ function sweepVerdict(entry, ownRoot) {
|
|
|
344
367
|
* `notifyWhen`. The reverse of the reconcile invariant, applied to an entry
|
|
345
368
|
* whose record the forward pass can never walk to (a delete leaves a bell that
|
|
346
369
|
* a walk over the SURVIVING records cannot clear). */
|
|
347
|
-
async function isStaleEntry(slug, itemId, opts) {
|
|
370
|
+
async function isStaleEntry(slug, itemId, opts, aid) {
|
|
348
371
|
const collection = await loadCollection(slug, opts);
|
|
372
|
+
if (aid !== void 0 && collection?.appId !== aid) return false;
|
|
349
373
|
if (!collection || !collection.schema.completionField) return true;
|
|
374
|
+
return recordNoLongerBells(collection, itemId, opts);
|
|
375
|
+
}
|
|
376
|
+
/** The record half of the staleness question: gone, done, or outside
|
|
377
|
+
* `notifyWhen`. Split out so the caller stays one decision — WHOSE entry this
|
|
378
|
+
* is — and this one stays the other. */
|
|
379
|
+
async function recordNoLongerBells(collection, itemId, opts) {
|
|
350
380
|
const item = await storeFor(collection, opts).read(itemId);
|
|
351
381
|
return item === null || itemIsDone(collection.schema, item) || !whenMatches(collection.schema.notifyWhen, item);
|
|
352
382
|
}
|
|
383
|
+
/** One entry's fate: whose it is (`sweepVerdict`), then — when it is this
|
|
384
|
+
* sweep's to judge — whether its record still justifies the bell. Extracted so
|
|
385
|
+
* the loop above stays a walk and this stays the decision. */
|
|
386
|
+
async function sweepOneEntry(entryId, parsed, ownRoot, opts) {
|
|
387
|
+
const verdict = sweepVerdict(parsed, ownRoot);
|
|
388
|
+
if (verdict === "skip") return;
|
|
389
|
+
if (verdict === "drop-legacy") {
|
|
390
|
+
await clear(entryId);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
const { slug, itemId } = parsed;
|
|
394
|
+
try {
|
|
395
|
+
if (await isStaleEntry(slug, itemId, opts, verdict === "shared" ? parsed.aid : void 0)) await clear(entryId);
|
|
396
|
+
} catch (err) {
|
|
397
|
+
log().warn("sweep entry failed", {
|
|
398
|
+
slug,
|
|
399
|
+
itemId,
|
|
400
|
+
error: errMsg(err)
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
}
|
|
353
404
|
async function sweepStaleActiveEntries(opts = {}) {
|
|
354
405
|
const adapter = requireAdapter();
|
|
355
406
|
let entries;
|
|
@@ -365,22 +416,7 @@ async function sweepStaleActiveEntries(opts = {}) {
|
|
|
365
416
|
if (!own) continue;
|
|
366
417
|
const parsed = parseCompletionLegacyId(own.legacyId);
|
|
367
418
|
if (!parsed) continue;
|
|
368
|
-
|
|
369
|
-
if (verdict === "skip") continue;
|
|
370
|
-
if (verdict === "drop-legacy") {
|
|
371
|
-
await clear(entry.id);
|
|
372
|
-
continue;
|
|
373
|
-
}
|
|
374
|
-
const { slug, itemId } = parsed;
|
|
375
|
-
try {
|
|
376
|
-
if (await isStaleEntry(slug, itemId, opts)) await clear(entry.id);
|
|
377
|
-
} catch (err) {
|
|
378
|
-
log().warn("sweep entry failed", {
|
|
379
|
-
slug,
|
|
380
|
-
itemId,
|
|
381
|
-
error: errMsg(err)
|
|
382
|
-
});
|
|
383
|
-
}
|
|
419
|
+
await sweepOneEntry(entry.id, parsed, ownRoot, opts);
|
|
384
420
|
}
|
|
385
421
|
}
|
|
386
422
|
/** Test-only: clear the per-key in-flight locks. */
|
|
@@ -590,9 +626,58 @@ async function _tickTimeTriggersForTesting(now, workspaceRoot) {
|
|
|
590
626
|
async function tickTimeTriggers(gen, now = evalNow()) {
|
|
591
627
|
for (const entry of gen.watchers.values()) {
|
|
592
628
|
const { schema } = entry.collection;
|
|
629
|
+
if (cannotReportChanges(gen, entry.collection)) continue;
|
|
593
630
|
if (!schema.triggerField && !schema.spawn) continue;
|
|
594
631
|
await reconcileAllItems(entry.collection, gen.discoveryOpts, now);
|
|
595
632
|
}
|
|
633
|
+
await tickUnwatchedCollections(gen, now);
|
|
634
|
+
}
|
|
635
|
+
/** True when the collection's store implements no `watch` — nothing will ever
|
|
636
|
+
* tell this module its records moved, so the clock tick is its only change
|
|
637
|
+
* detection. A capability question, deliberately not a backend one: the day a
|
|
638
|
+
* shared collection's store grows an `onSnapshot` watch, it stops being
|
|
639
|
+
* special here with no edit to this file. */
|
|
640
|
+
function cannotReportChanges(gen, collection) {
|
|
641
|
+
return storeFor(collection, gen.discoveryOpts).watch === void 0;
|
|
642
|
+
}
|
|
643
|
+
/** True when a schema declares behaviour that only a reconcile pass can
|
|
644
|
+
* produce: bells (`completionField`), date-triggered bells (`triggerField`),
|
|
645
|
+
* or recurrence successors (`spawn`). */
|
|
646
|
+
function needsReconcilePass(schema) {
|
|
647
|
+
return Boolean(schema.completionField ?? schema.triggerField ?? schema.spawn);
|
|
648
|
+
}
|
|
649
|
+
/** One unwatched collection's reconcile pass. Extracted so the single-flight
|
|
650
|
+
* callback doesn't close over loop state.
|
|
651
|
+
*
|
|
652
|
+
* `reconcileAllItems` already swallows a failing store read (it logs and
|
|
653
|
+
* returns), so a closed session or a denied rule surfaces there, not as a
|
|
654
|
+
* rejection here. This catch is only for the unexpected — it must not let one
|
|
655
|
+
* collection's fault abort the rest of the tick. */
|
|
656
|
+
async function reconcileUnwatched(gen, collection, now) {
|
|
657
|
+
try {
|
|
658
|
+
await runSingleFlight(gen.collectionSlots, collection.slug, () => reconcileAllItems(collection, gen.discoveryOpts, now));
|
|
659
|
+
} catch (err) {
|
|
660
|
+
log().warn("unwatched collection reconcile failed", {
|
|
661
|
+
slug: collection.slug,
|
|
662
|
+
error: errMsg(err)
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
/** Stand in for the store-change path (2) on backends that don't have one.
|
|
667
|
+
*
|
|
668
|
+
* Gated on `needsReconcilePass` so a collection declaring none of it costs
|
|
669
|
+
* nothing: unlike the local backends, every pass here is a network round trip.
|
|
670
|
+
*
|
|
671
|
+
* A collection that DROPS OUT of eligibility (its `completionField` edited
|
|
672
|
+
* away, or the collection removed) is not this pass's problem — `syncWatchers`
|
|
673
|
+
* already sweeps on a changed schema and on a vanished slug, and the sweep is
|
|
674
|
+
* what clears bells the declaration no longer justifies. */
|
|
675
|
+
async function tickUnwatchedCollections(gen, now) {
|
|
676
|
+
if (firestoreHandle() === null) return;
|
|
677
|
+
const pending = [...gen.watchers.values()].map((entry) => entry.collection).filter((collection) => cannotReportChanges(gen, collection) && needsReconcilePass(collection.schema));
|
|
678
|
+
if (pending.length === 0) return;
|
|
679
|
+
for (const collection of pending) await reconcileUnwatched(gen, collection, now);
|
|
680
|
+
await sweepStaleActiveEntries(gen.discoveryOpts);
|
|
596
681
|
}
|
|
597
682
|
/** Reconcile the watcher set against the currently-discovered
|
|
598
683
|
* collections. Adds watchers for new slugs (with a boot reconcile of
|
|
@@ -633,7 +718,13 @@ function stopVanishedWatchers(gen, liveSlugs) {
|
|
|
633
718
|
* modes. The mounted fs.watch is bound to the OLD location, so it must
|
|
634
719
|
* be remounted, not just re-reconciled. */
|
|
635
720
|
function storagePathChanged(previous, next) {
|
|
636
|
-
return previous.dataSource?.path !== next.dataSource?.path || previous.dataPath !== next.dataPath || previous.storage?.
|
|
721
|
+
return previous.dataSource?.path !== next.dataSource?.path || previous.dataPath !== next.dataPath || previous.storage?.type !== next.storage?.type || storageFilePath(previous.storage) !== storageFilePath(next.storage);
|
|
722
|
+
}
|
|
723
|
+
/** The on-disk path of a storage backend, or undefined when it has none (a
|
|
724
|
+
* shared collection keeps its records off this filesystem, so there is no
|
|
725
|
+
* mount to move — only a `type` change matters for it). */
|
|
726
|
+
function storageFilePath(storage) {
|
|
727
|
+
return storage?.type === "sqlite" ? storage.path : void 0;
|
|
637
728
|
}
|
|
638
729
|
/** Re-reconcile already-watched collections whose schema changed since
|
|
639
730
|
* the last tick. New collections fall through to `startNewWatchers`. */
|