@mulmoclaude/core 0.25.0 → 0.26.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 (82) hide show
  1. package/assets/helps/billing-clients-worklog.md +3 -2
  2. package/assets/helps/collection-skills.md +54 -1
  3. package/assets/helps/custom-view.md +37 -3
  4. package/assets/helps/error-recovery.md +30 -0
  5. package/assets/helps/todo-collection.md +2 -2
  6. package/dist/collection/core/fieldText.d.ts +11 -0
  7. package/dist/collection/core/project.d.ts +3 -0
  8. package/dist/collection/core/schema.d.ts +13 -1
  9. package/dist/collection/core/schemaZ.d.ts +16 -0
  10. package/dist/collection/index.cjs +18 -10
  11. package/dist/collection/index.cjs.map +1 -1
  12. package/dist/collection/index.d.ts +2 -0
  13. package/dist/collection/index.js +15 -11
  14. package/dist/collection/index.js.map +1 -1
  15. package/dist/collection/registry/server/index.cjs +2 -2
  16. package/dist/collection/registry/server/index.js +2 -2
  17. package/dist/collection/server/csvStore.d.ts +8 -1
  18. package/dist/collection/server/discoveredCollection.d.ts +4 -0
  19. package/dist/collection/server/discovery.d.ts +1 -0
  20. package/dist/collection/server/index.cjs +20 -17
  21. package/dist/collection/server/index.d.ts +1 -0
  22. package/dist/collection/server/index.js +4 -4
  23. package/dist/collection/server/queryRunner.d.ts +4 -0
  24. package/dist/collection/server/spawn.d.ts +2 -1
  25. package/dist/collection/server/sqliteStore.d.ts +14 -0
  26. package/dist/collection/server/store.d.ts +43 -8
  27. package/dist/collection/server/storePage.d.ts +32 -0
  28. package/dist/collection-watchers/index.cjs +167 -43
  29. package/dist/collection-watchers/index.cjs.map +1 -1
  30. package/dist/collection-watchers/index.d.ts +1 -1
  31. package/dist/collection-watchers/index.js +168 -45
  32. package/dist/collection-watchers/index.js.map +1 -1
  33. package/dist/collection-watchers/reconciler.d.ts +13 -11
  34. package/dist/collection-watchers/watcher.d.ts +4 -3
  35. package/dist/{discovery-3K_Wtkvn.js → discovery-QUi_7aNG.js} +882 -7
  36. package/dist/discovery-QUi_7aNG.js.map +1 -0
  37. package/dist/{discovery-BZ2ms1tE.cjs → discovery-cwtfyDxl.cjs} +994 -4
  38. package/dist/discovery-cwtfyDxl.cjs.map +1 -0
  39. package/dist/feeds/index.cjs +2 -2
  40. package/dist/feeds/index.js +2 -2
  41. package/dist/feeds/server/index.cjs +26 -14
  42. package/dist/feeds/server/index.cjs.map +1 -1
  43. package/dist/feeds/server/index.js +26 -14
  44. package/dist/feeds/server/index.js.map +1 -1
  45. package/dist/google/index.cjs +9 -11
  46. package/dist/google/index.cjs.map +1 -1
  47. package/dist/google/index.js +9 -11
  48. package/dist/google/index.js.map +1 -1
  49. package/dist/{ids-BR4U1dJD.cjs → ids-Dl9Ij63K.cjs} +14 -1
  50. package/dist/ids-Dl9Ij63K.cjs.map +1 -0
  51. package/dist/{ids-D4e10971.js → ids-P2T89Gou.js} +9 -2
  52. package/dist/ids-P2T89Gou.js.map +1 -0
  53. package/dist/{ingestTypes-BsGOjR2l.js → ingestTypes-C-IQB4nw.js} +2 -2
  54. package/dist/{ingestTypes-BsGOjR2l.js.map → ingestTypes-C-IQB4nw.js.map} +1 -1
  55. package/dist/{ingestTypes-CygUXsjf.cjs → ingestTypes-DZpAC3N_.cjs} +2 -2
  56. package/dist/{ingestTypes-CygUXsjf.cjs.map → ingestTypes-DZpAC3N_.cjs.map} +1 -1
  57. package/dist/project-BWI5w_BT.cjs +17 -0
  58. package/dist/project-BWI5w_BT.cjs.map +1 -0
  59. package/dist/project-bU98ycsy.js +12 -0
  60. package/dist/project-bU98ycsy.js.map +1 -0
  61. package/dist/{promptSafety-Boz5qOs-.js → promptSafety-B5KPkaNY.js} +32 -9
  62. package/dist/promptSafety-B5KPkaNY.js.map +1 -0
  63. package/dist/{promptSafety-Co4cYQ_Z.cjs → promptSafety-BOc1B3im.cjs} +43 -8
  64. package/dist/promptSafety-BOc1B3im.cjs.map +1 -0
  65. package/dist/remote-view/index.cjs +5 -3
  66. package/dist/remote-view/index.cjs.map +1 -1
  67. package/dist/remote-view/index.d.ts +3 -1
  68. package/dist/remote-view/index.js +5 -3
  69. package/dist/remote-view/index.js.map +1 -1
  70. package/dist/{server-DLRz-U7F.cjs → server-B7mtx4Cd.cjs} +302 -785
  71. package/dist/server-B7mtx4Cd.cjs.map +1 -0
  72. package/dist/{server-DZH7OjOD.js → server-CNLZZONa.js} +292 -696
  73. package/dist/server-CNLZZONa.js.map +1 -0
  74. package/package.json +1 -1
  75. package/dist/discovery-3K_Wtkvn.js.map +0 -1
  76. package/dist/discovery-BZ2ms1tE.cjs.map +0 -1
  77. package/dist/ids-BR4U1dJD.cjs.map +0 -1
  78. package/dist/ids-D4e10971.js.map +0 -1
  79. package/dist/promptSafety-Boz5qOs-.js.map +0 -1
  80. package/dist/promptSafety-Co4cYQ_Z.cjs.map +0 -1
  81. package/dist/server-DLRz-U7F.cjs.map +0 -1
  82. package/dist/server-DZH7OjOD.js.map +0 -1
