@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
@@ -16,6 +16,7 @@ import type {
16
16
  WearableCleanupSettings,
17
17
  WearableCorrectionRule,
18
18
  WearableMemoryMode,
19
+ WearableFusionConfig,
19
20
  WearableSourceSettings,
20
21
  WearablesConfig,
21
22
  } from "./types.js";
@@ -47,6 +48,11 @@ const MAX_AUTO_SYNC_WINDOW_DAYS = 90;
47
48
  const DEFAULT_SOURCE_TRUST = 0.8;
48
49
  const DEFAULT_AUTO_APPROVE_TRUST = 0.7;
49
50
  const DEFAULT_REVIEW_TRUST = 0.45;
51
+ // Cross-source fusion (#1810) — disabled by default; enabling is the
52
+ // only behavior change fusion introduces.
53
+ const DEFAULT_FUSION_ENABLED = false;
54
+ const DEFAULT_FUSION_PROXIMITY_GAP_MS = 300_000;
55
+ const DEFAULT_FUSION_WINDOW_TOLERANCE_MS = 30_000;
50
56
 
51
57
  // Sources whose provider emits per-utterance text but unreliable
52
58
  // speaker labels (Bee falls back to a generic "unknown" label when
@@ -91,6 +97,14 @@ export function defaultWearableSourceSettings(
91
97
  };
92
98
  }
93
99
 
