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