@rubytech/create-realagent-code 0.1.577 → 0.1.578

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) 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 +3 -1
  6. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +9 -0
  7. package/payload/platform/plugins/docs/references/platform.md +2 -0
  8. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +0 -2
  9. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
  10. package/payload/server/public/activity.html +4 -4
  11. package/payload/server/public/agents.html +3 -3
  12. package/payload/server/public/assets/{AdminLoginScreens-BLGZEaKU.js → AdminLoginScreens-CAZ3DSwA.js} +1 -1
  13. package/payload/server/public/assets/AdminLoginScreens-CAZ3DSwA.js.br +0 -0
  14. package/payload/server/public/assets/AdminLoginScreens-CAZ3DSwA.js.gz +0 -0
  15. package/payload/server/public/assets/{AdminShell-C_M7YE5Z.js → AdminShell-BLs5v0qF.js} +1 -1
  16. package/payload/server/public/assets/AdminShell-BLs5v0qF.js.br +0 -0
  17. package/payload/server/public/assets/AdminShell-BLs5v0qF.js.gz +0 -0
  18. package/payload/server/public/assets/{activity-BL6qbB9a.js → activity-BwPck2yT.js} +1 -1
  19. package/payload/server/public/assets/activity-BwPck2yT.js.br +0 -0
  20. package/payload/server/public/assets/activity-BwPck2yT.js.gz +0 -0
  21. package/payload/server/public/assets/{admin-jnJ3P2u4.js → admin-DjtOoabA.js} +1 -1
  22. package/payload/server/public/assets/admin-DjtOoabA.js.br +0 -0
  23. package/payload/server/public/assets/admin-DjtOoabA.js.gz +0 -0
  24. package/payload/server/public/assets/{agents-BzhfKZDN.js → agents-IQYaRvoh.js} +1 -1
  25. package/payload/server/public/assets/agents-IQYaRvoh.js.br +0 -0
  26. package/payload/server/public/assets/agents-IQYaRvoh.js.gz +0 -0
  27. package/payload/server/public/assets/{browser-C-6io2B9.js → browser-CPG8w8rQ.js} +1 -1
  28. package/payload/server/public/assets/browser-CPG8w8rQ.js.br +0 -0
  29. package/payload/server/public/assets/browser-CPG8w8rQ.js.gz +0 -0
  30. package/payload/server/public/assets/{calendar-Dt3gS88s.js → calendar-CRwrgNdq.js} +1 -1
  31. package/payload/server/public/assets/calendar-CRwrgNdq.js.br +0 -0
  32. package/payload/server/public/assets/calendar-CRwrgNdq.js.gz +0 -0
  33. package/payload/server/public/assets/chat-B7yZ4KLl.js +1 -0
  34. package/payload/server/public/assets/chat-B7yZ4KLl.js.br +3 -0
  35. package/payload/server/public/assets/chat-B7yZ4KLl.js.gz +0 -0
  36. package/payload/server/public/assets/chevron-left-BzKUQqLy.js +1 -0
  37. package/payload/server/public/assets/chevron-left-BzKUQqLy.js.br +0 -0
  38. package/payload/server/public/assets/clock-BafWFNGo.js +1 -0
  39. package/payload/server/public/assets/clock-BafWFNGo.js.br +0 -0
  40. package/payload/server/public/assets/{copy-TkPJgllS.js → copy-JyiMTV0k.js} +1 -1
  41. package/payload/server/public/assets/copy-JyiMTV0k.js.br +0 -0
  42. package/payload/server/public/assets/copy-JyiMTV0k.js.gz +0 -0
  43. package/payload/server/public/assets/data-DHtGCVrl.js +1 -0
  44. package/payload/server/public/assets/data-DHtGCVrl.js.br +0 -0
  45. package/payload/server/public/assets/data-DHtGCVrl.js.gz +0 -0
  46. package/payload/server/public/assets/{file-text-DyAL2DCh.js → file-text-cnHv9ZS6.js} +1 -1
  47. package/payload/server/public/assets/file-text-cnHv9ZS6.js.br +0 -0
  48. package/payload/server/public/assets/file-text-cnHv9ZS6.js.gz +0 -0
  49. package/payload/server/public/assets/{graph-PiNIcCfX.js → graph-lFwEUDSH.js} +1 -1
  50. package/payload/server/public/assets/graph-lFwEUDSH.js.br +0 -0
  51. package/payload/server/public/assets/graph-lFwEUDSH.js.gz +0 -0
  52. package/payload/server/public/assets/{graph-labels-8jKFrLHn.js → graph-labels-C6qUA0vO.js} +1 -1
  53. package/payload/server/public/assets/graph-labels-C6qUA0vO.js.br +0 -0
  54. package/payload/server/public/assets/graph-labels-C6qUA0vO.js.gz +0 -0
  55. package/payload/server/public/assets/{operator-DKGKGzrb.js → operator-BdoEy5mh.js} +1 -1
  56. package/payload/server/public/assets/operator-BdoEy5mh.js.br +0 -0
  57. package/payload/server/public/assets/operator-BdoEy5mh.js.gz +0 -0
  58. package/payload/server/public/assets/{page-DXz_K10G.js → page-DMrnnXNA.js} +1 -1
  59. package/payload/server/public/assets/page-DMrnnXNA.js.br +0 -0
  60. package/payload/server/public/assets/page-DMrnnXNA.js.gz +0 -0
  61. package/payload/server/public/assets/{page-BU5vlY9Y.js → page-DeMikGYL.js} +1 -1
  62. package/payload/server/public/assets/page-DeMikGYL.js.br +0 -0
  63. package/payload/server/public/assets/page-DeMikGYL.js.gz +0 -0
  64. package/payload/server/public/assets/play-D6OAQVJ_.js +1 -0
  65. package/payload/server/public/assets/play-D6OAQVJ_.js.br +0 -0
  66. package/payload/server/public/assets/play-D6OAQVJ_.js.gz +0 -0
  67. package/payload/server/public/assets/{public-BmA3-jLj.js → public-B-X1Cxfw.js} +1 -1
  68. package/payload/server/public/assets/public-B-X1Cxfw.js.br +0 -0
  69. package/payload/server/public/assets/public-B-X1Cxfw.js.gz +0 -0
  70. package/payload/server/public/assets/{rotate-ccw-fCH0LSMq.js → rotate-ccw-DVFvQAHG.js} +1 -1
  71. package/payload/server/public/assets/rotate-ccw-DVFvQAHG.js.br +0 -0
  72. package/payload/server/public/assets/rotate-ccw-DVFvQAHG.js.gz +0 -0
  73. package/payload/server/public/assets/{routines-BeFrmkls.js → routines-CgkkGJaS.js} +1 -1
  74. package/payload/server/public/assets/routines-CgkkGJaS.js.br +0 -0
  75. package/payload/server/public/assets/routines-CgkkGJaS.js.gz +0 -0
  76. package/payload/server/public/assets/{skills-mjls5HBa.js → skills-CW7gVcaG.js} +1 -1
  77. package/payload/server/public/assets/skills-CW7gVcaG.js.br +0 -0
  78. package/payload/server/public/assets/skills-CW7gVcaG.js.gz +0 -0
  79. package/payload/server/public/assets/{tasks-p6puHHD0.js → tasks-Ddjp0Rwh.js} +1 -1
  80. package/payload/server/public/assets/tasks-Ddjp0Rwh.js.br +0 -0
  81. package/payload/server/public/assets/tasks-Ddjp0Rwh.js.gz +0 -0
  82. package/payload/server/public/assets/{triangle-alert-CrZ1ewbd.js → triangle-alert-DUrUutX4.js} +1 -1
  83. package/payload/server/public/assets/triangle-alert-DUrUutX4.js.br +0 -0
  84. package/payload/server/public/assets/triangle-alert-DUrUutX4.js.gz +0 -0
  85. package/payload/server/public/assets/{useCopyFeedback-zOQNrIwd.js → useCopyFeedback-ClVINDrQ.js} +1 -1
  86. package/payload/server/public/assets/useCopyFeedback-ClVINDrQ.js.br +0 -0
  87. package/payload/server/public/assets/useCopyFeedback-ClVINDrQ.js.gz +0 -0
  88. package/payload/server/public/assets/{useMediaQuery-BmG090FR.js → useMediaQuery-BUFkZYRs.js} +1 -1
  89. package/payload/server/public/assets/useMediaQuery-BUFkZYRs.js.br +0 -0
  90. package/payload/server/public/assets/{useMediaQuery-BmG090FR.js.gz → useMediaQuery-BUFkZYRs.js.gz} +0 -0
  91. package/payload/server/public/assets/{useVoiceRecorder-9pf6FFJ-.js → useVoiceRecorder-DnyqQFet.js} +1 -1
  92. package/payload/server/public/assets/useVoiceRecorder-DnyqQFet.js.br +0 -0
  93. package/payload/server/public/assets/useVoiceRecorder-DnyqQFet.js.gz +0 -0
  94. package/payload/server/public/assets/{wrench-k4GYG5rc.js → wrench-BW28s_d0.js} +1 -1
  95. package/payload/server/public/assets/wrench-BW28s_d0.js.br +0 -0
  96. package/payload/server/public/assets/wrench-BW28s_d0.js.gz +0 -0
  97. package/payload/server/public/browser.html +3 -3
  98. package/payload/server/public/calendar.html +6 -6
  99. package/payload/server/public/chat.html +12 -12
  100. package/payload/server/public/data.html +10 -10
  101. package/payload/server/public/graph.html +8 -8
  102. package/payload/server/public/index.html +12 -12
  103. package/payload/server/public/operator.html +13 -13
  104. package/payload/server/public/public.html +12 -12
  105. package/payload/server/public/routines.html +6 -6
  106. package/payload/server/public/skills.html +4 -4
  107. package/payload/server/public/tasks.html +5 -5
  108. package/payload/server/server.js +520 -272
  109. package/payload/platform/plugins/dispatch/mcp/dist/scripts/converge-visit-instants.d.ts +0 -6
  110. package/payload/platform/plugins/dispatch/mcp/dist/scripts/converge-visit-instants.d.ts.map +0 -1
  111. package/payload/platform/plugins/dispatch/mcp/dist/scripts/converge-visit-instants.js +0 -102
  112. package/payload/platform/plugins/dispatch/mcp/dist/scripts/converge-visit-instants.js.map +0 -1
  113. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/converge-routine-description.test.d.ts +0 -2
  114. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/converge-routine-description.test.d.ts.map +0 -1
  115. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/converge-routine-description.test.js +0 -87
  116. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/converge-routine-description.test.js.map +0 -1
  117. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/converge-startdate-utc.test.d.ts +0 -2
  118. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/converge-startdate-utc.test.d.ts.map +0 -1
  119. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/converge-startdate-utc.test.js +0 -79
  120. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/converge-startdate-utc.test.js.map +0 -1
  121. package/payload/platform/plugins/scheduling/mcp/dist/scripts/converge-routine-description.d.ts +0 -35
  122. package/payload/platform/plugins/scheduling/mcp/dist/scripts/converge-routine-description.d.ts.map +0 -1
  123. package/payload/platform/plugins/scheduling/mcp/dist/scripts/converge-routine-description.js +0 -56
  124. package/payload/platform/plugins/scheduling/mcp/dist/scripts/converge-routine-description.js.map +0 -1
  125. package/payload/platform/plugins/scheduling/mcp/dist/scripts/converge-startdate-utc.d.ts +0 -24
  126. package/payload/platform/plugins/scheduling/mcp/dist/scripts/converge-startdate-utc.d.ts.map +0 -1
  127. package/payload/platform/plugins/scheduling/mcp/dist/scripts/converge-startdate-utc.js +0 -87
  128. package/payload/platform/plugins/scheduling/mcp/dist/scripts/converge-startdate-utc.js.map +0 -1
  129. package/payload/server/public/assets/AdminLoginScreens-BLGZEaKU.js.br +0 -0
  130. package/payload/server/public/assets/AdminLoginScreens-BLGZEaKU.js.gz +0 -0
  131. package/payload/server/public/assets/AdminShell-C_M7YE5Z.js.br +0 -0
  132. package/payload/server/public/assets/AdminShell-C_M7YE5Z.js.gz +0 -0
  133. package/payload/server/public/assets/activity-BL6qbB9a.js.br +0 -0
  134. package/payload/server/public/assets/activity-BL6qbB9a.js.gz +0 -0
  135. package/payload/server/public/assets/admin-jnJ3P2u4.js.br +0 -0
  136. package/payload/server/public/assets/admin-jnJ3P2u4.js.gz +0 -0
  137. package/payload/server/public/assets/agents-BzhfKZDN.js.br +0 -0
  138. package/payload/server/public/assets/agents-BzhfKZDN.js.gz +0 -0
  139. package/payload/server/public/assets/browser-C-6io2B9.js.br +0 -0
  140. package/payload/server/public/assets/browser-C-6io2B9.js.gz +0 -0
  141. package/payload/server/public/assets/calendar-Dt3gS88s.js.br +0 -0
  142. package/payload/server/public/assets/calendar-Dt3gS88s.js.gz +0 -0
  143. package/payload/server/public/assets/chat-IUOvVkL-.js +0 -1
  144. package/payload/server/public/assets/chat-IUOvVkL-.js.br +0 -1
  145. package/payload/server/public/assets/chat-IUOvVkL-.js.gz +0 -0
  146. package/payload/server/public/assets/chevron-left-DSzR9zR7.js +0 -1
  147. package/payload/server/public/assets/chevron-left-DSzR9zR7.js.br +0 -0
  148. package/payload/server/public/assets/clock-C5xE5p2I.js +0 -1
  149. package/payload/server/public/assets/clock-C5xE5p2I.js.br +0 -0
  150. package/payload/server/public/assets/copy-TkPJgllS.js.br +0 -0
  151. package/payload/server/public/assets/copy-TkPJgllS.js.gz +0 -0
  152. package/payload/server/public/assets/data-Bhv0Pvfl.js +0 -1
  153. package/payload/server/public/assets/data-Bhv0Pvfl.js.br +0 -0
  154. package/payload/server/public/assets/data-Bhv0Pvfl.js.gz +0 -0
  155. package/payload/server/public/assets/file-text-DyAL2DCh.js.br +0 -0
  156. package/payload/server/public/assets/file-text-DyAL2DCh.js.gz +0 -0
  157. package/payload/server/public/assets/graph-PiNIcCfX.js.br +0 -0
  158. package/payload/server/public/assets/graph-PiNIcCfX.js.gz +0 -0
  159. package/payload/server/public/assets/graph-labels-8jKFrLHn.js.br +0 -0
  160. package/payload/server/public/assets/graph-labels-8jKFrLHn.js.gz +0 -0
  161. package/payload/server/public/assets/operator-DKGKGzrb.js.br +0 -0
  162. package/payload/server/public/assets/operator-DKGKGzrb.js.gz +0 -0
  163. package/payload/server/public/assets/page-BU5vlY9Y.js.br +0 -0
  164. package/payload/server/public/assets/page-BU5vlY9Y.js.gz +0 -0
  165. package/payload/server/public/assets/page-DXz_K10G.js.br +0 -0
  166. package/payload/server/public/assets/page-DXz_K10G.js.gz +0 -0
  167. package/payload/server/public/assets/play-CmSyYrB6.js +0 -1
  168. package/payload/server/public/assets/play-CmSyYrB6.js.br +0 -0
  169. package/payload/server/public/assets/play-CmSyYrB6.js.gz +0 -0
  170. package/payload/server/public/assets/public-BmA3-jLj.js.br +0 -0
  171. package/payload/server/public/assets/public-BmA3-jLj.js.gz +0 -0
  172. package/payload/server/public/assets/rotate-ccw-fCH0LSMq.js.br +0 -0
  173. package/payload/server/public/assets/rotate-ccw-fCH0LSMq.js.gz +0 -0
  174. package/payload/server/public/assets/routines-BeFrmkls.js.br +0 -0
  175. package/payload/server/public/assets/routines-BeFrmkls.js.gz +0 -0
  176. package/payload/server/public/assets/skills-mjls5HBa.js.br +0 -0
  177. package/payload/server/public/assets/skills-mjls5HBa.js.gz +0 -0
  178. package/payload/server/public/assets/tasks-p6puHHD0.js.br +0 -0
  179. package/payload/server/public/assets/tasks-p6puHHD0.js.gz +0 -0
  180. package/payload/server/public/assets/triangle-alert-CrZ1ewbd.js.br +0 -0
  181. package/payload/server/public/assets/triangle-alert-CrZ1ewbd.js.gz +0 -0
  182. package/payload/server/public/assets/useCopyFeedback-zOQNrIwd.js.br +0 -0
  183. package/payload/server/public/assets/useCopyFeedback-zOQNrIwd.js.gz +0 -0
  184. package/payload/server/public/assets/useMediaQuery-BmG090FR.js.br +0 -0
  185. package/payload/server/public/assets/useVoiceRecorder-9pf6FFJ-.js.br +0 -0
  186. package/payload/server/public/assets/useVoiceRecorder-9pf6FFJ-.js.gz +0 -0
  187. package/payload/server/public/assets/wrench-k4GYG5rc.js.br +0 -0
  188. package/payload/server/public/assets/wrench-k4GYG5rc.js.gz +0 -0
