@rubytech/create-maxy-code 0.1.289 → 0.1.291

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 (177) hide show
  1. package/dist/index.js +14 -0
  2. package/package.json +1 -1
  3. package/payload/platform/lib/graph-style/dist/index.d.ts +2 -1
  4. package/payload/platform/lib/graph-style/dist/index.d.ts.map +1 -1
  5. package/payload/platform/lib/graph-style/dist/index.js +14 -1
  6. package/payload/platform/lib/graph-style/dist/index.js.map +1 -1
  7. package/payload/platform/lib/graph-style/src/index.ts +15 -1
  8. package/payload/platform/package.json +3 -2
  9. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +3 -1
  10. package/payload/platform/plugins/business-assistant/skills/e-sign/SKILL.md +21 -0
  11. package/payload/platform/plugins/cloudflare/references/manual-setup.md +9 -5
  12. package/payload/platform/plugins/docs/references/troubleshooting.md +2 -0
  13. package/payload/platform/plugins/whatsapp/PLUGIN.md +4 -0
  14. package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +28 -1
  15. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts +6 -0
  16. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  17. package/payload/platform/services/claude-session-manager/dist/http-server.js +65 -3
  18. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  19. package/payload/platform/services/claude-session-manager/dist/index.js +8 -0
  20. package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
  21. package/payload/platform/services/claude-session-manager/dist/webchat-channel-mcp.d.ts +27 -0
  22. package/payload/platform/services/claude-session-manager/dist/webchat-channel-mcp.d.ts.map +1 -0
  23. package/payload/platform/services/claude-session-manager/dist/webchat-channel-mcp.js +45 -0
  24. package/payload/platform/services/claude-session-manager/dist/webchat-channel-mcp.js.map +1 -0
  25. package/payload/platform/services/claude-session-manager/dist/webchat-channel-store.d.ts +29 -0
  26. package/payload/platform/services/claude-session-manager/dist/webchat-channel-store.d.ts.map +1 -0
  27. package/payload/platform/services/claude-session-manager/dist/webchat-channel-store.js +121 -0
  28. package/payload/platform/services/claude-session-manager/dist/webchat-channel-store.js.map +1 -0
  29. package/payload/platform/services/webchat-channel/dist/notification.d.ts +55 -0
  30. package/payload/platform/services/webchat-channel/dist/notification.d.ts.map +1 -0
  31. package/payload/platform/services/webchat-channel/dist/notification.js +59 -0
  32. package/payload/platform/services/webchat-channel/dist/notification.js.map +1 -0
  33. package/payload/platform/services/webchat-channel/dist/server.d.ts +3 -0
  34. package/payload/platform/services/webchat-channel/dist/server.d.ts.map +1 -0
  35. package/payload/platform/services/webchat-channel/dist/server.js +179 -0
  36. package/payload/platform/services/webchat-channel/dist/server.js.map +1 -0
  37. package/payload/platform/services/webchat-channel/package.json +20 -0
  38. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +27 -13
  39. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts.map +1 -1
  40. package/payload/platform/services/whatsapp-channel/dist/notification.js +32 -15
  41. package/payload/platform/services/whatsapp-channel/dist/notification.js.map +1 -1
  42. package/payload/server/public/assets/AdminShell-BgPlhiCM.js +1 -0
  43. package/payload/server/public/assets/{Checkbox-CsQq7YPC.js → Checkbox-DWx2NcU9.js} +1 -1
  44. package/payload/server/public/assets/SidebarSplitter-CidskiAr.js +1 -0
  45. package/payload/server/public/assets/Transcript-iuM5b4JT.js +1 -0
  46. package/payload/server/public/assets/admin-Cdz561nK.js +1 -0
  47. package/payload/server/public/assets/{arc-BW1WhwmV.js → arc-yRinR5h4.js} +1 -1
  48. package/payload/server/public/assets/architecture-YZFGNWBL-B2UAw-4R.js +1 -0
  49. package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-PGePlVpb.js → architectureDiagram-Q4EWVU46-AtvCAoKj.js} +1 -1
  50. package/payload/server/public/assets/{blockDiagram-DXYQGD6D-BJCbJRRs.js → blockDiagram-DXYQGD6D-CwUSTR3t.js} +1 -1
  51. package/payload/server/public/assets/browser-Bwd_rYu9.js +1 -0
  52. package/payload/server/public/assets/{c4Diagram-AHTNJAMY-DcMhi4gF.js → c4Diagram-AHTNJAMY-Btkecaf-.js} +1 -1
  53. package/payload/server/public/assets/channel-1MEkxMOf.js +1 -0
  54. package/payload/server/public/assets/chat-wbKqew6m.js +1 -0
  55. package/payload/server/public/assets/{chunk-2KRD3SAO-CKn-bEkI.js → chunk-2KRD3SAO-DAJbbBPY.js} +1 -1
  56. package/payload/server/public/assets/{chunk-336JU56O-0vhS0MI-.js → chunk-336JU56O-fJXOXFRr.js} +2 -2
  57. package/payload/server/public/assets/chunk-426QAEUC-28Pgly06.js +1 -0
  58. package/payload/server/public/assets/{chunk-4BX2VUAB-B2mSmC97.js → chunk-4BX2VUAB-BTCTbhtf.js} +1 -1
  59. package/payload/server/public/assets/{chunk-4TB4RGXK-dgx3qIz_.js → chunk-4TB4RGXK-IFvQOpiF.js} +1 -1
  60. package/payload/server/public/assets/{chunk-55IACEB6-JLT9m7Hd.js → chunk-55IACEB6-BY026nUv.js} +1 -1
  61. package/payload/server/public/assets/{chunk-5FUZZQ4R-BglTosdT.js → chunk-5FUZZQ4R-DD-XOAcm.js} +1 -1
  62. package/payload/server/public/assets/{chunk-5PVQY5BW-hZB3AxUF.js → chunk-5PVQY5BW-Ck-5mL2A.js} +1 -1
  63. package/payload/server/public/assets/{chunk-67CJDMHE-CCdP3Pdx.js → chunk-67CJDMHE-Dl7bLJGe.js} +1 -1
  64. package/payload/server/public/assets/{chunk-7N4EOEYR-bXgVzdM9.js → chunk-7N4EOEYR-CFBOyqnD.js} +1 -1
  65. package/payload/server/public/assets/{chunk-AA7GKIK3-CYeDvY8B.js → chunk-AA7GKIK3-BnnqjSlh.js} +1 -1
  66. package/payload/server/public/assets/{chunk-BSJP7CBP-U0Fi-Ers.js → chunk-BSJP7CBP-DKCTDAZE.js} +1 -1
  67. package/payload/server/public/assets/{chunk-CIAEETIT-C8QrV6Gg.js → chunk-CIAEETIT-jjeAEkQy.js} +1 -1
  68. package/payload/server/public/assets/{chunk-EDXVE4YY-DXqLJyYa.js → chunk-EDXVE4YY-BPqvO0TO.js} +1 -1
  69. package/payload/server/public/assets/{chunk-ENJZ2VHE-BD4I_a54.js → chunk-ENJZ2VHE-PN6Hbw2D.js} +1 -1
  70. package/payload/server/public/assets/{chunk-FMBD7UC4-DxAQS3UB.js → chunk-FMBD7UC4-B0zglVlf.js} +1 -1
  71. package/payload/server/public/assets/{chunk-FOC6F5B3-CyIK3zeY.js → chunk-FOC6F5B3-COVX_UuK.js} +1 -1
  72. package/payload/server/public/assets/{chunk-ICPOFSXX-BdQx0Ahc.js → chunk-ICPOFSXX-DkmwqyfQ.js} +2 -2
  73. package/payload/server/public/assets/{chunk-K5T4RW27-7GcDzjyJ.js → chunk-K5T4RW27-Bav1KRIS.js} +1 -1
  74. package/payload/server/public/assets/{chunk-KGLVRYIC-B7pAr9hf.js → chunk-KGLVRYIC-nj7A12cJ.js} +1 -1
  75. package/payload/server/public/assets/{chunk-LIHQZDEY-DEoU2jrz.js → chunk-LIHQZDEY-BmMkL9jD.js} +1 -1
  76. package/payload/server/public/assets/{chunk-ORNJ4GCN-BwMFuN24.js → chunk-ORNJ4GCN-BvGwrI3j.js} +1 -1
  77. package/payload/server/public/assets/{chunk-OYMX7WX6-BDjtbZvS.js → chunk-OYMX7WX6-CWyihoif.js} +1 -1
  78. package/payload/server/public/assets/chunk-QZHKN3VN-COXR_N4N.js +1 -0
  79. package/payload/server/public/assets/{chunk-U2HBQHQK-hYN7A3g_.js → chunk-U2HBQHQK-BCWQB0k9.js} +1 -1
  80. package/payload/server/public/assets/{chunk-X2U36JSP-D3njJ7WG.js → chunk-X2U36JSP-CcaCMsL0.js} +1 -1
  81. package/payload/server/public/assets/{chunk-XPW4576I-BA0a8Ygs.js → chunk-XPW4576I-DpLw5_cw.js} +1 -1
  82. package/payload/server/public/assets/{chunk-YZCP3GAM-BfZKEcvU.js → chunk-YZCP3GAM-BUTzrAoG.js} +1 -1
  83. package/payload/server/public/assets/{chunk-ZZ45TVLE-DLJMNqLn.js → chunk-ZZ45TVLE-BD9UGg96.js} +1 -1
  84. package/payload/server/public/assets/classDiagram-6PBFFD2Q-BcD2DnVi.js +1 -0
  85. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-ChvbGJh0.js +1 -0
  86. package/payload/server/public/assets/clone-DPfDeykW.js +1 -0
  87. package/payload/server/public/assets/{cose-bilkent-S5V4N54A-Df_JmB8E.js → cose-bilkent-S5V4N54A-bCIQHniJ.js} +1 -1
  88. package/payload/server/public/assets/{dagre-KV5264BT-DGjcX5uz.js → dagre-KV5264BT-DcOMoMzF.js} +1 -1
  89. package/payload/server/public/assets/{dagre-DVLE4-m6.js → dagre-dnTAouT6.js} +1 -1
  90. package/payload/server/public/assets/data-BSt0roMQ.js +1 -0
  91. package/payload/server/public/assets/{diagram-5BDNPKRD-CQI2o8Cv.js → diagram-5BDNPKRD-BuQUC75n.js} +1 -1
  92. package/payload/server/public/assets/{diagram-G4DWMVQ6-C8kXsEdQ.js → diagram-G4DWMVQ6-DHiu9U7B.js} +1 -1
  93. package/payload/server/public/assets/{diagram-MMDJMWI5-DaWrNg_c.js → diagram-MMDJMWI5-ypu7YJGF.js} +1 -1
  94. package/payload/server/public/assets/{diagram-TYMM5635-Ql9QPUxX.js → diagram-TYMM5635-DIwuWuqN.js} +1 -1
  95. package/payload/server/public/assets/{dist-BmvZ8OaX.js → dist-ePW3hz19.js} +1 -1
  96. package/payload/server/public/assets/{erDiagram-SMLLAGMA-B2fBZhPo.js → erDiagram-SMLLAGMA-CKxGMQZW.js} +1 -1
  97. package/payload/server/public/assets/{flatten-Bo6YRmWl.js → flatten-CpKIi5d2.js} +1 -1
  98. package/payload/server/public/assets/{flowDiagram-DWJPFMVM-BA0LiOm5.js → flowDiagram-DWJPFMVM-CqH2RE7a.js} +1 -1
  99. package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-CBKdEgUk.js → ganttDiagram-T4ZO3ILL-BfuGXUUh.js} +1 -1
  100. package/payload/server/public/assets/gitGraph-7Q5UKJZL-CwwaM7ed.js +1 -0
  101. package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-BIyEfuwB.js → gitGraphDiagram-UUTBAWPF-BlpUYLLd.js} +1 -1
  102. package/payload/server/public/assets/{graph-BxWBNsFs.js → graph-BEu_XQMZ.js} +19 -19
  103. package/payload/server/public/assets/graph-labels-DvO7i7Ez.js +1 -0
  104. package/payload/server/public/assets/{graphlib-Cv4SvsQN.js → graphlib-O_QsiopN.js} +1 -1
  105. package/payload/server/public/assets/info-OMHHGYJF-CiiU112m.js +1 -0
  106. package/payload/server/public/assets/infoDiagram-42DDH7IO-tHUu3AX-.js +2 -0
  107. package/payload/server/public/assets/{isEmpty-D6Kr-M1M.js → isEmpty-C3Vxk1It.js} +1 -1
  108. package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-haFza9s4.js → ishikawaDiagram-UXIWVN3A-CVMbR1ZD.js} +1 -1
  109. package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-l-8tJ21z.js → journeyDiagram-VCZTEJTY-7pbwsn7f.js} +1 -1
  110. package/payload/server/public/assets/{kanban-definition-6JOO6SKY-DSZUvFLs.js → kanban-definition-6JOO6SKY-CWFMjHiJ.js} +1 -1
  111. package/payload/server/public/assets/{line-mfTElknw.js → line-B4V-vQdN.js} +1 -1
  112. package/payload/server/public/assets/{linear-COm19-p7.js → linear-Cdt0EBMa.js} +1 -1
  113. package/payload/server/public/assets/{mermaid-parser.core-4temHHPS.js → mermaid-parser.core-Ba5Re-_1.js} +2 -2
  114. package/payload/server/public/assets/{mermaid.core-DO2iS9my.js → mermaid.core-C39ZL-9e.js} +3 -3
  115. package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-Cymf2IGG.js → mindmap-definition-QFDTVHPH-BrREiATP.js} +1 -1
  116. package/payload/server/public/assets/{ordinal-BDi6f4xk.js → ordinal-BLrOss5K.js} +1 -1
  117. package/payload/server/public/assets/packet-4T2RLAQJ-Bq_zPNb3.js +1 -0
  118. package/payload/server/public/assets/pie-ZZUOXDRM-R-cewHYM.js +1 -0
  119. package/payload/server/public/assets/{pieDiagram-DEJITSTG-_2tmuvMb.js → pieDiagram-DEJITSTG-sJOzKUIf.js} +1 -1
  120. package/payload/server/public/assets/{public-Rz_GzOrN.js → public-927mDSLH.js} +3 -3
  121. package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-CVCrGnBi.js → quadrantDiagram-34T5L4WZ-bxoGUruT.js} +1 -1
  122. package/payload/server/public/assets/radar-PYXPWWZC-CZpBUjmo.js +1 -0
  123. package/payload/server/public/assets/{reduce-CGi9ik8i.js → reduce-C5tBOlxC.js} +1 -1
  124. package/payload/server/public/assets/{requirementDiagram-MS252O5E-Bc-o8i8Z.js → requirementDiagram-MS252O5E-C8_uEDnp.js} +1 -1
  125. package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-DrTBxqE6.js → sankeyDiagram-XADWPNL6-CKpSBj6p.js} +1 -1
  126. package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-BjVjSsUJ.js → sequenceDiagram-FGHM5R23-BbVOmtji.js} +1 -1
  127. package/payload/server/public/assets/{src-DLEIsjER.js → src-DssjGmtU.js} +1 -1
  128. package/payload/server/public/assets/{stateDiagram-FHFEXIEX-DWAJP6ly.js → stateDiagram-FHFEXIEX-CLiLypPX.js} +1 -1
  129. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-Cn6OBfIT.js +1 -0
  130. package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-Br_nbteH.js → timeline-definition-GMOUNBTQ-Dzzc-pnB.js} +1 -1
  131. package/payload/server/public/assets/treeView-SZITEDCU-CjsdVrjr.js +1 -0
  132. package/payload/server/public/assets/treemap-W4RFUUIX-BZ8peN4O.js +1 -0
  133. package/payload/server/public/assets/{useSelectionMode-hZ5bdL8b.js → useSelectionMode-BBvWzgtz.js} +1 -1
  134. package/payload/server/public/assets/useSelectionMode-CzWzToaM.css +1 -0
  135. package/payload/server/public/assets/{vennDiagram-DHZGUBPP-D_ozeKV6.js → vennDiagram-DHZGUBPP-CEj_ctTP.js} +1 -1
  136. package/payload/server/public/assets/wardley-RL74JXVD-BYLmRSDV.js +1 -0
  137. package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-B87ZKC2B.js → wardleyDiagram-NUSXRM2D-Cnbz79qd.js} +1 -1
  138. package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-DosRlk2T.js → xychartDiagram-5P7HB3ND-MTjLweae.js} +1 -1
  139. package/payload/server/public/browser.html +6 -5
  140. package/payload/server/public/chat.html +19 -0
  141. package/payload/server/public/data.html +7 -6
  142. package/payload/server/public/graph.html +8 -7
  143. package/payload/server/public/index.html +8 -6
  144. package/payload/server/public/public.html +5 -5
  145. package/payload/server/server.js +852 -349
  146. package/payload/server/public/assets/AdminShell-DkP2rJdF.js +0 -1
  147. package/payload/server/public/assets/admin-C_bkVvXh.js +0 -1
  148. package/payload/server/public/assets/architecture-YZFGNWBL-xHbVjatf.js +0 -1
  149. package/payload/server/public/assets/browser-DynPFG_A.js +0 -1
  150. package/payload/server/public/assets/channel-idaGQqlP.js +0 -1
  151. package/payload/server/public/assets/chunk-426QAEUC-DbVU-p-q.js +0 -1
  152. package/payload/server/public/assets/chunk-QZHKN3VN-CHE_iZO7.js +0 -1
  153. package/payload/server/public/assets/classDiagram-6PBFFD2Q-CmRFpirU.js +0 -1
  154. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-DFy8xZjP.js +0 -1
  155. package/payload/server/public/assets/clone-kFPktUPQ.js +0 -1
  156. package/payload/server/public/assets/data-D1FFhz2k.js +0 -1
  157. package/payload/server/public/assets/gitGraph-7Q5UKJZL-CmHdoS27.js +0 -1
  158. package/payload/server/public/assets/graph-labels-DSLdDXoF.js +0 -1
  159. package/payload/server/public/assets/info-OMHHGYJF-DTA9msO-.js +0 -1
  160. package/payload/server/public/assets/infoDiagram-42DDH7IO-DovHLDHz.js +0 -2
  161. package/payload/server/public/assets/packet-4T2RLAQJ-6GjqIKgu.js +0 -1
  162. package/payload/server/public/assets/pie-ZZUOXDRM-T72DJ5JR.js +0 -1
  163. package/payload/server/public/assets/radar-PYXPWWZC-x-6xW1dE.js +0 -1
  164. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-BHDyrxpj.js +0 -1
  165. package/payload/server/public/assets/treeView-SZITEDCU-5-6wEryJ.js +0 -1
  166. package/payload/server/public/assets/treemap-W4RFUUIX-DF48Cc86.js +0 -1
  167. package/payload/server/public/assets/useSelectionMode-BLb-o9RX.css +0 -1
  168. package/payload/server/public/assets/wardley-RL74JXVD-duKIa6Hq.js +0 -1
  169. /package/payload/server/public/assets/{_baseFor-Cam2PbSt.js → _baseFor-WfS9pKAn.js} +0 -0
  170. /package/payload/server/public/assets/{array-DYRGGQae.js → array-HeX70jSN.js} +0 -0
  171. /package/payload/server/public/assets/{chunk-CnGqDkHZ.js → chunk-CWOGyPgy.js} +0 -0
  172. /package/payload/server/public/assets/{cytoscape.esm-nWsJMTNI.js → cytoscape.esm-CDZo0kst.js} +0 -0
  173. /package/payload/server/public/assets/{defaultLocale-Du1XY3Dp.js → defaultLocale-GJwWH1Jr.js} +0 -0
  174. /package/payload/server/public/assets/{init-B5BXBRcm.js → init-BPLPMQ3Y.js} +0 -0
  175. /package/payload/server/public/assets/{katex-HOUACuRw.js → katex-CKZ-HWMQ.js} +0 -0
  176. /package/payload/server/public/assets/{path-CNO468J-.js → path-YdFzr2W6.js} +0 -0
  177. /package/payload/server/public/assets/{rough.esm-DRO6hWPh.js → rough.esm-HAx67Hnb.js} +0 -0
