@mulmoclaude/core 5.8.0 → 5.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/dist/collection/firestore.cjs.map +1 -1
  2. package/dist/collection/firestore.js.map +1 -1
  3. package/dist/collection/index.cjs +3 -3
  4. package/dist/collection/index.cjs.map +1 -1
  5. package/dist/collection/index.js +3 -3
  6. package/dist/collection/registry/server/index.cjs +3 -3
  7. package/dist/collection/registry/server/index.cjs.map +1 -1
  8. package/dist/collection/registry/server/index.js +3 -3
  9. package/dist/collection/registry/server/index.js.map +1 -1
  10. package/dist/collection/server/index.cjs +2 -2
  11. package/dist/collection/server/index.js +2 -2
  12. package/dist/collection-watchers/index.cjs +4 -4
  13. package/dist/collection-watchers/index.cjs.map +1 -1
  14. package/dist/collection-watchers/index.js +4 -4
  15. package/dist/collection-watchers/index.js.map +1 -1
  16. package/dist/{discovery-XoYJI2Ot.js → discovery-CbwHtcwk.js} +3 -3
  17. package/dist/{discovery-XoYJI2Ot.js.map → discovery-CbwHtcwk.js.map} +1 -1
  18. package/dist/{discovery-CwBXvtyn.cjs → discovery-DgZRmO7F.cjs} +3 -3
  19. package/dist/discovery-DgZRmO7F.cjs.map +1 -0
  20. package/dist/{dist-Gj7ygaW2.cjs → dist-BZ62pKuU.cjs} +11 -1
  21. package/dist/dist-BZ62pKuU.cjs.map +1 -0
  22. package/dist/{dist-BzoA9pDR.js → dist-MFUV9FYo.js} +6 -2
  23. package/dist/dist-MFUV9FYo.js.map +1 -0
  24. package/dist/errorMessage-OEQzRwsi.cjs +1 -0
  25. package/dist/errorMessage-VEiXHRnX.js +1 -0
  26. package/dist/{errors-CdXsfFCF.cjs → errors-B0xloJ3E.cjs} +2 -2
  27. package/dist/errors-B0xloJ3E.cjs.map +1 -0
  28. package/dist/{errors-C7TI_t_4.js → errors-zTAsW7dS.js} +2 -2
  29. package/dist/{errors-C7TI_t_4.js.map → errors-zTAsW7dS.js.map} +1 -1
  30. package/dist/feeds/paths.cjs.map +1 -1
  31. package/dist/feeds/server/index.cjs +4 -4
  32. package/dist/feeds/server/index.cjs.map +1 -1
  33. package/dist/feeds/server/index.js +4 -4
  34. package/dist/feeds/server/index.js.map +1 -1
  35. package/dist/file-change/index.cjs.map +1 -1
  36. package/dist/files/index.cjs.map +1 -1
  37. package/dist/files/index.js.map +1 -1
  38. package/dist/global-config/index.cjs.map +1 -1
  39. package/dist/google/index.cjs +3 -3
  40. package/dist/google/index.cjs.map +1 -1
  41. package/dist/google/index.js +3 -3
  42. package/dist/google/index.js.map +1 -1
  43. package/dist/graph-CURotEkH.cjs.map +1 -1
  44. package/dist/ingestTypes-DS9HiR2I.cjs.map +1 -1
  45. package/dist/{listen-Dca-NoBi.js → listen-DUlzSOzX.js} +2 -2
  46. package/dist/{listen-Dca-NoBi.js.map → listen-DUlzSOzX.js.map} +1 -1
  47. package/dist/{listen-BXthb-9C.cjs → listen-N_8XdKz_.cjs} +2 -2
  48. package/dist/listen-N_8XdKz_.cjs.map +1 -0
  49. package/dist/notifier/index.cjs +1 -1
  50. package/dist/notifier/index.js +1 -1
  51. package/dist/{notifier-B65Mvngb.cjs → notifier-CiemUClt.cjs} +3 -6
  52. package/dist/notifier-CiemUClt.cjs.map +1 -0
  53. package/dist/{notifier-vAp5t6wL.js → notifier-dgkC-h80.js} +2 -5
  54. package/dist/notifier-dgkC-h80.js.map +1 -0
  55. package/dist/paths-C8-jxR0s.cjs.map +1 -1
  56. package/dist/plugin-vue/i18n.cjs.map +1 -1
  57. package/dist/plugin-vue/index.cjs +82 -16
  58. package/dist/plugin-vue/index.cjs.map +1 -1
  59. package/dist/plugin-vue/index.js +82 -16
  60. package/dist/plugin-vue/index.js.map +1 -1
  61. package/dist/{promptSafety-BGV1wrE6.cjs → promptSafety-auQEatax.cjs} +2 -2
  62. package/dist/promptSafety-auQEatax.cjs.map +1 -0
  63. package/dist/{promptSafety-B5uGroik.js → promptSafety-yG60Vof8.js} +2 -2
  64. package/dist/promptSafety-yG60Vof8.js.map +1 -0
  65. package/dist/relPath-CTAjGdCL.cjs.map +1 -1
  66. package/dist/remote-host/index.cjs +1 -1
  67. package/dist/remote-host/index.js +1 -1
  68. package/dist/remote-host/server/index.cjs +4 -4
  69. package/dist/remote-host/server/index.cjs.map +1 -1
  70. package/dist/remote-host/server/index.js +4 -4
  71. package/dist/remote-host/server/index.js.map +1 -1
  72. package/dist/{remote-host-DhAHBYzN.cjs → remote-host-BMjh_F-6.cjs} +2 -2
  73. package/dist/remote-host-BMjh_F-6.cjs.map +1 -0
  74. package/dist/{remote-host-Cjox2F2J.js → remote-host-CoNX9JEC.js} +2 -2
  75. package/dist/remote-host-CoNX9JEC.js.map +1 -0
  76. package/dist/remote-view/index.cjs +1 -1
  77. package/dist/remote-view/index.cjs.map +1 -1
  78. package/dist/remote-view/index.js +1 -1
  79. package/dist/root-BvwBUcol.cjs.map +1 -1
  80. package/dist/scheduler/index.cjs +2 -2
  81. package/dist/scheduler/index.cjs.map +1 -1
  82. package/dist/scheduler/index.js +2 -2
  83. package/dist/scheduler/index.js.map +1 -1
  84. package/dist/{server-CLhww8NL.cjs → server-RsnFqPt7.cjs} +5 -5
  85. package/dist/server-RsnFqPt7.cjs.map +1 -0
  86. package/dist/{server-CTkTHqEL.js → server-iYXYObKb.js} +5 -5
  87. package/dist/server-iYXYObKb.js.map +1 -0
  88. package/dist/skill-bridge/index.cjs.map +1 -1
  89. package/dist/utils/index.cjs +2 -2
  90. package/dist/utils/index.js +2 -2
  91. package/dist/whisper/index.cjs +2 -2
  92. package/dist/whisper/index.cjs.map +1 -1
  93. package/dist/whisper/index.js +2 -2
  94. package/dist/whisper/index.js.map +1 -1
  95. package/dist/wiki/index.cjs +1 -1
  96. package/dist/wiki/index.cjs.map +1 -1
  97. package/dist/wiki/index.js +1 -1
  98. package/dist/wiki/index.js.map +1 -1
  99. package/dist/wiki/paths.cjs.map +1 -1
  100. package/dist/wiki/server/index.cjs.map +1 -1
  101. package/dist/workspace-setup/index.js.map +1 -1
  102. package/package.json +6 -6
  103. package/dist/discovery-CwBXvtyn.cjs.map +0 -1
  104. package/dist/dist-BzoA9pDR.js.map +0 -1
  105. package/dist/dist-Gj7ygaW2.cjs.map +0 -1
  106. package/dist/errorMessage-BQNpSYdT.js +0 -1
  107. package/dist/errorMessage-CT-va_Pv.cjs +0 -1
  108. package/dist/errors-CdXsfFCF.cjs.map +0 -1
  109. package/dist/listen-BXthb-9C.cjs.map +0 -1
  110. package/dist/notifier-B65Mvngb.cjs.map +0 -1
  111. package/dist/notifier-vAp5t6wL.js.map +0 -1
  112. package/dist/promptSafety-B5uGroik.js.map +0 -1
  113. package/dist/promptSafety-BGV1wrE6.cjs.map +0 -1
  114. package/dist/remote-host-Cjox2F2J.js.map +0 -1
  115. package/dist/remote-host-DhAHBYzN.cjs.map +0 -1
  116. package/dist/server-CLhww8NL.cjs.map +0 -1
  117. package/dist/server-CTkTHqEL.js.map +0 -1
