@remnic/core 9.4.2 → 9.6.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 (379) hide show
  1. package/dist/access-admin-ops-surface.d.ts +6 -6
  2. package/dist/access-admin-ops-surface.js +28 -28
  3. package/dist/access-boundary.d.ts +27 -8
  4. package/dist/access-boundary.js +33 -29
  5. package/dist/access-cli.js +83 -81
  6. package/dist/access-cli.js.map +1 -1
  7. package/dist/access-http.d.ts +52 -6
  8. package/dist/access-http.js +40 -38
  9. package/dist/access-identity-continuity-surface.d.ts +5 -5
  10. package/dist/access-identity-continuity-surface.js +28 -28
  11. package/dist/access-lcm-surface.d.ts +6 -6
  12. package/dist/access-lcm-surface.js +28 -28
  13. package/dist/access-mcp.d.ts +16 -6
  14. package/dist/access-mcp.js +37 -35
  15. package/dist/access-observe-write-surface.d.ts +6 -6
  16. package/dist/access-observe-write-surface.js +28 -28
  17. package/dist/access-operations-batch.js +33 -32
  18. package/dist/access-operations.d.ts +9 -9
  19. package/dist/access-operations.js +35 -34
  20. package/dist/access-recall-surface.d.ts +6 -6
  21. package/dist/access-recall-surface.js +28 -28
  22. package/dist/access-schema.d.ts +4 -4
  23. package/dist/access-schema.js +4 -4
  24. package/dist/{access-service-m-2VO_Ig.d.ts → access-service-CXJaql3A.d.ts} +4 -4
  25. package/dist/access-service.d.ts +6 -6
  26. package/dist/access-service.js +28 -28
  27. package/dist/access-surface-catalog.d.ts +6 -6
  28. package/dist/access-surface-catalog.js +1 -0
  29. package/dist/access-surface-catalog.js.map +1 -1
  30. package/dist/access-token-capabilities.d.ts +174 -0
  31. package/dist/access-token-capabilities.js +33 -0
  32. package/dist/action-confidence.d.ts +1 -1
  33. package/dist/active-memory-bridge.d.ts +1 -1
  34. package/dist/active-recall.d.ts +1 -1
  35. package/dist/active-recall.js +2 -2
  36. package/dist/adapters/index.js +4 -4
  37. package/dist/adapters/registry.js +2 -2
  38. package/dist/{auto-sync-HMHQHUAG.js → auto-sync-4A5YBWYO.js} +4 -4
  39. package/dist/behavior-learner.d.ts +1 -1
  40. package/dist/behavior-signals.d.ts +1 -1
  41. package/dist/bootstrap.d.ts +5 -5
  42. package/dist/briefing.d.ts +2 -2
  43. package/dist/briefing.js +9 -9
  44. package/dist/buffer-surprise-report.d.ts +1 -1
  45. package/dist/buffer.d.ts +2 -2
  46. package/dist/calibration.d.ts +1 -1
  47. package/dist/capabilities.d.ts +1 -1
  48. package/dist/{capsule-crypto-CZJSLEFG.js → capsule-crypto-RAARDY3M.js} +2 -2
  49. package/dist/capsule-crypto-RAARDY3M.js.map +1 -0
  50. package/dist/{catalog-BxWokdC2.d.ts → catalog-C9cu67Ig.d.ts} +1 -1
  51. package/dist/causal-behavior.d.ts +1 -1
  52. package/dist/causal-behavior.js +5 -5
  53. package/dist/causal-chain.js +3 -3
  54. package/dist/causal-consolidation.d.ts +1 -1
  55. package/dist/causal-consolidation.js +13 -13
  56. package/dist/causal-retrieval.js +3 -3
  57. package/dist/causal-trajectory-graph.d.ts +1 -1
  58. package/dist/causal-trajectory.js +1 -1
  59. package/dist/{chunk-6ZIETXZ2.js → chunk-23JFRB73.js} +2 -2
  60. package/dist/{chunk-IC6QX5TC.js → chunk-3XIY7MDQ.js} +6 -6
  61. package/dist/{chunk-FZGGZAAU.js → chunk-4GNEDXDI.js} +146 -2
  62. package/dist/chunk-4GNEDXDI.js.map +1 -0
  63. package/dist/{chunk-LOR6ZM5R.js → chunk-4LV23CHK.js} +2 -2
  64. package/dist/{chunk-E2SPGGUI.js → chunk-4OKLXWDZ.js} +13 -6
  65. package/dist/chunk-4OKLXWDZ.js.map +1 -0
  66. package/dist/{chunk-AER6MT24.js → chunk-6SXVCD7W.js} +2 -2
  67. package/dist/{chunk-OHDJ34CC.js → chunk-72WG5QKN.js} +2 -2
  68. package/dist/{chunk-CZAZX7JP.js → chunk-7MW3CVLD.js} +4 -4
  69. package/dist/{chunk-CXLNZXG5.js → chunk-7XGSIUP5.js} +1 -1
  70. package/dist/{chunk-CZAA5N5S.js → chunk-AYZJID4S.js} +2 -2
  71. package/dist/{chunk-M7XQSUBB.js → chunk-B3ABVQ36.js} +116 -9
  72. package/dist/chunk-B3ABVQ36.js.map +1 -0
  73. package/dist/{chunk-6BISDEN6.js → chunk-BEM4D2QG.js} +14 -14
  74. package/dist/{chunk-ZPOROUTX.js → chunk-CYWA2CXR.js} +5 -5
  75. package/dist/{chunk-M5QKGHCR.js → chunk-CYWCKFCL.js} +5 -5
  76. package/dist/chunk-D7FOKIY5.js +173 -0
  77. package/dist/chunk-D7FOKIY5.js.map +1 -0
  78. package/dist/{chunk-4BFRBP45.js → chunk-DEG5ULFJ.js} +2 -2
  79. package/dist/{chunk-JLR3JSTL.js → chunk-DS7MM2ES.js} +3 -3
  80. package/dist/{chunk-2BFLDH5F.js → chunk-EEIROWEG.js} +2 -2
  81. package/dist/{chunk-5QZDMSSN.js → chunk-FKCRGLRD.js} +237 -82
  82. package/dist/{chunk-5QZDMSSN.js.map → chunk-FKCRGLRD.js.map} +1 -1
  83. package/dist/{chunk-X4A62ET7.js → chunk-H2DXDQCQ.js} +17 -17
  84. package/dist/{chunk-27FLVMDC.js → chunk-HLN7RROI.js} +7 -7
  85. package/dist/chunk-IC2TCZJ2.js +54 -0
  86. package/dist/chunk-IC2TCZJ2.js.map +1 -0
  87. package/dist/{chunk-2U2A5HF4.js → chunk-J3XHPFEU.js} +6 -6
  88. package/dist/{chunk-AKUCU7CI.js → chunk-JI5AVXYO.js} +5 -5
  89. package/dist/{chunk-KCJ5H6XQ.js → chunk-JSNVWNGX.js} +2 -2
  90. package/dist/{chunk-7Z2DZNWN.js → chunk-KL4UAXUY.js} +43 -2
  91. package/dist/{chunk-7Z2DZNWN.js.map → chunk-KL4UAXUY.js.map} +1 -1
  92. package/dist/{chunk-FLVL4L3L.js → chunk-KTULX4YO.js} +4 -4
  93. package/dist/{chunk-ILFJ7AVS.js → chunk-LADFWYHY.js} +35 -21
  94. package/dist/chunk-LADFWYHY.js.map +1 -0
  95. package/dist/{chunk-437S3G37.js → chunk-LNQUAJNC.js} +1 -1
  96. package/dist/{chunk-EPGWWCZF.js → chunk-LR5DZ56F.js} +6 -6
  97. package/dist/{chunk-YDSWRD33.js → chunk-N3JDIRBW.js} +4 -4
  98. package/dist/{chunk-OUMTBFSQ.js → chunk-NKCPUHCI.js} +5 -5
  99. package/dist/{chunk-7QWMTOIF.js → chunk-P454KXD6.js} +112 -34
  100. package/dist/chunk-P454KXD6.js.map +1 -0
  101. package/dist/{chunk-DR6PEJU3.js → chunk-PCLIWBGD.js} +44 -59
  102. package/dist/chunk-PCLIWBGD.js.map +1 -0
  103. package/dist/{chunk-BGTAL46Y.js → chunk-PNLFN2PZ.js} +2 -2
  104. package/dist/{chunk-P6SYWE4G.js → chunk-PVUJ6WVV.js} +10 -10
  105. package/dist/{chunk-YPEMNNNY.js → chunk-RGK27ELN.js} +6 -6
  106. package/dist/{chunk-2QSZNTDO.js → chunk-RKNJBZ55.js} +4 -4
  107. package/dist/{chunk-66FLU5BT.js → chunk-SIRPLF54.js} +6 -6
  108. package/dist/{chunk-M7GGMGMZ.js → chunk-SVOHGUMI.js} +1202 -126
  109. package/dist/chunk-SVOHGUMI.js.map +1 -0
  110. package/dist/{chunk-HRCACVHL.js → chunk-TLUDFPZL.js} +2 -2
  111. package/dist/{chunk-2FOD2YY3.js → chunk-UFUKHGZD.js} +236 -103
  112. package/dist/chunk-UFUKHGZD.js.map +1 -0
  113. package/dist/{chunk-2MR3MFQB.js → chunk-UPSAT4PY.js} +2 -2
  114. package/dist/{chunk-4P4626RY.js → chunk-UTRJFDP2.js} +4 -4
  115. package/dist/{chunk-CGS7UKWM.js → chunk-VCW74PNH.js} +2 -2
  116. package/dist/{chunk-5GPPACXK.js → chunk-VSMRDBFT.js} +2 -1
  117. package/dist/{chunk-NG7YBCJO.js → chunk-XCL57M6Q.js} +4 -4
  118. package/dist/{chunk-CLX5VDNY.js → chunk-Y4RKAQZ7.js} +4 -4
  119. package/dist/{chunk-DGYCWCQ7.js → chunk-Y7UNNAZD.js} +2 -2
  120. package/dist/{chunk-EZ25VE3G.js → chunk-YNDLCWXS.js} +4 -4
  121. package/dist/{chunk-XN3TUUX2.js → chunk-YQB32INI.js} +2 -2
  122. package/dist/{cli-EiBBOt3r.d.ts → cli-CqdPA6z9.d.ts} +3 -3
  123. package/dist/cli.d.ts +7 -7
  124. package/dist/cli.js +61 -59
  125. package/dist/compounding/engine.d.ts +2 -2
  126. package/dist/compounding/engine.js +9 -9
  127. package/dist/compounding/preference-consolidator.d.ts +1 -1
  128. package/dist/compression-optimizer.d.ts +1 -1
  129. package/dist/config.d.ts +1 -1
  130. package/dist/config.js +2 -2
  131. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  132. package/dist/connectors/codex-materialize-runner.js +9 -9
  133. package/dist/connectors/codex-materialize.d.ts +1 -1
  134. package/dist/connectors/index.d.ts +1 -1
  135. package/dist/connectors/index.js +13 -11
  136. package/dist/consolidation-provenance-check.d.ts +2 -2
  137. package/dist/consolidation-undo.d.ts +2 -2
  138. package/dist/contradiction/index.d.ts +2 -2
  139. package/dist/contradiction/index.js +4 -4
  140. package/dist/conversation-index/backend.d.ts +1 -1
  141. package/dist/conversation-index/backend.js +2 -2
  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/dashboard-runtime.js +2 -2
  147. package/dist/day-summary.d.ts +1 -1
  148. package/dist/delinearize.d.ts +1 -1
  149. package/dist/direct-answer-wiring.d.ts +1 -1
  150. package/dist/direct-answer.d.ts +1 -1
  151. package/dist/embedding-fallback.d.ts +1 -1
  152. package/dist/enrichment/index.d.ts +1 -1
  153. package/dist/entity-retrieval.d.ts +2 -2
  154. package/dist/entity-retrieval.js +9 -9
  155. package/dist/entity-schema.d.ts +1 -1
  156. package/dist/explicit-capture.d.ts +5 -5
  157. package/dist/extraction-faithfulness.d.ts +1 -1
  158. package/dist/extraction-judge-telemetry.d.ts +1 -1
  159. package/dist/extraction-judge-training.d.ts +1 -1
  160. package/dist/extraction-judge.d.ts +1 -1
  161. package/dist/extraction.d.ts +1 -1
  162. package/dist/fallback-llm.d.ts +1 -1
  163. package/dist/{first-start-migration-DY6YCRC2.js → first-start-migration-VBJQL6OI.js} +5 -5
  164. package/dist/graph-dashboard-diff.d.ts +1 -1
  165. package/dist/graph-dashboard-key.d.ts +1 -1
  166. package/dist/graph-dashboard-parser.d.ts +1 -1
  167. package/dist/graph-edge-reinforcement.d.ts +1 -1
  168. package/dist/graph-snapshot.d.ts +1 -1
  169. package/dist/graph.d.ts +1 -1
  170. package/dist/identity-continuity.d.ts +1 -1
  171. package/dist/importance.d.ts +1 -1
  172. package/dist/index.d.ts +271 -17
  173. package/dist/index.js +163 -99
  174. package/dist/intent.d.ts +1 -1
  175. package/dist/lcm/engine.d.ts +1 -1
  176. package/dist/lcm/engine.js +2 -2
  177. package/dist/lcm/index.d.ts +1 -1
  178. package/dist/lcm/index.js +2 -2
  179. package/dist/lcm/tools.d.ts +1 -1
  180. package/dist/lifecycle.d.ts +1 -1
  181. package/dist/live-connectors-runner.d.ts +1 -1
  182. package/dist/local-llm.d.ts +1 -1
  183. package/dist/local-model-endpoint.d.ts +1 -1
  184. package/dist/maintenance/memory-governance.d.ts +1 -1
  185. package/dist/maintenance/memory-governance.js +10 -10
  186. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +9 -9
  187. package/dist/maintenance/rebuild-memory-projection.js +11 -11
  188. package/dist/mcp-memory-inspector-app.d.ts +6 -6
  189. package/dist/mcp-read-only-tools.d.ts +16 -0
  190. package/dist/mcp-read-only-tools.js +8 -0
  191. package/dist/mcp-read-only-tools.js.map +1 -0
  192. package/dist/memory-action-policy.d.ts +1 -1
  193. package/dist/memory-cache.d.ts +1 -1
  194. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  195. package/dist/memory-projection-store.d.ts +1 -1
  196. package/dist/memory-provenance.d.ts +1 -1
  197. package/dist/memory-worth-outcomes.d.ts +2 -2
  198. package/dist/models-json.d.ts +1 -1
  199. package/dist/namespaces/migrate.d.ts +3 -3
  200. package/dist/namespaces/migrate.js +19 -19
  201. package/dist/namespaces/principal.d.ts +1 -1
  202. package/dist/namespaces/search.d.ts +1 -1
  203. package/dist/namespaces/search.js +10 -10
  204. package/dist/namespaces/storage.d.ts +3 -3
  205. package/dist/namespaces/storage.js +9 -9
  206. package/dist/native-knowledge.d.ts +1 -1
  207. package/dist/operator-toolkit.d.ts +2 -2
  208. package/dist/operator-toolkit.js +25 -25
  209. package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -2
  210. package/dist/orchestration/maintenance.d.ts +2 -2
  211. package/dist/orchestration/maintenance.js +11 -11
  212. package/dist/{orchestrator-B2Y28Z9t.d.ts → orchestrator-CWFrT8AF.d.ts} +91 -60
  213. package/dist/orchestrator.d.ts +5 -5
  214. package/dist/orchestrator.js +85 -83
  215. package/dist/patterns-cli.d.ts +1 -1
  216. package/dist/policy-runtime.d.ts +1 -1
  217. package/dist/provenance.d.ts +1 -1
  218. package/dist/{qmd-Db9mOkzB.d.ts → qmd--fnPMK60.d.ts} +1 -1
  219. package/dist/qmd-preflight.d.ts +2 -2
  220. package/dist/qmd-recall-cache.d.ts +1 -1
  221. package/dist/qmd.d.ts +2 -2
  222. package/dist/recall-disclosure-escalation.d.ts +1 -1
  223. package/dist/recall-explain-renderer.d.ts +1 -1
  224. package/dist/recall-planner-llm.d.ts +1 -1
  225. package/dist/recall-state.d.ts +1 -1
  226. package/dist/recall-tag-filter.d.ts +1 -1
  227. package/dist/recall-timings.d.ts +1 -1
  228. package/dist/recall-xray-cli.d.ts +1 -1
  229. package/dist/recall-xray-renderer.d.ts +1 -1
  230. package/dist/recall-xray.d.ts +1 -1
  231. package/dist/resolve-auth-token.d.ts +1 -1
  232. package/dist/resume-bundles.js +4 -4
  233. package/dist/retrieval-agents.d.ts +2 -2
  234. package/dist/retrieval-tiers.d.ts +1 -1
  235. package/dist/routing/engine.d.ts +1 -1
  236. package/dist/routing/store.d.ts +1 -1
  237. package/dist/schemas.d.ts +22 -22
  238. package/dist/search/document-scanner.js +2 -2
  239. package/dist/search/embed-helper.d.ts +1 -1
  240. package/dist/search/factory.d.ts +1 -1
  241. package/dist/search/factory.js +9 -9
  242. package/dist/search/index.d.ts +1 -1
  243. package/dist/search/index.js +13 -13
  244. package/dist/search/lancedb-backend.d.ts +1 -1
  245. package/dist/search/lancedb-backend.js +3 -3
  246. package/dist/search/meilisearch-backend.d.ts +1 -1
  247. package/dist/search/meilisearch-backend.js +3 -3
  248. package/dist/search/noop-backend.d.ts +1 -1
  249. package/dist/search/orama-backend.d.ts +1 -1
  250. package/dist/search/orama-backend.js +3 -3
  251. package/dist/search/port.d.ts +1 -1
  252. package/dist/search/remote-backend.d.ts +1 -1
  253. package/dist/{semantic-consolidation-BDVIbLRw.d.ts → semantic-consolidation-O5lMZ4LG.d.ts} +1 -1
  254. package/dist/semantic-consolidation.d.ts +2 -2
  255. package/dist/semantic-consolidation.js +11 -11
  256. package/dist/semantic-rule-promotion.js +9 -9
  257. package/dist/semantic-rule-verifier.d.ts +1 -1
  258. package/dist/semantic-rule-verifier.js +10 -10
  259. package/dist/session-observer-bands.d.ts +1 -1
  260. package/dist/session-observer-state.d.ts +1 -1
  261. package/dist/shared-context/manager.d.ts +1 -1
  262. package/dist/signal.d.ts +1 -1
  263. package/dist/storage-C1maCNbc.d.ts +1591 -0
  264. package/dist/storage.d.ts +5 -1239
  265. package/dist/storage.js +8 -8
  266. package/dist/summarizer.d.ts +1 -1
  267. package/dist/summary-snapshot.d.ts +1 -1
  268. package/dist/temporal-supersession.d.ts +2 -2
  269. package/dist/temporal-validity.d.ts +1 -1
  270. package/dist/threading.d.ts +1 -1
  271. package/dist/tier-migration.d.ts +2 -2
  272. package/dist/tier-routing.d.ts +1 -1
  273. package/dist/{tier-stats-TG6GUZ6N.js → tier-stats-5XF4UBRQ.js} +4 -4
  274. package/dist/tokens.d.ts +17 -2
  275. package/dist/tokens.js +3 -1
  276. package/dist/topics.d.ts +1 -1
  277. package/dist/transcript.d.ts +1 -1
  278. package/dist/transfer/backup.js +2 -2
  279. package/dist/transfer/capsule-export.js +3 -3
  280. package/dist/transfer/capsule-import.js +3 -3
  281. package/dist/transfer/types.d.ts +12 -12
  282. package/dist/trust-score-stage.d.ts +1 -1
  283. package/dist/trust-score.d.ts +1 -1
  284. package/dist/{types-ljkd82rn.d.ts → types-CGgcNWCG.d.ts} +31 -1
  285. package/dist/types.d.ts +1 -1
  286. package/dist/utility-runtime.d.ts +1 -1
  287. package/dist/verified-recall.js +10 -10
  288. package/package.json +2 -2
  289. package/src/access-boundary.test.ts +62 -0
  290. package/src/access-boundary.ts +165 -126
  291. package/src/access-http.test.ts +781 -0
  292. package/src/access-http.ts +334 -108
  293. package/src/access-mcp.test.ts +264 -0
  294. package/src/access-mcp.ts +45 -70
  295. package/src/access-operations-batch.ts +35 -12
  296. package/src/access-surface-catalog.test.ts +22 -14
  297. package/src/access-surface-catalog.ts +6 -3
  298. package/src/access-token-capabilities.test.ts +844 -0
  299. package/src/access-token-capabilities.ts +391 -0
  300. package/src/chat/chat-mcp.test.ts +232 -0
  301. package/src/chat/chat-session.ts +34 -0
  302. package/src/cli-wearables-fuse.test.ts +197 -0
  303. package/src/cli.ts +25 -0
  304. package/src/index.ts +14 -0
  305. package/src/mcp-read-only-tools.ts +61 -0
  306. package/src/storage.ts +16 -0
  307. package/src/tokens.ts +32 -3
  308. package/src/wearables/cli.test.ts +102 -0
  309. package/src/wearables/cli.ts +65 -0
  310. package/src/wearables/config.test.ts +32 -0
  311. package/src/wearables/config.ts +52 -0
  312. package/src/wearables/day-store.test.ts +246 -0
  313. package/src/wearables/day-store.ts +272 -3
  314. package/src/wearables/fusion/cluster.ts +230 -0
  315. package/src/wearables/fusion/fuse.ts +218 -0
  316. package/src/wearables/fusion/fusion.test.ts +2575 -0
  317. package/src/wearables/fusion/index.ts +50 -0
  318. package/src/wearables/fusion/reconcile.ts +831 -0
  319. package/src/wearables/fusion/reconstruct.ts +251 -0
  320. package/src/wearables/fusion/store.ts +480 -0
  321. package/src/wearables/fusion/types.ts +228 -0
  322. package/src/wearables/index.ts +41 -0
  323. package/src/wearables/service.test.ts +1030 -2
  324. package/src/wearables/service.ts +258 -16
  325. package/src/wearables/speakers.test.ts +128 -0
  326. package/src/wearables/speakers.ts +58 -4
  327. package/src/wearables/storage-io.test.ts +146 -0
  328. package/src/wearables/types.ts +31 -0
  329. package/dist/chunk-2FOD2YY3.js.map +0 -1
  330. package/dist/chunk-7QWMTOIF.js.map +0 -1
  331. package/dist/chunk-DR6PEJU3.js.map +0 -1
  332. package/dist/chunk-E2SPGGUI.js.map +0 -1
  333. package/dist/chunk-FZGGZAAU.js.map +0 -1
  334. package/dist/chunk-ILFJ7AVS.js.map +0 -1
  335. package/dist/chunk-M7GGMGMZ.js.map +0 -1
  336. package/dist/chunk-M7XQSUBB.js.map +0 -1
  337. /package/dist/{capsule-crypto-CZJSLEFG.js.map → access-token-capabilities.js.map} +0 -0
  338. /package/dist/{auto-sync-HMHQHUAG.js.map → auto-sync-4A5YBWYO.js.map} +0 -0
  339. /package/dist/{chunk-6ZIETXZ2.js.map → chunk-23JFRB73.js.map} +0 -0
  340. /package/dist/{chunk-IC6QX5TC.js.map → chunk-3XIY7MDQ.js.map} +0 -0
  341. /package/dist/{chunk-LOR6ZM5R.js.map → chunk-4LV23CHK.js.map} +0 -0
  342. /package/dist/{chunk-AER6MT24.js.map → chunk-6SXVCD7W.js.map} +0 -0
  343. /package/dist/{chunk-OHDJ34CC.js.map → chunk-72WG5QKN.js.map} +0 -0
  344. /package/dist/{chunk-CZAZX7JP.js.map → chunk-7MW3CVLD.js.map} +0 -0
  345. /package/dist/{chunk-CXLNZXG5.js.map → chunk-7XGSIUP5.js.map} +0 -0
  346. /package/dist/{chunk-CZAA5N5S.js.map → chunk-AYZJID4S.js.map} +0 -0
  347. /package/dist/{chunk-6BISDEN6.js.map → chunk-BEM4D2QG.js.map} +0 -0
  348. /package/dist/{chunk-ZPOROUTX.js.map → chunk-CYWA2CXR.js.map} +0 -0
  349. /package/dist/{chunk-M5QKGHCR.js.map → chunk-CYWCKFCL.js.map} +0 -0
  350. /package/dist/{chunk-4BFRBP45.js.map → chunk-DEG5ULFJ.js.map} +0 -0
  351. /package/dist/{chunk-JLR3JSTL.js.map → chunk-DS7MM2ES.js.map} +0 -0
  352. /package/dist/{chunk-2BFLDH5F.js.map → chunk-EEIROWEG.js.map} +0 -0
  353. /package/dist/{chunk-X4A62ET7.js.map → chunk-H2DXDQCQ.js.map} +0 -0
  354. /package/dist/{chunk-27FLVMDC.js.map → chunk-HLN7RROI.js.map} +0 -0
  355. /package/dist/{chunk-2U2A5HF4.js.map → chunk-J3XHPFEU.js.map} +0 -0
  356. /package/dist/{chunk-AKUCU7CI.js.map → chunk-JI5AVXYO.js.map} +0 -0
  357. /package/dist/{chunk-KCJ5H6XQ.js.map → chunk-JSNVWNGX.js.map} +0 -0
  358. /package/dist/{chunk-FLVL4L3L.js.map → chunk-KTULX4YO.js.map} +0 -0
  359. /package/dist/{chunk-437S3G37.js.map → chunk-LNQUAJNC.js.map} +0 -0
  360. /package/dist/{chunk-EPGWWCZF.js.map → chunk-LR5DZ56F.js.map} +0 -0
  361. /package/dist/{chunk-YDSWRD33.js.map → chunk-N3JDIRBW.js.map} +0 -0
  362. /package/dist/{chunk-OUMTBFSQ.js.map → chunk-NKCPUHCI.js.map} +0 -0
  363. /package/dist/{chunk-BGTAL46Y.js.map → chunk-PNLFN2PZ.js.map} +0 -0
  364. /package/dist/{chunk-P6SYWE4G.js.map → chunk-PVUJ6WVV.js.map} +0 -0
  365. /package/dist/{chunk-YPEMNNNY.js.map → chunk-RGK27ELN.js.map} +0 -0
  366. /package/dist/{chunk-2QSZNTDO.js.map → chunk-RKNJBZ55.js.map} +0 -0
  367. /package/dist/{chunk-66FLU5BT.js.map → chunk-SIRPLF54.js.map} +0 -0
  368. /package/dist/{chunk-HRCACVHL.js.map → chunk-TLUDFPZL.js.map} +0 -0
  369. /package/dist/{chunk-2MR3MFQB.js.map → chunk-UPSAT4PY.js.map} +0 -0
  370. /package/dist/{chunk-4P4626RY.js.map → chunk-UTRJFDP2.js.map} +0 -0
  371. /package/dist/{chunk-CGS7UKWM.js.map → chunk-VCW74PNH.js.map} +0 -0
  372. /package/dist/{chunk-5GPPACXK.js.map → chunk-VSMRDBFT.js.map} +0 -0
  373. /package/dist/{chunk-NG7YBCJO.js.map → chunk-XCL57M6Q.js.map} +0 -0
  374. /package/dist/{chunk-CLX5VDNY.js.map → chunk-Y4RKAQZ7.js.map} +0 -0
  375. /package/dist/{chunk-DGYCWCQ7.js.map → chunk-Y7UNNAZD.js.map} +0 -0
  376. /package/dist/{chunk-EZ25VE3G.js.map → chunk-YNDLCWXS.js.map} +0 -0
  377. /package/dist/{chunk-XN3TUUX2.js.map → chunk-YQB32INI.js.map} +0 -0
  378. /package/dist/{first-start-migration-DY6YCRC2.js.map → first-start-migration-VBJQL6OI.js.map} +0 -0
  379. /package/dist/{tier-stats-TG6GUZ6N.js.map → tier-stats-5XF4UBRQ.js.map} +0 -0
