@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
@@ -13,8 +13,25 @@ import {
13
13
  } from "./corrections.js";
14
14
  import { describeErrorForOperator, WearablesInputError } from "./errors.js";
15
15
  import { inferMemoryStatus } from "../memory-lifecycle-ledger-utils.js";
16
- import { isValidTranscriptDate, parseDayTranscript } from "./day-store.js";
16
+ import {
17
+ bodyIsEscaped,
18
+ decodeTranscriptBody,
19
+ escapeSegmentText,
20
+ isValidTranscriptDate,
21
+ parseDayTranscript,
22
+ } from "./day-store.js";
23
+ import {
24
+ composeFusionDayMeta,
25
+ type FusionArtifactStore,
26
+ fuseDay as fuseDayInputs,
27
+ hashFusionBody,
28
+ parseFusionDay,
29
+ reconstructFusionInputs,
30
+ serializeFusionDay,
31
+ type FusedWearableConversation,
32
+ } from "./fusion/index.js";
17
33
  import { stripAttributesSuffix } from "../storage.js";
34
+ import { log } from "../logger.js";
18
35
  import type { MemoryFrontmatter } from "../types.js";
19
36
  import type { WearableMemoryGenDeps } from "./memory-gen.js";
20
37
  import { WEARABLE_SOURCE_PREFIX, wearableSourceLabel } from "./memory-gen.js";
@@ -59,6 +76,7 @@ export interface WearableStorageIo {
59
76
  listWearableTranscriptDays(
60
77
  sourceId?: string,
61
78
  ): Promise<Array<{ source: string; date: string }>>;
79
+ fusionArtifactStore(): FusionArtifactStore;
62
80
  readAllMemories(): Promise<
63
81
  Array<{
64
82
  path: string;
@@ -443,7 +461,13 @@ export class WearablesService {
443
461
  source,
444
462
  date,
445
463
  meta: parsed?.meta ?? null,
446
- body: parsed?.body ?? raw,
464
+ // Decode escaped segment text for display: the stored body is an
465
+ // internal line-based serialization (newlines/backslashes escaped
466
+ // so segment text survives the serialize -> reconstruct round
467
+ // trip); user-facing view surfaces must show the original text.
468
+ // The fusion reconstruct path reads raw bodies separately and
469
+ // decodes once during parse, so this never double-decodes (#1849).
470
+ body: decodeTranscriptBody(parsed?.body ?? raw, bodyIsEscaped(parsed?.meta)),
447
471
  overlapsWith: [],
448
472
  });
449
473
  }
@@ -464,6 +488,192 @@ export class WearablesService {
464
488
  return storage.listWearableTranscriptDays(sourceId);
465
489
  }
466
490
 