@@ -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 { MinimalLogger } from \"@mulmoclaude/common\";\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 type CollectionWatcherLogger = Pick<MinimalLogger, \"info\" | \"warn\">;\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 *\n * `root` is the absolute project/workspace root the record lives under, and\n * is present ONLY when the engine call carried an explicit `workspaceRoot`\n * — so a single-workspace host never sees it and its targets are unchanged.\n * A multi-root host uses it to deep-link into the RIGHT project: two roots\n * can own the same slug, and a slug-only target would open whichever project\n * happens to be selected. The URL shape is entirely the host's — the engine\n * only guarantees to pass the root through. Resolve it to an OPAQUE project\n * id in the target rather than embedding the absolute path. */\n buildNavigateTarget: (slug: string, itemId: string, root?: 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: {\n legacyId: string;\n slug: string;\n itemId: string;\n priority: CompletionPriority;\n navigateTarget: string;\n /** The record's root, present only under an explicit-root call (see\n * `buildNavigateTarget`). `legacyId` already encodes it; this is the\n * parsed form so a host doesn't have to decode the key. */\n root?: string | undefined;\n }) => 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, isValidCollectionName, itemIsDone, whenMatches, type CollectionItem, type CollectionSchema } from \"../collection\";\nimport {\n canonicalRoot,\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/** Marks a key that carries a root. Placed FIRST so a rooted key can never be\n * mistaken for a slug — `<prefix>@<root>\\0<slug>:<itemId>` vs the original\n * `<prefix><slug>:<itemId>`. NUL cannot occur in a path, a slug or an itemId,\n * so the split is unambiguous. */\nconst ROOT_MARK = \"@\";\nconst ROOT_SEP = \"\\0\";\n\n/** Marks a key that belongs to a SHARED collection (`apps/{aid}/collections/\n * {cid}`) rather than to a directory: `<prefix>#<aid>\\0<cid>:<itemId>`. A\n * shared collection has no root, and its `cid` may well be the slug some\n * project already owns, so it needs a mark of its own — reusing the rootless\n * form would make the two dedupe into one bell. */\nconst SHARED_MARK = \"#\";\n\n/** Stable key encoding (root, 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.\n *\n * COMPATIBILITY RULE — read this before changing the format. Both apps\n * (MulmoClaude and MulmoTerminal) read ONE notifier file,\n * `<ws>/data/notifier/active.json`; they never run simultaneously, but an id\n * written by one must not produce a duplicate bell for the other. So:\n *\n * - with NO root, the id is byte-identical to the pre-multi-root format, and a\n * single-workspace host's existing `active.json` entries keep matching;\n * - a root is included ONLY when one was supplied, which is what lets two\n * roots owning the same slug hold two distinct bells instead of deduping\n * into each other;\n * - `parseCompletionLegacyId` accepts BOTH forms, forever.\n *\n * The root is canonicalised here as well as at the watcher's claim, because\n * this id is written to disk: a caller reaching `reconcileItem` directly with\n * `/proj/` where the watcher used `/proj` would otherwise publish a SECOND\n * bell for the same record, and neither pass would ever clear the other's. */\n/** Exported for the format pin in `test/collection-watchers/test_completionId.ts`.\n * Deliberately NOT on the package barrel: the id is a disk format, not an API. */\nexport function completionLegacyId(slug: string, itemId: string, root?: string, aid?: string): string {\n // The two scopes are mutually exclusive, and silently preferring one is the\n // ambiguity `collectionChangeKey` refuses for the same reason: a local bell\n // written into the shared namespace is SKIPPED by every root's sweep, so it\n // can never be cleared -- and nothing anywhere would say why.\n if (root !== undefined && aid !== undefined) {\n throw new Error(`completionLegacyId: \"${slug}\" carries both a root (${root}) and an app (${aid})`);\n }\n // Both names go through the SAME rule `CollectionKey` applies, because this\n // function takes raw strings and is reachable by a caller that never built a\n // key. Two ways this id breaks otherwise, and both are silent:\n // - the parse splits at the FIRST colon (which is what lets an itemId carry\n // one -- a timestamp, a natural key), so a name of `sales:2026` decodes\n // as slug `sales` with itemId `2026:<id>` and the sweep judges a live\n // bell against the WRONG collection and clears it;\n // - a NUL in the app id splits the scope early, so `salon\\0other` writes an\n // id that reads back as aid `salon`, slug `other\\0tasks`.\n // A malformed shared bell is also unretirable: every root's sweep skips a\n // parsed `aid`, so nothing cleans it up.\n if (!isValidCollectionName(slug)) {\n throw new Error(`completionLegacyId: collection name \"${slug}\" is not a valid collection name`);\n }\n if (aid !== undefined && !isValidCollectionName(aid)) {\n throw new Error(`completionLegacyId: app id \"${aid}\" is not a valid collection name`);\n }\n const scope = aid !== undefined ? `${SHARED_MARK}${aid}${ROOT_SEP}` : root === undefined ? \"\" : `${ROOT_MARK}${canonicalRoot(root)}${ROOT_SEP}`;\n return `${LEGACY_ID_PREFIX}${scope}${slug}:${itemId}`;\n}\n\n/** Decode a key back into its (root, slug, itemId), or null if the string\n * didn't originate from this module. Accepts both the rooted and the\n * original root-less form — see the compatibility rule above. Used by the\n * sweep step. */\n/** @see completionLegacyId — exported for the same reason. */\nexport function parseCompletionLegacyId(legacyId: string): { root?: string; aid?: string; slug: string; itemId: string } | null {\n if (!legacyId.startsWith(LEGACY_ID_PREFIX)) return null;\n let body = legacyId.slice(LEGACY_ID_PREFIX.length);\n let root: string | undefined;\n let aid: string | undefined;\n if (body.startsWith(ROOT_MARK) || body.startsWith(SHARED_MARK)) {\n const shared = body.startsWith(SHARED_MARK);\n const sep = body.indexOf(ROOT_SEP);\n if (sep < 0) return null;\n const scope = body.slice(1, sep);\n // Canonicalised on the way OUT as well as on the way in. Every id this\n // module writes is already canonical, so for its own output this is a\n // no-op -- but the file is read back from disk, and an entry written by\n // hand or by an older build with a trailing separator would otherwise\n // never equal the sweep's canonical root: the verdict would be \"another\n // root's, skip\" and the stale bell could never be cleared by anyone.\n if (shared) aid = scope;\n else root = canonicalRoot(scope);\n body = body.slice(sep + ROOT_SEP.length);\n }\n const colon = body.indexOf(\":\");\n if (colon < 0) return null;\n const parsed = { slug: body.slice(0, colon), itemId: body.slice(colon + 1) };\n if (aid !== undefined) return { aid, ...parsed };\n return root === undefined ? parsed : { root, ...parsed };\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/** WHICH obligation a bell is about — the scope half of its identity.\n *\n * A collection in a directory is scoped by its ROOT: two projects owning a\n * `tasks` each hold their own bells. A SHARED collection is scoped by its APP:\n * the same `(aid, cid, itemId)` is ONE obligation however many checkouts of\n * the repository can see it, so keying it by root would put a duplicate bell\n * in front of the user per worktree, and each root's sweep would manage only\n * its own copy.\n *\n * Mutually exclusive by construction — `bellScopeOf` produces one or the\n * other, and `completionLegacyId` throws on both. */\ninterface BellScope {\n root?: string | undefined;\n aid?: string | undefined;\n}\n\n/** The scope a collection's bells belong to.\n *\n * A shared collection's `appId` wins over the call's `workspaceRoot`: the\n * root is where this host happens to have the repository checked out, which\n * is exactly the fact that must NOT reach the identity. A local collection is\n * unchanged — its root is present only when the call carried an explicit one,\n * so a single-workspace host's ids stay byte-identical to what it has already\n * written to `active.json`. */\nfunction bellScopeOf(collection: Pick<LoadedCollection, \"appId\">, ioOpts: IoOptions): BellScope {\n return collection.appId === undefined ? { root: ioOpts.workspaceRoot } : { aid: collection.appId };\n}\n\nconst scopedLegacyId = (slug: string, itemId: string, scope: BellScope): string => completionLegacyId(slug, itemId, scope.root, scope.aid);\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, scope: BellScope): Promise<NotifierEntry[]> {\n const adapter = requireAdapter();\n const legacyId = scopedLegacyId(slug, itemId, scope);\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, scope: BellScope): Promise<string[]> {\n return (await findActiveEntries(slug, itemId, scope)).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 scope: BellScope,\n): Promise<void> {\n const legacyId = scopedLegacyId(slug, itemId, scope);\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, scope }) };\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, scope: BellScope): Promise<void> {\n const adapter = requireAdapter();\n // The adapter's shape carries a ROOT and nothing else, and its signature is\n // public API (MulmoTerminal implements it). A shared bell therefore hands it\n // no root — which is true: the obligation belongs to an app, not to a\n // checkout, and navigation resolves by slug either way. Only the IDENTITY\n // (`legacyId`) had to learn about apps, which is what the sweep keys on.\n const { root } = scope;\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, root),\n root,\n }),\n });\n }\n}\n\ninterface EnsureInput {\n slug: string;\n schema: CollectionSchema;\n itemId: string;\n legacyId: string;\n displayLabel: string;\n priority: CompletionPriority;\n scope: BellScope;\n}\n\nasync function doEnsureItemNotification({ slug, schema, itemId, legacyId, displayLabel, priority, scope }: EnsureInput): Promise<void> {\n const adapter = requireAdapter();\n // See `reconcileEntrySeverity` for why the adapter gets the root only.\n const { root } = scope;\n try {\n const existing = await findActiveEntries(slug, itemId, scope);\n if (existing.length > 0) {\n await reconcileEntrySeverity(slug, itemId, existing, priority, scope);\n return;\n }\n const navigateTarget = adapter.buildNavigateTarget(slug, itemId, root);\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, root }),\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, root?: string, aid?: string): Promise<void> {\n await clearScoped(slug, itemId, { root, aid });\n}\n\n/** The scoped form every internal caller uses. `clearItemNotification` keeps\n * its positional `(slug, itemId, root?)` shape because it is public API\n * (MulmoTerminal calls it); `aid` is additive. */\nasync function clearScoped(slug: string, itemId: string, scope: BellScope): Promise<void> {\n try {\n const ids = await findActiveEntryIds(slug, itemId, scope);\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 // The bell's root follows the same rule as the change payload's: present\n // only when THIS call carried an explicit `workspaceRoot`, so a\n // single-workspace host's ids stay byte-identical to what it already wrote.\n const scope = bellScopeOf(collection, ioOpts);\n if (!schema.completionField) {\n // Schema doesn't track completion — drop any stale entry.\n await clearScoped(slug, itemId, scope);\n return;\n }\n const item = await storeFor(collection, ioOpts).read(itemId);\n if (item === null) {\n await clearScoped(slug, itemId, scope);\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 clearScoped(slug, itemId, scope);\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 clearScoped(slug, itemId, scope);\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 clearScoped(slug, itemId, scope);\n return;\n }\n await ensureItemNotification(slug, schema, itemId, resolveDisplayLabel(schema, item, itemId), notifyPriorityForItem(schema, item), scope);\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). */\n/** What this sweep should do with an entry, given the root it belongs to and\n * the root the sweep is running for.\n *\n * - `mine` — same root (or both root-less: the single-workspace case, which\n * behaves exactly as it always did). Judge it against the records on disk.\n * - `skip` — ANOTHER root's entry. Its collection and records live in a tree\n * this sweep cannot see, so every check would read \"gone\" and clear a\n * perfectly live bell. Two roots share one notifier file; this is the\n * boundary between them.\n * - `drop-legacy` — a root-less entry seen by a sweep that HAS a root: a\n * leftover from before bell ids carried one. Nothing this host does from now\n * on produces a root-less id, so skipping it would strand a bell no pass can\n * ever clear, while the reconcile publishes a second rooted one beside it.\n * Clearing converges — the record republishes rooted if still pending. */\nfunction sweepVerdict(entry: { root?: string; aid?: string }, ownRoot: string | undefined): \"mine\" | \"skip\" | \"shared\" | \"drop-legacy\" {\n // A shared collection's bell belongs to an APP, not to a root, so no root's\n // sweep can claim it on the strength of its own root. It must not be dropped\n // as a root-less legacy entry either — that would silently clear a live bell.\n //\n // WHO MAY RETIRE ONE, then: a sweep that can resolve THAT app's collection on\n // this host. That check is deferred to `isStaleEntry` (it needs a disk read),\n // which compares the resolved collection's `appId` against the entry's and\n // leaves anything else alone. The rule follows from what the judgement needs\n // rather than from who happens to be running: judging a shared bell means\n // reading its records, and a host without the app cannot — every check would\n // read \"gone\" and clear bells belonging to an app it has never seen.\n if (entry.aid !== undefined) return \"shared\";\n if (entry.root === ownRoot) return \"mine\";\n if (entry.root === undefined) return \"drop-legacy\";\n return \"skip\";\n}\n\n/** Should this entry's bell be dropped? True when the collection is gone, no\n * longer tracks completion, or the record is missing / done / outside\n * `notifyWhen`. The reverse of the reconcile invariant, applied to an entry\n * whose record the forward pass can never walk to (a delete leaves a bell that\n * a walk over the SURVIVING records cannot clear). */\nasync function isStaleEntry(slug: string, itemId: string, opts: DiscoveryOptions, aid?: string): Promise<boolean> {\n const collection = await loadCollection(slug, opts);\n // A SHARED entry is only this sweep's to judge when this host actually has\n // that app's collection. Two ways it isn't, and both must leave the bell\n // alone rather than clear it: the slug resolves to nothing here (another\n // machine's app), or it resolves to a DIFFERENT collection that merely shares\n // the name — a local `tasks` must never retire the shared app's `tasks`.\n if (aid !== undefined && collection?.appId !== aid) return false;\n if (!collection || !collection.schema.completionField) return true;\n return recordNoLongerBells(collection, itemId, opts);\n}\n\n/** The record half of the staleness question: gone, done, or outside\n * `notifyWhen`. Split out so the caller stays one decision — WHOSE entry this\n * is — and this one stays the other. */\nasync function recordNoLongerBells(collection: LoadedCollection, itemId: string, opts: DiscoveryOptions): Promise<boolean> {\n const item = await storeFor(collection, opts).read(itemId);\n return item === null || itemIsDone(collection.schema, item) || !whenMatches(collection.schema.notifyWhen, item);\n}\n\n/** One entry's fate: whose it is (`sweepVerdict`), then — when it is this\n * sweep's to judge — whether its record still justifies the bell. Extracted so\n * the loop above stays a walk and this stays the decision. */\nasync function sweepOneEntry(\n entryId: string,\n parsed: { root?: string; aid?: string; slug: string; itemId: string },\n ownRoot: string | undefined,\n opts: DiscoveryOptions,\n): Promise<void> {\n const verdict = sweepVerdict(parsed, ownRoot);\n if (verdict === \"skip\") return;\n if (verdict === \"drop-legacy\") {\n await notifierClear(entryId);\n return;\n }\n const { slug, itemId } = parsed;\n try {\n if (await isStaleEntry(slug, itemId, opts, verdict === \"shared\" ? parsed.aid : undefined)) await notifierClear(entryId);\n } catch (err) {\n log().warn(\"sweep entry failed\", { slug, itemId, error: errMsg(err) });\n }\n}\n\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 const ownRoot = opts.workspaceRoot === undefined ? undefined : canonicalRoot(opts.workspaceRoot);\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 await sweepOneEntry(entry.id, parsed, ownRoot, opts);\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// FOUR paths can re-derive a collection's bells, and every backend —\n// including read-only `dataSource` — must be covered by all four. They are\n// listed because they were NOT: this module grew up when only JSON records\n// reconciled, so each path had its own `dataSource` short-circuit, and\n// routing every backend through the store contract left the short-circuits\n// behind. Three of the four were separate live bugs (PR #2243 review).\n//\n// 1. mount — `startWatcherFor`, boot + every remount\n// 2. store change — `handleStoreChange`, the backend reported bytes moved\n// 3. schema change — `reconcileChangedSchemas`, the RULES moved instead\n// 4. clock tick — `tickTimeTriggers`, `triggerField` came due\n//\n// 3 and 4 are the ones that look skippable and are not: a read-only backend's\n// rows never change, but the completion rules applied to them and the wall\n// clock both do, and neither produces a data event to react to. Before adding\n// a `dataSource` (or any per-backend) early-exit here, check it against all\n// four — the surviving ones below are view-refresh publishes, not skips.\n//\n// Path 2 exists only for stores that implement `watch`. A store WITHOUT it\n// (today: a shared collection's, until onSnapshot lands) never reports a\n// change at all, so for those the clock tick has to stand in for path 2 as\n// well — a full re-derivation plus a sweep, not just the date-trigger pass\n// (`tickUnwatchedCollections`).\n//\n// ONE GENERATION PER ROOT — the multi-root boundary.\n//\n// Every piece of watcher state (`watchers`, `itemSlots`, `collectionSlots`, the\n// rediscovery + trigger timers, `discoveryOpts`) lives on a `WatcherGeneration`\n// object, and `generations` holds one per root. A host may therefore watch\n// several projects at once; each generation stamps its own root on its change\n// payloads and drives its own bells.\n//\n// It could not always do that, and the reason is worth keeping: the watcher's\n// maps are keyed by slug, but so was the NOTIFICATION identity it drives —\n// `reconciler.ts` derived every bell's `legacyId` from `completionLegacyId(slug,\n// itemId)`. Two roots each owning a `tasks` collection collided on the BELL, not\n// merely on a watcher slot, so re-keying these maps alone would have traded a\n// loud failure for a quiet one. The bell identity now carries the root (and\n// omits it when there is none, so a single-workspace host's ids are unchanged),\n// which is what makes concurrency here correct rather than merely possible.\n//\n// A single-workspace host is unaffected: it starts exactly one generation, for\n// the host's configured root, and everything it observes — payload shape, bell\n// ids, `stop()` semantics — is what it saw before.\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 { access } from \"node:fs/promises\";\nimport {\n canonicalRoot,\n collectionChangePayload,\n sharedCollectionChangePayload,\n discoverCollections,\n firestoreHandle,\n itemFilePath,\n loadCollection,\n peekWorkspaceRoot,\n publishCollectionChange,\n storeFor,\n type CollectionChangePayload,\n type DiscoveryOptions,\n type LoadedCollection,\n type StoreChange,\n} from \"../collection/server\";\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 /** Unsubscribe from the store's change stream. The store owns HOW changes\n * are detected (which paths, which filenames are noise, how an atomic\n * replace is debounced); this module only holds the handle. */\n unsubscribe: () => void;\n /** Last-seen serialized schema, used ONLY as a change-detection\n * fingerprint. When a rediscovery tick observes a different value, the\n * watcher's items are reconciled and the cache is refreshed — this catches\n * schema-only edits (e.g. flipping `completionField` on or off) that don't\n * touch any record file and would otherwise leave bell state stale\n * indefinitely. Never parsed back: `collection.schema` below is the same\n * schema, already typed. */\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), and the typed schema every other read here goes through.\n * Refreshed whenever `schemaJson` is. */\n collection: LoadedCollection;\n}\n\n/** Per-key single-flight slot. */\ninterface ReconcileSlot {\n running: Promise<void>;\n pending: boolean;\n}\n\n/** One root's worth of watcher state. Everything that used to be module-global\n * hangs off here, so two roots can be watched side by side without sharing a\n * map, a timer or a guard. */\ninterface WatcherGeneration {\n /** Discovery options threaded into every `discoverCollections` /\n * `loadCollection` / `sweepStaleActiveEntries` call, and the source of the\n * `root` stamped on this generation's change payloads and bell ids.\n * Deliberately NOT normalized: an omitted `workspaceRoot` must stay omitted,\n * because the payload's `root` means \"an explicit root was passed\". */\n discoveryOpts: DiscoveryOptions;\n watchers: Map<string, CollectionWatcher>;\n itemSlots: Map<string, ReconcileSlot>;\n /** Per-slug single-flight for a COLLECTION-granularity reconcile — a burst\n * of changes the store couldn't attribute to a record collapses into one\n * pass plus one trailing re-run, mirroring the per-item slots. */\n collectionSlots: Map<string, ReconcileSlot>;\n rediscoveryTimer: ReturnType<typeof setInterval> | null;\n triggerTimer: ReturnType<typeof setInterval> | null;\n started: boolean;\n /** Guards the clock tick against overlapping itself. Paired with `alive`: a\n * teardown while a pass is in flight clears it, so that pass's `finally`\n * knows it belongs to a dead generation and must not clear a guard a\n * restarted generation now owns. `triggerTickInFlight` is what teardown\n * AWAITS — clearing the flag alone would let a restart run a second pass\n * alongside the first. */\n triggerTickRunning: boolean;\n triggerTickInFlight: Promise<void> | null;\n /** False from teardown onwards. See `triggerTickRunning`. */\n alive: boolean;\n /** The boot pass, from before its first await until it settles. This is the\n * generation CLAIM: `started` flips only at the end of boot, so it cannot by\n * itself stop two concurrent starts for the same root from racing each\n * other's state. Teardown awaits this too, so a boot in flight can't arm its\n * intervals after the stop that was meant to disarm them. */\n bootInFlight: Promise<void> | null;\n}\n\n/** Live generations, keyed by NORMALIZED root: an omitted `workspaceRoot`\n * resolves to the host's configured default, so `start()` and\n * `start({ workspaceRoot: <that same default> })` join one generation rather\n * than mounting two watchers over the same tree. */\nconst generations = new Map<string, WatcherGeneration>();\n\n/** Map key for a normalized root. `\"\"` stands for \"the host has no configured\n * default and this call named no root\" — a state in which discovery itself\n * would throw, so at most one such generation can meaningfully exist. */\nconst generationKey = (root: string | undefined): string => root ?? \"\";\n\n/** Retained for hosts that catch it. NO LONGER THROWN: starting a second root\n * now mounts a second generation instead of refusing. It stays exported\n * because the refusal was a documented, catchable failure and a host's\n * `.catch` branch for it must keep compiling — and because a future host-side\n * constraint that reintroduces a conflict should reuse this code rather than\n * invent a second one. */\nexport const WATCHER_ROOT_CONFLICT = \"WATCHER_ROOT_CONFLICT\";\n\n/** The root a start request resolves to: its explicit override, else the host's\n * configured default. `peekWorkspaceRoot` rather than `getWorkspaceRoot` — this\n * is a comparison, and under an explicit-root binding there is no default to\n * read, which is not an error here. */\nfunction effectiveRoot(opts: DiscoveryOptions | undefined): string | undefined {\n const root = opts?.workspaceRoot ?? peekWorkspaceRoot() ?? undefined;\n // Canonical, because this value IS the generation's identity — `/work/proj`\n // and `/work/proj/` must not mount two watcher sets over one tree.\n return root === undefined ? undefined : canonicalRoot(root);\n}\n\n/** The discovery options a generation runs under, with an explicit root put in\n * canonical form. Everything downstream — the change payload's `root`, the\n * bell id the reconciler derives — reads it from here, so canonicalising once\n * at the claim is what keeps those three consistent. An omitted root stays\n * omitted: \"no explicit root\" is a distinct state, not a root to normalise. */\nfunction canonicalDiscoveryOpts(opts: DiscoveryOptions | undefined): DiscoveryOptions {\n const base = opts ?? {};\n return base.workspaceRoot === undefined ? base : { ...base, workspaceRoot: canonicalRoot(base.workspaceRoot) };\n}\n\nfunction newGeneration(discoveryOpts: DiscoveryOptions): WatcherGeneration {\n return {\n discoveryOpts,\n watchers: new Map(),\n itemSlots: new Map(),\n collectionSlots: new Map(),\n rediscoveryTimer: null,\n triggerTimer: null,\n started: false,\n triggerTickRunning: false,\n triggerTickInFlight: null,\n alive: true,\n bootInFlight: null,\n };\n}\n\n/** The generation used when NO watcher is running at all. Several tests (and a\n * host that drives a reconcile directly) call the reconcile helpers without a\n * boot; they still need ONE stable set of single-flight slots, or a burst of\n * calls that should coalesce into a single publish would each get their own\n * slot. Created lazily and dropped by a full `stopCollectionWatchers()`. */\nlet detachedGeneration: WatcherGeneration | null = null;\n\nfunction fallbackGeneration(): WatcherGeneration {\n detachedGeneration ??= newGeneration({});\n return detachedGeneration;\n}\n\n/** The generation a test helper or a root-less call should act on: the one\n * named by `workspaceRoot`, else the only one running, else the default\n * root's. \"The only one running\" is what keeps the single-root test helpers\n * (which name no root) working against a generation booted for a tmpdir. */\nfunction resolveGeneration(workspaceRoot?: string): WatcherGeneration | undefined {\n if (workspaceRoot !== undefined) return generations.get(generationKey(workspaceRoot));\n if (generations.size === 1) return [...generations.values()][0];\n return generations.get(generationKey(effectiveRoot(undefined)));\n}\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 for the SAME root — a second call joins the first.\n *\n * A call for a DIFFERENT root mounts a SECOND generation beside the first, so\n * a multi-root host can watch several projects at once. Each generation keeps\n * its own watcher set, timers and single-flight slots, stamps its own root on\n * its change payloads, and drives bells whose ids carry that root — which is\n * what stops two projects' `tasks` collections from deduping into one bell. */\nexport async function startCollectionWatchers(opts: CollectionWatcherOptions = {}): Promise<void> {\n // Claim the generation SYNCHRONOUSLY. `started` only flips after two awaits,\n // so guarding on it alone lets two concurrent callers for the SAME root both\n // pass, mount two watcher sets over one tree, and each arm their own interval\n // — the first of which then escapes teardown, since the generation holds only\n // the last handle. `bootInFlight` is set before the first await, so a\n // concurrent caller sees the claim.\n const key = generationKey(effectiveRoot(opts.discoveryOpts));\n const existing = generations.get(key);\n if (existing) {\n // Join the in-flight boot rather than returning early: a caller that\n // returned here while the first pass was still mounting would believe\n // watchers were up before they were.\n await existing.bootInFlight;\n return;\n }\n const gen = newGeneration(canonicalDiscoveryOpts(opts.discoveryOpts));\n generations.set(key, gen);\n gen.bootInFlight = bootWatchers(key, gen, opts);\n try {\n await gen.bootInFlight;\n } finally {\n gen.bootInFlight = null;\n }\n}\n\n/** The boot pass itself, owned by exactly one caller — `startCollectionWatchers`\n * serialises entry into it. On failure it resets `discoveryOpts` so a\n * supervisor / test harness can retry instead of being permanently latched. */\nasync function bootWatchers(key: string, gen: WatcherGeneration, opts: CollectionWatcherOptions): Promise<void> {\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(gen.discoveryOpts);\n await syncWatchers(gen);\n const intervalMs = opts.rediscoveryIntervalMs === undefined ? REDISCOVERY_INTERVAL_MS : opts.rediscoveryIntervalMs;\n if (intervalMs !== null) {\n gen.rediscoveryTimer = setInterval(() => {\n syncWatchers(gen).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 gen.rediscoveryTimer.unref();\n }\n const triggerMs = opts.triggerTickIntervalMs === undefined ? TRIGGER_TICK_INTERVAL_MS : opts.triggerTickIntervalMs;\n if (triggerMs !== null) {\n gen.triggerTimer = setInterval(() => {\n // Skip rather than overlap. The pass is idempotent and the next one\n // is a minute away, so dropping a tick is harmless — whereas letting\n // firings pile up on a slow pass is not.\n if (gen.triggerTickRunning) return;\n gen.triggerTickRunning = true;\n gen.triggerTickInFlight = tickTimeTriggers(gen)\n .catch((err: unknown) => {\n log().warn(\"watcher trigger tick failed\", { error: errMsg(err) });\n })\n .finally(() => {\n // A teardown that ran while this pass was in flight already cleared\n // the guard; a dead generation must not undo it.\n if (!gen.alive) return;\n gen.triggerTickRunning = false;\n gen.triggerTickInFlight = null;\n });\n }, triggerMs);\n gen.triggerTimer.unref();\n }\n gen.started = true;\n } catch (err) {\n // Drop the claim so a supervisor / test harness can retry instead of being\n // permanently latched on a half-booted generation. Uses the key the CLAIM\n // was made under, not a recomputed one: a root-less claim resolves through\n // `peekWorkspaceRoot()`, so if the host's configured root changed in\n // between, recomputing would delete some other generation and leave this\n // dead one in the map — where a later start would find it, await a null\n // `bootInFlight`, and return believing watchers were up.\n gen.alive = false;\n generations.delete(key);\n throw err;\n }\n}\n\n/** Tear down watchers and stop their intervals, releasing the root claim so a\n * subsequent `startCollectionWatchers` may re-mount.\n *\n * SCOPE: `{ workspaceRoot }` stops exactly that root's generation and leaves\n * every other project running — what a multi-root host wants when it closes\n * one project. Called with NO options it stops ALL generations, which is what\n * it has always meant for a single-workspace host (and for a test teardown\n * that must not leak a timer into the next file).\n *\n * This is a production API, not a test-only one. Await it: it waits out a boot\n * still in flight and a clock pass still running, so the next start does not\n * race the one it replaced. */\nexport async function stopCollectionWatchers(opts?: { workspaceRoot?: string }): Promise<void> {\n if (opts?.workspaceRoot !== undefined) {\n const key = generationKey(effectiveRoot(opts));\n const gen = generations.get(key);\n if (gen) await stopGeneration(key, gen);\n return;\n }\n for (const [key, gen] of [...generations.entries()]) {\n await stopGeneration(key, gen);\n }\n if (opts?.workspaceRoot === undefined) detachedGeneration = null;\n}\n\nasync function stopGeneration(key: string, gen: WatcherGeneration): Promise<void> {\n // Let a boot in flight finish first. Disarming intervals it has not armed\n // yet would leave them running past the teardown that was meant to stop\n // them. A failed boot is fine to swallow here — we are tearing down anyway.\n await gen.bootInFlight?.catch(() => {});\n if (gen.rediscoveryTimer) {\n clearInterval(gen.rediscoveryTimer);\n gen.rediscoveryTimer = null;\n }\n if (gen.triggerTimer) {\n clearInterval(gen.triggerTimer);\n gen.triggerTimer = null;\n }\n // Wait for a clock pass that is still running: the interval is disarmed\n // above, but a pass already in flight keeps touching the notifier and the\n // slot maps, and a restart would otherwise run a second one beside it.\n await gen.triggerTickInFlight;\n gen.triggerTickInFlight = null;\n // Kill the generation AFTER the await: any later `finally` from that pass now\n // sees a dead generation and becomes a no-op, so it can't undo this teardown.\n gen.alive = false;\n gen.triggerTickRunning = false;\n for (const watcher of gen.watchers.values()) {\n try {\n watcher.unsubscribe();\n } catch {\n /* unsubscribe is best-effort */\n }\n }\n gen.watchers.clear();\n // Drain reconcile passes that were already running. The clock tick is awaited\n // above; these are the EVENT-driven ones, and they touch the notifier just\n // the same — a pass still running here lands its write after the caller has\n // moved on, which is how a bell entry ended up in the next test's notifier\n // file (2 where 1 was expected). Unsubscribing first is what bounds this:\n // no new event can extend a slot once the watchers are gone.\n await Promise.allSettled([...gen.itemSlots.values(), ...gen.collectionSlots.values()].map((slot) => slot.running));\n gen.itemSlots.clear();\n gen.collectionSlots.clear();\n gen.started = false;\n generations.delete(key);\n}\n\n/** Test-only: manually trigger one rediscovery + reconcile pass. */\nexport async function _syncWatchersForTesting(workspaceRoot?: string): Promise<boolean> {\n const gen = resolveGeneration(workspaceRoot);\n return gen ? syncWatchers(gen) : false;\n}\n\n/** Test-only: drive one wall-clock tick synchronously, with an optional\n * injected clock. */\nexport async function _tickTimeTriggersForTesting(now?: Date, workspaceRoot?: string): Promise<void> {\n const gen = resolveGeneration(workspaceRoot);\n if (gen) await tickTimeTriggers(gen, 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. Reads the watcher's cached\n * `collection.schema` to avoid a per-tick disk read. */\nasync function tickTimeTriggers(gen: WatcherGeneration, now: Date = evalNow()): Promise<void> {\n for (const entry of gen.watchers.values()) {\n const { schema } = entry.collection;\n // dataSource is NOT excluded. Its rows are read-only, but `triggerField`\n // is not among the keys zod forbids on it, and a trigger date fires from\n // the CLOCK — the one state change that arrives without the file moving.\n // Skipping it here left CSV rows that were pending-but-not-yet-due unable\n // to ever bell unless the file happened to be rewritten.\n //\n // A store that reports no changes is handled wholesale below instead — it\n // needs MORE than this pass, not less, so doing it here too would only\n // duplicate the work.\n if (cannotReportChanges(gen, entry.collection)) continue;\n if (!schema.triggerField && !schema.spawn) continue;\n await reconcileAllItems(entry.collection, gen.discoveryOpts, now);\n }\n await tickUnwatchedCollections(gen, now);\n}\n\n/** True when the collection's store implements no `watch` — nothing will ever\n * tell this module its records moved, so the clock tick is its only change\n * detection. A capability question, deliberately not a backend one: the day a\n * shared collection's store grows an `onSnapshot` watch, it stops being\n * special here with no edit to this file. */\nfunction cannotReportChanges(gen: WatcherGeneration, collection: LoadedCollection): boolean {\n return storeFor(collection, gen.discoveryOpts).watch === undefined;\n}\n\n/** True when a schema declares behaviour that only a reconcile pass can\n * produce: bells (`completionField`), date-triggered bells (`triggerField`),\n * or recurrence successors (`spawn`). */\nfunction needsReconcilePass(schema: LoadedCollection[\"schema\"]): boolean {\n return Boolean(schema.completionField ?? schema.triggerField ?? schema.spawn);\n}\n\n/** One unwatched collection's reconcile pass. Extracted so the single-flight\n * callback doesn't close over loop state.\n *\n * `reconcileAllItems` already swallows a failing store read (it logs and\n * returns), so a closed session or a denied rule surfaces there, not as a\n * rejection here. This catch is only for the unexpected — it must not let one\n * collection's fault abort the rest of the tick. */\nasync function reconcileUnwatched(gen: WatcherGeneration, collection: LoadedCollection, now: Date): Promise<void> {\n try {\n await runSingleFlight(gen.collectionSlots, collection.slug, () => reconcileAllItems(collection, gen.discoveryOpts, now));\n } catch (err) {\n log().warn(\"unwatched collection reconcile failed\", { slug: collection.slug, error: errMsg(err) });\n }\n}\n\n/** Stand in for the store-change path (2) on backends that don't have one.\n *\n * Gated on `needsReconcilePass` so a collection declaring none of it costs\n * nothing: unlike the local backends, every pass here is a network round trip.\n *\n * A collection that DROPS OUT of eligibility (its `completionField` edited\n * away, or the collection removed) is not this pass's problem — `syncWatchers`\n * already sweeps on a changed schema and on a vanished slug, and the sweep is\n * what clears bells the declaration no longer justifies. */\nasync function tickUnwatchedCollections(gen: WatcherGeneration, now: Date): Promise<void> {\n // No session: this tick can learn NOTHING, so it must change nothing.\n // Skipping the reads avoids a warning per collection per minute for a state\n // that can last hours (`reconcileAllItems` logs every failed read).\n // (A shared collection's is the only watch-less store today, and the\n // remote-host session is what its availability means; a second one would want\n // this expressed as a store capability instead of asked here.)\n if (firestoreHandle() === null) return;\n const pending = [...gen.watchers.values()]\n .map((entry) => entry.collection)\n .filter((collection) => cannotReportChanges(gen, collection) && needsReconcilePass(collection.schema));\n if (pending.length === 0) return;\n for (const collection of pending) await reconcileUnwatched(gen, collection, now);\n // A record deleted remotely leaves a stale bell that `reconcileAllItems`\n // (which only walks records that still exist) can't clear — the same pairing\n // `scheduleCollectionReconcile` uses for a watched store.\n await sweepStaleActiveEntries(gen.discoveryOpts);\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(gen: WatcherGeneration): Promise<boolean> {\n let collections;\n try {\n collections = await discoverCollections(gen.discoveryOpts);\n } catch (err) {\n log().warn(\"watcher discover failed\", { error: errMsg(err) });\n return false;\n }\n const liveSlugs = new Set(collections.map((collection) => collection.slug));\n const vanishedMutated = stopVanishedWatchers(gen, liveSlugs);\n const schemaMutated = await reconcileChangedSchemas(gen, collections);\n const addedMutated = await startNewWatchers(gen, collections);\n if (!vanishedMutated && !schemaMutated && !addedMutated) return false;\n await sweepStaleActiveEntries(gen.discoveryOpts);\n return true;\n}\n\nfunction stopVanishedWatchers(gen: WatcherGeneration, liveSlugs: Set<string>): boolean {\n let mutated = false;\n for (const slug of [...gen.watchers.keys()]) {\n if (liveSlugs.has(slug)) continue;\n const watcher = gen.watchers.get(slug);\n if (watcher) {\n try {\n watcher.unsubscribe();\n } catch {\n /* best-effort */\n }\n }\n gen.watchers.delete(slug);\n mutated = true;\n log().info(\"watcher stopped\", { slug });\n }\n return mutated;\n}\n\n/** True when the collection's records moved — a different `dataSource.path`, a\n * different `dataPath`, a flip between the two modes, or a different APP. The\n * mount (an fs.watch, or a Firestore listener) is bound to the OLD location,\n * so it must be remounted, not just re-reconciled.\n *\n * `appId` belongs here even though it is not in the schema: it is resolved\n * separately, from the repository's `app.json`. Point that file at another app\n * and every read and write follows immediately, while a listener armed on the\n * old one keeps running — so the collection would serve the NEW app's records\n * and be woken by the OLD app's changes, forever. */\nfunction backendMoved(previous: LoadedCollection, next: LoadedCollection): boolean {\n return previous.appId !== next.appId || storagePathChanged(previous.schema, next.schema);\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. */\nfunction storagePathChanged(previous: LoadedCollection[\"schema\"], next: LoadedCollection[\"schema\"]): boolean {\n return (\n previous.dataSource?.path !== next.dataSource?.path ||\n previous.dataPath !== next.dataPath ||\n previous.storage?.type !== next.storage?.type ||\n storageFilePath(previous.storage) !== storageFilePath(next.storage)\n );\n}\n\n/** The on-disk path of a storage backend, or undefined when it has none (a\n * shared collection keeps its records off this filesystem, so there is no\n * mount to move — only a `type` change matters for it). */\nfunction storageFilePath(storage: LoadedCollection[\"schema\"][\"storage\"]): string | undefined {\n return storage?.type === \"sqlite\" ? storage.path : undefined;\n}\n\n/** Re-reconcile already-watched collections whose schema changed since\n * the last tick. New collections fall through to `startNewWatchers`. */\n/** Unmount a collection whose records moved. `startNewWatchers` (right after\n * this pass in syncWatchers) remounts it on the new location. A dataSource\n * collection also gets a change ping so open views refetch immediately. */\nfunction dropStaleMount(gen: WatcherGeneration, existing: CollectionWatcher, collection: LoadedCollection): void {\n log().info(\"watcher backend moved, remounting\", { slug: collection.slug });\n try {\n existing.unsubscribe();\n } catch {\n /* best-effort */\n }\n gen.watchers.delete(collection.slug);\n if (collection.schema.dataSource !== undefined) safePublish(gen, collection, { slug: collection.slug, op: \"upsert\" });\n}\n\nasync function reconcileChangedSchemas(gen: WatcherGeneration, collections: readonly LoadedCollection[]): Promise<boolean> {\n let mutated = false;\n for (const collection of collections) {\n const existing = gen.watchers.get(collection.slug);\n if (!existing) continue;\n const nextJson = JSON.stringify(collection.schema);\n // The APP is checked even when the schema is byte-identical: an `app.json`\n // edit changes where the records live without touching a schema file.\n const moved = backendMoved(existing.collection, collection);\n if (existing.schemaJson === nextJson && !moved) continue;\n if (moved) {\n dropStaleMount(gen, existing, collection);\n mutated = true;\n continue;\n }\n existing.schemaJson = nextJson;\n existing.collection = collection;\n log().info(\"watcher schema changed, re-reconciling\", { slug: collection.slug });\n // Completion rules live in the SCHEMA, so a schema-only edit can change\n // which items are pending without any record changing. This runs for\n // every backend including dataSource: those rows are read-only, but the\n // rules applied to them are not. Re-deriving no-ops unless the schema\n // declares `completionField`; a completionField that was REMOVED is the\n // sweep's job, which `mutated` schedules at the end of the tick.\n await reconcileAllItems(collection, gen.discoveryOpts);\n if (collection.schema.dataSource !== undefined) {\n // Read-only rows can't have changed, but a schema edit changes what the\n // views render (fields, displayField, …), so ping them.\n safePublish(gen, collection, { slug: collection.slug, op: \"upsert\" });\n }\n mutated = true;\n }\n return mutated;\n}\n\n/** Mount a watcher for every collection that doesn't have one yet. Returns\n * whether the watcher SET actually changed — the starters report whether\n * they mounted, because ATTEMPTING is not mounting. A start that throws\n * (logged and swallowed inside the starter) leaves `watchers` untouched, so\n * the collection is retried next tick; counting that as a mutation made\n * `syncWatchers` sweep on every tick for as long as the failure persisted. */\nasync function startNewWatchers(gen: WatcherGeneration, collections: readonly LoadedCollection[]): Promise<boolean> {\n let mutated = false;\n for (const collection of collections) {\n if (gen.watchers.has(collection.slug)) continue;\n if (await startWatcherFor(gen, collection)) mutated = true;\n }\n return mutated;\n}\n\n/** Mount one collection's change subscription, whatever its backend.\n *\n * The store decides how to detect a change and at what granularity; this\n * decides what to do about one. That split is the point: adding a backend\n * means implementing `watch` on its store, not another branch here.\n *\n * A store without `watch` cannot report external changes at all — it is\n * still registered (so bells reconcile at boot and on the clock tick), just\n * without live updates. */\nasync function startWatcherFor(gen: WatcherGeneration, collection: LoadedCollection): Promise<boolean> {\n const { slug } = collection;\n try {\n // Boot reconcile BEFORE subscribing: an item that went pending while the\n // server was down needs its bell even if no event ever fires.\n await reconcileAllItems(collection, gen.discoveryOpts);\n const store = storeFor(collection, gen.discoveryOpts);\n const unsubscribe = store.watch\n ? await store.watch((change) => {\n void handleStoreChange(gen, slug, change).catch((err: unknown) => {\n log().warn(\"store change handling failed\", { slug, error: errMsg(err) });\n });\n })\n : () => {};\n // `null` means the backend HAS a watch but could not arm it this time.\n // Registering anyway would mark the slug mounted forever: `startNewWatchers`\n // skips slugs already in `watchers`, so nothing would re-arm it and the\n // collection would serve stale data until a restart. Leave it out and the\n // next sync tick retries — the boot reconcile above is idempotent.\n if (unsubscribe === null) {\n log().warn(\"collection watcher could not arm, retrying next sync\", { slug });\n return false;\n }\n gen.watchers.set(slug, { slug, dataDir: collection.dataDir, unsubscribe, schemaJson: JSON.stringify(collection.schema), collection });\n log().info(\"collection watcher started\", { slug, live: store.watch !== undefined });\n return true;\n } catch (err) {\n log().warn(\"collection watcher start failed\", { slug, error: errMsg(err) });\n return false;\n }\n}\n\n/** React to one reported change. Backend-agnostic by construction — it sees\n * only the granularity the store reported.\n *\n * `item`: reconcile just that record, then publish it. `collection`: the\n * store couldn't say which record, so re-derive everything and pair it with\n * a sweep — a record deleted remotely leaves a bell that a walk over the\n * SURVIVING records can never clear. */\nasync function handleStoreChange(gen: WatcherGeneration, slug: string, change: StoreChange): Promise<void> {\n if (change.kind === \"collection\") {\n await scheduleCollectionReconcile(gen, slug);\n return;\n }\n // Resolve the collection at EVENT time, not at mount time. A schema-only\n // edit (one that leaves the storage location alone, so nothing remounts)\n // refreshes `watchers`' entry in place — a callback closed over the mount-\n // time snapshot would keep reconciling against the old `completionField` /\n // `notifyWhen` / `triggerField` and undo what the schema-change pass had\n // just converged on.\n const current = gen.watchers.get(slug)?.collection;\n if (!current) return; // unmounted between the event and now\n await scheduleItemReconcile(gen, current, change.itemId);\n}\n\n/** Full re-derivation for a collection-granularity change, single-flighted\n * per slug so a burst of writes collapses into one pass plus one trailing\n * re-run. */\nfunction scheduleCollectionReconcile(gen: WatcherGeneration, slug: string): Promise<void> {\n let target: PublishTarget = gen.watchers.get(slug)?.collection ?? { appId: undefined };\n return runSingleFlight(\n gen.collectionSlots,\n slug,\n async () => {\n const collection = await loadCollection(slug, gen.discoveryOpts);\n if (!collection) return;\n target = collection;\n await reconcileAllItems(collection, gen.discoveryOpts);\n // A record deleted underneath us leaves a bell that a walk over the\n // SURVIVING records can never clear — the sweep is the other half.\n await sweepStaleActiveEntries(gen.discoveryOpts);\n },\n // Publish from `onSettled`, i.e. even when the pass above threw: the\n // data changed regardless of whether we managed to re-derive bells from\n // it, and a missed event leaves every open view silently stale. The slot\n // is the coalescing unit, so one burst still yields one publish.\n () => {\n // `target` may still be the mount-time entry (the load above can fail),\n // which is enough: what routes the publish is the APP, and a collection\n // does not change apps without remounting.\n safePublish(gen, target, { slug, op: \"upsert\" });\n return Promise.resolve();\n },\n );\n}\n\n/** Test-only: feed one store-reported change through the same path a live\n * subscription uses, so a test can pin how a change is reacted to without\n * depending on fs.watch timing. */\nexport function _handleStoreChangeForTesting(slug: string, change: StoreChange, workspaceRoot?: string): Promise<void> {\n const gen = resolveGeneration(workspaceRoot);\n return gen ? handleStoreChange(gen, slug, change) : Promise.resolve();\n}\n\n/** Test-only: drive one collection-granularity reconcile directly. */\nexport function _scheduleCollectionReconcileForTesting(slug: string, workspaceRoot?: string): Promise<void> {\n const gen = resolveGeneration(workspaceRoot) ?? fallbackGeneration();\n return scheduleCollectionReconcile(gen, slug);\n}\n\nexport function _scheduleItemReconcileForTesting(collection: LoadedCollection, itemId: string, workspaceRoot?: string): Promise<void> {\n // Falling back to the detached generation keeps the helpers usable with NO\n // watcher running at all — several tests drive the reconcile path directly\n // and never call `startCollectionWatchers`.\n const gen = resolveGeneration(workspaceRoot) ?? fallbackGeneration();\n return scheduleItemReconcile(gen, collection, itemId);\n}\n\nfunction scheduleItemReconcile(gen: WatcherGeneration, collection: LoadedCollection, itemId: string): Promise<void> {\n return runSingleFlight(\n gen.itemSlots,\n `${collection.slug}\\x00${itemId}`,\n () => reconcileItem(collection, itemId, gen.discoveryOpts),\n () => publishItemChange(gen, 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(gen: WatcherGeneration, collection: LoadedCollection, itemId: string): Promise<void> {\n const changeOp = (await recordStillExists(gen, collection, itemId)) ? \"upsert\" : \"delete\";\n safePublish(gen, collection, { slug: collection.slug, ids: [itemId], op: changeOp });\n}\n\n/** Upsert or delete? Answered by whatever actually holds the record.\n *\n * A shared collection's records are Firestore documents — there is no item\n * file, so the file check answers \"gone\" for every one of them and each live\n * change would be published as a DELETE. Asking the store is the question the\n * file check was standing in for; the file path keeps it because it is\n * cheaper and its semantics (an unreadable dir, an id that is not a safe\n * filename) are already settled there. */\nasync function recordStillExists(gen: WatcherGeneration, collection: LoadedCollection, itemId: string): Promise<boolean> {\n if (collection.appId === undefined) return itemFileExists(collection.dataDir, itemId);\n try {\n return (await storeFor(collection, gen.discoveryOpts).read(itemId)) !== null;\n } catch {\n // A closed session or a denied read tells us nothing about the record.\n // \"Still there\" is the safer guess: a spurious delete removes it from\n // every open view, a spurious upsert only costs a refetch.\n return true;\n }\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.\n *\n * Stamps the root the watcher is RUNNING for (`discoveryOpts.workspaceRoot`)\n * rather than deriving one from `collection.dataDir` — a `LoadedCollection`\n * carries only absolute paths, and a root reconstructed by string surgery\n * would be a guess. Undefined in a single-workspace host, which is exactly\n * what the payload contract means by \"the host's configured root\". */\n/** Publish a change on the channel the collection actually lives on.\n *\n * A SHARED collection is not keyed by this checkout: its identity is\n * `(aid, cid)`, and the same collection is open in every clone. Publishing it\n * as `(root, slug)` sends the refresh to a channel no shared subscriber is\n * listening on — the live update simply never arrives, and nothing errors.\n * That is why this takes the collection rather than a bare slug. */\nfunction safePublish(gen: WatcherGeneration, collection: PublishTarget, base: { slug: string; ids?: string[]; op?: \"upsert\" | \"delete\" }): void {\n const enriched: CollectionChangePayload =\n collection.appId === undefined ? collectionChangePayload(base, gen.discoveryOpts.workspaceRoot) : sharedCollectionChangePayload(base, collection.appId);\n try {\n publishCollectionChange(enriched);\n } catch (err) {\n log().warn(\"collection change publish failed\", { slug: enriched.slug, error: errMsg(err) });\n }\n}\n\n/** What `safePublish` needs to know: which app, if any, this belongs to. */\ninterface PublishTarget {\n appId?: string | undefined;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["isValidCollectionName","canonicalRoot","fieldText","listAll","notifierUpdate","notifierPublish","notifierClear","storeFor","maybeSpawnSuccessor","itemIsDone","isTriggerDue","whenMatches","loadCollection","peekWorkspaceRoot","canonicalRoot","storeFor","firestoreHandle","discoverCollections","loadCollection","access","itemFilePath","collectionChangePayload","sharedCollectionChangePayload"],"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 { MinimalLogger } from \"@mulmoclaude/common\";\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 type CollectionWatcherLogger = Pick<MinimalLogger, \"info\" | \"warn\">;\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 *\n * `root` is the absolute project/workspace root the record lives under, and\n * is present ONLY when the engine call carried an explicit `workspaceRoot`\n * — so a single-workspace host never sees it and its targets are unchanged.\n * A multi-root host uses it to deep-link into the RIGHT project: two roots\n * can own the same slug, and a slug-only target would open whichever project\n * happens to be selected. The URL shape is entirely the host's — the engine\n * only guarantees to pass the root through. Resolve it to an OPAQUE project\n * id in the target rather than embedding the absolute path. */\n buildNavigateTarget: (slug: string, itemId: string, root?: 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: {\n legacyId: string;\n slug: string;\n itemId: string;\n priority: CompletionPriority;\n navigateTarget: string;\n /** The record's root, present only under an explicit-root call (see\n * `buildNavigateTarget`). `legacyId` already encodes it; this is the\n * parsed form so a host doesn't have to decode the key. */\n root?: string | undefined;\n }) => 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, isValidCollectionName, itemIsDone, whenMatches, type CollectionItem, type CollectionSchema } from \"../collection\";\nimport {\n canonicalRoot,\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/** Marks a key that carries a root. Placed FIRST so a rooted key can never be\n * mistaken for a slug — `<prefix>@<root>\\0<slug>:<itemId>` vs the original\n * `<prefix><slug>:<itemId>`. NUL cannot occur in a path, a slug or an itemId,\n * so the split is unambiguous. */\nconst ROOT_MARK = \"@\";\nconst ROOT_SEP = \"\\0\";\n\n/** Marks a key that belongs to a SHARED collection (`apps/{aid}/collections/\n * {cid}`) rather than to a directory: `<prefix>#<aid>\\0<cid>:<itemId>`. A\n * shared collection has no root, and its `cid` may well be the slug some\n * project already owns, so it needs a mark of its own — reusing the rootless\n * form would make the two dedupe into one bell. */\nconst SHARED_MARK = \"#\";\n\n/** Stable key encoding (root, 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.\n *\n * COMPATIBILITY RULE — read this before changing the format. Both apps\n * (MulmoClaude and MulmoTerminal) read ONE notifier file,\n * `<ws>/data/notifier/active.json`; they never run simultaneously, but an id\n * written by one must not produce a duplicate bell for the other. So:\n *\n * - with NO root, the id is byte-identical to the pre-multi-root format, and a\n * single-workspace host's existing `active.json` entries keep matching;\n * - a root is included ONLY when one was supplied, which is what lets two\n * roots owning the same slug hold two distinct bells instead of deduping\n * into each other;\n * - `parseCompletionLegacyId` accepts BOTH forms, forever.\n *\n * The root is canonicalised here as well as at the watcher's claim, because\n * this id is written to disk: a caller reaching `reconcileItem` directly with\n * `/proj/` where the watcher used `/proj` would otherwise publish a SECOND\n * bell for the same record, and neither pass would ever clear the other's. */\n/** Exported for the format pin in `test/collection-watchers/test_completionId.ts`.\n * Deliberately NOT on the package barrel: the id is a disk format, not an API. */\nexport function completionLegacyId(slug: string, itemId: string, root?: string, aid?: string): string {\n // The two scopes are mutually exclusive, and silently preferring one is the\n // ambiguity `collectionChangeKey` refuses for the same reason: a local bell\n // written into the shared namespace is SKIPPED by every root's sweep, so it\n // can never be cleared -- and nothing anywhere would say why.\n if (root !== undefined && aid !== undefined) {\n throw new Error(`completionLegacyId: \"${slug}\" carries both a root (${root}) and an app (${aid})`);\n }\n // Both names go through the SAME rule `CollectionKey` applies, because this\n // function takes raw strings and is reachable by a caller that never built a\n // key. Two ways this id breaks otherwise, and both are silent:\n // - the parse splits at the FIRST colon (which is what lets an itemId carry\n // one -- a timestamp, a natural key), so a name of `sales:2026` decodes\n // as slug `sales` with itemId `2026:<id>` and the sweep judges a live\n // bell against the WRONG collection and clears it;\n // - a NUL in the app id splits the scope early, so `salon\\0other` writes an\n // id that reads back as aid `salon`, slug `other\\0tasks`.\n // A malformed shared bell is also unretirable: every root's sweep skips a\n // parsed `aid`, so nothing cleans it up.\n if (!isValidCollectionName(slug)) {\n throw new Error(`completionLegacyId: collection name \"${slug}\" is not a valid collection name`);\n }\n if (aid !== undefined && !isValidCollectionName(aid)) {\n throw new Error(`completionLegacyId: app id \"${aid}\" is not a valid collection name`);\n }\n const scope = aid !== undefined ? `${SHARED_MARK}${aid}${ROOT_SEP}` : root === undefined ? \"\" : `${ROOT_MARK}${canonicalRoot(root)}${ROOT_SEP}`;\n return `${LEGACY_ID_PREFIX}${scope}${slug}:${itemId}`;\n}\n\n/** Decode a key back into its (root, slug, itemId), or null if the string\n * didn't originate from this module. Accepts both the rooted and the\n * original root-less form — see the compatibility rule above. Used by the\n * sweep step. */\n/** @see completionLegacyId — exported for the same reason. */\nexport function parseCompletionLegacyId(legacyId: string): { root?: string; aid?: string; slug: string; itemId: string } | null {\n if (!legacyId.startsWith(LEGACY_ID_PREFIX)) return null;\n let body = legacyId.slice(LEGACY_ID_PREFIX.length);\n let root: string | undefined;\n let aid: string | undefined;\n if (body.startsWith(ROOT_MARK) || body.startsWith(SHARED_MARK)) {\n const shared = body.startsWith(SHARED_MARK);\n const sep = body.indexOf(ROOT_SEP);\n if (sep < 0) return null;\n const scope = body.slice(1, sep);\n // Canonicalised on the way OUT as well as on the way in. Every id this\n // module writes is already canonical, so for its own output this is a\n // no-op -- but the file is read back from disk, and an entry written by\n // hand or by an older build with a trailing separator would otherwise\n // never equal the sweep's canonical root: the verdict would be \"another\n // root's, skip\" and the stale bell could never be cleared by anyone.\n if (shared) aid = scope;\n else root = canonicalRoot(scope);\n body = body.slice(sep + ROOT_SEP.length);\n }\n const colon = body.indexOf(\":\");\n if (colon < 0) return null;\n const parsed = { slug: body.slice(0, colon), itemId: body.slice(colon + 1) };\n if (aid !== undefined) return { aid, ...parsed };\n return root === undefined ? parsed : { root, ...parsed };\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/** WHICH obligation a bell is about — the scope half of its identity.\n *\n * A collection in a directory is scoped by its ROOT: two projects owning a\n * `tasks` each hold their own bells. A SHARED collection is scoped by its APP:\n * the same `(aid, cid, itemId)` is ONE obligation however many checkouts of\n * the repository can see it, so keying it by root would put a duplicate bell\n * in front of the user per worktree, and each root's sweep would manage only\n * its own copy.\n *\n * Mutually exclusive by construction — `bellScopeOf` produces one or the\n * other, and `completionLegacyId` throws on both. */\ninterface BellScope {\n root?: string | undefined;\n aid?: string | undefined;\n}\n\n/** The scope a collection's bells belong to.\n *\n * A shared collection's `appId` wins over the call's `workspaceRoot`: the\n * root is where this host happens to have the repository checked out, which\n * is exactly the fact that must NOT reach the identity. A local collection is\n * unchanged — its root is present only when the call carried an explicit one,\n * so a single-workspace host's ids stay byte-identical to what it has already\n * written to `active.json`. */\nfunction bellScopeOf(collection: Pick<LoadedCollection, \"appId\">, ioOpts: IoOptions): BellScope {\n return collection.appId === undefined ? { root: ioOpts.workspaceRoot } : { aid: collection.appId };\n}\n\nconst scopedLegacyId = (slug: string, itemId: string, scope: BellScope): string => completionLegacyId(slug, itemId, scope.root, scope.aid);\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, scope: BellScope): Promise<NotifierEntry[]> {\n const adapter = requireAdapter();\n const legacyId = scopedLegacyId(slug, itemId, scope);\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, scope: BellScope): Promise<string[]> {\n return (await findActiveEntries(slug, itemId, scope)).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 scope: BellScope,\n): Promise<void> {\n const legacyId = scopedLegacyId(slug, itemId, scope);\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, scope }) };\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, scope: BellScope): Promise<void> {\n const adapter = requireAdapter();\n // The adapter's shape carries a ROOT and nothing else, and its signature is\n // public API (MulmoTerminal implements it). A shared bell therefore hands it\n // no root — which is true: the obligation belongs to an app, not to a\n // checkout, and navigation resolves by slug either way. Only the IDENTITY\n // (`legacyId`) had to learn about apps, which is what the sweep keys on.\n const { root } = scope;\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, root),\n root,\n }),\n });\n }\n}\n\ninterface EnsureInput {\n slug: string;\n schema: CollectionSchema;\n itemId: string;\n legacyId: string;\n displayLabel: string;\n priority: CompletionPriority;\n scope: BellScope;\n}\n\nasync function doEnsureItemNotification({ slug, schema, itemId, legacyId, displayLabel, priority, scope }: EnsureInput): Promise<void> {\n const adapter = requireAdapter();\n // See `reconcileEntrySeverity` for why the adapter gets the root only.\n const { root } = scope;\n try {\n const existing = await findActiveEntries(slug, itemId, scope);\n if (existing.length > 0) {\n await reconcileEntrySeverity(slug, itemId, existing, priority, scope);\n return;\n }\n const navigateTarget = adapter.buildNavigateTarget(slug, itemId, root);\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, root }),\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, root?: string, aid?: string): Promise<void> {\n await clearScoped(slug, itemId, { root, aid });\n}\n\n/** The scoped form every internal caller uses. `clearItemNotification` keeps\n * its positional `(slug, itemId, root?)` shape because it is public API\n * (MulmoTerminal calls it); `aid` is additive. */\nasync function clearScoped(slug: string, itemId: string, scope: BellScope): Promise<void> {\n try {\n const ids = await findActiveEntryIds(slug, itemId, scope);\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 // The bell's root follows the same rule as the change payload's: present\n // only when THIS call carried an explicit `workspaceRoot`, so a\n // single-workspace host's ids stay byte-identical to what it already wrote.\n const scope = bellScopeOf(collection, ioOpts);\n if (!schema.completionField) {\n // Schema doesn't track completion — drop any stale entry.\n await clearScoped(slug, itemId, scope);\n return;\n }\n const item = await storeFor(collection, ioOpts).read(itemId);\n if (item === null) {\n await clearScoped(slug, itemId, scope);\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 clearScoped(slug, itemId, scope);\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 clearScoped(slug, itemId, scope);\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 clearScoped(slug, itemId, scope);\n return;\n }\n await ensureItemNotification(slug, schema, itemId, resolveDisplayLabel(schema, item, itemId), notifyPriorityForItem(schema, item), scope);\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). */\n/** What this sweep should do with an entry, given the root it belongs to and\n * the root the sweep is running for.\n *\n * - `mine` — same root (or both root-less: the single-workspace case, which\n * behaves exactly as it always did). Judge it against the records on disk.\n * - `skip` — ANOTHER root's entry. Its collection and records live in a tree\n * this sweep cannot see, so every check would read \"gone\" and clear a\n * perfectly live bell. Two roots share one notifier file; this is the\n * boundary between them.\n * - `drop-legacy` — a root-less entry seen by a sweep that HAS a root: a\n * leftover from before bell ids carried one. Nothing this host does from now\n * on produces a root-less id, so skipping it would strand a bell no pass can\n * ever clear, while the reconcile publishes a second rooted one beside it.\n * Clearing converges — the record republishes rooted if still pending. */\nfunction sweepVerdict(entry: { root?: string; aid?: string }, ownRoot: string | undefined): \"mine\" | \"skip\" | \"shared\" | \"drop-legacy\" {\n // A shared collection's bell belongs to an APP, not to a root, so no root's\n // sweep can claim it on the strength of its own root. It must not be dropped\n // as a root-less legacy entry either — that would silently clear a live bell.\n //\n // WHO MAY RETIRE ONE, then: a sweep that can resolve THAT app's collection on\n // this host. That check is deferred to `isStaleEntry` (it needs a disk read),\n // which compares the resolved collection's `appId` against the entry's and\n // leaves anything else alone. The rule follows from what the judgement needs\n // rather than from who happens to be running: judging a shared bell means\n // reading its records, and a host without the app cannot — every check would\n // read \"gone\" and clear bells belonging to an app it has never seen.\n if (entry.aid !== undefined) return \"shared\";\n if (entry.root === ownRoot) return \"mine\";\n if (entry.root === undefined) return \"drop-legacy\";\n return \"skip\";\n}\n\n/** Should this entry's bell be dropped? True when the collection is gone, no\n * longer tracks completion, or the record is missing / done / outside\n * `notifyWhen`. The reverse of the reconcile invariant, applied to an entry\n * whose record the forward pass can never walk to (a delete leaves a bell that\n * a walk over the SURVIVING records cannot clear). */\nasync function isStaleEntry(slug: string, itemId: string, opts: DiscoveryOptions, aid?: string): Promise<boolean> {\n const collection = await loadCollection(slug, opts);\n // A SHARED entry is only this sweep's to judge when this host actually has\n // that app's collection. Two ways it isn't, and both must leave the bell\n // alone rather than clear it: the slug resolves to nothing here (another\n // machine's app), or it resolves to a DIFFERENT collection that merely shares\n // the name — a local `tasks` must never retire the shared app's `tasks`.\n if (aid !== undefined && collection?.appId !== aid) return false;\n if (!collection || !collection.schema.completionField) return true;\n return recordNoLongerBells(collection, itemId, opts);\n}\n\n/** The record half of the staleness question: gone, done, or outside\n * `notifyWhen`. Split out so the caller stays one decision — WHOSE entry this\n * is — and this one stays the other. */\nasync function recordNoLongerBells(collection: LoadedCollection, itemId: string, opts: DiscoveryOptions): Promise<boolean> {\n const item = await storeFor(collection, opts).read(itemId);\n return item === null || itemIsDone(collection.schema, item) || !whenMatches(collection.schema.notifyWhen, item);\n}\n\n/** One entry's fate: whose it is (`sweepVerdict`), then — when it is this\n * sweep's to judge — whether its record still justifies the bell. Extracted so\n * the loop above stays a walk and this stays the decision. */\nasync function sweepOneEntry(\n entryId: string,\n parsed: { root?: string; aid?: string; slug: string; itemId: string },\n ownRoot: string | undefined,\n opts: DiscoveryOptions,\n): Promise<void> {\n const verdict = sweepVerdict(parsed, ownRoot);\n if (verdict === \"skip\") return;\n if (verdict === \"drop-legacy\") {\n await notifierClear(entryId);\n return;\n }\n const { slug, itemId } = parsed;\n try {\n if (await isStaleEntry(slug, itemId, opts, verdict === \"shared\" ? parsed.aid : undefined)) await notifierClear(entryId);\n } catch (err) {\n log().warn(\"sweep entry failed\", { slug, itemId, error: errMsg(err) });\n }\n}\n\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 const ownRoot = opts.workspaceRoot === undefined ? undefined : canonicalRoot(opts.workspaceRoot);\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 await sweepOneEntry(entry.id, parsed, ownRoot, opts);\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// FOUR paths can re-derive a collection's bells, and every backend —\n// including read-only `dataSource` — must be covered by all four. They are\n// listed because they were NOT: this module grew up when only JSON records\n// reconciled, so each path had its own `dataSource` short-circuit, and\n// routing every backend through the store contract left the short-circuits\n// behind. Three of the four were separate live bugs (PR #2243 review).\n//\n// 1. mount — `startWatcherFor`, boot + every remount\n// 2. store change — `handleStoreChange`, the backend reported bytes moved\n// 3. schema change — `reconcileChangedSchemas`, the RULES moved instead\n// 4. clock tick — `tickTimeTriggers`, `triggerField` came due\n//\n// 3 and 4 are the ones that look skippable and are not: a read-only backend's\n// rows never change, but the completion rules applied to them and the wall\n// clock both do, and neither produces a data event to react to. Before adding\n// a `dataSource` (or any per-backend) early-exit here, check it against all\n// four — the surviving ones below are view-refresh publishes, not skips.\n//\n// Path 2 exists only for stores that implement `watch`. A store WITHOUT it\n// (today: a shared collection's, until onSnapshot lands) never reports a\n// change at all, so for those the clock tick has to stand in for path 2 as\n// well — a full re-derivation plus a sweep, not just the date-trigger pass\n// (`tickUnwatchedCollections`).\n//\n// ONE GENERATION PER ROOT — the multi-root boundary.\n//\n// Every piece of watcher state (`watchers`, `itemSlots`, `collectionSlots`, the\n// rediscovery + trigger timers, `discoveryOpts`) lives on a `WatcherGeneration`\n// object, and `generations` holds one per root. A host may therefore watch\n// several projects at once; each generation stamps its own root on its change\n// payloads and drives its own bells.\n//\n// It could not always do that, and the reason is worth keeping: the watcher's\n// maps are keyed by slug, but so was the NOTIFICATION identity it drives —\n// `reconciler.ts` derived every bell's `legacyId` from `completionLegacyId(slug,\n// itemId)`. Two roots each owning a `tasks` collection collided on the BELL, not\n// merely on a watcher slot, so re-keying these maps alone would have traded a\n// loud failure for a quiet one. The bell identity now carries the root (and\n// omits it when there is none, so a single-workspace host's ids are unchanged),\n// which is what makes concurrency here correct rather than merely possible.\n//\n// A single-workspace host is unaffected: it starts exactly one generation, for\n// the host's configured root, and everything it observes — payload shape, bell\n// ids, `stop()` semantics — is what it saw before.\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 { access } from \"node:fs/promises\";\nimport {\n canonicalRoot,\n collectionChangePayload,\n sharedCollectionChangePayload,\n discoverCollections,\n firestoreHandle,\n itemFilePath,\n loadCollection,\n peekWorkspaceRoot,\n publishCollectionChange,\n storeFor,\n type CollectionChangePayload,\n type DiscoveryOptions,\n type LoadedCollection,\n type StoreChange,\n} from \"../collection/server\";\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 /** Unsubscribe from the store's change stream. The store owns HOW changes\n * are detected (which paths, which filenames are noise, how an atomic\n * replace is debounced); this module only holds the handle. */\n unsubscribe: () => void;\n /** Last-seen serialized schema, used ONLY as a change-detection\n * fingerprint. When a rediscovery tick observes a different value, the\n * watcher's items are reconciled and the cache is refreshed — this catches\n * schema-only edits (e.g. flipping `completionField` on or off) that don't\n * touch any record file and would otherwise leave bell state stale\n * indefinitely. Never parsed back: `collection.schema` below is the same\n * schema, already typed. */\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), and the typed schema every other read here goes through.\n * Refreshed whenever `schemaJson` is. */\n collection: LoadedCollection;\n}\n\n/** Per-key single-flight slot. */\ninterface ReconcileSlot {\n running: Promise<void>;\n pending: boolean;\n}\n\n/** One root's worth of watcher state. Everything that used to be module-global\n * hangs off here, so two roots can be watched side by side without sharing a\n * map, a timer or a guard. */\ninterface WatcherGeneration {\n /** Discovery options threaded into every `discoverCollections` /\n * `loadCollection` / `sweepStaleActiveEntries` call, and the source of the\n * `root` stamped on this generation's change payloads and bell ids.\n * Deliberately NOT normalized: an omitted `workspaceRoot` must stay omitted,\n * because the payload's `root` means \"an explicit root was passed\". */\n discoveryOpts: DiscoveryOptions;\n watchers: Map<string, CollectionWatcher>;\n itemSlots: Map<string, ReconcileSlot>;\n /** Per-slug single-flight for a COLLECTION-granularity reconcile — a burst\n * of changes the store couldn't attribute to a record collapses into one\n * pass plus one trailing re-run, mirroring the per-item slots. */\n collectionSlots: Map<string, ReconcileSlot>;\n rediscoveryTimer: ReturnType<typeof setInterval> | null;\n triggerTimer: ReturnType<typeof setInterval> | null;\n started: boolean;\n /** Guards the clock tick against overlapping itself. Paired with `alive`: a\n * teardown while a pass is in flight clears it, so that pass's `finally`\n * knows it belongs to a dead generation and must not clear a guard a\n * restarted generation now owns. `triggerTickInFlight` is what teardown\n * AWAITS — clearing the flag alone would let a restart run a second pass\n * alongside the first. */\n triggerTickRunning: boolean;\n triggerTickInFlight: Promise<void> | null;\n /** False from teardown onwards. See `triggerTickRunning`. */\n alive: boolean;\n /** The boot pass, from before its first await until it settles. This is the\n * generation CLAIM: `started` flips only at the end of boot, so it cannot by\n * itself stop two concurrent starts for the same root from racing each\n * other's state. Teardown awaits this too, so a boot in flight can't arm its\n * intervals after the stop that was meant to disarm them. */\n bootInFlight: Promise<void> | null;\n}\n\n/** Live generations, keyed by NORMALIZED root: an omitted `workspaceRoot`\n * resolves to the host's configured default, so `start()` and\n * `start({ workspaceRoot: <that same default> })` join one generation rather\n * than mounting two watchers over the same tree. */\nconst generations = new Map<string, WatcherGeneration>();\n\n/** Map key for a normalized root. `\"\"` stands for \"the host has no configured\n * default and this call named no root\" — a state in which discovery itself\n * would throw, so at most one such generation can meaningfully exist. */\nconst generationKey = (root: string | undefined): string => root ?? \"\";\n\n/** Retained for hosts that catch it. NO LONGER THROWN: starting a second root\n * now mounts a second generation instead of refusing. It stays exported\n * because the refusal was a documented, catchable failure and a host's\n * `.catch` branch for it must keep compiling — and because a future host-side\n * constraint that reintroduces a conflict should reuse this code rather than\n * invent a second one. */\nexport const WATCHER_ROOT_CONFLICT = \"WATCHER_ROOT_CONFLICT\";\n\n/** The root a start request resolves to: its explicit override, else the host's\n * configured default. `peekWorkspaceRoot` rather than `getWorkspaceRoot` — this\n * is a comparison, and under an explicit-root binding there is no default to\n * read, which is not an error here. */\nfunction effectiveRoot(opts: DiscoveryOptions | undefined): string | undefined {\n const root = opts?.workspaceRoot ?? peekWorkspaceRoot() ?? undefined;\n // Canonical, because this value IS the generation's identity — `/work/proj`\n // and `/work/proj/` must not mount two watcher sets over one tree.\n return root === undefined ? undefined : canonicalRoot(root);\n}\n\n/** The discovery options a generation runs under, with an explicit root put in\n * canonical form. Everything downstream — the change payload's `root`, the\n * bell id the reconciler derives — reads it from here, so canonicalising once\n * at the claim is what keeps those three consistent. An omitted root stays\n * omitted: \"no explicit root\" is a distinct state, not a root to normalise. */\nfunction canonicalDiscoveryOpts(opts: DiscoveryOptions | undefined): DiscoveryOptions {\n const base = opts ?? {};\n return base.workspaceRoot === undefined ? base : { ...base, workspaceRoot: canonicalRoot(base.workspaceRoot) };\n}\n\nfunction newGeneration(discoveryOpts: DiscoveryOptions): WatcherGeneration {\n return {\n discoveryOpts,\n watchers: new Map(),\n itemSlots: new Map(),\n collectionSlots: new Map(),\n rediscoveryTimer: null,\n triggerTimer: null,\n started: false,\n triggerTickRunning: false,\n triggerTickInFlight: null,\n alive: true,\n bootInFlight: null,\n };\n}\n\n/** The generation used when NO watcher is running at all. Several tests (and a\n * host that drives a reconcile directly) call the reconcile helpers without a\n * boot; they still need ONE stable set of single-flight slots, or a burst of\n * calls that should coalesce into a single publish would each get their own\n * slot. Created lazily and dropped by a full `stopCollectionWatchers()`. */\nlet detachedGeneration: WatcherGeneration | null = null;\n\nfunction fallbackGeneration(): WatcherGeneration {\n detachedGeneration ??= newGeneration({});\n return detachedGeneration;\n}\n\n/** The generation a test helper or a root-less call should act on: the one\n * named by `workspaceRoot`, else the only one running, else the default\n * root's. \"The only one running\" is what keeps the single-root test helpers\n * (which name no root) working against a generation booted for a tmpdir. */\nfunction resolveGeneration(workspaceRoot?: string): WatcherGeneration | undefined {\n if (workspaceRoot !== undefined) return generations.get(generationKey(workspaceRoot));\n if (generations.size === 1) return [...generations.values()][0];\n return generations.get(generationKey(effectiveRoot(undefined)));\n}\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 for the SAME root — a second call joins the first.\n *\n * A call for a DIFFERENT root mounts a SECOND generation beside the first, so\n * a multi-root host can watch several projects at once. Each generation keeps\n * its own watcher set, timers and single-flight slots, stamps its own root on\n * its change payloads, and drives bells whose ids carry that root — which is\n * what stops two projects' `tasks` collections from deduping into one bell. */\nexport async function startCollectionWatchers(opts: CollectionWatcherOptions = {}): Promise<void> {\n // Claim the generation SYNCHRONOUSLY. `started` only flips after two awaits,\n // so guarding on it alone lets two concurrent callers for the SAME root both\n // pass, mount two watcher sets over one tree, and each arm their own interval\n // — the first of which then escapes teardown, since the generation holds only\n // the last handle. `bootInFlight` is set before the first await, so a\n // concurrent caller sees the claim.\n const key = generationKey(effectiveRoot(opts.discoveryOpts));\n const existing = generations.get(key);\n if (existing) {\n // Join the in-flight boot rather than returning early: a caller that\n // returned here while the first pass was still mounting would believe\n // watchers were up before they were.\n await existing.bootInFlight;\n return;\n }\n const gen = newGeneration(canonicalDiscoveryOpts(opts.discoveryOpts));\n generations.set(key, gen);\n gen.bootInFlight = bootWatchers(key, gen, opts);\n try {\n await gen.bootInFlight;\n } finally {\n gen.bootInFlight = null;\n }\n}\n\n/** The boot pass itself, owned by exactly one caller — `startCollectionWatchers`\n * serialises entry into it. On failure it resets `discoveryOpts` so a\n * supervisor / test harness can retry instead of being permanently latched. */\nasync function bootWatchers(key: string, gen: WatcherGeneration, opts: CollectionWatcherOptions): Promise<void> {\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(gen.discoveryOpts);\n await syncWatchers(gen);\n const intervalMs = opts.rediscoveryIntervalMs === undefined ? REDISCOVERY_INTERVAL_MS : opts.rediscoveryIntervalMs;\n if (intervalMs !== null) {\n gen.rediscoveryTimer = setInterval(() => {\n syncWatchers(gen).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 gen.rediscoveryTimer.unref();\n }\n const triggerMs = opts.triggerTickIntervalMs === undefined ? TRIGGER_TICK_INTERVAL_MS : opts.triggerTickIntervalMs;\n if (triggerMs !== null) {\n gen.triggerTimer = setInterval(() => {\n // Skip rather than overlap. The pass is idempotent and the next one\n // is a minute away, so dropping a tick is harmless — whereas letting\n // firings pile up on a slow pass is not.\n if (gen.triggerTickRunning) return;\n gen.triggerTickRunning = true;\n gen.triggerTickInFlight = tickTimeTriggers(gen)\n .catch((err: unknown) => {\n log().warn(\"watcher trigger tick failed\", { error: errMsg(err) });\n })\n .finally(() => {\n // A teardown that ran while this pass was in flight already cleared\n // the guard; a dead generation must not undo it.\n if (!gen.alive) return;\n gen.triggerTickRunning = false;\n gen.triggerTickInFlight = null;\n });\n }, triggerMs);\n gen.triggerTimer.unref();\n }\n gen.started = true;\n } catch (err) {\n // Drop the claim so a supervisor / test harness can retry instead of being\n // permanently latched on a half-booted generation. Uses the key the CLAIM\n // was made under, not a recomputed one: a root-less claim resolves through\n // `peekWorkspaceRoot()`, so if the host's configured root changed in\n // between, recomputing would delete some other generation and leave this\n // dead one in the map — where a later start would find it, await a null\n // `bootInFlight`, and return believing watchers were up.\n gen.alive = false;\n generations.delete(key);\n throw err;\n }\n}\n\n/** Tear down watchers and stop their intervals, releasing the root claim so a\n * subsequent `startCollectionWatchers` may re-mount.\n *\n * SCOPE: `{ workspaceRoot }` stops exactly that root's generation and leaves\n * every other project running — what a multi-root host wants when it closes\n * one project. Called with NO options it stops ALL generations, which is what\n * it has always meant for a single-workspace host (and for a test teardown\n * that must not leak a timer into the next file).\n *\n * This is a production API, not a test-only one. Await it: it waits out a boot\n * still in flight and a clock pass still running, so the next start does not\n * race the one it replaced. */\nexport async function stopCollectionWatchers(opts?: { workspaceRoot?: string }): Promise<void> {\n if (opts?.workspaceRoot !== undefined) {\n const key = generationKey(effectiveRoot(opts));\n const gen = generations.get(key);\n if (gen) await stopGeneration(key, gen);\n return;\n }\n for (const [key, gen] of [...generations.entries()]) {\n await stopGeneration(key, gen);\n }\n if (opts?.workspaceRoot === undefined) detachedGeneration = null;\n}\n\nasync function stopGeneration(key: string, gen: WatcherGeneration): Promise<void> {\n // Let a boot in flight finish first. Disarming intervals it has not armed\n // yet would leave them running past the teardown that was meant to stop\n // them. A failed boot is fine to swallow here — we are tearing down anyway.\n await gen.bootInFlight?.catch(() => {});\n if (gen.rediscoveryTimer) {\n clearInterval(gen.rediscoveryTimer);\n gen.rediscoveryTimer = null;\n }\n if (gen.triggerTimer) {\n clearInterval(gen.triggerTimer);\n gen.triggerTimer = null;\n }\n // Wait for a clock pass that is still running: the interval is disarmed\n // above, but a pass already in flight keeps touching the notifier and the\n // slot maps, and a restart would otherwise run a second one beside it.\n await gen.triggerTickInFlight;\n gen.triggerTickInFlight = null;\n // Kill the generation AFTER the await: any later `finally` from that pass now\n // sees a dead generation and becomes a no-op, so it can't undo this teardown.\n gen.alive = false;\n gen.triggerTickRunning = false;\n for (const watcher of gen.watchers.values()) {\n try {\n watcher.unsubscribe();\n } catch {\n /* unsubscribe is best-effort */\n }\n }\n gen.watchers.clear();\n // Drain reconcile passes that were already running. The clock tick is awaited\n // above; these are the EVENT-driven ones, and they touch the notifier just\n // the same — a pass still running here lands its write after the caller has\n // moved on, which is how a bell entry ended up in the next test's notifier\n // file (2 where 1 was expected). Unsubscribing first is what bounds this:\n // no new event can extend a slot once the watchers are gone.\n await Promise.allSettled([...gen.itemSlots.values(), ...gen.collectionSlots.values()].map((slot) => slot.running));\n gen.itemSlots.clear();\n gen.collectionSlots.clear();\n gen.started = false;\n generations.delete(key);\n}\n\n/** Test-only: manually trigger one rediscovery + reconcile pass. */\nexport async function _syncWatchersForTesting(workspaceRoot?: string): Promise<boolean> {\n const gen = resolveGeneration(workspaceRoot);\n return gen ? syncWatchers(gen) : false;\n}\n\n/** Test-only: drive one wall-clock tick synchronously, with an optional\n * injected clock. */\nexport async function _tickTimeTriggersForTesting(now?: Date, workspaceRoot?: string): Promise<void> {\n const gen = resolveGeneration(workspaceRoot);\n if (gen) await tickTimeTriggers(gen, 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. Reads the watcher's cached\n * `collection.schema` to avoid a per-tick disk read. */\nasync function tickTimeTriggers(gen: WatcherGeneration, now: Date = evalNow()): Promise<void> {\n for (const entry of gen.watchers.values()) {\n const { schema } = entry.collection;\n // dataSource is NOT excluded. Its rows are read-only, but `triggerField`\n // is not among the keys zod forbids on it, and a trigger date fires from\n // the CLOCK — the one state change that arrives without the file moving.\n // Skipping it here left CSV rows that were pending-but-not-yet-due unable\n // to ever bell unless the file happened to be rewritten.\n //\n // A store that reports no changes is handled wholesale below instead — it\n // needs MORE than this pass, not less, so doing it here too would only\n // duplicate the work.\n if (cannotReportChanges(gen, entry.collection)) continue;\n if (!schema.triggerField && !schema.spawn) continue;\n await reconcileAllItems(entry.collection, gen.discoveryOpts, now);\n }\n await tickUnwatchedCollections(gen, now);\n}\n\n/** True when the collection's store implements no `watch` — nothing will ever\n * tell this module its records moved, so the clock tick is its only change\n * detection. A capability question, deliberately not a backend one: the day a\n * shared collection's store grows an `onSnapshot` watch, it stops being\n * special here with no edit to this file. */\nfunction cannotReportChanges(gen: WatcherGeneration, collection: LoadedCollection): boolean {\n return storeFor(collection, gen.discoveryOpts).watch === undefined;\n}\n\n/** True when a schema declares behaviour that only a reconcile pass can\n * produce: bells (`completionField`), date-triggered bells (`triggerField`),\n * or recurrence successors (`spawn`). */\nfunction needsReconcilePass(schema: LoadedCollection[\"schema\"]): boolean {\n return Boolean(schema.completionField ?? schema.triggerField ?? schema.spawn);\n}\n\n/** One unwatched collection's reconcile pass. Extracted so the single-flight\n * callback doesn't close over loop state.\n *\n * `reconcileAllItems` already swallows a failing store read (it logs and\n * returns), so a closed session or a denied rule surfaces there, not as a\n * rejection here. This catch is only for the unexpected — it must not let one\n * collection's fault abort the rest of the tick. */\nasync function reconcileUnwatched(gen: WatcherGeneration, collection: LoadedCollection, now: Date): Promise<void> {\n try {\n await runSingleFlight(gen.collectionSlots, collection.slug, () => reconcileAllItems(collection, gen.discoveryOpts, now));\n } catch (err) {\n log().warn(\"unwatched collection reconcile failed\", { slug: collection.slug, error: errMsg(err) });\n }\n}\n\n/** Stand in for the store-change path (2) on backends that don't have one.\n *\n * Gated on `needsReconcilePass` so a collection declaring none of it costs\n * nothing: unlike the local backends, every pass here is a network round trip.\n *\n * A collection that DROPS OUT of eligibility (its `completionField` edited\n * away, or the collection removed) is not this pass's problem — `syncWatchers`\n * already sweeps on a changed schema and on a vanished slug, and the sweep is\n * what clears bells the declaration no longer justifies. */\nasync function tickUnwatchedCollections(gen: WatcherGeneration, now: Date): Promise<void> {\n // No session: this tick can learn NOTHING, so it must change nothing.\n // Skipping the reads avoids a warning per collection per minute for a state\n // that can last hours (`reconcileAllItems` logs every failed read).\n // (A shared collection's is the only watch-less store today, and the\n // remote-host session is what its availability means; a second one would want\n // this expressed as a store capability instead of asked here.)\n if (firestoreHandle() === null) return;\n const pending = [...gen.watchers.values()]\n .map((entry) => entry.collection)\n .filter((collection) => cannotReportChanges(gen, collection) && needsReconcilePass(collection.schema));\n if (pending.length === 0) return;\n for (const collection of pending) await reconcileUnwatched(gen, collection, now);\n // A record deleted remotely leaves a stale bell that `reconcileAllItems`\n // (which only walks records that still exist) can't clear — the same pairing\n // `scheduleCollectionReconcile` uses for a watched store.\n await sweepStaleActiveEntries(gen.discoveryOpts);\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(gen: WatcherGeneration): Promise<boolean> {\n let collections;\n try {\n collections = await discoverCollections(gen.discoveryOpts);\n } catch (err) {\n log().warn(\"watcher discover failed\", { error: errMsg(err) });\n return false;\n }\n const liveSlugs = new Set(collections.map((collection) => collection.slug));\n const vanishedMutated = stopVanishedWatchers(gen, liveSlugs);\n const schemaMutated = await reconcileChangedSchemas(gen, collections);\n const addedMutated = await startNewWatchers(gen, collections);\n if (!vanishedMutated && !schemaMutated && !addedMutated) return false;\n await sweepStaleActiveEntries(gen.discoveryOpts);\n return true;\n}\n\nfunction stopVanishedWatchers(gen: WatcherGeneration, liveSlugs: Set<string>): boolean {\n let mutated = false;\n for (const slug of [...gen.watchers.keys()]) {\n if (liveSlugs.has(slug)) continue;\n const watcher = gen.watchers.get(slug);\n if (watcher) {\n try {\n watcher.unsubscribe();\n } catch {\n /* best-effort */\n }\n }\n gen.watchers.delete(slug);\n mutated = true;\n log().info(\"watcher stopped\", { slug });\n }\n return mutated;\n}\n\n/** True when the collection's records moved — a different `dataSource.path`, a\n * different `dataPath`, a flip between the two modes, or a different APP. The\n * mount (an fs.watch, or a Firestore listener) is bound to the OLD location,\n * so it must be remounted, not just re-reconciled.\n *\n * `appId` belongs here even though it is not in the schema: it is resolved\n * separately, from the repository's `app.json`. Point that file at another app\n * and every read and write follows immediately, while a listener armed on the\n * old one keeps running — so the collection would serve the NEW app's records\n * and be woken by the OLD app's changes, forever. */\nfunction backendMoved(previous: LoadedCollection, next: LoadedCollection): boolean {\n return previous.appId !== next.appId || storagePathChanged(previous.schema, next.schema);\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. */\nfunction storagePathChanged(previous: LoadedCollection[\"schema\"], next: LoadedCollection[\"schema\"]): boolean {\n return (\n previous.dataSource?.path !== next.dataSource?.path ||\n previous.dataPath !== next.dataPath ||\n previous.storage?.type !== next.storage?.type ||\n storageFilePath(previous.storage) !== storageFilePath(next.storage)\n );\n}\n\n/** The on-disk path of a storage backend, or undefined when it has none (a\n * shared collection keeps its records off this filesystem, so there is no\n * mount to move — only a `type` change matters for it). */\nfunction storageFilePath(storage: LoadedCollection[\"schema\"][\"storage\"]): string | undefined {\n return storage?.type === \"sqlite\" ? storage.path : undefined;\n}\n\n/** Re-reconcile already-watched collections whose schema changed since\n * the last tick. New collections fall through to `startNewWatchers`. */\n/** Unmount a collection whose records moved. `startNewWatchers` (right after\n * this pass in syncWatchers) remounts it on the new location. A dataSource\n * collection also gets a change ping so open views refetch immediately. */\nfunction dropStaleMount(gen: WatcherGeneration, existing: CollectionWatcher, collection: LoadedCollection): void {\n log().info(\"watcher backend moved, remounting\", { slug: collection.slug });\n try {\n existing.unsubscribe();\n } catch {\n /* best-effort */\n }\n gen.watchers.delete(collection.slug);\n if (collection.schema.dataSource !== undefined) safePublish(gen, collection, { slug: collection.slug, op: \"upsert\" });\n}\n\nasync function reconcileChangedSchemas(gen: WatcherGeneration, collections: readonly LoadedCollection[]): Promise<boolean> {\n let mutated = false;\n for (const collection of collections) {\n const existing = gen.watchers.get(collection.slug);\n if (!existing) continue;\n const nextJson = JSON.stringify(collection.schema);\n // The APP is checked even when the schema is byte-identical: an `app.json`\n // edit changes where the records live without touching a schema file.\n const moved = backendMoved(existing.collection, collection);\n if (existing.schemaJson === nextJson && !moved) continue;\n if (moved) {\n dropStaleMount(gen, existing, collection);\n mutated = true;\n continue;\n }\n existing.schemaJson = nextJson;\n existing.collection = collection;\n log().info(\"watcher schema changed, re-reconciling\", { slug: collection.slug });\n // Completion rules live in the SCHEMA, so a schema-only edit can change\n // which items are pending without any record changing. This runs for\n // every backend including dataSource: those rows are read-only, but the\n // rules applied to them are not. Re-deriving no-ops unless the schema\n // declares `completionField`; a completionField that was REMOVED is the\n // sweep's job, which `mutated` schedules at the end of the tick.\n await reconcileAllItems(collection, gen.discoveryOpts);\n if (collection.schema.dataSource !== undefined) {\n // Read-only rows can't have changed, but a schema edit changes what the\n // views render (fields, displayField, …), so ping them.\n safePublish(gen, collection, { slug: collection.slug, op: \"upsert\" });\n }\n mutated = true;\n }\n return mutated;\n}\n\n/** Mount a watcher for every collection that doesn't have one yet. Returns\n * whether the watcher SET actually changed — the starters report whether\n * they mounted, because ATTEMPTING is not mounting. A start that throws\n * (logged and swallowed inside the starter) leaves `watchers` untouched, so\n * the collection is retried next tick; counting that as a mutation made\n * `syncWatchers` sweep on every tick for as long as the failure persisted. */\nasync function startNewWatchers(gen: WatcherGeneration, collections: readonly LoadedCollection[]): Promise<boolean> {\n let mutated = false;\n for (const collection of collections) {\n if (gen.watchers.has(collection.slug)) continue;\n if (await startWatcherFor(gen, collection)) mutated = true;\n }\n return mutated;\n}\n\n/** Mount one collection's change subscription, whatever its backend.\n *\n * The store decides how to detect a change and at what granularity; this\n * decides what to do about one. That split is the point: adding a backend\n * means implementing `watch` on its store, not another branch here.\n *\n * A store without `watch` cannot report external changes at all — it is\n * still registered (so bells reconcile at boot and on the clock tick), just\n * without live updates. */\nasync function startWatcherFor(gen: WatcherGeneration, collection: LoadedCollection): Promise<boolean> {\n const { slug } = collection;\n try {\n // Boot reconcile BEFORE subscribing: an item that went pending while the\n // server was down needs its bell even if no event ever fires.\n await reconcileAllItems(collection, gen.discoveryOpts);\n const store = storeFor(collection, gen.discoveryOpts);\n const unsubscribe = store.watch\n ? await store.watch((change) => {\n void handleStoreChange(gen, slug, change).catch((err: unknown) => {\n log().warn(\"store change handling failed\", { slug, error: errMsg(err) });\n });\n })\n : () => {};\n // `null` means the backend HAS a watch but could not arm it this time.\n // Registering anyway would mark the slug mounted forever: `startNewWatchers`\n // skips slugs already in `watchers`, so nothing would re-arm it and the\n // collection would serve stale data until a restart. Leave it out and the\n // next sync tick retries — the boot reconcile above is idempotent.\n if (unsubscribe === null) {\n log().warn(\"collection watcher could not arm, retrying next sync\", { slug });\n return false;\n }\n gen.watchers.set(slug, { slug, dataDir: collection.dataDir, unsubscribe, schemaJson: JSON.stringify(collection.schema), collection });\n log().info(\"collection watcher started\", { slug, live: store.watch !== undefined });\n return true;\n } catch (err) {\n log().warn(\"collection watcher start failed\", { slug, error: errMsg(err) });\n return false;\n }\n}\n\n/** React to one reported change. Backend-agnostic by construction — it sees\n * only the granularity the store reported.\n *\n * `item`: reconcile just that record, then publish it. `collection`: the\n * store couldn't say which record, so re-derive everything and pair it with\n * a sweep — a record deleted remotely leaves a bell that a walk over the\n * SURVIVING records can never clear. */\nasync function handleStoreChange(gen: WatcherGeneration, slug: string, change: StoreChange): Promise<void> {\n if (change.kind === \"collection\") {\n await scheduleCollectionReconcile(gen, slug);\n return;\n }\n // Resolve the collection at EVENT time, not at mount time. A schema-only\n // edit (one that leaves the storage location alone, so nothing remounts)\n // refreshes `watchers`' entry in place — a callback closed over the mount-\n // time snapshot would keep reconciling against the old `completionField` /\n // `notifyWhen` / `triggerField` and undo what the schema-change pass had\n // just converged on.\n const current = gen.watchers.get(slug)?.collection;\n if (!current) return; // unmounted between the event and now\n await scheduleItemReconcile(gen, current, change.itemId);\n}\n\n/** Full re-derivation for a collection-granularity change, single-flighted\n * per slug so a burst of writes collapses into one pass plus one trailing\n * re-run. */\nfunction scheduleCollectionReconcile(gen: WatcherGeneration, slug: string): Promise<void> {\n let target: PublishTarget = gen.watchers.get(slug)?.collection ?? { appId: undefined };\n return runSingleFlight(\n gen.collectionSlots,\n slug,\n async () => {\n const collection = await loadCollection(slug, gen.discoveryOpts);\n if (!collection) return;\n target = collection;\n await reconcileAllItems(collection, gen.discoveryOpts);\n // A record deleted underneath us leaves a bell that a walk over the\n // SURVIVING records can never clear — the sweep is the other half.\n await sweepStaleActiveEntries(gen.discoveryOpts);\n },\n // Publish from `onSettled`, i.e. even when the pass above threw: the\n // data changed regardless of whether we managed to re-derive bells from\n // it, and a missed event leaves every open view silently stale. The slot\n // is the coalescing unit, so one burst still yields one publish.\n () => {\n // `target` may still be the mount-time entry (the load above can fail),\n // which is enough: what routes the publish is the APP, and a collection\n // does not change apps without remounting.\n safePublish(gen, target, { slug, op: \"upsert\" });\n return Promise.resolve();\n },\n );\n}\n\n/** Test-only: feed one store-reported change through the same path a live\n * subscription uses, so a test can pin how a change is reacted to without\n * depending on fs.watch timing. */\nexport function _handleStoreChangeForTesting(slug: string, change: StoreChange, workspaceRoot?: string): Promise<void> {\n const gen = resolveGeneration(workspaceRoot);\n return gen ? handleStoreChange(gen, slug, change) : Promise.resolve();\n}\n\n/** Test-only: drive one collection-granularity reconcile directly. */\nexport function _scheduleCollectionReconcileForTesting(slug: string, workspaceRoot?: string): Promise<void> {\n const gen = resolveGeneration(workspaceRoot) ?? fallbackGeneration();\n return scheduleCollectionReconcile(gen, slug);\n}\n\nexport function _scheduleItemReconcileForTesting(collection: LoadedCollection, itemId: string, workspaceRoot?: string): Promise<void> {\n // Falling back to the detached generation keeps the helpers usable with NO\n // watcher running at all — several tests drive the reconcile path directly\n // and never call `startCollectionWatchers`.\n const gen = resolveGeneration(workspaceRoot) ?? fallbackGeneration();\n return scheduleItemReconcile(gen, collection, itemId);\n}\n\nfunction scheduleItemReconcile(gen: WatcherGeneration, collection: LoadedCollection, itemId: string): Promise<void> {\n return runSingleFlight(\n gen.itemSlots,\n `${collection.slug}\\x00${itemId}`,\n () => reconcileItem(collection, itemId, gen.discoveryOpts),\n () => publishItemChange(gen, 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(gen: WatcherGeneration, collection: LoadedCollection, itemId: string): Promise<void> {\n const changeOp = (await recordStillExists(gen, collection, itemId)) ? \"upsert\" : \"delete\";\n safePublish(gen, collection, { slug: collection.slug, ids: [itemId], op: changeOp });\n}\n\n/** Upsert or delete? Answered by whatever actually holds the record.\n *\n * A shared collection's records are Firestore documents — there is no item\n * file, so the file check answers \"gone\" for every one of them and each live\n * change would be published as a DELETE. Asking the store is the question the\n * file check was standing in for; the file path keeps it because it is\n * cheaper and its semantics (an unreadable dir, an id that is not a safe\n * filename) are already settled there. */\nasync function recordStillExists(gen: WatcherGeneration, collection: LoadedCollection, itemId: string): Promise<boolean> {\n if (collection.appId === undefined) return itemFileExists(collection.dataDir, itemId);\n try {\n return (await storeFor(collection, gen.discoveryOpts).read(itemId)) !== null;\n } catch {\n // A closed session or a denied read tells us nothing about the record.\n // \"Still there\" is the safer guess: a spurious delete removes it from\n // every open view, a spurious upsert only costs a refetch.\n return true;\n }\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.\n *\n * Stamps the root the watcher is RUNNING for (`discoveryOpts.workspaceRoot`)\n * rather than deriving one from `collection.dataDir` — a `LoadedCollection`\n * carries only absolute paths, and a root reconstructed by string surgery\n * would be a guess. Undefined in a single-workspace host, which is exactly\n * what the payload contract means by \"the host's configured root\". */\n/** Publish a change on the channel the collection actually lives on.\n *\n * A SHARED collection is not keyed by this checkout: its identity is\n * `(aid, cid)`, and the same collection is open in every clone. Publishing it\n * as `(root, slug)` sends the refresh to a channel no shared subscriber is\n * listening on — the live update simply never arrives, and nothing errors.\n * That is why this takes the collection rather than a bare slug. */\nfunction safePublish(gen: WatcherGeneration, collection: PublishTarget, base: { slug: string; ids?: string[]; op?: \"upsert\" | \"delete\" }): void {\n const enriched: CollectionChangePayload =\n collection.appId === undefined ? collectionChangePayload(base, gen.discoveryOpts.workspaceRoot) : sharedCollectionChangePayload(base, collection.appId);\n try {\n publishCollectionChange(enriched);\n } catch (err) {\n log().warn(\"collection change publish failed\", { slug: enriched.slug, error: errMsg(err) });\n }\n}\n\n/** What `safePublish` needs to know: which app, if any, this belongs to. */\ninterface PublishTarget {\n appId?: string | undefined;\n}\n"],"mappings":"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"}
@@ -1,10 +1,10 @@
1
1
  import { t as canonicalRoot } from "../root-BcS4-Kge.js";
