chatlab-cli 0.36.2 → 0.37.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 (201) hide show
  1. package/dist/{chat-command-SEV6N3AR.mjs → chat-command-WBAJBKIE.mjs} +15 -16
  2. package/dist/{chat-command-SEV6N3AR.mjs.map → chat-command-WBAJBKIE.mjs.map} +1 -1
  3. package/dist/chunk-2TV7M4CC.mjs +101 -0
  4. package/dist/chunk-2TV7M4CC.mjs.map +1 -0
  5. package/dist/{chunk-TRGR3KKJ.mjs → chunk-4J4MA3RC.mjs} +2605 -672
  6. package/dist/chunk-4J4MA3RC.mjs.map +1 -0
  7. package/dist/{chunk-TJ3NOBVI.mjs → chunk-5YOHLJFB.mjs} +98 -284
  8. package/dist/chunk-5YOHLJFB.mjs.map +1 -0
  9. package/dist/{chunk-WRBFV2GX.mjs → chunk-CR2MU7LN.mjs} +49 -55
  10. package/dist/chunk-CR2MU7LN.mjs.map +1 -0
  11. package/dist/{chunk-WJQOXOMR.mjs → chunk-DKIYHA3O.mjs} +281 -30
  12. package/dist/chunk-DKIYHA3O.mjs.map +1 -0
  13. package/dist/{chunk-VREVDZ4Q.mjs → chunk-DQR2YS3I.mjs} +1930 -33
  14. package/dist/chunk-DQR2YS3I.mjs.map +1 -0
  15. package/dist/{chunk-VUYWUHWW.mjs → chunk-EJBBVLUN.mjs} +3 -3
  16. package/dist/chunk-EJBBVLUN.mjs.map +1 -0
  17. package/dist/{chunk-HWIXK7SU.mjs → chunk-FM3GXNEO.mjs} +1643 -3
  18. package/dist/chunk-FM3GXNEO.mjs.map +1 -0
  19. package/dist/{chunk-YUH3H3QW.mjs → chunk-MSMDLDPB.mjs} +96 -5
  20. package/dist/chunk-MSMDLDPB.mjs.map +1 -0
  21. package/dist/{chunk-6EZPD3XJ.mjs → chunk-N2WGG3MS.mjs} +2 -2
  22. package/dist/{chunk-TWH422HC.mjs → chunk-RXMSZIYD.mjs} +3 -3
  23. package/dist/{chunk-CH5YAZ4Q.mjs → chunk-SQWN4GTQ.mjs} +2 -2
  24. package/dist/cli.mjs +9 -9
  25. package/dist/contacts-worker.mjs +2 -2
  26. package/dist/global-insight-worker.mjs +1 -1
  27. package/dist/{http-237XYNRT.mjs → http-X6OEOOSH.mjs} +12 -12
  28. package/dist/{import-R2E2TFAK.mjs → import-WBJ6YIQ7.mjs} +7 -7
  29. package/dist/index.mjs +12 -12
  30. package/dist/{mcp-BED4AARV.mjs → mcp-FR6OKWKL.mjs} +8 -8
  31. package/dist/people-relationships-worker.mjs +2 -2
  32. package/dist/semantic-index-worker.mjs +1 -1
  33. package/dist/{src-AYMALWU5.mjs → src-WE5HNN2O.mjs} +44 -14
  34. package/dist/{stream-import-RXNSOOYH.mjs → stream-import-YYQVZDGQ.mjs} +7 -7
  35. package/dist/{worker-runtime-JGAZU7MC.mjs → worker-runtime-YGB5ZDE4.mjs} +5 -5
  36. package/dist-cli-web/assets/AISettingsTab-Co2sM1iY.js +1 -0
  37. package/dist-cli-web/assets/{AboutTab-DCM1-glA.js → AboutTab-DQ8r6mYE.js} +1 -1
  38. package/dist-cli-web/assets/ActionToolsPanel-CmYpw8Mt.css +1 -0
  39. package/dist-cli-web/assets/ActionToolsPanel.vue_vue_type_script_setup_true_lang--RPlmfIu.js +75 -0
  40. package/dist-cli-web/assets/{Alert-D-egBWcb.js → Alert-DU1tLAAg.js} +1 -1
  41. package/dist-cli-web/assets/ApiSettingsTab-D5g5b79L.js +2 -0
  42. package/dist-cli-web/assets/BatchManageTab-BFUyVaGU.js +1 -0
  43. package/dist-cli-web/assets/{BufferResource-BE2_2bZK.js → BufferResource-C-RfYwQW.js} +1 -1
  44. package/dist-cli-web/assets/{CanvasRenderer-DDKdvEYQ.js → CanvasRenderer-Dz7JAMrR.js} +1 -1
  45. package/dist-cli-web/assets/CardDecoration-D28DI1G9.js +1 -0
  46. package/dist-cli-web/assets/ChatRecordDrawer-Cph819yY.js +1 -0
  47. package/dist-cli-web/assets/{ChatRecordWorkspace-DqUI4QiU.js → ChatRecordWorkspace-BAfERdPc.js} +1 -1
  48. package/dist-cli-web/assets/Checkbox-B8uqt13j.js +1 -0
  49. package/dist-cli-web/assets/DataDirCleanupNotice-3sGBE5vU.js +1 -0
  50. package/dist-cli-web/assets/{DatePicker.vue_vue_type_script_setup_true_lang-sPDoPzZ9.js → DatePicker.vue_vue_type_script_setup_true_lang-CQuub8-f.js} +1 -1
  51. package/dist-cli-web/assets/{DebugToolsPanel-BIHA6jw4.js → DebugToolsPanel-Cwkr0BOY.js} +1 -1
  52. package/dist-cli-web/assets/EChart.vue_vue_type_script_setup_true_lang-BuLujQCF.js +1 -0
  53. package/dist-cli-web/assets/EChartBar.vue_vue_type_script_setup_true_lang-DuA5xbcC.js +1 -0
  54. package/dist-cli-web/assets/EChartLine.vue_vue_type_script_setup_true_lang-CvmMEfYq.js +1 -0
  55. package/dist-cli-web/assets/EChartWordcloud-CHVtyPR7.js +1 -0
  56. package/dist-cli-web/assets/FileDropZone.vue_vue_type_script_setup_true_lang-BKPy9Qv-.js +1 -0
  57. package/dist-cli-web/assets/{Filter-kO4dIDJ2.js → Filter-CMfRHIoK.js} +1 -1
  58. package/dist-cli-web/assets/GlobalTaskBar-Jz6MASCu.js +1 -0
  59. package/dist-cli-web/assets/GlobalTaskBar-haLzezqY.css +1 -0
  60. package/dist-cli-web/assets/GroupRelationshipGalaxyView-Ch4obfEq.js +1 -0
  61. package/dist-cli-web/assets/Label-CuryuymO.js +1 -0
  62. package/dist-cli-web/assets/LoadingState.vue_vue_type_script_setup_true_lang-CoeQsaiL.js +1 -0
  63. package/dist-cli-web/assets/MessageExportModal-JqGZ-iLr.js +1 -0
  64. package/dist-cli-web/assets/PageHeader-CPLgiKT8.css +1 -0
  65. package/dist-cli-web/assets/PageHeader-D4up77TB.js +1 -0
  66. package/dist-cli-web/assets/{PageTabs.vue_vue_type_script_setup_true_lang-Cgungbjl.js → PageTabs.vue_vue_type_script_setup_true_lang-DYQRFk9s.js} +1 -1
  67. package/dist-cli-web/assets/{RelationshipGalaxyCanvas-Cod1JX9m.js → RelationshipGalaxyCanvas-Cn7Eta5U.js} +3 -3
  68. package/dist-cli-web/assets/{RelationshipGalaxyThreeCanvas-CkefzeFJ.js → RelationshipGalaxyThreeCanvas-LlaX57z6.js} +1 -1
  69. package/dist-cli-web/assets/{RenderTargetSystem-SuQ7cbze.js → RenderTargetSystem-JhA2PUFi.js} +1 -1
  70. package/dist-cli-web/assets/{RovingFocusItem-psoFYxrC.js → RovingFocusItem-DHaiTb4j.js} +1 -1
  71. package/dist-cli-web/assets/{ScreenCaptureModal-D4r9ixKh.js → ScreenCaptureModal-Bxhyt5g1.js} +1 -1
  72. package/dist-cli-web/assets/SectionTabs.vue_vue_type_script_setup_true_lang-BlAje3Tf.js +1 -0
  73. package/dist-cli-web/assets/Select-Beeqv0wU.js +1 -0
  74. package/dist-cli-web/assets/SettingsModal-DV8yDBJs.js +2 -0
  75. package/dist-cli-web/assets/{StorageTab-DhSJ-0Ir.js → StorageTab-B7GHadia.js} +1 -1
  76. package/dist-cli-web/assets/Switch-u4zlvUMa.js +1 -0
  77. package/dist-cli-web/assets/{Tabs-Bnm_Q7yW.js → Tabs-B7-Koq3x.js} +1 -1
  78. package/dist-cli-web/assets/Tabs-RWKHwtfM.js +1 -0
  79. package/dist-cli-web/assets/ThemeCard.vue_vue_type_script_setup_true_lang-ndsa3-aT.js +1 -0
  80. package/dist-cli-web/assets/{TimeSelect.vue_vue_type_script_setup_true_lang-Cjz8Wpg-.js → TimeSelect.vue_vue_type_script_setup_true_lang-DKa1flT-.js} +1 -1
  81. package/dist-cli-web/assets/{VisuallyHiddenInput-Bo_XwaLu.js → VisuallyHiddenInput-DGDqcvUy.js} +1 -1
  82. package/dist-cli-web/assets/{WebGLRenderer-D1x9WZRj.js → WebGLRenderer-ifvgA1fe.js} +1 -1
  83. package/dist-cli-web/assets/{WebGPURenderer-37Nw-tLm.js → WebGPURenderer-DwzmuWF0.js} +1 -1
  84. package/dist-cli-web/assets/aiChat-BY4C3UQe.js +8 -0
  85. package/dist-cli-web/assets/base-BWDX-164.js +1 -0
  86. package/dist-cli-web/assets/{browserAll-CWrz_-4U.js → browserAll-CSIIETeE.js} +1 -1
  87. package/dist-cli-web/assets/chatMessages--r0obAVw.css +1 -0
  88. package/dist-cli-web/assets/chatMessages-Zywjea1n.js +147 -0
  89. package/dist-cli-web/assets/cli-web-BRY49NWe.js +1 -0
  90. package/dist-cli-web/assets/{fetch-Du-wU0OO.js → fetch-Bf4cqeM3.js} +1 -1
  91. package/dist-cli-web/assets/{fetch-DPSWt6yX.js → fetch-BiXab0IF.js} +1 -1
  92. package/dist-cli-web/assets/{fetch-B69OkB6B.js → fetch-C8CcoTWY.js} +1 -1
  93. package/dist-cli-web/assets/{fetch-qd2FgBWQ.js → fetch-Co2vLmiE.js} +1 -1
  94. package/dist-cli-web/assets/{fetch-BGmCCyzz.js → fetch-D3nLtFBi.js} +1 -1
  95. package/dist-cli-web/assets/{fetch-CFpLXWTe.js → fetch-DNSDHSqQ.js} +1 -1
  96. package/dist-cli-web/assets/{fetch-DZ6OF-xW.js → fetch-DeHqF6yL.js} +1 -1
  97. package/dist-cli-web/assets/{fetch-CkXgOxcu.js → fetch-jx1VdM9j.js} +3 -3
  98. package/dist-cli-web/assets/{fetch-Gu4qQjUB.js → fetch-mYSeL4Ki.js} +1 -1
  99. package/dist-cli-web/assets/fetch-qBaUXqFt.js +1 -0
  100. package/dist-cli-web/assets/{fetch-6pcVxB9h.js → fetch-r6mhEGrP.js} +1 -1
  101. package/dist-cli-web/assets/{fetch-CgA1peJD.js → fetch-wzv71PYy.js} +1 -1
  102. package/dist-cli-web/assets/{host-CWsAWKgy.js → host-1emQf9_4.js} +1 -1
  103. package/dist-cli-web/assets/{host-D8zlA62K.js → host-jXJHyyOX.js} +1 -1
  104. package/dist-cli-web/assets/index-2dJCozDq.js +64 -0
  105. package/dist-cli-web/assets/index-6ejRbH3h.js +2 -0
  106. package/dist-cli-web/assets/index-B4mJkX_d.js +1 -0
  107. package/dist-cli-web/assets/index-B7biznha.js +1 -0
  108. package/dist-cli-web/assets/index-BOcFyPCa.js +1 -0
  109. package/dist-cli-web/assets/index-BQ463TS7.js +1 -0
  110. package/dist-cli-web/assets/index-BZpR015e.css +1 -0
  111. package/dist-cli-web/assets/index-BgelKQTW.css +1 -0
  112. package/dist-cli-web/assets/index-C1A1FwpO.css +1 -0
  113. package/dist-cli-web/assets/index-CIIyg_o9.css +1 -0
  114. package/dist-cli-web/assets/{index-a7CT8DQm.js → index-CYc_VLgK.js} +1 -1
  115. package/dist-cli-web/assets/index-ClHMs0-a.js +2 -0
  116. package/dist-cli-web/assets/{index-Dy0fRMDf.js → index-Cw0IhKZ6.js} +10 -10
  117. package/dist-cli-web/assets/{index-v6DlL4ue.js → index-DOqbUvhA.js} +1 -1
  118. package/dist-cli-web/assets/index-DnpnIyle.js +135 -0
  119. package/dist-cli-web/assets/index-LV9LfFNu.js +1 -0
  120. package/dist-cli-web/assets/index-kePiJ3ca.js +137 -0
  121. package/dist-cli-web/assets/index-slDUxib2.js +1 -0
  122. package/dist-cli-web/assets/{people-page-header-Czt9e1jv.js → people-page-header-Cx5r9Bk2.js} +1 -1
  123. package/dist-cli-web/assets/{prompt-DGazz143.js → prompt-BggEDxxi.js} +1 -1
  124. package/dist-cli-web/assets/{semantic-index-config-builder-BkFHzIUy.js → semantic-index-config-builder-1sO0aEc1.js} +1 -1
  125. package/dist-cli-web/assets/{service-CAp0YJoS.js → service-Bvq1fRkC.js} +1 -1
  126. package/dist-cli-web/assets/service-DkPheDt8.js +1 -0
  127. package/dist-cli-web/assets/service-DvI8O_7K.js +1 -0
  128. package/dist-cli-web/assets/useFormControl-B0aShtLp.js +1 -0
  129. package/dist-cli-web/assets/useTable-DC7oxmBl.js +1 -0
  130. package/dist-cli-web/assets/{useToast-YAHDkKDN.js → useToast-CCPBE-Sy.js} +1 -1
  131. package/dist-cli-web/assets/{vendor-echarts-hTLhTs52.js → vendor-echarts-C9ugTmum.js} +1 -1
  132. package/dist-cli-web/assets/{vendor-echarts-wordcloud-B8CF_NgP.js → vendor-echarts-wordcloud-DNiU8Psq.js} +1 -1
  133. package/dist-cli-web/assets/{webworkerAll-CvSp2Y8j.js → webworkerAll-FCJk6reG.js} +1 -1
  134. package/dist-cli-web/index.html +2 -2
  135. package/package.json +1 -1
  136. package/dist/chunk-6E46U4H2.mjs +0 -41
  137. package/dist/chunk-6E46U4H2.mjs.map +0 -1
  138. package/dist/chunk-HWIXK7SU.mjs.map +0 -1
  139. package/dist/chunk-TJ3NOBVI.mjs.map +0 -1
  140. package/dist/chunk-TRGR3KKJ.mjs.map +0 -1
  141. package/dist/chunk-VREVDZ4Q.mjs.map +0 -1
  142. package/dist/chunk-VUYWUHWW.mjs.map +0 -1
  143. package/dist/chunk-WJQOXOMR.mjs.map +0 -1
  144. package/dist/chunk-WRBFV2GX.mjs.map +0 -1
  145. package/dist/chunk-YUH3H3QW.mjs.map +0 -1
  146. package/dist-cli-web/assets/AISettingsTab-VgI7PiJd.js +0 -1
  147. package/dist-cli-web/assets/ActionToolsPanel-BTGgwXVD.css +0 -1
  148. package/dist-cli-web/assets/ActionToolsPanel.vue_vue_type_script_setup_true_lang-a7RMjjve.js +0 -112
  149. package/dist-cli-web/assets/ApiSettingsTab-Co3LWf1f.js +0 -2
  150. package/dist-cli-web/assets/BatchManageTab-2LrckmQm.js +0 -1
  151. package/dist-cli-web/assets/ChatRecordDrawer-DRhztqLG.js +0 -1
  152. package/dist-cli-web/assets/Checkbox-EfpQmv4A.js +0 -1
  153. package/dist-cli-web/assets/DataDirCleanupNotice-BYIiR58J.js +0 -1
  154. package/dist-cli-web/assets/EChart.vue_vue_type_script_setup_true_lang-DryMSoCz.js +0 -1
  155. package/dist-cli-web/assets/EChartBar.vue_vue_type_script_setup_true_lang-BdFW-hvt.js +0 -1
  156. package/dist-cli-web/assets/EChartLine.vue_vue_type_script_setup_true_lang-C69Kdw9X.js +0 -1
  157. package/dist-cli-web/assets/EChartWordcloud-DM7UEHdK.js +0 -1
  158. package/dist-cli-web/assets/GlobalTaskBar-BZS_h0zC.js +0 -1
  159. package/dist-cli-web/assets/GlobalTaskBar-COW_uBX6.css +0 -1
  160. package/dist-cli-web/assets/GroupRelationshipGalaxyView--WIaHx_F.js +0 -1
  161. package/dist-cli-web/assets/Label-CIuDXyeM.js +0 -1
  162. package/dist-cli-web/assets/LoadingState.vue_vue_type_script_setup_true_lang-B_7VHxTx.js +0 -1
  163. package/dist-cli-web/assets/MessageExportModal-BQsIpyxv.js +0 -1
  164. package/dist-cli-web/assets/PageHeader-DxhQHF_0.js +0 -1
  165. package/dist-cli-web/assets/PageHeader-XIXMTomn.css +0 -1
  166. package/dist-cli-web/assets/SectionTabs.vue_vue_type_script_setup_true_lang-v_7aRBgM.js +0 -1
  167. package/dist-cli-web/assets/Select-CxurgslB.js +0 -1
  168. package/dist-cli-web/assets/SettingsModal-DBZucuf6.js +0 -2
  169. package/dist-cli-web/assets/Switch-3D81G6uV.js +0 -1
  170. package/dist-cli-web/assets/Tabs-DB603EEu.js +0 -1
  171. package/dist-cli-web/assets/ThemeCard.vue_vue_type_script_setup_true_lang-BDTdkH5i.js +0 -1
  172. package/dist-cli-web/assets/aiChat-BCzYQeZq.js +0 -8
  173. package/dist-cli-web/assets/cli-web-C-8N0CJh.js +0 -1
  174. package/dist-cli-web/assets/fetch-z9M9FcIC.js +0 -1
  175. package/dist-cli-web/assets/index-BGd4ZfhO.js +0 -2
  176. package/dist-cli-web/assets/index-BYZF1ngN.js +0 -1
  177. package/dist-cli-web/assets/index-BcuwlCin.css +0 -1
  178. package/dist-cli-web/assets/index-BnaLj2dx.css +0 -1
  179. package/dist-cli-web/assets/index-Bq5q3kSU.js +0 -36
  180. package/dist-cli-web/assets/index-CPR3jdC2.js +0 -137
  181. package/dist-cli-web/assets/index-D3AjvaOX.js +0 -1
  182. package/dist-cli-web/assets/index-DAitScUA.js +0 -1
  183. package/dist-cli-web/assets/index-DBSMWAJ0.js +0 -1
  184. package/dist-cli-web/assets/index-DTn9htw-.js +0 -64
  185. package/dist-cli-web/assets/index-DV_rWxcH.js +0 -1
  186. package/dist-cli-web/assets/index-DxhAGqYM.css +0 -1
  187. package/dist-cli-web/assets/index-I0mIcDpE.css +0 -1
  188. package/dist-cli-web/assets/index-onwNShRw.js +0 -2
  189. package/dist-cli-web/assets/service-BMqwB74f.js +0 -1
  190. package/dist-cli-web/assets/service-CUQ8iHdS.js +0 -1
  191. package/dist-cli-web/assets/useFormControl-DcSMUc2d.js +0 -1
  192. package/dist-cli-web/assets/useTable-Clmq3sge.js +0 -1
  193. /package/dist/{chunk-6EZPD3XJ.mjs.map → chunk-N2WGG3MS.mjs.map} +0 -0
  194. /package/dist/{chunk-TWH422HC.mjs.map → chunk-RXMSZIYD.mjs.map} +0 -0
  195. /package/dist/{chunk-CH5YAZ4Q.mjs.map → chunk-SQWN4GTQ.mjs.map} +0 -0
  196. /package/dist/{http-237XYNRT.mjs.map → http-X6OEOOSH.mjs.map} +0 -0
  197. /package/dist/{import-R2E2TFAK.mjs.map → import-WBJ6YIQ7.mjs.map} +0 -0
  198. /package/dist/{mcp-BED4AARV.mjs.map → mcp-FR6OKWKL.mjs.map} +0 -0
  199. /package/dist/{src-AYMALWU5.mjs.map → src-WE5HNN2O.mjs.map} +0 -0
  200. /package/dist/{stream-import-RXNSOOYH.mjs.map → stream-import-YYQVZDGQ.mjs.map} +0 -0
  201. /package/dist/{worker-runtime-JGAZU7MC.mjs.map → worker-runtime-YGB5ZDE4.mjs.map} +0 -0