@@ -2,13 +2,26 @@ import assert from "node:assert/strict";
2
2
  import { mkdtempSync, rmSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import * as path from "node:path";
5
+ import { realpath as fsRealpath } from "node:fs/promises";
5
6
  import { test } from "node:test";
6
7
 
7
8
  import {
9
+ bodyIsEscaped,
8
10
  composeDayTranscriptBody,
9
11
  composeDayTranscriptMeta,
12
+ parseDayTranscript,
10
13
  serializeDayTranscript,
11
14
  } from "./day-store.js";
15
+ import {
16
+ DEFAULT_SOURCE_TRUST,
17
+ FUSION_ALGO_VERSION,
18
+ FusionArtifactStore,
19
+ canonicalDayKey,
20
+ composeFusionDayMeta,
21
+ parseFusionDay,
22
+ reconstructFusionInputs,
23
+ serializeFusionDay,
24
+ } from "./fusion/index.js";
12
25
  import { emptySpeakerRegistry } from "./speakers.js";
13
26
  import {
14
27
  createWearableMemoryWriter,
@@ -34,6 +47,46 @@ function makeStorage(memoryDir: string): WearableStorageIo & {
34
47
  content: string;
35
48
  }>;
36
49
  } {
50
+ const fusedFiles = new Map<string, string>();
51
+ const fusionStore = new FusionArtifactStore(path.join(memoryDir, "wearables"), memoryDir, {
52
+ writeFile: async (filePath, content) => {
53
+ fusedFiles.set(filePath, content);
54
+ },
55
+ readFile: async (filePath) => {
56
+ if (!fusedFiles.has(filePath)) {
57
+ const err = new Error(`ENOENT: ${filePath}`) as NodeJS.ErrnoException;
58
+ err.code = "ENOENT";
59
+ throw err;
60
+ }
61
+ return fusedFiles.get(filePath)!;
62
+ },
63
+ readDir: async (dirPath) =>
64
+ [...fusedFiles.keys()]
65
+ .filter((key) => path.dirname(key) === dirPath)
66
+ .map((key) => path.basename(key)),
67
+ deleteFile: async (filePath) => {
68
+ if (!fusedFiles.delete(filePath)) {
69
+ const err = new Error(`ENOENT: ${filePath}`) as NodeJS.ErrnoException;
70
+ err.code = "ENOENT";
71
+ throw err;
72
+ }
73
+ },
74
+ realpath: (filePath) => fsRealpath(filePath),
75
+ lstat: async (filePath) => {
76
+ // The in-memory mock models no real symlinks: a `_fusion` dir
77
+ // "exists" when at least one fused file lives beneath it, and is
78
+ // never itself a symbolic link.
79
+ const hasChildren = [...fusedFiles.keys()].some(
80
+ (k) => path.dirname(k) === filePath,
81
+ );
82
+ if (!hasChildren) {
83
+ const err = new Error(`ENOENT: ${filePath}`) as NodeJS.ErrnoException;
84
+ err.code = "ENOENT";
85
+ throw err;
86
+ }
87
+ return { isSymbolicLink: false };
88
+ },
89
+ });
37
90
  const files = new Map<string, string>();
38
91
  const storage = {
39
92
  dir: memoryDir,
@@ -65,6 +118,9 @@ function makeStorage(memoryDir: string): WearableStorageIo & {
65
118
  .filter((entry) => sourceId === undefined || entry.source === sourceId)
66
119
  .sort((a, b) => (a.date > b.date ? -1 : a.date < b.date ? 1 : 0));
67
120
  },
121
+ fusionArtifactStore() {
122
+ return fusionStore;
123
+ },
68
124
  async readAllMemories() {
69
125
  return storage.memories;
70
126
  },
@@ -110,6 +166,7 @@ function storeDay(
110
166
  sourceId: string,
111
167
  date: string,
112
168
  texts: string[],
169
+ timezone = "UTC",
113
170
  ): void {
114
171
  const registry = emptySpeakerRegistry();
115
172
  const conversations: WearableConversation[] = [
@@ -126,11 +183,11 @@ function storeDay(
126
183
  })),
127
184
  },
128
185
  ];
129
- const body = composeDayTranscriptBody(sourceId, date, "UTC", conversations, registry);
186
+ const body = composeDayTranscriptBody(sourceId, date, timezone, conversations, registry);
130
187
  const meta = composeDayTranscriptMeta(
131
188
  sourceId,
132
189
  date,
133
- "UTC",
190
+ timezone,
134
191
  conversations,
135
192
  registry,
136
193
  body,
@@ -273,6 +330,59 @@ test("zero in-scope indexed hits fall back to the bounded scan", async () => {
273
330
  }
274
331
  });
275
332
 
333
+ test("indexed search escapes the query and decodes the snippet (#1849)", async () => {
334
+ const dir = mkdtempSync(path.join(tmpdir(), "remnic-service-"));
335
+ try {
336
+ const storage = makeStorage(dir);
337
+ // Segment text carries a literal backslash. In the stored file the
338
+ // backslash is doubled, so the indexed text has C:\\GAPDIR while
339
+ // the original had C:\GAPDIR. The indexed backend must receive
340
+ // the escaped form to match the indexed representation.
341
+ storeDay(storage, "limitless", "2026-06-10", ["File at C:\\GAPDIR\\report.txt"]);
342
+
343
+ const receivedQueries: string[] = [];
344
+ const service = new WearablesService({
345
+ config: { ...defaultWearablesConfig(), enabled: true },
346
+ getStorage: async () => storage,
347
+ extract: null,
348
+ searchBackend: {
349
+ async search(query: string) {
350
+ receivedQueries.push(query);
351
+ // Simulate a hit whose preview carries the escaped form.
352
+ return [
353
+ {
354
+ path: "/memory/wearables/limitless/2026-06-10.md",
355
+ score: 0.9,
356
+ preview: "**Me (you)** [10:00]: File at C:\\\\GAPDIR\\\\report.txt",
357
+ },
358
+ ];
359
+ },
360
+ },
361
+ });
362
+
363
+ const results = await service.searchTranscripts("C:\\GAPDIR");
364
+ assert.equal(results.length, 1);
365
+ assert.equal(results[0].backend, "indexed");
366
+ assert.equal(results[0].source, "limitless");
367
+ // The escaped query form (doubled backslash) must reach the backend.
368
+ assert.ok(
369
+ receivedQueries.some((q) => q === "C:\\\\GAPDIR"),
370
+ "indexed backend must receive escaped query for backslash content",
371
+ );
372
+ // The snippet must show the DECODED original (single backslash).
373
+ assert.ok(
374
+ results[0].snippet.includes("C:\\GAPDIR"),
375
+ "snippet shows decoded single backslash",
376
+ );
377
+ assert.ok(
378
+ !results[0].snippet.includes("C:\\\\GAPDIR"),
379
+ "no escaped-backslash leak in snippet",
380
+ );
381
+ } finally {
382
+ rmSync(dir, { recursive: true, force: true });
383
+ }
384
+ });
385
+
276
386
  test("transcriptMemories filters by wearable source and day", async () => {
277
387
  const dir = mkdtempSync(path.join(tmpdir(), "remnic-service-"));
278
388
  try {
@@ -595,3 +705,921 @@ test("speaker and correction management round-trips through the service", async
595
705
  rmSync(dir, { recursive: true, force: true });
596
706
  }
597
707
  });
708
+
709
+ test("fuseDay is gated by wearables.fusion.enabled", async () => {
710
+ const storage = makeStorage(mkdtempSync(path.join(tmpdir(), "remnic-fusion-")));
711
+ try {
712
+ storeDay(storage, "limitless", "2026-06-10", ["Hello world."]);
713
+ const service = makeService(storage); // fusion defaults to disabled
714
+ await assert.rejects(() => service.fuseDay("2026-06-10"), /fusion is not enabled/);
715
+ } finally {
716
+ rmSync(storage.dir, { recursive: true, force: true });
717
+ }
718
+ });
719
+
720
+ test("fuseDay honors the wearables master gate before the fusion gate (issue #1849)", async () => {
721
+ const storage = makeStorage(mkdtempSync(path.join(tmpdir(), "remnic-fusion-")));
722
+ try {
723
+ storeDay(storage, "limitless", "2026-06-10", ["Hello world."]);
724
+ // wearables.enabled=false but fusion.enabled=true — fuseDay must refuse
725
+ // at the master gate (the same assertEnabled() sync/checkAuth use),
726
+ // before reading any source or touching the derived artifact store.
727
+ const service = makeService(storage, {
728
+ enabled: false,
729
+ sources: {
730
+ limitless: { ...defaultWearableSourceSettings(), enabled: true },
731
+ },
732
+ fusion: { enabled: true, proximityGapMs: 300_000, windowToleranceMs: 30_000 },
733
+ });
734
+ await assert.rejects(
735
+ () => service.fuseDay("2026-06-10"),
736
+ /wearables are not enabled/,
737
+ );
738
+ // The master-gate refusal must not write or delete any fusion artifact.
739
+ assert.equal(
740
+ await storage.fusionArtifactStore().readFusedDay("2026-06-10"),
741
+ null,
742
+ "master-gate refusal must not mutate derived fusion state",
743
+ );
744
+ } finally {
745
+ rmSync(storage.dir, { recursive: true, force: true });
746
+ }
747
+ });
748
+
749
+ test("fuseDay writes a derived artifact, is idempotent, and leaves raw transcripts untouched", async () => {
750
+ const storage = makeStorage(mkdtempSync(path.join(tmpdir(), "remnic-fusion-")));
751
+ try {
752
+ // Two enabled sources recorded the same overlapping window.
753
+ storeDay(storage, "limitless", "2026-06-10", ["We agreed to ship Friday."]);
754
+ storeDay(storage, "bee", "2026-06-10", ["We agreed to ship Friday."]);
755
+ const service = makeService(storage, {
756
+ sources: {
757
+ limitless: { ...defaultWearableSourceSettings(), enabled: true },
758
+ bee: { ...defaultWearableSourceSettings(), enabled: true },
759
+ },
760
+ fusion: {
761
+ enabled: true,
762
+ proximityGapMs: 300_000,
763
+ windowToleranceMs: 30_000,
764
+ },
765
+ });
766
+
767
+ // No fused artifact yet.
768
+ assert.equal(await storage.fusionArtifactStore().readFusedDay("2026-06-10"), null);
769
+
770
+ const first = await service.fuseDay("2026-06-10");
771
+ assert.equal(first.written, true);
772
+ assert.ok(first.conversationCount >= 1, "overlapping sources fuse into >=1 conversation");
773
+ assert.deepEqual([...first.sources].sort(), ["bee", "limitless"]);
774
+
775
+ // The derived artifact is readable via the listing surface.
776
+ const conversations = await service.fusedConversations("2026-06-10");
777
+ assert.equal(conversations.length, first.conversationCount);
778
+ assert.ok(conversations[0]!.id.startsWith("fusion-"));
779
+ assert.deepEqual(
780
+ [...conversations[0]!.sources].sort(),
781
+ ["bee", "limitless"],
782
+ );
783
+
784
+ // Idempotent re-run: identical inputs -> not written again.
785
+ const second = await service.fuseDay("2026-06-10");
786
+ assert.equal(second.written, false, "unchanged inputs must not rewrite the artifact");
787
+ assert.equal(second.contentHash, first.contentHash);
788
+
789
+ // Raw per-source transcripts remain readable and untouched.
790
+ const limitlessRaw = await storage.readWearableDayTranscript("limitless", "2026-06-10");
791
+ const beeRaw = await storage.readWearableDayTranscript("bee", "2026-06-10");
792
+ assert.notEqual(limitlessRaw, null);
793
+ assert.notEqual(beeRaw, null);
794
+ // Raw transcripts are byte-identical to what was stored (fusion never
795
+ // overwrites source transcripts).
796
+ assert.ok(limitlessRaw?.includes("We agreed to ship Friday."));
797
+ assert.ok(beeRaw?.includes("We agreed to ship Friday."));
798
+
799
+ // listFusedDays surfaces the fused date.
800
+ assert.deepEqual(await service.listFusedDays(), ["2026-06-10"]);
801
+ } finally {
802
+ rmSync(storage.dir, { recursive: true, force: true });
803
+ }
804
+ });
805
+
806
+ test("fuseDay rebuilds the artifact when fusion config changes (not skipped)", async () => {
807
+ const storage = makeStorage(mkdtempSync(path.join(tmpdir(), "remnic-fusion-")));
808
+ try {
809
+ // Two enabled sources recorded the same overlapping window.
810
+ storeDay(storage, "limitless", "2026-06-10", ["We agreed to ship Friday."]);
811
+ storeDay(storage, "bee", "2026-06-10", ["We agreed to ship Friday."]);
812
+
813
+ const baseSources = {
814
+ limitless: { ...defaultWearableSourceSettings(), enabled: true },
815
+ bee: { ...defaultWearableSourceSettings(), enabled: true },
816
+ };
817
+
818
+ const serviceGap5m = makeService(storage, {
819
+ sources: baseSources,
820
+ fusion: { enabled: true, proximityGapMs: 300_000, windowToleranceMs: 30_000 },
821
+ });
822
+
823
+ // Initial fuse writes the artifact.
824
+ const first = await serviceGap5m.fuseDay("2026-06-10");
825
+ assert.equal(first.written, true);
826
+ const firstHash = first.contentHash;
827
+
828
+ // Unchanged inputs + config => idempotent skip.
829
+ const rerun = await serviceGap5m.fuseDay("2026-06-10");
830
+ assert.equal(rerun.written, false, "unchanged inputs+config must not rewrite");
831
+ assert.equal(rerun.contentHash, firstHash);
832
+
833
+ // Tune proximityGapMs => new content hash => artifact rebuilt, not skipped.
834
+ const serviceGap10m = makeService(storage, {
835
+ sources: baseSources,
836
+ fusion: { enabled: true, proximityGapMs: 600_000, windowToleranceMs: 30_000 },
837
+ });
838
+ const afterGap = await serviceGap10m.fuseDay("2026-06-10");
839
+ assert.equal(afterGap.written, true, "proximityGapMs change must trigger a rebuild");
840
+ assert.notEqual(afterGap.contentHash, firstHash);
841
+
842
+ // Re-run under the new config => idempotent skip again.
843
+ const afterGapRerun = await serviceGap10m.fuseDay("2026-06-10");
844
+ assert.equal(afterGapRerun.written, false);
845
+ assert.equal(afterGapRerun.contentHash, afterGap.contentHash);
846
+
847
+ // Tune per-source trust => new content hash => artifact rebuilt.
848
+ const serviceTrust = makeService(storage, {
849
+ sources: {
850
+ limitless: { ...defaultWearableSourceSettings(), enabled: true, sourceTrust: 0.95 },
851
+ bee: { ...defaultWearableSourceSettings(), enabled: true },
852
+ },
853
+ fusion: { enabled: true, proximityGapMs: 600_000, windowToleranceMs: 30_000 },
854
+ });
855
+ const afterTrust = await serviceTrust.fuseDay("2026-06-10");
856
+ assert.equal(afterTrust.written, true, "per-source trust change must trigger a rebuild");
857
+ assert.notEqual(afterTrust.contentHash, afterGap.contentHash);
858
+ } finally {
859
+ rmSync(storage.dir, { recursive: true, force: true });
860
+ }
861
+ });
862
+
863
+ test("fuseDay regenerates a stale artifact written under an older algorithm version (issue #1849)", async () => {
864
+ const storage = makeStorage(mkdtempSync(path.join(tmpdir(), "remnic-fusion-")));
865
+ try {
866
+ storeDay(storage, "limitless", "2026-06-10", ["We agreed to ship Friday."]);
867
+ storeDay(storage, "bee", "2026-06-10", ["We agreed to ship Friday."]);
868
+ const baseSources = {
869
+ limitless: { ...defaultWearableSourceSettings(), enabled: true },
870
+ bee: { ...defaultWearableSourceSettings(), enabled: true },
871
+ };
872
+ const fusionConfig = { enabled: true, proximityGapMs: 300_000, windowToleranceMs: 30_000 };
873
+ const service = makeService(storage, { sources: baseSources, fusion: fusionConfig });
874
+
875
+ // Initial fuse writes the artifact under the current algorithm version.
876
+ const first = await service.fuseDay("2026-06-10");
877
+ assert.equal(first.written, true);
878
+ const currentHash = first.contentHash;
879
+
880
+ // Unchanged inputs + same version => idempotent skip.
881
+ const rerun = await service.fuseDay("2026-06-10");
882
+ assert.equal(rerun.written, false, "same version must be idempotent");
883
+
884
+ // Simulate a pre-bump artifact: reconstruct the same inputs the service
885
+ // fuses and compute the day hash under a STALE algorithm version, then
886
+ // overwrite the stored artifact with the SAME body but that stale hash.
887
+ const bodies = await Promise.all(
888
+ ["limitless", "bee"].map(async (source) => {
889
+ const raw = await storage.readWearableDayTranscript(source, "2026-06-10");
890
+ const parsed = parseDayTranscript(raw ?? "");
891
+ return {
892
+ source,
893
+ body: parsed?.body ?? raw ?? "",
894
+ escaped: bodyIsEscaped(parsed?.meta),
895
+ };
896
+ }),
897
+ );
898
+ const reconstructed = reconstructFusionInputs("2026-06-10", bodies);
899
+ const staleHash = canonicalDayKey(
900
+ "2026-06-10",
901
+ reconstructed,
902
+ {
903
+ proximityGapMs: 300_000,
904
+ windowToleranceMs: 30_000,
905
+ sourceTrust: { limitless: DEFAULT_SOURCE_TRUST, bee: DEFAULT_SOURCE_TRUST },
906
+ },
907
+ "2000-01-01-stale",
908
+ );
909
+ assert.notEqual(staleHash, currentHash, "sanity: stale-version hash differs");
910
+ // Sanity: the current-version hash matches what the service produced —
911
+ // the only difference between staleHash and currentHash is the version.
912
+ assert.equal(
913
+ canonicalDayKey(
914
+ "2026-06-10",
915
+ reconstructed,
916
+ {
917
+ proximityGapMs: 300_000,
918
+ windowToleranceMs: 30_000,
919
+ sourceTrust: { limitless: DEFAULT_SOURCE_TRUST, bee: DEFAULT_SOURCE_TRUST },
920
+ },
921
+ FUSION_ALGO_VERSION,
922
+ ),
923
+ currentHash,
924
+ "sanity: canonicalDayKey with the current version matches fuseDay output",
925
+ );
926
+
927
+ // Overwrite the stored artifact with the SAME conversations but the stale
928
+ // contentHash (same body => same bodyHash; only the version-derived
929
+ // contentHash differs).
930
+ const store = storage.fusionArtifactStore();
931
+ const rawArtifact = await store.readFusedDay("2026-06-10");
932
+ const parsedArtifact = parseFusionDay(rawArtifact ?? "");
933
+ assert.ok(parsedArtifact, "artifact must exist after initial fuse");
934
+ const staleMeta = composeFusionDayMeta(
935
+ "2026-06-10",
936
+ parsedArtifact!.conversations,
937
+ [...first.sources].sort(),
938
+ staleHash,
939
+ parsedArtifact!.meta.fusedAt,
940
+ );
941
+ await store.writeFusedDay(
942
+ "2026-06-10",
943
+ serializeFusionDay(staleMeta, parsedArtifact!.conversations),
944
+ );
945
+
946
+ // Re-fuse: the recomputed hash (current version) != stale stored hash, so
947
+ // the artifact is regenerated rather than skipped.
948
+ const afterStale = await service.fuseDay("2026-06-10");
949
+ assert.equal(afterStale.written, true, "stale algo-version artifact must be regenerated");
950
+ assert.equal(afterStale.contentHash, currentHash, "regenerated hash matches the current algorithm");
951
+ } finally {
952
+ rmSync(storage.dir, { recursive: true, force: true });
953
+ }
954
+ });
955
+ test("fuseDay rewrites an artifact whose body is corrupt despite a matching contentHash", async () => {
956
+ const storage = makeStorage(mkdtempSync(path.join(tmpdir(), "remnic-fusion-")));
957
+ try {
958
+ // Two enabled sources recorded the same overlapping window.
959
+ storeDay(storage, "limitless", "2026-06-10", ["We agreed to ship Friday."]);
960
+ storeDay(storage, "bee", "2026-06-10", ["We agreed to ship Friday."]);
961
+ const service = makeService(storage, {
962
+ sources: {
963
+ limitless: { ...defaultWearableSourceSettings(), enabled: true },
964
+ bee: { ...defaultWearableSourceSettings(), enabled: true },
965
+ },
966
+ fusion: {
967
+ enabled: true,
968
+ proximityGapMs: 300_000,
969
+ windowToleranceMs: 30_000,
970
+ },
971
+ });
972
+
973
+ // Initial fuse writes a valid artifact.
974
+ const first = await service.fuseDay("2026-06-10");
975
+ assert.equal(first.written, true);
976
+ assert.ok(first.conversationCount >= 1, "overlapping sources fuse into >=1 conversation");
977
+ const hash = first.contentHash;
978
+
979
+ // Corrupt the stored body while leaving the frontmatter (incl.
980
+ // contentHash) intact: a truncated/garbled JSON body that the lenient
981
+ // read parser reduces to zero conversations.
982
+ const fusionStore = storage.fusionArtifactStore();
983
+ const validRaw = await fusionStore.readFusedDay("2026-06-10");
984
+ assert.ok(validRaw);
985
+ const closeIdx = validRaw!.indexOf("\n---\n", 4);
986
+ assert.ok(closeIdx !== -1, "fused artifact has a closing frontmatter delimiter");
987
+ const corruptRaw = `${validRaw!.slice(0, closeIdx + 5)}\n{ this is not valid JSON`;
988
+ await fusionStore.writeFusedDay("2026-06-10", corruptRaw);
989
+
990
+ // The corrupted artifact surfaces as a corrupt-artifact error, not a
991
+ // clean empty list (issue #1849).
992
+ await assert.rejects(service.fusedConversations("2026-06-10"), /corrupt/);
993
+
994
+ // Re-fuse: matching contentHash but a corrupt body => self-repair
995
+ // (written: true), not a silent skip that leaves the bad file in place.
996
+ const repaired = await service.fuseDay("2026-06-10");
997
+ assert.equal(repaired.written, true, "a corrupt body must be rewritten despite a matching hash");
998
+ assert.equal(repaired.contentHash, hash, "inputs+config are unchanged so the hash is stable");
999
+ assert.equal(repaired.conversationCount, first.conversationCount);
1000
+
1001
+ // Subsequent read returns the correct conversations again.
1002
+ const conversations = await service.fusedConversations("2026-06-10");
1003
+ assert.equal(conversations.length, first.conversationCount);
1004
+ assert.ok(conversations[0]!.id.startsWith("fusion-"));
1005
+ assert.deepEqual(
1006
+ [...conversations[0]!.sources].sort(),
1007
+ ["bee", "limitless"],
1008
+ );
1009
+
1010
+ // A final re-run is idempotent again: the repaired body is valid, so
1011
+ // it is skipped (written: false), proving the rewrite was a one-off
1012
+ // self-repair rather than a permanent forced write.
1013
+ const idempotent = await service.fuseDay("2026-06-10");
1014
+ assert.equal(idempotent.written, false, "a repaired artifact must skip on the next run");
1015
+ assert.equal(idempotent.contentHash, hash);
1016
+ } finally {
1017
+ rmSync(storage.dir, { recursive: true, force: true });
1018
+ }
1019
+ });
1020
+
1021
+ test("fuseDay rewrites a corrupt body even when the expected conversation count is zero", async () => {
1022
+ const storage = makeStorage(mkdtempSync(path.join(tmpdir(), "remnic-fusion-")));
1023
+ try {
1024
+ // No stored transcripts for this day -> fusion yields ZERO conversations.
1025
+ const service = makeService(storage, {
1026
+ sources: {
1027
+ limitless: { ...defaultWearableSourceSettings(), enabled: true },
1028
+ },
1029
+ fusion: {
1030
+ enabled: true,
1031
+ proximityGapMs: 300_000,
1032
+ windowToleranceMs: 30_000,
1033
+ },
1034
+ });
1035
+
1036
+ // First fuse writes a valid (empty) artifact.
1037
+ const first = await service.fuseDay("2026-06-10");
1038
+ assert.equal(first.written, true);
1039
+ assert.equal(first.conversationCount, 0);
1040
+ const hash = first.contentHash;
1041
+
1042
+ // A legitimately-empty ([] body) artifact must SKIP on re-run: same
1043
+ // inputs + config, clean parse, 0 == 0 -> not rewritten.
1044
+ const idempotent = await service.fuseDay("2026-06-10");
1045
+ assert.equal(idempotent.written, false, "a valid empty body must skip");
1046
+ assert.equal(idempotent.contentHash, hash);
1047
+
1048
+ // Corrupt the stored body while leaving the frontmatter (incl.
1049
+ // contentHash) intact: truncated JSON that the lenient read parser
1050
+ // reduces to zero conversations.
1051
+ const fusionStore = storage.fusionArtifactStore();
1052
+ const validRaw = await fusionStore.readFusedDay("2026-06-10");
1053
+ assert.ok(validRaw);
1054
+ const closeIdx = validRaw!.indexOf("\n---\n", 4);
1055
+ assert.ok(closeIdx !== -1, "fused artifact has a closing frontmatter delimiter");
1056
+ const corruptRaw = `${validRaw!.slice(0, closeIdx + 5)}\n{ this is not valid JSON`;
1057
+ await fusionStore.writeFusedDay("2026-06-10", corruptRaw);
1058
+
1059
+ // The corrupted body surfaces as a corrupt-artifact error (not a clean
1060
+ // empty), and the recomputed result is ALSO zero — without the parseOk
1061
+ // signal this 0 == 0 match plus a matching hash would silently skip
1062
+ // and leave the bad file.
1063
+ await assert.rejects(service.fusedConversations("2026-06-10"), /corrupt/);
1064
+
1065
+ // Re-fuse: matching hash + 0 == 0 but a corrupt body (parseOk:false)
1066
+ // => self-repair (written: true), not a silent skip.
1067
+ const repaired = await service.fuseDay("2026-06-10");
1068
+ assert.equal(
1069
+ repaired.written,
1070
+ true,
1071
+ "a corrupt body must be rewritten even when the expected count is zero",
1072
+ );
1073
+ assert.equal(repaired.contentHash, hash);
1074
+ assert.equal(repaired.conversationCount, 0);
1075
+
1076
+ // A final re-run is idempotent again: the repaired body parses cleanly,
1077
+ // so it skips (written: false), proving the rewrite was a one-off
1078
+ // self-repair rather than a permanent forced write.
1079
+ const final = await service.fuseDay("2026-06-10");
1080
+ assert.equal(final.written, false, "a repaired artifact must skip on the next run");
1081
+ assert.equal(final.contentHash, hash);
1082
+ } finally {
1083
+ rmSync(storage.dir, { recursive: true, force: true });
1084
+ }
1085
+ });
1086
+
1087
+ test("fuseDay rewrites an artifact whose body has malformed elements despite matching hash + bodyHash + count", async () => {
1088
+ const storage = makeStorage(mkdtempSync(path.join(tmpdir(), "remnic-fusion-")));
1089
+ try {
1090
+ storeDay(storage, "limitless", "2026-06-10", ["We agreed to ship Friday."]);
1091
+ storeDay(storage, "bee", "2026-06-10", ["We agreed to ship Friday."]);
1092
+ const service = makeService(storage, {
1093
+ sources: {
1094
+ limitless: { ...defaultWearableSourceSettings(), enabled: true },
1095
+ bee: { ...defaultWearableSourceSettings(), enabled: true },
1096
+ },
1097
+ fusion: { enabled: true, proximityGapMs: 300_000, windowToleranceMs: 30_000 },
1098
+ });
1099
+
1100
+ const first = await service.fuseDay("2026-06-10");
1101
+ assert.equal(first.written, true);
1102
+ assert.ok(first.conversationCount >= 1);
1103
+ const hash = first.contentHash;
1104
+
1105
+ // Replace ONLY the body with a parseable-but-malformed array `[{}]`,
1106
+ // leaving every frontmatter field (contentHash, bodyHash, count)
1107
+ // intact. The body parses as JSON and is an array, but its element is
1108
+ // not a well-formed FusedWearableConversation — element validation
1109
+ // must drive parseOk:false so fuseDay rewrites rather than trusting
1110
+ // the matching hashes + count.
1111
+ const fusionStore = storage.fusionArtifactStore();
1112
+ const validRaw = await fusionStore.readFusedDay("2026-06-10");
1113
+ assert.ok(validRaw);
1114
+ const closeIdx = validRaw!.indexOf("\n---\n", 4);
1115
+ assert.ok(closeIdx !== -1);
1116
+ await fusionStore.writeFusedDay("2026-06-10", `${validRaw!.slice(0, closeIdx + 5)}\n[{}]`);
1117
+
1118
+ // The malformed body surfaces as a corrupt-artifact error (issue #1849).
1119
+ await assert.rejects(service.fusedConversations("2026-06-10"), /corrupt/);
1120
+
1121
+ // Re-fuse: matching hash + bodyHash + count but parseOk:false => rewrite.
1122
+ const repaired = await service.fuseDay("2026-06-10");
1123
+ assert.equal(repaired.written, true, "a malformed-element body must be rewritten");
1124
+ assert.equal(repaired.contentHash, hash);
1125
+ assert.equal(repaired.conversationCount, first.conversationCount);
1126
+
1127
+ // Repaired body is valid again, so the next run skips idempotently.
1128
+ const idempotent = await service.fuseDay("2026-06-10");
1129
+ assert.equal(idempotent.written, false, "a repaired artifact must skip on the next run");
1130
+ assert.equal(idempotent.contentHash, hash);
1131
+ } finally {
1132
+ rmSync(storage.dir, { recursive: true, force: true });
1133
+ }
1134
+ });
1135
+
1136
+ test("fusedConversations surfaces a corrupt artifact distinctly, not as a clean empty (issue #1849)", async () => {
1137
+ const storage = makeStorage(mkdtempSync(path.join(tmpdir(), "remnic-fusion-")));
1138
+ try {
1139
+ storeDay(storage, "limitless", "2026-06-10", ["We agreed to ship Friday."]);
1140
+ storeDay(storage, "bee", "2026-06-10", ["We agreed to ship Friday."]);
1141
+ const service = makeService(storage, {
1142
+ sources: {
1143
+ limitless: { ...defaultWearableSourceSettings(), enabled: true },
1144
+ bee: { ...defaultWearableSourceSettings(), enabled: true },
1145
+ },
1146
+ fusion: { enabled: true, proximityGapMs: 300_000, windowToleranceMs: 30_000 },
1147
+ });
1148
+
1149
+ // Initial fuse writes a valid artifact with >= 1 conversation.
1150
+ const first = await service.fuseDay("2026-06-10");
1151
+ assert.ok(first.conversationCount >= 1);
1152
+
1153
+ // A day that was never fused returns [] — the "no artifact" path.
1154
+ assert.deepEqual(await service.fusedConversations("2026-07-01"), []);
1155
+
1156
+ // Corrupt the body: valid frontmatter, garbled JSON body.
1157
+ const fusionStore = storage.fusionArtifactStore();
1158
+ const validRaw = await fusionStore.readFusedDay("2026-06-10");
1159
+ assert.ok(validRaw);
1160
+ const closeIdx = validRaw!.indexOf("\n---\n", 4);
1161
+ assert.ok(closeIdx !== -1);
1162
+ await fusionStore.writeFusedDay(
1163
+ "2026-06-10",
1164
+ `${validRaw!.slice(0, closeIdx + 5)}\n{ this is not valid JSON`,
1165
+ );
1166
+
1167
+ // The corrupt artifact must NOT look like "no conversations" — it must
1168
+ // throw so the caller/CLI can distinguish corruption from absence and
1169
+ // prompt a re-fuse, rather than silently returning an empty list.
1170
+ await assert.rejects(
1171
+ service.fusedConversations("2026-06-10"),
1172
+ /corrupt/,
1173
+ );
1174
+
1175
+ // The "never fused" path is unchanged — still returns [].
1176
+ assert.deepEqual(await service.fusedConversations("2026-07-01"), []);
1177
+ } finally {
1178
+ rmSync(storage.dir, { recursive: true, force: true });
1179
+ }
1180
+ });
1181
+
1182
+ test("fuseDay rewrites an artifact whose body bytes drifted despite matching contentHash + count", async () => {
1183
+ const storage = makeStorage(mkdtempSync(path.join(tmpdir(), "remnic-fusion-")));
1184
+ try {
1185
+ storeDay(storage, "limitless", "2026-06-10", ["We agreed to ship Friday."]);
1186
+ storeDay(storage, "bee", "2026-06-10", ["We agreed to ship Friday."]);
1187
+ const service = makeService(storage, {
1188
+ sources: {
1189
+ limitless: { ...defaultWearableSourceSettings(), enabled: true },
1190
+ bee: { ...defaultWearableSourceSettings(), enabled: true },
1191
+ },
1192
+ fusion: { enabled: true, proximityGapMs: 300_000, windowToleranceMs: 30_000 },
1193
+ });
1194
+
1195
+ const first = await service.fuseDay("2026-06-10");
1196
+ assert.equal(first.written, true);
1197
+ assert.ok(first.conversationCount >= 1);
1198
+ const hash = first.contentHash;
1199
+
1200
+ // Tamper: keep the body structurally valid (parses, every element is a
1201
+ // well-formed conversation, same conversation count) but ALTER its
1202
+ // bytes, leaving the frontmatter (contentHash + bodyHash + count)
1203
+ // untouched. parseOk stays true and contentHash still matches, so only
1204
+ // the body-hash recompute over the stored body can catch the drift.
1205
+ const fusionStore = storage.fusionArtifactStore();
1206
+ const validRaw = await fusionStore.readFusedDay("2026-06-10");
1207
+ assert.ok(validRaw);
1208
+ const closeIdx = validRaw!.indexOf("\n---\n", 4);
1209
+ assert.ok(closeIdx !== -1);
1210
+ const header = validRaw!.slice(0, closeIdx + 5);
1211
+ const bodyJson = validRaw!.slice(closeIdx + 5).replace(/^\n/, "").trimEnd();
1212
+ const convs = JSON.parse(bodyJson) as Array<{ segments: Array<{ text: string }> }>;
1213
+ convs[0]!.segments[0]!.text += " [tampered]";
1214
+ await fusionStore.writeFusedDay(
1215
+ "2026-06-10",
1216
+ `${header}\n${JSON.stringify(convs, null, 2)}\n`,
1217
+ );
1218
+
1219
+ // The tampered body still parses cleanly (valid structure + elements),
1220
+ // so it is served until the next fuseDay repairs it.
1221
+ const tampered = await service.fusedConversations("2026-06-10");
1222
+ assert.equal(tampered.length, first.conversationCount);
1223
+ assert.ok(tampered[0]!.segments[0]!.text.includes("[tampered]"));
1224
+
1225
+ // Re-fuse: contentHash matches + parseOk true, but the stored body
1226
+ // hash no longer matches a recompute over the stored body => rewrite.
1227
+ const repaired = await service.fuseDay("2026-06-10");
1228
+ assert.equal(
1229
+ repaired.written,
1230
+ true,
1231
+ "a byte-drifted body must be rewritten via the body-hash check",
1232
+ );
1233
+ assert.equal(repaired.contentHash, hash);
1234
+ assert.equal(repaired.conversationCount, first.conversationCount);
1235
+
1236
+ // The repaired body no longer carries the tampered text.
1237
+ const restored = await service.fusedConversations("2026-06-10");
1238
+ assert.ok(!restored[0]!.segments[0]!.text.includes("[tampered]"));
1239
+
1240
+ const idempotent = await service.fuseDay("2026-06-10");
1241
+ assert.equal(idempotent.written, false);
1242
+ assert.equal(idempotent.contentHash, hash);
1243
+ } finally {
1244
+ rmSync(storage.dir, { recursive: true, force: true });
1245
+ }
1246
+ });
1247
+
1248
+ test("fuseDay skips a day whose sources were rendered under conflicting timezones", async () => {
1249
+ const storage = makeStorage(mkdtempSync(path.join(tmpdir(), "remnic-fusion-")));
1250
+ try {
1251
+ // Two enabled sources recorded the same window but in DIFFERENT
1252
+ // timezones whose UTC offsets genuinely differ on this date
1253
+ // (America/Los_Angeles is UTC-7 in summer; Asia/Tokyo is UTC+9).
1254
+ storeDay(
1255
+ storage,
1256
+ "limitless",
1257
+ "2026-06-10",
1258
+ ["We agreed to ship Friday."],
1259
+ "America/Los_Angeles",
1260
+ );
1261
+ storeDay(
1262
+ storage,
1263
+ "bee",
1264
+ "2026-06-10",
1265
+ ["We agreed to ship Friday."],
1266
+ "Asia/Tokyo",
1267
+ );
1268
+ const service = makeService(storage, {
1269
+ sources: {
1270
+ limitless: { ...defaultWearableSourceSettings(), enabled: true },
1271
+ bee: { ...defaultWearableSourceSettings(), enabled: true },
1272
+ },
1273
+ fusion: { enabled: true, proximityGapMs: 300_000, windowToleranceMs: 30_000 },
1274
+ });
1275
+
1276
+ const result = await service.fuseDay("2026-06-10");
1277
+ // Fail-safe: the day is skipped with a reason, not silently misaligned.
1278
+ assert.equal(result.written, false);
1279
+ assert.equal(result.skipped?.reason, "conflicting-timezones");
1280
+ assert.equal(result.sources.length, 0);
1281
+ // No artifact is written for the conflicting day.
1282
+ assert.equal(await storage.fusionArtifactStore().readFusedDay("2026-06-10"), null);
1283
+ } finally {
1284
+ rmSync(storage.dir, { recursive: true, force: true });
1285
+ }
1286
+ });
1287
+
1288
+ test("fuseDay clears a stale fused artifact when a later run skips on conflicting timezones", async () => {
1289
+ const storage = makeStorage(mkdtempSync(path.join(tmpdir(), "remnic-fusion-")));
1290
+ try {
1291
+ // First, both sources share one timezone -> the day fuses successfully.
1292
+ storeDay(
1293
+ storage,
1294
+ "limitless",
1295
+ "2026-06-10",
1296
+ ["We agreed to ship Friday."],
1297
+ "America/Los_Angeles",
1298
+ );
1299
+ storeDay(
1300
+ storage,
1301
+ "bee",
1302
+ "2026-06-10",
1303
+ ["We agreed to ship Friday."],
1304
+ "America/Los_Angeles",
1305
+ );
1306
+ const service = makeService(storage, {
1307
+ sources: {
1308
+ limitless: { ...defaultWearableSourceSettings(), enabled: true },
1309
+ bee: { ...defaultWearableSourceSettings(), enabled: true },
1310
+ },
1311
+ fusion: { enabled: true, proximityGapMs: 300_000, windowToleranceMs: 30_000 },
1312
+ });
1313
+
1314
+ const first = await service.fuseDay("2026-06-10");
1315
+ assert.equal(first.written, true);
1316
+ assert.ok(first.conversationCount >= 1, "same-timezone sources fuse");
1317
+ // A fused artifact now exists and is served by the listing surface.
1318
+ assert.notEqual(await storage.fusionArtifactStore().readFusedDay("2026-06-10"), null);
1319
+ assert.ok((await service.fusedConversations("2026-06-10")).length >= 1);
1320
+
1321
+ // Later sync: bee's source is re-rendered under a DIFFERENT timezone
1322
+ // (Asia/Tokyo, UTC+9), conflicting with limitless (UTC-7 in summer).
1323
+ storeDay(
1324
+ storage,
1325
+ "bee",
1326
+ "2026-06-10",
1327
+ ["We agreed to ship Friday."],
1328
+ "Asia/Tokyo",
1329
+ );
1330
+
1331
+ // Re-fuse: the day is now skipped, and the stale artifact MUST be
1332
+ // cleared so fusedConversations() stops serving it (issue #1849).
1333
+ const result = await service.fuseDay("2026-06-10");
1334
+ assert.equal(result.written, false);
1335
+ assert.equal(result.skipped?.reason, "conflicting-timezones");
1336
+ assert.equal(await storage.fusionArtifactStore().readFusedDay("2026-06-10"), null);
1337
+ assert.equal((await service.fusedConversations("2026-06-10")).length, 0);
1338
+ } finally {
1339
+ rmSync(storage.dir, { recursive: true, force: true });
1340
+ }
1341
+ });
1342
+
1343
+ test("fuseDay skips sources that share a UTC offset but differ in timezone id", async () => {
1344
+ const storage = makeStorage(mkdtempSync(path.join(tmpdir(), "remnic-fusion-")));
1345
+ try {
1346
+ // America/Los_Angeles and America/Tijuana are both UTC-7 on this
1347
+ // summer date, so the OLD noon-offset guard fused them. Under the
1348
+ // tz-identity model their DIFFERENT IANA ids must skip instead:
1349
+ // reconstructFusionInputs only compares local HH:MM clocks safely
1350
+ // when every source carries one explicit, identical tz id.
1351
+ storeDay(
1352
+ storage,
1353
+ "limitless",
1354
+ "2026-06-10",
1355
+ ["We agreed to ship Friday."],
1356
+ "America/Los_Angeles",
1357
+ );
1358
+ storeDay(
1359
+ storage,
1360
+ "bee",
1361
+ "2026-06-10",
1362
+ ["We agreed to ship Friday."],
1363
+ "America/Tijuana",
1364
+ );
1365
+ const service = makeService(storage, {
1366
+ sources: {
1367
+ limitless: { ...defaultWearableSourceSettings(), enabled: true },
1368
+ bee: { ...defaultWearableSourceSettings(), enabled: true },
1369
+ },
1370
+ fusion: { enabled: true, proximityGapMs: 300_000, windowToleranceMs: 30_000 },
1371
+ });
1372
+
1373
+ const result = await service.fuseDay("2026-06-10");
1374
+ assert.equal(result.written, false, "differing tz ids skip even at equal offset");
1375
+ assert.equal(result.skipped?.reason, "conflicting-timezones");
1376
+ assert.equal(result.sources.length, 0);
1377
+ assert.equal(await storage.fusionArtifactStore().readFusedDay("2026-06-10"), null);
1378
+ assert.equal((await service.fusedConversations("2026-06-10")).length, 0);
1379
+ } finally {
1380
+ rmSync(storage.dir, { recursive: true, force: true });
1381
+ }
1382
+ });
1383
+
1384
+ test("fuseDay skips a DST-date pair whose noon offsets coincide (LA vs Phoenix on 2026-03-08)", async () => {
1385
+ const storage = makeStorage(mkdtempSync(path.join(tmpdir(), "remnic-fusion-")));
1386
+ try {
1387
+ // Regression for the codex finding: on 2026-03-08 America/Los_Angeles
1388
+ // springs forward (UTC-8 -> UTC-7) at 02:00 while America/Phoenix
1389
+ // stays UTC-7 all day. Both zones are UTC-7 by local NOON, so the old
1390
+ // single-offset guard fused the day — yet recordings before LA's
1391
+ // switch sat an hour apart and were misaligned on the fused timeline.
1392
+ // Exact tz-id identity now refuses this pair up front.
1393
+ storeDay(
1394
+ storage,
1395
+ "limitless",
1396
+ "2026-03-08",
1397
+ ["Morning standup before the DST switch."],
1398
+ "America/Los_Angeles",
1399
+ );
1400
+ storeDay(
1401
+ storage,
1402
+ "bee",
1403
+ "2026-03-08",
1404
+ ["Morning standup before the DST switch."],
1405
+ "America/Phoenix",
1406
+ );
1407
+ const service = makeService(storage, {
1408
+ sources: {
1409
+ limitless: { ...defaultWearableSourceSettings(), enabled: true },
1410
+ bee: { ...defaultWearableSourceSettings(), enabled: true },
1411
+ },
1412
+ fusion: { enabled: true, proximityGapMs: 300_000, windowToleranceMs: 30_000 },
1413
+ });
1414
+
1415
+ const result = await service.fuseDay("2026-03-08");
1416
+ assert.equal(result.written, false, "DST-date coincident-offset pair skips");
1417
+ assert.equal(result.skipped?.reason, "conflicting-timezones");
1418
+ assert.equal(result.sources.length, 0);
1419
+ assert.equal(await storage.fusionArtifactStore().readFusedDay("2026-03-08"), null);
1420
+ assert.equal((await service.fusedConversations("2026-03-08")).length, 0);
1421
+ } finally {
1422
+ rmSync(storage.dir, { recursive: true, force: true });
1423
+ }
1424
+ });
1425
+
1426
+ test("fuseDay skips when a source lacks a resolvable timezone id", async () => {
1427
+ const storage = makeStorage(mkdtempSync(path.join(tmpdir(), "remnic-fusion-")));
1428
+ try {
1429
+ // limitless carries an explicit tz id; bee's transcript is missing the
1430
+ // timezone field entirely. The guard must NOT coerce the missing id to
1431
+ // a default and silently match — any unresolvable tz fails safe.
1432
+ storeDay(
1433
+ storage,
1434
+ "limitless",
1435
+ "2026-06-10",
1436
+ ["We agreed to ship Friday."],
1437
+ "America/Los_Angeles",
1438
+ );
1439
+ // Build bee's transcript the normal way, then strip the timezone line
1440
+ // so the persisted frontmatter has no resolvable tz id.
1441
+ const registry = emptySpeakerRegistry();
1442
+ const conversations: WearableConversation[] = [
1443
+ {
1444
+ id: "bee-2026-06-10",
1445
+ source: "bee",
1446
+ title: "Stored conversation",
1447
+ startIso: "2026-06-10T10:00:00.000Z",
1448
+ endIso: "2026-06-10T10:30:00.000Z",
1449
+ segments: [{ speakerKey: "user", isWearer: true, text: "We agreed to ship Friday." }],
1450
+ },
1451
+ ];
1452
+ const beeBody = composeDayTranscriptBody(
1453
+ "bee",
1454
+ "2026-06-10",
1455
+ "UTC",
1456
+ conversations,
1457
+ registry,
1458
+ );
1459
+ const beeMeta = composeDayTranscriptMeta(
1460
+ "bee",
1461
+ "2026-06-10",
1462
+ "UTC",
1463
+ conversations,
1464
+ registry,
1465
+ beeBody,
1466
+ "2026-06-11T01:00:00.000Z",
1467
+ );
1468
+ const beeRaw = serializeDayTranscript(beeMeta, beeBody).replace(/^timezone: .*\n/m, "");
1469
+ storage.files.set("bee/2026-06-10", beeRaw);
1470
+
1471
+ const service = makeService(storage, {
1472
+ sources: {
1473
+ limitless: { ...defaultWearableSourceSettings(), enabled: true },
1474
+ bee: { ...defaultWearableSourceSettings(), enabled: true },
1475
+ },
1476
+ fusion: { enabled: true, proximityGapMs: 300_000, windowToleranceMs: 30_000 },
1477
+ });
1478
+
1479
+ const result = await service.fuseDay("2026-06-10");
1480
+ assert.equal(result.written, false, "a missing tz id fails safe");
1481
+ assert.equal(result.skipped?.reason, "conflicting-timezones");
1482
+ assert.equal(result.sources.length, 0);
1483
+ assert.equal(await storage.fusionArtifactStore().readFusedDay("2026-06-10"), null);
1484
+ assert.equal((await service.fusedConversations("2026-06-10")).length, 0);
1485
+ } finally {
1486
+ rmSync(storage.dir, { recursive: true, force: true });
1487
+ }
1488
+ });
1489
+
1490
+ test("fuseDay ignores a zero-conversation source when checking timezone identity (issue #1849)", async () => {
1491
+ const storage = makeStorage(mkdtempSync(path.join(tmpdir(), "remnic-fusion-")));
1492
+ try {
1493
+ // limitless CONTRIBUTES a real conversation under one timezone.
1494
+ storeDay(
1495
+ storage,
1496
+ "limitless",
1497
+ "2026-06-10",
1498
+ ["We agreed to ship Friday."],
1499
+ "America/Los_Angeles",
1500
+ );
1501
+ // bee is an EMPTY (all-elided / zero-conversation) transcript — the
1502
+ // sync path writes one explicitly for days whose segments were all
1503
+ // dropped. It carries a DIFFERENT timezone, but contributes NO
1504
+ // conversations/clocks, so it must not block the fusion.
1505
+ const registry = emptySpeakerRegistry();
1506
+ const beeBody = composeDayTranscriptBody(
1507
+ "bee",
1508
+ "2026-06-10",
1509
+ "Asia/Tokyo",
1510
+ [],
1511
+ registry,
1512
+ );
1513
+ const beeMeta = composeDayTranscriptMeta(
1514
+ "bee",
1515
+ "2026-06-10",
1516
+ "Asia/Tokyo",
1517
+ [],
1518
+ registry,
1519
+ beeBody,
1520
+ "2026-06-11T01:00:00.000Z",
1521
+ );
1522
+ storage.files.set("bee/2026-06-10", serializeDayTranscript(beeMeta, beeBody));
1523
+
1524
+ const service = makeService(storage, {
1525
+ sources: {
1526
+ limitless: { ...defaultWearableSourceSettings(), enabled: true },
1527
+ bee: { ...defaultWearableSourceSettings(), enabled: true },
1528
+ },
1529
+ fusion: { enabled: true, proximityGapMs: 300_000, windowToleranceMs: 30_000 },
1530
+ });
1531
+
1532
+ const result = await service.fuseDay("2026-06-10");
1533
+ // The zero-conversation source is ignored; the single contributing
1534
+ // source fuses normally (NOT skipped).
1535
+ assert.equal(result.written, true, "a zero-conversation source must not block fusion");
1536
+ assert.equal(result.skipped, undefined);
1537
+ assert.ok(result.conversationCount >= 1, "the contributing source still fuses");
1538
+ assert.deepEqual([...result.sources].sort(), ["limitless"]);
1539
+ } finally {
1540
+ rmSync(storage.dir, { recursive: true, force: true });
1541
+ }
1542
+ });
1543
+
1544
+ test("fuseDay still skips when two CONTRIBUTING sources disagree on timezone (issue #1849)", async () => {
1545
+ const storage = makeStorage(mkdtempSync(path.join(tmpdir(), "remnic-fusion-")));
1546
+ try {
1547
+ // Both sources CONTRIBUTE a real conversation but under DIFFERENT
1548
+ // timezones — the guard must still skip. This is the contrasting case
1549
+ // to ignoring zero-conversation sources: only sources that actually
1550
+ // contribute clocks are compared, and two disagreeing contributors skip.
1551
+ storeDay(
1552
+ storage,
1553
+ "limitless",
1554
+ "2026-06-10",
1555
+ ["We agreed to ship Friday."],
1556
+ "America/Los_Angeles",
1557
+ );
1558
+ storeDay(
1559
+ storage,
1560
+ "bee",
1561
+ "2026-06-10",
1562
+ ["We agreed to ship Friday."],
1563
+ "Asia/Tokyo",
1564
+ );
1565
+ const service = makeService(storage, {
1566
+ sources: {
1567
+ limitless: { ...defaultWearableSourceSettings(), enabled: true },
1568
+ bee: { ...defaultWearableSourceSettings(), enabled: true },
1569
+ },
1570
+ fusion: { enabled: true, proximityGapMs: 300_000, windowToleranceMs: 30_000 },
1571
+ });
1572
+
1573
+ const result = await service.fuseDay("2026-06-10");
1574
+ assert.equal(result.written, false, "two disagreeing contributing sources must skip");
1575
+ assert.equal(result.skipped?.reason, "conflicting-timezones");
1576
+ assert.equal(result.sources.length, 0);
1577
+ assert.equal(await storage.fusionArtifactStore().readFusedDay("2026-06-10"), null);
1578
+ assert.equal((await service.fusedConversations("2026-06-10")).length, 0);
1579
+ } finally {
1580
+ rmSync(storage.dir, { recursive: true, force: true });
1581
+ }
1582
+ });
1583
+
1584
+
1585
+ test("dayTranscript returns decoded segment text, not the escaped storage form (#1849)", async () => {
1586
+ const dir = mkdtempSync(path.join(tmpdir(), "remnic-service-"));
1587
+ try {
1588
+ const storage = makeStorage(dir);
1589
+ // Segment text carries a real newline + a literal backslash. The stored
1590
+ // file escapes both; the user-facing view must decode them back.
1591
+ const original = "Line one.\nLine two with a backslash \\ path.";
1592
+ storeDay(storage, "limitless", "2026-06-10", [original]);
1593
+ const service = makeService(storage);
1594
+ const views = await service.dayTranscript("2026-06-10");
1595
+ assert.equal(views.length, 1);
1596
+ assert.ok(views[0].body.includes(original), "view shows the original (decoded) text");
1597
+ assert.ok(
1598
+ views[0].body.includes("Line one.\\nLine two") === false,
1599
+ "no escaped-newline leak in the user-facing view",
1600
+ );
1601
+ } finally {
1602
+ rmSync(dir, { recursive: true, force: true });
1603
+ }
1604
+ });
1605
+
1606
+ test("searchTranscripts scan decodes segment text so snippets show the original (#1849)", async () => {
1607
+ const dir = mkdtempSync(path.join(tmpdir(), "remnic-service-"));
1608
+ try {
1609
+ const storage = makeStorage(dir);
1610
+ // Segment text spans a newline; the escaped storage form would leak a
1611
+ // literal backslash-n into the snippet unless the scan decodes first.
1612
+ storeDay(storage, "limitless", "2026-06-10", ["Alpha part.\nBeta GAPWORD here."]);
1613
+ const service = makeService(storage);
1614
+ const results = await service.searchTranscripts("GAPWORD");
1615
+ assert.equal(results.length, 1);
1616
+ assert.equal(results[0].backend, "scan");
1617
+ assert.ok(results[0].snippet.includes("GAPWORD"));
1618
+ assert.ok(
1619
+ results[0].snippet.includes("Alpha part.\\nBeta") === false,
1620
+ "snippet has no escaped-newline leak",
1621
+ );
1622
+ } finally {
1623
+ rmSync(dir, { recursive: true, force: true });
1624
+ }
1625
+ });