100
+ export function defaultWearableFusionConfig(): WearableFusionConfig {
101
+ return {
102
+ enabled: DEFAULT_FUSION_ENABLED,
103
+ proximityGapMs: DEFAULT_FUSION_PROXIMITY_GAP_MS,
104
+ windowToleranceMs: DEFAULT_FUSION_WINDOW_TOLERANCE_MS,
105
+ };
106
+ }
107
+
94
108
  export function defaultWearablesConfig(): WearablesConfig {
95
109
  return {
96
110
  enabled: false,
@@ -104,6 +118,7 @@ export function defaultWearablesConfig(): WearablesConfig {
104
118
  autoSyncDeepDays: DEFAULT_AUTO_SYNC_DEEP_DAYS,
105
119
  corrections: [],
106
120
  sources: {},
121
+ fusion: defaultWearableFusionConfig(),
107
122
  };
108
123
  }
109
124
 
@@ -331,6 +346,42 @@ function parseSourceSettings(
331
346
  * Parse the `wearables` config block. `undefined` yields the disabled
332
347
  * default config; any present-but-malformed value throws.
333
348
  */
349
+ function parseFusionSettings(
350
+ value: unknown,
351
+ keyPath: string,
352
+ ): WearableFusionConfig {
353
+ if (value === undefined) return defaultWearableFusionConfig();
354
+ const raw = requireObject(value, keyPath);
355
+ const defaults = defaultWearableFusionConfig();
356
+ const parsePositiveInt = (
357
+ value: unknown,
358
+ name: string,
359
+ fallback: number,
360
+ ): number => {
361
+ if (value === undefined) return fallback;
362
+ const coerced = coerceNumber(value);
363
+ if (coerced === undefined || !Number.isInteger(coerced) || coerced <= 0) {
364
+ throw new Error(
365
+ `${keyPath}.${name} must be a positive integer (got ${JSON.stringify(value)})`,
366
+ );
367
+ }
368
+ return coerced;
369
+ };
370
+ return {
371
+ enabled: parseBool(raw.enabled, `${keyPath}.enabled`, defaults.enabled),
372
+ proximityGapMs: parsePositiveInt(
373
+ raw.proximityGapMs,
374
+ "proximityGapMs",
375
+ defaults.proximityGapMs,
376
+ ),
377
+ windowToleranceMs: parsePositiveInt(
378
+ raw.windowToleranceMs,
379
+ "windowToleranceMs",
380
+ defaults.windowToleranceMs,
381
+ ),
382
+ };
383
+ }
384
+
334
385
  export function parseWearablesConfig(value: unknown): WearablesConfig {
335
386
  if (value === undefined) return defaultWearablesConfig();
336
387
  const raw = requireObject(value, "wearables");
@@ -455,6 +506,7 @@ export function parseWearablesConfig(value: unknown): WearablesConfig {
455
506
  autoSyncIntervalMinutes,
456
507
  autoSyncDays,
457
508
  autoSyncDeepDays,
509
+ fusion: parseFusionSettings(raw.fusion, "wearables.fusion"),
458
510
  corrections: parseCorrectionRules(raw.corrections, "wearables.corrections"),
459
511
  sources,
460
512
  };
@@ -2,12 +2,16 @@ import assert from "node:assert/strict";
2
2
  import { test } from "node:test";
3
3
 
4
4
  import {
5
+ bodyIsEscaped,
5
6
  composeDayTranscriptBody,
6
7
  composeDayTranscriptMeta,
8
+ decodeTranscriptBody,
7
9
  hashTranscriptBody,
8
10
  isValidTranscriptDate,
9
11
  parseDayTranscript,
12
+ parseTranscriptSegmentLine,
10
13
  serializeDayTranscript,
14
+ TRANSCRIPT_FORMAT_VERSION,
11
15
  } from "./day-store.js";
12
16
  import { emptySpeakerRegistry } from "./speakers.js";
13
17
  import type { WearableConversation } from "./types.js";
@@ -141,3 +145,245 @@ test("invalid timezone falls back to UTC clock rendering instead of crashing", (
141
145
  );
142
146
  assert.match(body, /## \d{2}:\d{2}–\d{2}:\d{2} · Morning coffee/);
143
147
  });
148
+
149
+
150
+ test("decodeTranscriptBody decodes segment text but leaves headings/locations untouched (#1849)", () => {
151
+ // Hand-written stored body: segment text carries the escaped forms
152
+ // (backslash-n for a newline) exactly as composeDayTranscriptBody
153
+ // serializes them, while the title/location are NOT escaped on write.
154
+ const body =
155
+ "# bee transcript — 2026-06-10\n" +
156
+ "\n" +
157
+ "## 09:00–09:10 · C:\\roadmap (conversation c1)\n" +
158
+ "\n" +
159
+ "*Location: D:\\data*\n" +
160
+ "\n" +
161
+ '**Me (you)** [09:00]: Line one.\\nLine two.\n';
162
+ const decoded = decodeTranscriptBody(body, true);
163
+ // Segment text IS decoded: backslash-n -> real newline.
164
+ assert.ok(decoded.includes("Line one.\nLine two."), "segment newline decoded");
165
+ assert.ok(!decoded.includes("Line one.\\nLine two."), "no escaped-newline leak in segment");
166
+ // Title and location are NOT escaped on write, so a literal backslash
167
+ // there must pass through verbatim (only segment text is decoded).
168
+ assert.ok(decoded.includes("· C:\\roadmap (conversation c1)"), "title backslash untouched");
169
+ assert.ok(decoded.includes("*Location: D:\\data*"), "location backslash untouched");
170
+ });
171
+
172
+ test("decodeTranscriptBody recovers original segment text from a composed body (#1849)", () => {
173
+ const original = "First line.\nSecond line with a backslash \\ here.";
174
+ const conversations: WearableConversation[] = [
175
+ {
176
+ id: "c1",
177
+ source: "bee",
178
+ startIso: "2026-06-10T09:00:00Z",
179
+ segments: [{ speakerKey: "user", isWearer: true, text: original }],
180
+ },
181
+ ];
182
+ const body = composeDayTranscriptBody("bee", "2026-06-10", "UTC", conversations, REGISTRY);
183
+ // The stored body carries the ESCAPED form, not the original...
184
+ assert.ok(!body.includes(original), "stored body is escaped, not the raw original");
185
+ assert.ok(body.includes("First line.\\nSecond line"), "stored body has the escaped newline");
186
+ // ...and decoding recovers the original utterance verbatim.
187
+ assert.ok(
188
+ decodeTranscriptBody(body, true).includes(original),
189
+ "decoded body recovers the original segment text",
190
+ );
191
+ });
192
+
193
+
194
+ test("speaker label with markdown delimiters round-trips through compose → parse and decodeTranscriptBody (#1849)", () => {
195
+ // A speaker label containing `**`, `[`, `]` — the exact characters that
196
+ // delimit a segment line. Without safe serialization the label breaks
197
+ // parseTranscriptSegmentLine parsing; with escape/unescape it round-trips
198
+ // losslessly through composition, parsing, and display decoding.
199
+ const registry = emptySpeakerRegistry();
200
+ registry.selfName = "Me";
201
+ registry.speakers["bee:SPEAKER_01"] = {
202
+ name: "A**B",
203
+ updatedAt: "2026-06-10T00:00:00Z",
204
+ };
205
+ const conversations: WearableConversation[] = [
206
+ {
207
+ id: "c1",
208
+ source: "bee",
209
+ startIso: "2026-06-10T09:00:00Z",
210
+ segments: [
211
+ { speakerKey: "user", isWearer: true, text: "Hi there.", startIso: "2026-06-10T09:00:00Z" },
212
+ { speakerKey: "SPEAKER_01", text: "Delimited label.", startIso: "2026-06-10T09:01:00Z" },
213
+ ],
214
+ },
215
+ ];
216
+ const body = composeDayTranscriptBody("bee", "2026-06-10", "UTC", conversations, registry);
217
+ // The raw stored body escapes the delimiters so the parser never sees
218
+ // a forged `**` or `[` boundary inside the label.
219
+ assert.ok(
220
+ body.includes("**A\\*\\*B**"),
221
+ "label delimiters are escaped in the stored body: " + body,
222
+ );
223
+ // Display decoding recovers the ORIGINAL label for view/search surfaces.
224
+ const decoded = decodeTranscriptBody(body, true);
225
+ assert.ok(
226
+ decoded.includes("**A**B** [09:01]: Delimited label."),
227
+ "decoded body shows the original unescaped label: " + decoded,
228
+ );
229
+ // parseDayTranscript preserves the body bytes verbatim.
230
+ const serialized = serializeDayTranscript(
231
+ composeDayTranscriptMeta("bee", "2026-06-10", "UTC", conversations, registry, body, "2026-06-11T01:00:00.000Z"),
232
+ body,
233
+ );
234
+ const parsed = parseDayTranscript(serialized);
235
+ assert.ok(parsed);
236
+ assert.equal(parsed!.body, body, "body survives serialization round-trip");
237
+ });
238
+
239
+ test("legacy raw label without escape sequences parses and decodes verbatim (#1849)", () => {
240
+ // A hand-written stored body whose label was NEVER escaped (a pre-
241
+ // escaper transcript). decodeTranscriptBody and the segment-line parser
242
+ // must treat it as a no-op: no backslash sequences => unchanged label.
243
+ const body =
244
+ "# bee transcript — 2026-06-10\n" +
245
+ "\n" +
246
+ "## 09:00–09:10 (conversation c1)\n" +
247
+ "\n" +
248
+ "**Me (you)** [09:00]: Hello world.\n" +
249
+ "**Guest** [09:01]: Good morning.\n";
250
+ const decoded = decodeTranscriptBody(body);
251
+ assert.equal(decoded, body, "legacy body with no escapes decodes verbatim");
252
+ });
253
+
254
+ test("legacy body with literal backslash-n/backslash-r is never decoded (#1849)", () => {
255
+ // A pre-escaper transcript whose segment text contains the LITERAL
256
+ // two-character sequences \n and \r (not escape sequences). The
257
+ // format-aware decoder MUST leave them byte-for-byte unchanged so the
258
+ // original content is never corrupted.
259
+ const legacyBody =
260
+ "# bee transcript — 2026-06-10\n" +
261
+ "\n" +
262
+ "## 09:00–09:10 (conversation c1)\n" +
263
+ "\n" +
264
+ "**Guest** [09:00]: A literal backslash-n \\n and backslash-r \\r here.\n";
265
+ // Default (escaped=false): legacy body is a no-op.
266
+ assert.equal(
267
+ decodeTranscriptBody(legacyBody),
268
+ legacyBody,
269
+ "legacy body must not be decoded without an explicit format marker",
270
+ );
271
+ // Even when explicitly told the body IS escaped, the literal sequences
272
+ // \n / \r WOULD be decoded — but only NEW-format bodies (meta carries
273
+ // formatVersion >= 2) are ever passed escaped=true by the service.
274
+ // Here we verify the SAFETY of the default: callers that forget the
275
+ // flag never corrupt legacy content.
276
+ assert.equal(
277
+ decodeTranscriptBody(legacyBody, false),
278
+ legacyBody,
279
+ "escaped=false leaves legacy literal escapes unchanged",
280
+ );
281
+ });
282
+
283
+ test("new-format body with formatVersion marker decodes correctly (#1849)", () => {
284
+ const conversations: WearableConversation[] = [
285
+ {
286
+ id: "c1",
287
+ source: "bee",
288
+ startIso: "2026-06-10T09:00:00Z",
289
+ segments: [{ speakerKey: "user", isWearer: true, text: "Line one.\nLine two." }],
290
+ },
291
+ ];
292
+ const body = composeDayTranscriptBody("bee", "2026-06-10", "UTC", conversations, REGISTRY);
293
+ const meta = composeDayTranscriptMeta(
294
+ "bee", "2026-06-10", "UTC", conversations, REGISTRY, body, "2026-06-11T01:00:00.000Z",
295
+ );
296
+ // The meta carries the format version marker.
297
+ assert.equal(meta.formatVersion, TRANSCRIPT_FORMAT_VERSION);
298
+ assert.ok(bodyIsEscaped(meta), "new-format meta is recognized as escaped");
299
+ // bodyIsEscaped returns false for legacy (no marker) and null.
300
+ assert.equal(bodyIsEscaped(null), false);
301
+ assert.equal(bodyIsEscaped(undefined), false);
302
+ assert.equal(bodyIsEscaped({}), false);
303
+ assert.equal(bodyIsEscaped({ formatVersion: 1 }), false);
304
+ // Decoding with the escaped flag recovers the original newline.
305
+ const decoded = decodeTranscriptBody(body, bodyIsEscaped(meta));
306
+ assert.ok(decoded.includes("Line one.\nLine two."), "new-format body decodes the newline");
307
+ });
308
+
309
+ test("composeDayTranscriptMeta stamps formatVersion and serialize/parse round-trips it (#1849)", () => {
310
+ const body = composeDayTranscriptBody(
311
+ "limitless", "2026-06-10", "UTC", CONVERSATIONS, REGISTRY,
312
+ );
313
+ const meta = composeDayTranscriptMeta(
314
+ "limitless", "2026-06-10", "UTC", CONVERSATIONS, REGISTRY, body, "2026-06-11T01:00:00.000Z",
315
+ );
316
+ assert.equal(meta.formatVersion, TRANSCRIPT_FORMAT_VERSION);
317
+ const serialized = serializeDayTranscript(meta, body);
318
+ assert.ok(serialized.includes("formatVersion: 2"), "serialized file carries the marker");
319
+ const parsed = parseDayTranscript(serialized);
320
+ assert.ok(parsed);
321
+ assert.equal(parsed!.meta.formatVersion, TRANSCRIPT_FORMAT_VERSION);
322
+ // A legacy file (hand-written, no formatVersion line) parses with
323
+ // formatVersion undefined so bodyIsEscaped returns false.
324
+ const legacyFile =
325
+ "---\nkind: wearable-transcript\nsource: \"bee\"\ndate: \"2026-06-10\"\n" +
326
+ "timezone: \"UTC\"\nconversationCount: 1\nsegmentCount: 1\n" +
327
+ "speakers: []\ndurationMinutes: 0\ncontentHash: \"x\"\nsyncedAt: \"y\"\n" +
328
+ "---\n\n# bee transcript — 2026-06-10\n";
329
+ const legacyParsed = parseDayTranscript(legacyFile);
330
+ assert.ok(legacyParsed);
331
+ assert.equal(legacyParsed!.meta.formatVersion, undefined);
332
+ assert.equal(bodyIsEscaped(legacyParsed!.meta), false);
333
+ });
334
+
335
+ test("parseTranscriptSegmentLine extracts label, clock, and text (#1849)", () => {
336
+ const m = parseTranscriptSegmentLine("**Pat** [09:30]: Hello world.");
337
+ assert.deepEqual(m, { label: "Pat", clock: "09:30", text: "Hello world." });
338
+ });
339
+
340
+ test("parseTranscriptSegmentLine handles single-char label and empty text", () => {
341
+ const m = parseTranscriptSegmentLine("**X** [00:00]: ");
342
+ assert.deepEqual(m, { label: "X", clock: "00:00", text: "" });
343
+ });
344
+
345
+ test("parseTranscriptSegmentLine preserves backslash escape sequences in label/text", () => {
346
+ // Escaped labels/text from the escape-aware serializer carry literal
347
+ // backslash sequences that must survive the linear parser unchanged.
348
+ const m = parseTranscriptSegmentLine("**Pat \\*Smith\\*** [09:00]: Hello \\n world.");
349
+ assert.ok(m);
350
+ assert.equal(m!.label, "Pat \\*Smith\\*");
351
+ assert.equal(m!.clock, "09:00");
352
+ assert.equal(m!.text, "Hello \\n world.");
353
+ });
354
+
355
+ test("parseTranscriptSegmentLine returns null for non-segment lines", () => {
356
+ assert.equal(parseTranscriptSegmentLine(""), null);
357
+ assert.equal(parseTranscriptSegmentLine("not a segment"), null);
358
+ assert.equal(parseTranscriptSegmentLine("**no close"), null);
359
+ assert.equal(parseTranscriptSegmentLine("**label** no bracket"), null);
360
+ assert.equal(parseTranscriptSegmentLine("**label** [clock] no colon-space"), null);
361
+ assert.equal(parseTranscriptSegmentLine("**label** [clock]:no space"), null);
362
+ assert.equal(parseTranscriptSegmentLine("**label** []: text"), null);
363
+ });
364
+
365
+ test("parseTranscriptSegmentLine is linear-safe on adversarial repeated delimiters (#1849)", () => {
366
+ // A line with many '*' chars must not cause polynomial backtracking.
367
+ // The linear scan finds the first '**' followed by \s[ — or returns
368
+ // null — without exploring exponential alternatives.
369
+ const longLabel = "a".repeat(10_000);
370
+ const adversarial = `**${longLabel}** [09:00]: text`;
371
+ const m = parseTranscriptSegmentLine(adversarial);
372
+ assert.ok(m, "long label with valid delimiters must parse");
373
+ assert.equal(m!.label, longLabel);
374
+ assert.equal(m!.clock, "09:00");
375
+ assert.equal(m!.text, "text");
376
+
377
+ // All stars, no closing \s[ — must return null quickly.
378
+ const noClose = "**" + "*".repeat(10_000);
379
+ assert.equal(parseTranscriptSegmentLine(noClose), null);
380
+
381
+ // All stars followed by valid suffix — the first '**' positions are
382
+ // all stars (not followed by \s), so the scan walks the entire run
383
+ // in O(n) and finds the '** ' before '['.
384
+ const allStars = "**" + "*".repeat(10_000) + "** [09:00]: text";
385
+ const m2 = parseTranscriptSegmentLine(allStars);
386
+ assert.ok(m2, "star-prefixed line with valid delimiters must parse");
387
+ assert.equal(m2!.clock, "09:00");
388
+ assert.equal(m2!.text, "text");
389
+ });
@@ -34,6 +34,127 @@ export const WEARABLES_DIR_NAME = "wearables";
34
34
 
35
35
  const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
36
36
 
37
+ /**
38
+ * Day-transcript body serialization format version. Folded into
39
+ * `hashTranscriptBody` so a version bump invalidates files written by an
40
+ * older serializer and forces an idempotent rewrite. Decoders gate on this:
41
+ * only bodies whose parsed meta carries >= this version have escape
42
+ * sequences decoded; legacy bodies (absent / older) are left byte-for-byte
43
+ * unchanged so a literal two-character `\n`/`\r` in a pre-escaper
44
+ * transcript is never altered (issue #1849).
45
+ */
46
+ export const TRANSCRIPT_FORMAT_VERSION = 2;
47
+
48
+ /**
49
+ * Result of parsing a rendered transcript segment line.
50
+ */
51
+ export interface TranscriptSegmentMatch {
52
+ /** Speaker label text between the `**` delimiters. */
53
+ label: string;
54
+ /** Clock text between the `[` `]` delimiters. */
55
+ clock: string;
56
+ /** Segment text after the colon. */
57
+ text: string;
58
+ }
59
+
60
+ /**
61
+ * Test a single character code point against the JavaScript regex `\s`
62
+ * whitespace class without invoking a regex. Used by the linear segment
63
+ * line parser so no quantified pattern is ever evaluated on untrusted
64
+ * transcript content (CodeQL polynomial-redos, issue #1849).
65
+ */
66
+ function isRegexWhitespace(code: number): boolean {
67
+ return (
68
+ (code >= 0x09 && code <= 0x0d) ||
69
+ code === 0x20 ||
70
+ code === 0xa0 ||
71
+ code === 0x1680 ||
72
+ (code >= 0x2000 && code <= 0x200a) ||
73
+ code === 0x2028 ||
74
+ code === 0x2029 ||
75
+ code === 0x202f ||
76
+ code === 0x205f ||
77
+ code === 0x3000 ||
78
+ code === 0xfeff
79
+ );
80
+ }
81
+
82
+ /**
83
+ * Parse a rendered transcript segment line (`**label** [clock]: text`)
84
+ * into its three components using a linear, bounded scan — a CodeQL-safe
85
+ * replacement for the polynomial-risk regex that previously matched this
86
+ * format. Returns null when the line is not a segment line.
87
+ *
88
+ * The scan finds the FIRST occurrence of each structural delimiter
89
+ * (`**` after the label, `]` after the clock) exactly as the original
90
+ * non-greedy regex did, and verifies the fixed separator characters
91
+ * (`\s`, `[`, `:`, `\s`) by single-character code-point comparison.
92
+ * No quantified regex is evaluated on the line (#1849).
93
+ *
94
+ * Escaped labels (formatVersion >= 2) never contain an unescaped `**`,
95
+ * `[`, or `]`, and rendered clocks are always `HH:MM` / `--:--`, so the
96
+ * first delimiter is always the correct one for every well-formed
97
+ * transcript. Legacy unescaped labels are used verbatim by the caller
98
+ * and parsed identically here.
99
+ */
100
+ export function parseTranscriptSegmentLine(
101
+ line: string,
102
+ ): TranscriptSegmentMatch | null {
103
+ // Must start with '**'.
104
+ if (!line.startsWith("**")) return null;
105
+
106
+ // Find the closing '**' of the label: the first '**' at index >= 3
107
+ // (label is at least 1 char) that is immediately followed by \s and
108
+ // then '['. This mirrors the non-greedy regex: the engine tries the
109
+ // shortest label first and extends only when the subsequent fixed
110
+ // delimiters do not line up. An escaped label ending with '\*' is
111
+ // correctly handled because the spurious '**' (escape-star + close-
112
+ // star) is not followed by \s and the scan continues.
113
+ let labelEnd = -1;
114
+ for (let i = 3; i < line.length; i++) {
115
+ if (
116
+ line.charCodeAt(i) !== 0x2a /* '*' */ ||
117
+ i + 3 >= line.length ||
118
+ line.charCodeAt(i + 1) !== 0x2a /* '*' */ ||
119
+ !isRegexWhitespace(line.charCodeAt(i + 2)) ||
120
+ line.charCodeAt(i + 3) !== 0x5b /* '[' */
121
+ ) {
122
+ continue;
123
+ }
124
+ labelEnd = i;
125
+ break;
126
+ }
127
+ if (labelEnd === -1) return null;
128
+
129
+ const label = line.slice(2, labelEnd);
130
+ // Clock content starts after the closing '**', the \s, and the '['.
131
+ const clockStart = labelEnd + 4;
132
+
133
+ // Find the closing ']' of the clock: the first ']' at index >=
134
+ // clockStart + 1 (clock is at least 1 char) that is immediately
135
+ // followed by ':' and \s — matching the non-greedy regex.
136
+ let clockEnd = -1;
137
+ for (let i = clockStart + 1; i < line.length; i++) {
138
+ if (
139
+ line.charCodeAt(i) !== 0x5d /* ']' */ ||
140
+ i + 2 >= line.length ||
141
+ line.charCodeAt(i + 1) !== 0x3a /* ':' */ ||
142
+ !isRegexWhitespace(line.charCodeAt(i + 2))
143
+ ) {
144
+ continue;
145
+ }
146
+ clockEnd = i;
147
+ break;
148
+ }
149
+ if (clockEnd === -1) return null;
150
+
151
+ return {
152
+ label,
153
+ clock: line.slice(clockStart, clockEnd),
154
+ text: line.slice(clockEnd + 3),
155
+ };
156
+ }
157
+
37
158
  export function isValidTranscriptDate(date: string): boolean {
38
159
  if (!DATE_PATTERN.test(date)) return false;
39
160
  const parsed = new Date(`${date}T00:00:00Z`);
@@ -41,7 +162,14 @@ export function isValidTranscriptDate(date: string): boolean {
41
162
  }
42
163
 
43
164
  export function hashTranscriptBody(body: string): string {
44
- return createHash("sha256").update(body, "utf-8").digest("hex");
165
+ // The version prefix makes the hash an idempotency key for the body AS
166
+ // SERIALIZED under the current format: when the escape encoding changes
167
+ // (version bump) every existing file's stored hash no longer matches and
168
+ // the pipeline rewrites it with the new marker — no separate migration
169
+ // pass needed (issue #1849).
170
+ return createHash("sha256")
171
+ .update(`v${TRANSCRIPT_FORMAT_VERSION}\n${body}`, "utf-8")
172
+ .digest("hex");
45
173
  }
46
174
 
47
175
  function formatClockTime(iso: string | undefined, timezone: string): string {
@@ -69,6 +197,135 @@ function conversationDurationMinutes(conversation: WearableConversation): number
69
197
  return (end - start) / 60_000;
70
198
  }
71
199
 
200
+ /**
201
+ * Escape segment text for the line-based markdown format so that embedded
202
+ * newlines, carriage returns, and backslashes survive the serialize →
203
+ * reconstruct round-trip losslessly. Reversed by `unescapeSegmentText`
204
+ * (this module — the single decode primitive shared with the fusion
205
+ * reconstruct path and every view/search/index surface).
206
+ *
207
+ * Without this, a segment whose text contains a newline is split across
208
+ * multiple physical lines; the reconstruct path treats each line
209
+ * independently and the continuation is silently dropped (or worse, a
210
+ * continuation line that looks like a heading/clock is mis-parsed).
211
+ */
212
+ export function escapeSegmentText(text: string): string {
213
+ return text
214
+ .replace(/\\/g, "\\\\")
215
+ .replace(/\n/g, "\\n")
216
+ .replace(/\r/g, "\\r");
217
+ }
218
+
219
+ /**
220
+ * Reverse `escapeSegmentText`. Unknown escape sequences (a lone
221
+ * backslash followed by a character the escaper never emits) are passed
222
+ * through literally so legacy transcripts that never went through the
223
+ * escaper still round-trip their original text. This is the SINGLE
224
+ * decode primitive: the fusion reconstruct path and every user-facing
225
+ * view/search/index surface call it so escaped storage never leaks to
226
+ * display (#1849).
227
+ */
228
+ export function unescapeSegmentText(text: string): string {
229
+ return text.replace(/\\(.)/g, (_match, ch: string) => {
230
+ if (ch === "n") return "\n";
231
+ if (ch === "r") return "\r";
232
+ if (ch === "\\") return "\\";
233
+ return "\\" + ch;
234
+ });
235
+ }
236
+
237
+ /**
238
+ * Escape a speaker label for the line-based markdown format so that
239
+ * markdown-delimiter characters in an arbitrary user/provider label
240
+ * (`**`, `[`, `]`), the escape character (`\`), and embedded newlines/
241
+ * carriage returns cannot break `parseTranscriptSegmentLine` parsing.
242
+ * Without this, a label containing `**` (or `** [clock]:`) can make the
243
+ * non-greedy delimiter match land on the wrong `**` and mis-parse the
244
+ * clock/text — or fail to parse the line at all. Reversed by
245
+ * `unescapeSpeakerLabel` (this module). Legacy transcripts whose labels
246
+ * were never escaped still parse: `unescapeSpeakerLabel` is a no-op on
247
+ * any label without a backslash escape sequence (#1849).
248
+ */
249
+ export function escapeSpeakerLabel(label: string): string {
250
+ return label
251
+ .replace(/\\/g, "\\\\")
252
+ .replace(/\*/g, "\\*")
253
+ .replace(/\[/g, "\\[")
254
+ .replace(/\]/g, "\\]")
255
+ .replace(/\n/g, "\\n")
256
+ .replace(/\r/g, "\\r");
257
+ }
258
+
259
+ /**
260
+ * Reverse `escapeSpeakerLabel`. Unknown escape sequences (a backslash
261
+ * followed by a character the escaper never emits) are passed through
262
+ * literally so legacy transcripts with raw labels still round-trip
263
+ * their original text, mirroring `unescapeSegmentText` for segment text.
264
+ */
265
+ export function unescapeSpeakerLabel(label: string): string {
266
+ return label.replace(/\\(.)/g, (_match, ch: string) => {
267
+ if (ch === "\\") return "\\";
268
+ if (ch === "*") return "*";
269
+ if (ch === "[") return "[";
270
+ if (ch === "]") return "]";
271
+ if (ch === "n") return "\n";
272
+ if (ch === "r") return "\r";
273
+ return "\\" + ch;
274
+ });
275
+ }
276
+
277
+ /**
278
+ * Whether a parsed day-transcript's body was written by the escape-aware
279
+ * serializer (meta carries `formatVersion` >= `TRANSCRIPT_FORMAT_VERSION`).
280
+ * Legacy bodies (no marker, or an older version) must NOT be decoded: their
281
+ * literal two-character `\n`, `\r`, and lone backslashes are original
282
+ * content, not escape sequences (issue #1849).
283
+ */
284
+ export function bodyIsEscaped(
285
+ meta: { formatVersion?: number } | null | undefined,
286
+ ): boolean {
287
+ return (
288
+ meta !== null &&
289
+ meta !== undefined &&
290
+ typeof meta.formatVersion === "number" &&
291
+ meta.formatVersion >= TRANSCRIPT_FORMAT_VERSION
292
+ );
293
+ }
294
+
295
+ /**
296
+ * Decode the escaped segment text AND speaker label of a stored
297
+ * transcript body into the ORIGINAL forms for user-facing
298
+ * view/search/index surfaces. Only segment lines are touched: the text
299
+ * is decoded via `unescapeSegmentText` and the label via
300
+ * `unescapeSpeakerLabel`; headings, locations, and the H1 header are
301
+ * NOT escaped on write and pass through unchanged, so a title or
302
+ * location containing a literal backslash is never altered. The fusion
303
+ * reconstruct path does NOT use this — it decodes each segment once
304
+ * during parse — so callers feeding bodies back into reconstruct must
305
+ * pass the RAW stored body, not a decoded one, to avoid double-decoding.
306
+ *
307
+ * FORMAT-AWARE (issue #1849): when `escaped` is falsy (the default) the
308
+ * body is returned byte-for-byte unchanged. Only bodies written by the
309
+ * escape-aware serializer (`escaped = true`, derived from
310
+ * `bodyIsEscaped(meta)`) are decoded, so a legacy transcript's literal
311
+ * two-character `\n`/`\r` or lone backslash is never altered. Callers
312
+ * that have the parsed meta should pass `bodyIsEscaped(meta)`.
313
+ */
314
+ export function decodeTranscriptBody(body: string, escaped = false): string {
315
+ if (!escaped) return body;
316
+ const lines = body.split("\n");
317
+ for (let i = 0; i < lines.length; i++) {
318
+ const match = parseTranscriptSegmentLine(lines[i]);
319
+ if (match === null) continue;
320
+ const { label, clock, text: rawText } = match;
321
+ const decodedText = unescapeSegmentText(rawText);
322
+ const decodedLabel = unescapeSpeakerLabel(label);
323
+ if (decodedText === rawText && decodedLabel === label) continue;
324
+ lines[i] = `**${decodedLabel}** [${clock}]: ${decodedText}`;
325
+ }
326
+ return lines.join("\n");
327
+ }
328
+
72
329
  /** Compose the markdown body (no frontmatter) for one source/day. */
73
330
  export function composeDayTranscriptBody(
74
331
  sourceId: string,
@@ -101,7 +358,9 @@ export function composeDayTranscriptBody(
101
358
  for (const segment of conversation.segments) {
102
359
  const { label } = resolveSpeaker(sourceId, segment, registry);
103
360
  const at = formatClockTime(segment.startIso, timezone);
104
- lines.push(`**${label}** [${at}]: ${segment.text}`);
361
+ lines.push(
362
+ `**${escapeSpeakerLabel(label)}** [${at}]: ${escapeSegmentText(segment.text)}`,
363
+ );
105
364
  }
106
365
  lines.push("");
107
366
  }
@@ -132,6 +391,7 @@ export function composeDayTranscriptMeta(
132
391
  durationMinutes,
133
392
  contentHash: hashTranscriptBody(body),
134
393
  syncedAt,
394
+ formatVersion: TRANSCRIPT_FORMAT_VERSION,
135
395
  };
136
396
  }
137
397
 
@@ -142,6 +402,9 @@ export function serializeDayTranscript(
142
402
  ): string {
143
403
  const lines: string[] = ["---"];
144
404
  lines.push(`kind: ${meta.kind}`);
405
+ if (typeof meta.formatVersion === "number") {
406
+ lines.push(`formatVersion: ${meta.formatVersion}`);
407
+ }
145
408
  lines.push(`source: ${JSON.stringify(meta.source)}`);
146
409
  lines.push(`date: ${JSON.stringify(meta.date)}`);
147
410
  lines.push(`timezone: ${JSON.stringify(meta.timezone)}`);
@@ -204,9 +467,15 @@ export function parseDayTranscript(raw: string): WearableDayTranscript | null {
204
467
 
205
468
  const meta: WearableDayTranscriptMeta = {
206
469
  kind: "wearable-transcript",
470
+ ...(scalars.has("formatVersion")
471
+ ? { formatVersion: parseNonNegativeInt(scalars.get("formatVersion")) }
472
+ : {}),
207
473
  source,
208
474
  date,
209
- timezone: scalars.get("timezone") ?? "UTC",
475
+ // Preserve a missing timezone field as "" rather than coercing to a
476
+ // default: the fusion identity guard must see "no resolvable tz id"
477
+ // and skip, never silently match a known zone.
478
+ timezone: scalars.get("timezone") ?? "",
210
479
  conversationCount: parseNonNegativeInt(scalars.get("conversationCount")),
211
480
  segmentCount: parseNonNegativeInt(scalars.get("segmentCount")),
212
481
  speakers,