@rubytech/create-maxy-code 0.1.576 → 0.1.578

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 (259) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/active-rules/dist/index.d.ts.map +1 -1
  3. package/payload/platform/lib/active-rules/dist/index.js +32 -1
  4. package/payload/platform/lib/active-rules/dist/index.js.map +1 -1
  5. package/payload/platform/lib/active-rules/src/index.test.ts +52 -3
  6. package/payload/platform/lib/active-rules/src/index.ts +36 -2
  7. package/payload/platform/lib/dispatch-read/dist/allocate.d.ts +57 -0
  8. package/payload/platform/lib/dispatch-read/dist/allocate.d.ts.map +1 -0
  9. package/payload/platform/lib/dispatch-read/dist/allocate.js +33 -0
  10. package/payload/platform/lib/dispatch-read/dist/allocate.js.map +1 -0
  11. package/payload/platform/lib/dispatch-read/dist/index.d.ts +2 -0
  12. package/payload/platform/lib/dispatch-read/dist/index.d.ts.map +1 -1
  13. package/payload/platform/lib/dispatch-read/dist/index.js +10 -1
  14. package/payload/platform/lib/dispatch-read/dist/index.js.map +1 -1
  15. package/payload/platform/lib/dispatch-read/src/__tests__/allocate.test.ts +41 -0
  16. package/payload/platform/lib/dispatch-read/src/allocate.ts +89 -0
  17. package/payload/platform/lib/dispatch-read/src/index.ts +14 -0
  18. package/payload/platform/lib/routine-templates/dist/index.d.ts +4 -4
  19. package/payload/platform/lib/routine-templates/dist/index.js +4 -4
  20. package/payload/platform/lib/routine-templates/src/index.ts +4 -4
  21. package/payload/platform/plugins/admin/hooks/__tests__/prompt-optimiser-compliance.test.sh +7 -2
  22. package/payload/platform/plugins/admin/hooks/prompt-optimiser-compliance.sh +8 -2
  23. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +4 -2
  24. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +17 -0
  25. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-visit-status-write.test.ts +333 -22
  26. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-visits-scope.test.ts +262 -0
  27. package/payload/platform/plugins/cloudflare/skills/data-portal/schema.sql +126 -21
  28. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/visit-status.ts +142 -2
  29. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/visits.ts +118 -6
  30. package/payload/platform/plugins/dispatch/PLUGIN.md +2 -0
  31. package/payload/platform/plugins/dispatch/mcp/dist/lib/allocate.d.ts +19 -50
  32. package/payload/platform/plugins/dispatch/mcp/dist/lib/allocate.d.ts.map +1 -1
  33. package/payload/platform/plugins/dispatch/mcp/dist/lib/allocate.js +1 -28
  34. package/payload/platform/plugins/dispatch/mcp/dist/lib/allocate.js.map +1 -1
  35. package/payload/platform/plugins/dispatch/skills/dispatch-page/SKILL.md +45 -6
  36. package/payload/platform/plugins/dispatch/skills/dispatch-page/template/planner.css +60 -0
  37. package/payload/platform/plugins/dispatch/skills/dispatch-page/template/planner.html +13 -0
  38. package/payload/platform/plugins/dispatch/skills/dispatch-page/template/planner.js +203 -5
  39. package/payload/platform/plugins/docs/references/internals.md +1 -1
  40. package/payload/platform/plugins/docs/references/platform.md +2 -0
  41. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +0 -2
  42. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
  43. package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-path.test.d.ts +2 -0
  44. package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-path.test.d.ts.map +1 -0
  45. package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-path.test.js +19 -0
  46. package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-path.test.js.map +1 -0
  47. package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-register.test.d.ts +2 -0
  48. package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-register.test.d.ts.map +1 -0
  49. package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-register.test.js +110 -0
  50. package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-register.test.js.map +1 -0
  51. package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-verdict.test.d.ts +2 -0
  52. package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-verdict.test.d.ts.map +1 -0
  53. package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-verdict.test.js +22 -0
  54. package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-verdict.test.js.map +1 -0
  55. package/payload/platform/plugins/telegram/mcp/dist/index.js +19 -163
  56. package/payload/platform/plugins/telegram/mcp/dist/index.js.map +1 -1
  57. package/payload/platform/plugins/telegram/mcp/dist/lib/account-write.d.ts +9 -0
  58. package/payload/platform/plugins/telegram/mcp/dist/lib/account-write.d.ts.map +1 -1
  59. package/payload/platform/plugins/telegram/mcp/dist/lib/account-write.js +11 -0
  60. package/payload/platform/plugins/telegram/mcp/dist/lib/account-write.js.map +1 -1
  61. package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.d.ts +30 -3
  62. package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.d.ts.map +1 -1
  63. package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.js +29 -4
  64. package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.js.map +1 -1
  65. package/payload/platform/plugins/telegram/mcp/dist/tools/webhook-register.d.ts +33 -0
  66. package/payload/platform/plugins/telegram/mcp/dist/tools/webhook-register.d.ts.map +1 -0
  67. package/payload/platform/plugins/telegram/mcp/dist/tools/webhook-register.js +145 -0
  68. package/payload/platform/plugins/telegram/mcp/dist/tools/webhook-register.js.map +1 -0
  69. package/payload/platform/plugins/telegram/skills/configure/SKILL.md +4 -2
  70. package/payload/platform/services/telegram-channel/dist/notification.d.ts +35 -3
  71. package/payload/platform/services/telegram-channel/dist/notification.d.ts.map +1 -1
  72. package/payload/platform/services/telegram-channel/dist/notification.js +38 -5
  73. package/payload/platform/services/telegram-channel/dist/notification.js.map +1 -1
  74. package/payload/platform/services/telegram-channel/dist/server.js +1 -1
  75. package/payload/platform/services/telegram-channel/dist/server.js.map +1 -1
  76. package/payload/platform/services/webchat-channel/dist/notification.d.ts +2 -1
  77. package/payload/platform/services/webchat-channel/dist/notification.d.ts.map +1 -1
  78. package/payload/platform/services/webchat-channel/dist/notification.js.map +1 -1
  79. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +2 -2
  80. package/payload/platform/templates/agents/admin/IDENTITY.md +1 -1
  81. package/payload/server/public/activity.html +4 -4
  82. package/payload/server/public/agents.html +3 -3
  83. package/payload/server/public/assets/{AdminLoginScreens-BLGZEaKU.js → AdminLoginScreens-CAZ3DSwA.js} +1 -1
  84. package/payload/server/public/assets/AdminLoginScreens-CAZ3DSwA.js.br +0 -0
  85. package/payload/server/public/assets/AdminLoginScreens-CAZ3DSwA.js.gz +0 -0
  86. package/payload/server/public/assets/{AdminShell-C_M7YE5Z.js → AdminShell-BLs5v0qF.js} +1 -1
  87. package/payload/server/public/assets/AdminShell-BLs5v0qF.js.br +0 -0
  88. package/payload/server/public/assets/AdminShell-BLs5v0qF.js.gz +0 -0
  89. package/payload/server/public/assets/{activity-BL6qbB9a.js → activity-BwPck2yT.js} +1 -1
  90. package/payload/server/public/assets/activity-BwPck2yT.js.br +0 -0
  91. package/payload/server/public/assets/activity-BwPck2yT.js.gz +0 -0
  92. package/payload/server/public/assets/{admin-jnJ3P2u4.js → admin-DjtOoabA.js} +1 -1
  93. package/payload/server/public/assets/admin-DjtOoabA.js.br +0 -0
  94. package/payload/server/public/assets/admin-DjtOoabA.js.gz +0 -0
  95. package/payload/server/public/assets/{agents-BzhfKZDN.js → agents-IQYaRvoh.js} +1 -1
  96. package/payload/server/public/assets/agents-IQYaRvoh.js.br +0 -0
  97. package/payload/server/public/assets/agents-IQYaRvoh.js.gz +0 -0
  98. package/payload/server/public/assets/{browser-C-6io2B9.js → browser-CPG8w8rQ.js} +1 -1
  99. package/payload/server/public/assets/browser-CPG8w8rQ.js.br +0 -0
  100. package/payload/server/public/assets/browser-CPG8w8rQ.js.gz +0 -0
  101. package/payload/server/public/assets/{calendar-Dt3gS88s.js → calendar-CRwrgNdq.js} +1 -1
  102. package/payload/server/public/assets/calendar-CRwrgNdq.js.br +0 -0
  103. package/payload/server/public/assets/calendar-CRwrgNdq.js.gz +0 -0
  104. package/payload/server/public/assets/chat-B7yZ4KLl.js +1 -0
  105. package/payload/server/public/assets/chat-B7yZ4KLl.js.br +3 -0
  106. package/payload/server/public/assets/chat-B7yZ4KLl.js.gz +0 -0
  107. package/payload/server/public/assets/chevron-left-BzKUQqLy.js +1 -0
  108. package/payload/server/public/assets/chevron-left-BzKUQqLy.js.br +0 -0
  109. package/payload/server/public/assets/clock-BafWFNGo.js +1 -0
  110. package/payload/server/public/assets/clock-BafWFNGo.js.br +0 -0
  111. package/payload/server/public/assets/{copy-TkPJgllS.js → copy-JyiMTV0k.js} +1 -1
  112. package/payload/server/public/assets/copy-JyiMTV0k.js.br +0 -0
  113. package/payload/server/public/assets/copy-JyiMTV0k.js.gz +0 -0
  114. package/payload/server/public/assets/data-DHtGCVrl.js +1 -0
  115. package/payload/server/public/assets/data-DHtGCVrl.js.br +0 -0
  116. package/payload/server/public/assets/data-DHtGCVrl.js.gz +0 -0
  117. package/payload/server/public/assets/{file-text-DyAL2DCh.js → file-text-cnHv9ZS6.js} +1 -1
  118. package/payload/server/public/assets/file-text-cnHv9ZS6.js.br +0 -0
  119. package/payload/server/public/assets/file-text-cnHv9ZS6.js.gz +0 -0
  120. package/payload/server/public/assets/{graph-PiNIcCfX.js → graph-lFwEUDSH.js} +1 -1
  121. package/payload/server/public/assets/graph-lFwEUDSH.js.br +0 -0
  122. package/payload/server/public/assets/graph-lFwEUDSH.js.gz +0 -0
  123. package/payload/server/public/assets/{graph-labels-8jKFrLHn.js → graph-labels-C6qUA0vO.js} +1 -1
  124. package/payload/server/public/assets/graph-labels-C6qUA0vO.js.br +0 -0
  125. package/payload/server/public/assets/graph-labels-C6qUA0vO.js.gz +0 -0
  126. package/payload/server/public/assets/{operator-DKGKGzrb.js → operator-BdoEy5mh.js} +1 -1
  127. package/payload/server/public/assets/operator-BdoEy5mh.js.br +0 -0
  128. package/payload/server/public/assets/operator-BdoEy5mh.js.gz +0 -0
  129. package/payload/server/public/assets/{page-DXz_K10G.js → page-DMrnnXNA.js} +1 -1
  130. package/payload/server/public/assets/page-DMrnnXNA.js.br +0 -0
  131. package/payload/server/public/assets/page-DMrnnXNA.js.gz +0 -0
  132. package/payload/server/public/assets/{page-BU5vlY9Y.js → page-DeMikGYL.js} +1 -1
  133. package/payload/server/public/assets/page-DeMikGYL.js.br +0 -0
  134. package/payload/server/public/assets/page-DeMikGYL.js.gz +0 -0
  135. package/payload/server/public/assets/play-D6OAQVJ_.js +1 -0
  136. package/payload/server/public/assets/play-D6OAQVJ_.js.br +0 -0
  137. package/payload/server/public/assets/play-D6OAQVJ_.js.gz +0 -0
  138. package/payload/server/public/assets/{public-BmA3-jLj.js → public-B-X1Cxfw.js} +1 -1
  139. package/payload/server/public/assets/public-B-X1Cxfw.js.br +0 -0
  140. package/payload/server/public/assets/public-B-X1Cxfw.js.gz +0 -0
  141. package/payload/server/public/assets/{rotate-ccw-fCH0LSMq.js → rotate-ccw-DVFvQAHG.js} +1 -1
  142. package/payload/server/public/assets/rotate-ccw-DVFvQAHG.js.br +0 -0
  143. package/payload/server/public/assets/rotate-ccw-DVFvQAHG.js.gz +0 -0
  144. package/payload/server/public/assets/{routines-BeFrmkls.js → routines-CgkkGJaS.js} +1 -1
  145. package/payload/server/public/assets/routines-CgkkGJaS.js.br +0 -0
  146. package/payload/server/public/assets/routines-CgkkGJaS.js.gz +0 -0
  147. package/payload/server/public/assets/{skills-mjls5HBa.js → skills-CW7gVcaG.js} +1 -1
  148. package/payload/server/public/assets/skills-CW7gVcaG.js.br +0 -0
  149. package/payload/server/public/assets/skills-CW7gVcaG.js.gz +0 -0
  150. package/payload/server/public/assets/{tasks-p6puHHD0.js → tasks-Ddjp0Rwh.js} +1 -1
  151. package/payload/server/public/assets/tasks-Ddjp0Rwh.js.br +0 -0
  152. package/payload/server/public/assets/tasks-Ddjp0Rwh.js.gz +0 -0
  153. package/payload/server/public/assets/{triangle-alert-CrZ1ewbd.js → triangle-alert-DUrUutX4.js} +1 -1
  154. package/payload/server/public/assets/triangle-alert-DUrUutX4.js.br +0 -0
  155. package/payload/server/public/assets/triangle-alert-DUrUutX4.js.gz +0 -0
  156. package/payload/server/public/assets/{useCopyFeedback-zOQNrIwd.js → useCopyFeedback-ClVINDrQ.js} +1 -1
  157. package/payload/server/public/assets/useCopyFeedback-ClVINDrQ.js.br +0 -0
  158. package/payload/server/public/assets/useCopyFeedback-ClVINDrQ.js.gz +0 -0
  159. package/payload/server/public/assets/{useMediaQuery-BmG090FR.js → useMediaQuery-BUFkZYRs.js} +1 -1
  160. package/payload/server/public/assets/useMediaQuery-BUFkZYRs.js.br +0 -0
  161. package/payload/server/public/assets/{useMediaQuery-BmG090FR.js.gz → useMediaQuery-BUFkZYRs.js.gz} +0 -0
  162. package/payload/server/public/assets/{useVoiceRecorder-9pf6FFJ-.js → useVoiceRecorder-DnyqQFet.js} +1 -1
  163. package/payload/server/public/assets/useVoiceRecorder-DnyqQFet.js.br +0 -0
  164. package/payload/server/public/assets/useVoiceRecorder-DnyqQFet.js.gz +0 -0
  165. package/payload/server/public/assets/{wrench-k4GYG5rc.js → wrench-BW28s_d0.js} +1 -1
  166. package/payload/server/public/assets/wrench-BW28s_d0.js.br +0 -0
  167. package/payload/server/public/assets/wrench-BW28s_d0.js.gz +0 -0
  168. package/payload/server/public/browser.html +3 -3
  169. package/payload/server/public/calendar.html +6 -6
  170. package/payload/server/public/chat.html +12 -12
  171. package/payload/server/public/data.html +10 -10
  172. package/payload/server/public/graph.html +8 -8
  173. package/payload/server/public/index.html +12 -12
  174. package/payload/server/public/operator.html +13 -13
  175. package/payload/server/public/public.html +12 -12
  176. package/payload/server/public/routines.html +6 -6
  177. package/payload/server/public/skills.html +4 -4
  178. package/payload/server/public/tasks.html +5 -5
  179. package/payload/server/server.js +1014 -375
  180. package/payload/platform/plugins/dispatch/mcp/dist/scripts/converge-visit-instants.d.ts +0 -6
  181. package/payload/platform/plugins/dispatch/mcp/dist/scripts/converge-visit-instants.d.ts.map +0 -1
  182. package/payload/platform/plugins/dispatch/mcp/dist/scripts/converge-visit-instants.js +0 -102
  183. package/payload/platform/plugins/dispatch/mcp/dist/scripts/converge-visit-instants.js.map +0 -1
  184. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/converge-routine-description.test.d.ts +0 -2
  185. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/converge-routine-description.test.d.ts.map +0 -1
  186. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/converge-routine-description.test.js +0 -87
  187. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/converge-routine-description.test.js.map +0 -1
  188. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/converge-startdate-utc.test.d.ts +0 -2
  189. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/converge-startdate-utc.test.d.ts.map +0 -1
  190. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/converge-startdate-utc.test.js +0 -79
  191. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/converge-startdate-utc.test.js.map +0 -1
  192. package/payload/platform/plugins/scheduling/mcp/dist/scripts/converge-routine-description.d.ts +0 -35
  193. package/payload/platform/plugins/scheduling/mcp/dist/scripts/converge-routine-description.d.ts.map +0 -1
  194. package/payload/platform/plugins/scheduling/mcp/dist/scripts/converge-routine-description.js +0 -56
  195. package/payload/platform/plugins/scheduling/mcp/dist/scripts/converge-routine-description.js.map +0 -1
  196. package/payload/platform/plugins/scheduling/mcp/dist/scripts/converge-startdate-utc.d.ts +0 -24
  197. package/payload/platform/plugins/scheduling/mcp/dist/scripts/converge-startdate-utc.d.ts.map +0 -1
  198. package/payload/platform/plugins/scheduling/mcp/dist/scripts/converge-startdate-utc.js +0 -87
  199. package/payload/platform/plugins/scheduling/mcp/dist/scripts/converge-startdate-utc.js.map +0 -1
  200. package/payload/server/public/assets/AdminLoginScreens-BLGZEaKU.js.br +0 -0
  201. package/payload/server/public/assets/AdminLoginScreens-BLGZEaKU.js.gz +0 -0
  202. package/payload/server/public/assets/AdminShell-C_M7YE5Z.js.br +0 -0
  203. package/payload/server/public/assets/AdminShell-C_M7YE5Z.js.gz +0 -0
  204. package/payload/server/public/assets/activity-BL6qbB9a.js.br +0 -0
  205. package/payload/server/public/assets/activity-BL6qbB9a.js.gz +0 -0
  206. package/payload/server/public/assets/admin-jnJ3P2u4.js.br +0 -0
  207. package/payload/server/public/assets/admin-jnJ3P2u4.js.gz +0 -0
  208. package/payload/server/public/assets/agents-BzhfKZDN.js.br +0 -0
  209. package/payload/server/public/assets/agents-BzhfKZDN.js.gz +0 -0
  210. package/payload/server/public/assets/browser-C-6io2B9.js.br +0 -0
  211. package/payload/server/public/assets/browser-C-6io2B9.js.gz +0 -0
  212. package/payload/server/public/assets/calendar-Dt3gS88s.js.br +0 -0
  213. package/payload/server/public/assets/calendar-Dt3gS88s.js.gz +0 -0
  214. package/payload/server/public/assets/chat-IUOvVkL-.js +0 -1
  215. package/payload/server/public/assets/chat-IUOvVkL-.js.br +0 -1
  216. package/payload/server/public/assets/chat-IUOvVkL-.js.gz +0 -0
  217. package/payload/server/public/assets/chevron-left-DSzR9zR7.js +0 -1
  218. package/payload/server/public/assets/chevron-left-DSzR9zR7.js.br +0 -0
  219. package/payload/server/public/assets/clock-C5xE5p2I.js +0 -1
  220. package/payload/server/public/assets/clock-C5xE5p2I.js.br +0 -0
  221. package/payload/server/public/assets/copy-TkPJgllS.js.br +0 -0
  222. package/payload/server/public/assets/copy-TkPJgllS.js.gz +0 -0
  223. package/payload/server/public/assets/data-Bhv0Pvfl.js +0 -1
  224. package/payload/server/public/assets/data-Bhv0Pvfl.js.br +0 -0
  225. package/payload/server/public/assets/data-Bhv0Pvfl.js.gz +0 -0
  226. package/payload/server/public/assets/file-text-DyAL2DCh.js.br +0 -0
  227. package/payload/server/public/assets/file-text-DyAL2DCh.js.gz +0 -0
  228. package/payload/server/public/assets/graph-PiNIcCfX.js.br +0 -0
  229. package/payload/server/public/assets/graph-PiNIcCfX.js.gz +0 -0
  230. package/payload/server/public/assets/graph-labels-8jKFrLHn.js.br +0 -0
  231. package/payload/server/public/assets/graph-labels-8jKFrLHn.js.gz +0 -0
  232. package/payload/server/public/assets/operator-DKGKGzrb.js.br +0 -0
  233. package/payload/server/public/assets/operator-DKGKGzrb.js.gz +0 -0
  234. package/payload/server/public/assets/page-BU5vlY9Y.js.br +0 -0
  235. package/payload/server/public/assets/page-BU5vlY9Y.js.gz +0 -0
  236. package/payload/server/public/assets/page-DXz_K10G.js.br +0 -0
  237. package/payload/server/public/assets/page-DXz_K10G.js.gz +0 -0
  238. package/payload/server/public/assets/play-CmSyYrB6.js +0 -1
  239. package/payload/server/public/assets/play-CmSyYrB6.js.br +0 -0
  240. package/payload/server/public/assets/play-CmSyYrB6.js.gz +0 -0
  241. package/payload/server/public/assets/public-BmA3-jLj.js.br +0 -0
  242. package/payload/server/public/assets/public-BmA3-jLj.js.gz +0 -0
  243. package/payload/server/public/assets/rotate-ccw-fCH0LSMq.js.br +0 -0
  244. package/payload/server/public/assets/rotate-ccw-fCH0LSMq.js.gz +0 -0
  245. package/payload/server/public/assets/routines-BeFrmkls.js.br +0 -0
  246. package/payload/server/public/assets/routines-BeFrmkls.js.gz +0 -0
  247. package/payload/server/public/assets/skills-mjls5HBa.js.br +0 -0
  248. package/payload/server/public/assets/skills-mjls5HBa.js.gz +0 -0
  249. package/payload/server/public/assets/tasks-p6puHHD0.js.br +0 -0
  250. package/payload/server/public/assets/tasks-p6puHHD0.js.gz +0 -0
  251. package/payload/server/public/assets/triangle-alert-CrZ1ewbd.js.br +0 -0
  252. package/payload/server/public/assets/triangle-alert-CrZ1ewbd.js.gz +0 -0
  253. package/payload/server/public/assets/useCopyFeedback-zOQNrIwd.js.br +0 -0
  254. package/payload/server/public/assets/useCopyFeedback-zOQNrIwd.js.gz +0 -0
  255. package/payload/server/public/assets/useMediaQuery-BmG090FR.js.br +0 -0
  256. package/payload/server/public/assets/useVoiceRecorder-9pf6FFJ-.js.br +0 -0
  257. package/payload/server/public/assets/useVoiceRecorder-9pf6FFJ-.js.gz +0 -0
  258. package/payload/server/public/assets/wrench-k4GYG5rc.js.br +0 -0
  259. package/payload/server/public/assets/wrench-k4GYG5rc.js.gz +0 -0
