@besales/mcp 0.48.0 → 1.0.0-beta.2

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 (587) hide show
  1. package/README.md +109 -164
  2. package/README.onprem.ru.md +35 -0
  3. package/bin/besales-mcp-http.js +9 -0
  4. package/bin/besales-onprem.mjs +19 -0
  5. package/dist/auth/connection-store.d.ts +3 -1
  6. package/dist/auth/connection-store.js +9 -3
  7. package/dist/auth/connection-store.js.map +1 -1
  8. package/dist/auth/oauth-client.d.ts +20 -27
  9. package/dist/auth/oauth-client.js +31 -23
  10. package/dist/auth/oauth-client.js.map +1 -1
  11. package/dist/auth/session-workspace.d.ts +1 -0
  12. package/dist/auth/session-workspace.js +10 -1
  13. package/dist/auth/session-workspace.js.map +1 -1
  14. package/dist/auth/token-storage.js +4 -1
  15. package/dist/auth/token-storage.js.map +1 -1
  16. package/dist/cli.d.ts +1 -0
  17. package/dist/cli.js +80 -19
  18. package/dist/cli.js.map +1 -1
  19. package/dist/constants/crm-catalog.d.ts +2 -0
  20. package/dist/constants/crm-catalog.js +3 -0
  21. package/dist/constants/crm-catalog.js.map +1 -0
  22. package/dist/http/api-client.d.ts +52 -7
  23. package/dist/http/api-client.js +131 -66
  24. package/dist/http/api-client.js.map +1 -1
  25. package/dist/http/feedback-document-error.d.ts +26 -0
  26. package/dist/http/feedback-document-error.js +22 -0
  27. package/dist/http/feedback-document-error.js.map +1 -0
  28. package/dist/http-server.d.ts +15 -0
  29. package/dist/http-server.js +378 -0
  30. package/dist/http-server.js.map +1 -0
  31. package/dist/instructions/server-instructions.d.ts +1 -1
  32. package/dist/instructions/server-instructions.js +6 -3
  33. package/dist/instructions/server-instructions.js.map +1 -1
  34. package/dist/network/constants.d.ts +25 -0
  35. package/dist/network/constants.js +26 -0
  36. package/dist/network/constants.js.map +1 -0
  37. package/dist/network/failed-verifications.d.ts +10 -0
  38. package/dist/network/failed-verifications.js +37 -0
  39. package/dist/network/failed-verifications.js.map +1 -0
  40. package/dist/network/http-test-fixture.d.ts +3 -0
  41. package/dist/network/http-test-fixture.js +86 -0
  42. package/dist/network/http-test-fixture.js.map +1 -0
  43. package/dist/network/known-keys.d.ts +10 -0
  44. package/dist/network/known-keys.js +38 -0
  45. package/dist/network/known-keys.js.map +1 -0
  46. package/dist/network/memory-credentials.d.ts +11 -0
  47. package/dist/network/memory-credentials.js +24 -0
  48. package/dist/network/memory-credentials.js.map +1 -0
  49. package/dist/network/rate-limit.d.ts +23 -0
  50. package/dist/network/rate-limit.js +62 -0
  51. package/dist/network/rate-limit.js.map +1 -0
  52. package/dist/network/rejected-keys.d.ts +9 -0
  53. package/dist/network/rejected-keys.js +39 -0
  54. package/dist/network/rejected-keys.js.map +1 -0
  55. package/dist/network/request-work.d.ts +8 -0
  56. package/dist/network/request-work.js +20 -0
  57. package/dist/network/request-work.js.map +1 -0
  58. package/dist/network/successful-keys.d.ts +17 -0
  59. package/dist/network/successful-keys.js +47 -0
  60. package/dist/network/successful-keys.js.map +1 -0
  61. package/dist/resources/concepts/external-execution.md +3 -1
  62. package/dist/resources/concepts/feedback-sheets.md +21 -12
  63. package/dist/resources/concepts/icp.md +8 -1
  64. package/dist/resources/concepts/sandbox.md +15 -6
  65. package/dist/resources/concepts/triggers.md +8 -0
  66. package/dist/resources/concepts/workbook-classification.md +104 -50
  67. package/dist/resources/docs/agent-behavior.md +51 -382
  68. package/dist/resources/docs/campaigns.md +9 -16
  69. package/dist/resources/docs/crm-integration.md +39 -523
  70. package/dist/resources/docs/files-and-uploads.md +23 -286
  71. package/dist/resources/docs/follow-up.md +47 -12
  72. package/dist/resources/docs/knowledge-base.md +52 -521
  73. package/dist/resources/docs/pipeline-builder.md +26 -216
  74. package/dist/resources/docs/pipeline-settings-deep.md +27 -220
  75. package/dist/resources/docs/platforms.md +106 -519
  76. package/dist/resources/docs/prompt-anatomy.md +39 -285
  77. package/dist/resources/docs/routing.md +8 -16
  78. package/dist/resources/docs/trigger-actions.md +40 -24
  79. package/dist/resources/docs/variables.md +8 -12
  80. package/dist/resources/operator-edit.contract.d.ts +7 -0
  81. package/dist/resources/operator-edit.contract.js +8 -0
  82. package/dist/resources/operator-edit.contract.js.map +1 -0
  83. package/dist/resources/registry.js +20 -4
  84. package/dist/resources/registry.js.map +1 -1
  85. package/dist/resources/workflows/audit-dialogues.md +23 -12
  86. package/dist/resources/workflows/compare-models.md +6 -6
  87. package/dist/resources/workflows/connect-crm-from-scratch.md +15 -12
  88. package/dist/resources/workflows/connect-datasource-from-scratch.md +5 -4
  89. package/dist/resources/workflows/extract-from-document.md +8 -6
  90. package/dist/resources/workflows/operator-edit.md +15 -0
  91. package/dist/resources/workflows/platform-setup-from-scratch.md +59 -70
  92. package/dist/resources/workflows/production-readiness-check.md +25 -15
  93. package/dist/schemas/mcp-tools.json +9902 -4734
  94. package/dist/schemas/voice-actions-api.contract.json +248 -0
  95. package/dist/shared/constants/broadcast-reason.constants.d.ts +2 -0
  96. package/dist/shared/constants/broadcast-reason.constants.js +47 -0
  97. package/dist/shared/constants/broadcast-reason.constants.js.map +1 -0
  98. package/dist/shared/constants/crm-stage-pool.constants.d.ts +2 -0
  99. package/dist/shared/constants/crm-stage-pool.constants.js +3 -0
  100. package/dist/shared/constants/crm-stage-pool.constants.js.map +1 -0
  101. package/dist/shared/constants/followup-note.constants.d.ts +3 -0
  102. package/dist/shared/constants/followup-note.constants.js +4 -0
  103. package/dist/shared/constants/followup-note.constants.js.map +1 -0
  104. package/dist/shared/constants/followup-session.constants.d.ts +2 -0
  105. package/dist/shared/constants/followup-session.constants.js +3 -0
  106. package/dist/shared/constants/followup-session.constants.js.map +1 -0
  107. package/dist/shared/constants/funnel-analytics.constants.d.ts +2 -0
  108. package/dist/shared/constants/funnel-analytics.constants.js +3 -0
  109. package/dist/shared/constants/funnel-analytics.constants.js.map +1 -0
  110. package/dist/shared/constants/message-filter.constants.d.ts +7 -0
  111. package/dist/shared/constants/message-filter.constants.js +8 -0
  112. package/dist/shared/constants/message-filter.constants.js.map +1 -0
  113. package/dist/shared/utils/trigger-repeat-mapping.d.ts +72 -0
  114. package/dist/shared/utils/trigger-repeat-mapping.js +53 -0
  115. package/dist/shared/utils/trigger-repeat-mapping.js.map +1 -0
  116. package/dist/skills/besales-dialogue-audit/SKILL.md +31 -20
  117. package/dist/tasks/task-state-store.js +5 -0
  118. package/dist/tasks/task-state-store.js.map +1 -1
  119. package/dist/tools/annotations.js +4 -3
  120. package/dist/tools/annotations.js.map +1 -1
  121. package/dist/tools/definitions/agent-design.d.ts +78 -21
  122. package/dist/tools/definitions/agent-design.js +246 -90
  123. package/dist/tools/definitions/agent-design.js.map +1 -1
  124. package/dist/tools/definitions/agent-model-notice.d.ts +5 -0
  125. package/dist/tools/definitions/agent-model-notice.js +31 -0
  126. package/dist/tools/definitions/agent-model-notice.js.map +1 -0
  127. package/dist/tools/definitions/agent-models.d.ts +5 -0
  128. package/dist/tools/definitions/agent-models.js +26 -0
  129. package/dist/tools/definitions/agent-models.js.map +1 -0
  130. package/dist/tools/definitions/agent-settings.d.ts +85 -0
  131. package/dist/tools/definitions/agent-settings.js +187 -0
  132. package/dist/tools/definitions/agent-settings.js.map +1 -0
  133. package/dist/tools/definitions/ai-costs.d.ts +12 -0
  134. package/dist/tools/definitions/ai-costs.js +142 -0
  135. package/dist/tools/definitions/ai-costs.js.map +1 -0
  136. package/dist/tools/definitions/api-tool-request.d.ts +15 -0
  137. package/dist/tools/definitions/api-tool-request.js +2 -0
  138. package/dist/tools/definitions/api-tool-request.js.map +1 -0
  139. package/dist/tools/definitions/behavior-shipped-form.d.ts +182 -0
  140. package/dist/tools/definitions/behavior-shipped-form.js +185 -0
  141. package/dist/tools/definitions/behavior-shipped-form.js.map +1 -0
  142. package/dist/tools/definitions/broadcast-alerts.d.ts +6 -0
  143. package/dist/tools/definitions/broadcast-alerts.js +72 -0
  144. package/dist/tools/definitions/broadcast-alerts.js.map +1 -0
  145. package/dist/tools/definitions/broadcast-analytics.d.ts +102 -18
  146. package/dist/tools/definitions/broadcast-analytics.js +732 -431
  147. package/dist/tools/definitions/broadcast-analytics.js.map +1 -1
  148. package/dist/tools/definitions/broadcast-scheduler.d.ts +10 -13
  149. package/dist/tools/definitions/broadcast-scheduler.js +196 -154
  150. package/dist/tools/definitions/broadcast-scheduler.js.map +1 -1
  151. package/dist/tools/definitions/call-operator-task.schema.d.ts +11 -0
  152. package/dist/tools/definitions/call-operator-task.schema.js +30 -0
  153. package/dist/tools/definitions/call-operator-task.schema.js.map +1 -0
  154. package/dist/tools/definitions/contacts.d.ts +3 -0
  155. package/dist/tools/definitions/contacts.js +57 -31
  156. package/dist/tools/definitions/contacts.js.map +1 -1
  157. package/dist/tools/definitions/crm-platform.d.ts +28 -41
  158. package/dist/tools/definitions/crm-platform.js +235 -368
  159. package/dist/tools/definitions/crm-platform.js.map +1 -1
  160. package/dist/tools/definitions/crm-stage-pool.messages.d.ts +39 -0
  161. package/dist/tools/definitions/crm-stage-pool.messages.js +43 -0
  162. package/dist/tools/definitions/crm-stage-pool.messages.js.map +1 -0
  163. package/dist/tools/definitions/crm-stage-pool.schema.d.ts +64 -0
  164. package/dist/tools/definitions/crm-stage-pool.schema.js +109 -0
  165. package/dist/tools/definitions/crm-stage-pool.schema.js.map +1 -0
  166. package/dist/tools/definitions/custom-reports.d.ts +12 -0
  167. package/dist/tools/definitions/custom-reports.js +68 -0
  168. package/dist/tools/definitions/custom-reports.js.map +1 -0
  169. package/dist/tools/definitions/datasource.d.ts +2 -1
  170. package/dist/tools/definitions/datasource.js +28 -13
  171. package/dist/tools/definitions/datasource.js.map +1 -1
  172. package/dist/tools/definitions/dialog-review-findings-response.d.ts +14 -0
  173. package/dist/tools/definitions/dialog-review-findings-response.js +23 -0
  174. package/dist/tools/definitions/dialog-review-findings-response.js.map +1 -0
  175. package/dist/tools/definitions/dialog-review-safe-response.d.ts +5 -0
  176. package/dist/tools/definitions/dialog-review-safe-response.js +152 -0
  177. package/dist/tools/definitions/dialog-review-safe-response.js.map +1 -0
  178. package/dist/tools/definitions/dialog-review.d.ts +49 -0
  179. package/dist/tools/definitions/dialog-review.js +383 -0
  180. package/dist/tools/definitions/dialog-review.js.map +1 -0
  181. package/dist/tools/definitions/dialogue-analysis.d.ts +46 -0
  182. package/dist/tools/definitions/dialogue-analysis.js +170 -0
  183. package/dist/tools/definitions/dialogue-analysis.js.map +1 -0
  184. package/dist/tools/definitions/dialogue-audit-agent-scope.d.ts +7 -0
  185. package/dist/tools/definitions/dialogue-audit-agent-scope.js +41 -0
  186. package/dist/tools/definitions/dialogue-audit-agent-scope.js.map +1 -0
  187. package/dist/tools/definitions/dialogue-audit-alias.d.ts +67 -0
  188. package/dist/tools/definitions/dialogue-audit-alias.js +559 -0
  189. package/dist/tools/definitions/dialogue-audit-alias.js.map +1 -0
  190. package/dist/tools/definitions/dialogue-export.d.ts +7 -1
  191. package/dist/tools/definitions/dialogue-export.js +89 -32
  192. package/dist/tools/definitions/dialogue-export.js.map +1 -1
  193. package/dist/tools/definitions/dialogue-followup-state.d.ts +9 -0
  194. package/dist/tools/definitions/dialogue-followup-state.js +69 -0
  195. package/dist/tools/definitions/dialogue-followup-state.js.map +1 -0
  196. package/dist/tools/definitions/edit-version-transport.test-fixtures.d.ts +13 -0
  197. package/dist/tools/definitions/edit-version-transport.test-fixtures.js +292 -0
  198. package/dist/tools/definitions/edit-version-transport.test-fixtures.js.map +1 -0
  199. package/dist/tools/definitions/edit-version.d.ts +5 -0
  200. package/dist/tools/definitions/edit-version.js +20 -0
  201. package/dist/tools/definitions/edit-version.js.map +1 -0
  202. package/dist/tools/definitions/external-api-channels.js +17 -15
  203. package/dist/tools/definitions/external-api-channels.js.map +1 -1
  204. package/dist/tools/definitions/feedback-document-access.d.ts +16 -0
  205. package/dist/tools/definitions/feedback-document-access.js +63 -0
  206. package/dist/tools/definitions/feedback-document-access.js.map +1 -0
  207. package/dist/tools/definitions/fixtures/shipped-setup-status.d.ts +3 -0
  208. package/dist/tools/definitions/fixtures/shipped-setup-status.js +9 -0
  209. package/dist/tools/definitions/fixtures/shipped-setup-status.js.map +1 -0
  210. package/dist/tools/definitions/follow-up-config.d.ts +79 -0
  211. package/dist/tools/definitions/follow-up-config.js +258 -0
  212. package/dist/tools/definitions/follow-up-config.js.map +1 -0
  213. package/dist/tools/definitions/follow-up-funnel-test.d.ts +4 -3
  214. package/dist/tools/definitions/follow-up-funnel-test.js +65 -52
  215. package/dist/tools/definitions/follow-up-funnel-test.js.map +1 -1
  216. package/dist/tools/definitions/follow-up-promotion-instructions.d.ts +2 -0
  217. package/dist/tools/definitions/follow-up-promotion-instructions.js +7 -0
  218. package/dist/tools/definitions/follow-up-promotion-instructions.js.map +1 -0
  219. package/dist/tools/definitions/follow-up-setup.d.ts +4 -28
  220. package/dist/tools/definitions/follow-up-setup.js +20 -87
  221. package/dist/tools/definitions/follow-up-setup.js.map +1 -1
  222. package/dist/tools/definitions/follow-up.d.ts +20 -122
  223. package/dist/tools/definitions/follow-up.js +200 -357
  224. package/dist/tools/definitions/follow-up.js.map +1 -1
  225. package/dist/tools/definitions/followup-getcourse-compat.d.ts +29 -0
  226. package/dist/tools/definitions/followup-getcourse-compat.js +62 -0
  227. package/dist/tools/definitions/followup-getcourse-compat.js.map +1 -0
  228. package/dist/tools/definitions/followup-sequence-config.schema.d.ts +10 -0
  229. package/dist/tools/definitions/followup-sequence-config.schema.js +14 -0
  230. package/dist/tools/definitions/followup-sequence-config.schema.js.map +1 -0
  231. package/dist/tools/definitions/funnel-analytics.d.ts +18 -0
  232. package/dist/tools/definitions/funnel-analytics.js +150 -0
  233. package/dist/tools/definitions/funnel-analytics.js.map +1 -0
  234. package/dist/tools/definitions/getcourse-anchor-refresh.d.ts +5 -9
  235. package/dist/tools/definitions/getcourse-anchor-refresh.js +26 -31
  236. package/dist/tools/definitions/getcourse-anchor-refresh.js.map +1 -1
  237. package/dist/tools/definitions/getcourse-cold-outbound-settings.d.ts +46 -0
  238. package/dist/tools/definitions/getcourse-cold-outbound-settings.js +37 -0
  239. package/dist/tools/definitions/getcourse-cold-outbound-settings.js.map +1 -0
  240. package/dist/tools/definitions/getcourse-cold-welcome.d.ts +14 -29
  241. package/dist/tools/definitions/getcourse-cold-welcome.js +180 -254
  242. package/dist/tools/definitions/getcourse-cold-welcome.js.map +1 -1
  243. package/dist/tools/definitions/getcourse-wave-welcome.messages.d.ts +10 -0
  244. package/dist/tools/definitions/getcourse-wave-welcome.messages.js +17 -0
  245. package/dist/tools/definitions/getcourse-wave-welcome.messages.js.map +1 -0
  246. package/dist/tools/definitions/help.js.map +1 -1
  247. package/dist/tools/definitions/icp-import-source.d.ts +75 -0
  248. package/dist/tools/definitions/icp-import-source.js +88 -0
  249. package/dist/tools/definitions/icp-import-source.js.map +1 -0
  250. package/dist/tools/definitions/inbound-analytics.d.ts +13 -0
  251. package/dist/tools/definitions/inbound-analytics.js +55 -0
  252. package/dist/tools/definitions/inbound-analytics.js.map +1 -0
  253. package/dist/tools/definitions/instagram-comment.templates.d.ts +19 -0
  254. package/dist/tools/definitions/instagram-comment.templates.js +34 -0
  255. package/dist/tools/definitions/instagram-comment.templates.js.map +1 -0
  256. package/dist/tools/definitions/instagram-settings-compat.d.ts +33 -0
  257. package/dist/tools/definitions/instagram-settings-compat.js +7 -0
  258. package/dist/tools/definitions/instagram-settings-compat.js.map +1 -0
  259. package/dist/tools/definitions/knowledge-chunking.d.ts +42 -0
  260. package/dist/tools/definitions/knowledge-chunking.js +83 -0
  261. package/dist/tools/definitions/knowledge-chunking.js.map +1 -0
  262. package/dist/tools/definitions/knowledge-space-search-fields.d.ts +12 -0
  263. package/dist/tools/definitions/knowledge-space-search-fields.js +30 -0
  264. package/dist/tools/definitions/knowledge-space-search-fields.js.map +1 -0
  265. package/dist/tools/definitions/knowledge.d.ts +24 -14
  266. package/dist/tools/definitions/knowledge.js +342 -233
  267. package/dist/tools/definitions/knowledge.js.map +1 -1
  268. package/dist/tools/definitions/legacy-trigger-repeat.d.ts +17 -0
  269. package/dist/tools/definitions/legacy-trigger-repeat.js +53 -0
  270. package/dist/tools/definitions/legacy-trigger-repeat.js.map +1 -0
  271. package/dist/tools/definitions/link-tracking.d.ts +8 -0
  272. package/dist/tools/definitions/link-tracking.js +141 -38
  273. package/dist/tools/definitions/link-tracking.js.map +1 -1
  274. package/dist/tools/definitions/mailing-variant-content.d.ts +75 -0
  275. package/dist/tools/definitions/mailing-variant-content.js +73 -0
  276. package/dist/tools/definitions/mailing-variant-content.js.map +1 -0
  277. package/dist/tools/definitions/model-comparison.d.ts +1 -0
  278. package/dist/tools/definitions/model-comparison.js +4 -5
  279. package/dist/tools/definitions/model-comparison.js.map +1 -1
  280. package/dist/tools/definitions/money-currency.d.ts +5 -0
  281. package/dist/tools/definitions/money-currency.js +10 -0
  282. package/dist/tools/definitions/money-currency.js.map +1 -0
  283. package/dist/tools/definitions/operator-workflow.d.ts +5 -5
  284. package/dist/tools/definitions/operator-workflow.js +80 -27
  285. package/dist/tools/definitions/operator-workflow.js.map +1 -1
  286. package/dist/tools/definitions/platform-funnel-analytics.d.ts +42 -0
  287. package/dist/tools/definitions/platform-funnel-analytics.js +72 -0
  288. package/dist/tools/definitions/platform-funnel-analytics.js.map +1 -0
  289. package/dist/tools/definitions/platform-getcourse-wave-field-bindings.d.ts +42 -0
  290. package/dist/tools/definitions/platform-getcourse-wave-field-bindings.js +51 -0
  291. package/dist/tools/definitions/platform-getcourse-wave-field-bindings.js.map +1 -0
  292. package/dist/tools/definitions/platform-settings-limits.d.ts +5 -0
  293. package/dist/tools/definitions/platform-settings-limits.js +6 -0
  294. package/dist/tools/definitions/platform-settings-limits.js.map +1 -0
  295. package/dist/tools/definitions/platform-settings-shipped-form.d.ts +37 -0
  296. package/dist/tools/definitions/platform-settings-shipped-form.js +310 -0
  297. package/dist/tools/definitions/platform-settings-shipped-form.js.map +1 -0
  298. package/dist/tools/definitions/platform-settings-update.d.ts +300 -0
  299. package/dist/tools/definitions/platform-settings-update.js +134 -0
  300. package/dist/tools/definitions/platform-settings-update.js.map +1 -0
  301. package/dist/tools/definitions/{getcourse-wave.d.ts → platform-setup-wave.d.ts} +75 -6
  302. package/dist/tools/definitions/platform-setup-wave.js +1079 -0
  303. package/dist/tools/definitions/platform-setup-wave.js.map +1 -0
  304. package/dist/tools/definitions/platform-setup.d.ts +686 -224
  305. package/dist/tools/definitions/platform-setup.js +313 -752
  306. package/dist/tools/definitions/platform-setup.js.map +1 -1
  307. package/dist/tools/definitions/platform-widget-page-rules.schema.d.ts +10 -0
  308. package/dist/tools/definitions/platform-widget-page-rules.schema.js +7 -0
  309. package/dist/tools/definitions/platform-widget-page-rules.schema.js.map +1 -0
  310. package/dist/tools/definitions/prompt-authoring.constants.d.ts +2 -0
  311. package/dist/tools/definitions/prompt-authoring.constants.js +3 -0
  312. package/dist/tools/definitions/prompt-authoring.constants.js.map +1 -0
  313. package/dist/tools/definitions/prompt-batch-controls.d.ts +5 -0
  314. package/dist/tools/definitions/prompt-batch-controls.js +40 -0
  315. package/dist/tools/definitions/prompt-batch-controls.js.map +1 -0
  316. package/dist/tools/definitions/prompt-draft-authoring.d.ts +5 -0
  317. package/dist/tools/definitions/prompt-draft-authoring.js +24 -0
  318. package/dist/tools/definitions/prompt-draft-authoring.js.map +1 -0
  319. package/dist/tools/definitions/prompt-text-projection.d.ts +11 -0
  320. package/dist/tools/definitions/prompt-text-projection.js +62 -0
  321. package/dist/tools/definitions/prompt-text-projection.js.map +1 -0
  322. package/dist/tools/definitions/reviewer-paid.d.ts +19 -0
  323. package/dist/tools/definitions/reviewer-paid.js +115 -0
  324. package/dist/tools/definitions/reviewer-paid.js.map +1 -0
  325. package/dist/tools/definitions/routing-condition-projection.d.ts +31 -0
  326. package/dist/tools/definitions/routing-condition-projection.js +97 -0
  327. package/dist/tools/definitions/routing-condition-projection.js.map +1 -0
  328. package/dist/tools/definitions/schedule-selection-hints.d.ts +3 -0
  329. package/dist/tools/definitions/schedule-selection-hints.js +42 -0
  330. package/dist/tools/definitions/schedule-selection-hints.js.map +1 -0
  331. package/dist/tools/definitions/{scheduled-tasks.d.ts → scheduled-task-alias.d.ts} +12 -10
  332. package/dist/tools/definitions/scheduled-task-alias.js +207 -0
  333. package/dist/tools/definitions/scheduled-task-alias.js.map +1 -0
  334. package/dist/tools/definitions/session.js +3 -3
  335. package/dist/tools/definitions/session.js.map +1 -1
  336. package/dist/tools/definitions/setup-init-operation.schema.d.ts +2 -0
  337. package/dist/tools/definitions/setup-init-operation.schema.js +6 -0
  338. package/dist/tools/definitions/setup-init-operation.schema.js.map +1 -0
  339. package/dist/tools/definitions/shared-input.helpers.d.ts +51 -0
  340. package/dist/tools/definitions/shared-input.helpers.js +323 -0
  341. package/dist/tools/definitions/shared-input.helpers.js.map +1 -0
  342. package/dist/tools/definitions/shared.d.ts +396 -307
  343. package/dist/tools/definitions/shared.js +430 -652
  344. package/dist/tools/definitions/shared.js.map +1 -1
  345. package/dist/tools/definitions/shipped-output-names.d.ts +19 -0
  346. package/dist/tools/definitions/shipped-output-names.js +123 -0
  347. package/dist/tools/definitions/shipped-output-names.js.map +1 -0
  348. package/dist/tools/definitions/task-state.d.ts +5 -5
  349. package/dist/tools/definitions/task-state.js +2 -1
  350. package/dist/tools/definitions/task-state.js.map +1 -1
  351. package/dist/tools/definitions/telegram-personal-capacity.d.ts +4 -0
  352. package/dist/tools/definitions/telegram-personal-capacity.js +18 -0
  353. package/dist/tools/definitions/telegram-personal-capacity.js.map +1 -0
  354. package/dist/tools/definitions/telegram-pool-policy.d.ts +8 -0
  355. package/dist/tools/definitions/telegram-pool-policy.js +56 -0
  356. package/dist/tools/definitions/telegram-pool-policy.js.map +1 -0
  357. package/dist/tools/definitions/touch-history-alias.d.ts +4 -0
  358. package/dist/tools/definitions/touch-history-alias.js +99 -0
  359. package/dist/tools/definitions/touch-history-alias.js.map +1 -0
  360. package/dist/tools/definitions/trigger-action-item.schema.d.ts +254 -0
  361. package/dist/tools/definitions/trigger-action-item.schema.js +121 -0
  362. package/dist/tools/definitions/trigger-action-item.schema.js.map +1 -0
  363. package/dist/tools/definitions/trigger-argument.schema.d.ts +14 -0
  364. package/dist/tools/definitions/trigger-argument.schema.js +23 -0
  365. package/dist/tools/definitions/trigger-argument.schema.js.map +1 -0
  366. package/dist/tools/definitions/trigger-claim-alias.d.ts +10 -0
  367. package/dist/tools/definitions/trigger-claim-alias.js +90 -0
  368. package/dist/tools/definitions/trigger-claim-alias.js.map +1 -0
  369. package/dist/tools/definitions/trigger-echo-projection.d.ts +1 -0
  370. package/dist/tools/definitions/trigger-echo-projection.js +41 -0
  371. package/dist/tools/definitions/trigger-echo-projection.js.map +1 -0
  372. package/dist/tools/definitions/trigger-upsert.tool.d.ts +29 -0
  373. package/dist/tools/definitions/trigger-upsert.tool.js +122 -0
  374. package/dist/tools/definitions/trigger-upsert.tool.js.map +1 -0
  375. package/dist/tools/definitions/v15-port-descriptions.d.ts +11 -0
  376. package/dist/tools/definitions/v15-port-descriptions.js +30 -0
  377. package/dist/tools/definitions/v15-port-descriptions.js.map +1 -0
  378. package/dist/tools/definitions/variables.d.ts +44 -0
  379. package/dist/tools/definitions/variables.js +310 -0
  380. package/dist/tools/definitions/variables.js.map +1 -0
  381. package/dist/tools/definitions/voice-action-input.d.ts +9 -0
  382. package/dist/tools/definitions/voice-action-input.js +141 -0
  383. package/dist/tools/definitions/voice-action-input.js.map +1 -0
  384. package/dist/tools/definitions/widget-customization-compat.d.ts +51 -0
  385. package/dist/tools/definitions/widget-customization-compat.js +23 -0
  386. package/dist/tools/definitions/widget-customization-compat.js.map +1 -0
  387. package/dist/tools/definitions.d.ts +1089 -636
  388. package/dist/tools/definitions.js +57 -27
  389. package/dist/tools/definitions.js.map +1 -1
  390. package/dist/tools/description-localization.js +3 -2
  391. package/dist/tools/description-localization.js.map +1 -1
  392. package/dist/tools/follow-up-request.d.ts +19 -0
  393. package/dist/tools/follow-up-request.js +81 -0
  394. package/dist/tools/follow-up-request.js.map +1 -0
  395. package/dist/tools/followup-revision.d.ts +4 -0
  396. package/dist/tools/followup-revision.js +29 -0
  397. package/dist/tools/followup-revision.js.map +1 -0
  398. package/dist/tools/incomplete-list.js +4 -0
  399. package/dist/tools/incomplete-list.js.map +1 -1
  400. package/dist/tools/inventory.js +47 -11
  401. package/dist/tools/inventory.js.map +1 -1
  402. package/dist/tools/operation-recovery.d.ts +4 -0
  403. package/dist/tools/operation-recovery.js +33 -0
  404. package/dist/tools/operation-recovery.js.map +1 -0
  405. package/dist/tools/registry-mapping-primary.cases.d.ts +2 -0
  406. package/dist/tools/registry-mapping-primary.cases.js +753 -0
  407. package/dist/tools/registry-mapping-primary.cases.js.map +1 -0
  408. package/dist/tools/registry-mapping-secondary.cases.d.ts +2 -0
  409. package/dist/tools/registry-mapping-secondary.cases.js +626 -0
  410. package/dist/tools/registry-mapping-secondary.cases.js.map +1 -0
  411. package/dist/tools/registry-mapping.cases.d.ts +8 -0
  412. package/dist/tools/registry-mapping.cases.js +13 -0
  413. package/dist/tools/registry-mapping.cases.js.map +1 -0
  414. package/dist/tools/registry-test-fixtures.d.ts +50 -0
  415. package/dist/tools/registry-test-fixtures.js +120 -0
  416. package/dist/tools/registry-test-fixtures.js.map +1 -0
  417. package/dist/tools/registry.d.ts +2 -0
  418. package/dist/tools/registry.js +82 -52
  419. package/dist/tools/registry.js.map +1 -1
  420. package/dist/tools/result.d.ts +1 -0
  421. package/dist/tools/result.js +90 -22
  422. package/dist/tools/result.js.map +1 -1
  423. package/dist/tools/toolsets.d.ts +16 -0
  424. package/dist/tools/toolsets.js +25 -1
  425. package/dist/tools/toolsets.js.map +1 -1
  426. package/dist/types/api-contract.gen.d.ts +105875 -22205
  427. package/dist/utils/credential-signatures.d.ts +3 -0
  428. package/dist/utils/credential-signatures.js +10 -0
  429. package/dist/utils/credential-signatures.js.map +1 -0
  430. package/dist/utils/data-source-column-name.d.ts +10 -0
  431. package/dist/utils/data-source-column-name.js +36 -0
  432. package/dist/utils/data-source-column-name.js.map +1 -0
  433. package/dist/utils/logger.d.ts +1 -0
  434. package/dist/utils/logger.js +32 -8
  435. package/dist/utils/logger.js.map +1 -1
  436. package/dist/utils/secret-read-boundary.d.ts +2 -0
  437. package/dist/utils/secret-read-boundary.js +88 -0
  438. package/dist/utils/secret-read-boundary.js.map +1 -0
  439. package/dist/utils/setup-secrets.d.ts +4 -0
  440. package/dist/utils/setup-secrets.js +40 -0
  441. package/dist/utils/setup-secrets.js.map +1 -0
  442. package/docs/agent-models.md +9 -0
  443. package/docs/agent-usage-guide.full.ru.md +1 -4
  444. package/docs/analytics-contract.md +67 -0
  445. package/docs/async-operation-lifecycle.md +10 -3
  446. package/docs/connect.ru.md +144 -4
  447. package/docs/contact-company-profile.md +17 -0
  448. package/docs/contact-recommendations.md +7 -0
  449. package/docs/crm-stage-pool.md +73 -0
  450. package/docs/customer-data.md +61 -0
  451. package/docs/edit-versions.md +36 -0
  452. package/docs/failure-semantics.md +17 -9
  453. package/docs/help-navigation.md +15 -42
  454. package/docs/host-compatibility-matrix.md +72 -93
  455. package/docs/host-setup.md +33 -35
  456. package/docs/invariants/public-api-and-contracts.md +16 -0
  457. package/docs/legacy-action-settings.md +21 -0
  458. package/docs/main-transfer-dependencies.md +38 -0
  459. package/docs/mcp2-parity.md +50 -0
  460. package/docs/modules/analytics.md +79 -0
  461. package/docs/modules/besales-mcp.md +137 -0
  462. package/docs/modules/dialog-review-report.md +38 -0
  463. package/docs/modules/dialogue-followup-adapters.md +39 -0
  464. package/docs/modules/message-reactions.md +17 -0
  465. package/docs/modules/network-mode.md +284 -0
  466. package/docs/modules/prompt-batch-and-dialogue-controls.md +69 -0
  467. package/docs/pii-policy.md +21 -16
  468. package/docs/response-size-budget.md +197 -7
  469. package/docs/secret-read-boundary.md +102 -0
  470. package/docs/task-state.md +7 -2
  471. package/docs/telegram-pool-policy.md +19 -0
  472. package/docs/tool-effects.md +15 -9
  473. package/docs/tool-surface.md +205 -4
  474. package/docs/voice-actions-and-inbox.md +95 -0
  475. package/docs/wazzup-instagram-comments.md +26 -0
  476. package/package.json +17 -10
  477. package/scripts/api-source-loader.spec.mjs +40 -0
  478. package/scripts/audit-bridge-controls.mjs +129 -0
  479. package/scripts/audit-revert-mutants.mjs +107 -0
  480. package/scripts/bridge-api-input.spec.mjs +98 -0
  481. package/scripts/bridge-controls-mutants.mjs +130 -0
  482. package/scripts/brx3-shipped-refusals.spec.mjs +149 -0
  483. package/scripts/brx4-ledger-payload.spec.mjs +127 -0
  484. package/scripts/build-onprem.mjs +108 -0
  485. package/scripts/connect-clients.sh +141 -0
  486. package/scripts/connect-scopes.mjs +166 -0
  487. package/scripts/connect-scopes.spec.mjs +83 -0
  488. package/scripts/emit-personal-link-carrier.mjs +31 -0
  489. package/scripts/emit-setup-init-schemas.mjs +24 -0
  490. package/scripts/emit-tool-routes.mjs +878 -0
  491. package/scripts/emit-tool-routes.spec.mjs +300 -0
  492. package/scripts/followup-registry.spec.mjs +107 -0
  493. package/scripts/generate-crm-rotation-contract.mjs +98 -0
  494. package/scripts/generate-followup-session-contract.mjs +16 -0
  495. package/scripts/icp-import-verification/mutate.cjs +70 -0
  496. package/scripts/import-secret-read-contract.mjs +53 -0
  497. package/scripts/install-claude-desktop.js +1 -1
  498. package/scripts/lag-decisions.mjs +37 -0
  499. package/scripts/lag-decisions.spec.mjs +87 -0
  500. package/scripts/lag-open-task-owners.mjs +46 -0
  501. package/scripts/lag-open-task-owners.spec.mjs +78 -0
  502. package/scripts/lag-payload.mjs +10 -0
  503. package/scripts/legacy-action-lag.spec.mjs +22 -0
  504. package/scripts/legacy-field-refusal.shipped.spec.mjs +99 -0
  505. package/scripts/mcpcompat-handler.mjs +34 -0
  506. package/scripts/mcpcompat-loader.mjs +36 -0
  507. package/scripts/mcpcompat-typescript.mjs +3 -0
  508. package/scripts/platform-settings-shipped-parity.spec.mjs +89 -0
  509. package/scripts/prbr-round1-recheck.spec.mjs +605 -0
  510. package/scripts/prbr-shipped-compatibility.spec.mjs +276 -0
  511. package/scripts/publish-mcp-registry.mjs +98 -0
  512. package/scripts/publish-mcp-registry.spec.mjs +68 -0
  513. package/scripts/record-scheduled-task-call.mjs +26 -0
  514. package/scripts/refusal-probe.mjs +17 -0
  515. package/scripts/refusal-probe.spec.mjs +29 -0
  516. package/scripts/registry-diff.mjs +502 -0
  517. package/scripts/registry-diff.spec.mjs +787 -0
  518. package/scripts/request-query.mjs +43 -0
  519. package/scripts/request-query.spec.mjs +82 -0
  520. package/scripts/route-lag.mjs +23 -0
  521. package/scripts/route-lag.spec.mjs +30 -0
  522. package/scripts/sample-args.mjs +174 -0
  523. package/scripts/sample-args.spec.mjs +160 -0
  524. package/scripts/shipped-definitions.mjs +197 -0
  525. package/scripts/shipped-definitions.spec.mjs +57 -0
  526. package/scripts/shipped-inputs.spec.mjs +136 -0
  527. package/scripts/shipped-recordings.spec.mjs +19 -0
  528. package/scripts/shipped-ref.d.mts +5 -0
  529. package/scripts/shipped-ref.mjs +20 -0
  530. package/scripts/shipped-ref.spec.mjs +59 -0
  531. package/scripts/shipped-registry.mjs +107 -0
  532. package/scripts/shipped-sample-candidates.mjs +69 -0
  533. package/scripts/shipped-sample-candidates.spec.mjs +117 -0
  534. package/scripts/shipped-sample.mjs +868 -0
  535. package/scripts/shipped-sample.spec.mjs +451 -0
  536. package/scripts/sync-agent-model-schemas.mjs +72 -0
  537. package/scripts/sync-api-contracts.mjs +85 -0
  538. package/scripts/sync-api-contracts.spec.mjs +144 -0
  539. package/scripts/sync-contract-ledger-schemas.mjs +161 -0
  540. package/scripts/sync-crm-stage-pool-schema.mjs +85 -0
  541. package/scripts/sync-prompt-draft-schema.mjs +92 -0
  542. package/scripts/sync-route-map.mjs +94 -0
  543. package/scripts/sync-schema-descriptions.mjs +25 -0
  544. package/scripts/sync-tool-inventory.mjs +27 -0
  545. package/scripts/sync-trigger-argument-schema.mjs +121 -0
  546. package/scripts/sync-trigger-repeat-mapping.mjs +12 -0
  547. package/scripts/sync-voice-actions-contract.mjs +67 -0
  548. package/scripts/sync-voice-actions-tools.mjs +97 -0
  549. package/scripts/sync-wave-id-schemas.mjs +55 -0
  550. package/scripts/tool-input-schemas.mjs +46 -0
  551. package/scripts/tool-input-schemas.spec.mjs +50 -0
  552. package/scripts/verify-followup-adapters.mjs +235 -0
  553. package/scripts/verify-secret-read-import.mjs +84 -0
  554. package/scripts/verify-secret-read-mutants.mjs +143 -0
  555. package/dist/tools/definitions/analytics.d.ts +0 -40
  556. package/dist/tools/definitions/analytics.js +0 -233
  557. package/dist/tools/definitions/analytics.js.map +0 -1
  558. package/dist/tools/definitions/broadcast-mailing-variants.d.ts +0 -41
  559. package/dist/tools/definitions/broadcast-mailing-variants.js +0 -194
  560. package/dist/tools/definitions/broadcast-mailing-variants.js.map +0 -1
  561. package/dist/tools/definitions/dialogue-audit.d.ts +0 -38
  562. package/dist/tools/definitions/dialogue-audit.js +0 -292
  563. package/dist/tools/definitions/dialogue-audit.js.map +0 -1
  564. package/dist/tools/definitions/getcourse-wave.js +0 -519
  565. package/dist/tools/definitions/getcourse-wave.js.map +0 -1
  566. package/dist/tools/definitions/operational-overview.d.ts +0 -3
  567. package/dist/tools/definitions/operational-overview.js +0 -23
  568. package/dist/tools/definitions/operational-overview.js.map +0 -1
  569. package/dist/tools/definitions/scheduled-tasks.js +0 -173
  570. package/dist/tools/definitions/scheduled-tasks.js.map +0 -1
  571. package/dist/tools/definitions/trigger-contact-claims.d.ts +0 -7
  572. package/dist/tools/definitions/trigger-contact-claims.js +0 -60
  573. package/dist/tools/definitions/trigger-contact-claims.js.map +0 -1
  574. package/dist/tools/registry-mapping-cases-core.d.ts +0 -38
  575. package/dist/tools/registry-mapping-cases-core.js +0 -963
  576. package/dist/tools/registry-mapping-cases-core.js.map +0 -1
  577. package/dist/tools/registry-mapping-cases-platform.d.ts +0 -2
  578. package/dist/tools/registry-mapping-cases-platform.js +0 -1052
  579. package/dist/tools/registry-mapping-cases-platform.js.map +0 -1
  580. package/dist/tools/trigger-action-config.d.ts +0 -49
  581. package/dist/tools/trigger-action-config.js +0 -171
  582. package/dist/tools/trigger-action-config.js.map +0 -1
  583. package/docs/evals/model-behavior-scorecard.md +0 -80
  584. package/docs/operation-coverage.md +0 -114
  585. package/scripts/mock-credentials.js +0 -77
  586. package/scripts/model-eval-server.js +0 -213
  587. package/scripts/run-model-eval.js +0 -447
