@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.
- package/assets/helps/billing-clients-worklog.md +3 -2
- package/assets/helps/collection-skills.md +54 -1
- package/assets/helps/custom-view.md +37 -3
- package/assets/helps/error-recovery.md +30 -0
- package/assets/helps/todo-collection.md +2 -2
- package/dist/collection/core/fieldText.d.ts +11 -0
- package/dist/collection/core/project.d.ts +3 -0
- package/dist/collection/core/schema.d.ts +13 -1
- package/dist/collection/core/schemaZ.d.ts +16 -0
- package/dist/collection/index.cjs +18 -10
- package/dist/collection/index.cjs.map +1 -1
- package/dist/collection/index.d.ts +2 -0
- package/dist/collection/index.js +15 -11
- package/dist/collection/index.js.map +1 -1
- package/dist/collection/registry/server/index.cjs +2 -2
- package/dist/collection/registry/server/index.js +2 -2
- package/dist/collection/server/csvStore.d.ts +8 -1
- package/dist/collection/server/discoveredCollection.d.ts +4 -0
- package/dist/collection/server/discovery.d.ts +1 -0
- package/dist/collection/server/index.cjs +20 -17
- package/dist/collection/server/index.d.ts +1 -0
- package/dist/collection/server/index.js +4 -4
- package/dist/collection/server/queryRunner.d.ts +4 -0
- package/dist/collection/server/spawn.d.ts +2 -1
- package/dist/collection/server/sqliteStore.d.ts +14 -0
- package/dist/collection/server/store.d.ts +43 -8
- package/dist/collection/server/storePage.d.ts +32 -0
- package/dist/collection-watchers/index.cjs +167 -43
- package/dist/collection-watchers/index.cjs.map +1 -1
- package/dist/collection-watchers/index.d.ts +1 -1
- package/dist/collection-watchers/index.js +168 -45
- package/dist/collection-watchers/index.js.map +1 -1
- package/dist/collection-watchers/reconciler.d.ts +13 -11
- package/dist/collection-watchers/watcher.d.ts +4 -3
- package/dist/{discovery-3K_Wtkvn.js → discovery-QUi_7aNG.js} +882 -7
- package/dist/discovery-QUi_7aNG.js.map +1 -0
- package/dist/{discovery-BZ2ms1tE.cjs → discovery-cwtfyDxl.cjs} +994 -4
- package/dist/discovery-cwtfyDxl.cjs.map +1 -0
- package/dist/feeds/index.cjs +2 -2
- package/dist/feeds/index.js +2 -2
- package/dist/feeds/server/index.cjs +26 -14
- package/dist/feeds/server/index.cjs.map +1 -1
- package/dist/feeds/server/index.js +26 -14
- package/dist/feeds/server/index.js.map +1 -1
- package/dist/google/index.cjs +9 -11
- package/dist/google/index.cjs.map +1 -1
- package/dist/google/index.js +9 -11
- package/dist/google/index.js.map +1 -1
- package/dist/{ids-BR4U1dJD.cjs → ids-Dl9Ij63K.cjs} +14 -1
- package/dist/ids-Dl9Ij63K.cjs.map +1 -0
- package/dist/{ids-D4e10971.js → ids-P2T89Gou.js} +9 -2
- package/dist/ids-P2T89Gou.js.map +1 -0
- package/dist/{ingestTypes-BsGOjR2l.js → ingestTypes-C-IQB4nw.js} +2 -2
- package/dist/{ingestTypes-BsGOjR2l.js.map → ingestTypes-C-IQB4nw.js.map} +1 -1
- package/dist/{ingestTypes-CygUXsjf.cjs → ingestTypes-DZpAC3N_.cjs} +2 -2
- package/dist/{ingestTypes-CygUXsjf.cjs.map → ingestTypes-DZpAC3N_.cjs.map} +1 -1
- package/dist/project-BWI5w_BT.cjs +17 -0
- package/dist/project-BWI5w_BT.cjs.map +1 -0
- package/dist/project-bU98ycsy.js +12 -0
- package/dist/project-bU98ycsy.js.map +1 -0
- package/dist/{promptSafety-Boz5qOs-.js → promptSafety-B5KPkaNY.js} +32 -9
- package/dist/promptSafety-B5KPkaNY.js.map +1 -0
- package/dist/{promptSafety-Co4cYQ_Z.cjs → promptSafety-BOc1B3im.cjs} +43 -8
- package/dist/promptSafety-BOc1B3im.cjs.map +1 -0
- package/dist/remote-view/index.cjs +5 -3
- package/dist/remote-view/index.cjs.map +1 -1
- package/dist/remote-view/index.d.ts +3 -1
- package/dist/remote-view/index.js +5 -3
- package/dist/remote-view/index.js.map +1 -1
- package/dist/{server-DLRz-U7F.cjs → server-B7mtx4Cd.cjs} +302 -785
- package/dist/server-B7mtx4Cd.cjs.map +1 -0
- package/dist/{server-DZH7OjOD.js → server-CNLZZONa.js} +292 -696
- package/dist/server-CNLZZONa.js.map +1 -0
- package/package.json +1 -1
- package/dist/discovery-3K_Wtkvn.js.map +0 -1
- package/dist/discovery-BZ2ms1tE.cjs.map +0 -1
- package/dist/ids-BR4U1dJD.cjs.map +0 -1
- package/dist/ids-D4e10971.js.map +0 -1
- package/dist/promptSafety-Boz5qOs-.js.map +0 -1
- package/dist/promptSafety-Co4cYQ_Z.cjs.map +0 -1
- package/dist/server-DLRz-U7F.cjs.map +0 -1
- package/dist/server-DZH7OjOD.js.map +0 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_rolldown_runtime = require("../rolldown-runtime-D6vf50IK.cjs");
|
|
3
|
-
const require_promptSafety = require("../promptSafety-
|
|
3
|
+
const require_promptSafety = require("../promptSafety-BOc1B3im.cjs");
|
|
4
4
|
require("../collection/index.cjs");
|
|
5
|
-
const require_discovery = require("../discovery-
|
|
6
|
-
const require_server = require("../server-
|
|
5
|
+
const require_discovery = require("../discovery-cwtfyDxl.cjs");
|
|
6
|
+
const require_server = require("../server-B7mtx4Cd.cjs");
|
|
7
7
|
const require_notifier = require("../notifier-bS8IEeLA.cjs");
|
|
8
8
|
let node_path = require("node:path");
|
|
9
9
|
node_path = require_rolldown_runtime.__toESM(node_path, 1);
|
|
@@ -76,9 +76,7 @@ function parseCompletionLegacyId(legacyId) {
|
|
|
76
76
|
function resolveDisplayLabel(schema, item, itemId) {
|
|
77
77
|
const { displayField } = schema;
|
|
78
78
|
if (!displayField) return itemId;
|
|
79
|
-
const
|
|
80
|
-
if (raw === void 0 || raw === null) return itemId;
|
|
81
|
-
const label = String(raw).trim();
|
|
79
|
+
const label = require_promptSafety.fieldText(item[displayField]).trim();
|
|
82
80
|
return label.length > 0 ? label : itemId;
|
|
83
81
|
}
|
|
84
82
|
/** Every active bell entry whose key matches this (slug, itemId).
|
|
@@ -185,24 +183,26 @@ async function clearItemNotification(slug, itemId) {
|
|
|
185
183
|
});
|
|
186
184
|
}
|
|
187
185
|
}
|
|
188
|
-
/** Reconcile one item to the desired bell state. Re-reads the record
|
|
189
|
-
*
|
|
190
|
-
*
|
|
186
|
+
/** Reconcile one item to the desired bell state. Re-reads the record
|
|
187
|
+
* through the collection's STORE (file, sqlite, …) so the decision is
|
|
188
|
+
* grounded in current truth, not in the event payload. Safe to call when
|
|
189
|
+
* the record is missing (delete path).
|
|
191
190
|
*
|
|
192
|
-
* `ioOpts` flows into
|
|
191
|
+
* `ioOpts` flows into the store's workspace-containment checks —
|
|
193
192
|
* production callers (the watcher) pass nothing; tests pass
|
|
194
193
|
* `{ workspaceRoot: <tmpdir> }` so the check accepts a fixture dataDir. */
|
|
195
|
-
async function reconcileItem(
|
|
194
|
+
async function reconcileItem(collection, itemId, ioOpts = {}, now = evalNow()) {
|
|
195
|
+
const { slug, schema } = collection;
|
|
196
196
|
if (!schema.completionField) {
|
|
197
197
|
await clearItemNotification(slug, itemId);
|
|
198
198
|
return;
|
|
199
199
|
}
|
|
200
|
-
const item = await require_discovery.
|
|
200
|
+
const item = await require_discovery.storeFor(collection, ioOpts).read(itemId);
|
|
201
201
|
if (item === null) {
|
|
202
202
|
await clearItemNotification(slug, itemId);
|
|
203
203
|
return;
|
|
204
204
|
}
|
|
205
|
-
await require_server.maybeSpawnSuccessor(
|
|
205
|
+
await require_server.maybeSpawnSuccessor(collection, item, itemId, ioOpts);
|
|
206
206
|
if (require_promptSafety.itemIsDone(schema, item)) {
|
|
207
207
|
await clearItemNotification(slug, itemId);
|
|
208
208
|
return;
|
|
@@ -226,19 +226,20 @@ async function reconcileItem(slug, schema, dataDir, itemId, ioOpts = {}, now = e
|
|
|
226
226
|
}
|
|
227
227
|
await ensureItemNotification(slug, schema, itemId, resolveDisplayLabel(schema, item, itemId), notifyPriorityForItem(schema, item));
|
|
228
228
|
}
|
|
229
|
-
/** Boot-time reconcile: walk every record
|
|
230
|
-
* reconcile it. Catches up changes that happened while the
|
|
231
|
-
* down. Deleted items are covered by
|
|
232
|
-
* function (it only sees
|
|
233
|
-
|
|
229
|
+
/** Boot-time reconcile: walk every record of the collection once (through
|
|
230
|
+
* its store) and reconcile it. Catches up changes that happened while the
|
|
231
|
+
* server was down. Deleted items are covered by
|
|
232
|
+
* `sweepStaleActiveEntries`, not this function (it only sees records
|
|
233
|
+
* that exist). */
|
|
234
|
+
async function reconcileAllItems(collection, ioOpts = {}, now = evalNow()) {
|
|
235
|
+
const { slug, schema } = collection;
|
|
234
236
|
if (!schema.completionField) return;
|
|
235
237
|
let items;
|
|
236
238
|
try {
|
|
237
|
-
items = await require_discovery.
|
|
239
|
+
items = await require_discovery.storeFor(collection, ioOpts).list();
|
|
238
240
|
} catch (err) {
|
|
239
241
|
log().warn("reconcile list failed", {
|
|
240
242
|
slug,
|
|
241
|
-
dataDir,
|
|
242
243
|
error: errMsg(err)
|
|
243
244
|
});
|
|
244
245
|
return;
|
|
@@ -247,7 +248,7 @@ async function reconcileAllItems(slug, schema, dataDir, ioOpts = {}, now = evalN
|
|
|
247
248
|
for (const item of items) {
|
|
248
249
|
const raw = item[primaryKey];
|
|
249
250
|
if (typeof raw !== "string" || raw.length === 0) continue;
|
|
250
|
-
await reconcileItem(
|
|
251
|
+
await reconcileItem(collection, raw, ioOpts, now);
|
|
251
252
|
}
|
|
252
253
|
}
|
|
253
254
|
/** Boot-time sweep over the active bell: drop any entries whose underlying
|
|
@@ -275,7 +276,7 @@ async function sweepStaleActiveEntries(opts = {}) {
|
|
|
275
276
|
await require_notifier.clear(entry.id);
|
|
276
277
|
continue;
|
|
277
278
|
}
|
|
278
|
-
const item = await require_discovery.
|
|
279
|
+
const item = await require_discovery.storeFor(collection, opts).read(itemId);
|
|
279
280
|
if (item === null || require_promptSafety.itemIsDone(collection.schema, item) || !require_promptSafety.whenMatches(collection.schema.notifyWhen, item)) await require_notifier.clear(entry.id);
|
|
280
281
|
} catch (err) {
|
|
281
282
|
log().warn("sweep entry failed", {
|
|
@@ -307,6 +308,10 @@ var started = false;
|
|
|
307
308
|
* without threading through every signature. */
|
|
308
309
|
var discoveryOpts = {};
|
|
309
310
|
var itemSlots = /* @__PURE__ */ new Map();
|
|
311
|
+
/** Per-slug single-flight for a storage (db-file) collection's full
|
|
312
|
+
* reconcile pass — a burst of db writes collapses to one pass + one
|
|
313
|
+
* trailing re-run, mirroring the per-item slots of the file watcher. */
|
|
314
|
+
var storageSlots = /* @__PURE__ */ new Map();
|
|
310
315
|
/** Trailing debounce per dataSource collection: an atomic file replace
|
|
311
316
|
* (Excel save, editor rename) surfaces as 2-3 fs events — collapse them
|
|
312
317
|
* into one change publish so live views refetch once. */
|
|
@@ -362,6 +367,7 @@ async function stopCollectionWatchers() {
|
|
|
362
367
|
} catch {}
|
|
363
368
|
watchers.clear();
|
|
364
369
|
itemSlots.clear();
|
|
370
|
+
storageSlots.clear();
|
|
365
371
|
for (const timer of dataSourceTimers.values()) clearTimeout(timer);
|
|
366
372
|
dataSourceTimers.clear();
|
|
367
373
|
discoveryOpts = {};
|
|
@@ -395,7 +401,7 @@ async function tickTimeTriggers(now = evalNow()) {
|
|
|
395
401
|
}
|
|
396
402
|
if (schema.dataSource !== void 0) continue;
|
|
397
403
|
if (!schema.triggerField && !schema.spawn) continue;
|
|
398
|
-
await reconcileAllItems(entry.
|
|
404
|
+
await reconcileAllItems(entry.collection, discoveryOpts, now);
|
|
399
405
|
}
|
|
400
406
|
}
|
|
401
407
|
/** Reconcile the watcher set against the currently-discovered
|
|
@@ -441,7 +447,7 @@ function storagePathChanged(previousJson, next) {
|
|
|
441
447
|
} catch {
|
|
442
448
|
return true;
|
|
443
449
|
}
|
|
444
|
-
return previous.dataSource?.path !== next.dataSource?.path || previous.dataPath !== next.dataPath;
|
|
450
|
+
return previous.dataSource?.path !== next.dataSource?.path || previous.dataPath !== next.dataPath || previous.storage?.path !== next.storage?.path;
|
|
445
451
|
}
|
|
446
452
|
/** Re-reconcile already-watched collections whose schema changed since
|
|
447
453
|
* the last tick. New collections fall through to `startNewWatchers`. */
|
|
@@ -466,6 +472,7 @@ async function reconcileChangedSchemas(collections) {
|
|
|
466
472
|
continue;
|
|
467
473
|
}
|
|
468
474
|
existing.schemaJson = nextJson;
|
|
475
|
+
existing.collection = collection;
|
|
469
476
|
if (collection.schema.dataSource !== void 0) {
|
|
470
477
|
log().info("dataSource watcher schema changed, publishing", { slug: collection.slug });
|
|
471
478
|
require_discovery.publishCollectionChange({
|
|
@@ -476,7 +483,7 @@ async function reconcileChangedSchemas(collections) {
|
|
|
476
483
|
continue;
|
|
477
484
|
}
|
|
478
485
|
log().info("watcher schema changed, re-reconciling", { slug: collection.slug });
|
|
479
|
-
await reconcileAllItems(collection
|
|
486
|
+
await reconcileAllItems(collection, discoveryOpts);
|
|
480
487
|
mutated = true;
|
|
481
488
|
}
|
|
482
489
|
return mutated;
|
|
@@ -486,7 +493,8 @@ async function startNewWatchers(collections) {
|
|
|
486
493
|
for (const collection of collections) {
|
|
487
494
|
if (watchers.has(collection.slug)) continue;
|
|
488
495
|
if (collection.schema.dataSource !== void 0) await startDataSourceWatcher(collection);
|
|
489
|
-
else
|
|
496
|
+
else if (collection.schema.storage !== void 0) await startStorageWatcher(collection);
|
|
497
|
+
else await startWatcherFor(collection);
|
|
490
498
|
mutated = true;
|
|
491
499
|
}
|
|
492
500
|
return mutated;
|
|
@@ -532,7 +540,8 @@ async function startDataSourceWatcher(collection) {
|
|
|
532
540
|
slug: collection.slug,
|
|
533
541
|
dataDir: dir,
|
|
534
542
|
watcher,
|
|
535
|
-
schemaJson: JSON.stringify(collection.schema)
|
|
543
|
+
schemaJson: JSON.stringify(collection.schema),
|
|
544
|
+
collection
|
|
536
545
|
});
|
|
537
546
|
log().info("dataSource watcher started", {
|
|
538
547
|
slug: collection.slug,
|
|
@@ -545,10 +554,76 @@ async function startDataSourceWatcher(collection) {
|
|
|
545
554
|
});
|
|
546
555
|
}
|
|
547
556
|
}
|
|
548
|
-
|
|
557
|
+
/** Watch a `storage` collection's database file. One db file holds every
|
|
558
|
+
* record, so an event can't name WHICH record changed — each (debounced)
|
|
559
|
+
* event runs a full `reconcileAllItems` pass (bells / spawn) plus a
|
|
560
|
+
* change publish so views refetch after EXTERNAL edits too (host writes
|
|
561
|
+
* already publish their own change events; the extra ping is debounced
|
|
562
|
+
* and idempotent). Mounts on the parent dir, same as the dataSource
|
|
563
|
+
* watcher, so an atomic replace can't strand the watch. */
|
|
564
|
+
async function startStorageWatcher(collection) {
|
|
565
|
+
const file = collection.storageFile;
|
|
566
|
+
if (file === void 0) return;
|
|
567
|
+
const dir = node_path.default.dirname(file);
|
|
568
|
+
const base = node_path.default.basename(file);
|
|
569
|
+
try {
|
|
570
|
+
await (0, node_fs_promises.mkdir)(dir, { recursive: true });
|
|
571
|
+
await reconcileAllItems(collection, discoveryOpts);
|
|
572
|
+
const watcher = (0, node_fs.watch)(dir, { persistent: false }, (_eventType, rawFilename) => {
|
|
573
|
+
const filename = rawFilename === null ? null : String(rawFilename);
|
|
574
|
+
if (filename !== null && !filename.startsWith(base)) return;
|
|
575
|
+
scheduleStorageReconcile(collection.slug).catch((err) => {
|
|
576
|
+
log().warn("storage watcher reconcile failed", {
|
|
577
|
+
slug: collection.slug,
|
|
578
|
+
error: errMsg(err)
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
watcher.on("error", (err) => {
|
|
583
|
+
log().warn("storage watcher error", {
|
|
584
|
+
slug: collection.slug,
|
|
585
|
+
error: errMsg(err)
|
|
586
|
+
});
|
|
587
|
+
});
|
|
588
|
+
watchers.set(collection.slug, {
|
|
589
|
+
slug: collection.slug,
|
|
590
|
+
dataDir: dir,
|
|
591
|
+
watcher,
|
|
592
|
+
schemaJson: JSON.stringify(collection.schema),
|
|
593
|
+
collection
|
|
594
|
+
});
|
|
595
|
+
log().info("storage watcher started", {
|
|
596
|
+
slug: collection.slug,
|
|
597
|
+
file
|
|
598
|
+
});
|
|
599
|
+
} catch (err) {
|
|
600
|
+
log().warn("storage watcher start failed", {
|
|
601
|
+
slug: collection.slug,
|
|
602
|
+
error: errMsg(err)
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
function scheduleStorageReconcile(slug) {
|
|
607
|
+
return runSingleFlight(storageSlots, slug, async () => {
|
|
608
|
+
const collection = await require_discovery.loadCollection(slug, discoveryOpts);
|
|
609
|
+
if (!collection) return;
|
|
610
|
+
await reconcileAllItems(collection, discoveryOpts);
|
|
611
|
+
await sweepStaleActiveEntries(discoveryOpts);
|
|
612
|
+
require_discovery.publishCollectionChange({
|
|
613
|
+
slug,
|
|
614
|
+
op: "upsert"
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
/** Test-only: drive one storage-collection reconcile pass directly. */
|
|
619
|
+
function _scheduleStorageReconcileForTesting(slug) {
|
|
620
|
+
return scheduleStorageReconcile(slug);
|
|
621
|
+
}
|
|
622
|
+
async function startWatcherFor(collection) {
|
|
623
|
+
const { slug, schema, dataDir } = collection;
|
|
549
624
|
try {
|
|
550
625
|
await (0, node_fs_promises.mkdir)(dataDir, { recursive: true });
|
|
551
|
-
await reconcileAllItems(
|
|
626
|
+
await reconcileAllItems(collection, discoveryOpts);
|
|
552
627
|
const watcher = (0, node_fs.watch)(dataDir, { persistent: false }, (_eventType, filename) => {
|
|
553
628
|
onEvent(slug, filename).catch((err) => {
|
|
554
629
|
log().warn("watcher event failed", {
|
|
@@ -568,7 +643,8 @@ async function startWatcherFor(slug, schema, dataDir) {
|
|
|
568
643
|
slug,
|
|
569
644
|
dataDir,
|
|
570
645
|
watcher,
|
|
571
|
-
schemaJson: JSON.stringify(schema)
|
|
646
|
+
schemaJson: JSON.stringify(schema),
|
|
647
|
+
collection
|
|
572
648
|
});
|
|
573
649
|
log().info("watcher started", {
|
|
574
650
|
slug,
|
|
@@ -590,12 +666,22 @@ async function startWatcherFor(slug, schema, dataDir) {
|
|
|
590
666
|
* and return — the running reconcile re-runs once after it completes.
|
|
591
667
|
* This collapses fs.watch's rapid-fire bursts (atomic rename surfaces as
|
|
592
668
|
* 2-3 events) into a single reconcile + one trailing re-run. */
|
|
593
|
-
function _scheduleItemReconcileForTesting(
|
|
594
|
-
return scheduleItemReconcile(
|
|
595
|
-
}
|
|
596
|
-
function scheduleItemReconcile(
|
|
597
|
-
|
|
598
|
-
|
|
669
|
+
function _scheduleItemReconcileForTesting(collection, itemId) {
|
|
670
|
+
return scheduleItemReconcile(collection, itemId);
|
|
671
|
+
}
|
|
672
|
+
function scheduleItemReconcile(collection, itemId) {
|
|
673
|
+
return runSingleFlight(itemSlots, `${collection.slug}\x00${itemId}`, () => reconcileItem(collection, itemId, discoveryOpts), () => publishItemChange(collection, itemId));
|
|
674
|
+
}
|
|
675
|
+
/** The shared single-flight loop behind both schedulers. Re-runs `pass`
|
|
676
|
+
* while events keep arriving — the trailing re-run captures any state
|
|
677
|
+
* change that landed during a prior pass. After each pass we read
|
|
678
|
+
* `pending` and zero it before the next iteration, so an event that
|
|
679
|
+
* fires *during* the last pass's await still triggers one more pass
|
|
680
|
+
* before the slot is freed. `onSettled`, if given, runs once in the
|
|
681
|
+
* `finally` after the slot is freed — the item scheduler uses it to
|
|
682
|
+
* publish exactly one live-refresh event per burst. */
|
|
683
|
+
function runSingleFlight(slots, key, pass, onSettled) {
|
|
684
|
+
const existing = slots.get(key);
|
|
599
685
|
if (existing) {
|
|
600
686
|
existing.pending = true;
|
|
601
687
|
return existing.running;
|
|
@@ -609,16 +695,50 @@ function scheduleItemReconcile(slug, schema, dataDir, itemId) {
|
|
|
609
695
|
let keepGoing = true;
|
|
610
696
|
while (keepGoing) {
|
|
611
697
|
slot.pending = false;
|
|
612
|
-
await
|
|
698
|
+
await pass();
|
|
613
699
|
keepGoing = slot.pending;
|
|
614
700
|
}
|
|
615
701
|
} finally {
|
|
616
|
-
|
|
702
|
+
slots.delete(key);
|
|
703
|
+
if (onSettled) await onSettled();
|
|
617
704
|
}
|
|
618
705
|
})();
|
|
619
|
-
|
|
706
|
+
slots.set(key, slot);
|
|
620
707
|
return slot.running;
|
|
621
708
|
}
|
|
709
|
+
/** Emit the live-refresh event for one record. `op` is derived from
|
|
710
|
+
* whether the file is still there — `fs.watch` reports neither the kind
|
|
711
|
+
* of change nor, reliably, which of `rename`/`change` means what. Only
|
|
712
|
+
* file-backed collections reach here; a `storage` (db) collection has no
|
|
713
|
+
* per-record file and publishes wholesale in `scheduleStorageReconcile`. */
|
|
714
|
+
async function publishItemChange(collection, itemId) {
|
|
715
|
+
const changeOp = await itemFileExists(collection.dataDir, itemId) ? "upsert" : "delete";
|
|
716
|
+
safePublish({
|
|
717
|
+
slug: collection.slug,
|
|
718
|
+
ids: [itemId],
|
|
719
|
+
op: changeOp
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
async function itemFileExists(dataDir, itemId) {
|
|
723
|
+
try {
|
|
724
|
+
await (0, node_fs_promises.access)(require_discovery.itemFilePath(dataDir, itemId));
|
|
725
|
+
return true;
|
|
726
|
+
} catch {
|
|
727
|
+
return false;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
/** The publisher is host-supplied, so treat it as untrusted: a throw here
|
|
731
|
+
* runs inside a `finally` and would mask the reconcile's own error. */
|
|
732
|
+
function safePublish(payload) {
|
|
733
|
+
try {
|
|
734
|
+
require_discovery.publishCollectionChange(payload);
|
|
735
|
+
} catch (err) {
|
|
736
|
+
log().warn("collection change publish failed", {
|
|
737
|
+
slug: payload.slug,
|
|
738
|
+
error: errMsg(err)
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
}
|
|
622
742
|
/** Handle a single fs.watch event. Re-loads the collection (schema may
|
|
623
743
|
* have changed since startup), filters out non-record files, and
|
|
624
744
|
* forwards to the single-flighted reconciler. `filename === null` (rare,
|
|
@@ -627,18 +747,22 @@ async function onEvent(slug, filename) {
|
|
|
627
747
|
const collection = await require_discovery.loadCollection(slug, discoveryOpts);
|
|
628
748
|
if (!collection) return;
|
|
629
749
|
if (filename === null) {
|
|
630
|
-
|
|
631
|
-
|
|
750
|
+
try {
|
|
751
|
+
await reconcileAllItems(collection, discoveryOpts);
|
|
752
|
+
await sweepStaleActiveEntries(discoveryOpts);
|
|
753
|
+
} finally {
|
|
754
|
+
safePublish({ slug });
|
|
755
|
+
}
|
|
632
756
|
return;
|
|
633
757
|
}
|
|
634
758
|
const name = typeof filename === "string" ? filename : filename.toString("utf-8");
|
|
635
759
|
if (!name.endsWith(".json") || name.startsWith(".")) return;
|
|
636
|
-
|
|
637
|
-
await scheduleItemReconcile(slug, collection.schema, collection.dataDir, itemId);
|
|
760
|
+
await scheduleItemReconcile(collection, name.slice(0, -5));
|
|
638
761
|
}
|
|
639
762
|
//#endregion
|
|
640
763
|
exports._resetReconcilerLocksForTesting = _resetReconcilerLocksForTesting;
|
|
641
764
|
exports._scheduleItemReconcileForTesting = _scheduleItemReconcileForTesting;
|
|
765
|
+
exports._scheduleStorageReconcileForTesting = _scheduleStorageReconcileForTesting;
|
|
642
766
|
exports._syncWatchersForTesting = _syncWatchersForTesting;
|
|
643
767
|
exports._tickTimeTriggersForTesting = _tickTimeTriggersForTesting;
|
|
644
768
|
exports.clearItemNotification = clearItemNotification;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../../src/collection-watchers/config.ts","../../src/collection-watchers/clock.ts","../../src/collection-watchers/reconciler.ts","../../src/collection-watchers/watcher.ts"],"sourcesContent":["// Host-injected configuration for the collection-completion watchers.\n// The reconciler logic + watcher plumbing are host-agnostic; the\n// notification TAXONOMY (which plugin namespace, how a record priority\n// maps to a bell severity) and the in-app ROUTING (the deep-link a bell\n// row navigates to) are host-specific, so the host supplies them via an\n// adapter. MulmoClaude wires its legacy notification machinery; a future\n// MulmoTerminal wires its own routes + pluginData shape.\n\nimport type { NotifierSeverity } from \"../notifier\";\n\n/** Two-level urgency a pending record can carry, derived from the\n * schema's `notifyWhen` spec. The host maps this onto its own severity\n * scale via `priorityToSeverity`. */\nexport type CompletionPriority = \"normal\" | \"high\";\n\nexport interface CollectionWatcherLogger {\n info: (message: string, data?: Record<string, unknown>) => void;\n warn: (message: string, data?: Record<string, unknown>) => void;\n}\n\n/** The host-specific notification surface the reconciler binds to. The\n * reconciler owns the internal `legacyId` key (it encodes slug+itemId\n * and round-trips it through `pluginData`); the adapter only wraps /\n * unwraps it into whatever shape the host's bell expects. */\nexport interface CollectionNotificationAdapter {\n /** Plugin namespace these bell entries publish under (MulmoClaude: \"todo\"). */\n pluginPkg: string;\n /** Map a record's completion priority onto the host's bell severity. */\n priorityToSeverity: (priority: CompletionPriority) => NotifierSeverity;\n /** Build the in-app deep-link the bell row routes to on click. */\n buildNavigateTarget: (slug: string, itemId: string) => string;\n /** Wrap the reconciler's internal key + priority into the host's\n * `pluginData` shape. Stored verbatim on the entry; recovered via\n * `readEntry`. */\n buildPluginData: (input: { legacyId: string; slug: string; itemId: string; priority: CompletionPriority; navigateTarget: string }) => unknown;\n /** Recognise a bell entry produced by this reconciler and recover its\n * internal key + stored priority. Returns null for entries that didn't\n * originate here, so `listAll()` scans skip foreign entries. */\n readEntry: (pluginData: unknown) => { legacyId: string; priority: CompletionPriority } | null;\n}\n\nconst NOOP_LOG: CollectionWatcherLogger = { info: () => {}, warn: () => {} };\n\nlet adapter: CollectionNotificationAdapter | null = null;\nlet activeLogger: CollectionWatcherLogger = NOOP_LOG;\n\n/** Wire the host adapter + logger. Call once at startup, before\n * `startCollectionWatchers` or any direct reconcile call. */\nexport function configureCollectionWatchers(config: { adapter: CollectionNotificationAdapter; log?: CollectionWatcherLogger }): void {\n ({ adapter } = config);\n activeLogger = config.log ?? NOOP_LOG;\n}\n\nexport function requireAdapter(): CollectionNotificationAdapter {\n if (!adapter) throw new Error(\"collection-watchers: configureCollectionWatchers() not called\");\n return adapter;\n}\n\nexport function log(): CollectionWatcherLogger {\n return activeLogger;\n}\n\n/** Test-only: clear the host wiring. */\nexport function resetCollectionWatchersConfig(): void {\n adapter = null;\n activeLogger = NOOP_LOG;\n}\n\nexport function errMsg(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n","// Evaluation-only injectable clock. When MULMOCLAUDE_FAKE_NOW is set to\n// a parseable date/datetime string, every reconcile pass derives \"now\"\n// from it instead of the wall clock — letting evaluation runs and tests\n// advance time deterministically (a `triggerField` coming due, a `spawn`\n// successor's own trigger firing later) without waiting for real days to\n// pass. Unset or unparseable (i.e. normal operation) → real wall clock.\n// Read per call, so a long-lived server picks up changes without restart.\nexport function evalNow(): Date {\n const raw = process.env.MULMOCLAUDE_FAKE_NOW;\n if (!raw) return new Date();\n const parsed = new Date(raw);\n return Number.isNaN(parsed.getTime()) ? new Date() : parsed;\n}\n","// Bell-notification reconciler for collections whose schema declares\n// `completionField`. Driven by `watcher.ts`, which calls into the\n// functions below on file-system events and on boot.\n//\n// The model is **convergent**: a watcher event re-reads the record from\n// disk and the reconciler enforces the invariant\n//\n// bell entry exists for (slug, itemId) ↔\n// schema has completionField ∧\n// file exists ∧\n// `String(item[completionField])` ∉ completionDoneValues\n// (∧ trigger due ∧ notifyWhen matches, when declared)\n//\n// Each reconcile is idempotent (`ensure*` / `clear*` no-op when state\n// already matches). This is why event-type quirks of `fs.watch`\n// (`rename` vs `change`, missed events, atomic-write coalescence) don't\n// matter — every event re-derives the desired state from the file.\n//\n// Lookup uses a deterministic internal `legacyId` derived from\n// `<slug>:<itemId>`, stashed on each entry's `pluginData` via the host\n// adapter, so the clear path and the dedup check both find the entry\n// without a side state file.\n\nimport { clear as notifierClear, listAll, publish as notifierPublish, updateForPlugin as notifierUpdate, type NotifierEntry } from \"../notifier\";\nimport { itemIsDone, whenMatches, type CollectionItem, type CollectionSchema } from \"../collection\";\nimport { type DiscoveryOptions, listItems, readItem, type IoOptions, isTriggerDue, maybeSpawnSuccessor, loadCollection } from \"../collection/server\";\nimport { type CompletionPriority, errMsg, log, requireAdapter } from \"./config.js\";\nimport { evalNow } from \"./clock.js\";\n\n/** The internal-id prefix every collection-completion bell entry carries.\n * Used both to build new keys and to filter sweep candidates from the\n * active bell. */\nconst LEGACY_ID_PREFIX = \"collection-completion:\";\n\n/** Stable key encoding slug + item, round-tripped through the entry's\n * `pluginData` so we can find it later without a side state file. Slug +\n * itemId are upstream-validated via `safeSlugName`, which forbids the\n * colon separator, so the two-segment parse below is unambiguous. */\nfunction completionLegacyId(slug: string, itemId: string): string {\n return `${LEGACY_ID_PREFIX}${slug}:${itemId}`;\n}\n\n/** Decode a key back into its (slug, itemId) pair, or null if the string\n * didn't originate from this module. Used by the sweep step. */\nfunction parseCompletionLegacyId(legacyId: string): { slug: string; itemId: string } | null {\n if (!legacyId.startsWith(LEGACY_ID_PREFIX)) return null;\n const body = legacyId.slice(LEGACY_ID_PREFIX.length);\n const colon = body.indexOf(\":\");\n if (colon < 0) return null;\n return { slug: body.slice(0, colon), itemId: body.slice(colon + 1) };\n}\n\n/** The human-readable label shown in a completion notification's title.\n * Uses the schema's `displayField` value when declared and non-empty;\n * otherwise falls back to the record's primaryKey (`itemId`). */\nexport function resolveDisplayLabel(schema: CollectionSchema, item: CollectionItem, itemId: string): string {\n const { displayField } = schema;\n if (!displayField) return itemId;\n const raw = item[displayField];\n if (raw === undefined || raw === null) return itemId;\n const label = String(raw).trim();\n return label.length > 0 ? label : itemId;\n}\n\n// The done predicate moved to `../collection/core/completion` (flag-aware,\n// browser-safe, shared with spawn and view filters). Re-exported so the\n// `collection-watchers` barrel keeps its public surface (MulmoTerminal).\nexport { itemIsDone };\n\n/** Every active bell entry whose key matches this (slug, itemId).\n * Returns multiple when defensive cleanup is needed. Scans `listAll()`\n * — cheap because the active set is bounded. */\nasync function findActiveEntries(slug: string, itemId: string): Promise<NotifierEntry[]> {\n const adapter = requireAdapter();\n const legacyId = completionLegacyId(slug, itemId);\n const entries = await listAll();\n return entries.filter((entry) => adapter.readEntry(entry.pluginData)?.legacyId === legacyId);\n}\n\nasync function findActiveEntryIds(slug: string, itemId: string): Promise<string[]> {\n return (await findActiveEntries(slug, itemId)).map((entry) => entry.id);\n}\n\n/** Per-key in-flight lock. Serializes concurrent `ensureItemNotification`\n * calls for the same (slug, itemId) so the `findActiveEntries → publish`\n * check stays atomic across callers — not just across watcher events.\n * `listAll` bypasses the engine's write queue, so without this lock two\n * reconcile paths (a watcher event + a `reconcileAllItems` pass that a\n * readdir-triggered event raced) could both miss each other's in-flight\n * publish and produce duplicate entries. */\ninterface EnsureLock {\n promise: Promise<void>;\n}\nconst ensureLocks = new Map<string, EnsureLock>();\n\n/** Bell priority for a record: the FIRST flagged value in `notifyWhen.in`\n * (most urgent) reads `high`, every other flagged value `normal`.\n * Collections with no `notifyWhen` (notify for every open record) stay\n * `normal`. */\nfunction notifyPriorityForItem(schema: CollectionSchema, item: CollectionItem): CompletionPriority {\n const spec = schema.notifyWhen;\n if (!spec) return \"normal\";\n const value = item[spec.field] === undefined || item[spec.field] === null ? \"\" : String(item[spec.field]);\n return spec.in.indexOf(value) === 0 ? \"high\" : \"normal\";\n}\n\nasync function ensureItemNotification(\n slug: string,\n schema: CollectionSchema,\n itemId: string,\n displayLabel: string,\n priority: CompletionPriority,\n): Promise<void> {\n const legacyId = completionLegacyId(slug, itemId);\n // Drain any in-flight publish for this key BEFORE our check + set. The\n // drain + claim runs synchronously between `ensureLocks.get` and\n // `ensureLocks.set`, so two callers can't both observe an empty slot.\n\n while (true) {\n const inflight = ensureLocks.get(legacyId);\n if (!inflight) break;\n await inflight.promise;\n }\n const lock: EnsureLock = { promise: doEnsureItemNotification(slug, schema, itemId, legacyId, displayLabel, priority) };\n ensureLocks.set(legacyId, lock);\n try {\n await lock.promise;\n } finally {\n // Only clear the slot if it still points at OUR lock — a\n // sufficiently-delayed cleanup must not stomp a later claim.\n if (ensureLocks.get(legacyId) === lock) {\n ensureLocks.delete(legacyId);\n }\n }\n}\n\n/** Converge any already-present bell entries to `priority`, updating in\n * place (preserving id / position / createdAt) so a record whose flagged\n * value changed while it stayed pending re-colours the bell without a\n * clear+republish flicker. No-op when the stored priority already matches. */\nasync function reconcileEntrySeverity(slug: string, itemId: string, entries: NotifierEntry[], priority: CompletionPriority): Promise<void> {\n const adapter = requireAdapter();\n for (const entry of entries) {\n const parsed = adapter.readEntry(entry.pluginData);\n if (!parsed || parsed.priority === priority) continue;\n await notifierUpdate(adapter.pluginPkg, entry.id, {\n severity: adapter.priorityToSeverity(priority),\n pluginData: adapter.buildPluginData({\n legacyId: parsed.legacyId,\n slug,\n itemId,\n priority,\n navigateTarget: adapter.buildNavigateTarget(slug, itemId),\n }),\n });\n }\n}\n\nasync function doEnsureItemNotification(\n slug: string,\n schema: CollectionSchema,\n itemId: string,\n legacyId: string,\n displayLabel: string,\n priority: CompletionPriority,\n): Promise<void> {\n const adapter = requireAdapter();\n try {\n const existing = await findActiveEntries(slug, itemId);\n if (existing.length > 0) {\n await reconcileEntrySeverity(slug, itemId, existing, priority);\n return;\n }\n const navigateTarget = adapter.buildNavigateTarget(slug, itemId);\n // `lifecycle: \"action\"` — these are state-of-the-world entries\n // mirroring an outstanding obligation (the item is pending), not\n // transient pings. Validation requires a non-info severity and a\n // non-empty `navigateTarget` (the slug + itemId deep-link).\n await notifierPublish({\n pluginPkg: adapter.pluginPkg,\n severity: adapter.priorityToSeverity(priority),\n lifecycle: \"action\",\n title: `${schema.title}: ${displayLabel}`,\n navigateTarget,\n pluginData: adapter.buildPluginData({ legacyId, slug, itemId, priority, navigateTarget }),\n });\n } catch (err) {\n log().warn(\"notify ensure failed\", { slug, itemId, error: errMsg(err) });\n }\n}\n\n/** Idempotently clear EVERY bell entry that matches this (slug, itemId).\n * Silent no-op when nothing matches. The \"every\" is defensive: if a\n * duplicate ever slips through, this drains the lot. */\nexport async function clearItemNotification(slug: string, itemId: string): Promise<void> {\n try {\n const ids = await findActiveEntryIds(slug, itemId);\n for (const entryId of ids) {\n await notifierClear(entryId);\n }\n } catch (err) {\n log().warn(\"notify clear failed\", { slug, itemId, error: errMsg(err) });\n }\n}\n\n/** Reconcile one item to the desired bell state. Re-reads the record from\n * disk so the decision is grounded in current truth, not in the event\n * payload. Safe to call when the file is missing (delete path).\n *\n * `ioOpts` flows into `readItem`'s workspace-containment check —\n * production callers (the watcher) pass nothing; tests pass\n * `{ workspaceRoot: <tmpdir> }` so the check accepts a fixture dataDir. */\nexport async function reconcileItem(\n slug: string,\n schema: CollectionSchema,\n dataDir: string,\n itemId: string,\n ioOpts: IoOptions = {},\n now: Date = evalNow(),\n): Promise<void> {\n if (!schema.completionField) {\n // Schema doesn't track completion — drop any stale entry.\n await clearItemNotification(slug, itemId);\n return;\n }\n const item = await readItem(dataDir, itemId, ioOpts);\n if (item === null) {\n await clearItemNotification(slug, itemId);\n return;\n }\n // Recurrence: predicate-gated + create-if-absent, idempotent and\n // independent of this item's own bell state. Runs before the done-clear\n // below so marking an item done still spawns its successor.\n await maybeSpawnSuccessor(slug, schema, dataDir, item, itemId, ioOpts);\n if (itemIsDone(schema, item)) {\n await clearItemNotification(slug, itemId);\n return;\n }\n // Time gate: when the schema declares `triggerField`, suppress the bell\n // until the clock reaches that date (minus `triggerLeadDays`).\n // Unparseable date ⇒ fail safe (no bell); warn ONLY when the field carries\n // a non-empty value that won't parse — an empty optional trigger date is a\n // normal state and must not spam a WARN every reconcile tick.\n if (schema.triggerField) {\n const triggerRaw = item[schema.triggerField];\n const due = isTriggerDue(triggerRaw, now, schema.triggerLeadDays);\n const isEmpty = triggerRaw === undefined || triggerRaw === null || triggerRaw === \"\";\n if (due === null && !isEmpty) {\n log().warn(\"trigger date unparseable, suppressing bell\", { slug, itemId, triggerField: schema.triggerField });\n }\n if (due !== true) {\n await clearItemNotification(slug, itemId);\n return;\n }\n }\n // Condition gate: when the schema declares `notifyWhen`, only bell\n // records matching the predicate. Convergent — a record that stops\n // matching has its bell cleared.\n if (!whenMatches(schema.notifyWhen, item)) {\n await clearItemNotification(slug, itemId);\n return;\n }\n await ensureItemNotification(slug, schema, itemId, resolveDisplayLabel(schema, item, itemId), notifyPriorityForItem(schema, item));\n}\n\n/** Boot-time reconcile: walk every record under `dataDir` once and\n * reconcile it. Catches up changes that happened while the server was\n * down. Deleted items are covered by `sweepStaleActiveEntries`, not this\n * function (it only sees files that exist). */\nexport async function reconcileAllItems(slug: string, schema: CollectionSchema, dataDir: string, ioOpts: IoOptions = {}, now: Date = evalNow()): Promise<void> {\n if (!schema.completionField) return;\n let items: CollectionItem[];\n try {\n items = await listItems(dataDir, ioOpts);\n } catch (err) {\n log().warn(\"reconcile list failed\", { slug, dataDir, error: errMsg(err) });\n return;\n }\n const { primaryKey } = schema;\n for (const item of items) {\n const raw = item[primaryKey];\n if (typeof raw !== \"string\" || raw.length === 0) continue;\n await reconcileItem(slug, schema, dataDir, raw, ioOpts, now);\n }\n}\n\n/** Boot-time sweep over the active bell: drop any entries whose underlying\n * file is gone, whose collection was deleted, whose schema no longer\n * tracks completion, or whose item is now done. Reverse-covers the cases\n * `reconcileAllItems` misses (it only walks files that exist). */\nexport async function sweepStaleActiveEntries(opts: DiscoveryOptions = {}): Promise<void> {\n const adapter = requireAdapter();\n let entries;\n try {\n entries = await listAll();\n } catch (err) {\n log().warn(\"sweep list failed\", { error: errMsg(err) });\n return;\n }\n for (const entry of entries) {\n const own = adapter.readEntry(entry.pluginData);\n if (!own) continue;\n const parsed = parseCompletionLegacyId(own.legacyId);\n if (!parsed) continue;\n const { slug, itemId } = parsed;\n try {\n const collection = await loadCollection(slug, opts);\n if (!collection || !collection.schema.completionField) {\n await notifierClear(entry.id);\n continue;\n }\n const item = await readItem(collection.dataDir, itemId, opts);\n if (item === null || itemIsDone(collection.schema, item) || !whenMatches(collection.schema.notifyWhen, item)) {\n await notifierClear(entry.id);\n }\n } catch (err) {\n log().warn(\"sweep entry failed\", { slug, itemId, error: errMsg(err) });\n }\n }\n}\n\n/** Test-only: clear the per-key in-flight locks. */\nexport function _resetReconcilerLocksForTesting(): void {\n ensureLocks.clear();\n}\n","// Filesystem watchers that drive collection-completion bell\n// notifications. One `fs.watch` per discovered collection's `dataDir`,\n// fanned out from a single boot call + a 30-second re-discovery interval\n// that catches newly-created / deleted collections (there is no\n// in-process \"collections changed\" event broadcast).\n//\n// Why a watcher, not just route hooks: the canonical pattern for\n// collection-skills has the agent Write records directly with the Write\n// tool — that path never hits the REST API, so a route-level hook would\n// miss most of the traffic the user generates. The watcher catches every\n// mutation regardless of who wrote the file.\n//\n// All decisions live in `reconciler.ts`; this module is pure plumbing:\n// discover, mkdir, fs.watch, forward events into the reconciler. Every\n// reconcile call is idempotent so fs.watch's well-known quirks (`rename`\n// vs `change`, atomic-write coalescence, filename === null on some\n// platforms) don't need special handling.\n\nimport { watch, type FSWatcher } from \"node:fs\";\nimport { mkdir } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { discoverCollections, loadCollection, publishCollectionChange, type DiscoveryOptions, type LoadedCollection } from \"../collection/server\";\nimport type { CollectionSchema } from \"../collection\";\nimport { errMsg, log } from \"./config.js\";\nimport { evalNow } from \"./clock.js\";\nimport { reconcileAllItems, reconcileItem, sweepStaleActiveEntries } from \"./reconciler.js\";\n\n// Collections don't get added / removed rapidly; 30 s is a comfortable\n// upper bound on how long a new schema can sit before its watcher is up.\nconst ONE_SECOND_MS = 1000;\nconst ONE_MINUTE_MS = 60 * ONE_SECOND_MS;\nconst REDISCOVERY_INTERVAL_MS = 30 * ONE_SECOND_MS;\n\n// Wall-clock tick that re-reconciles time-dependent collections (those\n// declaring `triggerField` and/or `spawn`). The fs.watcher only re-runs\n// the reconciler on FILE changes; a `triggerField` bell that should fire\n// \"when the clock reaches date X\" — and a `spawn` whose successor's own\n// trigger later comes due — change no file at that moment, so a periodic\n// re-derivation is required.\nconst TRIGGER_TICK_INTERVAL_MS = ONE_MINUTE_MS;\n\ninterface CollectionWatcher {\n slug: string;\n dataDir: string;\n watcher: FSWatcher;\n /** Last-seen serialized schema for change detection. When a rediscovery\n * tick observes a different value, the watcher's items are reconciled\n * and the cache is refreshed — this catches schema-only edits (e.g.\n * flipping `completionField` on or off) that don't touch any record\n * file and would otherwise leave bell state stale indefinitely. */\n schemaJson: string;\n}\n\nconst watchers = new Map<string, CollectionWatcher>();\nlet rediscoveryTimer: ReturnType<typeof setInterval> | null = null;\nlet triggerTimer: ReturnType<typeof setInterval> | null = null;\nlet started = false;\n/** Discovery options threaded into every `discoverCollections` /\n * `loadCollection` / `sweepStaleActiveEntries` call. Production: empty\n * (live workspace). Tests: `{ workspaceRoot, userSkillsDir }` pointing\n * at a fixture tree. Module-level so per-event handlers can read it\n * without threading through every signature. */\nlet discoveryOpts: DiscoveryOptions = {};\n\n/** Per-key single-flight slot (declared here so `stopCollectionWatchers`\n * can clear it during teardown). */\ninterface ReconcileSlot {\n running: Promise<void>;\n pending: boolean;\n}\nconst itemSlots = new Map<string, ReconcileSlot>();\n\n/** Trailing debounce per dataSource collection: an atomic file replace\n * (Excel save, editor rename) surfaces as 2-3 fs events — collapse them\n * into one change publish so live views refetch once. */\nconst DATA_SOURCE_DEBOUNCE_MS = 300;\nconst dataSourceTimers = new Map<string, ReturnType<typeof setTimeout>>();\n\n/** Test-only configuration knobs. Production callers pass nothing and get\n * the live workspace defaults; tests pass a tmpdir-rooted `discoveryOpts`\n * and override the tick cadences (or set them to `null` to disable the\n * auto-ticks so the test drives sync manually). */\nexport interface CollectionWatcherOptions {\n discoveryOpts?: DiscoveryOptions;\n rediscoveryIntervalMs?: number | null;\n triggerTickIntervalMs?: number | null;\n}\n\n/** Boot entry point: sweep stale active entries, then mount watchers for\n * every discovered collection and arm the periodic re-discovery poll.\n * Idempotent — a second call is a no-op. */\nexport async function startCollectionWatchers(opts: CollectionWatcherOptions = {}): Promise<void> {\n if (started) return;\n // `started` only flips on AFTER boot finishes. If sweep or syncWatchers\n // throws mid-boot, reset state on failure so a supervisor / test\n // harness can retry instead of being permanently latched.\n discoveryOpts = opts.discoveryOpts ?? {};\n try {\n // Boot reconcile is split in two: sweep first (drop bell entries whose\n // files / collections / schemas vanished while the server was down),\n // then `syncWatchers` runs the per-collection forward fill. Both paths\n // are idempotent and converge on the same end state.\n await sweepStaleActiveEntries(discoveryOpts);\n await syncWatchers();\n const intervalMs = opts.rediscoveryIntervalMs === undefined ? REDISCOVERY_INTERVAL_MS : opts.rediscoveryIntervalMs;\n if (intervalMs !== null) {\n rediscoveryTimer = setInterval(() => {\n syncWatchers().catch((err: unknown) => {\n log().warn(\"watcher rediscovery failed\", { error: errMsg(err) });\n });\n }, intervalMs);\n // `unref` so a clean process exit isn't blocked waiting for the tick.\n rediscoveryTimer.unref();\n }\n const triggerMs = opts.triggerTickIntervalMs === undefined ? TRIGGER_TICK_INTERVAL_MS : opts.triggerTickIntervalMs;\n if (triggerMs !== null) {\n triggerTimer = setInterval(() => {\n tickTimeTriggers().catch((err: unknown) => {\n log().warn(\"watcher trigger tick failed\", { error: errMsg(err) });\n });\n }, triggerMs);\n triggerTimer.unref();\n }\n started = true;\n } catch (err) {\n discoveryOpts = {};\n throw err;\n }\n}\n\n/** Tear down every watcher and stop the intervals. Used by tests;\n * production never calls this (process exit reclaims the fds). Resets\n * `started` so a subsequent `startCollectionWatchers` re-mounts. */\nexport async function stopCollectionWatchers(): Promise<void> {\n if (rediscoveryTimer) {\n clearInterval(rediscoveryTimer);\n rediscoveryTimer = null;\n }\n if (triggerTimer) {\n clearInterval(triggerTimer);\n triggerTimer = null;\n }\n for (const watcher of watchers.values()) {\n try {\n watcher.watcher.close();\n } catch {\n /* fs.watch close is best-effort */\n }\n }\n watchers.clear();\n itemSlots.clear();\n for (const timer of dataSourceTimers.values()) clearTimeout(timer);\n dataSourceTimers.clear();\n discoveryOpts = {};\n started = false;\n}\n\n/** Test-only: manually trigger one rediscovery + reconcile pass. */\nexport async function _syncWatchersForTesting(): Promise<void> {\n await syncWatchers();\n}\n\n/** Test-only: drive one wall-clock tick synchronously, with an optional\n * injected clock. */\nexport async function _tickTimeTriggersForTesting(now?: Date): Promise<void> {\n await tickTimeTriggers(now);\n}\n\n/** Re-reconcile every watched collection that depends on the clock — i.e.\n * declares `triggerField` (a bell that fires at a date) and/or `spawn`\n * (recurrence whose successors come due over time). Collections with\n * neither are skipped. Idempotent. The schema is parsed back from the\n * watcher's cached `schemaJson` to avoid a per-tick disk read. */\nasync function tickTimeTriggers(now: Date = evalNow()): Promise<void> {\n for (const entry of watchers.values()) {\n let schema: CollectionSchema;\n try {\n schema = JSON.parse(entry.schemaJson) as CollectionSchema;\n } catch (err) {\n log().warn(\"trigger tick: bad cached schema\", { slug: entry.slug, error: errMsg(err) });\n continue;\n }\n // dataSource collections have no reconcilable record files (and zod\n // forbids `spawn` on them) — the clock never changes their state.\n if (schema.dataSource !== undefined) continue;\n if (!schema.triggerField && !schema.spawn) continue;\n await reconcileAllItems(entry.slug, schema, entry.dataDir, discoveryOpts, now);\n }\n}\n\n/** Reconcile the watcher set against the currently-discovered\n * collections. Adds watchers for new slugs (with a boot reconcile of\n * their items), drops watchers for vanished slugs, and re-reconciles\n * items for collections whose schema changed. Runs a final sweep when\n * this tick changed the watcher set or any schema. */\nasync function syncWatchers(): Promise<void> {\n let collections;\n try {\n collections = await discoverCollections(discoveryOpts);\n } catch (err) {\n log().warn(\"watcher discover failed\", { error: errMsg(err) });\n return;\n }\n const liveSlugs = new Set(collections.map((collection) => collection.slug));\n const vanishedMutated = stopVanishedWatchers(liveSlugs);\n const schemaMutated = await reconcileChangedSchemas(collections);\n const addedMutated = await startNewWatchers(collections);\n if (vanishedMutated || schemaMutated || addedMutated) {\n await sweepStaleActiveEntries(discoveryOpts);\n }\n}\n\nfunction stopVanishedWatchers(liveSlugs: Set<string>): boolean {\n let mutated = false;\n for (const slug of [...watchers.keys()]) {\n if (liveSlugs.has(slug)) continue;\n const watcher = watchers.get(slug);\n if (watcher) {\n try {\n watcher.watcher.close();\n } catch {\n /* best-effort */\n }\n }\n watchers.delete(slug);\n mutated = true;\n log().info(\"watcher stopped\", { slug });\n }\n return mutated;\n}\n\n/** True when a schema edit moved the collection's storage — a different\n * `dataSource.path`, a different `dataPath`, or a flip between the two\n * modes. The mounted fs.watch is bound to the OLD location, so it must\n * be remounted, not just re-reconciled. */\nfunction storagePathChanged(previousJson: string, next: LoadedCollection[\"schema\"]): boolean {\n let previous: LoadedCollection[\"schema\"];\n try {\n previous = JSON.parse(previousJson) as LoadedCollection[\"schema\"];\n } catch {\n return true; // unreadable cache — remount to be safe\n }\n return previous.dataSource?.path !== next.dataSource?.path || previous.dataPath !== next.dataPath;\n}\n\n/** Re-reconcile already-watched collections whose schema changed since\n * the last tick. New collections fall through to `startNewWatchers`. */\nasync function reconcileChangedSchemas(collections: readonly LoadedCollection[]): Promise<boolean> {\n let mutated = false;\n for (const collection of collections) {\n const existing = watchers.get(collection.slug);\n if (!existing) continue;\n const nextJson = JSON.stringify(collection.schema);\n if (existing.schemaJson === nextJson) continue;\n if (storagePathChanged(existing.schemaJson, collection.schema)) {\n // Drop the stale mount; `startNewWatchers` (which runs right after\n // this pass in syncWatchers) remounts on the new location. A\n // dataSource collection also gets a change ping so open views\n // refetch against the new file immediately.\n log().info(\"watcher storage path changed, remounting\", { slug: collection.slug });\n try {\n existing.watcher.close();\n } catch {\n /* best-effort */\n }\n watchers.delete(collection.slug);\n if (collection.schema.dataSource !== undefined) publishCollectionChange({ slug: collection.slug, op: \"upsert\" });\n mutated = true;\n continue;\n }\n existing.schemaJson = nextJson;\n if (collection.schema.dataSource !== undefined) {\n // No record files to reconcile — but a schema edit can change what\n // the views render (fields, displayField, …), so ping them.\n log().info(\"dataSource watcher schema changed, publishing\", { slug: collection.slug });\n publishCollectionChange({ slug: collection.slug, op: \"upsert\" });\n mutated = true;\n continue;\n }\n log().info(\"watcher schema changed, re-reconciling\", { slug: collection.slug });\n await reconcileAllItems(collection.slug, collection.schema, collection.dataDir, discoveryOpts);\n mutated = true;\n }\n return mutated;\n}\n\nasync function startNewWatchers(collections: readonly LoadedCollection[]): Promise<boolean> {\n let mutated = false;\n for (const collection of collections) {\n if (watchers.has(collection.slug)) continue;\n if (collection.schema.dataSource !== undefined) await startDataSourceWatcher(collection);\n else await startWatcherFor(collection.slug, collection.schema, collection.dataDir);\n mutated = true;\n }\n return mutated;\n}\n\n/** Publish one (debounced) change event for a dataSource collection —\n * live views refetch the whole collection; no per-row diffing. */\nfunction scheduleDataSourcePublish(slug: string): void {\n const existing = dataSourceTimers.get(slug);\n if (existing) clearTimeout(existing);\n const timer = setTimeout(() => {\n dataSourceTimers.delete(slug);\n publishCollectionChange({ slug, op: \"upsert\" });\n }, DATA_SOURCE_DEBOUNCE_MS);\n timer.unref?.();\n dataSourceTimers.set(slug, timer);\n}\n\n/** Watch a dataSource collection's external data file. The watch mounts\n * on the PARENT directory (watching the file itself goes stale after an\n * atomic replace — the inode changes) and filters events to the file's\n * basename; a null filename (platform quirk) is treated as a hit. No\n * reconciler involvement — replacing the CSV just refreshes the views. */\nasync function startDataSourceWatcher(collection: LoadedCollection): Promise<void> {\n const file = collection.dataSourceFile;\n if (file === undefined) return;\n const dir = path.dirname(file);\n const base = path.basename(file);\n try {\n await mkdir(dir, { recursive: true });\n const watcher = watch(dir, { persistent: false }, (_eventType, filename) => {\n if (filename !== null && filename !== base) return;\n scheduleDataSourcePublish(collection.slug);\n });\n watcher.on(\"error\", (err) => {\n log().warn(\"dataSource watcher error\", { slug: collection.slug, error: errMsg(err) });\n });\n watchers.set(collection.slug, { slug: collection.slug, dataDir: dir, watcher, schemaJson: JSON.stringify(collection.schema) });\n log().info(\"dataSource watcher started\", { slug: collection.slug, file });\n } catch (err) {\n log().warn(\"dataSource watcher start failed\", { slug: collection.slug, error: errMsg(err) });\n }\n}\n\nasync function startWatcherFor(slug: string, schema: CollectionSchema, dataDir: string): Promise<void> {\n try {\n // `fs.watch` throws on a missing dir, so ensure it exists. New\n // collections legitimately start with no records — mkdir is the\n // canonical first-use bootstrap.\n await mkdir(dataDir, { recursive: true });\n // Boot reconcile this collection's existing items BEFORE mounting the\n // watcher: a pending item the user added during downtime needs its\n // bell entry even if no event fires today.\n await reconcileAllItems(slug, schema, dataDir, discoveryOpts);\n const watcher = watch(dataDir, { persistent: false }, (_eventType, filename) => {\n // Errors from inside the callback would propagate as unhandled\n // rejections — wrap so a single bad event can't unwind the watcher.\n onEvent(slug, filename).catch((err: unknown) => {\n log().warn(\"watcher event failed\", { slug, filename, error: errMsg(err) });\n });\n });\n watcher.on(\"error\", (err) => {\n log().warn(\"watcher error\", { slug, error: errMsg(err) });\n });\n watchers.set(slug, { slug, dataDir, watcher, schemaJson: JSON.stringify(schema) });\n log().info(\"watcher started\", { slug, dataDir });\n } catch (err) {\n log().warn(\"watcher start failed\", { slug, error: errMsg(err) });\n }\n}\n\n/** Test-only: the per-key single-flight scheduler. Exported so test code\n * can drive rapid-fire calls directly and observe the trailing coalesce\n * — `fs.watch` event timing is too flaky to assert against.\n *\n * Single-flight semantics: while a reconcile is in flight for a given\n * (slug, itemId), additional events on the same key set `pending = true`\n * and return — the running reconcile re-runs once after it completes.\n * This collapses fs.watch's rapid-fire bursts (atomic rename surfaces as\n * 2-3 events) into a single reconcile + one trailing re-run. */\nexport function _scheduleItemReconcileForTesting(slug: string, schema: CollectionSchema, dataDir: string, itemId: string): Promise<void> {\n return scheduleItemReconcile(slug, schema, dataDir, itemId);\n}\n\nfunction scheduleItemReconcile(slug: string, schema: CollectionSchema, dataDir: string, itemId: string): Promise<void> {\n const key = `${slug}\\x00${itemId}`;\n const existing = itemSlots.get(key);\n if (existing) {\n existing.pending = true;\n return existing.running;\n }\n const slot: ReconcileSlot = { running: Promise.resolve(), pending: false };\n slot.running = (async () => {\n try {\n // Re-run while events keep arriving — the trailing re-run captures\n // any state change that landed during a prior pass. After each pass\n // we read `pending` and zero it before the next iteration, so an\n // event that fires *during* the last reconcile's await still\n // triggers one more pass before the slot is freed.\n let keepGoing = true;\n while (keepGoing) {\n slot.pending = false;\n await reconcileItem(slug, schema, dataDir, itemId, discoveryOpts);\n keepGoing = slot.pending;\n }\n } finally {\n itemSlots.delete(key);\n }\n })();\n itemSlots.set(key, slot);\n return slot.running;\n}\n\n/** Handle a single fs.watch event. Re-loads the collection (schema may\n * have changed since startup), filters out non-record files, and\n * forwards to the single-flighted reconciler. `filename === null` (rare,\n * platform-specific) triggers a full directory rescan to be safe. */\nasync function onEvent(slug: string, filename: string | Buffer | null): Promise<void> {\n const collection = await loadCollection(slug, discoveryOpts);\n if (!collection) return;\n if (filename === null) {\n // Some platforms omit the filename on a watch event — we don't know\n // which record changed. `reconcileAllItems` covers items whose file\n // still exists; pair it with a sweep so any record deleted inside the\n // same opaque event has its stale bell entry cleared too.\n await reconcileAllItems(slug, collection.schema, collection.dataDir, discoveryOpts);\n await sweepStaleActiveEntries(discoveryOpts);\n return;\n }\n const name = typeof filename === \"string\" ? filename : filename.toString(\"utf-8\");\n // Filter: only record files (`*.json`), skip dot-prefixed (atomic\n // writes / OS metadata / editor swap files). The reconciler is\n // idempotent so a stray non-record event would be harmless, but\n // skipping early avoids needless I/O.\n if (!name.endsWith(\".json\") || name.startsWith(\".\")) return;\n const itemId = name.slice(0, -\".json\".length);\n await scheduleItemReconcile(slug, collection.schema, collection.dataDir, itemId);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../../src/collection-watchers/config.ts","../../src/collection-watchers/clock.ts","../../src/collection-watchers/reconciler.ts","../../src/collection-watchers/watcher.ts"],"sourcesContent":["// Host-injected configuration for the collection-completion watchers.\n// The reconciler logic + watcher plumbing are host-agnostic; the\n// notification TAXONOMY (which plugin namespace, how a record priority\n// maps to a bell severity) and the in-app ROUTING (the deep-link a bell\n// row navigates to) are host-specific, so the host supplies them via an\n// adapter. MulmoClaude wires its legacy notification machinery; a future\n// MulmoTerminal wires its own routes + pluginData shape.\n\nimport type { NotifierSeverity } from \"../notifier\";\n\n/** Two-level urgency a pending record can carry, derived from the\n * schema's `notifyWhen` spec. The host maps this onto its own severity\n * scale via `priorityToSeverity`. */\nexport type CompletionPriority = \"normal\" | \"high\";\n\nexport interface CollectionWatcherLogger {\n info: (message: string, data?: Record<string, unknown>) => void;\n warn: (message: string, data?: Record<string, unknown>) => void;\n}\n\n/** The host-specific notification surface the reconciler binds to. The\n * reconciler owns the internal `legacyId` key (it encodes slug+itemId\n * and round-trips it through `pluginData`); the adapter only wraps /\n * unwraps it into whatever shape the host's bell expects. */\nexport interface CollectionNotificationAdapter {\n /** Plugin namespace these bell entries publish under (MulmoClaude: \"todo\"). */\n pluginPkg: string;\n /** Map a record's completion priority onto the host's bell severity. */\n priorityToSeverity: (priority: CompletionPriority) => NotifierSeverity;\n /** Build the in-app deep-link the bell row routes to on click. */\n buildNavigateTarget: (slug: string, itemId: string) => string;\n /** Wrap the reconciler's internal key + priority into the host's\n * `pluginData` shape. Stored verbatim on the entry; recovered via\n * `readEntry`. */\n buildPluginData: (input: { legacyId: string; slug: string; itemId: string; priority: CompletionPriority; navigateTarget: string }) => unknown;\n /** Recognise a bell entry produced by this reconciler and recover its\n * internal key + stored priority. Returns null for entries that didn't\n * originate here, so `listAll()` scans skip foreign entries. */\n readEntry: (pluginData: unknown) => { legacyId: string; priority: CompletionPriority } | null;\n}\n\nconst NOOP_LOG: CollectionWatcherLogger = { info: () => {}, warn: () => {} };\n\nlet adapter: CollectionNotificationAdapter | null = null;\nlet activeLogger: CollectionWatcherLogger = NOOP_LOG;\n\n/** Wire the host adapter + logger. Call once at startup, before\n * `startCollectionWatchers` or any direct reconcile call. */\nexport function configureCollectionWatchers(config: { adapter: CollectionNotificationAdapter; log?: CollectionWatcherLogger }): void {\n ({ adapter } = config);\n activeLogger = config.log ?? NOOP_LOG;\n}\n\nexport function requireAdapter(): CollectionNotificationAdapter {\n if (!adapter) throw new Error(\"collection-watchers: configureCollectionWatchers() not called\");\n return adapter;\n}\n\nexport function log(): CollectionWatcherLogger {\n return activeLogger;\n}\n\n/** Test-only: clear the host wiring. */\nexport function resetCollectionWatchersConfig(): void {\n adapter = null;\n activeLogger = NOOP_LOG;\n}\n\nexport function errMsg(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n","// Evaluation-only injectable clock. When MULMOCLAUDE_FAKE_NOW is set to\n// a parseable date/datetime string, every reconcile pass derives \"now\"\n// from it instead of the wall clock — letting evaluation runs and tests\n// advance time deterministically (a `triggerField` coming due, a `spawn`\n// successor's own trigger firing later) without waiting for real days to\n// pass. Unset or unparseable (i.e. normal operation) → real wall clock.\n// Read per call, so a long-lived server picks up changes without restart.\nexport function evalNow(): Date {\n const raw = process.env.MULMOCLAUDE_FAKE_NOW;\n if (!raw) return new Date();\n const parsed = new Date(raw);\n return Number.isNaN(parsed.getTime()) ? new Date() : parsed;\n}\n","// Bell-notification reconciler for collections whose schema declares\n// `completionField`. Driven by `watcher.ts`, which calls into the\n// functions below on file-system events and on boot.\n//\n// The model is **convergent**: a watcher event re-reads the record from\n// disk and the reconciler enforces the invariant\n//\n// bell entry exists for (slug, itemId) ↔\n// schema has completionField ∧\n// file exists ∧\n// `String(item[completionField])` ∉ completionDoneValues\n// (∧ trigger due ∧ notifyWhen matches, when declared)\n//\n// Each reconcile is idempotent (`ensure*` / `clear*` no-op when state\n// already matches). This is why event-type quirks of `fs.watch`\n// (`rename` vs `change`, missed events, atomic-write coalescence) don't\n// matter — every event re-derives the desired state from the file.\n//\n// Lookup uses a deterministic internal `legacyId` derived from\n// `<slug>:<itemId>`, stashed on each entry's `pluginData` via the host\n// adapter, so the clear path and the dedup check both find the entry\n// without a side state file.\n\nimport { clear as notifierClear, listAll, publish as notifierPublish, updateForPlugin as notifierUpdate, type NotifierEntry } from \"../notifier\";\nimport { fieldText, itemIsDone, whenMatches, type CollectionItem, type CollectionSchema } from \"../collection\";\nimport {\n type DiscoveryOptions,\n type IoOptions,\n isTriggerDue,\n maybeSpawnSuccessor,\n loadCollection,\n storeFor,\n type LoadedCollection,\n} from \"../collection/server\";\nimport { type CompletionPriority, errMsg, log, requireAdapter } from \"./config.js\";\nimport { evalNow } from \"./clock.js\";\n\n/** The internal-id prefix every collection-completion bell entry carries.\n * Used both to build new keys and to filter sweep candidates from the\n * active bell. */\nconst LEGACY_ID_PREFIX = \"collection-completion:\";\n\n/** Stable key encoding slug + item, round-tripped through the entry's\n * `pluginData` so we can find it later without a side state file. Slug +\n * itemId are upstream-validated via `safeSlugName`, which forbids the\n * colon separator, so the two-segment parse below is unambiguous. */\nfunction completionLegacyId(slug: string, itemId: string): string {\n return `${LEGACY_ID_PREFIX}${slug}:${itemId}`;\n}\n\n/** Decode a key back into its (slug, itemId) pair, or null if the string\n * didn't originate from this module. Used by the sweep step. */\nfunction parseCompletionLegacyId(legacyId: string): { slug: string; itemId: string } | null {\n if (!legacyId.startsWith(LEGACY_ID_PREFIX)) return null;\n const body = legacyId.slice(LEGACY_ID_PREFIX.length);\n const colon = body.indexOf(\":\");\n if (colon < 0) return null;\n return { slug: body.slice(0, colon), itemId: body.slice(colon + 1) };\n}\n\n/** The human-readable label shown in a completion notification's title.\n * Uses the schema's `displayField` value when declared and non-empty;\n * otherwise falls back to the record's primaryKey (`itemId`). */\nexport function resolveDisplayLabel(schema: CollectionSchema, item: CollectionItem, itemId: string): string {\n const { displayField } = schema;\n if (!displayField) return itemId;\n // Same rule as `itemLabelOf`: an array/object display field falls back to\n // the id rather than labelling the record \"[object Object]\".\n const label = fieldText(item[displayField]).trim();\n return label.length > 0 ? label : itemId;\n}\n\n// The done predicate moved to `../collection/core/completion` (flag-aware,\n// browser-safe, shared with spawn and view filters). Re-exported so the\n// `collection-watchers` barrel keeps its public surface (MulmoTerminal).\nexport { itemIsDone };\n\n/** Every active bell entry whose key matches this (slug, itemId).\n * Returns multiple when defensive cleanup is needed. Scans `listAll()`\n * — cheap because the active set is bounded. */\nasync function findActiveEntries(slug: string, itemId: string): Promise<NotifierEntry[]> {\n const adapter = requireAdapter();\n const legacyId = completionLegacyId(slug, itemId);\n const entries = await listAll();\n return entries.filter((entry) => adapter.readEntry(entry.pluginData)?.legacyId === legacyId);\n}\n\nasync function findActiveEntryIds(slug: string, itemId: string): Promise<string[]> {\n return (await findActiveEntries(slug, itemId)).map((entry) => entry.id);\n}\n\n/** Per-key in-flight lock. Serializes concurrent `ensureItemNotification`\n * calls for the same (slug, itemId) so the `findActiveEntries → publish`\n * check stays atomic across callers — not just across watcher events.\n * `listAll` bypasses the engine's write queue, so without this lock two\n * reconcile paths (a watcher event + a `reconcileAllItems` pass that a\n * readdir-triggered event raced) could both miss each other's in-flight\n * publish and produce duplicate entries. */\ninterface EnsureLock {\n promise: Promise<void>;\n}\nconst ensureLocks = new Map<string, EnsureLock>();\n\n/** Bell priority for a record: the FIRST flagged value in `notifyWhen.in`\n * (most urgent) reads `high`, every other flagged value `normal`.\n * Collections with no `notifyWhen` (notify for every open record) stay\n * `normal`. */\nfunction notifyPriorityForItem(schema: CollectionSchema, item: CollectionItem): CompletionPriority {\n const spec = schema.notifyWhen;\n if (!spec) return \"normal\";\n const value = item[spec.field] === undefined || item[spec.field] === null ? \"\" : String(item[spec.field]);\n return spec.in.indexOf(value) === 0 ? \"high\" : \"normal\";\n}\n\nasync function ensureItemNotification(\n slug: string,\n schema: CollectionSchema,\n itemId: string,\n displayLabel: string,\n priority: CompletionPriority,\n): Promise<void> {\n const legacyId = completionLegacyId(slug, itemId);\n // Drain any in-flight publish for this key BEFORE our check + set. The\n // drain + claim runs synchronously between `ensureLocks.get` and\n // `ensureLocks.set`, so two callers can't both observe an empty slot.\n\n while (true) {\n const inflight = ensureLocks.get(legacyId);\n if (!inflight) break;\n await inflight.promise;\n }\n const lock: EnsureLock = { promise: doEnsureItemNotification(slug, schema, itemId, legacyId, displayLabel, priority) };\n ensureLocks.set(legacyId, lock);\n try {\n await lock.promise;\n } finally {\n // Only clear the slot if it still points at OUR lock — a\n // sufficiently-delayed cleanup must not stomp a later claim.\n if (ensureLocks.get(legacyId) === lock) {\n ensureLocks.delete(legacyId);\n }\n }\n}\n\n/** Converge any already-present bell entries to `priority`, updating in\n * place (preserving id / position / createdAt) so a record whose flagged\n * value changed while it stayed pending re-colours the bell without a\n * clear+republish flicker. No-op when the stored priority already matches. */\nasync function reconcileEntrySeverity(slug: string, itemId: string, entries: NotifierEntry[], priority: CompletionPriority): Promise<void> {\n const adapter = requireAdapter();\n for (const entry of entries) {\n const parsed = adapter.readEntry(entry.pluginData);\n if (!parsed || parsed.priority === priority) continue;\n await notifierUpdate(adapter.pluginPkg, entry.id, {\n severity: adapter.priorityToSeverity(priority),\n pluginData: adapter.buildPluginData({\n legacyId: parsed.legacyId,\n slug,\n itemId,\n priority,\n navigateTarget: adapter.buildNavigateTarget(slug, itemId),\n }),\n });\n }\n}\n\nasync function doEnsureItemNotification(\n slug: string,\n schema: CollectionSchema,\n itemId: string,\n legacyId: string,\n displayLabel: string,\n priority: CompletionPriority,\n): Promise<void> {\n const adapter = requireAdapter();\n try {\n const existing = await findActiveEntries(slug, itemId);\n if (existing.length > 0) {\n await reconcileEntrySeverity(slug, itemId, existing, priority);\n return;\n }\n const navigateTarget = adapter.buildNavigateTarget(slug, itemId);\n // `lifecycle: \"action\"` — these are state-of-the-world entries\n // mirroring an outstanding obligation (the item is pending), not\n // transient pings. Validation requires a non-info severity and a\n // non-empty `navigateTarget` (the slug + itemId deep-link).\n await notifierPublish({\n pluginPkg: adapter.pluginPkg,\n severity: adapter.priorityToSeverity(priority),\n lifecycle: \"action\",\n title: `${schema.title}: ${displayLabel}`,\n navigateTarget,\n pluginData: adapter.buildPluginData({ legacyId, slug, itemId, priority, navigateTarget }),\n });\n } catch (err) {\n log().warn(\"notify ensure failed\", { slug, itemId, error: errMsg(err) });\n }\n}\n\n/** Idempotently clear EVERY bell entry that matches this (slug, itemId).\n * Silent no-op when nothing matches. The \"every\" is defensive: if a\n * duplicate ever slips through, this drains the lot. */\nexport async function clearItemNotification(slug: string, itemId: string): Promise<void> {\n try {\n const ids = await findActiveEntryIds(slug, itemId);\n for (const entryId of ids) {\n await notifierClear(entryId);\n }\n } catch (err) {\n log().warn(\"notify clear failed\", { slug, itemId, error: errMsg(err) });\n }\n}\n\n/** Reconcile one item to the desired bell state. Re-reads the record\n * through the collection's STORE (file, sqlite, …) so the decision is\n * grounded in current truth, not in the event payload. Safe to call when\n * the record is missing (delete path).\n *\n * `ioOpts` flows into the store's workspace-containment checks —\n * production callers (the watcher) pass nothing; tests pass\n * `{ workspaceRoot: <tmpdir> }` so the check accepts a fixture dataDir. */\nexport async function reconcileItem(collection: LoadedCollection, itemId: string, ioOpts: IoOptions = {}, now: Date = evalNow()): Promise<void> {\n const { slug, schema } = collection;\n if (!schema.completionField) {\n // Schema doesn't track completion — drop any stale entry.\n await clearItemNotification(slug, itemId);\n return;\n }\n const item = await storeFor(collection, ioOpts).read(itemId);\n if (item === null) {\n await clearItemNotification(slug, itemId);\n return;\n }\n // Recurrence: predicate-gated + create-if-absent, idempotent and\n // independent of this item's own bell state. Runs before the done-clear\n // below so marking an item done still spawns its successor.\n await maybeSpawnSuccessor(collection, item, itemId, ioOpts);\n if (itemIsDone(schema, item)) {\n await clearItemNotification(slug, itemId);\n return;\n }\n // Time gate: when the schema declares `triggerField`, suppress the bell\n // until the clock reaches that date (minus `triggerLeadDays`).\n // Unparseable date ⇒ fail safe (no bell); warn ONLY when the field carries\n // a non-empty value that won't parse — an empty optional trigger date is a\n // normal state and must not spam a WARN every reconcile tick.\n if (schema.triggerField) {\n const triggerRaw = item[schema.triggerField];\n const due = isTriggerDue(triggerRaw, now, schema.triggerLeadDays);\n const isEmpty = triggerRaw === undefined || triggerRaw === null || triggerRaw === \"\";\n if (due === null && !isEmpty) {\n log().warn(\"trigger date unparseable, suppressing bell\", { slug, itemId, triggerField: schema.triggerField });\n }\n if (due !== true) {\n await clearItemNotification(slug, itemId);\n return;\n }\n }\n // Condition gate: when the schema declares `notifyWhen`, only bell\n // records matching the predicate. Convergent — a record that stops\n // matching has its bell cleared.\n if (!whenMatches(schema.notifyWhen, item)) {\n await clearItemNotification(slug, itemId);\n return;\n }\n await ensureItemNotification(slug, schema, itemId, resolveDisplayLabel(schema, item, itemId), notifyPriorityForItem(schema, item));\n}\n\n/** Boot-time reconcile: walk every record of the collection once (through\n * its store) and reconcile it. Catches up changes that happened while the\n * server was down. Deleted items are covered by\n * `sweepStaleActiveEntries`, not this function (it only sees records\n * that exist). */\nexport async function reconcileAllItems(collection: LoadedCollection, ioOpts: IoOptions = {}, now: Date = evalNow()): Promise<void> {\n const { slug, schema } = collection;\n if (!schema.completionField) return;\n let items: CollectionItem[];\n try {\n items = await storeFor(collection, ioOpts).list();\n } catch (err) {\n log().warn(\"reconcile list failed\", { slug, error: errMsg(err) });\n return;\n }\n const { primaryKey } = schema;\n for (const item of items) {\n const raw = item[primaryKey];\n if (typeof raw !== \"string\" || raw.length === 0) continue;\n await reconcileItem(collection, raw, ioOpts, now);\n }\n}\n\n/** Boot-time sweep over the active bell: drop any entries whose underlying\n * file is gone, whose collection was deleted, whose schema no longer\n * tracks completion, or whose item is now done. Reverse-covers the cases\n * `reconcileAllItems` misses (it only walks files that exist). */\nexport async function sweepStaleActiveEntries(opts: DiscoveryOptions = {}): Promise<void> {\n const adapter = requireAdapter();\n let entries;\n try {\n entries = await listAll();\n } catch (err) {\n log().warn(\"sweep list failed\", { error: errMsg(err) });\n return;\n }\n for (const entry of entries) {\n const own = adapter.readEntry(entry.pluginData);\n if (!own) continue;\n const parsed = parseCompletionLegacyId(own.legacyId);\n if (!parsed) continue;\n const { slug, itemId } = parsed;\n try {\n const collection = await loadCollection(slug, opts);\n if (!collection || !collection.schema.completionField) {\n await notifierClear(entry.id);\n continue;\n }\n const item = await storeFor(collection, opts).read(itemId);\n if (item === null || itemIsDone(collection.schema, item) || !whenMatches(collection.schema.notifyWhen, item)) {\n await notifierClear(entry.id);\n }\n } catch (err) {\n log().warn(\"sweep entry failed\", { slug, itemId, error: errMsg(err) });\n }\n }\n}\n\n/** Test-only: clear the per-key in-flight locks. */\nexport function _resetReconcilerLocksForTesting(): void {\n ensureLocks.clear();\n}\n","// Filesystem watchers that drive collection-completion bell\n// notifications AND the live-refresh change event. One `fs.watch` per\n// discovered collection's `dataDir`, fanned out from a single boot call\n// + a 30-second re-discovery interval that catches newly-created /\n// deleted collections (there is no in-process \"collections changed\"\n// event broadcast).\n//\n// Why a watcher, not just route hooks: the canonical pattern for\n// collection-skills has the agent Write records directly with the Write\n// tool — that path never hits the REST API, so a route-level hook would\n// miss most of the traffic the user generates. The watcher catches every\n// mutation regardless of who wrote the file.\n//\n// That same reasoning is why the watcher publishes change events: a\n// write through `io.ts` publishes its own (immediately, and reliably\n// even where fs.watch is unavailable), but a direct file write has no\n// other producer, so open views would never refresh. Both producers\n// firing for one `io.ts` write is intentional — the payload carries no\n// bodies, so a duplicate costs one redundant refetch, whereas a missed\n// event leaves the UI silently stale.\n//\n// All decisions live in `reconciler.ts`; this module is pure plumbing:\n// discover, mkdir, fs.watch, forward events into the reconciler. Every\n// reconcile call is idempotent so fs.watch's well-known quirks (`rename`\n// vs `change`, atomic-write coalescence, filename === null on some\n// platforms) don't need special handling.\n\nimport { watch, type FSWatcher } from \"node:fs\";\nimport { access, mkdir } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { discoverCollections, itemFilePath, loadCollection, publishCollectionChange, type DiscoveryOptions, type LoadedCollection } from \"../collection/server\";\nimport type { CollectionSchema } from \"../collection\";\nimport { errMsg, log } from \"./config.js\";\nimport { evalNow } from \"./clock.js\";\nimport { reconcileAllItems, reconcileItem, sweepStaleActiveEntries } from \"./reconciler.js\";\n\n// Collections don't get added / removed rapidly; 30 s is a comfortable\n// upper bound on how long a new schema can sit before its watcher is up.\nconst ONE_SECOND_MS = 1000;\nconst ONE_MINUTE_MS = 60 * ONE_SECOND_MS;\nconst REDISCOVERY_INTERVAL_MS = 30 * ONE_SECOND_MS;\n\n// Wall-clock tick that re-reconciles time-dependent collections (those\n// declaring `triggerField` and/or `spawn`). The fs.watcher only re-runs\n// the reconciler on FILE changes; a `triggerField` bell that should fire\n// \"when the clock reaches date X\" — and a `spawn` whose successor's own\n// trigger later comes due — change no file at that moment, so a periodic\n// re-derivation is required.\nconst TRIGGER_TICK_INTERVAL_MS = ONE_MINUTE_MS;\n\ninterface CollectionWatcher {\n slug: string;\n dataDir: string;\n watcher: FSWatcher;\n /** Last-seen serialized schema for change detection. When a rediscovery\n * tick observes a different value, the watcher's items are reconciled\n * and the cache is refreshed — this catches schema-only edits (e.g.\n * flipping `completionField` on or off) that don't touch any record\n * file and would otherwise leave bell state stale indefinitely. */\n schemaJson: string;\n /** The discovered collection this watcher was mounted for — what the\n * reconciler needs to pick the right STORE (file records vs a sqlite\n * `storage` db). Refreshed whenever `schemaJson` is. */\n collection: LoadedCollection;\n}\n\nconst watchers = new Map<string, CollectionWatcher>();\nlet rediscoveryTimer: ReturnType<typeof setInterval> | null = null;\nlet triggerTimer: ReturnType<typeof setInterval> | null = null;\nlet started = false;\n/** Discovery options threaded into every `discoverCollections` /\n * `loadCollection` / `sweepStaleActiveEntries` call. Production: empty\n * (live workspace). Tests: `{ workspaceRoot, userSkillsDir }` pointing\n * at a fixture tree. Module-level so per-event handlers can read it\n * without threading through every signature. */\nlet discoveryOpts: DiscoveryOptions = {};\n\n/** Per-key single-flight slot (declared here so `stopCollectionWatchers`\n * can clear it during teardown). */\ninterface ReconcileSlot {\n running: Promise<void>;\n pending: boolean;\n}\nconst itemSlots = new Map<string, ReconcileSlot>();\n\n/** Per-slug single-flight for a storage (db-file) collection's full\n * reconcile pass — a burst of db writes collapses to one pass + one\n * trailing re-run, mirroring the per-item slots of the file watcher. */\nconst storageSlots = new Map<string, ReconcileSlot>();\n\n/** Trailing debounce per dataSource collection: an atomic file replace\n * (Excel save, editor rename) surfaces as 2-3 fs events — collapse them\n * into one change publish so live views refetch once. */\nconst DATA_SOURCE_DEBOUNCE_MS = 300;\nconst dataSourceTimers = new Map<string, ReturnType<typeof setTimeout>>();\n\n/** Test-only configuration knobs. Production callers pass nothing and get\n * the live workspace defaults; tests pass a tmpdir-rooted `discoveryOpts`\n * and override the tick cadences (or set them to `null` to disable the\n * auto-ticks so the test drives sync manually). */\nexport interface CollectionWatcherOptions {\n discoveryOpts?: DiscoveryOptions;\n rediscoveryIntervalMs?: number | null;\n triggerTickIntervalMs?: number | null;\n}\n\n/** Boot entry point: sweep stale active entries, then mount watchers for\n * every discovered collection and arm the periodic re-discovery poll.\n * Idempotent — a second call is a no-op. */\nexport async function startCollectionWatchers(opts: CollectionWatcherOptions = {}): Promise<void> {\n if (started) return;\n // `started` only flips on AFTER boot finishes. If sweep or syncWatchers\n // throws mid-boot, reset state on failure so a supervisor / test\n // harness can retry instead of being permanently latched.\n discoveryOpts = opts.discoveryOpts ?? {};\n try {\n // Boot reconcile is split in two: sweep first (drop bell entries whose\n // files / collections / schemas vanished while the server was down),\n // then `syncWatchers` runs the per-collection forward fill. Both paths\n // are idempotent and converge on the same end state.\n await sweepStaleActiveEntries(discoveryOpts);\n await syncWatchers();\n const intervalMs = opts.rediscoveryIntervalMs === undefined ? REDISCOVERY_INTERVAL_MS : opts.rediscoveryIntervalMs;\n if (intervalMs !== null) {\n rediscoveryTimer = setInterval(() => {\n syncWatchers().catch((err: unknown) => {\n log().warn(\"watcher rediscovery failed\", { error: errMsg(err) });\n });\n }, intervalMs);\n // `unref` so a clean process exit isn't blocked waiting for the tick.\n rediscoveryTimer.unref();\n }\n const triggerMs = opts.triggerTickIntervalMs === undefined ? TRIGGER_TICK_INTERVAL_MS : opts.triggerTickIntervalMs;\n if (triggerMs !== null) {\n triggerTimer = setInterval(() => {\n tickTimeTriggers().catch((err: unknown) => {\n log().warn(\"watcher trigger tick failed\", { error: errMsg(err) });\n });\n }, triggerMs);\n triggerTimer.unref();\n }\n started = true;\n } catch (err) {\n discoveryOpts = {};\n throw err;\n }\n}\n\n/** Tear down every watcher and stop the intervals. Used by tests;\n * production never calls this (process exit reclaims the fds). Resets\n * `started` so a subsequent `startCollectionWatchers` re-mounts. */\nexport async function stopCollectionWatchers(): Promise<void> {\n if (rediscoveryTimer) {\n clearInterval(rediscoveryTimer);\n rediscoveryTimer = null;\n }\n if (triggerTimer) {\n clearInterval(triggerTimer);\n triggerTimer = null;\n }\n for (const watcher of watchers.values()) {\n try {\n watcher.watcher.close();\n } catch {\n /* fs.watch close is best-effort */\n }\n }\n watchers.clear();\n itemSlots.clear();\n storageSlots.clear();\n for (const timer of dataSourceTimers.values()) clearTimeout(timer);\n dataSourceTimers.clear();\n discoveryOpts = {};\n started = false;\n}\n\n/** Test-only: manually trigger one rediscovery + reconcile pass. */\nexport async function _syncWatchersForTesting(): Promise<void> {\n await syncWatchers();\n}\n\n/** Test-only: drive one wall-clock tick synchronously, with an optional\n * injected clock. */\nexport async function _tickTimeTriggersForTesting(now?: Date): Promise<void> {\n await tickTimeTriggers(now);\n}\n\n/** Re-reconcile every watched collection that depends on the clock — i.e.\n * declares `triggerField` (a bell that fires at a date) and/or `spawn`\n * (recurrence whose successors come due over time). Collections with\n * neither are skipped. Idempotent. The schema is parsed back from the\n * watcher's cached `schemaJson` to avoid a per-tick disk read. */\nasync function tickTimeTriggers(now: Date = evalNow()): Promise<void> {\n for (const entry of watchers.values()) {\n let schema: CollectionSchema;\n try {\n schema = JSON.parse(entry.schemaJson) as CollectionSchema;\n } catch (err) {\n log().warn(\"trigger tick: bad cached schema\", { slug: entry.slug, error: errMsg(err) });\n continue;\n }\n // dataSource collections have no reconcilable records (and zod\n // forbids `spawn` on them) — the clock never changes their state.\n if (schema.dataSource !== undefined) continue;\n if (!schema.triggerField && !schema.spawn) continue;\n await reconcileAllItems(entry.collection, discoveryOpts, now);\n }\n}\n\n/** Reconcile the watcher set against the currently-discovered\n * collections. Adds watchers for new slugs (with a boot reconcile of\n * their items), drops watchers for vanished slugs, and re-reconciles\n * items for collections whose schema changed. Runs a final sweep when\n * this tick changed the watcher set or any schema. */\nasync function syncWatchers(): Promise<void> {\n let collections;\n try {\n collections = await discoverCollections(discoveryOpts);\n } catch (err) {\n log().warn(\"watcher discover failed\", { error: errMsg(err) });\n return;\n }\n const liveSlugs = new Set(collections.map((collection) => collection.slug));\n const vanishedMutated = stopVanishedWatchers(liveSlugs);\n const schemaMutated = await reconcileChangedSchemas(collections);\n const addedMutated = await startNewWatchers(collections);\n if (vanishedMutated || schemaMutated || addedMutated) {\n await sweepStaleActiveEntries(discoveryOpts);\n }\n}\n\nfunction stopVanishedWatchers(liveSlugs: Set<string>): boolean {\n let mutated = false;\n for (const slug of [...watchers.keys()]) {\n if (liveSlugs.has(slug)) continue;\n const watcher = watchers.get(slug);\n if (watcher) {\n try {\n watcher.watcher.close();\n } catch {\n /* best-effort */\n }\n }\n watchers.delete(slug);\n mutated = true;\n log().info(\"watcher stopped\", { slug });\n }\n return mutated;\n}\n\n/** True when a schema edit moved the collection's storage — a different\n * `dataSource.path`, a different `dataPath`, or a flip between the two\n * modes. The mounted fs.watch is bound to the OLD location, so it must\n * be remounted, not just re-reconciled. */\nfunction storagePathChanged(previousJson: string, next: LoadedCollection[\"schema\"]): boolean {\n let previous: LoadedCollection[\"schema\"];\n try {\n previous = JSON.parse(previousJson) as LoadedCollection[\"schema\"];\n } catch {\n return true; // unreadable cache — remount to be safe\n }\n return previous.dataSource?.path !== next.dataSource?.path || previous.dataPath !== next.dataPath || previous.storage?.path !== next.storage?.path;\n}\n\n/** Re-reconcile already-watched collections whose schema changed since\n * the last tick. New collections fall through to `startNewWatchers`. */\nasync function reconcileChangedSchemas(collections: readonly LoadedCollection[]): Promise<boolean> {\n let mutated = false;\n for (const collection of collections) {\n const existing = watchers.get(collection.slug);\n if (!existing) continue;\n const nextJson = JSON.stringify(collection.schema);\n if (existing.schemaJson === nextJson) continue;\n if (storagePathChanged(existing.schemaJson, collection.schema)) {\n // Drop the stale mount; `startNewWatchers` (which runs right after\n // this pass in syncWatchers) remounts on the new location. A\n // dataSource collection also gets a change ping so open views\n // refetch against the new file immediately.\n log().info(\"watcher storage path changed, remounting\", { slug: collection.slug });\n try {\n existing.watcher.close();\n } catch {\n /* best-effort */\n }\n watchers.delete(collection.slug);\n if (collection.schema.dataSource !== undefined) publishCollectionChange({ slug: collection.slug, op: \"upsert\" });\n mutated = true;\n continue;\n }\n existing.schemaJson = nextJson;\n existing.collection = collection;\n if (collection.schema.dataSource !== undefined) {\n // No record files to reconcile — but a schema edit can change what\n // the views render (fields, displayField, …), so ping them.\n log().info(\"dataSource watcher schema changed, publishing\", { slug: collection.slug });\n publishCollectionChange({ slug: collection.slug, op: \"upsert\" });\n mutated = true;\n continue;\n }\n log().info(\"watcher schema changed, re-reconciling\", { slug: collection.slug });\n await reconcileAllItems(collection, discoveryOpts);\n mutated = true;\n }\n return mutated;\n}\n\nasync function startNewWatchers(collections: readonly LoadedCollection[]): Promise<boolean> {\n let mutated = false;\n for (const collection of collections) {\n if (watchers.has(collection.slug)) continue;\n if (collection.schema.dataSource !== undefined) await startDataSourceWatcher(collection);\n else if (collection.schema.storage !== undefined) await startStorageWatcher(collection);\n else await startWatcherFor(collection);\n mutated = true;\n }\n return mutated;\n}\n\n/** Publish one (debounced) change event for a dataSource collection —\n * live views refetch the whole collection; no per-row diffing. */\nfunction scheduleDataSourcePublish(slug: string): void {\n const existing = dataSourceTimers.get(slug);\n if (existing) clearTimeout(existing);\n const timer = setTimeout(() => {\n dataSourceTimers.delete(slug);\n publishCollectionChange({ slug, op: \"upsert\" });\n }, DATA_SOURCE_DEBOUNCE_MS);\n timer.unref?.();\n dataSourceTimers.set(slug, timer);\n}\n\n/** Watch a dataSource collection's external data file. The watch mounts\n * on the PARENT directory (watching the file itself goes stale after an\n * atomic replace — the inode changes) and filters events to the file's\n * basename; a null filename (platform quirk) is treated as a hit. No\n * reconciler involvement — replacing the CSV just refreshes the views. */\nasync function startDataSourceWatcher(collection: LoadedCollection): Promise<void> {\n const file = collection.dataSourceFile;\n if (file === undefined) return;\n const dir = path.dirname(file);\n const base = path.basename(file);\n try {\n await mkdir(dir, { recursive: true });\n const watcher = watch(dir, { persistent: false }, (_eventType, filename) => {\n if (filename !== null && filename !== base) return;\n scheduleDataSourcePublish(collection.slug);\n });\n watcher.on(\"error\", (err) => {\n log().warn(\"dataSource watcher error\", { slug: collection.slug, error: errMsg(err) });\n });\n watchers.set(collection.slug, { slug: collection.slug, dataDir: dir, watcher, schemaJson: JSON.stringify(collection.schema), collection });\n log().info(\"dataSource watcher started\", { slug: collection.slug, file });\n } catch (err) {\n log().warn(\"dataSource watcher start failed\", { slug: collection.slug, error: errMsg(err) });\n }\n}\n\n/** Watch a `storage` collection's database file. One db file holds every\n * record, so an event can't name WHICH record changed — each (debounced)\n * event runs a full `reconcileAllItems` pass (bells / spawn) plus a\n * change publish so views refetch after EXTERNAL edits too (host writes\n * already publish their own change events; the extra ping is debounced\n * and idempotent). Mounts on the parent dir, same as the dataSource\n * watcher, so an atomic replace can't strand the watch. */\nasync function startStorageWatcher(collection: LoadedCollection): Promise<void> {\n const file = collection.storageFile;\n if (file === undefined) return;\n const dir = path.dirname(file);\n const base = path.basename(file);\n try {\n await mkdir(dir, { recursive: true });\n await reconcileAllItems(collection, discoveryOpts);\n const watcher = watch(dir, { persistent: false }, (_eventType, rawFilename) => {\n // fs.watch can hand back a Buffer on some platforms despite the\n // string typing — stringify defensively (a Buffer has no startsWith,\n // so calling it directly would throw inside the callback and crash).\n const filename = rawFilename === null ? null : String(rawFilename);\n // Null filename (platform quirk) counts as a hit; otherwise accept\n // the db itself plus its sqlite sidecars (`<db>-wal`, `<db>-journal`).\n if (filename !== null && !filename.startsWith(base)) return;\n scheduleStorageReconcile(collection.slug).catch((err: unknown) => {\n log().warn(\"storage watcher reconcile failed\", { slug: collection.slug, error: errMsg(err) });\n });\n });\n watcher.on(\"error\", (err) => {\n log().warn(\"storage watcher error\", { slug: collection.slug, error: errMsg(err) });\n });\n watchers.set(collection.slug, { slug: collection.slug, dataDir: dir, watcher, schemaJson: JSON.stringify(collection.schema), collection });\n log().info(\"storage watcher started\", { slug: collection.slug, file });\n } catch (err) {\n log().warn(\"storage watcher start failed\", { slug: collection.slug, error: errMsg(err) });\n }\n}\n\nfunction scheduleStorageReconcile(slug: string): Promise<void> {\n return runSingleFlight(storageSlots, slug, async () => {\n const collection = await loadCollection(slug, discoveryOpts);\n if (!collection) return;\n await reconcileAllItems(collection, discoveryOpts);\n // One db file holds every record, so a DELETED row leaves no per-item\n // event — sweep the active bell so its entry converges like a\n // file-backed delete does (same pairing as the unknown-filename path).\n await sweepStaleActiveEntries(discoveryOpts);\n publishCollectionChange({ slug, op: \"upsert\" });\n });\n}\n\n/** Test-only: drive one storage-collection reconcile pass directly. */\nexport function _scheduleStorageReconcileForTesting(slug: string): Promise<void> {\n return scheduleStorageReconcile(slug);\n}\n\nasync function startWatcherFor(collection: LoadedCollection): Promise<void> {\n const { slug, schema, dataDir } = collection;\n try {\n // `fs.watch` throws on a missing dir, so ensure it exists. New\n // collections legitimately start with no records — mkdir is the\n // canonical first-use bootstrap.\n await mkdir(dataDir, { recursive: true });\n // Boot reconcile this collection's existing items BEFORE mounting the\n // watcher: a pending item the user added during downtime needs its\n // bell entry even if no event fires today.\n await reconcileAllItems(collection, discoveryOpts);\n const watcher = watch(dataDir, { persistent: false }, (_eventType, filename) => {\n // Errors from inside the callback would propagate as unhandled\n // rejections — wrap so a single bad event can't unwind the watcher.\n onEvent(slug, filename).catch((err: unknown) => {\n log().warn(\"watcher event failed\", { slug, filename, error: errMsg(err) });\n });\n });\n watcher.on(\"error\", (err) => {\n log().warn(\"watcher error\", { slug, error: errMsg(err) });\n });\n watchers.set(slug, { slug, dataDir, watcher, schemaJson: JSON.stringify(schema), collection });\n log().info(\"watcher started\", { slug, dataDir });\n } catch (err) {\n log().warn(\"watcher start failed\", { slug, error: errMsg(err) });\n }\n}\n\n/** Test-only: the per-key single-flight scheduler. Exported so test code\n * can drive rapid-fire calls directly and observe the trailing coalesce\n * — `fs.watch` event timing is too flaky to assert against.\n *\n * Single-flight semantics: while a reconcile is in flight for a given\n * (slug, itemId), additional events on the same key set `pending = true`\n * and return — the running reconcile re-runs once after it completes.\n * This collapses fs.watch's rapid-fire bursts (atomic rename surfaces as\n * 2-3 events) into a single reconcile + one trailing re-run. */\nexport function _scheduleItemReconcileForTesting(collection: LoadedCollection, itemId: string): Promise<void> {\n return scheduleItemReconcile(collection, itemId);\n}\n\nfunction scheduleItemReconcile(collection: LoadedCollection, itemId: string): Promise<void> {\n return runSingleFlight(\n itemSlots,\n `${collection.slug}\\x00${itemId}`,\n () => reconcileItem(collection, itemId, discoveryOpts),\n () => publishItemChange(collection, itemId),\n );\n}\n\n/** The shared single-flight loop behind both schedulers. Re-runs `pass`\n * while events keep arriving — the trailing re-run captures any state\n * change that landed during a prior pass. After each pass we read\n * `pending` and zero it before the next iteration, so an event that\n * fires *during* the last pass's await still triggers one more pass\n * before the slot is freed. `onSettled`, if given, runs once in the\n * `finally` after the slot is freed — the item scheduler uses it to\n * publish exactly one live-refresh event per burst. */\nfunction runSingleFlight(slots: Map<string, ReconcileSlot>, key: string, pass: () => Promise<void>, onSettled?: () => Promise<void>): Promise<void> {\n const existing = slots.get(key);\n if (existing) {\n existing.pending = true;\n return existing.running;\n }\n const slot: ReconcileSlot = { running: Promise.resolve(), pending: false };\n slot.running = (async () => {\n try {\n let keepGoing = true;\n while (keepGoing) {\n slot.pending = false;\n await pass();\n keepGoing = slot.pending;\n }\n } finally {\n slots.delete(key);\n // `onSettled` runs once after the slot is freed — the slot is the\n // coalescing primitive, so one burst yields one call. It's in the\n // `finally` because a failed reconcile still means a file changed,\n // and open views must be told to refetch either way.\n if (onSettled) await onSettled();\n }\n })();\n slots.set(key, slot);\n return slot.running;\n}\n\n/** Emit the live-refresh event for one record. `op` is derived from\n * whether the file is still there — `fs.watch` reports neither the kind\n * of change nor, reliably, which of `rename`/`change` means what. Only\n * file-backed collections reach here; a `storage` (db) collection has no\n * per-record file and publishes wholesale in `scheduleStorageReconcile`. */\nasync function publishItemChange(collection: LoadedCollection, itemId: string): Promise<void> {\n const changeOp = (await itemFileExists(collection.dataDir, itemId)) ? \"upsert\" : \"delete\";\n safePublish({ slug: collection.slug, ids: [itemId], op: changeOp });\n}\n\nasync function itemFileExists(dataDir: string, itemId: string): Promise<boolean> {\n try {\n await access(itemFilePath(dataDir, itemId));\n return true;\n } catch {\n return false;\n }\n}\n\n/** The publisher is host-supplied, so treat it as untrusted: a throw here\n * runs inside a `finally` and would mask the reconcile's own error. */\nfunction safePublish(payload: { slug: string; ids?: string[]; op?: \"upsert\" | \"delete\" }): void {\n try {\n publishCollectionChange(payload);\n } catch (err) {\n log().warn(\"collection change publish failed\", { slug: payload.slug, error: errMsg(err) });\n }\n}\n\n/** Handle a single fs.watch event. Re-loads the collection (schema may\n * have changed since startup), filters out non-record files, and\n * forwards to the single-flighted reconciler. `filename === null` (rare,\n * platform-specific) triggers a full directory rescan to be safe. */\nasync function onEvent(slug: string, filename: string | Buffer | null): Promise<void> {\n const collection = await loadCollection(slug, discoveryOpts);\n if (!collection) return;\n if (filename === null) {\n // Some platforms omit the filename on a watch event — we don't know\n // which record changed. `reconcileAllItems` covers items whose file\n // still exists; pair it with a sweep so any record deleted inside the\n // same opaque event has its stale bell entry cleared too.\n try {\n await reconcileAllItems(collection, discoveryOpts);\n await sweepStaleActiveEntries(discoveryOpts);\n } finally {\n // In `finally` for the same reason as the per-item path: a failed\n // reconcile still means a file changed. No id and no op — subscribers\n // refetch the whole collection anyway, and guessing either would lie.\n safePublish({ slug });\n }\n return;\n }\n const name = typeof filename === \"string\" ? filename : filename.toString(\"utf-8\");\n // Filter: only record files (`*.json`), skip dot-prefixed (atomic\n // writes / OS metadata / editor swap files). The reconciler is\n // idempotent so a stray non-record event would be harmless, but\n // skipping early avoids needless I/O.\n if (!name.endsWith(\".json\") || name.startsWith(\".\")) return;\n const itemId = name.slice(0, -\".json\".length);\n await scheduleItemReconcile(collection, itemId);\n}\n"],"mappings":"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"}
|