@@ -1,4 +1,4 @@
1
1
  export { configureCollectionWatchers, resetCollectionWatchersConfig, type CollectionNotificationAdapter, type CollectionWatcherLogger, type CompletionPriority, } from './config.js';
2
2
  export { reconcileItem, reconcileAllItems, sweepStaleActiveEntries, clearItemNotification, resolveDisplayLabel, itemIsDone, _resetReconcilerLocksForTesting, } from './reconciler.js';
3
3
  export { evalNow } from './clock.js';
4
- export { startCollectionWatchers, stopCollectionWatchers, _syncWatchersForTesting, _tickTimeTriggersForTesting, _scheduleItemReconcileForTesting, type CollectionWatcherOptions, } from './watcher.js';
4
+ export { startCollectionWatchers, stopCollectionWatchers, _syncWatchersForTesting, _tickTimeTriggersForTesting, _scheduleItemReconcileForTesting, _scheduleStorageReconcileForTesting, type CollectionWatcherOptions, } from './watcher.js';
@@ -1,11 +1,11 @@
1
- import { S as itemIsDone, j as whenMatches } from "../promptSafety-Boz5qOs-.js";
1
+ import { M as fieldText, S as itemIsDone, j as whenMatches } from "../promptSafety-B5KPkaNY.js";
2
2
  import "../collection/index.js";
3
- import { I as publishCollectionChange, _ as readItem, n as discoverCollections, p as listItems, r as loadCollection } from "../discovery-3K_Wtkvn.js";
4
- import { d as isTriggerDue, f as maybeSpawnSuccessor } from "../server-DZH7OjOD.js";
3
+ import { W as itemFilePath, n as discoverCollections, r as loadCollection, tt as publishCollectionChange, u as storeFor } from "../discovery-QUi_7aNG.js";
4
+ import { d as isTriggerDue, f as maybeSpawnSuccessor } from "../server-CNLZZONa.js";
5
5
  import { d as publish, m as updateForPlugin, n as clear, s as listAll } from "../notifier-ChpY0XrY.js";
