@rubytech/create-realagent-code 0.1.577 → 0.1.579

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 (213) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/routine-templates/dist/index.d.ts +4 -4
  3. package/payload/platform/lib/routine-templates/dist/index.js +4 -4
  4. package/payload/platform/lib/routine-templates/src/index.ts +4 -4
  5. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +11 -1
  6. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +19 -0
  7. package/payload/platform/plugins/docs/references/admin-ui.md +6 -0
  8. package/payload/platform/plugins/docs/references/platform.md +2 -0
  9. package/payload/platform/plugins/docs/references/troubleshooting.md +2 -0
  10. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +0 -2
  11. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
  12. package/payload/platform/scripts/check-no-esm-require.mjs +6 -0
  13. package/payload/platform/services/webchat-channel/dist/server.js +4 -4
  14. package/payload/platform/services/webchat-channel/dist/server.js.map +1 -1
  15. package/payload/server/public/activity.html +6 -6
  16. package/payload/server/public/agents.html +5 -5
  17. package/payload/server/public/assets/{AdminLoginScreens-BLGZEaKU.js → AdminLoginScreens-Bus-Y-rG.js} +1 -1
  18. package/payload/server/public/assets/AdminLoginScreens-Bus-Y-rG.js.br +0 -0
  19. package/payload/server/public/assets/AdminLoginScreens-Bus-Y-rG.js.gz +0 -0
  20. package/payload/server/public/assets/AdminShell-Bsm7q3CW.js +3 -0
  21. package/payload/server/public/assets/AdminShell-Bsm7q3CW.js.br +0 -0
  22. package/payload/server/public/assets/AdminShell-Bsm7q3CW.js.gz +0 -0
  23. package/payload/server/public/assets/{activity-BL6qbB9a.js → activity-BWqrCyYM.js} +1 -1
  24. package/payload/server/public/assets/activity-BWqrCyYM.js.br +0 -0
  25. package/payload/server/public/assets/activity-BWqrCyYM.js.gz +0 -0
  26. package/payload/server/public/assets/admin-zly7Y6EZ.js +1 -0
  27. package/payload/server/public/assets/admin-zly7Y6EZ.js.br +0 -0
  28. package/payload/server/public/assets/admin-zly7Y6EZ.js.gz +0 -0
  29. package/payload/server/public/assets/{agents-BzhfKZDN.js → agents-BsxGD1V1.js} +1 -1
  30. package/payload/server/public/assets/agents-BsxGD1V1.js.br +0 -0
  31. package/payload/server/public/assets/agents-BsxGD1V1.js.gz +0 -0
  32. package/payload/server/public/assets/{browser-C-6io2B9.js → browser-CRLxTaw2.js} +1 -1
  33. package/payload/server/public/assets/browser-CRLxTaw2.js.br +0 -0
  34. package/payload/server/public/assets/browser-CRLxTaw2.js.gz +0 -0
  35. package/payload/server/public/assets/{calendar-Dt3gS88s.js → calendar-Bmp6qI_P.js} +1 -1
  36. package/payload/server/public/assets/calendar-Bmp6qI_P.js.br +0 -0
  37. package/payload/server/public/assets/calendar-Bmp6qI_P.js.gz +0 -0
  38. package/payload/server/public/assets/chat-ChObDKHy.js +1 -0
  39. package/payload/server/public/assets/chat-ChObDKHy.js.br +0 -0
  40. package/payload/server/public/assets/chat-ChObDKHy.js.gz +0 -0
  41. package/payload/server/public/assets/chevron-left-D2l4jMcd.js +1 -0
  42. package/payload/server/public/assets/chevron-left-D2l4jMcd.js.br +0 -0
  43. package/payload/server/public/assets/clock-CMdfsroW.js +1 -0
  44. package/payload/server/public/assets/clock-CMdfsroW.js.br +0 -0
  45. package/payload/server/public/assets/clock-CMdfsroW.js.gz +0 -0
  46. package/payload/server/public/assets/{copy-TkPJgllS.js → copy-Bcjpxqzn.js} +1 -1
  47. package/payload/server/public/assets/copy-Bcjpxqzn.js.br +0 -0
  48. package/payload/server/public/assets/copy-Bcjpxqzn.js.gz +0 -0
  49. package/payload/server/public/assets/data-CFB5Lgw-.js +1 -0
  50. package/payload/server/public/assets/data-CFB5Lgw-.js.br +0 -0
  51. package/payload/server/public/assets/data-CFB5Lgw-.js.gz +0 -0
  52. package/payload/server/public/assets/{field-B490-evJ.js → field-ChKB2l_d.js} +1 -1
  53. package/payload/server/public/assets/field-ChKB2l_d.js.br +0 -0
  54. package/payload/server/public/assets/field-ChKB2l_d.js.gz +0 -0
  55. package/payload/server/public/assets/{file-text-DyAL2DCh.js → file-text-COZcf7mU.js} +1 -1
  56. package/payload/server/public/assets/file-text-COZcf7mU.js.br +0 -0
  57. package/payload/server/public/assets/file-text-COZcf7mU.js.gz +0 -0
  58. package/payload/server/public/assets/{graph-labels-8jKFrLHn.js → graph-labels-x-1MT-3c.js} +1 -1
  59. package/payload/server/public/assets/graph-labels-x-1MT-3c.js.br +0 -0
  60. package/payload/server/public/assets/graph-labels-x-1MT-3c.js.gz +0 -0
  61. package/payload/server/public/assets/{graph-PiNIcCfX.js → graph-nnV47iqM.js} +1 -1
  62. package/payload/server/public/assets/graph-nnV47iqM.js.br +0 -0
  63. package/payload/server/public/assets/graph-nnV47iqM.js.gz +0 -0
  64. package/payload/server/public/assets/{jsx-runtime-ChVh0xOm.css → jsx-runtime-CeNZasfC.css} +1 -1
  65. package/payload/server/public/assets/jsx-runtime-CeNZasfC.css.br +0 -0
  66. package/payload/server/public/assets/jsx-runtime-CeNZasfC.css.gz +0 -0
  67. package/payload/server/public/assets/{operator-DKGKGzrb.js → operator-BAO0-mjU.js} +1 -1
  68. package/payload/server/public/assets/operator-BAO0-mjU.js.br +0 -0
  69. package/payload/server/public/assets/operator-BAO0-mjU.js.gz +0 -0
  70. package/payload/server/public/assets/{page-DXz_K10G.js → page-B-F08pod.js} +1 -1
  71. package/payload/server/public/assets/page-B-F08pod.js.br +0 -0
  72. package/payload/server/public/assets/page-B-F08pod.js.gz +0 -0
  73. package/payload/server/public/assets/page-BMOCy2aD.js +1 -0
  74. package/payload/server/public/assets/page-BMOCy2aD.js.br +0 -0
  75. package/payload/server/public/assets/page-BMOCy2aD.js.gz +0 -0
  76. package/payload/server/public/assets/play-DFtlk8Zs.js +1 -0
  77. package/payload/server/public/assets/play-DFtlk8Zs.js.br +0 -0
  78. package/payload/server/public/assets/play-DFtlk8Zs.js.gz +0 -0
  79. package/payload/server/public/assets/{public-BmA3-jLj.js → public-_HJsUohA.js} +1 -1
  80. package/payload/server/public/assets/public-_HJsUohA.js.br +0 -0
  81. package/payload/server/public/assets/public-_HJsUohA.js.gz +0 -0
  82. package/payload/server/public/assets/{rotate-ccw-fCH0LSMq.js → rotate-ccw-DJeAnYLQ.js} +1 -1
  83. package/payload/server/public/assets/rotate-ccw-DJeAnYLQ.js.br +0 -0
  84. package/payload/server/public/assets/rotate-ccw-DJeAnYLQ.js.gz +0 -0
  85. package/payload/server/public/assets/{routines-BeFrmkls.js → routines-CoIVXRCB.js} +2 -2
  86. package/payload/server/public/assets/routines-CoIVXRCB.js.br +0 -0
  87. package/payload/server/public/assets/routines-CoIVXRCB.js.gz +0 -0
  88. package/payload/server/public/assets/{skills-mjls5HBa.js → skills-BjHt5lDG.js} +1 -1
  89. package/payload/server/public/assets/skills-BjHt5lDG.js.br +0 -0
  90. package/payload/server/public/assets/skills-BjHt5lDG.js.gz +0 -0
  91. package/payload/server/public/assets/{tasks-p6puHHD0.js → tasks-iUFfeA9_.js} +2 -2
  92. package/payload/server/public/assets/tasks-iUFfeA9_.js.br +0 -0
  93. package/payload/server/public/assets/tasks-iUFfeA9_.js.gz +0 -0
  94. package/payload/server/public/assets/{triangle-alert-CrZ1ewbd.js → triangle-alert-Dwa986Gg.js} +1 -1
  95. package/payload/server/public/assets/triangle-alert-Dwa986Gg.js.br +0 -0
  96. package/payload/server/public/assets/triangle-alert-Dwa986Gg.js.gz +0 -0
  97. package/payload/server/public/assets/{useCopyFeedback-zOQNrIwd.js → useCopyFeedback-B1Jp6CsE.js} +1 -1
  98. package/payload/server/public/assets/useCopyFeedback-B1Jp6CsE.js.br +0 -0
  99. package/payload/server/public/assets/useCopyFeedback-B1Jp6CsE.js.gz +0 -0
  100. package/payload/server/public/assets/{useMediaQuery-BmG090FR.js → useMediaQuery-CVZYjRf6.js} +2 -2
  101. package/payload/server/public/assets/useMediaQuery-CVZYjRf6.js.br +0 -0
  102. package/payload/server/public/assets/useMediaQuery-CVZYjRf6.js.gz +0 -0
  103. package/payload/server/public/assets/useVoiceRecorder-D5b4r4z0.js +2 -0
  104. package/payload/server/public/assets/useVoiceRecorder-D5b4r4z0.js.br +0 -0
  105. package/payload/server/public/assets/useVoiceRecorder-D5b4r4z0.js.gz +0 -0
  106. package/payload/server/public/assets/{wrench-k4GYG5rc.js → wrench-BD7xwbV1.js} +1 -1
  107. package/payload/server/public/assets/wrench-BD7xwbV1.js.br +0 -0
  108. package/payload/server/public/assets/wrench-BD7xwbV1.js.gz +0 -0
  109. package/payload/server/public/browser.html +5 -5
  110. package/payload/server/public/calendar.html +8 -8
  111. package/payload/server/public/chat.html +14 -14
  112. package/payload/server/public/data.html +12 -12
  113. package/payload/server/public/field.html +3 -3
  114. package/payload/server/public/graph.html +10 -10
  115. package/payload/server/public/index.html +14 -14
  116. package/payload/server/public/operator.html +16 -16
  117. package/payload/server/public/public.html +14 -14
  118. package/payload/server/public/routines.html +8 -8
  119. package/payload/server/public/skills.html +6 -6
  120. package/payload/server/public/tasks.html +7 -7
  121. package/payload/server/server.js +1169 -519
  122. package/payload/platform/plugins/dispatch/mcp/dist/scripts/converge-visit-instants.d.ts +0 -6
  123. package/payload/platform/plugins/dispatch/mcp/dist/scripts/converge-visit-instants.d.ts.map +0 -1
  124. package/payload/platform/plugins/dispatch/mcp/dist/scripts/converge-visit-instants.js +0 -102
  125. package/payload/platform/plugins/dispatch/mcp/dist/scripts/converge-visit-instants.js.map +0 -1
  126. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/converge-routine-description.test.d.ts +0 -2
  127. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/converge-routine-description.test.d.ts.map +0 -1
  128. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/converge-routine-description.test.js +0 -87
  129. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/converge-routine-description.test.js.map +0 -1
  130. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/converge-startdate-utc.test.d.ts +0 -2
  131. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/converge-startdate-utc.test.d.ts.map +0 -1
  132. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/converge-startdate-utc.test.js +0 -79
  133. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/converge-startdate-utc.test.js.map +0 -1
  134. package/payload/platform/plugins/scheduling/mcp/dist/scripts/converge-routine-description.d.ts +0 -35
  135. package/payload/platform/plugins/scheduling/mcp/dist/scripts/converge-routine-description.d.ts.map +0 -1
  136. package/payload/platform/plugins/scheduling/mcp/dist/scripts/converge-routine-description.js +0 -56
  137. package/payload/platform/plugins/scheduling/mcp/dist/scripts/converge-routine-description.js.map +0 -1
  138. package/payload/platform/plugins/scheduling/mcp/dist/scripts/converge-startdate-utc.d.ts +0 -24
  139. package/payload/platform/plugins/scheduling/mcp/dist/scripts/converge-startdate-utc.d.ts.map +0 -1
  140. package/payload/platform/plugins/scheduling/mcp/dist/scripts/converge-startdate-utc.js +0 -87
  141. package/payload/platform/plugins/scheduling/mcp/dist/scripts/converge-startdate-utc.js.map +0 -1
  142. package/payload/server/public/assets/AdminLoginScreens-BLGZEaKU.js.br +0 -0
  143. package/payload/server/public/assets/AdminLoginScreens-BLGZEaKU.js.gz +0 -0
  144. package/payload/server/public/assets/AdminShell-C_M7YE5Z.js +0 -3
  145. package/payload/server/public/assets/AdminShell-C_M7YE5Z.js.br +0 -0
  146. package/payload/server/public/assets/AdminShell-C_M7YE5Z.js.gz +0 -0
  147. package/payload/server/public/assets/activity-BL6qbB9a.js.br +0 -0
  148. package/payload/server/public/assets/activity-BL6qbB9a.js.gz +0 -0
  149. package/payload/server/public/assets/admin-jnJ3P2u4.js +0 -1
  150. package/payload/server/public/assets/admin-jnJ3P2u4.js.br +0 -0
  151. package/payload/server/public/assets/admin-jnJ3P2u4.js.gz +0 -0
  152. package/payload/server/public/assets/agents-BzhfKZDN.js.br +0 -0
  153. package/payload/server/public/assets/agents-BzhfKZDN.js.gz +0 -0
  154. package/payload/server/public/assets/browser-C-6io2B9.js.br +0 -0
  155. package/payload/server/public/assets/browser-C-6io2B9.js.gz +0 -0
  156. package/payload/server/public/assets/calendar-Dt3gS88s.js.br +0 -0
  157. package/payload/server/public/assets/calendar-Dt3gS88s.js.gz +0 -0
  158. package/payload/server/public/assets/chat-IUOvVkL-.js +0 -1
  159. package/payload/server/public/assets/chat-IUOvVkL-.js.br +0 -1
  160. package/payload/server/public/assets/chat-IUOvVkL-.js.gz +0 -0
  161. package/payload/server/public/assets/chevron-left-DSzR9zR7.js +0 -1
  162. package/payload/server/public/assets/chevron-left-DSzR9zR7.js.br +0 -0
  163. package/payload/server/public/assets/clock-C5xE5p2I.js +0 -1
  164. package/payload/server/public/assets/clock-C5xE5p2I.js.br +0 -0
  165. package/payload/server/public/assets/copy-TkPJgllS.js.br +0 -0
  166. package/payload/server/public/assets/copy-TkPJgllS.js.gz +0 -0
  167. package/payload/server/public/assets/data-Bhv0Pvfl.js +0 -1
  168. package/payload/server/public/assets/data-Bhv0Pvfl.js.br +0 -0
  169. package/payload/server/public/assets/data-Bhv0Pvfl.js.gz +0 -0
  170. package/payload/server/public/assets/field-B490-evJ.js.br +0 -0
  171. package/payload/server/public/assets/field-B490-evJ.js.gz +0 -0
  172. package/payload/server/public/assets/file-text-DyAL2DCh.js.br +0 -0
  173. package/payload/server/public/assets/file-text-DyAL2DCh.js.gz +0 -0
  174. package/payload/server/public/assets/graph-PiNIcCfX.js.br +0 -0
  175. package/payload/server/public/assets/graph-PiNIcCfX.js.gz +0 -0
  176. package/payload/server/public/assets/graph-labels-8jKFrLHn.js.br +0 -0
  177. package/payload/server/public/assets/graph-labels-8jKFrLHn.js.gz +0 -0
  178. package/payload/server/public/assets/jsx-runtime-ChVh0xOm.css.br +0 -0
  179. package/payload/server/public/assets/jsx-runtime-ChVh0xOm.css.gz +0 -0
  180. package/payload/server/public/assets/operator-DKGKGzrb.js.br +0 -0
  181. package/payload/server/public/assets/operator-DKGKGzrb.js.gz +0 -0
  182. package/payload/server/public/assets/page-BU5vlY9Y.js +0 -1
  183. package/payload/server/public/assets/page-BU5vlY9Y.js.br +0 -0
  184. package/payload/server/public/assets/page-BU5vlY9Y.js.gz +0 -0
  185. package/payload/server/public/assets/page-DXz_K10G.js.br +0 -0
  186. package/payload/server/public/assets/page-DXz_K10G.js.gz +0 -0
  187. package/payload/server/public/assets/play-CmSyYrB6.js +0 -1
  188. package/payload/server/public/assets/play-CmSyYrB6.js.br +0 -0
  189. package/payload/server/public/assets/play-CmSyYrB6.js.gz +0 -0
  190. package/payload/server/public/assets/public-BmA3-jLj.js.br +0 -0
  191. package/payload/server/public/assets/public-BmA3-jLj.js.gz +0 -0
  192. package/payload/server/public/assets/rotate-ccw-fCH0LSMq.js.br +0 -0
  193. package/payload/server/public/assets/rotate-ccw-fCH0LSMq.js.gz +0 -0
  194. package/payload/server/public/assets/routines-BeFrmkls.js.br +0 -0
  195. package/payload/server/public/assets/routines-BeFrmkls.js.gz +0 -0
  196. package/payload/server/public/assets/skills-mjls5HBa.js.br +0 -0
  197. package/payload/server/public/assets/skills-mjls5HBa.js.gz +0 -0
  198. package/payload/server/public/assets/tasks-p6puHHD0.js.br +0 -0
  199. package/payload/server/public/assets/tasks-p6puHHD0.js.gz +0 -0
  200. package/payload/server/public/assets/triangle-alert-CrZ1ewbd.js.br +0 -0
  201. package/payload/server/public/assets/triangle-alert-CrZ1ewbd.js.gz +0 -0
  202. package/payload/server/public/assets/useCopyFeedback-zOQNrIwd.js.br +0 -0
  203. package/payload/server/public/assets/useCopyFeedback-zOQNrIwd.js.gz +0 -0
  204. package/payload/server/public/assets/useMediaQuery-BmG090FR.js.br +0 -0
  205. package/payload/server/public/assets/useMediaQuery-BmG090FR.js.gz +0 -0
  206. package/payload/server/public/assets/useVoiceRecorder-9pf6FFJ-.js +0 -2
  207. package/payload/server/public/assets/useVoiceRecorder-9pf6FFJ-.js.br +0 -0
  208. package/payload/server/public/assets/useVoiceRecorder-9pf6FFJ-.js.gz +0 -0
  209. package/payload/server/public/assets/wrench-k4GYG5rc.js.br +0 -0
  210. package/payload/server/public/assets/wrench-k4GYG5rc.js.gz +0 -0
  211. /package/payload/server/public/assets/{jsx-runtime-YcUntiud.js → jsx-runtime-CjJ246F7.js} +0 -0
  212. /package/payload/server/public/assets/{jsx-runtime-YcUntiud.js.br → jsx-runtime-CjJ246F7.js.br} +0 -0
  213. /package/payload/server/public/assets/{jsx-runtime-YcUntiud.js.gz → jsx-runtime-CjJ246F7.js.gz} +0 -0
@@ -471,6 +471,16 @@ var init_agent_errors = __esm({
471
471
  });
472
472
 
473
473
  // app/lib/reader/parse-transcript.ts
474
+ function emptyEnvelopeCensus() {
475
+ return {
476
+ rows: 0,
477
+ wrapped: 0,
478
+ nested: 0,
479
+ stripped: 0,
480
+ unstripped: 0,
481
+ sources: { whatsapp: 0, webchat: 0, telegram: 0, none: 0 }
482
+ };
483
+ }
474
484
  function isCompactSummaryRow(row, content) {
475
485
  return row.isCompactSummary === true || content !== null && content.startsWith(COMPACT_SUMMARY_PREFIX);
476
486
  }
@@ -481,10 +491,11 @@ function isScheduleSource(text) {
481
491
  }