@@ -1,11 +1,18 @@
1
+ import { SCHEDULE_GUARD_HINT, SCHEDULE_BLOCKS_HINT } from './v15-port-descriptions.js';
2
+ // Планировщик рассылок: расписания (список, сохранение, удаление, история прогонов) и правило отбора
3
+ // сегмента. Схемы инструментов — ровно отгруженные: клиент, зовущий инструмент сегодня, не получает
4
+ // отказа ни на одном поле, которое принимал. Тело запроса — в именах сервера v2 (канал, а не
5
+ // «площадка»). «Запустить сейчас» и настройка источника фактов мосту не выставлены намеренно, как и у
6
+ // отгруженного: необратимая массовая отправка и адрес источника остаются за человеком.
1
7
  import { z } from 'zod';
8
+ import { SCHEDULE_EXCLUSION_HINT, SCHEDULE_POOL_GROUPS_HINT, SCHEDULE_CLOSE_CLASS_HINT } from './schedule-selection-hints.js';
2
9
  import { buildListQuery, compactObject, defineTool, getcourseTransportEnum, uuidSchema, withInstructions, } from './shared.js';
3
- /** Границы — паритет с DTO бэкенда: расхождение даёт 400 уже после подтверждения оператором. */
10
+ /** Границы расписания — паритет с DTO бэкенда: расхождение даёт 400 уже после подтверждения человеком. */
4
11
  const DAILY_CAP_MAX = 5000;
