@remnic/core 9.22.0 → 9.22.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 (399) 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 +13 -10
  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-service-BtxW52vv.d.ts → access-service-CKEROeFP.d.ts} +22 -11
  33. package/dist/access-service.d.ts +13 -10
  34. package/dist/access-service.js +25 -24
  35. package/dist/access-surface-catalog.d.ts +12 -9
  36. package/dist/access-surface-catalog.js +6 -0
  37. package/dist/access-surface-catalog.js.map +1 -1
  38. package/dist/access-wearables-meetings-surface.d.ts +78 -0
  39. package/dist/access-wearables-meetings-surface.js +22 -0
  40. package/dist/access-wearables-meetings-surface.js.map +1 -0
  41. package/dist/action-confidence.d.ts +1 -1
  42. package/dist/active-memory-bridge.d.ts +1 -1
  43. package/dist/active-recall.d.ts +1 -1
  44. package/dist/active-recall.js +2 -2
  45. package/dist/{auto-sync-UPTUVE4X.js → auto-sync-5OE5QOZB.js} +3 -3
  46. package/dist/behavior-learner.d.ts +1 -1
  47. package/dist/behavior-signals.d.ts +1 -1
  48. package/dist/bootstrap.d.ts +9 -7
  49. package/dist/briefing.d.ts +2 -2
  50. package/dist/briefing.js +5 -5
  51. package/dist/buffer-surprise-report.d.ts +1 -1
  52. package/dist/buffer.d.ts +2 -2
  53. package/dist/bulk-import/index.d.ts +3 -3
  54. package/dist/calibration.d.ts +1 -1
  55. package/dist/capabilities.d.ts +1 -1
  56. package/dist/{catalog-iWvWz61I.d.ts → catalog-BFTVo2Ez.d.ts} +1 -1
  57. package/dist/causal-behavior.d.ts +1 -1
  58. package/dist/causal-behavior.js +2 -2
  59. package/dist/causal-chain.js +2 -2
  60. package/dist/causal-consolidation.d.ts +1 -1
  61. package/dist/causal-consolidation.js +9 -9
  62. package/dist/causal-retrieval.js +2 -2
  63. package/dist/causal-trajectory-graph.d.ts +1 -1
  64. package/dist/causal-trajectory.js +1 -1
  65. package/dist/{chunk-GF22HWS2.js → chunk-2PHX3I5C.js} +7 -7
  66. package/dist/{chunk-JFDEYXF7.js → chunk-3NAHZQU3.js} +2 -2
  67. package/dist/{chunk-XBRPEGXF.js → chunk-3UOSVTFR.js} +5 -5
  68. package/dist/{chunk-TTKWTT72.js → chunk-3ZW6ZQO6.js} +3 -3
  69. package/dist/{chunk-UV72DEZD.js → chunk-4B77XSG5.js} +5 -5
  70. package/dist/{chunk-IC2TCZJ2.js → chunk-57QKLRBV.js} +3 -1
  71. package/dist/chunk-57QKLRBV.js.map +1 -0
  72. package/dist/{chunk-LMHDFOVV.js → chunk-5CPI2QWG.js} +3 -3
  73. package/dist/{chunk-P72WZMWQ.js → chunk-5FZQ74K5.js} +2 -2
  74. package/dist/{chunk-BRT7KEXU.js → chunk-6TWXQ4E2.js} +5 -5
  75. package/dist/{chunk-CV6ZYXK3.js → chunk-7I7AYIUQ.js} +3 -3
  76. package/dist/{chunk-TVVEYCNW.js → chunk-7K5Q6COX.js} +4 -4
  77. package/dist/{chunk-MONWCSVL.js → chunk-7RAJ3GXO.js} +2 -1
  78. package/dist/chunk-7RAJ3GXO.js.map +1 -0
  79. package/dist/{chunk-SQEDDWAM.js → chunk-A2RFKL3V.js} +5 -34
  80. package/dist/chunk-A2RFKL3V.js.map +1 -0
  81. package/dist/{chunk-JN57HZCD.js → chunk-ATEAL5EI.js} +6 -6
  82. package/dist/{chunk-4PYLBXX6.js → chunk-AYAROCWD.js} +2 -2
  83. package/dist/chunk-BLDBT6HZ.js +39 -0
  84. package/dist/chunk-BLDBT6HZ.js.map +1 -0
  85. package/dist/{chunk-NB2P47N7.js → chunk-CUUY5CC4.js} +6 -6
  86. package/dist/{chunk-KL5Y6PEV.js → chunk-E3ZMFFQZ.js} +2 -2
  87. package/dist/{chunk-55FDQYPS.js → chunk-ES5IHQ6M.js} +2 -2
  88. package/dist/{chunk-N47ZWQBO.js → chunk-G2NSSQES.js} +2 -2
  89. package/dist/{chunk-GSWPU5CR.js → chunk-GD3QOBMB.js} +2 -2
  90. package/dist/{chunk-6ANPNUVL.js → chunk-IIXLB5E7.js} +58 -36
  91. package/dist/chunk-IIXLB5E7.js.map +1 -0
  92. package/dist/{chunk-SACPRJ7F.js → chunk-JNIJBXVK.js} +2 -2
  93. package/dist/{chunk-W3XT32W7.js → chunk-JVGUY2AU.js} +23 -13
  94. package/dist/chunk-JVGUY2AU.js.map +1 -0
  95. package/dist/{chunk-DGLON3AL.js → chunk-KNRG6ZGJ.js} +6 -6
  96. package/dist/{chunk-GEUDRENF.js → chunk-MMTG3SCJ.js} +2 -2
  97. package/dist/{chunk-WZFAPHIK.js → chunk-MU7ZHSZE.js} +2 -2
  98. package/dist/{chunk-W4FBGYGK.js → chunk-NRV4Y3PM.js} +1 -1
  99. package/dist/{chunk-QTO2FGBT.js → chunk-OUW4PAY4.js} +191 -117
  100. package/dist/chunk-OUW4PAY4.js.map +1 -0
  101. package/dist/{chunk-KQ2XCQYN.js → chunk-P2BLHUL2.js} +2 -2
  102. package/dist/{chunk-ACGGTPU4.js → chunk-QCKQEBIY.js} +17 -9
  103. package/dist/chunk-QCKQEBIY.js.map +1 -0
  104. package/dist/{chunk-6L775BUB.js → chunk-QE63OHLB.js} +2 -2
  105. package/dist/{chunk-ONHRRJZM.js → chunk-QJAQNXWI.js} +6 -6
  106. package/dist/{chunk-EQYBHXRR.js → chunk-ROUL7RZU.js} +385 -59
  107. package/dist/chunk-ROUL7RZU.js.map +1 -0
  108. package/dist/{chunk-N6M7MRVN.js → chunk-RT4TSR2C.js} +9034 -8726
  109. package/dist/chunk-RT4TSR2C.js.map +1 -0
  110. package/dist/{chunk-6NZ7U4R4.js → chunk-SCOT5DTA.js} +1 -1
  111. package/dist/chunk-SCOT5DTA.js.map +1 -0
  112. package/dist/{chunk-6RYWNBSA.js → chunk-SVZGA2MK.js} +2 -2
  113. package/dist/chunk-SYOIVK7K.js +63 -0
  114. package/dist/chunk-SYOIVK7K.js.map +1 -0
  115. package/dist/{chunk-DM5A4ZFL.js → chunk-TWCOYET2.js} +11 -1
  116. package/dist/chunk-TWCOYET2.js.map +1 -0
  117. package/dist/{chunk-7S5OMI76.js → chunk-WS32QRLO.js} +109 -44
  118. package/dist/chunk-WS32QRLO.js.map +1 -0
  119. package/dist/{chunk-OCMLGND7.js → chunk-WWZ35GWW.js} +5 -5
  120. package/dist/{chunk-QMGIQUBG.js → chunk-XWPFZX6S.js} +503 -139
  121. package/dist/chunk-XWPFZX6S.js.map +1 -0
  122. package/dist/{chunk-A5N6ISRK.js → chunk-ZN4UZWMT.js} +2 -2
  123. package/dist/{chunk-7M6APKJL.js → chunk-ZYU7ZFLT.js} +2 -2
  124. package/dist/{cli-D9vXgWwM.d.ts → cli-BrwT9YMD.d.ts} +5 -5
  125. package/dist/cli.d.ts +14 -11
  126. package/dist/cli.js +57 -56
  127. package/dist/compounding/engine.d.ts +2 -2
  128. package/dist/compounding/engine.js +5 -5
  129. package/dist/compounding/preference-consolidator.d.ts +1 -1
  130. package/dist/compression-optimizer.d.ts +1 -1
  131. package/dist/config.d.ts +1 -1
  132. package/dist/config.js +2 -2
  133. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  134. package/dist/connectors/codex-materialize-runner.js +5 -5
  135. package/dist/connectors/codex-materialize.d.ts +1 -1
  136. package/dist/connectors/index.d.ts +1 -1
  137. package/dist/connectors/index.js +5 -5
  138. package/dist/consolidation-provenance-check.d.ts +2 -2
  139. package/dist/consolidation-undo.d.ts +2 -2
  140. package/dist/contradiction/index.d.ts +2 -2
  141. package/dist/conversation-index/backend.d.ts +1 -1
  142. package/dist/conversation-index/chunker.d.ts +1 -1
  143. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  144. package/dist/conversation-index/indexer.d.ts +1 -1
  145. package/dist/conversation-index/search.d.ts +1 -1
  146. package/dist/day-summary.d.ts +1 -1
  147. package/dist/delinearize.d.ts +1 -1
  148. package/dist/direct-answer-wiring.d.ts +1 -1
  149. package/dist/direct-answer.d.ts +1 -1
  150. package/dist/embedding-fallback.d.ts +1 -1
  151. package/dist/enrichment/index.d.ts +1 -1
  152. package/dist/entity-retrieval.d.ts +2 -2
  153. package/dist/entity-retrieval.js +5 -5
  154. package/dist/entity-schema.d.ts +1 -1
  155. package/dist/explicit-capture.d.ts +9 -7
  156. package/dist/extraction-error-classification.d.ts +22 -0
  157. package/dist/extraction-error-classification.js +11 -0
  158. package/dist/extraction-error-classification.js.map +1 -0
  159. package/dist/extraction-faithfulness.d.ts +1 -1
  160. package/dist/extraction-judge-telemetry.d.ts +1 -1
  161. package/dist/extraction-judge-training.d.ts +1 -1
  162. package/dist/extraction-judge.d.ts +1 -1
  163. package/dist/extraction.d.ts +3 -17
  164. package/dist/extraction.js +5 -3
  165. package/dist/fallback-llm.d.ts +1 -1
  166. package/dist/{first-start-migration-LEXL3XYL.js → first-start-migration-BKNGOFED.js} +4 -4
  167. package/dist/graph-dashboard-diff.d.ts +1 -1
  168. package/dist/graph-dashboard-key.d.ts +1 -1
  169. package/dist/graph-dashboard-parser.d.ts +1 -1
  170. package/dist/graph-edge-reinforcement.d.ts +1 -1
  171. package/dist/graph-snapshot.d.ts +1 -1
  172. package/dist/graph.d.ts +1 -1
  173. package/dist/importance.d.ts +1 -1
  174. package/dist/importers/index.d.ts +1 -1
  175. package/dist/in-flight-reads.d.ts +1 -1
  176. package/dist/index.d.ts +309 -519
  177. package/dist/index.js +176 -140
  178. package/dist/intent.d.ts +1 -1
  179. package/dist/lcm/engine.d.ts +1 -1
  180. package/dist/lcm/engine.js +3 -3
  181. package/dist/lcm/index.d.ts +1 -1
  182. package/dist/lcm/index.js +11 -11
  183. package/dist/lcm/tools.d.ts +1 -1
  184. package/dist/lifecycle.d.ts +1 -1
  185. package/dist/live-connectors-runner.d.ts +1 -1
  186. package/dist/local-llm.d.ts +1 -1
  187. package/dist/local-model-endpoint.d.ts +1 -1
  188. package/dist/maintenance/memory-governance.d.ts +1 -1
  189. package/dist/maintenance/memory-governance.js +6 -6
  190. package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +2 -2
  191. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +5 -5
  192. package/dist/maintenance/rebuild-memory-projection.d.ts +2 -2
  193. package/dist/maintenance/rebuild-memory-projection.js +7 -7
  194. package/dist/maintenance-C98njYb8.d.ts +366 -0
  195. package/dist/mcp-memory-inspector-app.d.ts +12 -9
  196. package/dist/mcp-read-only-tools.js +1 -1
  197. package/dist/memory-action-policy.d.ts +1 -1
  198. package/dist/memory-cache.d.ts +1 -1
  199. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  200. package/dist/memory-projection-store.d.ts +1 -1
  201. package/dist/memory-provenance.d.ts +1 -1
  202. package/dist/memory-worth-outcomes.d.ts +2 -2
  203. package/dist/models-json.d.ts +1 -1
  204. package/dist/namespaces/migrate.d.ts +3 -3
  205. package/dist/namespaces/migrate.js +12 -12
  206. package/dist/namespaces/principal.d.ts +1 -1
  207. package/dist/namespaces/search.d.ts +1 -1
  208. package/dist/namespaces/search.js +6 -6
  209. package/dist/namespaces/storage.d.ts +3 -3
  210. package/dist/namespaces/storage.js +5 -5
  211. package/dist/native-knowledge.d.ts +1 -1
  212. package/dist/offline-sync-impression-drain.d.ts +1 -1
  213. package/dist/operator-toolkit.d.ts +2 -2
  214. package/dist/operator-toolkit.js +18 -18
  215. package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -2
  216. package/dist/orchestration/maintenance.d.ts +6 -223
  217. package/dist/orchestration/maintenance.js +12 -12
  218. package/dist/{orchestrator-DB93S48I.d.ts → orchestrator-C9ZVppNh.d.ts} +12 -481
  219. package/dist/orchestrator.d.ts +10 -8
  220. package/dist/orchestrator.js +84 -82
  221. package/dist/patterns-cli.d.ts +1 -1
  222. package/dist/{pipeline-DmGc-H66.d.ts → pipeline-id_-sqYm.d.ts} +1 -1
  223. package/dist/policy-runtime.d.ts +1 -1
  224. package/dist/proactive-contention.d.ts +1 -1
  225. package/dist/provenance.d.ts +1 -1
  226. package/dist/{qmd-cJT0crkF.d.ts → qmd-R6XaQOZK.d.ts} +1 -1
  227. package/dist/qmd-preflight.d.ts +2 -2
  228. package/dist/qmd-recall-cache.d.ts +1 -1
  229. package/dist/qmd.d.ts +2 -2
  230. package/dist/qmd.js +4 -4
  231. package/dist/recall-concurrency-config.d.ts +1 -1
  232. package/dist/recall-disclosure-escalation.d.ts +1 -1
  233. package/dist/recall-explain-renderer.d.ts +1 -1
  234. package/dist/recall-explain-renderer.js +3 -3
  235. package/dist/recall-memory-map.d.ts +1 -1
  236. package/dist/recall-planner-llm.d.ts +1 -1
  237. package/dist/recall-state.d.ts +1 -1
  238. package/dist/recall-tag-filter.d.ts +1 -1
  239. package/dist/recall-timings.d.ts +1 -1
  240. package/dist/recall-xray-cli.d.ts +1 -1
  241. package/dist/recall-xray-cli.js +4 -4
  242. package/dist/recall-xray-renderer.d.ts +1 -1
  243. package/dist/recall-xray-renderer.js +3 -3
  244. package/dist/recall-xray.d.ts +1 -1
  245. package/dist/recall-xray.js +2 -2
  246. package/dist/replay/normalizers/chatgpt.d.ts +1 -1
  247. package/dist/replay/normalizers/claude.d.ts +1 -1
  248. package/dist/replay/normalizers/openclaw.d.ts +1 -1
  249. package/dist/replay/normalizers/shared.d.ts +1 -1
  250. package/dist/replay/runner.d.ts +1 -1
  251. package/dist/replay/types.d.ts +1 -1
  252. package/dist/resolve-auth-token.d.ts +1 -1
  253. package/dist/resume-bundles.js +4 -4
  254. package/dist/retrieval-agents.d.ts +2 -2
  255. package/dist/retrieval-tiers.d.ts +1 -1
  256. package/dist/routing/engine.d.ts +1 -1
  257. package/dist/routing/store.d.ts +1 -1
  258. package/dist/salvage-envelope.d.ts +1 -1
  259. package/dist/{scope-profiles-KG2nODX7.d.ts → scope-profiles-EhyuAKkd.d.ts} +1 -1
  260. package/dist/search/embed-helper.d.ts +1 -1
  261. package/dist/search/factory.d.ts +1 -1
  262. package/dist/search/factory.js +5 -5
  263. package/dist/search/index.d.ts +1 -1
  264. package/dist/search/index.js +9 -9
  265. package/dist/search/lancedb-backend.d.ts +1 -1
  266. package/dist/search/meilisearch-backend.d.ts +1 -1
  267. package/dist/search/noop-backend.d.ts +1 -1
  268. package/dist/search/orama-backend.d.ts +1 -1
  269. package/dist/search/port.d.ts +1 -1
  270. package/dist/search/remote-backend.d.ts +1 -1
  271. package/dist/{semantic-consolidation-BaZlg0D8.d.ts → semantic-consolidation-B2xo26UQ.d.ts} +1 -1
  272. package/dist/semantic-consolidation.d.ts +2 -2
  273. package/dist/semantic-consolidation.js +7 -7
  274. package/dist/semantic-rule-promotion.js +5 -5
  275. package/dist/semantic-rule-verifier.d.ts +1 -1
  276. package/dist/semantic-rule-verifier.js +6 -6
  277. package/dist/service-DgFWlZ-r.d.ts +835 -0
  278. package/dist/session-observer-bands.d.ts +1 -1
  279. package/dist/session-observer-state.d.ts +1 -1
  280. package/dist/shared-context/manager.d.ts +1 -1
  281. package/dist/signal.d.ts +1 -1
  282. package/dist/{storage-BKhHHnRo.d.ts → storage-XGawwqXE.d.ts} +131 -2
  283. package/dist/storage.d.ts +2 -2
  284. package/dist/storage.js +4 -4
  285. package/dist/summarizer.d.ts +1 -1
  286. package/dist/summary-snapshot.d.ts +1 -1
  287. package/dist/temporal-supersession.d.ts +2 -2
  288. package/dist/temporal-timeline-recall.d.ts +1 -1
  289. package/dist/temporal-validity.d.ts +1 -1
  290. package/dist/threading.d.ts +1 -1
  291. package/dist/tier-migration.d.ts +2 -2
  292. package/dist/tier-routing.d.ts +1 -1
  293. package/dist/topics.d.ts +1 -1
  294. package/dist/transcript.d.ts +1 -1
  295. package/dist/trust-score-stage.d.ts +1 -1
  296. package/dist/trust-score.d.ts +1 -1
  297. package/dist/{types-GBQZuT7l.d.ts → types-Bbs0qz6b.d.ts} +2 -3
  298. package/dist/types.d.ts +1 -1
  299. package/dist/types.js +1 -1
  300. package/dist/utility-runtime.d.ts +1 -1
  301. package/dist/verified-recall.js +6 -6
  302. package/dist/write-envelope.d.ts +1 -1
  303. package/package.json +2 -2
  304. package/src/access-boundary.ts +3 -0
  305. package/src/access-http.test.ts +63 -0
  306. package/src/access-http.ts +26 -37
  307. package/src/access-mcp-output-schemas.ts +10 -0
  308. package/src/access-mcp.ts +4 -121
  309. package/src/access-operations-batch.ts +10 -5
  310. package/src/access-service.ts +43 -46
  311. package/src/access-surface-catalog.ts +6 -0
  312. package/src/access-wearables-meetings-surface.ts +139 -0
  313. package/src/activity/digest.ts +1 -1
  314. package/src/activity/runner.test.ts +37 -0
  315. package/src/activity/runner.ts +12 -0
  316. package/src/activity/scheduler.test.ts +1 -0
  317. package/src/activity/sync-registration.ts +13 -0
  318. package/src/cli/meetings-commands.ts +70 -0
  319. package/src/cli.ts +7 -6
  320. package/src/extraction-error-classification.ts +52 -0
  321. package/src/extraction.ts +2 -50
  322. package/src/mcp-read-only-tools.ts +2 -0
  323. package/src/meetings/build-memory.test.ts +364 -0
  324. package/src/meetings/build-scheduler.test.ts +55 -0
  325. package/src/meetings/build-scheduler.ts +96 -0
  326. package/src/meetings/day-source.test.ts +275 -0
  327. package/src/meetings/day-source.ts +352 -0
  328. package/src/meetings/http-glue.ts +126 -0
  329. package/src/meetings/index.ts +5 -4
  330. package/src/meetings/manual-sync-flush.test.ts +72 -0
  331. package/src/meetings/mcp-tools.ts +65 -0
  332. package/src/meetings/memory-gen.test.ts +498 -0
  333. package/src/meetings/memory-gen.ts +549 -0
  334. package/src/meetings/memory-generator.ts +5 -0
  335. package/src/meetings/namespace-isolation.test.ts +159 -0
  336. package/src/meetings/service.test.ts +195 -0
  337. package/src/meetings/service.ts +157 -0
  338. package/src/meetings/summary-extractor.test.ts +287 -0
  339. package/src/meetings/summary-extractor.ts +264 -0
  340. package/src/orchestration/maintenance.ts +8 -0
  341. package/src/orchestration/orchestrator-init.ts +6 -1
  342. package/src/orchestration/workspace-ops.ts +172 -8
  343. package/src/orchestrator.ts +12 -13
  344. package/src/storage.ts +21 -0
  345. package/src/testing/subjects/meetings-namespace-harness.ts +159 -0
  346. package/src/testing/subjects/meetings-namespace.test.ts +93 -0
  347. package/src/types.ts +1 -2
  348. package/src/wearables/cli.ts +2 -1
  349. package/src/wearables/index.ts +1 -0
  350. package/src/wearables/mcp-tools.ts +143 -0
  351. package/src/wearables/registry.test.ts +11 -0
  352. package/src/wearables/registry.ts +13 -0
  353. package/src/wearables/service.test.ts +62 -1
  354. package/src/wearables/service.ts +30 -2
  355. package/src/wearables/transcript-search-namespace-isolation.test.ts +98 -0
  356. package/dist/chunk-6ANPNUVL.js.map +0 -1
  357. package/dist/chunk-6NZ7U4R4.js.map +0 -1
  358. package/dist/chunk-7S5OMI76.js.map +0 -1
  359. package/dist/chunk-ACGGTPU4.js.map +0 -1
  360. package/dist/chunk-DM5A4ZFL.js.map +0 -1
  361. package/dist/chunk-EQYBHXRR.js.map +0 -1
  362. package/dist/chunk-IC2TCZJ2.js.map +0 -1
  363. package/dist/chunk-MONWCSVL.js.map +0 -1
  364. package/dist/chunk-N6M7MRVN.js.map +0 -1
  365. package/dist/chunk-QMGIQUBG.js.map +0 -1
  366. package/dist/chunk-QTO2FGBT.js.map +0 -1
  367. package/dist/chunk-SQEDDWAM.js.map +0 -1
  368. package/dist/chunk-W3XT32W7.js.map +0 -1
  369. /package/dist/{auto-sync-UPTUVE4X.js.map → auto-sync-5OE5QOZB.js.map} +0 -0
  370. /package/dist/{chunk-GF22HWS2.js.map → chunk-2PHX3I5C.js.map} +0 -0
  371. /package/dist/{chunk-JFDEYXF7.js.map → chunk-3NAHZQU3.js.map} +0 -0
  372. /package/dist/{chunk-XBRPEGXF.js.map → chunk-3UOSVTFR.js.map} +0 -0
  373. /package/dist/{chunk-TTKWTT72.js.map → chunk-3ZW6ZQO6.js.map} +0 -0
  374. /package/dist/{chunk-UV72DEZD.js.map → chunk-4B77XSG5.js.map} +0 -0
  375. /package/dist/{chunk-LMHDFOVV.js.map → chunk-5CPI2QWG.js.map} +0 -0
  376. /package/dist/{chunk-P72WZMWQ.js.map → chunk-5FZQ74K5.js.map} +0 -0
  377. /package/dist/{chunk-BRT7KEXU.js.map → chunk-6TWXQ4E2.js.map} +0 -0
  378. /package/dist/{chunk-CV6ZYXK3.js.map → chunk-7I7AYIUQ.js.map} +0 -0
  379. /package/dist/{chunk-TVVEYCNW.js.map → chunk-7K5Q6COX.js.map} +0 -0
  380. /package/dist/{chunk-JN57HZCD.js.map → chunk-ATEAL5EI.js.map} +0 -0
  381. /package/dist/{chunk-4PYLBXX6.js.map → chunk-AYAROCWD.js.map} +0 -0
  382. /package/dist/{chunk-NB2P47N7.js.map → chunk-CUUY5CC4.js.map} +0 -0
  383. /package/dist/{chunk-KL5Y6PEV.js.map → chunk-E3ZMFFQZ.js.map} +0 -0
  384. /package/dist/{chunk-55FDQYPS.js.map → chunk-ES5IHQ6M.js.map} +0 -0
  385. /package/dist/{chunk-N47ZWQBO.js.map → chunk-G2NSSQES.js.map} +0 -0
  386. /package/dist/{chunk-GSWPU5CR.js.map → chunk-GD3QOBMB.js.map} +0 -0
  387. /package/dist/{chunk-SACPRJ7F.js.map → chunk-JNIJBXVK.js.map} +0 -0
  388. /package/dist/{chunk-DGLON3AL.js.map → chunk-KNRG6ZGJ.js.map} +0 -0
  389. /package/dist/{chunk-GEUDRENF.js.map → chunk-MMTG3SCJ.js.map} +0 -0
  390. /package/dist/{chunk-WZFAPHIK.js.map → chunk-MU7ZHSZE.js.map} +0 -0
  391. /package/dist/{chunk-W4FBGYGK.js.map → chunk-NRV4Y3PM.js.map} +0 -0
  392. /package/dist/{chunk-KQ2XCQYN.js.map → chunk-P2BLHUL2.js.map} +0 -0
  393. /package/dist/{chunk-6L775BUB.js.map → chunk-QE63OHLB.js.map} +0 -0
  394. /package/dist/{chunk-ONHRRJZM.js.map → chunk-QJAQNXWI.js.map} +0 -0
  395. /package/dist/{chunk-6RYWNBSA.js.map → chunk-SVZGA2MK.js.map} +0 -0
  396. /package/dist/{chunk-OCMLGND7.js.map → chunk-WWZ35GWW.js.map} +0 -0
  397. /package/dist/{chunk-A5N6ISRK.js.map → chunk-ZN4UZWMT.js.map} +0 -0
  398. /package/dist/{chunk-7M6APKJL.js.map → chunk-ZYU7ZFLT.js.map} +0 -0
  399. /package/dist/{first-start-migration-LEXL3XYL.js.map → first-start-migration-BKNGOFED.js.map} +0 -0
