@remnic/core 9.19.0 → 9.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (306) hide show
  1. package/dist/access-admin-ops-surface.d.ts +8 -8
  2. package/dist/access-admin-ops-surface.js +8 -8
  3. package/dist/access-boundary.d.ts +8 -8
  4. package/dist/access-boundary.js +8 -8
  5. package/dist/access-cli.js +32 -32
  6. package/dist/access-http.d.ts +8 -8
  7. package/dist/access-http.js +12 -12
  8. package/dist/access-identity-continuity-surface.d.ts +6 -6
  9. package/dist/access-identity-continuity-surface.js +8 -8
  10. package/dist/access-lcm-surface.d.ts +8 -8
  11. package/dist/access-lcm-surface.js +8 -8
  12. package/dist/access-mcp.d.ts +8 -8
  13. package/dist/access-mcp.js +11 -11
  14. package/dist/access-memory-search-fanout.d.ts +2 -2
  15. package/dist/{access-namespace-preflight-BsawJa-L.d.ts → access-namespace-preflight-tmwQgCT6.d.ts} +1 -1
  16. package/dist/access-namespace-preflight.d.ts +3 -3
  17. package/dist/access-namespace-preflight.js +8 -8
  18. package/dist/access-observe-write-surface.d.ts +8 -8
  19. package/dist/access-observe-write-surface.js +8 -8
  20. package/dist/access-operations-batch.js +9 -9
  21. package/dist/access-operations.d.ts +8 -8
  22. package/dist/access-operations.js +10 -10
  23. package/dist/access-recall-concurrency.d.ts +8 -8
  24. package/dist/access-recall-concurrency.js +8 -8
  25. package/dist/access-recall-response.d.ts +8 -8
  26. package/dist/access-recall-response.js +8 -8
  27. package/dist/access-recall-surface.d.ts +8 -8
  28. package/dist/access-recall-surface.js +8 -8
  29. package/dist/{access-service-DLuVnV6a.d.ts → access-service-BqUii7LY.d.ts} +6 -6
  30. package/dist/access-service.d.ts +8 -8
  31. package/dist/access-service.js +8 -8
  32. package/dist/access-surface-catalog.d.ts +8 -8
  33. package/dist/action-confidence.d.ts +1 -1
  34. package/dist/active-memory-bridge.d.ts +1 -1
  35. package/dist/active-recall.d.ts +1 -1
  36. package/dist/active-recall.js +1 -1
  37. package/dist/{auto-sync-IH3G25MT.js → auto-sync-UPTUVE4X.js} +3 -3
  38. package/dist/behavior-learner.d.ts +1 -1
  39. package/dist/behavior-signals.d.ts +1 -1
  40. package/dist/bootstrap.d.ts +6 -6
  41. package/dist/briefing.d.ts +2 -2
  42. package/dist/briefing.js +4 -4
  43. package/dist/buffer-surprise-report.d.ts +1 -1
  44. package/dist/buffer.d.ts +2 -2
  45. package/dist/bulk-import/index.d.ts +3 -3
  46. package/dist/calibration.d.ts +1 -1
  47. package/dist/capabilities.d.ts +1 -1
  48. package/dist/{catalog-O6P8694A.d.ts → catalog-iWvWz61I.d.ts} +1 -1
  49. package/dist/causal-behavior.d.ts +1 -1
  50. package/dist/causal-consolidation.d.ts +1 -1
  51. package/dist/causal-consolidation.js +5 -5
  52. package/dist/causal-trajectory-graph.d.ts +1 -1
  53. package/dist/{chunk-YUWFASAK.js → chunk-275ENJDK.js} +2 -2
  54. package/dist/{chunk-EZFVVEQG.js → chunk-3ZUY2VQF.js} +2 -2
  55. package/dist/{chunk-HRREDJSL.js → chunk-4F6AANKS.js} +3 -3
  56. package/dist/{chunk-U7D5HWNG.js → chunk-4PYLBXX6.js} +2 -2
  57. package/dist/{chunk-3LZSS77C.js → chunk-53WDAG7X.js} +2 -2
  58. package/dist/{chunk-QEEMLGCO.js → chunk-5IXPWEQB.js} +5 -5
  59. package/dist/{chunk-VSVX777Y.js → chunk-5JQLAQUZ.js} +2 -2
  60. package/dist/{chunk-DR65EEXG.js → chunk-62QM5MKY.js} +7 -7
  61. package/dist/{chunk-WYL5ODHJ.js → chunk-6NZ7U4R4.js} +1 -1
  62. package/dist/chunk-6NZ7U4R4.js.map +1 -0
  63. package/dist/{chunk-OFQUY5NZ.js → chunk-7M6APKJL.js} +331 -5
  64. package/dist/chunk-7M6APKJL.js.map +1 -0
  65. package/dist/{chunk-OOJQ6ID5.js → chunk-AT3TDN6O.js} +1017 -296
  66. package/dist/chunk-AT3TDN6O.js.map +1 -0
  67. package/dist/{chunk-DQCUBRTZ.js → chunk-B7C3TF5H.js} +2 -2
  68. package/dist/{chunk-RPOOJPMB.js → chunk-EILX7SQR.js} +3 -3
  69. package/dist/{chunk-D5H3B36B.js → chunk-EXU5IQ3S.js} +2 -2
  70. package/dist/{chunk-KUF7OIFK.js → chunk-EZKKHVFY.js} +2 -2
  71. package/dist/{chunk-S5HCKQ7Q.js → chunk-GEUDRENF.js} +2 -2
  72. package/dist/{chunk-X7GOH7Y6.js → chunk-HJEEAO4O.js} +2 -2
  73. package/dist/{chunk-FL7MNSH3.js → chunk-HPOY2PHQ.js} +3 -3
  74. package/dist/{chunk-Q55XM4SU.js → chunk-JFDEYXF7.js} +2 -2
  75. package/dist/{chunk-UVSDFM7R.js → chunk-JN3IUA2Q.js} +2 -2
  76. package/dist/{chunk-PLRDZ2OO.js → chunk-JWMYVQAM.js} +2 -2
  77. package/dist/{chunk-KOFVOVJG.js → chunk-KHRKEELC.js} +17 -17
  78. package/dist/{chunk-Y4JHE2ML.js → chunk-LI4ZOFHW.js} +2 -2
  79. package/dist/{chunk-MLW3J7IC.js → chunk-LMHDFOVV.js} +3 -3
  80. package/dist/{chunk-X6E5WAGX.js → chunk-N7EVMKP6.js} +6 -6
  81. package/dist/{chunk-HCNG42P3.js → chunk-Q3FWRDCP.js} +2 -2
  82. package/dist/{chunk-5MFHOX5T.js → chunk-QO2KODU6.js} +2 -2
  83. package/dist/{chunk-W3DJOT22.js → chunk-TCZWBJBO.js} +2 -2
  84. package/dist/{chunk-B3ABVQ36.js → chunk-UHVXE24Y.js} +2 -1
  85. package/dist/{chunk-UX3ZXN34.js → chunk-UV72DEZD.js} +2 -2
  86. package/dist/{chunk-OZSL2QZC.js → chunk-V5TSUGRB.js} +5 -5
  87. package/dist/{chunk-CH3BRJ3P.js → chunk-X5QS5C2O.js} +2 -2
  88. package/dist/{cli-0dQHR6e6.d.ts → cli-DPEhq0Cp.d.ts} +5 -5
  89. package/dist/cli.d.ts +10 -10
  90. package/dist/cli.js +25 -25
  91. package/dist/compounding/engine.d.ts +2 -2
  92. package/dist/compounding/engine.js +4 -4
  93. package/dist/compounding/preference-consolidator.d.ts +1 -1
  94. package/dist/compression-optimizer.d.ts +1 -1
  95. package/dist/config.d.ts +1 -1
  96. package/dist/config.js +1 -1
  97. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  98. package/dist/connectors/codex-materialize-runner.js +4 -4
  99. package/dist/connectors/codex-materialize.d.ts +1 -1
  100. package/dist/connectors/index.d.ts +1 -1
  101. package/dist/connectors/index.js +4 -4
  102. package/dist/consolidation-provenance-check.d.ts +2 -2
  103. package/dist/consolidation-undo.d.ts +2 -2
  104. package/dist/contradiction/index.d.ts +2 -2
  105. package/dist/conversation-index/backend.d.ts +1 -1
  106. package/dist/conversation-index/chunker.d.ts +1 -1
  107. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  108. package/dist/conversation-index/indexer.d.ts +1 -1
  109. package/dist/conversation-index/search.d.ts +1 -1
  110. package/dist/day-summary.d.ts +1 -1
  111. package/dist/delinearize.d.ts +1 -1
  112. package/dist/direct-answer-wiring.d.ts +1 -1
  113. package/dist/direct-answer.d.ts +1 -1
  114. package/dist/embedding-fallback.d.ts +1 -1
  115. package/dist/enrichment/index.d.ts +1 -1
  116. package/dist/entity-retrieval.d.ts +2 -2
  117. package/dist/entity-retrieval.js +4 -4
  118. package/dist/entity-schema.d.ts +1 -1
  119. package/dist/explicit-capture.d.ts +6 -6
  120. package/dist/extraction-faithfulness.d.ts +1 -1
  121. package/dist/extraction-judge-telemetry.d.ts +1 -1
  122. package/dist/extraction-judge-training.d.ts +1 -1
  123. package/dist/extraction-judge.d.ts +1 -1
  124. package/dist/extraction.d.ts +1 -1
  125. package/dist/fallback-llm.d.ts +1 -1
  126. package/dist/graph-dashboard-diff.d.ts +1 -1
  127. package/dist/graph-dashboard-key.d.ts +1 -1
  128. package/dist/graph-dashboard-parser.d.ts +1 -1
  129. package/dist/graph-edge-reinforcement.d.ts +1 -1
  130. package/dist/graph-snapshot.d.ts +1 -1
  131. package/dist/graph.d.ts +1 -1
  132. package/dist/importance.d.ts +1 -1
  133. package/dist/importers/index.d.ts +1 -1
  134. package/dist/in-flight-reads.d.ts +1 -1
  135. package/dist/index.d.ts +468 -88
  136. package/dist/index.js +66 -36
  137. package/dist/intent.d.ts +1 -1
  138. package/dist/lcm/engine.d.ts +1 -1
  139. package/dist/lcm/index.d.ts +1 -1
  140. package/dist/lcm/tools.d.ts +1 -1
  141. package/dist/lifecycle.d.ts +1 -1
  142. package/dist/live-connectors-runner.d.ts +1 -1
  143. package/dist/local-llm.d.ts +1 -1
  144. package/dist/local-model-endpoint.d.ts +1 -1
  145. package/dist/maintenance/memory-governance.d.ts +1 -1
  146. package/dist/maintenance/memory-governance.js +4 -4
  147. package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +2 -2
  148. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +4 -4
  149. package/dist/maintenance/rebuild-memory-projection.d.ts +2 -2
  150. package/dist/maintenance/rebuild-memory-projection.js +5 -5
  151. package/dist/mcp-memory-inspector-app.d.ts +8 -8
  152. package/dist/memory-action-policy.d.ts +1 -1
  153. package/dist/memory-cache.d.ts +1 -1
  154. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  155. package/dist/memory-projection-store.d.ts +1 -1
  156. package/dist/memory-provenance.d.ts +1 -1
  157. package/dist/memory-worth-outcomes.d.ts +2 -2
  158. package/dist/models-json.d.ts +1 -1
  159. package/dist/namespaces/migrate.d.ts +3 -3
  160. package/dist/namespaces/migrate.js +5 -5
  161. package/dist/namespaces/principal.d.ts +1 -1
  162. package/dist/namespaces/search.d.ts +1 -1
  163. package/dist/namespaces/storage.d.ts +3 -3
  164. package/dist/namespaces/storage.js +4 -4
  165. package/dist/native-knowledge.d.ts +1 -1
  166. package/dist/offline-sync-impression-drain.d.ts +1 -1
  167. package/dist/operator-toolkit.d.ts +2 -2
  168. package/dist/operator-toolkit.js +9 -9
  169. package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -2
  170. package/dist/orchestration/maintenance.d.ts +3 -3
  171. package/dist/orchestration/maintenance.js +9 -9
  172. package/dist/{orchestrator-B8ickLkd.d.ts → orchestrator-DB93S48I.d.ts} +5 -5
  173. package/dist/orchestrator.d.ts +6 -6
  174. package/dist/orchestrator.js +32 -32
  175. package/dist/patterns-cli.d.ts +1 -1
  176. package/dist/{pipeline-B669unGm.d.ts → pipeline-DmGc-H66.d.ts} +1 -1
  177. package/dist/policy-runtime.d.ts +1 -1
  178. package/dist/proactive-contention.d.ts +1 -1
  179. package/dist/provenance.d.ts +1 -1
  180. package/dist/{qmd-D0PVXmea.d.ts → qmd-cJT0crkF.d.ts} +1 -1
  181. package/dist/qmd-preflight.d.ts +2 -2
  182. package/dist/qmd-recall-cache.d.ts +1 -1
  183. package/dist/qmd.d.ts +2 -2
  184. package/dist/recall-concurrency-config.d.ts +1 -1
  185. package/dist/recall-disclosure-escalation.d.ts +1 -1
  186. package/dist/recall-explain-renderer.d.ts +1 -1
  187. package/dist/recall-explain-renderer.js +3 -3
  188. package/dist/recall-memory-map.d.ts +1 -1
  189. package/dist/recall-planner-llm.d.ts +1 -1
  190. package/dist/recall-state.d.ts +1 -1
  191. package/dist/recall-tag-filter.d.ts +1 -1
  192. package/dist/recall-timings.d.ts +1 -1
  193. package/dist/recall-xray-cli.d.ts +1 -1
  194. package/dist/recall-xray-cli.js +4 -4
  195. package/dist/recall-xray-renderer.d.ts +1 -1
  196. package/dist/recall-xray-renderer.js +3 -3
  197. package/dist/recall-xray.d.ts +1 -1
  198. package/dist/recall-xray.js +2 -2
  199. package/dist/replay/normalizers/chatgpt.d.ts +1 -1
  200. package/dist/replay/normalizers/claude.d.ts +1 -1
  201. package/dist/replay/normalizers/openclaw.d.ts +1 -1
  202. package/dist/replay/normalizers/shared.d.ts +1 -1
  203. package/dist/replay/runner.d.ts +1 -1
  204. package/dist/replay/types.d.ts +1 -1
  205. package/dist/resolve-auth-token.d.ts +1 -1
  206. package/dist/resume-bundles.js +2 -2
  207. package/dist/retrieval-agents.d.ts +2 -2
  208. package/dist/retrieval-tiers.d.ts +1 -1
  209. package/dist/routing/engine.d.ts +1 -1
  210. package/dist/routing/store.d.ts +1 -1
  211. package/dist/salvage-envelope.d.ts +1 -1
  212. package/dist/{scope-profiles-BOp3gSzp.d.ts → scope-profiles-KG2nODX7.d.ts} +1 -1
  213. package/dist/search/embed-helper.d.ts +1 -1
  214. package/dist/search/factory.d.ts +1 -1
  215. package/dist/search/index.d.ts +1 -1
  216. package/dist/search/lancedb-backend.d.ts +1 -1
  217. package/dist/search/meilisearch-backend.d.ts +1 -1
  218. package/dist/search/noop-backend.d.ts +1 -1
  219. package/dist/search/orama-backend.d.ts +1 -1
  220. package/dist/search/port.d.ts +1 -1
  221. package/dist/search/remote-backend.d.ts +1 -1
  222. package/dist/{semantic-consolidation-C7SZNzUJ.d.ts → semantic-consolidation-BaZlg0D8.d.ts} +1 -1
  223. package/dist/semantic-consolidation.d.ts +2 -2
  224. package/dist/semantic-consolidation.js +5 -5
  225. package/dist/semantic-rule-promotion.js +4 -4
  226. package/dist/semantic-rule-verifier.d.ts +1 -1
  227. package/dist/semantic-rule-verifier.js +4 -4
  228. package/dist/session-observer-bands.d.ts +1 -1
  229. package/dist/session-observer-state.d.ts +1 -1
  230. package/dist/shared-context/manager.d.ts +1 -1
  231. package/dist/signal.d.ts +1 -1
  232. package/dist/{storage-CJ_Fia4H.d.ts → storage-BKhHHnRo.d.ts} +2 -221
  233. package/dist/storage.d.ts +2 -2
  234. package/dist/storage.js +3 -3
  235. package/dist/summarizer.d.ts +1 -1
  236. package/dist/summary-snapshot.d.ts +1 -1
  237. package/dist/temporal-supersession.d.ts +2 -2
  238. package/dist/temporal-timeline-recall.d.ts +1 -1
  239. package/dist/temporal-validity.d.ts +1 -1
  240. package/dist/threading.d.ts +1 -1
  241. package/dist/tier-migration.d.ts +2 -2
  242. package/dist/tier-routing.d.ts +1 -1
  243. package/dist/topics.d.ts +1 -1
  244. package/dist/transcript.d.ts +1 -1
  245. package/dist/trust-score-stage.d.ts +1 -1
  246. package/dist/trust-score.d.ts +1 -1
  247. package/dist/{types-GeWGXq9E.d.ts → types-GBQZuT7l.d.ts} +395 -1
  248. package/dist/types.d.ts +1 -1
  249. package/dist/types.js +1 -1
  250. package/dist/utility-runtime.d.ts +1 -1
  251. package/dist/verified-recall.js +4 -4
  252. package/dist/write-envelope.d.ts +1 -1
  253. package/package.json +2 -2
  254. package/src/config.ts +3 -3
  255. package/src/meetings/build.test.ts +646 -0
  256. package/src/meetings/build.ts +365 -0
  257. package/src/meetings/cli.test.ts +276 -0
  258. package/src/meetings/cli.ts +242 -0
  259. package/src/meetings/config.test.ts +72 -0
  260. package/src/meetings/config.ts +183 -0
  261. package/src/meetings/errors.ts +13 -0
  262. package/src/meetings/fuse.test.ts +208 -0
  263. package/src/meetings/fuse.ts +297 -0
  264. package/src/meetings/index.ts +11 -1
  265. package/src/meetings/memory-generator.ts +116 -0
  266. package/src/meetings/recall-exclusion.test.ts +88 -0
  267. package/src/meetings/store.test.ts +232 -0
  268. package/src/meetings/store.ts +465 -0
  269. package/src/meetings/types.ts +111 -0
  270. package/src/orchestration/orchestrator-helpers.ts +23 -3
  271. package/src/orchestration/recall-search-pipeline.test.ts +182 -0
  272. package/src/orchestration/recall-search-pipeline.ts +36 -15
  273. package/src/types.ts +6 -0
  274. package/dist/chunk-OFQUY5NZ.js.map +0 -1
  275. package/dist/chunk-OOJQ6ID5.js.map +0 -1
  276. package/dist/chunk-WYL5ODHJ.js.map +0 -1
  277. /package/dist/{auto-sync-IH3G25MT.js.map → auto-sync-UPTUVE4X.js.map} +0 -0
  278. /package/dist/{chunk-YUWFASAK.js.map → chunk-275ENJDK.js.map} +0 -0
  279. /package/dist/{chunk-EZFVVEQG.js.map → chunk-3ZUY2VQF.js.map} +0 -0
  280. /package/dist/{chunk-HRREDJSL.js.map → chunk-4F6AANKS.js.map} +0 -0
  281. /package/dist/{chunk-U7D5HWNG.js.map → chunk-4PYLBXX6.js.map} +0 -0
  282. /package/dist/{chunk-3LZSS77C.js.map → chunk-53WDAG7X.js.map} +0 -0
  283. /package/dist/{chunk-QEEMLGCO.js.map → chunk-5IXPWEQB.js.map} +0 -0
  284. /package/dist/{chunk-VSVX777Y.js.map → chunk-5JQLAQUZ.js.map} +0 -0
  285. /package/dist/{chunk-DR65EEXG.js.map → chunk-62QM5MKY.js.map} +0 -0
  286. /package/dist/{chunk-DQCUBRTZ.js.map → chunk-B7C3TF5H.js.map} +0 -0
  287. /package/dist/{chunk-RPOOJPMB.js.map → chunk-EILX7SQR.js.map} +0 -0
  288. /package/dist/{chunk-D5H3B36B.js.map → chunk-EXU5IQ3S.js.map} +0 -0
  289. /package/dist/{chunk-KUF7OIFK.js.map → chunk-EZKKHVFY.js.map} +0 -0
  290. /package/dist/{chunk-S5HCKQ7Q.js.map → chunk-GEUDRENF.js.map} +0 -0
  291. /package/dist/{chunk-X7GOH7Y6.js.map → chunk-HJEEAO4O.js.map} +0 -0
  292. /package/dist/{chunk-FL7MNSH3.js.map → chunk-HPOY2PHQ.js.map} +0 -0
  293. /package/dist/{chunk-Q55XM4SU.js.map → chunk-JFDEYXF7.js.map} +0 -0
  294. /package/dist/{chunk-UVSDFM7R.js.map → chunk-JN3IUA2Q.js.map} +0 -0
  295. /package/dist/{chunk-PLRDZ2OO.js.map → chunk-JWMYVQAM.js.map} +0 -0
  296. /package/dist/{chunk-KOFVOVJG.js.map → chunk-KHRKEELC.js.map} +0 -0
  297. /package/dist/{chunk-Y4JHE2ML.js.map → chunk-LI4ZOFHW.js.map} +0 -0
  298. /package/dist/{chunk-MLW3J7IC.js.map → chunk-LMHDFOVV.js.map} +0 -0
  299. /package/dist/{chunk-X6E5WAGX.js.map → chunk-N7EVMKP6.js.map} +0 -0
  300. /package/dist/{chunk-HCNG42P3.js.map → chunk-Q3FWRDCP.js.map} +0 -0
  301. /package/dist/{chunk-5MFHOX5T.js.map → chunk-QO2KODU6.js.map} +0 -0
  302. /package/dist/{chunk-W3DJOT22.js.map → chunk-TCZWBJBO.js.map} +0 -0
  303. /package/dist/{chunk-B3ABVQ36.js.map → chunk-UHVXE24Y.js.map} +0 -0
  304. /package/dist/{chunk-UX3ZXN34.js.map → chunk-UV72DEZD.js.map} +0 -0
  305. /package/dist/{chunk-OZSL2QZC.js.map → chunk-V5TSUGRB.js.map} +0 -0
  306. /package/dist/{chunk-CH3BRJ3P.js.map → chunk-X5QS5C2O.js.map} +0 -0