5
12
  /**
6
- * ⚠️⚠️ 78, а не 365. Бэкенд выводит потолок из потолка источника фактов
7
- * (92 дня окна минус 14 дней добавки на созревание серии), и значение 79…365 проходило zod,
8
- * а падало 400 уже ПОСЛЕ того, как оператор подтвердил расписание.
13
+ * ⚠️⚠️ 78, а не 365. Сервер выводит потолок из потолка окна источника фактов (92 дня) минус запас
14
+ * окна регистраций (14 дней): значение 79…365 проходило бы схему и падало 400 уже ПОСЛЕ того, как
15
+ * человек подтвердил расписание.
9
16
  */
10
17
  const FRESHNESS_DAYS_MAX = 78;
11
18
  const COOLDOWN_DAYS_MAX = 365;
@@ -13,21 +20,22 @@ const COOLDOWN_DAYS_MAX = 365;
13
20
  const RESEND_PAUSE_DAYS_MAX = 365;
14
21
  /** Потолок списка «не писать после сегментов» — серверный `SCHEDULE_SKIP_AFTER_SEGMENTS_MAX`. */
15
22
  const SKIP_AFTER_SEGMENTS_MAX = 20;
23
+ /** Потолок списка каналов — отгруженный. */
24
+ const CHANNELS_MAX = 20;
16
25
  /**
17
26
  * Окно свежести строк без расписания в «Пуле сегодня» — паритет с `POOL_TODAY_WINDOW_DAYS_MAX`
18
27
  * бэкенда: окно регистраций строки (окно + 14 суток запаса) обязано уложиться в общую выкачку
19
28
  * (30 суток). Больше сервер отвергнет 400.
20
29
  */
21
30
  const POOL_TODAY_WINDOW_DAYS_MAX = 16;
22
- const WAIT_HOURS_MAX = 720;
23
- /** Локальная дата площадки — та же форма, что у бэкенда. */
31
+ /** Локальная дата канала — та же форма, что у бэкенда. */
24
32
  const LOCAL_DATE_RE = /^\d{4}-\d{2}-\d{2}$/u;
25
33
  /**
26
34
  * Списки фильтров — ОДНА форма у расписания и у «Пула сегодня»: `'all'` либо список кодов.
27
35
  *
28
- * ⚠️ Границы повторяют DTO бэкенда (`POOL_TODAY_FUNNELS_MAX`, `POOL_TODAY_PRODUCTS_MAX`,
29
- * `POOL_TODAY_FILTER_VALUE_MAX_LENGTH`): разойдясь, схема пропускала бы то, что сервер отвергает
30
- * 400-м, и модель узнавала бы о границе из ошибки.
36
+ * ⚠️ Границы повторяют DTO бэкенда (`SCHEDULE_FUNNELS_MAX`, `SCHEDULE_PRODUCT_CODES_MAX`,
37
+ * `SCHEDULE_FILTER_VALUE_MAX_LENGTH`): разойдясь, схема пропускала бы то, что сервер отвергает 400-м,
38
+ * и модель узнавала бы о границе из ошибки.
31
39
  */
32
40
  const FILTER_VALUE_MAX_LENGTH = 64;
33
41
  const FUNNELS_MAX = 100;
@@ -37,29 +45,42 @@ const filterListSchema = (max) => z.union([
37
45
  z.array(z.string().trim().min(1).max(FILTER_VALUE_MAX_LENGTH)).max(max),
38
46
  ]);
39
47
  const orderSchema = z.enum(['FRESH_FIRST', 'OLD_FIRST']);
48
+ /** Потолок ожидания после эфира — серверный `SEGMENT_WAIT_AFTER_SERIES_END_HOURS_MAX`. */
49
+ const WAIT_HOURS_MAX = 720;
50
+ /** Границы приоритета — серверные `SEGMENT_RULE_PRIORITY_MIN/MAX`. */
51
+ const PRIORITY_MIN = -1000;
52
+ const PRIORITY_MAX = 1000;
53
+ /**
54
+ * Суток с прошлого касания: снизу — граница владельца (повтор снимает единственную вечную защиту
55
+ * от второго сообщения), сверху — окно свежести расписания. Паритет серверных констант: ниже 30
56
+ * сервер откажет уже после подтверждения человеком.
57
+ */
58
+ const REPEAT_TOUCH_MIN_DAYS = 30;
59
+ const REPEAT_TOUCH_MAX_DAYS = 78;
60
+ /** Потолок длины имени прошлого касания — отгруженный; сервер принимает и длиннее. */
61
+ const PREVIOUS_SEGMENT_KEY_MAX_LENGTH = 64;
62
+ const triStateSchema = z.enum(['YES', 'NO', 'ANY']);
63
+ /**
64
+ * Откуда берётся момент входа человека в сегмент.
65
+ *
66
+ * ⚠️ Схема СТРОГАЯ: значение, объявленное на сервере и забытое здесь, отвергается ещё до запроса.
67
+ */
40
68
  const anchorSchema = z.enum([
41
69
  'SERIES_END',
42
70
  'SERIES_END_PLUS_WAIT',
43
71
  // Конец ПЕРВОГО эфира серии плюс ожидание — для сегментов, которые пишут пропустившему первый
44
- // эфир, ПОКА СЕРИЯ ИДЁТ. Требует фасета attendedFirstSession и перекрытия предохранителя серии
45
- // на расписании (guard_active_series), иначе отправку остановит правило площадки.
72
+ // эфир, пока серия идёт. Требует признака attendedFirstSession.
46
73
  'FIRST_SESSION_END_PLUS_WAIT',
47
74
  'LAST_APPLICATION',
48
75
  'REGISTRATION',
49
- // ⚠️ Схема СТРОГАЯ: значение, объявленное на сервере и забытое здесь, отвергается ещё до
50
- // запроса — фича остаётся недостижимой через MCP, и выглядит это как ошибка ввода агента.
51
76
  'PRELIST',
52
77
  ]);
53
- /**
54
- * Тело расписания. `funnels`/`product_codes` принимают литерал `all` — обычный пользователь
55
- * говорит «шли по всем воронкам», и заставлять его перечислять их руками незачем.
56
- */
57
78
  /**
58
79
  * Пара «дни недели + числа месяца» запрещена.
59
80
  *
60
- * ⚠️ Правило продублировано в схеме намеренно: бэкенд и CHECK базы её тоже не пускают, но модель
61
- * узнавала бы об этом из 400-й — а описание инструмента для того и существует, чтобы правило было
62
- * известно ДО вызова.
81
+ * ⚠️ Правило продублировано в схеме намеренно: сервер и проверка таблицы её тоже не пускают, но
82
+ * модель узнавала бы об этом из 400-й — а описание инструмента для того и существует, чтобы правило
83
+ * было известно ДО вызова.
63
84
  */
64
85
  const assertSingleRepeatAxis = (input, ctx) => {
65
86
  if ((input.days_of_week?.length ?? 0) > 0 && (input.days_of_month?.length ?? 0) > 0) {
@@ -71,6 +92,11 @@ const assertSingleRepeatAxis = (input, ctx) => {
71
92
  });
72
93
  }
73
94
  };