491
+ // -- cross-source fusion (#1810) -----------------------------------------
492
+
493
+ /**
494
+ * Fuse all enabled sources' stored transcripts for one day into a
495
+ * derived `FusedWearableConversation[]` artifact. Deterministic and
496
+ * idempotent: unchanged inputs and fusion config produce a stable
497
+ * content hash and the derived file is left untouched on re-run. Requires
498
+ * `wearables.fusion.enabled`; otherwise it throws. Raw per-source
499
+ * transcripts are never modified.
500
+ */
501
+ async fuseDay(date: string): Promise<{
502
+ date: string;
503
+ sources: string[];
504
+ conversationCount: number;
505
+ contentHash: string;
506
+ written: boolean;
507
+ skipped?: { reason: string };
508
+ }> {
509
+ this.assertEnabled();
510
+ if (!this.deps.config.fusion.enabled) {
511
+ throw new WearablesInputError(
512
+ "wearables fusion is not enabled — set `wearables.fusion.enabled: true` in the plugin config",
513
+ );
514
+ }
515
+ if (!isValidTranscriptDate(date)) {
516
+ throw new WearablesInputError(`invalid date '${date}' — expected YYYY-MM-DD`);
517
+ }
518
+ const storage = await this.deps.getStorage();
519
+ const enabled = this.enabledSources();
520
+ const bodies: Array<{ source: string; body: string; escaped?: boolean }> = [];
521
+ const sourceTimezones: string[] = [];
522
+ for (const [source] of enabled) {
523
+ const raw = await storage.readWearableDayTranscript(source, date);
524
+ if (raw === null) continue;
525
+ const parsed = parseDayTranscript(raw);
526
+ bodies.push({
527
+ source,
528
+ body: parsed?.body ?? raw,
529
+ escaped: bodyIsEscaped(parsed?.meta),
530
+ });
531
+ sourceTimezones.push(parsed?.meta.timezone ?? "");
532
+ }
533
+ // Reconstruct the normalized inputs once and derive the sources that
534
+ // actually CONTRIBUTE conversations/clocks for the day. The sync path
535
+ // can persist an explicit EMPTY (all-elided / zero-conversation)
536
+ // transcript for days whose segments were all dropped; such a file
537
+ // contributes no clocks, so it must not influence the timezone guard
538
+ // below.
539
+ const reconstructed = reconstructFusionInputs(date, bodies);
540
+ const contributing = bodies
541
+ .map((entry, i) => ({
542
+ source: entry.source,
543
+ timezone: sourceTimezones[i] ?? "",
544
+ }))
545
+ .filter((entry) =>
546
+ reconstructed.some((input) => input.source === entry.source),
547
+ );
548
+ // Mixed-timezone guard: reconstructFusionInputs rebuilds every clock as
549
+ // `${date}T${HH}:${MM}:00Z` and compares local HH:MM clocks across sources
550
+ // as if they shared one timezone. That comparison is ONLY valid when every
551
+ // CONTRIBUTING source (one that reconstructs to >=1 conversation) carries
552
+ // the SAME explicit IANA timezone id (meta.timezone). Sources that
553
+ // reconstruct to zero conversations contribute no clocks and are ignored,
554
+ // so an empty/all-elided transcript rendered under a different (or
555
+ // missing) timezone cannot block a genuine same-zone fusion. Sampling one
556
+ // UTC offset per source is insufficient: on DST-transition dates two
557
+ // different zones can share a noon offset yet differ during recorded hours
558
+ // (e.g. America/Los_Angeles vs America/Phoenix on 2026-03-08 — both UTC-7
559
+ // by noon, an hour apart before LA's spring-forward). A source whose
560
+ // timezone id is missing/empty is treated as unresolvable rather than
561
+ // silently coerced to a default, so any unknown tz fails safe. Require
562
+ // exact tz-id identity across all contributing sources.
563
+ if (contributing.length > 1) {
564
+ const referenceTz = contributing[0]!.timezone;
565
+ const allSameExplicitTz =
566
+ referenceTz.trim().length > 0 &&
567
+ contributing.every((entry) => entry.timezone === referenceTz);
568
+ if (!allSameExplicitTz) {
569
+ const detail = contributing
570
+ .map((entry) => `${entry.source}=${entry.timezone || "?"}`)
571
+ .join(", ");
572
+ log.warn(
573
+ `wearables fusion: skipping ${date} — sources were rendered under differing timezones (${detail}); reconstructFusionInputs only compares local clocks correctly when every source shares one explicit IANA timezone id`,
574
+ );
575
+ // Clear any previously-fused artifact for this day: a day that
576
+ // fused successfully before must not keep serving a stale view
577
+ // now that this run explicitly refuses to fuse (issue #1849).
578
+ await storage.fusionArtifactStore().deleteFusedDay(date);
579
+ return {
580
+ date,
581
+ sources: [],
582
+ conversationCount: 0,
583
+ contentHash: "",
584
+ written: false,
585
+ skipped: { reason: "conflicting-timezones" },
586
+ };
587
+ }
588
+ }
589
+ const sourceTrust: Record<string, number> = {};
590
+ for (const [id, settings] of enabled) {
591
+ sourceTrust[id] = settings.sourceTrust;
592
+ }
593
+ const result = fuseDayInputs(date, reconstructed, {
594
+ proximityGapMs: this.deps.config.fusion.proximityGapMs,
595
+ windowToleranceMs: this.deps.config.fusion.windowToleranceMs,
596
+ sourceTrust,
597
+ });
598
+ // Idempotent skip-unchanged: do not rewrite when the input content
599
+ // hash matches the stored artifact, the stored body parsed cleanly
600
+ // into a well-formed conversation set, AND the stored body hash still
601
+ // matches a recompute over the stored body itself. A
602
+ // truncated/corrupt/malformed-element body fails to parse
603
+ // (parseOk:false) even when the frontmatter hash matches, and a body
604
+ // whose bytes drifted fails the body-hash recompute even when the
605
+ // input hash + conversation count still match — so either forces a
606
+ // self-repair rewrite instead of trusting the hashes alone.
607
+ const existingRaw = await storage.fusionArtifactStore().readFusedDay(date);
608
+ const existing = parseFusionDay(existingRaw ?? "");
609
+ // parseOk is bound to a local so the skip condition can keep the
610
+ // explicit `existing !== null` guard for the `.meta` access below —
611
+ // an inline `existing.parseOk` member check there trips
612
+ // useOptionalChain (whose suggested fix would drop the guard and
613
+ // null-deref the later access), so we read it via optional chain.
614
+ const parsedCleanly = existing?.parseOk === true;
615
+ const skipUnchanged =
616
+ existing !== null &&
617
+ parsedCleanly &&
618
+ existing.meta.contentHash === result.contentHash &&
619
+ existing.meta.bodyHash === hashFusionBody(existing.conversations);
620
+ const written = !skipUnchanged;
621
+ if (written) {
622
+ const meta = composeFusionDayMeta(
623
+ date,
624
+ result.conversations,
625
+ result.sources,
626
+ result.contentHash,
627
+ new Date().toISOString(),
628
+ );
629
+ await storage.fusionArtifactStore().writeFusedDay(
630
+ date,
631
+ serializeFusionDay(meta, result.conversations),
632
+ );
633
+ }
634
+ return {
635
+ date: result.date,
636
+ sources: result.sources,
637
+ conversationCount: result.conversations.length,
638
+ contentHash: result.contentHash,
639
+ written,
640
+ };
641
+ }
642
+
643
+ /**
644
+ * List fused conversations for a date (the fusion listing surface).
645
+ * Returns the persisted derived artifact's conversations, or an empty
646
+ * array when no fused artifact has been written for that day. When a
647
+ * file EXISTS but its body is corrupt (parseOk:false), throws
648
+ * WearablesInputError so the caller can distinguish a broken artifact
649
+ * from a day that was never fused — never silently returns an empty
650
+ * list that looks identical to "no artifact" (issue #1849).
651
+ */
652
+ async fusedConversations(
653
+ date: string,
654
+ ): Promise<FusedWearableConversation[]> {
655
+ if (!isValidTranscriptDate(date)) {
656
+ throw new WearablesInputError(`invalid date '${date}' — expected YYYY-MM-DD`);
657
+ }
658
+ const storage = await this.deps.getStorage();
659
+ const raw = await storage.fusionArtifactStore().readFusedDay(date);
660
+ if (raw === null) return [];
661
+ const parsed = parseFusionDay(raw);
662
+ if (parsed === null) return [];
663
+ if (!parsed.parseOk) {
664
+ throw new WearablesInputError(
665
+ `fused artifact for ${date} is corrupt — re-run \`wearables fuse ${date}\` to repair`,
666
+ );
667
+ }
668
+ return parsed.conversations;
669
+ }
670
+
671
+ /** List dates with stored fused artifacts, newest first. */
672
+ async listFusedDays(): Promise<string[]> {
673
+ const storage = await this.deps.getStorage();
674
+ return storage.fusionArtifactStore().listFusedDays();
675
+ }
676
+
467
677
  /**
468
678
  * Search stored transcripts. Uses the indexed backend when available
469
679
  * and falls back to a bounded substring scan otherwise — the two
@@ -509,31 +719,54 @@ export class WearablesService {
509
719
  };
510
720
 
511
721
  if (this.deps.searchBackend) {
512
- const hits = await this.deps.searchBackend.search(trimmed, limit * 5);
513
- if (hits !== null) {
514
- const results: WearableTranscriptSearchResult[] = [];
722
+ // The index stores escaped segment text (real newlines become
723
+ // the two characters \n, lone backslashes are doubled). A query
724
+ // containing those characters in their ORIGINAL decoded form will
725
+ // not match the indexed representation, so we ALSO search the
726
+ // escaped form. This keeps the indexed path at parity with the
727
+ // scan fallback, which decodes the body before searching (#1849).
728
+ const escaped = escapeSegmentText(trimmed);
729
+ const queries =
730
+ escaped === trimmed ? [trimmed] : [trimmed, escaped];
731
+ const seen = new Set<string>();
732
+ const results: WearableTranscriptSearchResult[] = [];
733
+ const idxStorage = await this.deps.getStorage();
734
+ for (const q of queries) {
735
+ const hits = await this.deps.searchBackend.search(q, limit * 5);
736
+ if (hits === null) break; // backend unavailable
515
737
  for (const hit of hits) {
516
738
  const located = locateTranscriptPath(hit.path);
517
739
  if (!located) continue;
518
740
  if (!matchesScope(located.source, located.date)) continue;
741
+ const dedupeKey = `${located.source}:${located.date}`;
742
+ if (seen.has(dedupeKey)) continue;
743
+ seen.add(dedupeKey);
744
+ // The indexed snippet mirrors the stored body's escape encoding.
745
+ // Read the file once to learn whether it was written by the
746
+ // escape-aware serializer so a LEGACY file's literal two-character
747
+ // \n/\r is never decoded (#1849).
748
+ const idxRaw =
749
+ await idxStorage.readWearableDayTranscript(located.source, located.date);
750
+ const idxMeta = parseDayTranscript(idxRaw ?? "")?.meta ?? null;
519
751
  results.push({
520
752
  source: located.source,
521
753
  date: located.date,
522
754
  score: hit.score,
523
- snippet: hit.preview,
755
+ snippet: decodeTranscriptBody(hit.preview, bodyIsEscaped(idxMeta)),
524
756
  backend: "indexed",
525
757
  });
526
758
  if (results.length >= limit) break;
527
759
  }
528
- // The index spans the whole memory dir, so ordinary memory
529
- // files can crowd transcripts out of the top hits entirely.
530
- // Zero in-scope hits therefore doesn't mean "no transcript
531
- // matches" fall through to the bounded scan in that case
532
- // (Codex P2 on PR #1458). Partial result sets stay indexed-only
533
- // so the two backends never interleave in one response.
534
- if (results.length > 0) {
535
- return results;
536
- }
760
+ if (results.length >= limit) break;
761
+ }
762
+ // The index spans the whole memory dir, so ordinary memory
763
+ // files can crowd transcripts out of the top hits entirely.
764
+ // Zero in-scope hits therefore doesn't mean "no transcript
765
+ // matches" fall through to the bounded scan in that case
766
+ // (Codex P2 on PR #1458). Partial result sets stay indexed-only
767
+ // so the two backends never interleave in one response.
768
+ if (results.length > 0) {
769
+ return results;
537
770
  }
538
771
  }
539
772
 
@@ -546,7 +779,16 @@ export class WearablesService {
546
779
  if (!matchesScope(source, date)) continue;
547
780
  const raw = await storage.readWearableDayTranscript(source, date);
548
781
  if (raw === null) continue;
549
- const body = parseDayTranscript(raw)?.body ?? raw;
782
+ const scanParsed = parseDayTranscript(raw);
783
+ // Decode escaped segment text so searches match AND snippets show
784
+ // the ORIGINAL text (e.g. a real newline or backslash), not the
785
+ // internal escape serialization — but ONLY for bodies written by
786
+ // the escape-aware serializer; legacy bodies are searched verbatim
787
+ // (#1849).
788
+ const body = decodeTranscriptBody(
789
+ scanParsed?.body ?? raw,
790
+ bodyIsEscaped(scanParsed?.meta),
791
+ );
550
792
  const lower = body.toLowerCase();
551
793
  const index = lower.indexOf(needle);
552
794
  if (index === -1) continue;
@@ -8,10 +8,12 @@ import {
8
8
  DEFAULT_SELF_NAME,
9
9
  distinctSpeakerLabels,
10
10
  emptySpeakerRegistry,
11
+ hasSelfMarker,
11
12
  loadSpeakerRegistry,
12
13
  resolveSpeaker,
13
14
  saveSpeakerRegistry,
14
15
  speakerRegistryKey,
16
+ stripSelfMarker,
15
17
  } from "./speakers.js";
16
18
 
17
19
  test("resolution precedence: override > wearer flag > provider name > raw key", () => {
@@ -108,3 +110,129 @@ test("a malformed speakers file throws instead of silently resetting", async ()
108
110
  rmSync(dir, { recursive: true, force: true });
109
111
  }
110
112
  });
113
+
114
+ test("the reserved (you) self marker is stripped from non-self display names (issue #1849)", () => {
115
+ const registry = emptySpeakerRegistry();
116
+ registry.selfName = "Jordan";
117
+ // A non-self override whose stored name already ends with the reserved
118
+ // marker must NOT carry it into the rendered label — otherwise the
119
+ // fusion reconstructor would read the suffix as self metadata and
120
+ // attribute this speaker's words to the wearer.
121
+ registry.speakers[speakerRegistryKey("bee", "SPEAKER_01")] = {
122
+ name: "Pat (you)",
123
+ updatedAt: "2026-06-10T00:00:00Z",
124
+ };
125
+ assert.deepEqual(
126
+ resolveSpeaker("bee", { speakerKey: "SPEAKER_01" }, registry),
127
+ { label: "Pat", isSelf: false },
128
+ );
129
+
130
+ // A self override whose name ends with the marker renders exactly ONE
131
+ // marker (never "Pat (you) (you)").
132
+ registry.speakers[speakerRegistryKey("bee", "SPEAKER_02")] = {
133
+ name: "Pat (you)",
134
+ isSelf: true,
135
+ updatedAt: "2026-06-10T00:00:00Z",
136
+ };
137
+ assert.deepEqual(
138
+ resolveSpeaker("bee", { speakerKey: "SPEAKER_02" }, registry),
139
+ { label: "Pat (you)", isSelf: true },
140
+ );
141
+
142
+ // A provider-supplied name carrying the marker is also stripped.
143
+ assert.deepEqual(
144
+ resolveSpeaker(
145
+ "omi",
146
+ { speakerKey: "SPEAKER_03", speakerName: "Sam (you)" },
147
+ registry,
148
+ ),
149
+ { label: "Sam", isSelf: false },
150
+ );
151
+
152
+ });
153
+
154
+ test("distinctSpeakerLabels never emits a non-self label ending in (you)", () => {
155
+ const registry = emptySpeakerRegistry();
156
+ registry.speakers[speakerRegistryKey("bee", "0")] = {
157
+ name: "Pat (you)",
158
+ updatedAt: "2026-06-10T00:00:00Z",
159
+ };
160
+ const labels = distinctSpeakerLabels(
161
+ "bee",
162
+ [
163
+ { speakerKey: "user", isWearer: true },
164
+ { speakerKey: "0" },
165
+ ],
166
+ registry,
167
+ );
168
+ // The wearer keeps the marker; the non-self "Pat (you)" override does not.
169
+ assert.deepEqual(labels, ["Me (you)", "Pat"]);
170
+ });
171
+
172
+ test("stripSelfMarker / hasSelfMarker use linear string ops (CodeQL #1849)", () => {
173
+ // Standard cases — marker present and absent.
174
+ assert.equal(stripSelfMarker("Jordan (you)"), "Jordan");
175
+ assert.equal(stripSelfMarker("Jordan"), "Jordan");
176
+ assert.equal(hasSelfMarker("Jordan (you)"), true);
177
+ assert.equal(hasSelfMarker("Jordan"), false);
178
+
179
+ // Trailing whitespace around the marker is absorbed.
180
+ assert.equal(stripSelfMarker("Jordan (you) "), "Jordan");
181
+ assert.equal(hasSelfMarker("Jordan (you) \t"), true);
182
+
183
+ // Only the final marker is stripped — a second "(you)" survives.
184
+ assert.equal(stripSelfMarker("Jordan (you)(you)"), "Jordan (you)");
185
+
186
+ // Marker with leading whitespace but no name.
187
+ assert.equal(stripSelfMarker(" (you)"), "");
188
+ assert.equal(stripSelfMarker("(you)"), "");
189
+
190
+ // Empty / whitespace-only input — the case that caused O(n^2)
191
+ // backtracking with the old \s*\(you\)\s*$ regex.
192
+ assert.equal(stripSelfMarker(""), "");
193
+ assert.equal(stripSelfMarker(" "), "");
194
+ assert.equal(hasSelfMarker(" "), false);
195
+ assert.equal(hasSelfMarker(""), false);
196
+
197
+ // "(you)" not at the end is NOT a marker.
198
+ assert.equal(hasSelfMarker("(you) said hi"), false);
199
+ assert.equal(stripSelfMarker("(you) said hi"), "(you) said hi");
200
+ });
201
+
202
+ test("raw speakerKey fallback strips the reserved (you) marker (issue #1849)", () => {
203
+ // A provider speakerKey that literally contains the reserved `(you)`
204
+ // suffix must NEVER carry it into a rendered non-self label. Without
205
+ // the strip, the fusion reconstructor would read the suffix as self
206
+ // metadata and misattribute the speaker as the wearer.
207
+ const registry = emptySpeakerRegistry();
208
+ registry.selfName = "Jordan";
209
+
210
+ // Raw key with a trailing (you) — non-self fallback path.
211
+ const resolved = resolveSpeaker(
212
+ "bee",
213
+ { speakerKey: "Pat (you)" },
214
+ registry,
215
+ );
216
+ assert.equal(resolved.isSelf, false, "raw key is never self");
217
+ assert.equal(resolved.label, "Pat", "(you) stripped from raw key label");
218
+ assert.equal(hasSelfMarker(resolved.label), false, "no marker leaks");
219
+
220
+ // Bare-digit key with a trailing (you) becomes "Speaker <digits>"
221
+ // after stripping, never carrying the marker.
222
+ const digitResolved = resolveSpeaker(
223
+ "omi",
224
+ { speakerKey: "5 (you)" },
225
+ registry,
226
+ );
227
+ assert.equal(digitResolved.isSelf, false);
228
+ assert.equal(digitResolved.label, "Speaker 5");
229
+
230
+ // A key that is ONLY "(you)" strips to empty -> "Unknown speaker".
231
+ const onlyMarker = resolveSpeaker(
232
+ "limitless",
233
+ { speakerKey: "(you)" },
234
+ registry,
235
+ );
236
+ assert.equal(onlyMarker.isSelf, false);
237
+ assert.equal(onlyMarker.label, "Unknown speaker");
238
+ });
@@ -35,6 +35,43 @@ export interface SpeakerRegistry {
35
35
 
36
36
  export const DEFAULT_SELF_NAME = "Me";
37
37
 
38
+ /**
39
+ * Reserved self marker appended to a wearer's display label so the
40
+ * rendered transcript carries explicit, parseable self metadata. It is
41
+ * RESERVED: a non-self display label can never carry it (see
42
+ * `stripSelfMarker`), so when `reconstructFusionInputs` reads it back it
43
+ * is authoritative self metadata — never a coincidental display-name
44
+ * suffix that would misattribute another speaker as the wearer
45
+ * (issue #1849).
46
+ */
47
+ export const SELF_MARKER_SUFFIX = " (you)";
48
+ const SELF_MARKER = "(you)";
49
+
50
+ /**
51
+ * Remove a reserved `(you)` self marker from a display name. Applied to
52
+ * every base name before it becomes a label so the marker can only ever
53
+ * appear as the renderer-appended self flag, never as part of a stored
54
+ * non-self display name.
55
+ *
56
+ * Implemented with plain string ops instead of a regex so the leading
57
+ * `\s*` (no anchor) cannot cause the O(n²) backtracking CodeQL flags
58
+ * on whitespace-only input (js/polynomial-redos).
59
+ */
60
+ export function stripSelfMarker(name: string): string {
61
+ const trimmed = name.trimEnd();
62
+ if (!trimmed.endsWith(SELF_MARKER)) return trimmed;
63
+ return trimmed.slice(0, -SELF_MARKER.length).trimEnd();
64
+ }
65
+
66
+ /**
67
+ * Whether a rendered label carries the reserved self marker. This is the
68
+ * single authoritative self-status detector shared by the renderer and
69
+ * the fusion reconstructor (issue #1849).
70
+ */
71
+ export function hasSelfMarker(label: string): boolean {
72
+ return label.trimEnd().endsWith(SELF_MARKER);
73
+ }
74
+
38
75
  export function emptySpeakerRegistry(): SpeakerRegistry {
39
76
  return { version: 1, selfName: DEFAULT_SELF_NAME, speakers: {} };
40
77
  }