6
6
  import path from "node:path";
7
7
  import { watch } from "node:fs";
8
- import { mkdir } from "node:fs/promises";
8
+ import { access, mkdir } from "node:fs/promises";
9
9
  //#region src/collection-watchers/config.ts
10
10
  var NOOP_LOG = {
11
11
  info: () => {},
@@ -73,9 +73,7 @@ function parseCompletionLegacyId(legacyId) {
73
73
  function resolveDisplayLabel(schema, item, itemId) {
74
74
  const { displayField } = schema;
75
75
  if (!displayField) return itemId;
76
- const raw = item[displayField];
77
- if (raw === void 0 || raw === null) return itemId;
78
- const label = String(raw).trim();
76
+ const label = fieldText(item[displayField]).trim();
79
77
  return label.length > 0 ? label : itemId;
80
78
  }
81
79
  /** Every active bell entry whose key matches this (slug, itemId).
@@ -182,24 +180,26 @@ async function clearItemNotification(slug, itemId) {
182
180
  });
183
181
  }
184
182
  }
185
- /** Reconcile one item to the desired bell state. Re-reads the record from
186
- * disk so the decision is grounded in current truth, not in the event
187
- * payload. Safe to call when the file is missing (delete path).
183
+ /** Reconcile one item to the desired bell state. Re-reads the record
184
+ * through the collection's STORE (file, sqlite, …) so the decision is
185
+ * grounded in current truth, not in the event payload. Safe to call when
186
+ * the record is missing (delete path).
188
187
  *
189
- * `ioOpts` flows into `readItem`'s workspace-containment check
188
+ * `ioOpts` flows into the store's workspace-containment checks
190
189
  * production callers (the watcher) pass nothing; tests pass
191
190
  * `{ workspaceRoot: <tmpdir> }` so the check accepts a fixture dataDir. */
192
- async function reconcileItem(slug, schema, dataDir, itemId, ioOpts = {}, now = evalNow()) {
191
+ async function reconcileItem(collection, itemId, ioOpts = {}, now = evalNow()) {
192
+ const { slug, schema } = collection;
193
193
  if (!schema.completionField) {
194
194
  await clearItemNotification(slug, itemId);
195
195
  return;
196
196
  }
197
- const item = await readItem(dataDir, itemId, ioOpts);
197
+ const item = await storeFor(collection, ioOpts).read(itemId);
198
198
  if (item === null) {
199
199
  await clearItemNotification(slug, itemId);
200
200
  return;
201
201
  }
202
- await maybeSpawnSuccessor(slug, schema, dataDir, item, itemId, ioOpts);
202
+ await maybeSpawnSuccessor(collection, item, itemId, ioOpts);
203
203
  if (itemIsDone(schema, item)) {
204
204
  await clearItemNotification(slug, itemId);
205
205
  return;
@@ -223,19 +223,20 @@ async function reconcileItem(slug, schema, dataDir, itemId, ioOpts = {}, now = e
223
223
  }
224
224
  await ensureItemNotification(slug, schema, itemId, resolveDisplayLabel(schema, item, itemId), notifyPriorityForItem(schema, item));
225
225
  }
226
- /** Boot-time reconcile: walk every record under `dataDir` once and
227
- * reconcile it. Catches up changes that happened while the server was
228
- * down. Deleted items are covered by `sweepStaleActiveEntries`, not this
229
- * function (it only sees files that exist). */
230
- async function reconcileAllItems(slug, schema, dataDir, ioOpts = {}, now = evalNow()) {
226
+ /** Boot-time reconcile: walk every record of the collection once (through
227
+ * its store) and reconcile it. Catches up changes that happened while the
228
+ * server was down. Deleted items are covered by
229
+ * `sweepStaleActiveEntries`, not this function (it only sees records
230
+ * that exist). */
231
+ async function reconcileAllItems(collection, ioOpts = {}, now = evalNow()) {
232
+ const { slug, schema } = collection;
231
233
  if (!schema.completionField) return;
232
234
  let items;
233
235
  try {
234
- items = await listItems(dataDir, ioOpts);
236
+ items = await storeFor(collection, ioOpts).list();
235
237
  } catch (err) {
236
238
  log().warn("reconcile list failed", {
237
239
  slug,
238
- dataDir,
239
240
  error: errMsg(err)
240
241
  });
241
242
  return;
@@ -244,7 +245,7 @@ async function reconcileAllItems(slug, schema, dataDir, ioOpts = {}, now = evalN
244
245
  for (const item of items) {
245
246
  const raw = item[primaryKey];
246
247
  if (typeof raw !== "string" || raw.length === 0) continue;
247
- await reconcileItem(slug, schema, dataDir, raw, ioOpts, now);
248
+ await reconcileItem(collection, raw, ioOpts, now);
248
249
  }
249
250
  }
250
251
  /** Boot-time sweep over the active bell: drop any entries whose underlying
@@ -272,7 +273,7 @@ async function sweepStaleActiveEntries(opts = {}) {
272
273
  await clear(entry.id);
273
274
  continue;
274
275
  }
275
- const item = await readItem(collection.dataDir, itemId, opts);
276
+ const item = await storeFor(collection, opts).read(itemId);
276
277
  if (item === null || itemIsDone(collection.schema, item) || !whenMatches(collection.schema.notifyWhen, item)) await clear(entry.id);
277
278
  } catch (err) {
278
279
  log().warn("sweep entry failed", {
@@ -304,6 +305,10 @@ var started = false;
304
305
  * without threading through every signature. */
305
306
  var discoveryOpts = {};
306
307
  var itemSlots = /* @__PURE__ */ new Map();
308
+ /** Per-slug single-flight for a storage (db-file) collection's full
309
+ * reconcile pass — a burst of db writes collapses to one pass + one
310
+ * trailing re-run, mirroring the per-item slots of the file watcher. */
311
+ var storageSlots = /* @__PURE__ */ new Map();
307
312
  /** Trailing debounce per dataSource collection: an atomic file replace
308
313
  * (Excel save, editor rename) surfaces as 2-3 fs events — collapse them
309
314
  * into one change publish so live views refetch once. */
@@ -359,6 +364,7 @@ async function stopCollectionWatchers() {
359
364
  } catch {}
360
365
  watchers.clear();
361
366
  itemSlots.clear();
367
+ storageSlots.clear();
362
368
  for (const timer of dataSourceTimers.values()) clearTimeout(timer);
363
369
  dataSourceTimers.clear();
364
370
  discoveryOpts = {};
@@ -392,7 +398,7 @@ async function tickTimeTriggers(now = evalNow()) {
392
398
  }
393
399
  if (schema.dataSource !== void 0) continue;
394
400
  if (!schema.triggerField && !schema.spawn) continue;
395
- await reconcileAllItems(entry.slug, schema, entry.dataDir, discoveryOpts, now);
401
+ await reconcileAllItems(entry.collection, discoveryOpts, now);
396
402
  }
397
403
  }
398
404
  /** Reconcile the watcher set against the currently-discovered
@@ -438,7 +444,7 @@ function storagePathChanged(previousJson, next) {
438
444
  } catch {
439
445
  return true;
440
446
  }
441
- return previous.dataSource?.path !== next.dataSource?.path || previous.dataPath !== next.dataPath;
447
+ return previous.dataSource?.path !== next.dataSource?.path || previous.dataPath !== next.dataPath || previous.storage?.path !== next.storage?.path;
442
448
  }
443
449
  /** Re-reconcile already-watched collections whose schema changed since
444
450
  * the last tick. New collections fall through to `startNewWatchers`. */
@@ -463,6 +469,7 @@ async function reconcileChangedSchemas(collections) {
463
469
  continue;
464
470
  }
465
471
  existing.schemaJson = nextJson;
472
+ existing.collection = collection;
466
473
  if (collection.schema.dataSource !== void 0) {
467
474
  log().info("dataSource watcher schema changed, publishing", { slug: collection.slug });
468
475
  publishCollectionChange({
@@ -473,7 +480,7 @@ async function reconcileChangedSchemas(collections) {
473
480
  continue;
474
481
  }
475
482
  log().info("watcher schema changed, re-reconciling", { slug: collection.slug });
476
- await reconcileAllItems(collection.slug, collection.schema, collection.dataDir, discoveryOpts);
483
+ await reconcileAllItems(collection, discoveryOpts);
477
484
  mutated = true;
478
485
  }
479
486
  return mutated;
@@ -483,7 +490,8 @@ async function startNewWatchers(collections) {
483
490
  for (const collection of collections) {
484
491
  if (watchers.has(collection.slug)) continue;
485
492
  if (collection.schema.dataSource !== void 0) await startDataSourceWatcher(collection);
486
- else await startWatcherFor(collection.slug, collection.schema, collection.dataDir);
493
+ else if (collection.schema.storage !== void 0) await startStorageWatcher(collection);
494
+ else await startWatcherFor(collection);
487
495
  mutated = true;
488
496
  }
489
497
  return mutated;
@@ -529,7 +537,8 @@ async function startDataSourceWatcher(collection) {
529
537
  slug: collection.slug,
530
538
  dataDir: dir,
531
539
  watcher,
532
- schemaJson: JSON.stringify(collection.schema)
540
+ schemaJson: JSON.stringify(collection.schema),
541
+ collection
533
542
  });
534
543
  log().info("dataSource watcher started", {
535
544
  slug: collection.slug,
@@ -542,10 +551,76 @@ async function startDataSourceWatcher(collection) {
542
551
  });
543
552
  }
544
553
  }
545
- async function startWatcherFor(slug, schema, dataDir) {
554
+ /** Watch a `storage` collection's database file. One db file holds every
555
+ * record, so an event can't name WHICH record changed — each (debounced)
556
+ * event runs a full `reconcileAllItems` pass (bells / spawn) plus a
557
+ * change publish so views refetch after EXTERNAL edits too (host writes
558
+ * already publish their own change events; the extra ping is debounced
559
+ * and idempotent). Mounts on the parent dir, same as the dataSource
560
+ * watcher, so an atomic replace can't strand the watch. */
561
+ async function startStorageWatcher(collection) {
562
+ const file = collection.storageFile;
563
+ if (file === void 0) return;
564
+ const dir = path.dirname(file);
565
+ const base = path.basename(file);
566
+ try {
567
+ await mkdir(dir, { recursive: true });
568
+ await reconcileAllItems(collection, discoveryOpts);
569
+ const watcher = watch(dir, { persistent: false }, (_eventType, rawFilename) => {
570
+ const filename = rawFilename === null ? null : String(rawFilename);
571
+ if (filename !== null && !filename.startsWith(base)) return;
572
+ scheduleStorageReconcile(collection.slug).catch((err) => {
573
+ log().warn("storage watcher reconcile failed", {
574
+ slug: collection.slug,
575
+ error: errMsg(err)
576
+ });
577
+ });
578
+ });
579
+ watcher.on("error", (err) => {
580
+ log().warn("storage watcher error", {
581
+ slug: collection.slug,
582
+ error: errMsg(err)
583
+ });
584
+ });
585
+ watchers.set(collection.slug, {
586
+ slug: collection.slug,
587
+ dataDir: dir,
588
+ watcher,
589
+ schemaJson: JSON.stringify(collection.schema),
590
+ collection
591
+ });
592
+ log().info("storage watcher started", {
593
+ slug: collection.slug,
594
+ file
595
+ });
596
+ } catch (err) {
597
+ log().warn("storage watcher start failed", {
598
+ slug: collection.slug,
599
+ error: errMsg(err)
600
+ });
601
+ }
602
+ }
603
+ function scheduleStorageReconcile(slug) {
604
+ return runSingleFlight(storageSlots, slug, async () => {
605
+ const collection = await loadCollection(slug, discoveryOpts);
606
+ if (!collection) return;
607
+ await reconcileAllItems(collection, discoveryOpts);
608
+ await sweepStaleActiveEntries(discoveryOpts);
609
+ publishCollectionChange({
610
+ slug,
611
+ op: "upsert"
612
+ });
613
+ });
614
+ }
615
+ /** Test-only: drive one storage-collection reconcile pass directly. */
616
+ function _scheduleStorageReconcileForTesting(slug) {
617
+ return scheduleStorageReconcile(slug);
618
+ }
619
+ async function startWatcherFor(collection) {
620
+ const { slug, schema, dataDir } = collection;
546
621
  try {
547
622
  await mkdir(dataDir, { recursive: true });
548
- await reconcileAllItems(slug, schema, dataDir, discoveryOpts);
623
+ await reconcileAllItems(collection, discoveryOpts);
549
624
  const watcher = watch(dataDir, { persistent: false }, (_eventType, filename) => {
550
625
  onEvent(slug, filename).catch((err) => {
551
626
  log().warn("watcher event failed", {
@@ -565,7 +640,8 @@ async function startWatcherFor(slug, schema, dataDir) {
565
640
  slug,
566
641
  dataDir,
567
642
  watcher,
568
- schemaJson: JSON.stringify(schema)
643
+ schemaJson: JSON.stringify(schema),
644
+ collection
569
645
  });
570
646
  log().info("watcher started", {
571
647
  slug,
@@ -587,12 +663,22 @@ async function startWatcherFor(slug, schema, dataDir) {
587
663
  * and return — the running reconcile re-runs once after it completes.
588
664
  * This collapses fs.watch's rapid-fire bursts (atomic rename surfaces as
589
665
  * 2-3 events) into a single reconcile + one trailing re-run. */
590
- function _scheduleItemReconcileForTesting(slug, schema, dataDir, itemId) {
591
- return scheduleItemReconcile(slug, schema, dataDir, itemId);
592
- }
593
- function scheduleItemReconcile(slug, schema, dataDir, itemId) {
594
- const key = `${slug}\x00${itemId}`;
595
- const existing = itemSlots.get(key);
666
+ function _scheduleItemReconcileForTesting(collection, itemId) {
667
+ return scheduleItemReconcile(collection, itemId);
668
+ }
669
+ function scheduleItemReconcile(collection, itemId) {
670
+ return runSingleFlight(itemSlots, `${collection.slug}\x00${itemId}`, () => reconcileItem(collection, itemId, discoveryOpts), () => publishItemChange(collection, itemId));
671
+ }
672
+ /** The shared single-flight loop behind both schedulers. Re-runs `pass`
673
+ * while events keep arriving — the trailing re-run captures any state
674
+ * change that landed during a prior pass. After each pass we read
675
+ * `pending` and zero it before the next iteration, so an event that
676
+ * fires *during* the last pass's await still triggers one more pass
677
+ * before the slot is freed. `onSettled`, if given, runs once in the
678
+ * `finally` after the slot is freed — the item scheduler uses it to
679
+ * publish exactly one live-refresh event per burst. */
680
+ function runSingleFlight(slots, key, pass, onSettled) {
681
+ const existing = slots.get(key);
596
682
  if (existing) {
597
683
  existing.pending = true;
598
684
  return existing.running;
@@ -606,16 +692,50 @@ function scheduleItemReconcile(slug, schema, dataDir, itemId) {
606
692
  let keepGoing = true;
607
693
  while (keepGoing) {
608
694
  slot.pending = false;
609
- await reconcileItem(slug, schema, dataDir, itemId, discoveryOpts);
695
+ await pass();
610
696
  keepGoing = slot.pending;
611
697
  }
612
698
  } finally {
613
- itemSlots.delete(key);
699
+ slots.delete(key);
700
+ if (onSettled) await onSettled();
614
701
  }
615
702
  })();
616
- itemSlots.set(key, slot);
703
+ slots.set(key, slot);
617
704
  return slot.running;
618
705
  }
706
+ /** Emit the live-refresh event for one record. `op` is derived from
707
+ * whether the file is still there — `fs.watch` reports neither the kind
708
+ * of change nor, reliably, which of `rename`/`change` means what. Only
709
+ * file-backed collections reach here; a `storage` (db) collection has no
710
+ * per-record file and publishes wholesale in `scheduleStorageReconcile`. */
711
+ async function publishItemChange(collection, itemId) {
712
+ const changeOp = await itemFileExists(collection.dataDir, itemId) ? "upsert" : "delete";
713
+ safePublish({
714
+ slug: collection.slug,
715
+ ids: [itemId],
716
+ op: changeOp
717
+ });
718
+ }
719
+ async function itemFileExists(dataDir, itemId) {
720
+ try {
721
+ await access(itemFilePath(dataDir, itemId));
722
+ return true;
723
+ } catch {
724
+ return false;
725
+ }
726
+ }
727
+ /** The publisher is host-supplied, so treat it as untrusted: a throw here
728
+ * runs inside a `finally` and would mask the reconcile's own error. */
729
+ function safePublish(payload) {
730
+ try {
731
+ publishCollectionChange(payload);
732
+ } catch (err) {
733
+ log().warn("collection change publish failed", {
734
+ slug: payload.slug,
735
+ error: errMsg(err)
736
+ });
737
+ }
738
+ }
619
739
  /** Handle a single fs.watch event. Re-loads the collection (schema may
620
740
  * have changed since startup), filters out non-record files, and
621
741
  * forwards to the single-flighted reconciler. `filename === null` (rare,
@@ -624,16 +744,19 @@ async function onEvent(slug, filename) {
624
744
  const collection = await loadCollection(slug, discoveryOpts);
625
745
  if (!collection) return;
626
746
  if (filename === null) {
627
- await reconcileAllItems(slug, collection.schema, collection.dataDir, discoveryOpts);
628
- await sweepStaleActiveEntries(discoveryOpts);
747
+ try {
748
+ await reconcileAllItems(collection, discoveryOpts);
749
+ await sweepStaleActiveEntries(discoveryOpts);
750
+ } finally {
751
+ safePublish({ slug });
752
+ }
629
753
  return;
630
754
  }
631
755
  const name = typeof filename === "string" ? filename : filename.toString("utf-8");
632
756
  if (!name.endsWith(".json") || name.startsWith(".")) return;
633
- const itemId = name.slice(0, -5);
634
- await scheduleItemReconcile(slug, collection.schema, collection.dataDir, itemId);
757
+ await scheduleItemReconcile(collection, name.slice(0, -5));
635
758
  }
636
759
  //#endregion
637
- export { _resetReconcilerLocksForTesting, _scheduleItemReconcileForTesting, _syncWatchersForTesting, _tickTimeTriggersForTesting, clearItemNotification, configureCollectionWatchers, evalNow, itemIsDone, reconcileAllItems, reconcileItem, resetCollectionWatchersConfig, resolveDisplayLabel, startCollectionWatchers, stopCollectionWatchers, sweepStaleActiveEntries };
760
+ export { _resetReconcilerLocksForTesting, _scheduleItemReconcileForTesting, _scheduleStorageReconcileForTesting, _syncWatchersForTesting, _tickTimeTriggersForTesting, clearItemNotification, configureCollectionWatchers, evalNow, itemIsDone, reconcileAllItems, reconcileItem, resetCollectionWatchersConfig, resolveDisplayLabel, startCollectionWatchers, stopCollectionWatchers, sweepStaleActiveEntries };
638
761
 
639
762
  //# sourceMappingURL=index.js.map