@@ -0,0 +1,275 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+
4
+ import type { ActivitySnapshot } from "../activity/types.js";
5
+ import type { FusionConversationInput } from "../wearables/fusion/types.js";
6
+ import {
7
+ ActivityWearablesMeetingsDaySource,
8
+ buildAudioWindows,
9
+ deriveAppSpans,
10
+ type MeetingsActivityReader,
11
+ type MeetingsWearableReader,
12
+ } from "./day-source.js";
13
+ import { MeetingsInputError } from "./errors.js";
14
+ import { detectMeetings } from "./detect.js";
15
+ import { DEFAULT_MEETINGS_CONFIG } from "./config.js";
16
+ import type { MeetingsConfig } from "./types.js";
17
+
18
+ const DATE = "2026-03-10";
19
+
20
+ function config(overrides: Partial<MeetingsConfig> = {}): MeetingsConfig {
21
+ return {
22
+ ...DEFAULT_MEETINGS_CONFIG,
23
+ appPatterns: [...DEFAULT_MEETINGS_CONFIG.appPatterns],
24
+ enabled: true,
25
+ ...overrides,
26
+ };
27
+ }
28
+
29
+ function snap(overrides: Partial<ActivitySnapshot> = {}): ActivitySnapshot {
30
+ return {
31
+ machine: "workstation-a",
32
+ capturedAtUtc: "2026-03-10T14:00:00.000Z",
33
+ app: "Chrome",
34
+ windowTitle: "",
35
+ text: "",
36
+ textSource: "ax",
37
+ contentHash: Math.random().toString(36).slice(2),
38
+ ...overrides,
39
+ };
40
+ }
41
+
42
+ function conv(overrides: Partial<FusionConversationInput> = {}): FusionConversationInput {
43
+ return {
44
+ source: "desktop",
45
+ conversationId: "c1",
46
+ startIso: "2026-03-10T14:00:00.000Z",
47
+ segments: [],
48
+ ...overrides,
49
+ };
50
+ }
51
+
52
+ test("deriveAppSpans groups consecutive matching snapshots and breaks on a non-match", () => {
53
+ const spans = deriveAppSpans(
54
+ [
55
+ snap({ app: "Zoom", capturedAtUtc: "2026-03-10T14:00:00.000Z" }),
56
+ snap({ app: "Zoom", capturedAtUtc: "2026-03-10T14:01:00.000Z" }),
57
+ snap({ app: "Notes", capturedAtUtc: "2026-03-10T14:02:00.000Z" }), // breaks the run
58
+ snap({ app: "Zoom", capturedAtUtc: "2026-03-10T14:03:00.000Z" }),
59
+ ],
60
+ config().appPatterns,
61
+ );
62
+ assert.equal(spans.length, 2, "the non-matching snapshot splits the run into two spans");
63
+ assert.deepEqual(spans[0], { app: "Zoom", startUtc: "2026-03-10T14:00:00.000Z", endUtc: "2026-03-10T14:02:00.000Z" }, "the run ends at the next (non-matching) snapshot that bounds it");
64
+ assert.deepEqual(spans[1], { app: "Zoom", startUtc: "2026-03-10T14:03:00.000Z", endUtc: "2026-03-10T14:04:00.000Z" }, "a lone trailing run gets a finite end at the machine's cadence, not a zero-length point");
65
+ });
66
+
67
+ test("deriveAppSpans matches a meeting URL in the browser url, not just the app name", () => {
68
+ const spans = deriveAppSpans(
69
+ [
70
+ snap({ app: "Chrome", browserUrl: "https://meet.google.com/abc-defg-hij", capturedAtUtc: "2026-03-10T09:00:00.000Z" }),
71
+ snap({ app: "Chrome", browserUrl: "https://meet.google.com/abc-defg-hij", capturedAtUtc: "2026-03-10T09:05:00.000Z" }),
72
+ ],
73
+ config().appPatterns,
74
+ );
75
+ assert.equal(spans.length, 1);
76
+ assert.equal(spans[0]?.startUtc, "2026-03-10T09:00:00.000Z");
77
+ assert.equal(spans[0]?.endUtc, "2026-03-10T09:05:00.000Z");
78
+ });
79
+
80
+ test("deriveAppSpans returns nothing when there are no patterns", () => {
81
+ assert.deepEqual(deriveAppSpans([snap({ app: "Zoom" })], []), []);
82
+ });
83
+
84
+ test("buildAudioWindows counts distinct non-wearer speakers and derives the end", () => {
85
+ const windows = buildAudioWindows([
86
+ conv({
87
+ source: "limitless",
88
+ startIso: "2026-03-10T09:00:00.000Z",
89
+ segments: [
90
+ { speaker: "Me (you)", isSelf: true, text: "hi", startIso: "2026-03-10T09:00:00.000Z" },
91
+ { speaker: "Jane", isSelf: false, text: "hello", startIso: "2026-03-10T09:01:00.000Z", endIso: "2026-03-10T09:01:30.000Z" },
92
+ { speaker: "Bob", isSelf: false, text: "hey", startIso: "2026-03-10T09:02:00.000Z" },
93
+ { speaker: "Jane", isSelf: false, text: "again", startIso: "2026-03-10T09:03:00.000Z" },
94
+ ],
95
+ }),
96
+ ]);
97
+ assert.equal(windows.length, 1);
98
+ assert.equal(windows[0]?.source, "limitless");
99
+ assert.equal(windows[0]?.distinctNonWearerSpeakers, 2, "Jane + Bob, deduped; the wearer excluded");
100
+ assert.equal(windows[0]?.startUtc, "2026-03-10T09:00:00.000Z");
101
+ assert.equal(windows[0]?.endUtc, "2026-03-10T09:03:00.000Z", "latest segment instant when no conversation endIso");
102
+ });
103
+
104
+ test("buildAudioWindows prefers an explicit conversation endIso", () => {
105
+ const windows = buildAudioWindows([
106
+ conv({ startIso: "2026-03-10T09:00:00.000Z", endIso: "2026-03-10T10:00:00.000Z", segments: [] }),
107
+ ]);
108
+ assert.equal(windows[0]?.endUtc, "2026-03-10T10:00:00.000Z");
109
+ assert.equal(windows[0]?.distinctNonWearerSpeakers, 0);
110
+ });
111
+
112
+ test("loadDayData assembles detection + activity for the requested day, keyed to that day", async () => {
113
+ const activity: MeetingsActivityReader = {
114
+ listSnapshotsForDay: (_machine, startUtc, endUtc) => {
115
+ assert.ok(startUtc < endUtc, "half-open UTC window");
116
+ return [
117
+ snap({ app: "Zoom", windowTitle: "Standup", capturedAtUtc: "2026-03-10T14:00:00.000Z" }),
118
+ snap({ app: "Zoom", capturedAtUtc: "2026-03-10T14:05:00.000Z" }),
119
+ ];
120
+ },
121
+ };
122
+ const wearables: MeetingsWearableReader = { readDayBodies: async () => [] };
123
+ const source = new ActivityWearablesMeetingsDaySource({ activity, wearables, config: config(), timezone: "UTC" });
124
+ const data = await source.loadDayData(DATE);
125
+ assert.equal(data.detection.date, DATE, "detection is keyed to the requested day (builder asserts this)");
126
+ assert.equal(data.detection.appSpans.length, 1);
127
+ assert.equal(data.detection.appSpans[0]?.app, "Zoom");
128
+ assert.deepEqual(data.detection.audioWindows, [], "no wearable transcripts → no audio windows");
129
+ assert.equal(data.activity?.length, 2);
130
+ assert.equal(data.activity?.[0]?.title, "Standup");
131
+ assert.equal(data.conversations.length, 0);
132
+ });
133
+
134
+ test("loadDayData rejects a malformed day with a clean input error", async () => {
135
+ const source = new ActivityWearablesMeetingsDaySource({
136
+ activity: { listSnapshotsForDay: () => [] },
137
+ wearables: { readDayBodies: async () => [] },
138
+ config: config(),
139
+ timezone: "UTC",
140
+ });
141
+ await assert.rejects(() => Promise.resolve(source.loadDayData("2026-13-40")), MeetingsInputError);
142
+ });
143
+
144
+ test("deriveAppSpans gives a lone matching snapshot a non-zero span so app+audio detection is not missed (issue #1900)", () => {
145
+ const spans = deriveAppSpans(
146
+ [
147
+ snap({ app: "Zoom", capturedAtUtc: "2026-03-10T14:00:00.000Z" }), // single match
148
+ snap({ app: "Notes", capturedAtUtc: "2026-03-10T14:30:00.000Z" }), // ends + bounds it
149
+ ],
150
+ config().appPatterns,
151
+ );
152
+ assert.equal(spans.length, 1);
153
+ assert.deepEqual(spans[0], { app: "Zoom", startUtc: "2026-03-10T14:00:00.000Z", endUtc: "2026-03-10T14:30:00.000Z" });
154
+ assert.ok(
155
+ Date.parse(spans[0]!.endUtc) > Date.parse(spans[0]!.startUtc),
156
+ "the span is non-zero-length, so detect's isFinitePair keeps it",
157
+ );
158
+ // With a real span the overlapping audio window now classifies as app+audio;
159
+ // the old zero-length span was dropped and this returned nothing.
160
+ const detected = detectMeetings({
161
+ date: DATE,
162
+ appSpans: spans,
163
+ audioWindows: [
164
+ { source: "desktop", startUtc: "2026-03-10T14:00:00.000Z", endUtc: "2026-03-10T14:30:00.000Z", distinctNonWearerSpeakers: 1 },
165
+ ],
166
+ });
167
+ assert.equal(detected.length, 1);
168
+ assert.equal(detected[0]?.detectionSource, "app+audio");
169
+ });
170
+
171
+ test("deriveAppSpans derives runs per capture machine so another machine cannot split a call (issue #1900)", () => {
172
+ // Laptop is in Zoom continuously; a desktop ticks Chrome (non-match) in between.
173
+ const spans = deriveAppSpans(
174
+ [
175
+ snap({ machine: "laptop", app: "Zoom", capturedAtUtc: "2026-03-10T14:00:00.000Z" }),
176
+ snap({ machine: "desktop", app: "Chrome", capturedAtUtc: "2026-03-10T14:01:00.000Z" }),
177
+ snap({ machine: "laptop", app: "Zoom", capturedAtUtc: "2026-03-10T14:02:00.000Z" }),
178
+ snap({ machine: "desktop", app: "Chrome", capturedAtUtc: "2026-03-10T14:03:00.000Z" }),
179
+ snap({ machine: "laptop", app: "Zoom", capturedAtUtc: "2026-03-10T14:04:00.000Z" }),
180
+ ],
181
+ config().appPatterns,
182
+ );
183
+ assert.equal(spans.length, 1, "the desktop's Chrome ticks must not fragment the laptop's Zoom run");
184
+ assert.deepEqual(spans[0], { app: "Zoom", startUtc: "2026-03-10T14:00:00.000Z", endUtc: "2026-03-10T14:04:00.000Z" });
185
+ });
186
+
187
+ test("loadDayData converts the transcript timezone so audio lines up with UTC screen snapshots (issue #1900)", async () => {
188
+ const body = [
189
+ "# desktop transcript — 2026-03-10",
190
+ "",
191
+ "## 09:00–09:30 · Standup (conversation c1)",
192
+ "",
193
+ "**Me (you)** [09:00]: morning",
194
+ "**Jane** [09:05]: hi",
195
+ "**Bob** [09:10]: hey",
196
+ "",
197
+ ].join("\n");
198
+ const activity: MeetingsActivityReader = {
199
+ listSnapshotsForDay: () => [
200
+ snap({ app: "Zoom", capturedAtUtc: "2026-03-10T14:00:00.000Z" }),
201
+ snap({ app: "Notes", capturedAtUtc: "2026-03-10T14:35:00.000Z" }),
202
+ ],
203
+ };
204
+ const wearables: MeetingsWearableReader = {
205
+ readDayBodies: async () => [{ source: "desktop", body, timezone: "America/Chicago" }],
206
+ };
207
+ const source = new ActivityWearablesMeetingsDaySource({
208
+ activity,
209
+ wearables,
210
+ config: config(),
211
+ timezone: "America/Chicago",
212
+ });
213
+ const data = await source.loadDayData(DATE);
214
+ // 09:00 America/Chicago (CDT) is 14:00Z, not 09:00Z: the window now overlaps
215
+ // the 14:00Z Zoom snapshot; before the fix it drifted to 09:00Z and missed it.
216
+ assert.equal(data.detection.audioWindows.length, 1);
217
+ assert.equal(data.detection.audioWindows[0]?.startUtc, "2026-03-10T14:00:00.000Z");
218
+ assert.equal(data.detection.audioWindows[0]?.endUtc, "2026-03-10T14:30:00.000Z");
219
+ assert.equal(data.conversations[0]?.startIso, "2026-03-10T14:00:00.000Z");
220
+ const detected = detectMeetings(data.detection);
221
+ assert.equal(detected.length, 1);
222
+ assert.equal(detected[0]?.detectionSource, "app+audio");
223
+ });
224
+
225
+ test("deriveAppSpans gives a LONE TRAILING matching snapshot a finite span so end-of-window app+audio is not missed (issue #1900)", () => {
226
+ // A single Zoom tick at the end of the machine's snapshots, with NO later
227
+ // non-matching tick to bound it. Before the fix this pushed endUtc==startUtc,
228
+ // a zero-length point detect's isFinitePair dropped — so overlapping audio
229
+ // never paired and no meeting surfaced.
230
+ const spans = deriveAppSpans(
231
+ [snap({ app: "Zoom", capturedAtUtc: "2026-03-10T14:00:00.000Z" })],
232
+ config().appPatterns,
233
+ );
234
+ assert.equal(spans.length, 1);
235
+ assert.equal(spans[0]?.startUtc, "2026-03-10T14:00:00.000Z");
236
+ assert.ok(
237
+ Date.parse(spans[0]!.endUtc) > Date.parse(spans[0]!.startUtc),
238
+ "the trailing run gets a non-zero end so detect's isFinitePair keeps it",
239
+ );
240
+ const detected = detectMeetings(
241
+ {
242
+ date: DATE,
243
+ appSpans: spans,
244
+ audioWindows: [
245
+ { source: "desktop", startUtc: "2026-03-10T14:00:00.000Z", endUtc: "2026-03-10T14:30:00.000Z", distinctNonWearerSpeakers: 1 },
246
+ ],
247
+ },
248
+ config(),
249
+ );
250
+ assert.equal(detected.length, 1, "the finite trailing span now pairs with the overlapping audio window");
251
+ assert.equal(detected[0]?.detectionSource, "app+audio");
252
+ });
253
+
254
+ test("buildAudioWindows marks provider sources + carries the title so a titled provider conversation with no app span is detected (issue #1900)", () => {
255
+ const windows = buildAudioWindows([
256
+ conv({
257
+ source: "granola",
258
+ conversationId: "g1",
259
+ title: "Weekly sync",
260
+ startIso: "2026-03-10T16:00:00.000Z",
261
+ endIso: "2026-03-10T16:30:00.000Z",
262
+ segments: [{ speaker: "Jane", isSelf: false, text: "hi", startIso: "2026-03-10T16:05:00.000Z" }],
263
+ }),
264
+ ]);
265
+ assert.equal(windows.length, 1);
266
+ assert.equal(windows[0]?.providerMeeting, true, "a known provider source is marked so detect's provider branch fires");
267
+ assert.equal(windows[0]?.title, "Weekly sync", "the provider-supplied title is carried onto the window");
268
+ // No app spans and only one non-wearer speaker: the provider branch is the
269
+ // ONLY path to a meeting. Before the fix the window carried neither
270
+ // providerMeeting nor title, so nothing was detected.
271
+ const detected = detectMeetings({ date: DATE, appSpans: [], audioWindows: windows });
272
+ assert.equal(detected.length, 1);
273
+ assert.equal(detected[0]?.detectionSource, "provider");
274
+ assert.equal(detected[0]?.title, "Weekly sync");
275
+ });
@@ -0,0 +1,352 @@
1
+ /**
2
+ * Concrete meetings day source (issue #1900) — the wiring slice that feeds the
3
+ * builder from the ESTABLISHED stores rather than fixtures:
4
+ * - screen activity snapshots (the activity SQLite store), and
5
+ * - wearable day transcripts (the wearable day-store), reconstructed into the
6
+ * shared fusion conversation shape via `reconstructFusionInputs`.
7
+ *
8
+ * All timestamps stay UTC and every day read is a half-open [start, end)
9
+ * window resolved by `activityDayWindow(date, timezone)`, so the local day the
10
+ * caller asked for maps to one deterministic UTC window shared by both signals.
11
+ *
12
+ * Fidelity caveat (inherited, documented): a wearable day transcript is stored
13
+ * RENDERED at minute precision, so `reconstructFusionInputs` rebuilds each clock
14
+ * as `${date}THH:MM:00Z` — the same reconstruction the fusion subsystem uses.
15
+ * App spans come from true-UTC activity instants; the two frames coincide when
16
+ * the activity timezone is UTC (the default). Under a non-UTC timezone the
17
+ * app+audio overlap carries that reconstruction offset, exactly as wearable
18
+ * fusion does; audio-only detection is unaffected. Provider-meeting boundaries
19
+ * (Granola/Fireflies explicit titles) are a connector concern not present in the
20
+ * reconstructed day transcript, so audio windows never fabricate a provider flag.
21
+ */
22
+
23
+ import { activityDayWindow, isValidActivityDate, timezoneOffsetIso } from "../activity/digest.js";
24
+ import type { ActivitySnapshot } from "../activity/types.js";
25
+ import { bodyIsEscaped, parseDayTranscript } from "../wearables/day-store.js";
26
+ import { reconstructFusionInputs } from "../wearables/fusion/index.js";
27
+ import type { FusionConversationInput, FusionSegmentInput } from "../wearables/fusion/types.js";
28
+ import type { MeetingDayData, MeetingsDaySource } from "./build.js";
29
+ import { MeetingsInputError } from "./errors.js";
30
+ import type {
31
+ MeetingActivitySnapshot,
32
+ MeetingAppSpan,
33
+ MeetingAudioWindow,
34
+ MeetingsConfig,
35
+ } from "./types.js";
36
+
37
+ /** Reads the day's screen-activity snapshots. Satisfied by `ActivityStore`. */
38
+ export interface MeetingsActivityReader {
39
+ /** Snapshots whose capture instant is in the half-open [start, end) window. */
40
+ listSnapshotsForDay(
41
+ machine: string | null,
42
+ startUtcInclusive: string,
43
+ endUtcExclusive: string,
44
+ ): ActivitySnapshot[] | Promise<ActivitySnapshot[]>;
45
+ }
46
+
47
+ /** One stored wearable day transcript body, ready for reconstruction. */
48
+ export interface WearableDayBody {
49
+ source: string;
50
+ body: string;
51
+ escaped?: boolean;
52
+ /**
53
+ * IANA timezone the body's `HH:MM` clocks were rendered in (from the
54
+ * transcript frontmatter). Empty when the transcript stored no timezone —
55
+ * the clocks are then treated as UTC, matching the fusion identity guard.
56
+ */
57
+ timezone?: string;
58
+ }
59
+
60
+ /** Reads the day's rendered wearable transcript bodies (one per source). */
61
+ export interface MeetingsWearableReader {
62
+ readDayBodies(date: string): Promise<WearableDayBody[]>;
63
+ }
64
+
65
+ /** Narrow slice of `StorageManager` the wearable reader needs. */
66
+ export interface WearableDayTranscriptStore {
67
+ listWearableTranscriptDays(
68
+ sourceId?: string,
69
+ ): Promise<Array<{ source: string; date: string }>>;
70
+ readWearableDayTranscript(sourceId: string, date: string): Promise<string | null>;
71
+ }
72
+
73
+ /**
74
+ * Build a wearable reader over a storage manager: discover which sources stored
75
+ * a transcript for the day (store-driven, not config-driven, so a day is read
76
+ * exactly from what exists on disk), then read + parse each body.
77
+ */
78
+ export function storageWearableDayReader(store: WearableDayTranscriptStore): MeetingsWearableReader {
79
+ return {
80
+ async readDayBodies(date: string): Promise<WearableDayBody[]> {
81
+ const days = await store.listWearableTranscriptDays();
82
+ const sources = [...new Set(days.filter((d) => d.date === date).map((d) => d.source))].sort();
83
+ const bodies: WearableDayBody[] = [];
84
+ for (const source of sources) {
85
+ const raw = await store.readWearableDayTranscript(source, date);
86
+ if (raw === null) continue;
87
+ const parsed = parseDayTranscript(raw);
88
+ bodies.push({
89
+ source,
90
+ body: parsed?.body ?? raw,
91
+ escaped: bodyIsEscaped(parsed?.meta),
92
+ timezone: parsed?.meta.timezone ?? "",
93
+ });
94
+ }
95
+ return bodies;
96
+ },
97
+ };
98
+ }
99
+
100
+ /** Map an activity snapshot to the redaction-safe screen snapshot fusion sees. */
101
+ function toMeetingActivitySnapshot(snapshot: ActivitySnapshot): MeetingActivitySnapshot {
102
+ const out: MeetingActivitySnapshot = { tsUtc: snapshot.capturedAtUtc, app: snapshot.app };
103
+ if (snapshot.windowTitle.length > 0) out.title = snapshot.windowTitle;
104
+ if (snapshot.browserUrl !== undefined && snapshot.browserUrl.length > 0) out.url = snapshot.browserUrl;
105
+ if (snapshot.text.length > 0) out.text = snapshot.text;
106
+ return out;
107
+ }
108
+
109
+ /**
110
+ * Fallback dwell for a lone trailing meeting-app tick on a machine that
111
+ * captured no other snapshot to estimate a cadence from. Five minutes keeps
112
+ * the inferred span above the default `minOverlapMinutes`, so a single
113
+ * end-of-window tick with overlapping audio still pairs.
114
+ */
115
+ const LONE_TRAILING_SPAN_MS = 5 * 60_000;
116
+
117
+ /**
118
+ * Estimate a machine's capture cadence as the median positive gap between its
119
+ * consecutive (time-sorted) snapshots; falls back to {@link LONE_TRAILING_SPAN_MS}
120
+ * when there is no gap to observe (a single snapshot on the machine).
121
+ */
122
+ function estimateSamplingIntervalMs(sorted: readonly ActivitySnapshot[]): number {
123
+ const gaps: number[] = [];
124
+ let prevMs = Number.NaN;
125
+ for (const snapshot of sorted) {
126
+ const curMs = Date.parse(snapshot.capturedAtUtc);
127
+ if (Number.isFinite(prevMs) && Number.isFinite(curMs) && curMs > prevMs) gaps.push(curMs - prevMs);
128
+ prevMs = curMs;
129
+ }
130
+ if (gaps.length === 0) return LONE_TRAILING_SPAN_MS;
131
+ gaps.sort((a, b) => a - b);
132
+ const mid = Math.floor(gaps.length / 2);
133
+ const median = gaps.length % 2 === 0 ? (gaps[mid - 1]! + gaps[mid]!) / 2 : gaps[mid]!;
134
+ return Math.round(median);
135
+ }
136
+
137
+ /**
138
+ * Derive meeting-app foreground spans from the day's snapshots. A span is a
139
+ * maximal run of consecutive (time-ordered) snapshots on ONE capture machine
140
+ * whose app/title/url matches any configured pattern; the next non-matching
141
+ * snapshot ends the run AND bounds its end (so a lone matching snapshot still
142
+ * yields a non-zero span). A trailing run at the END of a machine's snapshots
143
+ * has no following non-matching tick to bound it, so a lone trailing tick is
144
+ * extended by the machine's sampling cadence rather than collapsing to a
145
+ * zero-length point the detector's `isFinitePair` rejects. Brief app switches
146
+ * during a call surface as adjacent spans and the detector's `mergeGapMinutes`
147
+ * re-merges them.
148
+ */
149
+ export function deriveAppSpans(
150
+ snapshots: readonly ActivitySnapshot[],
151
+ appPatterns: readonly string[],
152
+ ): MeetingAppSpan[] {
153
+ const patterns = appPatterns.map((p) => p.toLowerCase()).filter((p) => p.length > 0);
154
+ if (patterns.length === 0) return [];
155
+ const byCaptureTime = (a: ActivitySnapshot, b: ActivitySnapshot): number => {
156
+ const at = Date.parse(a.capturedAtUtc);
157
+ const bt = Date.parse(b.capturedAtUtc);
158
+ if (Number.isFinite(at) && Number.isFinite(bt) && at !== bt) return at < bt ? -1 : 1;
159
+ return a.capturedAtUtc < b.capturedAtUtc ? -1 : a.capturedAtUtc > b.capturedAtUtc ? 1 : 0;
160
+ };
161
+ // Group by capture machine: a global time sort otherwise lets a non-matching
162
+ // snapshot from ANOTHER machine (a desktop's Chrome tick) close a run on THIS
163
+ // machine (a laptop in Zoom), splitting one call into fragments. Runs are
164
+ // derived per-machine and merged into one span list.
165
+ const byMachine = new Map<string, ActivitySnapshot[]>();
166
+ for (const snapshot of snapshots) {
167
+ const list = byMachine.get(snapshot.machine);
168
+ if (list === undefined) byMachine.set(snapshot.machine, [snapshot]);
169
+ else list.push(snapshot);
170
+ }
171
+ const spans: MeetingAppSpan[] = [];
172
+ for (const machineSnaps of byMachine.values()) {
173
+ const sorted = [...machineSnaps].sort(byCaptureTime);
174
+ const intervalMs = estimateSamplingIntervalMs(sorted);
175
+ let run: MeetingAppSpan | null = null;
176
+ for (const snapshot of sorted) {
177
+ const hay = `${snapshot.app}\n${snapshot.windowTitle}\n${snapshot.browserUrl ?? ""}`.toLowerCase();
178
+ if (patterns.some((p) => hay.includes(p))) {
179
+ if (run === null) {
180
+ run = { app: snapshot.app, startUtc: snapshot.capturedAtUtc, endUtc: snapshot.capturedAtUtc };
181
+ } else {
182
+ run.endUtc = snapshot.capturedAtUtc;
183
+ }
184
+ } else if (run !== null) {
185
+ // End at this next (non-matching) snapshot rather than the last matching
186
+ // one: a single matching snapshot then yields a real [start, next) span
187
+ // instead of a zero-length point the detector's isFinitePair rejects.
188
+ run.endUtc = snapshot.capturedAtUtc;
189
+ spans.push(run);
190
+ run = null;
191
+ }
192
+ }
193
+ if (run !== null) {
194
+ // End-of-array trailing run: with no following non-matching snapshot to
195
+ // bound it, a lone matching tick collapses to a zero-length point that
196
+ // detect's isFinitePair drops. Infer the missing next tick at the
197
+ // machine's sampling cadence so an end-of-window Zoom/Teams tick still
198
+ // yields a finite span that pairs with overlapping audio.
199
+ if (run.endUtc === run.startUtc) {
200
+ const startMs = Date.parse(run.startUtc);
201
+ if (Number.isFinite(startMs)) run.endUtc = new Date(startMs + intervalMs).toISOString();
202
+ }
203
+ spans.push(run);
204
+ }
205
+ }
206
+ spans.sort((a, b) =>
207
+ a.startUtc < b.startUtc ? -1 : a.startUtc > b.startUtc ? 1
208
+ : a.app < b.app ? -1 : a.app > b.app ? 1
209
+ : a.endUtc < b.endUtc ? -1 : a.endUtc > b.endUtc ? 1 : 0,
210
+ );
211
+ return spans;
212
+ }
213
+
214
+ /**
215
+ * Convert an instant the transcript renderer wrote as `...Z` — whose `HH:MM`
216
+ * clock is really local to `timezone`, but which reconstruct labelled UTC —
217
+ * into the true UTC instant. An empty/UTC zone or an unresolvable offset
218
+ * returns the input unchanged, so a transcript with no stored zone keeps UTC
219
+ * semantics (matching the fusion identity guard).
220
+ */
221
+ function wallClockZToUtc(iso: string, timezone: string): string {
222
+ if (timezone === "" || timezone === "UTC") return iso;
223
+ const assumedMs = Date.parse(iso);
224
+ if (!Number.isFinite(assumedMs)) return iso;
225
+ let offset: string;
226
+ try {
227
+ offset = timezoneOffsetIso(new Date(assumedMs), timezone);
228
+ } catch {
229
+ return iso;
230
+ }
231
+ if (offset === "+00:00") return iso;
232
+ const wall = iso.endsWith("Z") ? iso.slice(0, -1) : iso;
233
+ const trueMs = Date.parse(`${wall}${offset}`);
234
+ return Number.isFinite(trueMs) ? new Date(trueMs).toISOString() : iso;
235
+ }
236
+
237
+ /**
238
+ * Re-anchor reconstructed conversations from their rendered (assumed-UTC) clocks
239
+ * to true UTC using each source's transcript timezone, so audio windows line up
240
+ * with the true-UTC screen snapshots instead of drifting by the zone offset
241
+ * (a 09:00 America/Chicago call must land at 14:00Z, not 09:00Z).
242
+ */
243
+ function shiftFusionInputsToUtc(
244
+ conversations: readonly FusionConversationInput[],
245
+ tzBySource: ReadonlyMap<string, string>,
246
+ ): FusionConversationInput[] {
247
+ return conversations.map((conversation) => {
248
+ const tz = tzBySource.get(conversation.source) ?? "";
249
+ if (tz === "" || tz === "UTC") return conversation;
250
+ const shifted: FusionConversationInput = {
251
+ ...conversation,
252
+ startIso: wallClockZToUtc(conversation.startIso, tz),
253
+ segments: conversation.segments.map((segment) => {
254
+ const out: FusionSegmentInput = { ...segment };
255
+ if (segment.startIso !== undefined) out.startIso = wallClockZToUtc(segment.startIso, tz);
256
+ if (segment.endIso !== undefined) out.endIso = wallClockZToUtc(segment.endIso, tz);
257
+ return out;
258
+ }),
259
+ };
260
+ if (conversation.endIso !== undefined) shifted.endIso = wallClockZToUtc(conversation.endIso, tz);
261
+ return shifted;
262
+ });
263
+ }
264
+
265
+ /**
266
+ * Cloud meeting-provider sources that supply explicit meeting boundaries and
267
+ * titles (Granola/Fireflies): such a conversation is a meeting on its own, so
268
+ * its audio window is marked `providerMeeting` to fire detect's provider branch
269
+ * even with no matching app span.
270
+ */
271
+ const PROVIDER_MEETING_SOURCES: Record<string, true> = { granola: true, fireflies: true };
272
+
273
+ /**
274
+ * Derive audio windows from reconstructed conversations. Each conversation is
275
+ * one window on its source; `distinctNonWearerSpeakers` counts distinct
276
+ * non-self speaker labels (the audio-only detection rule), and the end is the
277
+ * conversation end when known, else the latest segment instant, else the start.
278
+ */
279
+ export function buildAudioWindows(
280
+ conversations: readonly FusionConversationInput[],
281
+ ): MeetingAudioWindow[] {
282
+ return conversations.map((conversation) => {
283
+ const speakers = new Set<string>();
284
+ let maxEndMs = Date.parse(conversation.startIso);
285
+ for (const segment of conversation.segments) {
286
+ if (!segment.isSelf && segment.speaker.length > 0) speakers.add(segment.speaker);
287
+ for (const iso of [segment.endIso, segment.startIso]) {
288
+ if (iso === undefined) continue;
289
+ const ms = Date.parse(iso);
290
+ if (Number.isFinite(ms) && (!Number.isFinite(maxEndMs) || ms > maxEndMs)) maxEndMs = ms;
291
+ }
292
+ }
293
+ const endUtc =
294
+ conversation.endIso ??
295
+ (Number.isFinite(maxEndMs) ? new Date(maxEndMs).toISOString() : conversation.startIso);
296
+ const audioWindow: MeetingAudioWindow = {
297
+ source: conversation.source,
298
+ startUtc: conversation.startIso,
299
+ endUtc,
300
+ distinctNonWearerSpeakers: speakers.size,
301
+ };
302
+ // Carry the provider-supplied title so app+audio/audio/provider candidates
303
+ // can label the meeting; mark known cloud meeting providers so a titled
304
+ // provider conversation with no matching app span still fires detect's
305
+ // provider branch (a meeting on its own).
306
+ if (conversation.title !== undefined && conversation.title.length > 0) {
307
+ audioWindow.title = conversation.title;
308
+ }
309
+ if (PROVIDER_MEETING_SOURCES[conversation.source]) audioWindow.providerMeeting = true;
310
+ return audioWindow;
311
+ });
312
+ }
313
+
314
+ /**
315
+ * Reads a day's real activity + wearable signals and assembles the
316
+ * {@link MeetingDayData} the builder consumes. Injected readers keep this
317
+ * testable against fixtures and free of direct filesystem access.
318
+ */
319
+ export class ActivityWearablesMeetingsDaySource implements MeetingsDaySource {
320
+ constructor(
321
+ private readonly deps: {
322
+ /**
323
+ * Screen-activity reader. Optional (issue #2123): only the machine-owner
324
+ * (default) namespace consumes machine-scoped activity; every other caller
325
+ * namespace runs audio-only, so it is constructed with no activity reader
326
+ * and the load degrades to zero screen snapshots.
327
+ */
328
+ activity?: MeetingsActivityReader;
329
+ wearables: MeetingsWearableReader;
330
+ config: MeetingsConfig;
331
+ /** IANA timezone resolving the local day → UTC window (activity's zone). */
332
+ timezone: string;
333
+ },
334
+ ) {}
335
+
336
+ async loadDayData(date: string): Promise<MeetingDayData> {
337
+ if (!isValidActivityDate(date)) {
338
+ throw new MeetingsInputError(`invalid day '${date}' — expected a real YYYY-MM-DD`);
339
+ }
340
+ const { startUtc, endUtc } = activityDayWindow(date, this.deps.timezone);
341
+ const snapshots = this.deps.activity
342
+ ? await this.deps.activity.listSnapshotsForDay(null, startUtc, endUtc)
343
+ : [];
344
+ const activity = snapshots.map(toMeetingActivitySnapshot);
345
+ const appSpans = deriveAppSpans(snapshots, this.deps.config.appPatterns);
346
+ const bodies = await this.deps.wearables.readDayBodies(date);
347
+ const tzBySource = new Map(bodies.map((b) => [b.source, b.timezone ?? ""] as const));
348
+ const conversations = shiftFusionInputsToUtc(reconstructFusionInputs(date, bodies), tzBySource);
349
+ const audioWindows = buildAudioWindows(conversations);
350
+ return { detection: { date, appSpans, audioWindows }, conversations, activity };
351
+ }
352
+ }