@remnic/core 9.22.0 → 9.22.2

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 (402) hide show
  1. package/dist/access-admin-ops-surface.d.ts +15 -12
  2. package/dist/access-admin-ops-surface.js +25 -24
  3. package/dist/access-audit.js +2 -2
  4. package/dist/access-boundary.d.ts +13 -10
  5. package/dist/access-boundary.js +25 -24
  6. package/dist/access-cli.js +84 -82
  7. package/dist/access-cli.js.map +1 -1
  8. package/dist/access-http.d.ts +16 -9
  9. package/dist/access-http.js +33 -32
  10. package/dist/access-identity-continuity-surface.d.ts +9 -7
  11. package/dist/access-identity-continuity-surface.js +25 -24
  12. package/dist/access-lcm-surface.d.ts +12 -9
  13. package/dist/access-lcm-surface.js +25 -24
  14. package/dist/access-mcp-output-schemas.js +1 -1
  15. package/dist/access-mcp.d.ts +22 -10
  16. package/dist/access-mcp.js +31 -30
  17. package/dist/access-memory-search-fanout.d.ts +2 -2
  18. package/dist/{access-namespace-preflight-tmwQgCT6.d.ts → access-namespace-preflight-CmErR03h.d.ts} +1 -1
  19. package/dist/access-namespace-preflight.d.ts +3 -3
  20. package/dist/access-namespace-preflight.js +25 -24
  21. package/dist/access-observe-write-surface.d.ts +12 -9
  22. package/dist/access-observe-write-surface.js +25 -24
  23. package/dist/access-operations-batch.js +27 -26
  24. package/dist/access-operations.d.ts +16 -13
  25. package/dist/access-operations.js +28 -27
  26. package/dist/access-recall-concurrency.d.ts +12 -9
  27. package/dist/access-recall-concurrency.js +25 -24
  28. package/dist/access-recall-response.d.ts +12 -9
  29. package/dist/access-recall-response.js +25 -24
  30. package/dist/access-recall-surface.d.ts +14 -11
  31. package/dist/access-recall-surface.js +25 -24
  32. package/dist/access-schema.d.ts +76 -76
  33. package/dist/{access-service-BtxW52vv.d.ts → access-service-CKEROeFP.d.ts} +22 -11
  34. package/dist/access-service.d.ts +13 -10
  35. package/dist/access-service.js +25 -24
  36. package/dist/access-surface-catalog.d.ts +12 -9
  37. package/dist/access-surface-catalog.js +6 -0
  38. package/dist/access-surface-catalog.js.map +1 -1
  39. package/dist/access-wearables-meetings-surface.d.ts +78 -0
  40. package/dist/access-wearables-meetings-surface.js +22 -0
  41. package/dist/access-wearables-meetings-surface.js.map +1 -0
  42. package/dist/action-confidence.d.ts +1 -1
  43. package/dist/active-memory-bridge.d.ts +1 -1
  44. package/dist/active-recall.d.ts +1 -1
  45. package/dist/active-recall.js +2 -2
  46. package/dist/{auto-sync-UPTUVE4X.js → auto-sync-5OE5QOZB.js} +3 -3
  47. package/dist/behavior-learner.d.ts +1 -1
  48. package/dist/behavior-signals.d.ts +1 -1
  49. package/dist/bootstrap.d.ts +9 -7
  50. package/dist/briefing.d.ts +2 -2
  51. package/dist/briefing.js +5 -5
  52. package/dist/buffer-surprise-report.d.ts +1 -1
  53. package/dist/buffer.d.ts +2 -2
  54. package/dist/bulk-import/index.d.ts +3 -3
  55. package/dist/calibration.d.ts +1 -1
  56. package/dist/capabilities.d.ts +1 -1
  57. package/dist/{catalog-iWvWz61I.d.ts → catalog-BFTVo2Ez.d.ts} +1 -1
  58. package/dist/causal-behavior.d.ts +1 -1
  59. package/dist/causal-behavior.js +2 -2
  60. package/dist/causal-chain.js +2 -2
  61. package/dist/causal-consolidation.d.ts +1 -1
  62. package/dist/causal-consolidation.js +9 -9
  63. package/dist/causal-retrieval.js +2 -2
  64. package/dist/causal-trajectory-graph.d.ts +1 -1
  65. package/dist/causal-trajectory.js +1 -1
  66. package/dist/{chunk-GF22HWS2.js → chunk-2PHX3I5C.js} +7 -7
  67. package/dist/{chunk-JFDEYXF7.js → chunk-3NAHZQU3.js} +2 -2
  68. package/dist/{chunk-XBRPEGXF.js → chunk-3UOSVTFR.js} +5 -5
  69. package/dist/{chunk-TTKWTT72.js → chunk-3ZW6ZQO6.js} +3 -3
  70. package/dist/{chunk-UV72DEZD.js → chunk-4B77XSG5.js} +5 -5
  71. package/dist/{chunk-IC2TCZJ2.js → chunk-57QKLRBV.js} +3 -1
  72. package/dist/chunk-57QKLRBV.js.map +1 -0
  73. package/dist/{chunk-LMHDFOVV.js → chunk-5CPI2QWG.js} +3 -3
  74. package/dist/{chunk-P72WZMWQ.js → chunk-5FZQ74K5.js} +2 -2
  75. package/dist/{chunk-BRT7KEXU.js → chunk-6TWXQ4E2.js} +5 -5
  76. package/dist/{chunk-CV6ZYXK3.js → chunk-7I7AYIUQ.js} +3 -3
  77. package/dist/{chunk-TVVEYCNW.js → chunk-7K5Q6COX.js} +4 -4
  78. package/dist/{chunk-MONWCSVL.js → chunk-7RAJ3GXO.js} +2 -1
  79. package/dist/chunk-7RAJ3GXO.js.map +1 -0
  80. package/dist/{chunk-SQEDDWAM.js → chunk-A2RFKL3V.js} +5 -34
  81. package/dist/chunk-A2RFKL3V.js.map +1 -0
  82. package/dist/{chunk-JN57HZCD.js → chunk-ATEAL5EI.js} +6 -6
  83. package/dist/{chunk-4PYLBXX6.js → chunk-AYAROCWD.js} +2 -2
  84. package/dist/chunk-BLDBT6HZ.js +39 -0
  85. package/dist/chunk-BLDBT6HZ.js.map +1 -0
  86. package/dist/{chunk-NB2P47N7.js → chunk-CUUY5CC4.js} +6 -6
  87. package/dist/{chunk-KL5Y6PEV.js → chunk-E3ZMFFQZ.js} +2 -2
  88. package/dist/{chunk-55FDQYPS.js → chunk-ES5IHQ6M.js} +2 -2
  89. package/dist/{chunk-N47ZWQBO.js → chunk-G2NSSQES.js} +2 -2
  90. package/dist/{chunk-GSWPU5CR.js → chunk-GD3QOBMB.js} +2 -2
  91. package/dist/{chunk-6ANPNUVL.js → chunk-IIXLB5E7.js} +58 -36
  92. package/dist/chunk-IIXLB5E7.js.map +1 -0
  93. package/dist/{chunk-SACPRJ7F.js → chunk-JNIJBXVK.js} +2 -2
  94. package/dist/{chunk-W3XT32W7.js → chunk-JVGUY2AU.js} +23 -13
  95. package/dist/chunk-JVGUY2AU.js.map +1 -0
  96. package/dist/{chunk-DGLON3AL.js → chunk-KNRG6ZGJ.js} +6 -6
  97. package/dist/{chunk-GEUDRENF.js → chunk-MMTG3SCJ.js} +2 -2
  98. package/dist/{chunk-WZFAPHIK.js → chunk-MU7ZHSZE.js} +2 -2
  99. package/dist/{chunk-W4FBGYGK.js → chunk-NRV4Y3PM.js} +1 -1
  100. package/dist/{chunk-QTO2FGBT.js → chunk-OUW4PAY4.js} +191 -117
  101. package/dist/chunk-OUW4PAY4.js.map +1 -0
  102. package/dist/{chunk-KQ2XCQYN.js → chunk-P2BLHUL2.js} +2 -2
  103. package/dist/{chunk-ACGGTPU4.js → chunk-QCKQEBIY.js} +17 -9
  104. package/dist/chunk-QCKQEBIY.js.map +1 -0
  105. package/dist/{chunk-6L775BUB.js → chunk-QE63OHLB.js} +2 -2
  106. package/dist/{chunk-ONHRRJZM.js → chunk-QJAQNXWI.js} +6 -6
  107. package/dist/{chunk-EQYBHXRR.js → chunk-ROUL7RZU.js} +385 -59
  108. package/dist/chunk-ROUL7RZU.js.map +1 -0
  109. package/dist/{chunk-N6M7MRVN.js → chunk-RT4TSR2C.js} +9034 -8726
  110. package/dist/chunk-RT4TSR2C.js.map +1 -0
  111. package/dist/{chunk-6NZ7U4R4.js → chunk-SCOT5DTA.js} +1 -1
  112. package/dist/chunk-SCOT5DTA.js.map +1 -0
  113. package/dist/{chunk-6RYWNBSA.js → chunk-SVZGA2MK.js} +2 -2
  114. package/dist/chunk-SYOIVK7K.js +63 -0
  115. package/dist/chunk-SYOIVK7K.js.map +1 -0
  116. package/dist/{chunk-DM5A4ZFL.js → chunk-TWCOYET2.js} +11 -1
  117. package/dist/chunk-TWCOYET2.js.map +1 -0
  118. package/dist/{chunk-7S5OMI76.js → chunk-WS32QRLO.js} +109 -44
  119. package/dist/chunk-WS32QRLO.js.map +1 -0
  120. package/dist/{chunk-OCMLGND7.js → chunk-WWZ35GWW.js} +5 -5
  121. package/dist/{chunk-QMGIQUBG.js → chunk-XWPFZX6S.js} +503 -139
  122. package/dist/chunk-XWPFZX6S.js.map +1 -0
  123. package/dist/{chunk-A5N6ISRK.js → chunk-ZN4UZWMT.js} +2 -2
  124. package/dist/{chunk-7M6APKJL.js → chunk-ZYU7ZFLT.js} +2 -2
  125. package/dist/{cli-D9vXgWwM.d.ts → cli-BrwT9YMD.d.ts} +5 -5
  126. package/dist/cli.d.ts +14 -11
  127. package/dist/cli.js +57 -56
  128. package/dist/compounding/engine.d.ts +2 -2
  129. package/dist/compounding/engine.js +5 -5
  130. package/dist/compounding/preference-consolidator.d.ts +1 -1
  131. package/dist/compression-optimizer.d.ts +1 -1
  132. package/dist/config.d.ts +1 -1
  133. package/dist/config.js +2 -2
  134. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  135. package/dist/connectors/codex-materialize-runner.js +5 -5
  136. package/dist/connectors/codex-materialize.d.ts +1 -1
  137. package/dist/connectors/index.d.ts +1 -1
  138. package/dist/connectors/index.js +5 -5
  139. package/dist/consolidation-provenance-check.d.ts +2 -2
  140. package/dist/consolidation-undo.d.ts +2 -2
  141. package/dist/contradiction/index.d.ts +2 -2
  142. package/dist/conversation-index/backend.d.ts +1 -1
  143. package/dist/conversation-index/chunker.d.ts +1 -1
  144. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  145. package/dist/conversation-index/indexer.d.ts +1 -1
  146. package/dist/conversation-index/search.d.ts +1 -1
  147. package/dist/day-summary.d.ts +1 -1
  148. package/dist/delinearize.d.ts +1 -1
  149. package/dist/direct-answer-wiring.d.ts +1 -1
  150. package/dist/direct-answer.d.ts +1 -1
  151. package/dist/embedding-fallback.d.ts +1 -1
  152. package/dist/enrichment/index.d.ts +1 -1
  153. package/dist/entity-retrieval.d.ts +2 -2
  154. package/dist/entity-retrieval.js +5 -5
  155. package/dist/entity-schema.d.ts +1 -1
  156. package/dist/explicit-capture.d.ts +9 -7
  157. package/dist/extraction-error-classification.d.ts +22 -0
  158. package/dist/extraction-error-classification.js +11 -0
  159. package/dist/extraction-error-classification.js.map +1 -0
  160. package/dist/extraction-faithfulness.d.ts +1 -1
  161. package/dist/extraction-judge-telemetry.d.ts +1 -1
  162. package/dist/extraction-judge-training.d.ts +1 -1
  163. package/dist/extraction-judge.d.ts +1 -1
  164. package/dist/extraction.d.ts +3 -17
  165. package/dist/extraction.js +5 -3
  166. package/dist/fallback-llm.d.ts +1 -1
  167. package/dist/{first-start-migration-LEXL3XYL.js → first-start-migration-BKNGOFED.js} +4 -4
  168. package/dist/graph-dashboard-diff.d.ts +1 -1
  169. package/dist/graph-dashboard-key.d.ts +1 -1
  170. package/dist/graph-dashboard-parser.d.ts +1 -1
  171. package/dist/graph-edge-reinforcement.d.ts +1 -1
  172. package/dist/graph-snapshot.d.ts +1 -1
  173. package/dist/graph.d.ts +1 -1
  174. package/dist/importance.d.ts +1 -1
  175. package/dist/importers/index.d.ts +1 -1
  176. package/dist/in-flight-reads.d.ts +1 -1
  177. package/dist/index.d.ts +713 -923
  178. package/dist/index.js +176 -140
  179. package/dist/intent.d.ts +1 -1
  180. package/dist/lcm/engine.d.ts +1 -1
  181. package/dist/lcm/engine.js +3 -3
  182. package/dist/lcm/index.d.ts +1 -1
  183. package/dist/lcm/index.js +11 -11
  184. package/dist/lcm/tools.d.ts +1 -1
  185. package/dist/lifecycle.d.ts +1 -1
  186. package/dist/live-connectors-runner.d.ts +1 -1
  187. package/dist/local-llm.d.ts +1 -1
  188. package/dist/local-model-endpoint.d.ts +1 -1
  189. package/dist/maintenance/memory-governance.d.ts +1 -1
  190. package/dist/maintenance/memory-governance.js +6 -6
  191. package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +2 -2
  192. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +5 -5
  193. package/dist/maintenance/rebuild-memory-projection.d.ts +2 -2
  194. package/dist/maintenance/rebuild-memory-projection.js +7 -7
  195. package/dist/maintenance-C98njYb8.d.ts +366 -0
  196. package/dist/mcp-memory-inspector-app.d.ts +12 -9
  197. package/dist/mcp-read-only-tools.js +1 -1
  198. package/dist/memory-action-policy.d.ts +1 -1
  199. package/dist/memory-cache.d.ts +1 -1
  200. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  201. package/dist/memory-projection-store.d.ts +1 -1
  202. package/dist/memory-provenance.d.ts +1 -1
  203. package/dist/memory-worth-outcomes.d.ts +2 -2
  204. package/dist/models-json.d.ts +1 -1
  205. package/dist/namespaces/migrate.d.ts +3 -3
  206. package/dist/namespaces/migrate.js +12 -12
  207. package/dist/namespaces/principal.d.ts +1 -1
  208. package/dist/namespaces/search.d.ts +1 -1
  209. package/dist/namespaces/search.js +6 -6
  210. package/dist/namespaces/storage.d.ts +3 -3
  211. package/dist/namespaces/storage.js +5 -5
  212. package/dist/native-knowledge.d.ts +1 -1
  213. package/dist/offline-sync-impression-drain.d.ts +1 -1
  214. package/dist/operator-toolkit.d.ts +2 -2
  215. package/dist/operator-toolkit.js +18 -18
  216. package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -2
  217. package/dist/orchestration/maintenance.d.ts +6 -223
  218. package/dist/orchestration/maintenance.js +12 -12
  219. package/dist/{orchestrator-DB93S48I.d.ts → orchestrator-C9ZVppNh.d.ts} +12 -481
  220. package/dist/orchestrator.d.ts +10 -8
  221. package/dist/orchestrator.js +84 -82
  222. package/dist/patterns-cli.d.ts +1 -1
  223. package/dist/{pipeline-DmGc-H66.d.ts → pipeline-id_-sqYm.d.ts} +1 -1
  224. package/dist/policy-runtime.d.ts +1 -1
  225. package/dist/proactive-contention.d.ts +1 -1
  226. package/dist/provenance.d.ts +1 -1
  227. package/dist/{qmd-cJT0crkF.d.ts → qmd-R6XaQOZK.d.ts} +1 -1
  228. package/dist/qmd-preflight.d.ts +2 -2
  229. package/dist/qmd-recall-cache.d.ts +1 -1
  230. package/dist/qmd.d.ts +2 -2
  231. package/dist/qmd.js +4 -4
  232. package/dist/recall-concurrency-config.d.ts +1 -1
  233. package/dist/recall-disclosure-escalation.d.ts +1 -1
  234. package/dist/recall-explain-renderer.d.ts +1 -1
  235. package/dist/recall-explain-renderer.js +3 -3
  236. package/dist/recall-memory-map.d.ts +1 -1
  237. package/dist/recall-planner-llm.d.ts +1 -1
  238. package/dist/recall-state.d.ts +1 -1
  239. package/dist/recall-tag-filter.d.ts +1 -1
  240. package/dist/recall-timings.d.ts +1 -1
  241. package/dist/recall-xray-cli.d.ts +1 -1
  242. package/dist/recall-xray-cli.js +4 -4
  243. package/dist/recall-xray-renderer.d.ts +1 -1
  244. package/dist/recall-xray-renderer.js +3 -3
  245. package/dist/recall-xray.d.ts +1 -1
  246. package/dist/recall-xray.js +2 -2
  247. package/dist/replay/normalizers/chatgpt.d.ts +1 -1
  248. package/dist/replay/normalizers/claude.d.ts +1 -1
  249. package/dist/replay/normalizers/openclaw.d.ts +1 -1
  250. package/dist/replay/normalizers/shared.d.ts +1 -1
  251. package/dist/replay/runner.d.ts +1 -1
  252. package/dist/replay/types.d.ts +1 -1
  253. package/dist/resolve-auth-token.d.ts +1 -1
  254. package/dist/resume-bundles.js +4 -4
  255. package/dist/retrieval-agents.d.ts +2 -2
  256. package/dist/retrieval-tiers.d.ts +1 -1
  257. package/dist/routing/engine.d.ts +1 -1
  258. package/dist/routing/store.d.ts +1 -1
  259. package/dist/salvage-envelope.d.ts +1 -1
  260. package/dist/schemas.d.ts +76 -76
  261. package/dist/{scope-profiles-KG2nODX7.d.ts → scope-profiles-EhyuAKkd.d.ts} +1 -1
  262. package/dist/search/embed-helper.d.ts +1 -1
  263. package/dist/search/factory.d.ts +1 -1
  264. package/dist/search/factory.js +5 -5
  265. package/dist/search/index.d.ts +1 -1
  266. package/dist/search/index.js +9 -9
  267. package/dist/search/lancedb-backend.d.ts +1 -1
  268. package/dist/search/meilisearch-backend.d.ts +1 -1
  269. package/dist/search/noop-backend.d.ts +1 -1
  270. package/dist/search/orama-backend.d.ts +1 -1
  271. package/dist/search/port.d.ts +1 -1
  272. package/dist/search/remote-backend.d.ts +1 -1
  273. package/dist/{semantic-consolidation-BaZlg0D8.d.ts → semantic-consolidation-B2xo26UQ.d.ts} +1 -1
  274. package/dist/semantic-consolidation.d.ts +2 -2
  275. package/dist/semantic-consolidation.js +7 -7
  276. package/dist/semantic-rule-promotion.js +5 -5
  277. package/dist/semantic-rule-verifier.d.ts +1 -1
  278. package/dist/semantic-rule-verifier.js +6 -6
  279. package/dist/service-DgFWlZ-r.d.ts +835 -0
  280. package/dist/session-observer-bands.d.ts +1 -1
  281. package/dist/session-observer-state.d.ts +1 -1
  282. package/dist/shared-context/manager.d.ts +9 -9
  283. package/dist/signal.d.ts +1 -1
  284. package/dist/{storage-BKhHHnRo.d.ts → storage-XGawwqXE.d.ts} +131 -2
  285. package/dist/storage.d.ts +2 -2
  286. package/dist/storage.js +4 -4
  287. package/dist/summarizer.d.ts +1 -1
  288. package/dist/summary-snapshot.d.ts +1 -1
  289. package/dist/temporal-supersession.d.ts +2 -2
  290. package/dist/temporal-timeline-recall.d.ts +1 -1
  291. package/dist/temporal-validity.d.ts +1 -1
  292. package/dist/threading.d.ts +1 -1
  293. package/dist/tier-migration.d.ts +2 -2
  294. package/dist/tier-routing.d.ts +1 -1
  295. package/dist/topics.d.ts +1 -1
  296. package/dist/transcript.d.ts +1 -1
  297. package/dist/transfer/types.d.ts +66 -66
  298. package/dist/trust-score-stage.d.ts +1 -1
  299. package/dist/trust-score.d.ts +1 -1
  300. package/dist/{types-GBQZuT7l.d.ts → types-Bbs0qz6b.d.ts} +2 -3
  301. package/dist/types.d.ts +1 -1
  302. package/dist/types.js +1 -1
  303. package/dist/utility-runtime.d.ts +1 -1
  304. package/dist/verified-recall.js +6 -6
  305. package/dist/write-envelope.d.ts +1 -1
  306. package/package.json +2 -2
  307. package/src/access-boundary.ts +3 -0
  308. package/src/access-http.test.ts +63 -0
  309. package/src/access-http.ts +26 -37
  310. package/src/access-mcp-output-schemas.ts +10 -0
  311. package/src/access-mcp.ts +4 -121
  312. package/src/access-operations-batch.ts +10 -5
  313. package/src/access-service.ts +43 -46
  314. package/src/access-surface-catalog.ts +6 -0
  315. package/src/access-wearables-meetings-surface.ts +139 -0
  316. package/src/activity/digest.ts +1 -1
  317. package/src/activity/runner.test.ts +37 -0
  318. package/src/activity/runner.ts +12 -0
  319. package/src/activity/scheduler.test.ts +1 -0
  320. package/src/activity/sync-registration.ts +13 -0
  321. package/src/cli/meetings-commands.ts +70 -0
  322. package/src/cli.ts +7 -6
  323. package/src/extraction-error-classification.ts +52 -0
  324. package/src/extraction.ts +2 -50
  325. package/src/mcp-read-only-tools.ts +2 -0
  326. package/src/meetings/build-memory.test.ts +364 -0
  327. package/src/meetings/build-scheduler.test.ts +55 -0
  328. package/src/meetings/build-scheduler.ts +96 -0
  329. package/src/meetings/day-source.test.ts +275 -0
  330. package/src/meetings/day-source.ts +352 -0
  331. package/src/meetings/http-glue.ts +126 -0
  332. package/src/meetings/index.ts +5 -4
  333. package/src/meetings/manual-sync-flush.test.ts +72 -0
  334. package/src/meetings/mcp-tools.ts +65 -0
  335. package/src/meetings/memory-gen.test.ts +498 -0
  336. package/src/meetings/memory-gen.ts +549 -0
  337. package/src/meetings/memory-generator.ts +5 -0
  338. package/src/meetings/namespace-isolation.test.ts +159 -0
  339. package/src/meetings/service.test.ts +195 -0
  340. package/src/meetings/service.ts +157 -0
  341. package/src/meetings/summary-extractor.test.ts +287 -0
  342. package/src/meetings/summary-extractor.ts +264 -0
  343. package/src/orchestration/maintenance.ts +8 -0
  344. package/src/orchestration/orchestrator-init.ts +6 -1
  345. package/src/orchestration/workspace-ops.ts +172 -8
  346. package/src/orchestrator.ts +12 -13
  347. package/src/storage.ts +21 -0
  348. package/src/testing/subjects/meetings-namespace-harness.ts +159 -0
  349. package/src/testing/subjects/meetings-namespace.test.ts +93 -0
  350. package/src/types.ts +1 -2
  351. package/src/wearables/cli.ts +2 -1
  352. package/src/wearables/index.ts +1 -0
  353. package/src/wearables/mcp-tools.ts +143 -0
  354. package/src/wearables/registry.test.ts +11 -0
  355. package/src/wearables/registry.ts +13 -0
  356. package/src/wearables/service.test.ts +62 -1
  357. package/src/wearables/service.ts +30 -2
  358. package/src/wearables/transcript-search-namespace-isolation.test.ts +98 -0
  359. package/dist/chunk-6ANPNUVL.js.map +0 -1
  360. package/dist/chunk-6NZ7U4R4.js.map +0 -1
  361. package/dist/chunk-7S5OMI76.js.map +0 -1
  362. package/dist/chunk-ACGGTPU4.js.map +0 -1
  363. package/dist/chunk-DM5A4ZFL.js.map +0 -1
  364. package/dist/chunk-EQYBHXRR.js.map +0 -1
  365. package/dist/chunk-IC2TCZJ2.js.map +0 -1
  366. package/dist/chunk-MONWCSVL.js.map +0 -1
  367. package/dist/chunk-N6M7MRVN.js.map +0 -1
  368. package/dist/chunk-QMGIQUBG.js.map +0 -1
  369. package/dist/chunk-QTO2FGBT.js.map +0 -1
  370. package/dist/chunk-SQEDDWAM.js.map +0 -1
  371. package/dist/chunk-W3XT32W7.js.map +0 -1
  372. /package/dist/{auto-sync-UPTUVE4X.js.map → auto-sync-5OE5QOZB.js.map} +0 -0
  373. /package/dist/{chunk-GF22HWS2.js.map → chunk-2PHX3I5C.js.map} +0 -0
  374. /package/dist/{chunk-JFDEYXF7.js.map → chunk-3NAHZQU3.js.map} +0 -0
  375. /package/dist/{chunk-XBRPEGXF.js.map → chunk-3UOSVTFR.js.map} +0 -0
  376. /package/dist/{chunk-TTKWTT72.js.map → chunk-3ZW6ZQO6.js.map} +0 -0
  377. /package/dist/{chunk-UV72DEZD.js.map → chunk-4B77XSG5.js.map} +0 -0
  378. /package/dist/{chunk-LMHDFOVV.js.map → chunk-5CPI2QWG.js.map} +0 -0
  379. /package/dist/{chunk-P72WZMWQ.js.map → chunk-5FZQ74K5.js.map} +0 -0
  380. /package/dist/{chunk-BRT7KEXU.js.map → chunk-6TWXQ4E2.js.map} +0 -0
  381. /package/dist/{chunk-CV6ZYXK3.js.map → chunk-7I7AYIUQ.js.map} +0 -0
  382. /package/dist/{chunk-TVVEYCNW.js.map → chunk-7K5Q6COX.js.map} +0 -0
  383. /package/dist/{chunk-JN57HZCD.js.map → chunk-ATEAL5EI.js.map} +0 -0
  384. /package/dist/{chunk-4PYLBXX6.js.map → chunk-AYAROCWD.js.map} +0 -0
  385. /package/dist/{chunk-NB2P47N7.js.map → chunk-CUUY5CC4.js.map} +0 -0
  386. /package/dist/{chunk-KL5Y6PEV.js.map → chunk-E3ZMFFQZ.js.map} +0 -0
  387. /package/dist/{chunk-55FDQYPS.js.map → chunk-ES5IHQ6M.js.map} +0 -0
  388. /package/dist/{chunk-N47ZWQBO.js.map → chunk-G2NSSQES.js.map} +0 -0
  389. /package/dist/{chunk-GSWPU5CR.js.map → chunk-GD3QOBMB.js.map} +0 -0
  390. /package/dist/{chunk-SACPRJ7F.js.map → chunk-JNIJBXVK.js.map} +0 -0
  391. /package/dist/{chunk-DGLON3AL.js.map → chunk-KNRG6ZGJ.js.map} +0 -0
  392. /package/dist/{chunk-GEUDRENF.js.map → chunk-MMTG3SCJ.js.map} +0 -0
  393. /package/dist/{chunk-WZFAPHIK.js.map → chunk-MU7ZHSZE.js.map} +0 -0
  394. /package/dist/{chunk-W4FBGYGK.js.map → chunk-NRV4Y3PM.js.map} +0 -0
  395. /package/dist/{chunk-KQ2XCQYN.js.map → chunk-P2BLHUL2.js.map} +0 -0
  396. /package/dist/{chunk-6L775BUB.js.map → chunk-QE63OHLB.js.map} +0 -0
  397. /package/dist/{chunk-ONHRRJZM.js.map → chunk-QJAQNXWI.js.map} +0 -0
  398. /package/dist/{chunk-6RYWNBSA.js.map → chunk-SVZGA2MK.js.map} +0 -0
  399. /package/dist/{chunk-OCMLGND7.js.map → chunk-WWZ35GWW.js.map} +0 -0
  400. /package/dist/{chunk-A5N6ISRK.js.map → chunk-ZN4UZWMT.js.map} +0 -0
  401. /package/dist/{chunk-7M6APKJL.js.map → chunk-ZYU7ZFLT.js.map} +0 -0
  402. /package/dist/{first-start-migration-LEXL3XYL.js.map → first-start-migration-BKNGOFED.js.map} +0 -0