95
+ /**
96
+ * Тело расписания. `funnels`/`product_codes` принимают литерал `all` — человек говорит «шли по всем
97
+ * воронкам», и заставлять его перечислять их руками незачем. Имена полей входа — отгруженные
98
+ * (`platform_id`, `handoff_from_school_platforms`): клиент зовёт инструмент ими уже сегодня.
99
+ */
74
100
  const scheduleShape = {
75
101
  workspace_id: uuidSchema.optional(),
76
102
  segment_id: uuidSchema,
@@ -81,46 +107,56 @@ const scheduleShape = {
81
107
  product_codes: filterListSchema(PRODUCTS_MAX).optional(),
82
108
  freshness_days: z.number().int().min(1).max(FRESHNESS_DAYS_MAX),
83
109
  window_mode: z.enum(['ROLLING', 'FIXED']).optional(),
84
- // ⚠️ ЛОКАЛЬНАЯ дата площадки, а не момент: момент сдвинул бы границу на смещение зоны.
110
+ // ⚠️ ЛОКАЛЬНАЯ дата канала, а не момент: момент сдвинул бы границу на смещение пояса.
85
111
  window_from_local: z.string().regex(LOCAL_DATE_RE, 'window_from_local: дата ГГГГ-ММ-ДД').optional(),
86
112
  // Конец ВКЛЮЧИТЕЛЬНО: «по 20 августа» берёт весь двадцатый день.
87
113
  window_to_local: z.string().regex(LOCAL_DATE_RE, 'window_to_local: дата ГГГГ-ММ-ДД').optional(),
88
114
  time_local: z
89
115
  .string()
90
- .regex(/^([01]\d|2[0-3]):([0-5]\d)$/, 'time_local: формат HH:mm в таймзоне площадки'),
116
+ .regex(/^([01]\d|2[0-3]):([0-5]\d)$/, 'time_local: формат HH:mm в часовом поясе канала'),
91
117
  days_of_week: z.array(z.number().int().min(1).max(7)).max(7).optional(),
92
- // ⚠️ Взаимоисключимо с `days_of_week`: вместе они означают «31-е И понедельник». Пару отвергает
93
- // и бэкенд 400-й, и CHECK базы — но узнавать об этом из ошибки сервера модель не должна, поэтому
94
- // запрет продублирован схемой (см. `superRefine` ниже). Число больше длины месяца попадает на
95
- // ПОСЛЕДНИЙ его день.
118
+ // ⚠️ Взаимоисключимо с `days_of_week` (см. `assertSingleRepeatAxis`). Число больше длины месяца
119
+ // попадает на ПОСЛЕДНИЙ его день.
96
120
  days_of_month: z.array(z.number().int().min(1).max(31)).max(31).optional(),
97
121
  daily_cap: z.number().int().min(1).max(DAILY_CAP_MAX),
98
122
  selection_order: orderSchema,
99
123
  cooldown_days: z.number().int().min(0).max(COOLDOWN_DAYS_MAX),
100
- handoff_from_school_platforms: z.boolean().optional().describe('ПЕРЕДАЧА: забирать людей с ДРУГИХ площадок GetCourse той же школы В ДОПОЛНЕНИЕ к своим. Своим людям площадки и людям без карточек расписание пишет как обычно, а людей, чья карточка живёт на других площадках школы (и тех, кого прошлая попытка уже перевезла, но сообщение не ушло), перед отправкой переносит сюда — с перепином на заказ сегмента и остановкой цепочек прежней площадки. Поэтому флаг включают прямо на основной авторассылке сегмента, второе расписание на тот же сегмент не нужно (так с 15.09.2026; раньше флаг заменял пул, и свои люди площадки текста не получали). Площадки-источники считает сам отбор («вся школа минус своя»), поэтому новая площадка школы подхватывается без правки расписания. Включайте на площадке, КУДА переносить людей (обычно квал-площадка): на площадке серии флаг заберёт к ней людей квал-площадки. Флаг нужен на ВСЕХ пересекающихся старших сегментах площадки: претензия старшего сегмента о флаге не знает, и человек с соседней площадки иначе не получит ничего. Требует сегмента-ПРОДОЛЖЕНИЯ (снят «только нетронутым», без правила повтора): у человека с соседней площадки право первого сообщения уже сожжено. Ещё требует заполненного домена школы у площадки расписания и хотя бы одной другой площадки GetCourse в школе. Выключено — передачи нет.'),
101
- resend_segment_on_new_registration: z.boolean().optional().describe('ПОВТОР СЕГМЕНТА ПО НОВОЙ РЕГИСТРАЦИИ (с 16.09.2026). Включено — человеку, которому этим сегментом уже писали, расписание напишет снова, только если он зарегистрировался ПОСЛЕ нашей последней отправки этого сегмента и с неё прошла пауза resend_segment_pause_days; продукт регистрации может быть любым. Выключено — сегмент человеку не повторяется никогда (причина alreadySentSegment). Действует только у сегмента-ПРОДОЛЖЕНИЯ без правила повтора: у сегмента «только нетронутым» второе сообщение держит право первого касания (alreadyGreeted). Не передано — у нового расписания включено, у существующего остаётся как было.'),
102
- resend_segment_pause_days: z.number().int().min(0).max(RESEND_PAUSE_DAYS_MAX).optional().describe('Пауза повтора сегмента: сутки по 24 часа от нашей последней отправки этого сегмента человеку. Пока не прошла — причина resendPause, человек вернётся в пул сам. Не передано — у нового расписания 3, у существующего остаётся как было. ⚠️ Меньше трёх суток повтор обычно не ускорит: разговор после нашей реплики закрывается тремя сутками тишины, и до тех пор человека держит причина dialogQuiet.'),
124
+ handoff_from_school_platforms: z
125
+ .boolean()
126
+ .optional()
127
+ .describe('ПЕРЕДАЧА: забирать людей с ДРУГИХ каналов GetCourse той же школы В ДОПОЛНЕНИЕ к своим. Требует сегмента-ПРОДОЛЖЕНИЯ (снят «только нетронутым», без правила повтора), заполненного домена школы у канала расписания и хотя бы одного другого канала GetCourse в школе. Не передано — у существующего остаётся как было.'),
128
+ resend_segment_on_new_registration: z
129
+ .boolean()
130
+ .optional()
131
+ .describe('ПОВТОР СЕГМЕНТА ПО НОВОЙ РЕГИСТРАЦИИ. Включено — человеку, которому этим сегментом уже писали, расписание напишет снова, только если он зарегистрировался ПОСЛЕ нашей последней отправки этого сегмента и с неё прошла пауза resend_segment_pause_days. Действует у продолжения без previousSegment; продукт регистрации повтор не ограничивает. У «только нетронутым» сохраняется право первого сообщения. Не передано — у нового расписания включено, у существующего остаётся как было.'),
132
+ resend_segment_pause_days: z
133
+ .number()
134
+ .int()
135
+ .min(0)
136
+ .max(RESEND_PAUSE_DAYS_MAX)
137
+ .optional()
138
+ .describe('Пауза повтора сегмента: сутки по 24 часа от нашей последней отправки этого сегмента человеку. Не передано — у нового расписания 3, у существующего остаётся как было.'),
103
139
  channels: z
104
- // ⚠️ Канон транспортов, а не свободная строка: иначе про допустимые значения модель узнаёт
105
- // из 400-й ошибки. Ровно этот дефект уже закрыт parity-тестом контракта для соседних тулов.
140
+ // ⚠️ Канон транспортов, а не свободная строка: иначе про допустимые значения модель узнаёт из
141
+ // 400-й ошибки.
106
142
  .array(getcourseTransportEnum)
107
- .max(20)
143
+ .max(CHANNELS_MAX)
108
144
  .optional()
109
- .describe('Каналы «Первого сообщения» (GetCourseTransport). Пусто или ключ не передан — дефолт рассылки. Канал вне списка площадки отвергается: школа не сужает набор, а отвергает его ЦЕЛИКОМ.'),
145
+ .describe('Каналы «Первого сообщения» (GetCourseTransport), первый — канал денег. [] — каналы рассылки; не передано — у существующего остаётся как было. Канал вне способов отправки канала GetCourse отвергается: школа не сужает набор, а отвергает его ЦЕЛИКОМ.'),
110
146
  channels_mode: z
111
147
  .enum(['RESTRICT', 'ADD'])
112
148
  .optional()
113
- .describe(`⚠️ УСТАРЕЛ и на поведение НЕ влияет (с 23.08.2026). Список channels значит «слать сюда», судьбу КАЖДОГО канала решает сервер: мессенджер уходит, только если школа подтвердила его наличие; SMS — только при известном телефоне; Email, WhatsApp, Viber, Сайт, Chatium — вслепую. Поле принимается ради обратной совместимости и игнорируется.`),
149
+ .describe('⚠️ УСТАРЕЛ и на поведение НЕ влияет. Список channels значит «слать сюда», судьбу КАЖДОГО канала решает сервер. Поле принимается ради обратной совместимости и игнорируется.'),
114
150
  guard_active_series: z
115
151
  .enum(['OFF', 'WARN', 'BLOCK'])
116
152
  .nullable()
117
153
  .optional()
118
- .describe('Перекрытие предохранителя «идёт вебинарная серия» ДЛЯ ЭТОГО расписания. Не передано — у существующего остаётся как было; null — вернуть под настройку площадки. OFF — расписание пишет и во время серии человека; WARN — причина пишется в аудит, отправка идёт; BLOCK — отправка останавливается. Нужно сегментам, которые пишут внутрь серии ПО ЗАМЫСЛУ (например «пропустил первый эфир», якорь FIRST_SESSION_END_PLUS_WAIT): у них серия идёт по определению, а площадочный переключатель один на ВСЕ расписания площадки и ослабил бы защиту у postweb*, prelist, closed-app и cta разом. Перекрывается только правило серии: заявка и оплата остаются площадочными.'),
154
+ .describe('Перекрытие предохранителя «идёт вебинарная серия» ДЛЯ ЭТОГО расписания. Не передано — у существующего остаётся как было; null — вернуть под настройку канала. Перекрывается только правило серии: заявка и оплата остаются настройкой канала.'),
119
155
  skip_after_segment_ids: z
120
156
  .array(uuidSchema)
121
157
  .max(SKIP_AFTER_SEGMENTS_MAX)
122
158
  .optional()
123
- .describe('НЕ ПИСАТЬ ПОСЛЕ СЕГМЕНТОВ (с 22.09.2026): id сегментов справочника (segment.id из besales_broadcast_schedule_list). Человеку, которому текст любого из них ушёл НЕ РАНЬШЕ его регистрации — той, по которой он попал в пул, — это расписание не пишет (причина skipAfterSegment); новая регистрация после того касания открывает человека снова. Сценарий: у расписания reg-no-show указать сегмент reg-no-show-day1 — получившему «пропустил первый эфир» обычное «не смогли прийти» по той же серии не уйдёт, остальным уйдёт. Не передано — у существующего остаётся как было; [] — снять правило. Сам сегмент расписания и чужие id сервер отвергает. Именно id, а не имена: имя сегмента переименовывают.'),
159
+ .describe('НЕ ПИСАТЬ ПОСЛЕ СЕГМЕНТОВ: идентификаторы сегментов справочника (segment.id из besales_broadcast_schedule_list). Не передано — у существующего остаётся как было; [] — снять правило. Запрет действует, если текст указанного сегмента ушёл не раньше регистрации, по которой человек попал в пул; регистрация новее касания открывает человека снова, неизвестная регистрация запрет не снимает. Сам сегмент расписания, чужие идентификаторы и сегменты со служебным именем сервер отвергает.'),
124
160
  is_enabled: z.boolean().optional(),
125
161
  };
126
162
  /** `all` — это «фильтра нет», то есть ПУСТОЙ список на стороне API. */
@@ -129,10 +165,18 @@ const toList = (value) => {
129
165
  return undefined;
130
166
  return value === 'all' ? [] : value;
131
167
  };
168
+ /**
169
+ * Вход инструмента — в тело сервера v2.
170
+ *
171
+ * ⚠️ `compactObject` режет только `undefined`: поле, которого нет во входе, не уходит вовсе и у
172
+ * существующего расписания остаётся как было, а `null` у `guard_active_series` доезжает и снимает
173
+ * перекрытие — «не передал» обязано отличаться от «верни под канал».
174
+ */
132
175
  const toScheduleBody = (input) => compactObject({
133
176
  segmentId: input.segment_id,
134
177
  mailingVariantId: input.mailing_variant_id,
135
- platformId: input.platform_id,
178
+ // Имя v2 — канал; отгруженное имя поля входа сохранено ради клиента.
179
+ channelId: input.platform_id,
136
180
  mechanism: input.mechanism,
137
181
  funnels: toList(input.funnels),
138
182
  productCodes: toList(input.product_codes),
@@ -146,26 +190,20 @@ const toScheduleBody = (input) => compactObject({
146
190
  dailyCap: input.daily_cap,
147
191
  selectionOrder: input.selection_order,
148
192
  cooldownDays: input.cooldown_days,
149
- handoffFromSchoolPlatforms: input.handoff_from_school_platforms,
193
+ handoffFromSchoolChannels: input.handoff_from_school_platforms,
150
194
  resendSegmentOnNewRegistration: input.resend_segment_on_new_registration,
151
195
  resendSegmentPauseDays: input.resend_segment_pause_days,
152
- channels: input.channels,
153
- // ⚠️ Режим передаётся ТОЛЬКО если его задал вызывающий: сам он на поведение не влияет, но
154
- // подставлять сюда `RESTRICT` значит записать в строку расписания решение, которого никто
155
- // не принимал, — а прочитано оно будет как действующая настройка.
156
- channelsMode: input.channels_mode,
157
- // ⚠️ `null` проходит намеренно (`compactObject` режет только `undefined`): им снимают
158
- // перекрытие, и «не передал» обязано отличаться от «верни под площадку».
196
+ transports: input.channels,
197
+ // ⚠️ Режим передаётся ТОЛЬКО если его задал вызывающий: сервер его принимает и игнорирует, а
198
+ // подставленный мостом он выглядел бы решением, которого никто не принимал.
199
+ transportsMode: input.channels_mode,
159
200
  guardActiveSeries: input.guard_active_series,
160
201
  skipAfterSegmentIds: input.skip_after_segment_ids,
161
202
  isEnabled: input.is_enabled,
162
203
  });
163
204
  /**
164
- * Планировщик рассылок: сегмент-правило × расписание, отбор идёт кроном.
165
- *
166
- * ⚠️⚠️ Расписание отправляет ЖИВЫМ ЛЮДЯМ без участия человека в момент отправки. Поэтому
167
- * создание всегда даёт выключенное расписание, а включение — отдельное действие после
168
- * предпросмотра.
205
+ * ⚠️⚠️ Расписание отправляет ЖИВЫМ ЛЮДЯМ без участия человека в момент отправки. Поэтому создание
206
+ * по умолчанию даёт выключенное расписание, а включение — отдельное действие после предпросмотра.
169
207
  */
170
208
  export const broadcastSchedulerTools = [
171
209
  defineTool({
@@ -179,8 +217,9 @@ export const broadcastSchedulerTools = [
179
217
  path: `/workspaces/${input.workspace_id}/broadcast-schedules`,
180
218
  }),
181
219
  enhanceResponse: (response) => withInstructions(response, [
182
- 'isEnabled=false означает, что расписание заведено, но крон его не берёт. Это нормальное состояние сразу после создания.',
220
+ 'isEnabled=false означает, что расписание заведено, но проход планировщика его не берёт. Это нормальное состояние сразу после создания.',
183
221
  'Смотри на segment.priority: при пересечении сегментов побеждает больший — это и решает, какой текст получит человек, попавший под два правила.',
222
+ 'Расписаний, убранных в архив (удалённых при существующей истории прогонов), в списке нет; их история читается besales_broadcast_schedule_runs по идентификатору.',
184
223
  ]),
185
224
  }),
186
225
  defineTool({
@@ -199,41 +238,31 @@ export const broadcastSchedulerTools = [
199
238
  ],
200
239
  defaultWorkspaceId: true,
201
240
  schema: z.object(scheduleShape).strict().superRefine(assertSingleRepeatAxis),
241
+ // Предпросмотр нового расписания: журнала подач у него нет. Предпросмотр правки существующего с его
242
+ // журналом — отдельный маршрут сервера; отгруженный инструмент его не зовёт.
202
243
  createRequest: (input) => ({
203
244
  method: 'POST',
204
245
  path: `/workspaces/${input.workspace_id}/broadcast-schedules/preview`,
205
246
  body: toScheduleBody(input),
206
247
  }),
207
248
  enhanceResponse: (response) => withInstructions(response, [
208
- 'Ничего не отправляет и не пишет: считает ТЕМ ЖЕ путём, что и крон — тот же источник фактов, тот же компилятор правила, те же исключения.',
209
- '⚠️ Показывай пользователю excludedBy числами. alreadySentSegment — этим сегментом уже слали (при включённом resend_segment_on_new_registration — и регистрации новее нашей отправки нет); alreadyGreeted — человек уже получал «Первое сообщение» (одно на контакт навсегда, любым сегментом; прежнее имя ключа coldWelcomeClaimed приходит от старого бэкенда в окне выкатки); activeEpisode — человека уже ведёт автоприём регистраций (приветствие должны и не отдали, либо подготовлен повторный вход на новый эфир); touchClaimed — касание этого сегмента уже занято в журнале, либо тот же текст ушёл человеку другим сегментом меньше трёх суток назад (с 16.09.2026 другой сегмент тем же шаблоном можно, но после трёх суток тишины); журнал подач этого расписания (с 16.09.2026 тремя причинами вместо journal): journalSubmitted — подача ушла, ещё идёт или касание занято, journalNoMessenger — прошлая подача закончилась «карточку не завели» или «каналами не достать» (пауза 7 суток), journalRetryPause — прошлая попытка не удалась, и пауза после неё ещё идёт; contactBlocked — бан, удалённая карточка или действующий мьют; contactWithManager — с человеком работает менеджер (операторский статус или передача диалога в отдел моложе трёх суток); прежние ключи journal и contactHeld приходят только от старого бэкенда в окне выкатки; cooldown — получал любую нашу рассылку в окне.',
210
- '⚠️⚠️ productNotInList — у расписания задан список продуктов, а у этих людей продукт другой или не указан вовсе. Прежде этот отсев шёл МОЛЧА: строка выбрасывалась без счётчика, и часть пула исчезала без причины. Чинится списком продуктов в расписании.',
211
- '⚠️⚠️ notReachable — ни один из ВЫБРАННЫХ каналов до человека не доходит, и это подтвердил ответ школы. Про невыбранные каналы число не говорит ничего. Чинится составом каналов расписания или рассылки, а не правилом сегмента: вердикт записывается вместе с набором каналов и перестаёт держать паузу, как только набор изменили.',
212
- '⚠️⚠️ factsUnknown — НЕ отсев: эти люди в пуле. Число значит «школу про них ещё не спрашивали», и без него ноль в notReachable читается как «все достижимы». Доставки оно НЕ обещает: тот же прогон опрашивает школу перед подачей (по 300 за раз) и часть из них тогда отсеет — в отчёте прогона эти числа уже пересчитаны.',
213
- '⚠️ Если notReachable равен размеру пула, сперва проверь площадку: когда НИ ОДИН выбранный канал ею не обслуживается, недостижимы будут все до единого, и чинится это в настройках площадки, а не в расписании. Предпросмотр в этом случае показывает отдельное предупреждение, а прогон закрывается своей причиной.', '⚠️ unverifiable — «нечем проверить», а НЕ «не подходит»: нет назначения серии, серия ещё идёт, нет длительности просмотра. Эти люди в рассылку не идут, и это правильно: отсутствие данных о явке не равно отсутствию человека на эфире.',
214
- '⚠️⚠️ truncatedSeriesSkipped — ЕДИНСТВЕННАЯ источниковая причина, которую чинит сам оператор: выборка упёрлась в потолок строк, и запись о явке серии могла остаться за его границей, поэтому судить о ней нечем. Чинится окном расписания за минуту — сузить свежесть, список воронок или продуктов. До 10.09.2026 это число складывалось с unverifiable и читалось как жалоба на данные школы: не путай их снова.',
215
- '⚠️⚠️ claimedByHigherPriority — НЕ отсев по данным, а приоритет: человек подходит и под сегмент с БОЛЬШИМ приоритетом, у которого на этой площадке включено расписание ТОГО ЖЕ механизма, он в его окне и фильтрах, и то расписание его возьмёт — или уже отправило ему свой текст, в том числе волной школы. Исключение одно: старшее пишет только нетронутым, а этому человеку мы уже писали ДРУГИМ сегментом — тогда он остаётся младшему. Если старшее этому человеку уже писало (с 16.09.2026): без регистрации новее той отправки он остаётся за старшим; при новой регистрации его забирает старшее, если само повторит сегмент (resend_segment_on_new_registration у сегмента-продолжения), а иначе он уходит младшему — только когда продукт младшего не совпадает ни с одной отправкой старшего (неизвестный продукт держит). Большое число — не поломка, а мера пересечения; меняется приоритетами сегментов. ⚠️ Число НЕ обещает, что человеку вообще что-то уйдёт: (1) потолок и день запуска старшего не проверяются — упрётся старшее в потолок, излишек ждёт следующего запуска, а при FRESH_FIRST самые старые из него выходят из окна ни с чем; (2) если текст старшего уже ушёл волной школы, подхватить человека может только расписание ПОВТОРА старшего сегмента, и его надо завести отдельно; (3) человек без живой карточки на этой площадке не получит ничего ни от кого — повтор старшего отсечёт его причиной repeatNoCard; (4) если у расписаний РАЗНЫЕ списки воронок, разведение неполно: воронки отсекаются на стороне источника фактов, и строк человека из чужой воронки в снимке нет вовсе — ставь пересекающимся сегментам одинаковые воронки.',
216
- '⚠️⚠️ repeatNoCard — кандидат на ПОВТОР (прошлое касание годится), но живой карточки на ЭТОЙ площадке у человека нет: чаще всего волну школы записали на эту площадку, а карточка у него на соседней. Без карточки не проверить ответы и дожимы, а импорт карточку здесь не заведёт (обновит соседнюю), поэтому такие люди исключаются. Расписанием это не чинится.',
217
- '⚠️⚠️ cardOnSiblingPlatform — человек живёт на СОСЕДНЕЙ площадке той же школы GetCourse, а на площадке расписания карточки у него нет. Импорт её сюда не перенесёт: он ищет человека по школе и обновляет ту карточку, что уже есть, — поэтому такие люди исключаются, а не уезжают в импорт (иначе каждую неделю они занимали бы слот дневного потолка и закрывались исходом NOT_IMPORTED). Расписанием не чинится: рассылать надо с той площадки, где человек живёт. Большое число у расписания на новой площадке — норма: люди школы живут на старой.',
218
- '⚠️⚠️ noMessengerVerdict — школа УЖЕ отвечала, что привязанных мессенджеров у человека нет, и ответ ещё в силе. Карточки в базе у него нет, импорт её не заведёт, поэтому подача сожгла бы слот дневного потолка и слот импортов воркспейса и закрылась бы исходом NOT_IMPORTED на 7 суток. Срок доверия ответу задаётся у ПЛОЩАДКИ (getcourse_settings.reachability_ttl_days, по умолчанию 7 суток; 0 — спрашивать школу каждый раз). Срок считается СУТКАМИ площадки: ответ в силе, пока он новее полуночи дня запуска минус срок — поэтому «Пул сегодня» и любой запуск того же дня отвечают одинаково, как бы ни задержался отбор (с 17.09.2026); когда срок истечёт, человек снова уедет в импорт и школу спросят заново. Чинится почтой, но включать её надо В ДВУХ местах: в allowlist ПЛОЩАДКИ (getcourse_settings.available_transports) и в каналах рассылки/расписания. Гейт сужает каналы по allowlist площадки, поэтому Email только в рассылке ничего не меняет — и это верно: канал вне allowlist школа не отправит никогда.',
219
- '⚠️⚠️ alreadyInDialog — с человеком идёт ЖИВОЙ разговор: он написал нам после нашей последней реплики (агента, оператора, дожима или процесса школы из импортированной истории). Сам такой разговор не закроется, пока ему не ответят. Это ЗЕРКАЛО отказа доставки SKIPPED_ALREADY_IN_DIALOG, поэтому место в дневном лимите он не занимает. С 16.09.2026 одинаково у сегмента «только нетронутым» и у продолжения. Расписанием не чинится.',
220
- '⚠️⚠️ dialogQuiet — пауза тишины разговора, а не отсев навсегда: наша последняя реплика в чате моложе трёх суток (по 24 часа), человек после неё не писал. После трёх суток тишины разговор закрыт, и человек вернётся в пул сам — в том числе тот, кто когда-то нам отвечал (решение 16.09.2026). Доставка отказывает таким тем же SKIPPED_ALREADY_IN_DIALOG. «Пул сегодня» считает эту причину на момент запуска авторассылки.',
221
- '⚠️⚠️ skipAfterSegment — человеку уже ушёл текст одного из сегментов списка skip_after_segment_ids расписания, и он не регистрировался после того касания (например, получил «пропустил первый эфир», и «не смогли прийти» по той же серии ему не уходит). Новая регистрация открывает его снова. У расписания без списка число всегда 0.',
222
- '⚠️⚠️ resendPause — этим сегментом человеку уже писали, после нашей отправки он зарегистрировался снова, и расписание повторит ему сегмент (resend_segment_on_new_registration), но пауза resend_segment_pause_days ещё идёт. Вернётся в пул сам. При выключенном флаге такие люди стоят в alreadySentSegment.',
223
- 'resendOpened — НЕ отсев: сколько из selectedCount пустил повтор сегмента по новой регистрации (этим сегментом им уже писали, они зарегистрировались снова, и пауза прошла). Это мера самого правила: при выключенном флаге всегда 0.',
224
- '⚠️⚠️ guardActiveSeries / guardPaid / guardActiveApplication / guardUnverified — ЗЕРКАЛО предохранителя школы (идёт вебинарная серия; guardPaid — купил продукт после регистрации, по которой человек попал в сегмент; guardActiveApplication — после этой регистрации есть действующая заявка на продукт, с человеком работает менеджер; заявки и оплаты до этой регистрации не держат; оплата и заявка сразу — guardPaid; прежний ключ guardActiveDeal приходит только от старого бэкенда; не смог проверить — нет в данных школы или продукт оплаты не определён). Отбор спрашивает те же правила площадки, что и отправка, поэтому остановленные не занимают дневной лимит, и его добирают следующие по очереди. ⚠️ Числа считаются только до набора дневного лимита: кто после — в overCap, их предохранитель не судил. Серия проходит сама: человек вернётся в отбор, как только она закончится. Режимы правил — в настройках площадки (getcourse_settings.guard_*).',
225
- '⚠️⚠️ guardUnchecked — НЕ отсев: предохранитель не проверил столько отобранных (cause=SOURCE_UNAVAILABLE — источник фактов не ответил, error — текст отказа; OUT_OF_TIME — проверка не уложилась в время запроса). Они стоят в selectedCount без проверки серии, заявки и оплаты; перед отправкой их проверит сама отправка, и часть отсеется. Скажи пользователю, что число «уйдёт» в этот раз завышено. На крупной площадке предпросмотр почти всегда отвечает OUT_OF_TIME (выкачка кандидатов съедает почти весь предел ответа) — полная картина с проверкой в besales_broadcast_pool_today. После отказа источника проверка минуту не повторяется: повторный вызов сразу вернёт SOURCE_UNAVAILABLE.',
226
- '⚠️⚠️ notHandoffCandidate — у расписания с передачей (включён handoff_from_school_platforms) человек живёт на ДРУГОЙ площадке той же школы, но переносить его нельзя: его серия там ещё идёт (последний шаг календарной цепочки источника впереди или у контакта нет даты эфира) либо у него карточки сразу на нескольких других площадках. Свои люди площадки и люди без карточек сюда НЕ попадают: с 15.09.2026 расписание берёт их как обычно. У обычного расписания число всегда ноль.',
227
- '⚠️⚠️ handoffReturn — этого человека передача уже увозила С ЭТОЙ площадки, и в рамках текущего входа в сегмент он сюда не вернётся: иначе два расписания-продолжения возили бы его по кругу, а каждый переезд необратим. Новый вход спустя время запрету не подлежит. Ненулевое число означает, что у вас кольцо из двух расписаний передачи — это настройка, а не сбой данных.',
228
- '⚠️⚠️ handoffBlocked — карточку не перенесли: предохранитель школы (идёт серия, действующая заявка, оплата), контактом занят человек (бан, оператор, мьют), личность занята другим процессом либо на площадке-адресате уже есть карточка с этим внешним id (последнее — навсегда). Такие люди в подачу не идут и получают исход HANDOFF_BLOCKED в журнале подач.',
229
- 'factsAsOf — момент, на который источник считает данные полными. Сильно устаревшее значение означает, что аналитика отстаёт и «не пришедшие» могут быть ложными.',
230
- '⚠️⚠️ overlaps — контакты, которых за последнюю неделю подавали ДРУГИЕ расписания. Непустой список означает, что человек рискует получить несколько сообщений: разведи расписания кулдауном или приоритетом сегментов, прежде чем включать.',
231
- 'byProduct показывает, как отбор разложится по продуктам — крон нарежет подачу этими же блоками.',
249
+ 'Ничего не отправляет и не пишет: считает ТЕМ ЖЕ отбором, что прогон, — тот же источник фактов, те же исключения, тот же предохранитель последним.',
250
+ SCHEDULE_EXCLUSION_HINT,
251
+ '⚠️ Отсев источника фактов — до пула: unverifiable — «нечем проверить» (нет назначения серии, серия идёт, нет длительности), а НЕ «не подходит»; missingIdentifiers — строки без номера ученика или заказа; lowCoverageSkipped — серии воронок с недостоверной явкой (воронки — в lowCoverageFunnels); truncatedSeriesSkipped — выборка упёрлась в потолок строк: чинится сужением окна, воронок или продуктов; productNotInList — продукт строки не из списка продуктов расписания.',
252
+ '⚠️⚠️ guardActiveSeries / guardPaid / guardActiveApplication / guardUnverified — зеркало предохранителя школы (идёт серия; купил после регистрации; живая заявка у менеджера; не смог проверить). Остановленные не занимают дневной потолок — его добирают следующие по очереди. guardUnchecked — НЕ отсев: столько отобранных предохранитель не проверил (cause=SOURCE_UNAVAILABLE — реестр не ответил; OUT_OF_TIME — проверка не уложилась во время запроса); их проверит сама отправка, и часть отсеется. На крупном канале предпросмотр почти всегда отвечает OUT_OF_TIME — полная картина с проверкой в besales_broadcast_pool_today.',
253
+ 'factsUnknown — НЕ отсев: отобранные, про которых школу о мессенджерах ещё не спрашивали, — без этого числа ноль в notReachable читался бы как «все достижимы». needsImport — скольким прогон сначала заведёт карточку или адрес на канале: они уйдут не сразу. resendOpened — скольким повтор сегмента по новой регистрации открыт.',
254
+ '⚠️ allChannelsUnserved=true — ни один выбранный способ отправки канал GetCourse не обслуживает: недостижимы будут все, и чинится это настройкой канала, а не правилом сегмента.',
255
+ 'factsAsOf — момент, на который источник считает данные полными; attendanceIngestAt=null — сигнала о приёме посещаемости нет. truncated=true — снимок упёрся в бюджет строк, показан не весь пул.',
256
+ '⚠️⚠️ overlaps — скольких из отобранных за последние 7 суток уже подавали ДРУГИЕ включённые расписания той же школы. Непустой список — риск нескольких сообщений одному человеку: разведи расписания приоритетом сегментов или тихим периодом, прежде чем включать.',
257
+ '⚠️⚠️ warnings — цена этой настройки, которую получит и сохранение: перескажи человеку каждое message своими словами ДО сохранения. SENIOR_FUNNELS_WIDER — у включённого расписания старшего сегмента того же канала список воронок шире: регистраций человека в его воронках этот отбор не видит, и приоритет пересечение не разведёт — человек может получить текст этого сегмента вместо старшего или оба. Предложи сделать списки funnels у этих расписаний одинаковыми.',
258
+ 'byProduct показывает, как отбор разложится по продуктам: прогон нарежет подачу этими же блоками.',
232
259
  ]),
233
260
  }),
234
261
  defineTool({
235
262
  name: 'besales_broadcast_schedule_upsert',
236
263
  sideEffect: 'write',
264
+ // Без schedule_id вызов СОЗДАЁТ расписание: при неизвестном исходе повтор завёл бы второе.
265
+ verifyWithToolName: 'besales_broadcast_schedule_list',
237
266
  requiredFields: [
238
267
  'segment_id',
239
268
  'mailing_variant_id',
@@ -262,16 +291,21 @@ export const broadcastSchedulerTools = [
262
291
  body: toScheduleBody(input),
263
292
  },
264
293
  enhanceResponse: (response) => withInstructions(response, [
265
- '⚠️ Создавай расписание ПОСЛЕ предпросмотра и без is_enabled=true: включённое расписание начнёт слать живым людям на ближайшем тике крона (каждые 5 минут).',
266
- 'time_local — в таймзоне ПЛОЩАДКИ, не в UTC и не в твоей. Сутки расписания тоже считаются по ней.',
294
+ 'Почему люди отсеялись — зови besales_broadcast_schedule_preview: в его подсказке объяснена каждая причина.',
295
+ '⚠️ Создавай расписание ПОСЛЕ предпросмотра и без is_enabled=true: включённое расписание начнёт слать живым людям на ближайшем проходе планировщика (каждые 5 минут).',
296
+ 'time_local — в часовом поясе КАНАЛА, не в UTC и не в твоём. Сутки расписания тоже считаются по нему.',
267
297
  'daily_cap — потолок ОТОБРАННЫХ за прогон, а не доставленных: часть отсеется гейтами отправки уже внутри пакета. Историческую разницу смотри в besales_broadcast_schedule_runs.',
268
298
  'Пустые funnels/product_codes (или "all") означают «без фильтра», а не «ничего»: это правило пустого списка во всей платформе.',
299
+ 'Поле, которого нет во входе, у существующего расписания остаётся как было; пустой список, false и null у guard_active_series — это значение, а не «не менять».',
300
+ '⚠️ Отказ сохранения — 400 с полем field (какое поле поправить) и объяснением в message: перескажи объяснение человеку своими словами и не повторяй вызов без правки названного поля.',
301
+ '⚠️⚠️ warnings — расписание СОХРАНЕНО, но у настройки есть цена: перескажи человеку каждое message своими словами. SENIOR_FUNNELS_WIDER — у включённого расписания старшего сегмента того же канала список воронок шире: приоритет не разведёт пересечение, человек может получить текст этого сегмента вместо старшего или оба. Предложи выровнять funnels у этих расписаний; повторять сохранение ради предупреждения не нужно.',
269
302
  'Механизм пока только cold_welcome: у волны свой контракт запуска, и планировщик не притворяется, что умеет её ставить в расписание.',
270
303
  ]),
271
304
  }),
272
305
  defineTool({
273
306
  name: 'besales_broadcast_schedule_delete',
274
307
  sideEffect: 'write',
308
+ verifyWithToolName: 'besales_broadcast_schedule_list',
275
309
  requiredFields: ['schedule_id', 'explicit_confirmation'],
276
310
  defaultWorkspaceId: true,
277
311
  schema: z
@@ -286,7 +320,9 @@ export const broadcastSchedulerTools = [
286
320
  path: `/workspaces/${input.workspace_id}/broadcast-schedules/${input.schedule_id}`,
287
321
  }),
288
322
  enhanceResponse: (response) => withInstructions(response, [
289
- 'Удаление уносит и историю прогонов расписания. Журнал подач исчезнет вместе с ней — а он держит информацию «кому мы уже подавали»: после удаления те же контакты снова станут кандидатами для нового расписания с тем же сегментом.',
323
+ 'outcome=DELETED — прогонов у расписания не было, оно удалено. outcome=ARCHIVED — прогоны были: расписание выключено и убрано из списка, история прогонов и журнал подач остались (история читается besales_broadcast_schedule_runs).',
324
+ '⚠️ Журнал подач читается по расписанию: новое расписание того же сегмента его не видит. От второго сообщения человека держат журнал касаний («этим сегментом уже слали») и право первого касания, а не архив.',
325
+ '409 — идёт прогон этого расписания: дождись его конца (besales_broadcast_schedule_runs) и повтори.',
290
326
  'Если нужно просто остановить рассылку — не удаляй, а сохрани расписание с is_enabled=false.',
291
327
  ]),
292
328
  }),
@@ -302,39 +338,42 @@ export const broadcastSchedulerTools = [
302
338
  limit: z.number().int().min(1).max(100).optional(),
303
339
  })
304
340
  .strict(),
305
- // ⚠️⚠️ Параметр клеится в ПУТЬ. Транспорт (`ApiToolRequest`) знает только method/path/body,
306
- // поля `query` в нём нет — оно выбрасывалось МОЛЧА, и `limit` не доезжал до бэкенда никогда:
307
- // сколько бы прогонов агент ни просил, приходило серверных 30, без ошибки и без признака
308
- // усечения. Тот же приём уже применён в `follow-up.ts` и `platform-setup.ts`.
341
+ // ⚠️⚠️ Параметр клеится в ПУТЬ: у транспорта (`ApiToolRequest`) поля `query` нет, и отдельное
342
+ // поле выбрасывалось бы молча — сервер отдавал бы свои 30 без признака усечения.
309
343
  createRequest: (input) => ({
310
344
  method: 'GET',
311
345
  path: `/workspaces/${input.workspace_id}/broadcast-schedules/${input.schedule_id}/runs` +
312
346
  buildListQuery({ limit: input.limit }),
313
347
  }),
314
348
  enhanceResponse: (response) => withInstructions(response, [
315
- 'periodBucket — локальная ДАТА площадки: один прогон на сутки, повтор в тот же день невозможен.',
316
- '⚠️ Статусы различают ПРИЧИНЫ, и чинятся они разным: DONE — пакет завершился; WAITING_RESOURCE — прогон отложен, период НЕ сгорел: воркспейс был занят другим импортом или пакетом (ручным или соседнего расписания), не ответил источник фактов («Источник фактов: …») или не прочитались старшие сегменты («Старшие сегменты не прочитаны: …») — причина в error; прогон повторится ближайшим тиком, когда расписание созреет (в тот же день и не раньше его времени, поэтому «Запустить сейчас» до времени расписания сам не повторится); SKIPPED — правило отбора не дало кандидатов (смотри excludedBy); FAILED — упал запуск пакета; RUNNING — пакет ещё идёт.',
317
- '⚠️ selectedCount — сколько ОТОБРАНО, launchedCount — сколько фактически доставлено. Расхождение нормально: часть отсеивают гейты отправки внутри пакета. Не называй selectedCount «отправлено».',
318
- '⚠️ snapshot.guardUnchecked у прогона — в этот день предохранитель не проверил часть отобранных (источник фактов не ответил или кончилось время), и отсев доставки по серии, заявке и оплате был выше обычного. Отсеянных ОТБОРОМ по предохранителю можно найти в аудите предохранителя: строки с details.stage=SCHEDULE_SELECTION и runId прогона.',
319
- 'attempts растёт на каждом WAITING_RESOURCE: большое число при error «Воркспейс занят …» означает, что воркспейс постоянно занят и расписание стоит передвинуть по времени; при «Источник фактов: …» чинится источник, а не время.',
320
- '⚠️ Все блоки «продукт × воронка» прогона уходят ОДНИМ пакетом, без потолка (прежде — не больше 10). excludedBy.droppedOverBlockLimit бывает только у старых прогонов — люди блоков сверх 10, которые ничего не получили.',
321
- '⚠️ excludedBy.blockPlaceholdersUnresolved — люди блоков, снятых проверкой плана: в тексте блока не подставились переменные (обычно у агента нет перекрытия переменной). Остальные блоки ушли; эти люди вернутся к следующему запуску (исход SKIPPED_UNRESOLVED_VARS, пауза 6 ч), и, пока агента не поправят, блок снимается на каждом запуске, занимая место в потолке прогона (dailyCap); имена переменных — в причине их строк журнала подач. Чинится значениями переменных или перекрытиями у агента — скажи об этом пользователю.',
349
+ 'periodBucket — локальная ДАТА канала: один прогон на сутки, повтор в тот же день невозможен.',
350
+ '⚠️ Статусы различают ПРИЧИНЫ, и чинятся они разным: DONE — пакет завершился; WAITING_RESOURCE — прогон отложен, период НЕ сгорел, waitReason называет чем: WORKSPACE_BUSY_IMPORT — воркспейс занят импортом контактов; WORKSPACE_BUSY_BATCH — пакетом рассылки (ручным или соседнего расписания); FACTS_SOURCE_UNAVAILABLE — не ответил источник фактов; HIGHER_PRIORITY_CLAIMS_UNREADABLE — не прочитались старшие сегменты; CHANNEL_NOT_READY — канал школы не готов: подключение не читается или не отмечен ни один способ отправки (чинится настройкой канала); SEGMENT_RULE_CHANGED — правило сегмента правили, пока шёл отбор (отбор повторится по новому правилу); REPEAT_SEGMENT_PENDING — сегмент-повтор, его шаг отбора ещё не построен; ERASURE_RACE — во время отбора или записи плана стирали данные человека. Прогон повторится ближайшим проходом в тот же день и не раньше времени расписания; не дождался до конца дня — закрывается FAILED с closeReason PERIOD_ENDED_WAITING. SKIPPED и FAILED — период потрачен: closeReason называет причину (RUN_ABANDONED — исполнитель прогона оборвался, а период уже не догнать), closeReasonClass — её род (ANSWER — законный ответ «сегодня некому», FAILURE — отказ, который чинит оператор или настройка, OPERATOR — решение человека, UNKNOWN — перенесённый прогон, причина — текстом прежней системы в error). FAILED без closeReason — отбор отказал по самой настройке (расписания, сегмента или рассылки больше нет) или решение отбора прогона не читается: причина только словами в error. PENDING, RUNNING и IMPORTING — прогон ещё идёт; у IMPORTING тоже бывает waitReason (WORKSPACE_BUSY_IMPORT, WORKSPACE_BUSY_BATCH, CHANNEL_NOT_READY, ERASURE_RACE): карточки заведены, а подача ждёт — не дольше потолка фазы импорта, дальше FAILED с closeReason IMPORT_PHASE_TIMEOUT.',
351
+ SCHEDULE_CLOSE_CLASS_HINT,
352
+ SCHEDULE_GUARD_HINT,
353
+ SCHEDULE_BLOCKS_HINT,
354
+ 'error — та же причина ожидания или закрытия СЛОВАМИ на языке воркспейса: человеку пересказывай её, а не ключ waitReason или closeReason.',
355
+ '⚠️ selectedCount — сколько ОТОБРАНО, launchedCount — сколько ПОПЫТОК отправки: доставленные плюс касания, занятые без доказательства доставки. Это не «доставлено»: не называй «доставлено» ни selectedCount, ни launchedCount. Расхождение с selectedCount нормально: часть отсеивают гейты отправки внутри пакета.',
356
+ SCHEDULE_EXCLUSION_HINT,
357
+ 'attempts растёт на каждом WAITING_RESOURCE: большое число при WORKSPACE_BUSY_* означает, что воркспейс постоянно занят и расписание стоит передвинуть по времени; при FACTS_SOURCE_UNAVAILABLE чинится источник, при CHANNEL_NOT_READY — настройка канала, а не время.',
358
+ 'trigger=RUN_NOW — прогон запущен кнопкой «Запустить сейчас»; actorType и actorId называют, кто её нажал.',
322
359
  ]),
323
360
  }),
