@rubytech/create-sitedesk-code 0.1.520 → 0.1.521

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 (201) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +4 -2
  3. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +6 -0
  4. package/payload/platform/plugins/docs/references/admin-ui.md +1 -1
  5. package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-metadata-calendar.test.js +46 -1
  6. package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-metadata-calendar.test.js.map +1 -1
  7. package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts +14 -0
  8. package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts.map +1 -1
  9. package/payload/platform/plugins/email/mcp/dist/lib/imap.js +35 -4
  10. package/payload/platform/plugins/email/mcp/dist/lib/imap.js.map +1 -1
  11. package/payload/platform/plugins/email/references/email-reference.md +1 -1
  12. package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +3 -1
  13. package/payload/platform/scripts/check-architecture-skill-no-drift.mjs +20 -6
  14. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +17 -5
  15. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts.map +1 -1
  16. package/payload/platform/services/whatsapp-channel/dist/notification.js +30 -6
  17. package/payload/platform/services/whatsapp-channel/dist/notification.js.map +1 -1
  18. package/payload/platform/services/whatsapp-channel/dist/server.d.ts.map +1 -1
  19. package/payload/platform/services/whatsapp-channel/dist/server.js +53 -21
  20. package/payload/platform/services/whatsapp-channel/dist/server.js.map +1 -1
  21. package/payload/platform/services/whatsapp-channel/dist/turn-follow.d.ts +16 -0
  22. package/payload/platform/services/whatsapp-channel/dist/turn-follow.d.ts.map +1 -1
  23. package/payload/platform/services/whatsapp-channel/dist/turn-follow.js +41 -2
  24. package/payload/platform/services/whatsapp-channel/dist/turn-follow.js.map +1 -1
  25. package/payload/server/{chunk-TIIWMF5G.js → chunk-AZLT4LVO.js} +40 -7
  26. package/payload/server/{manager-B6OFOXSW.js → manager-CQBSUJXR.js} +1 -1
  27. package/payload/server/public/activity.html +5 -5
  28. package/payload/server/public/agents.html +4 -4
  29. package/payload/server/public/assets/{AdminLoginScreens-SW7mEMcP.js → AdminLoginScreens-Dar_MMJm.js} +1 -1
  30. package/payload/server/public/assets/AdminLoginScreens-Dar_MMJm.js.br +0 -0
  31. package/payload/server/public/assets/AdminLoginScreens-Dar_MMJm.js.gz +0 -0
  32. package/payload/server/public/assets/AdminShell-CpaI2F_p.js +2 -0
  33. package/payload/server/public/assets/AdminShell-CpaI2F_p.js.br +0 -0
  34. package/payload/server/public/assets/AdminShell-CpaI2F_p.js.gz +0 -0
  35. package/payload/server/public/assets/{activity-sqbyX0pX.js → activity-B1rt4GIy.js} +1 -1
  36. package/payload/server/public/assets/activity-B1rt4GIy.js.br +0 -0
  37. package/payload/server/public/assets/activity-B1rt4GIy.js.gz +0 -0
  38. package/payload/server/public/assets/{admin-D9Bt47GY.js → admin-DE3lLNje.js} +1 -1
  39. package/payload/server/public/assets/admin-DE3lLNje.js.br +0 -0
  40. package/payload/server/public/assets/admin-DE3lLNje.js.gz +0 -0
  41. package/payload/server/public/assets/{agents-BFYM5EDA.js → agents-BXq8woVf.js} +1 -1
  42. package/payload/server/public/assets/agents-BXq8woVf.js.br +0 -0
  43. package/payload/server/public/assets/agents-BXq8woVf.js.gz +0 -0
  44. package/payload/server/public/assets/{browser-DP5GZe4G.js → browser-1QSCPl3X.js} +1 -1
  45. package/payload/server/public/assets/browser-1QSCPl3X.js.br +0 -0
  46. package/payload/server/public/assets/browser-1QSCPl3X.js.gz +0 -0
  47. package/payload/server/public/assets/{calendar-BKaemLDG.js → calendar-D009qWVV.js} +1 -1
  48. package/payload/server/public/assets/calendar-D009qWVV.js.br +0 -0
  49. package/payload/server/public/assets/calendar-D009qWVV.js.gz +0 -0
  50. package/payload/server/public/assets/chat-CUg1fTqJ.js +1 -0
  51. package/payload/server/public/assets/chat-CUg1fTqJ.js.br +1 -0
  52. package/payload/server/public/assets/chat-CUg1fTqJ.js.gz +0 -0
  53. package/payload/server/public/assets/chevron-left-C2JEy7uU.js +1 -0
  54. package/payload/server/public/assets/chevron-left-C2JEy7uU.js.br +0 -0
  55. package/payload/server/public/assets/chevron-right-DWa047Ys.js +1 -0
  56. package/payload/server/public/assets/chevron-right-DWa047Ys.js.br +0 -0
  57. package/payload/server/public/assets/clock-C3OqfCnn.js +1 -0
  58. package/payload/server/public/assets/clock-C3OqfCnn.js.br +0 -0
  59. package/payload/server/public/assets/clock-C3OqfCnn.js.gz +0 -0
  60. package/payload/server/public/assets/data-Bu_yCScC.js +1 -0
  61. package/payload/server/public/assets/data-Bu_yCScC.js.br +0 -0
  62. package/payload/server/public/assets/data-Bu_yCScC.js.gz +0 -0
  63. package/payload/server/public/assets/{file-text-BKK7R-k_.js → file-text-DwGAfCsd.js} +1 -1
  64. package/payload/server/public/assets/file-text-DwGAfCsd.js.br +0 -0
  65. package/payload/server/public/assets/file-text-DwGAfCsd.js.gz +0 -0
  66. package/payload/server/public/assets/{graph-FuxYySjK.js → graph-D-DGlfhW.js} +2 -2
  67. package/payload/server/public/assets/graph-D-DGlfhW.js.br +0 -0
  68. package/payload/server/public/assets/graph-D-DGlfhW.js.gz +0 -0
  69. package/payload/server/public/assets/{graph-labels-Cip6fmKK.js → graph-labels-BbX3fXzl.js} +1 -1
  70. package/payload/server/public/assets/graph-labels-BbX3fXzl.js.br +0 -0
  71. package/payload/server/public/assets/graph-labels-BbX3fXzl.js.gz +0 -0
  72. package/payload/server/public/assets/{maximize-2-D79AhVDo.js → maximize-2-C_-KapxH.js} +1 -1
  73. package/payload/server/public/assets/maximize-2-C_-KapxH.js.br +0 -0
  74. package/payload/server/public/assets/maximize-2-C_-KapxH.js.gz +0 -0
  75. package/payload/server/public/assets/{operator-BPAfgFhP.js → operator-CFrZVcv6.js} +1 -1
  76. package/payload/server/public/assets/operator-CFrZVcv6.js.br +0 -0
  77. package/payload/server/public/assets/operator-CFrZVcv6.js.gz +0 -0
  78. package/payload/server/public/assets/{page-BqEyS0W4.js → page-CHt62f1_.js} +1 -1
  79. package/payload/server/public/assets/page-CHt62f1_.js.br +0 -0
  80. package/payload/server/public/assets/page-CHt62f1_.js.gz +0 -0
  81. package/payload/server/public/assets/page-E74MYB18.js +32 -0
  82. package/payload/server/public/assets/page-E74MYB18.js.br +0 -0
  83. package/payload/server/public/assets/page-E74MYB18.js.gz +0 -0
  84. package/payload/server/public/assets/{public-D1xs3FzD.js → public-CI6K1sNW.js} +1 -1
  85. package/payload/server/public/assets/public-CI6K1sNW.js.br +0 -0
  86. package/payload/server/public/assets/public-CI6K1sNW.js.gz +0 -0
  87. package/payload/server/public/assets/{rotate-ccw-CcU_XD8B.js → rotate-ccw-PWu_QGrD.js} +1 -1
  88. package/payload/server/public/assets/rotate-ccw-PWu_QGrD.js.br +0 -0
  89. package/payload/server/public/assets/rotate-ccw-PWu_QGrD.js.gz +0 -0
  90. package/payload/server/public/assets/routines-Cp3_XS7S.js +1 -0
  91. package/payload/server/public/assets/routines-Cp3_XS7S.js.br +0 -0
  92. package/payload/server/public/assets/routines-Cp3_XS7S.js.gz +0 -0
  93. package/payload/server/public/assets/skills-g8HvunPA.js +1 -0
  94. package/payload/server/public/assets/skills-g8HvunPA.js.br +0 -0
  95. package/payload/server/public/assets/skills-g8HvunPA.js.gz +0 -0
  96. package/payload/server/public/assets/{tasks-DgieyZqz.js → tasks-CMC_jDVm.js} +1 -1
  97. package/payload/server/public/assets/tasks-CMC_jDVm.js.br +0 -0
  98. package/payload/server/public/assets/tasks-CMC_jDVm.js.gz +0 -0
  99. package/payload/server/public/assets/{time-entry-format-IFqinQJj.js → time-entry-format-BXWuQ6Sw.js} +1 -1
  100. package/payload/server/public/assets/time-entry-format-BXWuQ6Sw.js.br +2 -0
  101. package/payload/server/public/assets/time-entry-format-BXWuQ6Sw.js.gz +0 -0
  102. package/payload/server/public/assets/{triangle-alert-C5vH__sq.js → triangle-alert-BNshRC13.js} +1 -1
  103. package/payload/server/public/assets/triangle-alert-BNshRC13.js.br +0 -0
  104. package/payload/server/public/assets/triangle-alert-BNshRC13.js.gz +0 -0
  105. package/payload/server/public/assets/{useCopyFeedback-BeL2gPl6.js → useCopyFeedback-CwTZ9DqH.js} +1 -1
  106. package/payload/server/public/assets/useCopyFeedback-CwTZ9DqH.js.br +0 -0
  107. package/payload/server/public/assets/useCopyFeedback-CwTZ9DqH.js.gz +0 -0
  108. package/payload/server/public/assets/useSubAccountSwitcher-BuUYMDWl.js +14 -0
  109. package/payload/server/public/assets/useSubAccountSwitcher-BuUYMDWl.js.br +0 -0
  110. package/payload/server/public/assets/useSubAccountSwitcher-BuUYMDWl.js.gz +0 -0
  111. package/payload/server/public/assets/useSubAccountSwitcher-dzDnmWq9.css +1 -0
  112. package/payload/server/public/assets/useSubAccountSwitcher-dzDnmWq9.css.br +0 -0
  113. package/payload/server/public/assets/useSubAccountSwitcher-dzDnmWq9.css.gz +0 -0
  114. package/payload/server/public/assets/{useVoiceRecorder-Cpw3aOSt.js → useVoiceRecorder-BGqV5R6u.js} +1 -1
  115. package/payload/server/public/assets/useVoiceRecorder-BGqV5R6u.js.br +0 -0
  116. package/payload/server/public/assets/useVoiceRecorder-BGqV5R6u.js.gz +0 -0
  117. package/payload/server/public/assets/{wrench-BBEup3hd.js → wrench-DQ045Pyk.js} +1 -1
  118. package/payload/server/public/assets/wrench-DQ045Pyk.js.br +0 -0
  119. package/payload/server/public/assets/wrench-DQ045Pyk.js.gz +0 -0
  120. package/payload/server/public/browser.html +4 -4
  121. package/payload/server/public/calendar.html +7 -7
  122. package/payload/server/public/chat.html +13 -13
  123. package/payload/server/public/data.html +11 -11
  124. package/payload/server/public/graph.html +9 -9
  125. package/payload/server/public/index.html +14 -14
  126. package/payload/server/public/operator.html +14 -14
  127. package/payload/server/public/public.html +13 -13
  128. package/payload/server/public/routines.html +6 -6
  129. package/payload/server/public/skills.html +5 -5
  130. package/payload/server/public/tasks.html +6 -6
  131. package/payload/server/server.js +234 -49
  132. package/payload/server/public/assets/AdminLoginScreens-SW7mEMcP.js.br +0 -0
  133. package/payload/server/public/assets/AdminLoginScreens-SW7mEMcP.js.gz +0 -0
  134. package/payload/server/public/assets/AdminShell-Djg6ndne.js +0 -2
  135. package/payload/server/public/assets/AdminShell-Djg6ndne.js.br +0 -0
  136. package/payload/server/public/assets/AdminShell-Djg6ndne.js.gz +0 -0
  137. package/payload/server/public/assets/activity-sqbyX0pX.js.br +0 -0
  138. package/payload/server/public/assets/activity-sqbyX0pX.js.gz +0 -0
  139. package/payload/server/public/assets/admin-D9Bt47GY.js.br +0 -0
  140. package/payload/server/public/assets/admin-D9Bt47GY.js.gz +0 -0
  141. package/payload/server/public/assets/agents-BFYM5EDA.js.br +0 -0
  142. package/payload/server/public/assets/agents-BFYM5EDA.js.gz +0 -0
  143. package/payload/server/public/assets/browser-DP5GZe4G.js.br +0 -0
  144. package/payload/server/public/assets/browser-DP5GZe4G.js.gz +0 -0
  145. package/payload/server/public/assets/calendar-BKaemLDG.js.br +0 -0
  146. package/payload/server/public/assets/calendar-BKaemLDG.js.gz +0 -0
  147. package/payload/server/public/assets/chat-DPhT6urD.js +0 -1
  148. package/payload/server/public/assets/chat-DPhT6urD.js.br +0 -1
  149. package/payload/server/public/assets/chat-DPhT6urD.js.gz +0 -0
  150. package/payload/server/public/assets/chevron-left-BSdXvJcH.js +0 -1
  151. package/payload/server/public/assets/chevron-right-DI7vI3IB.js +0 -1
  152. package/payload/server/public/assets/chevron-right-DI7vI3IB.js.br +0 -0
  153. package/payload/server/public/assets/clock-YUTPXZH5.js +0 -1
  154. package/payload/server/public/assets/clock-YUTPXZH5.js.br +0 -0
  155. package/payload/server/public/assets/clock-YUTPXZH5.js.gz +0 -0
  156. package/payload/server/public/assets/data-CfjttTvR.js +0 -1
  157. package/payload/server/public/assets/data-CfjttTvR.js.br +0 -2
  158. package/payload/server/public/assets/data-CfjttTvR.js.gz +0 -0
  159. package/payload/server/public/assets/file-text-BKK7R-k_.js.br +0 -0
  160. package/payload/server/public/assets/file-text-BKK7R-k_.js.gz +0 -0
  161. package/payload/server/public/assets/graph-FuxYySjK.js.br +0 -0
  162. package/payload/server/public/assets/graph-FuxYySjK.js.gz +0 -0
  163. package/payload/server/public/assets/graph-labels-Cip6fmKK.js.br +0 -0
  164. package/payload/server/public/assets/graph-labels-Cip6fmKK.js.gz +0 -0
  165. package/payload/server/public/assets/maximize-2-D79AhVDo.js.br +0 -0
  166. package/payload/server/public/assets/maximize-2-D79AhVDo.js.gz +0 -0
  167. package/payload/server/public/assets/operator-BPAfgFhP.js.br +0 -0
  168. package/payload/server/public/assets/operator-BPAfgFhP.js.gz +0 -0
  169. package/payload/server/public/assets/page--78CbdFs.js +0 -32
  170. package/payload/server/public/assets/page--78CbdFs.js.br +0 -0
  171. package/payload/server/public/assets/page--78CbdFs.js.gz +0 -0
  172. package/payload/server/public/assets/page-BqEyS0W4.js.br +0 -0
  173. package/payload/server/public/assets/page-BqEyS0W4.js.gz +0 -0
  174. package/payload/server/public/assets/public-D1xs3FzD.js.br +0 -0
  175. package/payload/server/public/assets/public-D1xs3FzD.js.gz +0 -0
  176. package/payload/server/public/assets/rotate-ccw-CcU_XD8B.js.br +0 -0
  177. package/payload/server/public/assets/rotate-ccw-CcU_XD8B.js.gz +0 -0
  178. package/payload/server/public/assets/routines-CA7D0myr.js +0 -1
  179. package/payload/server/public/assets/routines-CA7D0myr.js.br +0 -0
  180. package/payload/server/public/assets/routines-CA7D0myr.js.gz +0 -0
  181. package/payload/server/public/assets/skills-CfVPPSxO.js +0 -1
  182. package/payload/server/public/assets/skills-CfVPPSxO.js.br +0 -0
  183. package/payload/server/public/assets/skills-CfVPPSxO.js.gz +0 -0
  184. package/payload/server/public/assets/tasks-DgieyZqz.js.br +0 -0
  185. package/payload/server/public/assets/tasks-DgieyZqz.js.gz +0 -0
  186. package/payload/server/public/assets/time-entry-format-IFqinQJj.js.br +0 -0
  187. package/payload/server/public/assets/time-entry-format-IFqinQJj.js.gz +0 -0
  188. package/payload/server/public/assets/triangle-alert-C5vH__sq.js.br +0 -0
  189. package/payload/server/public/assets/triangle-alert-C5vH__sq.js.gz +0 -0
  190. package/payload/server/public/assets/useCopyFeedback-BeL2gPl6.js.br +0 -0
  191. package/payload/server/public/assets/useCopyFeedback-BeL2gPl6.js.gz +0 -0
  192. package/payload/server/public/assets/useSubAccountSwitcher-CH9qm2Ad.js +0 -14
  193. package/payload/server/public/assets/useSubAccountSwitcher-CH9qm2Ad.js.br +0 -0
  194. package/payload/server/public/assets/useSubAccountSwitcher-CH9qm2Ad.js.gz +0 -0
  195. package/payload/server/public/assets/useSubAccountSwitcher-DLqUQzOl.css +0 -1
  196. package/payload/server/public/assets/useSubAccountSwitcher-DLqUQzOl.css.br +0 -0
  197. package/payload/server/public/assets/useSubAccountSwitcher-DLqUQzOl.css.gz +0 -0
  198. package/payload/server/public/assets/useVoiceRecorder-Cpw3aOSt.js.br +0 -0
  199. package/payload/server/public/assets/useVoiceRecorder-Cpw3aOSt.js.gz +0 -0
  200. package/payload/server/public/assets/wrench-BBEup3hd.js.br +0 -0
  201. package/payload/server/public/assets/wrench-BBEup3hd.js.gz +0 -0