@@ -130,21 +167,38 @@ export function resolveSpeaker(
130
167
  const override = registry.speakers[speakerRegistryKey(sourceId, segment.speakerKey)];
131
168
  if (override) {
132
169
  const isSelf = override.isSelf === true;
170
+ // Reserve the `(you)` marker: strip any pre-existing marker from the
171
+ // base name so a NON-self label can never carry it. Without this, a
172
+ // speaker override like "Pat (you)" set WITHOUT --self would render
173
+ // as "Pat (you)" and the fusion reconstructor would read the suffix
174
+ // as self metadata, attributing Pat's words to the wearer (issue
175
+ // #1849).
176
+ const base = stripSelfMarker(override.name);
133
177
  return {
134
- label: isSelf ? `${override.name} (you)` : override.name,
178
+ label: isSelf ? `${base}${SELF_MARKER_SUFFIX}` : base,
135
179
  isSelf,
136
180
  };
137
181
  }
138
182
  if (segment.isWearer === true) {
139
- return { label: `${registry.selfName} (you)`, isSelf: true };
183
+ return {
184
+ label: `${stripSelfMarker(registry.selfName)}${SELF_MARKER_SUFFIX}`,
185
+ isSelf: true,
186
+ };
140
187
  }
141
188
  if (
142
189
  typeof segment.speakerName === "string" &&
143
190
  segment.speakerName.trim().length > 0
144
191
  ) {
145
- return { label: segment.speakerName.trim(), isSelf: false };
192
+ // Provider-supplied names are never self here; strip a stray marker
193
+ // so a provider label cannot masquerade as the reserved self flag.
194
+ return { label: stripSelfMarker(segment.speakerName.trim()), isSelf: false };
146
195
  }
147
- const key = segment.speakerKey.trim();
196
+ // Strip the reserved `(you)` marker from the raw key so a provider
197
+ // speakerKey like "Pat (you)" can never leak the marker into a non-self
198
+ // rendered label — the fusion reconstructor reads the suffix as
199
+ // authoritative self metadata and would misattribute the speaker as the
200
+ // wearer (issue #1849).
201
+ const key = stripSelfMarker(segment.speakerKey.trim());
148
202
  // Bare diarization indexes read better with a prefix.
149
203
  if (/^\d+$/.test(key)) {
150
204
  return { label: `Speaker ${key}`, isSelf: false };