324
361
  /**
325
- * «Пул сегодня»: сегменты с правилом на площадке — сколько сейчас в пуле и сколько уйдёт.
362
+ * «Пул сегодня»: сегменты с правилом на канале — сколько сейчас в пуле и сколько уйдёт.
326
363
  *
327
- * ⚠️⚠️ Считает СЕРВЕР фоном, одной выкачкой источника фактов на площадку: живой предпросмотр
328
- * боевого расписания в таймаут MCP не укладывается (замер 09.09.2026). Вызов заказывает расчёт
329
- * и сразу отдаёт последний посчитанный; повторный вызов с теми же параметрами — опрос, новый
330
- * расчёт не заводится. С `local_day` — чтение утреннего расчёта того дня (история).
364
+ * ⚠️⚠️ Считает СЕРВЕР фоном, одной выкачкой источника фактов на канал: живой предпросмотр боевого
365
+ * расписания в таймаут MCP не укладывается. Вызов заказывает расчёт и сразу отдаёт последний
366
+ * посчитанный; повторный вызов с теми же параметрами — опрос, новый расчёт не заводится. С
367
+ * `local_day` — чтение утреннего расчёта того дня (история), без побочного эффекта.
331
368
  *
332
- * ⚠️⚠️ Побочный эффект ЕСТЬ, и инструмент не читающий: вызов без `local_day` — это POST,
333
- * который заводит расчёт (строка живёт до двух недель) и снимает прежние неначатые заказы того
334
- * же ключа. Пометка «только чтение» отдала бы его хосту в профиль, где вызовы одобряются без
335
- * спроса. Идемпотентность объявлена честно: повтор с теми же параметрами переиспользует живой
336
- * и свежий (до 5 минут) расчёт — на этом и держится опрос; исключение — `force`, он и назван
369
+ * ⚠️⚠️ Побочный эффект ЕСТЬ, и инструмент не читающий: вызов без `local_day` — это POST, который
370
+ * заводит расчёт (строка живёт до двух недель) и снимает прежние неначатые заказы того же
371
+ * заказчика. Пометка «только чтение» отдала бы его хосту в профиль, где вызовы одобряются без
372
+ * спроса. Идемпотентность объявлена честно: повтор с теми же параметрами переиспользует живой и
373
+ * свежий (до 5 минут) расчёт — на этом и держится опрос; исключение — `force`, он и назван
337
374
  * «только после своей правки».
375
+ *
376
+ * Схема — отгруженная; тело и строка запроса — в именах сервера v2 (`channelId`, а не `platformId`).
338
377
  */