@@ -0,0 +1,364 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+
4
+ import { MeetingsBuilder, type MeetingDayData, type MeetingsDayBuildSummary } from "./build.js";
5
+ import { DEFAULT_MEETINGS_CONFIG } from "./config.js";
6
+ import { MeetingRecordStore, type MeetingRecordFileIo } from "./store.js";
7
+ import type { MeetingsConfig, MeetingAppSpan, MeetingAudioWindow } from "./types.js";
8
+ import type { FusionConversationInput, FusionSegmentInput } from "../wearables/fusion/types.js";
9
+ import {
10
+ createMeetingMemoryGenerator,
11
+ meetingSourceLabel,
12
+ type MeetingFactCandidate,
13
+ type MeetingSummaryExtractor,
14
+ } from "./memory-gen.js";
15
+ import type { MeetingMemoryWriter } from "./memory-generator.js";
16
+ import type { SealedMemoryEnvelope } from "../write-envelope.js";
17
+ import type { MemoryWriteResult } from "../storage.js";
18
+
19
+ // Integration coverage for the builder driving the CONCRETE memory generator
20
+ // (createMeetingMemoryGenerator) through the seam — migrated out of the engine
21
+ // build.test.ts (issue #1900 re-slice). The engine test asserts SEAM contract
22
+ // with an inline fake; these tests assert the real memory-generation BEHAVIOR
23
+ // (episode idempotency, retract-on-delete, and smart/review/off trust-gating)
24
+ // end to end through MeetingsBuilder.
25
+
26
+ const MEMORY_DIR = "/mem";
27
+ const DATE = "2026-03-10";
28
+ const START = "2026-03-10T14:00:00.000Z";
29
+ const END = "2026-03-10T15:00:00.000Z";
30
+
31
+ class InMemoryIo implements MeetingRecordFileIo {
32
+ files = new Map<string, string>();
33
+ async writeFile(p: string, c: string): Promise<void> {
34
+ this.files.set(p, c);
35
+ }
36
+ async readFile(p: string): Promise<string> {
37
+ const v = this.files.get(p);
38
+ if (v === undefined) throw enoent();
39
+ return v;
40
+ }
41
+ async readDir(dirPath: string): Promise<string[]> {
42
+ const prefix = dirPath.endsWith("/") ? dirPath : `${dirPath}/`;
43
+ const names = new Set<string>();
44
+ let found = false;
45
+ for (const key of this.files.keys()) {
46
+ if (!key.startsWith(prefix)) continue;
47
+ found = true;
48
+ names.add(key.slice(prefix.length).split("/")[0]!);
49
+ }
50
+ if (!found) throw enoent();
51
+ return [...names];
52
+ }
53
+ async deleteFile(p: string): Promise<void> {
54
+ if (!this.files.delete(p)) throw enoent();
55
+ }
56
+ async realpath(p: string): Promise<string> {
57
+ return p;
58
+ }
59
+ async lstat(): Promise<{ isSymbolicLink: boolean }> {
60
+ return { isSymbolicLink: false };
61
+ }
62
+ }
63
+
64
+ function enoent(): NodeJS.ErrnoException {
65
+ const err = new Error("ENOENT") as NodeJS.ErrnoException;
66
+ err.code = "ENOENT";
67
+ return err;
68
+ }
69
+
70
+ /** Fake meeting memory writer modelling source-scoped dedup + retire. */
71
+ class FakeMeetingWriter implements MeetingMemoryWriter {
72
+ writes: Array<{ source: string; content?: string; status?: string; category: string; meetingId?: string }> = [];
73
+ retired: string[] = [];
74
+ private bySource = new Map<string, Set<string>>();
75
+
76
+ async writeSealedMemory(
77
+ envelope: SealedMemoryEnvelope,
78
+ extras: { contentHashSource?: string; status?: string },
79
+ ): Promise<MemoryWriteResult> {
80
+ this.writes.push({
81
+ source: envelope.source,
82
+ ...(extras.contentHashSource !== undefined ? { content: extras.contentHashSource } : {}),
83
+ ...(extras.status !== undefined ? { status: extras.status } : {}),
84
+ category: envelope.category,
85
+ ...(envelope.rawStructuredAttributes?.meetingId !== undefined
86
+ ? { meetingId: envelope.rawStructuredAttributes.meetingId }
87
+ : {}),
88
+ });
89
+ if (extras.contentHashSource !== undefined) {
90
+ const set = this.bySource.get(envelope.source) ?? new Set<string>();
91
+ set.add(extras.contentHashSource);
92
+ this.bySource.set(envelope.source, set);
93
+ }
94
+ return { id: `m${this.writes.length}`, path: `mem/m${this.writes.length}.md` } as unknown as MemoryWriteResult;
95
+ }
96
+ async hasMemoryFromSource(source: string, content: string): Promise<boolean> {
97
+ return this.bySource.get(source)?.has(content) ?? false;
98
+ }
99
+ async retireMemoriesFromSource(source: string): Promise<number> {
100
+ this.retired.push(source);
101
+ const count = this.bySource.get(source)?.size ?? 0;
102
+ this.bySource.delete(source);
103
+ return count;
104
+ }
105
+ }
106
+
107
+ function spyExtractor(
108
+ summary: string,
109
+ candidates: MeetingFactCandidate[],
110
+ ): MeetingSummaryExtractor & { calls: number } {
111
+ return {
112
+ calls: 0,
113
+ async extract() {
114
+ this.calls++;
115
+ return { summary, candidates };
116
+ },
117
+ };
118
+ }
119
+
120
+ function config(overrides: Partial<MeetingsConfig> = {}): MeetingsConfig {
121
+ return {
122
+ ...DEFAULT_MEETINGS_CONFIG,
123
+ appPatterns: [...DEFAULT_MEETINGS_CONFIG.appPatterns],
124
+ enabled: true,
125
+ ...overrides,
126
+ };
127
+ }
128
+
129
+ function fixedSource(data: MeetingDayData) {
130
+ return { loadDayData: () => data };
131
+ }
132
+
133
+ function appSpan(app: string, start: string, end: string): MeetingAppSpan {
134
+ return { app, startUtc: start, endUtc: end };
135
+ }
136
+
137
+ function audioWin(
138
+ source: string,
139
+ start: string,
140
+ end: string,
141
+ overrides: Partial<MeetingAudioWindow> = {},
142
+ ): MeetingAudioWindow {
143
+ return { source, startUtc: start, endUtc: end, distinctNonWearerSpeakers: 2, ...overrides };
144
+ }
145
+
146
+ function seg(text: string, startIso: string): FusionSegmentInput {
147
+ return { speaker: "Jane", isSelf: false, text, startIso };
148
+ }
149
+
150
+ function conv(source: string, id: string, segments: FusionSegmentInput[]): FusionConversationInput {
151
+ return { source, conversationId: id, startIso: START, endIso: END, segments };
152
+ }
153
+
154
+ test("builder writes a deterministic episode memory per built record when a writer is injected", async () => {
155
+ const store = new MeetingRecordStore(MEMORY_DIR, new InMemoryIo());
156
+ const memoryWriter = new FakeMeetingWriter();
157
+ const data: MeetingDayData = {
158
+ detection: { date: DATE, appSpans: [appSpan("Zoom", START, END)], audioWindows: [audioWin("desktop", START, END)] },
159
+ conversations: [conv("desktop", "d1", [seg("hello", "2026-03-10T14:05:00.000Z")])],
160
+ };
161
+ const cfg = config();
162
+ const builder = new MeetingsBuilder({
163
+ source: fixedSource(data),
164
+ store,
165
+ config: cfg,
166
+ memoryGenerator: createMeetingMemoryGenerator(memoryWriter, cfg),
167
+ });
168
+ const first = await builder.buildDay(DATE);
169
+ assert.equal(first.meetings.length, 1);
170
+ assert.deepEqual(first.episodes, { written: 1, skipped: 0 });
171
+ assert.equal(memoryWriter.writes.length, 1);
172
+ // Rebuild with an unchanged record: the memory layer skips it entirely
173
+ // (round 2) — no episode probe, no second write, no episode aggregate.
174
+ const second = await builder.buildDay(DATE);
175
+ assert.equal(second.episodes, undefined, "an unchanged rebuild generates nothing");
176
+ assert.equal(memoryWriter.writes.length, 1);
177
+ });
178
+
179
+ test("finding 2 — an unchanged-record rebuild fires zero LLM calls and writes nothing", async () => {
180
+ const store = new MeetingRecordStore(MEMORY_DIR, new InMemoryIo());
181
+ const memoryWriter = new FakeMeetingWriter();
182
+ const extractor = spyExtractor("We shipped v2.", SUMMARY_CANDIDATES);
183
+ const cfg = config({ summaryMode: "smart" });
184
+ const calls: MeetingsDayBuildSummary[] = [];
185
+ const build = (): Promise<MeetingsDayBuildSummary> =>
186
+ new MeetingsBuilder({
187
+ source: fixedSource(summaryData()),
188
+ store,
189
+ config: cfg,
190
+ memoryGenerator: createMeetingMemoryGenerator(memoryWriter, cfg, { extractor }),
191
+ hooks: { reindex: (s) => { calls.push(s); } },
192
+ }).buildDay(DATE);
193
+
194
+ const first = await build();
195
+ assert.equal(first.built, 1, "the first build persists the record");
196
+ assert.equal(extractor.calls, 1, "the extractor runs once on the first build");
197
+ const writesAfterFirst = memoryWriter.writes.length;
198
+ assert.ok(writesAfterFirst > 0, "the first build writes memories");
199
+ assert.equal(calls.length, 1, "reindex fires on the first build");
200
+
201
+ // Rebuild with byte-identical data: the record is unchanged, so the memory
202
+ // layer must skip it entirely — no extractor call, no rewrite, no reindex
203
+ // (round 2 — the LLM must never re-run on an idempotent rebuild).
204
+ const second = await build();
205
+ assert.equal(second.built, 0, "the record was unchanged");
206
+ assert.equal(extractor.calls, 1, "no extractor call on an unchanged rebuild");
207
+ assert.equal(memoryWriter.writes.length, writesAfterFirst, "no memory was written on the rebuild");
208
+ assert.equal(second.episodes, undefined, "no episode generation on an unchanged rebuild");
209
+ assert.equal(second.facts, undefined, "no fact generation on an unchanged rebuild");
210
+ assert.equal(calls.length, 1, "no reindex on a no-op rebuild");
211
+ });
212
+
213
+ test("finding 3 — a changed record under a kept id refreshes its memories, not duplicates", async () => {
214
+ const store = new MeetingRecordStore(MEMORY_DIR, new InMemoryIo());
215
+ const memoryWriter = new FakeMeetingWriter();
216
+ const cfg = config({ summaryMode: "smart" });
217
+ const firstData: MeetingDayData = {
218
+ detection: { date: DATE, appSpans: [appSpan("Zoom", START, END)], audioWindows: [audioWin("desktop", START, END)] },
219
+ conversations: [conv("desktop", "d1", [seg("we shipped v2", "2026-03-10T14:05:00.000Z")])],
220
+ };
221
+ const first = await new MeetingsBuilder({
222
+ source: fixedSource(firstData),
223
+ store,
224
+ config: cfg,
225
+ memoryGenerator: createMeetingMemoryGenerator(memoryWriter, cfg, { extractor: spyExtractor("v2 summary", SUMMARY_CANDIDATES) }),
226
+ }).buildDay(DATE);
227
+ const keptId = first.meetings[0]!.id;
228
+ const staleEpisode = memoryWriter.writes.find(
229
+ (w) => w.source === meetingSourceLabel(keptId) && w.category === "moment",
230
+ )?.content;
231
+ assert.ok(staleEpisode !== undefined, "the first build wrote an episode");
232
+
233
+ // Rebuild: identical window (full overlap → same id) but the record changed
234
+ // (app + transcript) → updatedIds. The memory layer must RETRACT the prior
235
+ // memories and regenerate, so recall is refreshed and never duplicated/stale.
236
+ const secondData: MeetingDayData = {
237
+ detection: { date: DATE, appSpans: [appSpan("Meet", START, END)], audioWindows: [audioWin("desktop", START, END)] },
238
+ conversations: [conv("desktop", "d1", [seg("we shipped v3", "2026-03-10T14:05:00.000Z")])],
239
+ };
240
+ const extractor2 = spyExtractor("v3 summary", SUMMARY_CANDIDATES);
241
+ const second = await new MeetingsBuilder({
242
+ source: fixedSource(secondData),
243
+ store,
244
+ config: cfg,
245
+ memoryGenerator: createMeetingMemoryGenerator(memoryWriter, cfg, { extractor: extractor2 }),
246
+ }).buildDay(DATE);
247
+ assert.equal(second.meetings[0]?.id, keptId, "the record keeps its id across the change");
248
+ assert.equal(second.built, 1, "the changed record was rewritten");
249
+ assert.equal(second.removed.length, 0, "the meeting was updated in place, not removed");
250
+ assert.ok(
251
+ memoryWriter.retired.includes(meetingSourceLabel(keptId)),
252
+ "the kept meeting's prior memories were retracted before regenerating",
253
+ );
254
+ assert.equal(extractor2.calls, 1, "the updated record is re-extracted to refresh its facts");
255
+ assert.equal(
256
+ await memoryWriter.hasMemoryFromSource(meetingSourceLabel(keptId), staleEpisode!),
257
+ false,
258
+ "the stale episode no longer resolves for the source",
259
+ );
260
+ const episodes = memoryWriter.writes.filter(
261
+ (w) => w.source === meetingSourceLabel(keptId) && w.category === "moment",
262
+ );
263
+ assert.notEqual(episodes.at(-1)?.content, staleEpisode, "the refreshed episode reflects the changed record");
264
+ });
265
+
266
+ test("finding 9 — removing a meeting retires its episode/summary memories", async () => {
267
+ const store = new MeetingRecordStore(MEMORY_DIR, new InMemoryIo());
268
+ const memoryWriter = new FakeMeetingWriter();
269
+ const first: MeetingDayData = {
270
+ detection: { date: DATE, appSpans: [appSpan("Zoom", START, END)], audioWindows: [audioWin("desktop", START, END)] },
271
+ conversations: [conv("desktop", "d1", [seg("hello", "2026-03-10T14:05:00.000Z")])],
272
+ };
273
+ const cfg = config();
274
+ const firstSummary = await new MeetingsBuilder({ source: fixedSource(first), store, config: cfg, memoryGenerator: createMeetingMemoryGenerator(memoryWriter, cfg) }).buildDay(DATE);
275
+ const staleId = firstSummary.meetings[0]!.id;
276
+ const episodeContent = memoryWriter.writes.find((w) => w.source === meetingSourceLabel(staleId))?.content;
277
+ assert.ok(episodeContent !== undefined, "episode written for the meeting");
278
+ assert.equal(await memoryWriter.hasMemoryFromSource(meetingSourceLabel(staleId), episodeContent!), true);
279
+
280
+ // Rebuild: a non-overlapping later meeting replaces it → the stale meeting is
281
+ // removed, and its meeting-derived memories must be retired too.
282
+ const LATER_START = "2026-03-10T16:00:00.000Z";
283
+ const LATER_END = "2026-03-10T17:00:00.000Z";
284
+ const second: MeetingDayData = {
285
+ detection: { date: DATE, appSpans: [appSpan("Zoom", LATER_START, LATER_END)], audioWindows: [audioWin("desktop", LATER_START, LATER_END)] },
286
+ conversations: [
287
+ { source: "desktop", conversationId: "d2", startIso: LATER_START, endIso: LATER_END, segments: [{ speaker: "Jane", isSelf: false, text: "later", startIso: "2026-03-10T16:05:00.000Z" }] },
288
+ ],
289
+ };
290
+ const secondSummary = await new MeetingsBuilder({ source: fixedSource(second), store, config: cfg, memoryGenerator: createMeetingMemoryGenerator(memoryWriter, cfg) }).buildDay(DATE);
291
+ assert.deepEqual(secondSummary.removed, [staleId]);
292
+ assert.ok(memoryWriter.retired.includes(meetingSourceLabel(staleId)), "the removed meeting's memories are retired");
293
+ assert.equal(
294
+ await memoryWriter.hasMemoryFromSource(meetingSourceLabel(staleId), episodeContent!),
295
+ false,
296
+ "no memory remains for the removed meeting",
297
+ );
298
+ });
299
+
300
+ function summaryData(): MeetingDayData {
301
+ return {
302
+ detection: { date: DATE, appSpans: [appSpan("Zoom", START, END)], audioWindows: [audioWin("desktop", START, END)] },
303
+ conversations: [conv("desktop", "d1", [seg("we shipped v2", "2026-03-10T14:05:00.000Z")])],
304
+ };
305
+ }
306
+
307
+ const SUMMARY_CANDIDATES: MeetingFactCandidate[] = [
308
+ { content: "Team decided to ship v2 on Friday", category: "decision", confidence: 0.9 },
309
+ { content: "Sam will send the recap by EOD", category: "commitment", confidence: 0.9 },
310
+ ];
311
+
312
+ test("finding 11+12 — smart mode wires trust-gated decision + commitment on the production build path", async () => {
313
+ const store = new MeetingRecordStore(MEMORY_DIR, new InMemoryIo());
314
+ const memoryWriter = new FakeMeetingWriter();
315
+ const extractor = spyExtractor("We shipped v2.", SUMMARY_CANDIDATES);
316
+ const cfg = config({ summaryMode: "smart" });
317
+ const summary = await new MeetingsBuilder({
318
+ source: fixedSource(summaryData()),
319
+ store,
320
+ config: cfg,
321
+ memoryGenerator: createMeetingMemoryGenerator(memoryWriter, cfg, { extractor }),
322
+ }).buildDay(DATE);
323
+ const builtId = summary.meetings[0]!.id;
324
+ assert.equal(extractor.calls, 1, "the extractor runs on the production path");
325
+ assert.ok(summary.facts !== undefined, "facts aggregate is present");
326
+ assert.equal(summary.facts?.llmInvoked, true);
327
+ assert.equal(summary.facts?.active, 2, "both high-trust claims auto-active");
328
+ const commitment = memoryWriter.writes.find((w) => w.category === "commitment");
329
+ assert.ok(commitment, "a commitment fact was written");
330
+ assert.equal(commitment?.status, "active");
331
+ assert.equal(commitment?.meetingId, builtId, "commitment carries meetingId provenance");
332
+ assert.ok(memoryWriter.writes.some((w) => w.category === "decision" && w.status === "active"), "a decision fact was written active");
333
+ });
334
+
335
+ test("finding 11+12 — review mode queues facts, off mode makes zero LLM calls", async () => {
336
+ const reviewStore = new MeetingRecordStore(MEMORY_DIR, new InMemoryIo());
337
+ const reviewWriter = new FakeMeetingWriter();
338
+ const reviewExtractor = spyExtractor("We shipped v2.", SUMMARY_CANDIDATES);
339
+ const reviewCfg = config({ summaryMode: "review" });
340
+ const reviewSummary = await new MeetingsBuilder({
341
+ source: fixedSource(summaryData()),
342
+ store: reviewStore,
343
+ config: reviewCfg,
344
+ memoryGenerator: createMeetingMemoryGenerator(reviewWriter, reviewCfg, { extractor: reviewExtractor }),
345
+ }).buildDay(DATE);
346
+ assert.equal(reviewExtractor.calls, 1);
347
+ assert.equal(reviewSummary.facts?.active, 0, "review mode never auto-actives");
348
+ assert.equal(reviewSummary.facts?.review, 2, "both candidates queued for review");
349
+ assert.ok(reviewWriter.writes.some((w) => w.category === "commitment" && w.status === "pending_review"));
350
+
351
+ const offStore = new MeetingRecordStore(MEMORY_DIR, new InMemoryIo());
352
+ const offWriter = new FakeMeetingWriter();
353
+ const offExtractor = spyExtractor("We shipped v2.", SUMMARY_CANDIDATES);
354
+ const offCfg = config({ summaryMode: "off" });
355
+ const offSummary = await new MeetingsBuilder({
356
+ source: fixedSource(summaryData()),
357
+ store: offStore,
358
+ config: offCfg,
359
+ memoryGenerator: createMeetingMemoryGenerator(offWriter, offCfg, { extractor: offExtractor }),
360
+ }).buildDay(DATE);
361
+ assert.equal(offExtractor.calls, 0, "off mode must NEVER invoke the extractor");
362
+ assert.equal(offSummary.facts, undefined, "no fact aggregate in off mode");
363
+ assert.ok(offWriter.writes.every((w) => w.category === "moment"), "off mode writes only the episode, no summary facts");
364
+ });
@@ -0,0 +1,55 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+
4
+ import { MeetingsBuildScheduler } from "./build-scheduler.js";
5
+
6
+ /** A build fn recording the days it was asked to build. */
7
+ function recorder(): { build: (date: string) => Promise<void>; calls: string[] } {
8
+ const calls: string[] = [];
9
+ return { calls, build: async (date: string) => { calls.push(date); } };
10
+ }
11
+
12
+ test("coalesces a burst of requests for one day into a single build", async () => {
13
+ const rec = recorder();
14
+ const scheduler = new MeetingsBuildScheduler({ debounceMs: 10_000, build: rec.build });
15
+ scheduler.requestBuild("2026-03-10");
16
+ scheduler.requestBuild("2026-03-10");
17
+ scheduler.requestBuild("2026-03-10");
18
+ await scheduler.flush();
19
+ assert.deepEqual(rec.calls, ["2026-03-10"], "the re-armed timer fires exactly once");
20
+ });
21
+
22
+ test("distinct days each build once", async () => {
23
+ const rec = recorder();
24
+ const scheduler = new MeetingsBuildScheduler({ debounceMs: 10_000, build: rec.build });
25
+ scheduler.requestBuild("2026-03-10");
26
+ scheduler.requestBuild("2026-03-11");
27
+ await scheduler.flush();
28
+ assert.deepEqual([...rec.calls].sort(), ["2026-03-10", "2026-03-11"]);
29
+ });
30
+
31
+ test("a build failure is isolated and routed to onError, never rejecting flush", async () => {
32
+ const errors: Array<[string, string]> = [];
33
+ const scheduler = new MeetingsBuildScheduler({
34
+ debounceMs: 10_000,
35
+ build: async (date) => {
36
+ throw new Error(`boom ${date}`);
37
+ },
38
+ onError: (date, err) => errors.push([date, err instanceof Error ? err.message : String(err)]),
39
+ });
40
+ scheduler.requestBuild("2026-03-10");
41
+ await scheduler.flush(); // must resolve despite the throw
42
+ assert.deepEqual(errors, [["2026-03-10", "boom 2026-03-10"]]);
43
+ });
44
+
45
+ test("dispose cancels armed timers so no build runs, and later requests are no-ops", async () => {
46
+ const rec = recorder();
47
+ const scheduler = new MeetingsBuildScheduler({ debounceMs: 10_000, build: rec.build });
48
+ scheduler.requestBuild("2026-03-10");
49
+ scheduler.dispose();
50
+ await scheduler.flush();
51
+ assert.deepEqual(rec.calls, [], "a disposed scheduler builds nothing");
52
+ scheduler.requestBuild("2026-03-11");
53
+ await scheduler.flush();
54
+ assert.deepEqual(rec.calls, [], "requestBuild after dispose is a no-op");
55
+ });
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Debounced meetings build scheduler (issue #1900) — the tail step after a
3
+ * sync. A completed activity or wearable sync for a day calls
4
+ * `requestBuild(date)`; this coalesces bursts (a wearable window sync touches
5
+ * several days; an activity tick and a wearable tick can land together) onto a
6
+ * per-day trailing-edge timer so the day is (re)built at most once per debounce
7
+ * window instead of once per sync signal.
8
+ *
9
+ * Single-flight per day: a build in flight for a date defers a fresh request to
10
+ * a follow-up build after it settles, so a late sync signal is never dropped.
11
+ * `debounceMs: 0` builds on the next tick (no coalescing window). Errors are
12
+ * isolated per day and routed to `onError` — a failed build must never reject
13
+ * the sync path that requested it.
14
+ */
15
+
16
+ export interface MeetingsBuildSchedulerOptions {
17
+ /** Trailing-edge coalescing window (ms). 0 = build on the next tick. */
18
+ debounceMs: number;
19
+ /** Build one day. Rejections are caught and routed to `onError`. */
20
+ build(date: string): Promise<unknown>;
21
+ /** Observe a build failure (logging). Never called for success. */
22
+ onError?(date: string, err: unknown): void;
23
+ }
24
+
25
+ export class MeetingsBuildScheduler {
26
+ private readonly timers = new Map<string, NodeJS.Timeout>();
27
+ /** Days whose timer has fired and are running (single-flight promise). */
28
+ private readonly running = new Map<string, Promise<void>>();
29
+ /** Days requested again while their build was in flight (trailing rebuild). */
30
+ private readonly rerun = new Set<string>();
31
+ private disposed = false;
32
+
33
+ constructor(private readonly opts: MeetingsBuildSchedulerOptions) {}
34
+
35
+ /** Request a (debounced) build for a day. Idempotent within the window. */
36
+ requestBuild(date: string): void {
37
+ if (this.disposed) return;
38
+ clearTimeout(this.timers.get(date));
39
+ const timer = setTimeout(() => {
40
+ this.timers.delete(date);
41
+ void this.fire(date);
42
+ }, Math.max(0, this.opts.debounceMs));
43
+ // Do not keep the event loop alive for a pending trailing build.
44
+ timer.unref();
45
+ this.timers.set(date, timer);
46
+ }
47
+
48
+ /** Run every armed timer's build now and await completion (teardown/tests). */
49
+ async flush(): Promise<void> {
50
+ for (const [date, timer] of this.timers) {
51
+ clearTimeout(timer);
52
+ this.timers.delete(date);
53
+ void this.fire(date);
54
+ }
55
+ // Drain in-flight builds (and any trailing reruns they spawn).
56
+ while (this.running.size > 0) {
57
+ await Promise.allSettled([...this.running.values()]);
58
+ }
59
+ }
60
+
61
+ /** Cancel all armed timers without running them. */
62
+ dispose(): void {
63
+ this.disposed = true;
64
+ for (const timer of this.timers.values()) clearTimeout(timer);
65
+ this.timers.clear();
66
+ }
67
+
68
+ /** True while any day has an armed timer or an in-flight build. */
69
+ get pending(): boolean {
70
+ return this.timers.size > 0 || this.running.size > 0;
71
+ }
72
+
73
+ private fire(date: string): Promise<void> {
74
+ // Single-flight: fold a concurrent request into a trailing rebuild.
75
+ const inflight = this.running.get(date);
76
+ if (inflight !== undefined) {
77
+ this.rerun.add(date);
78
+ return inflight;
79
+ }
80
+ const run = this.runBuild(date).then(() => {
81
+ this.running.delete(date);
82
+ if (this.rerun.delete(date) && !this.disposed) return this.fire(date);
83
+ return undefined;
84
+ });
85
+ this.running.set(date, run);
86
+ return run;
87
+ }
88
+
89
+ private async runBuild(date: string): Promise<void> {
90
+ try {
91
+ await this.opts.build(date);
92
+ } catch (err) {
93
+ this.opts.onError?.(date, err);
94
+ }
95
+ }
96
+ }