@@ -9,7 +9,7 @@ import {
9
9
  createEmptyContactsDiagnostics,
10
10
  normalizeContactsTimeRangePreset,
11
11
  resolveContactsTimeRange
12
- } from "./chunk-VUYWUHWW.mjs";
12
+ } from "./chunk-EJBBVLUN.mjs";
13
13
  import {
14
14
  PEOPLE_RELATIONSHIPS_ALGORITHM_VERSION,
15
15
  buildPeopleRelationshipsNeighborhoodGraph,
@@ -18,7 +18,7 @@ import {
18
18
  computePeopleRelationshipsSnapshot,
19
19
  normalizePeopleRelationshipsTimeRangePreset,
20
20
  resolvePeopleRelationshipsTimeRange
21
- } from "./chunk-6EZPD3XJ.mjs";
21
+ } from "./chunk-N2WGG3MS.mjs";
22
22
  import {
23
23
  ANNUAL_SUMMARY_ALGORITHM_VERSION,
24
24
  TIME_INVESTMENT_ALGORITHM_VERSION,
@@ -37,6 +37,7 @@ import {
37
37
  NAVIGATION_LAYOUT_SCHEMA_VERSION,
38
38
  appLogger,
39
39
  applyPreprocessingPipeline,
40
+ buildContactKey,
40
41
  buildSessionInfo,
41
42
  checkSessionsCanGenerateSummary,
42
43
  cleanupAnnualSummarySnapshotTempFiles,
@@ -47,6 +48,7 @@ import {
47
48
  deleteMember,
48
49
  deleteMembers,
49
50
  deleteTimeInvestmentSnapshots,
51
+ desensitizeText,
50
52
  generateIncrementalSessionIndex,
51
53
  generateMessageKey,
52
54
  generateSessionIndex,
@@ -55,15 +57,22 @@ import {
55
57
  getChatSessionList,
56
58
  getContactsDir,
57
59
  getContactsFactsCacheDir,
60
+ getCrossChatSessionActivityFacts,
58
61
  getDbFileVersion,
59
62
  getGlobalInsightDir,
60
63
  getGlobalInsightFactsCacheDir,
61
64
  getMemberNameHistory,
62
65
  getMembersPaginated,
63
66
  getMembersWithAliases,
67
+ getNonSystemMembersForContacts,
68
+ getParticipantSessionFacts,
69
+ getParticipantSetInteractionFacts,
64
70
  getPeopleRelationshipsDir,
65
71
  getPeopleRelationshipsFactsCacheDir,
66
72
  getPrivateChatMemberAvatar,
73
+ getRecentMessages,
74
+ getSearchMessageContext,
75
+ getSegmentSummaries,
67
76
  getSegmentSummary,
68
77
  getSessionIndexStats,
69
78
  getSessionInfo,
@@ -80,16 +89,22 @@ import {
80
89
  isSupportedChatLabFormatVersion,
81
90
  loadSegmentMessages,
82
91
  matchOwnerProfile,
92
+ matchesBlacklist,
83
93
  mergeConfirmedNames,
84
94
  mergeMembers,
85
95
  normalizeAnnualSummaryRange,
86
96
  normalizeSessionGapThreshold,
87
97
  openBetterSqliteDatabase,
98
+ preprocessMessages,
88
99
  readAnnualSummarySnapshot,
89
100
  readTimeInvestmentSnapshot,
90
101
  renameSession,
91
102
  resolveApiKey,
103
+ resolveContactMember,
104
+ resolveOwnerMember,
92
105
  saveSegmentSummary,
106
+ searchMessagesByKeywords,
107
+ shouldScopeContactToSession,
93
108
  snapshotPathProvider,
94
109
  toAnnualSummaryLocalDate,
95
110
  toAnnualSummaryRangeKey,
@@ -99,7 +114,7 @@ import {
99
114
  writeAuthProfile,
100
115
  writeParseResultToDb,
101
116
  writeTimeInvestmentSnapshot
102
- } from "./chunk-TRGR3KKJ.mjs";
117
+ } from "./chunk-4J4MA3RC.mjs";
103
118
  import {
104
119
  __commonJS,
105
120
  __export,
@@ -4778,6 +4793,11 @@ var feature5 = {
4778
4793
  requiredFields: ["weflow", "session"]
4779
4794
  }
4780
4795
  };
4796
+ function normalizePlatformMessageId(value) {
4797
+ if (typeof value !== "string" && typeof value !== "number") return void 0;
4798
+ const id = String(value).trim();
4799
+ return id && id !== "0" ? id : void 0;
4800
+ }
4781
4801
  function convertMessageType3(typeStr) {
4782
4802
  switch (typeStr) {
4783
4803
  case "\u6587\u672C\u6D88\u606F":
@@ -5080,9 +5100,9 @@ async function* parseWeFlow(options2) {
5080
5100
  const rawContent = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
5081
5101
  content = rawContent.trim() || null;
5082
5102
  }
5103
+ const platformMessageId = normalizePlatformMessageId(msg.platformMessageId);
5083
5104
  return {
5084
- platformMessageId: String(msg.localId),
5085
- // 消息的平台原始 ID(用于回复关联查询)
5105
+ platformMessageId,
5086
5106
  senderPlatformId: platformId,
5087
5107
  senderAccountName: accountName,
5088
5108
  // WeFlow 格式没有单独的群昵称字段
@@ -7931,22 +7951,56 @@ function generateFallbackMessageKey(message) {
7931
7951
  const contentKey = generateMessageKey(message.timestamp, message.senderPlatformId, message.content);
7932
7952
  return JSON.stringify([contentKey, message.type, message.replyToMessageId || null]);
7933
7953
  }
7934
- function createMessageDedupState(platformMessageIds = [], fallbackKeys = [], fallbackOnlyKeys = []) {
7954
+ function createMessageDedupState(platformMessageIds = [], fallbackKeys = [], fallbackOnlyKeys = [], options2 = {}) {
7955
+ const fallbackKeySet = new Set(fallbackKeys);
7956
+ const fallbackOnlyKeySet = new Set(fallbackOnlyKeys);
7957
+ const fallbackMultiplicityBalances = /* @__PURE__ */ new Map();
7958
+ if (options2.preserveFallbackMultiplicity) {
7959
+ for (const key of fallbackKeySet) fallbackMultiplicityBalances.set(key, -1);
7960
+ for (const key of fallbackOnlyKeySet) fallbackMultiplicityBalances.set(key, 1);
7961
+ }
7935
7962
  return {
7936
7963
  platformMessageIds: new Set(platformMessageIds),
7937
- fallbackKeys: new Set(fallbackKeys),
7938
- fallbackOnlyKeys: new Set(fallbackOnlyKeys)
7964
+ fallbackKeys: fallbackKeySet,
7965
+ fallbackOnlyKeys: fallbackOnlyKeySet,
7966
+ preserveFallbackMultiplicity: options2.preserveFallbackMultiplicity ?? false,
7967
+ fallbackMultiplicityBalances
7939
7968
  };
7940
7969
  }
7970
+ function setFallbackMultiplicityBalance(state, key, balance) {
7971
+ if (balance === 0) state.fallbackMultiplicityBalances.delete(key);
7972
+ else state.fallbackMultiplicityBalances.set(key, balance);
7973
+ }
7974
+ function hasUnmatchedFallbackOnlyMessage(state, fallbackKey) {
7975
+ return state.preserveFallbackMultiplicity ? (state.fallbackMultiplicityBalances.get(fallbackKey) ?? 0) > 0 : state.fallbackOnlyKeys.has(fallbackKey);
7976
+ }
7941
7977
  function registerMessageAndCheckDuplicate(message, state) {
7942
7978
  const key = generateFallbackMessageKey(message);
7943
7979
  if (message.platformMessageId) {
7944
7980
  if (state.platformMessageIds.has(message.platformMessageId)) return true;
7945
7981
  state.platformMessageIds.add(message.platformMessageId);
7982
+ if (state.preserveFallbackMultiplicity) {
7983
+ const balance = state.fallbackMultiplicityBalances.get(key) ?? 0;
7984
+ if (balance > 0) {
7985
+ setFallbackMultiplicityBalance(state, key, balance - 1);
7986
+ return true;
7987
+ }
7988
+ setFallbackMultiplicityBalance(state, key, balance - 1);
7989
+ return false;
7990
+ }
7946
7991
  if (state.fallbackOnlyKeys.delete(key)) return true;
7947
7992
  state.fallbackKeys.add(key);
7948
7993
  return false;
7949
7994
  }
7995
+ if (state.preserveFallbackMultiplicity) {
7996
+ const balance = state.fallbackMultiplicityBalances.get(key) ?? 0;
7997
+ if (balance < 0) {
7998
+ setFallbackMultiplicityBalance(state, key, balance + 1);
7999
+ return true;
8000
+ }
8001
+ setFallbackMultiplicityBalance(state, key, balance + 1);
8002
+ return false;
8003
+ }
7950
8004
  if (state.fallbackKeys.has(key)) return true;
7951
8005
  state.fallbackKeys.add(key);
7952
8006
  state.fallbackOnlyKeys.add(key);
@@ -7954,6 +8008,9 @@ function registerMessageAndCheckDuplicate(message, state) {
7954
8008
  }
7955
8009
 
7956
8010
  // ../../packages/node-runtime/src/import/streaming-importer.ts
8011
+ function shouldPreserveFallbackMultiplicity(formatId) {
8012
+ return formatId === PARSER_FORMAT_IDS.WEFLOW || formatId === PARSER_FORMAT_IDS.ECHOTRACE;
8013
+ }
7957
8014
  var BATCH_COMMIT_SIZE = 5e4;
7958
8015
  var CHECKPOINT_INTERVAL = 2e5;
7959
8016
  var SYSTEM_SENDER_ID2 = "SYSTEM";
@@ -8251,7 +8308,9 @@ async function streamImportSingle(filePath, deps, formatFeature, formatOptions,
8251
8308
  skippedInvalidTimestamp: 0,
8252
8309
  skippedNoType: 0
8253
8310
  };
8254
- const dedupState = createMessageDedupState();
8311
+ const dedupState = createMessageDedupState([], [], [], {
8312
+ preserveFallbackMultiplicity: shouldPreserveFallbackMultiplicity(formatFeature.id)
8313
+ });
8255
8314
  logger?.info("Starting streamParseFile...");
8256
8315
  const parsePipelineStartedAt = now();
8257
8316
  let parserTimingFinished = false;
@@ -8591,7 +8650,9 @@ async function analyzeNewImport(filePath, onProgress, options2) {
8591
8650
  }
8592
8651
  let meta = null;
8593
8652
  const memberSet = /* @__PURE__ */ new Set();
8594
- const dedupState = createMessageDedupState();
8653
+ const dedupState = createMessageDedupState([], [], [], {
8654
+ preserveFallbackMultiplicity: shouldPreserveFallbackMultiplicity(formatFeature.id)
8655
+ });
8595
8656
  let totalMessages = 0;
8596
8657
  let newMessageCount = 0;
8597
8658
  let duplicateCount = 0;
@@ -8816,8 +8877,9 @@ function createExistingMessageLookup(db) {
8816
8877
  db,
8817
8878
  maxMessageId,
8818
8879
  hasFallbackOnlyMessages,
8819
- consumedFallbackOnlyKeys: /* @__PURE__ */ new Set(),
8880
+ consumedFallbackOnlyOccurrenceCounts: /* @__PURE__ */ new Map(),
8820
8881
  bridgedPlatformMessageIds: /* @__PURE__ */ new Set(),
8882
+ consumedFallbackOccurrenceCounts: /* @__PURE__ */ new Map(),
8821
8883
  candidateStatement: db.prepare(
8822
8884
  `SELECT msg.type, msg.content, msg.reply_to_message_id, msg.platform_message_id
8823
8885
  FROM member m
@@ -8868,28 +8930,36 @@ function candidateFallbackKey(message, candidate) {
8868
8930
  replyToMessageId: candidate.reply_to_message_id ?? void 0
8869
8931
  });
8870
8932
  }
8871
- function isExistingMessageDuplicate(lookup, message, existingBatchPlatformIds, candidateCache) {
8933
+ function isExistingMessageDuplicate(lookup, message, existingBatchPlatformIds, candidateCache, preserveFallbackMultiplicity) {
8872
8934
  if (message.platformMessageId && (existingBatchPlatformIds.has(message.platformMessageId) || lookup.bridgedPlatformMessageIds.has(message.platformMessageId))) {
8873
8935
  return true;
8874
8936
  }
8875
8937
  const fallbackKey = generateFallbackMessageKey(message);
8876
8938
  if (message.platformMessageId) {
8877
- if (!lookup.hasFallbackOnlyMessages || lookup.consumedFallbackOnlyKeys.has(fallbackKey)) return false;
8878
- const matchesFallbackOnly = getExistingCandidates(lookup, message, candidateCache).some(
8939
+ if (!lookup.hasFallbackOnlyMessages) return false;
8940
+ const matchingFallbackOnlyCount = getExistingCandidates(lookup, message, candidateCache).filter(
8879
8941
  (candidate) => !candidate.platform_message_id && candidateFallbackKey(message, candidate) === fallbackKey
8880
- );
8942
+ ).length;
8943
+ const consumedCount2 = lookup.consumedFallbackOnlyOccurrenceCounts.get(fallbackKey) ?? 0;
8944
+ const matchesFallbackOnly = preserveFallbackMultiplicity ? consumedCount2 < matchingFallbackOnlyCount : consumedCount2 === 0 && matchingFallbackOnlyCount > 0;
8881
8945
  if (matchesFallbackOnly) {
8882
- lookup.consumedFallbackOnlyKeys.add(fallbackKey);
8946
+ lookup.consumedFallbackOnlyOccurrenceCounts.set(fallbackKey, consumedCount2 + 1);
8883
8947
  lookup.bridgedPlatformMessageIds.add(message.platformMessageId);
8884
8948
  }
8885
8949
  return matchesFallbackOnly;
8886
8950
  }
8887
- return getExistingCandidates(lookup, message, candidateCache).some(
8951
+ const matchingCount = getExistingCandidates(lookup, message, candidateCache).filter(
8888
8952
  (candidate) => candidateFallbackKey(message, candidate) === fallbackKey
8889
- );
8953
+ ).length;
8954
+ if (!preserveFallbackMultiplicity) return matchingCount > 0;
8955
+ const consumedCount = lookup.consumedFallbackOccurrenceCounts.get(fallbackKey) ?? 0;
8956
+ if (consumedCount >= matchingCount) return false;
8957
+ lookup.consumedFallbackOccurrenceCounts.set(fallbackKey, consumedCount + 1);
8958
+ return true;
8890
8959
  }
8891
- function isDuplicate(lookup, incomingState, message, existingBatchPlatformIds, candidateCache) {
8892
- if (isExistingMessageDuplicate(lookup, message, existingBatchPlatformIds, candidateCache)) return true;
8960
+ function isDuplicate(lookup, incomingState, message, existingBatchPlatformIds, candidateCache, preserveFallbackMultiplicity) {
8961
+ if (isExistingMessageDuplicate(lookup, message, existingBatchPlatformIds, candidateCache, preserveFallbackMultiplicity))
8962
+ return true;
8893
8963
  return registerMessageAndCheckDuplicate(message, incomingState);
8894
8964
  }
8895
8965
  function normalizeImportTimestamp(timestamp) {
@@ -8912,9 +8982,10 @@ async function analyzeIncrementalImport(sessionId, filePath, deps, options2) {
8912
8982
  let duplicateCount = 0;
8913
8983
  let platform = formatFeature.platform;
8914
8984
  const mapPlatformId = createPlatformIdMapper(options2?.senderPlatformIdMappings);
8985
+ const preserveFallbackMultiplicity = shouldPreserveFallbackMultiplicity(formatFeature.id);
8915
8986
  try {
8916
8987
  const existingLookup = createExistingMessageLookup(db);
8917
- const incomingState = createMessageDedupState();
8988
+ const incomingState = createMessageDedupState([], [], [], { preserveFallbackMultiplicity });
8918
8989
  await streamParseFile(
8919
8990
  filePath,
8920
8991
  {
@@ -8946,7 +9017,8 @@ async function analyzeIncrementalImport(sessionId, filePath, deps, options2) {
8946
9017
  incomingState,
8947
9018
  { ...msg, timestamp },
8948
9019
  existingBatchPlatformIds,
8949
- candidateCache
9020
+ candidateCache,
9021
+ preserveFallbackMultiplicity
8950
9022
  )) {
8951
9023
  duplicateCount++;
8952
9024
  } else {
@@ -8976,6 +9048,7 @@ async function incrementalImport(sessionId, filePath, deps, options2) {
8976
9048
  const metaUpdateMode = options2?.metaUpdateMode ?? "patch";
8977
9049
  const memberUpdateMode = options2?.memberUpdateMode ?? "upsert";
8978
9050
  const mapPlatformId = createPlatformIdMapper(options2?.senderPlatformIdMappings);
9051
+ const preserveFallbackMultiplicity = shouldPreserveFallbackMultiplicity(formatFeature.id);
8979
9052
  try {
8980
9053
  let trackError2 = function(index, reason, detail) {
8981
9054
  errorCount++;
@@ -8986,7 +9059,7 @@ async function incrementalImport(sessionId, filePath, deps, options2) {
8986
9059
  };
8987
9060
  var trackError = trackError2;
8988
9061
  const existingLookup = createExistingMessageLookup(db);
8989
- const incomingState = createMessageDedupState();
9062
+ const incomingState = createMessageDedupState([], [], [], { preserveFallbackMultiplicity });
8990
9063
  const memberIdMap = /* @__PURE__ */ new Map();
8991
9064
  const existingMembers = db.prepare("SELECT id, platform_id FROM member").all();
8992
9065
  for (const m of existingMembers) {
@@ -9105,7 +9178,8 @@ async function incrementalImport(sessionId, filePath, deps, options2) {
9105
9178
  incomingState,
9106
9179
  { ...msg, timestamp },
9107
9180
  existingBatchPlatformIds,
9108
- candidateCache
9181
+ candidateCache,
9182
+ preserveFallbackMultiplicity
9109
9183
  )) {
9110
9184
  duplicateCount++;
9111
9185
  continue;
@@ -11693,7 +11767,8 @@ function buildContactsSignature(adapter, timeRangePreset) {
11693
11767
  `algorithm:${CONTACTS_ALGORITHM_VERSION}`,
11694
11768
  `range:${normalizeContactsTimeRangePreset(timeRangePreset)}`
11695
11769
  ];
11696
- for (const sessionId of [...adapter.listSessionIds()].sort()) {
11770
+ const sessionIds = adapter.listSessionCandidateIds?.() ?? adapter.listSessionIds();
11771
+ for (const sessionId of [...sessionIds].sort()) {
11697
11772
  const dbPath = adapter.getDbPath(sessionId);
11698
11773
  parts.push(`${sessionId}:${getDbFileVersion(dbPath)}`);
11699
11774
  }
@@ -11941,6 +12016,7 @@ var DefaultContactsService = class {
11941
12016
  }
11942
12017
  ensureTaskStarted(signature, timeRangePreset) {
11943
12018
  if (this.inFlight) return;
12019
+ const sessionIds = this.deps.adapter.listSessionCandidateIds?.() ?? this.deps.adapter.listSessionIds();
11944
12020
  const taskId = `contacts_${this.now()}_${Math.random().toString(36).slice(2)}`;
11945
12021
  this.task = {
11946
12022
  id: taskId,
@@ -11948,7 +12024,7 @@ var DefaultContactsService = class {
11948
12024
  startedAt: this.now(),
11949
12025
  finishedAt: null,
11950
12026
  processedSessions: 0,
11951
- totalSessions: this.deps.adapter.listSessionIds().length,
12027
+ totalSessions: sessionIds.length,
11952
12028
  timeRangePreset
11953
12029
  };
11954
12030
  const abortController = new AbortController();
@@ -12220,6 +12296,1821 @@ __export(contacts_exports, {
12220
12296
  getContactsFactsCacheDir: () => getContactsFactsCacheDir
12221
12297
  });
12222
12298
 
12299
+ // ../../packages/node-runtime/src/services/cross-chat-analysis/service.ts
12300
+ import { createHash as createHash2 } from "crypto";
12301
+ var DEFAULT_MAX_SESSIONS = 24;
12302
+ var MAX_MAX_SESSIONS = 100;
12303
+ var DEFAULT_MAX_EVIDENCE = 1e3;
12304
+ var MAX_MAX_EVIDENCE = 1e3;
12305
+ var DEFAULT_MAX_WALL_TIME_MS = 8e3;
12306
+ var MAX_MAX_WALL_TIME_MS = 3e4;
12307
+ var DEFAULT_CONTEXT_SIZE = 10;
12308
+ var MAX_CONTEXT_SIZE = 50;
12309
+ var RECENT_SESSION_MESSAGE_LIMIT = 200;
12310
+ var RECENT_SESSION_SUMMARY_LIMIT = 5;
12311
+ var SEARCH_CONTEXT_BEFORE = 2;
12312
+ var SEARCH_CONTEXT_AFTER = 2;
12313
+ var MAX_RECENT_DAYS = 3650;
12314
+ var CONTACT_SESSIONS_ALGORITHM_VERSION = "contact-sessions-v1";
12315
+ var DEFAULT_INSPECTION_PAGE_SIZE = 50;
12316
+ var MAX_INSPECTION_PAGE_SIZE = 100;
12317
+ var SHARED_INTERACTIONS_ALGORITHM_VERSION = "shared-interactions-v1";
12318
+ var PROXIMITY_ALGORITHM_VERSION = "lookahead-3-decay-120-gap-1800-v2";
12319
+ var DEFAULT_SHARED_INTERACTIONS_PAGE_SIZE = 20;
12320
+ var MAX_SHARED_INTERACTIONS_PAGE_SIZE = 50;
12321
+ var DEFAULT_MAX_ANCHORS_PER_PAIR = 4;
12322
+ var MAX_MAX_ANCHORS_PER_PAIR = 8;
12323
+ var PRIVATE_CONTACTS_RANKING_ALGORITHM_VERSION = "private-contacts-ranking-v1";
12324
+ var GROUP_SESSIONS_RANKING_ALGORITHM_VERSION = "group-sessions-ranking-v1";
12325
+ var DEFAULT_RANKING_LIMIT = 10;
12326
+ var MAX_RANKING_LIMIT = 50;
12327
+ var RANKING_MAX_WALL_TIME_MS = 3e4;
12328
+ var OVERVIEW_TOP_MEMBERS_LIMIT = 5;
12329
+ function createCrossChatAnalysisService(deps) {
12330
+ return new DefaultCrossChatAnalysisService(deps);
12331
+ }
12332
+ var DefaultCrossChatAnalysisService = class {
12333
+ constructor(deps) {
12334
+ this.deps = deps;
12335
+ }
12336
+ deps;
12337
+ lookupContact(rawQuery) {
12338
+ const query = rawQuery.trim();
12339
+ const response = this.deps.contactsService.getContactsPage({
12340
+ acceptStale: true,
12341
+ page: 1,
12342
+ pageSize: 200,
12343
+ query,
12344
+ timeRangePreset: "all"
12345
+ });
12346
+ const normalizedQuery = normalizeContactName(query);
12347
+ const exactMatches = response.contacts.filter(
12348
+ (contact) => [contact.displayName, ...contact.aliases].some((name) => normalizeContactName(name) === normalizedQuery)
12349
+ );
12350
+ const matchedContacts = exactMatches.length > 0 ? exactMatches : response.contacts;
12351
+ const totalCandidates = exactMatches.length > 0 ? exactMatches.length : response.pagination.total;
12352
+ const candidates = matchedContacts.slice(0, 8).map((contact) => {
12353
+ const detail = this.deps.contactsService.getContactDetail(contact.key, {
12354
+ acceptStale: true,
12355
+ timeRangePreset: "all"
12356
+ });
12357
+ return {
12358
+ contactKey: contact.key,
12359
+ displayName: contact.displayName,
12360
+ platform: contact.platform,
12361
+ aliases: contact.aliases,
12362
+ sourceSessions: detail.contact?.sourceSessions.map((session) => ({
12363
+ id: session.id,
12364
+ name: session.name,
12365
+ type: session.type
12366
+ })) ?? []
12367
+ };
12368
+ });
12369
+ const status = totalCandidates === 1 ? "resolved" : totalCandidates > 1 ? "ambiguous" : response.cache.status === "missing" || response.task?.status === "running" ? "unavailable" : "not_found";
12370
+ return {
12371
+ query,
12372
+ status,
12373
+ cacheStatus: response.cache.status,
12374
+ totalCandidates,
12375
+ candidates
12376
+ };
12377
+ }
12378
+ async resolveEntities(refs, options2 = {}) {
12379
+ throwIfAborted(options2.signal);
12380
+ const contacts = [];
12381
+ const sessions = [];
12382
+ const unresolved = [];
12383
+ const candidateSessionIds = /* @__PURE__ */ new Set();
12384
+ const resolvedSessionIds = /* @__PURE__ */ new Set();
12385
+ const failedSessionIds = /* @__PURE__ */ new Set();
12386
+ for (const ref of refs) {
12387
+ throwIfAborted(options2.signal);
12388
+ if (ref.type === "session") {
12389
+ candidateSessionIds.add(ref.sessionId);
12390
+ const descriptor = this.tryGetSessionDescriptor(ref.sessionId);
12391
+ if (descriptor) {
12392
+ resolvedSessionIds.add(ref.sessionId);
12393
+ sessions.push({ ref, status: "resolved", session: descriptor });
12394
+ } else {
12395
+ failedSessionIds.add(ref.sessionId);
12396
+ sessions.push({ ref, status: "unresolved" });
12397
+ unresolved.push({ ref, reason: "session_not_found" });
12398
+ }
12399
+ await yieldToEventLoop2();
12400
+ continue;
12401
+ }
12402
+ const detail = this.deps.contactsService.getContactDetail(ref.contactKey, {
12403
+ acceptStale: true,
12404
+ timeRangePreset: "all"
12405
+ });
12406
+ const contact = detail.contact;
12407
+ if (!contact) {
12408
+ contacts.push({
12409
+ ref,
12410
+ status: "unresolved",
12411
+ cacheStatus: detail.cache.status,
12412
+ sessions: [],
12413
+ unresolvedSessionIds: [],
12414
+ failedSessionIds: []
12415
+ });
12416
+ unresolved.push({
12417
+ ref,
12418
+ reason: detail.cache.status === "missing" ? "contact_snapshot_missing" : "contact_not_found"
12419
+ });
12420
+ await yieldToEventLoop2();
12421
+ continue;
12422
+ }
12423
+ const sourceSessions = contact.sessionScoped ? contact.sourceSessions.filter((source) => source.id === contact.sessionId) : contact.sourceSessions;
12424
+ const resolvedContactSessions = [];
12425
+ const unresolvedContactSessions = [];
12426
+ const failedContactSessions = [];
12427
+ for (const source of sourceSessions) {
12428
+ throwIfAborted(options2.signal);
12429
+ candidateSessionIds.add(source.id);
12430
+ try {
12431
+ const db = this.deps.adapter.openReadonly(source.id);
12432
+ if (!db) {
12433
+ failedContactSessions.push(source.id);
12434
+ failedSessionIds.add(source.id);
12435
+ continue;
12436
+ }
12437
+ const descriptor = getSessionDescriptor(source.id, db);
12438
+ const member = resolveContactMember(db, contact.platformId);
12439
+ if (!descriptor || !member) {
12440
+ unresolvedContactSessions.push(source.id);
12441
+ continue;
12442
+ }
12443
+ resolvedContactSessions.push({
12444
+ ...descriptor,
12445
+ memberId: member.id,
12446
+ memberPlatformId: member.platformId,
12447
+ memberName: member.name
12448
+ });
12449
+ resolvedSessionIds.add(source.id);
12450
+ } catch (error) {
12451
+ failedContactSessions.push(source.id);
12452
+ failedSessionIds.add(source.id);
12453
+ appLogger.warn("cross-chat-analysis", `failed to resolve contact in source session: ${source.id}`, error);
12454
+ } finally {
12455
+ await yieldToEventLoop2();
12456
+ }
12457
+ }
12458
+ const status = resolvedContactSessions.length === 0 ? "unresolved" : unresolvedContactSessions.length > 0 || failedContactSessions.length > 0 ? "partial" : "resolved";
12459
+ contacts.push({
12460
+ ref,
12461
+ status,
12462
+ cacheStatus: detail.cache.status,
12463
+ sessions: resolvedContactSessions,
12464
+ unresolvedSessionIds: unresolvedContactSessions,
12465
+ failedSessionIds: failedContactSessions
12466
+ });
12467
+ if (status === "unresolved") unresolved.push({ ref, reason: "member_not_found" });
12468
+ if (sourceSessions.length === 0) await yieldToEventLoop2();
12469
+ }
12470
+ throwIfAborted(options2.signal);
12471
+ return {
12472
+ contacts,
12473
+ sessions,
12474
+ unresolved,
12475
+ coverage: {
12476
+ requestedEntities: refs.length,
12477
+ resolvedEntities: contacts.filter((item) => item.status !== "unresolved").length + sessions.filter((item) => item.status === "resolved").length,
12478
+ candidateSessions: candidateSessionIds.size,
12479
+ resolvedSessions: resolvedSessionIds.size,
12480
+ failedSessions: failedSessionIds.size
12481
+ }
12482
+ };
12483
+ }
12484
+ async inspectContactSessions(request, options2 = {}) {
12485
+ throwIfAborted(options2.signal);
12486
+ const contactKey = request.contactKey.trim();
12487
+ if (!contactKey) throw new Error("contactKey is required");
12488
+ const rangeRequest = normalizeInspectionRangeRequest(request.startTs, request.endTs, request.recentDays);
12489
+ const requestedRange = resolveInspectionRange(rangeRequest, () => this.now());
12490
+ const includeRosterOnly = request.includeRosterOnly !== false;
12491
+ const pageSize = clampInteger(request.pageSize, DEFAULT_INSPECTION_PAGE_SIZE, 1, MAX_INSPECTION_PAGE_SIZE);
12492
+ const maxWallTimeMs = clampInteger(request.maxWallTimeMs, DEFAULT_MAX_WALL_TIME_MS, 1, MAX_MAX_WALL_TIME_MS);
12493
+ const detail = this.deps.contactsService.getContactDetail(contactKey, {
12494
+ acceptStale: true,
12495
+ timeRangePreset: "all"
12496
+ });
12497
+ const contact = detail.contact;
12498
+ if (!contact) {
12499
+ return emptyContactSessionsResult(detail.cache.status, requestedRange);
12500
+ }
12501
+ const startedAt = this.now();
12502
+ const candidateSessionIds = contact.sessionScoped ? contact.sessionId ? [contact.sessionId] : [] : [...new Set(this.deps.adapter.listSessionCandidateIds?.() ?? this.deps.adapter.listSessionIds())].sort(
12503
+ (left, right) => left.localeCompare(right)
12504
+ );
12505
+ const cursor = parseInspectionCursor(
12506
+ request.cursor,
12507
+ (range2) => createInspectionFingerprint({
12508
+ candidateSessionIds,
12509
+ contactKey,
12510
+ rangeRequest,
12511
+ startTs: range2.startTs,
12512
+ endTs: range2.endTs,
12513
+ includeRosterOnly
12514
+ }),
12515
+ candidateSessionIds,
12516
+ requestedRange
12517
+ );
12518
+ const range = cursor.range;
12519
+ const sessions = [];
12520
+ const failedSessionIds = [];
12521
+ const truncatedReasons = /* @__PURE__ */ new Set();
12522
+ let scannedSessions = 0;
12523
+ let nextCandidateIndex = cursor.nextCandidateIndex;
12524
+ let dataEarliestMessageTs = null;
12525
+ let dataLatestMessageTs = null;
12526
+ options2.onProgress?.({ processedSessions: 0, totalSessions: candidateSessionIds.length });
12527
+ while (nextCandidateIndex < candidateSessionIds.length && sessions.length < pageSize) {
12528
+ throwIfAborted(options2.signal);
12529
+ if (this.now() - startedAt >= maxWallTimeMs) {
12530
+ truncatedReasons.add("time_budget");
12531
+ break;
12532
+ }
12533
+ const sessionId = candidateSessionIds[nextCandidateIndex];
12534
+ options2.onProgress?.({
12535
+ processedSessions: nextCandidateIndex,
12536
+ totalSessions: candidateSessionIds.length,
12537
+ currentSessionId: sessionId
12538
+ });
12539
+ try {
12540
+ const db = this.deps.adapter.openReadonly(sessionId);
12541
+ if (!db) {
12542
+ failedSessionIds.push(sessionId);
12543
+ } else {
12544
+ const descriptor = getSessionDescriptor(sessionId, db);
12545
+ const meta = getSessionMeta(db);
12546
+ if (descriptor && meta?.platform === contact.platform) {
12547
+ const member = getNonSystemMembersForContacts(db).find((item) => item.platformId === contact.platformId);
12548
+ if (member) {
12549
+ const facts = getParticipantSessionFacts(db, member.id, range);
12550
+ if (includeRosterOnly || facts.ownMessageCount > 0) {
12551
+ sessions.push({
12552
+ ...descriptor,
12553
+ memberId: member.id,
12554
+ memberName: member.name,
12555
+ presence: facts.ownMessageCount > 0 ? "spoke" : "roster_only",
12556
+ presenceObservedInRange: facts.ownMessageCount > 0,
12557
+ ownMessageCount: facts.ownMessageCount,
12558
+ sessionMessageCount: facts.sessionMessageCount,
12559
+ messageShare: facts.sessionMessageCount > 0 ? facts.ownMessageCount / facts.sessionMessageCount : null,
12560
+ firstOwnMessageTs: facts.firstOwnMessageTs,
12561
+ lastOwnMessageTs: facts.lastOwnMessageTs,
12562
+ activeDays: facts.activeDays,
12563
+ memberCount: descriptor.sessionType === "group" /* GROUP */ ? facts.memberCount : null,
12564
+ sessionFirstMessageTs: facts.sessionFirstMessageTs,
12565
+ lastMessageTs: facts.sessionLastMessageTs
12566
+ });
12567
+ dataEarliestMessageTs = minNullable(dataEarliestMessageTs, facts.sessionFirstMessageTs);
12568
+ dataLatestMessageTs = maxNullable(dataLatestMessageTs, facts.sessionLastMessageTs);
12569
+ }
12570
+ }
12571
+ }
12572
+ }
12573
+ } catch (error) {
12574
+ failedSessionIds.push(sessionId);
12575
+ appLogger.warn("cross-chat-analysis", `failed to inspect contact session: ${sessionId}`, error);
12576
+ } finally {
12577
+ scannedSessions++;
12578
+ nextCandidateIndex++;
12579
+ }
12580
+ await yieldToEventLoop2();
12581
+ }
12582
+ if (sessions.length >= pageSize && nextCandidateIndex < candidateSessionIds.length) {
12583
+ truncatedReasons.add("page_size");
12584
+ }
12585
+ const complete = nextCandidateIndex >= candidateSessionIds.length;
12586
+ const nextCursor = complete ? null : createInspectionCursor(cursor.fingerprint, candidateSessionIds[nextCandidateIndex - 1] ?? null, range);
12587
+ options2.onProgress?.({ processedSessions: nextCandidateIndex, totalSessions: candidateSessionIds.length });
12588
+ return {
12589
+ algorithmVersion: CONTACT_SESSIONS_ALGORITHM_VERSION,
12590
+ contact: {
12591
+ contactKey: contact.key,
12592
+ displayName: contact.displayName,
12593
+ platform: contact.platform,
12594
+ sessionScoped: contact.sessionScoped
12595
+ },
12596
+ appliedRange: {
12597
+ ...range,
12598
+ dataEarliestMessageTs,
12599
+ dataLatestMessageTs
12600
+ },
12601
+ summary: summarizeContactSessions(sessions, !request.cursor && complete),
12602
+ sessions,
12603
+ coverage: {
12604
+ candidateSessions: candidateSessionIds.length,
12605
+ scannedSessions,
12606
+ matchedSessions: sessions.length,
12607
+ returnedSessions: sessions.length,
12608
+ failedSessions: failedSessionIds.length,
12609
+ failedSessionIds,
12610
+ complete,
12611
+ nextCursor,
12612
+ truncated: truncatedReasons.size > 0,
12613
+ truncatedReasons: [...truncatedReasons],
12614
+ contactCacheStatus: detail.cache.status
12615
+ }
12616
+ };
12617
+ }
12618
+ async inspectSharedInteractions(request, options2 = {}) {
12619
+ throwIfAborted(options2.signal);
12620
+ const participantRefs = normalizeParticipantRefs(request.participants);
12621
+ const rangeRequest = normalizeInspectionRangeRequest(request.startTs, request.endTs, request.recentDays);
12622
+ const requestedRange = resolveInspectionRange(rangeRequest, () => this.now());
12623
+ const pageSize = clampInteger(
12624
+ request.pageSize,
12625
+ DEFAULT_SHARED_INTERACTIONS_PAGE_SIZE,
12626
+ 1,
12627
+ MAX_SHARED_INTERACTIONS_PAGE_SIZE
12628
+ );
12629
+ const maxAnchorsPerPair = clampInteger(
12630
+ request.maxAnchorsPerPair,
12631
+ DEFAULT_MAX_ANCHORS_PER_PAIR,
12632
+ 0,
12633
+ MAX_MAX_ANCHORS_PER_PAIR
12634
+ );
12635
+ const maxWallTimeMs = clampInteger(request.maxWallTimeMs, DEFAULT_MAX_WALL_TIME_MS, 1, MAX_MAX_WALL_TIME_MS);
12636
+ const startedAt = this.now();
12637
+ const resolvedParticipants = participantRefs.map((ref, index) => {
12638
+ if (ref.type === "owner") {
12639
+ return {
12640
+ index,
12641
+ ref,
12642
+ status: "resolved",
12643
+ displayName: "owner",
12644
+ platform: void 0,
12645
+ cacheStatus: void 0,
12646
+ contact: void 0
12647
+ };
12648
+ }
12649
+ const detail = this.deps.contactsService.getContactDetail(ref.contactKey, {
12650
+ acceptStale: true,
12651
+ timeRangePreset: "all"
12652
+ });
12653
+ return {
12654
+ index,
12655
+ ref,
12656
+ status: detail.contact ? "resolved" : "unresolved",
12657
+ displayName: detail.contact?.displayName ?? ref.contactKey,
12658
+ platform: detail.contact?.platform,
12659
+ cacheStatus: detail.cache.status,
12660
+ contact: detail.contact
12661
+ };
12662
+ });
12663
+ const unresolvedParticipantIndexes = resolvedParticipants.filter((participant) => participant.status === "unresolved").map((participant) => participant.index);
12664
+ const publicParticipants = resolvedParticipants.map(({ contact: _contact, ...participant }) => participant);
12665
+ if (unresolvedParticipantIndexes.length > 0) {
12666
+ return emptySharedInteractionsResult(publicParticipants, unresolvedParticipantIndexes, requestedRange);
12667
+ }
12668
+ throwIfAborted(options2.signal);
12669
+ const sessionScopedIds = resolvedParticipants.flatMap(
12670
+ (participant) => participant.ref.type === "contact" && participant.contact?.sessionScoped && participant.contact.sessionId ? [participant.contact.sessionId] : []
12671
+ ).filter((sessionId, index, all) => all.indexOf(sessionId) === index);
12672
+ const candidateSessionIds = (sessionScopedIds.length > 1 ? [] : sessionScopedIds.length === 1 ? sessionScopedIds : [...new Set(this.deps.adapter.listSessionCandidateIds?.() ?? this.deps.adapter.listSessionIds())]).sort((left, right) => left.localeCompare(right));
12673
+ throwIfAborted(options2.signal);
12674
+ const cursor = parseInspectionCursor(
12675
+ request.cursor,
12676
+ (range2) => createInspectionFingerprint({
12677
+ candidateSessionIds,
12678
+ participantRefs,
12679
+ rangeRequest,
12680
+ startTs: range2.startTs,
12681
+ endTs: range2.endTs,
12682
+ maxAnchorsPerPair
12683
+ }),
12684
+ candidateSessionIds,
12685
+ requestedRange
12686
+ );
12687
+ const range = cursor.range;
12688
+ const sessions = [];
12689
+ const failedSessionIds = [];
12690
+ const truncatedReasons = /* @__PURE__ */ new Set();
12691
+ const ownerResolution = participantRefs.some((ref) => ref.type === "owner") ? { resolvedSessions: 0, missingOwnerSessions: 0, unresolvedOwnerSessions: 0 } : void 0;
12692
+ let identityCollisionSessions = 0;
12693
+ let scannedSessions = 0;
12694
+ let nextCandidateIndex = cursor.nextCandidateIndex;
12695
+ let dataEarliestMessageTs = null;
12696
+ let dataLatestMessageTs = null;
12697
+ options2.onProgress?.({ processedSessions: 0, totalSessions: candidateSessionIds.length });
12698
+ while (nextCandidateIndex < candidateSessionIds.length && sessions.length < pageSize) {
12699
+ throwIfAborted(options2.signal);
12700
+ if (this.now() - startedAt >= maxWallTimeMs) {
12701
+ truncatedReasons.add("time_budget");
12702
+ break;
12703
+ }
12704
+ const sessionId = candidateSessionIds[nextCandidateIndex];
12705
+ options2.onProgress?.({
12706
+ processedSessions: nextCandidateIndex,
12707
+ totalSessions: candidateSessionIds.length,
12708
+ currentSessionId: sessionId
12709
+ });
12710
+ try {
12711
+ const db = this.deps.adapter.openReadonly(sessionId);
12712
+ if (!db) {
12713
+ failedSessionIds.push(sessionId);
12714
+ } else {
12715
+ const descriptor = getSessionDescriptor(sessionId, db);
12716
+ const meta = getSessionMeta(db);
12717
+ if (descriptor && meta) {
12718
+ const members = getNonSystemMembersForContacts(db);
12719
+ const participantMembers = resolvedParticipants.map((participant) => {
12720
+ if (participant.ref.type === "owner") {
12721
+ if (!meta.ownerId?.trim()) {
12722
+ if (ownerResolution) ownerResolution.missingOwnerSessions++;
12723
+ return null;
12724
+ }
12725
+ const owner = members.find((member) => member.platformId === meta.ownerId);
12726
+ if (!owner) {
12727
+ if (ownerResolution) ownerResolution.unresolvedOwnerSessions++;
12728
+ return null;
12729
+ }
12730
+ if (ownerResolution) ownerResolution.resolvedSessions++;
12731
+ return owner;
12732
+ }
12733
+ if (!participant.contact || participant.contact.platform !== meta.platform) return null;
12734
+ return members.find((member) => member.platformId === participant.contact?.platformId) ?? null;
12735
+ });
12736
+ if (participantMembers.every((member) => member !== null)) {
12737
+ const resolvedMembers = participantMembers.filter((member) => member !== null);
12738
+ if (new Set(resolvedMembers.map((member) => member.id)).size !== resolvedMembers.length) {
12739
+ identityCollisionSessions++;
12740
+ } else {
12741
+ const facts = getParticipantSetInteractionFacts(
12742
+ db,
12743
+ resolvedMembers.map((member) => member.id),
12744
+ {
12745
+ ...range,
12746
+ maxAnchorsPerPair
12747
+ }
12748
+ );
12749
+ const participantIndexByMemberId = new Map(
12750
+ resolvedMembers.map((member, index) => [member.id, resolvedParticipants[index].index])
12751
+ );
12752
+ const item = mapSharedInteractionSession(descriptor, resolvedMembers, participantIndexByMemberId, facts);
12753
+ sessions.push(item);
12754
+ dataEarliestMessageTs = minNullable(dataEarliestMessageTs, facts.sessionFirstMessageTs);
12755
+ dataLatestMessageTs = maxNullable(dataLatestMessageTs, facts.sessionLastMessageTs);
12756
+ if (facts.proximityStatus !== "complete") truncatedReasons.add("message_budget");
12757
+ }
12758
+ }
12759
+ }
12760
+ }
12761
+ } catch (error) {
12762
+ failedSessionIds.push(sessionId);
12763
+ appLogger.warn("cross-chat-analysis", `failed to inspect shared interactions: ${sessionId}`, error);
12764
+ } finally {
12765
+ scannedSessions++;
12766
+ nextCandidateIndex++;
12767
+ }
12768
+ await yieldToEventLoop2();
12769
+ }
12770
+ if (sessions.length >= pageSize && nextCandidateIndex < candidateSessionIds.length) {
12771
+ truncatedReasons.add("page_size");
12772
+ }
12773
+ sessions.sort(compareSharedInteractionSessions);
12774
+ const complete = nextCandidateIndex >= candidateSessionIds.length;
12775
+ const nextCursor = complete ? null : createInspectionCursor(cursor.fingerprint, candidateSessionIds[nextCandidateIndex - 1] ?? null, range);
12776
+ options2.onProgress?.({ processedSessions: nextCandidateIndex, totalSessions: candidateSessionIds.length });
12777
+ return {
12778
+ algorithmVersion: SHARED_INTERACTIONS_ALGORITHM_VERSION,
12779
+ proximityAlgorithmVersion: PROXIMITY_ALGORITHM_VERSION,
12780
+ participants: publicParticipants,
12781
+ appliedRange: {
12782
+ ...range,
12783
+ dataEarliestMessageTs,
12784
+ dataLatestMessageTs
12785
+ },
12786
+ summary: summarizeSharedInteractions(sessions, !request.cursor && complete),
12787
+ sessions,
12788
+ coverage: {
12789
+ candidateSessions: candidateSessionIds.length,
12790
+ scannedSessions,
12791
+ matchedSessions: sessions.length,
12792
+ returnedSessions: sessions.length,
12793
+ failedSessions: failedSessionIds.length,
12794
+ failedSessionIds,
12795
+ complete,
12796
+ nextCursor,
12797
+ truncated: truncatedReasons.size > 0,
12798
+ truncatedReasons: [...truncatedReasons],
12799
+ unresolvedParticipantIndexes,
12800
+ identityCollisionSessions,
12801
+ ownerResolution
12802
+ }
12803
+ };
12804
+ }
12805
+ async searchMessages(request, options2 = {}) {
12806
+ const keywords = request.keywords.map((keyword) => keyword.trim()).filter(Boolean);
12807
+ if (keywords.length === 0 && (!request.scopes || request.scopes.length === 0)) {
12808
+ throw new Error("At least one search keyword is required for an unscoped search");
12809
+ }
12810
+ throwIfAborted(options2.signal);
12811
+ const sender = request.sender === "owner" ? "owner" : "all";
12812
+ const recentDays = normalizeRecentDays(request.recentDays);
12813
+ const effectiveRecentDays = request.startTs === void 0 ? recentDays : void 0;
12814
+ let startTs = request.startTs;
12815
+ let endTs = request.endTs;
12816
+ if (effectiveRecentDays !== void 0) {
12817
+ const range = normalizeInspectionRange(void 0, endTs, effectiveRecentDays, () => this.now());
12818
+ startTs = range.startTs ?? void 0;
12819
+ endTs = range.endTs ?? void 0;
12820
+ }
12821
+ const ownerResolution = sender === "owner" ? { resolvedSessions: 0, missingOwnerSessions: 0, unresolvedOwnerSessions: 0 } : void 0;
12822
+ const maxSessions = clampInteger(request.maxSessions, DEFAULT_MAX_SESSIONS, 1, MAX_MAX_SESSIONS);
12823
+ const maxEvidence = clampInteger(request.maxEvidence, DEFAULT_MAX_EVIDENCE, 1, MAX_MAX_EVIDENCE);
12824
+ const maxWallTimeMs = clampInteger(request.maxWallTimeMs, DEFAULT_MAX_WALL_TIME_MS, 1, MAX_MAX_WALL_TIME_MS);
12825
+ const startedAt = this.now();
12826
+ const failedSessionIds = /* @__PURE__ */ new Set();
12827
+ const { candidates, candidateSessionCount, eligibleCandidateCount, timedOut } = await this.resolveSearchCandidates(
12828
+ request.scopes,
12829
+ failedSessionIds,
12830
+ sender,
12831
+ ownerResolution,
12832
+ maxSessions,
12833
+ startedAt + maxWallTimeMs,
12834
+ options2.signal
12835
+ );
12836
+ const selected = candidates;
12837
+ const truncatedReasons = /* @__PURE__ */ new Set();
12838
+ if (eligibleCandidateCount > selected.length) truncatedReasons.add("session_budget");
12839
+ if (timedOut) truncatedReasons.add("time_budget");
12840
+ const sessionResults = [];
12841
+ let totalMatches = 0;
12842
+ let scannedSessions = 0;
12843
+ let matchedSessions = 0;
12844
+ let processedSessions = 0;
12845
+ options2.onProgress?.({ processedSessions: 0, totalSessions: selected.length });
12846
+ for (const [index, candidate] of selected.entries()) {
12847
+ throwIfAborted(options2.signal);
12848
+ if (this.now() - startedAt >= maxWallTimeMs) {
12849
+ truncatedReasons.add("time_budget");
12850
+ break;
12851
+ }
12852
+ options2.onProgress?.({
12853
+ processedSessions: index,
12854
+ totalSessions: selected.length,
12855
+ currentSessionId: candidate.descriptor.sessionId
12856
+ });
12857
+ throwIfAborted(options2.signal);
12858
+ try {
12859
+ const db = this.deps.adapter.openReadonly(candidate.descriptor.sessionId);
12860
+ if (!db) {
12861
+ failedSessionIds.add(candidate.descriptor.sessionId);
12862
+ continue;
12863
+ }
12864
+ const result = searchMessagesByKeywords(db, keywords, {
12865
+ startTs,
12866
+ endTs,
12867
+ senderIds: candidate.memberIds,
12868
+ matchMode: request.matchMode,
12869
+ sort: request.sort,
12870
+ limit: maxEvidence
12871
+ });
12872
+ scannedSessions++;
12873
+ totalMatches += result.total ?? result.messages.length;
12874
+ if (result.messages.length > 0) matchedSessions++;
12875
+ if ((result.total ?? result.messages.length) > result.messages.length) truncatedReasons.add("evidence_budget");
12876
+ sessionResults.push({
12877
+ candidate,
12878
+ db,
12879
+ totalMatches: result.total ?? result.messages.length,
12880
+ matches: result.messages.map((message) => ({
12881
+ ...toCrossChatMessage(candidate.descriptor, message),
12882
+ evidenceRole: "match"
12883
+ }))
12884
+ });
12885
+ } catch (error) {
12886
+ failedSessionIds.add(candidate.descriptor.sessionId);
12887
+ appLogger.warn("cross-chat-analysis", `failed to search session: ${candidate.descriptor.sessionId}`, error);
12888
+ } finally {
12889
+ processedSessions++;
12890
+ }
12891
+ await yieldToEventLoop2();
12892
+ }
12893
+ sessionResults.sort(compareSessionSearchResults);
12894
+ const evidence = collectSearchEvidence(sessionResults, maxEvidence, startTs, endTs, {
12895
+ deadline: startedAt + maxWallTimeMs,
12896
+ now: () => this.now(),
12897
+ signal: options2.signal
12898
+ });
12899
+ const messages = evidence.messages;
12900
+ if (evidence.timedOut) truncatedReasons.add("time_budget");
12901
+ const returnedMatchCount = messages.filter((message) => message.evidenceRole === "match").length;
12902
+ if (returnedMatchCount < totalMatches) truncatedReasons.add("evidence_budget");
12903
+ const sortMultiplier = request.sort === "asc" ? 1 : -1;
12904
+ messages.sort(
12905
+ (left, right) => (left.timestamp - right.timestamp || left.sessionId.localeCompare(right.sessionId) || left.messageId - right.messageId) * sortMultiplier
12906
+ );
12907
+ options2.onProgress?.({ processedSessions, totalSessions: selected.length });
12908
+ return {
12909
+ messages,
12910
+ totalMatches,
12911
+ appliedFilters: {
12912
+ startTs: startTs ?? null,
12913
+ endTs: endTs ?? null,
12914
+ recentDays: effectiveRecentDays ?? null,
12915
+ sender
12916
+ },
12917
+ coverage: {
12918
+ candidateSessions: candidateSessionCount,
12919
+ scannedSessions,
12920
+ matchedSessions,
12921
+ failedSessions: failedSessionIds.size,
12922
+ ownerResolution,
12923
+ truncated: truncatedReasons.size > 0,
12924
+ truncatedReasons: [...truncatedReasons]
12925
+ }
12926
+ };
12927
+ }
12928
+ readRecentSession(sessionId) {
12929
+ const db = this.deps.adapter.ensureReadonly(sessionId);
12930
+ const descriptor = getSessionDescriptor(sessionId, db);
12931
+ if (!descriptor) throw createNotFoundError(`Session not found: ${sessionId}`);
12932
+ const recent = searchMessagesByKeywords(db, [], {
12933
+ sort: "desc",
12934
+ limit: RECENT_SESSION_MESSAGE_LIMIT
12935
+ });
12936
+ const summaries = getSegmentSummaries(db, { limit: RECENT_SESSION_SUMMARY_LIMIT }).filter((segment) => typeof segment.summary === "string").map((segment) => ({
12937
+ segmentId: segment.id,
12938
+ startTs: segment.startTs,
12939
+ endTs: segment.endTs,
12940
+ messageCount: segment.messageCount,
12941
+ participants: segment.participants,
12942
+ summary: segment.summary
12943
+ }));
12944
+ const messages = recent.messages.map((message) => toCrossChatMessage(descriptor, message));
12945
+ const totalMessages = recent.total ?? messages.length;
12946
+ return {
12947
+ source: descriptor,
12948
+ messages,
12949
+ summaries,
12950
+ coverage: {
12951
+ totalMessages,
12952
+ returnedMessages: messages.length,
12953
+ returnedSummaries: summaries.length,
12954
+ hasEarlierMessages: totalMessages > messages.length
12955
+ }
12956
+ };
12957
+ }
12958
+ async rankPrivateContacts(request, options2 = {}) {
12959
+ throwIfAborted(options2.signal);
12960
+ const range = normalizeInspectionRange(request.startTs, request.endTs, request.recentDays, () => this.now());
12961
+ const rankBy = request.rankBy === "active_days" ? "active_days" : "message_count";
12962
+ const limit = clampInteger(request.limit, DEFAULT_RANKING_LIMIT, 1, MAX_RANKING_LIMIT);
12963
+ const excludedSessionIds = new Set(this.deps.getExcludedSessionIds?.() ?? []);
12964
+ const sessionIds = this.deps.adapter.listSessionCandidateIds?.() ?? this.deps.adapter.listSessionIds();
12965
+ const deadline = this.now() + RANKING_MAX_WALL_TIME_MS;
12966
+ const accumulators = /* @__PURE__ */ new Map();
12967
+ const failedSessionIds = [];
12968
+ const coverage = {
12969
+ candidateSessions: 0,
12970
+ scannedSessions: 0,
12971
+ analyzedSessions: 0,
12972
+ excludedSessions: 0,
12973
+ missingOwnerSessions: 0,
12974
+ unresolvedOwnerSessions: 0,
12975
+ missingContactSessions: 0,
12976
+ ambiguousContactSessions: 0,
12977
+ failedSessions: 0,
12978
+ failedSessionIds,
12979
+ complete: true,
12980
+ truncated: false,
12981
+ truncatedReasons: []
12982
+ };
12983
+ let dataEarliestMessageTs = null;
12984
+ let dataLatestMessageTs = null;
12985
+ let processedSessions = 0;
12986
+ options2.onProgress?.({ processedSessions: 0, totalSessions: sessionIds.length });
12987
+ for (const [index, sessionId] of sessionIds.entries()) {
12988
+ throwIfAborted(options2.signal);
12989
+ if (this.now() >= deadline) {
12990
+ coverage.truncated = true;
12991
+ coverage.truncatedReasons.push("time_budget");
12992
+ break;
12993
+ }
12994
+ options2.onProgress?.({ processedSessions: index, totalSessions: sessionIds.length, currentSessionId: sessionId });
12995
+ try {
12996
+ const db = this.deps.adapter.openReadonly(sessionId);
12997
+ if (!db) {
12998
+ coverage.failedSessions++;
12999
+ failedSessionIds.push(sessionId);
13000
+ continue;
13001
+ }
13002
+ const descriptor = getSessionDescriptor(sessionId, db);
13003
+ if (!descriptor || descriptor.sessionType !== "private" /* PRIVATE */) continue;
13004
+ coverage.candidateSessions++;
13005
+ coverage.scannedSessions++;
13006
+ if (excludedSessionIds.has(sessionId)) {
13007
+ coverage.excludedSessions++;
13008
+ continue;
13009
+ }
13010
+ const facts = getCrossChatSessionActivityFacts(db, range);
13011
+ dataEarliestMessageTs = minNullable(dataEarliestMessageTs, facts.dataEarliestMessageTs);
13012
+ dataLatestMessageTs = maxNullable(dataLatestMessageTs, facts.dataLatestMessageTs);
13013
+ const meta = getSessionMeta(db);
13014
+ if (!meta?.ownerId?.trim()) {
13015
+ coverage.missingOwnerSessions++;
13016
+ continue;
13017
+ }
13018
+ const owner = resolveOwnerMember(db);
13019
+ if (!owner) {
13020
+ coverage.unresolvedOwnerSessions++;
13021
+ continue;
13022
+ }
13023
+ const candidates = getNonSystemMembersForContacts(db).filter((member) => member.id !== owner.id);
13024
+ if (candidates.length === 0) {
13025
+ coverage.missingContactSessions++;
13026
+ continue;
13027
+ }
13028
+ const activeMemberIds = new Set(
13029
+ facts.members.filter((member) => member.messageCount > 0).map((member) => member.memberId)
13030
+ );
13031
+ const activeCandidates = candidates.filter((candidate) => activeMemberIds.has(candidate.id));
13032
+ const resolvedCandidates = activeCandidates.length > 0 ? activeCandidates : candidates;
13033
+ if (resolvedCandidates.length > 1) {
13034
+ coverage.ambiguousContactSessions++;
13035
+ continue;
13036
+ }
13037
+ coverage.analyzedSessions++;
13038
+ if (facts.totalMessages === 0) continue;
13039
+ const contact = resolvedCandidates[0];
13040
+ const contactKey = buildContactKey(
13041
+ descriptor.platform,
13042
+ contact.platformId,
13043
+ shouldScopeContactToSession(descriptor.platform, contact) ? sessionId : void 0
13044
+ );
13045
+ const ownerActivity = facts.members.find((member) => member.memberId === owner.id);
13046
+ const contactActivity = facts.members.find((member) => member.memberId === contact.id);
13047
+ const accumulator = accumulators.get(contactKey) ?? {
13048
+ contactKey,
13049
+ platformId: contact.platformId,
13050
+ displayName: pickPreferredContactDisplayName(contact.platformId, contact.name, descriptor.sessionName),
13051
+ platform: descriptor.platform,
13052
+ totalMessages: 0,
13053
+ ownerMessages: 0,
13054
+ contactMessages: 0,
13055
+ activeDayKeys: /* @__PURE__ */ new Set(),
13056
+ firstMessageTs: null,
13057
+ lastMessageTs: null,
13058
+ sessionIds: []
13059
+ };
13060
+ accumulator.displayName = pickPreferredContactDisplayName(
13061
+ contact.platformId,
13062
+ accumulator.displayName,
13063
+ contact.name,
13064
+ descriptor.sessionName
13065
+ );
13066
+ accumulator.totalMessages += facts.totalMessages;
13067
+ accumulator.ownerMessages += ownerActivity?.messageCount ?? 0;
13068
+ accumulator.contactMessages += contactActivity?.messageCount ?? 0;
13069
+ facts.activeDayKeys.forEach((day) => accumulator.activeDayKeys.add(day));
13070
+ accumulator.firstMessageTs = minNullable(accumulator.firstMessageTs, facts.firstMessageTs);
13071
+ accumulator.lastMessageTs = maxNullable(accumulator.lastMessageTs, facts.lastMessageTs);
13072
+ accumulator.sessionIds.push(sessionId);
13073
+ accumulators.set(contactKey, accumulator);
13074
+ } catch (error) {
13075
+ coverage.failedSessions++;
13076
+ failedSessionIds.push(sessionId);
13077
+ appLogger.warn("cross-chat-analysis", `failed to rank private session: ${sessionId}`, error);
13078
+ } finally {
13079
+ processedSessions = index + 1;
13080
+ await yieldToEventLoop2();
13081
+ }
13082
+ }
13083
+ options2.onProgress?.({ processedSessions, totalSessions: sessionIds.length });
13084
+ coverage.complete = !coverage.truncated && coverage.failedSessions === 0 && coverage.missingOwnerSessions === 0 && coverage.unresolvedOwnerSessions === 0 && coverage.missingContactSessions === 0 && coverage.ambiguousContactSessions === 0;
13085
+ const ranked = [...accumulators.values()].sort(
13086
+ (left, right) => comparePrivateContactRankAccumulators(left, right, rankBy)
13087
+ );
13088
+ const items = ranked.slice(0, limit).map((item, index) => {
13089
+ const contactDetail = this.deps.contactsService.getContactDetail(item.contactKey, {
13090
+ acceptStale: true,
13091
+ timeRangePreset: "all"
13092
+ });
13093
+ return {
13094
+ rank: index + 1,
13095
+ contactKey: item.contactKey,
13096
+ displayName: pickPreferredContactDisplayName(
13097
+ item.platformId,
13098
+ contactDetail.contact?.displayName,
13099
+ item.displayName
13100
+ ),
13101
+ platform: item.platform,
13102
+ totalMessages: item.totalMessages,
13103
+ ownerMessages: item.ownerMessages,
13104
+ contactMessages: item.contactMessages,
13105
+ activeDays: item.activeDayKeys.size,
13106
+ firstMessageTs: item.firstMessageTs,
13107
+ lastMessageTs: item.lastMessageTs,
13108
+ sessionIds: [...item.sessionIds].sort()
13109
+ };
13110
+ });
13111
+ const currentTs = Math.floor(this.now() / 1e3);
13112
+ return {
13113
+ algorithmVersion: PRIVATE_CONTACTS_RANKING_ALGORITHM_VERSION,
13114
+ rankBy,
13115
+ appliedRange: {
13116
+ ...range,
13117
+ dataEarliestMessageTs,
13118
+ dataLatestMessageTs,
13119
+ currentTs
13120
+ },
13121
+ items,
13122
+ coverage
13123
+ };
13124
+ }
13125
+ async rankGroupSessions(request, options2 = {}) {
13126
+ throwIfAborted(options2.signal);
13127
+ if (request.mode !== "owner_activity" && request.mode !== "total_activity") {
13128
+ throw new Error("mode must be owner_activity or total_activity");
13129
+ }
13130
+ const range = normalizeInspectionRange(request.startTs, request.endTs, request.recentDays, () => this.now());
13131
+ const limit = clampInteger(request.limit, DEFAULT_RANKING_LIMIT, 1, MAX_RANKING_LIMIT);
13132
+ const excludedSessionIds = new Set(this.deps.getExcludedSessionIds?.() ?? []);
13133
+ const sessionIds = this.deps.adapter.listSessionCandidateIds?.() ?? this.deps.adapter.listSessionIds();
13134
+ const deadline = this.now() + RANKING_MAX_WALL_TIME_MS;
13135
+ const items = [];
13136
+ const failedSessionIds = [];
13137
+ const coverage = {
13138
+ candidateSessions: 0,
13139
+ scannedSessions: 0,
13140
+ analyzedSessions: 0,
13141
+ excludedSessions: 0,
13142
+ missingOwnerSessions: 0,
13143
+ unresolvedOwnerSessions: 0,
13144
+ failedSessions: 0,
13145
+ failedSessionIds,
13146
+ complete: true,
13147
+ truncated: false,
13148
+ truncatedReasons: []
13149
+ };
13150
+ let dataEarliestMessageTs = null;
13151
+ let dataLatestMessageTs = null;
13152
+ let processedSessions = 0;
13153
+ options2.onProgress?.({ processedSessions: 0, totalSessions: sessionIds.length });
13154
+ for (const [index, sessionId] of sessionIds.entries()) {
13155
+ throwIfAborted(options2.signal);
13156
+ if (this.now() >= deadline) {
13157
+ coverage.truncated = true;
13158
+ coverage.truncatedReasons.push("time_budget");
13159
+ break;
13160
+ }
13161
+ options2.onProgress?.({ processedSessions: index, totalSessions: sessionIds.length, currentSessionId: sessionId });
13162
+ try {
13163
+ const db = this.deps.adapter.openReadonly(sessionId);
13164
+ if (!db) {
13165
+ coverage.failedSessions++;
13166
+ failedSessionIds.push(sessionId);
13167
+ continue;
13168
+ }
13169
+ const descriptor = getSessionDescriptor(sessionId, db);
13170
+ if (!descriptor || descriptor.sessionType !== "group" /* GROUP */) continue;
13171
+ coverage.candidateSessions++;
13172
+ coverage.scannedSessions++;
13173
+ if (excludedSessionIds.has(sessionId)) {
13174
+ coverage.excludedSessions++;
13175
+ continue;
13176
+ }
13177
+ const facts = getCrossChatSessionActivityFacts(db, range);
13178
+ dataEarliestMessageTs = minNullable(dataEarliestMessageTs, facts.dataEarliestMessageTs);
13179
+ dataLatestMessageTs = maxNullable(dataLatestMessageTs, facts.dataLatestMessageTs);
13180
+ const meta = getSessionMeta(db);
13181
+ let ownerStatus = "missing";
13182
+ let ownerMessages = null;
13183
+ let ownerActiveDays = null;
13184
+ if (!meta?.ownerId?.trim()) {
13185
+ coverage.missingOwnerSessions++;
13186
+ } else {
13187
+ const owner = resolveOwnerMember(db);
13188
+ if (!owner) {
13189
+ ownerStatus = "unresolved";
13190
+ coverage.unresolvedOwnerSessions++;
13191
+ } else {
13192
+ ownerStatus = "resolved";
13193
+ const ownerActivity = facts.members.find((member) => member.memberId === owner.id);
13194
+ ownerMessages = ownerActivity?.messageCount ?? 0;
13195
+ ownerActiveDays = ownerActivity?.activeDays ?? 0;
13196
+ }
13197
+ }
13198
+ if (request.mode === "owner_activity" && ownerStatus !== "resolved") continue;
13199
+ coverage.analyzedSessions++;
13200
+ if (facts.totalMessages === 0) continue;
13201
+ items.push({
13202
+ ...descriptor,
13203
+ rank: 0,
13204
+ lastMessageTs: facts.lastMessageTs,
13205
+ totalMessages: facts.totalMessages,
13206
+ ownerMessages,
13207
+ ownerMessageShare: ownerMessages === null || facts.totalMessages === 0 ? null : roundRatio(ownerMessages, facts.totalMessages),
13208
+ ownerActiveDays,
13209
+ activeMembers: facts.activeMembers,
13210
+ activeDays: facts.activeDays,
13211
+ firstMessageTs: facts.firstMessageTs,
13212
+ ownerStatus
13213
+ });
13214
+ } catch (error) {
13215
+ coverage.failedSessions++;
13216
+ failedSessionIds.push(sessionId);
13217
+ appLogger.warn("cross-chat-analysis", `failed to rank group session: ${sessionId}`, error);
13218
+ } finally {
13219
+ processedSessions = index + 1;
13220
+ await yieldToEventLoop2();
13221
+ }
13222
+ }
13223
+ options2.onProgress?.({ processedSessions, totalSessions: sessionIds.length });
13224
+ coverage.complete = !coverage.truncated && coverage.failedSessions === 0 && (request.mode === "total_activity" || coverage.missingOwnerSessions === 0 && coverage.unresolvedOwnerSessions === 0);
13225
+ items.sort((left, right) => compareGroupSessionRankItems(left, right, request.mode));
13226
+ const rankedItems = items.slice(0, limit).map((item, index) => ({ ...item, rank: index + 1 }));
13227
+ return {
13228
+ algorithmVersion: GROUP_SESSIONS_RANKING_ALGORITHM_VERSION,
13229
+ mode: request.mode,
13230
+ appliedRange: {
13231
+ ...range,
13232
+ dataEarliestMessageTs,
13233
+ dataLatestMessageTs,
13234
+ currentTs: Math.floor(this.now() / 1e3)
13235
+ },
13236
+ items: rankedItems,
13237
+ coverage
13238
+ };
13239
+ }
13240
+ getGlobalActivitySummary(request) {
13241
+ const service = this.deps.globalInsightService;
13242
+ if (!service) throw new Error("Global activity summary is unavailable in this runtime");
13243
+ const mode = request.mode ?? "year";
13244
+ if (mode !== "year" && mode !== "recent_365") {
13245
+ throw new Error("mode must be year or recent_365");
13246
+ }
13247
+ if (request.year !== void 0 && (!Number.isInteger(request.year) || request.year < 1970)) {
13248
+ throw new Error("year must be an integer greater than or equal to 1970");
13249
+ }
13250
+ const summary = service.getAnnualSummary(
13251
+ mode === "recent_365" ? { mode: "recent", days: 365, acceptStale: true } : { mode: "year", year: request.year, acceptStale: true }
13252
+ );
13253
+ const dataState = summary.cache.status === "fresh" ? "fresh" : summary.cache.status === "stale" ? "stale" : summary.task.status === "failed" ? "failed" : "preparing";
13254
+ return { mode, dataState, summary };
13255
+ }
13256
+ getMessageContext(request) {
13257
+ const db = this.deps.adapter.ensureReadonly(request.sessionId);
13258
+ const descriptor = getSessionDescriptor(request.sessionId, db);
13259
+ if (!descriptor) throw createNotFoundError(`Session not found: ${request.sessionId}`);
13260
+ const contextSize = clampInteger(request.contextSize, DEFAULT_CONTEXT_SIZE, 0, MAX_CONTEXT_SIZE);
13261
+ const messages = getSearchMessageContext(db, [request.messageId], contextSize, contextSize);
13262
+ if (!messages.some((message) => message.id === request.messageId)) {
13263
+ throw createNotFoundError(`Message not found: ${request.messageId}`);
13264
+ }
13265
+ return {
13266
+ source: descriptor,
13267
+ messages: messages.map((message) => toCrossChatMessage(descriptor, message))
13268
+ };
13269
+ }
13270
+ async getOverview(request, options2 = {}) {
13271
+ throwIfAborted(options2.signal);
13272
+ const scopes = normalizeOverviewScopes(request.scopes);
13273
+ const range = normalizeInspectionRange(request.startTs, request.endTs, request.recentDays, () => this.now());
13274
+ const maxSessions = clampInteger(request.maxSessions, DEFAULT_MAX_SESSIONS, 1, MAX_MAX_SESSIONS);
13275
+ const maxWallTimeMs = clampInteger(request.maxWallTimeMs, DEFAULT_MAX_WALL_TIME_MS, 1, MAX_MAX_WALL_TIME_MS);
13276
+ const selected = scopes.slice(0, maxSessions);
13277
+ const excludedSessionIds = new Set(this.deps.getExcludedSessionIds?.() ?? []);
13278
+ const startedAt = this.now();
13279
+ const items = [];
13280
+ let failedSessions = 0;
13281
+ const failedSessionIds = [];
13282
+ let excludedSessions = 0;
13283
+ let missingOwnerSessions = 0;
13284
+ let unresolvedOwnerSessions = 0;
13285
+ let processedSessions = 0;
13286
+ let dataEarliestMessageTs = null;
13287
+ let dataLatestMessageTs = null;
13288
+ const truncatedReasons = /* @__PURE__ */ new Set();
13289
+ if (selected.length < scopes.length) truncatedReasons.add("session_budget");
13290
+ options2.onProgress?.({ processedSessions: 0, totalSessions: selected.length });
13291
+ for (const [index, scope] of selected.entries()) {
13292
+ throwIfAborted(options2.signal);
13293
+ if (this.now() - startedAt >= maxWallTimeMs) {
13294
+ truncatedReasons.add("time_budget");
13295
+ break;
13296
+ }
13297
+ options2.onProgress?.({
13298
+ processedSessions: index,
13299
+ totalSessions: selected.length,
13300
+ currentSessionId: scope.sessionId
13301
+ });
13302
+ throwIfAborted(options2.signal);
13303
+ try {
13304
+ const db = this.deps.adapter.openReadonly(scope.sessionId);
13305
+ if (!db) {
13306
+ failedSessions++;
13307
+ failedSessionIds.push(scope.sessionId);
13308
+ continue;
13309
+ }
13310
+ const descriptor = getSessionDescriptor(scope.sessionId, db);
13311
+ if (!descriptor) {
13312
+ failedSessions++;
13313
+ failedSessionIds.push(scope.sessionId);
13314
+ continue;
13315
+ }
13316
+ const facts = getCrossChatSessionActivityFacts(db, range);
13317
+ dataEarliestMessageTs = minNullable(dataEarliestMessageTs, facts.dataEarliestMessageTs);
13318
+ dataLatestMessageTs = maxNullable(dataLatestMessageTs, facts.dataLatestMessageTs);
13319
+ let ownerStatus = "missing";
13320
+ let ownerMemberId = null;
13321
+ if (excludedSessionIds.has(scope.sessionId)) {
13322
+ ownerStatus = "excluded";
13323
+ excludedSessions++;
13324
+ } else {
13325
+ const meta = getSessionMeta(db);
13326
+ if (!meta?.ownerId?.trim()) {
13327
+ missingOwnerSessions++;
13328
+ } else {
13329
+ const owner = resolveOwnerMember(db);
13330
+ if (!owner) {
13331
+ ownerStatus = "unresolved";
13332
+ unresolvedOwnerSessions++;
13333
+ } else {
13334
+ ownerStatus = "resolved";
13335
+ ownerMemberId = owner.id;
13336
+ }
13337
+ }
13338
+ }
13339
+ items.push(buildOverviewItem(descriptor, db, scope, facts, ownerStatus, ownerMemberId));
13340
+ } catch (error) {
13341
+ failedSessions++;
13342
+ failedSessionIds.push(scope.sessionId);
13343
+ appLogger.warn("cross-chat-analysis", `failed to build session overview: ${scope.sessionId}`, error);
13344
+ } finally {
13345
+ processedSessions++;
13346
+ }
13347
+ await yieldToEventLoop2();
13348
+ }
13349
+ options2.onProgress?.({ processedSessions, totalSessions: selected.length });
13350
+ return {
13351
+ appliedRange: {
13352
+ ...range,
13353
+ dataEarliestMessageTs,
13354
+ dataLatestMessageTs,
13355
+ currentTs: Math.floor(this.now() / 1e3)
13356
+ },
13357
+ items,
13358
+ coverage: {
13359
+ candidateSessions: scopes.length,
13360
+ analyzedSessions: items.length,
13361
+ excludedSessions,
13362
+ missingOwnerSessions,
13363
+ unresolvedOwnerSessions,
13364
+ failedSessions,
13365
+ failedSessionIds,
13366
+ complete: truncatedReasons.size === 0 && failedSessions === 0,
13367
+ truncated: truncatedReasons.size > 0,
13368
+ truncatedReasons: [...truncatedReasons]
13369
+ }
13370
+ };
13371
+ }
13372
+ async resolveSearchCandidates(scopes, failedSessionIds, sender, ownerResolution, maxSessions = DEFAULT_MAX_SESSIONS, deadline = Number.POSITIVE_INFINITY, signal) {
13373
+ throwIfAborted(signal);
13374
+ const normalizedScopes = scopes ? normalizeScopes(scopes) : (this.deps.adapter.listSessionCandidateIds?.() ?? this.deps.adapter.listSessionIds()).map((sessionId) => ({
13375
+ sessionId
13376
+ }));
13377
+ const candidates = [];
13378
+ let eligibleCandidateCount = 0;
13379
+ let timedOut = false;
13380
+ for (const scope of normalizedScopes) {
13381
+ throwIfAborted(signal);
13382
+ if (this.now() >= deadline) {
13383
+ timedOut = true;
13384
+ break;
13385
+ }
13386
+ const descriptor = this.tryGetSessionDescriptor(scope.sessionId);
13387
+ if (!descriptor) {
13388
+ failedSessionIds.add(scope.sessionId);
13389
+ await yieldToEventLoop2();
13390
+ continue;
13391
+ }
13392
+ if (this.now() >= deadline) {
13393
+ timedOut = true;
13394
+ break;
13395
+ }
13396
+ if (sender === "owner") {
13397
+ const db = this.deps.adapter.openReadonly(scope.sessionId);
13398
+ if (!db) {
13399
+ failedSessionIds.add(scope.sessionId);
13400
+ await yieldToEventLoop2();
13401
+ continue;
13402
+ }
13403
+ const meta = getSessionMeta(db);
13404
+ if (!meta?.ownerId?.trim()) {
13405
+ if (ownerResolution) ownerResolution.missingOwnerSessions++;
13406
+ await yieldToEventLoop2();
13407
+ continue;
13408
+ }
13409
+ const owner = resolveOwnerMember(db);
13410
+ if (!owner) {
13411
+ if (ownerResolution) ownerResolution.unresolvedOwnerSessions++;
13412
+ await yieldToEventLoop2();
13413
+ continue;
13414
+ }
13415
+ if (ownerResolution) ownerResolution.resolvedSessions++;
13416
+ eligibleCandidateCount++;
13417
+ addBoundedSearchCandidate(candidates, { descriptor, memberIds: [owner.id] }, maxSessions);
13418
+ } else {
13419
+ eligibleCandidateCount++;
13420
+ addBoundedSearchCandidate(candidates, { descriptor, memberIds: scope.memberIds }, maxSessions);
13421
+ }
13422
+ await yieldToEventLoop2();
13423
+ }
13424
+ throwIfAborted(signal);
13425
+ if (!timedOut && this.now() >= deadline) timedOut = true;
13426
+ return {
13427
+ candidates,
13428
+ candidateSessionCount: normalizedScopes.length,
13429
+ eligibleCandidateCount,
13430
+ timedOut
13431
+ };
13432
+ }
13433
+ tryGetSessionDescriptor(sessionId) {
13434
+ try {
13435
+ const db = this.deps.adapter.openReadonly(sessionId);
13436
+ return db ? getSessionDescriptor(sessionId, db) : null;
13437
+ } catch (error) {
13438
+ appLogger.warn("cross-chat-analysis", `failed to inspect session metadata: ${sessionId}`, error);
13439
+ return null;
13440
+ }
13441
+ }
13442
+ now() {
13443
+ return this.deps.now?.() ?? Date.now();
13444
+ }
13445
+ };
13446
+ function normalizeContactName(value) {
13447
+ return value.trim().toLocaleLowerCase();
13448
+ }
13449
+ function pickPreferredContactDisplayName(platformId, ...candidates) {
13450
+ const normalizedPlatformId = platformId.trim();
13451
+ for (const candidate of candidates) {
13452
+ const value = candidate?.trim();
13453
+ if (value && value !== normalizedPlatformId) return value;
13454
+ }
13455
+ return candidates.find((candidate) => candidate?.trim())?.trim() ?? normalizedPlatformId;
13456
+ }
13457
+ function normalizeRecentDays(value) {
13458
+ if (value === void 0) return void 0;
13459
+ if (!Number.isFinite(value) || value <= 0) throw new Error("recentDays must be a positive number");
13460
+ return Math.min(MAX_RECENT_DAYS, Math.max(1, Math.floor(value)));
13461
+ }
13462
+ function comparePrivateContactRankAccumulators(left, right, rankBy) {
13463
+ const primaryDifference = rankBy === "active_days" ? right.activeDayKeys.size - left.activeDayKeys.size : right.totalMessages - left.totalMessages;
13464
+ return primaryDifference || (right.lastMessageTs ?? Number.NEGATIVE_INFINITY) - (left.lastMessageTs ?? Number.NEGATIVE_INFINITY) || left.contactKey.localeCompare(right.contactKey);
13465
+ }
13466
+ function compareGroupSessionRankItems(left, right, mode) {
13467
+ const primaryDifference = mode === "owner_activity" ? (right.ownerMessages ?? Number.NEGATIVE_INFINITY) - (left.ownerMessages ?? Number.NEGATIVE_INFINITY) : right.totalMessages - left.totalMessages;
13468
+ return primaryDifference || (right.lastMessageTs ?? Number.NEGATIVE_INFINITY) - (left.lastMessageTs ?? Number.NEGATIVE_INFINITY) || left.sessionId.localeCompare(right.sessionId);
13469
+ }
13470
+ function roundRatio(numerator, denominator) {
13471
+ if (denominator <= 0) return 0;
13472
+ return Math.round(numerator / denominator * 1e4) / 1e4;
13473
+ }
13474
+ function compareSessionSearchResults(left, right) {
13475
+ const leftPrivate = left.candidate.descriptor.sessionType === "private" /* PRIVATE */;
13476
+ const rightPrivate = right.candidate.descriptor.sessionType === "private" /* PRIVATE */;
13477
+ if (leftPrivate !== rightPrivate) return leftPrivate ? -1 : 1;
13478
+ return right.totalMatches - left.totalMatches || (right.candidate.descriptor.lastMessageTs ?? Number.NEGATIVE_INFINITY) - (left.candidate.descriptor.lastMessageTs ?? Number.NEGATIVE_INFINITY);
13479
+ }
13480
+ function collectSearchEvidence(results, maxMessages, startTs, endTs, options2 = {
13481
+ deadline: Number.POSITIVE_INFINITY,
13482
+ now: Date.now
13483
+ }) {
13484
+ const selected = /* @__PURE__ */ new Map();
13485
+ let timedOut = false;
13486
+ for (const result of results) {
13487
+ let matchIndex = 0;
13488
+ while (matchIndex < result.matches.length && selected.size < maxMessages) {
13489
+ throwIfAborted(options2.signal);
13490
+ if (options2.now() >= options2.deadline) {
13491
+ timedOut = true;
13492
+ break;
13493
+ }
13494
+ const remaining = maxMessages - selected.size;
13495
+ const expectedBlockSize = SEARCH_CONTEXT_BEFORE + SEARCH_CONTEXT_AFTER + 1;
13496
+ const batchSize = Math.min(
13497
+ result.matches.length - matchIndex,
13498
+ remaining < expectedBlockSize ? 1 : Math.max(1, Math.floor(remaining / expectedBlockSize))
13499
+ );
13500
+ const batch = result.matches.slice(matchIndex, matchIndex + batchSize);
13501
+ const matchIds = new Set(batch.map((message) => message.messageId));
13502
+ const contextMessages = getSearchMessageContext(
13503
+ result.db,
13504
+ [...matchIds],
13505
+ SEARCH_CONTEXT_BEFORE,
13506
+ SEARCH_CONTEXT_AFTER
13507
+ ).map((message) => ({
13508
+ ...toCrossChatMessage(result.candidate.descriptor, message),
13509
+ evidenceRole: matchIds.has(message.id) ? "match" : "context"
13510
+ })).filter((message) => isWithinSearchRange(message.timestamp, startTs, endTs));
13511
+ for (const matchId of matchIds) {
13512
+ const key = crossChatMessageKey(result.candidate.descriptor.sessionId, matchId);
13513
+ const existing = selected.get(key);
13514
+ if (existing?.evidenceRole === "context") selected.set(key, { ...existing, evidenceRole: "match" });
13515
+ }
13516
+ const unseen = contextMessages.filter(
13517
+ (message) => !selected.has(crossChatMessageKey(message.sessionId, message.messageId))
13518
+ );
13519
+ const accepted = unseen.length <= remaining ? unseen : prioritizeContextMessages(unseen, matchIds).slice(0, remaining);
13520
+ for (const message of accepted) {
13521
+ const key = crossChatMessageKey(message.sessionId, message.messageId);
13522
+ const existing = selected.get(key);
13523
+ if (!existing || existing.evidenceRole === "context" && message.evidenceRole === "match") {
13524
+ selected.set(key, message);
13525
+ }
13526
+ }
13527
+ matchIndex += batchSize;
13528
+ }
13529
+ if (timedOut || selected.size >= maxMessages) break;
13530
+ }
13531
+ return { messages: [...selected.values()], timedOut };
13532
+ }
13533
+ function prioritizeContextMessages(messages, matchIds) {
13534
+ return [...messages].sort((left, right) => {
13535
+ const leftMatch = matchIds.has(left.messageId);
13536
+ const rightMatch = matchIds.has(right.messageId);
13537
+ if (leftMatch !== rightMatch) return leftMatch ? -1 : 1;
13538
+ const leftDistance = nearestMessageIdDistance(left.messageId, matchIds);
13539
+ const rightDistance = nearestMessageIdDistance(right.messageId, matchIds);
13540
+ return leftDistance - rightDistance || left.messageId - right.messageId;
13541
+ });
13542
+ }
13543
+ function nearestMessageIdDistance(messageId, matchIds) {
13544
+ let distance = Number.POSITIVE_INFINITY;
13545
+ for (const matchId of matchIds) distance = Math.min(distance, Math.abs(messageId - matchId));
13546
+ return distance;
13547
+ }
13548
+ function crossChatMessageKey(sessionId, messageId) {
13549
+ return `${sessionId}:${messageId}`;
13550
+ }
13551
+ function isWithinSearchRange(timestamp, startTs, endTs) {
13552
+ return (startTs === void 0 || timestamp >= startTs) && (endTs === void 0 || timestamp <= endTs);
13553
+ }
13554
+ function addBoundedSearchCandidate(candidates, candidate, maxSessions) {
13555
+ candidates.push(candidate);
13556
+ candidates.sort(compareSearchCandidates);
13557
+ if (candidates.length > maxSessions) candidates.length = maxSessions;
13558
+ }
13559
+ function compareSearchCandidates(left, right) {
13560
+ return (right.descriptor.lastMessageTs ?? Number.NEGATIVE_INFINITY) - (left.descriptor.lastMessageTs ?? Number.NEGATIVE_INFINITY);
13561
+ }
13562
+ function getSessionDescriptor(sessionId, db) {
13563
+ const meta = getSessionMeta(db);
13564
+ if (!meta || meta.type !== "private" /* PRIVATE */ && meta.type !== "group" /* GROUP */) return null;
13565
+ const latest = getRecentMessages(db, { limit: 1 })[0];
13566
+ return {
13567
+ sessionId,
13568
+ sessionName: meta.name,
13569
+ sessionType: meta.type,
13570
+ platform: meta.platform,
13571
+ lastMessageTs: latest?.timestamp ?? null
13572
+ };
13573
+ }
13574
+ function toCrossChatMessage(descriptor, message) {
13575
+ return {
13576
+ ...descriptor,
13577
+ messageId: message.id,
13578
+ senderId: message.senderId,
13579
+ senderName: message.senderName,
13580
+ senderPlatformId: message.senderPlatformId,
13581
+ content: message.content,
13582
+ timestamp: message.timestamp,
13583
+ messageType: message.type
13584
+ };
13585
+ }
13586
+ function normalizeScopes(scopes) {
13587
+ const bySession = /* @__PURE__ */ new Map();
13588
+ for (const scope of scopes) {
13589
+ const sessionId = scope.sessionId.trim();
13590
+ if (!sessionId) continue;
13591
+ const existing = bySession.get(sessionId);
13592
+ if (!existing) {
13593
+ bySession.set(sessionId, {
13594
+ sessionId,
13595
+ memberIds: scope.memberIds ? [...new Set(scope.memberIds)] : void 0,
13596
+ label: scope.label
13597
+ });
13598
+ continue;
13599
+ }
13600
+ if (!existing.memberIds || !scope.memberIds) {
13601
+ existing.memberIds = void 0;
13602
+ } else {
13603
+ existing.memberIds = [.../* @__PURE__ */ new Set([...existing.memberIds, ...scope.memberIds])];
13604
+ }
13605
+ existing.label ??= scope.label;
13606
+ }
13607
+ return [...bySession.values()];
13608
+ }
13609
+ function normalizeOverviewScopes(scopes) {
13610
+ return scopes.flatMap((scope) => {
13611
+ const sessionId = scope.sessionId.trim();
13612
+ if (!sessionId) return [];
13613
+ return [
13614
+ {
13615
+ sessionId,
13616
+ memberIds: scope.memberIds ? [...new Set(scope.memberIds)] : void 0,
13617
+ label: scope.label
13618
+ }
13619
+ ];
13620
+ });
13621
+ }
13622
+ function buildOverviewItem(descriptor, db, scope, facts, ownerStatus, ownerMemberId) {
13623
+ const rosterById = new Map(getNonSystemMembersForContacts(db).map((member) => [member.id, member]));
13624
+ const factsByMemberId = new Map(facts.members.map((member) => [member.memberId, member]));
13625
+ const memberActivities = (scope.memberIds ?? []).flatMap((memberId) => {
13626
+ const rosterMember = rosterById.get(memberId);
13627
+ if (!rosterMember) return [];
13628
+ const activity = factsByMemberId.get(memberId);
13629
+ return [
13630
+ {
13631
+ memberId,
13632
+ platformId: rosterMember.platformId,
13633
+ memberName: rosterMember.name,
13634
+ messageCount: activity?.messageCount ?? 0,
13635
+ activeDays: activity?.activeDays ?? 0,
13636
+ firstMessageTs: activity?.firstMessageTs ?? null,
13637
+ lastMessageTs: activity?.lastMessageTs ?? null
13638
+ }
13639
+ ];
13640
+ });
13641
+ const selectedFacts = scope.memberIds ? scope.memberIds.flatMap((memberId) => factsByMemberId.get(memberId) ?? []) : [];
13642
+ const selectedActiveDayKeys = new Set(selectedFacts.flatMap((member) => member.activeDayKeys));
13643
+ const ownerActivity = ownerMemberId === null ? void 0 : factsByMemberId.get(ownerMemberId);
13644
+ const topMembers = descriptor.sessionType === "group" /* GROUP */ ? [...facts.members].sort(
13645
+ (left, right) => right.messageCount - left.messageCount || (right.lastMessageTs ?? Number.NEGATIVE_INFINITY) - (left.lastMessageTs ?? Number.NEGATIVE_INFINITY) || left.memberId - right.memberId
13646
+ ).slice(0, OVERVIEW_TOP_MEMBERS_LIMIT).map(toOverviewMemberActivity) : [];
13647
+ const totalMessages = scope.memberIds ? selectedFacts.reduce((sum, member) => sum + member.messageCount, 0) : facts.totalMessages;
13648
+ const firstMessageTs = scope.memberIds ? selectedFacts.reduce((earliest, member) => minNullable(earliest, member.firstMessageTs), null) : facts.firstMessageTs;
13649
+ const lastMessageTs = scope.memberIds ? selectedFacts.reduce((latest, member) => maxNullable(latest, member.lastMessageTs), null) : facts.lastMessageTs;
13650
+ return {
13651
+ ...descriptor,
13652
+ label: scope.label ?? descriptor.sessionName,
13653
+ ...scope.memberIds ? {
13654
+ memberIds: scope.memberIds,
13655
+ memberNames: memberActivities.map((member) => member.memberName)
13656
+ } : {},
13657
+ memberActivities,
13658
+ totalMessages,
13659
+ activeDays: scope.memberIds ? selectedActiveDayKeys.size : facts.activeDays,
13660
+ activeMembers: scope.memberIds ? selectedFacts.length : facts.activeMembers,
13661
+ firstMessageTs,
13662
+ lastMessageTs,
13663
+ ownerStatus,
13664
+ ownerMessages: ownerStatus === "resolved" ? ownerActivity?.messageCount ?? 0 : null,
13665
+ ownerActiveDays: ownerStatus === "resolved" ? ownerActivity?.activeDays ?? 0 : null,
13666
+ topMembers
13667
+ };
13668
+ }
13669
+ function toOverviewMemberActivity(member) {
13670
+ return {
13671
+ memberId: member.memberId,
13672
+ platformId: member.platformId,
13673
+ memberName: member.memberName,
13674
+ messageCount: member.messageCount,
13675
+ activeDays: member.activeDays,
13676
+ firstMessageTs: member.firstMessageTs,
13677
+ lastMessageTs: member.lastMessageTs
13678
+ };
13679
+ }
13680
+ function clampInteger(value, fallback, min, max) {
13681
+ if (!Number.isFinite(value)) return fallback;
13682
+ return Math.min(max, Math.max(min, Math.floor(value)));
13683
+ }
13684
+ function normalizeInspectionRange(startTs, endTs, recentDays, now) {
13685
+ return resolveInspectionRange(normalizeInspectionRangeRequest(startTs, endTs, recentDays), now);
13686
+ }
13687
+ function normalizeInspectionRangeRequest(startTs, endTs, recentDays) {
13688
+ const normalizedStart = normalizeOptionalTimestamp(startTs, "startTs");
13689
+ const normalizedEnd = normalizeOptionalTimestamp(endTs, "endTs");
13690
+ if (normalizedStart !== null && normalizedEnd !== null && normalizedStart > normalizedEnd) {
13691
+ throw new Error("startTs must be less than or equal to endTs");
13692
+ }
13693
+ return {
13694
+ startTs: normalizedStart,
13695
+ endTs: normalizedEnd,
13696
+ recentDays: normalizedStart === null ? normalizeRecentDays(recentDays) ?? null : null
13697
+ };
13698
+ }
13699
+ function resolveInspectionRange(request, now) {
13700
+ if (request.recentDays !== null) {
13701
+ const anchor = new Date((request.endTs ?? Math.floor(now() / 1e3)) * 1e3);
13702
+ const start = new Date(anchor);
13703
+ start.setHours(0, 0, 0, 0);
13704
+ start.setDate(start.getDate() - (request.recentDays - 1));
13705
+ const endExclusive = new Date(anchor);
13706
+ endExclusive.setHours(0, 0, 0, 0);
13707
+ endExclusive.setDate(endExclusive.getDate() + 1);
13708
+ return {
13709
+ startTs: Math.floor(start.getTime() / 1e3),
13710
+ endTs: Math.floor(endExclusive.getTime() / 1e3) - 1
13711
+ };
13712
+ }
13713
+ return { startTs: request.startTs, endTs: request.endTs };
13714
+ }
13715
+ function normalizeOptionalTimestamp(value, name) {
13716
+ if (value === void 0) return null;
13717
+ if (!Number.isFinite(value)) throw new Error(`${name} must be a finite timestamp`);
13718
+ return Math.floor(value);
13719
+ }
13720
+ function summarizeContactSessions(sessions, completeResult) {
13721
+ let ownMessageCount = 0;
13722
+ let firstOwnMessageTs = null;
13723
+ let lastOwnMessageTs = null;
13724
+ let privateSessions = 0;
13725
+ let groupSessions = 0;
13726
+ let spokeSessions = 0;
13727
+ let rosterOnlySessions = 0;
13728
+ for (const session of sessions) {
13729
+ ownMessageCount += session.ownMessageCount;
13730
+ firstOwnMessageTs = minNullable(firstOwnMessageTs, session.firstOwnMessageTs);
13731
+ lastOwnMessageTs = maxNullable(lastOwnMessageTs, session.lastOwnMessageTs);
13732
+ if (session.sessionType === "private" /* PRIVATE */) privateSessions++;
13733
+ else groupSessions++;
13734
+ if (session.presence === "spoke") spokeSessions++;
13735
+ else rosterOnlySessions++;
13736
+ }
13737
+ return {
13738
+ scope: completeResult ? "complete_result" : "current_batch",
13739
+ matchedSessions: sessions.length,
13740
+ privateSessions,
13741
+ groupSessions,
13742
+ spokeSessions,
13743
+ rosterOnlySessions,
13744
+ ownMessageCount,
13745
+ firstOwnMessageTs,
13746
+ lastOwnMessageTs
13747
+ };
13748
+ }
13749
+ function normalizeParticipantRefs(participants) {
13750
+ if (!Array.isArray(participants)) throw new Error("participants must be an array");
13751
+ const deduplicated = /* @__PURE__ */ new Map();
13752
+ for (const participant of participants) {
13753
+ if (participant?.type === "owner") {
13754
+ deduplicated.set("owner", { type: "owner" });
13755
+ continue;
13756
+ }
13757
+ if (participant?.type === "contact") {
13758
+ const contactKey = participant.contactKey?.trim();
13759
+ if (!contactKey) throw new Error("participant contactKey is required");
13760
+ deduplicated.set(`contact:${contactKey}`, { type: "contact", contactKey });
13761
+ continue;
13762
+ }
13763
+ throw new Error("participant type must be owner or contact");
13764
+ }
13765
+ const refs = [...deduplicated.values()];
13766
+ if (refs.length < 2 || refs.length > 5) throw new Error("participants must contain 2 to 5 distinct people");
13767
+ return refs;
13768
+ }
13769
+ function mapSharedInteractionSession(descriptor, members, participantIndexByMemberId, facts) {
13770
+ const participants = facts.participants.map((participant, index) => ({
13771
+ participantIndex: participantIndexByMemberId.get(participant.memberId) ?? index,
13772
+ memberId: participant.memberId,
13773
+ memberName: members[index]?.name ?? String(participant.memberId),
13774
+ messageCount: participant.messageCount,
13775
+ firstMessageTs: participant.firstMessageTs,
13776
+ lastMessageTs: participant.lastMessageTs,
13777
+ activeDays: participant.activeDays,
13778
+ presenceObservedInRange: participant.messageCount > 0
13779
+ }));
13780
+ const pairs = facts.pairs.map((pair) => ({
13781
+ sourceParticipantIndex: participantIndexByMemberId.get(pair.sourceMemberId) ?? 0,
13782
+ targetParticipantIndex: participantIndexByMemberId.get(pair.targetMemberId) ?? 0,
13783
+ directReplyCount: pair.directReplyCount,
13784
+ repliesFromSourceToTarget: pair.repliesFromSourceToTarget,
13785
+ repliesFromTargetToSource: pair.repliesFromTargetToSource,
13786
+ lastDirectReplyTs: pair.lastDirectReplyTs,
13787
+ coOccurrenceCount: pair.coOccurrenceCount,
13788
+ coOccurrenceRawScore: pair.coOccurrenceRawScore,
13789
+ lastProximityTs: pair.lastProximityTs,
13790
+ coActiveDays: pair.coActiveDays,
13791
+ anchors: pair.anchors.map((anchor) => ({
13792
+ sessionId: descriptor.sessionId,
13793
+ messageId: anchor.messageId,
13794
+ relatedMessageId: anchor.relatedMessageId,
13795
+ timestamp: anchor.timestamp,
13796
+ signal: anchor.signal,
13797
+ fromParticipantIndex: participantIndexByMemberId.get(anchor.fromMemberId) ?? 0,
13798
+ toParticipantIndex: participantIndexByMemberId.get(anchor.toMemberId) ?? 0
13799
+ })),
13800
+ anchorsTruncated: pair.anchorsTruncated
13801
+ }));
13802
+ const priorityReasons = [];
13803
+ if (pairs.some((pair) => pair.directReplyCount > 0)) priorityReasons.push("has_direct_reply");
13804
+ if (pairs.some((pair) => (pair.coOccurrenceCount ?? 0) > 0)) priorityReasons.push("has_proximity");
13805
+ if (participants.every((participant) => participant.presenceObservedInRange)) {
13806
+ priorityReasons.push("all_participants_spoke");
13807
+ }
13808
+ return {
13809
+ ...descriptor,
13810
+ lastMessageTs: facts.sessionLastMessageTs,
13811
+ memberCount: descriptor.sessionType === "group" /* GROUP */ ? facts.memberCount : null,
13812
+ participants,
13813
+ overlapRange: facts.overlapRange,
13814
+ allParticipantsCoActiveDays: facts.allParticipantsCoActiveDays,
13815
+ pairs,
13816
+ priorityReasons,
13817
+ proximityStatus: facts.proximityStatus
13818
+ };
13819
+ }
13820
+ function summarizeSharedInteractions(sessions, completeResult) {
13821
+ return {
13822
+ scope: completeResult ? "complete_result" : "current_batch",
13823
+ commonSessions: sessions.length,
13824
+ commonPrivateSessions: sessions.filter((session) => session.sessionType === "private" /* PRIVATE */).length,
13825
+ commonGroupSessions: sessions.filter((session) => session.sessionType === "group" /* GROUP */).length,
13826
+ sessionsWithDirectReplies: sessions.filter((session) => session.pairs.some((pair) => pair.directReplyCount > 0)).length,
13827
+ sessionsWithProximitySignals: sessions.filter(
13828
+ (session) => session.pairs.some((pair) => (pair.coOccurrenceCount ?? 0) > 0)
13829
+ ).length
13830
+ };
13831
+ }
13832
+ function emptySharedInteractionsResult(participants, unresolvedParticipantIndexes, range) {
13833
+ return {
13834
+ algorithmVersion: SHARED_INTERACTIONS_ALGORITHM_VERSION,
13835
+ proximityAlgorithmVersion: PROXIMITY_ALGORITHM_VERSION,
13836
+ participants,
13837
+ appliedRange: {
13838
+ ...range,
13839
+ dataEarliestMessageTs: null,
13840
+ dataLatestMessageTs: null
13841
+ },
13842
+ summary: {
13843
+ scope: "complete_result",
13844
+ commonSessions: 0,
13845
+ commonPrivateSessions: 0,
13846
+ commonGroupSessions: 0,
13847
+ sessionsWithDirectReplies: 0,
13848
+ sessionsWithProximitySignals: 0
13849
+ },
13850
+ sessions: [],
13851
+ coverage: {
13852
+ candidateSessions: 0,
13853
+ scannedSessions: 0,
13854
+ matchedSessions: 0,
13855
+ returnedSessions: 0,
13856
+ failedSessions: 0,
13857
+ failedSessionIds: [],
13858
+ complete: true,
13859
+ nextCursor: null,
13860
+ truncated: false,
13861
+ truncatedReasons: [],
13862
+ unresolvedParticipantIndexes,
13863
+ identityCollisionSessions: 0,
13864
+ ownerResolution: participants.some((participant) => participant.ref.type === "owner") ? { resolvedSessions: 0, missingOwnerSessions: 0, unresolvedOwnerSessions: 0 } : void 0
13865
+ }
13866
+ };
13867
+ }
13868
+ function compareSharedInteractionSessions(left, right) {
13869
+ const leftReplies = left.pairs.reduce((sum, pair) => sum + pair.directReplyCount, 0);
13870
+ const rightReplies = right.pairs.reduce((sum, pair) => sum + pair.directReplyCount, 0);
13871
+ if (leftReplies !== rightReplies) return rightReplies - leftReplies;
13872
+ const leftProximity = left.pairs.reduce((sum, pair) => sum + (pair.coOccurrenceRawScore ?? 0), 0);
13873
+ const rightProximity = right.pairs.reduce((sum, pair) => sum + (pair.coOccurrenceRawScore ?? 0), 0);
13874
+ if (leftProximity !== rightProximity) return rightProximity - leftProximity;
13875
+ const leftCoActiveDays = left.pairs.reduce((sum, pair) => sum + pair.coActiveDays, 0);
13876
+ const rightCoActiveDays = right.pairs.reduce((sum, pair) => sum + pair.coActiveDays, 0);
13877
+ if (leftCoActiveDays !== rightCoActiveDays) return rightCoActiveDays - leftCoActiveDays;
13878
+ const leftLastInteractionTs = latestPairInteractionTs(left.pairs);
13879
+ const rightLastInteractionTs = latestPairInteractionTs(right.pairs);
13880
+ if (leftLastInteractionTs !== rightLastInteractionTs) return rightLastInteractionTs - leftLastInteractionTs;
13881
+ if (left.memberCount !== right.memberCount) {
13882
+ return (left.memberCount ?? Number.POSITIVE_INFINITY) - (right.memberCount ?? Number.POSITIVE_INFINITY);
13883
+ }
13884
+ return left.sessionId.localeCompare(right.sessionId);
13885
+ }
13886
+ function latestPairInteractionTs(pairs) {
13887
+ let latest = 0;
13888
+ for (const pair of pairs) {
13889
+ latest = Math.max(latest, pair.lastDirectReplyTs ?? 0, pair.lastProximityTs ?? 0);
13890
+ }
13891
+ return latest;
13892
+ }
13893
+ function emptyContactSessionsResult(contactCacheStatus, range) {
13894
+ return {
13895
+ algorithmVersion: CONTACT_SESSIONS_ALGORITHM_VERSION,
13896
+ contact: null,
13897
+ appliedRange: {
13898
+ ...range,
13899
+ dataEarliestMessageTs: null,
13900
+ dataLatestMessageTs: null
13901
+ },
13902
+ summary: {
13903
+ scope: "complete_result",
13904
+ matchedSessions: 0,
13905
+ privateSessions: 0,
13906
+ groupSessions: 0,
13907
+ spokeSessions: 0,
13908
+ rosterOnlySessions: 0,
13909
+ ownMessageCount: 0,
13910
+ firstOwnMessageTs: null,
13911
+ lastOwnMessageTs: null
13912
+ },
13913
+ sessions: [],
13914
+ coverage: {
13915
+ candidateSessions: 0,
13916
+ scannedSessions: 0,
13917
+ matchedSessions: 0,
13918
+ returnedSessions: 0,
13919
+ failedSessions: 0,
13920
+ failedSessionIds: [],
13921
+ complete: true,
13922
+ nextCursor: null,
13923
+ truncated: false,
13924
+ truncatedReasons: [],
13925
+ contactCacheStatus
13926
+ }
13927
+ };
13928
+ }
13929
+ function minNullable(left, right) {
13930
+ if (left === null) return right;
13931
+ if (right === null) return left;
13932
+ return Math.min(left, right);
13933
+ }
13934
+ function maxNullable(left, right) {
13935
+ if (left === null) return right;
13936
+ if (right === null) return left;
13937
+ return Math.max(left, right);
13938
+ }
13939
+ function createInspectionFingerprint(value) {
13940
+ return createHash2("sha256").update(JSON.stringify(value)).digest("base64url");
13941
+ }
13942
+ function createInspectionCursor(fingerprint, afterSessionId, range) {
13943
+ const payload = { version: 2, fingerprint, afterSessionId, range };
13944
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
13945
+ }
13946
+ function parseInspectionCursor(value, createFingerprint, candidateSessionIds, defaultRange) {
13947
+ if (!value) {
13948
+ return {
13949
+ nextCandidateIndex: 0,
13950
+ fingerprint: createFingerprint(defaultRange),
13951
+ range: defaultRange
13952
+ };
13953
+ }
13954
+ try {
13955
+ const payload = JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
13956
+ if (payload.version !== 2 || !isInspectionRange(payload.range)) throw new Error("cursor payload is invalid");
13957
+ const fingerprint = createFingerprint(payload.range);
13958
+ if (payload.fingerprint !== fingerprint) throw new Error("cursor mismatch");
13959
+ if (payload.afterSessionId === null) {
13960
+ return { nextCandidateIndex: 0, fingerprint, range: payload.range };
13961
+ }
13962
+ if (typeof payload.afterSessionId !== "string") throw new Error("cursor session is missing");
13963
+ const index = candidateSessionIds.indexOf(payload.afterSessionId);
13964
+ if (index < 0) throw new Error("cursor session no longer exists");
13965
+ return { nextCandidateIndex: index + 1, fingerprint, range: payload.range };
13966
+ } catch {
13967
+ throw new Error("cursor is invalid or does not match the current inspection request");
13968
+ }
13969
+ }
13970
+ function isInspectionRange(value) {
13971
+ if (!value || typeof value !== "object") return false;
13972
+ const range = value;
13973
+ const isTimestamp = (timestamp) => timestamp === null || typeof timestamp === "number" && Number.isFinite(timestamp);
13974
+ return isTimestamp(range.startTs) && isTimestamp(range.endTs) && (range.startTs === null || range.endTs === null || range.startTs <= range.endTs);
13975
+ }
13976
+ function throwIfAborted(signal) {
13977
+ if (!signal?.aborted) return;
13978
+ const error = new Error("Cross-chat analysis was interrupted");
13979
+ error.name = "AbortError";
13980
+ throw error;
13981
+ }
13982
+ function yieldToEventLoop2() {
13983
+ return new Promise((resolve4) => setImmediate(resolve4));
13984
+ }
13985
+ function createNotFoundError(message) {
13986
+ return Object.assign(new Error(message), { statusCode: 404 });
13987
+ }
13988
+
13989
+ // ../../packages/node-runtime/src/services/cross-chat-analysis/preprocess.ts
13990
+ function preprocessCrossChatMessages(adapter, sessionId, messages, config) {
13991
+ const safeConfig = config ? { ...config, mergeConsecutive: false, anonymizeNames: false } : void 0;
13992
+ const processed = preprocessMessages(
13993
+ messages.map((message) => ({ ...message })),
13994
+ safeConfig
13995
+ );
13996
+ if (!config?.anonymizeNames) return processed;
13997
+ let ownerPlatformId = null;
13998
+ try {
13999
+ const db = adapter.openReadonly(sessionId);
14000
+ ownerPlatformId = db ? getSessionMeta(db)?.ownerId?.trim() || null : null;
14001
+ } catch {
14002
+ }
14003
+ return processed.map((message) => ({
14004
+ ...message,
14005
+ senderName: ownerPlatformId && message.senderPlatformId === ownerPlatformId ? "Owner" : `U${message.senderId}@${sessionId}`
14006
+ }));
14007
+ }
14008
+ function preprocessCrossChatSummaries(adapter, sessionId, summaries, config) {
14009
+ if (!config) return summaries.map(cloneSummary);
14010
+ const participantPseudonyms = config.anonymizeNames ? buildParticipantPseudonyms(
14011
+ adapter,
14012
+ sessionId,
14013
+ summaries.flatMap((summary) => summary.participants)
14014
+ ) : null;
14015
+ return summaries.flatMap((summary) => {
14016
+ if (matchesBlacklist(summary.summary, config.blacklistKeywords)) return [];
14017
+ let safeSummary = config.desensitize ? desensitizeText(summary.summary, config.desensitizeRules) : summary.summary;
14018
+ if (participantPseudonyms) {
14019
+ const anonymizedSummary = replaceKnownParticipantNames(safeSummary, participantPseudonyms);
14020
+ if (anonymizedSummary === null) return [];
14021
+ safeSummary = anonymizedSummary;
14022
+ }
14023
+ const participants = summary.participants.filter((participant) => !matchesBlacklist(participant, config.blacklistKeywords)).map((participant) => {
14024
+ if (participantPseudonyms) return participantPseudonyms.get(participant) ?? `Participant@${sessionId}`;
14025
+ return config.desensitize ? desensitizeText(participant, config.desensitizeRules) : participant;
14026
+ }).filter((participant, index, values) => participant.length > 0 && values.indexOf(participant) === index);
14027
+ return [{ ...summary, participants, summary: safeSummary }];
14028
+ });
14029
+ }
14030
+ function preprocessCrossChatLabel(value, pseudonym, config) {
14031
+ if (!config) return value;
14032
+ if (config.anonymizeNames || matchesBlacklist(value, config.blacklistKeywords)) return pseudonym;
14033
+ return config.desensitize ? desensitizeText(value, config.desensitizeRules) : value;
14034
+ }
14035
+ function cloneSummary(summary) {
14036
+ return { ...summary, participants: [...summary.participants] };
14037
+ }
14038
+ function replaceKnownParticipantNames(summary, pseudonyms) {
14039
+ const replacements = /* @__PURE__ */ new Map();
14040
+ for (const [participantName, pseudonym] of pseudonyms) {
14041
+ const trimmedName = participantName.trim();
14042
+ if (participantName) replacements.set(participantName, pseudonym);
14043
+ if (trimmedName) replacements.set(trimmedName, pseudonym);
14044
+ }
14045
+ if (replacements.size === 0) return summary;
14046
+ const ambiguousLabels = /* @__PURE__ */ new Set();
14047
+ const alternatives = [...replacements.keys()].sort((left, right) => Array.from(right).length - Array.from(left).length).map((participantName) => {
14048
+ const escapedName = escapeRegExp(participantName);
14049
+ if (/^[\p{Script=Latin}\p{N}_]+$/u.test(participantName)) {
14050
+ return `(?<![\\p{Script=Latin}\\p{N}_])${escapedName}(?![\\p{Script=Latin}\\p{N}_])`;
14051
+ }
14052
+ if (Array.from(participantName).length === 1 && /[\p{L}\p{N}]/u.test(participantName)) {
14053
+ ambiguousLabels.add(participantName);
14054
+ return `(?<![\\p{L}\\p{N}_])${escapedName}(?![\\p{L}\\p{N}_])`;
14055
+ }
14056
+ return escapedName;
14057
+ });
14058
+ const pattern = new RegExp(alternatives.join("|"), "gu");
14059
+ const anonymized = summary.replace(pattern, (participantName) => replacements.get(participantName) ?? participantName);
14060
+ return [...ambiguousLabels].some((participantName) => anonymized.includes(participantName)) ? null : anonymized;
14061
+ }
14062
+ function escapeRegExp(value) {
14063
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
14064
+ }
14065
+ function buildParticipantPseudonyms(adapter, sessionId, participantNames) {
14066
+ const pseudonyms = /* @__PURE__ */ new Map();
14067
+ const fallbackPseudonyms = /* @__PURE__ */ new Map();
14068
+ let ownerPlatformId = null;
14069
+ let members = [];
14070
+ try {
14071
+ const db = adapter.openReadonly(sessionId);
14072
+ if (db) {
14073
+ ownerPlatformId = getSessionMeta(db)?.ownerId?.trim() || null;
14074
+ members = getNonSystemMembersForContacts(db);
14075
+ }
14076
+ } catch {
14077
+ }
14078
+ for (const participantName of participantNames) {
14079
+ if (pseudonyms.has(participantName)) continue;
14080
+ const normalizedName = participantName.trim().toLocaleLowerCase();
14081
+ const matches = members.filter(
14082
+ (member) => [member.name, member.platformId, ...member.aliases].some(
14083
+ (candidate) => candidate.trim().toLocaleLowerCase() === normalizedName
14084
+ )
14085
+ );
14086
+ const uniqueMatches = [...new Map(matches.map((member) => [member.id, member])).values()];
14087
+ if (uniqueMatches.length === 1) {
14088
+ const member = uniqueMatches[0];
14089
+ pseudonyms.set(
14090
+ participantName,
14091
+ ownerPlatformId && member.platformId === ownerPlatformId ? "Owner" : `U${member.id}@${sessionId}`
14092
+ );
14093
+ continue;
14094
+ }
14095
+ let fallback = fallbackPseudonyms.get(normalizedName);
14096
+ if (!fallback) {
14097
+ fallback = `Participant${fallbackPseudonyms.size + 1}@${sessionId}`;
14098
+ fallbackPseudonyms.set(normalizedName, fallback);
14099
+ }
14100
+ pseudonyms.set(participantName, fallback);
14101
+ }
14102
+ return pseudonyms;
14103
+ }
14104
+
14105
+ // ../../packages/node-runtime/src/services/cross-chat-analysis/index.ts
14106
+ var cross_chat_analysis_exports = {};
14107
+ __export(cross_chat_analysis_exports, {
14108
+ createCrossChatAnalysisService: () => createCrossChatAnalysisService,
14109
+ preprocessCrossChatLabel: () => preprocessCrossChatLabel,
14110
+ preprocessCrossChatMessages: () => preprocessCrossChatMessages,
14111
+ preprocessCrossChatSummaries: () => preprocessCrossChatSummaries
14112
+ });
14113
+
12223
14114
  // ../../packages/node-runtime/src/services/people/relationships/signature.ts
12224
14115
  function buildPeopleRelationshipsSignature(adapter, timeRangePreset) {
12225
14116
  const parts = [
@@ -13624,7 +15515,7 @@ var LocalEmbeddingProvider = class {
13624
15515
  import path22 from "path";
13625
15516
 
13626
15517
  // ../../packages/node-runtime/src/semantic-index/chunker-config.ts
13627
- import { createHash as createHash2 } from "crypto";
15518
+ import { createHash as createHash3 } from "crypto";
13628
15519
  var CHUNKER_VERSION = "v1.3";
13629
15520
  var STRATEGY_ID = "balanced";
13630
15521
  var DEFAULT_CHUNKER_CONFIG = {
@@ -13639,7 +15530,7 @@ var DEFAULT_CHUNKER_CONFIG = {
13639
15530
  semanticVoidSkipThreshold: 20
13640
15531
  };
13641
15532
  function sha256Hex(input) {
13642
- return createHash2("sha256").update(input, "utf-8").digest("hex");
15533
+ return createHash3("sha256").update(input, "utf-8").digest("hex");
13643
15534
  }
13644
15535
  function computeChunkerConfigHash(config) {
13645
15536
  const canonical = JSON.stringify({
@@ -14327,7 +16218,7 @@ var SemanticIndexJobQueue = class {
14327
16218
  };
14328
16219
 
14329
16220
  // ../../packages/node-runtime/src/semantic-index/chunker.ts
14330
- import { createHash as createHash3 } from "crypto";
16221
+ import { createHash as createHash4 } from "crypto";
14331
16222
  var VOID_MESSAGE_TYPES = /* @__PURE__ */ new Set([
14332
16223
  1 /* IMAGE */,
14333
16224
  2 /* VOICE */,
@@ -14410,7 +16301,7 @@ function sumEffectiveChars(messages) {
14410
16301
  return total;
14411
16302
  }
14412
16303
  function sha256Hex2(input) {
14413
- return createHash3("sha256").update(input, "utf-8").digest("hex");
16304
+ return createHash4("sha256").update(input, "utf-8").digest("hex");
14414
16305
  }
14415
16306
  var EMBEDDING_HEADER_BUDGET_RATIO = 0.25;
14416
16307
  var SOURCE_TITLE_HEADER_SHARE = 1 / 3;
@@ -16344,10 +18235,10 @@ async function executePushImportUnlocked(deps, sessionId, payload) {
16344
18235
  }
16345
18236
 
16346
18237
  // ../../packages/node-runtime/src/services/import-idempotency.ts
16347
- import { createHash as createHash4 } from "crypto";
18238
+ import { createHash as createHash5 } from "crypto";
16348
18239
  var DEFAULT_IMPORT_IDEMPOTENCY_TTL_MS = 60 * 60 * 1e3;
16349
18240
  function hashImportBody(body) {
16350
- return createHash4("sha256").update(JSON.stringify(body ?? "")).digest("hex");
18241
+ return createHash5("sha256").update(JSON.stringify(body ?? "")).digest("hex");
16351
18242
  }
16352
18243
  var ImportIdempotencyCache = class {
16353
18244
  constructor(ttlMs = DEFAULT_IMPORT_IDEMPOTENCY_TTL_MS) {
@@ -16414,6 +18305,7 @@ export {
16414
18305
  MessageBatchInserter,
16415
18306
  generateFallbackMessageKey,
16416
18307
  createMessageDedupState,
18308
+ hasUnmatchedFallbackOnlyMessage,
16417
18309
  registerMessageAndCheckDuplicate,
16418
18310
  streamingImport,
16419
18311
  analyzeNewImport,
@@ -16454,6 +18346,11 @@ export {
16454
18346
  owner_profile_service_exports,
16455
18347
  createContactsService,
16456
18348
  contacts_exports,
18349
+ createCrossChatAnalysisService,
18350
+ preprocessCrossChatMessages,
18351
+ preprocessCrossChatSummaries,
18352
+ preprocessCrossChatLabel,
18353
+ cross_chat_analysis_exports,
16457
18354
  createPeopleRelationshipsService,
16458
18355
  relationships_exports,
16459
18356
  createGlobalInsightService,
@@ -16525,4 +18422,4 @@ export {
16525
18422
  hashImportBody,
16526
18423
  ImportIdempotencyCache
16527
18424
  };
16528
- //# sourceMappingURL=chunk-VREVDZ4Q.mjs.map
18425
+ //# sourceMappingURL=chunk-DQR2YS3I.mjs.map