@@ -471,6 +471,16 @@ var init_agent_errors = __esm({
471
471
  });
472
472
 
473
473
  // app/lib/reader/parse-transcript.ts
474
+ function emptyEnvelopeCensus() {
475
+ return {
476
+ rows: 0,
477
+ wrapped: 0,
478
+ nested: 0,
479
+ stripped: 0,
480
+ unstripped: 0,
481
+ sources: { whatsapp: 0, webchat: 0, telegram: 0, none: 0 }
482
+ };
483
+ }
474
484
  function isCompactSummaryRow(row, content) {
475
485
  return row.isCompactSummary === true || content !== null && content.startsWith(COMPACT_SUMMARY_PREFIX);
476
486
  }
@@ -481,10 +491,11 @@ function isScheduleSource(text) {
481
491
  }
482
492
  function unwrapChannel(text) {
483
493
  const m = text.match(CHANNEL_WRAPPER);
484
- if (!m) return { text };
494
+ if (!m) return { text, wrapped: false, nested: false };
485
495
  let source = SOURCE_MAP[m[1].match(SOURCE_ATTR)?.[1] ?? ""];
486
496
  let inner = m[2];
487
497
  const im = inner.match(CHANNEL_WRAPPER);
498
+ const nested = im !== null;
488
499
  if (im) {
489
500
  const innerSource = SOURCE_MAP[im[1].match(SOURCE_ATTR)?.[1] ?? ""];
490
501
  if (innerSource) {
@@ -492,7 +503,7 @@ function unwrapChannel(text) {
492
503
  inner = im[2];
493
504
  }
494
505
  }
495
- return source ? { text: inner, source } : { text: inner };
506
+ return source ? { text: inner, source, wrapped: true, nested } : { text: inner, wrapped: true, nested };
496
507
  }
497
508
  function splitReplyNote(payload) {
498
509
  const m = payload.match(REPLY_NOTE);
@@ -508,7 +519,17 @@ function uploadCountOf(text) {
508
519
  const n = Number(text.match(UPLOAD_COUNT)?.[1]);
509
520
  return Number.isInteger(n) && n > 0 ? n : void 0;
510
521
  }
511
- function operatorInboundTurn(u, ts, uuid) {
522
+ function noteEnvelope(census, u) {
523
+ if (!census) return;
524
+ census.rows++;
525
+ if (u.wrapped) census.wrapped++;
526
+ if (u.nested) census.nested++;
527
+ if (u.text.startsWith("<channel ")) census.unstripped++;
528
+ else if (u.wrapped) census.stripped++;
529
+ census.sources[u.source ?? "none"]++;
530
+ }
531
+ function operatorInboundTurn(u, ts, uuid, census) {
532
+ noteEnvelope(census, u);
512
533
  const framed = rawFromComposedAdminFrame(u.text);
513
534
  const split = framed === void 0 ? void 0 : splitReplyNote(framed);
514
535
  const rawBody = split?.body;
@@ -635,12 +656,12 @@ function lastTurnTs(turns) {
635
656
  function isModelGated(turns) {
636
657
  return turns.some((t) => t.kind === "agent-error" && t.code === "model_unavailable");
637
658
  }
638
- function renderOrSuppressChannelInbound(u, ts, out, queuedPendingSuppress, uuid) {
659
+ function renderOrSuppressChannelInbound(u, ts, out, queuedPendingSuppress, uuid, census) {
639
660
  const armed2 = queuedPendingSuppress.get(u.text) ?? 0;
640
661
  if (armed2 > 0) {
641
662
  queuedPendingSuppress.set(u.text, armed2 - 1);
642
663
  } else {
643
- out.push(operatorInboundTurn(u, ts, uuid));
664
+ out.push(operatorInboundTurn(u, ts, uuid, census));
644
665
  }
645
666
  }
646
667
  function parseTranscript(lines, queuedPendingSuppress = /* @__PURE__ */ new Map(), turnCtx = {}) {
@@ -678,7 +699,7 @@ function parseTranscript(lines, queuedPendingSuppress = /* @__PURE__ */ new Map(
678
699
  const u = unwrapChannel(content);
679
700
  turnCtx.lastChannelSource = u.source;
680
701
  if (!isScheduleSource(content)) {
681
- out.push(operatorInboundTurn(u, ts));
702
+ out.push(operatorInboundTurn(u, ts, void 0, turnCtx.envelopeCensus));
682
703
  queuedPendingSuppress.set(u.text, (queuedPendingSuppress.get(u.text) ?? 0) + 1);
683
704
  }
684
705
  }
@@ -691,7 +712,7 @@ function parseTranscript(lines, queuedPendingSuppress = /* @__PURE__ */ new Map(
691
712
  const u = unwrapChannel(att.prompt);
692
713
  turnCtx.lastChannelSource = u.source;
693
714
  if (!isScheduleSource(att.prompt)) {
694
- renderOrSuppressChannelInbound(u, ts, out, queuedPendingSuppress, asString(row.uuid) ?? void 0);
715
+ renderOrSuppressChannelInbound(u, ts, out, queuedPendingSuppress, asString(row.uuid) ?? void 0, turnCtx.envelopeCensus);
695
716
  }
696
717
  }
697
718
  continue;
@@ -720,7 +741,7 @@ function parseTranscript(lines, queuedPendingSuppress = /* @__PURE__ */ new Map(
720
741
  const u = unwrapChannel(text);
721
742
  turnCtx.lastChannelSource = u.source;
722
743
  if (!isScheduleSource(text)) {
723
- renderOrSuppressChannelInbound(u, ts, out, queuedPendingSuppress, asString(row.uuid) ?? void 0);
744
+ renderOrSuppressChannelInbound(u, ts, out, queuedPendingSuppress, asString(row.uuid) ?? void 0, turnCtx.envelopeCensus);
724
745
  }
725
746
  } else if (row.isMeta === true) {
726
747
  out.push({ kind: "harness-internal", text, ts });
@@ -747,7 +768,7 @@ var init_parse_transcript = __esm({
747
768
  HARNESS_LIFECYCLE_PREFIXES = ["<task-notification>"];
748
769
  CC_IMAGE_VISION_HINT = /^\[Image:[^\]]*Multiply coordinates by [\d.]+ to map to original image\.\]$/;
749
770
  COMPACT_SUMMARY_PREFIX = "This session is being continued from a previous conversation that ran out of context.";
750
- CHANNEL_WRAPPER = /^<channel\b([^>]*)>\n?([\s\S]*?)\n?<\/channel>$/;
771
+ CHANNEL_WRAPPER = /^<channel\b([^>]*)>\n?([\s\S]*?)\n?<\\?\/channel>$/;
751
772
  SOURCE_ATTR = /\bsource="([^"]+)"/;
752
773
  SOURCE_MAP = {
753
774
  whatsapp: "whatsapp",
@@ -757,7 +778,7 @@ var init_parse_transcript = __esm({
757
778
  webchat: "webchat",
758
779
  "webchat-channel": "webchat"
759
780
  };
760
- COMPOSED_ADMIN_FRAME = /^(?:## Scheduled automation \(platform-verified\)\n[\s\S]*?\n\n)?(?:## Message provenance \(platform-verified\)\n[\s\S]*?\n\n)?(?:## Standing rules\n[\s\S]*?\n\n)?## Context\n([\s\S]*)\n\n## Instruction\n[\s\S]*$/;
781
+ COMPOSED_ADMIN_FRAME = /^(?:## Scheduled automation \(platform-verified\)\n[\s\S]*?\n\n)?(?:## Message provenance \(platform-verified\)\n[\s\S]*?\n\n)?(?:## Standing rules(?: \(account owner\))?\n[\s\S]*?\n\n)?## Context\n([\s\S]*)\n\n## Instruction\n[\s\S]*$/;
761
782
  SCHEDULED_MARKER_PREFIX = "## Scheduled automation (platform-verified)\n";
762
783
  UPLOAD_ANNOTATION = /(?:\n\n)?\[The operator uploaded \d+ file\(s\): [\s\S]*\]$/;
763
784
  REPLY_NOTE = /^\[In reply to an earlier message in this conversation(?:\.|: "([\s\S]*?)")\]\n\n/;
@@ -815,7 +836,7 @@ var init_audio_mime = __esm({
815
836
  // app/lib/stt/voice-note.ts
816
837
  import { writeFile, mkdtemp, rm } from "fs/promises";
817
838
  import { tmpdir as tmpdir2 } from "os";
818
- import { join as join6 } from "path";
839
+ import { join as join7 } from "path";
819
840
  async function transcribeVoiceNote(file, source) {
820
841
  const startMs = Date.now();
821
842
  const bytes = file.size;
@@ -826,9 +847,9 @@ async function transcribeVoiceNote(file, source) {
826
847
  let tempDir;
827
848
  let tempPath;
828
849
  try {
829
- tempDir = await mkdtemp(join6(tmpdir2(), "voice-"));
850
+ tempDir = await mkdtemp(join7(tmpdir2(), "voice-"));
830
851
  const ext = audioExtension(mimeType);
831
- tempPath = join6(tempDir, `recording${ext}`);
852
+ tempPath = join7(tempDir, `recording${ext}`);
832
853
  const buffer = Buffer.from(await file.arrayBuffer());
833
854
  await writeFile(tempPath, buffer);
834
855
  } catch (err) {
@@ -1239,10 +1260,56 @@ function buildStoreConversationRows(accountId, summaries, chatMeta) {
1239
1260
  }
1240
1261
  return rows.sort((a, b) => (b.lastMessageAt ?? "").localeCompare(a.lastMessageAt ?? ""));
1241
1262
  }
1263
+ function buildTelegramStoreConversationRows(accountId, summaries) {
1264
+ const rows = [];
1265
+ for (const [channelKey, { first, last }] of summaries) {
1266
+ rows.push({
1267
+ sessionId: "",
1268
+ projectDir: "",
1269
+ title: channelKey,
1270
+ senderId: last.senderId,
1271
+ startedAt: first.dateSent,
1272
+ channel: "telegram",
1273
+ role: last.scope === "admin" ? "admin" : "public",
1274
+ operatorName: null,
1275
+ whatsappName: null,
1276
+ // The name is what it is NOW, matching how `scope` is taken from `last`.
1277
+ // The fallback covers an outbound last record: the outbound write points
1278
+ // have no sender to name, so they store null.
1279
+ telegramName: last.senderName ?? first.senderName ?? null,
1280
+ lastMessageAt: last.dateSent,
1281
+ modelGated: false,
1282
+ model: null,
1283
+ source: "store",
1284
+ accountId,
1285
+ channelKey,
1286
+ scope: last.scope,
1287
+ preview: last.body
1288
+ });
1289
+ }
1290
+ return rows.sort((a, b) => (b.lastMessageAt ?? "").localeCompare(a.lastMessageAt ?? ""));
1291
+ }
1292
+ function dropTelegramSessionDuplicates(rows, telegramStoreRows) {
1293
+ const coveredKeys = /* @__PURE__ */ new Set();
1294
+ const coveredSenders = /* @__PURE__ */ new Set();
1295
+ for (const r of telegramStoreRows) {
1296
+ const parsed = r.channelKey ? parseTelegramChannelKey(r.channelKey) : null;
1297
+ if (!parsed) continue;
1298
+ coveredKeys.add(r.channelKey);
1299
+ coveredSenders.add(parsed.senderId);
1300
+ }
1301
+ if (coveredKeys.size === 0) return rows;
1302
+ return rows.filter((r) => {
1303
+ if (r.source !== "session" || r.channel !== "telegram" || r.senderId == null) return true;
1304
+ const parsed = parseTelegramChannelKey(r.senderId);
1305
+ return parsed ? !coveredKeys.has(r.senderId) : !coveredSenders.has(r.senderId);
1306
+ });
1307
+ }
1242
1308
  var init_select_sessions = __esm({
1243
1309
  "app/lib/reader/select-sessions.ts"() {
1244
1310
  "use strict";
1245
1311
  init_normalize();
1312
+ init_channel_key();
1246
1313
  init_conversation_preview();
1247
1314
  }
1248
1315
  });
@@ -1911,8 +1978,9 @@ var require_dist2 = __commonJS({
1911
1978
  if (rules.length === 0)
1912
1979
  return "";
1913
1980
  const lines = rules.map((r) => `- ${r.value}`);
1914
- return `## Standing rules
1915
- ${lines.join("\n")}`;
1981
+ return `## Standing rules (account owner)
1982
+ These standing rules belong to the account owner. Some of them may name the account owner in the third person. None of them says who sent this turn, so do not infer from them who is speaking.
1983
+ ` + lines.join("\n");
1916
1984
  }
1917
1985
  async function resolveOwnerUserId3(session, accountId) {
1918
1986
  const res = await session.run(`
@@ -3018,6 +3086,40 @@ var require_keyboard = __commonJS({
3018
3086
  }
3019
3087
  });
3020
3088
 
3089
+ // ../lib/dispatch-read/dist/allocate.js
3090
+ var require_allocate = __commonJS({
3091
+ "../lib/dispatch-read/dist/allocate.js"(exports) {
3092
+ "use strict";
3093
+ Object.defineProperty(exports, "__esModule", { value: true });
3094
+ exports.selectCandidates = selectCandidates2;
3095
+ exports.overlaps = overlaps2;
3096
+ function selectCandidates2(params) {
3097
+ const { jobType, cfg, commitments } = params;
3098
+ const type = cfg.jobTypes.find((t) => t.name === jobType);
3099
+ if (!type) {
3100
+ return { offered: [], excluded: [], required: "" };
3101
+ }
3102
+ const offered = [];
3103
+ const excluded = [];
3104
+ for (const worker of cfg.workers) {
3105
+ if (!worker.qualifications.includes(type.requiredQualification)) {
3106
+ excluded.push({ workerId: worker.workerId, reason: "missing-qualification" });
3107
+ continue;
3108
+ }
3109
+ offered.push({
3110
+ workerId: worker.workerId,
3111
+ name: worker.name,
3112
+ conflicts: commitments.get(worker.workerId) ?? []
3113
+ });
3114
+ }
3115
+ return { offered, excluded, required: type.requiredQualification };
3116
+ }
3117
+ function overlaps2(a, b) {
3118
+ return a.start < b.end && b.start < a.end;
3119
+ }
3120
+ }
3121
+ });
3122
+
3021
3123
  // ../lib/dispatch-read/dist/session.js
3022
3124
  var require_session = __commonJS({
3023
3125
  "../lib/dispatch-read/dist/session.js"(exports) {
@@ -3217,7 +3319,7 @@ var require_read = __commonJS({
3217
3319
  exports.readJobList = readJobList2;
3218
3320
  exports.readCounts = readCounts2;
3219
3321
  var session_js_1 = require_session();
3220
- function num4(v) {
3322
+ function num5(v) {
3221
3323
  if (typeof v === "number")
3222
3324
  return v;
3223
3325
  if (v && typeof v === "object" && "low" in v) {
@@ -3241,7 +3343,7 @@ var require_read = __commonJS({
3241
3343
  workerId: String(a.workerId),
3242
3344
  arrivedPressedAt: str2(a.arrivedPressedAt),
3243
3345
  arrivedObservedAt: str2(a.arrivedObservedAt),
3244
- arrivedObservedDistanceM: a.arrivedObservedDistanceM == null ? null : num4(a.arrivedObservedDistanceM)
3346
+ arrivedObservedDistanceM: a.arrivedObservedDistanceM == null ? null : num5(a.arrivedObservedDistanceM)
3245
3347
  })).sort((x, y) => x.workerId < y.workerId ? -1 : x.workerId > y.workerId ? 1 : 0);
3246
3348
  const sole = attendances.length === 1 ? attendances[0] : null;
3247
3349
  return {
@@ -3334,12 +3436,12 @@ var require_read = __commonJS({
3334
3436
  }
3335
3437
  const g = r.records[0];
3336
3438
  return {
3337
- notes: num4(g.get("notes")),
3338
- media: num4(g.get("media")),
3339
- parts: num4(g.get("parts")),
3340
- checklistDone: num4(g.get("checklistDone")),
3341
- checklistTotal: num4(g.get("checklistTotal")),
3342
- signatures: num4(g.get("signatures"))
3439
+ notes: num5(g.get("notes")),
3440
+ media: num5(g.get("media")),
3441
+ parts: num5(g.get("parts")),
3442
+ checklistDone: num5(g.get("checklistDone")),
3443
+ checklistTotal: num5(g.get("checklistTotal")),
3444
+ signatures: num5(g.get("signatures"))
3343
3445
  };
3344
3446
  }
3345
3447
  async function readVisitsForJob3(session, accountId, jobId) {
@@ -3392,7 +3494,7 @@ var require_read = __commonJS({
3392
3494
  workerId,
3393
3495
  arrivedPressedAt: str2(g.get("ownArrivedPressedAt")),
3394
3496
  arrivedObservedAt: str2(g.get("ownArrivedObservedAt")),
3395
- arrivedObservedDistanceM: dist == null ? null : num4(dist)
3497
+ arrivedObservedDistanceM: dist == null ? null : num5(dist)
3396
3498
  };
3397
3499
  });
3398
3500
  }
@@ -3517,9 +3619,9 @@ var require_read = __commonJS({
3517
3619
  const rad = g.get("geofenceRadiusM");
3518
3620
  return {
3519
3621
  siteId: String(g.get("siteId")),
3520
- latitude: lat == null ? null : num4(lat),
3521
- longitude: lon == null ? null : num4(lon),
3522
- geofenceRadiusM: rad == null ? null : num4(rad)
3622
+ latitude: lat == null ? null : num5(lat),
3623
+ longitude: lon == null ? null : num5(lon),
3624
+ geofenceRadiusM: rad == null ? null : num5(rad)
3523
3625
  };
3524
3626
  }
3525
3627
  var DRAFT_RETURN = `d.draftId AS draftId, d.receivedAt AS receivedAt, d.source AS source,
@@ -3584,7 +3686,7 @@ var require_read = __commonJS({
3584
3686
  notes: g.get("notes") ?? [],
3585
3687
  // A count, not the references. A worker at a door needs to know whether
3586
3688
  // photographs exist before deciding to load them on a bad signal.
3587
- mediaCount: num4(g.get("mediaCount") ?? 0),
3689
+ mediaCount: num5(g.get("mediaCount") ?? 0),
3588
3690
  ...projectAttendances2(g)
3589
3691
  }));
3590
3692
  }
@@ -3709,7 +3811,7 @@ var require_read = __commonJS({
3709
3811
  workerId,
3710
3812
  arrivedPressedAt: str2(g.get("ownArrivedPressedAt")),
3711
3813
  arrivedObservedAt: str2(g.get("ownArrivedObservedAt")),
3712
- arrivedObservedDistanceM: dist == null ? null : num4(dist)
3814
+ arrivedObservedDistanceM: dist == null ? null : num5(dist)
3713
3815
  });
3714
3816
  });
3715
3817
  });
@@ -3749,9 +3851,9 @@ var require_read = __commonJS({
3749
3851
  sum(CASE WHEN overdue THEN 1 ELSE 0 END) AS overdue`, { accountId, nowIso, terminalJob: terminalJobStatuses2 });
3750
3852
  const g = r.records[0];
3751
3853
  return {
3752
- total: num4(g?.get("total")),
3753
- unallocated: num4(g?.get("unallocated")),
3754
- overdue: num4(g?.get("overdue"))
3854
+ total: num5(g?.get("total")),
3855
+ unallocated: num5(g?.get("unallocated")),
3856
+ overdue: num5(g?.get("overdue"))
3755
3857
  };
3756
3858
  });
3757
3859
  }
@@ -3792,7 +3894,7 @@ var require_dist3 = __commonJS({
3792
3894
  "../lib/dispatch-read/dist/index.js"(exports) {
3793
3895
  "use strict";
3794
3896
  Object.defineProperty(exports, "__esModule", { value: true });
3795
- exports.readWorkerDay = exports.readJobList = exports.readDay = exports.readCounts = exports.nonZuluStarts = exports.geofenceVerdict = exports.distanceM = exports.readVisitsInWindow = exports.readVisitsForJob = exports.readVisitRecordState = exports.readVisit = exports.readSiteLocation = exports.readSiteHistory = exports.readPreviousVisits = exports.readOpenVisitsForWorker = exports.readNodeId = exports.readJobs = exports.readJob = exports.readFieldVisit = exports.readDrafts = exports.readDraft = exports.readCommitments = exports.projectVisit = exports.projectJob = exports.projectFieldVisit = exports.buildJobListQuery = exports.zonedRangeBounds = exports.zonedDayStartUtc = exports.ymdInZone = exports.minutesIntoDay = exports.daysBetween = exports.currentWeekYmd = exports.addDaysYmd = exports.MAX_RANGE_DAYS = exports.resolveAccountZone = exports.withSession = exports.parseDispatchCallback = exports.dispatchCallbackData = exports.buildVisitKeyboard = exports.CALLBACK_DATA_MAX_BYTES = exports.workerForTelegramUser = exports.terminalVisitStatuses = exports.terminalJobStatuses = exports.loadDispatchConfig = exports.RECORDABLE_FIELDS = void 0;
3897
+ exports.readWorkerDay = exports.readJobList = exports.readDay = exports.readCounts = exports.nonZuluStarts = exports.geofenceVerdict = exports.distanceM = exports.readVisitsInWindow = exports.readVisitsForJob = exports.readVisitRecordState = exports.readVisit = exports.readSiteLocation = exports.readSiteHistory = exports.readPreviousVisits = exports.readOpenVisitsForWorker = exports.readNodeId = exports.readJobs = exports.readJob = exports.readFieldVisit = exports.readDrafts = exports.readDraft = exports.readCommitments = exports.projectVisit = exports.projectJob = exports.projectFieldVisit = exports.buildJobListQuery = exports.zonedRangeBounds = exports.zonedDayStartUtc = exports.ymdInZone = exports.minutesIntoDay = exports.daysBetween = exports.currentWeekYmd = exports.addDaysYmd = exports.MAX_RANGE_DAYS = exports.resolveAccountZone = exports.withSession = exports.selectCandidates = exports.overlaps = exports.parseDispatchCallback = exports.dispatchCallbackData = exports.buildVisitKeyboard = exports.CALLBACK_DATA_MAX_BYTES = exports.workerForTelegramUser = exports.terminalVisitStatuses = exports.terminalJobStatuses = exports.loadDispatchConfig = exports.RECORDABLE_FIELDS = void 0;
3796
3898
  var config_js_1 = require_config();
3797
3899
  Object.defineProperty(exports, "RECORDABLE_FIELDS", { enumerable: true, get: function() {
3798
3900
  return config_js_1.RECORDABLE_FIELDS;
@@ -3822,6 +3924,13 @@ var require_dist3 = __commonJS({
3822
3924
  Object.defineProperty(exports, "parseDispatchCallback", { enumerable: true, get: function() {
3823
3925
  return keyboard_js_1.parseDispatchCallback;
3824
3926
  } });
3927
+ var allocate_js_1 = require_allocate();
3928
+ Object.defineProperty(exports, "overlaps", { enumerable: true, get: function() {
3929
+ return allocate_js_1.overlaps;
3930
+ } });
3931
+ Object.defineProperty(exports, "selectCandidates", { enumerable: true, get: function() {
3932
+ return allocate_js_1.selectCandidates;
3933
+ } });
3825
3934
  var session_js_1 = require_session();
3826
3935
  Object.defineProperty(exports, "withSession", { enumerable: true, get: function() {
3827
3936
  return session_js_1.withSession;
@@ -5418,9 +5527,186 @@ import { homedir as homedir5 } from "os";
5418
5527
  init_route_cache();
5419
5528
  init_account();
5420
5529
  init_message_store();
5421
- init_src2();
5422
5530
  import { watch } from "fs";
5423
- import { resolve } from "path";
5531
+ import { resolve as resolve2 } from "path";
5532
+
5533
+ // app/lib/telegram/message-store.ts
5534
+ import { existsSync, mkdirSync, readFileSync, appendFileSync, readdirSync, statSync } from "fs";
5535
+ import { homedir } from "os";
5536
+ import { dirname, join, resolve } from "path";
5537
+ function configDirName() {
5538
+ const platformRoot5 = process.env.MAXY_PLATFORM_ROOT ?? resolve(process.cwd(), "..");
5539
+ const brandPath = join(platformRoot5, "config", "brand.json");
5540
+ if (existsSync(brandPath)) {
5541
+ try {
5542
+ return JSON.parse(readFileSync(brandPath, "utf-8")).configDir ?? ".maxy";
5543
+ } catch {
5544
+ }
5545
+ }
5546
+ return ".maxy";
5547
+ }
5548
+ var STORE_ROOT2 = () => join(process.env.MAXY_DIR_OVERRIDE ?? resolve(homedir(), configDirName()), "data", "telegram-messages");
5549
+ function isStorableChannelKey(channelKey) {
5550
+ return /^-?\d+:-?\d+$/.test(channelKey);
5551
+ }
5552
+ function channelKeyToFileStem(channelKey) {
5553
+ if (!isStorableChannelKey(channelKey)) {
5554
+ throw new Error(`telegram-message-store: refusing unstorable channelKey ${JSON.stringify(channelKey)}`);
5555
+ }
5556
+ return channelKey.replace(":", "_");
5557
+ }
5558
+ function fileStemToChannelKey(stem) {
5559
+ const i = stem.indexOf("_");
5560
+ return i <= 0 ? stem : `${stem.slice(0, i)}:${stem.slice(i + 1)}`;
5561
+ }
5562
+ function conversationFile(accountId, channelKey) {
5563
+ return join(STORE_ROOT2(), accountId, `${channelKeyToFileStem(channelKey)}.jsonl`);
5564
+ }
5565
+ function conversationFilePath2(accountId, channelKey) {
5566
+ return conversationFile(accountId, channelKey);
5567
+ }
5568
+ function isStorableStem(stem) {
5569
+ return isStorableChannelKey(fileStemToChannelKey(stem));
5570
+ }
5571
+ function listStoredAccountIds2() {
5572
+ const root = STORE_ROOT2();
5573
+ if (!existsSync(root)) return [];
5574
+ return readdirSync(root).filter((name) => {
5575
+ try {
5576
+ return statSync(join(root, name)).isDirectory();
5577
+ } catch {
5578
+ return false;
5579
+ }
5580
+ });
5581
+ }
5582
+ function parseLines(raw) {
5583
+ const out = [];
5584
+ for (const line of raw.split("\n")) {
5585
+ if (!line.trim()) continue;
5586
+ try {
5587
+ out.push(JSON.parse(line));
5588
+ } catch {
5589
+ console.error(`[telegram-message-store] skip reason=corrupt-line`);
5590
+ }
5591
+ }
5592
+ return out;
5593
+ }
5594
+ var byDateSentAsc = (a, b) => a.dateSent.localeCompare(b.dateSent);
5595
+ function appendMessage2(accountId, record) {
5596
+ if (!isStorableChannelKey(record.channelKey)) {
5597
+ console.error(
5598
+ `[telegram-message-store] FAIL op=append reason=unstorable-channel-key messageId=${record.messageId}`
5599
+ );
5600
+ return { created: false };
5601
+ }
5602
+ const file = conversationFile(accountId, record.channelKey);
5603
+ try {
5604
+ mkdirSync(dirname(file), { recursive: true });
5605
+ } catch (err) {
5606
+ const reason = err instanceof Error ? err.message : String(err);
5607
+ console.error(
5608
+ `[telegram-message-store] FAIL op=append reason=${reason.slice(0, 100)} messageId=${record.messageId}`
5609
+ );
5610
+ return { created: false };
5611
+ }
5612
+ try {
5613
+ if (existsSync(file)) {
5614
+ const existing = parseLines(readFileSync(file, "utf8"));
5615
+ if (existing.some((r) => r.messageId === record.messageId)) {
5616
+ console.error(
5617
+ `[telegram-message-store] op=append channelKey=${record.channelKey} origin=${record.origin} created=false`
5618
+ );
5619
+ return { created: false };
5620
+ }
5621
+ }
5622
+ appendFileSync(file, `${JSON.stringify(record)}
5623
+ `);
5624
+ console.error(
5625
+ `[telegram-message-store] op=append channelKey=${record.channelKey} origin=${record.origin} created=true`
5626
+ );
5627
+ return { created: true };
5628
+ } catch (err) {
5629
+ const reason = err instanceof Error ? err.message : String(err);
5630
+ console.error(
5631
+ `[telegram-message-store] FAIL op=append reason=${reason.slice(0, 100)} messageId=${record.messageId}`
5632
+ );
5633
+ return { created: false };
5634
+ }
5635
+ }
5636
+ function readConversation2(accountId, channelKey) {
5637
+ if (!isStorableChannelKey(channelKey)) return [];
5638
+ const file = conversationFile(accountId, channelKey);
5639
+ try {
5640
+ if (!existsSync(file)) return [];
5641
+ return parseLines(readFileSync(file, "utf8")).sort(byDateSentAsc);
5642
+ } catch (err) {
5643
+ const reason = err instanceof Error ? err.message : String(err);
5644
+ console.error(
5645
+ `[telegram-message-store] FAIL op=read reason=${reason.slice(0, 100)} channelKey=${channelKey}`
5646
+ );
5647
+ return [];
5648
+ }
5649
+ }
5650
+ var summaryCache = /* @__PURE__ */ new Map();
5651
+ function summariseRecords(records2) {
5652
+ if (records2.length === 0) return null;
5653
+ const sorted = records2.slice().sort(byDateSentAsc);
5654
+ return { first: sorted[0], last: sorted[sorted.length - 1] };
5655
+ }
5656
+ function readConversationSummaries2(accountId) {
5657
+ const dir = join(STORE_ROOT2(), accountId);
5658
+ const out = /* @__PURE__ */ new Map();
5659
+ const seen = /* @__PURE__ */ new Set();
5660
+ try {
5661
+ if (!existsSync(dir)) return out;
5662
+ for (const entry2 of readdirSync(dir)) {
5663
+ if (!entry2.endsWith(".jsonl")) continue;
5664
+ const stem = entry2.slice(0, -".jsonl".length);
5665
+ if (!isStorableStem(stem)) continue;
5666
+ const channelKey = fileStemToChannelKey(stem);
5667
+ const file = join(dir, entry2);
5668
+ seen.add(file);
5669
+ let size;
5670
+ let mtimeMs;
5671
+ try {
5672
+ const st = statSync(file);
5673
+ size = st.size;
5674
+ mtimeMs = st.mtimeMs;
5675
+ } catch {
5676
+ continue;
5677
+ }
5678
+ const cached3 = summaryCache.get(file);
5679
+ if (cached3 && cached3.size === size && cached3.mtimeMs === mtimeMs) {
5680
+ out.set(channelKey, cached3.summary);
5681
+ continue;
5682
+ }
5683
+ let summary;
5684
+ try {
5685
+ summary = summariseRecords(parseLines(readFileSync(file, "utf8")));
5686
+ } catch (err) {
5687
+ const reason = err instanceof Error ? err.message : String(err);
5688
+ console.error(
5689
+ `[telegram-message-store] FAIL op=summarise reason=${reason.slice(0, 100)} channelKey=${channelKey}`
5690
+ );
5691
+ continue;
5692
+ }
5693
+ if (!summary) continue;
5694
+ summaryCache.set(file, { size, mtimeMs, summary });
5695
+ out.set(channelKey, summary);
5696
+ }
5697
+ } catch (err) {
5698
+ const reason = err instanceof Error ? err.message : String(err);
5699
+ console.error(`[telegram-message-store] FAIL op=summarise-dir reason=${reason.slice(0, 100)}`);
5700
+ return out;
5701
+ }
5702
+ for (const key2 of [...summaryCache.keys()]) {
5703
+ if (key2.startsWith(dir) && !seen.has(key2)) summaryCache.delete(key2);
5704
+ }
5705
+ return out;
5706
+ }
5707
+
5708
+ // server/lib/route-cache-watchers.ts
5709
+ init_src2();
5424
5710
  function watchDirInto(dir, resource, accountId) {
5425
5711
  try {
5426
5712
  const w = watch(dir, { recursive: true }, () => {
@@ -5441,9 +5727,9 @@ function watchDirInto(dir, resource, accountId) {
5441
5727
  var perAccountAttached = /* @__PURE__ */ new Set();
5442
5728
  function ensureAccountWatchers(accountId, accountDir) {
5443
5729
  for (const [dir, resource] of [
5444
- [resolve(accountDir, "plugins"), "skills"],
5445
- [resolve(accountDir, "agents", "admin"), "sidebar-artefacts"],
5446
- [resolve(accountDir, "specialists", "agents"), "sidebar-artefacts"]
5730
+ [resolve2(accountDir, "plugins"), "skills"],
5731
+ [resolve2(accountDir, "agents", "admin"), "sidebar-artefacts"],
5732
+ [resolve2(accountDir, "specialists", "agents"), "sidebar-artefacts"]
5447
5733
  ]) {
5448
5734
  if (perAccountAttached.has(dir)) continue;
5449
5735
  const { attached: attached2 } = watchDirInto(dir, resource, accountId);
@@ -5456,11 +5742,11 @@ function attachRouteCacheWatchers() {
5456
5742
  attached = true;
5457
5743
  const configDir = process.env.CLAUDE_CONFIG_DIR;
5458
5744
  if (configDir) {
5459
- watchDirInto(resolve(configDir, "projects"), "sidebar-sessions", null);
5745
+ watchDirInto(resolve2(configDir, "projects"), "sidebar-sessions", null);
5460
5746
  } else {
5461
5747
  console.error("[admin-events] op=watch-failed resource=sidebar-sessions reason=CLAUDE_CONFIG_DIR-unset");
5462
5748
  }
5463
- watchDirInto(resolve(PLATFORM_ROOT, "templates", "specialists", "agents"), "sidebar-artefacts", null);
5749
+ watchDirInto(resolve2(PLATFORM_ROOT, "templates", "specialists", "agents"), "sidebar-artefacts", null);
5464
5750
  let houseId = null;
5465
5751
  try {
5466
5752
  houseId = resolveHouseOrSoleAccountId(ACCOUNTS_DIR);
@@ -5469,6 +5755,7 @@ function attachRouteCacheWatchers() {
5469
5755
  }
5470
5756
  if (houseId) {
5471
5757
  watchDirInto(STORE_ROOT(), "conversations-all", houseId);
5758
+ watchDirInto(STORE_ROOT2(), "conversations-all", houseId);
5472
5759
  } else {
5473
5760
  console.error("[admin-events] op=watch-failed resource=conversations-all reason=house-account-unresolved");
5474
5761
  }
@@ -5665,8 +5952,8 @@ var _baseMimes = {
5665
5952
  var baseMimes = _baseMimes;
5666
5953
 
5667
5954
  // node_modules/@hono/node-server/dist/serve-static.mjs
5668
- import { createReadStream, statSync, existsSync } from "fs";
5669
- import { join } from "path";
5955
+ import { createReadStream, statSync as statSync2, existsSync as existsSync2 } from "fs";
5956
+ import { join as join2 } from "path";
5670
5957
  import { versions } from "process";
5671
5958
  import { Readable } from "stream";
5672
5959
  var COMPRESSIBLE_CONTENT_TYPE_REGEX = /^\s*(?:text\/[^;\s]+|application\/(?:javascript|json|xml|xml-dtd|ecmascript|dart|postscript|rtf|tar|toml|vnd\.dart|vnd\.ms-fontobject|vnd\.ms-opentype|wasm|x-httpd-php|x-javascript|x-ns-proxy-autoconfig|x-sh|x-tar|x-virtualbox-hdd|x-virtualbox-ova|x-virtualbox-ovf|x-virtualbox-vbox|x-virtualbox-vdi|x-virtualbox-vhd|x-virtualbox-vmdk|x-www-form-urlencoded)|font\/(?:otf|ttf)|image\/(?:bmp|vnd\.adobe\.photoshop|vnd\.microsoft\.icon|vnd\.ms-dds|x-icon|x-ms-bmp)|message\/rfc822|model\/gltf-binary|x-shader\/x-fragment|x-shader\/x-vertex|[^;\s]+?\+(?:json|text|xml|yaml))(?:[;\s]|$)/i;
@@ -5706,7 +5993,7 @@ var createStreamBody = (stream2) => {
5706
5993
  var getStats = (path) => {
5707
5994
  let stats;
5708
5995
  try {
5709
- stats = statSync(path);
5996
+ stats = statSync2(path);
5710
5997
  } catch {
5711
5998
  }
5712
5999
  return stats;
@@ -5728,7 +6015,7 @@ var tryDecodeURI = (str2) => tryDecode(str2, decodeURI);
5728
6015
  var serveStatic = (options = { root: "" }) => {
5729
6016
  const root = options.root || "";
5730
6017
  const optionPath = options.path;
5731
- if (root !== "" && !existsSync(root)) {
6018
+ if (root !== "" && !existsSync2(root)) {
5732
6019
  console.error(`serveStatic: root path '${root}' is not found, are you sure it's correct?`);
5733
6020
  }
5734
6021
  return async (c, next) => {
@@ -5749,14 +6036,14 @@ var serveStatic = (options = { root: "" }) => {
5749
6036
  return next();
5750
6037
  }
5751
6038
  }
5752
- let path = join(
6039
+ let path = join2(
5753
6040
  root,
5754
6041
  !optionPath && options.rewriteRequestPath ? options.rewriteRequestPath(filename, c) : filename
5755
6042
  );
5756
6043
  let stats = getStats(path);
5757
6044
  if (stats && stats.isDirectory()) {
5758
6045
  const indexFile2 = options.index ?? "index.html";
5759
- path = join(path, indexFile2);
6046
+ path = join2(path, indexFile2);
5760
6047
  stats = getStats(path);
5761
6048
  }
5762
6049
  if (!stats) {
@@ -5814,13 +6101,13 @@ var serveStatic = (options = { root: "" }) => {
5814
6101
  };
5815
6102
 
5816
6103
  // server/lib/static-cache.ts
5817
- import { statSync as statSync2 } from "fs";
5818
- import { join as join2 } from "path";
6104
+ import { statSync as statSync3 } from "fs";
6105
+ import { join as join3 } from "path";
5819
6106
  var IMMUTABLE = "public, max-age=31536000, immutable";
5820
6107
  var REVALIDATE = "public, no-cache";
5821
6108
  function statOrNull(path) {
5822
6109
  try {
5823
- return statSync2(path);
6110
+ return statSync3(path);
5824
6111
  } catch {
5825
6112
  return null;
5826
6113
  }
@@ -5841,10 +6128,10 @@ function tryDecodeURI2(str2) {
5841
6128
  function resolveStaticFile(root, reqPath) {
5842
6129
  const filename = tryDecodeURI2(reqPath);
5843
6130
  if (/(?:^|[/\\])\.\.(?:$|[/\\])/.test(filename)) return null;
5844
- let path = join2(root, filename);
6131
+ let path = join3(root, filename);
5845
6132
  let stats = statOrNull(path);
5846
6133
  if (stats?.isDirectory()) {
5847
- path = join2(path, "index.html");
6134
+ path = join3(path, "index.html");
5848
6135
  stats = statOrNull(path);
5849
6136
  }
5850
6137
  if (!stats?.isFile()) return null;
@@ -5919,8 +6206,8 @@ ${clientErrorReporterBody}
5919
6206
  </script>`;
5920
6207
 
5921
6208
  // app/lib/whatsapp/ingest-audit.ts
5922
- import { readdirSync, readFileSync, statSync as statSync3 } from "fs";
5923
- import { join as join3 } from "path";
6209
+ import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync4 } from "fs";
6210
+ import { join as join4 } from "path";
5924
6211
  init_message_store();
5925
6212
  var TAG2 = "[wa-ingest-audit]";
5926
6213
  var NOTIFY_TAG = "[wa-notify]";
@@ -5941,16 +6228,16 @@ function readAllConversationsCached(accountId) {
5941
6228
  const out = /* @__PURE__ */ new Map();
5942
6229
  let dir;
5943
6230
  try {
5944
- dir = readdirSync(join3(STORE_ROOT(), accountId));
6231
+ dir = readdirSync2(join4(STORE_ROOT(), accountId));
5945
6232
  } catch {
5946
6233
  return out;
5947
6234
  }
5948
6235
  for (const file of dir) {
5949
6236
  if (!file.endsWith(".jsonl")) continue;
5950
- const path = join3(STORE_ROOT(), accountId, file);
6237
+ const path = join4(STORE_ROOT(), accountId, file);
5951
6238
  let stat11;
5952
6239
  try {
5953
- stat11 = statSync3(path);
6240
+ stat11 = statSync4(path);
5954
6241
  } catch {
5955
6242
  continue;
5956
6243
  }
@@ -5961,7 +6248,7 @@ function readAllConversationsCached(accountId) {
5961
6248
  }
5962
6249
  const parsed = [];
5963
6250
  try {
5964
- for (const line of readFileSync(path, "utf8").split("\n")) {
6251
+ for (const line of readFileSync2(path, "utf8").split("\n")) {
5965
6252
  if (!line.trim()) continue;
5966
6253
  try {
5967
6254
  const row = JSON.parse(line);
@@ -6077,17 +6364,17 @@ function runNotifyReconcile(opts) {
6077
6364
  init_parse_transcript();
6078
6365
  import { mkdtempSync, rmSync, writeFileSync } from "fs";
6079
6366
  import { tmpdir } from "os";
6080
- import { join as join4 } from "path";
6367
+ import { join as join5 } from "path";
6081
6368
 
6082
6369
  // server/lib/tail-window.ts
6083
- import { openSync, readSync, closeSync, statSync as statSync4 } from "fs";
6370
+ import { openSync, readSync, closeSync, statSync as statSync5 } from "fs";
6084
6371
  var TAIL_WINDOW_BYTES = 1024 * 1024;
6085
6372
  var ALIGN_SCAN_MAX_BYTES = 4 * 1024 * 1024;
6086
6373
  function tailWindowStart(fileEnd, budget = TAIL_WINDOW_BYTES) {
6087
6374
  return Math.max(0, fileEnd - budget);
6088
6375
  }
6089
6376
  function readRange(path, from, to) {
6090
- const size = statSync4(path).size;
6377
+ const size = statSync5(path).size;
6091
6378
  const start = Math.max(0, from);
6092
6379
  const end = Math.min(to, size);
6093
6380
  if (end <= start) return Buffer.alloc(0);
@@ -6209,8 +6496,62 @@ function censusSnapshot() {
6209
6496
  return { windowed: windowedOpens.size, withBackfill };
6210
6497
  }
6211
6498
 
6499
+ // server/lib/frame-strip-census.ts
6500
+ var MAX_ACCOUNTS = 50;
6501
+ var MAX_UNSTRIPPED_SESSIONS = 3;
6502
+ var byAccount = /* @__PURE__ */ new Map();
6503
+ function rowFor(accountId) {
6504
+ const existing = byAccount.get(accountId);
6505
+ if (existing) return existing;
6506
+ if (byAccount.size >= MAX_ACCOUNTS) {
6507
+ const oldest = byAccount.keys().next();
6508
+ if (!oldest.done) byAccount.delete(oldest.value);
6509
+ }
6510
+ const fresh = {
6511
+ accountId,
6512
+ rows: 0,
6513
+ wrapped: 0,
6514
+ nested: 0,
6515
+ stripped: 0,
6516
+ unstripped: 0,
6517
+ sources: { whatsapp: 0, webchat: 0, telegram: 0, none: 0 },
6518
+ unstrippedSessions: []
6519
+ };
6520
+ byAccount.set(accountId, fresh);
6521
+ return fresh;
6522
+ }
6523
+ function foldFrameCensus(accountId, sessionId, acc) {
6524
+ if (acc.rows === 0) return;
6525
+ const row = rowFor(accountId);
6526
+ row.rows += acc.rows;
6527
+ row.wrapped += acc.wrapped;
6528
+ row.nested += acc.nested;
6529
+ row.stripped += acc.stripped;
6530
+ row.unstripped += acc.unstripped;
6531
+ row.sources.whatsapp += acc.sources.whatsapp;
6532
+ row.sources.webchat += acc.sources.webchat;
6533
+ row.sources.telegram += acc.sources.telegram;
6534
+ row.sources.none += acc.sources.none;
6535
+ if (acc.unstripped > 0 && row.unstrippedSessions.length < MAX_UNSTRIPPED_SESSIONS && !row.unstrippedSessions.includes(sessionId)) {
6536
+ row.unstrippedSessions.push(sessionId);
6537
+ }
6538
+ acc.rows = 0;
6539
+ acc.wrapped = 0;
6540
+ acc.nested = 0;
6541
+ acc.stripped = 0;
6542
+ acc.unstripped = 0;
6543
+ acc.sources.whatsapp = 0;
6544
+ acc.sources.webchat = 0;
6545
+ acc.sources.telegram = 0;
6546
+ acc.sources.none = 0;
6547
+ }
6548
+ function frameCensusSnapshot() {
6549
+ return [...byAccount.values()];
6550
+ }
6551
+
6212
6552
  // server/lib/reader-window-audit.ts
6213
6553
  var TAG3 = "[reader-audit]";
6554
+ var FRAME_TAG = "[chat-reader]";
6214
6555
  var FIXTURE_ROWS = 60;
6215
6556
  var FIXTURE_BUDGET = 2e3;
6216
6557
  function fixtureBody() {
@@ -6244,8 +6585,25 @@ var key = (t) => JSON.stringify(t);
6244
6585
  function runReaderWindowAudit() {
6245
6586
  const census = censusSnapshot();
6246
6587
  console.log(`${TAG3} op=backfill-reach windowed=${census.windowed} withBackfill=${census.withBackfill}`);
6247
- const dir = mkdtempSync(join4(tmpdir(), "reader-audit-"));
6248
- const path = join4(dir, "fixture.jsonl");
6588
+ const frames = frameCensusSnapshot();
6589
+ let frameRows = 0;
6590
+ let frameUnstripped = 0;
6591
+ if (frames.length === 0) {
6592
+ console.log(`${FRAME_TAG} op=frame-census accounts=0`);
6593
+ }
6594
+ for (const f of frames) {
6595
+ frameRows += f.rows;
6596
+ frameUnstripped += f.unstripped;
6597
+ console.log(
6598
+ `${FRAME_TAG} op=frame-census accountId=${f.accountId} rows=${f.rows} wrapped=${f.wrapped} nested=${f.nested} stripped=${f.stripped} sources=whatsapp:${f.sources.whatsapp},webchat:${f.sources.webchat},telegram:${f.sources.telegram},none:${f.sources.none}`
6599
+ );
6600
+ console.log(
6601
+ `${FRAME_TAG} op=frame-unstripped accountId=${f.accountId} rows=${f.unstripped} sessions=${f.unstrippedSessions.join(",")}`
6602
+ );
6603
+ }
6604
+ const frameSummary = `frames=${frameRows} unstripped=${frameUnstripped}`;
6605
+ const dir = mkdtempSync(join5(tmpdir(), "reader-audit-"));
6606
+ const path = join5(dir, "fixture.jsonl");
6249
6607
  try {
6250
6608
  const body = fixtureBody();
6251
6609
  writeFileSync(path, body);
@@ -6290,11 +6648,11 @@ function runReaderWindowAudit() {
6290
6648
  `${TAG3} op=window-kinds fixture=synthetic windowTurns=${windowTurns.length} mismatched=${mismatched}`
6291
6649
  );
6292
6650
  const ok2 = duplicates === 0 && gaps === 0 && ordered && walkTerminated && mismatched === 0;
6293
- return `continuity=${ok2 ? "ok" : "FAILED"} full=${full.length} assembled=${assembled.length} walk=${walkTerminated ? "ok" : "STUCK"} reach=${census.withBackfill}/${census.windowed}`;
6651
+ return `continuity=${ok2 ? "ok" : "FAILED"} full=${full.length} assembled=${assembled.length} walk=${walkTerminated ? "ok" : "STUCK"} reach=${census.withBackfill}/${census.windowed} ${frameSummary}`;
6294
6652
  } catch (err) {
6295
6653
  const reason = err instanceof Error ? err.message : String(err);
6296
6654
  console.error(`${TAG3} op=audit-failed reason=${reason.slice(0, 120)}`);
6297
- return `continuity=ERROR ${reason.slice(0, 60)}`;
6655
+ return `continuity=ERROR ${reason.slice(0, 60)} ${frameSummary}`;
6298
6656
  } finally {
6299
6657
  try {
6300
6658
  rmSync(dir, { recursive: true, force: true });
@@ -6356,12 +6714,12 @@ init_src();
6356
6714
  init_pwa();
6357
6715
 
6358
6716
  // server/webchat-descriptor.ts
6359
- import { resolve as resolve2, join as join5 } from "path";
6717
+ import { resolve as resolve3, join as join6 } from "path";
6360
6718
  function webchatGatewayUrl() {
6361
6719
  return `http://127.0.0.1:${process.env.MAXY_UI_INTERNAL_PORT ?? ""}`;
6362
6720
  }
6363
6721
  function webchatServerPath() {
6364
- return process.env.MAXY_WEBCHAT_CHANNEL_SERVER_PATH ?? resolve2(process.env.MAXY_PLATFORM_ROOT ?? join5(__dirname, ".."), "services/webchat-channel/dist/server.js");
6722
+ return process.env.MAXY_WEBCHAT_CHANNEL_SERVER_PATH ?? resolve3(process.env.MAXY_PLATFORM_ROOT ?? join6(__dirname, ".."), "services/webchat-channel/dist/server.js");
6365
6723
  }
6366
6724
  function webchatChannelDescriptor(key2) {
6367
6725
  return { key: key2, gatewayUrl: webchatGatewayUrl(), serverPath: webchatServerPath() };
@@ -6379,7 +6737,7 @@ init_paths();
6379
6737
  // app/lib/claude-auth.ts
6380
6738
  init_paths();
6381
6739
  init_loop_registry();
6382
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, renameSync, unlinkSync } from "fs";
6740
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, renameSync, unlinkSync } from "fs";
6383
6741
  import { sep } from "path";
6384
6742
  import { lock } from "proper-lockfile";
6385
6743
  if (!CLAUDE_CREDENTIALS_FILE.startsWith(MAXY_DIR + sep)) {
@@ -6396,7 +6754,7 @@ var REFRESH_NOT_FIRING_THRESHOLD = 2;
6396
6754
  function readCredentials() {
6397
6755
  let raw;
6398
6756
  try {
6399
- raw = readFileSync2(CLAUDE_CREDENTIALS_FILE, "utf-8");
6757
+ raw = readFileSync3(CLAUDE_CREDENTIALS_FILE, "utf-8");
6400
6758
  } catch {
6401
6759
  return null;
6402
6760
  }
@@ -6424,7 +6782,7 @@ function writeOAuthCredentials(tokens) {
6424
6782
  try {
6425
6783
  let fileData = {};
6426
6784
  try {
6427
- const rawFile = readFileSync2(CLAUDE_CREDENTIALS_FILE, "utf-8");
6785
+ const rawFile = readFileSync3(CLAUDE_CREDENTIALS_FILE, "utf-8");
6428
6786
  fileData = JSON.parse(rawFile);
6429
6787
  } catch {
6430
6788
  }
@@ -6652,7 +7010,7 @@ init_helpers();
6652
7010
  // server/routes/health.ts
6653
7011
  init_dist();
6654
7012
  init_paths();
6655
- import { existsSync as existsSync2, readFileSync as readFileSync4 } from "fs";
7013
+ import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
6656
7014
  import { createConnection as createConnection2 } from "net";
6657
7015
 
6658
7016
  // app/lib/network.ts
@@ -6677,11 +7035,11 @@ function getLanIp() {
6677
7035
  init_paths();
6678
7036
  import { spawnSync, execFileSync } from "child_process";
6679
7037
  import { createConnection } from "net";
6680
- import { mkdirSync, readFileSync as readFileSync3, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
6681
- import { resolve as resolve3 } from "path";
7038
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync4, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
7039
+ import { resolve as resolve4 } from "path";
6682
7040
  init_remote_auth();
6683
- var PLATFORM_ROOT2 = process.env.MAXY_PLATFORM_ROOT ?? resolve3(process.cwd(), "..");
6684
- var VNC_SCRIPT = resolve3(PLATFORM_ROOT2, "scripts/vnc.sh");
7041
+ var PLATFORM_ROOT2 = process.env.MAXY_PLATFORM_ROOT ?? resolve4(process.cwd(), "..");
7042
+ var VNC_SCRIPT = resolve4(PLATFORM_ROOT2, "scripts/vnc.sh");
6685
7043
  var displayMode = process.env.DISPLAY_MODE ?? "virtual";
6686
7044
  if (displayMode === "native") {
6687
7045
  console.log(`[vnc] DISPLAY_MODE=native \u2014 local requests use desktop display, remote requests use VNC`);
@@ -6736,7 +7094,7 @@ function discoverNativeDisplay() {
6736
7094
  const leaderPid = leaderResult.stdout?.trim();
6737
7095
  if (leaderPid) {
6738
7096
  try {
6739
- const environ = readFileSync3(`/proc/${leaderPid}/environ`, "utf8");
7097
+ const environ = readFileSync4(`/proc/${leaderPid}/environ`, "utf8");
6740
7098
  const match = environ.split("\0").find((e) => e.startsWith("WAYLAND_DISPLAY="));
6741
7099
  if (match) waylandDisplay = match.split("=")[1];
6742
7100
  } catch {
@@ -6791,10 +7149,10 @@ async function waitForPort(port2, timeoutMs = 12e3) {
6791
7149
  return false;
6792
7150
  }
6793
7151
  function ensureLogDir() {
6794
- mkdirSync(LOG_DIR, { recursive: true });
7152
+ mkdirSync2(LOG_DIR, { recursive: true });
6795
7153
  }
6796
7154
  function logPath(name) {
6797
- return resolve3(LOG_DIR, `${name}.log`);
7155
+ return resolve4(LOG_DIR, `${name}.log`);
6798
7156
  }
6799
7157
  async function ensureVnc() {
6800
7158
  if (process.platform !== "linux") {
@@ -6879,19 +7237,19 @@ async function ensureCdp(transport = "vnc") {
6879
7237
  function killChromium() {
6880
7238
  spawnSync("pkill", ["-f", "chromium"], { stdio: "pipe" });
6881
7239
  }
6882
- var AUTH_TARGET_ID_FILE = resolve3(LOG_DIR, "claude-auth-target.id");
7240
+ var AUTH_TARGET_ID_FILE = resolve4(LOG_DIR, "claude-auth-target.id");
6883
7241
  function readAuthTargetId() {
6884
7242
  try {
6885
- const id = readFileSync3(AUTH_TARGET_ID_FILE, "utf8").trim();
7243
+ const id = readFileSync4(AUTH_TARGET_ID_FILE, "utf8").trim();
6886
7244
  return id.length > 0 ? id : null;
6887
7245
  } catch {
6888
7246
  return null;
6889
7247
  }
6890
7248
  }
6891
7249
  function writeChromiumWrapper() {
6892
- mkdirSync(BIN_DIR, { recursive: true });
7250
+ mkdirSync2(BIN_DIR, { recursive: true });
6893
7251
  rmSync2(AUTH_TARGET_ID_FILE, { force: true });
6894
- const wrapperPath = resolve3(BIN_DIR, "chromium");
7252
+ const wrapperPath = resolve4(BIN_DIR, "chromium");
6895
7253
  writeFileSync3(wrapperPath, `#!/bin/bash
6896
7254
  # Generated by writeChromiumWrapper (app/lib/vnc.ts) \u2014 Task 1562.
6897
7255
  # Hands its URL argument to the brand's running Chromium over CDP.
@@ -6993,8 +7351,8 @@ app.get("/", async (c) => {
6993
7351
  const browserTransport = resolveBrowserTransport(c.req.raw, c.env?.incoming?.socket?.remoteAddress);
6994
7352
  let pinConfigured = false;
6995
7353
  try {
6996
- if (existsSync2(USERS_FILE)) {
6997
- const raw = readFileSync4(USERS_FILE, "utf-8").trim();
7354
+ if (existsSync3(USERS_FILE)) {
7355
+ const raw = readFileSync5(USERS_FILE, "utf-8").trim();
6998
7356
  if (raw) {
6999
7357
  const users = JSON.parse(raw);
7000
7358
  pinConfigured = Array.isArray(users) && users.length > 0;
@@ -7487,183 +7845,6 @@ init_dist();
7487
7845
  init_channel_key();
7488
7846
  import { statSync as statSync7 } from "fs";
7489
7847
  import { join as join12 } from "path";
7490
-
7491
- // app/lib/telegram/message-store.ts
7492
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync5, appendFileSync, readdirSync as readdirSync2, statSync as statSync5 } from "fs";
7493
- import { homedir } from "os";
7494
- import { dirname, join as join7, resolve as resolve4 } from "path";
7495
- function configDirName() {
7496
- const platformRoot5 = process.env.MAXY_PLATFORM_ROOT ?? resolve4(process.cwd(), "..");
7497
- const brandPath = join7(platformRoot5, "config", "brand.json");
7498
- if (existsSync3(brandPath)) {
7499
- try {
7500
- return JSON.parse(readFileSync5(brandPath, "utf-8")).configDir ?? ".maxy";
7501
- } catch {
7502
- }
7503
- }
7504
- return ".maxy";
7505
- }
7506
- var STORE_ROOT2 = () => join7(process.env.MAXY_DIR_OVERRIDE ?? resolve4(homedir(), configDirName()), "data", "telegram-messages");
7507
- function isStorableChannelKey(channelKey) {
7508
- return /^-?\d+:-?\d+$/.test(channelKey);
7509
- }
7510
- function channelKeyToFileStem(channelKey) {
7511
- if (!isStorableChannelKey(channelKey)) {
7512
- throw new Error(`telegram-message-store: refusing unstorable channelKey ${JSON.stringify(channelKey)}`);
7513
- }
7514
- return channelKey.replace(":", "_");
7515
- }
7516
- function fileStemToChannelKey(stem) {
7517
- const i = stem.indexOf("_");
7518
- return i <= 0 ? stem : `${stem.slice(0, i)}:${stem.slice(i + 1)}`;
7519
- }
7520
- function conversationFile(accountId, channelKey) {
7521
- return join7(STORE_ROOT2(), accountId, `${channelKeyToFileStem(channelKey)}.jsonl`);
7522
- }
7523
- function conversationFilePath2(accountId, channelKey) {
7524
- return conversationFile(accountId, channelKey);
7525
- }
7526
- function isStorableStem(stem) {
7527
- return isStorableChannelKey(fileStemToChannelKey(stem));
7528
- }
7529
- function listStoredAccountIds2() {
7530
- const root = STORE_ROOT2();
7531
- if (!existsSync3(root)) return [];
7532
- return readdirSync2(root).filter((name) => {
7533
- try {
7534
- return statSync5(join7(root, name)).isDirectory();
7535
- } catch {
7536
- return false;
7537
- }
7538
- });
7539
- }
7540
- function parseLines(raw) {
7541
- const out = [];
7542
- for (const line of raw.split("\n")) {
7543
- if (!line.trim()) continue;
7544
- try {
7545
- out.push(JSON.parse(line));
7546
- } catch {
7547
- console.error(`[telegram-message-store] skip reason=corrupt-line`);
7548
- }
7549
- }
7550
- return out;
7551
- }
7552
- var byDateSentAsc = (a, b) => a.dateSent.localeCompare(b.dateSent);
7553
- function appendMessage2(accountId, record) {
7554
- if (!isStorableChannelKey(record.channelKey)) {
7555
- console.error(
7556
- `[telegram-message-store] FAIL op=append reason=unstorable-channel-key messageId=${record.messageId}`
7557
- );
7558
- return { created: false };
7559
- }
7560
- const file = conversationFile(accountId, record.channelKey);
7561
- try {
7562
- mkdirSync2(dirname(file), { recursive: true });
7563
- } catch (err) {
7564
- const reason = err instanceof Error ? err.message : String(err);
7565
- console.error(
7566
- `[telegram-message-store] FAIL op=append reason=${reason.slice(0, 100)} messageId=${record.messageId}`
7567
- );
7568
- return { created: false };
7569
- }
7570
- try {
7571
- if (existsSync3(file)) {
7572
- const existing = parseLines(readFileSync5(file, "utf8"));
7573
- if (existing.some((r) => r.messageId === record.messageId)) {
7574
- console.error(
7575
- `[telegram-message-store] op=append channelKey=${record.channelKey} origin=${record.origin} created=false`
7576
- );
7577
- return { created: false };
7578
- }
7579
- }
7580
- appendFileSync(file, `${JSON.stringify(record)}
7581
- `);
7582
- console.error(
7583
- `[telegram-message-store] op=append channelKey=${record.channelKey} origin=${record.origin} created=true`
7584
- );
7585
- return { created: true };
7586
- } catch (err) {
7587
- const reason = err instanceof Error ? err.message : String(err);
7588
- console.error(
7589
- `[telegram-message-store] FAIL op=append reason=${reason.slice(0, 100)} messageId=${record.messageId}`
7590
- );
7591
- return { created: false };
7592
- }
7593
- }
7594
- function readConversation2(accountId, channelKey) {
7595
- if (!isStorableChannelKey(channelKey)) return [];
7596
- const file = conversationFile(accountId, channelKey);
7597
- try {
7598
- if (!existsSync3(file)) return [];
7599
- return parseLines(readFileSync5(file, "utf8")).sort(byDateSentAsc);
7600
- } catch (err) {
7601
- const reason = err instanceof Error ? err.message : String(err);
7602
- console.error(
7603
- `[telegram-message-store] FAIL op=read reason=${reason.slice(0, 100)} channelKey=${channelKey}`
7604
- );
7605
- return [];
7606
- }
7607
- }
7608
- var summaryCache = /* @__PURE__ */ new Map();
7609
- function summariseRecords(records2) {
7610
- if (records2.length === 0) return null;
7611
- const sorted = records2.slice().sort(byDateSentAsc);
7612
- return { first: sorted[0], last: sorted[sorted.length - 1] };
7613
- }
7614
- function readConversationSummaries2(accountId) {
7615
- const dir = join7(STORE_ROOT2(), accountId);
7616
- const out = /* @__PURE__ */ new Map();
7617
- const seen = /* @__PURE__ */ new Set();
7618
- try {
7619
- if (!existsSync3(dir)) return out;
7620
- for (const entry2 of readdirSync2(dir)) {
7621
- if (!entry2.endsWith(".jsonl")) continue;
7622
- const stem = entry2.slice(0, -".jsonl".length);
7623
- if (!isStorableStem(stem)) continue;
7624
- const channelKey = fileStemToChannelKey(stem);
7625
- const file = join7(dir, entry2);
7626
- seen.add(file);
7627
- let size;
7628
- let mtimeMs;
7629
- try {
7630
- const st = statSync5(file);
7631
- size = st.size;
7632
- mtimeMs = st.mtimeMs;
7633
- } catch {
7634
- continue;
7635
- }
7636
- const cached3 = summaryCache.get(file);
7637
- if (cached3 && cached3.size === size && cached3.mtimeMs === mtimeMs) {
7638
- out.set(channelKey, cached3.summary);
7639
- continue;
7640
- }
7641
- let summary;
7642
- try {
7643
- summary = summariseRecords(parseLines(readFileSync5(file, "utf8")));
7644
- } catch (err) {
7645
- const reason = err instanceof Error ? err.message : String(err);
7646
- console.error(
7647
- `[telegram-message-store] FAIL op=summarise reason=${reason.slice(0, 100)} channelKey=${channelKey}`
7648
- );
7649
- continue;
7650
- }
7651
- if (!summary) continue;
7652
- summaryCache.set(file, { size, mtimeMs, summary });
7653
- out.set(channelKey, summary);
7654
- }
7655
- } catch (err) {
7656
- const reason = err instanceof Error ? err.message : String(err);
7657
- console.error(`[telegram-message-store] FAIL op=summarise-dir reason=${reason.slice(0, 100)}`);
7658
- return out;
7659
- }
7660
- for (const key2 of [...summaryCache.keys()]) {
7661
- if (key2.startsWith(dir) && !seen.has(key2)) summaryCache.delete(key2);
7662
- }
7663
- return out;
7664
- }
7665
-
7666
- // server/routes/channel/schedule-inject.ts
7667
7848
  init_admin_session_id();
7668
7849
  init_normalize();
7669
7850
 
@@ -8246,6 +8427,32 @@ async function sendTelegramText(botToken, chatId, text) {
8246
8427
  }
8247
8428
  }
8248
8429
 
8430
+ // app/lib/telegram/outbound/send-chat-action.ts
8431
+ async function sendTelegramChatAction(botToken, chatId, key2, site) {
8432
+ const report = (ok2, reason) => console.error(
8433
+ `[telegram-native] op=chat-action key=${key2} site=${site} ok=${ok2}${ok2 ? "" : ` reason=${reason}`}`
8434
+ );
8435
+ try {
8436
+ const res = await fetch(`https://api.telegram.org/bot${botToken}/sendChatAction`, {
8437
+ method: "POST",
8438
+ headers: { "Content-Type": "application/json" },
8439
+ body: JSON.stringify({ chat_id: chatId, action: "typing" })
8440
+ });
8441
+ const data = await res.json();
8442
+ if (data.ok) {
8443
+ report(true);
8444
+ return { ok: true };
8445
+ }
8446
+ const error = data.description ?? "chat action failed";
8447
+ report(false, error);
8448
+ return { ok: false, error };
8449
+ } catch (e) {
8450
+ const error = e instanceof Error ? e.message : String(e);
8451
+ report(false, error);
8452
+ return { ok: false, error };
8453
+ }
8454
+ }
8455
+
8249
8456
  // server/routes/whatsapp.ts
8250
8457
  init_dist();
8251
8458
  import { basename as basename3, join as join17, resolve as resolve8 } from "path";
@@ -12706,6 +12913,32 @@ function buildVisitKeyboard(p) {
12706
12913
  return { rows, dropped };
12707
12914
  }
12708
12915
 
12916
+ // ../lib/dispatch-read/src/allocate.ts
12917
+ function selectCandidates(params) {
12918
+ const { jobType, cfg, commitments } = params;
12919
+ const type = cfg.jobTypes.find((t) => t.name === jobType);
12920
+ if (!type) {
12921
+ return { offered: [], excluded: [], required: "" };
12922
+ }
12923
+ const offered = [];
12924
+ const excluded = [];
12925
+ for (const worker of cfg.workers) {
12926
+ if (!worker.qualifications.includes(type.requiredQualification)) {
12927
+ excluded.push({ workerId: worker.workerId, reason: "missing-qualification" });
12928
+ continue;
12929
+ }
12930
+ offered.push({
12931
+ workerId: worker.workerId,
12932
+ name: worker.name,
12933
+ conflicts: commitments.get(worker.workerId) ?? []
12934
+ });
12935
+ }
12936
+ return { offered, excluded, required: type.requiredQualification };
12937
+ }
12938
+ function overlaps(a, b) {
12939
+ return a.start < b.end && b.start < a.end;
12940
+ }
12941
+
12709
12942
  // ../lib/dispatch-read/src/session.ts
12710
12943
  import { readFileSync as readFileSync15 } from "fs";
12711
12944
  import { resolve as resolve12 } from "path";
@@ -12989,6 +13222,30 @@ async function readOpenVisitsForWorker(session, accountId, workerId, terminalSta
12989
13222
  };
12990
13223
  });
12991
13224
  }
13225
+ async function readCommitments(session, accountId, from, to) {
13226
+ const r = await session.run(
13227
+ // One row per ATTENDANCE is correct here and nowhere else: a commitment
13228
+ // belongs to a worker, so a two-worker visit legitimately commits both.
13229
+ `MATCH (v:Visit {accountId: $accountId})-[:HAS_ATTENDANCE]->(:Attendance)-[:BY_ENGINEER]->(e:Engineer)
13230
+ WHERE v.startDate < $to AND coalesce(v.endDate, v.startDate) > $from
13231
+ RETURN e.engineerId AS workerId, v.visitId AS visitId,
13232
+ v.startDate AS start, coalesce(v.endDate, v.startDate) AS end`,
13233
+ { accountId, from, to }
13234
+ );
13235
+ const out = /* @__PURE__ */ new Map();
13236
+ for (const g of r.records) {
13237
+ const workerId = str(g.get("workerId"));
13238
+ if (!workerId) continue;
13239
+ const list = out.get(workerId) ?? [];
13240
+ list.push({
13241
+ visitId: String(g.get("visitId")),
13242
+ start: String(g.get("start")),
13243
+ end: String(g.get("end"))
13244
+ });
13245
+ out.set(workerId, list);
13246
+ }
13247
+ return out;
13248
+ }
12992
13249
  async function readSiteLocation(session, accountId, siteId) {
12993
13250
  const r = await session.run(
12994
13251
  `MATCH (s:Site {accountId: $accountId, siteId: $siteId})
@@ -13170,6 +13427,7 @@ var ZONE_FALLBACK = "UTC";
13170
13427
  var EMPTY_META = {
13171
13428
  vocabulary: { job: "", visit: "", worker: "" },
13172
13429
  statuses: [],
13430
+ transitions: [],
13173
13431
  zone: ""
13174
13432
  };
13175
13433
  var PORTAL_VISITS_PUSH_INTERVAL_MS = 6e4;
@@ -13185,17 +13443,28 @@ function parseVisitRows(raw) {
13185
13443
  const visitId = typeof o.visitId === "string" ? o.visitId : "";
13186
13444
  if (!visitId) continue;
13187
13445
  const str2 = (v) => typeof v === "string" ? v : "";
13446
+ const cnt = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
13188
13447
  out.push({
13189
13448
  visitId,
13190
13449
  ownerId: str2(o.ownerId),
13191
13450
  jobId: str2(o.jobId),
13192
13451
  status: str2(o.status),
13193
13452
  startDate: str2(o.startDate),
13194
- purpose: str2(o.purpose)
13453
+ purpose: str2(o.purpose),
13454
+ notes: cnt(o.notes),
13455
+ media: cnt(o.media),
13456
+ parts: cnt(o.parts),
13457
+ checklistDone: cnt(o.checklistDone),
13458
+ checklistTotal: cnt(o.checklistTotal),
13459
+ signatures: cnt(o.signatures)
13195
13460
  });
13196
13461
  }
13197
13462
  return out;
13198
13463
  }
13464
+ function num2(v) {
13465
+ const n = Number(v?.toNumber?.() ?? v);
13466
+ return Number.isFinite(n) ? n : 0;
13467
+ }
13199
13468
  async function readVisitsForAccount(accountId) {
13200
13469
  const session = getSession();
13201
13470
  try {
@@ -13207,6 +13476,12 @@ async function readVisitsForAccount(accountId) {
13207
13476
  // the worker sees no job and has nothing to report but confusion, where a
13208
13477
  // duplicate on the account-scope list is visible and reads as what it is.
13209
13478
  // The duplicate is real and is Task 2551.
13479
+ // The record counts ride the visit row (Task 2511). One projection rather
13480
+ // than a `readVisitRecordState` call per visit: this query already runs
13481
+ // once for the whole account, and a per-visit round trip would multiply
13482
+ // every tick by the account's whole visit history. The six expressions
13483
+ // are `readVisitRecordState`'s own, so the two cannot disagree about what
13484
+ // a count means.
13210
13485
  `MATCH (j:Job {accountId: $accountId})-[:HAS_VISIT]->(v:Visit {accountId: $accountId})
13211
13486
  OPTIONAL MATCH (v)-[:HAS_ATTENDANCE]->(:Attendance)-[:BY_ENGINEER]->(e:Engineer)
13212
13487
  RETURN toString(v.visitId) AS visitId,
@@ -13214,7 +13489,13 @@ async function readVisitsForAccount(accountId) {
13214
13489
  coalesce(toString(j.jobId), '') AS jobId,
13215
13490
  coalesce(toString(v.status), '') AS status,
13216
13491
  coalesce(toString(v.startDate), '') AS startDate,
13217
- coalesce(toString(v.purpose), '') AS purpose`,
13492
+ coalesce(toString(v.purpose), '') AS purpose,
13493
+ size(coalesce(v.notes, [])) AS notes,
13494
+ size(coalesce(v.media, [])) AS media,
13495
+ size(coalesce(v.parts, [])) AS parts,
13496
+ size(coalesce(v.checklistDone, [])) AS checklistDone,
13497
+ size(coalesce(v.checklist, [])) AS checklistTotal,
13498
+ size(coalesce(v.signatures, [])) AS signatures`,
13218
13499
  { accountId }
13219
13500
  );
13220
13501
  return parseVisitRows([
@@ -13225,7 +13506,13 @@ async function readVisitsForAccount(accountId) {
13225
13506
  jobId: rec.get("jobId"),
13226
13507
  status: rec.get("status"),
13227
13508
  startDate: rec.get("startDate"),
13228
- purpose: rec.get("purpose")
13509
+ purpose: rec.get("purpose"),
13510
+ notes: num2(rec.get("notes")),
13511
+ media: num2(rec.get("media")),
13512
+ parts: num2(rec.get("parts")),
13513
+ checklistDone: num2(rec.get("checklistDone")),
13514
+ checklistTotal: num2(rec.get("checklistTotal")),
13515
+ signatures: num2(rec.get("signatures"))
13229
13516
  }))
13230
13517
  }
13231
13518
  ]);
@@ -13256,8 +13543,72 @@ async function readRosterForAccount(accountId, workers) {
13256
13543
  await session.close();
13257
13544
  }
13258
13545
  }
13546
+ async function readCandidatesForAccount(accountId, cfg) {
13547
+ const session = getSession();
13548
+ try {
13549
+ const open3 = await session.run(
13550
+ `MATCH (j:Job {accountId: $accountId})-[:HAS_VISIT]->(v:Visit {accountId: $accountId})
13551
+ WHERE NOT (v)-[:HAS_ATTENDANCE]->()
13552
+ RETURN toString(v.visitId) AS visitId,
13553
+ coalesce(toString(j.jobType), '') AS jobType,
13554
+ coalesce(toString(v.startDate), '') AS startDate,
13555
+ coalesce(toString(v.endDate), toString(v.startDate), '') AS endDate`,
13556
+ { accountId }
13557
+ );
13558
+ if (open3.records.length === 0) return [];
13559
+ const windows = open3.records.map((rec) => ({
13560
+ visitId: String(rec.get("visitId")),
13561
+ jobType: String(rec.get("jobType")),
13562
+ start: String(rec.get("startDate")),
13563
+ end: String(rec.get("endDate"))
13564
+ }));
13565
+ const dated = windows.filter((w) => w.start !== "");
13566
+ const from = dated.reduce((lo, w) => lo === "" || w.start < lo ? w.start : lo, "");
13567
+ const to = dated.reduce((hi, w) => w.end > hi ? w.end : hi, "");
13568
+ const committed = from === "" ? /* @__PURE__ */ new Map() : await readCommitments(session, accountId, from, to);
13569
+ const out = [];
13570
+ const nameOf = new Map(cfg.workers.map((w) => [w.workerId, w.name]));
13571
+ for (const w of windows) {
13572
+ const windowed = new Map(
13573
+ [...committed].map(([workerId, list]) => [
13574
+ workerId,
13575
+ // A visit with no start overlaps nothing rather than everything: an
13576
+ // undated visit is already listed separately by the page, and
13577
+ // reporting the whole roster as clashing with it would be false.
13578
+ w.start === "" ? [] : list.filter((c) => overlaps({ start: w.start, end: w.end }, c))
13579
+ ])
13580
+ );
13581
+ const selection = selectCandidates({ jobType: w.jobType, cfg, commitments: windowed });
13582
+ for (const c of selection.offered) {
13583
+ out.push({
13584
+ visitId: w.visitId,
13585
+ workerId: c.workerId,
13586
+ name: c.name,
13587
+ kind: "offered",
13588
+ reason: "",
13589
+ conflicts: c.conflicts
13590
+ });
13591
+ }
13592
+ for (const e of selection.excluded) {
13593
+ out.push({
13594
+ visitId: w.visitId,
13595
+ workerId: e.workerId,
13596
+ // The config is the authority on a worker's name, and an excluded
13597
+ // worker is never in `selection.offered` to carry one.
13598
+ name: nameOf.get(e.workerId) ?? "",
13599
+ kind: "excluded",
13600
+ reason: e.reason,
13601
+ conflicts: []
13602
+ });
13603
+ }
13604
+ }
13605
+ return out;
13606
+ } finally {
13607
+ await session.close();
13608
+ }
13609
+ }
13259
13610
  async function pushVisitsForAccount(opts) {
13260
- const { accountId, client, rows, roster, meta, log: log2 } = opts;
13611
+ const { accountId, client, rows, roster, meta, candidates, log: log2 } = opts;
13261
13612
  const unbound = rows.filter((r) => r.ownerId === "").length;
13262
13613
  const seen = /* @__PURE__ */ new Set();
13263
13614
  const shared = /* @__PURE__ */ new Set();
@@ -13268,7 +13619,7 @@ async function pushVisitsForAccount(opts) {
13268
13619
  const unresolved = roster.filter((w) => w.ownerId === "").length;
13269
13620
  if (rows.length === 0 && roster.length === 0) {
13270
13621
  log2(`${TAG19} op=skip account=${accountId} reason=no-visits-no-roster`);
13271
- return { rows: 0, unbound: 0, roster: 0, unresolved: 0 };
13622
+ return { rows: 0, unbound: 0, roster: 0, unresolved: 0, candidates: 0 };
13272
13623
  }
13273
13624
  const state = await client.query(
13274
13625
  "SELECT currentGeneration FROM visits_state WHERE accountId = ?",
@@ -13281,8 +13632,23 @@ async function pushVisitsForAccount(opts) {
13281
13632
  const next = Math.max(current, Number(staged[0]?.g ?? 0)) + 1;
13282
13633
  for (const row of rows) {
13283
13634
  await client.query(
13284
- "INSERT INTO visits (accountId, visitId, ownerId, jobId, status, startDate, purpose, generation) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
13285
- [accountId, row.visitId, row.ownerId, row.jobId, row.status, row.startDate, row.purpose, next]
13635
+ "INSERT INTO visits (accountId, visitId, ownerId, jobId, status, startDate, purpose, notes, media, parts, checklistDone, checklistTotal, signatures, generation) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
13636
+ [
13637
+ accountId,
13638
+ row.visitId,
13639
+ row.ownerId,
13640
+ row.jobId,
13641
+ row.status,
13642
+ row.startDate,
13643
+ row.purpose,
13644
+ row.notes,
13645
+ row.media,
13646
+ row.parts,
13647
+ row.checklistDone,
13648
+ row.checklistTotal,
13649
+ row.signatures,
13650
+ next
13651
+ ]
13286
13652
  );
13287
13653
  }
13288
13654
  for (const w of roster) {
@@ -13293,19 +13659,26 @@ async function pushVisitsForAccount(opts) {
13293
13659
  }
13294
13660
  if (roster.length > 0) {
13295
13661
  await client.query(
13296
- "INSERT INTO dispatch_meta (accountId, vocabulary, statuses, zone, publishedAt, generation) VALUES (?, ?, ?, ?, ?, ?)",
13662
+ "INSERT INTO dispatch_meta (accountId, vocabulary, statuses, transitions, zone, publishedAt, generation) VALUES (?, ?, ?, ?, ?, ?, ?)",
13297
13663
  [
13298
13664
  accountId,
13299
13665
  JSON.stringify(meta.vocabulary),
13300
13666
  JSON.stringify(meta.statuses),
13667
+ JSON.stringify(meta.transitions),
13301
13668
  meta.zone,
13302
13669
  (/* @__PURE__ */ new Date()).toISOString(),
13303
13670
  next
13304
13671
  ]
13305
13672
  );
13306
13673
  }
13674
+ for (const c of candidates) {
13675
+ await client.query(
13676
+ "INSERT INTO visit_candidates (accountId, visitId, workerId, name, kind, reason, conflicts, generation) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
13677
+ [accountId, c.visitId, c.workerId, c.name, c.kind, c.reason, JSON.stringify(c.conflicts), next]
13678
+ );
13679
+ }
13307
13680
  log2(
13308
- `${TAG19} op=stage account=${accountId} generation=${next} rows=${rows.length} visits=${seen.size} shared=${shared.size} roster=${roster.length}`
13681
+ `${TAG19} op=stage account=${accountId} generation=${next} rows=${rows.length} visits=${seen.size} shared=${shared.size} roster=${roster.length} candidates=${candidates.length}`
13309
13682
  );
13310
13683
  await client.query(
13311
13684
  "INSERT INTO visits_state (accountId, currentGeneration) VALUES (?, ?) ON CONFLICT (accountId) DO UPDATE SET currentGeneration = excluded.currentGeneration WHERE excluded.currentGeneration > visits_state.currentGeneration",
@@ -13325,6 +13698,7 @@ async function pushVisitsForAccount(opts) {
13325
13698
  unbound,
13326
13699
  roster: roster.length,
13327
13700
  unresolved,
13701
+ candidates: candidates.length,
13328
13702
  generation: next,
13329
13703
  superseded: true
13330
13704
  };
@@ -13345,12 +13719,23 @@ async function pushVisitsForAccount(opts) {
13345
13719
  accountId,
13346
13720
  next
13347
13721
  ]);
13722
+ await client.query("DELETE FROM visit_candidates WHERE accountId = ? AND generation < ?", [
13723
+ accountId,
13724
+ next
13725
+ ]);
13348
13726
  } catch (err) {
13349
13727
  log2(
13350
13728
  `${TAG19} op=sweep account=${accountId} generation=${next} result=failed err="${err instanceof Error ? err.message : String(err)}"`
13351
13729
  );
13352
13730
  }
13353
- return { rows: rows.length, unbound, roster: roster.length, unresolved, generation: next };
13731
+ return {
13732
+ rows: rows.length,
13733
+ unbound,
13734
+ roster: roster.length,
13735
+ unresolved,
13736
+ candidates: candidates.length,
13737
+ generation: next
13738
+ };
13354
13739
  }
13355
13740
  function visitsStateFilePath(platformRoot5) {
13356
13741
  return resolve13(platformRoot5, "..", "data", "portal-visits-state.json");
@@ -13358,17 +13743,40 @@ function visitsStateFilePath(platformRoot5) {
13358
13743
  function visitsDigest(rows) {
13359
13744
  const h = createHash3("sha256");
13360
13745
  for (const r of [...rows].sort((a, b) => a.visitId.localeCompare(b.visitId))) {
13361
- h.update(JSON.stringify([r.visitId, r.ownerId, r.jobId, r.status, r.startDate, r.purpose]));
13746
+ h.update(
13747
+ JSON.stringify([
13748
+ r.visitId,
13749
+ r.ownerId,
13750
+ r.jobId,
13751
+ r.status,
13752
+ r.startDate,
13753
+ r.purpose,
13754
+ // Task 2511's counts are fields of a visit, so they belong in the
13755
+ // visits half. Listed explicitly rather than spread, because the array
13756
+ // literal is what fixes field order here rather than key insertion.
13757
+ r.notes,
13758
+ r.media,
13759
+ r.parts,
13760
+ r.checklistDone,
13761
+ r.checklistTotal,
13762
+ r.signatures
13763
+ ])
13764
+ );
13362
13765
  }
13363
13766
  return h.digest("hex");
13364
13767
  }
13365
- function payloadDigest(rows, roster, meta) {
13768
+ function payloadDigest(rows, roster, meta, candidates) {
13366
13769
  const h = createHash3("sha256");
13367
13770
  h.update(visitsDigest(rows));
13368
13771
  for (const w of [...roster].sort((a, b) => a.workerId.localeCompare(b.workerId))) {
13369
13772
  h.update(JSON.stringify([w.workerId, w.ownerId, w.name, w.workingHours]));
13370
13773
  }
13371
- h.update(JSON.stringify([meta.vocabulary, meta.statuses, meta.zone]));
13774
+ h.update(JSON.stringify([meta.vocabulary, meta.statuses, meta.transitions, meta.zone]));
13775
+ for (const c of [...candidates].sort(
13776
+ (a, b) => a.visitId.localeCompare(b.visitId) || a.workerId.localeCompare(b.workerId)
13777
+ )) {
13778
+ h.update(JSON.stringify([c.visitId, c.workerId, c.name, c.kind, c.reason, c.conflicts]));
13779
+ }
13372
13780
  return h.digest("hex");
13373
13781
  }
13374
13782
  function readVisitsPushState(platformRoot5) {
@@ -13416,9 +13824,11 @@ async function runPortalVisitsPush(root) {
13416
13824
  const meta = cfg.present ? {
13417
13825
  vocabulary: cfg.value.vocabulary,
13418
13826
  statuses: cfg.value.statuses.visit,
13827
+ transitions: cfg.value.statuses.visitTransitions,
13419
13828
  zone: await resolveAccountZone(portal.accountId) ?? ZONE_FALLBACK
13420
13829
  } : EMPTY_META;
13421
- const digest = payloadDigest(rows, roster, meta);
13830
+ const candidates = cfg.present ? await readCandidatesForAccount(portal.accountId, cfg.value) : [];
13831
+ const digest = payloadDigest(rows, roster, meta, candidates);
13422
13832
  const unresolved = roster.filter((w) => w.ownerId === "").length;
13423
13833
  if (state[portal.accountId]?.digest === digest && (rows.length > 0 || roster.length > 0)) {
13424
13834
  recordVisitsPush(root, portal.accountId, {
@@ -13443,10 +13853,11 @@ async function runPortalVisitsPush(root) {
13443
13853
  rows,
13444
13854
  roster,
13445
13855
  meta,
13856
+ candidates,
13446
13857
  log: (l) => console.error(l)
13447
13858
  });
13448
13859
  console.error(
13449
- `${TAG19} op=push account=${portal.accountId} visits=${rows.length} pushed=${r.rows} roster=${r.roster} workers-unresolved=${r.unresolved} ms=${Date.now() - startedAt}`
13860
+ `${TAG19} op=push account=${portal.accountId} visits=${rows.length} pushed=${r.rows} roster=${r.roster} workers-unresolved=${r.unresolved} transitions=${meta.transitions.length} candidates=${r.candidates} ms=${Date.now() - startedAt}`
13450
13861
  );
13451
13862
  if (r.generation !== void 0) {
13452
13863
  recordVisitsPush(root, portal.accountId, {
@@ -13491,12 +13902,15 @@ function parsePendingStatusRows(raw) {
13491
13902
  statusAt: str2(o.statusAt),
13492
13903
  dayHundredths: typeof o.dayHundredths === "number" ? o.dayHundredths : 0,
13493
13904
  workDate: str2(o.workDate),
13494
- workNote: str2(o.workNote)
13905
+ workNote: str2(o.workNote),
13906
+ assignedOwnerId: str2(o.assignedOwnerId),
13907
+ startDate: str2(o.startDate)
13495
13908
  });
13496
13909
  }
13497
13910
  return out;
13498
13911
  }
13499
13912
  var WORK_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
13913
+ var INSTANT_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
13500
13914
  function buildVisitStatusWrite(accountId, row) {
13501
13915
  const base = {
13502
13916
  accountId,
@@ -13504,6 +13918,72 @@ function buildVisitStatusWrite(accountId, row) {
13504
13918
  status: row.status,
13505
13919
  statusAt: row.statusAt
13506
13920
  };
13921
+ if (row.assignedOwnerId !== "" || row.startDate !== "") {
13922
+ if (row.startDate !== "" && !INSTANT_RE.test(row.startDate)) return { kind: "bad-instant" };
13923
+ if (row.dayHundredths > 0) return { kind: "move-with-labour" };
13924
+ return {
13925
+ kind: "move",
13926
+ // EXACTLY ONE ATTENDANCE to replace, and EXACTLY ONE ENGINEER to replace
13927
+ // it with, or nothing runs at all.
13928
+ //
13929
+ // A visit two workers share cannot be re-assigned by replacing "the"
13930
+ // attendance: whichever went, the other worker would silently lose the
13931
+ // job and the arrival times they had already pressed. A per-worker move
13932
+ // is Task 2551. The engineer gate is the mirror: the `portalOwnerId`
13933
+ // binding is a property and is not unique by construction, so an id
13934
+ // naming several must not resolve to whichever came back first. Both turn
13935
+ // into the zero count this function already returns for a visit missing
13936
+ // its job, so the row stays unclaimed and counted.
13937
+ //
13938
+ // NO `CALL` SUBQUERY, deliberately. A returning subquery replaces each
13939
+ // outer row with the rows it yields, so one that yields nothing on a
13940
+ // status-only or re-time-only edit would eliminate the visit before the
13941
+ // SET ran: the re-timing would write nothing, return 0, and the row would
13942
+ // never drain. A unit subquery does not filter, but then an unresolvable
13943
+ // ownerId would leave the SET running and the row CLAIMED with nobody
13944
+ // moved. Both gates therefore sit in the main query's WHERE, where they
13945
+ // fail the whole statement, and the writes ride FOREACH over lists that
13946
+ // are empty when this edit re-assigns nobody.
13947
+ //
13948
+ // The re-assignment REPLACES rather than adds, because a drag onto
13949
+ // another worker's row means "move this", not "add a second person". The
13950
+ // attendance key is `engineerId + '-' + visitId`, the key
13951
+ // `dispatch-allocate` merges on, so allocating that worker to that visit
13952
+ // afterwards is a no-op rather than a second attendance — and the delete
13953
+ // skips an attendance already held by the incoming engineer, so
13954
+ // re-assigning somebody to the visit they are already on keeps the
13955
+ // arrival times they pressed instead of resetting them.
13956
+ cypher: `MATCH (v:Visit {accountId: $accountId, visitId: $visitId})
13957
+ OPTIONAL MATCH (v)-[:HAS_ATTENDANCE]->(a:Attendance)
13958
+ WITH v, collect(a) AS atts
13959
+ OPTIONAL MATCH (e:Engineer {accountId: $accountId})
13960
+ WHERE $assignedOwnerId <> ''
13961
+ AND e.portalOwnerId = $assignedOwnerId
13962
+ AND e.engineerId IS NOT NULL
13963
+ WITH v, atts, collect(e) AS es
13964
+ WHERE $assignedOwnerId = '' OR (size(atts) = 1 AND size(es) = 1)
13965
+ SET v.status = $status, v.statusAt = $statusAt
13966
+ FOREACH (_ IN CASE WHEN $startDate <> '' THEN [1] ELSE [] END |
13967
+ SET v.startDate = $startDate)
13968
+ FOREACH (old IN [x IN atts
13969
+ WHERE NOT x.engineerId IN [eng IN es | eng.engineerId]] |
13970
+ DETACH DELETE old)
13971
+ FOREACH (eng IN es |
13972
+ MERGE (a2:Attendance {
13973
+ accountId: $accountId,
13974
+ attendanceId: eng.engineerId + '-' + $visitId
13975
+ })
13976
+ ON CREATE SET a2.engineerId = eng.engineerId, a2.createdAt = $statusAt
13977
+ MERGE (v)-[:HAS_ATTENDANCE]->(a2)
13978
+ MERGE (a2)-[:BY_ENGINEER]->(eng))
13979
+ RETURN count(v) AS n`,
13980
+ params: {
13981
+ ...base,
13982
+ assignedOwnerId: row.assignedOwnerId,
13983
+ startDate: row.startDate
13984
+ }
13985
+ };
13986
+ }
13507
13987
  if (row.dayHundredths <= 0) {
13508
13988
  return {
13509
13989
  kind: "status",
@@ -13562,6 +14042,14 @@ async function pullVisitStatusForAccount(opts) {
13562
14042
  await claim(row.id);
13563
14043
  merged += 1;
13564
14044
  log2(`${TAG20} op=merged account=${accountId} id=${row.id} labour=${row.dayHundredths > 0}`);
14045
+ const fields = ["status"];
14046
+ if (row.workDate !== "") fields.push("workDate");
14047
+ if (row.workNote !== "") fields.push("workNote");
14048
+ if (row.assignedOwnerId !== "") fields.push("assignedOwnerId");
14049
+ if (row.startDate !== "") fields.push("startDate");
14050
+ for (const field2 of fields) {
14051
+ log2(`${TAG20} op=applied visitId=${row.visitId} field=${field2}`);
14052
+ }
13565
14053
  } catch (err) {
13566
14054
  failed2 += 1;
13567
14055
  const detail = err.message.split("\n")[0].replaceAll('"', "'").slice(0, 160);
@@ -13589,7 +14077,7 @@ async function runPortalVisitsPull(root) {
13589
14077
  const pending = parsePendingStatusRows(
13590
14078
  await cf.d1Exec(
13591
14079
  portal.portalDbName,
13592
- "SELECT id, visitId, ownerId, status, statusAt, dayHundredths, workDate, workNote FROM visit_status WHERE accountId = ? AND claimed = 0",
14080
+ "SELECT id, visitId, ownerId, status, statusAt, dayHundredths, workDate, workNote, assignedOwnerId, startDate FROM visit_status WHERE accountId = ? AND claimed = 0",
13593
14081
  [portal.accountId]
13594
14082
  )
13595
14083
  );
@@ -13598,7 +14086,9 @@ async function runPortalVisitsPull(root) {
13598
14086
  pending,
13599
14087
  applyToGraph: async (accountId, row) => {
13600
14088
  const write = buildVisitStatusWrite(accountId, row);
13601
- if (write.kind === "undated") return false;
14089
+ if (write.kind === "undated" || write.kind === "bad-instant" || write.kind === "move-with-labour") {
14090
+ return false;
14091
+ }
13602
14092
  const session = getSession();
13603
14093
  try {
13604
14094
  const res = await session.run(write.cypher, write.params);
@@ -13642,7 +14132,7 @@ function buildVisitsAuditLine(input) {
13642
14132
  input.d1Error !== null ? `d1:${auditErrorText(input.d1Error)}` : null,
13643
14133
  input.neo4jError !== null ? `neo4j:${auditErrorText(input.neo4jError)}` : null
13644
14134
  ].filter(Boolean).join(" | ");
13645
- return `${TAG21} account=${input.accountId} pushedRows=${na(input.pushedRows)} graphVisits=${na(input.graphVisits)} unboundVisits=${na(input.unboundVisits)} pendingStatusRows=${na(input.pendingStatusRows)} oldestPendingHrs=${na(input.oldestPendingHrs, 1)} unresolvedVisitIds=${na(input.unresolvedVisitIds)} labourClaims=${na(input.labourClaims)} labourCosts=${na(input.labourCosts)} undatedClaims=${na(input.undatedClaims)} lastPushAgeSec=${na(input.lastPushAgeSec)}` + (degradation ? ` degraded=${degradation} err="${reasons}"` : "");
14135
+ return `${TAG21} account=${input.accountId} pushedRows=${na(input.pushedRows)} graphVisits=${na(input.graphVisits)} unboundVisits=${na(input.unboundVisits)} pendingStatusRows=${na(input.pendingStatusRows)} oldestPendingHrs=${na(input.oldestPendingHrs, 1)} unresolvedVisitIds=${na(input.unresolvedVisitIds)} labourClaims=${na(input.labourClaims)} labourCosts=${na(input.labourCosts)} undatedClaims=${na(input.undatedClaims)} stalePendingRows=${na(input.stalePendingRows)} lastPushAgeSec=${na(input.lastPushAgeSec)}` + (degradation ? ` degraded=${degradation} err="${reasons}"` : "");
13646
14136
  }
13647
14137
  function readLastVisitsPushAgeSec(platformRoot5, accountId, nowMs) {
13648
14138
  try {
@@ -13655,7 +14145,7 @@ function readLastVisitsPushAgeSec(platformRoot5, accountId, nowMs) {
13655
14145
  return null;
13656
14146
  }
13657
14147
  }
13658
- var num2 = (raw, field2) => {
14148
+ var num3 = (raw, field2) => {
13659
14149
  const envelope = Array.isArray(raw) ? raw[0] : null;
13660
14150
  const results = envelope?.results;
13661
14151
  if (!Array.isArray(results)) {
@@ -13681,12 +14171,13 @@ async function runPortalVisitsAudit(root) {
13681
14171
  let unresolvedVisitIds = null;
13682
14172
  let labourClaims = null;
13683
14173
  let undatedClaims = null;
14174
+ let stalePendingRows = null;
13684
14175
  let d1Error = null;
13685
14176
  try {
13686
14177
  if (!SAFE_ACCOUNT_ID3.test(portal.accountId)) {
13687
14178
  throw new Error(`refusing to query for unsafe accountId "${portal.accountId}"`);
13688
14179
  }
13689
- pushedRows = num2(
14180
+ pushedRows = num3(
13690
14181
  await cf.d1Exec(
13691
14182
  portal.portalDbName,
13692
14183
  "SELECT COUNT(*) AS n FROM visits v JOIN visits_state s ON s.accountId = v.accountId AND s.currentGeneration = v.generation WHERE v.accountId = ?",
@@ -13694,7 +14185,7 @@ async function runPortalVisitsAudit(root) {
13694
14185
  ),
13695
14186
  "n"
13696
14187
  );
13697
- unboundVisits = num2(
14188
+ unboundVisits = num3(
13698
14189
  await cf.d1Exec(
13699
14190
  portal.portalDbName,
13700
14191
  "SELECT COUNT(*) AS n FROM visits v JOIN visits_state s ON s.accountId = v.accountId AND s.currentGeneration = v.generation WHERE v.accountId = ? AND v.ownerId = ''",
@@ -13707,12 +14198,12 @@ async function runPortalVisitsAudit(root) {
13707
14198
  "SELECT COUNT(*) AS n, MIN(statusAt) AS oldest FROM visit_status WHERE accountId = ? AND claimed = 0",
13708
14199
  [portal.accountId]
13709
14200
  );
13710
- pendingStatusRows = num2(pending, "n");
14201
+ pendingStatusRows = num3(pending, "n");
13711
14202
  const envelope = Array.isArray(pending) ? pending[0] : null;
13712
14203
  const oldest = envelope?.results?.[0]?.oldest;
13713
14204
  const oldestMs = typeof oldest === "string" ? Date.parse(oldest) : NaN;
13714
14205
  oldestPendingHrs = Number.isNaN(oldestMs) ? pendingStatusRows === 0 ? 0 : null : Math.max(0, (nowMs - oldestMs) / 36e5);
13715
- unresolvedVisitIds = num2(
14206
+ unresolvedVisitIds = num3(
13716
14207
  await cf.d1Exec(
13717
14208
  portal.portalDbName,
13718
14209
  "SELECT COUNT(*) AS n FROM visit_status vs WHERE vs.accountId = ? AND vs.claimed = 0 AND NOT EXISTS (SELECT 1 FROM visits v JOIN visits_state s ON s.accountId = v.accountId AND s.currentGeneration = v.generation WHERE v.accountId = vs.accountId AND v.visitId = vs.visitId)",
@@ -13720,7 +14211,7 @@ async function runPortalVisitsAudit(root) {
13720
14211
  ),
13721
14212
  "n"
13722
14213
  );
13723
- labourClaims = num2(
14214
+ labourClaims = num3(
13724
14215
  await cf.d1Exec(
13725
14216
  portal.portalDbName,
13726
14217
  "SELECT COUNT(*) AS n FROM visit_status WHERE accountId = ? AND claimed = 0 AND dayHundredths > 0",
@@ -13728,7 +14219,7 @@ async function runPortalVisitsAudit(root) {
13728
14219
  ),
13729
14220
  "n"
13730
14221
  );
13731
- undatedClaims = num2(
14222
+ undatedClaims = num3(
13732
14223
  await cf.d1Exec(
13733
14224
  portal.portalDbName,
13734
14225
  "SELECT COUNT(*) AS n FROM visit_status WHERE accountId = ? AND claimed = 0 AND dayHundredths > 0 AND workDate NOT GLOB '[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]'",
@@ -13736,6 +14227,15 @@ async function runPortalVisitsAudit(root) {
13736
14227
  ),
13737
14228
  "n"
13738
14229
  );
14230
+ const staleCutoff = new Date(nowMs - 2 * PORTAL_VISITS_PULL_INTERVAL_MS).toISOString();
14231
+ stalePendingRows = num3(
14232
+ await cf.d1Exec(
14233
+ portal.portalDbName,
14234
+ "SELECT COUNT(*) AS n FROM visit_status WHERE accountId = ? AND claimed = 0 AND statusAt < ?",
14235
+ [portal.accountId, staleCutoff]
14236
+ ),
14237
+ "n"
14238
+ );
13739
14239
  } catch (err) {
13740
14240
  d1Error = err.message;
13741
14241
  }
@@ -13774,6 +14274,7 @@ async function runPortalVisitsAudit(root) {
13774
14274
  labourClaims,
13775
14275
  labourCosts,
13776
14276
  undatedClaims,
14277
+ stalePendingRows,
13777
14278
  lastPushAgeSec: readLastVisitsPushAgeSec(root, portal.accountId, nowMs),
13778
14279
  d1Error,
13779
14280
  neo4jError
@@ -15213,6 +15714,7 @@ function pickActivity(entries2, grown, nowMs, concurrencyWindowMs) {
15213
15714
  }
15214
15715
 
15215
15716
  // server/routes/session-reader.ts
15717
+ init_session_store();
15216
15718
  init_dist();
15217
15719
  var app7 = new Hono();
15218
15720
  function probeDataRef(path) {
@@ -15388,6 +15890,7 @@ data: ${JSON.stringify(turn)}
15388
15890
 
15389
15891
  `));
15390
15892
  const connId = `${sessionId.slice(0, 8)}-${process.hrtime.bigint().toString(36).slice(-6)}`;
15893
+ const censusAccountId = getAccountIdForSession(c.get("cacheKey") ?? "") ?? "unresolved";
15391
15894
  const startedAt = Date.now();
15392
15895
  const lastEventId = c.req.header("Last-Event-ID");
15393
15896
  let teardown = () => {
@@ -15423,7 +15926,8 @@ data: ${JSON.stringify(obj)}
15423
15926
  log: (line) => console.log(line)
15424
15927
  });
15425
15928
  const queuedPendingSuppress = /* @__PURE__ */ new Map();
15426
- const turnCtx = {};
15929
+ const envelopeCensus = emptyEnvelopeCensus();
15930
+ const turnCtx = { envelopeCensus };
15427
15931
  const drain = () => {
15428
15932
  try {
15429
15933
  const { buf, end } = readFrom(jsonlPath, offset);
@@ -15440,6 +15944,7 @@ data: ${JSON.stringify(obj)}
15440
15944
  n++;
15441
15945
  }
15442
15946
  if (n > 0) {
15947
+ foldFrameCensus(censusAccountId, sessionId, envelopeCensus);
15443
15948
  actx.parentGrewThisTick = true;
15444
15949
  console.log(`[wa-stream] op=drain conn=${connId} turns=${n}`);
15445
15950
  }
@@ -15486,6 +15991,7 @@ data: ${JSON.stringify({ from: alignedFrom, fileEnd: fileEnd0 })}
15486
15991
  }
15487
15992
  n++;
15488
15993
  }
15994
+ foldFrameCensus(censusAccountId, sessionId, envelopeCensus);
15489
15995
  console.log(
15490
15996
  `[wa-stream] op=backlog conn=${connId} turns=${n} bytes=${end - alignedFrom} from=${alignedFrom}`
15491
15997
  );
@@ -15669,7 +16175,7 @@ import { basename as basename8, dirname as dirname9, join as join26 } from "path
15669
16175
  // app/lib/reader/operator-conversations.ts
15670
16176
  function operatorConvName(row) {
15671
16177
  if (row.isGroup === true) return row.chatName ?? row.title;
15672
- const name = row.operatorName ?? row.chatName ?? row.whatsappName;
16178
+ const name = row.operatorName ?? row.chatName ?? row.whatsappName ?? row.telegramName;
15673
16179
  const senderId = row.senderId != null && row.channel === "whatsapp" ? formatSenderPhone(row.senderId) : row.senderId;
15674
16180
  if (name) return senderId ? `${name} ${senderId}` : name;
15675
16181
  return senderId ?? row.title;
@@ -15679,6 +16185,7 @@ function operatorConvNameSource(row) {
15679
16185
  if (row.operatorName) return "operator";
15680
16186
  if (row.chatName) return "chatmeta";
15681
16187
  if (row.whatsappName) return "inbound-pushname";
16188
+ if (row.telegramName) return "telegram-sender-name";
15682
16189
  if (row.senderId != null) return "counterparty-number";
15683
16190
  return "jid";
15684
16191
  }
@@ -15898,6 +16405,14 @@ app8.get("/conversations", requireAdminSession, async (c) => {
15898
16405
  );
15899
16406
  const adminScope = allStoreRows.filter((r) => r.scope === "admin").length;
15900
16407
  const publicScope = allStoreRows.length - adminScope;
16408
+ const telegramStoreRows = account ? buildTelegramStoreConversationRows(account.accountId, readConversationSummaries2(account.accountId)) : [];
16409
+ const dedupedSessionRows = dropTelegramSessionDuplicates(sessionConversations, telegramStoreRows);
16410
+ const telegramRows = [
16411
+ ...dedupedSessionRows.filter((r) => r.channel === "telegram"),
16412
+ ...telegramStoreRows
16413
+ ].sort((a, b) => (b.lastMessageAt ?? "").localeCompare(a.lastMessageAt ?? ""));
16414
+ const otherSessionRows = dedupedSessionRows.filter((r) => r.channel !== "telegram");
16415
+ const conversations = [...otherSessionRows, ...allStoreRows, ...telegramRows];
15901
16416
  const census = buildLabelCensus(allStoreRows, selfPushNames(summaries));
15902
16417
  for (const f of fallbackLabelSources(allStoreRows)) {
15903
16418
  console.log(`[wa-label] op=row-label jid=${f.jid} source=${f.source} route=conversations`);
@@ -15905,11 +16420,11 @@ app8.get("/conversations", requireAdminSession, async (c) => {
15905
16420
  console.log(
15906
16421
  `[wa-label-census] route=conversations chats=${census.chats} withMeta=${census.withMeta} groups=${census.groups} groupsWithSubject=${census.groupsWithSubject} labelledByMember=${census.labelledByMember} dms=${census.dms} namedDms=${census.namedDms} labelledBySelfPushName=${census.labelledBySelfPushName}`
15907
16422
  );
15908
- const sessionRowCount = sessionConversations.length;
16423
+ const sessionRowCount = dedupedSessionRows.length;
15909
16424
  console.log(
15910
- `[wa-reader] op=list pinnedAccount=${scopeAccountId?.slice(0, 8) ?? "null"} resolvedAccount=${account?.accountId.slice(0, 8) ?? "null"} multiAccount=${multiAccount} conversations=${sessionRowCount + allStoreRows.length} sessionRows=${sessionRowCount} storeRows=${allStoreRows.length} adminScope=${adminScope} publicScope=${publicScope} sessionRowsExcludedUntagged=${sessionRowsExcludedUntagged} houseResolved=${houseId ? houseId.slice(0, 8) : "null"} managerPhonesBound=${managerPhonesBound} managedStoreRows=${managedRows.length}`
16425
+ `[wa-reader] op=list pinnedAccount=${scopeAccountId?.slice(0, 8) ?? "null"} resolvedAccount=${account?.accountId.slice(0, 8) ?? "null"} multiAccount=${multiAccount} conversations=${conversations.length} sessionRows=${sessionRowCount} storeRows=${allStoreRows.length} storeRowsByChannel=whatsapp:${allStoreRows.length},telegram:${telegramStoreRows.length} adminScope=${adminScope} publicScope=${publicScope} sessionRowsExcludedUntagged=${sessionRowsExcludedUntagged} houseResolved=${houseId ? houseId.slice(0, 8) : "null"} managerPhonesBound=${managerPhonesBound} managedStoreRows=${managedRows.length}`
15911
16426
  );
15912
- return c.json({ conversations: [...sessionConversations, ...allStoreRows] });
16427
+ return c.json({ conversations });
15913
16428
  });
15914
16429
  function tagAndMergeInstallConversations(perAccount) {
15915
16430
  const all = [];
@@ -15920,7 +16435,7 @@ function tagAndMergeInstallConversations(perAccount) {
15920
16435
  }
15921
16436
  function dropHouseRowsOwnedBySubAccount(rows, subManagerPhones) {
15922
16437
  return rows.filter((r) => {
15923
- if (r.isHouse !== true || r.source !== "store" || r.senderId == null) return true;
16438
+ if (r.channel !== "whatsapp" || r.isHouse !== true || r.source !== "store" || r.senderId == null) return true;
15924
16439
  let owner = null;
15925
16440
  for (const [subId, phones] of subManagerPhones) {
15926
16441
  if (counterpartyBoundToSubAccount(phones, r.senderId)) {
@@ -16060,7 +16575,14 @@ app8.get("/conversations-all", requireAdminSession, async (c) => {
16060
16575
  readConversationSummaries(account.accountId),
16061
16576
  readChatMetadata(account.accountId)
16062
16577
  );
16063
- perAccount.push({ isHouse, rows: [...named, ...storeRows] });
16578
+ const telegramStoreRows = buildTelegramStoreConversationRows(
16579
+ account.accountId,
16580
+ readConversationSummaries2(account.accountId)
16581
+ );
16582
+ perAccount.push({
16583
+ isHouse,
16584
+ rows: [...dropTelegramSessionDuplicates(named, telegramStoreRows), ...storeRows, ...telegramStoreRows]
16585
+ });
16064
16586
  }
16065
16587
  const houseAccount = validAccounts.find((a) => a.accountId === houseId) ?? null;
16066
16588
  const subManagerPhones = /* @__PURE__ */ new Map();
@@ -16078,16 +16600,19 @@ app8.get("/conversations-all", requireAdminSession, async (c) => {
16078
16600
  for (const account of validAccounts) {
16079
16601
  for (const [jid, s] of readConversationSummaries(account.accountId)) allSummaries.set(`${account.accountId}:${jid}`, s);
16080
16602
  }
16081
- const census = buildLabelCensus(conversations, selfPushNames(allSummaries));
16082
- for (const f of fallbackLabelSources(conversations)) {
16603
+ const whatsappRows = conversations.filter((r) => r.channel === "whatsapp");
16604
+ const census = buildLabelCensus(whatsappRows, selfPushNames(allSummaries));
16605
+ for (const f of fallbackLabelSources(whatsappRows)) {
16083
16606
  console.log(`[wa-label] op=row-label jid=${f.jid} source=${f.source} route=conversations-all`);
16084
16607
  }
16085
16608
  console.log(
16086
16609
  `[wa-label-census] route=conversations-all chats=${census.chats} withMeta=${census.withMeta} groups=${census.groups} groupsWithSubject=${census.groupsWithSubject} labelledByMember=${census.labelledByMember} dms=${census.dms} namedDms=${census.namedDms} labelledBySelfPushName=${census.labelledBySelfPushName}`
16087
16610
  );
16088
16611
  const houseRows = conversations.filter((r) => r.isHouse).length;
16612
+ const storeByChannel = conversations.filter((r) => r.source === "store");
16613
+ const waStoreRows = storeByChannel.filter((r) => r.channel === "whatsapp").length;
16089
16614
  console.log(
16090
- `[wa-reader] op=conversations-all accounts=${validAccounts.length} rows=${conversations.length} house=${houseRows} sub=${conversations.length - houseRows} deduped=${merged.length - conversations.length}`
16615
+ `[wa-reader] op=conversations-all accounts=${validAccounts.length} rows=${conversations.length} house=${houseRows} sub=${conversations.length - houseRows} deduped=${merged.length - conversations.length} storeRowsByChannel=whatsapp:${waStoreRows},telegram:${storeByChannel.length - waStoreRows}`
16091
16616
  );
16092
16617
  return { conversations };
16093
16618
  });
@@ -30168,7 +30693,7 @@ app55.get("/", requireAdminSession, async (c) => {
30168
30693
  }
30169
30694
  });
30170
30695
  function mapRun(props) {
30171
- const num4 = (v) => {
30696
+ const num5 = (v) => {
30172
30697
  if (v === null || v === void 0) return null;
30173
30698
  if (typeof v === "number") return v;
30174
30699
  if (typeof v === "object" && typeof v.toNumber === "function") {
@@ -30188,11 +30713,11 @@ function mapRun(props) {
30188
30713
  startedAt: props.startedAt,
30189
30714
  endedAt: props.endedAt ?? null,
30190
30715
  finishedAt: props.finishedAt ?? null,
30191
- ranMs: num4(props.ranMs),
30192
- httpStatus: num4(props.httpStatus),
30716
+ ranMs: num5(props.ranMs),
30717
+ httpStatus: num5(props.httpStatus),
30193
30718
  dispatchResult: props.dispatchResult ?? null,
30194
30719
  sessionKey: props.sessionKey ?? null,
30195
- transcriptOffset: num4(props.transcriptOffset),
30720
+ transcriptOffset: num5(props.transcriptOffset),
30196
30721
  // Strict identity: an absent or null `bounded` means the boundary was never
30197
30722
  // established, which is the false case.
30198
30723
  bounded: props.bounded === true,
@@ -33931,7 +34456,7 @@ function formatConfigAuditLine(a, accountId) {
33931
34456
  function formatRecordCensus(f, accountId) {
33932
34457
  return `${RECORD_CENSUS_TAG}${accountId ? ` account=${accountId}` : ""} closedVisits=${f.closedVisits} withNote=${f.withNote} withMedia=${f.withMedia} medianRecordLagS=${f.medianRecordLagS} p90RecordLagS=${f.p90RecordLagS}`;
33933
34458
  }
33934
- function num3(v) {
34459
+ function num4(v) {
33935
34460
  if (typeof v === "number") return v;
33936
34461
  if (v && typeof v === "object" && "low" in v) {
33937
34462
  return Number(v.low);
@@ -34027,15 +34552,15 @@ async function readCensusCounts(session, accountId, terminal, now) {
34027
34552
  { accountId, canonical: `^${CANONICAL_INSTANT_PATTERN}$` }
34028
34553
  );
34029
34554
  return {
34030
- jobs: num3(g?.get("jobs")),
34031
- unallocated: num3(g?.get("unallocated")),
34555
+ jobs: num4(g?.get("jobs")),
34556
+ unallocated: num4(g?.get("unallocated")),
34032
34557
  oldestUnallocatedAgeH,
34033
- overdue: num3(g?.get("overdue")),
34034
- visitsOpen: num3(vg?.get("visitsOpen")),
34035
- visitsOpenOver24h: num3(vg?.get("visitsOpenOver24h")),
34036
- intakeUnreviewed: num3(dg?.get("intakeUnreviewed")),
34558
+ overdue: num4(g?.get("overdue")),
34559
+ visitsOpen: num4(vg?.get("visitsOpen")),
34560
+ visitsOpenOver24h: num4(vg?.get("visitsOpenOver24h")),
34561
+ intakeUnreviewed: num4(dg?.get("intakeUnreviewed")),
34037
34562
  oldestIntakeAgeH,
34038
- nonCanonicalInstants: num3(nc.records[0]?.get("nonCanonicalInstants"))
34563
+ nonCanonicalInstants: num4(nc.records[0]?.get("nonCanonicalInstants"))
34039
34564
  };
34040
34565
  }
34041
34566
  async function readRecordCensus(session, accountId, terminal, now) {
@@ -34053,8 +34578,8 @@ async function readRecordCensus(session, accountId, terminal, now) {
34053
34578
  let withMedia = 0;
34054
34579
  const lags = [];
34055
34580
  for (const g of r.records) {
34056
- if (num3(g.get("notes")) > 0) withNote++;
34057
- if (num3(g.get("media")) > 0) withMedia++;
34581
+ if (num4(g.get("notes")) > 0) withNote++;
34582
+ if (num4(g.get("media")) > 0) withMedia++;
34058
34583
  const closedAt = g.get("closedAt");
34059
34584
  const dueToLeaveAt = g.get("dueToLeaveAt");
34060
34585
  if (typeof closedAt === "string" && typeof dueToLeaveAt === "string") {
@@ -35489,15 +36014,15 @@ function createWaChannelRoutes(deps) {
35489
36014
  }
35490
36015
  const interactive = body?.interactive === true;
35491
36016
  const finalText = typeof body?.finalText === "string" ? body.finalText : "";
35492
- const num4 = (v) => typeof v === "number" && Number.isFinite(v) ? v : -1;
35493
- const finalBytes = num4(body?.finalBytes);
35494
- const queueDepth = num4(body?.queueDepth);
35495
- const pendingTurns = num4(body?.pendingTurns);
35496
- const consumed2 = num4(body?.consumed);
36017
+ const num5 = (v) => typeof v === "number" && Number.isFinite(v) ? v : -1;
36018
+ const finalBytes = num5(body?.finalBytes);
36019
+ const queueDepth = num5(body?.queueDepth);
36020
+ const pendingTurns = num5(body?.pendingTurns);
36021
+ const consumed2 = num5(body?.consumed);
35497
36022
  const source = typeof body?.source === "string" ? body.source : null;
35498
36023
  const sid = sessionId.slice(0, 8);
35499
36024
  console.error(
35500
- `[whatsapp-native] op=turn-context sessionId=${sid} cacheReadTokens=${num4(body?.cacheReadTokens)} inputTokens=${num4(body?.inputTokens)} outputTokens=${num4(body?.outputTokens)} thinkingBlocks=${num4(body?.thinkingBlocks)} thinkingChars=${num4(body?.thinkingChars)}`
36025
+ `[whatsapp-native] op=turn-context sessionId=${sid} cacheReadTokens=${num5(body?.cacheReadTokens)} inputTokens=${num5(body?.inputTokens)} outputTokens=${num5(body?.outputTokens)} thinkingBlocks=${num5(body?.thinkingBlocks)} thinkingChars=${num5(body?.thinkingChars)}`
35501
36026
  );
35502
36027
  const role = body?.role;
35503
36028
  if (role !== "admin" && role !== "public") {
@@ -36459,12 +36984,12 @@ function createWebchatChannelRoutes(deps) {
36459
36984
  const finalBytes = typeof body?.finalBytes === "number" && Number.isFinite(body.finalBytes) ? body.finalBytes : 0;
36460
36985
  const k = key2.slice(0, 8);
36461
36986
  const sid = sessionId.slice(0, 8);
36462
- const num4 = (v) => typeof v === "number" && Number.isFinite(v) ? v : -1;
36987
+ const num5 = (v) => typeof v === "number" && Number.isFinite(v) ? v : -1;
36463
36988
  console.error(
36464
- `[webchat-native] op=turn-context sessionId=${sid} cacheReadTokens=${num4(body?.cacheReadTokens)} inputTokens=${num4(body?.inputTokens)} outputTokens=${num4(body?.outputTokens)} thinkingBlocks=${num4(body?.thinkingBlocks)} thinkingChars=${num4(body?.thinkingChars)}`
36989
+ `[webchat-native] op=turn-context sessionId=${sid} cacheReadTokens=${num5(body?.cacheReadTokens)} inputTokens=${num5(body?.inputTokens)} outputTokens=${num5(body?.outputTokens)} thinkingBlocks=${num5(body?.thinkingBlocks)} thinkingChars=${num5(body?.thinkingChars)}`
36465
36990
  );
36466
- const segmentStart = num4(body?.segmentStart);
36467
- const segmentEnd = num4(body?.segmentEnd);
36991
+ const segmentStart = num5(body?.segmentStart);
36992
+ const segmentEnd = num5(body?.segmentEnd);
36468
36993
  const resolved = deps.onTurnEnd?.(key2, role, { sessionId, segmentStart, segmentEnd }) ?? "none";
36469
36994
  const resolvedField = role === "public" ? ` resolved=${resolved}` : "";
36470
36995
  console.error(`[webchat-native] op=turn-end channel=webchat key=${k} role=${role} sessionId=${sid} trailing=${finalBytes}${resolvedField}`);
@@ -37793,21 +38318,22 @@ function startNativeFileFollower(input) {
37793
38318
  }
37794
38319
 
37795
38320
  // app/lib/channel-delivery/follower-registry.ts
37796
- function createFollowerRegistry(start) {
38321
+ function createFollowerRegistry(start, keyOf2 = (p) => p.senderId) {
37797
38322
  const controllers = /* @__PURE__ */ new Map();
37798
38323
  const params = /* @__PURE__ */ new Map();
37799
38324
  return {
37800
38325
  attach(p) {
37801
- controllers.get(p.senderId)?.abort();
37802
- params.set(p.senderId, p);
38326
+ const doorId = keyOf2(p);
38327
+ controllers.get(doorId)?.abort();
38328
+ params.set(doorId, p);
37803
38329
  let ac;
37804
38330
  ac = start(p, () => {
37805
- if (controllers.get(p.senderId) === ac) controllers.delete(p.senderId);
38331
+ if (controllers.get(doorId) === ac) controllers.delete(doorId);
37806
38332
  });
37807
- controllers.set(p.senderId, ac);
38333
+ controllers.set(doorId, ac);
37808
38334
  },
37809
38335
  live: () => [...controllers.keys()],
37810
- paramsFor: (senderId) => params.get(senderId) ?? null
38336
+ paramsFor: (doorId) => params.get(doorId) ?? null
37811
38337
  };
37812
38338
  }
37813
38339
 
@@ -37820,39 +38346,49 @@ var id82 = (s) => s.slice(0, 8);
37820
38346
  function followerCensusTick(deps, state = makeCensusState()) {
37821
38347
  const bound = deps.bound();
37822
38348
  const live = new Set(deps.live());
37823
- for (const senderId of live) state.attempts.delete(senderId);
37824
- const missing = bound.filter((b) => !live.has(b.senderId));
38349
+ const label = deps.label ?? id82;
38350
+ for (const doorId of live) state.attempts.delete(doorId);
38351
+ const missing = bound.filter((doorId) => !live.has(doorId));
37825
38352
  console.error(
37826
- `[channel-follower] op=follower-census bound=${bound.length} attached=${live.size} missing=${missing.length} missingSenders=${missing.map((m) => id82(m.senderId)).join(",") || "none"}`
38353
+ `[channel-follower] op=follower-census channel=${deps.channel} bound=${bound.length} attached=${live.size} missing=${missing.length} missingSenders=${missing.map(label).join(",") || "none"}`
37827
38354
  );
37828
- for (const m of missing) {
37829
- const used = state.attempts.get(m.senderId) ?? 0;
38355
+ for (const doorId of missing) {
38356
+ const used = state.attempts.get(doorId) ?? 0;
37830
38357
  if (used >= CENSUS_MAX_REATTEMPTS) {
37831
38358
  console.error(
37832
- `[channel-follower] op=follower-reattach sessionId=${id82(deps.paramsFor(m.senderId)?.sessionId ?? "none")} senderId=${id82(m.senderId)} outcome=skipped reason=reattach-exhausted attempts=${used}`
38359
+ `[channel-follower] op=follower-reattach channel=${deps.channel} sessionId=${id82(deps.paramsFor(doorId)?.sessionId ?? "none")} senderId=${label(doorId)} outcome=skipped reason=reattach-exhausted attempts=${used}`
37833
38360
  );
37834
38361
  continue;
37835
38362
  }
37836
- const p = deps.paramsFor(m.senderId);
38363
+ const p = deps.paramsFor(doorId);
37837
38364
  if (!p) {
37838
38365
  console.error(
37839
- `[channel-follower] op=follower-reattach sessionId=none senderId=${id82(m.senderId)} outcome=failed reason=no-attach-params`
38366
+ `[channel-follower] op=follower-reattach channel=${deps.channel} sessionId=none senderId=${label(doorId)} outcome=failed reason=no-attach-params`
37840
38367
  );
37841
38368
  continue;
37842
38369
  }
37843
38370
  try {
37844
- state.attempts.set(m.senderId, used + 1);
38371
+ state.attempts.set(doorId, used + 1);
37845
38372
  deps.attach(p);
37846
38373
  console.error(
37847
- `[channel-follower] op=follower-reattach sessionId=${id82(p.sessionId)} senderId=${id82(p.senderId)} outcome=attached reason=none`
38374
+ `[channel-follower] op=follower-reattach channel=${deps.channel} sessionId=${id82(p.sessionId)} senderId=${label(doorId)} outcome=attached reason=none`
37848
38375
  );
37849
38376
  } catch (err) {
37850
38377
  console.error(
37851
- `[channel-follower] op=follower-reattach sessionId=${id82(p.sessionId)} senderId=${id82(p.senderId)} outcome=failed reason=${err instanceof Error ? err.message : String(err)}`
38378
+ `[channel-follower] op=follower-reattach channel=${deps.channel} sessionId=${id82(p.sessionId)} senderId=${label(doorId)} outcome=failed reason=${err instanceof Error ? err.message : String(err)}`
37852
38379
  );
37853
38380
  }
37854
38381
  }
37855
- return `${bound.length} door(s), ${missing.length} without a reader`;
38382
+ return `${deps.channel}: ${bound.length} door(s), ${missing.length} without a reader`;
38383
+ }
38384
+
38385
+ // app/lib/channel-delivery/whatsapp-door.ts
38386
+ function whatsappDoorId(p) {
38387
+ return hubKey(p.effectiveAccountId, p.senderId);
38388
+ }
38389
+ function whatsappDoorLabel(doorId) {
38390
+ const [decoded] = decodeHubKeys([doorId]);
38391
+ return decoded ? `${id82(decoded.accountId)}:${id82(decoded.senderId)}` : id82(doorId);
37856
38392
  }
37857
38393
 
37858
38394
  // app/lib/telegram/gateway/inbound-hub.ts
@@ -38008,7 +38544,8 @@ function createTelegramChannelRoutes(deps) {
38008
38544
  if (typeof key2 !== "string" || typeof messageId !== "string") {
38009
38545
  return c.json({ error: "key and messageId required" }, 400);
38010
38546
  }
38011
- deps.onReceived?.(key2, messageId);
38547
+ const provenance = typeof body?.provenance === "boolean" ? body.provenance : void 0;
38548
+ deps.onReceived?.(key2, messageId, provenance);
38012
38549
  return c.json({ ok: true });
38013
38550
  });
38014
38551
  app81.post("/tg-channel/turn-end", async (c) => {
@@ -38022,9 +38559,9 @@ function createTelegramChannelRoutes(deps) {
38022
38559
  const interactive = body?.interactive === true;
38023
38560
  const finalText = typeof body?.finalText === "string" ? body.finalText : "";
38024
38561
  const sid = sessionId.slice(0, 8);
38025
- const num4 = (v) => typeof v === "number" && Number.isFinite(v) ? v : -1;
38562
+ const num5 = (v) => typeof v === "number" && Number.isFinite(v) ? v : -1;
38026
38563
  console.error(
38027
- `[telegram-native] op=turn-context sessionId=${sid} cacheReadTokens=${num4(body?.cacheReadTokens)} inputTokens=${num4(body?.inputTokens)} outputTokens=${num4(body?.outputTokens)} thinkingBlocks=${num4(body?.thinkingBlocks)} thinkingChars=${num4(body?.thinkingChars)}`
38564
+ `[telegram-native] op=turn-context sessionId=${sid} cacheReadTokens=${num5(body?.cacheReadTokens)} inputTokens=${num5(body?.inputTokens)} outputTokens=${num5(body?.outputTokens)} thinkingBlocks=${num5(body?.thinkingBlocks)} thinkingChars=${num5(body?.thinkingChars)}`
38028
38565
  );
38029
38566
  const role = body?.role;
38030
38567
  if (role !== "admin" && role !== "public") {
@@ -38142,6 +38679,16 @@ var TelegramGateway = class {
38142
38679
  }
38143
38680
  hub = new InboundHub3();
38144
38681
  replies = /* @__PURE__ */ new Map();
38682
+ /** Task 2559 — the keys whose last inbound was `role:'admin'`. The follower
38683
+ * census's bound set is the doors that SHOULD have a reader, and a public
38684
+ * Telegram session is given none by design: the public branch of
38685
+ * `buildTelegramEnsureSession` returns before the attach block, because the
38686
+ * file follower is an admin surface. Including a public key would report it
38687
+ * missing on every tick for ever and stop `missing=` ever reaching zero,
38688
+ * which is the one signal the census exists to give. Removed on a public
38689
+ * inbound, because the role is read from account config on every inbound and
38690
+ * a bot can be flipped between the two. */
38691
+ adminKeys = /* @__PURE__ */ new Set();
38145
38692
  spawning = /* @__PURE__ */ new Set();
38146
38693
  /** Task 2428 — keys whose channel server signalled ready while their spawn was
38147
38694
  * still in flight. The harness discards a channel notification that arrives
@@ -38169,7 +38716,7 @@ var TelegramGateway = class {
38169
38716
  }
38170
38717
  this.drain(key2);
38171
38718
  },
38172
- onReceived: (key2, messageId) => {
38719
+ onReceived: (key2, messageId, provenance) => {
38173
38720
  const acked = this.hub.ack(key2, messageId);
38174
38721
  if (acked !== null && (acked.source ?? "user") === "user") {
38175
38722
  noteReceived(key2, parseTelegramChannelKey(key2)?.botId ?? "unknown", messageId, Date.now());
@@ -38177,6 +38724,10 @@ var TelegramGateway = class {
38177
38724
  console.error(
38178
38725
  `[telegram-native] op=channel-received key=${key2} messageId=${messageId}${acked !== null ? "" : " note=unknown"}`
38179
38726
  );
38727
+ const prov = provenance === void 0 ? "unknown" : provenance ? "yes" : "no";
38728
+ console.error(
38729
+ `[telegram:reframe] op=compose key=${key2} provenance=${prov} source=${acked?.source ?? "unknown"}`
38730
+ );
38180
38731
  }
38181
38732
  });
38182
38733
  }
@@ -38249,6 +38800,8 @@ var TelegramGateway = class {
38249
38800
  const willDeliverNow = this.hub.isReady(key2);
38250
38801
  const source = input.source ?? "user";
38251
38802
  this.replies.set(key2, input.reply);
38803
+ if (input.role === "admin") this.adminKeys.add(key2);
38804
+ else this.adminKeys.delete(key2);
38252
38805
  let standingRules;
38253
38806
  if (input.role === "admin" && this.deps.fetchStandingRules) {
38254
38807
  const res = await this.deps.fetchStandingRules(input.accountId);
@@ -38260,13 +38813,36 @@ var TelegramGateway = class {
38260
38813
  );
38261
38814
  }
38262
38815
  this.hub.deliver(
38263
- { key: key2, text: input.text, media: input.media, messageId: `tg-${++this.seq}`, source, standingRules, scheduleProvenance: input.scheduleProvenance },
38816
+ {
38817
+ key: key2,
38818
+ text: input.text,
38819
+ media: input.media,
38820
+ messageId: `tg-${++this.seq}`,
38821
+ source,
38822
+ // Task 2561 — passed explicitly rather than parsed out of `key`:
38823
+ // accountId is not in the key, and the channel service imports nothing
38824
+ // from app/lib. Without them the composer cannot name who is speaking,
38825
+ // and the turn opens straight into rules written about the owner.
38826
+ senderId: input.senderId,
38827
+ accountId: input.accountId,
38828
+ standingRules,
38829
+ scheduleProvenance: input.scheduleProvenance
38830
+ },
38264
38831
  Date.now()
38265
38832
  );
38266
38833
  const resolvedSessionId = this.sessionIdFor(input);
38267
38834
  console.error(
38268
38835
  `[telegram-native] op=inbound key=${key2} botId=${input.botId} accountId=${input.accountId} role=${input.role} agent=${input.agentSlug} bytes=${bytes} source=${source} delivery=${willDeliverNow ? "immediate" : "queued"} sessionId=${resolvedSessionId}`
38269
38836
  );
38837
+ try {
38838
+ if (source === "user") {
38839
+ this.deps.sendChatAction?.({ botToken: input.botToken, chatId: input.chatId, key: key2 });
38840
+ }
38841
+ } catch (err) {
38842
+ console.error(
38843
+ `[telegram-native] op=chat-action key=${key2} site=inbound ok=false reason=${err instanceof Error ? err.message : String(err)}`
38844
+ );
38845
+ }
38270
38846
  if (!hadSubscriber && !this.spawning.has(key2)) {
38271
38847
  this.spawning.add(key2);
38272
38848
  console.error(`[telegram-native] op=spawn-trigger key=${key2} botId=${input.botId} role=${input.role}`);
@@ -38300,6 +38876,18 @@ var TelegramGateway = class {
38300
38876
  liveReplyTarget(key2) {
38301
38877
  return this.replies.get(key2) ?? null;
38302
38878
  }
38879
+ /** Task 2559 — every admin door this gateway currently holds a live delivery
38880
+ * closure for, as the `<botId>:<senderId>` channel key the follower registry
38881
+ * keys on. The twin of `WaGateway.boundTargets()`: the census reconciles
38882
+ * against exactly this set, so "should have a reader" and "can be sent to"
38883
+ * are one fact and cannot drift. A key `parseTelegramChannelKey` rejects is
38884
+ * the pre-2368 bare-sender shape, skipped rather than halved at an arbitrary
38885
+ * offset, which would yield a door id nothing keys on. */
38886
+ boundTargets() {
38887
+ return [...this.replies.keys()].filter(
38888
+ (key2) => this.adminKeys.has(key2) && parseTelegramChannelKey(key2) !== null
38889
+ );
38890
+ }
38303
38891
  async sendOutbound(key2, text) {
38304
38892
  const reply = this.replies.get(key2);
38305
38893
  if (!reply) throw new Error(`telegram-gateway: no reply route for key ${key2}`);
@@ -38394,24 +38982,13 @@ function buildTelegramEnsureSession(deps) {
38394
38982
  `${TAG62} op=spawn botId=${input.botId} sessionId=${req.sessionId} targetAccountId=${input.accountId} role=admin`
38395
38983
  );
38396
38984
  noteSpawnOk2(input.botId);
38397
- deps.followers.get(req.channelKey)?.abort();
38398
- let ac;
38399
- ac = deps.startFileFollower({
38985
+ deps.followers.attach({
38400
38986
  sessionId: req.sessionId,
38401
38987
  senderId: req.channelKey,
38402
38988
  accountId: input.accountId,
38403
38989
  chatId: input.chatId,
38404
- botToken: input.botToken,
38405
- // Task 2521 — resolved per answer, not captured once, so a door that goes
38406
- // live after this session started still receives.
38407
- fanTargets: () => deps.fanTargets(req.channelKey),
38408
- // Identity-guarded eviction: only delete if this controller is still the
38409
- // current one, so a stale self-close never removes a live follower.
38410
- onClose: () => {
38411
- if (deps.followers.get(req.channelKey) === ac) deps.followers.delete(req.channelKey);
38412
- }
38990
+ botToken: input.botToken
38413
38991
  });
38414
- deps.followers.set(req.channelKey, ac);
38415
38992
  };
38416
38993
  }
38417
38994
 
@@ -38530,6 +39107,7 @@ function makeTelegramFileDelivery(entry2, botToken) {
38530
39107
  }
38531
39108
 
38532
39109
  // app/lib/telegram/gateway/native-file-follower.ts
39110
+ var CHAT_ACTION_REFRESH_MS = 4e3;
38533
39111
  function startTelegramNativeFileFollower(input) {
38534
39112
  const entry2 = {
38535
39113
  sessionId: input.sessionId,
@@ -38549,6 +39127,16 @@ function startTelegramNativeFileFollower(input) {
38549
39127
  // The chat id the document is addressed to (set per spawn from the inbound).
38550
39128
  replyTarget: input.chatId
38551
39129
  };
39130
+ let lastChatActionAt = Number.NEGATIVE_INFINITY;
39131
+ const onActivity = () => {
39132
+ const now = Date.now();
39133
+ if (now - lastChatActionAt < CHAT_ACTION_REFRESH_MS) return;
39134
+ lastChatActionAt = now;
39135
+ void sendTelegramChatAction(input.botToken, input.chatId, input.senderId, "activity");
39136
+ };
39137
+ const onTurnComplete = () => {
39138
+ lastChatActionAt = Number.NEGATIVE_INFINITY;
39139
+ };
38552
39140
  return startEmitter({
38553
39141
  entry: entry2,
38554
39142
  tag: "[telegram-adaptor]",
@@ -38562,6 +39150,8 @@ function startTelegramNativeFileFollower(input) {
38562
39150
  // A resumed session's JSONL already holds prior SendUserFile tool_uses;
38563
39151
  // suppress replay so historical files are not re-sent on attach.
38564
39152
  suppressResumeReplay: true,
39153
+ onActivity,
39154
+ onTurnComplete,
38565
39155
  // Task 2521 — the answer reaches every bound door through the one fan-out.
38566
39156
  // Ungated by design: whatever woke the turn, the reader and the chats show
38567
39157
  // the same thing.
@@ -38995,6 +39585,15 @@ function* walkTelegramSidecars() {
38995
39585
  }
38996
39586
  }
38997
39587
 
39588
+ // app/lib/telegram/gateway/origin-target.ts
39589
+ function answeringOriginTarget(send, key2, noteAnswered3) {
39590
+ if (!send) return null;
39591
+ return async (text) => {
39592
+ await send(text);
39593
+ noteAnswered3(key2);
39594
+ };
39595
+ }
39596
+
38998
39597
  // server/telegram-migrate.ts
38999
39598
  import { readFileSync as readFileSync53, writeFileSync as writeFileSync19, existsSync as existsSync53, renameSync as renameSync11, mkdirSync as mkdirSync11 } from "fs";
39000
39599
  import { dirname as dirname18, join as join63 } from "path";
@@ -39495,7 +40094,12 @@ var nativeFileFollowers = createFollowerRegistry(
39495
40094
  // delivered session 8e5bc4e8's answer to nothing.
39496
40095
  fanTargets: () => whatsappFanTargets(p.effectiveAccountId, p.senderId),
39497
40096
  onClose
39498
- })
40097
+ }),
40098
+ // Task 2569 — the door is (effective account, sender), the same pair the
40099
+ // reply map and the session id key on. Keyed on the sender alone, a second
40100
+ // account's spawn for the same phone aborted the first account's live reader
40101
+ // and overwrote its recorded parameters, and the census called that healthy.
40102
+ whatsappDoorId
39499
40103
  );
39500
40104
  var webchatFileFollowers = /* @__PURE__ */ new Map();
39501
40105
  async function fetchAccountStandingRules(accountId) {
@@ -39736,20 +40340,6 @@ var waGateway = new WaGateway({
39736
40340
  });
39737
40341
  }
39738
40342
  });
39739
- var followerCensusState = makeCensusState();
39740
- registerLoop({
39741
- name: "channel-follower",
39742
- intervalMs: 6e4,
39743
- run: () => followerCensusTick(
39744
- {
39745
- bound: () => waGateway.boundTargets(),
39746
- live: () => nativeFileFollowers.live(),
39747
- paramsFor: (senderId) => nativeFileFollowers.paramsFor(senderId),
39748
- attach: (p) => nativeFileFollowers.attach(p)
39749
- },
39750
- followerCensusState
39751
- )
39752
- });
39753
40343
  app80.route("/", waGateway.routes());
39754
40344
  waGateway.startSweeper();
39755
40345
  registerLoop({
@@ -39917,7 +40507,6 @@ setWebchatGateway(webchatGateway);
39917
40507
  app80.route("/", webchatGateway.routes());
39918
40508
  webchatGateway.startSweeper();
39919
40509
  webchatGateway.startPublicReaper();
39920
- var telegramFileFollowers = /* @__PURE__ */ new Map();
39921
40510
  var telegramFanTargets = (key2) => {
39922
40511
  const account = listValidAccounts().find(
39923
40512
  (a) => listBotEntries(a.config.telegram).some((e) => e.id === key2.slice(0, key2.indexOf(":")))
@@ -39934,6 +40523,19 @@ var telegramFanTargets = (key2) => {
39934
40523
  deps: adminFanDeps("telegram", account.accountId)
39935
40524
  });
39936
40525
  };
40526
+ var telegramFileFollowers = createFollowerRegistry(
40527
+ (p, onClose) => startTelegramNativeFileFollower({
40528
+ sessionId: p.sessionId,
40529
+ senderId: p.senderId,
40530
+ accountId: p.accountId,
40531
+ chatId: p.chatId,
40532
+ botToken: p.botToken,
40533
+ // Task 2521 — resolved per answer, not captured once, so a door that goes
40534
+ // live after this session started still receives.
40535
+ fanTargets: () => telegramFanTargets(p.senderId),
40536
+ onClose
40537
+ })
40538
+ );
39937
40539
  var telegramGateway = new TelegramGateway({
39938
40540
  fetchStandingRules: fetchAccountStandingRules,
39939
40541
  // Task 2353 — no turn-instruction resolver on this door. See the dep comment in
@@ -39945,6 +40547,12 @@ var telegramGateway = new TelegramGateway({
39945
40547
  resolveSessionOverride: (accountId, channelKey) => resolveChannelOverride(accountId, "telegram", channelKey),
39946
40548
  // Task 2521 — hoisted above; see `telegramFanTargets`.
39947
40549
  fanTargetsFor: telegramFanTargets,
40550
+ // Task 2576 — the typing indicator. Injected here because the gateway is
40551
+ // contracted to hold no Bot API import; see the dep comment in
40552
+ // telegram-gateway.ts. Not awaited: an indicator never paces a turn.
40553
+ sendChatAction: ({ botToken, chatId, key: key2 }) => {
40554
+ void sendTelegramChatAction(botToken, chatId, key2, "inbound");
40555
+ },
39948
40556
  gatewayUrl: telegramGatewayUrl(),
39949
40557
  serverPath: telegramServerPath(),
39950
40558
  // Task 2368 — the role branch lives in its own module so it has a test that
@@ -39954,14 +40562,45 @@ var telegramGateway = new TelegramGateway({
39954
40562
  managerRcSpawn,
39955
40563
  managerSpawn,
39956
40564
  resolveSessionOverride: (accountId, channelKey) => resolveChannelOverride(accountId, "telegram", channelKey),
39957
- startFileFollower: startTelegramNativeFileFollower,
39958
- fanTargets: telegramFanTargets,
39959
40565
  followers: telegramFileFollowers
39960
40566
  })
39961
40567
  });
39962
40568
  setTelegramGateway(telegramGateway);
39963
40569
  app80.route("/", telegramGateway.routes());
39964
40570
  telegramGateway.startSweeper();
40571
+ var followerCensusState = { whatsapp: makeCensusState(), telegram: makeCensusState() };
40572
+ registerLoop({
40573
+ name: "channel-follower",
40574
+ intervalMs: 6e4,
40575
+ run: () => [
40576
+ followerCensusTick(
40577
+ {
40578
+ channel: "whatsapp",
40579
+ // Task 2569 — the whole hub key, not its sender half. `boundTargets()`
40580
+ // decodes the reply-map keys and this re-encodes them through the same
40581
+ // helper pair, so the census and the gateway compare one fact. Halving
40582
+ // it here is what let one phone's two accounts collapse to one door.
40583
+ bound: () => waGateway.boundTargets().map((t) => hubKey(t.accountId, t.senderId)),
40584
+ live: () => nativeFileFollowers.live(),
40585
+ paramsFor: (doorId) => nativeFileFollowers.paramsFor(doorId),
40586
+ attach: (p) => nativeFileFollowers.attach(p),
40587
+ // The hub key carries a NUL, which must never reach server.log.
40588
+ label: whatsappDoorLabel
40589
+ },
40590
+ followerCensusState.whatsapp
40591
+ ),
40592
+ followerCensusTick(
40593
+ {
40594
+ channel: "telegram",
40595
+ bound: () => telegramGateway.boundTargets(),
40596
+ live: () => telegramFileFollowers.live(),
40597
+ paramsFor: (doorId) => telegramFileFollowers.paramsFor(doorId),
40598
+ attach: (p) => telegramFileFollowers.attach(p)
40599
+ },
40600
+ followerCensusState.telegram
40601
+ )
40602
+ ].join("; ")
40603
+ });
39965
40604
  var chatRoutes = createChatRoutes({
39966
40605
  handleInbound: (input) => webchatGateway.handleInbound(input),
39967
40606
  awaitReply: (key2, timeoutMs) => webchatGateway.awaitReply(key2, timeoutMs)
@@ -41588,7 +42227,7 @@ function whatsappSender(accountId, key2) {
41588
42227
  };
41589
42228
  }
41590
42229
  function telegramOriginTarget(key2) {
41591
- return telegramGateway.liveReplyTarget(key2);
42230
+ return answeringOriginTarget(telegramGateway.liveReplyTarget(key2), key2, noteAnswered);
41592
42231
  }
41593
42232
  var configDirForWhatsApp = basename17(MAXY_DIR) || ".maxy";
41594
42233
  var bootAccount = resolveAccount();