339
378
  defineTool({
340
379
  name: 'besales_broadcast_pool_today',
@@ -348,11 +387,14 @@ export const broadcastSchedulerTools = [
348
387
  workspace_id: uuidSchema.optional(),
349
388
  platform_id: uuidSchema
350
389
  .optional()
351
- .describe('GetCourse-площадка. Не передан — первая активная площадка с расписанием.'),
390
+ .describe('Канал GetCourse. Не передан — самый старый по дате создания активный канал GetCourse с расписанием, иначе самый старый активный'),
352
391
  window_days: z.number().int().min(1).max(POOL_TODAY_WINDOW_DAYS_MAX).optional(),
353
392
  funnels: filterListSchema(FUNNELS_MAX).optional(),
354
393
  product_codes: filterListSchema(PRODUCTS_MAX).optional(),
355
- channels: z.array(getcourseTransportEnum).max(getcourseTransportEnum.options.length).optional(),
394
+ channels: z
395
+ .array(getcourseTransportEnum)
396
+ .max(getcourseTransportEnum.options.length)
397
+ .optional(),
356
398
  cooldown_days: z.number().int().min(0).max(COOLDOWN_DAYS_MAX).optional(),
357
399
  // ⚠️ Проверяется только ФОРМА: существование даты сверяет сервер.
358
400
  local_day: z.string().regex(LOCAL_DATE_RE, 'local_day: дата ГГГГ-ММ-ДД').optional(),
@@ -360,7 +402,7 @@ export const broadcastSchedulerTools = [
360
402
  })
361
403
  .strict()
362
404
  .superRefine((input, ctx) => {
363
- // ⚠️ Утренний расчёт дня считан с настройками по умолчанию: фильтры рядом с днём сервер
405
+ // ⚠️ Утренний расчёт дня посчитан с настройками по умолчанию: фильтры рядом с днём сервер
364
406
  // отвергает 400-й, и узнавать об этом из ошибки модель не должна.
365
407
  const withSettings = input.window_days !== undefined ||
366
408
  input.funnels !== undefined ||
@@ -372,7 +414,7 @@ export const broadcastSchedulerTools = [
372
414
  ctx.addIssue({
373
415
  code: z.ZodIssueCode.custom,
374
416
  path: ['local_day'],
375
- message: 'local_day читает утренний расчёт дня с настройками по умолчанию — окно, воронки, продукты, каналы, кулдаун и force с ним не передаются',
417
+ message: 'local_day читает утренний расчёт дня с настройками по умолчанию — окно, воронки, продукты, каналы, тихий период и force с ним не передаются',
376
418
  });
377
419
  }
378
420
  }),
@@ -380,35 +422,41 @@ export const broadcastSchedulerTools = [
380
422
  ? {
381
423
  method: 'GET',
382
424
  path: `/workspaces/${input.workspace_id}/broadcast-pool` +
383
- buildListQuery({ platformId: input.platform_id, day: input.local_day }),
425
+ buildListQuery({ channelId: input.platform_id, day: input.local_day }),
384
426
  }
385
427
  : {
386
428
  method: 'POST',
387
429
  path: `/workspaces/${input.workspace_id}/broadcast-pool/refresh`,
388
430
  body: compactObject({
389
- platformId: input.platform_id,
431
+ channelId: input.platform_id,
390
432
  windowDays: input.window_days,
391
433
  funnels: toList(input.funnels),
392
434
  products: toList(input.product_codes),
393
- channels: input.channels,
435
+ transports: input.channels,
394
436
  cooldownDays: input.cooldown_days,
395
437
  force: input.force,
396
438
  }),
397
439
  },
398
440
  enhanceResponse: (response) => withInstructions(response, [
399
- '⚠️⚠️ activeRun не пуст — идёт расчёт этих настроек: вызови инструмент ЕЩЁ РАЗ через минуту с теми же параметрами. Повторный вызов — опрос, новый расчёт он не заказывает. run — последний ПОСЧИТАННЫЙ расчёт этих настроек (run.finishedAt — когда); run=null — ещё не считали.',
400
- '⚠️⚠️ Строка с расписанием (scheduleId не null) считается ТЕМ ЖЕ путём, что его тик: окно, воронки, продукты, каналы и тихий период — самого расписания; параметры вызова на неё не действуют. У выключенного расписания selectedCount значит «уйдёт, если включить». Строка без расписания считается по параметрам вызова (окно window_days, по умолчанию 3 суток; воронки, продукты, каналы, кулдаун), без рассылки и с потолком одного запуска — это верхняя граница того, что дало бы расписание.',
401
- 'Числа строки: poolSize — кандидаты после гейтов источника; selectedCount — отобрано на момент запуска авторассылки строки (у строки без расписания — запуска дня) по тому, что известно сейчас, а не «уже уехало»; отсеяно = poolSize − selectedCount, причины — preview.excludedBy тем же реестром, что у besales_broadcast_schedule_preview; preview.unverifiable — «нечем проверить», отсев источника ДО пула, а не причина отбора.',
402
- '⚠️ status: COMPUTED — посчитана; FAILED — не смогли (причина в error, числа null, а не 0); SKIPPED — не считается по правилу, код в skip.reason: REPEAT_NEEDS_SCHEDULE (повторному касанию нужно окно не короче skip.minDays — считается только расписанием), WINDOW_NOT_SHARED (окно регистраций шире общей выборки — только расписанием), PLATFORM_SERVES_NO_CHANNELS (площадка не обслуживает ни одного канала), DEADLINE (расчёт упёрся в потолок skip.deadlineMinutes раньше, чем дошёл до строки).',
403
- '⚠️ preview.truncated=true — «не весь пул»: выборка источника не дошла до начала окна строки, самые давние регистрации не рассмотрены. Совет «сузить свежесть» здесь не годится: выборку обрезал общий расчёт площадки.',
404
- 'failedRun — отказ (status=FAILED, причина в error) или замена новым заказом того же заказчика (CANCELLED), случившиеся ПОСЛЕ показанного расчёта. factsSource ≠ READY — источник фактов не настроен или выключен: расчёта не будет вовсе.',
441
+ '⚠️⚠️ activeRun не пуст — идёт расчёт этих настроек: вызови инструмент ЕЩЁ РАЗ через минуту с теми же параметрами. Повторный вызов — опрос, новый расчёт он не заказывает. run — последний ПОСЧИТАННЫЙ расчёт этих настроек (run.finishedAt — когда); run=null — ещё не считали. channelId — канал расчёта; channelId=null — у воркспейса нет активного канала GetCourse, считать нечего.',
442
+ '⚠️ 409 с кодом BROADCAST_POOL_WORKSPACE_ORDER_LIMIT — воркспейс уже держит два живых заказа с другими настройками (расчёт идёт по одному на всю систему): дождись готовности одного из них и повтори, НЕ перебирай параметры. factsSource ≠ READY (MISSING — источник фактов не настроен, DISABLED — выключен) — расчёта не будет вовсе. channelServesNoTransports=true — канал не обслуживает ни одного способа отправки: отправлять некуда, чинится настройкой канала.',
443
+ '⚠️⚠️ Строка с расписанием (scheduleId не null) считается ТЕМ ЖЕ путём, что его прогон, на момент ЗАПУСКА этой авторассылки: окно, воронки, продукты, способы отправки, тихий период, «не писать после» и перекрытие предохранителя серии — самого расписания; параметры вызова на неё не действуют. У выключенного расписания (schedule.isEnabled=false) selectedCount значит «уйдёт, если включить». Строка без расписания считается по параметрам вызова (окно window_days, по умолчанию 3 суток; воронки, продукты, способы отправки, тихий период), без рассылки и с потолком одного запуска — это верхняя граница того, что дало бы расписание. Сегмент без правила строки не получает.',
444
+ 'Числа строки: poolSize — кандидаты после гейтов источника; selectedCount — отобрано на момент запуска авторассылки строки (у строки без расписания — на момент дня) по тому, что известно сейчас, а не «уже уехало»; отсеяно = poolSize − selectedCount, причины — preview.excludedBy тем же реестром, что у besales_broadcast_schedule_preview; unverifiable, missingIdentifiers, lowCoverageSkipped и соседи там — отсев источника ДО пула, а не причина отбора. preview.guardUnchecked не пуст — предохранитель не проверил столько отобранных: «уйдёт» в этот раз завышено.',
445
+ '⚠️ status: COMPUTED — посчитана; FAILED — не смогли: failure.label — причина словами (её и называй человеку), failure.code — для тебя (SCHEDULE_NOT_FOUND, SEGMENT_NOT_FOUND, MAILING_VARIANT_NOT_FOUND, CHANNEL_NOT_READY — отказ по самой настройке; RULE_MISSING, RULE_UNREADABLE, SOURCE_FAILED, ATTENDANCE_INGEST_* и соседи — отказ правила или источника фактов; HIGHER_PRIORITY_CLAIMS_UNREADABLE — не прочитались расписания старших сегментов; UNEXPECTED — непредвиденный сбой; LEGACY_REASON — причина, записанная прежней системой, её текст в label), числа null, а не 0; SKIPPED — не считается по правилу, skip.reason и skip.label: REPEAT_NEEDS_SCHEDULE (повторному касанию нужно окно не короче skip.minDays — считается только расписанием), WINDOW_NOT_SHARED (окно регистраций skip.registrationDays суток шире общей выборки skip.maxDays — только расписанием), CHANNEL_SERVES_NO_TRANSPORTS (канал не обслуживает ни одного способа отправки), DEADLINE (расчёт упёрся в потолок skip.deadlineMinutes минут раньше, чем дошёл до строки).',
446
+ '⚠️ preview.truncated=true — «не весь пул»: выборка источника не дошла до начала окна строки, самые давние регистрации не рассмотрены. Совет «сузить свежесть» здесь не годится: выборку обрезал общий расчёт канала.',
447
+ 'failedRun — отказ (status=FAILED) или замена новым заказом того же заказчика (status=CANCELLED, reason.code=REPLACED_BY_NEW_ORDER), случившиеся ПОСЛЕ показанного расчёта; reason.label — причина словами (HEARTBEAT_LOST — расчёт прервался, DEADLINE — упёрся в потолок времени, FACTS_SOURCE — не ответил источник фактов).',
405
448
  'force=true — только после своей правки расписания, правила сегмента или запуска: без него свежий (до 5 минут) расчёт тех же настроек переиспользуется (запуск дня, отобравший людей или завершившийся после расчёта, переиспользование сбрасывает сам). Опрашивая идущий расчёт, force НЕ передавай — он заказал бы ещё один расчёт следом. local_day — утренний расчёт того дня (история «сегмент × день»), всегда с настройками по умолчанию.',
406
- '⚠️ Вызов с ДРУГИМИ параметрами снимает твой прежний неначатый заказ (расчёт идёт по одному на всю систему). Сравнивая площадки или окна, дождись готовности одной комбинации и только потом заказывай следующую — иначе ни одна не досчитается.',
407
- 'Суммы по строкам не называй «людьми»: человек в двух сегментах посчитан дважды. Поимённых списков в ответе нет — людей смотрят в интерфейсе, под правом на контакты.',
408
- '⚠️⚠️ «Сколько уйдёт сегодня» — это run.dayBreakdown, а не суммы poolSize/selectedCount: runDay (ГГГГ-ММ-ДД) — сегодня, пока хоть один запуск дня не завершился, потом — день ближайшего запуска; с local_day — сам этот день со всеми его запусками; scheduleIds — авторассылки этого дня; notComputedScheduleIds — авторассылки дня, которые не посчитались: числа дня без них неполные, так и скажи; ⚠️ план дня (registrations, people, excluded, selected, carriedOver, matured) — только по запускам, которые людей ещё не отбирали: ranScheduleIds — авторассылки дня, чей запуск уже прошёл: их итог — только facts.bySchedule, а у их сегментов run.rows[].dayBreakdown = null (списки после запуска планом не являются); с local_day непустой ranScheduleIds значит, что утренний расчёт досчитался уже после этих запусков, — так и скажи; allRan=true — все запуски дня уже прошли, плана нет: числа плана нулевые, не называй их, итог дня — facts; ranDuringComputation=true — запуск прошёл во время расчёта, числа неточны: без local_day повтори вызов с теми же параметрами (прошедший запуск сам сбрасывает переиспользование расчёта), с local_day утро не пересчитывается — оговори неточность; runTimes — время запусков дня по часам площадки (прошедший — когда прогон отобрал людей, предстоящий — по правилу); ⚠️⚠️ план считается на момент ЗАПУСКА, а не расчёта: кто созреет к запуску по эфиру, который уже закончился, — уже в числах; явка эфира, который ещё идёт, не известна, и таких людей в числах нет, пока эфир не кончится; matured — registrations плюс outsideSegments (null — не посчитано), outsideSegments — созреют к запуску в окне авторассылок дня, которые их рассматривают (их воронки и продукты, не раньше их свежести, по закончившимся эфирам), но не попали в пул ни одной; outsideSegmentsBy — они же по причинам (одна на регистрацию, сумма равна outsideSegments): paid — оплатил в этой воронке, application — живая заявка, attendanceNoMinutes — был на эфире, минут не записано, paidElsewhere — покупал в ДРУГОЙ воронке, seriesRunning — серия ещё идёт (их возьмут авторассылки по концу эфира, когда она закончится), other — прочее; у расчётов до 01.10.2026 paidElsewhere и seriesRunning нет — эти люди там в other; registrations — регистраций, вошедших в сегменты окнами своих авторассылок (от прошлого отбора до запуска; enteredFrom…enteredTo — общий охват этих окон); people — людей из них, КАЖДЫЙ ОДИН раз при любом числе сегментов; excluded — людей по причине отсева (noMessenger — нет мессенджера (в том числе прошлая подача этим расписанием закончилась «карточку не завели» или «каналами не достать»), inContact — на связи или писали недавно (этим сегментом уже писали, идёт разговор или трое суток тишины после нашей реплики, пауза повтора сегмента, пауза после любого нашего сообщения (поле расписания «Не писать, если писали за N дней»), подача этим расписанием ушла или ещё идёт), paid — купил продукт после регистрации, по которой попал в сегмент, withManager — с ним работает менеджер (действующая заявка на продукт после этой регистрации, операторский статус или передача диалога в отдел моложе трёх суток), blocked — бан, удалённая карточка или действующий мьют, activeSeries — идёт другая вебинарная серия, retryPause — прошлая попытка отправки этим расписанием не удалась, ждём паузу, overCap — не поместились в дневной потолок, higherPriority — уйдут в сегмент с большим приоритетом, otherPlatform — карточка на другой площадке школы, и эта авторассылка её не забирает (передача выключена, там ещё идёт серия эфиров или её дата неизвестна, карточек несколько, перенос не удался или уже увёз человека отсюда; у расчётов и прогонов до 22.09.2026 этой группы нет — эти люди в other), other — прочее (предохранитель не смог проверить заявки и оплаты, карточки на этой площадке нет, повтору не нашлось прошлого касания, непригодный номер человека); у расчётов до 16.09.2026 вместо новых приходят прежние группы alreadyWritten (уже писали), held (купил, менеджер, бан или мьют), pause (пауза); у запусков до 16.09.2026 (facts.scheduleRuns) к новым группам добавляются прежние held и pause, только ненулевые — называй их так и не складывай с новыми); people = selected + сумма excluded; carriedOver — уйдут этим запуском люди с прошлых дней. ⚠️⚠️ После запуска экран показывает «Уйдёт» числом «Взято запусками» (facts: sent + noMessenger + notSent + pending), когда все авторассылки плана (scheduleIds без ranScheduleIds и notComputedScheduleIds) уже отобрали людей и ни одна отобравшая не лежит вне плана; разница с selected + carriedOver показана как «добавились к запуску» / «отпали к запуску»: авторассылки отбирают людей по очереди, по приоритету, и за это время у части людей кончаются паузы. Называя «Уйдёт» дня, чьи запуски прошли, называй это число и разницу с планом. У каждого сегмента то же в run.rows[].dayBreakdown (без matured; null — сегмент не посчитан или запуск его авторассылки уже прошёл), и его runDay может быть другим днём — у авторассылки не на каждый день; у выключенной авторассылки selected — «уйдёт, если включить», в план дня она не входит.',
409
- 'facts — исход запусков дня run.dayBreakdown.runDay по людям, по всем авторассылкам площадки: sent — отправлено, noMessenger — карточку не завели или ни один канал не доходит (школа ответила, что мессенджеров нет, каналы не подошли или сорвалась запись импорта), pending — подан в ещё идущий прогон, notSent — не ушло или доставка не подтвердилась (в том числе сорванная отправка); notSentByReason — те же notSent по причинам: один исход на человека, сумма равна notSent; ключ — исход отказа отправки (SKIPPED_BOT_SILENCED — бот выключен у контакта, SKIPPED_ALREADY_IN_DIALOG — разговор уже идёт, SKIPPED_UNRESOLVED_VARS — переменные текста без значения и соседи, как в outcomes у besales_broadcast_launches_list), без следа отправки — исход журнала подач (UNRESOLVED_AFTER_BATCH — причина не записана: прогоны до 16.09.2026, RELEASED_LAUNCH_FAILED — запуск сорвался); человеку называй причины словами, а не кодами; bySchedule — то же по авторассылкам; scheduleRuns — что увидел каждый запуск дня при отборе (без фильтров): poolSize — пул авторассылки на момент отбора (окно свежести, а не люди дня; null — ещё не отбирал), collapsed — свёрнутые дубли одного человека, excluded — отсев по тем же группам, taken — взято запуском (люди в журнале подач; с этого числа начинается отправка авторассылки в bySchedule, а люди, которых импорт не завёл, в нём есть), unrecorded — люди пула, которых не объясняют ни причины, ни взятые (должно быть 0; не ноль — счётчики прогона не сходятся с его журналом); суммы пулов разных авторассылок людьми дня не называй — человек в двух сегментах посчитан дважды; selectedScheduleIds — авторассылки, чей прогон дня уже отобрал людей или завершился (без фильтров): есть там, но нет в run.dayBreakdown.ranScheduleIds — запуск прошёл уже после расчёта, и без local_day план устарел: повтори вызов с теми же параметрами; ⚠️ под фильтром воронки или продукта план берёт человека с любой регистрацией в них, а факты — по регистрации, с которой ему написали: у людей с регистрациями в разных воронках план и факты расходятся; facts=null — прогонов авторассылок этого дня нет (запуска ещё не было). days — до 14 дней с утренним расчётом (в 8:00, до запусков дня и впереди заказов в очереди; опоздавший виден по непустому ranScheduleIds) и хотя бы одним запуском, а также дни с прошедшим запуском без утреннего расчёта, свежие первыми (для local_day); у такого дня run — null или без dayBreakdown, итог дня по людям — только facts, а отбор каждого запуска — facts.scheduleRuns. Человеку называй сегменты и авторассылки, а не «строки».',
410
- '⚠️⚠️ «Сколько ушло СЕГОДНЯ» — это todayFacts, а не facts: today — сегодняшний день площадки (ГГГГ-ММ-ДД, по её часам), todayFacts — отправка сегодня в том же виде, что facts, какой бы день ни показывал расклад. После всех запусков дня run.dayBreakdown.runDay и facts уже про ЗАВТРА (facts=null — завтра запусков ещё не было), а сегодняшние итоги остаются в todayFacts. Отвечая, разделяй «Сегодня» (todayFacts) и план ближайшего дня (run.dayBreakdown). todayFacts=null — сегодня запусков не было; с local_day today и todayFacts всегда null — у дня истории итог только facts.',
411
- 'Сводка выкачки у run: fetchedRows (строк источника), fetchTruncated (выборка упёрлась в бюджет — у строк она отзывается preview.truncated), registeredFrom/registeredTo (окно регистраций), trigger (MORNING — утренний расчёт истории, ON_DEMAND — заказанный), settings (настройки строк без расписания). Называй их, объясняя расхождение чисел между днями.',
449
+ '⚠️ Вызов с ДРУГИМИ параметрами снимает твой прежний неначатый заказ (расчёт идёт по одному на всю систему). Сравнивая каналы или окна, дождись готовности одной комбинации и только потом заказывай следующую — иначе ни одна не досчитается.',
450
+ 'Суммы по строкам не называй «людьми»: человек в двух сегментах посчитан дважды. Поимённых списков в ответе нет — людей смотрят в интерфейсе, под правом на контакты (membersAvailable — список строки ещё хранится: только у двух последних расчётов канала и не дольше суток).',
451
+ '⚠️⚠️ «Сколько уйдёт сегодня» — это run.dayBreakdown, а не суммы poolSize/selectedCount: runDay (ГГГГ-ММ-ДД) — сегодня, пока хоть один запуск дня не завершился, потом — день ближайшего запуска; с local_day — сам этот день со всеми его запусками; scheduleIds — авторассылки этого дня; notComputedScheduleIds — авторассылки дня, которые не посчитались: числа дня без них неполные, так и скажи; ⚠️ план дня (registrations, people, excluded, selected, carriedOver, matured) — только по запускам, которые людей ещё не отбирали: ranScheduleIds — авторассылки дня, чей запуск уже прошёл: их итог — только facts.bySchedule, а у их сегментов run.rows[].dayBreakdown = null (списки после запуска планом не являются); с local_day непустой ranScheduleIds значит, что утренний расчёт досчитался уже после этих запусков, — так и скажи; allRan=true — все запуски дня уже прошли, плана нет: числа плана нулевые, не называй их, итог дня — facts; ranDuringComputation=true — запуск прошёл во время расчёта, числа неточны: без local_day повтори вызов с теми же параметрами (прошедший запуск сам сбрасывает переиспользование расчёта), с local_day утро не пересчитывается — оговори неточность; runTimes — время запусков дня по часам канала (прошедший — когда прогон отобрал людей, предстоящий — по правилу); ⚠️⚠️ план считается на момент ЗАПУСКА, а не расчёта: кто созреет к запуску по эфиру, который уже закончился, — уже в числах; явка эфира, который ещё идёт, не известна, и таких людей в числах нет, пока эфир не кончится; matured — registrations плюс outsideSegments (null — не посчитано), outsideSegments — созреют к запуску в окне авторассылок дня, которые их рассматривают (их воронки и продукты, не раньше их свежести, по закончившимся эфирам), но не попали в пул ни одной; outsideSegmentsBy — те же по причинам, только ненулевые ({code, label, count}: paid — купил, application — есть живая заявка, attendanceNoMinutes — был на эфире, минут не записано, paidElsewhere — покупал в другой воронке, seriesRunning — серия ещё идёт, other — прочее); одна причина на регистрацию, сумма равна outsideSegments; старые снимки могли относить paidElsewhere и seriesRunning к other; registrations — регистраций, вошедших в сегменты окнами своих авторассылок (от прошлого отбора до запуска; enteredFrom…enteredTo — общий охват этих окон); people — людей из них, КАЖДЫЙ ОДИН раз при любом числе сегментов; people = selected + сумма excluded; carriedOver — уйдут этим запуском люди с прошлых дней; unrecorded — люди без записанной причины (должно быть 0).',
452
+ SCHEDULE_POOL_GROUPS_HINT,
453
+ SCHEDULE_EXCLUSION_HINT,
454
+ '⚠️⚠️ После запуска экран показывает «Уйдёт» числом «Взято запусками» (facts.taken), когда все авторассылки плана (scheduleIds без ranScheduleIds и notComputedScheduleIds) уже отобрали людей и ни одна отобравшая не лежит вне плана; facts.morningPlanned — «Уйдёт» утреннего плана этого дня (selected + carriedOver утреннего расчёта; null — утреннего плана нет), разница с ним — «добавились к запуску» / «отпали к запуску»: авторассылки отбирают людей по очереди, по приоритету, и за это время у части людей кончаются паузы. Называя «Уйдёт» дня, чьи запуски прошли, называй «взято» и разницу с планом. У каждого сегмента то же в run.rows[].dayBreakdown (без matured; null — сегмент не посчитан или запуск его авторассылки уже прошёл), и его runDay может быть другим днём — у авторассылки не на каждый день; у выключенной авторассылки selected — «уйдёт, если включить», в план дня она не входит.',
455
+ 'facts — исход запусков дня run.dayBreakdown.runDay по людям, по всем авторассылкам канала: taken — взято (люди журнала подач), sent — отправлено, noMessenger — карточку не завели или ни один канал не доходит, pending — подан в ещё идущий прогон, notSent — не ушло или доставка не подтвердилась; notSentByReason — те же notSent по причинам, список {code, label, count}: один исход на человека, сумма равна notSent; code — исход отказа отправки (SKIPPED_BOT_SILENCED — бот выключен у контакта, SKIPPED_ALREADY_IN_DIALOG — разговор уже идёт, SKIPPED_UNRESOLVED_VARS — переменные текста без значения и соседи, как в outcomes у besales_broadcast_launches_list), без следа отправки — исход журнала подач (UNRESOLVED_AFTER_BATCH — причина не записана: прогоны до 16.09.2026, RELEASED_LAUNCH_FAILED — запуск сорвался); человеку называй label, а не коды; bySchedule — то же по авторассылкам (scheduleId).',
456
+ 'facts.scheduleRuns — запуски дня теми же числами, что строка прогона в besales_broadcast_launches_list: headline — «взято · отправлено» (poolMeasured=false — «счётчиков отбора нет», taken=null, и это НЕ ноль), selection — что увидел запуск при отборе (collapsed — несколько регистраций одного человека, excluded — люди ПУЛА по тем же группам, taken — взято, unrecorded — без записанной причины, должно быть 0; selection=null — пул не измерен), delivery — отправка прогона (null — «ещё неизвестно»: не завершён и никого не взял), planned — «Уйдёт» по утреннему плану сегмента на этот день, planDelta — «взято» минус план. Пул запуска — регистрации окна свежести, а не люди дня: суммы пулов разных авторассылок людьми дня не называй. selectedScheduleIds — авторассылки, чей прогон дня уже отобрал людей или завершился (без фильтров): есть там, но нет в run.dayBreakdown.ranScheduleIds — запуск прошёл уже после расчёта, и без local_day план устарел: повтори вызов с теми же параметрами. ⚠️ Под фильтром воронки или продукта план берёт человека с любой регистрацией в них, а факты — по регистрации, с которой ему написали: у людей с регистрациями в разных воронках план и факты расходятся. facts=null — прогонов авторассылок этого дня нет (запуска ещё не было).',
457
+ '⚠️⚠️ «Сколько ушло СЕГОДНЯ» — это todayFacts, а не facts: today — сегодняшний день канала (ГГГГ-ММ-ДД, по его часам), todayFacts — отправка сегодня в том же виде, что facts, какой бы день ни показывал расклад. После всех запусков дня run.dayBreakdown.runDay и facts уже про ЗАВТРА (facts=null — завтра запусков ещё не было), а сегодняшние итоги остаются в todayFacts. Отвечая, разделяй «Сегодня» (todayFacts) и план ближайшего дня (run.dayBreakdown). todayFacts=null — сегодня запусков не было; с local_day today и todayFacts всегда null — у дня истории итог только facts.',
458
+ 'days — до 14 дней канала, свежие первыми (для local_day): дни с утренним расчётом (в 8:00 по часам канала, до запусков дня и впереди заказов в очереди; опоздавший виден по непустому ranScheduleIds) и хотя бы одним запуском, а также дни с прошедшим запуском без утреннего расчёта; у такого дня run — null, итог дня по людям — только facts, а отбор каждого запуска — facts.scheduleRuns. Человеку называй сегменты и авторассылки, а не «строки».',
459
+ 'Сводка выкачки у run: fetchedRows (строк источника), fetchTruncated (выборка упёрлась в бюджет — у строк она отзывается preview.truncated), registeredFrom/registeredTo (окно регистраций), trigger (MORNING — утренний расчёт истории, ON_DEMAND — заказанный), settings (настройки строк без расписания). options — варианты фильтров (воронки и продукты, встреченные в выкачке). Называй их, объясняя расхождение чисел между днями.',
412
460
  ]),
413
461
  }),
414
462
  defineTool({
@@ -422,57 +470,50 @@ export const broadcastSchedulerTools = [
422
470
  segment_id: uuidSchema,
423
471
  facets: z
424
472
  .object({
425
- attended: z.enum(['YES', 'NO', 'ANY']).optional(),
473
+ attended: triStateSchema.optional(),
426
474
  /**
427
- * Был на ПЕРВОМ эфире серии. `NO` — пропустил первый эфир; вопрос отвечается, пока
428
- * серия ещё идёт, поэтому с `attended` они не взаимозаменяемы: та спрашивает про всю
429
- * серию и до её конца равна «нечем проверить».
475
+ * Был на ПЕРВОМ эфире серии. `NO` — пропустил первый эфир; вопрос отвечается, пока серия
476
+ * ещё идёт, поэтому с `attended` они не взаимозаменяемы.
430
477
  */
431
- attendedFirstSession: z.enum(['YES', 'NO', 'ANY']).optional(),
478
+ attendedFirstSession: triStateSchema.optional(),
432
479
  activeSecondsMin: z.number().int().min(0).optional(),
433
480
  activeSecondsMax: z.number().int().min(0).optional(),
434
481
  webinarDayMin: z.number().int().min(0).optional(),
435
482
  webinarDayMax: z.number().int().min(0).optional(),
436
- bannerClick: z.enum(['YES', 'NO', 'ANY']).optional(),
437
- // Применимость и метки заказа предсписка объявляет ВОРОНКА в источнике фактов, а не
438
- // мы: у нас настройки больше нет. Правила у воронки нет или данные не готовы — строки
439
- // приходят непроверяемыми, и людям не уйдёт ничего.
440
- prelist: z.enum(['YES', 'NO', 'ANY']).optional(),
483
+ bannerClick: triStateSchema.optional(),
484
+ prelist: triStateSchema.optional(),
441
485
  /**
442
- * ⚠️⚠️ ПОВТОРНОЕ касание. Сегмент уходит тем, у кого `key` был ПОСЛЕДНИМ касанием —
443
- * нашей отправкой или волной школы на этой площадке — и кто нам не ответил. Снимает
444
- * вечный одноразовый claim приветствия — единственную структурную защиту от второго
445
- * сообщения живому человеку, — поэтому порог снизу зеркалит серверную константу и здесь:
446
- * ниже 30 суток сервер откажет.
486
+ * ⚠️⚠️ ПОВТОРНОЕ касание: снимает вечный одноразовый claim приветствия — единственную
487
+ * структурную защиту от второго сообщения живому человеку. Имя сегмента сервер
488
+ * переводит в его идентификатор при сохранении: переименование сегмента правило
489
+ * дальше не ломает.
447
490
  */
448
491
  previousSegment: z
449
492
  .object({
450
- key: z.string().min(1).max(64),
451
- minDaysSince: z.number().int().min(30).max(78),
493
+ key: z.string().min(1).max(PREVIOUS_SEGMENT_KEY_MAX_LENGTH),
494
+ minDaysSince: z
495
+ .number()
496
+ .int()
497
+ .min(REPEAT_TOUCH_MIN_DAYS)
498
+ .max(REPEAT_TOUCH_MAX_DAYS),
452
499
  })
453
500
  .strict()
454
501
  .optional(),
455
502
  /**
456
- * Право первого касания: сегмент уходит только тем, кому мы ещё НИКОГДА не писали.
457
- *
458
- * ⚠️⚠️ Молчание значит `true` — это сегодняшнее поведение платформы. Выключение
459
- * осознанное и снимает ровно одну защиту: человеку, уже получившему от нас холодное
460
- * приветствие, уйдёт ВТОРОЕ первое сообщение. Остальные гейты (ответ, оператор,
461
- * тихий период, «этим сегментом уже слали») продолжают работать. Единственный фасет,
462
- * который меняется и у замороженного сегмента (решение владельца 15.09.2026).
503
+ * Право первого касания: молчание значит `true`. Единственный признак, который
504
+ * меняется и у замороженного сегмента.
463
505
  */
464
506
  untouchedOnly: z.boolean().optional(),
465
- // ⚠️⚠️ ОБЯЗАТЕЛЬНЫ на сервере (`SEGMENT_FACETS_REQUIRED_GUARDS`): необязательное
466
- // поле с дефолтом «неважно» делало законным правило целиком из умолчаний, а такое
467
- // правило матчит КАЖДОГО в окне — рассылка ушла бы и людям с живой заявкой у
468
- // менеджера, и уже заплатившим. `ANY` допустим, но выбирается ЯВНО.
507
+ // ⚠️⚠️ ОБЯЗАТЕЛЬНЫ: правило целиком из умолчаний подошло бы КАЖДОМУ в окне — рассылка
508
+ // ушла бы и людям с живой заявкой у менеджера, и уже заплатившим. `ANY` допустим, но
509
+ // выбирается ЯВНО.
469
510
  application: z.enum(['NONE', 'CANCELLED', 'ANY']),
470
511
  payment: z.enum(['NOT_IN_FUNNEL', 'NOT_ANYWHERE', 'ANY']),
471
512
  })
472
513
  .strict(),
473
514
  freshness_anchor: anchorSchema,
474
515
  wait_after_series_end_hours: z.number().int().min(0).max(WAIT_HOURS_MAX).optional(),
475
- priority: z.number().int().min(-1000).max(1000).optional(),
516
+ priority: z.number().int().min(PRIORITY_MIN).max(PRIORITY_MAX).optional(),
476
517
  })
477
518
  .strict(),
478
519
  createRequest: (input) => ({
@@ -486,14 +527,15 @@ export const broadcastSchedulerTools = [
486
527
  }),
487
528
  }),
488
529
  enhanceResponse: (response) => withInstructions(response, [
489
- '⚠️⚠️ 409 означает, что сегментом УЖЕ отправляли и правило заморожено. Это не ошибка доступа: строки отчётов уже несут это имя, и правка правила задним числом сделала бы разрез несопоставимым сам с собой. Заведи НОВЫЙ сегмент. Исключения — priority и facets.untouchedOnly: их меняют и у замороженного сегмента, передав остальное правило (фасеты, freshness_anchor, wait_after_series_end_hours) ТЕМ ЖЕ, что у сегмента сейчас; с даты переключения отчёт сегмента смешивает первые и повторные сообщения — скажи это человеку.',
490
- 'attendedFirstSession=NO — «пропустил ПЕРВЫЙ эфир», и это единственный способ написать человеку, пока его серия идёт. Требует: freshness_anchor=FIRST_SESSION_END_PLUS_WAIT (иначе вход в сегмент считался бы от конца серии — на 4–5 суток позже) и guard_active_series=OFF у РАСПИСАНИЯ (иначе предохранитель площадки остановит отправку: серия у человека идёт по определению). Источник фактов обязан отдавать явку по первому эфиру; пока не отдаёт, строки приходят непроверяемыми и никому ничего не уйдёт.',
491
- 'attended=NO проходит только достоверный «не пришёл». Человек, про которого данных нет (серия ещё идёт, назначения нет), в сегмент НЕ попадает — и это защита: отсутствие данных о явке не равно отсутствию человека на эфире.',
492
- 'Секунды, а не минуты: «от 120 минут» — это activeSecondsMin=7200. Контакт без известной длительности порог не проходит.',
493
- 'wait_after_series_end_hours — сколько ждать после эфира, прежде чем считать отсутствие явки доказанным. Дефолт 1 час: касание свежее, но явка к этому моменту досчитана у меньшинства (6 ч → 56 %, 24 ч → 74 %, 48 ч → 81 %), и недосчитанный контакт в отбор не попадает. Поэтому при коротком ожидании ставьте freshness_days от 3: при 1 человека смотрят ровно один раз.',
494
- 'application=CANCELLED требует отменённой заявки И отсутствия живой: с человеком, у которого заявка в работе, разговаривает менеджер.',
530
+ '⚠️⚠️ 409 означает, что сегментом УЖЕ отправляли и правило заморожено. Это не ошибка доступа: строки отчётов уже несут это имя, и правка правила задним числом сделала бы разрез несопоставимым сам с собой. Заведи НОВЫЙ сегмент. Исключения — priority и facets.untouchedOnly: их меняют и у замороженного сегмента, передав остальное правило (фасеты, freshness_anchor, wait_after_series_end_hours) ТЕМ ЖЕ, что у сегмента сейчас.',
531
+ 'warnings в ответе — правка принята, но у неё есть цена: перескажи её человеку своими словами (например, смена права первого касания у сегмента, которым уже отправляли, смешивает в его отчёте первые и повторные сообщения).',
532
+ 'attendedFirstSession=NO — «пропустил ПЕРВЫЙ эфир», единственный способ написать человеку, пока его серия идёт. Требует freshness_anchor=FIRST_SESSION_END_PLUS_WAIT (иначе сервер откажет: вход считался бы от конца серии — на 4–5 суток позже), а ни одно расписание сегмента не должно останавливать отправки внутри серии (guard_active_series не BLOCK — иначе сервер откажет, потому что пул отсеивался бы целиком на каждом запуске).',
533
+ 'attended=NO проходит только достоверный «не пришёл» — и одни короткие заходы меньше пяти минут в сумме. Человек, про которого данных нет (серия ещё идёт, назначения нет), в сегмент НЕ попадает: отсутствие данных о явке не равно отсутствию человека на эфире.',
534
+ 'Секунды, а не минуты: «от 120 минут» — это activeSecondsMin=7200. Человек без известной длительности порог не проходит.',
535
+ 'wait_after_series_end_hours — сколько ждать после эфира, прежде чем считать отсутствие явки доказанным. Не передано — 24 часа: к этому моменту явка досчитана примерно у трёх четвертей (6 ч → 56 %, 24 ч → 74 %, 48 ч → 81 %), а недосчитанный человек в отбор не попадает.',
536
+ 'application=CANCELLED требует отменённой заявки и отсутствия любой другой: живая, закрытая или незнакомая заявка бьёт отменённую.',
495
537
  'payment=NOT_ANYWHERE исключает всех, кто когда-либо успешно платил — включая тех, кому потом сделали возврат: он действительно платил.',
496
- 'untouchedOnly не указан = true: сегмент уходит только тем, кому мы никогда не писали. Ставьте false осознанно — это разрешает второе первое сообщение человеку, уже получившему от нас холодное приветствие в другой воронке.',
538
+ 'previousSegment.key — имя сегмента; сервер хранит его идентификатор, и в ответе прошлое касание приходит идентификатором с текущим именем. untouchedOnly не указан = true: сегмент уходит только тем, кому мы никогда не писали. Ставь false осознанно — это разрешает второе первое сообщение человеку, уже получившему от нас холодное приветствие.',
497
539
  ]),
498
540
  }),
499
541
  ];