@@ -471,6 +471,16 @@ var init_agent_errors = __esm({
471
471
  });
472
472
 
473
473
  // app/lib/reader/parse-transcript.ts
474
+ function emptyEnvelopeCensus() {
475
+ return {
476
+ rows: 0,
477
+ wrapped: 0,
478
+ nested: 0,
479
+ stripped: 0,
480
+ unstripped: 0,
481
+ sources: { whatsapp: 0, webchat: 0, telegram: 0, none: 0 }
482
+ };
483
+ }
474
484
  function isCompactSummaryRow(row, content) {
475
485
  return row.isCompactSummary === true || content !== null && content.startsWith(COMPACT_SUMMARY_PREFIX);
476
486
  }
@@ -481,10 +491,11 @@ function isScheduleSource(text) {
481
491
  }
482
492
  function unwrapChannel(text) {
483
493
  const m = text.match(CHANNEL_WRAPPER);
484
- if (!m) return { text };
494
+ if (!m) return { text, wrapped: false, nested: false };
485
495
  let source = SOURCE_MAP[m[1].match(SOURCE_ATTR)?.[1] ?? ""];
486
496
  let inner = m[2];
487
497
  const im = inner.match(CHANNEL_WRAPPER);
498
+ const nested = im !== null;
488
499
  if (im) {
489
500
  const innerSource = SOURCE_MAP[im[1].match(SOURCE_ATTR)?.[1] ?? ""];
490
501
  if (innerSource) {
@@ -492,7 +503,7 @@ function unwrapChannel(text) {
492
503
  inner = im[2];
493
504
  }
494
505
  }
495
- return source ? { text: inner, source } : { text: inner };
506
+ return source ? { text: inner, source, wrapped: true, nested } : { text: inner, wrapped: true, nested };
496
507
  }
497
508
  function splitReplyNote(payload) {
498
509
  const m = payload.match(REPLY_NOTE);
@@ -508,7 +519,17 @@ function uploadCountOf(text) {
508
519
  const n = Number(text.match(UPLOAD_COUNT)?.[1]);
509
520
  return Number.isInteger(n) && n > 0 ? n : void 0;
510
521
  }
511
- function operatorInboundTurn(u, ts, uuid) {
522
+ function noteEnvelope(census, u) {
523
+ if (!census) return;
524
+ census.rows++;
525
+ if (u.wrapped) census.wrapped++;
526
+ if (u.nested) census.nested++;
527
+ if (u.text.startsWith("<channel ")) census.unstripped++;
528
+ else if (u.wrapped) census.stripped++;
529
+ census.sources[u.source ?? "none"]++;
530
+ }
531
+ function operatorInboundTurn(u, ts, uuid, census) {
532
+ noteEnvelope(census, u);
512
533
  const framed = rawFromComposedAdminFrame(u.text);
513
534
  const split = framed === void 0 ? void 0 : splitReplyNote(framed);
514
535
  const rawBody = split?.body;
@@ -635,12 +656,12 @@ function lastTurnTs(turns) {
635
656
  function isModelGated(turns) {
636
657
  return turns.some((t) => t.kind === "agent-error" && t.code === "model_unavailable");
637
658
  }
638
- function renderOrSuppressChannelInbound(u, ts, out, queuedPendingSuppress, uuid) {
659
+ function renderOrSuppressChannelInbound(u, ts, out, queuedPendingSuppress, uuid, census) {
639
660
  const armed2 = queuedPendingSuppress.get(u.text) ?? 0;
640
661
  if (armed2 > 0) {
641
662
  queuedPendingSuppress.set(u.text, armed2 - 1);
642
663
  } else {
643
- out.push(operatorInboundTurn(u, ts, uuid));
664
+ out.push(operatorInboundTurn(u, ts, uuid, census));
644
665
  }
645
666
  }
646
667
  function parseTranscript(lines, queuedPendingSuppress = /* @__PURE__ */ new Map(), turnCtx = {}) {
@@ -678,7 +699,7 @@ function parseTranscript(lines, queuedPendingSuppress = /* @__PURE__ */ new Map(
678
699
  const u = unwrapChannel(content);
679
700
  turnCtx.lastChannelSource = u.source;
680
701
  if (!isScheduleSource(content)) {
681
- out.push(operatorInboundTurn(u, ts));
702
+ out.push(operatorInboundTurn(u, ts, void 0, turnCtx.envelopeCensus));
682
703
  queuedPendingSuppress.set(u.text, (queuedPendingSuppress.get(u.text) ?? 0) + 1);
683
704
  }
684
705
  }
@@ -691,7 +712,7 @@ function parseTranscript(lines, queuedPendingSuppress = /* @__PURE__ */ new Map(
691
712
  const u = unwrapChannel(att.prompt);
692
713
  turnCtx.lastChannelSource = u.source;
693
714
  if (!isScheduleSource(att.prompt)) {
694
- renderOrSuppressChannelInbound(u, ts, out, queuedPendingSuppress, asString(row.uuid) ?? void 0);
715
+ renderOrSuppressChannelInbound(u, ts, out, queuedPendingSuppress, asString(row.uuid) ?? void 0, turnCtx.envelopeCensus);
695
716
  }
696
717
  }
697
718
  continue;
@@ -720,7 +741,7 @@ function parseTranscript(lines, queuedPendingSuppress = /* @__PURE__ */ new Map(
720
741
  const u = unwrapChannel(text);
721
742
  turnCtx.lastChannelSource = u.source;
722
743
  if (!isScheduleSource(text)) {
723
- renderOrSuppressChannelInbound(u, ts, out, queuedPendingSuppress, asString(row.uuid) ?? void 0);
744
+ renderOrSuppressChannelInbound(u, ts, out, queuedPendingSuppress, asString(row.uuid) ?? void 0, turnCtx.envelopeCensus);
724
745
  }
725
746
  } else if (row.isMeta === true) {
726
747
  out.push({ kind: "harness-internal", text, ts });
@@ -747,7 +768,7 @@ var init_parse_transcript = __esm({
747
768
  HARNESS_LIFECYCLE_PREFIXES = ["<task-notification>"];
748
769
  CC_IMAGE_VISION_HINT = /^\[Image:[^\]]*Multiply coordinates by [\d.]+ to map to original image\.\]$/;
749
770
  COMPACT_SUMMARY_PREFIX = "This session is being continued from a previous conversation that ran out of context.";
750
- CHANNEL_WRAPPER = /^<channel\b([^>]*)>\n?([\s\S]*?)\n?<\/channel>$/;
771
+ CHANNEL_WRAPPER = /^<channel\b([^>]*)>\n?([\s\S]*?)\n?<\\?\/channel>$/;
751
772
  SOURCE_ATTR = /\bsource="([^"]+)"/;
752
773
  SOURCE_MAP = {
753
774
  whatsapp: "whatsapp",
@@ -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
  });
@@ -5460,9 +5527,186 @@ import { homedir as homedir5 } from "os";
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
  }
@@ -6466,7 +6782,7 @@ function writeOAuthCredentials(tokens) {
6466
6782
  try {
6467
6783
  let fileData = {};
6468
6784
  try {
6469
- const rawFile = readFileSync2(CLAUDE_CREDENTIALS_FILE, "utf-8");
6785
+ const rawFile = readFileSync3(CLAUDE_CREDENTIALS_FILE, "utf-8");
6470
6786
  fileData = JSON.parse(rawFile);
6471
6787
  } catch {
6472
6788
  }
@@ -6694,7 +7010,7 @@ init_helpers();
6694
7010
  // server/routes/health.ts
6695
7011
  init_dist();
6696
7012
  init_paths();
6697
- import { existsSync as existsSync2, readFileSync as readFileSync4 } from "fs";
7013
+ import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
6698
7014
  import { createConnection as createConnection2 } from "net";
6699
7015
 
6700
7016
  // app/lib/network.ts
@@ -6719,11 +7035,11 @@ function getLanIp() {
6719
7035
  init_paths();
6720
7036
  import { spawnSync, execFileSync } from "child_process";
6721
7037
  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";
7038
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync4, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
7039
+ import { resolve as resolve4 } from "path";
6724
7040
  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");
7041
+ var PLATFORM_ROOT2 = process.env.MAXY_PLATFORM_ROOT ?? resolve4(process.cwd(), "..");
7042
+ var VNC_SCRIPT = resolve4(PLATFORM_ROOT2, "scripts/vnc.sh");
6727
7043
  var displayMode = process.env.DISPLAY_MODE ?? "virtual";
6728
7044
  if (displayMode === "native") {
6729
7045
  console.log(`[vnc] DISPLAY_MODE=native \u2014 local requests use desktop display, remote requests use VNC`);
@@ -6778,7 +7094,7 @@ function discoverNativeDisplay() {
6778
7094
  const leaderPid = leaderResult.stdout?.trim();
6779
7095
  if (leaderPid) {
6780
7096
  try {
6781
- const environ = readFileSync3(`/proc/${leaderPid}/environ`, "utf8");
7097
+ const environ = readFileSync4(`/proc/${leaderPid}/environ`, "utf8");
6782
7098
  const match = environ.split("\0").find((e) => e.startsWith("WAYLAND_DISPLAY="));
6783
7099
  if (match) waylandDisplay = match.split("=")[1];
6784
7100
  } catch {
@@ -6833,10 +7149,10 @@ async function waitForPort(port2, timeoutMs = 12e3) {
6833
7149
  return false;
6834
7150
  }
6835
7151
  function ensureLogDir() {
6836
- mkdirSync(LOG_DIR, { recursive: true });
7152
+ mkdirSync2(LOG_DIR, { recursive: true });
6837
7153
  }
6838
7154
  function logPath(name) {
6839
- return resolve3(LOG_DIR, `${name}.log`);
7155
+ return resolve4(LOG_DIR, `${name}.log`);
6840
7156
  }
6841
7157
  async function ensureVnc() {
6842
7158
  if (process.platform !== "linux") {
@@ -6921,19 +7237,19 @@ async function ensureCdp(transport = "vnc") {
6921
7237
  function killChromium() {
6922
7238
  spawnSync("pkill", ["-f", "chromium"], { stdio: "pipe" });
6923
7239
  }
6924
- var AUTH_TARGET_ID_FILE = resolve3(LOG_DIR, "claude-auth-target.id");
7240
+ var AUTH_TARGET_ID_FILE = resolve4(LOG_DIR, "claude-auth-target.id");
6925
7241
  function readAuthTargetId() {
6926
7242
  try {
6927
- const id = readFileSync3(AUTH_TARGET_ID_FILE, "utf8").trim();
7243
+ const id = readFileSync4(AUTH_TARGET_ID_FILE, "utf8").trim();
6928
7244
  return id.length > 0 ? id : null;
6929
7245
  } catch {
6930
7246
  return null;
6931
7247
  }
6932
7248
  }
6933
7249
  function writeChromiumWrapper() {
6934
- mkdirSync(BIN_DIR, { recursive: true });
7250
+ mkdirSync2(BIN_DIR, { recursive: true });
6935
7251
  rmSync2(AUTH_TARGET_ID_FILE, { force: true });
6936
- const wrapperPath = resolve3(BIN_DIR, "chromium");
7252
+ const wrapperPath = resolve4(BIN_DIR, "chromium");
6937
7253
  writeFileSync3(wrapperPath, `#!/bin/bash
6938
7254
  # Generated by writeChromiumWrapper (app/lib/vnc.ts) \u2014 Task 1562.
6939
7255
  # Hands its URL argument to the brand's running Chromium over CDP.
@@ -7035,8 +7351,8 @@ app.get("/", async (c) => {
7035
7351
  const browserTransport = resolveBrowserTransport(c.req.raw, c.env?.incoming?.socket?.remoteAddress);
7036
7352
  let pinConfigured = false;
7037
7353
  try {
7038
- if (existsSync2(USERS_FILE)) {
7039
- const raw = readFileSync4(USERS_FILE, "utf-8").trim();
7354
+ if (existsSync3(USERS_FILE)) {
7355
+ const raw = readFileSync5(USERS_FILE, "utf-8").trim();
7040
7356
  if (raw) {
7041
7357
  const users = JSON.parse(raw);
7042
7358
  pinConfigured = Array.isArray(users) && users.length > 0;
@@ -7529,183 +7845,6 @@ init_dist();
7529
7845
  init_channel_key();
7530
7846
  import { statSync as statSync7 } from "fs";
7531
7847
  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
7848
  init_admin_session_id();
7710
7849
  init_normalize();
7711
7850
 
@@ -8288,6 +8427,32 @@ async function sendTelegramText(botToken, chatId, text) {
8288
8427
  }
8289
8428
  }
8290
8429
 
8430
+ // app/lib/telegram/outbound/send-chat-action.ts
8431
+ async function sendTelegramChatAction(botToken, chatId, key2, site) {
8432
+ const report = (ok2, reason) => console.error(
8433
+ `[telegram-native] op=chat-action key=${key2} site=${site} ok=${ok2}${ok2 ? "" : ` reason=${reason}`}`
8434
+ );
8435
+ try {
8436
+ const res = await fetch(`https://api.telegram.org/bot${botToken}/sendChatAction`, {
8437
+ method: "POST",
8438
+ headers: { "Content-Type": "application/json" },
8439
+ body: JSON.stringify({ chat_id: chatId, action: "typing" })
8440
+ });
8441
+ const data = await res.json();
8442
+ if (data.ok) {
8443
+ report(true);
8444
+ return { ok: true };
8445
+ }
8446
+ const error = data.description ?? "chat action failed";
8447
+ report(false, error);
8448
+ return { ok: false, error };
8449
+ } catch (e) {
8450
+ const error = e instanceof Error ? e.message : String(e);
8451
+ report(false, error);
8452
+ return { ok: false, error };
8453
+ }
8454
+ }
8455
+
8291
8456
  // server/routes/whatsapp.ts
8292
8457
  init_dist();
8293
8458
  import { basename as basename3, join as join17, resolve as resolve8 } from "path";
@@ -15549,6 +15714,7 @@ function pickActivity(entries2, grown, nowMs, concurrencyWindowMs) {
15549
15714
  }
15550
15715
 
15551
15716
  // server/routes/session-reader.ts
15717
+ init_session_store();
15552
15718
  init_dist();
15553
15719
  var app7 = new Hono();
15554
15720
  function probeDataRef(path) {
@@ -15724,6 +15890,7 @@ data: ${JSON.stringify(turn)}
15724
15890
 
15725
15891
  `));
15726
15892
  const connId = `${sessionId.slice(0, 8)}-${process.hrtime.bigint().toString(36).slice(-6)}`;
15893
+ const censusAccountId = getAccountIdForSession(c.get("cacheKey") ?? "") ?? "unresolved";
15727
15894
  const startedAt = Date.now();
15728
15895
  const lastEventId = c.req.header("Last-Event-ID");
15729
15896
  let teardown = () => {
@@ -15759,7 +15926,8 @@ data: ${JSON.stringify(obj)}
15759
15926
  log: (line) => console.log(line)
15760
15927
  });
15761
15928
  const queuedPendingSuppress = /* @__PURE__ */ new Map();
15762
- const turnCtx = {};
15929
+ const envelopeCensus = emptyEnvelopeCensus();
15930
+ const turnCtx = { envelopeCensus };
15763
15931
  const drain = () => {
15764
15932
  try {
15765
15933
  const { buf, end } = readFrom(jsonlPath, offset);
@@ -15776,6 +15944,7 @@ data: ${JSON.stringify(obj)}
15776
15944
  n++;
15777
15945
  }
15778
15946
  if (n > 0) {
15947
+ foldFrameCensus(censusAccountId, sessionId, envelopeCensus);
15779
15948
  actx.parentGrewThisTick = true;
15780
15949
  console.log(`[wa-stream] op=drain conn=${connId} turns=${n}`);
15781
15950
  }
@@ -15822,6 +15991,7 @@ data: ${JSON.stringify({ from: alignedFrom, fileEnd: fileEnd0 })}
15822
15991
  }
15823
15992
  n++;
15824
15993
  }
15994
+ foldFrameCensus(censusAccountId, sessionId, envelopeCensus);
15825
15995
  console.log(
15826
15996
  `[wa-stream] op=backlog conn=${connId} turns=${n} bytes=${end - alignedFrom} from=${alignedFrom}`
15827
15997
  );
@@ -16005,7 +16175,7 @@ import { basename as basename8, dirname as dirname9, join as join26 } from "path
16005
16175
  // app/lib/reader/operator-conversations.ts
16006
16176
  function operatorConvName(row) {
16007
16177
  if (row.isGroup === true) return row.chatName ?? row.title;
16008
- const name = row.operatorName ?? row.chatName ?? row.whatsappName;
16178
+ const name = row.operatorName ?? row.chatName ?? row.whatsappName ?? row.telegramName;
16009
16179
  const senderId = row.senderId != null && row.channel === "whatsapp" ? formatSenderPhone(row.senderId) : row.senderId;
16010
16180
  if (name) return senderId ? `${name} ${senderId}` : name;
16011
16181
  return senderId ?? row.title;
@@ -16015,6 +16185,7 @@ function operatorConvNameSource(row) {
16015
16185
  if (row.operatorName) return "operator";
16016
16186
  if (row.chatName) return "chatmeta";
16017
16187
  if (row.whatsappName) return "inbound-pushname";
16188
+ if (row.telegramName) return "telegram-sender-name";
16018
16189
  if (row.senderId != null) return "counterparty-number";
16019
16190
  return "jid";
16020
16191
  }
@@ -16234,6 +16405,14 @@ app8.get("/conversations", requireAdminSession, async (c) => {
16234
16405
  );
16235
16406
  const adminScope = allStoreRows.filter((r) => r.scope === "admin").length;
16236
16407
  const publicScope = allStoreRows.length - adminScope;
16408
+ const telegramStoreRows = account ? buildTelegramStoreConversationRows(account.accountId, readConversationSummaries2(account.accountId)) : [];
16409
+ const dedupedSessionRows = dropTelegramSessionDuplicates(sessionConversations, telegramStoreRows);
16410
+ const telegramRows = [
16411
+ ...dedupedSessionRows.filter((r) => r.channel === "telegram"),
16412
+ ...telegramStoreRows
16413
+ ].sort((a, b) => (b.lastMessageAt ?? "").localeCompare(a.lastMessageAt ?? ""));
16414
+ const otherSessionRows = dedupedSessionRows.filter((r) => r.channel !== "telegram");
16415
+ const conversations = [...otherSessionRows, ...allStoreRows, ...telegramRows];
16237
16416
  const census = buildLabelCensus(allStoreRows, selfPushNames(summaries));
16238
16417
  for (const f of fallbackLabelSources(allStoreRows)) {
16239
16418
  console.log(`[wa-label] op=row-label jid=${f.jid} source=${f.source} route=conversations`);
@@ -16241,11 +16420,11 @@ app8.get("/conversations", requireAdminSession, async (c) => {
16241
16420
  console.log(
16242
16421
  `[wa-label-census] route=conversations chats=${census.chats} withMeta=${census.withMeta} groups=${census.groups} groupsWithSubject=${census.groupsWithSubject} labelledByMember=${census.labelledByMember} dms=${census.dms} namedDms=${census.namedDms} labelledBySelfPushName=${census.labelledBySelfPushName}`
16243
16422
  );
16244
- const sessionRowCount = sessionConversations.length;
16423
+ const sessionRowCount = dedupedSessionRows.length;
16245
16424
  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}`
16425
+ `[wa-reader] op=list pinnedAccount=${scopeAccountId?.slice(0, 8) ?? "null"} resolvedAccount=${account?.accountId.slice(0, 8) ?? "null"} multiAccount=${multiAccount} conversations=${conversations.length} sessionRows=${sessionRowCount} storeRows=${allStoreRows.length} storeRowsByChannel=whatsapp:${allStoreRows.length},telegram:${telegramStoreRows.length} adminScope=${adminScope} publicScope=${publicScope} sessionRowsExcludedUntagged=${sessionRowsExcludedUntagged} houseResolved=${houseId ? houseId.slice(0, 8) : "null"} managerPhonesBound=${managerPhonesBound} managedStoreRows=${managedRows.length}`
16247
16426
  );
16248
- return c.json({ conversations: [...sessionConversations, ...allStoreRows] });
16427
+ return c.json({ conversations });
16249
16428
  });
16250
16429
  function tagAndMergeInstallConversations(perAccount) {
16251
16430
  const all = [];
@@ -16256,7 +16435,7 @@ function tagAndMergeInstallConversations(perAccount) {
16256
16435
  }
16257
16436
  function dropHouseRowsOwnedBySubAccount(rows, subManagerPhones) {
16258
16437
  return rows.filter((r) => {
16259
- if (r.isHouse !== true || r.source !== "store" || r.senderId == null) return true;
16438
+ if (r.channel !== "whatsapp" || r.isHouse !== true || r.source !== "store" || r.senderId == null) return true;
16260
16439
  let owner = null;
16261
16440
  for (const [subId, phones] of subManagerPhones) {
16262
16441
  if (counterpartyBoundToSubAccount(phones, r.senderId)) {
@@ -16396,7 +16575,14 @@ app8.get("/conversations-all", requireAdminSession, async (c) => {
16396
16575
  readConversationSummaries(account.accountId),
16397
16576
  readChatMetadata(account.accountId)
16398
16577
  );
16399
- perAccount.push({ isHouse, rows: [...named, ...storeRows] });
16578
+ const telegramStoreRows = buildTelegramStoreConversationRows(
16579
+ account.accountId,
16580
+ readConversationSummaries2(account.accountId)
16581
+ );
16582
+ perAccount.push({
16583
+ isHouse,
16584
+ rows: [...dropTelegramSessionDuplicates(named, telegramStoreRows), ...storeRows, ...telegramStoreRows]
16585
+ });
16400
16586
  }
16401
16587
  const houseAccount = validAccounts.find((a) => a.accountId === houseId) ?? null;
16402
16588
  const subManagerPhones = /* @__PURE__ */ new Map();
@@ -16414,16 +16600,19 @@ app8.get("/conversations-all", requireAdminSession, async (c) => {
16414
16600
  for (const account of validAccounts) {
16415
16601
  for (const [jid, s] of readConversationSummaries(account.accountId)) allSummaries.set(`${account.accountId}:${jid}`, s);
16416
16602
  }
16417
- const census = buildLabelCensus(conversations, selfPushNames(allSummaries));
16418
- for (const f of fallbackLabelSources(conversations)) {
16603
+ const whatsappRows = conversations.filter((r) => r.channel === "whatsapp");
16604
+ const census = buildLabelCensus(whatsappRows, selfPushNames(allSummaries));
16605
+ for (const f of fallbackLabelSources(whatsappRows)) {
16419
16606
  console.log(`[wa-label] op=row-label jid=${f.jid} source=${f.source} route=conversations-all`);
16420
16607
  }
16421
16608
  console.log(
16422
16609
  `[wa-label-census] route=conversations-all chats=${census.chats} withMeta=${census.withMeta} groups=${census.groups} groupsWithSubject=${census.groupsWithSubject} labelledByMember=${census.labelledByMember} dms=${census.dms} namedDms=${census.namedDms} labelledBySelfPushName=${census.labelledBySelfPushName}`
16423
16610
  );
16424
16611
  const houseRows = conversations.filter((r) => r.isHouse).length;
16612
+ const storeByChannel = conversations.filter((r) => r.source === "store");
16613
+ const waStoreRows = storeByChannel.filter((r) => r.channel === "whatsapp").length;
16425
16614
  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}`
16615
+ `[wa-reader] op=conversations-all accounts=${validAccounts.length} rows=${conversations.length} house=${houseRows} sub=${conversations.length - houseRows} deduped=${merged.length - conversations.length} storeRowsByChannel=whatsapp:${waStoreRows},telegram:${storeByChannel.length - waStoreRows}`
16427
16616
  );
16428
16617
  return { conversations };
16429
16618
  });
@@ -38129,21 +38318,22 @@ function startNativeFileFollower(input) {
38129
38318
  }
38130
38319
 
38131
38320
  // app/lib/channel-delivery/follower-registry.ts
38132
- function createFollowerRegistry(start) {
38321
+ function createFollowerRegistry(start, keyOf2 = (p) => p.senderId) {
38133
38322
  const controllers = /* @__PURE__ */ new Map();
38134
38323
  const params = /* @__PURE__ */ new Map();
38135
38324
  return {
38136
38325
  attach(p) {
38137
- controllers.get(p.senderId)?.abort();
38138
- params.set(p.senderId, p);
38326
+ const doorId = keyOf2(p);
38327
+ controllers.get(doorId)?.abort();
38328
+ params.set(doorId, p);
38139
38329
  let ac;
38140
38330
  ac = start(p, () => {
38141
- if (controllers.get(p.senderId) === ac) controllers.delete(p.senderId);
38331
+ if (controllers.get(doorId) === ac) controllers.delete(doorId);
38142
38332
  });
38143
- controllers.set(p.senderId, ac);
38333
+ controllers.set(doorId, ac);
38144
38334
  },
38145
38335
  live: () => [...controllers.keys()],
38146
- paramsFor: (senderId) => params.get(senderId) ?? null
38336
+ paramsFor: (doorId) => params.get(doorId) ?? null
38147
38337
  };
38148
38338
  }
38149
38339
 
@@ -38156,41 +38346,51 @@ var id82 = (s) => s.slice(0, 8);
38156
38346
  function followerCensusTick(deps, state = makeCensusState()) {
38157
38347
  const bound = deps.bound();
38158
38348
  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));
38349
+ const label = deps.label ?? id82;
38350
+ for (const doorId of live) state.attempts.delete(doorId);
38351
+ const missing = bound.filter((doorId) => !live.has(doorId));
38161
38352
  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"}`
38353
+ `[channel-follower] op=follower-census channel=${deps.channel} bound=${bound.length} attached=${live.size} missing=${missing.length} missingSenders=${missing.map(label).join(",") || "none"}`
38163
38354
  );
38164
- for (const senderId of missing) {
38165
- const used = state.attempts.get(senderId) ?? 0;
38355
+ for (const doorId of missing) {
38356
+ const used = state.attempts.get(doorId) ?? 0;
38166
38357
  if (used >= CENSUS_MAX_REATTEMPTS) {
38167
38358
  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}`
38359
+ `[channel-follower] op=follower-reattach channel=${deps.channel} sessionId=${id82(deps.paramsFor(doorId)?.sessionId ?? "none")} senderId=${label(doorId)} outcome=skipped reason=reattach-exhausted attempts=${used}`
38169
38360
  );
38170
38361
  continue;
38171
38362
  }
38172
- const p = deps.paramsFor(senderId);
38363
+ const p = deps.paramsFor(doorId);
38173
38364
  if (!p) {
38174
38365
  console.error(
38175
- `[channel-follower] op=follower-reattach channel=${deps.channel} sessionId=none senderId=${id82(senderId)} outcome=failed reason=no-attach-params`
38366
+ `[channel-follower] op=follower-reattach channel=${deps.channel} sessionId=none senderId=${label(doorId)} outcome=failed reason=no-attach-params`
38176
38367
  );
38177
38368
  continue;
38178
38369
  }
38179
38370
  try {
38180
- state.attempts.set(senderId, used + 1);
38371
+ state.attempts.set(doorId, used + 1);
38181
38372
  deps.attach(p);
38182
38373
  console.error(
38183
- `[channel-follower] op=follower-reattach channel=${deps.channel} sessionId=${id82(p.sessionId)} senderId=${id82(p.senderId)} outcome=attached reason=none`
38374
+ `[channel-follower] op=follower-reattach channel=${deps.channel} sessionId=${id82(p.sessionId)} senderId=${label(doorId)} outcome=attached reason=none`
38184
38375
  );
38185
38376
  } catch (err) {
38186
38377
  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)}`
38378
+ `[channel-follower] op=follower-reattach channel=${deps.channel} sessionId=${id82(p.sessionId)} senderId=${label(doorId)} outcome=failed reason=${err instanceof Error ? err.message : String(err)}`
38188
38379
  );
38189
38380
  }
38190
38381
  }
38191
38382
  return `${deps.channel}: ${bound.length} door(s), ${missing.length} without a reader`;
38192
38383
  }
38193
38384
 
38385
+ // app/lib/channel-delivery/whatsapp-door.ts
38386
+ function whatsappDoorId(p) {
38387
+ return hubKey(p.effectiveAccountId, p.senderId);
38388
+ }
38389
+ function whatsappDoorLabel(doorId) {
38390
+ const [decoded] = decodeHubKeys([doorId]);
38391
+ return decoded ? `${id82(decoded.accountId)}:${id82(decoded.senderId)}` : id82(doorId);
38392
+ }
38393
+
38194
38394
  // app/lib/telegram/gateway/inbound-hub.ts
38195
38395
  var InboundHub3 = class {
38196
38396
  keys = /* @__PURE__ */ new Map();
@@ -38634,6 +38834,15 @@ var TelegramGateway = class {
38634
38834
  console.error(
38635
38835
  `[telegram-native] op=inbound key=${key2} botId=${input.botId} accountId=${input.accountId} role=${input.role} agent=${input.agentSlug} bytes=${bytes} source=${source} delivery=${willDeliverNow ? "immediate" : "queued"} sessionId=${resolvedSessionId}`
38636
38836
  );
38837
+ try {
38838
+ if (source === "user") {
38839
+ this.deps.sendChatAction?.({ botToken: input.botToken, chatId: input.chatId, key: key2 });
38840
+ }
38841
+ } catch (err) {
38842
+ console.error(
38843
+ `[telegram-native] op=chat-action key=${key2} site=inbound ok=false reason=${err instanceof Error ? err.message : String(err)}`
38844
+ );
38845
+ }
38637
38846
  if (!hadSubscriber && !this.spawning.has(key2)) {
38638
38847
  this.spawning.add(key2);
38639
38848
  console.error(`[telegram-native] op=spawn-trigger key=${key2} botId=${input.botId} role=${input.role}`);
@@ -38898,6 +39107,7 @@ function makeTelegramFileDelivery(entry2, botToken) {
38898
39107
  }
38899
39108
 
38900
39109
  // app/lib/telegram/gateway/native-file-follower.ts
39110
+ var CHAT_ACTION_REFRESH_MS = 4e3;
38901
39111
  function startTelegramNativeFileFollower(input) {
38902
39112
  const entry2 = {
38903
39113
  sessionId: input.sessionId,
@@ -38917,6 +39127,16 @@ function startTelegramNativeFileFollower(input) {
38917
39127
  // The chat id the document is addressed to (set per spawn from the inbound).
38918
39128
  replyTarget: input.chatId
38919
39129
  };
39130
+ let lastChatActionAt = Number.NEGATIVE_INFINITY;
39131
+ const onActivity = () => {
39132
+ const now = Date.now();
39133
+ if (now - lastChatActionAt < CHAT_ACTION_REFRESH_MS) return;
39134
+ lastChatActionAt = now;
39135
+ void sendTelegramChatAction(input.botToken, input.chatId, input.senderId, "activity");
39136
+ };
39137
+ const onTurnComplete = () => {
39138
+ lastChatActionAt = Number.NEGATIVE_INFINITY;
39139
+ };
38920
39140
  return startEmitter({
38921
39141
  entry: entry2,
38922
39142
  tag: "[telegram-adaptor]",
@@ -38930,6 +39150,8 @@ function startTelegramNativeFileFollower(input) {
38930
39150
  // A resumed session's JSONL already holds prior SendUserFile tool_uses;
38931
39151
  // suppress replay so historical files are not re-sent on attach.
38932
39152
  suppressResumeReplay: true,
39153
+ onActivity,
39154
+ onTurnComplete,
38933
39155
  // Task 2521 — the answer reaches every bound door through the one fan-out.
38934
39156
  // Ungated by design: whatever woke the turn, the reader and the chats show
38935
39157
  // the same thing.
@@ -39363,6 +39585,15 @@ function* walkTelegramSidecars() {
39363
39585
  }
39364
39586
  }
39365
39587
 
39588
+ // app/lib/telegram/gateway/origin-target.ts
39589
+ function answeringOriginTarget(send, key2, noteAnswered3) {
39590
+ if (!send) return null;
39591
+ return async (text) => {
39592
+ await send(text);
39593
+ noteAnswered3(key2);
39594
+ };
39595
+ }
39596
+
39366
39597
  // server/telegram-migrate.ts
39367
39598
  import { readFileSync as readFileSync53, writeFileSync as writeFileSync19, existsSync as existsSync53, renameSync as renameSync11, mkdirSync as mkdirSync11 } from "fs";
39368
39599
  import { dirname as dirname18, join as join63 } from "path";
@@ -39863,7 +40094,12 @@ var nativeFileFollowers = createFollowerRegistry(
39863
40094
  // delivered session 8e5bc4e8's answer to nothing.
39864
40095
  fanTargets: () => whatsappFanTargets(p.effectiveAccountId, p.senderId),
39865
40096
  onClose
39866
- })
40097
+ }),
40098
+ // Task 2569 — the door is (effective account, sender), the same pair the
40099
+ // reply map and the session id key on. Keyed on the sender alone, a second
40100
+ // account's spawn for the same phone aborted the first account's live reader
40101
+ // and overwrote its recorded parameters, and the census called that healthy.
40102
+ whatsappDoorId
39867
40103
  );
39868
40104
  var webchatFileFollowers = /* @__PURE__ */ new Map();
39869
40105
  async function fetchAccountStandingRules(accountId) {
@@ -40311,6 +40547,12 @@ var telegramGateway = new TelegramGateway({
40311
40547
  resolveSessionOverride: (accountId, channelKey) => resolveChannelOverride(accountId, "telegram", channelKey),
40312
40548
  // Task 2521 — hoisted above; see `telegramFanTargets`.
40313
40549
  fanTargetsFor: telegramFanTargets,
40550
+ // Task 2576 — the typing indicator. Injected here because the gateway is
40551
+ // contracted to hold no Bot API import; see the dep comment in
40552
+ // telegram-gateway.ts. Not awaited: an indicator never paces a turn.
40553
+ sendChatAction: ({ botToken, chatId, key: key2 }) => {
40554
+ void sendTelegramChatAction(botToken, chatId, key2, "inbound");
40555
+ },
40314
40556
  gatewayUrl: telegramGatewayUrl(),
40315
40557
  serverPath: telegramServerPath(),
40316
40558
  // Task 2368 — the role branch lives in its own module so it has a test that
@@ -40334,10 +40576,16 @@ registerLoop({
40334
40576
  followerCensusTick(
40335
40577
  {
40336
40578
  channel: "whatsapp",
40337
- bound: () => waGateway.boundTargets().map((t) => t.senderId),
40579
+ // Task 2569 — the whole hub key, not its sender half. `boundTargets()`
40580
+ // decodes the reply-map keys and this re-encodes them through the same
40581
+ // helper pair, so the census and the gateway compare one fact. Halving
40582
+ // it here is what let one phone's two accounts collapse to one door.
40583
+ bound: () => waGateway.boundTargets().map((t) => hubKey(t.accountId, t.senderId)),
40338
40584
  live: () => nativeFileFollowers.live(),
40339
- paramsFor: (senderId) => nativeFileFollowers.paramsFor(senderId),
40340
- attach: (p) => nativeFileFollowers.attach(p)
40585
+ paramsFor: (doorId) => nativeFileFollowers.paramsFor(doorId),
40586
+ attach: (p) => nativeFileFollowers.attach(p),
40587
+ // The hub key carries a NUL, which must never reach server.log.
40588
+ label: whatsappDoorLabel
40341
40589
  },
40342
40590
  followerCensusState.whatsapp
40343
40591
  ),
@@ -40346,7 +40594,7 @@ registerLoop({
40346
40594
  channel: "telegram",
40347
40595
  bound: () => telegramGateway.boundTargets(),
40348
40596
  live: () => telegramFileFollowers.live(),
40349
- paramsFor: (senderId) => telegramFileFollowers.paramsFor(senderId),
40597
+ paramsFor: (doorId) => telegramFileFollowers.paramsFor(doorId),
40350
40598
  attach: (p) => telegramFileFollowers.attach(p)
40351
40599
  },
40352
40600
  followerCensusState.telegram
@@ -41979,7 +42227,7 @@ function whatsappSender(accountId, key2) {
41979
42227
  };
41980
42228
  }
41981
42229
  function telegramOriginTarget(key2) {
41982
- return telegramGateway.liveReplyTarget(key2);
42230
+ return answeringOriginTarget(telegramGateway.liveReplyTarget(key2), key2, noteAnswered);
41983
42231
  }
41984
42232
  var configDirForWhatsApp = basename17(MAXY_DIR) || ".maxy";
41985
42233
  var bootAccount = resolveAccount();