2
2
  import { $ as fieldText, P as isValidCollectionName } from "../itemId-C9s-Jd5s.js";
3
- import { E as whenMatches, f as itemIsDone } from "../promptSafety-B5uGroik.js";
3
+ import { E as whenMatches, f as itemIsDone } from "../promptSafety-yG60Vof8.js";
4
4
  import "../collection/index.js";
5
- import { $ as collectionChangePayload, U as itemFilePath, ct as peekWorkspaceRoot, d as storeFor, mt as sharedCollectionChangePayload, n as discoverCollections, nt as firestoreHandle, r as loadCollection, ut as publishCollectionChange } from "../discovery-XoYJI2Ot.js";
6
- import { h as maybeSpawnSuccessor, m as isTriggerDue } from "../server-CTkTHqEL.js";
7
- import { d as publish, m as updateForPlugin, n as clear, s as listAll } from "../notifier-vAp5t6wL.js";
5
+ import { $ as collectionChangePayload, U as itemFilePath, ct as peekWorkspaceRoot, d as storeFor, mt as sharedCollectionChangePayload, n as discoverCollections, nt as firestoreHandle, r as loadCollection, ut as publishCollectionChange } from "../discovery-CbwHtcwk.js";
6
+ import { h as maybeSpawnSuccessor, m as isTriggerDue } from "../server-iYXYObKb.js";
7
+ import { d as publish, m as updateForPlugin, n as clear, s as listAll } from "../notifier-dgkC-h80.js";
8
8
  import { access } from "node:fs/promises";
9
9
  //#region src/collection-watchers/config.ts
10
10
  var NOOP_LOG = {