482
492
  function unwrapChannel(text) {
483
493
  const m = text.match(CHANNEL_WRAPPER);
484
- if (!m) return { text };
494
+ if (!m) return { text, wrapped: false, nested: false };
485
495
  let source = SOURCE_MAP[m[1].match(SOURCE_ATTR)?.[1] ?? ""];
486
496
  let inner = m[2];
487
497
  const im = inner.match(CHANNEL_WRAPPER);
498
+ const nested = im !== null;
488
499
  if (im) {
489
500
  const innerSource = SOURCE_MAP[im[1].match(SOURCE_ATTR)?.[1] ?? ""];
490
501
  if (innerSource) {
@@ -492,7 +503,7 @@ function unwrapChannel(text) {
492
503
  inner = im[2];
493
504
  }
494
505
  }
495
- return source ? { text: inner, source } : { text: inner };
506
+ return source ? { text: inner, source, wrapped: true, nested } : { text: inner, wrapped: true, nested };
496
507
  }
497
508
  function splitReplyNote(payload) {
498
509
  const m = payload.match(REPLY_NOTE);
@@ -508,7 +519,17 @@ function uploadCountOf(text) {
508
519
  const n = Number(text.match(UPLOAD_COUNT)?.[1]);
509
520
  return Number.isInteger(n) && n > 0 ? n : void 0;
510
521
  }
511
- function operatorInboundTurn(u, ts, uuid) {
522
+ function noteEnvelope(census, u) {
523
+ if (!census) return;
524
+ census.rows++;
525
+ if (u.wrapped) census.wrapped++;
526
+ if (u.nested) census.nested++;
527
+ if (u.text.startsWith("<channel ")) census.unstripped++;
528
+ else if (u.wrapped) census.stripped++;
529
+ census.sources[u.source ?? "none"]++;
530
+ }
531
+ function operatorInboundTurn(u, ts, uuid, census) {
532
+ noteEnvelope(census, u);
512
533
  const framed = rawFromComposedAdminFrame(u.text);
513
534
  const split = framed === void 0 ? void 0 : splitReplyNote(framed);
514
535
  const rawBody = split?.body;
@@ -635,12 +656,12 @@ function lastTurnTs(turns) {
635
656
  function isModelGated(turns) {
636
657
  return turns.some((t) => t.kind === "agent-error" && t.code === "model_unavailable");
637
658
  }
638
- function renderOrSuppressChannelInbound(u, ts, out, queuedPendingSuppress, uuid) {
659
+ function renderOrSuppressChannelInbound(u, ts, out, queuedPendingSuppress, uuid, census) {
639
660
  const armed2 = queuedPendingSuppress.get(u.text) ?? 0;
640
661
  if (armed2 > 0) {
641
662
  queuedPendingSuppress.set(u.text, armed2 - 1);
642
663
  } else {
643
- out.push(operatorInboundTurn(u, ts, uuid));
664
+ out.push(operatorInboundTurn(u, ts, uuid, census));
644
665
  }
645
666
  }
646
667
  function parseTranscript(lines, queuedPendingSuppress = /* @__PURE__ */ new Map(), turnCtx = {}) {
@@ -678,7 +699,7 @@ function parseTranscript(lines, queuedPendingSuppress = /* @__PURE__ */ new Map(
678
699
  const u = unwrapChannel(content);
679
700
  turnCtx.lastChannelSource = u.source;
680
701
  if (!isScheduleSource(content)) {
681
- out.push(operatorInboundTurn(u, ts));
702
+ out.push(operatorInboundTurn(u, ts, void 0, turnCtx.envelopeCensus));
682
703
  queuedPendingSuppress.set(u.text, (queuedPendingSuppress.get(u.text) ?? 0) + 1);
683
704
  }
684
705
  }
@@ -691,7 +712,7 @@ function parseTranscript(lines, queuedPendingSuppress = /* @__PURE__ */ new Map(
691
712
  const u = unwrapChannel(att.prompt);
692
713
  turnCtx.lastChannelSource = u.source;
693
714
  if (!isScheduleSource(att.prompt)) {
694
- renderOrSuppressChannelInbound(u, ts, out, queuedPendingSuppress, asString(row.uuid) ?? void 0);
715
+ renderOrSuppressChannelInbound(u, ts, out, queuedPendingSuppress, asString(row.uuid) ?? void 0, turnCtx.envelopeCensus);
695
716
  }
696
717
  }
697
718
  continue;
@@ -720,7 +741,7 @@ function parseTranscript(lines, queuedPendingSuppress = /* @__PURE__ */ new Map(
720
741
  const u = unwrapChannel(text);
721
742
  turnCtx.lastChannelSource = u.source;
722
743
  if (!isScheduleSource(text)) {
723
- renderOrSuppressChannelInbound(u, ts, out, queuedPendingSuppress, asString(row.uuid) ?? void 0);
744
+ renderOrSuppressChannelInbound(u, ts, out, queuedPendingSuppress, asString(row.uuid) ?? void 0, turnCtx.envelopeCensus);
724
745
  }
725
746
  } else if (row.isMeta === true) {
726
747
  out.push({ kind: "harness-internal", text, ts });
@@ -747,7 +768,7 @@ var init_parse_transcript = __esm({
747
768
  HARNESS_LIFECYCLE_PREFIXES = ["<task-notification>"];
748
769
  CC_IMAGE_VISION_HINT = /^\[Image:[^\]]*Multiply coordinates by [\d.]+ to map to original image\.\]$/;
749
770
  COMPACT_SUMMARY_PREFIX = "This session is being continued from a previous conversation that ran out of context.";
750
- CHANNEL_WRAPPER = /^<channel\b([^>]*)>\n?([\s\S]*?)\n?<\/channel>$/;
771
+ CHANNEL_WRAPPER = /^<channel\b([^>]*)>\n?([\s\S]*?)\n?<\\?\/channel>$/;
751
772
  SOURCE_ATTR = /\bsource="([^"]+)"/;
752
773
  SOURCE_MAP = {
753
774
  whatsapp: "whatsapp",
@@ -815,7 +836,7 @@ var init_audio_mime = __esm({
815
836
  // app/lib/stt/voice-note.ts
816
837
  import { writeFile, mkdtemp, rm } from "fs/promises";
817
838
  import { tmpdir as tmpdir2 } from "os";
818
- import { join as join6 } from "path";
839
+ import { join as join7 } from "path";
819
840
  async function transcribeVoiceNote(file, source) {
820
841
  const startMs = Date.now();
821
842
  const bytes = file.size;
@@ -826,9 +847,9 @@ async function transcribeVoiceNote(file, source) {
826
847
  let tempDir;
827
848
  let tempPath;
828
849
  try {
829
- tempDir = await mkdtemp(join6(tmpdir2(), "voice-"));
850
+ tempDir = await mkdtemp(join7(tmpdir2(), "voice-"));
830
851
  const ext = audioExtension(mimeType);
831
- tempPath = join6(tempDir, `recording${ext}`);
852
+ tempPath = join7(tempDir, `recording${ext}`);
832
853
  const buffer = Buffer.from(await file.arrayBuffer());
833
854
  await writeFile(tempPath, buffer);
834
855
  } catch (err) {
@@ -1239,10 +1260,56 @@ function buildStoreConversationRows(accountId, summaries, chatMeta) {
1239
1260
  }
1240
1261
  return rows.sort((a, b) => (b.lastMessageAt ?? "").localeCompare(a.lastMessageAt ?? ""));
1241
1262
  }
1263
+ function buildTelegramStoreConversationRows(accountId, summaries) {
1264
+ const rows = [];
1265
+ for (const [channelKey, { first, last }] of summaries) {
1266
+ rows.push({
1267
+ sessionId: "",
1268
+ projectDir: "",
1269
+ title: channelKey,
1270
+ senderId: last.senderId,
1271
+ startedAt: first.dateSent,
1272
+ channel: "telegram",
1273
+ role: last.scope === "admin" ? "admin" : "public",
1274
+ operatorName: null,
1275
+ whatsappName: null,
1276
+ // The name is what it is NOW, matching how `scope` is taken from `last`.
1277
+ // The fallback covers an outbound last record: the outbound write points
1278
+ // have no sender to name, so they store null.
1279
+ telegramName: last.senderName ?? first.senderName ?? null,
1280
+ lastMessageAt: last.dateSent,
1281
+ modelGated: false,
1282
+ model: null,
1283
+ source: "store",
1284
+ accountId,
1285
+ channelKey,
1286
+ scope: last.scope,
1287
+ preview: last.body
1288
+ });
1289
+ }
1290
+ return rows.sort((a, b) => (b.lastMessageAt ?? "").localeCompare(a.lastMessageAt ?? ""));
1291
+ }
1292
+ function dropTelegramSessionDuplicates(rows, telegramStoreRows) {
1293
+ const coveredKeys = /* @__PURE__ */ new Set();
1294
+ const coveredSenders = /* @__PURE__ */ new Set();
1295
+ for (const r of telegramStoreRows) {
1296
+ const parsed = r.channelKey ? parseTelegramChannelKey(r.channelKey) : null;
1297
+ if (!parsed) continue;
1298
+ coveredKeys.add(r.channelKey);
1299
+ coveredSenders.add(parsed.senderId);
1300
+ }
1301
+ if (coveredKeys.size === 0) return rows;
1302
+ return rows.filter((r) => {
1303
+ if (r.source !== "session" || r.channel !== "telegram" || r.senderId == null) return true;
1304
+ const parsed = parseTelegramChannelKey(r.senderId);
1305
+ return parsed ? !coveredKeys.has(r.senderId) : !coveredSenders.has(r.senderId);
1306
+ });
1307
+ }
1242
1308
  var init_select_sessions = __esm({
1243
1309
  "app/lib/reader/select-sessions.ts"() {
1244
1310
  "use strict";
1245
1311
  init_normalize();
1312
+ init_channel_key();
1246
1313
  init_conversation_preview();
1247
1314
  }
1248
1315
  });
@@ -5450,19 +5517,196 @@ var require_dist7 = __commonJS({
5450
5517
 
5451
5518
  // server/index.ts
5452
5519
  init_dist();
5453
- import { readFileSync as readFileSync56, existsSync as existsSync56, watchFile, readdirSync as readdirSync32, statSync as statSync30 } from "fs";
5520
+ import { readFileSync as readFileSync57, existsSync as existsSync56, watchFile, readdirSync as readdirSync33, statSync as statSync30 } from "fs";
5454
5521
  import { spawn as spawn3 } from "child_process";
5455
5522
  import { createHash as createHash8 } from "crypto";
5456
- import { resolve as resolve51, join as join64, basename as basename17 } from "path";
5457
- import { homedir as homedir5 } from "os";
5523
+ import { resolve as resolve51, join as join65, basename as basename17 } from "path";
5524
+ import { homedir as homedir6 } from "os";
5458
5525
 
5459
5526
  // server/lib/route-cache-watchers.ts
5460
5527
  init_route_cache();
5461
5528
  init_account();
5462
5529
  init_message_store();
5463
- init_src2();
5464
5530
  import { watch } from "fs";
5465
- import { resolve } from "path";
5531
+ import { resolve as resolve2 } from "path";
5532
+
5533
+ // app/lib/telegram/message-store.ts
5534
+ import { existsSync, mkdirSync, readFileSync, appendFileSync, readdirSync, statSync } from "fs";
5535
+ import { homedir } from "os";
5536
+ import { dirname, join, resolve } from "path";
5537
+ function configDirName() {
5538
+ const platformRoot5 = process.env.MAXY_PLATFORM_ROOT ?? resolve(process.cwd(), "..");
5539
+ const brandPath = join(platformRoot5, "config", "brand.json");
5540
+ if (existsSync(brandPath)) {
5541
+ try {
5542
+ return JSON.parse(readFileSync(brandPath, "utf-8")).configDir ?? ".maxy";
5543
+ } catch {
5544
+ }
5545
+ }
5546
+ return ".maxy";
5547
+ }
5548
+ var STORE_ROOT2 = () => join(process.env.MAXY_DIR_OVERRIDE ?? resolve(homedir(), configDirName()), "data", "telegram-messages");
5549
+ function isStorableChannelKey(channelKey) {
5550
+ return /^-?\d+:-?\d+$/.test(channelKey);
5551
+ }
5552
+ function channelKeyToFileStem(channelKey) {
5553
+ if (!isStorableChannelKey(channelKey)) {
5554
+ throw new Error(`telegram-message-store: refusing unstorable channelKey ${JSON.stringify(channelKey)}`);
5555
+ }
5556
+ return channelKey.replace(":", "_");
5557
+ }
5558
+ function fileStemToChannelKey(stem) {
5559
+ const i = stem.indexOf("_");
5560
+ return i <= 0 ? stem : `${stem.slice(0, i)}:${stem.slice(i + 1)}`;
5561
+ }
5562
+ function conversationFile(accountId, channelKey) {
5563
+ return join(STORE_ROOT2(), accountId, `${channelKeyToFileStem(channelKey)}.jsonl`);
5564
+ }
5565
+ function conversationFilePath2(accountId, channelKey) {
5566
+ return conversationFile(accountId, channelKey);
5567
+ }
5568
+ function isStorableStem(stem) {
5569
+ return isStorableChannelKey(fileStemToChannelKey(stem));
5570
+ }
5571
+ function listStoredAccountIds2() {
5572
+ const root = STORE_ROOT2();
5573
+ if (!existsSync(root)) return [];
5574
+ return readdirSync(root).filter((name) => {
5575
+ try {
5576
+ return statSync(join(root, name)).isDirectory();
5577
+ } catch {
5578
+ return false;
5579
+ }
5580
+ });
5581
+ }
5582
+ function parseLines(raw) {
5583
+ const out = [];
5584
+ for (const line of raw.split("\n")) {
5585
+ if (!line.trim()) continue;
5586
+ try {
5587
+ out.push(JSON.parse(line));
5588
+ } catch {
5589
+ console.error(`[telegram-message-store] skip reason=corrupt-line`);
5590
+ }
5591
+ }
5592
+ return out;
5593
+ }
5594
+ var byDateSentAsc = (a, b) => a.dateSent.localeCompare(b.dateSent);
5595
+ function appendMessage2(accountId, record) {
5596
+ if (!isStorableChannelKey(record.channelKey)) {
5597
+ console.error(
5598
+ `[telegram-message-store] FAIL op=append reason=unstorable-channel-key messageId=${record.messageId}`
5599
+ );
5600
+ return { created: false };
5601
+ }
5602
+ const file = conversationFile(accountId, record.channelKey);
5603
+ try {
5604
+ mkdirSync(dirname(file), { recursive: true });
5605
+ } catch (err) {
5606
+ const reason = err instanceof Error ? err.message : String(err);
5607
+ console.error(
5608
+ `[telegram-message-store] FAIL op=append reason=${reason.slice(0, 100)} messageId=${record.messageId}`
5609
+ );
5610
+ return { created: false };
5611
+ }
5612
+ try {
5613
+ if (existsSync(file)) {
5614
+ const existing = parseLines(readFileSync(file, "utf8"));
5615
+ if (existing.some((r) => r.messageId === record.messageId)) {
5616
+ console.error(
5617
+ `[telegram-message-store] op=append channelKey=${record.channelKey} origin=${record.origin} created=false`
5618
+ );
5619
+ return { created: false };
5620
+ }
5621
+ }
5622
+ appendFileSync(file, `${JSON.stringify(record)}
5623
+ `);
5624
+ console.error(
5625
+ `[telegram-message-store] op=append channelKey=${record.channelKey} origin=${record.origin} created=true`
5626
+ );
5627
+ return { created: true };
5628
+ } catch (err) {
5629
+ const reason = err instanceof Error ? err.message : String(err);
5630
+ console.error(
5631
+ `[telegram-message-store] FAIL op=append reason=${reason.slice(0, 100)} messageId=${record.messageId}`
5632
+ );
5633
+ return { created: false };
5634
+ }
5635
+ }
5636
+ function readConversation2(accountId, channelKey) {
5637
+ if (!isStorableChannelKey(channelKey)) return [];
5638
+ const file = conversationFile(accountId, channelKey);
5639
+ try {
5640
+ if (!existsSync(file)) return [];
5641
+ return parseLines(readFileSync(file, "utf8")).sort(byDateSentAsc);
5642
+ } catch (err) {
5643
+ const reason = err instanceof Error ? err.message : String(err);
5644
+ console.error(
5645
+ `[telegram-message-store] FAIL op=read reason=${reason.slice(0, 100)} channelKey=${channelKey}`
5646
+ );
5647
+ return [];
5648
+ }
5649
+ }
5650
+ var summaryCache = /* @__PURE__ */ new Map();
5651
+ function summariseRecords(records2) {
5652
+ if (records2.length === 0) return null;
5653
+ const sorted = records2.slice().sort(byDateSentAsc);
5654
+ return { first: sorted[0], last: sorted[sorted.length - 1] };
5655
+ }
5656
+ function readConversationSummaries2(accountId) {
5657
+ const dir = join(STORE_ROOT2(), accountId);
5658
+ const out = /* @__PURE__ */ new Map();
5659
+ const seen = /* @__PURE__ */ new Set();
5660
+ try {
5661
+ if (!existsSync(dir)) return out;
5662
+ for (const entry2 of readdirSync(dir)) {
5663
+ if (!entry2.endsWith(".jsonl")) continue;
5664
+ const stem = entry2.slice(0, -".jsonl".length);
5665
+ if (!isStorableStem(stem)) continue;
5666
+ const channelKey = fileStemToChannelKey(stem);
5667
+ const file = join(dir, entry2);
5668
+ seen.add(file);
5669
+ let size;
5670
+ let mtimeMs;
5671
+ try {
5672
+ const st = statSync(file);
5673
+ size = st.size;
5674
+ mtimeMs = st.mtimeMs;
5675
+ } catch {
5676
+ continue;
5677
+ }
5678
+ const cached3 = summaryCache.get(file);
5679
+ if (cached3 && cached3.size === size && cached3.mtimeMs === mtimeMs) {
5680
+ out.set(channelKey, cached3.summary);
5681
+ continue;
5682
+ }
5683
+ let summary;
5684
+ try {
5685
+ summary = summariseRecords(parseLines(readFileSync(file, "utf8")));
5686
+ } catch (err) {
5687
+ const reason = err instanceof Error ? err.message : String(err);
5688
+ console.error(
5689
+ `[telegram-message-store] FAIL op=summarise reason=${reason.slice(0, 100)} channelKey=${channelKey}`
5690
+ );
5691
+ continue;
5692
+ }
5693
+ if (!summary) continue;
5694
+ summaryCache.set(file, { size, mtimeMs, summary });
5695
+ out.set(channelKey, summary);
5696
+ }
5697
+ } catch (err) {
5698
+ const reason = err instanceof Error ? err.message : String(err);
5699
+ console.error(`[telegram-message-store] FAIL op=summarise-dir reason=${reason.slice(0, 100)}`);
5700
+ return out;
5701
+ }
5702
+ for (const key2 of [...summaryCache.keys()]) {
5703
+ if (key2.startsWith(dir) && !seen.has(key2)) summaryCache.delete(key2);
5704
+ }
5705
+ return out;
5706
+ }
5707
+
5708
+ // server/lib/route-cache-watchers.ts
5709
+ init_src2();
5466
5710
  function watchDirInto(dir, resource, accountId) {
5467
5711
  try {
5468
5712
  const w = watch(dir, { recursive: true }, () => {
@@ -5483,9 +5727,9 @@ function watchDirInto(dir, resource, accountId) {
5483
5727
  var perAccountAttached = /* @__PURE__ */ new Set();
5484
5728
  function ensureAccountWatchers(accountId, accountDir) {
5485
5729
  for (const [dir, resource] of [
5486
- [resolve(accountDir, "plugins"), "skills"],
5487
- [resolve(accountDir, "agents", "admin"), "sidebar-artefacts"],
5488
- [resolve(accountDir, "specialists", "agents"), "sidebar-artefacts"]
5730
+ [resolve2(accountDir, "plugins"), "skills"],
5731
+ [resolve2(accountDir, "agents", "admin"), "sidebar-artefacts"],
5732
+ [resolve2(accountDir, "specialists", "agents"), "sidebar-artefacts"]
5489
5733
  ]) {
5490
5734
  if (perAccountAttached.has(dir)) continue;
5491
5735
  const { attached: attached2 } = watchDirInto(dir, resource, accountId);
@@ -5498,11 +5742,11 @@ function attachRouteCacheWatchers() {
5498
5742
  attached = true;
5499
5743
  const configDir = process.env.CLAUDE_CONFIG_DIR;
5500
5744
  if (configDir) {
5501
- watchDirInto(resolve(configDir, "projects"), "sidebar-sessions", null);
5745
+ watchDirInto(resolve2(configDir, "projects"), "sidebar-sessions", null);
5502
5746
  } else {
5503
5747
  console.error("[admin-events] op=watch-failed resource=sidebar-sessions reason=CLAUDE_CONFIG_DIR-unset");
5504
5748
  }
5505
- watchDirInto(resolve(PLATFORM_ROOT, "templates", "specialists", "agents"), "sidebar-artefacts", null);
5749
+ watchDirInto(resolve2(PLATFORM_ROOT, "templates", "specialists", "agents"), "sidebar-artefacts", null);
5506
5750
  let houseId = null;
5507
5751
  try {
5508
5752
  houseId = resolveHouseOrSoleAccountId(ACCOUNTS_DIR);
@@ -5511,6 +5755,7 @@ function attachRouteCacheWatchers() {
5511
5755
  }
5512
5756
  if (houseId) {
5513
5757
  watchDirInto(STORE_ROOT(), "conversations-all", houseId);
5758
+ watchDirInto(STORE_ROOT2(), "conversations-all", houseId);
5514
5759
  } else {
5515
5760
  console.error("[admin-events] op=watch-failed resource=conversations-all reason=house-account-unresolved");
5516
5761
  }
@@ -5707,8 +5952,8 @@ var _baseMimes = {
5707
5952
  var baseMimes = _baseMimes;
5708
5953
 
5709
5954
  // node_modules/@hono/node-server/dist/serve-static.mjs
5710
- import { createReadStream, statSync, existsSync } from "fs";
5711
- import { join } from "path";
5955
+ import { createReadStream, statSync as statSync2, existsSync as existsSync2 } from "fs";
5956
+ import { join as join2 } from "path";
5712
5957
  import { versions } from "process";
5713
5958
  import { Readable } from "stream";
5714
5959
  var COMPRESSIBLE_CONTENT_TYPE_REGEX = /^\s*(?:text\/[^;\s]+|application\/(?:javascript|json|xml|xml-dtd|ecmascript|dart|postscript|rtf|tar|toml|vnd\.dart|vnd\.ms-fontobject|vnd\.ms-opentype|wasm|x-httpd-php|x-javascript|x-ns-proxy-autoconfig|x-sh|x-tar|x-virtualbox-hdd|x-virtualbox-ova|x-virtualbox-ovf|x-virtualbox-vbox|x-virtualbox-vdi|x-virtualbox-vhd|x-virtualbox-vmdk|x-www-form-urlencoded)|font\/(?:otf|ttf)|image\/(?:bmp|vnd\.adobe\.photoshop|vnd\.microsoft\.icon|vnd\.ms-dds|x-icon|x-ms-bmp)|message\/rfc822|model\/gltf-binary|x-shader\/x-fragment|x-shader\/x-vertex|[^;\s]+?\+(?:json|text|xml|yaml))(?:[;\s]|$)/i;
@@ -5748,7 +5993,7 @@ var createStreamBody = (stream2) => {
5748
5993
  var getStats = (path) => {
5749
5994
  let stats;
5750
5995
  try {
5751
- stats = statSync(path);
5996
+ stats = statSync2(path);
5752
5997
  } catch {
5753
5998
  }
5754
5999
  return stats;
@@ -5770,7 +6015,7 @@ var tryDecodeURI = (str2) => tryDecode(str2, decodeURI);
5770
6015
  var serveStatic = (options = { root: "" }) => {
5771
6016
  const root = options.root || "";
5772
6017
  const optionPath = options.path;
5773
- if (root !== "" && !existsSync(root)) {
6018
+ if (root !== "" && !existsSync2(root)) {
5774
6019
  console.error(`serveStatic: root path '${root}' is not found, are you sure it's correct?`);
5775
6020
  }
5776
6021
  return async (c, next) => {
@@ -5791,14 +6036,14 @@ var serveStatic = (options = { root: "" }) => {
5791
6036
  return next();
5792
6037
  }
5793
6038
  }
5794
- let path = join(
6039
+ let path = join2(
5795
6040
  root,
5796
6041
  !optionPath && options.rewriteRequestPath ? options.rewriteRequestPath(filename, c) : filename
5797
6042
  );
5798
6043
  let stats = getStats(path);
5799
6044
  if (stats && stats.isDirectory()) {
5800
6045
  const indexFile2 = options.index ?? "index.html";
5801
- path = join(path, indexFile2);
6046
+ path = join2(path, indexFile2);
5802
6047
  stats = getStats(path);
5803
6048
  }
5804
6049
  if (!stats) {
@@ -5856,13 +6101,13 @@ var serveStatic = (options = { root: "" }) => {
5856
6101
  };
5857
6102
 
5858
6103
  // server/lib/static-cache.ts
5859
- import { statSync as statSync2 } from "fs";
5860
- import { join as join2 } from "path";
6104
+ import { statSync as statSync3 } from "fs";
6105
+ import { join as join3 } from "path";
5861
6106
  var IMMUTABLE = "public, max-age=31536000, immutable";
5862
6107
  var REVALIDATE = "public, no-cache";
5863
6108
  function statOrNull(path) {
5864
6109
  try {
5865
- return statSync2(path);
6110
+ return statSync3(path);
5866
6111
  } catch {
5867
6112
  return null;
5868
6113
  }
@@ -5883,10 +6128,10 @@ function tryDecodeURI2(str2) {
5883
6128
  function resolveStaticFile(root, reqPath) {
5884
6129
  const filename = tryDecodeURI2(reqPath);
5885
6130
  if (/(?:^|[/\\])\.\.(?:$|[/\\])/.test(filename)) return null;
5886
- let path = join2(root, filename);
6131
+ let path = join3(root, filename);
5887
6132
  let stats = statOrNull(path);
5888
6133
  if (stats?.isDirectory()) {
5889
- path = join2(path, "index.html");
6134
+ path = join3(path, "index.html");
5890
6135
  stats = statOrNull(path);
5891
6136
  }
5892
6137
  if (!stats?.isFile()) return null;
@@ -5961,8 +6206,8 @@ ${clientErrorReporterBody}
5961
6206
  </script>`;
5962
6207
 
5963
6208
  // app/lib/whatsapp/ingest-audit.ts
5964
- import { readdirSync, readFileSync, statSync as statSync3 } from "fs";
5965
- import { join as join3 } from "path";
6209
+ import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync4 } from "fs";
6210
+ import { join as join4 } from "path";
5966
6211
  init_message_store();
5967
6212
  var TAG2 = "[wa-ingest-audit]";
5968
6213
  var NOTIFY_TAG = "[wa-notify]";
@@ -5983,16 +6228,16 @@ function readAllConversationsCached(accountId) {
5983
6228
  const out = /* @__PURE__ */ new Map();
5984
6229
  let dir;
5985
6230
  try {
5986
- dir = readdirSync(join3(STORE_ROOT(), accountId));
6231
+ dir = readdirSync2(join4(STORE_ROOT(), accountId));
5987
6232
  } catch {
5988
6233
  return out;
5989
6234
  }
5990
6235
  for (const file of dir) {
5991
6236
  if (!file.endsWith(".jsonl")) continue;
5992
- const path = join3(STORE_ROOT(), accountId, file);
6237
+ const path = join4(STORE_ROOT(), accountId, file);
5993
6238
  let stat11;
5994
6239
  try {
5995
- stat11 = statSync3(path);
6240
+ stat11 = statSync4(path);
5996
6241
  } catch {
5997
6242
  continue;
5998
6243
  }
@@ -6003,7 +6248,7 @@ function readAllConversationsCached(accountId) {
6003
6248
  }
6004
6249
  const parsed = [];
6005
6250
  try {
6006
- for (const line of readFileSync(path, "utf8").split("\n")) {
6251
+ for (const line of readFileSync2(path, "utf8").split("\n")) {
6007
6252
  if (!line.trim()) continue;
6008
6253
  try {
6009
6254
  const row = JSON.parse(line);
@@ -6119,17 +6364,17 @@ function runNotifyReconcile(opts) {
6119
6364
  init_parse_transcript();
6120
6365
  import { mkdtempSync, rmSync, writeFileSync } from "fs";
6121
6366
  import { tmpdir } from "os";
6122
- import { join as join4 } from "path";
6367
+ import { join as join5 } from "path";
6123
6368
 
6124
6369
  // server/lib/tail-window.ts
6125
- import { openSync, readSync, closeSync, statSync as statSync4 } from "fs";
6370
+ import { openSync, readSync, closeSync, statSync as statSync5 } from "fs";
6126
6371
  var TAIL_WINDOW_BYTES = 1024 * 1024;
6127
6372
  var ALIGN_SCAN_MAX_BYTES = 4 * 1024 * 1024;
6128
6373
  function tailWindowStart(fileEnd, budget = TAIL_WINDOW_BYTES) {
6129
6374
  return Math.max(0, fileEnd - budget);
6130
6375
  }
6131
6376
  function readRange(path, from, to) {
6132
- const size = statSync4(path).size;
6377
+ const size = statSync5(path).size;
6133
6378
  const start = Math.max(0, from);
6134
6379
  const end = Math.min(to, size);
6135
6380
  if (end <= start) return Buffer.alloc(0);
@@ -6251,8 +6496,62 @@ function censusSnapshot() {
6251
6496
  return { windowed: windowedOpens.size, withBackfill };
6252
6497
  }
6253
6498
 
6499
+ // server/lib/frame-strip-census.ts
6500
+ var MAX_ACCOUNTS = 50;
6501
+ var MAX_UNSTRIPPED_SESSIONS = 3;
6502
+ var byAccount = /* @__PURE__ */ new Map();
6503
+ function rowFor(accountId) {
6504
+ const existing = byAccount.get(accountId);
6505
+ if (existing) return existing;
6506
+ if (byAccount.size >= MAX_ACCOUNTS) {
6507
+ const oldest = byAccount.keys().next();
6508
+ if (!oldest.done) byAccount.delete(oldest.value);
6509
+ }
6510
+ const fresh = {
6511
+ accountId,
6512
+ rows: 0,
6513
+ wrapped: 0,
6514
+ nested: 0,
6515
+ stripped: 0,
6516
+ unstripped: 0,
6517
+ sources: { whatsapp: 0, webchat: 0, telegram: 0, none: 0 },
6518
+ unstrippedSessions: []
6519
+ };
6520
+ byAccount.set(accountId, fresh);
6521
+ return fresh;
6522
+ }
6523
+ function foldFrameCensus(accountId, sessionId, acc) {
6524
+ if (acc.rows === 0) return;
6525
+ const row = rowFor(accountId);
6526
+ row.rows += acc.rows;
6527
+ row.wrapped += acc.wrapped;
6528
+ row.nested += acc.nested;
6529
+ row.stripped += acc.stripped;
6530
+ row.unstripped += acc.unstripped;
6531
+ row.sources.whatsapp += acc.sources.whatsapp;
6532
+ row.sources.webchat += acc.sources.webchat;
6533
+ row.sources.telegram += acc.sources.telegram;
6534
+ row.sources.none += acc.sources.none;
6535
+ if (acc.unstripped > 0 && row.unstrippedSessions.length < MAX_UNSTRIPPED_SESSIONS && !row.unstrippedSessions.includes(sessionId)) {
6536
+ row.unstrippedSessions.push(sessionId);
6537
+ }
6538
+ acc.rows = 0;
6539
+ acc.wrapped = 0;
6540
+ acc.nested = 0;
6541
+ acc.stripped = 0;
6542
+ acc.unstripped = 0;
6543
+ acc.sources.whatsapp = 0;
6544
+ acc.sources.webchat = 0;
6545
+ acc.sources.telegram = 0;
6546
+ acc.sources.none = 0;
6547
+ }
6548
+ function frameCensusSnapshot() {
6549
+ return [...byAccount.values()];
6550
+ }
6551
+
6254
6552
  // server/lib/reader-window-audit.ts
6255
6553
  var TAG3 = "[reader-audit]";
6554
+ var FRAME_TAG = "[chat-reader]";
6256
6555
  var FIXTURE_ROWS = 60;
6257
6556
  var FIXTURE_BUDGET = 2e3;
6258
6557
  function fixtureBody() {
@@ -6286,8 +6585,25 @@ var key = (t) => JSON.stringify(t);
6286
6585
  function runReaderWindowAudit() {
6287
6586
  const census = censusSnapshot();
6288
6587
  console.log(`${TAG3} op=backfill-reach windowed=${census.windowed} withBackfill=${census.withBackfill}`);
6289
- const dir = mkdtempSync(join4(tmpdir(), "reader-audit-"));
6290
- const path = join4(dir, "fixture.jsonl");
6588
+ const frames = frameCensusSnapshot();
6589
+ let frameRows = 0;
6590
+ let frameUnstripped = 0;
6591
+ if (frames.length === 0) {
6592
+ console.log(`${FRAME_TAG} op=frame-census accounts=0`);
6593
+ }
6594
+ for (const f of frames) {
6595
+ frameRows += f.rows;
6596
+ frameUnstripped += f.unstripped;
6597
+ console.log(
6598
+ `${FRAME_TAG} op=frame-census accountId=${f.accountId} rows=${f.rows} wrapped=${f.wrapped} nested=${f.nested} stripped=${f.stripped} sources=whatsapp:${f.sources.whatsapp},webchat:${f.sources.webchat},telegram:${f.sources.telegram},none:${f.sources.none}`
6599
+ );
6600
+ console.log(
6601
+ `${FRAME_TAG} op=frame-unstripped accountId=${f.accountId} rows=${f.unstripped} sessions=${f.unstrippedSessions.join(",")}`
6602
+ );
6603
+ }
6604
+ const frameSummary = `frames=${frameRows} unstripped=${frameUnstripped}`;
6605
+ const dir = mkdtempSync(join5(tmpdir(), "reader-audit-"));
6606
+ const path = join5(dir, "fixture.jsonl");
6291
6607
  try {
6292
6608
  const body = fixtureBody();
6293
6609
  writeFileSync(path, body);
@@ -6332,11 +6648,11 @@ function runReaderWindowAudit() {
6332
6648
  `${TAG3} op=window-kinds fixture=synthetic windowTurns=${windowTurns.length} mismatched=${mismatched}`
6333
6649
  );
6334
6650
  const ok2 = duplicates === 0 && gaps === 0 && ordered && walkTerminated && mismatched === 0;
6335
- return `continuity=${ok2 ? "ok" : "FAILED"} full=${full.length} assembled=${assembled.length} walk=${walkTerminated ? "ok" : "STUCK"} reach=${census.withBackfill}/${census.windowed}`;
6651
+ return `continuity=${ok2 ? "ok" : "FAILED"} full=${full.length} assembled=${assembled.length} walk=${walkTerminated ? "ok" : "STUCK"} reach=${census.withBackfill}/${census.windowed} ${frameSummary}`;
6336
6652
  } catch (err) {
6337
6653
  const reason = err instanceof Error ? err.message : String(err);
6338
6654
  console.error(`${TAG3} op=audit-failed reason=${reason.slice(0, 120)}`);
6339
- return `continuity=ERROR ${reason.slice(0, 60)}`;
6655
+ return `continuity=ERROR ${reason.slice(0, 60)} ${frameSummary}`;
6340
6656
  } finally {
6341
6657
  try {
6342
6658
  rmSync(dir, { recursive: true, force: true });
@@ -6398,12 +6714,12 @@ init_src();
6398
6714
  init_pwa();
6399
6715
 
6400
6716
  // server/webchat-descriptor.ts
6401
- import { resolve as resolve2, join as join5 } from "path";
6717
+ import { resolve as resolve3, join as join6 } from "path";
6402
6718
  function webchatGatewayUrl() {
6403
6719
  return `http://127.0.0.1:${process.env.MAXY_UI_INTERNAL_PORT ?? ""}`;
6404
6720
  }
6405
6721
  function webchatServerPath() {
6406
- return process.env.MAXY_WEBCHAT_CHANNEL_SERVER_PATH ?? resolve2(process.env.MAXY_PLATFORM_ROOT ?? join5(__dirname, ".."), "services/webchat-channel/dist/server.js");
6722
+ return process.env.MAXY_WEBCHAT_CHANNEL_SERVER_PATH ?? resolve3(process.env.MAXY_PLATFORM_ROOT ?? join6(__dirname, ".."), "services/webchat-channel/dist/server.js");
6407
6723
  }
6408
6724
  function webchatChannelDescriptor(key2) {
6409
6725
  return { key: key2, gatewayUrl: webchatGatewayUrl(), serverPath: webchatServerPath() };
@@ -6421,7 +6737,7 @@ init_paths();
6421
6737
  // app/lib/claude-auth.ts
6422
6738
  init_paths();
6423
6739
  init_loop_registry();
6424
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, renameSync, unlinkSync } from "fs";
6740
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, renameSync, unlinkSync } from "fs";
6425
6741
  import { sep } from "path";
6426
6742
  import { lock } from "proper-lockfile";
6427
6743
  if (!CLAUDE_CREDENTIALS_FILE.startsWith(MAXY_DIR + sep)) {
@@ -6438,7 +6754,7 @@ var REFRESH_NOT_FIRING_THRESHOLD = 2;
6438
6754
  function readCredentials() {
6439
6755
  let raw;
6440
6756
  try {
6441
- raw = readFileSync2(CLAUDE_CREDENTIALS_FILE, "utf-8");
6757
+ raw = readFileSync3(CLAUDE_CREDENTIALS_FILE, "utf-8");
6442
6758
  } catch {
6443
6759
  return null;
6444
6760
  }
@@ -6461,20 +6777,32 @@ function readCredentials() {
6461
6777
  expiresAt
6462
6778
  };
6463
6779
  }
6464
- function writeOAuthCredentials(tokens) {
6780
+ function logCredentialWrite(site, refreshTokenLen) {
6781
+ console.log(`[claude-auth] op=cred-write site=${site} refreshTokenLen=${refreshTokenLen}`);
6782
+ }
6783
+ function writeHostCredsFile(credentialsFile, blob, refreshTokenLen) {
6784
+ const tmp = `${credentialsFile}.${process.pid}.tmp`;
6785
+ writeFileSync2(tmp, blob, { mode: 384 });
6786
+ renameSync(tmp, credentialsFile);
6787
+ logCredentialWrite("login-writeback", refreshTokenLen);
6788
+ }
6789
+ function writeOAuthCredentials(tokens, site) {
6465
6790
  const newExpiresAt = typeof tokens.expiresIn === "number" && tokens.expiresIn > 0 ? Date.now() + tokens.expiresIn * 1e3 : Date.now() + 3600 * 1e3;
6466
6791
  try {
6467
6792
  let fileData = {};
6468
6793
  try {
6469
- const rawFile = readFileSync2(CLAUDE_CREDENTIALS_FILE, "utf-8");
6794
+ const rawFile = readFileSync3(CLAUDE_CREDENTIALS_FILE, "utf-8");
6470
6795
  fileData = JSON.parse(rawFile);
6471
6796
  } catch {
6472
6797
  }
6473
6798
  const existingOauth = fileData.claudeAiOauth;
6799
+ const existingRefresh = typeof existingOauth?.refreshToken === "string" ? existingOauth.refreshToken : "";
6800
+ const incomingRefresh = typeof tokens.refreshToken === "string" ? tokens.refreshToken : "";
6801
+ const blankingRefused = incomingRefresh.length === 0 && existingRefresh.length > 0;
6474
6802
  fileData.claudeAiOauth = {
6475
6803
  ...existingOauth,
6476
6804
  accessToken: tokens.accessToken,
6477
- ...tokens.refreshToken != null ? { refreshToken: tokens.refreshToken } : {},
6805
+ ...incomingRefresh.length > 0 ? { refreshToken: incomingRefresh } : {},
6478
6806
  expiresAt: newExpiresAt
6479
6807
  };
6480
6808
  const tmpPath = `${CLAUDE_CREDENTIALS_FILE}.${process.pid}.tmp`;
@@ -6488,6 +6816,13 @@ function writeOAuthCredentials(tokens) {
6488
6816
  }
6489
6817
  throw writeErr;
6490
6818
  }
6819
+ if (blankingRefused) {
6820
+ console.warn(
6821
+ `[claude-auth] op=cred-write-refused site=${site} reason=blank-refresh-token storedLen=${existingRefresh.length}`
6822
+ );
6823
+ }
6824
+ const landed = fileData.claudeAiOauth.refreshToken;
6825
+ logCredentialWrite(site, typeof landed === "string" ? landed.length : 0);
6491
6826
  return newExpiresAt;
6492
6827
  } catch (err) {
6493
6828
  console.error(`[claude-auth] credential write failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -6617,9 +6952,11 @@ async function doRefreshLocked() {
6617
6952
  }
6618
6953
  const newExpiresAt = writeOAuthCredentials({
6619
6954
  accessToken: newAccessToken,
6620
- refreshToken: newRefreshToken ?? creds.refreshToken,
6955
+ // Task 2572 — `??` does not fall back on the empty string, so a response
6956
+ // carrying `refresh_token: ""` used to overwrite a good stored token.
6957
+ refreshToken: typeof newRefreshToken === "string" && newRefreshToken.length > 0 ? newRefreshToken : creds.refreshToken,
6621
6958
  expiresIn: typeof expiresIn === "number" && expiresIn > 0 ? expiresIn : void 0
6622
- });
6959
+ }, "refresh");
6623
6960
  if (newExpiresAt) {
6624
6961
  console.log(`[auth-refresh] op=renewed expiresAt=${new Date(newExpiresAt).toISOString()}`);
6625
6962
  } else {
@@ -6694,7 +7031,7 @@ init_helpers();
6694
7031
  // server/routes/health.ts
6695
7032
  init_dist();
6696
7033
  init_paths();
6697
- import { existsSync as existsSync2, readFileSync as readFileSync4 } from "fs";
7034
+ import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
6698
7035
  import { createConnection as createConnection2 } from "net";
6699
7036
 
6700
7037
  // app/lib/network.ts
@@ -6719,11 +7056,11 @@ function getLanIp() {
6719
7056
  init_paths();
6720
7057
  import { spawnSync, execFileSync } from "child_process";
6721
7058
  import { createConnection } from "net";
6722
- import { mkdirSync, readFileSync as readFileSync3, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
6723
- import { resolve as resolve3 } from "path";
7059
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync4, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
7060
+ import { resolve as resolve4 } from "path";
6724
7061
  init_remote_auth();
6725
- var PLATFORM_ROOT2 = process.env.MAXY_PLATFORM_ROOT ?? resolve3(process.cwd(), "..");
6726
- var VNC_SCRIPT = resolve3(PLATFORM_ROOT2, "scripts/vnc.sh");
7062
+ var PLATFORM_ROOT2 = process.env.MAXY_PLATFORM_ROOT ?? resolve4(process.cwd(), "..");
7063
+ var VNC_SCRIPT = resolve4(PLATFORM_ROOT2, "scripts/vnc.sh");
6727
7064
  var displayMode = process.env.DISPLAY_MODE ?? "virtual";
6728
7065
  if (displayMode === "native") {
6729
7066
  console.log(`[vnc] DISPLAY_MODE=native \u2014 local requests use desktop display, remote requests use VNC`);
@@ -6778,7 +7115,7 @@ function discoverNativeDisplay() {
6778
7115
  const leaderPid = leaderResult.stdout?.trim();
6779
7116
  if (leaderPid) {
6780
7117
  try {
6781
- const environ = readFileSync3(`/proc/${leaderPid}/environ`, "utf8");
7118
+ const environ = readFileSync4(`/proc/${leaderPid}/environ`, "utf8");
6782
7119
  const match = environ.split("\0").find((e) => e.startsWith("WAYLAND_DISPLAY="));
6783
7120
  if (match) waylandDisplay = match.split("=")[1];
6784
7121
  } catch {
@@ -6833,10 +7170,10 @@ async function waitForPort(port2, timeoutMs = 12e3) {
6833
7170
  return false;
6834
7171
  }
6835
7172
  function ensureLogDir() {
6836
- mkdirSync(LOG_DIR, { recursive: true });
7173
+ mkdirSync2(LOG_DIR, { recursive: true });
6837
7174
  }
6838
7175
  function logPath(name) {
6839
- return resolve3(LOG_DIR, `${name}.log`);
7176
+ return resolve4(LOG_DIR, `${name}.log`);
6840
7177
  }
6841
7178
  async function ensureVnc() {
6842
7179
  if (process.platform !== "linux") {
@@ -6921,19 +7258,36 @@ async function ensureCdp(transport = "vnc") {
6921
7258
  function killChromium() {
6922
7259
  spawnSync("pkill", ["-f", "chromium"], { stdio: "pipe" });
6923
7260
  }
6924
- var AUTH_TARGET_ID_FILE = resolve3(LOG_DIR, "claude-auth-target.id");
7261
+ var AUTH_TARGET_ID_FILE = resolve4(LOG_DIR, "claude-auth-target.id");
6925
7262
  function readAuthTargetId() {
6926
7263
  try {
6927
- const id = readFileSync3(AUTH_TARGET_ID_FILE, "utf8").trim();
7264
+ const parts = readFileSync4(AUTH_TARGET_ID_FILE, "utf8").trim().split(/\s+/);
7265
+ const id = parts[parts.length - 1];
6928
7266
  return id.length > 0 ? id : null;
6929
7267
  } catch {
6930
7268
  return null;
6931
7269
  }
6932
7270
  }
6933
- function writeChromiumWrapper() {
6934
- mkdirSync(BIN_DIR, { recursive: true });
7271
+ function readAuthAttemptId() {
7272
+ try {
7273
+ const parts = readFileSync4(AUTH_TARGET_ID_FILE, "utf8").trim().split(/\s+/);
7274
+ return parts.length === 2 && parts[0].length > 0 ? parts[0] : null;
7275
+ } catch {
7276
+ return null;
7277
+ }
7278
+ }
7279
+ async function closeAuthTarget(targetId) {
7280
+ try {
7281
+ const res = await fetch(`http://127.0.0.1:${CDP_PORT}/json/close/${encodeURIComponent(targetId)}`);
7282
+ return res.ok;
7283
+ } catch {
7284
+ return false;
7285
+ }
7286
+ }
7287
+ function writeChromiumWrapper(attemptId) {
7288
+ mkdirSync2(BIN_DIR, { recursive: true });
6935
7289
  rmSync2(AUTH_TARGET_ID_FILE, { force: true });
6936
- const wrapperPath = resolve3(BIN_DIR, "chromium");
7290
+ const wrapperPath = resolve4(BIN_DIR, "chromium");
6937
7291
  writeFileSync3(wrapperPath, `#!/bin/bash
6938
7292
  # Generated by writeChromiumWrapper (app/lib/vnc.ts) \u2014 Task 1562.
6939
7293
  # Hands its URL argument to the brand's running Chromium over CDP.
@@ -6941,6 +7295,9 @@ LOG_DIR="${LOG_DIR}"
6941
7295
  LOG="$LOG_DIR/chromium.log"
6942
7296
  TARGET_ID_FILE="$LOG_DIR/claude-auth-target.id"
6943
7297
  CDP="http://127.0.0.1:${CDP_PORT}/json/new"
7298
+ # The sign-in this wrapper was generated for. Server-minted hex from
7299
+ # randomUUID().slice(0, 8), never caller-supplied, so it carries no shell.
7300
+ ATTEMPT_ID="${attemptId}"
6944
7301
 
6945
7302
  mkdir -p "$LOG_DIR"
6946
7303
  echo "==== [$(date)] chromium wrapper (cdp-handover) ====" >> "$LOG"
@@ -6972,9 +7329,9 @@ if [ -z "$TARGET_ID" ]; then
6972
7329
  exit 65
6973
7330
  fi
6974
7331
 
6975
- printf '%s' "$TARGET_ID" > "$TARGET_ID_FILE"
6976
- echo " opened target $TARGET_ID" >> "$LOG"
6977
- echo "[chromium-wrapper] op=cdp-handover outcome=ready targetId=$TARGET_ID" >> "$LOG"
7332
+ printf '%s %s' "$ATTEMPT_ID" "$TARGET_ID" > "$TARGET_ID_FILE"
7333
+ echo " opened target $TARGET_ID for attempt $ATTEMPT_ID" >> "$LOG"
7334
+ echo "[chromium-wrapper] op=cdp-handover outcome=ready attempt=$ATTEMPT_ID targetId=$TARGET_ID" >> "$LOG"
6978
7335
  `, { mode: 493 });
6979
7336
  return wrapperPath;
6980
7337
  }
@@ -7035,8 +7392,8 @@ app.get("/", async (c) => {
7035
7392
  const browserTransport = resolveBrowserTransport(c.req.raw, c.env?.incoming?.socket?.remoteAddress);
7036
7393
  let pinConfigured = false;
7037
7394
  try {
7038
- if (existsSync2(USERS_FILE)) {
7039
- const raw = readFileSync4(USERS_FILE, "utf-8").trim();
7395
+ if (existsSync3(USERS_FILE)) {
7396
+ const raw = readFileSync5(USERS_FILE, "utf-8").trim();
7040
7397
  if (raw) {
7041
7398
  const users = JSON.parse(raw);
7042
7399
  pinConfigured = Array.isArray(users) && users.length > 0;
@@ -7529,183 +7886,6 @@ init_dist();
7529
7886
  init_channel_key();
7530
7887
  import { statSync as statSync7 } from "fs";
7531
7888
  import { join as join12 } from "path";
7532
-
7533
- // app/lib/telegram/message-store.ts
7534
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync5, appendFileSync, readdirSync as readdirSync2, statSync as statSync5 } from "fs";
7535
- import { homedir } from "os";
7536
- import { dirname, join as join7, resolve as resolve4 } from "path";
7537
- function configDirName() {
7538
- const platformRoot5 = process.env.MAXY_PLATFORM_ROOT ?? resolve4(process.cwd(), "..");
7539
- const brandPath = join7(platformRoot5, "config", "brand.json");
7540
- if (existsSync3(brandPath)) {
7541
- try {
7542
- return JSON.parse(readFileSync5(brandPath, "utf-8")).configDir ?? ".maxy";
7543
- } catch {
7544
- }
7545
- }
7546
- return ".maxy";
7547
- }
7548
- var STORE_ROOT2 = () => join7(process.env.MAXY_DIR_OVERRIDE ?? resolve4(homedir(), configDirName()), "data", "telegram-messages");
7549
- function isStorableChannelKey(channelKey) {
7550
- return /^-?\d+:-?\d+$/.test(channelKey);
7551
- }
7552
- function channelKeyToFileStem(channelKey) {
7553
- if (!isStorableChannelKey(channelKey)) {
7554
- throw new Error(`telegram-message-store: refusing unstorable channelKey ${JSON.stringify(channelKey)}`);
7555
- }
7556
- return channelKey.replace(":", "_");
7557
- }
7558
- function fileStemToChannelKey(stem) {
7559
- const i = stem.indexOf("_");
7560
- return i <= 0 ? stem : `${stem.slice(0, i)}:${stem.slice(i + 1)}`;
7561
- }
7562
- function conversationFile(accountId, channelKey) {
7563
- return join7(STORE_ROOT2(), accountId, `${channelKeyToFileStem(channelKey)}.jsonl`);
7564
- }
7565
- function conversationFilePath2(accountId, channelKey) {
7566
- return conversationFile(accountId, channelKey);
7567
- }
7568
- function isStorableStem(stem) {
7569
- return isStorableChannelKey(fileStemToChannelKey(stem));
7570
- }
7571
- function listStoredAccountIds2() {
7572
- const root = STORE_ROOT2();
7573
- if (!existsSync3(root)) return [];
7574
- return readdirSync2(root).filter((name) => {
7575
- try {
7576
- return statSync5(join7(root, name)).isDirectory();
7577
- } catch {
7578
- return false;
7579
- }
7580
- });
7581
- }
7582
- function parseLines(raw) {
7583
- const out = [];
7584
- for (const line of raw.split("\n")) {
7585
- if (!line.trim()) continue;
7586
- try {
7587
- out.push(JSON.parse(line));
7588
- } catch {
7589
- console.error(`[telegram-message-store] skip reason=corrupt-line`);
7590
- }
7591
- }
7592
- return out;
7593
- }
7594
- var byDateSentAsc = (a, b) => a.dateSent.localeCompare(b.dateSent);
7595
- function appendMessage2(accountId, record) {
7596
- if (!isStorableChannelKey(record.channelKey)) {
7597
- console.error(
7598
- `[telegram-message-store] FAIL op=append reason=unstorable-channel-key messageId=${record.messageId}`
7599
- );
7600
- return { created: false };
7601
- }
7602
- const file = conversationFile(accountId, record.channelKey);
7603
- try {
7604
- mkdirSync2(dirname(file), { recursive: true });
7605
- } catch (err) {
7606
- const reason = err instanceof Error ? err.message : String(err);
7607
- console.error(
7608
- `[telegram-message-store] FAIL op=append reason=${reason.slice(0, 100)} messageId=${record.messageId}`
7609
- );
7610
- return { created: false };
7611
- }
7612
- try {
7613
- if (existsSync3(file)) {
7614
- const existing = parseLines(readFileSync5(file, "utf8"));
7615
- if (existing.some((r) => r.messageId === record.messageId)) {
7616
- console.error(
7617
- `[telegram-message-store] op=append channelKey=${record.channelKey} origin=${record.origin} created=false`
7618
- );
7619
- return { created: false };
7620
- }
7621
- }
7622
- appendFileSync(file, `${JSON.stringify(record)}
7623
- `);
7624
- console.error(
7625
- `[telegram-message-store] op=append channelKey=${record.channelKey} origin=${record.origin} created=true`
7626
- );
7627
- return { created: true };
7628
- } catch (err) {
7629
- const reason = err instanceof Error ? err.message : String(err);
7630
- console.error(
7631
- `[telegram-message-store] FAIL op=append reason=${reason.slice(0, 100)} messageId=${record.messageId}`
7632
- );
7633
- return { created: false };
7634
- }
7635
- }
7636
- function readConversation2(accountId, channelKey) {
7637
- if (!isStorableChannelKey(channelKey)) return [];
7638
- const file = conversationFile(accountId, channelKey);
7639
- try {
7640
- if (!existsSync3(file)) return [];
7641
- return parseLines(readFileSync5(file, "utf8")).sort(byDateSentAsc);
7642
- } catch (err) {
7643
- const reason = err instanceof Error ? err.message : String(err);
7644
- console.error(
7645
- `[telegram-message-store] FAIL op=read reason=${reason.slice(0, 100)} channelKey=${channelKey}`
7646
- );
7647
- return [];
7648
- }
7649
- }
7650
- var summaryCache = /* @__PURE__ */ new Map();
7651
- function summariseRecords(records2) {
7652
- if (records2.length === 0) return null;
7653
- const sorted = records2.slice().sort(byDateSentAsc);
7654
- return { first: sorted[0], last: sorted[sorted.length - 1] };
7655
- }
7656
- function readConversationSummaries2(accountId) {
7657
- const dir = join7(STORE_ROOT2(), accountId);
7658
- const out = /* @__PURE__ */ new Map();
7659
- const seen = /* @__PURE__ */ new Set();
7660
- try {
7661
- if (!existsSync3(dir)) return out;
7662
- for (const entry2 of readdirSync2(dir)) {
7663
- if (!entry2.endsWith(".jsonl")) continue;
7664
- const stem = entry2.slice(0, -".jsonl".length);
7665
- if (!isStorableStem(stem)) continue;
7666
- const channelKey = fileStemToChannelKey(stem);
7667
- const file = join7(dir, entry2);
7668
- seen.add(file);
7669
- let size;
7670
- let mtimeMs;
7671
- try {
7672
- const st = statSync5(file);
7673
- size = st.size;
7674
- mtimeMs = st.mtimeMs;
7675
- } catch {
7676
- continue;
7677
- }
7678
- const cached3 = summaryCache.get(file);
7679
- if (cached3 && cached3.size === size && cached3.mtimeMs === mtimeMs) {
7680
- out.set(channelKey, cached3.summary);
7681
- continue;
7682
- }
7683
- let summary;
7684
- try {
7685
- summary = summariseRecords(parseLines(readFileSync5(file, "utf8")));
7686
- } catch (err) {
7687
- const reason = err instanceof Error ? err.message : String(err);
7688
- console.error(
7689
- `[telegram-message-store] FAIL op=summarise reason=${reason.slice(0, 100)} channelKey=${channelKey}`
7690
- );
7691
- continue;
7692
- }
7693
- if (!summary) continue;
7694
- summaryCache.set(file, { size, mtimeMs, summary });
7695
- out.set(channelKey, summary);
7696
- }
7697
- } catch (err) {
7698
- const reason = err instanceof Error ? err.message : String(err);
7699
- console.error(`[telegram-message-store] FAIL op=summarise-dir reason=${reason.slice(0, 100)}`);
7700
- return out;
7701
- }
7702
- for (const key2 of [...summaryCache.keys()]) {
7703
- if (key2.startsWith(dir) && !seen.has(key2)) summaryCache.delete(key2);
7704
- }
7705
- return out;
7706
- }
7707
-
7708
- // server/routes/channel/schedule-inject.ts
7709
7889
  init_admin_session_id();
7710
7890
  init_normalize();
7711
7891
 
@@ -8288,6 +8468,32 @@ async function sendTelegramText(botToken, chatId, text) {
8288
8468
  }
8289
8469
  }
8290
8470
 
8471
+ // app/lib/telegram/outbound/send-chat-action.ts
8472
+ async function sendTelegramChatAction(botToken, chatId, key2, site) {
8473
+ const report = (ok2, reason) => console.error(
8474
+ `[telegram-native] op=chat-action key=${key2} site=${site} ok=${ok2}${ok2 ? "" : ` reason=${reason}`}`
8475
+ );
8476
+ try {
8477
+ const res = await fetch(`https://api.telegram.org/bot${botToken}/sendChatAction`, {
8478
+ method: "POST",
8479
+ headers: { "Content-Type": "application/json" },
8480
+ body: JSON.stringify({ chat_id: chatId, action: "typing" })
8481
+ });
8482
+ const data = await res.json();
8483
+ if (data.ok) {
8484
+ report(true);
8485
+ return { ok: true };
8486
+ }
8487
+ const error = data.description ?? "chat action failed";
8488
+ report(false, error);
8489
+ return { ok: false, error };
8490
+ } catch (e) {
8491
+ const error = e instanceof Error ? e.message : String(e);
8492
+ report(false, error);
8493
+ return { ok: false, error };
8494
+ }
8495
+ }
8496
+
8291
8497
  // server/routes/whatsapp.ts
8292
8498
  init_dist();
8293
8499
  import { basename as basename3, join as join17, resolve as resolve8 } from "path";
@@ -15549,6 +15755,7 @@ function pickActivity(entries2, grown, nowMs, concurrencyWindowMs) {
15549
15755
  }
15550
15756
 
15551
15757
  // server/routes/session-reader.ts
15758
+ init_session_store();
15552
15759
  init_dist();
15553
15760
  var app7 = new Hono();
15554
15761
  function probeDataRef(path) {
@@ -15724,6 +15931,7 @@ data: ${JSON.stringify(turn)}
15724
15931
 
15725
15932
  `));
15726
15933
  const connId = `${sessionId.slice(0, 8)}-${process.hrtime.bigint().toString(36).slice(-6)}`;
15934
+ const censusAccountId = getAccountIdForSession(c.get("cacheKey") ?? "") ?? "unresolved";
15727
15935
  const startedAt = Date.now();
15728
15936
  const lastEventId = c.req.header("Last-Event-ID");
15729
15937
  let teardown = () => {
@@ -15759,7 +15967,8 @@ data: ${JSON.stringify(obj)}
15759
15967
  log: (line) => console.log(line)
15760
15968
  });
15761
15969
  const queuedPendingSuppress = /* @__PURE__ */ new Map();
15762
- const turnCtx = {};
15970
+ const envelopeCensus = emptyEnvelopeCensus();
15971
+ const turnCtx = { envelopeCensus };
15763
15972
  const drain = () => {
15764
15973
  try {
15765
15974
  const { buf, end } = readFrom(jsonlPath, offset);
@@ -15776,6 +15985,7 @@ data: ${JSON.stringify(obj)}
15776
15985
  n++;
15777
15986
  }
15778
15987
  if (n > 0) {
15988
+ foldFrameCensus(censusAccountId, sessionId, envelopeCensus);
15779
15989
  actx.parentGrewThisTick = true;
15780
15990
  console.log(`[wa-stream] op=drain conn=${connId} turns=${n}`);
15781
15991
  }
@@ -15822,6 +16032,7 @@ data: ${JSON.stringify({ from: alignedFrom, fileEnd: fileEnd0 })}
15822
16032
  }
15823
16033
  n++;
15824
16034
  }
16035
+ foldFrameCensus(censusAccountId, sessionId, envelopeCensus);
15825
16036
  console.log(
15826
16037
  `[wa-stream] op=backlog conn=${connId} turns=${n} bytes=${end - alignedFrom} from=${alignedFrom}`
15827
16038
  );
@@ -16005,7 +16216,7 @@ import { basename as basename8, dirname as dirname9, join as join26 } from "path
16005
16216
  // app/lib/reader/operator-conversations.ts
16006
16217
  function operatorConvName(row) {
16007
16218
  if (row.isGroup === true) return row.chatName ?? row.title;
16008
- const name = row.operatorName ?? row.chatName ?? row.whatsappName;
16219
+ const name = row.operatorName ?? row.chatName ?? row.whatsappName ?? row.telegramName;
16009
16220
  const senderId = row.senderId != null && row.channel === "whatsapp" ? formatSenderPhone(row.senderId) : row.senderId;
16010
16221
  if (name) return senderId ? `${name} ${senderId}` : name;
16011
16222
  return senderId ?? row.title;
@@ -16015,6 +16226,7 @@ function operatorConvNameSource(row) {
16015
16226
  if (row.operatorName) return "operator";
16016
16227
  if (row.chatName) return "chatmeta";
16017
16228
  if (row.whatsappName) return "inbound-pushname";
16229
+ if (row.telegramName) return "telegram-sender-name";
16018
16230
  if (row.senderId != null) return "counterparty-number";
16019
16231
  return "jid";
16020
16232
  }
@@ -16234,6 +16446,14 @@ app8.get("/conversations", requireAdminSession, async (c) => {
16234
16446
  );
16235
16447
  const adminScope = allStoreRows.filter((r) => r.scope === "admin").length;
16236
16448
  const publicScope = allStoreRows.length - adminScope;
16449
+ const telegramStoreRows = account ? buildTelegramStoreConversationRows(account.accountId, readConversationSummaries2(account.accountId)) : [];
16450
+ const dedupedSessionRows = dropTelegramSessionDuplicates(sessionConversations, telegramStoreRows);
16451
+ const telegramRows = [
16452
+ ...dedupedSessionRows.filter((r) => r.channel === "telegram"),
16453
+ ...telegramStoreRows
16454
+ ].sort((a, b) => (b.lastMessageAt ?? "").localeCompare(a.lastMessageAt ?? ""));
16455
+ const otherSessionRows = dedupedSessionRows.filter((r) => r.channel !== "telegram");
16456
+ const conversations = [...otherSessionRows, ...allStoreRows, ...telegramRows];
16237
16457
  const census = buildLabelCensus(allStoreRows, selfPushNames(summaries));
16238
16458
  for (const f of fallbackLabelSources(allStoreRows)) {
16239
16459
  console.log(`[wa-label] op=row-label jid=${f.jid} source=${f.source} route=conversations`);
@@ -16241,11 +16461,11 @@ app8.get("/conversations", requireAdminSession, async (c) => {
16241
16461
  console.log(
16242
16462
  `[wa-label-census] route=conversations chats=${census.chats} withMeta=${census.withMeta} groups=${census.groups} groupsWithSubject=${census.groupsWithSubject} labelledByMember=${census.labelledByMember} dms=${census.dms} namedDms=${census.namedDms} labelledBySelfPushName=${census.labelledBySelfPushName}`
16243
16463
  );
16244
- const sessionRowCount = sessionConversations.length;
16464
+ const sessionRowCount = dedupedSessionRows.length;
16245
16465
  console.log(
16246
- `[wa-reader] op=list pinnedAccount=${scopeAccountId?.slice(0, 8) ?? "null"} resolvedAccount=${account?.accountId.slice(0, 8) ?? "null"} multiAccount=${multiAccount} conversations=${sessionRowCount + allStoreRows.length} sessionRows=${sessionRowCount} storeRows=${allStoreRows.length} adminScope=${adminScope} publicScope=${publicScope} sessionRowsExcludedUntagged=${sessionRowsExcludedUntagged} houseResolved=${houseId ? houseId.slice(0, 8) : "null"} managerPhonesBound=${managerPhonesBound} managedStoreRows=${managedRows.length}`
16466
+ `[wa-reader] op=list pinnedAccount=${scopeAccountId?.slice(0, 8) ?? "null"} resolvedAccount=${account?.accountId.slice(0, 8) ?? "null"} multiAccount=${multiAccount} conversations=${conversations.length} sessionRows=${sessionRowCount} storeRows=${allStoreRows.length} storeRowsByChannel=whatsapp:${allStoreRows.length},telegram:${telegramStoreRows.length} adminScope=${adminScope} publicScope=${publicScope} sessionRowsExcludedUntagged=${sessionRowsExcludedUntagged} houseResolved=${houseId ? houseId.slice(0, 8) : "null"} managerPhonesBound=${managerPhonesBound} managedStoreRows=${managedRows.length}`
16247
16467
  );
16248
- return c.json({ conversations: [...sessionConversations, ...allStoreRows] });
16468
+ return c.json({ conversations });
16249
16469
  });
16250
16470
  function tagAndMergeInstallConversations(perAccount) {
16251
16471
  const all = [];
@@ -16256,7 +16476,7 @@ function tagAndMergeInstallConversations(perAccount) {
16256
16476
  }
16257
16477
  function dropHouseRowsOwnedBySubAccount(rows, subManagerPhones) {
16258
16478
  return rows.filter((r) => {
16259
- if (r.isHouse !== true || r.source !== "store" || r.senderId == null) return true;
16479
+ if (r.channel !== "whatsapp" || r.isHouse !== true || r.source !== "store" || r.senderId == null) return true;
16260
16480
  let owner = null;
16261
16481
  for (const [subId, phones] of subManagerPhones) {
16262
16482
  if (counterpartyBoundToSubAccount(phones, r.senderId)) {
@@ -16396,7 +16616,14 @@ app8.get("/conversations-all", requireAdminSession, async (c) => {
16396
16616
  readConversationSummaries(account.accountId),
16397
16617
  readChatMetadata(account.accountId)
16398
16618
  );
16399
- perAccount.push({ isHouse, rows: [...named, ...storeRows] });
16619
+ const telegramStoreRows = buildTelegramStoreConversationRows(
16620
+ account.accountId,
16621
+ readConversationSummaries2(account.accountId)
16622
+ );
16623
+ perAccount.push({
16624
+ isHouse,
16625
+ rows: [...dropTelegramSessionDuplicates(named, telegramStoreRows), ...storeRows, ...telegramStoreRows]
16626
+ });
16400
16627
  }
16401
16628
  const houseAccount = validAccounts.find((a) => a.accountId === houseId) ?? null;
16402
16629
  const subManagerPhones = /* @__PURE__ */ new Map();
@@ -16414,16 +16641,19 @@ app8.get("/conversations-all", requireAdminSession, async (c) => {
16414
16641
  for (const account of validAccounts) {
16415
16642
  for (const [jid, s] of readConversationSummaries(account.accountId)) allSummaries.set(`${account.accountId}:${jid}`, s);
16416
16643
  }
16417
- const census = buildLabelCensus(conversations, selfPushNames(allSummaries));
16418
- for (const f of fallbackLabelSources(conversations)) {
16644
+ const whatsappRows = conversations.filter((r) => r.channel === "whatsapp");
16645
+ const census = buildLabelCensus(whatsappRows, selfPushNames(allSummaries));
16646
+ for (const f of fallbackLabelSources(whatsappRows)) {
16419
16647
  console.log(`[wa-label] op=row-label jid=${f.jid} source=${f.source} route=conversations-all`);
16420
16648
  }
16421
16649
  console.log(
16422
16650
  `[wa-label-census] route=conversations-all chats=${census.chats} withMeta=${census.withMeta} groups=${census.groups} groupsWithSubject=${census.groupsWithSubject} labelledByMember=${census.labelledByMember} dms=${census.dms} namedDms=${census.namedDms} labelledBySelfPushName=${census.labelledBySelfPushName}`
16423
16651
  );
16424
16652
  const houseRows = conversations.filter((r) => r.isHouse).length;
16653
+ const storeByChannel = conversations.filter((r) => r.source === "store");
16654
+ const waStoreRows = storeByChannel.filter((r) => r.channel === "whatsapp").length;
16425
16655
  console.log(
16426
- `[wa-reader] op=conversations-all accounts=${validAccounts.length} rows=${conversations.length} house=${houseRows} sub=${conversations.length - houseRows} deduped=${merged.length - conversations.length}`
16656
+ `[wa-reader] op=conversations-all accounts=${validAccounts.length} rows=${conversations.length} house=${houseRows} sub=${conversations.length - houseRows} deduped=${merged.length - conversations.length} storeRowsByChannel=whatsapp:${waStoreRows},telegram:${storeByChannel.length - waStoreRows}`
16427
16657
  );
16428
16658
  return { conversations };
16429
16659
  });
@@ -18653,7 +18883,7 @@ var quickbooks_default = app12;
18653
18883
  // server/routes/onboarding.ts
18654
18884
  init_dist();
18655
18885
  import { spawn, spawnSync as spawnSync2, execFileSync as execFileSync4 } from "child_process";
18656
- import { openSync as openSync8, closeSync as closeSync8, writeFileSync as writeFileSync14, writeSync, existsSync as existsSync26, readFileSync as readFileSync32, unlinkSync as unlinkSync3, renameSync as renameSync8 } from "fs";
18886
+ import { openSync as openSync8, closeSync as closeSync8, appendFileSync as appendFileSync3, writeFileSync as writeFileSync14, writeSync, existsSync as existsSync26, readFileSync as readFileSync32, unlinkSync as unlinkSync3 } from "fs";
18657
18887
  import { createHash as createHash4, randomUUID as randomUUID4 } from "crypto";
18658
18888
  init_claude_agent();
18659
18889
  init_neo4j_store();
@@ -18726,8 +18956,10 @@ function bridgeKeychainToFile(deps) {
18726
18956
  deps.log(`[onboarding] op=login-writeback result=no-claudeAiOauth service=${JSON.stringify(service)}`);
18727
18957
  return { wrote: false, reason: "no-claudeAiOauth", bytes: 0, service };
18728
18958
  }
18959
+ const oauth = parsed.claudeAiOauth;
18960
+ const refreshTokenLen = typeof oauth.refreshToken === "string" ? oauth.refreshToken.length : 0;
18729
18961
  try {
18730
- deps.writeFile(blob);
18962
+ deps.writeFile(blob, refreshTokenLen);
18731
18963
  } catch (err) {
18732
18964
  deps.log(`[onboarding] op=login-writeback result=write-failed service=${JSON.stringify(service)} err=${err instanceof Error ? err.message : String(err)}`);
18733
18965
  return { wrote: false, reason: "write-failed", bytes: 0, service };
@@ -18991,7 +19223,35 @@ function readUsersFile2() {
18991
19223
  if (!raw) return [];
18992
19224
  return JSON.parse(raw);
18993
19225
  }
18994
- var darwinAuthChild = null;
19226
+ var pendingAuthLogin = null;
19227
+ async function endLogin(pending) {
19228
+ if (!pending) return { priorId: null, priorPid: null, tabClosed: null };
19229
+ let tabClosed = null;
19230
+ const targetId = readAuthTargetId();
19231
+ if (targetId && readAuthAttemptId() === pending.attemptId) {
19232
+ tabClosed = await closeAuthTarget(targetId);
19233
+ }
19234
+ const priorPid = pending.child?.pid ?? null;
19235
+ try {
19236
+ pending.child?.kill("SIGTERM");
19237
+ } catch {
19238
+ }
19239
+ return { priorId: pending.attemptId, priorPid, tabClosed };
19240
+ }
19241
+ function adoptOrStopChild(attemptId, child) {
19242
+ if (pendingAuthLogin?.attemptId === attemptId) {
19243
+ pendingAuthLogin.child = child;
19244
+ return true;
19245
+ }
19246
+ try {
19247
+ child.kill("SIGTERM");
19248
+ } catch {
19249
+ }
19250
+ return false;
19251
+ }
19252
+ function formatEnded(ended) {
19253
+ return `superseded=${ended.priorId ?? "none"} priorPid=${ended.priorPid ?? "none"} tabClosed=${ended.tabClosed === null ? "na" : ended.tabClosed}`;
19254
+ }
18995
19255
  var darwinCredsBefore = {};
18996
19256
  async function checkAuthStatus() {
18997
19257
  let status;
@@ -19032,7 +19292,11 @@ app13.post("/claude-auth", async (c) => {
19032
19292
  const remoteAddress = c.env?.incoming?.socket?.remoteAddress;
19033
19293
  const transport = resolveBrowserTransport(c.req.raw, remoteAddress);
19034
19294
  if (body.action === "stop") {
19035
- return c.json({ stopped: true });
19295
+ const superseded2 = pendingAuthLogin;
19296
+ pendingAuthLogin = null;
19297
+ const ended2 = await endLogin(superseded2);
19298
+ console.log(`[onboarding] op=auth-login-stop attempt=${ended2.priorId ?? "none"} outcome=${ended2.priorId ? "stopped" : "no-pending"} priorPid=${ended2.priorPid ?? "none"} tabClosed=${ended2.tabClosed === null ? "na" : ended2.tabClosed}`);
19299
+ return c.json({ stopped: ended2.priorId !== null });
19036
19300
  }
19037
19301
  if (body.action === "logout") {
19038
19302
  const start = Date.now();
@@ -19057,7 +19321,7 @@ app13.post("/claude-auth", async (c) => {
19057
19321
  }
19058
19322
  if (body.action === "paste-code") {
19059
19323
  const code = typeof body.code === "string" ? body.code : "";
19060
- const child = darwinAuthChild;
19324
+ const child = pendingAuthLogin?.child ?? null;
19061
19325
  if (!child || !child.stdin || child.stdin.destroyed) {
19062
19326
  console.log("[onboarding] op=claude-paste-code accepted=false reason=no-active-login");
19063
19327
  return c.json({ accepted: false, error: "No active login to paste into. Start sign-in first." }, 400);
@@ -19079,8 +19343,13 @@ app13.post("/claude-auth", async (c) => {
19079
19343
  const cdpReady = await ensureCdp(transport);
19080
19344
  return c.json({ launched: cdpReady });
19081
19345
  }
19346
+ const attemptId = randomUUID4().slice(0, 8);
19347
+ const superseded = pendingAuthLogin;
19348
+ pendingAuthLogin = { child: null, attemptId, startedAt: Date.now() };
19349
+ const ended = await endLogin(superseded);
19082
19350
  ensureLogDir();
19083
- writeFileSync14(logPath("claude-auth"), "");
19351
+ appendFileSync3(logPath("claude-auth"), `==== attempt ${attemptId} ${(/* @__PURE__ */ new Date()).toISOString()} ====
19352
+ `);
19084
19353
  const claudeAuthLogFd = openSync8(logPath("claude-auth"), "a");
19085
19354
  const onClaudeOutput = (chunk) => writeSync(claudeAuthLogFd, chunk);
19086
19355
  if (process.platform === "darwin") {
@@ -19091,7 +19360,8 @@ app13.post("/claude-auth", async (c) => {
19091
19360
  stdio: ["pipe", "pipe", "pipe"]
19092
19361
  });
19093
19362
  claudeProc2.unref();
19094
- darwinAuthChild = claudeProc2;
19363
+ const adopted2 = adoptOrStopChild(attemptId, claudeProc2);
19364
+ console.log(`[onboarding] op=auth-login-start attempt=${attemptId} platform=darwin transport=native outcome=${adopted2 ? "started" : "self-superseded"} ${formatEnded(ended)}`);
19095
19365
  claudeProc2.stdin?.on("error", () => {
19096
19366
  });
19097
19367
  claudeProc2.stdout?.on("data", onClaudeOutput);
@@ -19103,11 +19373,9 @@ app13.post("/claude-auth", async (c) => {
19103
19373
  before: darwinCredsBefore,
19104
19374
  listMdats: listHostCredsMdats,
19105
19375
  readBlob: readHostCredsBlob,
19106
- writeFile: (blob) => {
19107
- const tmp = `${CLAUDE_CREDENTIALS_FILE}.${process.pid}.tmp`;
19108
- writeFileSync14(tmp, blob, { mode: 384 });
19109
- renameSync8(tmp, CLAUDE_CREDENTIALS_FILE);
19110
- },
19376
+ // Task 2572 — the write itself lives in claude-auth.ts beside the other
19377
+ // writer of this file, so both emit provenance and both are testable.
19378
+ writeFile: (blob, refreshTokenLen) => writeHostCredsFile(CLAUDE_CREDENTIALS_FILE, blob, refreshTokenLen),
19111
19379
  log: (line) => console.log(line)
19112
19380
  });
19113
19381
  persistKeychainService(
@@ -19115,7 +19383,7 @@ app13.post("/claude-auth", async (c) => {
19115
19383
  (service) => writeFileSync14(join37(dirname13(CLAUDE_CREDENTIALS_FILE), ".keychain-service"), service, { mode: 384 }),
19116
19384
  (line) => console.log(line)
19117
19385
  );
19118
- if (darwinAuthChild === claudeProc2) darwinAuthChild = null;
19386
+ if (pendingAuthLogin?.child === claudeProc2) pendingAuthLogin = null;
19119
19387
  });
19120
19388
  return c.json({ started: true, transport: "native" });
19121
19389
  }
@@ -19124,7 +19392,7 @@ app13.post("/claude-auth", async (c) => {
19124
19392
  if (!vncReady) return c.json({ error: "VNC display failed to start" }, 500);
19125
19393
  }
19126
19394
  await ensureCdp(transport);
19127
- const chromiumWrapper = writeChromiumWrapper();
19395
+ const chromiumWrapper = writeChromiumWrapper(attemptId);
19128
19396
  const x11Env = buildX11Env(chromiumWrapper, transport);
19129
19397
  vncLog("claude-auth", { action: "start", transport });
19130
19398
  const claudeProc = spawn("claude", ["auth", "login"], {
@@ -19132,9 +19400,14 @@ app13.post("/claude-auth", async (c) => {
19132
19400
  stdio: ["ignore", "pipe", "pipe"]
19133
19401
  });
19134
19402
  claudeProc.unref();
19403
+ const adopted = adoptOrStopChild(attemptId, claudeProc);
19404
+ console.log(`[onboarding] op=auth-login-start attempt=${attemptId} platform=${process.platform} transport=${transport} outcome=${adopted ? "started" : "self-superseded"} ${formatEnded(ended)}`);
19135
19405
  claudeProc.stdout?.on("data", onClaudeOutput);
19136
19406
  claudeProc.stderr?.on("data", onClaudeOutput);
19137
- claudeProc.once("close", () => closeSync8(claudeAuthLogFd));
19407
+ claudeProc.once("close", () => {
19408
+ closeSync8(claudeAuthLogFd);
19409
+ if (pendingAuthLogin?.child === claudeProc) pendingAuthLogin = null;
19410
+ });
19138
19411
  await waitForAuthPage(2e4);
19139
19412
  return c.json({ started: true, transport });
19140
19413
  });
@@ -19814,7 +20087,7 @@ init_dist();
19814
20087
  init_paths();
19815
20088
  init_remote_auth();
19816
20089
  init_loop_registry();
19817
- import { appendFileSync as appendFileSync3, existsSync as existsSync27, renameSync as renameSync9, statSync as statSync19 } from "fs";
20090
+ import { appendFileSync as appendFileSync4, existsSync as existsSync27, renameSync as renameSync9, statSync as statSync19 } from "fs";
19818
20091
  import { join as join38 } from "path";
19819
20092
  var CLIENT_ERRORS_LOG = join38(LOG_DIR, "client-errors.log");
19820
20093
  var MAX_LOG_SIZE = 10 * 1024 * 1024;
@@ -19960,7 +20233,7 @@ app15.post("/", async (c) => {
19960
20233
  const safe = typeof v === "string" ? truncate(v, 200) : typeof v === "number" || typeof v === "boolean" ? String(v) : JSON.stringify(v).slice(0, 200);
19961
20234
  return `${k}=${safe}`;
19962
20235
  }).join(" ");
19963
- const TAGGED_PREFIX_SOURCES = /* @__PURE__ */ new Set(["stream-log-click", "pwa", "graph-settle", "dashboard-load", "routines", "calendar-legibility"]);
20236
+ const TAGGED_PREFIX_SOURCES = /* @__PURE__ */ new Set(["stream-log-click", "pwa", "graph-settle", "dashboard-load", "routines", "calendar-legibility", "channels-card"]);
19964
20237
  if (TAGGED_PREFIX_SOURCES.has(source)) {
19965
20238
  console.log(
19966
20239
  `[${source}] ts=${ts} ip=${ip} version=${version || "unknown"}${extra ? " " + extra : ""}`
@@ -19993,7 +20266,7 @@ app15.post("/", async (c) => {
19993
20266
  tag: typeof body.tag === "string" ? truncate(body.tag, 32) : void 0,
19994
20267
  status: typeof body.status === "number" ? body.status : void 0
19995
20268
  };
19996
- appendFileSync3(CLIENT_ERRORS_LOG, JSON.stringify(payload) + "\n", "utf-8");
20269
+ appendFileSync4(CLIENT_ERRORS_LOG, JSON.stringify(payload) + "\n", "utf-8");
19997
20270
  } catch (err) {
19998
20271
  console.error(`[client-error] append failed: ${err instanceof Error ? err.message : String(err)}`);
19999
20272
  }
@@ -23086,18 +23359,18 @@ var ALLOWED_EVENTS = /* @__PURE__ */ new Set([
23086
23359
  ]);
23087
23360
  var app27 = new Hono();
23088
23361
  app27.post("/", async (c) => {
23089
- const TAG66 = "[admin:events]";
23362
+ const TAG68 = "[admin:events]";
23090
23363
  let body;
23091
23364
  try {
23092
23365
  body = await c.req.json();
23093
23366
  } catch (err) {
23094
23367
  const detail = err instanceof Error ? err.message : String(err);
23095
- console.error(`${TAG66} reject reason=body-not-json detail=${detail}`);
23368
+ console.error(`${TAG68} reject reason=body-not-json detail=${detail}`);
23096
23369
  return c.json({ ok: false, detail: "Request body was not valid JSON" }, 400);
23097
23370
  }
23098
23371
  const event = typeof body.event === "string" ? body.event : "";
23099
23372
  if (!ALLOWED_EVENTS.has(event)) {
23100
- console.error(`${TAG66} reject reason=event-not-allowed event=${JSON.stringify(event)}`);
23373
+ console.error(`${TAG68} reject reason=event-not-allowed event=${JSON.stringify(event)}`);
23101
23374
  return c.json({ ok: false, detail: `Event "${event}" is not allowed` }, 400);
23102
23375
  }
23103
23376
  const rawFields = body.fields && typeof body.fields === "object" ? body.fields : {};
@@ -34796,6 +35069,68 @@ function startSharedFolderCensus() {
34796
35069
  });
34797
35070
  }
34798
35071
 
35072
+ // app/lib/cred-refresh-census.ts
35073
+ init_loop_registry();
35074
+ import { readdirSync as readdirSync31, readFileSync as readFileSync53 } from "fs";
35075
+ import { homedir as homedir5 } from "os";
35076
+ import { join as join59 } from "path";
35077
+ var TAG60 = "[claude-auth]";
35078
+ function refreshTokenLength(raw) {
35079
+ let data;
35080
+ try {
35081
+ data = JSON.parse(raw);
35082
+ } catch {
35083
+ return null;
35084
+ }
35085
+ const oauth = data.claudeAiOauth;
35086
+ if (!oauth || typeof oauth !== "object") return null;
35087
+ return typeof oauth.refreshToken === "string" ? oauth.refreshToken.length : null;
35088
+ }
35089
+ function surveyBrandCredentials(home) {
35090
+ let entries2;
35091
+ try {
35092
+ entries2 = readdirSync31(home);
35093
+ } catch {
35094
+ return [];
35095
+ }
35096
+ const rows = [];
35097
+ for (const entry2 of entries2.sort()) {
35098
+ if (!entry2.startsWith(".")) continue;
35099
+ let raw;
35100
+ try {
35101
+ raw = readFileSync53(join59(home, entry2, ".claude", ".credentials.json"), "utf-8");
35102
+ } catch {
35103
+ continue;
35104
+ }
35105
+ rows.push({ brand: entry2.slice(1), refreshTokenLen: refreshTokenLength(raw) });
35106
+ }
35107
+ return rows;
35108
+ }
35109
+ function isBlank(row) {
35110
+ return row.refreshTokenLen === null || row.refreshTokenLen === 0;
35111
+ }
35112
+ function censusResult(rows) {
35113
+ if (rows.length === 0) return "no-brands-found";
35114
+ return rows.some(isBlank) ? "blank-tokens" : "ok";
35115
+ }
35116
+ function runCredRefreshCensus(home = homedir5()) {
35117
+ const rows = surveyBrandCredentials(home);
35118
+ const blank = rows.filter(isBlank).length;
35119
+ const detail = rows.map((r) => `${r.brand}:${r.refreshTokenLen === null ? "unreadable" : r.refreshTokenLen}`).join(",");
35120
+ console.log(
35121
+ `${TAG60} op=cred-refresh-census result=${censusResult(rows)} brands=${rows.length} blank=${blank} detail=${detail}`
35122
+ );
35123
+ return `${rows.length} brand(s), ${blank} blank`;
35124
+ }
35125
+ function startCredRefreshCensus() {
35126
+ registerLoop({
35127
+ name: "cred-refresh-census",
35128
+ intervalMs: 60 * 60 * 1e3,
35129
+ firstRunDelayMs: 0,
35130
+ run: () => runCredRefreshCensus()
35131
+ });
35132
+ }
35133
+
34799
35134
  // app/lib/file-watcher.ts
34800
35135
  init_data_path();
34801
35136
  import * as fsp2 from "fs/promises";
@@ -35033,7 +35368,7 @@ init_account();
35033
35368
  init_neo4j_store();
35034
35369
  import { readdir as readdir7, readFile as readFile12, rename as rename5, writeFile as writeFile5, unlink as unlink4 } from "fs/promises";
35035
35370
  import { existsSync as existsSync50 } from "fs";
35036
- import { join as join59 } from "path";
35371
+ import { join as join60 } from "path";
35037
35372
  var ADMIN_ROLE2 = "admin";
35038
35373
  var WEBCHAT_CHANNEL2 = "webchat";
35039
35374
  var SESSION_META_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.meta\.json$/i;
@@ -35078,7 +35413,7 @@ async function collectLiveSidecars(projectsRoot) {
35078
35413
  }
35079
35414
  for (const slug of slugs) {
35080
35415
  if (!slug.isDirectory()) continue;
35081
- const slugDir = join59(projectsRoot, slug.name);
35416
+ const slugDir = join60(projectsRoot, slug.name);
35082
35417
  let entries2;
35083
35418
  try {
35084
35419
  entries2 = await readdir7(slugDir, { withFileTypes: true });
@@ -35087,9 +35422,9 @@ async function collectLiveSidecars(projectsRoot) {
35087
35422
  }
35088
35423
  for (const entry2 of entries2) {
35089
35424
  if (entry2.isFile() && SESSION_META_RE.test(entry2.name)) {
35090
- out.push(join59(slugDir, entry2.name));
35425
+ out.push(join60(slugDir, entry2.name));
35091
35426
  } else if (entry2.isDirectory() && entry2.name === "subagents") {
35092
- const subDir = join59(slugDir, entry2.name);
35427
+ const subDir = join60(slugDir, entry2.name);
35093
35428
  let subs;
35094
35429
  try {
35095
35430
  subs = await readdir7(subDir, { withFileTypes: true });
@@ -35097,7 +35432,7 @@ async function collectLiveSidecars(projectsRoot) {
35097
35432
  continue;
35098
35433
  }
35099
35434
  for (const s of subs) {
35100
- if (s.isFile() && SESSION_META_RE.test(s.name)) out.push(join59(subDir, s.name));
35435
+ if (s.isFile() && SESSION_META_RE.test(s.name)) out.push(join60(subDir, s.name));
35101
35436
  }
35102
35437
  }
35103
35438
  }
@@ -35109,7 +35444,7 @@ function shortId(path) {
35109
35444
  return base.slice(0, 8);
35110
35445
  }
35111
35446
  async function migrateAdminWebchatSidecars(opts = {}) {
35112
- const projectsRoot = opts.projectsRoot ?? (process.env.CLAUDE_CONFIG_DIR ? join59(process.env.CLAUDE_CONFIG_DIR, "projects") : null) ?? "";
35447
+ const projectsRoot = opts.projectsRoot ?? (process.env.CLAUDE_CONFIG_DIR ? join60(process.env.CLAUDE_CONFIG_DIR, "projects") : null) ?? "";
35113
35448
  const usersFile = opts.usersFile ?? USERS_FILE;
35114
35449
  const accountId = opts.accountId ?? resolveAccount()?.accountId ?? null;
35115
35450
  const resolveName = opts.resolveName ?? defaultResolveName;
@@ -35187,7 +35522,7 @@ async function migrateAdminWebchatSidecars(opts = {}) {
35187
35522
  init_account();
35188
35523
  import { readdir as readdir8, readFile as readFile13, rename as rename6, writeFile as writeFile6, unlink as unlink5 } from "fs/promises";
35189
35524
  import { existsSync as existsSync51 } from "fs";
35190
- import { join as join60 } from "path";
35525
+ import { join as join61 } from "path";
35191
35526
  var SESSION_META_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.meta\.json$/i;
35192
35527
  function projectSlugForCwd(cwd) {
35193
35528
  return cwd.replace(/\//g, "-");
@@ -35233,7 +35568,7 @@ async function collectSidecars(projectsRoot) {
35233
35568
  }
35234
35569
  for (const slug of slugs) {
35235
35570
  if (!slug.isDirectory()) continue;
35236
- const slugDir = join60(projectsRoot, slug.name);
35571
+ const slugDir = join61(projectsRoot, slug.name);
35237
35572
  let entries2;
35238
35573
  try {
35239
35574
  entries2 = await readdir8(slugDir, { withFileTypes: true });
@@ -35242,9 +35577,9 @@ async function collectSidecars(projectsRoot) {
35242
35577
  }
35243
35578
  for (const entry2 of entries2) {
35244
35579
  if (entry2.isFile() && SESSION_META_RE2.test(entry2.name)) {
35245
- out.push({ path: join60(slugDir, entry2.name), slug: slug.name });
35580
+ out.push({ path: join61(slugDir, entry2.name), slug: slug.name });
35246
35581
  } else if (entry2.isDirectory() && (entry2.name === "subagents" || entry2.name === "archive")) {
35247
- const subDir = join60(slugDir, entry2.name);
35582
+ const subDir = join61(slugDir, entry2.name);
35248
35583
  let subs;
35249
35584
  try {
35250
35585
  subs = await readdir8(subDir, { withFileTypes: true });
@@ -35253,7 +35588,7 @@ async function collectSidecars(projectsRoot) {
35253
35588
  }
35254
35589
  for (const s of subs) {
35255
35590
  if (s.isFile() && SESSION_META_RE2.test(s.name)) {
35256
- out.push({ path: join60(subDir, s.name), slug: slug.name });
35591
+ out.push({ path: join61(subDir, s.name), slug: slug.name });
35257
35592
  }
35258
35593
  }
35259
35594
  }
@@ -35262,7 +35597,7 @@ async function collectSidecars(projectsRoot) {
35262
35597
  return out;
35263
35598
  }
35264
35599
  async function migrateSidecarAccountIds(opts = {}) {
35265
- const projectsRoot = opts.projectsRoot ?? (process.env.CLAUDE_CONFIG_DIR ? join60(process.env.CLAUDE_CONFIG_DIR, "projects") : null) ?? "";
35600
+ const projectsRoot = opts.projectsRoot ?? (process.env.CLAUDE_CONFIG_DIR ? join61(process.env.CLAUDE_CONFIG_DIR, "projects") : null) ?? "";
35266
35601
  const accounts = opts.accounts ?? listValidAccounts();
35267
35602
  const result = {
35268
35603
  scanned: 0,
@@ -35357,13 +35692,13 @@ var InboundHub = class {
35357
35692
  if (s.subscriber && s.ready) {
35358
35693
  if (payload.accountId !== s.accountId) this.crossAccountDelivered++;
35359
35694
  s.subscriber(payload);
35360
- s.inFlight.push({ payload, state: "drained", enqueuedAt: now, drainedAt: now });
35695
+ s.inFlight.push({ payload, state: "drained", enqueuedAt: now, drainedAt: now, spawnUndeliveredAt: null });
35361
35696
  return;
35362
35697
  }
35363
35698
  if (this.subscriberAccounts(payload.senderId).some((a) => a !== payload.accountId)) {
35364
35699
  this.crossAccountQueued++;
35365
35700
  }
35366
- s.inFlight.push({ payload, state: "queued", enqueuedAt: now, drainedAt: null });
35701
+ s.inFlight.push({ payload, state: "queued", enqueuedAt: now, drainedAt: null, spawnUndeliveredAt: null });
35367
35702
  }
35368
35703
  /** This account's channel server for the sender connected. Register the
35369
35704
  * subscriber but do NOT drain yet — the drain waits for markReady so nothing
@@ -35422,13 +35757,16 @@ var InboundHub = class {
35422
35757
  * is attached -> inbound-undelivered (the readiness handoff broke);
35423
35758
  * - a still-queued message older than queuedThresholdMs with NO subscriber
35424
35759
  * attached -> spawn-undelivered (the channel server never attached: spawn
35425
- * failed, or the session ended and never resumed);
35760
+ * failed, or the session ended and never resumed). Task 2575 — reported
35761
+ * ONCE and RETAINED, because the gateway retries the spawn for it;
35762
+ * - a still-queued message with NO subscriber older than giveUpMs ->
35763
+ * spawn-given-up (Task 2575: the retry window closed), removed;
35426
35764
  * - a drained message unacked past ackThresholdMs -> drain-undelivered (the
35427
35765
  * channel server never confirmed receipt — the silent cold-start drop).
35428
35766
  * Task 2290 — "a subscriber" means one attached for the SAME account, so a
35429
35767
  * message waiting on its own account's cold start is not masked by another
35430
35768
  * account's live subscriber on the same phone. Each message is emitted at
35431
- * most once (removed when emitted). */
35769
+ * most once per kind; every kind but spawn-undelivered removes it. */
35432
35770
  sweep(now, t) {
35433
35771
  const events = [];
35434
35772
  for (const s of this.senders.values()) {
@@ -35438,9 +35776,17 @@ var InboundHub = class {
35438
35776
  events.push({ kind: "inbound-undelivered", accountId, senderId, waMessageId: e.payload.waMessageId, ageMs: now - e.enqueuedAt });
35439
35777
  return false;
35440
35778
  }
35441
- if (e.state === "queued" && !s.subscriber && now - e.enqueuedAt > t.queuedThresholdMs) {
35442
- events.push({ kind: "spawn-undelivered", accountId, senderId, waMessageId: e.payload.waMessageId, ageMs: now - e.enqueuedAt });
35443
- return false;
35779
+ if (e.state === "queued" && !s.subscriber) {
35780
+ const age = now - e.enqueuedAt;
35781
+ if (age > t.giveUpMs) {
35782
+ events.push({ kind: "spawn-given-up", accountId, senderId, waMessageId: e.payload.waMessageId, ageMs: age });
35783
+ return false;
35784
+ }
35785
+ if (age > t.queuedThresholdMs && e.spawnUndeliveredAt == null) {
35786
+ e.spawnUndeliveredAt = now;
35787
+ events.push({ kind: "spawn-undelivered", accountId, senderId, waMessageId: e.payload.waMessageId, ageMs: age });
35788
+ }
35789
+ return true;
35444
35790
  }
35445
35791
  if (e.state === "drained" && e.drainedAt != null && now - e.drainedAt > t.ackThresholdMs) {
35446
35792
  events.push({ kind: "drain-undelivered", accountId, senderId, waMessageId: e.payload.waMessageId, ageMs: now - e.drainedAt });
@@ -35451,6 +35797,23 @@ var InboundHub = class {
35451
35797
  }
35452
35798
  return events;
35453
35799
  }
35800
+ /** Task 2575 — every key holding queued work with NO subscriber whose oldest
35801
+ * message is older than `thresholdMs`. This is the population a refused spawn
35802
+ * leaves behind: the gateway's sweep re-invokes the retained spawn arguments
35803
+ * for each one. Read-only — it emits nothing and mutates nothing, so calling
35804
+ * it cannot consume a terminal event the sweep is about to report. */
35805
+ heldSpawns(now, thresholdMs) {
35806
+ const out = [];
35807
+ for (const s of this.senders.values()) {
35808
+ if (s.subscriber) continue;
35809
+ const queued = s.inFlight.filter((e) => e.state === "queued");
35810
+ if (queued.length === 0) continue;
35811
+ const oldestAgeMs = Math.max(...queued.map((e) => now - e.enqueuedAt));
35812
+ if (oldestAgeMs <= thresholdMs) continue;
35813
+ out.push({ accountId: s.accountId, senderId: s.senderId, oldestAgeMs, count: queued.length });
35814
+ }
35815
+ return out;
35816
+ }
35454
35817
  /** Task 2290 — every account holding a live subscriber for this phone, sorted.
35455
35818
  * Read-only. Feeds the cross-account field on the inbound log line and the
35456
35819
  * queued census, so a phone shared across accounts is greppable at the moment
@@ -35953,6 +36316,7 @@ init_loop_registry();
35953
36316
  var QUEUED_THRESHOLD_MS = 3e4;
35954
36317
  var ACK_THRESHOLD_MS = 3e4;
35955
36318
  var SWEEP_INTERVAL_MS = 15e3;
36319
+ var GIVE_UP_MS = 3e5;
35956
36320
  var MEDIA_STANZA_STALE_MS = 12e4;
35957
36321
  var WaGateway = class {
35958
36322
  constructor(deps) {
@@ -35974,6 +36338,14 @@ var WaGateway = class {
35974
36338
  // effective session account — the file-path validation scope.
35975
36339
  docContexts = /* @__PURE__ */ new Map();
35976
36340
  spawning = /* @__PURE__ */ new Set();
36341
+ /** Task 2575 — the arguments of the last `ensureChannelSession` call per hub
36342
+ * key, so the sweep can re-invoke a spawn that was refused. The refusal is not
36343
+ * visible here — `ensureChannelSession` returns void and server/index.ts logs
36344
+ * and returns on the `{ error }` envelope — so the retry keys off queue state
36345
+ * (a queued message with no subscriber), which is the observable consequence
36346
+ * of the refusal. `attempts` is the per-key retry counter that rides the log
36347
+ * line, reset whenever a fresh inbound takes the spawn path. */
36348
+ heldSpawnArgs = /* @__PURE__ */ new Map();
35977
36349
  seq = 0;
35978
36350
  // Task 2110 — senderId -> the arrival ms of every still-unemitted non-audio
35979
36351
  // media stanza for that sender, oldest first. This is the race window: a reply
@@ -36110,7 +36482,8 @@ var WaGateway = class {
36110
36482
  reconcile(now) {
36111
36483
  const events = this.hub.sweep(now, {
36112
36484
  queuedThresholdMs: QUEUED_THRESHOLD_MS,
36113
- ackThresholdMs: ACK_THRESHOLD_MS
36485
+ ackThresholdMs: ACK_THRESHOLD_MS,
36486
+ giveUpMs: GIVE_UP_MS
36114
36487
  });
36115
36488
  for (const e of events) {
36116
36489
  console.error(
@@ -36132,6 +36505,53 @@ var WaGateway = class {
36132
36505
  );
36133
36506
  return events;
36134
36507
  }
36508
+ /** Task 2575 — re-invoke the retained spawn for every key holding queued work
36509
+ * with no channel server attached. Driven by startSweeper() beside reconcile();
36510
+ * called directly with an injected clock in tests. Returns the number of
36511
+ * ensureChannelSession calls made.
36512
+ *
36513
+ * This is the missing action behind `op=spawn-undelivered`. Before it, that
36514
+ * line was emitted and nothing consumed it: on 2026-08-08 it fired for wa-1 at
36515
+ * ageMs=37743 and the message was never delivered.
36516
+ *
36517
+ * A retry while auth is still dead re-enters the same refusal and returns; the
36518
+ * cost is one refusal line plus one op=spawn-retry line per key per sweep,
36519
+ * bounded by the hub's 5-minute give-up. */
36520
+ async retryHeldSpawns(now) {
36521
+ const held = this.hub.heldSpawns(now, QUEUED_THRESHOLD_MS);
36522
+ let calls = 0;
36523
+ for (const h of held) {
36524
+ const key2 = hubKey(h.accountId, h.senderId);
36525
+ if (this.spawning.has(key2)) continue;
36526
+ const entry2 = this.heldSpawnArgs.get(key2);
36527
+ if (!entry2) {
36528
+ console.error(
36529
+ `[whatsapp-native] op=spawn-retry-skipped senderId=${h.senderId} accountId=${h.accountId} ageMs=${h.oldestAgeMs} queued=${h.count} reason=no-args`
36530
+ );
36531
+ continue;
36532
+ }
36533
+ entry2.attempts++;
36534
+ this.spawning.add(key2);
36535
+ calls++;
36536
+ try {
36537
+ await this.deps.ensureChannelSession(entry2.args);
36538
+ console.error(
36539
+ `[whatsapp-native] op=spawn-retry senderId=${h.senderId} accountId=${h.accountId} attempt=${entry2.attempts} ageMs=${h.oldestAgeMs} queued=${h.count} ok=true`
36540
+ );
36541
+ } catch (err) {
36542
+ console.error(
36543
+ `[whatsapp-native] op=spawn-retry senderId=${h.senderId} accountId=${h.accountId} attempt=${entry2.attempts} ageMs=${h.oldestAgeMs} queued=${h.count} ok=false reason=${err instanceof Error ? err.message : String(err)}`
36544
+ );
36545
+ } finally {
36546
+ this.spawning.delete(key2);
36547
+ }
36548
+ }
36549
+ const heldKeys = new Set(held.map((h) => hubKey(h.accountId, h.senderId)));
36550
+ for (const k of [...this.heldSpawnArgs.keys()]) {
36551
+ if (!heldKeys.has(k) && !this.spawning.has(k)) this.heldSpawnArgs.delete(k);
36552
+ }
36553
+ return calls;
36554
+ }
36135
36555
  /** Arm the periodic reconciliation sweep in the long-lived UI process.
36136
36556
  *
36137
36557
  * Task 1814 — armed through the registry, so it appears on /activity. This
@@ -36149,9 +36569,14 @@ var WaGateway = class {
36149
36569
  return registerLoop({
36150
36570
  name: "whatsapp-native-sweep",
36151
36571
  intervalMs,
36152
- run: () => {
36153
- const events = this.reconcile(Date.now());
36154
- return events.length > 0 ? `${events.length} terminal events` : void 0;
36572
+ run: async () => {
36573
+ const now = Date.now();
36574
+ const events = this.reconcile(now);
36575
+ const retried = await this.retryHeldSpawns(now);
36576
+ const parts = [];
36577
+ if (events.length > 0) parts.push(`${events.length} terminal events`);
36578
+ if (retried > 0) parts.push(`${retried} spawn retries`);
36579
+ return parts.length > 0 ? parts.join(", ") : void 0;
36155
36580
  }
36156
36581
  });
36157
36582
  }
@@ -36259,33 +36684,35 @@ var WaGateway = class {
36259
36684
  console.error(
36260
36685
  `[whatsapp-native] op=spawn-trigger senderId=${senderId} accountId=${input.effectiveAccountId}`
36261
36686
  );
36687
+ const spawnArgs = {
36688
+ accountId: input.accountId,
36689
+ effectiveAccountId: input.effectiveAccountId,
36690
+ senderId,
36691
+ role: input.role ?? "admin",
36692
+ personId: input.personId ?? null,
36693
+ gatewayUrl: this.deps.gatewayUrl,
36694
+ serverPath: this.deps.serverPath,
36695
+ // Task 1575 — the gate's owner-self verdict; drives owner attribution
36696
+ // for an unbound owner self-phone at the spawn site.
36697
+ ownerSelfPhone: input.ownerSelfPhone,
36698
+ // Task 2262 — resolved once in onMessage; its presence routes this
36699
+ // spawn to /public-spawn under the named public agent.
36700
+ publicAgentSlug: input.publicAgentSlug,
36701
+ // Task 2281 — the correlation id, so the spawn outcome lines join the
36702
+ // rest of this inbound's lifeline. Only this call site takes it: the
36703
+ // passive branch above never reaches the public spawn, which is where
36704
+ // op=spawn-ok and op=spawn-failed are emitted.
36705
+ inboundId: input.inboundId,
36706
+ // Task 2234 — the registered-party label, forwarded verbatim so the
36707
+ // spawn env carries WA_SENDER_PARTY. Undefined for every other sender.
36708
+ // Only this call site takes it: the passive branch above hardcodes
36709
+ // role:'admin', and a registered party is public, so the combination
36710
+ // cannot arise there.
36711
+ party: input.party
36712
+ };
36713
+ this.heldSpawnArgs.set(spawnKey, { args: spawnArgs, attempts: 0 });
36262
36714
  try {
36263
- await this.deps.ensureChannelSession({
36264
- accountId: input.accountId,
36265
- effectiveAccountId: input.effectiveAccountId,
36266
- senderId,
36267
- role: input.role ?? "admin",
36268
- personId: input.personId ?? null,
36269
- gatewayUrl: this.deps.gatewayUrl,
36270
- serverPath: this.deps.serverPath,
36271
- // Task 1575 — the gate's owner-self verdict; drives owner attribution
36272
- // for an unbound owner self-phone at the spawn site.
36273
- ownerSelfPhone: input.ownerSelfPhone,
36274
- // Task 2262 — resolved once in onMessage; its presence routes this
36275
- // spawn to /public-spawn under the named public agent.
36276
- publicAgentSlug: input.publicAgentSlug,
36277
- // Task 2281 — the correlation id, so the spawn outcome lines join the
36278
- // rest of this inbound's lifeline. Only this call site takes it: the
36279
- // passive branch above never reaches the public spawn, which is where
36280
- // op=spawn-ok and op=spawn-failed are emitted.
36281
- inboundId: input.inboundId,
36282
- // Task 2234 — the registered-party label, forwarded verbatim so the
36283
- // spawn env carries WA_SENDER_PARTY. Undefined for every other sender.
36284
- // Only this call site takes it: the passive branch above hardcodes
36285
- // role:'admin', and a registered party is public, so the combination
36286
- // cannot arise there.
36287
- party: input.party
36288
- });
36715
+ await this.deps.ensureChannelSession(spawnArgs);
36289
36716
  } finally {
36290
36717
  this.spawning.delete(spawnKey);
36291
36718
  }
@@ -36813,8 +37240,41 @@ function createWebchatChannelRoutes(deps) {
36813
37240
  }
36814
37241
 
36815
37242
  // app/lib/webchat/gateway/resolve-turn-answer.ts
36816
- import { existsSync as existsSync52, openSync as openSync9, readSync as readSync8, closeSync as closeSync9, readdirSync as readdirSync31 } from "fs";
36817
- import { join as join61 } from "path";
37243
+ import { existsSync as existsSync52, openSync as openSync9, readSync as readSync8, closeSync as closeSync9, readdirSync as readdirSync32 } from "fs";
37244
+ import { join as join62 } from "path";
37245
+
37246
+ // app/lib/channel-delivery/outbound-guard.ts
37247
+ var INTERNAL_LEAK_PATTERNS = [
37248
+ { name: "task-id", re: /\bTasks?[\s-]+\d{2,4}\b/g },
37249
+ { name: "media-path", re: /\/tmp\/maxy-media[A-Za-z0-9_./-]*/g },
37250
+ { name: "repo-path", re: /\bplatform\/[A-Za-z0-9_./-]+/g },
37251
+ { name: "bundle", re: /\bserver\.(?:js|cjs|mjs)\b/g },
37252
+ { name: "source-file", re: /\b[A-Za-z0-9_-]+\.tsx?\b/g },
37253
+ { name: "log-tag", re: /\[[a-z]+(?:-[a-z]+)+\]/g },
37254
+ { name: "op-tag", re: /\bop=[a-z][a-z-]*\b/g }
37255
+ ];
37256
+ function redactInternalLeaks(text) {
37257
+ const redactions = [];
37258
+ let clean = text;
37259
+ for (const { name, re } of INTERNAL_LEAK_PATTERNS) {
37260
+ clean = clean.replace(re, (m) => {
37261
+ redactions.push({ pattern: name, match: m });
37262
+ return "[redacted]";
37263
+ });
37264
+ }
37265
+ return { clean, redactions };
37266
+ }
37267
+ function guardOutbound(senderId, text) {
37268
+ const { clean, redactions } = redactInternalLeaks(text);
37269
+ for (const r of redactions) {
37270
+ console.error(
37271
+ `[channel-guard] op=redact senderId=${senderId} pattern=${r.pattern} bytes=${Buffer.byteLength(r.match, "utf8")}`
37272
+ );
37273
+ }
37274
+ return clean;
37275
+ }
37276
+
37277
+ // app/lib/webchat/gateway/resolve-turn-answer.ts
36818
37278
  function resolveTurnAnswer(p) {
36819
37279
  if (!SESSION_ID_RE2.test(p.sessionId)) return "";
36820
37280
  const path = findJsonl(p.projectsRoot, p.sessionId);
@@ -36839,18 +37299,18 @@ function resolveTurnAnswer(p) {
36839
37299
  for (const turn of turns) {
36840
37300
  if (turn.kind === "agent-webchat-final" || turn.kind === "agent-reply") parts.push(turn.text);
36841
37301
  }
36842
- return segmentText(parts.join("\n\n")).join("\n\n");
37302
+ return segmentText(guardOutbound(p.senderId, parts.join("\n\n"))).join("\n\n");
36843
37303
  }
36844
37304
  function findJsonl(projectsRoot, sessionId) {
36845
37305
  if (!existsSync52(projectsRoot)) return null;
36846
37306
  let slugs;
36847
37307
  try {
36848
- slugs = readdirSync31(projectsRoot, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
37308
+ slugs = readdirSync32(projectsRoot, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
36849
37309
  } catch {
36850
37310
  return null;
36851
37311
  }
36852
37312
  for (const slug of slugs) {
36853
- const live = join61(projectsRoot, slug, `${sessionId}.jsonl`);
37313
+ const live = join62(projectsRoot, slug, `${sessionId}.jsonl`);
36854
37314
  if (existsSync52(live)) return live;
36855
37315
  }
36856
37316
  return null;
@@ -36998,7 +37458,7 @@ var WebchatGateway = class _WebchatGateway {
36998
37458
  if (rec) rec.lastActivityAt = Date.now();
36999
37459
  const awaiter = this.replyAwaiters.get(key2);
37000
37460
  const projectsRoot = this.deps.publicProjectsRoot?.() ?? "";
37001
- const text = projectsRoot ? resolveTurnAnswer({ ...turn, projectsRoot }) : "";
37461
+ const text = projectsRoot ? resolveTurnAnswer({ ...turn, senderId: keyDisplay(key2), projectsRoot }) : "";
37002
37462
  const bytes = Buffer.byteLength(text, "utf8");
37003
37463
  const deliveredAt = this.lastDeliveredAt.get(key2);
37004
37464
  const latency = deliveredAt != null ? Date.now() - deliveredAt : -1;
@@ -37493,37 +37953,6 @@ var AnswerAccumulator = class {
37493
37953
  }
37494
37954
  };
37495
37955
 
37496
- // app/lib/channel-delivery/outbound-guard.ts
37497
- var INTERNAL_LEAK_PATTERNS = [
37498
- { name: "task-id", re: /\bTasks?[\s-]+\d{2,4}\b/g },
37499
- { name: "media-path", re: /\/tmp\/maxy-media[A-Za-z0-9_./-]*/g },
37500
- { name: "repo-path", re: /\bplatform\/[A-Za-z0-9_./-]+/g },
37501
- { name: "bundle", re: /\bserver\.(?:js|cjs|mjs)\b/g },
37502
- { name: "source-file", re: /\b[A-Za-z0-9_-]+\.tsx?\b/g },
37503
- { name: "log-tag", re: /\[[a-z]+(?:-[a-z]+)+\]/g },
37504
- { name: "op-tag", re: /\bop=[a-z][a-z-]*\b/g }
37505
- ];
37506
- function redactInternalLeaks(text) {
37507
- const redactions = [];
37508
- let clean = text;
37509
- for (const { name, re } of INTERNAL_LEAK_PATTERNS) {
37510
- clean = clean.replace(re, (m) => {
37511
- redactions.push({ pattern: name, match: m });
37512
- return "[redacted]";
37513
- });
37514
- }
37515
- return { clean, redactions };
37516
- }
37517
- function guardOutbound(senderId, text) {
37518
- const { clean, redactions } = redactInternalLeaks(text);
37519
- for (const r of redactions) {
37520
- console.error(
37521
- `[channel-guard] op=redact senderId=${senderId} pattern=${r.pattern} bytes=${Buffer.byteLength(r.match, "utf8")}`
37522
- );
37523
- }
37524
- return clean;
37525
- }
37526
-
37527
37956
  // app/lib/channel-delivery/emitter.ts
37528
37957
  function followerPendingMaxMs() {
37529
37958
  return Number(process.env.CHANNEL_PTY_FOLLOWER_PENDING_MAX_MS ?? String(3e5));
@@ -37865,14 +38294,14 @@ function makeFileDelivery(opts) {
37865
38294
  }
37866
38295
 
37867
38296
  // app/lib/webchat/file-delivery.ts
37868
- var TAG60 = "[webchat-adaptor]";
38297
+ var TAG61 = "[webchat-adaptor]";
37869
38298
  function platformRoot2() {
37870
38299
  return process.env.MAXY_PLATFORM_ROOT || "";
37871
38300
  }
37872
38301
  function makeWebchatSendFile(entry2) {
37873
38302
  return async (filePath) => {
37874
38303
  if (!entry2.accountId) {
37875
- console.error(`${TAG60} file-delivery reject reason=no-account sender=${entry2.senderId}`);
38304
+ console.error(`${TAG61} file-delivery reject reason=no-account sender=${entry2.senderId}`);
37876
38305
  return { ok: false, error: "no-account" };
37877
38306
  }
37878
38307
  const accountDir = resolve47(platformRoot2(), "..", "data/accounts", entry2.accountId);
@@ -37880,14 +38309,14 @@ function makeWebchatSendFile(entry2) {
37880
38309
  const resolved = realpathSync9(filePath);
37881
38310
  const accountResolved = realpathSync9(accountDir);
37882
38311
  if (!resolved.startsWith(accountResolved + "/")) {
37883
- console.error(`${TAG60} file-delivery reject reason=outside_account_directory sender=${entry2.senderId}`);
38312
+ console.error(`${TAG61} file-delivery reject reason=outside_account_directory sender=${entry2.senderId}`);
37884
38313
  return { ok: false, error: "outside-account" };
37885
38314
  }
37886
38315
  return { ok: true };
37887
38316
  } catch (err) {
37888
38317
  const code = err.code;
37889
38318
  console.error(
37890
- `${TAG60} file-delivery reject reason=${code === "ENOENT" ? "not-found" : "path-error"} sender=${entry2.senderId}`
38319
+ `${TAG61} file-delivery reject reason=${code === "ENOENT" ? "not-found" : "path-error"} sender=${entry2.senderId}`
37891
38320
  );
37892
38321
  return { ok: false, error: code === "ENOENT" ? "not-found" : "path-error" };
37893
38322
  }
@@ -37896,7 +38325,7 @@ function makeWebchatSendFile(entry2) {
37896
38325
  function makeWebchatFileDelivery(entry2) {
37897
38326
  return makeFileDelivery({
37898
38327
  entry: entry2,
37899
- tag: TAG60,
38328
+ tag: TAG61,
37900
38329
  channel: "webchat",
37901
38330
  sendFile: makeWebchatSendFile(entry2),
37902
38331
  deferUntilVerdict: true
@@ -37976,7 +38405,7 @@ function buildPublicWebchatSpawnRequest(input) {
37976
38405
  }
37977
38406
 
37978
38407
  // app/lib/whatsapp/inbound/file-delivery-bridge.ts
37979
- var TAG61 = "[whatsapp-adaptor]";
38408
+ var TAG62 = "[whatsapp-adaptor]";
37980
38409
  var SEND_USER_FILE2 = "SendUserFile";
37981
38410
  var WHATSAPP_SEND_DOCUMENT = "whatsapp-send-document";
37982
38411
  function platformRoot3() {
@@ -37994,7 +38423,7 @@ function makeWhatsAppSendFile(entry2, maxyAccountId) {
37994
38423
  });
37995
38424
  if (result.ok) return { ok: true };
37996
38425
  console.error(
37997
- `${TAG61} file-delivery reject reason=send-failed sender=${entry2.senderId} status=${result.status} message=${result.error}`
38426
+ `${TAG62} file-delivery reject reason=send-failed sender=${entry2.senderId} status=${result.status} message=${result.error}`
37998
38427
  );
37999
38428
  return { ok: false, error: result.error };
38000
38429
  };
@@ -38002,7 +38431,7 @@ function makeWhatsAppSendFile(entry2, maxyAccountId) {
38002
38431
  function makeWhatsAppFileDelivery(entry2, maxyAccountId) {
38003
38432
  const shared = makeFileDelivery({
38004
38433
  entry: entry2,
38005
- tag: TAG61,
38434
+ tag: TAG62,
38006
38435
  channel: "whatsapp",
38007
38436
  sendFile: makeWhatsAppSendFile(entry2, maxyAccountId)
38008
38437
  });
@@ -38037,7 +38466,7 @@ function makeWhatsAppFileDelivery(entry2, maxyAccountId) {
38037
38466
  if (!delivered) {
38038
38467
  const fileField = call.filePath !== void 0 ? ` file=${call.filePath}` : "";
38039
38468
  console.error(
38040
- `${TAG61} file-delivery-unreconciled sender=${entry2.senderId} sessionId=${sid} tool=${WHATSAPP_SEND_DOCUMENT}${fileField}`
38469
+ `${TAG62} file-delivery-unreconciled sender=${entry2.senderId} sessionId=${sid} tool=${WHATSAPP_SEND_DOCUMENT}${fileField}`
38041
38470
  );
38042
38471
  }
38043
38472
  }
@@ -38129,21 +38558,22 @@ function startNativeFileFollower(input) {
38129
38558
  }
38130
38559
 
38131
38560
  // app/lib/channel-delivery/follower-registry.ts
38132
- function createFollowerRegistry(start) {
38561
+ function createFollowerRegistry(start, keyOf2 = (p) => p.senderId) {
38133
38562
  const controllers = /* @__PURE__ */ new Map();
38134
38563
  const params = /* @__PURE__ */ new Map();
38135
38564
  return {
38136
38565
  attach(p) {
38137
- controllers.get(p.senderId)?.abort();
38138
- params.set(p.senderId, p);
38566
+ const doorId = keyOf2(p);
38567
+ controllers.get(doorId)?.abort();
38568
+ params.set(doorId, p);
38139
38569
  let ac;
38140
38570
  ac = start(p, () => {
38141
- if (controllers.get(p.senderId) === ac) controllers.delete(p.senderId);
38571
+ if (controllers.get(doorId) === ac) controllers.delete(doorId);
38142
38572
  });
38143
- controllers.set(p.senderId, ac);
38573
+ controllers.set(doorId, ac);
38144
38574
  },
38145
38575
  live: () => [...controllers.keys()],
38146
- paramsFor: (senderId) => params.get(senderId) ?? null
38576
+ paramsFor: (doorId) => params.get(doorId) ?? null
38147
38577
  };
38148
38578
  }
38149
38579
 
@@ -38156,41 +38586,79 @@ var id82 = (s) => s.slice(0, 8);
38156
38586
  function followerCensusTick(deps, state = makeCensusState()) {
38157
38587
  const bound = deps.bound();
38158
38588
  const live = new Set(deps.live());
38159
- for (const senderId of live) state.attempts.delete(senderId);
38160
- const missing = bound.filter((senderId) => !live.has(senderId));
38589
+ const label = deps.label ?? id82;
38590
+ for (const doorId of live) state.attempts.delete(doorId);
38591
+ const missing = bound.filter((doorId) => !live.has(doorId));
38161
38592
  console.error(
38162
- `[channel-follower] op=follower-census channel=${deps.channel} bound=${bound.length} attached=${live.size} missing=${missing.length} missingSenders=${missing.map(id82).join(",") || "none"}`
38593
+ `[channel-follower] op=follower-census channel=${deps.channel} bound=${bound.length} attached=${live.size} missing=${missing.length} missingSenders=${missing.map(label).join(",") || "none"}`
38163
38594
  );
38164
- for (const senderId of missing) {
38165
- const used = state.attempts.get(senderId) ?? 0;
38595
+ for (const doorId of missing) {
38596
+ const used = state.attempts.get(doorId) ?? 0;
38166
38597
  if (used >= CENSUS_MAX_REATTEMPTS) {
38167
38598
  console.error(
38168
- `[channel-follower] op=follower-reattach channel=${deps.channel} sessionId=${id82(deps.paramsFor(senderId)?.sessionId ?? "none")} senderId=${id82(senderId)} outcome=skipped reason=reattach-exhausted attempts=${used}`
38599
+ `[channel-follower] op=follower-reattach channel=${deps.channel} sessionId=${id82(deps.paramsFor(doorId)?.sessionId ?? "none")} senderId=${label(doorId)} outcome=skipped reason=reattach-exhausted attempts=${used}`
38169
38600
  );
38170
38601
  continue;
38171
38602
  }
38172
- const p = deps.paramsFor(senderId);
38603
+ const p = deps.paramsFor(doorId);
38173
38604
  if (!p) {
38174
38605
  console.error(
38175
- `[channel-follower] op=follower-reattach channel=${deps.channel} sessionId=none senderId=${id82(senderId)} outcome=failed reason=no-attach-params`
38606
+ `[channel-follower] op=follower-reattach channel=${deps.channel} sessionId=none senderId=${label(doorId)} outcome=failed reason=no-attach-params`
38176
38607
  );
38177
38608
  continue;
38178
38609
  }
38179
38610
  try {
38180
- state.attempts.set(senderId, used + 1);
38611
+ state.attempts.set(doorId, used + 1);
38181
38612
  deps.attach(p);
38182
38613
  console.error(
38183
- `[channel-follower] op=follower-reattach channel=${deps.channel} sessionId=${id82(p.sessionId)} senderId=${id82(p.senderId)} outcome=attached reason=none`
38614
+ `[channel-follower] op=follower-reattach channel=${deps.channel} sessionId=${id82(p.sessionId)} senderId=${label(doorId)} outcome=attached reason=none`
38184
38615
  );
38185
38616
  } catch (err) {
38186
38617
  console.error(
38187
- `[channel-follower] op=follower-reattach channel=${deps.channel} sessionId=${id82(p.sessionId)} senderId=${id82(p.senderId)} outcome=failed reason=${err instanceof Error ? err.message : String(err)}`
38618
+ `[channel-follower] op=follower-reattach channel=${deps.channel} sessionId=${id82(p.sessionId)} senderId=${label(doorId)} outcome=failed reason=${err instanceof Error ? err.message : String(err)}`
38188
38619
  );
38189
38620
  }
38190
38621
  }
38191
38622
  return `${deps.channel}: ${bound.length} door(s), ${missing.length} without a reader`;
38192
38623
  }
38193
38624
 
38625
+ // app/lib/channel-delivery/whatsapp-door.ts
38626
+ function whatsappDoorId(p) {
38627
+ return hubKey(p.effectiveAccountId, p.senderId);
38628
+ }
38629
+ function whatsappDoorLabel(doorId) {
38630
+ const [decoded] = decodeHubKeys([doorId]);
38631
+ return decoded ? `${id82(decoded.accountId)}:${id82(decoded.senderId)}` : id82(doorId);
38632
+ }
38633
+
38634
+ // app/lib/channel-delivery/refusal-notice.ts
38635
+ var TAG63 = "[channel-delivery]";
38636
+ var notified = /* @__PURE__ */ new Set();
38637
+ function refusalNoticeText(reason) {
38638
+ return reason === "claude-auth-dead" ? "Your message arrived but no assistant could be started: the Claude sign-in on this install is not active. An operator has to sign in again; send this again once they have." : `Your message arrived but no assistant could be started (${reason}). Nothing was lost; try again shortly.`;
38639
+ }
38640
+ async function sendRefusalNotice(input) {
38641
+ if (notified.has(input.doorId)) return;
38642
+ const fields = `channel=${input.channel} sender=${input.sender} reason=${input.reason}`;
38643
+ if (!input.send) {
38644
+ console.error(`${TAG63} op=refusal-notice-failed ${fields} error=no-target`);
38645
+ return;
38646
+ }
38647
+ try {
38648
+ await input.send(guardOutbound(input.sender, refusalNoticeText(input.reason)));
38649
+ } catch (e) {
38650
+ console.error(
38651
+ `${TAG63} op=refusal-notice-failed ${fields} error=${e instanceof Error ? e.message : String(e)}`
38652
+ );
38653
+ return;
38654
+ }
38655
+ notified.add(input.doorId);
38656
+ console.error(`${TAG63} op=refusal-notice ${fields} status=${input.status}`);
38657
+ }
38658
+ function clearRefusalNotice(doorId) {
38659
+ notified.delete(doorId);
38660
+ }
38661
+
38194
38662
  // app/lib/telegram/gateway/inbound-hub.ts
38195
38663
  var InboundHub3 = class {
38196
38664
  keys = /* @__PURE__ */ new Map();
@@ -38211,9 +38679,9 @@ var InboundHub3 = class {
38211
38679
  const s = this.state(payload.key);
38212
38680
  if (s.subscriber && s.ready) {
38213
38681
  s.subscriber(payload);
38214
- s.inFlight.push({ payload, state: "drained", enqueuedAt: now, drainedAt: now });
38682
+ s.inFlight.push({ payload, state: "drained", enqueuedAt: now, drainedAt: now, spawnUndeliveredAt: null });
38215
38683
  } else {
38216
- s.inFlight.push({ payload, state: "queued", enqueuedAt: now, drainedAt: null });
38684
+ s.inFlight.push({ payload, state: "queued", enqueuedAt: now, drainedAt: null, spawnUndeliveredAt: null });
38217
38685
  }
38218
38686
  }
38219
38687
  /** A key's channel server connected. Register the subscriber but do NOT
@@ -38273,9 +38741,18 @@ var InboundHub3 = class {
38273
38741
  * that never reached the agent:
38274
38742
  * - a still-queued message older than queuedThresholdMs WHILE a subscriber
38275
38743
  * is attached -> inbound-undelivered (the readiness handoff broke);
38744
+ * - a still-queued message older than queuedThresholdMs with NO subscriber
38745
+ * attached -> spawn-undelivered (the channel server never attached: the
38746
+ * spawn was refused, or the session ended and never resumed). Task 2575 —
38747
+ * this case did not exist on this door at all before, which is why tg-1
38748
+ * survived the 2026-08-08 outage unreported. Reported ONCE and RETAINED,
38749
+ * because the gateway retries the spawn for it;
38750
+ * - a still-queued message with NO subscriber older than giveUpMs ->
38751
+ * spawn-given-up (Task 2575: the retry window closed), removed;
38276
38752
  * - a drained message unacked past ackThresholdMs -> drain-undelivered (the
38277
38753
  * channel server never confirmed receipt — the silent cold-start drop).
38278
- * Each message is emitted at most once (removed when emitted). */
38754
+ * Each message is emitted at most once per kind; every kind but
38755
+ * spawn-undelivered removes it. */
38279
38756
  sweep(now, t) {
38280
38757
  const events = [];
38281
38758
  for (const [key2, s] of this.keys) {
@@ -38284,6 +38761,18 @@ var InboundHub3 = class {
38284
38761
  events.push({ kind: "inbound-undelivered", key: key2, messageId: e.payload.messageId, ageMs: now - e.enqueuedAt });
38285
38762
  return false;
38286
38763
  }
38764
+ if (e.state === "queued" && !s.subscriber) {
38765
+ const age = now - e.enqueuedAt;
38766
+ if (age > t.giveUpMs) {
38767
+ events.push({ kind: "spawn-given-up", key: key2, messageId: e.payload.messageId, ageMs: age });
38768
+ return false;
38769
+ }
38770
+ if (age > t.queuedThresholdMs && e.spawnUndeliveredAt == null) {
38771
+ e.spawnUndeliveredAt = now;
38772
+ events.push({ kind: "spawn-undelivered", key: key2, messageId: e.payload.messageId, ageMs: age });
38773
+ }
38774
+ return true;
38775
+ }
38287
38776
  if (e.state === "drained" && e.drainedAt != null && now - e.drainedAt > t.ackThresholdMs) {
38288
38777
  events.push({ kind: "drain-undelivered", key: key2, messageId: e.payload.messageId, ageMs: now - e.drainedAt });
38289
38778
  return false;
@@ -38293,6 +38782,23 @@ var InboundHub3 = class {
38293
38782
  }
38294
38783
  return events;
38295
38784
  }
38785
+ /** Task 2575 — every key holding queued work with NO subscriber whose oldest
38786
+ * message is older than `thresholdMs`. The gateway's sweep re-invokes the
38787
+ * retained spawn arguments for each one. Read-only — it emits nothing and
38788
+ * mutates nothing, so calling it cannot consume a terminal event the sweep is
38789
+ * about to report. */
38790
+ heldSpawns(now, thresholdMs) {
38791
+ const out = [];
38792
+ for (const [key2, s] of this.keys) {
38793
+ if (s.subscriber) continue;
38794
+ const queued = s.inFlight.filter((e) => e.state === "queued");
38795
+ if (queued.length === 0) continue;
38796
+ const oldestAgeMs = Math.max(...queued.map((e) => now - e.enqueuedAt));
38797
+ if (oldestAgeMs <= thresholdMs) continue;
38798
+ out.push({ key: key2, oldestAgeMs, count: queued.length });
38799
+ }
38800
+ return out;
38801
+ }
38296
38802
  /** Whether a key currently has a live channel server attached. The gateway
38297
38803
  * uses this to decide whether an inbound needs a cold-start spawn/resume. */
38298
38804
  hasSubscriber(key2) {
@@ -38466,6 +38972,7 @@ init_loop_registry();
38466
38972
  var QUEUED_THRESHOLD_MS3 = 3e4;
38467
38973
  var ACK_THRESHOLD_MS3 = 3e4;
38468
38974
  var SWEEP_INTERVAL_MS3 = 15e3;
38975
+ var GIVE_UP_MS2 = 3e5;
38469
38976
  function telegramSubjectSessionId(input) {
38470
38977
  if (input.role === "admin") {
38471
38978
  const adminUserId = input.adminUserId ?? void 0;
@@ -38479,16 +38986,6 @@ var TelegramGateway = class {
38479
38986
  }
38480
38987
  hub = new InboundHub3();
38481
38988
  replies = /* @__PURE__ */ new Map();
38482
- /** Task 2559 — the keys whose last inbound was `role:'admin'`. The follower
38483
- * census's bound set is the doors that SHOULD have a reader, and a public
38484
- * Telegram session is given none by design: the public branch of
38485
- * `buildTelegramEnsureSession` returns before the attach block, because the
38486
- * file follower is an admin surface. Including a public key would report it
38487
- * missing on every tick for ever and stop `missing=` ever reaching zero,
38488
- * which is the one signal the census exists to give. Removed on a public
38489
- * inbound, because the role is read from account config on every inbound and
38490
- * a bot can be flipped between the two. */
38491
- adminKeys = /* @__PURE__ */ new Set();
38492
38989
  spawning = /* @__PURE__ */ new Set();
38493
38990
  /** Task 2428 — keys whose channel server signalled ready while their spawn was
38494
38991
  * still in flight. The harness discards a channel notification that arrives
@@ -38499,6 +38996,14 @@ var TelegramGateway = class {
38499
38996
  * `resume-bound`. Holding the signal here rather than draining leaves
38500
38997
  * `hub.ready` false, so every message in the window queues and none is lost. */
38501
38998
  readyWhileSpawning = /* @__PURE__ */ new Set();
38999
+ /** Task 2575 — the arguments of the last `ensureChannelSession` call per key,
39000
+ * so the sweep can re-invoke a spawn that was refused. The refusal is not
39001
+ * visible here — `ensureChannelSession` returns void and the refusal sites in
39002
+ * server/telegram-ensure-session.ts log and return — so the retry keys off
39003
+ * queue state, the observable consequence of the refusal. `attempts` is the
39004
+ * per-key retry counter that rides the log line, reset whenever a fresh
39005
+ * inbound takes the spawn path. */
39006
+ heldSpawnArgs = /* @__PURE__ */ new Map();
38502
39007
  seq = 0;
38503
39008
  /** Hono sub-app exposing the loopback channel routes; mount on the UI app. */
38504
39009
  routes() {
@@ -38545,7 +39050,8 @@ var TelegramGateway = class {
38545
39050
  reconcile(now) {
38546
39051
  const events = this.hub.sweep(now, {
38547
39052
  queuedThresholdMs: QUEUED_THRESHOLD_MS3,
38548
- ackThresholdMs: ACK_THRESHOLD_MS3
39053
+ ackThresholdMs: ACK_THRESHOLD_MS3,
39054
+ giveUpMs: GIVE_UP_MS2
38549
39055
  });
38550
39056
  for (const e of events) {
38551
39057
  console.error(
@@ -38554,6 +39060,49 @@ var TelegramGateway = class {
38554
39060
  }
38555
39061
  return events;
38556
39062
  }
39063
+ /** Task 2575 — re-invoke the retained spawn for every key holding queued work
39064
+ * with no channel server attached. Driven by startSweeper() beside reconcile();
39065
+ * called directly with an injected clock in tests. Returns the number of
39066
+ * ensureChannelSession calls made.
39067
+ *
39068
+ * This door kept its held message across the 2026-08-08 outage but still
39069
+ * waited 4m41s for it, because the drain fires only when a later inbound
39070
+ * spawns successfully. This is the trigger that removes that wait. */
39071
+ async retryHeldSpawns(now) {
39072
+ const held = this.hub.heldSpawns(now, QUEUED_THRESHOLD_MS3);
39073
+ let calls = 0;
39074
+ for (const h of held) {
39075
+ if (this.spawning.has(h.key)) continue;
39076
+ const entry2 = this.heldSpawnArgs.get(h.key);
39077
+ if (!entry2) {
39078
+ console.error(
39079
+ `[telegram-native] op=spawn-retry-skipped key=${h.key} ageMs=${h.oldestAgeMs} queued=${h.count} reason=no-args`
39080
+ );
39081
+ continue;
39082
+ }
39083
+ entry2.attempts++;
39084
+ this.spawning.add(h.key);
39085
+ calls++;
39086
+ try {
39087
+ await this.deps.ensureChannelSession(entry2.args);
39088
+ console.error(
39089
+ `[telegram-native] op=spawn-retry key=${h.key} attempt=${entry2.attempts} ageMs=${h.oldestAgeMs} queued=${h.count} ok=true`
39090
+ );
39091
+ } catch (err) {
39092
+ console.error(
39093
+ `[telegram-native] op=spawn-retry key=${h.key} attempt=${entry2.attempts} ageMs=${h.oldestAgeMs} queued=${h.count} ok=false reason=${err instanceof Error ? err.message : String(err)}`
39094
+ );
39095
+ } finally {
39096
+ this.spawning.delete(h.key);
39097
+ if (this.readyWhileSpawning.delete(h.key)) this.drain(h.key);
39098
+ }
39099
+ }
39100
+ const heldKeys = new Set(held.map((h) => h.key));
39101
+ for (const k of [...this.heldSpawnArgs.keys()]) {
39102
+ if (!heldKeys.has(k) && !this.spawning.has(k)) this.heldSpawnArgs.delete(k);
39103
+ }
39104
+ return calls;
39105
+ }
38557
39106
  /** Arm the periodic reconciliation sweep in the long-lived UI process.
38558
39107
  *
38559
39108
  * Task 1814 — armed through the registry, so it appears on /activity. This
@@ -38571,9 +39120,14 @@ var TelegramGateway = class {
38571
39120
  return registerLoop({
38572
39121
  name: "telegram-native-sweep",
38573
39122
  intervalMs,
38574
- run: () => {
38575
- const events = this.reconcile(Date.now());
38576
- return events.length > 0 ? `${events.length} terminal events` : void 0;
39123
+ run: async () => {
39124
+ const now = Date.now();
39125
+ const events = this.reconcile(now);
39126
+ const retried = await this.retryHeldSpawns(now);
39127
+ const parts = [];
39128
+ if (events.length > 0) parts.push(`${events.length} terminal events`);
39129
+ if (retried > 0) parts.push(`${retried} spawn retries`);
39130
+ return parts.length > 0 ? parts.join(", ") : void 0;
38577
39131
  }
38578
39132
  });
38579
39133
  }
@@ -38600,8 +39154,6 @@ var TelegramGateway = class {
38600
39154
  const willDeliverNow = this.hub.isReady(key2);
38601
39155
  const source = input.source ?? "user";
38602
39156
  this.replies.set(key2, input.reply);
38603
- if (input.role === "admin") this.adminKeys.add(key2);
38604
- else this.adminKeys.delete(key2);
38605
39157
  let standingRules;
38606
39158
  if (input.role === "admin" && this.deps.fetchStandingRules) {
38607
39159
  const res = await this.deps.fetchStandingRules(input.accountId);
@@ -38634,25 +39186,36 @@ var TelegramGateway = class {
38634
39186
  console.error(
38635
39187
  `[telegram-native] op=inbound key=${key2} botId=${input.botId} accountId=${input.accountId} role=${input.role} agent=${input.agentSlug} bytes=${bytes} source=${source} delivery=${willDeliverNow ? "immediate" : "queued"} sessionId=${resolvedSessionId}`
38636
39188
  );
39189
+ try {
39190
+ if (source === "user") {
39191
+ this.deps.sendChatAction?.({ botToken: input.botToken, chatId: input.chatId, key: key2 });
39192
+ }
39193
+ } catch (err) {
39194
+ console.error(
39195
+ `[telegram-native] op=chat-action key=${key2} site=inbound ok=false reason=${err instanceof Error ? err.message : String(err)}`
39196
+ );
39197
+ }
38637
39198
  if (!hadSubscriber && !this.spawning.has(key2)) {
38638
39199
  this.spawning.add(key2);
38639
39200
  console.error(`[telegram-native] op=spawn-trigger key=${key2} botId=${input.botId} role=${input.role}`);
38640
39201
  noteSpawnAttempt2(input.botId);
39202
+ const spawnArgs = {
39203
+ accountId: input.accountId,
39204
+ accountDir: input.accountDir,
39205
+ botId: input.botId,
39206
+ botToken: input.botToken,
39207
+ agentSlug: input.agentSlug,
39208
+ channelKey: key2,
39209
+ senderId: input.senderId,
39210
+ role: input.role,
39211
+ personId: input.personId ?? null,
39212
+ chatId: input.chatId,
39213
+ gatewayUrl: this.deps.gatewayUrl,
39214
+ serverPath: this.deps.serverPath
39215
+ };
39216
+ this.heldSpawnArgs.set(key2, { args: spawnArgs, attempts: 0 });
38641
39217
  try {
38642
- await this.deps.ensureChannelSession({
38643
- accountId: input.accountId,
38644
- accountDir: input.accountDir,
38645
- botId: input.botId,
38646
- botToken: input.botToken,
38647
- agentSlug: input.agentSlug,
38648
- channelKey: key2,
38649
- senderId: input.senderId,
38650
- role: input.role,
38651
- personId: input.personId ?? null,
38652
- chatId: input.chatId,
38653
- gatewayUrl: this.deps.gatewayUrl,
38654
- serverPath: this.deps.serverPath
38655
- });
39218
+ await this.deps.ensureChannelSession(spawnArgs);
38656
39219
  } finally {
38657
39220
  this.spawning.delete(key2);
38658
39221
  if (this.readyWhileSpawning.delete(key2)) this.drain(key2);
@@ -38667,17 +39230,21 @@ var TelegramGateway = class {
38667
39230
  liveReplyTarget(key2) {
38668
39231
  return this.replies.get(key2) ?? null;
38669
39232
  }
38670
- /** Task 2559 — every admin door this gateway currently holds a live delivery
38671
- * closure for, as the `<botId>:<senderId>` channel key the follower registry
38672
- * keys on. The twin of `WaGateway.boundTargets()`: the census reconciles
38673
- * against exactly this set, so "should have a reader" and "can be sent to"
38674
- * are one fact and cannot drift. A key `parseTelegramChannelKey` rejects is
38675
- * the pre-2368 bare-sender shape, skipped rather than halved at an arbitrary
38676
- * offset, which would yield a door id nothing keys on. */
39233
+ /** Task 2559 — every door this gateway currently holds a live delivery closure
39234
+ * for, as the `<botId>:<senderId>` channel key the follower registry keys on.
39235
+ * The twin of `WaGateway.boundTargets()`: the census reconciles against
39236
+ * exactly this set, so "should have a reader" and "can be sent to" are one
39237
+ * fact and cannot drift. A key `parseTelegramChannelKey` rejects is the
39238
+ * pre-2368 bare-sender shape, skipped rather than halved at an arbitrary
39239
+ * offset, which would yield a door id nothing keys on.
39240
+ *
39241
+ * Task 2541 — every role, not admin only. 2559 filtered public keys out of
39242
+ * this set because a public session was given no reader; it has one now, so
39243
+ * the filter would hide the door instead of the absence. That is what it did:
39244
+ * `bound=1 attached=1 missing=0` printed on every tick while a public
39245
+ * visitor's answer reached no chat at all. */
38677
39246
  boundTargets() {
38678
- return [...this.replies.keys()].filter(
38679
- (key2) => this.adminKeys.has(key2) && parseTelegramChannelKey(key2) !== null
38680
- );
39247
+ return [...this.replies.keys()].filter((key2) => parseTelegramChannelKey(key2) !== null);
38681
39248
  }
38682
39249
  async sendOutbound(key2, text) {
38683
39250
  const reply = this.replies.get(key2);
@@ -38719,7 +39286,7 @@ function buildTelegramSpawnRequest(input) {
38719
39286
  }
38720
39287
 
38721
39288
  // server/telegram-ensure-session.ts
38722
- var TAG62 = "[telegram-inbound]";
39289
+ var TAG64 = "[telegram-inbound]";
38723
39290
  function buildTelegramEnsureSession(deps) {
38724
39291
  return async (input) => {
38725
39292
  const req = buildTelegramSpawnRequest({
@@ -38733,6 +39300,18 @@ function buildTelegramEnsureSession(deps) {
38733
39300
  // Task 1789 — admin rows only; see the WhatsApp twin.
38734
39301
  overrideSessionId: input.role === "admin" ? deps.resolveSessionOverride(input.accountId, input.channelKey) : null
38735
39302
  });
39303
+ const notifySend = async (text) => {
39304
+ const sent = await deps.sendText(input.botToken, Number(input.chatId), text);
39305
+ if (!sent.ok) throw new Error(sent.error ?? "send failed");
39306
+ };
39307
+ const notifyRefused = (result2) => sendRefusalNotice({
39308
+ channel: "telegram",
39309
+ doorId: req.channelKey,
39310
+ sender: req.channelKey,
39311
+ reason: result2.error,
39312
+ status: result2.status,
39313
+ send: notifySend
39314
+ });
38736
39315
  if (input.role === "public") {
38737
39316
  const result2 = await deps.managerRcSpawn({
38738
39317
  senderId: req.channelKey,
@@ -38750,15 +39329,24 @@ function buildTelegramEnsureSession(deps) {
38750
39329
  `[telegram-native] op=spawn-failed botId=${input.botId} key=${req.channelKey} role=public agent=${input.agentSlug} error=${result2.error} status=${result2.status}`
38751
39330
  );
38752
39331
  noteSpawnFailed2(input.botId);
39332
+ await notifyRefused(result2);
38753
39333
  return;
38754
39334
  }
38755
39335
  console.error(
38756
- `${TAG62} op=telegram-spawn-door botId=${input.botId} role=public door=rc agent=${input.agentSlug}`
39336
+ `${TAG64} op=telegram-spawn-door botId=${input.botId} role=public door=rc agent=${input.agentSlug}`
38757
39337
  );
38758
39338
  console.error(
38759
- `${TAG62} op=spawn botId=${input.botId} sessionId=${result2.sessionId} targetAccountId=${input.accountId} role=public`
39339
+ `${TAG64} op=spawn botId=${input.botId} sessionId=${result2.sessionId} targetAccountId=${input.accountId} role=public`
38760
39340
  );
38761
39341
  noteSpawnOk2(input.botId);
39342
+ clearRefusalNotice(req.channelKey);
39343
+ deps.followers.attach({
39344
+ sessionId: result2.sessionId,
39345
+ senderId: req.channelKey,
39346
+ accountId: input.accountId,
39347
+ chatId: input.chatId,
39348
+ botToken: input.botToken
39349
+ });
38762
39350
  return;
38763
39351
  }
38764
39352
  const result = await deps.managerRcSpawn(req);
@@ -38767,12 +39355,14 @@ function buildTelegramEnsureSession(deps) {
38767
39355
  `[telegram-native] op=spawn-failed botId=${input.botId} key=${req.channelKey} role=admin error=${result.error} status=${result.status}`
38768
39356
  );
38769
39357
  noteSpawnFailed2(input.botId);
39358
+ await notifyRefused(result);
38770
39359
  return;
38771
39360
  }
38772
39361
  console.error(
38773
- `${TAG62} op=spawn botId=${input.botId} sessionId=${req.sessionId} targetAccountId=${input.accountId} role=admin`
39362
+ `${TAG64} op=spawn botId=${input.botId} sessionId=${req.sessionId} targetAccountId=${input.accountId} role=admin`
38774
39363
  );
38775
39364
  noteSpawnOk2(input.botId);
39365
+ clearRefusalNotice(req.channelKey);
38776
39366
  deps.followers.attach({
38777
39367
  sessionId: req.sessionId,
38778
39368
  senderId: req.channelKey,
@@ -38788,7 +39378,7 @@ init_attachments();
38788
39378
  import { realpathSync as realpathSync10 } from "fs";
38789
39379
  import { readFile as readFile14, stat as stat10 } from "fs/promises";
38790
39380
  import { resolve as resolve48, basename as basename16 } from "path";
38791
- var TAG63 = "[telegram:outbound]";
39381
+ var TAG65 = "[telegram:outbound]";
38792
39382
  var TELEGRAM_DOCUMENT_MAX_BYTES = 50 * 1024 * 1024;
38793
39383
  async function sendTelegramDocument(input) {
38794
39384
  const { botToken, chatId, filePath, caption, maxyAccountId, platformRoot: platformRoot5 } = input;
@@ -38805,10 +39395,10 @@ async function sendTelegramDocument(input) {
38805
39395
  } catch (err) {
38806
39396
  const code = err.code;
38807
39397
  if (code === "ENOENT") {
38808
- console.error(`${TAG63} document ENOENT scope=account-dir accountDir=${accountDir} maxyAccountId=${maxyAccountId}`);
39398
+ console.error(`${TAG65} document ENOENT scope=account-dir accountDir=${accountDir} maxyAccountId=${maxyAccountId}`);
38809
39399
  return { ok: false, status: 500, error: `Account directory not resolvable for ${maxyAccountId}` };
38810
39400
  }
38811
- console.error(`${TAG63} document path error scope=account-dir maxyAccountId=${maxyAccountId}: ${String(err)}`);
39401
+ console.error(`${TAG65} document path error scope=account-dir maxyAccountId=${maxyAccountId}: ${String(err)}`);
38812
39402
  return { ok: false, status: 500, error: String(err) };
38813
39403
  }
38814
39404
  let resolvedPath;
@@ -38817,14 +39407,14 @@ async function sendTelegramDocument(input) {
38817
39407
  } catch (err) {
38818
39408
  const code = err.code;
38819
39409
  if (code === "ENOENT") {
38820
- console.error(`${TAG63} document ENOENT scope=file path=${filePath} maxyAccountId=${maxyAccountId}`);
39410
+ console.error(`${TAG65} document ENOENT scope=file path=${filePath} maxyAccountId=${maxyAccountId}`);
38821
39411
  return { ok: false, status: 404, error: `File not found: ${filePath}` };
38822
39412
  }
38823
- console.error(`${TAG63} document path error scope=file maxyAccountId=${maxyAccountId}: ${String(err)}`);
39413
+ console.error(`${TAG65} document path error scope=file maxyAccountId=${maxyAccountId}: ${String(err)}`);
38824
39414
  return { ok: false, status: 500, error: String(err) };
38825
39415
  }
38826
39416
  if (!resolvedPath.startsWith(accountResolved + "/")) {
38827
- console.error(`${TAG63} document REJECTED reason=outside_account_directory maxyAccountId=${maxyAccountId}`);
39417
+ console.error(`${TAG65} document REJECTED reason=outside_account_directory maxyAccountId=${maxyAccountId}`);
38828
39418
  return { ok: false, status: 403, error: "Access denied: file is outside the account directory" };
38829
39419
  }
38830
39420
  const fileStat = await stat10(resolvedPath);
@@ -38857,24 +39447,24 @@ async function sendTelegramDocument(input) {
38857
39447
  error = e instanceof Error ? e.message : String(e);
38858
39448
  }
38859
39449
  console.error(
38860
- `${TAG63} sent document to=${chatId} file=${filename} bytes=${fileStat.size} ok=${ok2}` + (messageId !== void 0 ? ` id=${messageId}` : "")
39450
+ `${TAG65} sent document to=${chatId} file=${filename} bytes=${fileStat.size} ok=${ok2}` + (messageId !== void 0 ? ` id=${messageId}` : "")
38861
39451
  );
38862
39452
  return ok2 ? { ok: true, messageId } : { ok: false, status: 500, error };
38863
39453
  }
38864
39454
 
38865
39455
  // app/lib/telegram/outbound/file-delivery.ts
38866
- var TAG64 = "[telegram:outbound]";
39456
+ var TAG66 = "[telegram:outbound]";
38867
39457
  function platformRoot4() {
38868
39458
  return process.env.MAXY_PLATFORM_ROOT || "";
38869
39459
  }
38870
39460
  function makeTelegramSendFile(entry2, botToken) {
38871
39461
  return async (filePath, caption) => {
38872
39462
  if (!botToken) {
38873
- console.error(`${TAG64} file-delivery reject reason=no-bot-token sender=${entry2.senderId} role=${entry2.role}`);
39463
+ console.error(`${TAG66} file-delivery reject reason=no-bot-token sender=${entry2.senderId} role=${entry2.role}`);
38874
39464
  return { ok: false, error: "no-bot-token" };
38875
39465
  }
38876
39466
  if (entry2.replyTarget == null) {
38877
- console.error(`${TAG64} file-delivery reject reason=no-reply-target sender=${entry2.senderId} role=${entry2.role}`);
39467
+ console.error(`${TAG66} file-delivery reject reason=no-reply-target sender=${entry2.senderId} role=${entry2.role}`);
38878
39468
  return { ok: false, error: "no-reply-target" };
38879
39469
  }
38880
39470
  const result = await sendTelegramDocument({
@@ -38891,13 +39481,14 @@ function makeTelegramSendFile(entry2, botToken) {
38891
39481
  function makeTelegramFileDelivery(entry2, botToken) {
38892
39482
  return makeFileDelivery({
38893
39483
  entry: entry2,
38894
- tag: TAG64,
39484
+ tag: TAG66,
38895
39485
  channel: "telegram",
38896
39486
  sendFile: makeTelegramSendFile(entry2, botToken)
38897
39487
  });
38898
39488
  }
38899
39489
 
38900
39490
  // app/lib/telegram/gateway/native-file-follower.ts
39491
+ var CHAT_ACTION_REFRESH_MS = 4e3;
38901
39492
  function startTelegramNativeFileFollower(input) {
38902
39493
  const entry2 = {
38903
39494
  sessionId: input.sessionId,
@@ -38917,6 +39508,16 @@ function startTelegramNativeFileFollower(input) {
38917
39508
  // The chat id the document is addressed to (set per spawn from the inbound).
38918
39509
  replyTarget: input.chatId
38919
39510
  };
39511
+ let lastChatActionAt = Number.NEGATIVE_INFINITY;
39512
+ const onActivity = () => {
39513
+ const now = Date.now();
39514
+ if (now - lastChatActionAt < CHAT_ACTION_REFRESH_MS) return;
39515
+ lastChatActionAt = now;
39516
+ void sendTelegramChatAction(input.botToken, input.chatId, input.senderId, "activity");
39517
+ };
39518
+ const onTurnComplete = () => {
39519
+ lastChatActionAt = Number.NEGATIVE_INFINITY;
39520
+ };
38920
39521
  return startEmitter({
38921
39522
  entry: entry2,
38922
39523
  tag: "[telegram-adaptor]",
@@ -38930,6 +39531,8 @@ function startTelegramNativeFileFollower(input) {
38930
39531
  // A resumed session's JSONL already holds prior SendUserFile tool_uses;
38931
39532
  // suppress replay so historical files are not re-sent on attach.
38932
39533
  suppressResumeReplay: true,
39534
+ onActivity,
39535
+ onTurnComplete,
38933
39536
  // Task 2521 — the answer reaches every bound door through the one fan-out.
38934
39537
  // Ungated by design: whatever woke the turn, the reader and the chats show
38935
39538
  // the same thing.
@@ -38968,7 +39571,7 @@ init_webchat();
38968
39571
  // app/lib/channel-pty-bridge/public-session-end-review.ts
38969
39572
  init_src3();
38970
39573
  import { randomUUID as randomUUID14 } from "crypto";
38971
- var TAG65 = "[public-session-review]";
39574
+ var TAG67 = "[public-session-review]";
38972
39575
  var CONSUMED_CAP = 500;
38973
39576
  var consumed = /* @__PURE__ */ new Set();
38974
39577
  function consumeOnce(sessionId) {
@@ -38995,7 +39598,7 @@ async function fetchJsonl(sessionId) {
38995
39598
  return await res.text();
38996
39599
  } catch (err) {
38997
39600
  console.error(
38998
- `${TAG65} jsonl-fetch-failed sessionId=${sessionId} err="${err instanceof Error ? err.message : String(err)}"`
39601
+ `${TAG67} jsonl-fetch-failed sessionId=${sessionId} err="${err instanceof Error ? err.message : String(err)}"`
38999
39602
  );
39000
39603
  return null;
39001
39604
  }
@@ -39019,7 +39622,7 @@ async function fetchPriorWrites(sliceToken, accountId) {
39019
39622
  const res = await fetch(url);
39020
39623
  if (!res.ok) {
39021
39624
  console.error(
39022
- `${TAG65} prior-writes-fetch-failed sliceToken=${sliceToken.slice(0, 8)} status=${res.status}`
39625
+ `${TAG67} prior-writes-fetch-failed sliceToken=${sliceToken.slice(0, 8)} status=${res.status}`
39023
39626
  );
39024
39627
  return [];
39025
39628
  }
@@ -39027,7 +39630,7 @@ async function fetchPriorWrites(sliceToken, accountId) {
39027
39630
  return Array.isArray(body.writes) ? body.writes : [];
39028
39631
  } catch (err) {
39029
39632
  console.error(
39030
- `${TAG65} prior-writes-fetch-threw sliceToken=${sliceToken.slice(0, 8)} err="${err instanceof Error ? err.message : String(err)}"`
39633
+ `${TAG67} prior-writes-fetch-threw sliceToken=${sliceToken.slice(0, 8)} err="${err instanceof Error ? err.message : String(err)}"`
39031
39634
  );
39032
39635
  return [];
39033
39636
  }
@@ -39056,7 +39659,7 @@ function composeInitialMessage(input) {
39056
39659
  }
39057
39660
  async function dispatchReviewer(input, initialMessage) {
39058
39661
  const sessionId = randomUUID14();
39059
- console.log(`${TAG65} route target=rc-spawn sessionId=${sessionId.slice(0, 8)} sourceSession=${input.sessionId.slice(0, 8)}`);
39662
+ console.log(`${TAG67} route target=rc-spawn sessionId=${sessionId.slice(0, 8)} sourceSession=${input.sessionId.slice(0, 8)}`);
39060
39663
  const spawned = await managerRcSpawn({
39061
39664
  sessionId,
39062
39665
  initialMessage,
@@ -39080,21 +39683,21 @@ async function firePublicSessionEndReview(input) {
39080
39683
  const dispatchedAt = Date.now();
39081
39684
  if (consumed.has(input.sessionId)) {
39082
39685
  console.log(
39083
- `${TAG65} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-already-reviewed`
39686
+ `${TAG67} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-already-reviewed`
39084
39687
  );
39085
39688
  return;
39086
39689
  }
39087
39690
  const jsonl = await fetchJsonl(input.sessionId);
39088
39691
  if (!jsonl) {
39089
39692
  console.log(
39090
- `${TAG65} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-jsonl-missing`
39693
+ `${TAG67} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-jsonl-missing`
39091
39694
  );
39092
39695
  return;
39093
39696
  }
39094
39697
  const operatorTurns = countOperatorTurns(jsonl);
39095
39698
  if (operatorTurns === 0) {
39096
39699
  console.log(
39097
- `${TAG65} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-no-turns`
39700
+ `${TAG67} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-no-turns`
39098
39701
  );
39099
39702
  return;
39100
39703
  }
@@ -39108,19 +39711,19 @@ async function firePublicSessionEndReview(input) {
39108
39711
  });
39109
39712
  if (!consumeOnce(input.sessionId)) {
39110
39713
  console.log(
39111
- `${TAG65} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-already-reviewed`
39714
+ `${TAG67} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-already-reviewed`
39112
39715
  );
39113
39716
  return;
39114
39717
  }
39115
39718
  const dispatched = await dispatchReviewer(input, initialMessage);
39116
39719
  if (!dispatched.ok) {
39117
39720
  console.error(
39118
- `${TAG65} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-spawn-failed reason=${dispatched.reason}`
39721
+ `${TAG67} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=skipped-spawn-failed reason=${dispatched.reason}`
39119
39722
  );
39120
39723
  return;
39121
39724
  }
39122
39725
  console.log(
39123
- `${TAG65} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=dispatched managerSessionId=${dispatched.managerSessionId} operatorTurns=${operatorTurns} priorWrites=${priorWrites.length} ms=${Date.now() - dispatchedAt}`
39726
+ `${TAG67} dispatchFor=${input.dispatchFor} sessionId=${input.sessionId} sliceToken=${sliceShort} personId=${personField} result=dispatched managerSessionId=${dispatched.managerSessionId} operatorTurns=${operatorTurns} priorWrites=${priorWrites.length} ms=${Date.now() - dispatchedAt}`
39124
39727
  );
39125
39728
  }
39126
39729
 
@@ -39272,7 +39875,7 @@ init_account();
39272
39875
 
39273
39876
  // server/telegram-audit.ts
39274
39877
  import { statSync as statSync28 } from "fs";
39275
- import { join as join62 } from "path";
39878
+ import { join as join63 } from "path";
39276
39879
  init_channel_key();
39277
39880
  init_sidebar_sessions();
39278
39881
  function buildTelegramAuditLines(input) {
@@ -39338,7 +39941,7 @@ function countTelegramSessionsForAccount(accountId) {
39338
39941
  function* walkTelegramSidecars() {
39339
39942
  const cfg = claudeConfigDir();
39340
39943
  if (!cfg) return;
39341
- const projectsRoot = join62(cfg, "projects");
39944
+ const projectsRoot = join63(cfg, "projects");
39342
39945
  for (const { path } of enumerateSessionSidecars(projectsRoot)) {
39343
39946
  const meta = readSidecarMeta(path);
39344
39947
  if (meta.channel !== "telegram" || !meta.accountId) continue;
@@ -39363,16 +39966,25 @@ function* walkTelegramSidecars() {
39363
39966
  }
39364
39967
  }
39365
39968
 
39969
+ // app/lib/telegram/gateway/origin-target.ts
39970
+ function answeringOriginTarget(send, key2, noteAnswered3) {
39971
+ if (!send) return null;
39972
+ return async (text) => {
39973
+ await send(text);
39974
+ noteAnswered3(key2);
39975
+ };
39976
+ }
39977
+
39366
39978
  // server/telegram-migrate.ts
39367
- import { readFileSync as readFileSync53, writeFileSync as writeFileSync19, existsSync as existsSync53, renameSync as renameSync11, mkdirSync as mkdirSync11 } from "fs";
39368
- import { dirname as dirname18, join as join63 } from "path";
39979
+ import { readFileSync as readFileSync54, writeFileSync as writeFileSync19, existsSync as existsSync53, renameSync as renameSync11, mkdirSync as mkdirSync11 } from "fs";
39980
+ import { dirname as dirname18, join as join64 } from "path";
39369
39981
  function migrateTelegramTwoSlot(input) {
39370
39982
  const out = [];
39371
39983
  for (const account of input.accounts) {
39372
- const configPath3 = join63(account.accountDir, "account.json");
39984
+ const configPath3 = join64(account.accountDir, "account.json");
39373
39985
  let config;
39374
39986
  try {
39375
- config = JSON.parse(readFileSync53(configPath3, "utf-8"));
39987
+ config = JSON.parse(readFileSync54(configPath3, "utf-8"));
39376
39988
  } catch {
39377
39989
  console.error(`[telegram-audit] op=migrate-unreadable accountId=${account.accountId}`);
39378
39990
  continue;
@@ -39474,14 +40086,14 @@ function backfillMisfiledRecords(params) {
39474
40086
  init_message_store();
39475
40087
 
39476
40088
  // server/lib/booking-site-accounts.ts
39477
- import { existsSync as existsSync54, readFileSync as readFileSync54 } from "fs";
40089
+ import { existsSync as existsSync54, readFileSync as readFileSync55 } from "fs";
39478
40090
  import { resolve as resolve49 } from "path";
39479
40091
  function listBookingSiteAccounts(accounts) {
39480
40092
  return accounts.filter((a) => {
39481
40093
  const availPath = resolve49(a.accountDir, "calendar-availability.json");
39482
40094
  if (!existsSync54(availPath)) return false;
39483
40095
  try {
39484
- const cfg = JSON.parse(readFileSync54(availPath, "utf-8"));
40096
+ const cfg = JSON.parse(readFileSync55(availPath, "utf-8"));
39485
40097
  return typeof cfg.bookingDbName === "string" && cfg.bookingDbName.length > 0;
39486
40098
  } catch {
39487
40099
  return false;
@@ -39526,7 +40138,7 @@ init_paths();
39526
40138
 
39527
40139
  // ../lib/entitlement/src/index.ts
39528
40140
  import { createPublicKey, createHash as createHash7, verify as cryptoVerify } from "crypto";
39529
- import { existsSync as existsSync55, readFileSync as readFileSync55, statSync as statSync29 } from "fs";
40141
+ import { existsSync as existsSync55, readFileSync as readFileSync56, statSync as statSync29 } from "fs";
39530
40142
  import { resolve as resolve50 } from "path";
39531
40143
 
39532
40144
  // ../lib/entitlement/src/canonicalize.ts
@@ -39590,7 +40202,7 @@ function resolveEntitlement(brand, account) {
39590
40202
  function verifyAndResolve(brand, entitlementPath, account) {
39591
40203
  let pubkeyPem;
39592
40204
  try {
39593
- pubkeyPem = readFileSync55(pubkeyPath(brand), "utf-8");
40205
+ pubkeyPem = readFileSync56(pubkeyPath(brand), "utf-8");
39594
40206
  } catch (err) {
39595
40207
  return logResolved(anonymousFallback("pubkey-missing"), {
39596
40208
  reason: "pubkey-missing"
@@ -39604,7 +40216,7 @@ function verifyAndResolve(brand, entitlementPath, account) {
39604
40216
  }
39605
40217
  let envelope;
39606
40218
  try {
39607
- envelope = JSON.parse(readFileSync55(entitlementPath, "utf-8"));
40219
+ envelope = JSON.parse(readFileSync56(entitlementPath, "utf-8"));
39608
40220
  } catch {
39609
40221
  return logResolved(anonymousFallback("malformed"), { reason: "malformed" });
39610
40222
  }
@@ -39766,14 +40378,14 @@ function clientFrom(c) {
39766
40378
  }
39767
40379
  installMediaDownloadGuard();
39768
40380
  var PLATFORM_ROOT6 = process.env.MAXY_PLATFORM_ROOT || "";
39769
- var BRAND_JSON_PATH = PLATFORM_ROOT6 ? join64(PLATFORM_ROOT6, "config", "brand.json") : "";
40381
+ var BRAND_JSON_PATH = PLATFORM_ROOT6 ? join65(PLATFORM_ROOT6, "config", "brand.json") : "";
39770
40382
  var BRAND = { productName: "Maxy", hostname: "maxy", configDir: ".maxy", marketingUrl: "https://getmaxy.com" };
39771
40383
  if (BRAND_JSON_PATH && !existsSync56(BRAND_JSON_PATH)) {
39772
40384
  console.error(`[brand] WARNING: brand.json not found at ${BRAND_JSON_PATH} \u2014 using Maxy defaults`);
39773
40385
  }
39774
40386
  if (BRAND_JSON_PATH && existsSync56(BRAND_JSON_PATH)) {
39775
40387
  try {
39776
- const parsed = JSON.parse(readFileSync56(BRAND_JSON_PATH, "utf-8"));
40388
+ const parsed = JSON.parse(readFileSync57(BRAND_JSON_PATH, "utf-8"));
39777
40389
  BRAND = { ...BRAND, ...parsed };
39778
40390
  } catch (err) {
39779
40391
  console.error(`[brand] Failed to parse brand.json: ${err.message}`);
@@ -39805,11 +40417,11 @@ var brandLoginOpts = {
39805
40417
  appleTouchIconPath: brandAppIcon512Path,
39806
40418
  themeColor: BRAND.defaultColors?.primary
39807
40419
  };
39808
- var ALIAS_DOMAINS_PATH = join64(homedir5(), BRAND.configDir, "alias-domains.json");
40420
+ var ALIAS_DOMAINS_PATH = join65(homedir6(), BRAND.configDir, "alias-domains.json");
39809
40421
  function loadAliasDomains() {
39810
40422
  try {
39811
40423
  if (!existsSync56(ALIAS_DOMAINS_PATH)) return null;
39812
- const parsed = JSON.parse(readFileSync56(ALIAS_DOMAINS_PATH, "utf-8"));
40424
+ const parsed = JSON.parse(readFileSync57(ALIAS_DOMAINS_PATH, "utf-8"));
39813
40425
  if (!Array.isArray(parsed)) {
39814
40426
  console.error("[alias-domains] malformed alias-domains.json \u2014 expected array");
39815
40427
  return null;
@@ -39863,7 +40475,12 @@ var nativeFileFollowers = createFollowerRegistry(
39863
40475
  // delivered session 8e5bc4e8's answer to nothing.
39864
40476
  fanTargets: () => whatsappFanTargets(p.effectiveAccountId, p.senderId),
39865
40477
  onClose
39866
- })
40478
+ }),
40479
+ // Task 2569 — the door is (effective account, sender), the same pair the
40480
+ // reply map and the session id key on. Keyed on the sender alone, a second
40481
+ // account's spawn for the same phone aborted the first account's live reader
40482
+ // and overwrote its recorded parameters, and the census called that healthy.
40483
+ whatsappDoorId
39867
40484
  );
39868
40485
  var webchatFileFollowers = /* @__PURE__ */ new Map();
39869
40486
  async function fetchAccountStandingRules(accountId) {
@@ -39945,7 +40562,7 @@ var waGateway = new WaGateway({
39945
40562
  // resolver and `fanTargetsFor` is a constructor dep, not a callable method.
39946
40563
  fanTargetsFor: whatsappFanTargets,
39947
40564
  gatewayUrl: `http://127.0.0.1:${process.env.MAXY_UI_INTERNAL_PORT ?? ""}`,
39948
- serverPath: process.env.MAXY_WA_CHANNEL_SERVER_PATH ?? resolve51(process.env.MAXY_PLATFORM_ROOT ?? join64(__dirname, ".."), "services/whatsapp-channel/dist/server.js"),
40565
+ serverPath: process.env.MAXY_WA_CHANNEL_SERVER_PATH ?? resolve51(process.env.MAXY_PLATFORM_ROOT ?? join65(__dirname, ".."), "services/whatsapp-channel/dist/server.js"),
39949
40566
  // Task 751 / 1390 — file delivery on the native channel. `maxyAccountId` (the
39950
40567
  // path-validation scope) is the sender's effective SESSION account, resolved
39951
40568
  // once at the inbound gate and threaded here via the gateway's per-sender doc
@@ -39960,7 +40577,7 @@ var waGateway = new WaGateway({
39960
40577
  caption,
39961
40578
  accountId,
39962
40579
  maxyAccountId,
39963
- platformRoot: resolve51(process.env.MAXY_PLATFORM_ROOT ?? join64(__dirname, ".."))
40580
+ platformRoot: resolve51(process.env.MAXY_PLATFORM_ROOT ?? join65(__dirname, ".."))
39964
40581
  });
39965
40582
  return result.ok ? { ok: true, messageId: result.messageId } : { ok: false, error: result.error };
39966
40583
  },
@@ -40050,11 +40667,20 @@ var waGateway = new WaGateway({
40050
40667
  `[whatsapp:public-agent] op=spawn-failed inboundId=${inboundId} senderId=${senderId} slug=${publicAgentSlug} error=${publicResult.error}`
40051
40668
  );
40052
40669
  noteSpawnFailed();
40670
+ await sendRefusalNotice({
40671
+ channel: "whatsapp",
40672
+ doorId: whatsappDoorId({ effectiveAccountId, senderId }),
40673
+ sender: senderId,
40674
+ reason: publicResult.error,
40675
+ status: publicResult.status,
40676
+ send: waGateway.liveReplyTarget(effectiveAccountId, senderId)
40677
+ });
40053
40678
  } else {
40054
40679
  console.error(
40055
40680
  `[whatsapp:public-agent] op=spawn-ok inboundId=${inboundId} senderId=${senderId} slug=${publicAgentSlug} ms=${Date.now() - publicSpawnStartedAt}`
40056
40681
  );
40057
40682
  noteSpawnOk();
40683
+ clearRefusalNotice(whatsappDoorId({ effectiveAccountId, senderId }));
40058
40684
  }
40059
40685
  return;
40060
40686
  }
@@ -40093,8 +40719,17 @@ var waGateway = new WaGateway({
40093
40719
  const result = await managerRcSpawn({ ...req, userId, party });
40094
40720
  if ("error" in result) {
40095
40721
  console.error(`[whatsapp-native] spawn-failed senderId=${senderId} role=${role} error=${result.error} status=${result.status}`);
40722
+ await sendRefusalNotice({
40723
+ channel: "whatsapp",
40724
+ doorId: whatsappDoorId({ effectiveAccountId, senderId }),
40725
+ sender: senderId,
40726
+ reason: result.error,
40727
+ status: result.status,
40728
+ send: waGateway.liveReplyTarget(effectiveAccountId, senderId)
40729
+ });
40096
40730
  return;
40097
40731
  }
40732
+ clearRefusalNotice(whatsappDoorId({ effectiveAccountId, senderId }));
40098
40733
  nativeFileFollowers.attach({
40099
40734
  sessionId: req.sessionId,
40100
40735
  senderId,
@@ -40117,7 +40752,7 @@ function runDuplicateSenderAudit() {
40117
40752
  try {
40118
40753
  const configDir = process.env.CLAUDE_CONFIG_DIR;
40119
40754
  if (!configDir) return;
40120
- const projectsRoot = join64(configDir, "projects");
40755
+ const projectsRoot = join65(configDir, "projects");
40121
40756
  const records2 = [];
40122
40757
  for (const { path: jsonlPath, isSubagent, archived } of enumerateJsonls(projectsRoot)) {
40123
40758
  if (isSubagent || archived) continue;
@@ -40169,7 +40804,7 @@ var webchatGateway = new WebchatGateway({
40169
40804
  // rather than hanging the POST.
40170
40805
  publicProjectsRoot: () => {
40171
40806
  const cfg = claudeConfigDir();
40172
- return cfg ? join64(cfg, "projects") : "";
40807
+ return cfg ? join65(cfg, "projects") : "";
40173
40808
  },
40174
40809
  gatewayUrl: webchatGatewayUrl(),
40175
40810
  serverPath: webchatServerPath(),
@@ -40311,6 +40946,12 @@ var telegramGateway = new TelegramGateway({
40311
40946
  resolveSessionOverride: (accountId, channelKey) => resolveChannelOverride(accountId, "telegram", channelKey),
40312
40947
  // Task 2521 — hoisted above; see `telegramFanTargets`.
40313
40948
  fanTargetsFor: telegramFanTargets,
40949
+ // Task 2576 — the typing indicator. Injected here because the gateway is
40950
+ // contracted to hold no Bot API import; see the dep comment in
40951
+ // telegram-gateway.ts. Not awaited: an indicator never paces a turn.
40952
+ sendChatAction: ({ botToken, chatId, key: key2 }) => {
40953
+ void sendTelegramChatAction(botToken, chatId, key2, "inbound");
40954
+ },
40314
40955
  gatewayUrl: telegramGatewayUrl(),
40315
40956
  serverPath: telegramServerPath(),
40316
40957
  // Task 2368 — the role branch lives in its own module so it has a test that
@@ -40320,6 +40961,8 @@ var telegramGateway = new TelegramGateway({
40320
40961
  managerRcSpawn,
40321
40962
  managerSpawn,
40322
40963
  resolveSessionOverride: (accountId, channelKey) => resolveChannelOverride(accountId, "telegram", channelKey),
40964
+ // Task 2573 — the send the refusal notice goes out on.
40965
+ sendText: sendTelegramText,
40323
40966
  followers: telegramFileFollowers
40324
40967
  })
40325
40968
  });
@@ -40334,10 +40977,16 @@ registerLoop({
40334
40977
  followerCensusTick(
40335
40978
  {
40336
40979
  channel: "whatsapp",
40337
- bound: () => waGateway.boundTargets().map((t) => t.senderId),
40980
+ // Task 2569 — the whole hub key, not its sender half. `boundTargets()`
40981
+ // decodes the reply-map keys and this re-encodes them through the same
40982
+ // helper pair, so the census and the gateway compare one fact. Halving
40983
+ // it here is what let one phone's two accounts collapse to one door.
40984
+ bound: () => waGateway.boundTargets().map((t) => hubKey(t.accountId, t.senderId)),
40338
40985
  live: () => nativeFileFollowers.live(),
40339
- paramsFor: (senderId) => nativeFileFollowers.paramsFor(senderId),
40340
- attach: (p) => nativeFileFollowers.attach(p)
40986
+ paramsFor: (doorId) => nativeFileFollowers.paramsFor(doorId),
40987
+ attach: (p) => nativeFileFollowers.attach(p),
40988
+ // The hub key carries a NUL, which must never reach server.log.
40989
+ label: whatsappDoorLabel
40341
40990
  },
40342
40991
  followerCensusState.whatsapp
40343
40992
  ),
@@ -40346,7 +40995,7 @@ registerLoop({
40346
40995
  channel: "telegram",
40347
40996
  bound: () => telegramGateway.boundTargets(),
40348
40997
  live: () => telegramFileFollowers.live(),
40349
- paramsFor: (senderId) => telegramFileFollowers.paramsFor(senderId),
40998
+ paramsFor: (doorId) => telegramFileFollowers.paramsFor(doorId),
40350
40999
  attach: (p) => telegramFileFollowers.attach(p)
40351
41000
  },
40352
41001
  followerCensusState.telegram
@@ -40799,7 +41448,7 @@ app80.get("/agent-assets/:slug/:filename", (c) => {
40799
41448
  const ext = "." + filename.split(".").pop()?.toLowerCase();
40800
41449
  const contentType = IMAGE_MIME[ext] || "application/octet-stream";
40801
41450
  console.log(`[agent-assets] serve slug=${slug} file=${filename} status=200`);
40802
- const body = readFileSync56(filePath);
41451
+ const body = readFileSync57(filePath);
40803
41452
  return c.body(body, 200, {
40804
41453
  "Content-Type": contentType,
40805
41454
  "Cache-Control": "public, max-age=3600"
@@ -40829,7 +41478,7 @@ app80.get("/generated/:filename", (c) => {
40829
41478
  const ext = "." + filename.split(".").pop()?.toLowerCase();
40830
41479
  const contentType = IMAGE_MIME[ext] || "application/octet-stream";
40831
41480
  console.log(`[generated] serve file=${filename} status=200`);
40832
- const body = readFileSync56(filePath);
41481
+ const body = readFileSync57(filePath);
40833
41482
  return c.body(body, 200, {
40834
41483
  "Content-Type": contentType,
40835
41484
  "Cache-Control": "public, max-age=86400"
@@ -40844,7 +41493,7 @@ var brandLogoPath = "/brand/maxy-monochrome.png";
40844
41493
  var brandIconPath = "/brand/maxy-monochrome.png";
40845
41494
  if (BRAND_JSON_PATH && existsSync56(BRAND_JSON_PATH)) {
40846
41495
  try {
40847
- const fullBrand = JSON.parse(readFileSync56(BRAND_JSON_PATH, "utf-8"));
41496
+ const fullBrand = JSON.parse(readFileSync57(BRAND_JSON_PATH, "utf-8"));
40848
41497
  if (fullBrand.assets?.logo) brandLogoPath = `/brand/${fullBrand.assets.logo}`;
40849
41498
  brandIconPath = fullBrand.assets?.icon ? `/brand/${fullBrand.assets.icon}` : brandLogoPath;
40850
41499
  } catch {
@@ -40875,7 +41524,7 @@ var brandAppIconsExist = [brandAppIcon192Path, brandAppIcon512Path, brandMaskabl
40875
41524
  );
40876
41525
  var SW_SOURCE = (() => {
40877
41526
  try {
40878
- return readFileSync56(resolve51(process.cwd(), "public", "sw.js"), "utf-8");
41527
+ return readFileSync57(resolve51(process.cwd(), "public", "sw.js"), "utf-8");
40879
41528
  } catch {
40880
41529
  return null;
40881
41530
  }
@@ -40883,9 +41532,9 @@ var SW_SOURCE = (() => {
40883
41532
  function readInstalledVersion() {
40884
41533
  try {
40885
41534
  if (!PLATFORM_ROOT6) return "unknown";
40886
- const versionFile = join64(PLATFORM_ROOT6, "config", `.${BRAND.hostname}-version`);
41535
+ const versionFile = join65(PLATFORM_ROOT6, "config", `.${BRAND.hostname}-version`);
40887
41536
  if (!existsSync56(versionFile)) return "unknown";
40888
- const content = readFileSync56(versionFile, "utf-8").trim();
41537
+ const content = readFileSync57(versionFile, "utf-8").trim();
40889
41538
  return content || "unknown";
40890
41539
  } catch {
40891
41540
  return "unknown";
@@ -40896,7 +41545,7 @@ var versionScript = `<script>window.__MAXY_VERSION__=${JSON.stringify(INSTALLED_
40896
41545
  function cachedHtml(file) {
40897
41546
  let html = htmlCache.get(file);
40898
41547
  if (!html) {
40899
- html = readFileSync56(resolve51(process.cwd(), "public", file), "utf-8");
41548
+ html = readFileSync57(resolve51(process.cwd(), "public", file), "utf-8");
40900
41549
  const productNameEsc = escapeHtml(BRAND.productName);
40901
41550
  html = html.replace(/<title>([^<]*)<\/title>/, (_match, inner) => `<title>${escapeHtml(inner).replace(/Maxy/g, productNameEsc)}</title>`);
40902
41551
  html = html.replace('href="/favicon.ico"', `href="${escapeHtml(brandFaviconPath)}"`);
@@ -40913,14 +41562,14 @@ ${clientErrorReporterScript}
40913
41562
  return html;
40914
41563
  }
40915
41564
  function loadBrandingCache(agentSlug) {
40916
- const configDir = join64(homedir5(), BRAND.configDir);
41565
+ const configDir = join65(homedir6(), BRAND.configDir);
40917
41566
  try {
40918
41567
  const verdict = resolvePublicAddressFromDisk(agentSlug);
40919
41568
  const accountId = verdict.kind === "served" ? verdict.owner.accountId : getDefaultAccountId();
40920
41569
  if (!accountId) return null;
40921
- const cachePath = join64(configDir, "branding-cache", accountId, `${agentSlug}.json`);
41570
+ const cachePath = join65(configDir, "branding-cache", accountId, `${agentSlug}.json`);
40922
41571
  if (!existsSync56(cachePath)) return null;
40923
- return JSON.parse(readFileSync56(cachePath, "utf-8"));
41572
+ return JSON.parse(readFileSync57(cachePath, "utf-8"));
40924
41573
  } catch {
40925
41574
  return null;
40926
41575
  }
@@ -41015,7 +41664,7 @@ app80.use("/vnc-popout.html", logViewerFetch);
41015
41664
  app80.get("/vnc-popout.html", (c) => {
41016
41665
  let html = htmlCache.get("vnc-popout.html");
41017
41666
  if (!html) {
41018
- html = readFileSync56(resolve51(process.cwd(), "public", "vnc-popout.html"), "utf-8");
41667
+ html = readFileSync57(resolve51(process.cwd(), "public", "vnc-popout.html"), "utf-8");
41019
41668
  const name = escapeHtml(BRAND.productName);
41020
41669
  html = html.replace("<title>Browser \u2014 Maxy</title>", `<title>${name}</title>`);
41021
41670
  html = html.replace("</head>", ` ${brandScript}
@@ -41169,7 +41818,7 @@ var hostname = process.env.HOSTNAME ?? "127.0.0.1";
41169
41818
  var httpServer = serve({ fetch: app80.fetch, port, hostname });
41170
41819
  console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
41171
41820
  {
41172
- const reconcilePlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join64(__dirname, "..");
41821
+ const reconcilePlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join65(__dirname, "..");
41173
41822
  const reconcileScript = resolve51(reconcilePlatformRoot, "plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js");
41174
41823
  const runReconcile = (ctx) => {
41175
41824
  if (!existsSync56(reconcileScript)) return;
@@ -41192,7 +41841,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
41192
41841
  detached: true,
41193
41842
  run: runReconcile
41194
41843
  });
41195
- const reconcileStatePath = join64(MAXY_DIR, "booking-reconcile-state.json");
41844
+ const reconcileStatePath = join65(MAXY_DIR, "booking-reconcile-state.json");
41196
41845
  registerLoop({
41197
41846
  name: "calendar-reconcile-liveness",
41198
41847
  intervalMs: 6e5,
@@ -41210,7 +41859,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
41210
41859
  }
41211
41860
  {
41212
41861
  const DREAM_CYCLE_SWEEP_INTERVAL_MS = 36e5;
41213
- const dreamPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join64(__dirname, "..");
41862
+ const dreamPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join65(__dirname, "..");
41214
41863
  const dreamSweepScript = resolve51(dreamPlatformRoot, "plugins/memory/mcp/dist/scripts/dream-cycle-sweep.js");
41215
41864
  registerLoop({
41216
41865
  name: "dream-cycle-sweep",
@@ -41234,7 +41883,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
41234
41883
  });
41235
41884
  }
41236
41885
  {
41237
- const outlookPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join64(__dirname, "..");
41886
+ const outlookPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join65(__dirname, "..");
41238
41887
  const outlookScript = resolve51(outlookPlatformRoot, "plugins/outlook/mcp/dist/scripts/complete-registration.js");
41239
41888
  const OUTLOOK_COMPLETE_INTERVAL_MS = 3e4;
41240
41889
  const runOutlookComplete = (ctx) => {
@@ -41260,7 +41909,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
41260
41909
  });
41261
41910
  }
41262
41911
  {
41263
- const auditRoot = process.env.MAXY_PLATFORM_ROOT ?? join64(__dirname, "..");
41912
+ const auditRoot = process.env.MAXY_PLATFORM_ROOT ?? join65(__dirname, "..");
41264
41913
  const strandedAccountsDir = resolve51(auditRoot, "..", "data/accounts");
41265
41914
  const STRANDED_AUDIT_INTERVAL_MS = 3e5;
41266
41915
  const STRANDED_AGE_MS = 16 * 6e4;
@@ -41269,7 +41918,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
41269
41918
  try {
41270
41919
  if (!existsSync56(strandedAccountsDir)) return;
41271
41920
  const now = Date.now();
41272
- for (const name of readdirSync32(strandedAccountsDir)) {
41921
+ for (const name of readdirSync33(strandedAccountsDir)) {
41273
41922
  if (!STRANDED_UUID_RE.test(name)) continue;
41274
41923
  const pendingPath2 = resolve51(strandedAccountsDir, name, "secrets/outlook/pending-devicecode.enc");
41275
41924
  if (!existsSync56(pendingPath2)) continue;
@@ -41290,7 +41939,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
41290
41939
  });
41291
41940
  }
41292
41941
  {
41293
- const googleRoot = process.env.MAXY_PLATFORM_ROOT ?? join64(__dirname, "..");
41942
+ const googleRoot = process.env.MAXY_PLATFORM_ROOT ?? join65(__dirname, "..");
41294
41943
  const googleAccountsDir = resolve51(googleRoot, "..", "data/accounts");
41295
41944
  const GOOGLE_PENDING_AUDIT_INTERVAL_MS = 3e5;
41296
41945
  const runGooglePendingAuditSafe = () => {
@@ -41334,7 +41983,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
41334
41983
  });
41335
41984
  }
41336
41985
  {
41337
- const auditPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join64(__dirname, "..");
41986
+ const auditPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join65(__dirname, "..");
41338
41987
  const STORAGE_AUDIT_INTERVAL_MS = 3e5;
41339
41988
  const runStorageAuditSafe = () => runStorageAudit(auditPlatformRoot).catch((err) => {
41340
41989
  console.error(`[storage-audit] error="${err.message}"`);
@@ -41370,7 +42019,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
41370
42019
  accounts: listValidAccounts().map((a) => ({ accountId: a.accountId, accountDir: a.accountDir })),
41371
42020
  secretPathFor: secretPath,
41372
42021
  legacySecretPathFor: (botType, accountId) => resolve51(
41373
- homedir5(),
42022
+ homedir6(),
41374
42023
  configDirName2(),
41375
42024
  `${botType === "admin" ? ".telegram-admin-webhook-secret" : ".telegram-webhook-secret"}.${accountId}`
41376
42025
  ),
@@ -41393,7 +42042,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
41393
42042
  const lastByBot = newestTelegramSessionByBot();
41394
42043
  let secretFilesOnDisk = [];
41395
42044
  try {
41396
- secretFilesOnDisk = readdirSync32(telegramSecretsDir());
42045
+ secretFilesOnDisk = readdirSync33(telegramSecretsDir());
41397
42046
  } catch {
41398
42047
  }
41399
42048
  const lines = buildTelegramAuditLines({
@@ -41536,7 +42185,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
41536
42185
  });
41537
42186
  }
41538
42187
  {
41539
- const publishPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join64(__dirname, "..");
42188
+ const publishPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join65(__dirname, "..");
41540
42189
  const publishScript = resolve51(publishPlatformRoot, "plugins/scheduling/mcp/dist/scripts/publish-availability.js");
41541
42190
  const PUBLISH_INTERVAL_MS = 3e5;
41542
42191
  const bookingAccounts = () => {
@@ -41571,7 +42220,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
41571
42220
  let hasBookingSite = false;
41572
42221
  try {
41573
42222
  if (existsSync56(availPath)) {
41574
- const cfg = JSON.parse(readFileSync56(availPath, "utf-8"));
42223
+ const cfg = JSON.parse(readFileSync57(availPath, "utf-8"));
41575
42224
  hasBookingSite = typeof cfg.bookingDbName === "string" && cfg.bookingDbName.length > 0;
41576
42225
  }
41577
42226
  } catch {
@@ -41581,7 +42230,7 @@ console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
41581
42230
  let lastSuccessAt = null;
41582
42231
  if (existsSync56(statePath)) {
41583
42232
  try {
41584
- const rec = JSON.parse(readFileSync56(statePath, "utf-8"));
42233
+ const rec = JSON.parse(readFileSync57(statePath, "utf-8"));
41585
42234
  lastSuccessAt = rec.lastSuccessAt ?? null;
41586
42235
  } catch {
41587
42236
  console.error(`[calendar-availability] op=audit accountId=${account.accountId} snapshotAgeMs=na reason=bad-state-file`);
@@ -41616,7 +42265,7 @@ startDispatchCensus(() => getSession());
41616
42265
  startDispatchFieldCensus(() => getSession());
41617
42266
  startDispatchTgCensus(() => getSession());
41618
42267
  {
41619
- const auditPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join64(__dirname, "..");
42268
+ const auditPlatformRoot = process.env.MAXY_PLATFORM_ROOT ?? join65(__dirname, "..");
41620
42269
  const auditScript = resolve51(auditPlatformRoot, "plugins/connector/mcp/dist/scripts/audit-connectors.js");
41621
42270
  const auditLogDir = process.env.LOG_DIR ?? LOG_DIR;
41622
42271
  const CONNECTOR_AUDIT_INTERVAL_MS = 3e5;
@@ -41747,7 +42396,7 @@ async function runAdminUserReconcileTick() {
41747
42396
  console.error("[adminuser-self-heal] skip reason=no-users-file");
41748
42397
  return;
41749
42398
  }
41750
- const usersRaw = readFileSync56(USERS_FILE, "utf-8").trim();
42399
+ const usersRaw = readFileSync57(USERS_FILE, "utf-8").trim();
41751
42400
  if (!usersRaw) {
41752
42401
  console.error("[adminuser-self-heal] skip reason=empty-users-file");
41753
42402
  return;
@@ -41829,8 +42478,8 @@ var runIngestAuditTick = () => {
41829
42478
  },
41830
42479
  agentExists: (accountId, slug) => {
41831
42480
  if (!house) return false;
41832
- const dir = isHouseAccountKey(house.accountId, accountId) ? house.accountDir : join64(ACCOUNTS_DIR, accountId);
41833
- return existsSync56(join64(dir, "agents", slug, "config.json"));
42481
+ const dir = isHouseAccountKey(house.accountId, accountId) ? house.accountDir : join65(ACCOUNTS_DIR, accountId);
42482
+ return existsSync56(join65(dir, "agents", slug, "config.json"));
41834
42483
  }
41835
42484
  });
41836
42485
  } catch (err) {
@@ -41878,9 +42527,9 @@ registerLoop({
41878
42527
  runPublicAddressCollisionAudit({
41879
42528
  houseAccountId: () => resolveAccount()?.accountId ?? null,
41880
42529
  activeSlugs: () => listValidAccounts().map((a) => {
41881
- const agentsRoot = join64(a.accountDir, "agents");
42530
+ const agentsRoot = join65(a.accountDir, "agents");
41882
42531
  if (!existsSync56(agentsRoot)) return { accountId: a.accountId, slugs: [] };
41883
- const slugs = readdirSync32(agentsRoot, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).filter((slug) => isActiveAgentSlug(a.accountDir, slug));
42532
+ const slugs = readdirSync33(agentsRoot, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).filter((slug) => isActiveAgentSlug(a.accountDir, slug));
41884
42533
  return { accountId: a.accountId, slugs };
41885
42534
  })
41886
42535
  });
@@ -41894,7 +42543,7 @@ registerLoop({
41894
42543
  var USERS_RECONCILE_INTERVAL_MS = 60 * 60 * 1e3;
41895
42544
  function countUsersRows() {
41896
42545
  if (!existsSync56(USERS_FILE)) return 0;
41897
- const raw = readFileSync56(USERS_FILE, "utf-8").trim();
42546
+ const raw = readFileSync57(USERS_FILE, "utf-8").trim();
41898
42547
  if (!raw) return 0;
41899
42548
  const users = JSON.parse(raw);
41900
42549
  return users.filter((u) => typeof u.userId === "string").length;
@@ -41925,6 +42574,7 @@ registerLoop({
41925
42574
  });
41926
42575
  startGraphHealthTimer();
41927
42576
  startSharedFolderCensus();
42577
+ startCredRefreshCensus();
41928
42578
  void migrateUploads().catch((err) => console.error(`[uploads-migrate] failed err="${err instanceof Error ? err.message : String(err)}"`)).finally(() => {
41929
42579
  void startFileWatcher().catch((err) => {
41930
42580
  console.error(`[file-watcher] start-failed err="${err instanceof Error ? err.message : String(err)}"`);
@@ -41979,7 +42629,7 @@ function whatsappSender(accountId, key2) {
41979
42629
  };
41980
42630
  }
41981
42631
  function telegramOriginTarget(key2) {
41982
- return telegramGateway.liveReplyTarget(key2);
42632
+ return answeringOriginTarget(telegramGateway.liveReplyTarget(key2), key2, noteAnswered);
41983
42633
  }
41984
42634
  var configDirForWhatsApp = basename17(MAXY_DIR) || ".maxy";
41985
42635
  var bootAccount = resolveAccount();
@@ -42047,7 +42697,7 @@ if (bootAccountConfig?.whatsapp) {
42047
42697
  }
42048
42698
  init({
42049
42699
  configDir: configDirForWhatsApp,
42050
- platformRoot: resolve51(process.env.MAXY_PLATFORM_ROOT ?? join64(__dirname, "..")),
42700
+ platformRoot: resolve51(process.env.MAXY_PLATFORM_ROOT ?? join65(__dirname, "..")),
42051
42701
  accountConfig: bootAccountConfig,
42052
42702
  // Task 2074 — enrich with the sub-account name and fan out to every open
42053
42703
  // admin SSE stream. Fire-and-forget: the manager's persist path must not