@@ -163,7 +163,6 @@ import {
163
163
  sendMediaMessage,
164
164
  sendPaused,
165
165
  sendTextMessage,
166
- senderJidFromTelephone,
167
166
  sessionScratchpadRoot,
168
167
  shouldDispatchInbound,
169
168
  shutdown,
@@ -178,7 +177,7 @@ import {
178
177
  validateFilePathInAccount,
179
178
  waitForConnection,
180
179
  warnNonUuidAccount
181
- } from "./chunk-TIIWMF5G.js";
180
+ } from "./chunk-AZLT4LVO.js";
182
181
  import {
183
182
  ACCOUNTS_DIR,
184
183
  BIN_DIR,
@@ -3919,7 +3918,7 @@ function createScheduleInjectRoutes(deps) {
3919
3918
  var import_dist9 = __toESM(require_dist(), 1);
3920
3919
 
3921
3920
  // app/lib/whatsapp/avatar-store.ts
3922
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3, readdirSync as readdirSync2, statSync as statSync4 } from "fs";
3921
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3, readdirSync as readdirSync2, statSync as statSync4, rmSync } from "fs";
3923
3922
  import { join as join6 } from "path";
3924
3923
  function safeName(jid) {
3925
3924
  if (jid.length === 0 || jid.includes("/") || jid.includes("\\") || jid.includes("..")) {
@@ -3964,30 +3963,41 @@ function writeAvatarAbsent(accountId, jid) {
3964
3963
  const dir = dirFor(accountId);
3965
3964
  const base = join6(dir, safeName(jid));
3966
3965
  mkdirSync2(dir, { recursive: true });
3966
+ rmSync(`${base}.bin`, { force: true });
3967
3967
  const meta = { jid, state: "absent", fetchedAt: (/* @__PURE__ */ new Date()).toISOString() };
3968
3968
  writeFileSync3(`${base}.json`, JSON.stringify(meta));
3969
3969
  }
3970
- function listAvatarJids(accountId) {
3970
+ function removeAvatar(accountId, jid) {
3971
+ const base = join6(dirFor(accountId), safeName(jid));
3972
+ rmSync(`${base}.bin`, { force: true });
3973
+ rmSync(`${base}.json`, { force: true });
3974
+ }
3975
+ function listAvatarEntries(accountId) {
3971
3976
  const dir = dirFor(accountId);
3972
3977
  if (!existsSync3(dir)) return [];
3973
3978
  const out = [];
3974
3979
  for (const f of readdirSync2(dir)) {
3975
3980
  if (!f.endsWith(".json")) continue;
3976
3981
  const meta = readMeta(join6(dir, f));
3977
- if (meta?.jid) out.push(meta.jid);
3982
+ if (!meta?.jid) continue;
3983
+ let bytes = 0;
3984
+ if (meta.state === "present") {
3985
+ try {
3986
+ bytes = statSync4(join6(dir, `${f.slice(0, -".json".length)}.bin`)).size;
3987
+ } catch {
3988
+ bytes = 0;
3989
+ }
3990
+ }
3991
+ out.push({ jid: meta.jid, state: meta.state, bytes, fetchedAt: meta.fetchedAt });
3978
3992
  }
3979
3993
  return out;
3980
3994
  }
3995
+ function listAvatarJids(accountId) {
3996
+ return listAvatarEntries(accountId).map((e) => e.jid);
3997
+ }
3981
3998
  function avatarCensus(accountId) {
3982
- const dir = dirFor(accountId);
3983
- if (!existsSync3(dir)) return { entries: 0, bytes: 0 };
3984
- let entries2 = 0;
3985
- let bytes = 0;
3986
- for (const f of readdirSync2(dir)) {
3987
- if (f.endsWith(".json")) entries2++;
3988
- else if (f.endsWith(".bin")) bytes += statSync4(join6(dir, f)).size;
3989
- }
3990
- return { entries: entries2, bytes };
3999
+ const entries2 = listAvatarEntries(accountId);
4000
+ return { entries: entries2.length, bytes: entries2.reduce((n, e) => n + e.bytes, 0) };
3991
4001
  }
3992
4002
 
3993
4003
  // app/lib/whatsapp/avatar-fetch.ts
@@ -4007,7 +4017,7 @@ async function defaultHttpGet(url) {
4007
4017
  }
4008
4018
  }
4009
4019
  async function defaultResolve(accountId) {
4010
- const { resolveSocket: resolveSocket2 } = await import("./manager-B6OFOXSW.js");
4020
+ const { resolveSocket: resolveSocket2 } = await import("./manager-CQBSUJXR.js");
4011
4021
  return resolveSocket2(accountId);
4012
4022
  }
4013
4023
  async function fetchAvatar(accountId, jid, deps = {}) {
@@ -4055,25 +4065,66 @@ async function fetchAvatar(accountId, jid, deps = {}) {
4055
4065
  // app/lib/whatsapp/avatar-sweep.ts
4056
4066
  var TAG7 = "[wa-avatar]";
4057
4067
  var CENSUS_TAG = "[wa-avatar-census]";
4068
+ var AVATAR_STORE_MAX_BYTES = 16 * 1024 * 1024;
4058
4069
  function liveJidsForAccount(accountId) {
4059
4070
  const live = /* @__PURE__ */ new Set();
4060
4071
  for (const [remoteJid, records] of readAllConversations(accountId)) {
4061
- live.add(remoteJid);
4062
- for (const r of records) {
4063
- if (r.fromMe) continue;
4064
- const jid = senderJidFromTelephone(r.senderTelephone);
4065
- if (jid) live.add(jid);
4066
- }
4072
+ for (const jid of avatarJidsForConversation(remoteJid, records)) live.add(jid);
4067
4073
  }
4068
4074
  return live;
4069
4075
  }
4076
+ function avatarJidsForConversation(remoteJid, records) {
4077
+ const jids = /* @__PURE__ */ new Set([remoteJid]);
4078
+ if (!isGroupJid(remoteJid)) return jids;
4079
+ for (const r of records) {
4080
+ if (r.fromMe) continue;
4081
+ if (r.senderJid && isUserJid(r.senderJid)) jids.add(r.senderJid);
4082
+ }
4083
+ return jids;
4084
+ }
4085
+ function evictAvatars(accountId, live) {
4086
+ let unreferenced = 0;
4087
+ let overBound = 0;
4088
+ let freedBytes = 0;
4089
+ const kept = [];
4090
+ for (const e of listAvatarEntries(accountId)) {
4091
+ if (live.has(e.jid)) {
4092
+ kept.push(e);
4093
+ continue;
4094
+ }
4095
+ removeAvatar(accountId, e.jid);
4096
+ unreferenced++;
4097
+ freedBytes += e.bytes;
4098
+ }
4099
+ let bytes = kept.reduce((n, e) => n + e.bytes, 0);
4100
+ if (bytes > AVATAR_STORE_MAX_BYTES) {
4101
+ const oldestFirst = kept.filter((e) => e.state === "present").sort((a, b) => a.fetchedAt.localeCompare(b.fetchedAt));
4102
+ for (const e of oldestFirst) {
4103
+ if (bytes <= AVATAR_STORE_MAX_BYTES) break;
4104
+ removeAvatar(accountId, e.jid);
4105
+ overBound++;
4106
+ freedBytes += e.bytes;
4107
+ bytes -= e.bytes;
4108
+ }
4109
+ }
4110
+ return { unreferenced, overBound, freedBytes };
4111
+ }
4070
4112
  async function sweepAvatars() {
4071
4113
  const started2 = Date.now();
4072
4114
  let jids = 0;
4073
4115
  let refetched = 0;
4074
4116
  let skipped = 0;
4075
4117
  let failed = 0;
4118
+ let evicted = 0;
4076
4119
  for (const accountId of listStoredAccountIds()) {
4120
+ const live = liveJidsForAccount(accountId);
4121
+ const ev = evictAvatars(accountId, live);
4122
+ evicted += ev.unreferenced + ev.overBound;
4123
+ if (ev.unreferenced > 0 || ev.overBound > 0) {
4124
+ console.log(
4125
+ `${TAG7} op=evict account=${accountId.slice(0, 8)} unreferenced=${ev.unreferenced} overBound=${ev.overBound} freedBytes=${ev.freedBytes}`
4126
+ );
4127
+ }
4077
4128
  const cached3 = listAvatarJids(accountId);
4078
4129
  for (const jid of cached3) {
4079
4130
  jids++;
@@ -4089,11 +4140,10 @@ async function sweepAvatars() {
4089
4140
  else if (outcome === "failed") failed++;
4090
4141
  else skipped++;
4091
4142
  }
4092
- const live = liveJidsForAccount(accountId);
4093
4143
  const census = avatarCensus(accountId);
4094
4144
  console.log(`${CENSUS_TAG} account=${accountId.slice(0, 8)} entries=${census.entries} bytes=${census.bytes} live=${live.size}`);
4095
4145
  }
4096
- const note = `jids=${jids} refetched=${refetched} skipped=${skipped} failed=${failed} ms=${Date.now() - started2}`;
4146
+ const note = `jids=${jids} refetched=${refetched} skipped=${skipped} failed=${failed} evicted=${evicted} ms=${Date.now() - started2}`;
4097
4147
  console.log(`${TAG7} op=sweep ${note}`);
4098
4148
  return note;
4099
4149
  }
@@ -4104,7 +4154,9 @@ var OFFENDER_CAP = 10;
4104
4154
  function auditGroupOutbound() {
4105
4155
  let convos = 0;
4106
4156
  let outbound = 0;
4107
- let unmarked = 0;
4157
+ let operator = 0;
4158
+ let agent = 0;
4159
+ let unstamped = 0;
4108
4160
  const offenders = [];
4109
4161
  for (const accountId of listStoredAccountIds()) {
4110
4162
  for (const [remoteJid, records] of readAllConversations(accountId)) {
@@ -4113,18 +4165,20 @@ function auditGroupOutbound() {
4113
4165
  for (const r of records) {
4114
4166
  if (!r.fromMe) continue;
4115
4167
  outbound++;
4116
- if (r.origin === "operator-manual") continue;
4117
- unmarked++;
4118
- if (offenders.length < OFFENDER_CAP) offenders.push(r.msgKeyId);
4168
+ if (r.origin === "operator-manual") operator++;
4169
+ else if (r.origin === "agent") {
4170
+ agent++;
4171
+ if (offenders.length < OFFENDER_CAP) offenders.push(r.msgKeyId);
4172
+ } else unstamped++;
4119
4173
  }
4120
4174
  }
4121
4175
  }
4122
- return { convos, outbound, unmarked, offenders };
4176
+ return { convos, outbound, operator, agent, unstamped, offenders };
4123
4177
  }
4124
4178
  function auditGroupOutboundNote() {
4125
4179
  const a = auditGroupOutbound();
4126
- const tail = a.unmarked > 0 ? ` offenders=${a.offenders.join(",")}` : "";
4127
- const note = `convos=${a.convos} outbound=${a.outbound} unmarked=${a.unmarked}${tail}`;
4180
+ const tail = a.agent > 0 ? ` offenders=${a.offenders.join(",")}` : "";
4181
+ const note = `convos=${a.convos} outbound=${a.outbound} operator=${a.operator} agent=${a.agent} unstamped=${a.unstamped}${tail}`;
4128
4182
  console.log(`${TAG8} op=group-outbound-audit ${note}`);
4129
4183
  return note;
4130
4184
  }
@@ -5868,6 +5922,7 @@ app2.post("/reply", async (c) => {
5868
5922
  quotedId: null,
5869
5923
  quotedSender: null,
5870
5924
  scope: template.scope,
5925
+ origin: "agent",
5871
5926
  createdAt: nowIso
5872
5927
  };
5873
5928
  appendMessage(storeKey, rec);
@@ -8020,12 +8075,7 @@ function resolveReplyAppendAccount(args) {
8020
8075
  return args.resolve(args.sendAccountId, args.counterpartyPhone) || args.sendAccountId;
8021
8076
  }
8022
8077
  function warmConversationAvatars(accountId, remoteJid, records) {
8023
- const jids = /* @__PURE__ */ new Set([remoteJid]);
8024
- for (const r of records) {
8025
- if (r.fromMe) continue;
8026
- const jid = senderJidFromTelephone(r.senderTelephone);
8027
- if (jid) jids.add(jid);
8028
- }
8078
+ const jids = avatarJidsForConversation(remoteJid, records);
8029
8079
  console.log(`[wa-avatar] op=request account=${accountId.slice(0, 8)} convo=${remoteJid.slice(0, 12)} jids=${jids.size} reason=reader-open`);
8030
8080
  for (const jid of jids) {
8031
8081
  void fetchAvatar(accountId, jid).catch((e) => {
@@ -8096,6 +8146,10 @@ app4.get("/store-stream", requireAdminSession, (c) => {
8096
8146
  // Task 2081 — the group transcript's per-sender colour key. Stable across a
8097
8147
  // display-name change, which `senderName` is not.
8098
8148
  senderTelephone: r.senderTelephone,
8149
+ // Task 2096 — the sender's wire JID, for the per-bubble avatar. Omitted
8150
+ // rather than sent null when the record has none, so the client's own
8151
+ // presence check is the whole test. Pre-2096 records have none.
8152
+ ...r.senderJid ? { senderJid: r.senderJid } : {},
8099
8153
  origin: r.origin,
8100
8154
  msgKeyId: r.msgKeyId,
8101
8155
  // Task 1619 — folded inbound reactions for this bubble (omitted when none).
@@ -11727,7 +11781,7 @@ var telegram_default = app10;
11727
11781
 
11728
11782
  // server/routes/quickbooks.ts
11729
11783
  import { join as join28 } from "path";
11730
- import { existsSync as existsSync21, readFileSync as readFileSync23, writeFileSync as writeFileSync9, mkdirSync as mkdirSync4, rmSync, renameSync as renameSync5 } from "fs";
11784
+ import { existsSync as existsSync21, readFileSync as readFileSync23, writeFileSync as writeFileSync9, mkdirSync as mkdirSync4, rmSync as rmSync2, renameSync as renameSync5 } from "fs";
11731
11785
  var TAG18 = "[quickbooks]";
11732
11786
  var INTUIT_TOKEN_URL = "https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer";
11733
11787
  var STATE_RE = /^[A-Za-z0-9_-]+$/;
@@ -11808,7 +11862,7 @@ async function completeConsent(args) {
11808
11862
  console.error(`${TAG18} op=token-exchange realmId=${realmId} persisted=${persisted}`);
11809
11863
  return { ok: persisted, status: persisted ? 200 : 500, message: persisted ? `Connected company ${realmId}.` : "Failed to persist the connection.", stateValid: true };
11810
11864
  } finally {
11811
- rmSync(claimFile, { force: true });
11865
+ rmSync2(claimFile, { force: true });
11812
11866
  }
11813
11867
  }
11814
11868
  function page(title, detail) {
@@ -13243,7 +13297,7 @@ var session_upload_default = app19;
13243
13297
 
13244
13298
  // server/routes/admin/agents.ts
13245
13299
  import { resolve as resolve20 } from "path";
13246
- import { readdirSync as readdirSync21, readFileSync as readFileSync28, existsSync as existsSync30, rmSync as rmSync2, renameSync as renameSync9, mkdirSync as mkdirSync6, writeFileSync as writeFileSync12 } from "fs";
13300
+ import { readdirSync as readdirSync21, readFileSync as readFileSync28, existsSync as existsSync30, rmSync as rmSync3, renameSync as renameSync9, mkdirSync as mkdirSync6, writeFileSync as writeFileSync12 } from "fs";
13247
13301
  import { execFileSync as execFileSync6 } from "child_process";
13248
13302
 
13249
13303
  // ../services/claude-session-manager/src/tool-surface.ts
@@ -14136,7 +14190,7 @@ app20.delete("/:slug", async (c) => {
14136
14190
  return c.json({ error: `Graph cleanup failed: ${msg}` }, 500);
14137
14191
  }
14138
14192
  try {
14139
- rmSync2(agentDir, { recursive: true, force: true });
14193
+ rmSync3(agentDir, { recursive: true, force: true });
14140
14194
  console.log(`[admin/agents] op=delete accountId=${account.accountId.slice(0, 8)} slug=${slug} outcome=ok`);
14141
14195
  return c.json({ ok: true });
14142
14196
  } catch (err) {
@@ -20234,6 +20288,7 @@ function resolveSpawnPlan(host, operatorDomains, body, mintId = randomUUID7, adm
20234
20288
  const operator = isOperatorHost(host, operatorDomains);
20235
20289
  const resumeId = typeof body.sessionId === "string" && body.sessionId.length > 0 ? body.sessionId : void 0;
20236
20290
  const name = typeof body.name === "string" && body.name.length > 0 ? body.name : void 0;
20291
+ const rawTitle = typeof body.title === "string" ? body.title.trim() : "";
20237
20292
  const webchatBind = resumeId === void 0 || operator;
20238
20293
  const kind = resumeId === void 0 ? "new" : "resume";
20239
20294
  const origin = operator ? "operator" : "admin";
@@ -20264,13 +20319,25 @@ function resolveSpawnPlan(host, operatorDomains, body, mintId = randomUUID7, adm
20264
20319
  payload.sessionId = resumeId;
20265
20320
  if (name) payload.name = name;
20266
20321
  }
20267
- return { webchatBind, origin, kind, sessionId, payload, model: pickedModel, effort: pickedEffort, paramsSource };
20322
+ return {
20323
+ webchatBind,
20324
+ origin,
20325
+ kind,
20326
+ sessionId,
20327
+ payload,
20328
+ model: pickedModel,
20329
+ effort: pickedEffort,
20330
+ paramsSource,
20331
+ // Never rides `payload`: the manager's rc-spawn takes no title. The route
20332
+ // applies it through the rename endpoint once the session id exists.
20333
+ title: kind === "new" && webchatBind && rawTitle.length > 0 ? rawTitle : null
20334
+ };
20268
20335
  }
20269
- function shapeWebchatResponse(sessionId, manager) {
20336
+ function shapeWebchatResponse(sessionId, manager, titleSource = "none") {
20270
20337
  const outcome = typeof manager?.outcome === "string" ? manager.outcome : "bound";
20271
20338
  const redirected = manager?.redirected === true && typeof manager?.sessionId === "string";
20272
20339
  const landed = redirected ? manager.sessionId : sessionId;
20273
- return { sessionId: landed, outcome, target: `/chat?session=${landed}` };
20340
+ return { sessionId: landed, outcome, target: `/chat?session=${landed}`, titleSource };
20274
20341
  }
20275
20342
  var app38 = new Hono();
20276
20343
  app38.post("/", requireAdminSession, async (c) => {
@@ -20349,9 +20416,40 @@ app38.post("/", requireAdminSession, async (c) => {
20349
20416
  );
20350
20417
  }
20351
20418
  }
20419
+ let titleSource = "none";
20420
+ if (plan.kind === "new") {
20421
+ if (plan.title === null) {
20422
+ console.log(
20423
+ `[chat-spawn] op=title spawnReqId=${spawnReqId} sessionId=${plan.sessionId} titleLen=0 source=absent outcome=skipped`
20424
+ );
20425
+ } else {
20426
+ try {
20427
+ const tRes = await fetch(`${managerBase("session-rc-spawn:title")}/${plan.sessionId}/rename`, {
20428
+ method: "POST",
20429
+ headers: { "Content-Type": "application/json" },
20430
+ body: JSON.stringify({ title: plan.title })
20431
+ });
20432
+ if (tRes.status === 200) {
20433
+ titleSource = "user";
20434
+ console.log(
20435
+ `[chat-spawn] op=title spawnReqId=${spawnReqId} sessionId=${plan.sessionId} titleLen=${plan.title.length} source=modal outcome=applied`
20436
+ );
20437
+ } else {
20438
+ const reason = (await tRes.json().catch(() => ({}))).reason ?? `status-${tRes.status}`;
20439
+ console.error(
20440
+ `[chat-spawn] op=title spawnReqId=${spawnReqId} sessionId=${plan.sessionId} titleLen=${plan.title.length} source=modal outcome=rejected reason=${reason}`
20441
+ );
20442
+ }
20443
+ } catch (err) {
20444
+ console.error(
20445
+ `[chat-spawn] op=title spawnReqId=${spawnReqId} sessionId=${plan.sessionId} titleLen=${plan.title.length} source=modal outcome=failed reason=${err instanceof Error ? err.message : String(err)}`
20446
+ );
20447
+ }
20448
+ }
20449
+ }
20352
20450
  console.log(`[chat-spawn] op=navigate spawnReqId=${spawnReqId} target=${target}`);
20353
20451
  console.log(`[chat-spawn] op=exit spawnReqId=${spawnReqId} outcome=bound`);
20354
- return c.json(shapeWebchatResponse(plan.sessionId, parsed));
20452
+ return c.json(shapeWebchatResponse(plan.sessionId, parsed, titleSource));
20355
20453
  }
20356
20454
  return c.json(parsed ?? {});
20357
20455
  });
@@ -22162,7 +22260,7 @@ app51.delete("/:eventId", requireAdminSession, async (c) => {
22162
22260
  var routines_default = app51;
22163
22261
 
22164
22262
  // server/routes/admin/skills.ts
22165
- import { existsSync as existsSync39, readdirSync as readdirSync24, readFileSync as readFileSync36, rmSync as rmSync3 } from "fs";
22263
+ import { existsSync as existsSync39, readdirSync as readdirSync24, readFileSync as readFileSync36, rmSync as rmSync4 } from "fs";
22166
22264
  import { resolve as resolve30 } from "path";
22167
22265
  var app52 = new Hono();
22168
22266
  var NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
@@ -22268,9 +22366,9 @@ app52.delete("/:plugin/:skill", requireAdminSession, async (c) => {
22268
22366
  return c.json({ error: "Skill not found" }, 404);
22269
22367
  }
22270
22368
  try {
22271
- rmSync3(skillDir, { recursive: true, force: true });
22369
+ rmSync4(skillDir, { recursive: true, force: true });
22272
22370
  const pluginDirRemoved = subdirs2(skillsDir).length === 0;
22273
- if (pluginDirRemoved) rmSync3(pluginDir, { recursive: true, force: true });
22371
+ if (pluginDirRemoved) rmSync4(pluginDir, { recursive: true, force: true });
22274
22372
  const skillDirGone = !existsSync39(skillDir);
22275
22373
  if (!skillDirGone) {
22276
22374
  console.error(`[admin:skills] op=delete plugin=${plugin} skill=${skill} outcome=rejected reason=rm-incomplete skillDirGone=false`);
@@ -25749,6 +25847,7 @@ function createWaChannelRoutes(deps) {
25749
25847
  return c.json({ error: message, terminal }, 500);
25750
25848
  }
25751
25849
  console.error(`[whatsapp-native] op=reply-dispatch senderId=${senderId} bytes=${bytes}`);
25850
+ deps.onReplyDispatch?.(senderId, Date.now());
25752
25851
  return c.json({ ok: true });
25753
25852
  });
25754
25853
  app73.post("/wa-channel/reply-document", async (c) => {
@@ -25823,9 +25922,10 @@ function createWaChannelRoutes(deps) {
25823
25922
  const finalBytes = num(body?.finalBytes);
25824
25923
  const queueDepth = num(body?.queueDepth);
25825
25924
  const pendingTurns = num(body?.pendingTurns);
25925
+ const consumed2 = num(body?.consumed);
25826
25926
  const sid = sessionId.slice(0, 8);
25827
25927
  const deliver = interactive && !replied && finalText.trim().length > 0;
25828
- const decision = `sessionId=${sid} replied=${replied ? "yes" : "no"} interactive=${interactive ? "yes" : "no"} finalBytes=${finalBytes} queueDepth=${queueDepth} pendingTurns=${pendingTurns}`;
25928
+ const decision = `sessionId=${sid} replied=${replied ? "yes" : "no"} interactive=${interactive ? "yes" : "no"} finalBytes=${finalBytes} queueDepth=${queueDepth} pendingTurns=${pendingTurns} consumed=${consumed2}`;
25829
25929
  if (!replied && finalBytes > 0 && !deliver) {
25830
25930
  const op = interactive ? "turn-answer-dropped" : "turn-answer-withheld";
25831
25931
  console.error(
@@ -25844,6 +25944,7 @@ function createWaChannelRoutes(deps) {
25844
25944
  return c.json({ ok: true, delivered: "undelivered" });
25845
25945
  }
25846
25946
  console.error(`[whatsapp-native] op=turn-end ${decision} delivered=fallback bytes=${bytes}`);
25947
+ deps.onReplyDispatch?.(senderId, Date.now());
25847
25948
  return c.json({ ok: true, delivered: "fallback" });
25848
25949
  }
25849
25950
  if (replied) {
@@ -25860,6 +25961,7 @@ function createWaChannelRoutes(deps) {
25860
25961
  var QUEUED_THRESHOLD_MS = 3e4;
25861
25962
  var ACK_THRESHOLD_MS = 3e4;
25862
25963
  var SWEEP_INTERVAL_MS = 15e3;
25964
+ var MEDIA_STANZA_STALE_MS = 12e4;
25863
25965
  var WaGateway = class {
25864
25966
  constructor(deps) {
25865
25967
  this.deps = deps;
@@ -25874,6 +25976,66 @@ var WaGateway = class {
25874
25976
  docContexts = /* @__PURE__ */ new Map();
25875
25977
  spawning = /* @__PURE__ */ new Set();
25876
25978
  seq = 0;
25979
+ // Task 2110 — senderId -> the arrival ms of every still-unemitted non-audio
25980
+ // media stanza for that sender, oldest first. This is the race window: a reply
25981
+ // dispatched while the list is non-empty was composed by a turn that could not
25982
+ // yet see an incoming file, and the window is measured from the OLDEST entry.
25983
+ //
25984
+ // One entry per stanza, not one per sender. Downloads finish out of order and
25985
+ // the debouncer flushes whatever has landed, so a small file can be emitted
25986
+ // while a large one is still downloading. Holding a single timestamp meant that
25987
+ // first inbound closed the window and the still-blind replies that followed
25988
+ // were never counted, with the census reading zero throughout.
25989
+ pendingMediaStanza = /* @__PURE__ */ new Map();
25990
+ // Counters for the per-sweep census, reset on every print.
25991
+ mediaRaces = 0;
25992
+ mediaRaceSenders = /* @__PURE__ */ new Set();
25993
+ mediaStanzaExpired = 0;
25994
+ /** Task 2110 — the WhatsApp manager reports that a non-audio media stanza for
25995
+ * this sender has started downloading. The gateway cannot derive this itself:
25996
+ * it first learns of the media when the completed inbound arrives, which is
25997
+ * the CLOSE of the window, not its open. Audio is excluded by the caller —
25998
+ * its transcript rides in as `text`, so no turn is ever blind to a voice note.
25999
+ * The key is canonicalised on write, the same rule handleInbound applies, so
26000
+ * a "+"-prefixed value and a digits-only one address one window. */
26001
+ noteMediaStanza(senderId, atMs) {
26002
+ const key = normalizeTarget(senderId) ?? senderId;
26003
+ const inFlight2 = this.pendingMediaStanza.get(key);
26004
+ if (inFlight2) inFlight2.push(atMs);
26005
+ else this.pendingMediaStanza.set(key, [atMs]);
26006
+ }
26007
+ /** Task 2110 — drop every entry past the staleness cap, oldest first, counting
26008
+ * each into the census. Entries are appended in arrival order, so the scan
26009
+ * stops at the first one still inside the cap. A download that never produced
26010
+ * an inbound leaves its entry here; without this it would hold the window open
26011
+ * forever and make every later reply to that sender read as a race. */
26012
+ expireStaleStanzas(key, inFlight2, now) {
26013
+ while (inFlight2.length > 0 && now - inFlight2[0] > MEDIA_STANZA_STALE_MS) {
26014
+ inFlight2.shift();
26015
+ this.mediaStanzaExpired++;
26016
+ }
26017
+ if (inFlight2.length === 0) this.pendingMediaStanza.delete(key);
26018
+ }
26019
+ /** Task 2110 — a reply was dispatched to this sender. When a media stanza for
26020
+ * the same sender is in flight, the turn that composed this reply could not
26021
+ * see the incoming file, so the exposure is recorded whether or not the agent
26022
+ * actually misspoke: a steering clause has no machine-checkable
26023
+ * post-condition, and this counts entry into the window, never the phrasing.
26024
+ * A window past the staleness cap is a download that never produced an
26025
+ * inbound; it is dropped and counted as expired, not reported as a race. */
26026
+ noteReplyDispatch(senderId, atMs) {
26027
+ const key = normalizeTarget(senderId) ?? senderId;
26028
+ const inFlight2 = this.pendingMediaStanza.get(key);
26029
+ if (inFlight2 === void 0) return;
26030
+ this.expireStaleStanzas(key, inFlight2, atMs);
26031
+ if (inFlight2.length === 0) return;
26032
+ const stanzaAt = inFlight2[0];
26033
+ this.mediaRaces++;
26034
+ this.mediaRaceSenders.add(key);
26035
+ console.error(
26036
+ `[whatsapp-native] op=media-race senderId=${key} replyAt=${new Date(atMs).toISOString()} mediaStanzaAt=${new Date(stanzaAt).toISOString()} gapMs=${atMs - stanzaAt}`
26037
+ );
26038
+ }
25877
26039
  /** Hono sub-app exposing the loopback channel routes; mount on the UI app.
25878
26040
  * Task 1836 — the process-local unseated-sender census that used to live here
25879
26041
  * was retired. Its quiet state was ambiguous (a UI restart emptied it, so
@@ -25895,7 +26057,8 @@ var WaGateway = class {
25895
26057
  console.error(
25896
26058
  `[whatsapp-native] op=channel-received senderId=${senderId} waMessageId=${waMessageId}${found ? "" : " note=unknown"}`
25897
26059
  );
25898
- }
26060
+ },
26061
+ onReplyDispatch: (senderId, atMs) => this.noteReplyDispatch(senderId, atMs)
25899
26062
  });
25900
26063
  }
25901
26064
  /** Run one reconciliation pass: log every terminal delivery event the sweep
@@ -25915,6 +26078,15 @@ var WaGateway = class {
25915
26078
  `[whatsapp-native] op=${e.kind} senderId=${e.senderId} waMessageId=${e.waMessageId} ageMs=${e.ageMs}`
25916
26079
  );
25917
26080
  }
26081
+ for (const [key, inFlight2] of [...this.pendingMediaStanza]) {
26082
+ this.expireStaleStanzas(key, inFlight2, now);
26083
+ }
26084
+ console.error(
26085
+ `[whatsapp-native] op=media-race-census races=${this.mediaRaces} senders=${this.mediaRaceSenders.size} expired=${this.mediaStanzaExpired}`
26086
+ );
26087
+ this.mediaRaces = 0;
26088
+ this.mediaRaceSenders.clear();
26089
+ this.mediaStanzaExpired = 0;
25918
26090
  return events;
25919
26091
  }
25920
26092
  /** Arm the periodic reconciliation sweep in the long-lived UI process.
@@ -25946,6 +26118,14 @@ var WaGateway = class {
25946
26118
  if (senderId !== input.senderId) {
25947
26119
  console.error(`[whatsapp-native] op=sender-key raw=${input.senderId} canonical=${senderId}`);
25948
26120
  }
26121
+ const arrived = input.media.filter((m) => m.type !== "audio").length;
26122
+ if (arrived > 0) {
26123
+ const inFlight2 = this.pendingMediaStanza.get(senderId);
26124
+ if (inFlight2) {
26125
+ inFlight2.splice(0, arrived);
26126
+ if (inFlight2.length === 0) this.pendingMediaStanza.delete(senderId);
26127
+ }
26128
+ }
25949
26129
  if (input.passive) {
25950
26130
  const msgId = `wa-${++this.seq}`;
25951
26131
  await this.deps.ensureChannelSession({
@@ -30276,6 +30456,11 @@ init({
30276
30456
  );
30277
30457
  });
30278
30458
  },
30459
+ // Task 2110 — the manager is the only place that sees a media stanza start
30460
+ // downloading; the gateway first sees the media on the completed inbound,
30461
+ // which is the race window's close. Wiring it here keeps the manager free of
30462
+ // any gateway import, exactly as onMessage and onNotify are wired.
30463
+ onMediaStanza: (senderId, atMs) => waGateway.noteMediaStanza(senderId, atMs),
30279
30464
  onMessage: async (msg) => {
30280
30465
  if (isObserveAccount(msg.accountId)) {
30281
30466
  console.error(`[whatsapp:observe] op=BREACH account=${msg.accountId} sender=${msg.senderPhone} \u2014 dispatch reached the router from an observe socket`);