@@ -411,6 +411,18 @@ var require_dist2 = __commonJS({
411
411
  Property: "#5C7A99",
412
412
  Viewing: "#D4A574",
413
413
  Offer: "#B08850",
414
+ // Construction ontology (Task 773; labels registered in schema.cypher by
415
+ // Task 650). Muted brick-red family — the construction register sits
416
+ // apart from people-terracotta, viewing-amber, and task-rose bands so
417
+ // siteoffice legends read as their own group. Top-level (operator-entry)
418
+ // labels only; child labels (LineItem, Milestone, Valuation,
419
+ // VariationNote, TimeLog) intentionally stay fallback-grey.
420
+ Job: "#8C3B2E",
421
+ QuoteDocument: "#A65A4A",
422
+ InboundInvoice: "#6B2A20",
423
+ SubContractor: "#BF7E6E",
424
+ SubInvoice: "#52201A",
425
+ WhatsAppGroup: "#D4A096",
414
426
  // External business entity — schema:Organization, written by memory-write
415
427
  // as the Supplier label. Warm tobacco — distinct hue from internal-business
416
428
  // slates so external entities are scannable at a glance.
@@ -1203,7 +1215,7 @@ var serveStatic = (options = { root: "" }) => {
1203
1215
 
1204
1216
  // server/index.ts
1205
1217
  import { readFileSync as readFileSync24, existsSync as existsSync25, watchFile } from "fs";
1206
- import { resolve as resolve27, join as join19, basename as basename7 } from "path";
1218
+ import { resolve as resolve27, join as join20, basename as basename8 } from "path";
1207
1219
  import { homedir as homedir3 } from "os";
1208
1220
  import { monitorEventLoopDelay } from "perf_hooks";
1209
1221
 
@@ -2432,8 +2444,45 @@ function isDuplicateInbound(key) {
2432
2444
  return true;
2433
2445
  }
2434
2446
 
2447
+ // app/lib/whatsapp/inbound/self-chat.ts
2448
+ var TAG4 = "[whatsapp:self-chat]";
2449
+ function normalizeJid(jid) {
2450
+ const m = jid.match(/^(\d+)(?::\d+)?@(s\.whatsapp\.net|lid)$/i);
2451
+ return m ? `${m[1]}@${m[2].toLowerCase()}` : jid;
2452
+ }
2453
+ function isSelfChatJid(remoteJid, self) {
2454
+ const candidates = /* @__PURE__ */ new Set();
2455
+ if (self.selfJid) candidates.add(normalizeJid(self.selfJid));
2456
+ if (self.selfLid) candidates.add(normalizeJid(self.selfLid));
2457
+ if (self.selfPhone) candidates.add(`${self.selfPhone}@s.whatsapp.net`);
2458
+ if (candidates.size === 0) return false;
2459
+ return candidates.has(normalizeJid(remoteJid));
2460
+ }
2461
+ function evaluateSelfChatCommand(input) {
2462
+ if (!input.fromMe) return { kind: "not-self-chat" };
2463
+ if (!isSelfChatJid(input.remoteJid, input)) return { kind: "not-self-chat" };
2464
+ const tsMs = input.messageTimestampSec ? input.messageTimestampSec * 1e3 : 0;
2465
+ const connAtMs = input.lastConnectedAtMs ?? 0;
2466
+ const msgId = input.msgId ?? "?";
2467
+ console.error(
2468
+ `${TAG4} op=seen account=${input.accountId} msgId=${msgId} tsMs=${tsMs} connAtMs=${connAtMs} fromMe=true`
2469
+ );
2470
+ const skip = (reason) => {
2471
+ console.error(`${TAG4} op=skip account=${input.accountId} msgId=${msgId} reason=${reason}`);
2472
+ return { kind: "skip", reason };
2473
+ };
2474
+ if (input.msgId && isAgentSentMessage(input.msgId)) return skip("echo");
2475
+ if (!(tsMs > connAtMs)) return skip("backfill");
2476
+ if (isDuplicateInbound(`${input.accountId}:${input.remoteJid}:${input.msgId}`)) return skip("duplicate");
2477
+ if (!input.text) return skip("no-text");
2478
+ console.error(
2479
+ `${TAG4} op=dispatch account=${input.accountId} msgId=${msgId} bytes=${Buffer.byteLength(input.text, "utf8")} agentType=admin`
2480
+ );
2481
+ return { kind: "dispatch", text: input.text };
2482
+ }
2483
+
2435
2484
  // app/lib/whatsapp/activity.ts
2436
- var TAG4 = "[whatsapp:activity]";
2485
+ var TAG5 = "[whatsapp:activity]";
2437
2486
  var RECENT_EVENTS_MAX = 200;
2438
2487
  var counters = /* @__PURE__ */ new Map();
2439
2488
  var recentEvents = [];
@@ -2461,10 +2510,10 @@ function recordActivity(event) {
2461
2510
  recentEvents.splice(0, recentEvents.length - RECENT_EVENTS_MAX);
2462
2511
  }
2463
2512
  console.error(
2464
- `${TAG4} channel-activity: direction=${direction} account=${accountId} jid=${jid}` + (messageType ? ` type=${messageType}` : "")
2513
+ `${TAG5} channel-activity: direction=${direction} account=${accountId} jid=${jid}` + (messageType ? ` type=${messageType}` : "")
2465
2514
  );
2466
2515
  } catch (err) {
2467
- console.error(`${TAG4} recording failed: ${String(err)}`);
2516
+ console.error(`${TAG5} recording failed: ${String(err)}`);
2468
2517
  }
2469
2518
  }
2470
2519
  function getChannelActivity(accountId) {
@@ -2485,7 +2534,7 @@ function getChannelActivity(accountId) {
2485
2534
  }
2486
2535
 
2487
2536
  // app/lib/whatsapp/outbound/send.ts
2488
- var TAG5 = "[whatsapp:outbound]";
2537
+ var TAG6 = "[whatsapp:outbound]";
2489
2538
  async function sendTextMessage(sock, to, text, opts) {
2490
2539
  try {
2491
2540
  const jid = to.includes("@") ? to : toWhatsappJid(to);
@@ -2497,14 +2546,14 @@ async function sendTextMessage(sock, to, text, opts) {
2497
2546
  const messageId = result?.key?.id;
2498
2547
  if (messageId) {
2499
2548
  trackAgentSentMessage(messageId);
2500
- console.error(`${TAG5} sent text to=${jid} id=${messageId}`);
2549
+ console.error(`${TAG6} sent text to=${jid} id=${messageId}`);
2501
2550
  }
2502
2551
  if (opts?.accountId) {
2503
2552
  recordActivity({ accountId: opts.accountId, direction: "outbound", jid, messageType: "text" });
2504
2553
  }
2505
2554
  return { success: true, messageId: messageId ?? void 0 };
2506
2555
  } catch (err) {
2507
- console.error(`${TAG5} send failed to=${to}: ${String(err)}`);
2556
+ console.error(`${TAG6} send failed to=${to}: ${String(err)}`);
2508
2557
  return { success: false, error: String(err) };
2509
2558
  }
2510
2559
  }
@@ -2515,6 +2564,13 @@ async function sendComposing(sock, to) {
2515
2564
  } catch {
2516
2565
  }
2517
2566
  }
2567
+ async function sendPaused(sock, to) {
2568
+ try {
2569
+ const jid = to.includes("@") ? to : toWhatsappJid(to);
2570
+ await sock.sendPresenceUpdate("paused", jid);
2571
+ } catch {
2572
+ }
2573
+ }
2518
2574
  async function sendReadReceipt(sock, chatJid, messageIds, participant) {
2519
2575
  try {
2520
2576
  await sock.readMessages([
@@ -2566,20 +2622,20 @@ async function sendMediaMessage(sock, to, media, opts) {
2566
2622
  const messageId = result?.key?.id;
2567
2623
  if (messageId) {
2568
2624
  trackAgentSentMessage(messageId);
2569
- console.error(`${TAG5} sent ${media.type} to=${jid} id=${messageId}`);
2625
+ console.error(`${TAG6} sent ${media.type} to=${jid} id=${messageId}`);
2570
2626
  }
2571
2627
  if (opts?.accountId) {
2572
2628
  recordActivity({ accountId: opts.accountId, direction: "outbound", jid, messageType: media.type });
2573
2629
  }
2574
2630
  return { success: true, messageId: messageId ?? void 0 };
2575
2631
  } catch (err) {
2576
- console.error(`${TAG5} send media failed to=${to}: ${String(err)}`);
2632
+ console.error(`${TAG6} send media failed to=${to}: ${String(err)}`);
2577
2633
  return { success: false, error: String(err) };
2578
2634
  }
2579
2635
  }
2580
2636
 
2581
2637
  // app/lib/whatsapp/persist-message.ts
2582
- var TAG6 = "[whatsapp-persist]";
2638
+ var TAG7 = "[whatsapp-persist]";
2583
2639
  var sessionWriteLocks = /* @__PURE__ */ new Map();
2584
2640
  async function persistWhatsAppMessage(input) {
2585
2641
  if (!input.body || !input.msgKeyId) return null;
@@ -2684,7 +2740,7 @@ async function persistWhatsAppMessage(input) {
2684
2740
  const result = await session.run(cypher, params);
2685
2741
  const ms = Date.now() - t0;
2686
2742
  if (result.records.length === 0) {
2687
- console.error(`${TAG6} skip reason=conversation-not-found accountId=${input.platformAccountId} cacheKey=${input.cacheKey} messageId=${messageId}`);
2743
+ console.error(`${TAG7} skip reason=conversation-not-found accountId=${input.platformAccountId} cacheKey=${input.cacheKey} messageId=${messageId}`);
2688
2744
  return null;
2689
2745
  }
2690
2746
  const rec = result.records[0];
@@ -2693,26 +2749,26 @@ async function persistWhatsAppMessage(input) {
2693
2749
  const senderElementId = rec.get("senderElementId");
2694
2750
  const senderReused = rec.get("senderReused") === true;
2695
2751
  if (existed) {
2696
- console.error(`${TAG6} noop reason=existing-message messageId=${messageId}`);
2752
+ console.error(`${TAG7} noop reason=existing-message messageId=${messageId}`);
2697
2753
  return { messageId, created: false, senderElementId };
2698
2754
  }
2699
2755
  console.error(
2700
- `${TAG6} sender-resolved telephone=${senderTelephone} nodeKind=Person elementId=${senderElementId} reused=${senderReused}`
2756
+ `${TAG7} sender-resolved telephone=${senderTelephone} nodeKind=Person elementId=${senderElementId} reused=${senderReused}`
2701
2757
  );
2702
2758
  if (prevMessageId) {
2703
- console.error(`${TAG6} next-merge prior=${prevMessageId} next=${messageId}`);
2759
+ console.error(`${TAG7} next-merge prior=${prevMessageId} next=${messageId}`);
2704
2760
  } else {
2705
- console.error(`${TAG6} next-skip reason=no-prior msgId=${messageId}`);
2761
+ console.error(`${TAG7} next-skip reason=no-prior msgId=${messageId}`);
2706
2762
  }
2707
2763
  console.error(
2708
- `${TAG6} write messageId=${messageId} accountId=${input.platformAccountId} cacheKey=${input.cacheKey} fromMe=${input.fromMe} dateSent=${dateSentIso} bodyBytes=${Buffer.byteLength(input.body, "utf8")} ms=${ms}`
2764
+ `${TAG7} write messageId=${messageId} accountId=${input.platformAccountId} cacheKey=${input.cacheKey} fromMe=${input.fromMe} dateSent=${dateSentIso} bodyBytes=${Buffer.byteLength(input.body, "utf8")} ms=${ms}`
2709
2765
  );
2710
2766
  return { messageId, created: true, senderElementId };
2711
2767
  } catch (err) {
2712
2768
  const ms = Date.now() - t0;
2713
2769
  const reason = sanitizeReason(err);
2714
2770
  console.error(
2715
- `${TAG6} FAIL accountId=${input.platformAccountId} waname=${input.accountId} remoteJid=${input.remoteJid} msgKey=${input.msgKeyId} reason=${reason} ms=${ms}`
2771
+ `${TAG7} FAIL accountId=${input.platformAccountId} waname=${input.accountId} remoteJid=${input.remoteJid} msgKey=${input.msgKeyId} reason=${reason} ms=${ms}`
2716
2772
  );
2717
2773
  return null;
2718
2774
  } finally {
@@ -2745,7 +2801,7 @@ function sanitizeReason(err) {
2745
2801
  }
2746
2802
 
2747
2803
  // app/lib/whatsapp/ensure-conversation.ts
2748
- var TAG7 = "[whatsapp-persist]";
2804
+ var TAG8 = "[whatsapp-persist]";
2749
2805
  async function ensureWhatsAppConversation(input) {
2750
2806
  const t0 = Date.now();
2751
2807
  try {
@@ -2757,19 +2813,19 @@ async function ensureWhatsAppConversation(input) {
2757
2813
  const ms = Date.now() - t0;
2758
2814
  if (!result.sessionId) {
2759
2815
  console.error(
2760
- `${TAG7} conversation-merged FAIL cacheKey=${input.cacheKey} reason=null-sessionId ms=${ms}`
2816
+ `${TAG8} conversation-merged FAIL cacheKey=${input.cacheKey} reason=null-sessionId ms=${ms}`
2761
2817
  );
2762
2818
  return null;
2763
2819
  }
2764
2820
  console.error(
2765
- `${TAG7} conversation-merged cacheKey=${input.cacheKey} agentType=${input.agentType} channel=whatsapp created=${result.created} ms=${ms}`
2821
+ `${TAG8} conversation-merged cacheKey=${input.cacheKey} agentType=${input.agentType} channel=whatsapp created=${result.created} ms=${ms}`
2766
2822
  );
2767
2823
  return { sessionId: result.sessionId, created: result.created };
2768
2824
  } catch (err) {
2769
2825
  const ms = Date.now() - t0;
2770
2826
  const reason = err instanceof Error ? `${err.name}:${err.message.slice(0, 200)}` : String(err).slice(0, 200);
2771
2827
  console.error(
2772
- `${TAG7} conversation-merged FAIL cacheKey=${input.cacheKey} reason=${reason} ms=${ms}`
2828
+ `${TAG8} conversation-merged FAIL cacheKey=${input.cacheKey} reason=${reason} ms=${ms}`
2773
2829
  );
2774
2830
  return null;
2775
2831
  }
@@ -2860,7 +2916,7 @@ import {
2860
2916
  downloadContentFromMessage,
2861
2917
  normalizeMessageContent as normalizeMessageContent2
2862
2918
  } from "@whiskeysockets/baileys";
2863
- var TAG8 = "[whatsapp:media]";
2919
+ var TAG9 = "[whatsapp:media]";
2864
2920
  var MEDIA_DIR = "/tmp/maxy-media";
2865
2921
  function mimeToExt(mimetype) {
2866
2922
  const map = {
@@ -2916,25 +2972,25 @@ async function downloadInboundMedia(msg, sock, opts) {
2916
2972
  }
2917
2973
  );
2918
2974
  if (!buffer || buffer.length === 0) {
2919
- console.error(`${TAG8} primary download returned empty, trying direct fallback`);
2975
+ console.error(`${TAG9} primary download returned empty, trying direct fallback`);
2920
2976
  const downloadable = getDownloadableContent(content);
2921
2977
  if (downloadable) {
2922
2978
  try {
2923
2979
  const stream2 = await downloadContentFromMessage(downloadable.downloadable, downloadable.mediaType);
2924
2980
  buffer = await streamToBuffer(stream2);
2925
2981
  } catch (fallbackErr) {
2926
- console.error(`${TAG8} direct download fallback failed: ${String(fallbackErr)}`);
2982
+ console.error(`${TAG9} direct download fallback failed: ${String(fallbackErr)}`);
2927
2983
  }
2928
2984
  }
2929
2985
  }
2930
2986
  if (!buffer || buffer.length === 0) {
2931
- console.error(`${TAG8} download failed: empty buffer for ${mimetype ?? "unknown"}`);
2987
+ console.error(`${TAG9} download failed: empty buffer for ${mimetype ?? "unknown"}`);
2932
2988
  return void 0;
2933
2989
  }
2934
2990
  if (buffer.length > maxBytes) {
2935
2991
  const sizeMB = (buffer.length / (1024 * 1024)).toFixed(1);
2936
2992
  const limitMB = (maxBytes / (1024 * 1024)).toFixed(0);
2937
- console.error(`${TAG8} media too large type=${mimetype ?? "unknown"} size=${sizeMB}MB limit=${limitMB}MB`);
2993
+ console.error(`${TAG9} media too large type=${mimetype ?? "unknown"} size=${sizeMB}MB limit=${limitMB}MB`);
2938
2994
  return void 0;
2939
2995
  }
2940
2996
  await mkdir(MEDIA_DIR, { recursive: true });
@@ -2943,20 +2999,20 @@ async function downloadInboundMedia(msg, sock, opts) {
2943
2999
  const filePath = join2(MEDIA_DIR, filename);
2944
3000
  await writeFile(filePath, buffer);
2945
3001
  const sizeKB = (buffer.length / 1024).toFixed(0);
2946
- console.error(`${TAG8} media downloaded type=${mimetype ?? "unknown"} size=${sizeKB}KB path=${filePath}`);
3002
+ console.error(`${TAG9} media downloaded type=${mimetype ?? "unknown"} size=${sizeKB}KB path=${filePath}`);
2947
3003
  return {
2948
3004
  path: filePath,
2949
3005
  mimetype: mimetype ?? "application/octet-stream",
2950
3006
  size: buffer.length
2951
3007
  };
2952
3008
  } catch (err) {
2953
- console.error(`${TAG8} media download failed type=${mimetype ?? "unknown"} error=${String(err)}`);
3009
+ console.error(`${TAG9} media download failed type=${mimetype ?? "unknown"} error=${String(err)}`);
2954
3010
  return void 0;
2955
3011
  }
2956
3012
  }
2957
3013
 
2958
3014
  // app/lib/whatsapp/inbound/debounce.ts
2959
- var TAG9 = "[whatsapp:debounce]";
3015
+ var TAG10 = "[whatsapp:debounce]";
2960
3016
  var STT_TAG = "[whatsapp:stt-await]";
2961
3017
  function createInboundDebouncer(opts) {
2962
3018
  const { debounceMs, buildKey, onFlush, onError } = opts;
@@ -2987,7 +3043,7 @@ function createInboundDebouncer(opts) {
2987
3043
  pending.delete(key);
2988
3044
  const batchSize = batch.entries.length;
2989
3045
  try {
2990
- console.error(`${TAG9} debounce flush key=${key} batchSize=${batchSize}`);
3046
+ console.error(`${TAG10} debounce flush key=${key} batchSize=${batchSize}`);
2991
3047
  const result = onFlush(batch.entries);
2992
3048
  if (result && typeof result.catch === "function") {
2993
3049
  result.catch(onError);
@@ -3067,7 +3123,7 @@ function combineInboundBatch(entries) {
3067
3123
  }
3068
3124
 
3069
3125
  // app/lib/whatsapp/opening-hours.ts
3070
- var TAG10 = "[whatsapp:hours]";
3126
+ var TAG11 = "[whatsapp:hours]";
3071
3127
  async function isBusinessOpen(accountId) {
3072
3128
  try {
3073
3129
  const timezone = await resolveTimezone(accountId);
@@ -3085,7 +3141,7 @@ async function isBusinessOpen(accountId) {
3085
3141
  { accountId, dayOfWeek, previousDayOfWeek }
3086
3142
  );
3087
3143
  if (result.records.length === 0) {
3088
- console.error(`${TAG10} [${accountId}] business hours check: no opening hours configured, treating as open`);
3144
+ console.error(`${TAG11} [${accountId}] business hours check: no opening hours configured, treating as open`);
3089
3145
  return { open: true, reason: "no opening hours configured" };
3090
3146
  }
3091
3147
  const specs = result.records.filter((r) => r.get("opens") != null && r.get("closes") != null).map((r) => ({
@@ -3094,7 +3150,7 @@ async function isBusinessOpen(accountId) {
3094
3150
  closes: String(r.get("closes")).trim()
3095
3151
  }));
3096
3152
  if (specs.length === 0) {
3097
- console.error(`${TAG10} [${accountId}] business hours check: no opening hours configured, treating as open`);
3153
+ console.error(`${TAG11} [${accountId}] business hours check: no opening hours configured, treating as open`);
3098
3154
  return { open: true, reason: "no opening hours configured" };
3099
3155
  }
3100
3156
  for (const spec of specs) {
@@ -3104,7 +3160,7 @@ async function isBusinessOpen(accountId) {
3104
3160
  if (spec.opens > spec.closes && currentTime < spec.closes) {
3105
3161
  const hoursStr = `${spec.opens}-${spec.closes} (${spec.day})`;
3106
3162
  console.error(
3107
- `${TAG10} [${accountId}] business hours check: open (day=${dayOfWeek}, time=${currentTime}, hours=${hoursStr})`
3163
+ `${TAG11} [${accountId}] business hours check: open (day=${dayOfWeek}, time=${currentTime}, hours=${hoursStr})`
3108
3164
  );
3109
3165
  return {
3110
3166
  open: true,
@@ -3117,7 +3173,7 @@ async function isBusinessOpen(accountId) {
3117
3173
  } else if (isTimeInRange(currentTime, spec.opens, spec.closes)) {
3118
3174
  const hoursStr = `${spec.opens}-${spec.closes}`;
3119
3175
  console.error(
3120
- `${TAG10} [${accountId}] business hours check: open (day=${dayOfWeek}, time=${currentTime}, hours=${hoursStr})`
3176
+ `${TAG11} [${accountId}] business hours check: open (day=${dayOfWeek}, time=${currentTime}, hours=${hoursStr})`
3121
3177
  );
3122
3178
  return {
3123
3179
  open: true,
@@ -3131,7 +3187,7 @@ async function isBusinessOpen(accountId) {
3131
3187
  const todaySpecs = specs.filter((s) => s.day === dayOfWeek);
3132
3188
  const allHours = todaySpecs.map((s) => `${s.opens}-${s.closes}`).join(", ") || "none today";
3133
3189
  console.error(
3134
- `${TAG10} [${accountId}] business hours check: closed (day=${dayOfWeek}, time=${currentTime}, hours=${allHours})`
3190
+ `${TAG11} [${accountId}] business hours check: closed (day=${dayOfWeek}, time=${currentTime}, hours=${allHours})`
3135
3191
  );
3136
3192
  return {
3137
3193
  open: false,
@@ -3145,7 +3201,7 @@ async function isBusinessOpen(accountId) {
3145
3201
  }
3146
3202
  } catch (err) {
3147
3203
  console.error(
3148
- `${TAG10} [${accountId}] business hours check failed, treating as open: ${err instanceof Error ? err.message : String(err)}`
3204
+ `${TAG11} [${accountId}] business hours check failed, treating as open: ${err instanceof Error ? err.message : String(err)}`
3149
3205
  );
3150
3206
  return { open: true, reason: "hours check failed (treating as open)" };
3151
3207
  }
@@ -3191,7 +3247,7 @@ import { execFile } from "child_process";
3191
3247
  import { unlink, stat } from "fs/promises";
3192
3248
  import { promisify } from "util";
3193
3249
  var execFileAsync = promisify(execFile);
3194
- var TAG11 = "[stt]";
3250
+ var TAG12 = "[stt]";
3195
3251
  var WHISPER_BINARY = process.env.WHISPER_BINARY ?? "/opt/whisper.cpp/build/bin/whisper-cli";
3196
3252
  var WHISPER_MODEL = process.env.WHISPER_MODEL ?? "/opt/whisper.cpp/models/ggml-base.bin";
3197
3253
  var WHISPER_TIMEOUT_MS = 20 * 60 * 1e3;
@@ -3202,11 +3258,11 @@ async function transcribe(audioPath, mimetype) {
3202
3258
  const s = await stat(audioPath);
3203
3259
  audioBytes = s.size;
3204
3260
  } catch {
3205
- console.error(`${TAG11} failed: file not readable path=${audioPath}`);
3261
+ console.error(`${TAG12} failed: file not readable path=${audioPath}`);
3206
3262
  return void 0;
3207
3263
  }
3208
3264
  console.error(
3209
- `${TAG11} start provider=whisper-local audio_bytes=${audioBytes} mimetype=${mimetype} path=${audioPath}`
3265
+ `${TAG12} start provider=whisper-local audio_bytes=${audioBytes} mimetype=${mimetype} path=${audioPath}`
3210
3266
  );
3211
3267
  const wavPath = audioPath.replace(/\.[^.]+$/, "") + ".wav";
3212
3268
  try {
@@ -3225,7 +3281,7 @@ async function transcribe(audioPath, mimetype) {
3225
3281
  ], { timeout: 3e4 });
3226
3282
  } catch (err) {
3227
3283
  const reason = err instanceof Error ? err.message : String(err);
3228
- console.error(`${TAG11} failed: ffmpeg conversion error=${reason}`);
3284
+ console.error(`${TAG12} failed: ffmpeg conversion error=${reason}`);
3229
3285
  return void 0;
3230
3286
  }
3231
3287
  try {
@@ -3243,20 +3299,20 @@ async function transcribe(audioPath, mimetype) {
3243
3299
  const text = stdout.trim();
3244
3300
  const durationMs = Date.now() - startMs;
3245
3301
  if (!text) {
3246
- console.error(`${TAG11} failed: whisper returned empty output duration_ms=${durationMs}`);
3302
+ console.error(`${TAG12} failed: whisper returned empty output duration_ms=${durationMs}`);
3247
3303
  return void 0;
3248
3304
  }
3249
3305
  const langMatch = stderr.match(/auto-detected language:\s*(\w+)/);
3250
3306
  const language = langMatch?.[1] ?? "unknown";
3251
3307
  const words = text.split(/\s+/).filter(Boolean).length;
3252
3308
  console.error(
3253
- `${TAG11} done provider=whisper-local duration_ms=${durationMs} words=${words} lang=${language}`
3309
+ `${TAG12} done provider=whisper-local duration_ms=${durationMs} words=${words} lang=${language}`
3254
3310
  );
3255
3311
  return { text, language, durationMs };
3256
3312
  } catch (err) {
3257
3313
  const durationMs = Date.now() - startMs;
3258
3314
  const reason = err instanceof Error ? err.message : String(err);
3259
- console.error(`${TAG11} failed provider=whisper-local duration_ms=${durationMs} error=${reason}`);
3315
+ console.error(`${TAG12} failed provider=whisper-local duration_ms=${durationMs} error=${reason}`);
3260
3316
  return void 0;
3261
3317
  } finally {
3262
3318
  unlink(wavPath).catch(() => {
@@ -3275,7 +3331,7 @@ var WaReplyError = class extends Error {
3275
3331
  };
3276
3332
 
3277
3333
  // app/lib/whatsapp/manager.ts
3278
- var TAG12 = "[whatsapp:manager]";
3334
+ var TAG13 = "[whatsapp:manager]";
3279
3335
  var MAX_RECONNECT_ATTEMPTS = 10;
3280
3336
  var MESSAGE_STORE_MAX = 500;
3281
3337
  var connections = /* @__PURE__ */ new Map();
@@ -3294,7 +3350,7 @@ function storeMessage(storeKey, entry) {
3294
3350
  if (entries.length > MESSAGE_STORE_MAX) {
3295
3351
  const trimmed = entries.length - MESSAGE_STORE_MAX;
3296
3352
  entries.splice(0, trimmed);
3297
- console.error(`${TAG12} message store trimmed for ${storeKey}: ${trimmed} oldest messages removed`);
3353
+ console.error(`${TAG13} message store trimmed for ${storeKey}: ${trimmed} oldest messages removed`);
3298
3354
  }
3299
3355
  }
3300
3356
  function deriveCacheKey(input) {
@@ -3311,9 +3367,28 @@ function deriveCacheKey(input) {
3311
3367
  }
3312
3368
  return `whatsapp:${input.accountId}:${input.senderPhone}`;
3313
3369
  }
3370
+ function buildSelfChatDispatch(input) {
3371
+ const senderPhone = input.selfPhone ?? "owner";
3372
+ return {
3373
+ accountId: input.accountId,
3374
+ agentType: "admin",
3375
+ senderPhone,
3376
+ text: input.text,
3377
+ isGroup: false,
3378
+ reply: input.reply,
3379
+ composing: input.composing,
3380
+ cacheKey: deriveCacheKey({
3381
+ agentType: "admin",
3382
+ accountId: input.accountId,
3383
+ senderPhone,
3384
+ isGroup: false
3385
+ }),
3386
+ media: []
3387
+ };
3388
+ }
3314
3389
  async function init(opts) {
3315
3390
  if (initialized) {
3316
- console.error(`${TAG12} already initialized`);
3391
+ console.error(`${TAG13} already initialized`);
3317
3392
  return;
3318
3393
  }
3319
3394
  configDir = opts.configDir;
@@ -3321,20 +3396,20 @@ async function init(opts) {
3321
3396
  loadConfig(opts.accountConfig);
3322
3397
  const accountIds = listCredentialAccountIds(configDir);
3323
3398
  if (accountIds.length === 0) {
3324
- console.error(`${TAG12} init: no stored WhatsApp credentials found`);
3399
+ console.error(`${TAG13} init: no stored WhatsApp credentials found`);
3325
3400
  initialized = true;
3326
3401
  return;
3327
3402
  }
3328
- console.error(`${TAG12} init: found ${accountIds.length} credentialed account(s): ${accountIds.join(", ")}`);
3403
+ console.error(`${TAG13} init: found ${accountIds.length} credentialed account(s): ${accountIds.join(", ")}`);
3329
3404
  initialized = true;
3330
3405
  for (const accountId of accountIds) {
3331
3406
  const accountCfg = whatsAppConfig.accounts?.[accountId];
3332
3407
  if (accountCfg?.enabled === false) {
3333
- console.error(`${TAG12} skipping disabled account=${accountId}`);
3408
+ console.error(`${TAG13} skipping disabled account=${accountId}`);
3334
3409
  continue;
3335
3410
  }
3336
3411
  startConnection(accountId).catch((err) => {
3337
- console.error(`${TAG12} failed to auto-start account=${accountId}: ${formatError(err)}`);
3412
+ console.error(`${TAG13} failed to auto-start account=${accountId}: ${formatError(err)}`);
3338
3413
  });
3339
3414
  }
3340
3415
  }
@@ -3343,7 +3418,7 @@ async function startConnection(accountId) {
3343
3418
  const authDir = resolveAuthDir(configDir, accountId);
3344
3419
  const hasAuth = await authExists(authDir);
3345
3420
  if (!hasAuth) {
3346
- console.error(`${TAG12} no credentials for account=${accountId}`);
3421
+ console.error(`${TAG13} no credentials for account=${accountId}`);
3347
3422
  return;
3348
3423
  }
3349
3424
  let platformAccountId;
@@ -3393,11 +3468,11 @@ async function stopConnection(accountId) {
3393
3468
  conn.sock.ev.removeAllListeners("creds.update");
3394
3469
  conn.sock.ws?.close?.();
3395
3470
  } catch (err) {
3396
- console.warn(`${TAG12} socket cleanup error during stop account=${accountId}: ${String(err)}`);
3471
+ console.warn(`${TAG13} socket cleanup error during stop account=${accountId}: ${String(err)}`);
3397
3472
  }
3398
3473
  }
3399
3474
  connections.delete(accountId);
3400
- console.error(`${TAG12} stopped account=${accountId}`);
3475
+ console.error(`${TAG13} stopped account=${accountId}`);
3401
3476
  }
3402
3477
  function getStatus() {
3403
3478
  return Array.from(connections.values()).map((conn) => ({
@@ -3459,9 +3534,9 @@ async function registerLoginSocket(accountId, sock, authDir) {
3459
3534
  connections.set(accountId, conn);
3460
3535
  try {
3461
3536
  await sock.sendPresenceUpdate("available");
3462
- console.error(`${TAG12} presence set to available account=${accountId}`);
3537
+ console.error(`${TAG13} presence set to available account=${accountId}`);
3463
3538
  } catch (err) {
3464
- console.error(`${TAG12} presence update failed account=${accountId}: ${String(err)}`);
3539
+ console.error(`${TAG13} presence update failed account=${accountId}: ${String(err)}`);
3465
3540
  }
3466
3541
  await runInitQueries(sock, {
3467
3542
  accountId,
@@ -3473,7 +3548,7 @@ async function registerLoginSocket(accountId, sock, authDir) {
3473
3548
  monitorInbound(conn);
3474
3549
  watchForDisconnect(conn);
3475
3550
  attachSelfIdRefreshOnCredsUpdate(conn);
3476
- console.error(`${TAG12} registered login socket for account=${accountId} phone=${selfId.e164 ?? "unknown"}`);
3551
+ console.error(`${TAG13} registered login socket for account=${accountId} phone=${selfId.e164 ?? "unknown"}`);
3477
3552
  }
3478
3553
  function attachSelfIdRefreshOnCredsUpdate(conn) {
3479
3554
  if (!conn.sock) return;
@@ -3524,7 +3599,7 @@ async function shutdown() {
3524
3599
  const ids = Array.from(connections.keys());
3525
3600
  await Promise.all(ids.map((id) => stopConnection(id)));
3526
3601
  initialized = false;
3527
- console.error(`${TAG12} shutdown complete`);
3602
+ console.error(`${TAG13} shutdown complete`);
3528
3603
  }
3529
3604
  function loadConfig(accountConfig) {
3530
3605
  try {
@@ -3535,19 +3610,19 @@ function loadConfig(accountConfig) {
3535
3610
  if (result.ok) {
3536
3611
  whatsAppConfig = result.data;
3537
3612
  if (result.droppedKeys.length > 0) {
3538
- console.error(`${TAG12} config: dropped unknown keys=[${result.droppedKeys.join(",")}] preserved known config`);
3613
+ console.error(`${TAG13} config: dropped unknown keys=[${result.droppedKeys.join(",")}] preserved known config`);
3539
3614
  }
3540
3615
  } else {
3541
- console.error(`${TAG12} config validation failed: ${result.error.message}`);
3616
+ console.error(`${TAG13} config validation failed: ${result.error.message}`);
3542
3617
  whatsAppConfig = {};
3543
3618
  }
3544
3619
  const diskAdmin = Array.isArray(wa.adminPhones) ? wa.adminPhones : [];
3545
3620
  if (diskAdmin.length > 0 && (whatsAppConfig.adminPhones?.length ?? 0) === 0) {
3546
- console.error(`${TAG12} WARN adminPhones present on disk but empty after load`);
3621
+ console.error(`${TAG13} WARN adminPhones present on disk but empty after load`);
3547
3622
  }
3548
3623
  }
3549
3624
  } catch (err) {
3550
- console.error(`${TAG12} config load error: ${String(err)}`);
3625
+ console.error(`${TAG13} config load error: ${String(err)}`);
3551
3626
  whatsAppConfig = {};
3552
3627
  }
3553
3628
  }
@@ -3558,13 +3633,13 @@ async function connectWithReconnect(conn) {
3558
3633
  let cycleError = null;
3559
3634
  let connectedAt;
3560
3635
  try {
3561
- console.error(`${TAG12} connecting account=${conn.accountId} attempt=${conn.reconnectAttempts}`);
3636
+ console.error(`${TAG13} connecting account=${conn.accountId} attempt=${conn.reconnectAttempts}`);
3562
3637
  const sock = await createWaSocket({
3563
3638
  authDir: conn.authDir,
3564
3639
  silent: true
3565
3640
  });
3566
3641
  conn.sock = sock;
3567
- console.error(`${TAG12} socket created account=${conn.accountId} \u2014 waiting for connection`);
3642
+ console.error(`${TAG13} socket created account=${conn.accountId} \u2014 waiting for connection`);
3568
3643
  await waitForConnection(sock);
3569
3644
  const selfId = readSelfId(conn.authDir);
3570
3645
  connectedAt = Date.now();
@@ -3582,12 +3657,12 @@ async function connectWithReconnect(conn) {
3582
3657
  } else {
3583
3658
  console.error(`[whatsapp:lid] FAIL self-lid-resolution accountId=${conn.accountId} reason=creds-missing-lid`);
3584
3659
  }
3585
- console.error(`${TAG12} connected account=${conn.accountId} phone=${selfId.e164 ?? "unknown"}`);
3660
+ console.error(`${TAG13} connected account=${conn.accountId} phone=${selfId.e164 ?? "unknown"}`);
3586
3661
  try {
3587
3662
  await sock.sendPresenceUpdate("available");
3588
- console.error(`${TAG12} presence set to available account=${conn.accountId}`);
3663
+ console.error(`${TAG13} presence set to available account=${conn.accountId}`);
3589
3664
  } catch (err) {
3590
- console.error(`${TAG12} presence update failed account=${conn.accountId}: ${String(err)}`);
3665
+ console.error(`${TAG13} presence update failed account=${conn.accountId}: ${String(err)}`);
3591
3666
  }
3592
3667
  await runInitQueries(sock, {
3593
3668
  accountId: conn.accountId,
@@ -3615,10 +3690,10 @@ async function connectWithReconnect(conn) {
3615
3690
  }
3616
3691
  const classification = classifyDisconnect(err);
3617
3692
  conn.lastError = classification.message;
3618
- console.error(`${TAG12} disconnect account=${conn.accountId}: ${classification.kind} \u2014 ${classification.message}`);
3693
+ console.error(`${TAG13} disconnect account=${conn.accountId}: ${classification.kind} \u2014 ${classification.message}`);
3619
3694
  if (!classification.shouldRetry) {
3620
3695
  conn.terminalReason = classification.message;
3621
- console.error(`${TAG12} terminal disconnect for account=${conn.accountId}, stopping reconnection`);
3696
+ console.error(`${TAG13} terminal disconnect for account=${conn.accountId}, stopping reconnection`);
3622
3697
  return;
3623
3698
  }
3624
3699
  }
@@ -3631,7 +3706,7 @@ async function connectWithReconnect(conn) {
3631
3706
  if (decision.action === "abort") {
3632
3707
  const stuckReason = `GIVING UP account=${conn.accountId} attempts=${decision.finalAttempts}/${maxAttempts} uptimeMsLast=${uptimeMs} stableThresholdMs=${STABLE_UPTIME_MS} lastError=${conn.lastError ?? "(none)"}`;
3633
3708
  console.error(
3634
- `${TAG12} ${stuckReason} \u2014 re-pair via QR or restart required; WhatsApp will not reconnect automatically`
3709
+ `${TAG13} ${stuckReason} \u2014 re-pair via QR or restart required; WhatsApp will not reconnect automatically`
3635
3710
  );
3636
3711
  conn.sessionStuckReason = stuckReason;
3637
3712
  conn.lastError = stuckReason;
@@ -3640,17 +3715,17 @@ async function connectWithReconnect(conn) {
3640
3715
  conn.reconnectAttempts = decision.nextAttempts;
3641
3716
  if (decision.reason === "short-lived") {
3642
3717
  console.error(
3643
- `${TAG12} short-lived session account=${conn.accountId} uptimeMs=${uptimeMs} attempt=${decision.nextAttempts}/${maxAttempts} lastError=${conn.lastError ?? "(clean disconnect)"}`
3718
+ `${TAG13} short-lived session account=${conn.accountId} uptimeMs=${uptimeMs} attempt=${decision.nextAttempts}/${maxAttempts} lastError=${conn.lastError ?? "(clean disconnect)"}`
3644
3719
  );
3645
3720
  } else {
3646
3721
  console.error(
3647
- `${TAG12} session stable account=${conn.accountId} uptimeMs=${uptimeMs} \u2014 reconnect counter reset`
3722
+ `${TAG13} session stable account=${conn.accountId} uptimeMs=${uptimeMs} \u2014 reconnect counter reset`
3648
3723
  );
3649
3724
  }
3650
3725
  if (decision.reason === "short-lived" || cycleError) {
3651
3726
  const delay = computeBackoff(Math.max(1, decision.nextAttempts));
3652
3727
  console.error(
3653
- `${TAG12} reconnecting account=${conn.accountId} in ${delay}ms (attempt ${decision.nextAttempts}/${maxAttempts})`
3728
+ `${TAG13} reconnecting account=${conn.accountId} in ${delay}ms (attempt ${decision.nextAttempts}/${maxAttempts})`
3654
3729
  );
3655
3730
  await new Promise((resolve28) => {
3656
3731
  const timer2 = setTimeout(resolve28, delay);
@@ -3684,11 +3759,11 @@ function watchForDisconnect(conn) {
3684
3759
  conn.sock.ev.on("connection.update", (update) => {
3685
3760
  if (update.connection === "close") {
3686
3761
  if (connections.get(conn.accountId) !== conn) return;
3687
- console.error(`${TAG12} socket disconnected for account=${conn.accountId}`);
3762
+ console.error(`${TAG13} socket disconnected for account=${conn.accountId}`);
3688
3763
  conn.connected = false;
3689
3764
  conn.sock = null;
3690
3765
  connectWithReconnect(conn).catch((err) => {
3691
- console.error(`${TAG12} reconnection failed for account=${conn.accountId}: ${formatError(err)}`);
3766
+ console.error(`${TAG13} reconnection failed for account=${conn.accountId}: ${formatError(err)}`);
3692
3767
  });
3693
3768
  }
3694
3769
  });
@@ -3697,7 +3772,7 @@ function monitorInbound(conn) {
3697
3772
  if (!conn.sock || !onInboundMessage) return;
3698
3773
  const sock = conn.sock;
3699
3774
  const debounceMs = whatsAppConfig.accounts?.[conn.accountId]?.debounceMs ?? whatsAppConfig.debounceMs ?? 0;
3700
- console.error(`${TAG12} monitorInbound started account=${conn.accountId} debounceMs=${debounceMs}`);
3775
+ console.error(`${TAG13} monitorInbound started account=${conn.accountId} debounceMs=${debounceMs}`);
3701
3776
  conn.debouncer = createInboundDebouncer({
3702
3777
  debounceMs,
3703
3778
  buildKey: (payload) => {
@@ -3707,12 +3782,12 @@ function monitorInbound(conn) {
3707
3782
  onFlush: (entries) => {
3708
3783
  if (!onInboundMessage) return;
3709
3784
  if (entries.length > 1) {
3710
- console.error(`${TAG12} debounce: combining ${entries.length} messages account=${conn.accountId} from=${entries[0].senderPhone}`);
3785
+ console.error(`${TAG13} debounce: combining ${entries.length} messages account=${conn.accountId} from=${entries[0].senderPhone}`);
3711
3786
  }
3712
3787
  onInboundMessage(combineInboundBatch(entries));
3713
3788
  },
3714
3789
  onError: (err) => {
3715
- console.error(`${TAG12} debounce flush error account=${conn.accountId}: ${String(err)}`);
3790
+ console.error(`${TAG13} debounce flush error account=${conn.accountId}: ${String(err)}`);
3716
3791
  }
3717
3792
  });
3718
3793
  sock.ev.on("messages.upsert", async (upsert) => {
@@ -3740,7 +3815,7 @@ function monitorInbound(conn) {
3740
3815
  });
3741
3816
  const entries = messageStore.get(storeKey);
3742
3817
  console.error(
3743
- `${TAG12} stored message ${msg.key.id ?? "?"} for ${remoteJid} (type: ${upsert.type}, store size: ${entries?.length ?? 0}/${MESSAGE_STORE_MAX}) account=${conn.accountId}`
3818
+ `${TAG13} stored message ${msg.key.id ?? "?"} for ${remoteJid} (type: ${upsert.type}, store size: ${entries?.length ?? 0}/${MESSAGE_STORE_MAX}) account=${conn.accountId}`
3744
3819
  );
3745
3820
  recordActivity({
3746
3821
  accountId: conn.accountId,
@@ -3803,14 +3878,46 @@ function monitorInbound(conn) {
3803
3878
  isGroupJid(remoteJid) ? msg.key.participant ?? void 0 : void 0
3804
3879
  );
3805
3880
  }
3881
+ const decision = evaluateSelfChatCommand({
3882
+ accountId: conn.accountId,
3883
+ remoteJid,
3884
+ fromMe: Boolean(msg.key.fromMe),
3885
+ msgId: msg.key.id ?? null,
3886
+ messageTimestampSec: msg.messageTimestamp ? Number(msg.messageTimestamp) : null,
3887
+ lastConnectedAtMs: conn.lastConnectedAt ?? null,
3888
+ selfJid: conn.selfJid,
3889
+ selfLid: conn.selfLid,
3890
+ selfPhone: conn.selfPhone,
3891
+ text: extracted.text || null
3892
+ });
3893
+ if (decision.kind === "dispatch" && onInboundMessage) {
3894
+ const reply = async (text) => {
3895
+ const currentSock = conn.sock;
3896
+ if (!currentSock) throw new Error("WhatsApp disconnected \u2014 cannot reply");
3897
+ await sendTextMessage(currentSock, remoteJid, text, { accountId: conn.accountId });
3898
+ };
3899
+ const composing = async () => {
3900
+ const currentSock = conn.sock;
3901
+ if (currentSock) await sendComposing(currentSock, remoteJid);
3902
+ };
3903
+ onInboundMessage(buildSelfChatDispatch({
3904
+ accountId: conn.accountId,
3905
+ selfPhone: conn.selfPhone,
3906
+ remoteJid,
3907
+ text: decision.text,
3908
+ reply,
3909
+ composing
3910
+ }));
3911
+ continue;
3912
+ }
3806
3913
  console.error(
3807
- `${TAG12} append-type message ${msg.key.id ?? "?"} stored, dispatch skipped account=${conn.accountId}`
3914
+ `${TAG13} append-type message ${msg.key.id ?? "?"} stored, dispatch skipped account=${conn.accountId}`
3808
3915
  );
3809
3916
  continue;
3810
3917
  }
3811
3918
  await handleInboundMessage(conn, msg);
3812
3919
  } catch (err) {
3813
- console.error(`${TAG12} inbound handler error account=${conn.accountId}: ${String(err)}`);
3920
+ console.error(`${TAG13} inbound handler error account=${conn.accountId}: ${String(err)}`);
3814
3921
  }
3815
3922
  }
3816
3923
  });
@@ -3820,31 +3927,31 @@ async function handleInboundMessage(conn, msg) {
3820
3927
  const remoteJid = msg.key.remoteJid;
3821
3928
  if (!remoteJid) return;
3822
3929
  if (remoteJid === "status@broadcast") {
3823
- console.error(`${TAG12} drop: status broadcast account=${conn.accountId}`);
3930
+ console.error(`${TAG13} drop: status broadcast account=${conn.accountId}`);
3824
3931
  return;
3825
3932
  }
3826
3933
  if (!msg.message) {
3827
- console.error(`${TAG12} drop: empty message account=${conn.accountId} from=${remoteJid}`);
3934
+ console.error(`${TAG13} drop: empty message account=${conn.accountId} from=${remoteJid}`);
3828
3935
  return;
3829
3936
  }
3830
3937
  const dedupKey = `${conn.accountId}:${remoteJid}:${msg.key.id}`;
3831
3938
  if (isDuplicateInbound(dedupKey)) {
3832
- console.error(`${TAG12} drop: duplicate account=${conn.accountId} key=${dedupKey}`);
3939
+ console.error(`${TAG13} drop: duplicate account=${conn.accountId} key=${dedupKey}`);
3833
3940
  return;
3834
3941
  }
3835
3942
  if (msg.key.fromMe) {
3836
3943
  if (msg.key.id && isAgentSentMessage(msg.key.id)) {
3837
- console.error(`${TAG12} drop: echo suppression account=${conn.accountId} msgId=${msg.key.id}`);
3944
+ console.error(`${TAG13} drop: echo suppression account=${conn.accountId} msgId=${msg.key.id}`);
3838
3945
  return;
3839
3946
  }
3840
3947
  const extracted2 = extractMessage(msg);
3841
3948
  if (!extracted2.text) {
3842
- console.error(`${TAG12} owner reply skipped \u2014 no text content account=${conn.accountId}`);
3949
+ console.error(`${TAG13} owner reply skipped \u2014 no text content account=${conn.accountId}`);
3843
3950
  return;
3844
3951
  }
3845
3952
  const isGroup2 = isGroupJid(remoteJid);
3846
3953
  const senderPhone2 = conn.selfPhone ?? "owner";
3847
- console.error(`${TAG12} owner reply mirrored to session from=${senderPhone2} account=${conn.accountId}`);
3954
+ console.error(`${TAG13} owner reply mirrored to session from=${senderPhone2} account=${conn.accountId}`);
3848
3955
  const reply2 = async (text) => {
3849
3956
  const currentSock = conn.sock;
3850
3957
  if (!currentSock) throw new Error("WhatsApp disconnected \u2014 cannot reply");
@@ -3878,7 +3985,7 @@ async function handleInboundMessage(conn, msg) {
3878
3985
  }
3879
3986
  const extracted = extractMessage(msg);
3880
3987
  if (!extracted.text && !extracted.mediaType) {
3881
- console.error(`${TAG12} drop: no text or media account=${conn.accountId} from=${remoteJid}`);
3988
+ console.error(`${TAG13} drop: no text or media account=${conn.accountId} from=${remoteJid}`);
3882
3989
  return;
3883
3990
  }
3884
3991
  let mediaResult;
@@ -3888,7 +3995,7 @@ async function handleInboundMessage(conn, msg) {
3888
3995
  maxBytes: maxMb * 1024 * 1024
3889
3996
  });
3890
3997
  if (!mediaResult) {
3891
- console.error(`${TAG12} media download returned undefined account=${conn.accountId} type=${extracted.mediaType} from=${remoteJid}`);
3998
+ console.error(`${TAG13} media download returned undefined account=${conn.accountId} type=${extracted.mediaType} from=${remoteJid}`);
3892
3999
  }
3893
4000
  }
3894
4001
  const isGroup = isGroupJid(remoteJid);
@@ -3926,7 +4033,7 @@ async function handleInboundMessage(conn, msg) {
3926
4033
  });
3927
4034
  }
3928
4035
  console.error(
3929
- `${TAG12} inbound account=${conn.accountId} from=${senderPhone} group=${isGroup} access=${accessResult.allowed ? "allowed" : "blocked"}(${accessResult.reason}) agent=${accessResult.agentType}` + (extracted.mediaType ? ` media=${extracted.mediaType}` : "") + (mediaResult ? ` mediaPath=${mediaResult.path}` : "") + (extracted.quotedMessage ? ` replyTo=${extracted.quotedMessage.id}` : "")
4036
+ `${TAG13} inbound account=${conn.accountId} from=${senderPhone} group=${isGroup} access=${accessResult.allowed ? "allowed" : "blocked"}(${accessResult.reason}) agent=${accessResult.agentType}` + (extracted.mediaType ? ` media=${extracted.mediaType}` : "") + (mediaResult ? ` mediaPath=${mediaResult.path}` : "") + (extracted.quotedMessage ? ` replyTo=${extracted.quotedMessage.id}` : "")
3930
4037
  );
3931
4038
  if (!accessResult.allowed) return;
3932
4039
  const sendReceipts = whatsAppConfig.accounts?.[conn.accountId]?.sendReadReceipts ?? whatsAppConfig.sendReadReceipts ?? true;
@@ -3935,7 +4042,7 @@ async function handleInboundMessage(conn, msg) {
3935
4042
  }
3936
4043
  const reply = async (text) => {
3937
4044
  if (isGroupJid(remoteJid)) {
3938
- console.error(`${TAG12} op=group-reply-blocked jid=${remoteJid}`);
4045
+ console.error(`${TAG13} op=group-reply-blocked jid=${remoteJid}`);
3939
4046
  throw new WaReplyError("group reply blocked \u2014 observe-only", false);
3940
4047
  }
3941
4048
  const currentSock = conn.sock;
@@ -3973,15 +4080,15 @@ async function handleInboundMessage(conn, msg) {
3973
4080
  if (accessResult.agentType === "public") {
3974
4081
  const hoursResult = await isBusinessOpen(conn.accountId);
3975
4082
  if (!hoursResult.open) {
3976
- console.error(`${TAG12} [${conn.accountId}] dispatch skipped: business closed`);
4083
+ console.error(`${TAG13} [${conn.accountId}] dispatch skipped: business closed`);
3977
4084
  const afterHoursMessage = whatsAppConfig.accounts?.[conn.accountId]?.afterHoursMessage ?? whatsAppConfig.afterHoursMessage;
3978
4085
  if (afterHoursMessage) {
3979
4086
  try {
3980
4087
  await reply(afterHoursMessage);
3981
- console.error(`${TAG12} [${conn.accountId}] after-hours auto-reply sent to ${remoteJid}`);
4088
+ console.error(`${TAG13} [${conn.accountId}] after-hours auto-reply sent to ${remoteJid}`);
3982
4089
  } catch (err) {
3983
4090
  console.error(
3984
- `${TAG12} [${conn.accountId}] after-hours auto-reply failed: ${err instanceof Error ? err.message : String(err)}`
4091
+ `${TAG13} [${conn.accountId}] after-hours auto-reply failed: ${err instanceof Error ? err.message : String(err)}`
3985
4092
  );
3986
4093
  }
3987
4094
  }
@@ -4495,7 +4602,7 @@ async function storeComponentArtefact(accountId, attachmentId, mimeType, content
4495
4602
  import { writeFile as writeFile3, mkdtemp, rm } from "fs/promises";
4496
4603
  import { tmpdir } from "os";
4497
4604
  import { join as join3 } from "path";
4498
- var TAG13 = "[voice]";
4605
+ var TAG14 = "[voice]";
4499
4606
  var AUDIO_MIME_TYPES = /* @__PURE__ */ new Set([
4500
4607
  "audio/ogg",
4501
4608
  "audio/opus",
@@ -4533,7 +4640,7 @@ async function transcribeVoiceNote(file, source) {
4533
4640
  const bytes = file.size;
4534
4641
  const mimeType = file.type;
4535
4642
  console.error(
4536
- `${TAG13} recording send source=${source} duration_ms=unknown bytes=${bytes} format=${mimeType}`
4643
+ `${TAG14} recording send source=${source} duration_ms=unknown bytes=${bytes} format=${mimeType}`
4537
4644
  );
4538
4645
  let tempDir;
4539
4646
  let tempPath;
@@ -4545,7 +4652,7 @@ async function transcribeVoiceNote(file, source) {
4545
4652
  await writeFile3(tempPath, buffer);
4546
4653
  } catch (err) {
4547
4654
  const reason = err instanceof Error ? err.message : String(err);
4548
- console.error(`${TAG13} failed source=${source} error=temp-file-write: ${reason}`);
4655
+ console.error(`${TAG14} failed source=${source} error=temp-file-write: ${reason}`);
4549
4656
  return { ok: false, error: "Could not process voice note" };
4550
4657
  }
4551
4658
  try {
@@ -4553,7 +4660,7 @@ async function transcribeVoiceNote(file, source) {
4553
4660
  if (!sttResult) {
4554
4661
  const elapsed2 = Date.now() - startMs;
4555
4662
  console.error(
4556
- `${TAG13} failed source=${source} error=transcription-failed duration_ms=${elapsed2}`
4663
+ `${TAG14} failed source=${source} error=transcription-failed duration_ms=${elapsed2}`
4557
4664
  );
4558
4665
  return { ok: false, error: "Could not transcribe voice note. Please try again or type your message." };
4559
4666
  }
@@ -4561,7 +4668,7 @@ async function transcribeVoiceNote(file, source) {
4561
4668
  const elapsed = Date.now() - startMs;
4562
4669
  const words = rawText.split(/\s+/).filter(Boolean).length;
4563
4670
  console.error(
4564
- `${TAG13} transcribed source=${source} duration_ms=${elapsed} stt_ms=${sttResult.durationMs} words=${words}`
4671
+ `${TAG14} transcribed source=${source} duration_ms=${elapsed} stt_ms=${sttResult.durationMs} words=${words}`
4565
4672
  );
4566
4673
  return {
4567
4674
  ok: true,
@@ -4571,7 +4678,7 @@ async function transcribeVoiceNote(file, source) {
4571
4678
  const elapsed = Date.now() - startMs;
4572
4679
  const reason = err instanceof Error ? err.message : String(err);
4573
4680
  console.error(
4574
- `${TAG13} failed source=${source} error=${reason} duration_ms=${elapsed}`
4681
+ `${TAG14} failed source=${source} error=${reason} duration_ms=${elapsed}`
4575
4682
  );
4576
4683
  return { ok: false, error: "Voice note transcription failed. Please try again or type your message." };
4577
4684
  } finally {
@@ -4640,6 +4747,7 @@ async function managerRcSpawn(opts) {
4640
4747
  personId: opts.personId,
4641
4748
  userId: opts.userId,
4642
4749
  waChannel: opts.waChannel,
4750
+ webchatChannel: opts.webchatChannel,
4643
4751
  role: opts.role,
4644
4752
  channel: opts.channel,
4645
4753
  senderId: opts.senderId
@@ -4711,7 +4819,7 @@ function adminSessionIdFor(accountId, senderId) {
4711
4819
 
4712
4820
  // app/lib/channel-pty-bridge/public-session-end-review.ts
4713
4821
  import { randomUUID as randomUUID4 } from "crypto";
4714
- var TAG14 = "[public-session-review]";
4822
+ var TAG15 = "[public-session-review]";
4715
4823
  var CONSUMED_CAP = 500;
4716
4824
  var consumed = /* @__PURE__ */ new Set();
4717
4825
  function consumeOnce(sessionId) {
@@ -4738,7 +4846,7 @@ async function fetchJsonl(sessionId) {
4738
4846
  return await res.text();
4739
4847
  } catch (err) {
4740
4848
  console.error(
4741
- `${TAG14} jsonl-fetch-failed sessionId=${sessionId} err="${err instanceof Error ? err.message : String(err)}"`
4849
+ `${TAG15} jsonl-fetch-failed sessionId=${sessionId} err="${err instanceof Error ? err.message : String(err)}"`
4742
4850
  );
4743
4851
  return null;
4744
4852
  }
@@ -4762,7 +4870,7 @@ async function fetchPriorWrites(sliceToken, accountId) {
4762
4870
  const res = await fetch(url);
4763
4871
  if (!res.ok) {
4764
4872
  console.error(
4765
- `${TAG14} prior-writes-fetch-failed sliceToken=${sliceToken.slice(0, 8)} status=${res.status}`
4873
+ `${TAG15} prior-writes-fetch-failed sliceToken=${sliceToken.slice(0, 8)} status=${res.status}`
4766
4874
  );
4767
4875
  return [];
4768
4876
  }
@@ -4770,7 +4878,7 @@ async function fetchPriorWrites(sliceToken, accountId) {
4770
4878
  return Array.isArray(body.writes) ? body.writes : [];
4771
4879
  } catch (err) {
4772
4880
  console.error(
4773
- `${TAG14} prior-writes-fetch-threw sliceToken=${sliceToken.slice(0, 8)} err="${err instanceof Error ? err.message : String(err)}"`
4881
+ `${TAG15} prior-writes-fetch-threw sliceToken=${sliceToken.slice(0, 8)} err="${err instanceof Error ? err.message : String(err)}"`
4774
4882
  );
4775
4883
  return [];
4776
4884
  }
@@ -4799,7 +4907,7 @@ function composeInitialMessage(input) {
4799
4907
  }
4800
4908
  async function dispatchReviewer(input, initialMessage) {
4801
4909
  const sessionId = randomUUID4();
4802
- console.log(`${TAG14} route target=rc-spawn sessionId=${sessionId.slice(0, 8)} sourceSession=${input.sessionId.slice(0, 8)}`);
4910
+ console.log(`${TAG15} route target=rc-spawn sessionId=${sessionId.slice(0, 8)} sourceSession=${input.sessionId.slice(0, 8)}`);
4803
4911
  const spawned = await managerRcSpawn({
4804
4912
  sessionId,
4805
4913
  initialMessage,
@@ -4819,21 +4927,21 @@ async function firePublicSessionEndReview(input) {
4819
4927
  const dispatchedAt = Date.now();
4820
4928
  if (consumed.has(input.sessionId)) {
4821
4929
  console.log(
4822
- `${TAG14} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-already-reviewed`
4930
+ `${TAG15} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-already-reviewed`
4823
4931
  );
4824
4932
  return;
4825
4933
  }
4826
4934
  const jsonl = await fetchJsonl(input.sessionId);
4827
4935
  if (!jsonl) {
4828
4936
  console.log(
4829
- `${TAG14} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-jsonl-missing`
4937
+ `${TAG15} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-jsonl-missing`
4830
4938
  );
4831
4939
  return;
4832
4940
  }
4833
4941
  const operatorTurns = countOperatorTurns(jsonl);
4834
4942
  if (operatorTurns === 0) {
4835
4943
  console.log(
4836
- `${TAG14} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-no-turns`
4944
+ `${TAG15} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-no-turns`
4837
4945
  );
4838
4946
  return;
4839
4947
  }
@@ -4847,19 +4955,19 @@ async function firePublicSessionEndReview(input) {
4847
4955
  });
4848
4956
  if (!consumeOnce(input.sessionId)) {
4849
4957
  console.log(
4850
- `${TAG14} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-already-reviewed`
4958
+ `${TAG15} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-already-reviewed`
4851
4959
  );
4852
4960
  return;
4853
4961
  }
4854
4962
  const dispatched = await dispatchReviewer(input, initialMessage);
4855
4963
  if (!dispatched.ok) {
4856
4964
  console.error(
4857
- `${TAG14} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-spawn-failed reason=${dispatched.reason}`
4965
+ `${TAG15} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-spawn-failed reason=${dispatched.reason}`
4858
4966
  );
4859
4967
  return;
4860
4968
  }
4861
4969
  console.log(
4862
- `${TAG14} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=dispatched managerSessionId=${dispatched.managerSessionId} operatorTurns=${operatorTurns} priorWrites=${priorWrites.length} ms=${Date.now() - dispatchedAt}`
4970
+ `${TAG15} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=dispatched managerSessionId=${dispatched.managerSessionId} operatorTurns=${operatorTurns} priorWrites=${priorWrites.length} ms=${Date.now() - dispatchedAt}`
4863
4971
  );
4864
4972
  }
4865
4973
 
@@ -4952,22 +5060,27 @@ function startFollower(opts) {
4952
5060
  if (event.type !== "assistant") continue;
4953
5061
  const msg = event.message;
4954
5062
  if (!msg) continue;
5063
+ let sawContent = false;
4955
5064
  if (Array.isArray(msg.content)) {
4956
5065
  for (const block of msg.content) {
4957
5066
  if (block?.type === "text" && typeof block.text === "string") {
4958
5067
  entry.pendingTurnText += block.text;
4959
- } else if (fileDelivery && block?.type === "tool_use" && typeof block.name === "string" && fileDelivery.isFileDeliveryTool(block.name)) {
4960
- if (suppressing) {
4961
- discardedFileTools += 1;
4962
- continue;
4963
- }
4964
- firedFileTools.push(block.name);
4965
- try {
4966
- await fileDelivery.onFileToolUse({ toolName: block.name, input: block.input });
4967
- } catch (err) {
4968
- console.error(
4969
- `${tag} file-delivery-error sessionId=${sid} tool=${block.name} message=${err instanceof Error ? err.message : String(err)}`
4970
- );
5068
+ sawContent = true;
5069
+ } else if (block?.type === "tool_use") {
5070
+ sawContent = true;
5071
+ if (fileDelivery && typeof block.name === "string" && fileDelivery.isFileDeliveryTool(block.name)) {
5072
+ if (suppressing) {
5073
+ discardedFileTools += 1;
5074
+ continue;
5075
+ }
5076
+ firedFileTools.push(block.name);
5077
+ try {
5078
+ await fileDelivery.onFileToolUse({ toolName: block.name, input: block.input });
5079
+ } catch (err) {
5080
+ console.error(
5081
+ `${tag} file-delivery-error sessionId=${sid} tool=${block.name} message=${err instanceof Error ? err.message : String(err)}`
5082
+ );
5083
+ }
4971
5084
  }
4972
5085
  }
4973
5086
  }
@@ -4995,6 +5108,9 @@ function startFollower(opts) {
4995
5108
  );
4996
5109
  }
4997
5110
  }
5111
+ opts.onTurnComplete?.();
5112
+ } else if (sawContent && !suppressing) {
5113
+ opts.onActivity?.();
4998
5114
  }
4999
5115
  }
5000
5116
  }
@@ -5025,21 +5141,21 @@ async function fanOut(subscribers, text, onError, tag) {
5025
5141
  import { realpathSync as realpathSync2 } from "fs";
5026
5142
  import { readFile, stat as stat2 } from "fs/promises";
5027
5143
  import { resolve as resolve4, basename } from "path";
5028
- var TAG15 = "[whatsapp:outbound]";
5144
+ var TAG16 = "[whatsapp:outbound]";
5029
5145
  var WHATSAPP_DOCUMENT_MAX_BYTES = 100 * 1024 * 1024;
5030
5146
  var lastDocumentOutboundAt = /* @__PURE__ */ new Map();
5031
5147
  var lastRouteDocumentOutboundAt = /* @__PURE__ */ new Map();
5032
- function normalizeJid(to) {
5148
+ function normalizeJid2(to) {
5033
5149
  return to.includes("@") ? to : toWhatsappJid(to);
5034
5150
  }
5035
5151
  function routeKey(to, filePath) {
5036
- return `${normalizeJid(to)}\0${filePath}`;
5152
+ return `${normalizeJid2(to)}\0${filePath}`;
5037
5153
  }
5038
5154
  function documentOutboundAt(to) {
5039
- return lastDocumentOutboundAt.get(normalizeJid(to));
5155
+ return lastDocumentOutboundAt.get(normalizeJid2(to));
5040
5156
  }
5041
5157
  function recordDocumentOutbound(to) {
5042
- lastDocumentOutboundAt.set(normalizeJid(to), Date.now());
5158
+ lastDocumentOutboundAt.set(normalizeJid2(to), Date.now());
5043
5159
  }
5044
5160
  function recordRouteDocumentOutbound(to, filePath) {
5045
5161
  lastRouteDocumentOutboundAt.set(routeKey(to, filePath), Date.now());
@@ -5062,16 +5178,16 @@ async function sendWhatsAppDocument(input) {
5062
5178
  const accountResolved = realpathSync2(accountDir);
5063
5179
  if (!resolvedPath.startsWith(accountResolved + "/")) {
5064
5180
  const sanitised = filePath.replace(accountDir, "<account>/");
5065
- console.error(`${TAG15} document REJECTED path=${sanitised} reason=outside_account_directory`);
5181
+ console.error(`${TAG16} document REJECTED path=${sanitised} reason=outside_account_directory`);
5066
5182
  return { ok: false, status: 403, error: "Access denied: file is outside the account directory" };
5067
5183
  }
5068
5184
  } catch (err) {
5069
5185
  const code = err.code;
5070
5186
  if (code === "ENOENT") {
5071
- console.error(`${TAG15} document ENOENT path=${filePath}`);
5187
+ console.error(`${TAG16} document ENOENT path=${filePath}`);
5072
5188
  return { ok: false, status: 404, error: `File not found: ${filePath}` };
5073
5189
  }
5074
- console.error(`${TAG15} document path error: ${String(err)}`);
5190
+ console.error(`${TAG16} document path error: ${String(err)}`);
5075
5191
  return { ok: false, status: 500, error: String(err) };
5076
5192
  }
5077
5193
  const fileStat = await stat2(resolvedPath);
@@ -5083,10 +5199,10 @@ async function sendWhatsAppDocument(input) {
5083
5199
  };
5084
5200
  }
5085
5201
  const filename = basename(resolvedPath);
5086
- const jid = normalizeJid(to);
5202
+ const jid = normalizeJid2(to);
5087
5203
  const sock = getSocket(accountId);
5088
5204
  if (!sock) {
5089
- console.error(`${TAG15} sent document to=${jid} file=${filename} bytes=${fileStat.size} ok=false reason=not-connected`);
5205
+ console.error(`${TAG16} sent document to=${jid} file=${filename} bytes=${fileStat.size} ok=false reason=not-connected`);
5090
5206
  return { ok: false, status: 503, error: `WhatsApp account "${accountId}" is not connected` };
5091
5207
  }
5092
5208
  const buffer = Buffer.from(await readFile(resolvedPath));
@@ -5098,7 +5214,7 @@ async function sendWhatsAppDocument(input) {
5098
5214
  { accountId }
5099
5215
  );
5100
5216
  console.error(
5101
- `${TAG15} sent document to=${jid} file=${filename} bytes=${fileStat.size} ok=${result.success}` + (result.messageId ? ` id=${result.messageId}` : "")
5217
+ `${TAG16} sent document to=${jid} file=${filename} bytes=${fileStat.size} ok=${result.success}` + (result.messageId ? ` id=${result.messageId}` : "")
5102
5218
  );
5103
5219
  if (result.success) {
5104
5220
  recordDocumentOutbound(to);
@@ -5108,7 +5224,7 @@ async function sendWhatsAppDocument(input) {
5108
5224
  }
5109
5225
 
5110
5226
  // app/lib/whatsapp/inbound/file-delivery-bridge.ts
5111
- var TAG16 = "[whatsapp-adaptor]";
5227
+ var TAG17 = "[whatsapp-adaptor]";
5112
5228
  var SEND_USER_FILE = "SendUserFile";
5113
5229
  var WHATSAPP_SEND_DOCUMENT = "whatsapp-send-document";
5114
5230
  function platformRoot() {
@@ -5143,7 +5259,7 @@ function makeWhatsAppFileDelivery(entry) {
5143
5259
  maxyAccountId = resolvePlatformAccountId();
5144
5260
  } catch (err) {
5145
5261
  console.error(
5146
- `${TAG16} file-delivery reject reason=account-unresolved sender=${entry.senderId} message=${err instanceof Error ? err.message : String(err)}`
5262
+ `${TAG17} file-delivery reject reason=account-unresolved sender=${entry.senderId} message=${err instanceof Error ? err.message : String(err)}`
5147
5263
  );
5148
5264
  return;
5149
5265
  }
@@ -5160,7 +5276,7 @@ function makeWhatsAppFileDelivery(entry) {
5160
5276
  if (!result.ok) {
5161
5277
  failedFiles.push(files[i]);
5162
5278
  console.error(
5163
- `${TAG16} file-delivery reject reason=send-failed sender=${entry.senderId} status=${result.status} message=${result.error}`
5279
+ `${TAG17} file-delivery reject reason=send-failed sender=${entry.senderId} status=${result.status} message=${result.error}`
5164
5280
  );
5165
5281
  }
5166
5282
  }
@@ -5177,7 +5293,7 @@ function makeWhatsAppFileDelivery(entry) {
5177
5293
  const sid = entry.sessionId.slice(0, 8);
5178
5294
  for (const file of failed) {
5179
5295
  console.error(
5180
- `${TAG16} file-delivery-unreconciled sender=${entry.senderId} sessionId=${sid} tool=${SEND_USER_FILE} file=${file}`
5296
+ `${TAG17} file-delivery-unreconciled sender=${entry.senderId} sessionId=${sid} tool=${SEND_USER_FILE} file=${file}`
5181
5297
  );
5182
5298
  }
5183
5299
  const okAt = documentOutboundAt(entry.senderId);
@@ -5188,13 +5304,13 @@ function makeWhatsAppFileDelivery(entry) {
5188
5304
  if (!delivered) {
5189
5305
  const fileField = call.filePath !== void 0 ? ` file=${call.filePath}` : "";
5190
5306
  console.error(
5191
- `${TAG16} file-delivery-unreconciled sender=${entry.senderId} sessionId=${sid} tool=${WHATSAPP_SEND_DOCUMENT}${fileField}`
5307
+ `${TAG17} file-delivery-unreconciled sender=${entry.senderId} sessionId=${sid} tool=${WHATSAPP_SEND_DOCUMENT}${fileField}`
5192
5308
  );
5193
5309
  }
5194
5310
  }
5195
5311
  if (firedTools.includes(SEND_USER_FILE) && attempts === 0 && !documentWentOut) {
5196
5312
  console.error(
5197
- `${TAG16} file-delivery-unreconciled sender=${entry.senderId} sessionId=${sid} tool=${SEND_USER_FILE}`
5313
+ `${TAG17} file-delivery-unreconciled sender=${entry.senderId} sessionId=${sid} tool=${SEND_USER_FILE}`
5198
5314
  );
5199
5315
  }
5200
5316
  }
@@ -5776,7 +5892,7 @@ import { randomUUID as randomUUID6 } from "crypto";
5776
5892
  // app/lib/whatsapp/config-persist.ts
5777
5893
  import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, existsSync as existsSync4 } from "fs";
5778
5894
  import { resolve as resolve6, join as join4 } from "path";
5779
- var TAG17 = "[whatsapp:config]";
5895
+ var TAG18 = "[whatsapp:config]";
5780
5896
  function configPath(accountDir) {
5781
5897
  return resolve6(accountDir, "account.json");
5782
5898
  }
@@ -5793,9 +5909,9 @@ function reloadManagerConfig(accountDir) {
5793
5909
  try {
5794
5910
  const config = readConfig(accountDir);
5795
5911
  reloadConfig(config);
5796
- console.error(`${TAG17} reloaded manager config`);
5912
+ console.error(`${TAG18} reloaded manager config`);
5797
5913
  } catch (err) {
5798
- console.error(`${TAG17} manager config reload failed: ${String(err)}`);
5914
+ console.error(`${TAG18} manager config reload failed: ${String(err)}`);
5799
5915
  }
5800
5916
  }
5801
5917
  var E164_PATTERN = /^\+\d{7,15}$/;
@@ -5821,25 +5937,25 @@ function persistAfterPairing(accountDir, accountId, selfPhone) {
5821
5937
  const adminPhones = wa.adminPhones;
5822
5938
  if (!adminPhones.includes(normalized)) {
5823
5939
  adminPhones.push(normalized);
5824
- console.error(`${TAG17} added selfPhone=${normalized} to adminPhones`);
5940
+ console.error(`${TAG18} added selfPhone=${normalized} to adminPhones`);
5825
5941
  }
5826
5942
  } else {
5827
- console.error(`${TAG17} skipping adminPhones \u2014 selfPhone is null account=${accountId}`);
5943
+ console.error(`${TAG18} skipping adminPhones \u2014 selfPhone is null account=${accountId}`);
5828
5944
  }
5829
5945
  const parsed = WhatsAppConfigSchema.safeParse(wa);
5830
5946
  if (!parsed.success) {
5831
5947
  const msg = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
5832
- console.error(`${TAG17} validation failed after pairing: ${msg}`);
5948
+ console.error(`${TAG18} validation failed after pairing: ${msg}`);
5833
5949
  return { ok: false, error: `Validation failed: ${msg}` };
5834
5950
  }
5835
5951
  config.whatsapp = parsed.data;
5836
5952
  writeConfig(accountDir, config);
5837
- console.error(`${TAG17} persisted after pairing account=${accountId} phone=${selfPhone ?? "null"}`);
5953
+ console.error(`${TAG18} persisted after pairing account=${accountId} phone=${selfPhone ?? "null"}`);
5838
5954
  reloadManagerConfig(accountDir);
5839
5955
  return { ok: true };
5840
5956
  } catch (err) {
5841
5957
  const msg = err instanceof Error ? err.message : String(err);
5842
- console.error(`${TAG17} persist failed account=${accountId}: ${msg}`);
5958
+ console.error(`${TAG18} persist failed account=${accountId}: ${msg}`);
5843
5959
  return { ok: false, error: msg };
5844
5960
  }
5845
5961
  }
@@ -5869,12 +5985,12 @@ function addAdminPhone(accountDir, phone) {
5869
5985
  }
5870
5986
  config.whatsapp = parsed.data;
5871
5987
  writeConfig(accountDir, config);
5872
- console.error(`${TAG17} added admin phone=${normalized}`);
5988
+ console.error(`${TAG18} added admin phone=${normalized}`);
5873
5989
  reloadManagerConfig(accountDir);
5874
5990
  return { ok: true, message: `Added ${normalized} as admin phone. Messages from this number will route to the admin agent.` };
5875
5991
  } catch (err) {
5876
5992
  const msg = err instanceof Error ? err.message : String(err);
5877
- console.error(`${TAG17} addAdminPhone failed: ${msg}`);
5993
+ console.error(`${TAG18} addAdminPhone failed: ${msg}`);
5878
5994
  return { ok: false, error: msg };
5879
5995
  }
5880
5996
  }
@@ -5902,12 +6018,12 @@ function removeAdminPhone(accountDir, phone) {
5902
6018
  }
5903
6019
  config.whatsapp = parsed.data;
5904
6020
  writeConfig(accountDir, config);
5905
- console.error(`${TAG17} removed admin phone=${normalized}`);
6021
+ console.error(`${TAG18} removed admin phone=${normalized}`);
5906
6022
  reloadManagerConfig(accountDir);
5907
6023
  return { ok: true, message: `Removed ${normalized} from admin phones. Messages from this number will now route to the public agent.` };
5908
6024
  } catch (err) {
5909
6025
  const msg = err instanceof Error ? err.message : String(err);
5910
- console.error(`${TAG17} removeAdminPhone failed: ${msg}`);
6026
+ console.error(`${TAG18} removeAdminPhone failed: ${msg}`);
5911
6027
  return { ok: false, error: msg };
5912
6028
  }
5913
6029
  }
@@ -5945,12 +6061,12 @@ function setPublicAgent(accountDir, slug) {
5945
6061
  }
5946
6062
  config.whatsapp = parsed.data;
5947
6063
  writeConfig(accountDir, config);
5948
- console.error(`${TAG17} publicAgent set to ${trimmed}`);
6064
+ console.error(`${TAG18} publicAgent set to ${trimmed}`);
5949
6065
  reloadManagerConfig(accountDir);
5950
6066
  return { ok: true, message: `Public agent set to "${trimmed}". WhatsApp messages from non-admin phones will be handled by this agent.` };
5951
6067
  } catch (err) {
5952
6068
  const msg = err instanceof Error ? err.message : String(err);
5953
- console.error(`${TAG17} setPublicAgent failed: ${msg}`);
6069
+ console.error(`${TAG18} setPublicAgent failed: ${msg}`);
5954
6070
  return { ok: false, error: msg };
5955
6071
  }
5956
6072
  }
@@ -5991,17 +6107,17 @@ function updateConfig(accountDir, fields) {
5991
6107
  const parsed = WhatsAppConfigSchema.safeParse(wa);
5992
6108
  if (!parsed.success) {
5993
6109
  const msg = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
5994
- console.error(`${TAG17} update validation failed: ${msg}`);
6110
+ console.error(`${TAG18} update validation failed: ${msg}`);
5995
6111
  return { ok: false, error: `Validation failed: ${msg}` };
5996
6112
  }
5997
6113
  config.whatsapp = parsed.data;
5998
6114
  writeConfig(accountDir, config);
5999
- console.error(`${TAG17} updated fields=[${fieldNames.join(",")}]`);
6115
+ console.error(`${TAG18} updated fields=[${fieldNames.join(",")}]`);
6000
6116
  reloadManagerConfig(accountDir);
6001
6117
  return { ok: true, message: `Updated WhatsApp config: ${fieldNames.join(", ")}.` };
6002
6118
  } catch (err) {
6003
6119
  const msg = err instanceof Error ? err.message : String(err);
6004
- console.error(`${TAG17} updateConfig failed: ${msg}`);
6120
+ console.error(`${TAG18} updateConfig failed: ${msg}`);
6005
6121
  return { ok: false, error: msg };
6006
6122
  }
6007
6123
  }
@@ -6038,17 +6154,17 @@ function migrateRemovedConfigKeys(accountDir) {
6038
6154
  }
6039
6155
  writeConfig(accountDir, config);
6040
6156
  console.error(
6041
- `${TAG17} migration: stripped unknown keys=[${result.droppedKeys.join(",")}] from account.json`
6157
+ `${TAG18} migration: stripped unknown keys=[${result.droppedKeys.join(",")}] from account.json`
6042
6158
  );
6043
6159
  return { dropped: result.droppedKeys };
6044
6160
  } catch (err) {
6045
- console.error(`${TAG17} migration failed: ${String(err)}`);
6161
+ console.error(`${TAG18} migration failed: ${String(err)}`);
6046
6162
  return { dropped: [] };
6047
6163
  }
6048
6164
  }
6049
6165
 
6050
6166
  // app/lib/whatsapp/login.ts
6051
- var TAG18 = "[whatsapp:login]";
6167
+ var TAG19 = "[whatsapp:login]";
6052
6168
  function maskPhone(digits) {
6053
6169
  if (digits.length <= 4) return "*".repeat(digits.length);
6054
6170
  return digits.slice(0, 2) + "*".repeat(digits.length - 4) + digits.slice(-2);
@@ -6059,7 +6175,7 @@ function closeSocket(sock) {
6059
6175
  try {
6060
6176
  sock.ws?.close?.();
6061
6177
  } catch (err) {
6062
- console.warn(`${TAG18} socket close error during cleanup: ${String(err)}`);
6178
+ console.warn(`${TAG19} socket close error during cleanup: ${String(err)}`);
6063
6179
  }
6064
6180
  }
6065
6181
  function resetActiveLogin(accountId) {
@@ -6086,18 +6202,18 @@ async function runPostPairing(login) {
6086
6202
  const masked = selfPhone ? `${selfPhone.slice(0, 4)}***` : "null";
6087
6203
  console.error(`[whatsapp-persist] wa-persist-on-connect account=${login.accountId} phone=${masked}`);
6088
6204
  } else {
6089
- console.error(`${TAG18} wa-persist-on-connect FAILED account=${login.accountId}: ${result.error}`);
6205
+ console.error(`${TAG19} wa-persist-on-connect FAILED account=${login.accountId}: ${result.error}`);
6090
6206
  }
6091
6207
  } catch (err) {
6092
- console.error(`${TAG18} wa-persist-on-connect error account=${login.accountId}: ${String(err)}`);
6208
+ console.error(`${TAG19} wa-persist-on-connect error account=${login.accountId}: ${String(err)}`);
6093
6209
  }
6094
6210
  } else {
6095
- console.error(`${TAG18} wa-persist-on-connect skipped \u2014 no accountDir account=${login.accountId}`);
6211
+ console.error(`${TAG19} wa-persist-on-connect skipped \u2014 no accountDir account=${login.accountId}`);
6096
6212
  }
6097
6213
  try {
6098
6214
  await registerLoginSocket(login.accountId, login.sock, login.authDir);
6099
6215
  } catch (err) {
6100
- console.error(`${TAG18} registerLoginSocket failed account=${login.accountId}: ${String(err)}`);
6216
+ console.error(`${TAG19} registerLoginSocket failed account=${login.accountId}: ${String(err)}`);
6101
6217
  }
6102
6218
  }
6103
6219
  async function loginConnectionLoop(accountId, login) {
@@ -6109,7 +6225,7 @@ async function loginConnectionLoop(accountId, login) {
6109
6225
  if (current?.id === login.id) {
6110
6226
  await runPostPairing(current);
6111
6227
  current.connected = true;
6112
- console.error(`${TAG18} loginConnectionLoop: connected account=${accountId} attempt=${attempt} configPersisted=${current.configPersisted}`);
6228
+ console.error(`${TAG19} loginConnectionLoop: connected account=${accountId} attempt=${attempt} configPersisted=${current.configPersisted}`);
6113
6229
  }
6114
6230
  return;
6115
6231
  } catch (err) {
@@ -6119,7 +6235,7 @@ async function loginConnectionLoop(accountId, login) {
6119
6235
  if (!classification.shouldRetry || attempt >= LOGIN_MAX_RECONNECTS) {
6120
6236
  if (attempt >= LOGIN_MAX_RECONNECTS) {
6121
6237
  console.error(
6122
- `${TAG18} login reconnect attempts exhausted (${attempt}/${LOGIN_MAX_RECONNECTS}) \u2014 surfacing error to agent`
6238
+ `${TAG19} login reconnect attempts exhausted (${attempt}/${LOGIN_MAX_RECONNECTS}) \u2014 surfacing error to agent`
6123
6239
  );
6124
6240
  current.error = `Login failed after ${attempt} reconnect attempts: ${formatError(err)}`;
6125
6241
  } else {
@@ -6131,7 +6247,7 @@ async function loginConnectionLoop(accountId, login) {
6131
6247
  attempt++;
6132
6248
  const delay = LOGIN_RECONNECT_DELAYS[attempt - 1] ?? 8e3;
6133
6249
  console.error(
6134
- `${TAG18} status=${classification.statusCode ?? "unknown"} restart required \u2014 reconnecting with saved creds (attempt ${attempt}/${LOGIN_MAX_RECONNECTS}) delay=${delay}ms`
6250
+ `${TAG19} status=${classification.statusCode ?? "unknown"} restart required \u2014 reconnecting with saved creds (attempt ${attempt}/${LOGIN_MAX_RECONNECTS}) delay=${delay}ms`
6135
6251
  );
6136
6252
  closeSocket(current.sock);
6137
6253
  await new Promise((r) => setTimeout(r, delay));
@@ -6142,7 +6258,7 @@ async function loginConnectionLoop(accountId, login) {
6142
6258
  current.sock = newSock;
6143
6259
  } catch (sockErr) {
6144
6260
  console.error(
6145
- `${TAG18} reconnect socket creation failed (attempt ${attempt}/${LOGIN_MAX_RECONNECTS}): ${String(sockErr)}`
6261
+ `${TAG19} reconnect socket creation failed (attempt ${attempt}/${LOGIN_MAX_RECONNECTS}): ${String(sockErr)}`
6146
6262
  );
6147
6263
  current.error = `Reconnection failed: ${String(sockErr)}`;
6148
6264
  return;
@@ -6156,7 +6272,7 @@ async function startLogin(opts) {
6156
6272
  const hasAuth = await authExists(authDir);
6157
6273
  const selfId = readSelfId(authDir);
6158
6274
  console.error(
6159
- `${TAG18} startLogin account=${accountId} force=${!!force} hasAuth=${hasAuth}` + (existing0 ? ` activeLogin={id=${existing0.id.slice(0, 8)},age=${Math.round((Date.now() - existing0.startedAt) / 1e3)}s,hasCode=${!!existing0.pairingCode}}` : " activeLogin=none")
6275
+ `${TAG19} startLogin account=${accountId} force=${!!force} hasAuth=${hasAuth}` + (existing0 ? ` activeLogin={id=${existing0.id.slice(0, 8)},age=${Math.round((Date.now() - existing0.startedAt) / 1e3)}s,hasCode=${!!existing0.pairingCode}}` : " activeLogin=none")
6160
6276
  );
6161
6277
  if (hasAuth && !force) {
6162
6278
  const who = selfId.e164 ?? selfId.jid ?? "unknown";
@@ -6171,7 +6287,7 @@ async function startLogin(opts) {
6171
6287
  }
6172
6288
  const existing = activeLogins.get(accountId);
6173
6289
  if (existing && isLoginFresh(existing) && existing.pairingCode && !force) {
6174
- console.error(`${TAG18} startLogin account=${accountId} guard: returning existing pairing code (age=${Math.round((Date.now() - existing.startedAt) / 1e3)}s)`);
6290
+ console.error(`${TAG19} startLogin account=${accountId} guard: returning existing pairing code (age=${Math.round((Date.now() - existing.startedAt) / 1e3)}s)`);
6175
6291
  return {
6176
6292
  pairingCode: existing.pairingCode,
6177
6293
  phone: existing.phone,
@@ -6179,7 +6295,7 @@ async function startLogin(opts) {
6179
6295
  };
6180
6296
  }
6181
6297
  if (existing) {
6182
- console.error(`${TAG18} startLogin account=${accountId} ${force ? "force override" : "stale/no-code"}, resetting active login`);
6298
+ console.error(`${TAG19} startLogin account=${accountId} ${force ? "force override" : "stale/no-code"}, resetting active login`);
6183
6299
  }
6184
6300
  resetActiveLogin(accountId);
6185
6301
  await clearAuth(authDir);
@@ -6209,7 +6325,7 @@ async function startLogin(opts) {
6209
6325
  const current = activeLogins.get(accountId);
6210
6326
  if (current?.id !== login.id) return;
6211
6327
  if (!current.pairingCode) current.pairingCode = code;
6212
- console.error(`${TAG18} pairing-code-issued account=${accountId} phone=${maskPhone(digits)} codeLen=${code.length}`);
6328
+ console.error(`${TAG19} pairing-code-issued account=${accountId} phone=${maskPhone(digits)} codeLen=${code.length}`);
6213
6329
  resolveCode?.(code);
6214
6330
  }).catch((err) => {
6215
6331
  clearTimeout(codeTimer);
@@ -6238,7 +6354,7 @@ async function startLogin(opts) {
6238
6354
  const ttlTimer = setTimeout(() => {
6239
6355
  const current = activeLogins.get(accountId);
6240
6356
  if (current?.id === login.id && !current.connected) {
6241
- console.error(`${TAG18} pairing-window-elapsed account=${accountId} (ttl sweep)`);
6357
+ console.error(`${TAG19} pairing-window-elapsed account=${accountId} (ttl sweep)`);
6242
6358
  resetActiveLogin(accountId);
6243
6359
  }
6244
6360
  }, ACTIVE_LOGIN_TTL_MS);
@@ -6246,7 +6362,7 @@ async function startLogin(opts) {
6246
6362
  ttlTimer.unref();
6247
6363
  }
6248
6364
  loginConnectionLoop(accountId, login).catch((err) => {
6249
- console.error(`${TAG18} loginConnectionLoop unexpected error: ${String(err)}`);
6365
+ console.error(`${TAG19} loginConnectionLoop unexpected error: ${String(err)}`);
6250
6366
  const current = activeLogins.get(accountId);
6251
6367
  if (current?.id === login.id) {
6252
6368
  current.error = `Unexpected login error: ${String(err)}`;
@@ -6271,13 +6387,13 @@ async function waitForLogin(opts) {
6271
6387
  const { accountId, timeoutMs = 6e4 } = opts;
6272
6388
  const login = activeLogins.get(accountId);
6273
6389
  console.error(
6274
- `${TAG18} waitForLogin account=${accountId} timeout=${timeoutMs}ms` + (login ? ` login={id=${login.id.slice(0, 8)},age=${Math.round((Date.now() - login.startedAt) / 1e3)}s,connected=${login.connected},hasCode=${!!login.pairingCode}}` : " login=none")
6390
+ `${TAG19} waitForLogin account=${accountId} timeout=${timeoutMs}ms` + (login ? ` login={id=${login.id.slice(0, 8)},age=${Math.round((Date.now() - login.startedAt) / 1e3)}s,connected=${login.connected},hasCode=${!!login.pairingCode}}` : " login=none")
6275
6391
  );
6276
6392
  if (!login) {
6277
6393
  return { connected: false, message: "No active WhatsApp login in progress.", configPersisted: false };
6278
6394
  }
6279
6395
  if (!isLoginFresh(login)) {
6280
- console.error(`${TAG18} pairing-window-elapsed account=${accountId}`);
6396
+ console.error(`${TAG19} pairing-window-elapsed account=${accountId}`);
6281
6397
  resetActiveLogin(accountId);
6282
6398
  return { connected: false, message: "The pairing window expired. Ask me to generate a new code.", configPersisted: false };
6283
6399
  }
@@ -6285,8 +6401,8 @@ async function waitForLogin(opts) {
6285
6401
  while (Date.now() < deadline) {
6286
6402
  if (login.connected) {
6287
6403
  const selfId = readSelfId(login.authDir);
6288
- console.error(`${TAG18} pairing-connected account=${accountId} phone=${selfId.e164 ?? "unknown"}`);
6289
- console.error(`${TAG18} login complete for account=${accountId} phone=${selfId.e164 ?? "unknown"} configPersisted=${login.configPersisted}`);
6404
+ console.error(`${TAG19} pairing-connected account=${accountId} phone=${selfId.e164 ?? "unknown"}`);
6405
+ console.error(`${TAG19} login complete for account=${accountId} phone=${selfId.e164 ?? "unknown"} configPersisted=${login.configPersisted}`);
6290
6406
  const configPersisted = login.configPersisted;
6291
6407
  activeLogins.delete(accountId);
6292
6408
  return {
@@ -6304,7 +6420,7 @@ async function waitForLogin(opts) {
6304
6420
  await new Promise((r) => setTimeout(r, 1e3));
6305
6421
  }
6306
6422
  const elapsed = Math.round((Date.now() - (deadline - timeoutMs)) / 1e3);
6307
- console.error(`${TAG18} waitForLogin poll timeout account=${accountId} elapsed=${elapsed}s \u2014 login kept alive (pairing code still valid)`);
6423
+ console.error(`${TAG19} waitForLogin poll timeout account=${accountId} elapsed=${elapsed}s \u2014 login kept alive (pairing code still valid)`);
6308
6424
  return { connected: false, message: "Still waiting for you to enter the pairing code. The code is still valid \u2014 ask me to keep waiting, or request a new code.", configPersisted: false };
6309
6425
  }
6310
6426
 
@@ -6419,7 +6535,7 @@ function serializeWhatsAppSchema() {
6419
6535
  // app/lib/whatsapp/status-reconcile.ts
6420
6536
  import { readdirSync as readdirSync2 } from "fs";
6421
6537
  import { join as join5 } from "path";
6422
- var TAG19 = "[whatsapp:reconcile]";
6538
+ var TAG20 = "[whatsapp:reconcile]";
6423
6539
  function listCredsAccountIds(credsRoot) {
6424
6540
  try {
6425
6541
  return readdirSync2(credsRoot, { withFileTypes: true }).filter((e) => e.isDirectory() && hasCredsSync(join5(credsRoot, e.name))).map((e) => e.name);
@@ -6441,14 +6557,14 @@ function reconcileCredsOnDisk(opts) {
6441
6557
  const selfPhone = readSelfId(authDir).e164 ?? void 0;
6442
6558
  const configured = accountDir ? isAccountConfigured(accountDir, accountId) : false;
6443
6559
  console.error(
6444
- `${TAG19} wa-link-unconfigured account=${accountId} creds=present config=${configured ? "present" : "absent"}`
6560
+ `${TAG20} wa-link-unconfigured account=${accountId} creds=present config=${configured ? "present" : "absent"}`
6445
6561
  );
6446
6562
  if (!configured && accountDir) {
6447
6563
  const result = persistAfterPairing(accountDir, accountId, selfPhone ?? null);
6448
6564
  if (result.ok) {
6449
- console.error(`${TAG19} self-healed account=${accountId}`);
6565
+ console.error(`${TAG20} self-healed account=${accountId}`);
6450
6566
  } else {
6451
- console.error(`${TAG19} self-heal FAILED account=${accountId}: ${result.error}`);
6567
+ console.error(`${TAG20} self-heal FAILED account=${accountId}: ${result.error}`);
6452
6568
  }
6453
6569
  }
6454
6570
  entries.push({ accountId, selfPhone, connected: false, linkedUnconfigured: !configured });
@@ -6457,7 +6573,7 @@ function reconcileCredsOnDisk(opts) {
6457
6573
  }
6458
6574
 
6459
6575
  // server/routes/whatsapp.ts
6460
- var TAG20 = "[whatsapp:api]";
6576
+ var TAG21 = "[whatsapp:api]";
6461
6577
  var PLATFORM_ROOT4 = process.env.MAXY_PLATFORM_ROOT || "";
6462
6578
  var app3 = new Hono();
6463
6579
  app3.get("/status", (c) => {
@@ -6469,10 +6585,10 @@ app3.get("/status", (c) => {
6469
6585
  const reconciled = reconcileCredsOnDisk({ credsRoot, accountDir, liveAccountIds: liveIds });
6470
6586
  const accounts = [...live, ...reconciled];
6471
6587
  const summary = accounts.map((a) => `${a.accountId}:${a.connected ? "up" : a.linkedUnconfigured ? "linked-unconfigured" : "down"}`).join(", ");
6472
- console.error(`${TAG20} status accounts=${accounts.length} [${summary}]`);
6588
+ console.error(`${TAG21} status accounts=${accounts.length} [${summary}]`);
6473
6589
  return c.json({ accounts });
6474
6590
  } catch (err) {
6475
- console.error(`${TAG20} status error: ${String(err)}`);
6591
+ console.error(`${TAG21} status error: ${String(err)}`);
6476
6592
  return c.json({ error: String(err) }, 500);
6477
6593
  }
6478
6594
  });
@@ -6488,10 +6604,10 @@ app3.post("/login/start", async (c) => {
6488
6604
  const authDir = join6(MAXY_DIR, "credentials", "whatsapp", accountId);
6489
6605
  const accountDir = resolveAccount()?.accountDir ?? null;
6490
6606
  const result = await startLogin({ accountId, authDir, phone, accountDir, force });
6491
- console.error(`${TAG20} login/start result account=${accountId} hasCode=${!!result.pairingCode}${result.selfPhone ? ` phone=${result.selfPhone}` : ""}`);
6607
+ console.error(`${TAG21} login/start result account=${accountId} hasCode=${!!result.pairingCode}${result.selfPhone ? ` phone=${result.selfPhone}` : ""}`);
6492
6608
  return c.json(result);
6493
6609
  } catch (err) {
6494
- console.error(`${TAG20} login/start error: ${String(err)}`);
6610
+ console.error(`${TAG21} login/start error: ${String(err)}`);
6495
6611
  return c.json({ error: String(err) }, 500);
6496
6612
  }
6497
6613
  });
@@ -6501,7 +6617,7 @@ app3.post("/login/wait", async (c) => {
6501
6617
  const accountId = validateAccountId(body.accountId);
6502
6618
  const timeoutMs = body.timeoutMs ?? 6e4;
6503
6619
  const result = await waitForLogin({ accountId, timeoutMs });
6504
- console.error(`${TAG20} login/wait result account=${accountId} connected=${result.connected}${result.selfPhone ? ` phone=${result.selfPhone}` : ""} configPersisted=${result.configPersisted}`);
6620
+ console.error(`${TAG21} login/wait result account=${accountId} connected=${result.connected}${result.selfPhone ? ` phone=${result.selfPhone}` : ""} configPersisted=${result.configPersisted}`);
6505
6621
  return c.json({
6506
6622
  connected: result.connected,
6507
6623
  message: result.message,
@@ -6509,7 +6625,7 @@ app3.post("/login/wait", async (c) => {
6509
6625
  configPersisted: result.configPersisted
6510
6626
  });
6511
6627
  } catch (err) {
6512
- console.error(`${TAG20} login/wait error: ${String(err)}`);
6628
+ console.error(`${TAG21} login/wait error: ${String(err)}`);
6513
6629
  return c.json({ error: String(err) }, 500);
6514
6630
  }
6515
6631
  });
@@ -6520,7 +6636,7 @@ app3.post("/disconnect", async (c) => {
6520
6636
  await stopConnection(accountId);
6521
6637
  return c.json({ disconnected: true, accountId });
6522
6638
  } catch (err) {
6523
- console.error(`${TAG20} disconnect error: ${String(err)}`);
6639
+ console.error(`${TAG21} disconnect error: ${String(err)}`);
6524
6640
  return c.json({ error: String(err) }, 500);
6525
6641
  }
6526
6642
  });
@@ -6531,7 +6647,7 @@ app3.post("/reconnect", async (c) => {
6531
6647
  await startConnection(accountId);
6532
6648
  return c.json({ reconnecting: true, accountId });
6533
6649
  } catch (err) {
6534
- console.error(`${TAG20} reconnect error: ${String(err)}`);
6650
+ console.error(`${TAG21} reconnect error: ${String(err)}`);
6535
6651
  return c.json({ error: String(err) }, 500);
6536
6652
  }
6537
6653
  });
@@ -6552,7 +6668,7 @@ app3.post("/config", async (c) => {
6552
6668
  return c.json({ ok: false, error: 'Missing required field "phone" (E.164 format, e.g. +441234567890).' }, 400);
6553
6669
  }
6554
6670
  const result = addAdminPhone(account.accountDir, phone);
6555
- console.error(`${TAG20} config action=add-admin-phone phone=${phone} ok=${result.ok}`);
6671
+ console.error(`${TAG21} config action=add-admin-phone phone=${phone} ok=${result.ok}`);
6556
6672
  return c.json(result, result.ok ? 200 : 400);
6557
6673
  }
6558
6674
  case "remove-admin-phone": {
@@ -6560,12 +6676,12 @@ app3.post("/config", async (c) => {
6560
6676
  return c.json({ ok: false, error: 'Missing required field "phone".' }, 400);
6561
6677
  }
6562
6678
  const result = removeAdminPhone(account.accountDir, phone);
6563
- console.error(`${TAG20} config action=remove-admin-phone phone=${phone} ok=${result.ok}`);
6679
+ console.error(`${TAG21} config action=remove-admin-phone phone=${phone} ok=${result.ok}`);
6564
6680
  return c.json(result, result.ok ? 200 : 400);
6565
6681
  }
6566
6682
  case "list-admin-phones": {
6567
6683
  const phones = readAdminPhones(account.accountDir);
6568
- console.error(`${TAG20} config action=list-admin-phones count=${phones.length}`);
6684
+ console.error(`${TAG21} config action=list-admin-phones count=${phones.length}`);
6569
6685
  return c.json({ ok: true, phones });
6570
6686
  }
6571
6687
  case "set-public-agent": {
@@ -6573,13 +6689,13 @@ app3.post("/config", async (c) => {
6573
6689
  return c.json({ ok: false, error: 'Missing required field "slug" (the agent directory name, e.g. "my-agent").' }, 400);
6574
6690
  }
6575
6691
  const result = setPublicAgent(account.accountDir, slug);
6576
- console.error(`${TAG20} config action=set-public-agent slug=${slug} ok=${result.ok}`);
6692
+ console.error(`${TAG21} config action=set-public-agent slug=${slug} ok=${result.ok}`);
6577
6693
  return c.json(result, result.ok ? 200 : 400);
6578
6694
  }
6579
6695
  case "get-public-agent": {
6580
6696
  const targetAccount = typeof accountId === "string" && accountId.trim() ? accountId.trim() : "default";
6581
6697
  const resolved = resolvePublicAgent(account.accountDir, { accountId: targetAccount });
6582
- console.error(`${TAG20} config action=get-public-agent accountId=${targetAccount} slug=${resolved?.slug ?? "none"} source=${resolved?.source ?? "none"}`);
6698
+ console.error(`${TAG21} config action=get-public-agent accountId=${targetAccount} slug=${resolved?.slug ?? "none"} source=${resolved?.source ?? "none"}`);
6583
6699
  return c.json({ ok: true, slug: resolved?.slug ?? null, source: resolved?.source ?? null });
6584
6700
  }
6585
6701
  case "list-public-agents": {
@@ -6596,26 +6712,26 @@ app3.post("/config", async (c) => {
6596
6712
  const config = JSON.parse(readFileSync7(configPath2, "utf-8"));
6597
6713
  agents.push({ slug: entry.name, displayName: config.displayName ?? entry.name });
6598
6714
  } catch {
6599
- console.error(`${TAG20} config action=list-public-agents error="failed to parse config.json for agent ${entry.name}" \u2014 skipping`);
6715
+ console.error(`${TAG21} config action=list-public-agents error="failed to parse config.json for agent ${entry.name}" \u2014 skipping`);
6600
6716
  }
6601
6717
  }
6602
6718
  } catch (err) {
6603
- console.error(`${TAG20} config action=list-public-agents error="failed to scan agents directory: ${String(err)}"`);
6719
+ console.error(`${TAG21} config action=list-public-agents error="failed to scan agents directory: ${String(err)}"`);
6604
6720
  }
6605
6721
  }
6606
- console.error(`${TAG20} config action=list-public-agents count=${agents.length}`);
6722
+ console.error(`${TAG21} config action=list-public-agents count=${agents.length}`);
6607
6723
  return c.json({ ok: true, agents });
6608
6724
  }
6609
6725
  case "schema": {
6610
6726
  const text = serializeWhatsAppSchema();
6611
- console.error(`${TAG20} config action=schema`);
6727
+ console.error(`${TAG21} config action=schema`);
6612
6728
  return c.json({ ok: true, text });
6613
6729
  }
6614
6730
  case "list-groups": {
6615
6731
  const groupAccountId = accountId ?? "default";
6616
6732
  const sock = getSocket(groupAccountId);
6617
6733
  if (!sock) {
6618
- console.error(`${TAG20} config action=list-groups error="not connected" accountId=${groupAccountId}`);
6734
+ console.error(`${TAG21} config action=list-groups error="not connected" accountId=${groupAccountId}`);
6619
6735
  return c.json({ ok: false, error: `WhatsApp account "${groupAccountId}" is not connected. Connect first, then retry.` });
6620
6736
  }
6621
6737
  try {
@@ -6625,10 +6741,10 @@ app3.post("/config", async (c) => {
6625
6741
  name: g.subject ?? g.id,
6626
6742
  participantCount: Array.isArray(g.participants) ? g.participants.length : 0
6627
6743
  }));
6628
- console.error(`${TAG20} config action=list-groups count=${groups.length} accountId=${groupAccountId}`);
6744
+ console.error(`${TAG21} config action=list-groups count=${groups.length} accountId=${groupAccountId}`);
6629
6745
  return c.json({ ok: true, groups });
6630
6746
  } catch (err) {
6631
- console.error(`${TAG20} config action=list-groups error="${String(err)}" accountId=${groupAccountId}`);
6747
+ console.error(`${TAG21} config action=list-groups error="${String(err)}" accountId=${groupAccountId}`);
6632
6748
  return c.json({ ok: false, error: `Failed to fetch groups: ${String(err)}` });
6633
6749
  }
6634
6750
  }
@@ -6638,12 +6754,12 @@ app3.post("/config", async (c) => {
6638
6754
  }
6639
6755
  const result = updateConfig(account.accountDir, fields);
6640
6756
  const fieldNames = Object.keys(fields);
6641
- console.error(`${TAG20} config action=update-config fields=[${fieldNames.join(",")}] ok=${result.ok}`);
6757
+ console.error(`${TAG21} config action=update-config fields=[${fieldNames.join(",")}] ok=${result.ok}`);
6642
6758
  return c.json(result, result.ok ? 200 : 400);
6643
6759
  }
6644
6760
  case "get-config": {
6645
6761
  const waConfig = getConfig(account.accountDir);
6646
- console.error(`${TAG20} config action=get-config`);
6762
+ console.error(`${TAG21} config action=get-config`);
6647
6763
  return c.json({ ok: true, config: waConfig });
6648
6764
  }
6649
6765
  default:
@@ -6653,7 +6769,7 @@ app3.post("/config", async (c) => {
6653
6769
  );
6654
6770
  }
6655
6771
  } catch (err) {
6656
- console.error(`${TAG20} config error: ${String(err)}`);
6772
+ console.error(`${TAG21} config error: ${String(err)}`);
6657
6773
  return c.json({ ok: false, error: String(err) }, 500);
6658
6774
  }
6659
6775
  });
@@ -6674,7 +6790,7 @@ app3.post("/send-document", async (c) => {
6674
6790
  recordRouteDocumentOutbound(to, filePath);
6675
6791
  return c.json({ success: true, messageId: result.messageId });
6676
6792
  } catch (err) {
6677
- console.error(`${TAG20} send-document error: ${String(err)}`);
6793
+ console.error(`${TAG21} send-document error: ${String(err)}`);
6678
6794
  return c.json({ error: String(err) }, 500);
6679
6795
  }
6680
6796
  });
@@ -6684,11 +6800,11 @@ app3.get("/activity", (c) => {
6684
6800
  const result = getChannelActivity(accountId);
6685
6801
  const total = result.accounts.reduce((sum, a) => sum + a.total, 0);
6686
6802
  console.error(
6687
- `${TAG20} activity accounts=${result.accounts.length} total=${total} recentEvents=${result.recentEvents.length}` + (accountId ? ` filter=${accountId}` : "")
6803
+ `${TAG21} activity accounts=${result.accounts.length} total=${total} recentEvents=${result.recentEvents.length}` + (accountId ? ` filter=${accountId}` : "")
6688
6804
  );
6689
6805
  return c.json(result);
6690
6806
  } catch (err) {
6691
- console.error(`${TAG20} activity error: ${String(err)}`);
6807
+ console.error(`${TAG21} activity error: ${String(err)}`);
6692
6808
  return c.json({ error: String(err) }, 500);
6693
6809
  }
6694
6810
  });
@@ -6707,10 +6823,10 @@ app3.get("/conversations", (c) => {
6707
6823
  };
6708
6824
  });
6709
6825
  conversations.sort((a, b) => (b.lastMessageTimestamp ?? 0) - (a.lastMessageTimestamp ?? 0));
6710
- console.error(`${TAG20} conversations account=${accountId} count=${conversations.length}`);
6826
+ console.error(`${TAG21} conversations account=${accountId} count=${conversations.length}`);
6711
6827
  return c.json({ conversations });
6712
6828
  } catch (err) {
6713
- console.error(`${TAG20} conversations error: ${String(err)}`);
6829
+ console.error(`${TAG21} conversations error: ${String(err)}`);
6714
6830
  return c.json({ error: String(err) }, 500);
6715
6831
  }
6716
6832
  });
@@ -6725,10 +6841,10 @@ app3.get("/messages", (c) => {
6725
6841
  const limit = limitParam ? parseInt(limitParam, 10) : void 0;
6726
6842
  const effectiveLimit = limit && !Number.isNaN(limit) && limit > 0 ? limit : void 0;
6727
6843
  const messages = getMessages(accountId, jid, effectiveLimit);
6728
- console.error(`${TAG20} messages account=${accountId} jid=${jid} limit=${effectiveLimit ?? "all"} returned=${messages.length}`);
6844
+ console.error(`${TAG21} messages account=${accountId} jid=${jid} limit=${effectiveLimit ?? "all"} returned=${messages.length}`);
6729
6845
  return c.json({ messages });
6730
6846
  } catch (err) {
6731
- console.error(`${TAG20} messages error: ${String(err)}`);
6847
+ console.error(`${TAG21} messages error: ${String(err)}`);
6732
6848
  return c.json({ error: String(err) }, 500);
6733
6849
  }
6734
6850
  });
@@ -6809,7 +6925,7 @@ app3.get("/conversation-graph-state", async (c) => {
6809
6925
  }
6810
6926
  } catch (err) {
6811
6927
  const msg = err instanceof Error ? err.message : String(err);
6812
- console.error(`${TAG20} conversation-graph-state ERR cacheKey=${cacheKey} reason=${msg}`);
6928
+ console.error(`${TAG21} conversation-graph-state ERR cacheKey=${cacheKey} reason=${msg}`);
6813
6929
  return c.json({ error: `Graph query failed: ${msg}`, cacheKey, cypher: cypher.trim() }, 500);
6814
6930
  }
6815
6931
  const ms = Date.now() - t0;
@@ -6822,7 +6938,7 @@ app3.get("/conversation-graph-state", async (c) => {
6822
6938
  ms
6823
6939
  });
6824
6940
  } catch (err) {
6825
- console.error(`${TAG20} conversation-graph-state error: ${String(err)}`);
6941
+ console.error(`${TAG21} conversation-graph-state error: ${String(err)}`);
6826
6942
  return c.json({ error: String(err) }, 500);
6827
6943
  }
6828
6944
  });
@@ -6834,12 +6950,12 @@ app3.get("/group-info", async (c) => {
6834
6950
  return c.json({ error: "Missing required parameter: jid" }, 400);
6835
6951
  }
6836
6952
  if (!isGroupJid(jid)) {
6837
- console.error(`${TAG20} group-info error="not a group JID" jid=${jid} account=${accountId}`);
6953
+ console.error(`${TAG21} group-info error="not a group JID" jid=${jid} account=${accountId}`);
6838
6954
  return c.json({ error: `"${jid}" is not a group JID. Group JIDs end with @g.us.` }, 400);
6839
6955
  }
6840
6956
  const sock = getSocket(accountId);
6841
6957
  if (!sock) {
6842
- console.error(`${TAG20} group-info error="not connected" account=${accountId}`);
6958
+ console.error(`${TAG21} group-info error="not connected" account=${accountId}`);
6843
6959
  return c.json({ error: `WhatsApp account "${accountId}" is not connected. Connect first, then retry.` }, 400);
6844
6960
  }
6845
6961
  const meta = await sock.groupMetadata(jid);
@@ -6852,10 +6968,10 @@ app3.get("/group-info", async (c) => {
6852
6968
  participantCount: meta.participants.length,
6853
6969
  participants: meta.participants.map((p) => ({ jid: p.id, admin: p.admin ?? null }))
6854
6970
  };
6855
- console.error(`${TAG20} group-info jid=${jid} subject="${meta.subject}" participants=${meta.participants.length} account=${accountId}`);
6971
+ console.error(`${TAG21} group-info jid=${jid} subject="${meta.subject}" participants=${meta.participants.length} account=${accountId}`);
6856
6972
  return c.json(result);
6857
6973
  } catch (err) {
6858
- console.error(`${TAG20} group-info error="${String(err)}" jid=${jid} account=${accountId}`);
6974
+ console.error(`${TAG21} group-info error="${String(err)}" jid=${jid} account=${accountId}`);
6859
6975
  return c.json({ error: `Failed to fetch group info: ${String(err)}` }, 500);
6860
6976
  }
6861
6977
  });
@@ -7231,6 +7347,9 @@ function resolveOperatorDisplay(userIdRes, nameRes) {
7231
7347
  }
7232
7348
 
7233
7349
  // app/lib/whatsapp-reader/parse-transcript.ts
7350
+ var WA_REPLY_TOOL = "mcp__whatsapp-channel__reply";
7351
+ var WA_REPLY_DOCUMENT_TOOL = "mcp__whatsapp-channel__reply-document";
7352
+ var WEBCHAT_REPLY_TOOL = "mcp__webchat-channel__reply";
7234
7353
  var CLI_MARKER_PREFIXES2 = ["<local-command-", "<command-name>", "<command-message>"];
7235
7354
  var CHANNEL_WRAPPER = /^<channel\b[^>]*>\n?([\s\S]*?)\n?<\/channel>$/;
7236
7355
  function asString(content) {
@@ -7247,6 +7366,14 @@ function assistantTurns(content, ts) {
7247
7366
  const b = block;
7248
7367
  if (b.type === "text" && typeof b.text === "string") {
7249
7368
  out.push({ kind: "agent-text", text: b.text, ts });
7369
+ } else if (b.type === "tool_use" && (b.name === WA_REPLY_TOOL || b.name === WEBCHAT_REPLY_TOOL)) {
7370
+ const text = b.input?.text;
7371
+ out.push({ kind: "agent-reply", text: typeof text === "string" ? text : "", ts });
7372
+ } else if (b.type === "tool_use" && b.name === WA_REPLY_DOCUMENT_TOOL) {
7373
+ const input = b.input ?? {};
7374
+ const files = Array.isArray(input.files) ? input.files.filter((f) => typeof f === "string") : [];
7375
+ const caption = typeof input.caption === "string" ? input.caption : null;
7376
+ out.push({ kind: "agent-reply-document", files, caption, ts });
7250
7377
  } else if (b.type === "tool_use" && typeof b.name === "string") {
7251
7378
  out.push({ kind: "tool-call", name: b.name, input: b.input ?? null, ts });
7252
7379
  }
@@ -7291,6 +7418,16 @@ function parseTranscript(lines) {
7291
7418
  out.push(...assistantTurns(msg?.content, ts));
7292
7419
  continue;
7293
7420
  }
7421
+ if (row.type === "attachment") {
7422
+ const att = row.attachment;
7423
+ const isChannelQueued = att?.type === "queued_command" && att.isMeta === true && typeof att.origin === "object" && att.origin !== null && att.origin.kind === "channel" && typeof att.prompt === "string";
7424
+ if (isChannelQueued) {
7425
+ const prompt = att.prompt;
7426
+ const m = prompt.match(CHANNEL_WRAPPER);
7427
+ out.push({ kind: "operator-inbound", text: m ? m[1] : prompt, ts });
7428
+ }
7429
+ continue;
7430
+ }
7294
7431
  if (row.type !== "user" || !msg) continue;
7295
7432
  if (Array.isArray(msg.content)) {
7296
7433
  out.push(...toolResultTurns(msg.content, ts));
@@ -7565,9 +7702,57 @@ app5.get("/directive", requireAdminSession, (c) => {
7565
7702
  });
7566
7703
  var whatsapp_reader_default = app5;
7567
7704
 
7705
+ // server/routes/webchat.ts
7706
+ import { basename as basename3, dirname as dirname2 } from "path";
7707
+ import { join as join10 } from "path";
7708
+ var WEBCHAT_ADMIN_KEY = "webchat-admin";
7709
+ function createWebchatRoutes(deps) {
7710
+ const app48 = new Hono();
7711
+ app48.post("/send", requireAdminSession, async (c) => {
7712
+ const body = await c.req.json().catch(() => null);
7713
+ const text = body?.text;
7714
+ if (typeof text !== "string" || text.length === 0) {
7715
+ return c.json({ error: "text required" }, 400);
7716
+ }
7717
+ let accountId;
7718
+ try {
7719
+ accountId = resolvePlatformAccountId();
7720
+ } catch (err) {
7721
+ const detail = err instanceof Error ? err.message : String(err);
7722
+ console.error(`[webchat:inbound] op=send-refused reason=account-unresolved detail=${detail}`);
7723
+ return c.json({ error: "account unresolved" }, 503);
7724
+ }
7725
+ await deps.handleInbound({ role: "admin", accountId, key: WEBCHAT_ADMIN_KEY, text });
7726
+ return c.json({ ok: true });
7727
+ });
7728
+ app48.get("/session", requireAdminSession, (c) => {
7729
+ let accountId;
7730
+ try {
7731
+ accountId = resolvePlatformAccountId();
7732
+ } catch (err) {
7733
+ const detail = err instanceof Error ? err.message : String(err);
7734
+ console.error(`[webchat:inbound] op=session-refused reason=account-unresolved detail=${detail}`);
7735
+ return c.json({ error: "account unresolved" }, 503);
7736
+ }
7737
+ const sessionId = adminSessionIdFor(accountId, WEBCHAT_ADMIN_KEY);
7738
+ const cfg = claudeConfigDir();
7739
+ let projectDir = null;
7740
+ if (cfg) {
7741
+ for (const { path: path2 } of enumerateJsonls(join10(cfg, "projects"))) {
7742
+ if (basename3(path2) === `${sessionId}.jsonl`) {
7743
+ projectDir = dirname2(path2);
7744
+ break;
7745
+ }
7746
+ }
7747
+ }
7748
+ return c.json({ sessionId, projectDir });
7749
+ });
7750
+ return app48;
7751
+ }
7752
+
7568
7753
  // server/routes/telegram.ts
7569
7754
  import { homedir } from "os";
7570
- import { resolve as resolve10, join as join10 } from "path";
7755
+ import { resolve as resolve10, join as join11 } from "path";
7571
7756
  import { existsSync as existsSync8, readFileSync as readFileSync11 } from "fs";
7572
7757
 
7573
7758
  // app/lib/telegram/access-control.ts
@@ -7615,11 +7800,11 @@ function routeTelegramUpdate(input) {
7615
7800
  }
7616
7801
 
7617
7802
  // server/routes/telegram.ts
7618
- var TAG21 = "[telegram-inbound]";
7803
+ var TAG22 = "[telegram-inbound]";
7619
7804
  var DISPATCH_TIMEOUT_MS = 12e4;
7620
7805
  function configDirName() {
7621
7806
  const platformRoot2 = process.env.MAXY_PLATFORM_ROOT ?? resolve10(process.cwd(), "..");
7622
- const brandPath = join10(platformRoot2, "config", "brand.json");
7807
+ const brandPath = join11(platformRoot2, "config", "brand.json");
7623
7808
  if (existsSync8(brandPath)) {
7624
7809
  try {
7625
7810
  return JSON.parse(readFileSync11(brandPath, "utf-8")).configDir ?? ".maxy";
@@ -7650,18 +7835,18 @@ app6.post("/", async (c) => {
7650
7835
  const botParam = c.req.query("bot");
7651
7836
  const botType = botParam === "admin" ? "admin" : botParam === "public" ? "public" : null;
7652
7837
  if (!botType) {
7653
- console.error(`${TAG21} op=reject reason=missing-bot-param`);
7838
+ console.error(`${TAG22} op=reject reason=missing-bot-param`);
7654
7839
  return c.json({ ok: false }, 400);
7655
7840
  }
7656
7841
  const sp = secretPath(botType);
7657
7842
  if (!existsSync8(sp)) {
7658
- console.error(`${TAG21} op=reject reason=no-secret-file botType=${botType}`);
7843
+ console.error(`${TAG22} op=reject reason=no-secret-file botType=${botType}`);
7659
7844
  return c.json({ ok: false }, 401);
7660
7845
  }
7661
7846
  const expected = readFileSync11(sp, "utf-8").trim();
7662
7847
  const got = c.req.header("x-telegram-bot-api-secret-token") ?? "";
7663
7848
  if (got !== expected) {
7664
- console.error(`${TAG21} op=reject reason=bad-secret botType=${botType}`);
7849
+ console.error(`${TAG22} op=reject reason=bad-secret botType=${botType}`);
7665
7850
  return c.json({ ok: false }, 401);
7666
7851
  }
7667
7852
  let update;
@@ -7672,25 +7857,25 @@ app6.post("/", async (c) => {
7672
7857
  }
7673
7858
  const account = resolveAccount();
7674
7859
  if (!account) {
7675
- console.error(`${TAG21} op=reject reason=no-account`);
7860
+ console.error(`${TAG22} op=reject reason=no-account`);
7676
7861
  return c.json({ ok: true }, 200);
7677
7862
  }
7678
7863
  const decision = routeTelegramUpdate({ update, botType, config: account.config.telegram ?? {} });
7679
7864
  if (decision.kind === "ignore") {
7680
- console.error(`${TAG21} op=ignore reason=${decision.reason}`);
7865
+ console.error(`${TAG22} op=ignore reason=${decision.reason}`);
7681
7866
  return c.json({ ok: true }, 200);
7682
7867
  }
7683
7868
  if (decision.kind === "denied") {
7684
- console.error(`${TAG21} op=denied reason=${decision.reason} agentType=${decision.agentType}`);
7869
+ console.error(`${TAG22} op=denied reason=${decision.reason} agentType=${decision.agentType}`);
7685
7870
  return c.json({ ok: true }, 200);
7686
7871
  }
7687
7872
  const role = decision.agentType === "admin" ? "admin" : "public";
7688
7873
  const agentSlug = role === "admin" ? "admin" : resolveDefaultAgentSlug(account.accountDir);
7689
7874
  if (!agentSlug) {
7690
- console.error(`${TAG21} op=reject reason=no-default-agent`);
7875
+ console.error(`${TAG22} op=reject reason=no-default-agent`);
7691
7876
  return c.json({ ok: true }, 200);
7692
7877
  }
7693
- console.error(`${TAG21} op=update accountId=${account.accountId} from=${decision.senderId}`);
7878
+ console.error(`${TAG22} op=update accountId=${account.accountId} from=${decision.senderId}`);
7694
7879
  const botToken = botType === "admin" ? account.config.telegram?.adminBotToken : account.config.telegram?.publicBotToken;
7695
7880
  const { senderId, chatId, text } = decision;
7696
7881
  void (async () => {
@@ -7704,16 +7889,16 @@ app6.post("/", async (c) => {
7704
7889
  timeoutMs: DISPATCH_TIMEOUT_MS
7705
7890
  });
7706
7891
  if ("error" in result) {
7707
- console.error(`${TAG21} op=dispatch-failed from=${senderId} error=${result.error}`);
7892
+ console.error(`${TAG22} op=dispatch-failed from=${senderId} error=${result.error}`);
7708
7893
  return;
7709
7894
  }
7710
7895
  if (!botToken) {
7711
7896
  const reason = account.config.telegram ? "no-bot-token" : "no-telegram-config";
7712
- console.error(`${TAG21} op=reply-dropped reason=${reason} botType=${botType}`);
7897
+ console.error(`${TAG22} op=reply-dropped reason=${reason} botType=${botType}`);
7713
7898
  return;
7714
7899
  }
7715
7900
  const sent = await sendTelegram(botToken, chatId, result.turnText);
7716
- console.error(`${TAG21} op=reply-sent from=${senderId} ok=${sent.ok}${sent.ok ? "" : ` error=${sent.error}`}`);
7901
+ console.error(`${TAG22} op=reply-sent from=${senderId} ok=${sent.ok}${sent.ok ? "" : ` error=${sent.error}`}`);
7717
7902
  })();
7718
7903
  return c.json({ ok: true }, 200);
7719
7904
  });
@@ -7726,7 +7911,7 @@ import { createHash as createHash3, randomUUID as randomUUID7 } from "crypto";
7726
7911
 
7727
7912
  // ../lib/admins-write/src/index.ts
7728
7913
  import { existsSync as existsSync9, readFileSync as readFileSync12, writeFileSync as writeFileSync5, renameSync, mkdirSync as mkdirSync2, readdirSync as readdirSync7, statSync as statSync5 } from "fs";
7729
- import { dirname as dirname2, join as join11 } from "path";
7914
+ import { dirname as dirname3, join as join12 } from "path";
7730
7915
  function logLine(input, result) {
7731
7916
  const userIdShort = input.userId.slice(0, 8);
7732
7917
  console.error(
@@ -7734,7 +7919,7 @@ function logLine(input, result) {
7734
7919
  );
7735
7920
  }
7736
7921
  function writeFileAtomic(filePath, contents, mode) {
7737
- mkdirSync2(dirname2(filePath), { recursive: true });
7922
+ mkdirSync2(dirname3(filePath), { recursive: true });
7738
7923
  const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
7739
7924
  writeFileSync5(tempPath, contents, mode !== void 0 ? { mode } : void 0);
7740
7925
  renameSync(tempPath, filePath);
@@ -7768,7 +7953,7 @@ function writeAdminEntry(input) {
7768
7953
  return result;
7769
7954
  }
7770
7955
  try {
7771
- const accountJsonPath = join11(input.accountDir, "account.json");
7956
+ const accountJsonPath = join12(input.accountDir, "account.json");
7772
7957
  if (!existsSync9(accountJsonPath)) {
7773
7958
  throw new Error(`account.json not found at ${accountJsonPath}`);
7774
7959
  }
@@ -7823,13 +8008,13 @@ function checkAdminAuthInvariant(input) {
7823
8008
  }
7824
8009
  for (const entry of entries) {
7825
8010
  if (entry.startsWith(".")) continue;
7826
- const accountDir = join11(input.accountsDir, entry);
8011
+ const accountDir = join12(input.accountsDir, entry);
7827
8012
  try {
7828
8013
  if (!statSync5(accountDir).isDirectory()) continue;
7829
8014
  } catch {
7830
8015
  continue;
7831
8016
  }
7832
- const accountJsonPath = join11(accountDir, "account.json");
8017
+ const accountJsonPath = join12(accountDir, "account.json");
7833
8018
  if (!existsSync9(accountJsonPath)) continue;
7834
8019
  let admins = [];
7835
8020
  try {
@@ -7920,11 +8105,17 @@ app7.post("/claude-auth", async (c) => {
7920
8105
  return c.json({ stopped: true });
7921
8106
  }
7922
8107
  if (body.action === "logout") {
8108
+ const start = Date.now();
8109
+ let logoutError = null;
7923
8110
  try {
7924
8111
  execFileSync2("claude", ["auth", "logout"], { encoding: "utf-8", timeout: 8e3 });
7925
- } catch {
8112
+ } catch (err) {
8113
+ logoutError = err instanceof Error ? err.message : String(err);
7926
8114
  }
7927
- return c.json({ logged_out: true });
8115
+ const credentialsPresent = existsSync10(CLAUDE_CREDENTIALS_FILE);
8116
+ const ranMs = Date.now() - start;
8117
+ console.log(`[onboarding] op=claude-logout credentialsPresent=${credentialsPresent} ranMs=${ranMs} logoutError=${logoutError ? JSON.stringify(logoutError.slice(0, 120)) : "none"}`);
8118
+ return c.json({ logged_out: !credentialsPresent });
7928
8119
  }
7929
8120
  if (body.action === "wait") {
7930
8121
  const authenticated = await checkAuthStatus();
@@ -8112,8 +8303,8 @@ var onboarding_default = app7;
8112
8303
 
8113
8304
  // server/routes/client-error.ts
8114
8305
  import { appendFileSync, existsSync as existsSync11, renameSync as renameSync2, statSync as statSync6 } from "fs";
8115
- import { join as join12 } from "path";
8116
- var CLIENT_ERRORS_LOG = join12(LOG_DIR, "client-errors.log");
8306
+ import { join as join13 } from "path";
8307
+ var CLIENT_ERRORS_LOG = join13(LOG_DIR, "client-errors.log");
8117
8308
  var MAX_LOG_SIZE = 10 * 1024 * 1024;
8118
8309
  var MAX_BODY_SIZE = 8 * 1024;
8119
8310
  var MAX_STACK_LEN = 2e3;
@@ -8439,16 +8630,16 @@ var session_default = app9;
8439
8630
 
8440
8631
  // server/routes/admin/logs.ts
8441
8632
  import { existsSync as existsSync13, readdirSync as readdirSync8, readFileSync as readFileSync14, statSync as statSync7 } from "fs";
8442
- import { resolve as resolve11, basename as basename3 } from "path";
8633
+ import { resolve as resolve11, basename as basename4 } from "path";
8443
8634
 
8444
8635
  // app/lib/logs-read-resolve.ts
8445
8636
  import { existsSync as existsSync12 } from "fs";
8446
- import { join as join13 } from "path";
8637
+ import { join as join14 } from "path";
8447
8638
  function resolveSessionLogPaths(filename, logDirs) {
8448
8639
  const tried = [filename];
8449
8640
  const hits = [];
8450
8641
  for (const dir of logDirs) {
8451
- const fullPath = join13(dir, filename);
8642
+ const fullPath = join14(dir, filename);
8452
8643
  if (existsSync12(fullPath)) {
8453
8644
  hits.push({ path: fullPath, dir });
8454
8645
  }
@@ -8471,7 +8662,7 @@ app10.get("/", async (c) => {
8471
8662
  if (accountLogDir) logDirs.push(accountLogDir);
8472
8663
  logDirs.push(LOG_DIR);
8473
8664
  if (fileParam) {
8474
- const safe = basename3(fileParam);
8665
+ const safe = basename4(fileParam);
8475
8666
  const searched = [];
8476
8667
  for (const dir of logDirs) {
8477
8668
  const filePath = resolve11(dir, safe);
@@ -8541,7 +8732,7 @@ app10.get("/", async (c) => {
8541
8732
  if (hit) {
8542
8733
  console.info(`[admin/logs] resolved cacheKey=${cacheKeySlice} sessionId=${sessionIdSlice} via=${primaryId === cacheKey ? "cacheKey" : primaryId === sessionKeyFromConv ? "reverse-lookup" : "sessionId-fallback"}`);
8543
8734
  try {
8544
- const filename = basename3(hit.path);
8735
+ const filename = basename4(hit.path);
8545
8736
  const buffer = readFileSync14(hit.path);
8546
8737
  const onDiskBytes = statSync7(hit.path).size;
8547
8738
  const headers = {
@@ -9450,7 +9641,7 @@ var sessions_default = app14;
9450
9641
 
9451
9642
  // app/lib/claude-agent/spawn-context.ts
9452
9643
  import { existsSync as existsSync17, readFileSync as readFileSync16, readdirSync as readdirSync10, statSync as statSync8 } from "fs";
9453
- import { dirname as dirname3, resolve as resolve14, join as join14 } from "path";
9644
+ import { dirname as dirname4, resolve as resolve14, join as join15 } from "path";
9454
9645
  async function resolveOwnerProfileBlock(accountId, userId) {
9455
9646
  if (!userId) return { ok: false, reason: "missing-user-id" };
9456
9647
  try {
@@ -9479,8 +9670,8 @@ function frontmatterField(manifest, field) {
9479
9670
  function extractPluginToolDescriptions(pluginDir) {
9480
9671
  const out = /* @__PURE__ */ new Map();
9481
9672
  const candidates = [
9482
- join14(pluginDir, "mcp/dist/index.js"),
9483
- join14(pluginDir, "mcp/src/index.ts")
9673
+ join15(pluginDir, "mcp/dist/index.js"),
9674
+ join15(pluginDir, "mcp/src/index.ts")
9484
9675
  ];
9485
9676
  let raw = null;
9486
9677
  for (const path2 of candidates) {
@@ -9538,21 +9729,21 @@ function listPluginDirs() {
9538
9729
  const platformPlugins = resolve14(PLATFORM_ROOT, "plugins");
9539
9730
  if (existsSync17(platformPlugins)) {
9540
9731
  for (const name of readdirSync10(platformPlugins)) {
9541
- const dir = join14(platformPlugins, name);
9732
+ const dir = join15(platformPlugins, name);
9542
9733
  try {
9543
9734
  if (statSync8(dir).isDirectory()) out.set(name, dir);
9544
9735
  } catch {
9545
9736
  }
9546
9737
  }
9547
9738
  }
9548
- const premiumRoot = resolve14(dirname3(PLATFORM_ROOT), "premium-plugins");
9739
+ const premiumRoot = resolve14(dirname4(PLATFORM_ROOT), "premium-plugins");
9549
9740
  if (existsSync17(premiumRoot)) {
9550
9741
  for (const bundle of readdirSync10(premiumRoot)) {
9551
- const bundlePlugins = join14(premiumRoot, bundle, "plugins");
9742
+ const bundlePlugins = join15(premiumRoot, bundle, "plugins");
9552
9743
  if (!existsSync17(bundlePlugins)) continue;
9553
9744
  try {
9554
9745
  for (const name of readdirSync10(bundlePlugins)) {
9555
- const dir = join14(bundlePlugins, name);
9746
+ const dir = join15(bundlePlugins, name);
9556
9747
  try {
9557
9748
  if (statSync8(dir).isDirectory()) out.set(name, dir);
9558
9749
  } catch {
@@ -9595,7 +9786,7 @@ function computeActivePlugins(accountId) {
9595
9786
  for (const name of Array.from(enabled).sort()) {
9596
9787
  const dir = pluginDirs.get(name);
9597
9788
  if (!dir) continue;
9598
- const fm = readFrontmatter(join14(dir, "PLUGIN.md"));
9789
+ const fm = readFrontmatter(join15(dir, "PLUGIN.md"));
9599
9790
  if (!fm) continue;
9600
9791
  const description = frontmatterField(`---
9601
9792
  ${fm}
@@ -9612,7 +9803,7 @@ ${fm}
9612
9803
  `plugin-manifest-tool-coverage plugin=${name} tools=${tools.length} with-desc=${withDesc}`
9613
9804
  );
9614
9805
  if (toolNames.length > 0 && descMap.size === 0) {
9615
- const hasSource = existsSync17(join14(dir, "mcp/dist/index.js")) || existsSync17(join14(dir, "mcp/src/index.ts"));
9806
+ const hasSource = existsSync17(join15(dir, "mcp/dist/index.js")) || existsSync17(join15(dir, "mcp/src/index.ts"));
9616
9807
  if (hasSource) {
9617
9808
  console.warn(
9618
9809
  `[spawn-context] WARN plugin=${name} mcp source present but tool-description regex matched zero entries \u2014 SDK upgrade may have changed the registration shape`
@@ -9622,7 +9813,7 @@ ${fm}
9622
9813
  const skillPaths = parseSkillPathsFromFrontmatter(fm);
9623
9814
  const skills = [];
9624
9815
  for (const relPath of skillPaths) {
9625
- const skillPath = join14(dir, relPath);
9816
+ const skillPath = join15(dir, relPath);
9626
9817
  const skillFm = readFrontmatter(skillPath);
9627
9818
  if (!skillFm) continue;
9628
9819
  const skillName = frontmatterField(`---
@@ -9666,7 +9857,7 @@ function computeSpecialistDomains(accountId) {
9666
9857
  const out = [];
9667
9858
  for (const file of entries.sort()) {
9668
9859
  if (!file.endsWith(".md")) continue;
9669
- const fm = readFrontmatter(join14(specialistsDir, file));
9860
+ const fm = readFrontmatter(join15(specialistsDir, file));
9670
9861
  if (!fm) continue;
9671
9862
  const wrapped = `---
9672
9863
  ${fm}
@@ -9759,7 +9950,7 @@ function resolveTunnelUrl() {
9759
9950
  if (!state) return null;
9760
9951
  return `https://${hostname2}.${state.domain}`;
9761
9952
  }
9762
- var TAG22 = "[claude-session-manager:wrapper]";
9953
+ var TAG23 = "[claude-session-manager:wrapper]";
9763
9954
  async function refuseIfClaudeAuthDead(c, route, sessionId) {
9764
9955
  const auth = await ensureAuth();
9765
9956
  if (auth.status !== "dead" && auth.status !== "missing") return null;
@@ -9777,12 +9968,12 @@ async function performSpawnWithInitialMessage(args) {
9777
9968
  const aboutOwner = await resolveOwnerProfileBlock(args.senderId, args.userId);
9778
9969
  const ownerMs = Date.now() - ownerStart;
9779
9970
  const aboutOwnerStatus = aboutOwner == null ? "absent" : "ok" in aboutOwner && aboutOwner.ok === false ? `unresolved:${aboutOwner.reason}` : "ok";
9780
- console.log(`${TAG22} about-owner-resolved status=${aboutOwnerStatus} ms=${ownerMs}`);
9971
+ console.log(`${TAG23} about-owner-resolved status=${aboutOwnerStatus} ms=${ownerMs}`);
9781
9972
  const dormantPlugins = computeDormantPlugins(args.senderId);
9782
9973
  const activePlugins = computeActivePlugins(args.senderId);
9783
9974
  const specialistDomains = computeSpecialistDomains(args.senderId);
9784
9975
  const tunnelUrl = resolveTunnelUrl();
9785
- console.log(`${TAG22} tunnel-url-resolved value=${tunnelUrl ?? "null"}`);
9976
+ console.log(`${TAG23} tunnel-url-resolved value=${tunnelUrl ?? "null"}`);
9786
9977
  const upstreamPayload = JSON.stringify({
9787
9978
  senderId: args.senderId,
9788
9979
  // Task 205 — pass userId through to the manager so it lands as
@@ -9809,24 +10000,24 @@ async function performSpawnWithInitialMessage(args) {
9809
10000
  // unshapely values.
9810
10001
  conversationNodeId: args.conversationNodeId
9811
10002
  });
9812
- console.log(`${TAG22} forward-spawn-start managerBase=${managerBase3("claude-session-manager:wrapper")} bytes=${upstreamPayload.length} hidden=${args.hidden} specialist=${args.specialist ?? "none"}`);
10003
+ console.log(`${TAG23} forward-spawn-start managerBase=${managerBase3("claude-session-manager:wrapper")} bytes=${upstreamPayload.length} hidden=${args.hidden} specialist=${args.specialist ?? "none"}`);
9813
10004
  const forwardStart = Date.now();
9814
10005
  const upstream = await fetch(`${managerBase3("claude-session-manager:wrapper")}/public-spawn`, {
9815
10006
  method: "POST",
9816
10007
  headers: { "content-type": "application/json" },
9817
10008
  body: upstreamPayload
9818
10009
  }).catch((err) => {
9819
- console.error(`${TAG22} fetch-failed op=spawn message=${err instanceof Error ? err.message : String(err)} ms=${Date.now() - forwardStart}`);
10010
+ console.error(`${TAG23} fetch-failed op=spawn message=${err instanceof Error ? err.message : String(err)} ms=${Date.now() - forwardStart}`);
9820
10011
  return null;
9821
10012
  });
9822
10013
  if (!upstream) return {
9823
10014
  response: new Response(JSON.stringify({ error: "manager-unreachable" }), { status: 503, headers: { "content-type": "application/json" } }),
9824
10015
  claudeSessionId: null
9825
10016
  };
9826
- console.log(`${TAG22} forward-spawn-done status=${upstream.status} ms=${Date.now() - forwardStart}`);
10017
+ console.log(`${TAG23} forward-spawn-done status=${upstream.status} ms=${Date.now() - forwardStart}`);
9827
10018
  if (args.initialMessage) {
9828
10019
  const inputBytes = Buffer.byteLength(args.initialMessage, "utf8");
9829
- console.log(`${TAG22} initial-message-inlined bytes=${inputBytes}`);
10020
+ console.log(`${TAG23} initial-message-inlined bytes=${inputBytes}`);
9830
10021
  }
9831
10022
  const bodyText = await upstream.text().catch(() => "");
9832
10023
  let claudeSessionId = null;
@@ -9861,7 +10052,7 @@ app15.post("/", async (c) => {
9861
10052
  if (refusal) return refusal;
9862
10053
  const senderId = getAccountIdForSession(cacheKey) ?? "";
9863
10054
  if (!senderId) {
9864
- console.error(`${TAG22} reject reason=no-account-id cacheKey-prefix=${cacheKey.slice(0, 8)}`);
10055
+ console.error(`${TAG23} reject reason=no-account-id cacheKey-prefix=${cacheKey.slice(0, 8)}`);
9865
10056
  return c.json({ error: "admin-account-not-resolved" }, 500);
9866
10057
  }
9867
10058
  const userId = getUserIdForSession(cacheKey) ?? void 0;
@@ -9870,7 +10061,7 @@ app15.post("/", async (c) => {
9870
10061
  const permissionMode = typeof body.permissionMode === "string" ? body.permissionMode : void 0;
9871
10062
  const specialist = typeof body.specialist === "string" && /^[A-Za-z0-9_-]{1,64}$/.test(body.specialist) ? body.specialist : void 0;
9872
10063
  const model = typeof body.model === "string" && /^[A-Za-z0-9._-]{1,64}$/.test(body.model) ? body.model : void 0;
9873
- console.log(`${TAG22} spawn-request-in surface=cookie accountId=${senderId.slice(0, 8)} userId=${userId ? userId.slice(0, 8) : "absent"} channel=${channel} permissionMode=${permissionMode ?? "default"} specialist=${specialist ?? "none"} model=${model ?? "default"} initialMessage=${initialMessage ? "yes" : "no"}`);
10064
+ console.log(`${TAG23} spawn-request-in surface=cookie accountId=${senderId.slice(0, 8)} userId=${userId ? userId.slice(0, 8) : "absent"} channel=${channel} permissionMode=${permissionMode ?? "default"} specialist=${specialist ?? "none"} model=${model ?? "default"} initialMessage=${initialMessage ? "yes" : "no"}`);
9874
10065
  const conversationNodeId = cacheKey ? getSessionIdForSession(cacheKey) : void 0;
9875
10066
  const { response, claudeSessionId } = await performSpawnWithInitialMessage({
9876
10067
  senderId,
@@ -9889,13 +10080,13 @@ app15.post("/", async (c) => {
9889
10080
  claudeSessionId,
9890
10081
  senderId
9891
10082
  );
9892
- console.log(`${TAG22} route-done surface=cookie status=${response.status} route-ms=${Date.now() - routeStart}`);
10083
+ console.log(`${TAG23} route-done surface=cookie status=${response.status} route-ms=${Date.now() - routeStart}`);
9893
10084
  return response;
9894
10085
  });
9895
10086
  var claude_sessions_default = app15;
9896
10087
 
9897
10088
  // server/routes/admin/log-ingest.ts
9898
- var TAG23 = "[log-ingest]";
10089
+ var TAG24 = "[log-ingest]";
9899
10090
  var TAG_PATTERN = /^[A-Za-z0-9_:-]{1,32}$/;
9900
10091
  var LEVELS = /* @__PURE__ */ new Set(["debug", "info", "warn", "error"]);
9901
10092
  var MAX_LINE_BYTES = 4096;
@@ -9906,7 +10097,7 @@ function isLoopbackAddr(addr) {
9906
10097
  app16.post("/", async (c) => {
9907
10098
  const remoteAddr = c.env?.incoming?.socket?.remoteAddress ?? "";
9908
10099
  if (!isLoopbackAddr(remoteAddr)) {
9909
- console.error(`${TAG23} reject reason=non-loopback remoteAddr=${remoteAddr}`);
10100
+ console.error(`${TAG24} reject reason=non-loopback remoteAddr=${remoteAddr}`);
9910
10101
  return c.json({ error: "log-ingest-loopback-only" }, 403);
9911
10102
  }
9912
10103
  const xffHeader = c.env?.incoming?.headers?.["x-forwarded-for"];
@@ -9915,7 +10106,7 @@ app16.post("/", async (c) => {
9915
10106
  const tokens = xffRaw.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
9916
10107
  const offender = tokens.find((t) => !isLoopbackAddr(t));
9917
10108
  if (offender !== void 0) {
9918
- console.error(`${TAG23} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
10109
+ console.error(`${TAG24} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
9919
10110
  return c.json({ error: "log-ingest-loopback-only" }, 403);
9920
10111
  }
9921
10112
  }
@@ -9957,18 +10148,18 @@ var ALLOWED_EVENTS = /* @__PURE__ */ new Set([
9957
10148
  ]);
9958
10149
  var app17 = new Hono();
9959
10150
  app17.post("/", async (c) => {
9960
- const TAG31 = "[admin:events]";
10151
+ const TAG32 = "[admin:events]";
9961
10152
  let body;
9962
10153
  try {
9963
10154
  body = await c.req.json();
9964
10155
  } catch (err) {
9965
10156
  const detail = err instanceof Error ? err.message : String(err);
9966
- console.error(`${TAG31} reject reason=body-not-json detail=${detail}`);
10157
+ console.error(`${TAG32} reject reason=body-not-json detail=${detail}`);
9967
10158
  return c.json({ ok: false, detail: "Request body was not valid JSON" }, 400);
9968
10159
  }
9969
10160
  const event = typeof body.event === "string" ? body.event : "";
9970
10161
  if (!ALLOWED_EVENTS.has(event)) {
9971
- console.error(`${TAG31} reject reason=event-not-allowed event=${JSON.stringify(event)}`);
10162
+ console.error(`${TAG32} reject reason=event-not-allowed event=${JSON.stringify(event)}`);
9972
10163
  return c.json({ ok: false, detail: `Event "${event}" is not allowed` }, 400);
9973
10164
  }
9974
10165
  const rawFields = body.fields && typeof body.fields === "object" ? body.fields : {};
@@ -9993,7 +10184,7 @@ var events_default = app17;
9993
10184
  import { createReadStream as createReadStream2 } from "fs";
9994
10185
  import { readdir as readdir3, readFile as readFile4, stat as stat4, mkdir as mkdir3, writeFile as writeFile4, unlink as unlink2 } from "fs/promises";
9995
10186
  import { realpathSync as realpathSync4 } from "fs";
9996
- import { basename as basename5, dirname as dirname4, join as join16, resolve as resolve18, sep as sep5 } from "path";
10187
+ import { basename as basename6, dirname as dirname5, join as join17, resolve as resolve18, sep as sep5 } from "path";
9997
10188
  import { Readable as Readable2 } from "stream";
9998
10189
 
9999
10190
  // app/lib/data-path.ts
@@ -10373,7 +10564,7 @@ async function cascadeDeleteDocument(params) {
10373
10564
 
10374
10565
  // app/lib/file-index.ts
10375
10566
  import * as fsp from "fs/promises";
10376
- import { resolve as resolve17, relative as relative2, join as join15, basename as basename4, extname as extname2, sep as sep4 } from "path";
10567
+ import { resolve as resolve17, relative as relative2, join as join16, basename as basename5, extname as extname2, sep as sep4 } from "path";
10377
10568
  import { tmpdir as tmpdir2 } from "os";
10378
10569
  import { execFile as execFile2 } from "child_process";
10379
10570
  import { promisify as promisify2 } from "util";
@@ -10444,7 +10635,7 @@ async function extractPdf(absolute) {
10444
10635
  return stdout.trim();
10445
10636
  }
10446
10637
  async function ocrPdf(absolute) {
10447
- const outPdf = join15(tmpdir2(), `file-index-ocr-${process.pid}-${Date.now()}.pdf`);
10638
+ const outPdf = join16(tmpdir2(), `file-index-ocr-${process.pid}-${Date.now()}.pdf`);
10448
10639
  try {
10449
10640
  await execFileAsync2(
10450
10641
  "ocrmypdf",
@@ -10500,13 +10691,13 @@ async function extractFileContent(absolute) {
10500
10691
  }
10501
10692
  }
10502
10693
  async function readDisplayName(absolute) {
10503
- const dir = absolute.slice(0, absolute.length - basename4(absolute).length);
10504
- const base = basename4(absolute);
10694
+ const dir = absolute.slice(0, absolute.length - basename5(absolute).length);
10695
+ const base = basename5(absolute);
10505
10696
  const dot = base.lastIndexOf(".");
10506
10697
  const stem = dot === -1 ? base : base.slice(0, dot);
10507
10698
  if (base === `${stem}.meta.json`) return null;
10508
10699
  try {
10509
- const raw = await fsp.readFile(join15(dir, `${stem}.meta.json`), "utf-8");
10700
+ const raw = await fsp.readFile(join16(dir, `${stem}.meta.json`), "utf-8");
10510
10701
  const meta = JSON.parse(raw);
10511
10702
  const dn = meta.displayName ?? meta.filename;
10512
10703
  return typeof dn === "string" && dn.length > 0 ? dn : null;
@@ -10526,7 +10717,7 @@ async function walkSubtree(root, dataRoot, out) {
10526
10717
  let clean = true;
10527
10718
  for (const entry of entries) {
10528
10719
  if (entry.isSymbolicLink()) continue;
10529
- const abs = join15(root, entry.name);
10720
+ const abs = join16(root, entry.name);
10530
10721
  if (entry.isDirectory()) {
10531
10722
  const sub = await walkSubtree(abs, dataRoot, out);
10532
10723
  if (!sub.clean) clean = false;
@@ -10638,7 +10829,7 @@ async function cascadeTrashLinkedKnowledgeDocs(session, accountId, relativePaths
10638
10829
  return { kds, sections, chunks };
10639
10830
  }
10640
10831
  async function buildArtifact(accountId, wf, embed2) {
10641
- const name = basename4(wf.absolute);
10832
+ const name = basename5(wf.absolute);
10642
10833
  const { content, route } = await extractFileContent(wf.absolute);
10643
10834
  const displayName = await readDisplayName(wf.absolute);
10644
10835
  const embedInput = `${name}
@@ -10797,7 +10988,7 @@ async function dropFileIndex(accountId, relativePath, opts) {
10797
10988
  var UUID_RE3 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
10798
10989
  async function readMeta(absDir, baseName) {
10799
10990
  try {
10800
- const raw = await readFile4(join16(absDir, `${baseName}.meta.json`), "utf8");
10991
+ const raw = await readFile4(join17(absDir, `${baseName}.meta.json`), "utf8");
10801
10992
  const parsed = JSON.parse(raw);
10802
10993
  if (typeof parsed?.filename === "string") {
10803
10994
  return { filename: parsed.filename, mimeType: typeof parsed.mimeType === "string" ? parsed.mimeType : void 0 };
@@ -10835,7 +11026,7 @@ async function readAccountNames() {
10835
11026
  }
10836
11027
  async function enrich(absolute, entry, accountNames) {
10837
11028
  if (entry.kind === "directory" && UUID_RE3.test(entry.name)) {
10838
- const meta = await readMeta(join16(absolute, entry.name), entry.name);
11029
+ const meta = await readMeta(join17(absolute, entry.name), entry.name);
10839
11030
  if (meta?.filename) {
10840
11031
  entry.displayName = meta.filename;
10841
11032
  entry.mimeType = meta.mimeType;
@@ -10933,7 +11124,7 @@ app18.get("/", requireAdminSession, async (c) => {
10933
11124
  continue;
10934
11125
  }
10935
11126
  try {
10936
- const entryPath = join16(absolute, name);
11127
+ const entryPath = join17(absolute, name);
10937
11128
  const s = await stat4(entryPath);
10938
11129
  entries.push({
10939
11130
  name,
@@ -10999,7 +11190,7 @@ app18.get("/download", requireAdminSession, async (c) => {
10999
11190
  if (!info.isFile()) {
11000
11191
  return c.json({ error: "Path is not a file" }, 400);
11001
11192
  }
11002
- const filename = basename5(absolute);
11193
+ const filename = basename6(absolute);
11003
11194
  const mimeType = detectMimeType(absolute);
11004
11195
  const nodeStream = createReadStream2(absolute);
11005
11196
  const webStream = Readable2.toWeb(nodeStream);
@@ -11056,7 +11247,7 @@ app18.post("/upload", requireAdminSession, async (c) => {
11056
11247
  error: `Unsupported file type: "${file.type}". Supported: ${[...SUPPORTED_MIME_TYPES].join(", ")}.`
11057
11248
  }, 422);
11058
11249
  }
11059
- const safeName = basename5(file.name).replace(/[\0/\\]/g, "_");
11250
+ const safeName = basename6(file.name).replace(/[\0/\\]/g, "_");
11060
11251
  const finalName = `${Date.now()}-${safeName}`;
11061
11252
  const destDir = resolve18(DATA_ROOT, "uploads", accountId);
11062
11253
  const destPath = resolve18(destDir, finalName);
@@ -11108,7 +11299,7 @@ app18.delete("/", requireAdminSession, async (c) => {
11108
11299
  console.error(`[data] account-scope-blocked endpoint="DELETE /api/admin/files" path="${relPath}" account=${accountId.slice(0, 8)}\u2026`);
11109
11300
  return c.json({ error: "Not found" }, 404);
11110
11301
  }
11111
- const base = basename5(absolute);
11302
+ const base = basename6(absolute);
11112
11303
  const segments = relPath.split("/").filter(Boolean);
11113
11304
  if (base === "account.json" || segments.includes(".git")) {
11114
11305
  console.error(`[data] file-delete blocked path="${relPath}" reason="protected"`);
@@ -11124,7 +11315,7 @@ app18.delete("/", requireAdminSession, async (c) => {
11124
11315
  }
11125
11316
  const dot = base.lastIndexOf(".");
11126
11317
  const stem = dot === -1 ? base : base.slice(0, dot);
11127
- const sidecarPath = UUID_RE3.test(stem) && base !== `${stem}.meta.json` ? join16(dirname4(absolute), `${stem}.meta.json`) : null;
11318
+ const sidecarPath = UUID_RE3.test(stem) && base !== `${stem}.meta.json` ? join17(dirname5(absolute), `${stem}.meta.json`) : null;
11128
11319
  await unlink2(absolute);
11129
11320
  if (sidecarPath) {
11130
11321
  try {
@@ -12100,6 +12291,12 @@ var FILTER_TOP_LEVEL_LABELS = Object.freeze(
12100
12291
  "Property",
12101
12292
  "Viewing",
12102
12293
  "Offer",
12294
+ "Job",
12295
+ "QuoteDocument",
12296
+ "InboundInvoice",
12297
+ "SubContractor",
12298
+ "SubInvoice",
12299
+ "WhatsAppGroup",
12103
12300
  "AdminConversation",
12104
12301
  "PublicConversation",
12105
12302
  "Task",
@@ -13167,7 +13364,7 @@ var graph_default_view_default = app24;
13167
13364
 
13168
13365
  // server/routes/admin/sidebar-artefacts.ts
13169
13366
  import { readdir as readdir4, stat as stat5 } from "fs/promises";
13170
- import { resolve as resolve19, relative as relative3, isAbsolute, sep as sep6, basename as basename6 } from "path";
13367
+ import { resolve as resolve19, relative as relative3, isAbsolute, sep as sep6, basename as basename7 } from "path";
13171
13368
  import { existsSync as existsSync19 } from "fs";
13172
13369
  var LIMIT = 50;
13173
13370
  var ADMIN_AGENT_FILES = ["IDENTITY.md", "SOUL.md", "KNOWLEDGE.md"];
@@ -13219,7 +13416,7 @@ async function fetchAccountFileArtefacts(accountId) {
13219
13416
  const modifiedAt = r.get("modifiedAt") ?? "";
13220
13417
  return {
13221
13418
  id: `account-file:${relativePath}`,
13222
- name: displayName ?? basename6(relativePath),
13419
+ name: displayName ?? basename7(relativePath),
13223
13420
  kind: "account-file",
13224
13421
  updatedAt: modifiedAt,
13225
13422
  mimeType,
@@ -13625,10 +13822,10 @@ var system_stats_default = app30;
13625
13822
 
13626
13823
  // server/routes/admin/health.ts
13627
13824
  import { existsSync as existsSync20, readFileSync as readFileSync18 } from "fs";
13628
- import { resolve as resolve20, join as join17 } from "path";
13825
+ import { resolve as resolve20, join as join18 } from "path";
13629
13826
  var PLATFORM_ROOT6 = process.env.MAXY_PLATFORM_ROOT ?? resolve20(process.cwd(), "..");
13630
13827
  var brandHostname = "maxy";
13631
- var brandJsonPath = join17(PLATFORM_ROOT6, "config", "brand.json");
13828
+ var brandJsonPath = join18(PLATFORM_ROOT6, "config", "brand.json");
13632
13829
  if (existsSync20(brandJsonPath)) {
13633
13830
  try {
13634
13831
  const brand = JSON.parse(readFileSync18(brandJsonPath, "utf-8"));
@@ -13693,7 +13890,7 @@ var health_default2 = app31;
13693
13890
 
13694
13891
  // server/routes/admin/linkedin-ingest.ts
13695
13892
  import { randomUUID as randomUUID9 } from "crypto";
13696
- var TAG24 = "[linkedin-ingest-route]";
13893
+ var TAG25 = "[linkedin-ingest-route]";
13697
13894
  var UUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
13698
13895
  var ISO = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?(?:Z|[+-]\d{2}:\d{2})$/;
13699
13896
  var LINKEDIN_URL = /^https:\/\/www\.linkedin\.com\//;
@@ -13797,29 +13994,29 @@ app32.post("/", requireAdminSession, async (c) => {
13797
13994
  try {
13798
13995
  body = await c.req.json();
13799
13996
  } catch {
13800
- console.error(TAG24 + " rejected status=400 reason=schema:body-not-json");
13997
+ console.error(TAG25 + " rejected status=400 reason=schema:body-not-json");
13801
13998
  return c.json({ ok: false, error: "schema", reason: "body-not-json" }, 400);
13802
13999
  }
13803
14000
  const v = validate(body);
13804
14001
  if (!v.ok) {
13805
- console.error(TAG24 + " rejected status=" + v.status + " reason=" + v.reason + " missing=" + v.missing.join(","));
14002
+ console.error(TAG25 + " rejected status=" + v.status + " reason=" + v.reason + " missing=" + v.missing.join(","));
13806
14003
  return c.json({ ok: false, error: v.error, reason: v.reason, missing: v.missing }, v.status);
13807
14004
  }
13808
14005
  const envelope = v.envelope;
13809
14006
  const cacheKey = c.var.cacheKey ?? "";
13810
14007
  const senderId = getAccountIdForSession(cacheKey) ?? "";
13811
14008
  if (!senderId) {
13812
- console.error(TAG24 + " rejected status=500 reason=admin-account-not-resolved");
14009
+ console.error(TAG25 + " rejected status=500 reason=admin-account-not-resolved");
13813
14010
  return c.json({ ok: false, error: "admin-account-not-resolved" }, 500);
13814
14011
  }
13815
14012
  const payloadBytes = JSON.stringify(envelope).length;
13816
14013
  console.log(
13817
- TAG24 + " received kind=" + envelope.kind + " account=" + senderId.slice(0, 8) + " pageUrl=" + envelope.pageUrl + " dispatchId=" + envelope.dispatchId + " payloadBytes=" + payloadBytes
14014
+ TAG25 + " received kind=" + envelope.kind + " account=" + senderId.slice(0, 8) + " pageUrl=" + envelope.pageUrl + " dispatchId=" + envelope.dispatchId + " payloadBytes=" + payloadBytes
13818
14015
  );
13819
14016
  const initialMessage = buildInitialMessage(envelope);
13820
14017
  const spawnStart = Date.now();
13821
14018
  const sessionId = randomUUID9();
13822
- console.log(TAG24 + " route target=rc-spawn dispatchId=" + envelope.dispatchId + " sessionId=" + sessionId.slice(0, 8));
14019
+ console.log(TAG25 + " route target=rc-spawn dispatchId=" + envelope.dispatchId + " sessionId=" + sessionId.slice(0, 8));
13823
14020
  const spawned = await managerRcSpawn({
13824
14021
  sessionId,
13825
14022
  initialMessage,
@@ -13828,12 +14025,12 @@ app32.post("/", requireAdminSession, async (c) => {
13828
14025
  });
13829
14026
  if ("error" in spawned) {
13830
14027
  console.error(
13831
- TAG24 + " dispatch-failed dispatchId=" + envelope.dispatchId + " status=" + spawned.status + " ms=" + (Date.now() - spawnStart) + " message=" + spawned.error
14028
+ TAG25 + " dispatch-failed dispatchId=" + envelope.dispatchId + " status=" + spawned.status + " ms=" + (Date.now() - spawnStart) + " message=" + spawned.error
13832
14029
  );
13833
14030
  return c.json({ ok: false, error: "dispatch-failed", upstreamStatus: spawned.status, detail: spawned.error }, 502);
13834
14031
  }
13835
14032
  console.log(
13836
- TAG24 + " dispatched dispatchId=" + envelope.dispatchId + " taskId=" + spawned.sessionId + " ms=" + (Date.now() - spawnStart)
14033
+ TAG25 + " dispatched dispatchId=" + envelope.dispatchId + " taskId=" + spawned.sessionId + " ms=" + (Date.now() - spawnStart)
13837
14034
  );
13838
14035
  return c.json({ ok: true, dispatchId: envelope.dispatchId, taskId: spawned.sessionId }, 202);
13839
14036
  });
@@ -13841,7 +14038,7 @@ var linkedin_ingest_default = app32;
13841
14038
 
13842
14039
  // server/routes/admin/post-turn-context.ts
13843
14040
  import neo4j2 from "neo4j-driver";
13844
- var TAG25 = "[post-turn-context]";
14041
+ var TAG26 = "[post-turn-context]";
13845
14042
  var STRIPPED_PROPERTIES2 = /* @__PURE__ */ new Set([
13846
14043
  "embedding",
13847
14044
  "passwordHash",
@@ -13883,7 +14080,7 @@ var app33 = new Hono();
13883
14080
  app33.get("/", async (c) => {
13884
14081
  const remoteAddr = c.env?.incoming?.socket?.remoteAddress ?? "";
13885
14082
  if (!isLoopbackAddr2(remoteAddr)) {
13886
- console.error(`${TAG25} reject reason=non-loopback remoteAddr=${remoteAddr}`);
14083
+ console.error(`${TAG26} reject reason=non-loopback remoteAddr=${remoteAddr}`);
13887
14084
  return c.json({ error: "post-turn-context-loopback-only" }, 403);
13888
14085
  }
13889
14086
  const xffHeader = c.env?.incoming?.headers?.["x-forwarded-for"];
@@ -13892,7 +14089,7 @@ app33.get("/", async (c) => {
13892
14089
  const tokens = xffRaw.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
13893
14090
  const offender = tokens.find((t) => !isLoopbackAddr2(t));
13894
14091
  if (offender !== void 0) {
13895
- console.error(`${TAG25} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
14092
+ console.error(`${TAG26} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
13896
14093
  return c.json({ error: "post-turn-context-loopback-only" }, 403);
13897
14094
  }
13898
14095
  }
@@ -13924,7 +14121,7 @@ app33.get("/", async (c) => {
13924
14121
  writes.sort((a, b) => a.sortKey.localeCompare(b.sortKey));
13925
14122
  const total = Date.now() - started;
13926
14123
  console.log(
13927
- `${TAG25} sessionId=${sessionId} accountId=${accountId} writes=${writes.length} ms=${total}`
14124
+ `${TAG26} sessionId=${sessionId} accountId=${accountId} writes=${writes.length} ms=${total}`
13928
14125
  );
13929
14126
  return c.json({
13930
14127
  writes: writes.map(({ elementId, labels, properties }) => ({ elementId, labels, properties }))
@@ -13933,7 +14130,7 @@ app33.get("/", async (c) => {
13933
14130
  const elapsed = Date.now() - started;
13934
14131
  const message = err instanceof Error ? err.message : String(err);
13935
14132
  console.error(
13936
- `${TAG25} neo4j-unreachable sessionId=${sessionId} ms=${elapsed} err="${message}"`
14133
+ `${TAG26} neo4j-unreachable sessionId=${sessionId} ms=${elapsed} err="${message}"`
13937
14134
  );
13938
14135
  return c.json({ error: `post-turn-context unavailable: ${message}` }, 503);
13939
14136
  } finally {
@@ -13944,7 +14141,7 @@ var post_turn_context_default = app33;
13944
14141
 
13945
14142
  // server/routes/admin/public-session-context.ts
13946
14143
  import neo4j3 from "neo4j-driver";
13947
- var TAG26 = "[public-session-context]";
14144
+ var TAG27 = "[public-session-context]";
13948
14145
  var STRIPPED_PROPERTIES3 = /* @__PURE__ */ new Set([
13949
14146
  "embedding",
13950
14147
  "passwordHash",
@@ -13986,7 +14183,7 @@ var app34 = new Hono();
13986
14183
  app34.get("/", async (c) => {
13987
14184
  const remoteAddr = c.env?.incoming?.socket?.remoteAddress ?? "";
13988
14185
  if (!isLoopbackAddr3(remoteAddr)) {
13989
- console.error(`${TAG26} reject reason=non-loopback remoteAddr=${remoteAddr}`);
14186
+ console.error(`${TAG27} reject reason=non-loopback remoteAddr=${remoteAddr}`);
13990
14187
  return c.json({ error: "public-session-context-loopback-only" }, 403);
13991
14188
  }
13992
14189
  const xffHeader = c.env?.incoming?.headers?.["x-forwarded-for"];
@@ -13995,7 +14192,7 @@ app34.get("/", async (c) => {
13995
14192
  const tokens = xffRaw.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
13996
14193
  const offender = tokens.find((t) => !isLoopbackAddr3(t));
13997
14194
  if (offender !== void 0) {
13998
- console.error(`${TAG26} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
14195
+ console.error(`${TAG27} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
13999
14196
  return c.json({ error: "public-session-context-loopback-only" }, 403);
14000
14197
  }
14001
14198
  }
@@ -14026,7 +14223,7 @@ app34.get("/", async (c) => {
14026
14223
  writes.sort((a, b) => a.sortKey.localeCompare(b.sortKey));
14027
14224
  const total = Date.now() - started;
14028
14225
  console.log(
14029
- `${TAG26} sliceToken=${sliceToken.slice(0, 8)} writes=${writes.length} ms=${total}`
14226
+ `${TAG27} sliceToken=${sliceToken.slice(0, 8)} writes=${writes.length} ms=${total}`
14030
14227
  );
14031
14228
  return c.json({
14032
14229
  writes: writes.map(({ elementId, labels, properties }) => ({ elementId, labels, properties }))
@@ -14035,7 +14232,7 @@ app34.get("/", async (c) => {
14035
14232
  const elapsed = Date.now() - started;
14036
14233
  const message = err instanceof Error ? err.message : String(err);
14037
14234
  console.error(
14038
- `${TAG26} neo4j-unreachable sliceToken=${sliceToken.slice(0, 8)} ms=${elapsed} err="${message}"`
14235
+ `${TAG27} neo4j-unreachable sliceToken=${sliceToken.slice(0, 8)} ms=${elapsed} err="${message}"`
14039
14236
  );
14040
14237
  return c.json({ error: `public-session-context unavailable: ${message}` }, 503);
14041
14238
  } finally {
@@ -14045,7 +14242,7 @@ app34.get("/", async (c) => {
14045
14242
  var public_session_context_default = app34;
14046
14243
 
14047
14244
  // server/routes/admin/public-session-exit.ts
14048
- var TAG27 = "[public-session-exit-route]";
14245
+ var TAG28 = "[public-session-exit-route]";
14049
14246
  function isLoopbackAddr4(addr) {
14050
14247
  return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
14051
14248
  }
@@ -14053,7 +14250,7 @@ var app35 = new Hono();
14053
14250
  app35.post("/", async (c) => {
14054
14251
  const remoteAddr = c.env?.incoming?.socket?.remoteAddress ?? "";
14055
14252
  if (!isLoopbackAddr4(remoteAddr)) {
14056
- console.error(`${TAG27} reject reason=non-loopback remoteAddr=${remoteAddr}`);
14253
+ console.error(`${TAG28} reject reason=non-loopback remoteAddr=${remoteAddr}`);
14057
14254
  return c.json({ error: "public-session-exit-loopback-only" }, 403);
14058
14255
  }
14059
14256
  const xffHeader = c.env?.incoming?.headers?.["x-forwarded-for"];
@@ -14062,7 +14259,7 @@ app35.post("/", async (c) => {
14062
14259
  const tokens = xffRaw.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
14063
14260
  const offender = tokens.find((t) => !isLoopbackAddr4(t));
14064
14261
  if (offender !== void 0) {
14065
- console.error(`${TAG27} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
14262
+ console.error(`${TAG28} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
14066
14263
  return c.json({ error: "public-session-exit-loopback-only" }, 403);
14067
14264
  }
14068
14265
  }
@@ -14080,7 +14277,7 @@ app35.post("/", async (c) => {
14080
14277
  var public_session_exit_default = app35;
14081
14278
 
14082
14279
  // server/routes/admin/access-session-evict.ts
14083
- var TAG28 = "[access-session-evict]";
14280
+ var TAG29 = "[access-session-evict]";
14084
14281
  function isLoopbackAddr5(addr) {
14085
14282
  return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
14086
14283
  }
@@ -14088,7 +14285,7 @@ var app36 = new Hono();
14088
14285
  app36.post("/", async (c) => {
14089
14286
  const remoteAddr = c.env?.incoming?.socket?.remoteAddress ?? "";
14090
14287
  if (!isLoopbackAddr5(remoteAddr)) {
14091
- console.error(`${TAG28} reject reason=non-loopback remoteAddr=${remoteAddr}`);
14288
+ console.error(`${TAG29} reject reason=non-loopback remoteAddr=${remoteAddr}`);
14092
14289
  return c.json({ error: "access-session-evict-loopback-only" }, 403);
14093
14290
  }
14094
14291
  const xffHeader = c.env?.incoming?.headers?.["x-forwarded-for"];
@@ -14097,7 +14294,7 @@ app36.post("/", async (c) => {
14097
14294
  const tokens = xffRaw.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
14098
14295
  const offender = tokens.find((t) => !isLoopbackAddr5(t));
14099
14296
  if (offender !== void 0) {
14100
- console.error(`${TAG28} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
14297
+ console.error(`${TAG29} reject reason=xff-non-loopback xff=${JSON.stringify(xffRaw)}`);
14101
14298
  return c.json({ error: "access-session-evict-loopback-only" }, 403);
14102
14299
  }
14103
14300
  }
@@ -14110,7 +14307,7 @@ app36.post("/", async (c) => {
14110
14307
  const grantId = typeof body.grantId === "string" ? body.grantId.trim() : "";
14111
14308
  if (!grantId) return c.json({ error: "grantId required" }, 400);
14112
14309
  const dropped = evictAccessSessionsByGrant(grantId);
14113
- console.log(`${TAG28} grantId=${grantId} dropped=${dropped}`);
14310
+ console.log(`${TAG29} grantId=${grantId} dropped=${dropped}`);
14114
14311
  return c.json({ ok: true, dropped });
14115
14312
  });
14116
14313
  var access_session_evict_default = app36;
@@ -14390,7 +14587,7 @@ async function generateNewMagicToken(grantId) {
14390
14587
  }
14391
14588
 
14392
14589
  // server/routes/access/verify-token.ts
14393
- var TAG29 = "[access-verify]";
14590
+ var TAG30 = "[access-verify]";
14394
14591
  var MINT_TAG = "[access-session-mint]";
14395
14592
  var COOKIE_NAME = "__access_session";
14396
14593
  var app39 = new Hono();
@@ -14409,39 +14606,39 @@ app39.post("/", async (c) => {
14409
14606
  }
14410
14607
  const rateMsg = checkAccessRateLimit(ip, agentSlug);
14411
14608
  if (rateMsg) {
14412
- console.error(`${TAG29} grantId=- agentSlug=${agentSlug} result=rate-limited ip=${ip}`);
14609
+ console.error(`${TAG30} grantId=- agentSlug=${agentSlug} result=rate-limited ip=${ip}`);
14413
14610
  return c.json({ error: rateMsg }, 429);
14414
14611
  }
14415
14612
  const grant = await findGrantByMagicToken(token);
14416
14613
  if (!grant) {
14417
14614
  recordAccessFailedAttempt(ip, agentSlug);
14418
- console.error(`${TAG29} grantId=- agentSlug=${agentSlug} result=notfound ip=${ip}`);
14615
+ console.error(`${TAG30} grantId=- agentSlug=${agentSlug} result=notfound ip=${ip}`);
14419
14616
  return c.json({ error: "invalid-or-expired-link" }, 401);
14420
14617
  }
14421
14618
  if (grant.agentSlug !== agentSlug) {
14422
14619
  recordAccessFailedAttempt(ip, agentSlug);
14423
14620
  console.error(
14424
- `${TAG29} grantId=${grant.grantId} agentSlug=${agentSlug} result=agent-mismatch grantAgent=${grant.agentSlug} ip=${ip}`
14621
+ `${TAG30} grantId=${grant.grantId} agentSlug=${agentSlug} result=agent-mismatch grantAgent=${grant.agentSlug} ip=${ip}`
14425
14622
  );
14426
14623
  return c.json({ error: "invalid-or-expired-link" }, 401);
14427
14624
  }
14428
14625
  if (grant.status === "expired" || grant.status === "revoked") {
14429
14626
  recordAccessFailedAttempt(ip, agentSlug);
14430
14627
  console.error(
14431
- `${TAG29} grantId=${grant.grantId} agentSlug=${agentSlug} result=expired status=${grant.status} ip=${ip}`
14628
+ `${TAG30} grantId=${grant.grantId} agentSlug=${agentSlug} result=expired status=${grant.status} ip=${ip}`
14432
14629
  );
14433
14630
  return c.json({ error: "access-no-longer-valid" }, 401);
14434
14631
  }
14435
14632
  if (grant.expiresAt !== null && grant.expiresAt < Date.now()) {
14436
14633
  recordAccessFailedAttempt(ip, agentSlug);
14437
14634
  console.error(
14438
- `${TAG29} grantId=${grant.grantId} agentSlug=${agentSlug} result=expired reason=expiresAt-past ip=${ip}`
14635
+ `${TAG30} grantId=${grant.grantId} agentSlug=${agentSlug} result=expired reason=expiresAt-past ip=${ip}`
14439
14636
  );
14440
14637
  return c.json({ error: "access-no-longer-valid" }, 401);
14441
14638
  }
14442
14639
  if (!grant.sliceToken) {
14443
14640
  console.error(
14444
- `${TAG29} grantId=${grant.grantId} agentSlug=${agentSlug} result=no-slice-token reason=schema-violation`
14641
+ `${TAG30} grantId=${grant.grantId} agentSlug=${agentSlug} result=no-slice-token reason=schema-violation`
14445
14642
  );
14446
14643
  return c.json({ error: "grant-misconfigured" }, 500);
14447
14644
  }
@@ -14457,12 +14654,12 @@ app39.post("/", async (c) => {
14457
14654
  await consumeMagicTokenAndActivate(grant.grantId);
14458
14655
  } catch (err) {
14459
14656
  console.error(
14460
- `${TAG29} grantId=${grant.grantId} agentSlug=${agentSlug} result=consume-failed err="${err instanceof Error ? err.message : String(err)}"`
14657
+ `${TAG30} grantId=${grant.grantId} agentSlug=${agentSlug} result=consume-failed err="${err instanceof Error ? err.message : String(err)}"`
14461
14658
  );
14462
14659
  return c.json({ error: "verification-failed" }, 500);
14463
14660
  }
14464
14661
  clearAccessRateLimit(ip, agentSlug);
14465
- console.log(`${TAG29} grantId=${grant.grantId} agentSlug=${agentSlug} result=ok ip=${ip}`);
14662
+ console.log(`${TAG30} grantId=${grant.grantId} agentSlug=${agentSlug} result=ok ip=${ip}`);
14466
14663
  console.log(
14467
14664
  `${MINT_TAG} grantId=${grant.grantId} sliceToken=${grant.sliceToken.slice(0, 8)} agentSlug=${agentSlug} personId=${grant.personId ?? "none"}`
14468
14665
  );
@@ -14538,7 +14735,7 @@ async function sendMagicLinkEmail(payload) {
14538
14735
  }
14539
14736
 
14540
14737
  // server/routes/access/request-magic-link.ts
14541
- var TAG30 = "[access-request-link]";
14738
+ var TAG31 = "[access-request-link]";
14542
14739
  var app40 = new Hono();
14543
14740
  var VISITOR_MESSAGE = "If that email is on the invite list, a fresh link is on the way.";
14544
14741
  app40.post("/", async (c) => {
@@ -14555,18 +14752,18 @@ app40.post("/", async (c) => {
14555
14752
  }
14556
14753
  const rateMsg = checkRequestLinkRateLimit(contactValue);
14557
14754
  if (rateMsg) {
14558
- console.error(`${TAG30} contactValue=${maskContact(contactValue)} result=rate-limited`);
14755
+ console.error(`${TAG31} contactValue=${maskContact(contactValue)} result=rate-limited`);
14559
14756
  return c.json({ error: rateMsg }, 429);
14560
14757
  }
14561
14758
  recordRequestLinkAttempt(contactValue);
14562
14759
  const accountId = process.env.ACCOUNT_ID ?? "";
14563
14760
  if (!accountId) {
14564
- console.error(`${TAG30} contactValue=${maskContact(contactValue)} result=no-account-id`);
14761
+ console.error(`${TAG31} contactValue=${maskContact(contactValue)} result=no-account-id`);
14565
14762
  return c.json({ message: VISITOR_MESSAGE }, 200);
14566
14763
  }
14567
14764
  const grant = await findActiveGrantByContact(contactValue, agentSlug, accountId);
14568
14765
  if (!grant) {
14569
- console.log(`${TAG30} contactValue=${maskContact(contactValue)} result=notfound`);
14766
+ console.log(`${TAG31} contactValue=${maskContact(contactValue)} result=notfound`);
14570
14767
  return c.json({ message: VISITOR_MESSAGE }, 200);
14571
14768
  }
14572
14769
  let token;
@@ -14574,7 +14771,7 @@ app40.post("/", async (c) => {
14574
14771
  token = await generateNewMagicToken(grant.grantId);
14575
14772
  } catch (err) {
14576
14773
  console.error(
14577
- `${TAG30} contactValue=${maskContact(contactValue)} result=mint-failed err="${err instanceof Error ? err.message : String(err)}"`
14774
+ `${TAG31} contactValue=${maskContact(contactValue)} result=mint-failed err="${err instanceof Error ? err.message : String(err)}"`
14578
14775
  );
14579
14776
  return c.json({ message: VISITOR_MESSAGE }, 200);
14580
14777
  }
@@ -14606,12 +14803,12 @@ app40.post("/", async (c) => {
14606
14803
  });
14607
14804
  if (!sendResult.ok) {
14608
14805
  console.error(
14609
- `${TAG30} contactValue=${maskContact(contactValue)} result=send-failed err="${sendResult.error}"`
14806
+ `${TAG31} contactValue=${maskContact(contactValue)} result=send-failed err="${sendResult.error}"`
14610
14807
  );
14611
14808
  return c.json({ message: VISITOR_MESSAGE }, 200);
14612
14809
  }
14613
14810
  console.log(
14614
- `${TAG30} contactValue=${maskContact(contactValue)} result=ok messageId=${sendResult.messageId}`
14811
+ `${TAG31} contactValue=${maskContact(contactValue)} result=ok messageId=${sendResult.messageId}`
14615
14812
  );
14616
14813
  return c.json({ message: VISITOR_MESSAGE }, 200);
14617
14814
  });
@@ -14765,7 +14962,7 @@ var sites_default = app42;
14765
14962
  // app/lib/visitor-token.ts
14766
14963
  import { createHmac, randomBytes, timingSafeEqual } from "crypto";
14767
14964
  import { mkdirSync as mkdirSync4, readFileSync as readFileSync21, writeFileSync as writeFileSync7 } from "fs";
14768
- import { dirname as dirname5 } from "path";
14965
+ import { dirname as dirname6 } from "path";
14769
14966
  var TOKEN_PREFIX = "v1.";
14770
14967
  var SECRET_BYTES = 32;
14771
14968
  var DEFAULT_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
@@ -14782,7 +14979,7 @@ function getSecret() {
14782
14979
  }
14783
14980
  const fresh = randomBytes(SECRET_BYTES).toString("hex");
14784
14981
  try {
14785
- mkdirSync4(dirname5(VISITOR_TOKEN_SECRET_FILE), { recursive: true, mode: 448 });
14982
+ mkdirSync4(dirname6(VISITOR_TOKEN_SECRET_FILE), { recursive: true, mode: 448 });
14786
14983
  writeFileSync7(VISITOR_TOKEN_SECRET_FILE, fresh, { mode: 384, flag: "wx" });
14787
14984
  console.log(`[visitor-token] secret minted path=${VISITOR_TOKEN_SECRET_FILE}`);
14788
14985
  } catch {
@@ -14841,7 +15038,7 @@ var VISITOR_COOKIE_MAX_AGE_SECONDS = Math.floor(DEFAULT_TTL_MS / 1e3);
14841
15038
 
14842
15039
  // app/lib/brand-config.ts
14843
15040
  import { existsSync as existsSync22, readFileSync as readFileSync22 } from "fs";
14844
- import { join as join18 } from "path";
15041
+ import { join as join19 } from "path";
14845
15042
  var cached2 = null;
14846
15043
  var cachedAttempted = false;
14847
15044
  function readBrandConfig() {
@@ -14849,7 +15046,7 @@ function readBrandConfig() {
14849
15046
  cachedAttempted = true;
14850
15047
  const platformRoot2 = process.env.MAXY_PLATFORM_ROOT;
14851
15048
  if (!platformRoot2) return null;
14852
- const brandPath = join18(platformRoot2, "config", "brand.json");
15049
+ const brandPath = join19(platformRoot2, "config", "brand.json");
14853
15050
  if (!existsSync22(brandPath)) return null;
14854
15051
  try {
14855
15052
  cached2 = JSON.parse(readFileSync22(brandPath, "utf-8"));
@@ -16295,7 +16492,264 @@ var WaGateway = class {
16295
16492
  }
16296
16493
  };
16297
16494
 
16495
+ // app/lib/webchat/gateway/inbound-hub.ts
16496
+ var InboundHub2 = class {
16497
+ keys = /* @__PURE__ */ new Map();
16498
+ state(key) {
16499
+ let s = this.keys.get(key);
16500
+ if (!s) {
16501
+ s = { subscriber: null, ready: false, inFlight: [] };
16502
+ this.keys.set(key, s);
16503
+ }
16504
+ return s;
16505
+ }
16506
+ /** Route an inbound message. Delivers immediately only if the key's channel
16507
+ * server is attached AND has signalled readiness; otherwise queues it (cold
16508
+ * start, or the attach->ready window) so it is never written before the
16509
+ * channel server can consume it. Every accepted message is tracked in-flight
16510
+ * until it reaches a terminal state. */
16511
+ deliver(payload, now) {
16512
+ const s = this.state(payload.key);
16513
+ if (s.subscriber && s.ready) {
16514
+ s.subscriber(payload);
16515
+ s.inFlight.push({ payload, state: "drained", enqueuedAt: now, drainedAt: now });
16516
+ } else {
16517
+ s.inFlight.push({ payload, state: "queued", enqueuedAt: now, drainedAt: null });
16518
+ }
16519
+ }
16520
+ /** A key's channel server connected. Register the subscriber but do NOT
16521
+ * drain yet — the drain waits for markReady so nothing is written before the
16522
+ * channel server is consuming. Replaces any prior subscriber. */
16523
+ attach(key, subscriber) {
16524
+ const s = this.state(key);
16525
+ s.subscriber = subscriber;
16526
+ s.ready = false;
16527
+ }
16528
+ /** The key's channel server reported it is connected and consuming. Drain
16529
+ * every queued message to it in arrival order, marking each drained-awaiting-
16530
+ * ack, then route live. Returns the number drained. */
16531
+ markReady(key, now) {
16532
+ const s = this.state(key);
16533
+ s.ready = true;
16534
+ if (!s.subscriber) return 0;
16535
+ let count = 0;
16536
+ for (const e of s.inFlight) {
16537
+ if (e.state === "queued") {
16538
+ s.subscriber(e.payload);
16539
+ e.state = "drained";
16540
+ e.drainedAt = now;
16541
+ count++;
16542
+ }
16543
+ }
16544
+ return count;
16545
+ }
16546
+ /** The channel server confirmed it received (emitted) a drained message. The
16547
+ * message has reached the agent; drop it from in-flight. Returns whether a
16548
+ * matching in-flight message was found. */
16549
+ ack(key, messageId) {
16550
+ const s = this.keys.get(key);
16551
+ if (!s) return false;
16552
+ const i = s.inFlight.findIndex((e) => e.payload.messageId === messageId);
16553
+ if (i < 0) return false;
16554
+ s.inFlight.splice(i, 1);
16555
+ return true;
16556
+ }
16557
+ /** A key's channel server disconnected (session ended/restarting).
16558
+ * Subsequent messages queue again until re-attach + ready, so nothing is lost
16559
+ * across a restart. In-flight drained messages stay tracked for ack/sweep. */
16560
+ detach(key) {
16561
+ const s = this.keys.get(key);
16562
+ if (s) {
16563
+ s.subscriber = null;
16564
+ s.ready = false;
16565
+ }
16566
+ }
16567
+ /** Reconciliation from queue state alone — does not wait for a future inbound
16568
+ * to expose a loss. Returns (and removes) a terminal event for every message
16569
+ * that never reached the agent:
16570
+ * - a still-queued message older than queuedThresholdMs WHILE a subscriber
16571
+ * is attached -> inbound-undelivered (the readiness handoff broke);
16572
+ * - a still-queued message older than queuedThresholdMs with NO subscriber
16573
+ * attached -> spawn-undelivered (the channel server never attached: spawn
16574
+ * failed, or the session ended and never resumed);
16575
+ * - a drained message unacked past ackThresholdMs -> drain-undelivered (the
16576
+ * channel server never confirmed receipt — the silent cold-start drop).
16577
+ * Each message is emitted at most once (removed when emitted). */
16578
+ sweep(now, t) {
16579
+ const events = [];
16580
+ for (const [key, s] of this.keys) {
16581
+ s.inFlight = s.inFlight.filter((e) => {
16582
+ if (e.state === "queued" && s.subscriber && now - e.enqueuedAt > t.queuedThresholdMs) {
16583
+ events.push({ kind: "inbound-undelivered", key, messageId: e.payload.messageId, ageMs: now - e.enqueuedAt });
16584
+ return false;
16585
+ }
16586
+ if (e.state === "queued" && !s.subscriber && now - e.enqueuedAt > t.queuedThresholdMs) {
16587
+ events.push({ kind: "spawn-undelivered", key, messageId: e.payload.messageId, ageMs: now - e.enqueuedAt });
16588
+ return false;
16589
+ }
16590
+ if (e.state === "drained" && e.drainedAt != null && now - e.drainedAt > t.ackThresholdMs) {
16591
+ events.push({ kind: "drain-undelivered", key, messageId: e.payload.messageId, ageMs: now - e.drainedAt });
16592
+ return false;
16593
+ }
16594
+ return true;
16595
+ });
16596
+ }
16597
+ return events;
16598
+ }
16599
+ /** Whether a key currently has a live channel server attached. The gateway
16600
+ * uses this to decide whether an inbound needs a cold-start spawn/resume. */
16601
+ hasSubscriber(key) {
16602
+ return this.keys.get(key)?.subscriber != null;
16603
+ }
16604
+ /** Whether a key's channel server is attached AND has signalled readiness.
16605
+ * Used only to log delivery=immediate vs queued accurately. */
16606
+ isReady(key) {
16607
+ const s = this.keys.get(key);
16608
+ return s?.subscriber != null && s.ready;
16609
+ }
16610
+ };
16611
+
16612
+ // app/lib/webchat/gateway/routes.ts
16613
+ function createWebchatChannelRoutes(deps) {
16614
+ const app48 = new Hono();
16615
+ app48.get("/webchat-channel/inbound", (c) => {
16616
+ const key = c.req.query("key");
16617
+ if (!key) return c.json({ error: "key required" }, 400);
16618
+ return streamSSE(c, async (stream2) => {
16619
+ deps.hub.attach(key, (payload) => {
16620
+ void stream2.writeSSE({ data: JSON.stringify(payload) });
16621
+ });
16622
+ console.error(`[webchat:inbound] op=channel-attached key=${key}`);
16623
+ stream2.onAbort(() => {
16624
+ deps.hub.detach(key);
16625
+ console.error(`[webchat:inbound] op=channel-detached key=${key}`);
16626
+ });
16627
+ while (!stream2.aborted) {
16628
+ await stream2.sleep(15e3);
16629
+ await stream2.writeSSE({ data: "", event: "ping" });
16630
+ }
16631
+ });
16632
+ });
16633
+ app48.post("/webchat-channel/reply", async (c) => {
16634
+ const body = await c.req.json().catch(() => null);
16635
+ const key = body?.key;
16636
+ const text = body?.text;
16637
+ if (typeof key !== "string" || typeof text !== "string") {
16638
+ return c.json({ error: "key and text required" }, 400);
16639
+ }
16640
+ deps.onReply?.(key, Buffer.byteLength(text, "utf8"));
16641
+ return c.json({ ok: true });
16642
+ });
16643
+ app48.post("/webchat-channel/ready", async (c) => {
16644
+ const body = await c.req.json().catch(() => null);
16645
+ const key = body?.key;
16646
+ if (typeof key !== "string") {
16647
+ return c.json({ error: "key required" }, 400);
16648
+ }
16649
+ deps.onReady?.(key);
16650
+ return c.json({ ok: true });
16651
+ });
16652
+ app48.post("/webchat-channel/received", async (c) => {
16653
+ const body = await c.req.json().catch(() => null);
16654
+ const key = body?.key;
16655
+ const messageId = body?.messageId;
16656
+ if (typeof key !== "string" || typeof messageId !== "string") {
16657
+ return c.json({ error: "key and messageId required" }, 400);
16658
+ }
16659
+ deps.onReceived?.(key, messageId);
16660
+ return c.json({ ok: true });
16661
+ });
16662
+ return app48;
16663
+ }
16664
+
16665
+ // app/lib/webchat/gateway/webchat-gateway.ts
16666
+ var QUEUED_THRESHOLD_MS2 = 3e4;
16667
+ var ACK_THRESHOLD_MS2 = 3e4;
16668
+ var SWEEP_INTERVAL_MS2 = 15e3;
16669
+ var WebchatGateway = class {
16670
+ constructor(deps) {
16671
+ this.deps = deps;
16672
+ }
16673
+ hub = new InboundHub2();
16674
+ spawning = /* @__PURE__ */ new Set();
16675
+ /** Last delivery time per key, the latency basis for the op=reply line. */
16676
+ lastDeliveredAt = /* @__PURE__ */ new Map();
16677
+ seq = 0;
16678
+ /** Hono sub-app exposing the loopback channel routes; mount on the UI app. */
16679
+ routes() {
16680
+ return createWebchatChannelRoutes({
16681
+ hub: this.hub,
16682
+ onReady: (key) => {
16683
+ const now = Date.now();
16684
+ const count = this.hub.markReady(key, now);
16685
+ if (count > 0) {
16686
+ console.error(`[webchat:inbound] op=drained key=${key} count=${count}`);
16687
+ this.lastDeliveredAt.set(key, now);
16688
+ }
16689
+ },
16690
+ onReceived: (key, messageId) => {
16691
+ const found = this.hub.ack(key, messageId);
16692
+ console.error(
16693
+ `[webchat:inbound] op=channel-received key=${key} messageId=${messageId}${found ? "" : " note=unknown"}`
16694
+ );
16695
+ },
16696
+ onReply: (key, bytes) => {
16697
+ const deliveredAt = this.lastDeliveredAt.get(key);
16698
+ const latency = deliveredAt != null ? Date.now() - deliveredAt : -1;
16699
+ console.error(`[webchat:inbound] op=reply key=${key} bytes=${bytes} latencyMs=${latency}`);
16700
+ }
16701
+ });
16702
+ }
16703
+ /** Run one reconciliation pass: log every terminal delivery event the sweep
16704
+ * surfaces. Returns the events (for tests). Driven by startSweeper() in
16705
+ * production; called directly with an injected clock in tests. */
16706
+ reconcile(now) {
16707
+ const events = this.hub.sweep(now, {
16708
+ queuedThresholdMs: QUEUED_THRESHOLD_MS2,
16709
+ ackThresholdMs: ACK_THRESHOLD_MS2
16710
+ });
16711
+ for (const e of events) {
16712
+ console.error(`[webchat:inbound] op=${e.kind} key=${e.key} messageId=${e.messageId} ageMs=${e.ageMs}`);
16713
+ }
16714
+ return events;
16715
+ }
16716
+ /** Start the periodic reconciliation sweep in the long-lived UI process. */
16717
+ startSweeper(intervalMs = SWEEP_INTERVAL_MS2) {
16718
+ return setInterval(() => this.reconcile(Date.now()), intervalMs);
16719
+ }
16720
+ /** Handle one inbound webchat message (browser POST → here). */
16721
+ async handleInbound(input) {
16722
+ const bytes = Buffer.byteLength(input.text, "utf8");
16723
+ const hadSubscriber = this.hub.hasSubscriber(input.key);
16724
+ const willDeliverNow = this.hub.isReady(input.key);
16725
+ this.hub.deliver(
16726
+ { key: input.key, text: input.text, messageId: `wc-${++this.seq}` },
16727
+ Date.now()
16728
+ );
16729
+ if (willDeliverNow) this.lastDeliveredAt.set(input.key, Date.now());
16730
+ console.error(
16731
+ `[webchat:inbound] op=received key=${input.key.slice(0, 8)} role=${input.role} bytes=${bytes} delivery=${willDeliverNow ? "immediate" : "queued"}`
16732
+ );
16733
+ if (!hadSubscriber && !this.spawning.has(input.key)) {
16734
+ this.spawning.add(input.key);
16735
+ console.error(`[webchat:inbound] op=spawn-trigger key=${input.key} role=${input.role}`);
16736
+ try {
16737
+ await this.deps.ensureChannelSession({
16738
+ accountId: input.accountId,
16739
+ key: input.key,
16740
+ role: input.role,
16741
+ gatewayUrl: this.deps.gatewayUrl,
16742
+ serverPath: this.deps.serverPath
16743
+ });
16744
+ } finally {
16745
+ this.spawning.delete(input.key);
16746
+ }
16747
+ }
16748
+ }
16749
+ };
16750
+
16298
16751
  // app/lib/whatsapp/gateway/native-file-follower.ts
16752
+ var COMPOSING_REFRESH_MS = 1e4;
16299
16753
  function startNativeFileFollower(input) {
16300
16754
  const entry = {
16301
16755
  sessionId: input.sessionId,
@@ -16314,6 +16768,24 @@ function startNativeFileFollower(input) {
16314
16768
  personId: null,
16315
16769
  name: null
16316
16770
  };
16771
+ const sid = input.senderId.slice(0, 8);
16772
+ let lastComposingAt = Number.NEGATIVE_INFINITY;
16773
+ const onActivity = () => {
16774
+ const now = Date.now();
16775
+ if (now - lastComposingAt < COMPOSING_REFRESH_MS) return;
16776
+ lastComposingAt = now;
16777
+ const sock = getSocket(input.accountId);
16778
+ if (!sock) return;
16779
+ console.error(`[whatsapp-adaptor] op=presence state=composing sender=${sid}`);
16780
+ void sendComposing(sock, input.senderId);
16781
+ };
16782
+ const onTurnComplete = () => {
16783
+ lastComposingAt = Number.NEGATIVE_INFINITY;
16784
+ const sock = getSocket(input.accountId);
16785
+ if (!sock) return;
16786
+ console.error(`[whatsapp-adaptor] op=presence state=paused sender=${sid}`);
16787
+ void sendPaused(sock, input.senderId);
16788
+ };
16317
16789
  return startFollower({
16318
16790
  entry,
16319
16791
  tag: "[whatsapp-adaptor]",
@@ -16321,6 +16793,8 @@ function startNativeFileFollower(input) {
16321
16793
  // A resumed session's JSONL already holds prior SendUserFile tool_uses;
16322
16794
  // suppress replay so historical files are not re-sent on attach.
16323
16795
  suppressResumeReplay: true,
16796
+ onActivity,
16797
+ onTurnComplete,
16324
16798
  onClose: input.onClose ?? (() => {
16325
16799
  })
16326
16800
  });
@@ -16630,7 +17104,7 @@ function clientFrom(c) {
16630
17104
  );
16631
17105
  }
16632
17106
  var PLATFORM_ROOT9 = process.env.MAXY_PLATFORM_ROOT || "";
16633
- var BRAND_JSON_PATH = PLATFORM_ROOT9 ? join19(PLATFORM_ROOT9, "config", "brand.json") : "";
17107
+ var BRAND_JSON_PATH = PLATFORM_ROOT9 ? join20(PLATFORM_ROOT9, "config", "brand.json") : "";
16634
17108
  var BRAND = { productName: "Maxy", hostname: "maxy", configDir: ".maxy", marketingUrl: "https://getmaxy.com" };
16635
17109
  if (BRAND_JSON_PATH && !existsSync25(BRAND_JSON_PATH)) {
16636
17110
  console.error(`[brand] WARNING: brand.json not found at ${BRAND_JSON_PATH} \u2014 using Maxy defaults`);
@@ -16656,7 +17130,7 @@ var brandLoginOpts = {
16656
17130
  bodyFont: BRAND.defaultFonts?.body,
16657
17131
  logoContainsName: !!BRAND.logoContainsName
16658
17132
  };
16659
- var ALIAS_DOMAINS_PATH = join19(homedir3(), BRAND.configDir, "alias-domains.json");
17133
+ var ALIAS_DOMAINS_PATH = join20(homedir3(), BRAND.configDir, "alias-domains.json");
16660
17134
  function loadAliasDomains() {
16661
17135
  try {
16662
17136
  if (!existsSync25(ALIAS_DOMAINS_PATH)) return null;
@@ -16688,7 +17162,7 @@ var app47 = new Hono();
16688
17162
  var nativeFileFollowers = /* @__PURE__ */ new Map();
16689
17163
  var waGateway = new WaGateway({
16690
17164
  gatewayUrl: `http://127.0.0.1:${process.env.MAXY_UI_INTERNAL_PORT ?? ""}`,
16691
- serverPath: process.env.MAXY_WA_CHANNEL_SERVER_PATH ?? resolve27(process.env.MAXY_PLATFORM_ROOT ?? join19(__dirname, ".."), "services/whatsapp-channel/dist/server.js"),
17165
+ serverPath: process.env.MAXY_WA_CHANNEL_SERVER_PATH ?? resolve27(process.env.MAXY_PLATFORM_ROOT ?? join20(__dirname, ".."), "services/whatsapp-channel/dist/server.js"),
16692
17166
  // Task 751 — file delivery on the native channel: resolve the platform
16693
17167
  // account + path validation here and funnel through the shared send core.
16694
17168
  sendDocument: async ({ senderId, accountId, filePath, caption }) => {
@@ -16704,7 +17178,7 @@ var waGateway = new WaGateway({
16704
17178
  caption,
16705
17179
  accountId,
16706
17180
  maxyAccountId,
16707
- platformRoot: resolve27(process.env.MAXY_PLATFORM_ROOT ?? join19(__dirname, ".."))
17181
+ platformRoot: resolve27(process.env.MAXY_PLATFORM_ROOT ?? join20(__dirname, ".."))
16708
17182
  });
16709
17183
  return result.ok ? { ok: true, messageId: result.messageId } : { ok: false, error: result.error };
16710
17184
  },
@@ -16742,6 +17216,29 @@ var waGateway = new WaGateway({
16742
17216
  });
16743
17217
  app47.route("/", waGateway.routes());
16744
17218
  waGateway.startSweeper();
17219
+ var webchatGateway = new WebchatGateway({
17220
+ gatewayUrl: `http://127.0.0.1:${process.env.MAXY_UI_INTERNAL_PORT ?? ""}`,
17221
+ serverPath: process.env.MAXY_WEBCHAT_CHANNEL_SERVER_PATH ?? resolve27(process.env.MAXY_PLATFORM_ROOT ?? join20(__dirname, ".."), "services/webchat-channel/dist/server.js"),
17222
+ ensureChannelSession: async ({ accountId, key, role, gatewayUrl, serverPath }) => {
17223
+ if (role !== "admin") {
17224
+ console.error(`[webchat:inbound] op=spawn-refused key=${key} role=${role} reason=public-path-not-wired`);
17225
+ return;
17226
+ }
17227
+ const result = await managerRcSpawn({
17228
+ sessionId: adminSessionIdFor(accountId, key),
17229
+ role: "admin",
17230
+ channel: "webchat",
17231
+ senderId: key,
17232
+ webchatChannel: { key, gatewayUrl, serverPath },
17233
+ logContext: `channel=webchat key=${key} role=${role}`
17234
+ });
17235
+ if ("error" in result) {
17236
+ console.error(`[webchat:inbound] op=spawn-failed key=${key} error=${result.error} status=${result.status}`);
17237
+ }
17238
+ }
17239
+ });
17240
+ app47.route("/", webchatGateway.routes());
17241
+ webchatGateway.startSweeper();
16745
17242
  app47.use("*", clientIpMiddleware);
16746
17243
  app47.use("*", async (c, next) => {
16747
17244
  await next();
@@ -17028,6 +17525,7 @@ app47.route("/api/health", health_default);
17028
17525
  app47.route("/api/chat", chat_default);
17029
17526
  app47.route("/api/whatsapp", whatsapp_default);
17030
17527
  app47.route("/api/whatsapp-reader", whatsapp_reader_default);
17528
+ app47.route("/api/webchat", createWebchatRoutes({ handleInbound: (input) => webchatGateway.handleInbound(input) }));
17031
17529
  app47.route("/api/telegram", telegram_default);
17032
17530
  app47.route("/api/onboarding", onboarding_default);
17033
17531
  app47.route("/api/admin", admin_default);
@@ -17135,7 +17633,7 @@ var brandScript = `<script>window.__BRAND__=${JSON.stringify({
17135
17633
  function readInstalledVersion() {
17136
17634
  try {
17137
17635
  if (!PLATFORM_ROOT9) return "unknown";
17138
- const versionFile = join19(PLATFORM_ROOT9, "config", `.${BRAND.hostname}-version`);
17636
+ const versionFile = join20(PLATFORM_ROOT9, "config", `.${BRAND.hostname}-version`);
17139
17637
  if (!existsSync25(versionFile)) return "unknown";
17140
17638
  const content = readFileSync24(versionFile, "utf-8").trim();
17141
17639
  return content || "unknown";
@@ -17194,11 +17692,11 @@ ${clientErrorReporterScript}
17194
17692
  }
17195
17693
  var brandedHtmlCache = /* @__PURE__ */ new Map();
17196
17694
  function loadBrandingCache(agentSlug) {
17197
- const configDir2 = join19(homedir3(), BRAND.configDir);
17695
+ const configDir2 = join20(homedir3(), BRAND.configDir);
17198
17696
  try {
17199
17697
  const accountId = getDefaultAccountId();
17200
17698
  if (!accountId) return null;
17201
- const cachePath = join19(configDir2, "branding-cache", accountId, `${agentSlug}.json`);
17699
+ const cachePath = join20(configDir2, "branding-cache", accountId, `${agentSlug}.json`);
17202
17700
  if (!existsSync25(cachePath)) return null;
17203
17701
  return JSON.parse(readFileSync24(cachePath, "utf-8"));
17204
17702
  } catch {
@@ -17327,6 +17825,11 @@ app47.get("/graph", (c) => {
17327
17825
  if (isPublicHost(host)) return c.text("Not found", 404);
17328
17826
  return c.html(cachedHtml("graph.html"));
17329
17827
  });
17828
+ app47.get("/chat", (c) => {
17829
+ const host = (c.req.header("host") ?? "").split(":")[0];
17830
+ if (isPublicHost(host)) return c.text("Not found", 404);
17831
+ return c.html(cachedHtml("chat.html"));
17832
+ });
17330
17833
  app47.get("/sessions", (c) => {
17331
17834
  const host = (c.req.header("host") ?? "").split(":")[0];
17332
17835
  if (isPublicHost(host)) return c.text("Not found", 404);
@@ -17496,7 +17999,7 @@ try {
17496
17999
  } catch (err) {
17497
18000
  console.error(`[graph-health] account-enumeration unavailable reason=${err instanceof Error ? err.message : String(err)}`);
17498
18001
  }
17499
- var configDirForWhatsApp = basename7(MAXY_DIR) || ".maxy";
18002
+ var configDirForWhatsApp = basename8(MAXY_DIR) || ".maxy";
17500
18003
  var bootAccount = resolveAccount();
17501
18004
  if (bootAccount) {
17502
18005
  migrateRemovedConfigKeys(bootAccount.accountDir);
@@ -17558,7 +18061,7 @@ if (bootAccountConfig?.whatsapp) {
17558
18061
  }
17559
18062
  init({
17560
18063
  configDir: configDirForWhatsApp,
17561
- platformRoot: resolve27(process.env.MAXY_PLATFORM_ROOT ?? join19(__dirname, "..")),
18064
+ platformRoot: resolve27(process.env.MAXY_PLATFORM_ROOT ?? join20(__dirname, "..")),
17562
18065
  accountConfig: bootAccountConfig,
17563
18066
  onMessage: async (msg) => {
17564
18067
  if (msg.isOwnerMirror) {