@@ -0,0 +1,208 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+
4
+ import { DEFAULT_MEETINGS_CONFIG } from "./config.js";
5
+ import { fuseMeeting } from "./fuse.js";
6
+ import type {
7
+ DetectedMeeting,
8
+ MeetingActivitySnapshot,
9
+ MeetingsConfig,
10
+ } from "./types.js";
11
+ import type { FusionConversationInput, FusionSegmentInput } from "../wearables/fusion/types.js";
12
+
13
+ const DATE = "2026-03-10";
14
+ const WINDOW_START = "2026-03-10T14:00:00.000Z";
15
+ const WINDOW_END = "2026-03-10T14:30:00.000Z";
16
+
17
+ function config(overrides: Partial<MeetingsConfig> = {}): MeetingsConfig {
18
+ return { ...DEFAULT_MEETINGS_CONFIG, appPatterns: [...DEFAULT_MEETINGS_CONFIG.appPatterns], ...overrides };
19
+ }
20
+
21
+ function meeting(overrides: Partial<DetectedMeeting> = {}): DetectedMeeting {
22
+ return {
23
+ id: "mtg-2026-03-10-abcdef01",
24
+ date: DATE,
25
+ startUtc: WINDOW_START,
26
+ endUtc: WINDOW_END,
27
+ app: "Zoom",
28
+ detectionSource: "app+audio",
29
+ sources: ["desktop"],
30
+ ...overrides,
31
+ };
32
+ }
33
+
34
+ function seg(
35
+ speaker: string,
36
+ isSelf: boolean,
37
+ text: string,
38
+ startIso: string,
39
+ endIso?: string,
40
+ ): FusionSegmentInput {
41
+ return { speaker, isSelf, text, startIso, ...(endIso !== undefined ? { endIso } : {}) };
42
+ }
43
+
44
+ function conv(
45
+ source: string,
46
+ id: string,
47
+ startIso: string,
48
+ endIso: string,
49
+ segments: FusionSegmentInput[],
50
+ ): FusionConversationInput {
51
+ return { source, conversationId: id, startIso, endIso, segments };
52
+ }
53
+
54
+ function snap(overrides: Partial<MeetingActivitySnapshot> & { tsUtc: string; app: string }): MeetingActivitySnapshot {
55
+ return { ...overrides };
56
+ }
57
+
58
+ test("fusion prefers the higher-trust source in overlap regions and records corroboratedBy without duplicating text", () => {
59
+ const fused = fuseMeeting(
60
+ {
61
+ meeting: meeting(),
62
+ conversations: [
63
+ conv("desktop", "d1", "2026-03-10T14:05:00.000Z", "2026-03-10T14:07:00.000Z", [
64
+ seg("Jane", false, "hello everyone", "2026-03-10T14:05:00.000Z", "2026-03-10T14:05:04.000Z"),
65
+ seg("Me (you)", true, "lets begin", "2026-03-10T14:06:00.000Z"),
66
+ ]),
67
+ conv("pendant", "p1", "2026-03-10T14:05:00.000Z", "2026-03-10T14:05:10.000Z", [
68
+ seg("Jane", false, "hello everyone", "2026-03-10T14:05:00.000Z", "2026-03-10T14:05:04.000Z"),
69
+ ]),
70
+ ],
71
+ },
72
+ config(),
73
+ { sourceTrust: { desktop: 0.9, pendant: 0.6 } },
74
+ );
75
+
76
+ // Same utterance recorded by two sources → ONE fused segment (no dup).
77
+ const helloSegments = fused.transcript.filter((s) => s.text === "hello everyone");
78
+ assert.equal(helloSegments.length, 1);
79
+ // Higher-trust source won the overlapping region.
80
+ assert.equal(helloSegments[0]?.provenance.source, "desktop");
81
+ assert.equal(helloSegments[0]?.provenance.reason, "higher-trust");
82
+ // The out-voted source is recorded as corroboration.
83
+ assert.deepEqual(fused.corroboratedBy, ["pendant"]);
84
+ assert.deepEqual(fused.sources, ["desktop", "pendant"]);
85
+ // Jane merged + wearer utterance = two segments total; wearer is not an attendee.
86
+ assert.equal(fused.transcript.length, 2);
87
+ assert.deepEqual(fused.attendees, ["Jane"]);
88
+ });
89
+
90
+ test("screen context includes a >=20s dwell and omits a 5s alt-tab; meeting-app text feeds excerpts", () => {
91
+ const fused = fuseMeeting(
92
+ {
93
+ meeting: meeting(),
94
+ conversations: [
95
+ conv("desktop", "d1", "2026-03-10T14:05:00.000Z", "2026-03-10T14:12:00.000Z", [
96
+ seg("Jane", false, "agenda item one", "2026-03-10T14:10:00.000Z"),
97
+ ]),
98
+ ],
99
+ activity: [
100
+ snap({ tsUtc: "2026-03-10T14:05:00.000Z", app: "Preview", title: "Q3-roadmap.pdf", text: "Q3 roadmap draft" }),
101
+ snap({ tsUtc: "2026-03-10T14:05:25.000Z", app: "Notes", title: "scratch" }), // Preview dwell 25s → include; Notes starts
102
+ snap({ tsUtc: "2026-03-10T14:05:30.000Z", app: "Preview", title: "Q3-roadmap.pdf" }), // Notes dwell 5s → exclude
103
+ snap({ tsUtc: "2026-03-10T14:10:00.000Z", app: "Zoom", text: "Jane: agenda item one" }), // meeting-app → excerpt
104
+ ],
105
+ },
106
+ config(),
107
+ );
108
+
109
+ assert.equal(fused.snapshotCount, 4);
110
+ const apps = fused.screenContext.map((e) => e.app);
111
+ assert.ok(apps.includes("Preview"), "Preview dwell should be in the timeline");
112
+ assert.ok(!apps.includes("Notes"), "5s alt-tab must be excluded");
113
+ const preview = fused.screenContext.find((e) => e.app === "Preview");
114
+ assert.equal(preview?.label, "Preview: Q3-roadmap.pdf");
115
+ assert.ok(preview !== undefined && preview.dwellSeconds >= 20);
116
+ assert.ok(fused.contextExcerpts.includes("Jane: agenda item one"));
117
+ });
118
+
119
+ test("context excerpts respect maxContextChars", () => {
120
+ const fused = fuseMeeting(
121
+ {
122
+ meeting: meeting(),
123
+ conversations: [],
124
+ activity: [
125
+ snap({ tsUtc: "2026-03-10T14:01:00.000Z", app: "Zoom", text: "aaaa" }),
126
+ snap({ tsUtc: "2026-03-10T14:02:00.000Z", app: "Zoom", text: "bbbb" }),
127
+ snap({ tsUtc: "2026-03-10T14:03:00.000Z", app: "Zoom", text: "cccc" }),
128
+ ],
129
+ },
130
+ config({ maxContextChars: 8 }),
131
+ );
132
+ // 4-char excerpts, cap 8 → exactly two fit.
133
+ assert.deepEqual(fused.contextExcerpts, ["aaaa", "bbbb"]);
134
+ });
135
+
136
+ test("degradation — no activity yields an empty screen context but a full transcript", () => {
137
+ const fused = fuseMeeting(
138
+ {
139
+ meeting: meeting({ detectionSource: "audio", app: undefined }),
140
+ conversations: [
141
+ conv("limitless", "l1", "2026-03-10T14:05:00.000Z", "2026-03-10T14:20:00.000Z", [
142
+ seg("Jane", false, "quarterly numbers", "2026-03-10T14:05:00.000Z"),
143
+ ]),
144
+ ],
145
+ },
146
+ config(),
147
+ );
148
+ assert.equal(fused.snapshotCount, 0);
149
+ assert.deepEqual(fused.screenContext, []);
150
+ assert.deepEqual(fused.contextExcerpts, []);
151
+ assert.equal(fused.transcript.length, 1);
152
+ assert.deepEqual(fused.sources, ["limitless"]);
153
+ });
154
+
155
+ test("degradation — no audio yields an empty transcript without crashing", () => {
156
+ const fused = fuseMeeting(
157
+ { meeting: meeting(), conversations: [] },
158
+ config(),
159
+ );
160
+ assert.deepEqual(fused.transcript, []);
161
+ assert.deepEqual(fused.sources, []);
162
+ assert.deepEqual(fused.corroboratedBy, []);
163
+ assert.deepEqual(fused.attendees, []);
164
+ });
165
+
166
+ test("segments outside the half-open window are clipped out", () => {
167
+ const fused = fuseMeeting(
168
+ {
169
+ meeting: meeting(),
170
+ conversations: [
171
+ conv("desktop", "d1", "2026-03-10T13:50:00.000Z", "2026-03-10T14:35:00.000Z", [
172
+ seg("Jane", false, "before start", "2026-03-10T13:59:59.000Z"),
173
+ seg("Jane", false, "in window", "2026-03-10T14:05:00.000Z"),
174
+ seg("Jane", false, "at end boundary", WINDOW_END), // half-open: end excluded
175
+ ]),
176
+ ],
177
+ },
178
+ config(),
179
+ );
180
+ const texts = fused.transcript.map((s) => s.text);
181
+ assert.deepEqual(texts, ["in window"]);
182
+ });
183
+
184
+ test("throws on an invalid meeting window", () => {
185
+ assert.throws(
186
+ () => fuseMeeting({ meeting: meeting({ endUtc: WINDOW_START }), conversations: [] }, config()),
187
+ /invalid window/,
188
+ );
189
+ });
190
+
191
+ test("a lone trailing other-app snapshot is not inflated to the meeting end", () => {
192
+ const fused = fuseMeeting(
193
+ {
194
+ meeting: meeting(),
195
+ conversations: [
196
+ conv("desktop", "d1", "2026-03-10T14:05:00.000Z", "2026-03-10T14:12:00.000Z", [
197
+ seg("Jane", false, "agenda", "2026-03-10T14:06:00.000Z"),
198
+ ]),
199
+ ],
200
+ // A single Notes snapshot 1 min before the window end. With no later
201
+ // snapshot, the trailing run must NOT count dwell up to the meeting end.
202
+ activity: [snap({ tsUtc: "2026-03-10T14:29:00.000Z", app: "Notes", title: "scratch" })],
203
+ },
204
+ config(),
205
+ );
206
+ assert.equal(fused.snapshotCount, 1);
207
+ assert.deepEqual(fused.screenContext, [], "a lone trailing snapshot has zero observed dwell");
208
+ });
@@ -0,0 +1,297 @@
1
+ /**
2
+ * Meeting fusion (issue #1900, Phase 4 slice 2).
3
+ *
4
+ * For a detected meeting window `[startUtc, endUtc)` (half-open, AGENTS.md §23),
5
+ * fuse the concurrent signals into a screen-aware `FusedMeeting`:
6
+ *
7
+ * 1. Transcript — clip every wearable source's segments to the window and run
8
+ * them through the SHARED wearables reconciliation (`fuseCluster`). Higher-
9
+ * `sourceTrust` text wins overlapping regions and the out-voted sources are
10
+ * recorded per segment; this module never re-implements alignment/dedup —
11
+ * the repo rule is "extend the shared fusion module, never fork it".
12
+ * 2. Screen context — other-app foreground dwell within the window, kept only
13
+ * when it lasts `contextDwellSeconds`, plus deduped on-screen text excerpts
14
+ * capped at `maxContextChars`.
15
+ * 3. Attendees — distinct non-wearer speaker labels from the fused transcript
16
+ * (already resolved through the wearables speaker registry upstream).
17
+ *
18
+ * Pure and deterministic: same inputs ⇒ same `FusedMeeting`.
19
+ */
20
+
21
+ import { fuseCluster } from "../wearables/fusion/reconcile.js";
22
+ import type {
23
+ FusionConversationInput,
24
+ FusionSegmentInput,
25
+ } from "../wearables/fusion/types.js";
26
+ import type {
27
+ FusedMeeting,
28
+ MeetingActivitySnapshot,
29
+ MeetingBuildInput,
30
+ MeetingScreenContextEvent,
31
+ MeetingsConfig,
32
+ } from "./types.js";
33
+
34
+ /** Per-run tuning that mirrors the wearables fusion knobs. */
35
+ export interface MeetingFusionOptions {
36
+ /** Per-source trust priors handed to reconciliation. */
37
+ sourceTrust?: Record<string, number>;
38
+ /** Max drift (ms) for two segments to align across sources. */
39
+ windowToleranceMs?: number;
40
+ }
41
+
42
+ const SORT_STR = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0);
43
+
44
+ /** `HH:MM` in UTC for a parseable ISO instant (mirrors the day-store fallback). */
45
+ function formatClockUtc(ms: number): string {
46
+ return new Date(ms).toISOString().slice(11, 16);
47
+ }
48
+
49
+ /** Overlap of two half-open [start,end) windows in ms (0 when disjoint). */
50
+ function overlapMs(aStart: number, aEnd: number, bStart: number, bEnd: number): number {
51
+ return Math.max(0, Math.min(aEnd, bEnd) - Math.max(aStart, bStart));
52
+ }
53
+
54
+ /** Whether a segment falls in the half-open window; null when it carries no
55
+ * parseable start (the caller decides via the conversation-level overlap). */
56
+ function segmentInWindow(
57
+ seg: FusionSegmentInput,
58
+ windowStartMs: number,
59
+ windowEndMs: number,
60
+ ): boolean | null {
61
+ const startMs = seg.startIso === undefined ? NaN : Date.parse(seg.startIso);
62
+ if (Number.isNaN(startMs)) return null;
63
+ const endParsed = seg.endIso === undefined ? NaN : Date.parse(seg.endIso);
64
+ const endMs = Number.isNaN(endParsed) ? startMs : endParsed;
65
+ if (endMs === startMs) return startMs >= windowStartMs && startMs < windowEndMs;
66
+ return overlapMs(startMs, endMs, windowStartMs, windowEndMs) > 0;
67
+ }
68
+
69
+ /** Clip one conversation to the window, keeping only in-window segments. */
70
+ function clipConversation(
71
+ conv: FusionConversationInput,
72
+ windowStartMs: number,
73
+ windowEndMs: number,
74
+ ): FusionConversationInput | null {
75
+ const convStart = Date.parse(conv.startIso);
76
+ const convEnd = conv.endIso === undefined ? convStart : Date.parse(conv.endIso);
77
+ const convOverlaps =
78
+ Number.isFinite(convStart) &&
79
+ overlapMs(convStart, Number.isFinite(convEnd) ? convEnd : convStart, windowStartMs, windowEndMs) > 0;
80
+
81
+ const kept: FusionSegmentInput[] = [];
82
+ let earliestStart: number | undefined;
83
+ let earliestIso: string | undefined;
84
+ let latestEnd: number | undefined;
85
+ let latestIso: string | undefined;
86
+ for (const seg of conv.segments) {
87
+ const decision = segmentInWindow(seg, windowStartMs, windowEndMs);
88
+ // Untimed segments ride along only when the conversation itself overlaps.
89
+ if (decision === false || (decision === null && !convOverlaps)) continue;
90
+ kept.push(seg);
91
+ if (seg.startIso !== undefined) {
92
+ const s = Date.parse(seg.startIso);
93
+ if (Number.isFinite(s) && (earliestStart === undefined || s < earliestStart)) {
94
+ earliestStart = s;
95
+ earliestIso = seg.startIso;
96
+ }
97
+ const endIso = seg.endIso ?? seg.startIso;
98
+ const e = Date.parse(endIso);
99
+ if (Number.isFinite(e) && (latestEnd === undefined || e > latestEnd)) {
100
+ latestEnd = e;
101
+ latestIso = endIso;
102
+ }
103
+ }
104
+ }
105
+ if (kept.length === 0) return null;
106
+ return {
107
+ source: conv.source,
108
+ conversationId: conv.conversationId,
109
+ ...(conv.title !== undefined ? { title: conv.title } : {}),
110
+ ...(conv.summary !== undefined ? { summary: conv.summary } : {}),
111
+ startIso: earliestIso ?? conv.startIso,
112
+ ...(latestIso !== undefined ? { endIso: latestIso } : conv.endIso !== undefined ? { endIso: conv.endIso } : {}),
113
+ segments: kept,
114
+ };
115
+ }
116
+
117
+ /** Strip the URL scheme for a compact screen-context label. */
118
+ function shortenUrl(url: string): string {
119
+ return url.replace(/^[a-z]+:\/\//i, "");
120
+ }
121
+
122
+ function contextLabel(snap: MeetingActivitySnapshot): string {
123
+ if (snap.url !== undefined && snap.url.length > 0) return `${snap.app}: ${shortenUrl(snap.url)}`;
124
+ if (snap.title !== undefined && snap.title.length > 0) return `${snap.app}: ${snap.title}`;
125
+ return snap.app;
126
+ }
127
+
128
+ /** True when a snapshot is the meeting app itself (captions/chat/participants),
129
+ * not other context. Matches the meeting's app or any configured app pattern. */
130
+ function isMeetingAppSnapshot(
131
+ snap: MeetingActivitySnapshot,
132
+ meetingApp: string | undefined,
133
+ appPatterns: readonly string[],
134
+ ): boolean {
135
+ const hay = `${snap.app} ${snap.title ?? ""} ${snap.url ?? ""}`.toLowerCase();
136
+ if (meetingApp !== undefined && meetingApp.length > 0 && hay.includes(meetingApp.toLowerCase())) {
137
+ return true;
138
+ }
139
+ for (const pattern of appPatterns) {
140
+ if (pattern.length > 0 && hay.includes(pattern.toLowerCase())) return true;
141
+ }
142
+ return false;
143
+ }
144
+
145
+ const MEETING_APP_KEY = "\u0000meeting-app";
146
+
147
+ interface ScreenContextResult {
148
+ events: MeetingScreenContextEvent[];
149
+ excerpts: string[];
150
+ snapshotCount: number;
151
+ }
152
+
153
+ /**
154
+ * Build the screen-context timeline + excerpts from the day's activity clipped
155
+ * to the meeting window. Consecutive snapshots sharing a foreground key form a
156
+ * dwell run whose length is bounded by the NEXT foreground change (or the
157
+ * window end); other-app runs are emitted only when the dwell clears
158
+ * `contextDwellSeconds`, so a brief alt-tab is dropped and a lingered doc is
159
+ * kept. Meeting-app snapshots never enter the timeline but feed excerpts.
160
+ */
161
+ function buildScreenContext(
162
+ activity: readonly MeetingActivitySnapshot[],
163
+ windowStartMs: number,
164
+ windowEndMs: number,
165
+ meetingApp: string | undefined,
166
+ config: MeetingsConfig,
167
+ ): ScreenContextResult {
168
+ const win = activity
169
+ .map((snap) => ({ snap, ms: Date.parse(snap.tsUtc) }))
170
+ .filter((entry) => Number.isFinite(entry.ms) && entry.ms >= windowStartMs && entry.ms < windowEndMs)
171
+ .sort((a, b) => a.ms - b.ms || SORT_STR(a.snap.app, b.snap.app));
172
+
173
+ const keyOf = (snap: MeetingActivitySnapshot): string =>
174
+ isMeetingAppSnapshot(snap, meetingApp, config.appPatterns)
175
+ ? MEETING_APP_KEY
176
+ : `${snap.app}\u0000${snap.url ?? snap.title ?? ""}`;
177
+
178
+ const events: MeetingScreenContextEvent[] = [];
179
+ const includedText: string[] = [];
180
+
181
+ let runStartIdx = 0;
182
+ while (runStartIdx < win.length) {
183
+ const key = keyOf(win[runStartIdx]!.snap);
184
+ let runEndIdx = runStartIdx + 1;
185
+ while (runEndIdx < win.length && keyOf(win[runEndIdx]!.snap) === key) runEndIdx++;
186
+ const runStartMs = win[runStartIdx]!.ms;
187
+ // A run's dwell ends at the NEXT foreground change (next different-key
188
+ // snapshot). The trailing run has no observed switch-away, so bound it by the
189
+ // LAST snapshot in the run — never inflate to the meeting end (a lone trailing
190
+ // alt-tab would otherwise look like it dwelled until the meeting ended).
191
+ const runEndMs = runEndIdx < win.length ? win[runEndIdx]!.ms : win[runEndIdx - 1]!.ms;
192
+ const dwellSeconds = Math.floor((runEndMs - runStartMs) / 1000);
193
+ const head = win[runStartIdx]!.snap;
194
+
195
+ const isMeetingApp = key === MEETING_APP_KEY;
196
+ if (isMeetingApp) {
197
+ // Captions / chat / participant text visible on the meeting app itself.
198
+ for (let i = runStartIdx; i < runEndIdx; i++) {
199
+ const text = win[i]!.snap.text;
200
+ if (text !== undefined && text.trim().length > 0) includedText.push(text.trim());
201
+ }
202
+ } else if (dwellSeconds >= config.contextDwellSeconds) {
203
+ events.push({
204
+ tsUtc: new Date(runStartMs).toISOString(),
205
+ clock: formatClockUtc(runStartMs),
206
+ app: head.app,
207
+ label: contextLabel(head),
208
+ dwellSeconds,
209
+ });
210
+ for (let i = runStartIdx; i < runEndIdx; i++) {
211
+ const text = win[i]!.snap.text;
212
+ if (text !== undefined && text.trim().length > 0) includedText.push(text.trim());
213
+ }
214
+ }
215
+ runStartIdx = runEndIdx;
216
+ }
217
+
218
+ // Dedup excerpts (first occurrence wins) and cap total characters.
219
+ const excerpts: string[] = [];
220
+ const seen = new Set<string>();
221
+ let used = 0;
222
+ for (const text of includedText) {
223
+ if (seen.has(text)) continue;
224
+ if (used + text.length > config.maxContextChars) break;
225
+ seen.add(text);
226
+ excerpts.push(text);
227
+ used += text.length;
228
+ }
229
+
230
+ return { events, excerpts, snapshotCount: win.length };
231
+ }
232
+
233
+ /** Fuse one detected meeting's transcript + screen context into a FusedMeeting. */
234
+ export function fuseMeeting(
235
+ input: MeetingBuildInput,
236
+ config: MeetingsConfig,
237
+ options: MeetingFusionOptions = {},
238
+ ): FusedMeeting {
239
+ const windowStartMs = Date.parse(input.meeting.startUtc);
240
+ const windowEndMs = Date.parse(input.meeting.endUtc);
241
+ if (!Number.isFinite(windowStartMs) || !Number.isFinite(windowEndMs) || windowEndMs <= windowStartMs) {
242
+ throw new RangeError(
243
+ `meetings: meeting ${input.meeting.id} has an invalid window [${input.meeting.startUtc}, ${input.meeting.endUtc}).`,
244
+ );
245
+ }
246
+
247
+ const clipped: FusionConversationInput[] = [];
248
+ for (const conv of input.conversations) {
249
+ const clip = clipConversation(conv, windowStartMs, windowEndMs);
250
+ if (clip !== null) clipped.push(clip);
251
+ }
252
+
253
+ const fused =
254
+ clipped.length > 0
255
+ ? fuseCluster(clipped, {
256
+ ...(options.sourceTrust !== undefined ? { sourceTrust: options.sourceTrust } : {}),
257
+ ...(options.windowToleranceMs !== undefined ? { windowToleranceMs: options.windowToleranceMs } : {}),
258
+ })
259
+ : { sources: [], speakers: [], segments: [], disagreements: [], startIso: input.meeting.startUtc };
260
+
261
+ const corroborated = new Set<string>();
262
+ for (const segment of fused.segments) {
263
+ for (const alt of segment.provenance.alternatives) {
264
+ // Suppress a source only as corroborator of the SAME segment it won
265
+ // (a winner is never its own alternative, so this is a guard). A source
266
+ // that WON one segment but lost another still corroborates the one it
267
+ // lost — deleting it globally erased legitimate multi-device corroboration
268
+ // and dropped the summary/fact trust boost.
269
+ if (alt.source === segment.provenance.source) continue;
270
+ corroborated.add(alt.source);
271
+ }
272
+ }
273
+
274
+ const attendees = new Set<string>();
275
+ for (const speaker of fused.speakers) {
276
+ if (!speaker.isSelf) attendees.add(speaker.label);
277
+ }
278
+
279
+ const screen = buildScreenContext(
280
+ input.activity ?? [],
281
+ windowStartMs,
282
+ windowEndMs,
283
+ input.meeting.app,
284
+ config,
285
+ );
286
+
287
+ return {
288
+ attendees: [...attendees].sort(SORT_STR),
289
+ sources: [...fused.sources].sort(SORT_STR),
290
+ corroboratedBy: [...corroborated].sort(SORT_STR),
291
+ screenContext: screen.events,
292
+ contextExcerpts: screen.excerpts,
293
+ transcript: fused.segments,
294
+ speakers: fused.speakers,
295
+ snapshotCount: screen.snapshotCount,
296
+ };
297
+ }
@@ -1,8 +1,18 @@
1
1
  /**
2
2
  * Public entry for the retrospective meeting-detection subsystem (issue #1900):
3
- * pure detection over a day's already-ingested audio + app-span signals.
3
+ * the deterministic engine — detection, fusion, record store, day build, and the
4
+ * CLI over a day's already-ingested audio + app-span signals — plus the
5
+ * memory-generation SEAM the builder depends on. The concrete memory generator
6
+ * and the day-source/service/scheduler wiring ship in the surface slice.
4
7
  * Re-exported from the package root (`src/index.ts`) so consumers import it from
5
8
  * `@remnic/core`, matching the wearables/activity subsystems' surfacing.
6
9
  */
7
10
  export * from "./types.js";
8
11
  export * from "./detect.js";
12
+ export * from "./config.js";
13
+ export * from "./errors.js";
14
+ export * from "./fuse.js";
15
+ export * from "./store.js";
16
+ export * from "./memory-generator.js";
17
+ export * from "./build.js";
18
+ export * from "./cli.js";
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Meeting memory-generation SEAM (issue #1900 — engine/memory decouple).
3
+ *
4
+ * Engine-layer boundary between the deterministic meeting builder and the
5
+ * memory-generation layer. `MeetingsBuilder` (detect/fuse/store/build) depends
6
+ * ONLY on `MeetingMemoryGenerator` declared here — never on `memory-gen.ts` — so
7
+ * the deterministic engine carries zero dependency on episode/fact generation
8
+ * and ships in its own scope-budgeted PR. The concrete generator lives in
9
+ * `memory-gen.ts` behind `createMeetingMemoryGenerator`, which implements this
10
+ * interface by driving the existing episode/fact/retract logic.
11
+ */
12
+
13
+ import type { MemoryWriteResult } from "../index.js";
14
+ import type { ImportanceScore, MemoryStatus } from "../types.js";
15
+ import type { SealedMemoryEnvelope } from "../write-envelope.js";
16
+ import type { MeetingRecord } from "./types.js";
17
+
18
+ /**
19
+ * Narrow writer interface satisfied by `StorageManager` — the same sealed
20
+ * write entry point the wearables generator uses, so meeting memories inherit
21
+ * encrypted-at-rest + atomic write + dedup without this module knowing the key.
22
+ */
23
+ export interface MeetingMemoryWriter {
24
+ writeSealedMemory(
25
+ envelope: SealedMemoryEnvelope,
26
+ extras: {
27
+ importance?: ImportanceScore;
28
+ contentHashSource?: string;
29
+ status?: MemoryStatus;
30
+ memoryKind?: "episode" | "note" | "box" | "dream" | "procedural";
31
+ },
32
+ ): Promise<MemoryWriteResult>;
33
+ /**
34
+ * True when an active/pending memory whose frontmatter `source` equals
35
+ * `source` already carries this exact content. Meeting memories are episodes
36
+ * (`moment`) and decisions/commitments (`decision`/`commitment`) — categories
37
+ * the storage fact-hash index does NOT cover — so idempotency is a
38
+ * source-scoped content scan, mirroring the wearables writer, NOT
39
+ * `hasFactContentHash` (fact-only; always false here → duplicates on rebuild).
40
+ */
41
+ hasMemoryFromSource(source: string, content: string): Promise<boolean>;
42
+ /**
43
+ * Retire (invalidate) every memory whose frontmatter `source` equals `source`.
44
+ * Returns the count retired. Used to reconcile away a removed meeting's
45
+ * episode/summary memories so recall never surfaces a meeting `show` can't load.
46
+ */
47
+ retireMemoriesFromSource(source: string): Promise<number>;
48
+ }
49
+
50
+ /** Result of generating episode memories for a day's meetings. */
51
+ export interface MeetingEpisodeGenResult {
52
+ /** Episodes newly written. */
53
+ written: number;
54
+ /** Episodes skipped because an identical one already existed. */
55
+ skipped: number;
56
+ }
57
+
58
+ /** Day-level aggregate of the trust-gated summary/fact layer across records. */
59
+ export interface MeetingsDayFactGenResult {
60
+ /** True when the LLM extractor ran for at least one record. */
61
+ llmInvoked: boolean;
62
+ /** Facts written active. */
63
+ active: number;
64
+ /** Facts queued for review (pending_review). */
65
+ review: number;
66
+ /** Candidates/summaries dropped below the trust bar or judge-rejected. */
67
+ dropped: number;
68
+ /** Candidates/summaries skipped because an identical memory already existed. */
69
+ skipped: number;
70
+ /** Count of records whose summary memory was written. */
71
+ summariesWritten: number;
72
+ }
73
+
74
+ /**
75
+ * Outcome the memory generator returns to the builder after a day's records are
76
+ * built and persisted. Counts feed the day-build summary; `reindexNeeded` folds
77
+ * the "reindex when new episodes/facts were written even if no record changed"
78
+ * rule so the builder never reaches into the memory layer to decide it.
79
+ */
80
+ export interface MeetingMemoryOutcome {
81
+ /** Episode-memory counts, present only when records were built. */
82
+ episodes?: MeetingEpisodeGenResult;
83
+ /** Trust-gated summary/fact counts, present only when summary deps ran. */
84
+ facts?: MeetingsDayFactGenResult;
85
+ /** True when newly written memories require an isolated reindex. */
86
+ reindexNeeded: boolean;
87
+ /** Non-fatal warnings raised while generating memories. */
88
+ warnings: readonly string[];
89
+ }
90
+
91
+ /**
92
+ * Seam the deterministic builder depends on. Invoked once per day build after
93
+ * the record-store writes with, for the day:
94
+ * - `built` — every record composed this build (new, updated, unchanged).
95
+ * - `removedIds` — stale ids the rebuild reconciled away (retract memories).
96
+ * - `unchangedIds`— built ids whose stored `contentHash` was IDENTICAL this
97
+ * build (nothing rewritten). The generator SKIPS all
98
+ * episode/summary/fact work for these: an idempotent rebuild
99
+ * must not re-invoke the LLM or duplicate memories.
100
+ * - `updatedIds` — built ids that existed before AND were rewritten this build
101
+ * (same id, changed `contentHash`). The generator REFRESHES
102
+ * them: retract the stale episode/summary memories for that
103
+ * source, then regenerate from the new record.
104
+ * Ids not in `unchangedIds` or `updatedIds` are newly built — generated fresh
105
+ * with no retract. The three id sets are disjoint subsets of `built`. The
106
+ * implementation reports the counts + reindex signal the builder folds into its
107
+ * day summary.
108
+ */
109
+ export interface MeetingMemoryGenerator {
110
+ onRecordsBuilt(input: {
111
+ built: readonly MeetingRecord[];
112
+ removedIds: readonly string[];
113
+ unchangedIds: readonly string[];
114
+ updatedIds: readonly string[];
115
+ }): Promise<MeetingMemoryOutcome>;
116
+ }