volute 0.49.0 → 0.50.0

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 (210) hide show
  1. package/README.md +2 -2
  2. package/dist/{accept-KYNC5X3U.js → accept-5NK5XJB5.js} +2 -1
  3. package/dist/{activity-events-GJJAI2WT.js → activity-events-H7YI3BTF.js} +3 -3
  4. package/dist/{ai-service-A7FS7EC3.js → ai-service-V4CTNGI3.js} +5 -4
  5. package/dist/{archive-7E3YE6VF.js → archive-ORM3ENHI.js} +3 -3
  6. package/dist/{auth-ABYLOEZ7.js → auth-DT7GY7EZ.js} +10 -6
  7. package/dist/{avatar-image-6ATV2IOZ.js → avatar-image-QEPAP3A5.js} +3 -3
  8. package/dist/{backup-XYAMT4CU.js → backup-IE6PMXJO.js} +11 -12
  9. package/dist/{bridge-3FNSWLNQ.js → bridge-YK7NTRU5.js} +2 -1
  10. package/dist/{channels-SGQZHNQJ.js → channels-FSAHOQWI.js} +2 -1
  11. package/dist/{chat-OGXAEXMV.js → chat-W5K6HCBE.js} +9 -9
  12. package/dist/{chunk-3B427GUK.js → chunk-3BAFMGCJ.js} +2 -2
  13. package/dist/{chunk-4B62E4CG.js → chunk-4CN7OCJE.js} +2 -2
  14. package/dist/{chunk-VNRSXEBF.js → chunk-574M6I7F.js} +38 -5
  15. package/dist/{chunk-HCYQRME2.js → chunk-5FVANDON.js} +2 -2
  16. package/dist/{chunk-P4LHVRNE.js → chunk-5IIVOWSP.js} +27 -19
  17. package/dist/{chunk-TXJL36ZQ.js → chunk-5PKFJHCR.js} +2 -2
  18. package/dist/{chunk-JPGHSXSV.js → chunk-5TPC4KCV.js} +23 -9
  19. package/dist/{chunk-YIJGKHIS.js → chunk-632FJ23O.js} +24 -20
  20. package/dist/{chunk-IH5TKXX6.js → chunk-636GFQNE.js} +11 -11
  21. package/dist/{chunk-EFT2OALG.js → chunk-6BM2I736.js} +20 -5
  22. package/dist/{chunk-JCBPU57A.js → chunk-74T3CEXK.js} +6 -6
  23. package/dist/{chunk-EG5LSKHU.js → chunk-77IZUVBB.js} +2 -2
  24. package/dist/{chunk-K3JOLGWF.js → chunk-7DXLIYPY.js} +76 -63
  25. package/dist/chunk-7FDWPQCU.js +289 -0
  26. package/dist/chunk-7OZWKHA3.js +548 -0
  27. package/dist/{chunk-PY24ZL62.js → chunk-7UWXAJMK.js} +1 -1
  28. package/dist/{chunk-MG3Z34YE.js → chunk-7XDOTN4R.js} +1 -1
  29. package/dist/{chunk-SX72DOO3.js → chunk-AFSLNUTT.js} +1 -1
  30. package/dist/{chunk-DVN6DNMI.js → chunk-AJWFDIEY.js} +17 -2
  31. package/dist/chunk-ALIVXDM4.js +1014 -0
  32. package/dist/{chunk-RSBL3FAD.js → chunk-CAIYV33P.js} +234 -24
  33. package/dist/{chunk-ZX46PGCC.js → chunk-E5S3ILVA.js} +1 -1
  34. package/dist/{chunk-SHKM5RGD.js → chunk-EEOMA4DX.js} +1 -1
  35. package/dist/chunk-EQKSRVDE.js +2857 -0
  36. package/dist/chunk-FNJCBRD3.js +31 -0
  37. package/dist/{chunk-EADX54RG.js → chunk-HW6Q7AMB.js} +4 -4
  38. package/dist/chunk-JI67D7O2.js +1222 -0
  39. package/dist/{chunk-KHCKZ36O.js → chunk-KAEZOVP6.js} +1 -1
  40. package/dist/chunk-LNNI3MS4.js +54 -0
  41. package/dist/{chunk-BCG45AQB.js → chunk-M74I3HHF.js} +15 -5
  42. package/dist/chunk-MLZBBLMT.js +109 -0
  43. package/dist/{chunk-5T5YMX6S.js → chunk-PW65BXY5.js} +5 -0
  44. package/dist/{chunk-NOIAHMJB.js → chunk-QFVGPKJI.js} +1 -1
  45. package/dist/{chunk-VIUH5CHQ.js → chunk-SFRYRW37.js} +50 -33
  46. package/dist/{chunk-L27YYONJ.js → chunk-SSNZ6ZNN.js} +1 -1
  47. package/dist/{chunk-GWVFDUNB.js → chunk-TGJHGVBZ.js} +8 -10
  48. package/dist/{chunk-6HGEZJDH.js → chunk-U3EJJ7JN.js} +2 -2
  49. package/dist/{chunk-4DSGN4FL.js → chunk-UC6BNMGT.js} +43 -13
  50. package/dist/{chunk-JBIFXB6I.js → chunk-WBBKYUGF.js} +3 -5
  51. package/dist/{chunk-FBBS4BVR.js → chunk-X67VT4QO.js} +1 -1
  52. package/dist/{chunk-DAIIMBLB.js → chunk-YQOYZ6G7.js} +3 -5
  53. package/dist/{chunk-JCWB2ITW.js → chunk-ZUG76D3P.js} +35 -5
  54. package/dist/cli.js +34 -27
  55. package/dist/{clock-6JZL2FS6.js → clock-LH7UFNLJ.js} +4 -3
  56. package/dist/{cloud-sync-VFDTNNCA.js → cloud-sync-IDJFVDEP.js} +22 -19
  57. package/dist/{config-RPSLA2HX.js → config-UGJS57VT.js} +3 -3
  58. package/dist/{conversations-TUM5PJJ4.js → conversations-S2232Y5I.js} +8 -4
  59. package/dist/{create-Z5FSDNVZ.js → create-2M7DAHQX.js} +2 -2
  60. package/dist/{create-VIQVP3QB.js → create-M727VUDF.js} +2 -1
  61. package/dist/{daemon-client-ZFY5MOAL.js → daemon-client-VVYFZI2D.js} +2 -1
  62. package/dist/{daemon-restart-54L4B6AQ.js → daemon-restart-2AAGYFHE.js} +6 -7
  63. package/dist/daemon.js +1625 -529
  64. package/dist/{db-F6UMFRYD.js → db-QAY45Z56.js} +2 -2
  65. package/dist/{delete-ESEP7NGQ.js → delete-O7PEFQDF.js} +1 -1
  66. package/dist/delivery-manager-B4OKWD2P.js +33 -0
  67. package/dist/{delivery-router-VUOL5YSA.js → delivery-router-JE7SHYNU.js} +7 -5
  68. package/dist/{down-6X7KX7BZ.js → down-D4IRNEMJ.js} +5 -5
  69. package/dist/echo-text-WYAAODBV.js +39 -0
  70. package/dist/{env-ZYMSCB7B.js → env-ABM5IMA7.js} +3 -2
  71. package/dist/{env-KUL4XKN2.js → env-AZK5X4W7.js} +3 -3
  72. package/dist/{exec-FBXA3CM4.js → exec-2H65LMTA.js} +3 -3
  73. package/dist/{export-TNBPDH77.js → export-3E3EL6W3.js} +4 -4
  74. package/dist/{extension-J4ZQ2GHV.js → extension-MRYJWSMV.js} +2 -1
  75. package/dist/{extensions-AWNO3SZF.js → extensions-7KP3X6G7.js} +22 -19
  76. package/dist/{files-BVBRJJ2P.js → files-6BMKVG5G.js} +5 -4
  77. package/dist/{import-ZUNKMCKC.js → import-UPNTEBBT.js} +8 -8
  78. package/dist/{isolation-OPCZQRTH.js → isolation-OKDFM6WA.js} +5 -3
  79. package/dist/{join-34TR2GXQ.js → join-PYA36HEC.js} +1 -1
  80. package/dist/{list-PMUXZNAM.js → list-4BVGH7AN.js} +8 -3
  81. package/dist/{login-KAKNFCW5.js → login-MOXVLULE.js} +4 -3
  82. package/dist/{login-LKNLCJPA.js → login-RJJTCVR3.js} +2 -1
  83. package/dist/{logout-YZRMZ3O2.js → logout-BYV433X6.js} +2 -1
  84. package/dist/{logout-RLT62RQP.js → logout-DWD3OQVT.js} +4 -3
  85. package/dist/message-delivery-3456PXB7.js +43 -0
  86. package/dist/migrate-thread-config-4A5LW36Z.js +90 -0
  87. package/dist/{mind-IF6CGMFW.js → mind-NYRALSV2.js} +19 -19
  88. package/dist/{mind-activity-tracker-YMMERB6B.js → mind-activity-tracker-L7GCYXQR.js} +4 -4
  89. package/dist/{mind-contacts-BKUFPUCU.js → mind-contacts-QYU4TPEA.js} +3 -2
  90. package/dist/{mind-history-WI5OI3BW.js → mind-history-NXXOOTX6.js} +30 -7
  91. package/dist/{mind-list-OTHV4ZO4.js → mind-list-SYRA7CH5.js} +3 -2
  92. package/dist/{mind-profile-LUG353ZT.js → mind-profile-MRCVZPYT.js} +1 -1
  93. package/dist/mind-service-5TA7UJAT.js +47 -0
  94. package/dist/{mind-sleep-VBV7YRSU.js → mind-sleep-TH6BRQ5X.js} +4 -3
  95. package/dist/{mind-status-BJNPGXKC.js → mind-status-Z2LQFA6F.js} +3 -2
  96. package/dist/{mind-wake-LVTZ55EZ.js → mind-wake-LDGPRA42.js} +2 -1
  97. package/dist/{package-PD67VPHB.js → package-XYSV7NUT.js} +23 -23
  98. package/dist/{prompts-MDHDLWEX.js → prompts-QKFPX6DQ.js} +3 -3
  99. package/dist/{read-YSWUXYL7.js → read-ZXGUT4QC.js} +5 -3
  100. package/dist/{register-TWQDKQRT.js → register-OX4HRSGC.js} +2 -1
  101. package/dist/{registry-3ELJPUGJ.js → registry-4UF6OCOE.js} +6 -4
  102. package/dist/{reject-SXGCYN4A.js → reject-TWB4QJHG.js} +2 -1
  103. package/dist/{restart-5F5QDBOG.js → restart-AGEGNSYQ.js} +2 -1
  104. package/dist/{sandbox-XPYA7UIF.js → sandbox-EGEU5GGE.js} +7 -4
  105. package/dist/scheduler-UV6RUJ7P.js +25 -0
  106. package/dist/{schema-6YONVTKZ.js → schema-C2GIPD4V.js} +3 -3
  107. package/dist/{seed-NQED7PSX.js → seed-3IMBOAGK.js} +1 -1
  108. package/dist/{seed-check-TYD7RRZY.js → seed-check-NRJEQZST.js} +14 -5
  109. package/dist/{seed-cmd-QF3F2G4S.js → seed-cmd-U73XVQMI.js} +3 -3
  110. package/dist/{seed-create-XOO4N2OE.js → seed-create-FBLNVSUP.js} +4 -4
  111. package/dist/{seed-readiness-3LRYASA4.js → seed-readiness-QRA46ZJQ.js} +4 -5
  112. package/dist/{seed-sprout-HSBBGSS2.js → seed-sprout-3B4SZX7J.js} +9 -9
  113. package/dist/{send-IHRJYNXA.js → send-NBRZJXKX.js} +32 -10
  114. package/dist/{service-YCOPCTSZ.js → service-XMXAXGJD.js} +4 -4
  115. package/dist/{service-install-Y46OTF55.js → service-install-MOKTEG7B.js} +5 -5
  116. package/dist/{setup-TCN7VH5U.js → setup-CAJFOR2N.js} +10 -3
  117. package/dist/{setup-F4TE64YI.js → setup-JGYAXXW7.js} +6 -7
  118. package/dist/{skill-4CYJLYVY.js → skill-NXIHDQCG.js} +3 -2
  119. package/dist/skills/dreaming/references/INSTALL.md +2 -2
  120. package/dist/skills/imagegen/SKILL.md +21 -4
  121. package/dist/skills/imagegen/scripts/imagegen.ts +124 -21
  122. package/dist/skills/tending/SKILL.md +7 -7
  123. package/dist/skills/volute-mind/SKILL.md +3 -3
  124. package/dist/skills/volute-mind/references/integrations.md +2 -2
  125. package/dist/skills/volute-mind/references/routing.md +15 -15
  126. package/dist/{skills-NN5UZCUU.js → skills-5MSMT7PT.js} +4 -5
  127. package/dist/sleep-manager-LIEPO6WG.js +41 -0
  128. package/dist/{spirit-SSJMYC6A.js → spirit-IL5EMWYZ.js} +10 -7
  129. package/dist/spirit-availability-YFEGJNJ2.js +42 -0
  130. package/dist/{split-JF2VWTXA.js → split-BU5VWX6P.js} +1 -1
  131. package/dist/{sprout-AXA2SECQ.js → sprout-UPXGJ5HX.js} +1 -1
  132. package/dist/{src-YH6PQ7LJ.js → src-HGNE2XG4.js} +7 -1
  133. package/dist/{src-ZN4LXMPI.js → src-LCSOQY6Z.js} +57 -51
  134. package/dist/{src-JAOVXO5W.js → src-UDMJZ6MH.js} +8 -8
  135. package/dist/{start-BTVMTP3K.js → start-MUMNFR52.js} +2 -1
  136. package/dist/{status-VJGXMQ4L.js → status-IL67CYM4.js} +7 -6
  137. package/dist/{stop-YORESK4U.js → stop-EKERNYTX.js} +2 -1
  138. package/dist/system-events-A6IGCEC3.js +46 -0
  139. package/dist/system-install-YZSWCPDK.js +8 -0
  140. package/dist/{systems-5MH2ELQE.js → systems-VDRUZXNF.js} +5 -4
  141. package/dist/{tailscale-AMMVKDXB.js → tailscale-SJUW2ETR.js} +2 -2
  142. package/dist/{template-staleness-HKGCTS3H.js → template-staleness-T27VCGYA.js} +3 -3
  143. package/dist/{turn-tracker-JQ2YUUY2.js → turn-tracker-6EI2EL7G.js} +6 -6
  144. package/dist/{up-SDLTLPAO.js → up-CLL4665A.js} +5 -6
  145. package/dist/{update-WEA4N2TR.js → update-HVAUCQPN.js} +5 -5
  146. package/dist/{update-check-DZOTONII.js → update-check-2MGBBAFF.js} +3 -3
  147. package/dist/{upgrade-K5FIQPQD.js → upgrade-LIE46UBB.js} +1 -1
  148. package/dist/variant-cleanup-HJ7Z75R6.js +22 -0
  149. package/dist/{version-notify-WFA5XB5A.js → version-notify-7BUHWZNT.js} +10 -27
  150. package/dist/web-assets/assets/index-ClsUcXiW.js +95 -0
  151. package/dist/web-assets/assets/index-Dz3wTbtR.css +1 -0
  152. package/dist/web-assets/index.html +2 -2
  153. package/dist/xai-P42CRCZA.js +129 -0
  154. package/drizzle/0014_system_events.sql +26 -0
  155. package/drizzle/0015_rename_session_to_thread.sql +12 -0
  156. package/drizzle/0016_brain_to_human.sql +4 -0
  157. package/drizzle/0017_event_history_rows.sql +9 -0
  158. package/drizzle/meta/_journal.json +28 -0
  159. package/package.json +23 -23
  160. package/packages/extensions/notes/dist/ui/assets/index-BBs8pyel.js +67 -0
  161. package/packages/extensions/notes/dist/ui/index.html +1 -1
  162. package/packages/extensions/pages/dist/ui/assets/index-B589e8Xe.js +2 -0
  163. package/packages/extensions/pages/dist/ui/index.html +1 -1
  164. package/packages/extensions/plan/dist/ui/assets/index-B8s8wxyt.js +67 -0
  165. package/packages/extensions/plan/dist/ui/index.html +1 -1
  166. package/templates/_base/.init/.config/prompts.json +1 -0
  167. package/templates/_base/.init/.local/hooks/startup-context.ts +24 -9
  168. package/templates/_base/home/.config/routes.json +2 -2
  169. package/templates/_base/home/VOLUTE.md +18 -5
  170. package/templates/_base/src/lib/daemon-client.ts +1 -1
  171. package/templates/_base/src/lib/event-turn.ts +39 -0
  172. package/templates/_base/src/lib/format-prefix.ts +21 -2
  173. package/templates/_base/src/lib/router.ts +6 -2
  174. package/templates/_base/src/lib/routing.ts +8 -8
  175. package/templates/_base/src/lib/startup.ts +9 -1
  176. package/templates/_base/src/lib/types.ts +7 -1
  177. package/templates/_base/src/lib/volute-server.ts +30 -1
  178. package/templates/claude/.init/.config/routes.json +3 -3
  179. package/templates/claude/.init/CLAUDE.md +1 -1
  180. package/templates/claude/package.json.tmpl +4 -4
  181. package/templates/claude/src/agent.ts +4 -1
  182. package/templates/claude/src/lib/hooks/reply-instructions.ts +26 -1
  183. package/templates/codex/.init/.config/routes.json +3 -3
  184. package/templates/codex/.init/AGENTS.md +1 -1
  185. package/templates/codex/package.json.tmpl +4 -4
  186. package/templates/codex/src/agent.ts +11 -11
  187. package/templates/codex/src/lib/turn-context.ts +51 -0
  188. package/templates/pi/.init/.config/routes.json +3 -3
  189. package/templates/pi/.init/MINDS.md +1 -1
  190. package/templates/pi/package.json.tmpl +6 -6
  191. package/templates/pi/src/agent.ts +9 -1
  192. package/templates/pi/src/lib/event-handler.ts +1 -1
  193. package/templates/pi/src/lib/reply-instructions-extension.ts +28 -1
  194. package/dist/chunk-NJHN6IRB.js +0 -177
  195. package/dist/chunk-QQBOI3QA.js +0 -5345
  196. package/dist/chunk-XTEKINWG.js +0 -158
  197. package/dist/delivery-manager-RBXBPVWM.js +0 -36
  198. package/dist/echo-text-B6NZ5FGE.js +0 -36
  199. package/dist/message-delivery-ABPV76EA.js +0 -42
  200. package/dist/mind-service-42GDO4KH.js +0 -44
  201. package/dist/notices-N6BCGHFV.js +0 -25
  202. package/dist/scheduler-K7Y7Z7FQ.js +0 -34
  203. package/dist/sleep-manager-DSPXFHGX.js +0 -38
  204. package/dist/system-chat-M2XOBKEM.js +0 -40
  205. package/dist/variant-cleanup-P4MBNLJD.js +0 -33
  206. package/dist/web-assets/assets/index-BostMFoR.css +0 -1
  207. package/dist/web-assets/assets/index-CKlqUkyG.js +0 -92
  208. package/packages/extensions/notes/dist/ui/assets/index-D0-DgYyj.js +0 -67
  209. package/packages/extensions/pages/dist/ui/assets/index-DtzftuPA.js +0 -2
  210. package/packages/extensions/plan/dist/ui/assets/index-Cfzs-0G9.js +0 -67
@@ -0,0 +1,1222 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ tryGetMindManager
4
+ } from "./chunk-ALIVXDM4.js";
5
+ import {
6
+ extractTextContent,
7
+ getRoutingConfig,
8
+ resolveDeliveryMode,
9
+ resolveRoute,
10
+ setRoutesChangeListener,
11
+ shouldGate
12
+ } from "./chunk-6BM2I736.js";
13
+ import {
14
+ linkInboundToActiveTurn
15
+ } from "./chunk-7DXLIYPY.js";
16
+ import {
17
+ publish
18
+ } from "./chunk-LNNI3MS4.js";
19
+ import {
20
+ getParticipants,
21
+ publish as publish2
22
+ } from "./chunk-574M6I7F.js";
23
+ import {
24
+ onMindEvent
25
+ } from "./chunk-E5S3ILVA.js";
26
+ import {
27
+ renderAvatarBlock
28
+ } from "./chunk-X67VT4QO.js";
29
+ import {
30
+ readVoluteConfig
31
+ } from "./chunk-WCFSBH33.js";
32
+ import {
33
+ logger_default
34
+ } from "./chunk-QUSMYGNQ.js";
35
+ import {
36
+ findMind,
37
+ getBaseName,
38
+ getDb,
39
+ mindDir,
40
+ voluteHome
41
+ } from "./chunk-CAIYV33P.js";
42
+ import {
43
+ channelGates,
44
+ deliveryQueue,
45
+ messages,
46
+ mindHistory
47
+ } from "./chunk-5IIVOWSP.js";
48
+
49
+ // packages/daemon/src/lib/delivery/delivery-manager.ts
50
+ import { realpath } from "fs/promises";
51
+ import { resolve } from "path";
52
+ import { and as and2, eq as eq2, inArray, sql } from "drizzle-orm";
53
+
54
+ // packages/daemon/src/lib/chat/typing.ts
55
+ var DEFAULT_TTL_MS = 1e4;
56
+ var SWEEP_INTERVAL_MS = 5e3;
57
+ var TypingMap = class {
58
+ channels = /* @__PURE__ */ new Map();
59
+ sweepTimer;
60
+ constructor() {
61
+ this.sweepTimer = setInterval(() => this.sweep(), SWEEP_INTERVAL_MS);
62
+ this.sweepTimer.unref();
63
+ }
64
+ set(channel, sender, opts) {
65
+ const expiresAt = opts?.persistent ? Infinity : Date.now() + (opts?.ttlMs ?? DEFAULT_TTL_MS);
66
+ let senders = this.channels.get(channel);
67
+ if (!senders) {
68
+ senders = /* @__PURE__ */ new Map();
69
+ this.channels.set(channel, senders);
70
+ }
71
+ senders.set(sender, { expiresAt });
72
+ }
73
+ delete(channel, sender) {
74
+ const senders = this.channels.get(channel);
75
+ if (senders) {
76
+ senders.delete(sender);
77
+ if (senders.size === 0) {
78
+ this.channels.delete(channel);
79
+ }
80
+ }
81
+ }
82
+ /** Remove a sender from all channels (e.g. when a mind finishes processing). Returns affected channel names. */
83
+ deleteSender(sender) {
84
+ const affected = [];
85
+ for (const [channel, senders] of this.channels) {
86
+ if (senders.has(sender)) {
87
+ senders.delete(sender);
88
+ affected.push(channel);
89
+ }
90
+ if (senders.size === 0) {
91
+ this.channels.delete(channel);
92
+ }
93
+ }
94
+ return affected;
95
+ }
96
+ get(channel) {
97
+ const senders = this.channels.get(channel);
98
+ if (!senders) return [];
99
+ const now = Date.now();
100
+ const result = [];
101
+ for (const [sender, entry] of senders) {
102
+ if (entry.expiresAt > now) {
103
+ result.push(sender);
104
+ }
105
+ }
106
+ return result;
107
+ }
108
+ dispose() {
109
+ clearInterval(this.sweepTimer);
110
+ this.channels.clear();
111
+ if (instance === this) instance = void 0;
112
+ }
113
+ sweep() {
114
+ const now = Date.now();
115
+ for (const [channel, senders] of this.channels) {
116
+ for (const [sender, entry] of senders) {
117
+ if (entry.expiresAt <= now) {
118
+ senders.delete(sender);
119
+ }
120
+ }
121
+ if (senders.size === 0) {
122
+ this.channels.delete(channel);
123
+ }
124
+ }
125
+ }
126
+ };
127
+ var instance;
128
+ function getTypingMap() {
129
+ if (!instance) {
130
+ instance = new TypingMap();
131
+ }
132
+ return instance;
133
+ }
134
+ function isConversationId(channel) {
135
+ return !channel.startsWith("@") && !channel.startsWith("#") && !channel.includes(":") && !channel.includes("/");
136
+ }
137
+ function publishTypingForChannels(channels, map) {
138
+ for (const channel of channels) {
139
+ if (isConversationId(channel)) {
140
+ publish2(channel, { type: "typing", senders: map.get(channel) });
141
+ }
142
+ }
143
+ }
144
+
145
+ // packages/daemon/src/lib/delivery/send-gate.ts
146
+ import { and, asc, desc, eq, gt } from "drizzle-orm";
147
+ var state = /* @__PURE__ */ new Map();
148
+ function extractText(raw) {
149
+ if (!raw) return "";
150
+ try {
151
+ const blocks = JSON.parse(raw);
152
+ if (Array.isArray(blocks)) {
153
+ return blocks.filter((b) => b?.type === "text" && typeof b.text === "string").map((b) => b.text).join(" ").trim();
154
+ }
155
+ } catch {
156
+ }
157
+ return String(raw);
158
+ }
159
+ async function maxMessageId(conversationId) {
160
+ const db = await getDb();
161
+ const rows = await db.select({ id: messages.id }).from(messages).where(eq(messages.conversation_id, conversationId)).orderBy(desc(messages.id)).limit(1);
162
+ return rows[0]?.id ?? 0;
163
+ }
164
+ async function onDeliveredToMind(mindBase, conversationId) {
165
+ if (!conversationId) return;
166
+ let conv = state.get(mindBase);
167
+ if (!conv) {
168
+ conv = /* @__PURE__ */ new Map();
169
+ state.set(mindBase, conv);
170
+ }
171
+ const existing = conv.get(conversationId);
172
+ if (existing?.turnOpen) return;
173
+ const baselineId = await maxMessageId(conversationId);
174
+ conv.set(conversationId, { baselineId, heldThisTurn: false, turnOpen: true });
175
+ }
176
+ async function checkStaleSend(mindBase, conversationId, excludeSenders) {
177
+ const entry = state.get(mindBase)?.get(conversationId);
178
+ if (!entry?.turnOpen || entry.heldThisTurn) return { held: false };
179
+ const db = await getDb();
180
+ const rows = await db.select({ id: messages.id, sender: messages.sender_name, content: messages.content }).from(messages).where(and(eq(messages.conversation_id, conversationId), gt(messages.id, entry.baselineId))).orderBy(asc(messages.id));
181
+ const excluded = new Set(excludeSenders.filter(Boolean));
182
+ const unseen = rows.filter((r) => r.sender && !excluded.has(r.sender));
183
+ if (unseen.length === 0) return { held: false };
184
+ entry.heldThisTurn = true;
185
+ entry.baselineId = rows[rows.length - 1].id;
186
+ return {
187
+ held: true,
188
+ unseen: unseen.map((r) => ({ sender: r.sender, text: extractText(r.content) }))
189
+ };
190
+ }
191
+ function clearMind(mindBase) {
192
+ state.delete(mindBase);
193
+ }
194
+ function resetTurn(mindBase) {
195
+ clearMind(mindBase);
196
+ }
197
+ function formatHoldNotice(channelLabel, unseen) {
198
+ const lines = unseen.map((m) => {
199
+ const t = m.text.length > 120 ? `${m.text.slice(0, 119)}\u2026` : m.text;
200
+ return ` ${m.sender}: ${JSON.stringify(t)}`;
201
+ });
202
+ const n = unseen.length;
203
+ return [
204
+ `\u26A0 Not sent \u2014 ${n} newer message${n === 1 ? "" : "s"} arrived on ${channelLabel} while you were composing:`,
205
+ ...lines,
206
+ "Your message was NOT posted. Revise it in light of these, or run the same send again to post as-is."
207
+ ].join("\n");
208
+ }
209
+
210
+ // packages/daemon/src/lib/delivery/delivery-manager.ts
211
+ var dlog = logger_default.child("delivery-manager");
212
+ var MAX_BATCH_SIZE = 50;
213
+ var REDRIVE_INTERVAL_MS = 15e3;
214
+ var RETRY_BASE_MS = 5e3;
215
+ var RETRY_MAX_MS = 5 * 6e4;
216
+ var REDRIVE_BATCH_LIMIT = 200;
217
+ var GATED_RELEASE_LIMIT_PER_CHANNEL = 10;
218
+ var GATED_NOTIFY_EVERY = 10;
219
+ var mentionRegexCache = /* @__PURE__ */ new Map();
220
+ var avatarBlocksCache = /* @__PURE__ */ new Map();
221
+ var AVATAR_CACHE_TTL = 5 * 60 * 1e3;
222
+ var DeliveryManager = class {
223
+ sessionStates = /* @__PURE__ */ new Map();
224
+ batchBuffers = /* @__PURE__ */ new Map();
225
+ /**
226
+ * delivery_queue row ids currently owned in-memory — either buffered in a batch
227
+ * buffer or actively being POSTed. The redrive sweep skips these so it never
228
+ * double-delivers a row that the normal path is already handling.
229
+ */
230
+ inFlight = /* @__PURE__ */ new Set();
231
+ /**
232
+ * Per-`(baseName:session)` promise chain that serializes POSTs so two rapid
233
+ * messages to the same session can't be reordered by resolvePort/enrichment latency.
234
+ */
235
+ drainChains = /* @__PURE__ */ new Map();
236
+ redriveTimer = null;
237
+ /** Predicate for whether a mind is up; overridable in tests. */
238
+ isMindRunning = (name) => tryGetMindManager()?.isRunning(name) ?? false;
239
+ /** Delivers a channel event to a mind (invites, release summaries); overridable in tests. */
240
+ notify = async (mindName, text) => {
241
+ const { deliverEvent } = await import("./system-events-A6IGCEC3.js");
242
+ await deliverEvent(mindName, { type: "channel", body: text });
243
+ };
244
+ constructor() {
245
+ setRoutesChangeListener((mind) => {
246
+ this.releaseGated(mind).catch(
247
+ (err) => dlog.warn(`failed to release gated messages for ${mind}`, logger_default.errorData(err))
248
+ );
249
+ });
250
+ }
251
+ /** Test seam: override the mind-running predicate. */
252
+ setRunningCheck(fn) {
253
+ this.isMindRunning = fn;
254
+ }
255
+ /** Test seam: capture/override system notifications sent to minds. */
256
+ setNotifier(fn) {
257
+ this.notify = fn;
258
+ }
259
+ // --- Public API ---
260
+ /**
261
+ * Route and deliver a message to a mind. This is the main entry point.
262
+ * The message is routed via the mind's routes.json, then either delivered immediately
263
+ * or queued for batching depending on the session's delivery mode.
264
+ */
265
+ async routeAndDeliver(mindName, payload) {
266
+ const baseName = await getBaseName(mindName);
267
+ const config = getRoutingConfig(baseName);
268
+ if (payload.session) {
269
+ let sessionName2 = payload.session;
270
+ if (sessionName2 === "$new") {
271
+ sessionName2 = `new-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
272
+ }
273
+ const sessionConfig2 = resolveDeliveryMode(config, sessionName2);
274
+ if (sessionConfig2.delivery.mode === "batch") {
275
+ await this.enqueueBatch(mindName, sessionName2, payload, sessionConfig2);
276
+ return { routed: true, session: sessionName2, destination: "mind", mode: "batch" };
277
+ }
278
+ const queueId2 = await this.persistToQueue(mindName, sessionName2, payload);
279
+ await this.deliverToMind(mindName, sessionName2, payload, sessionConfig2, queueId2);
280
+ return { routed: true, session: sessionName2, destination: "mind", mode: "immediate" };
281
+ }
282
+ const meta = {
283
+ channel: payload.channel,
284
+ sender: payload.sender ?? void 0,
285
+ isDM: payload.isDM,
286
+ participantCount: payload.participantCount
287
+ };
288
+ const route = resolveRoute(config, meta);
289
+ dlog.debug(
290
+ `route for ${mindName} ch=${payload.channel}: dest=${route.destination} matched=${route.matched}`
291
+ );
292
+ if (route.destination === "file") {
293
+ return { routed: true, session: route.path, destination: "file", mode: "immediate" };
294
+ }
295
+ if (shouldGate(config, route)) {
296
+ dlog.debug(`gating unmatched channel ${payload.channel} for ${mindName}`);
297
+ await this.gateMessage(mindName, route.session, payload);
298
+ return { routed: true, session: route.session, destination: "mind", mode: "gated" };
299
+ }
300
+ if (route.mode === "mention" && payload.sender) {
301
+ const text = extractTextContent(payload.content);
302
+ let pattern = mentionRegexCache.get(baseName);
303
+ if (!pattern) {
304
+ const escaped = baseName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
305
+ pattern = new RegExp(`\\b${escaped}\\b`, "i");
306
+ mentionRegexCache.set(baseName, pattern);
307
+ }
308
+ if (!pattern.test(text)) {
309
+ dlog.debug(`mention-filtered message on ${payload.channel} for ${mindName}`);
310
+ return { routed: false, reason: "mention-filtered" };
311
+ }
312
+ }
313
+ let sessionName = route.session;
314
+ if (sessionName === "$new") {
315
+ sessionName = `new-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
316
+ }
317
+ const sessionConfig = resolveDeliveryMode(config, sessionName, route.rule);
318
+ if (sessionConfig.delivery.mode === "batch") {
319
+ dlog.debug(`enqueueing batch message for ${mindName}/${sessionName}`);
320
+ await this.enqueueBatch(mindName, sessionName, payload, sessionConfig);
321
+ return { routed: true, session: sessionName, destination: "mind", mode: "batch" };
322
+ }
323
+ const queueId = await this.persistToQueue(mindName, sessionName, payload);
324
+ await this.deliverToMind(mindName, sessionName, payload, sessionConfig, queueId);
325
+ return { routed: true, session: sessionName, destination: "mind", mode: "immediate" };
326
+ }
327
+ /**
328
+ * Called when a mind's session emits a "done" event — decrements active count
329
+ * and may trigger batch flush if session goes idle.
330
+ *
331
+ * This method is intentionally synchronous to avoid race conditions: the caller
332
+ * has already resolved baseName, and any async yield here (e.g. getBaseName)
333
+ * would allow concurrent deliveries to incrementActive before the decrement runs,
334
+ * causing isSessionBusy to return true even when no deliveries are pending.
335
+ */
336
+ sessionDone(baseName, session) {
337
+ resetTurn(baseName);
338
+ if (session) {
339
+ this.decrementActive(baseName, session);
340
+ } else {
341
+ const mindSessions = this.sessionStates.get(baseName);
342
+ if (mindSessions) {
343
+ for (const [sessionName] of mindSessions) {
344
+ this.decrementActive(baseName, sessionName);
345
+ }
346
+ }
347
+ }
348
+ }
349
+ /**
350
+ * Restore queued messages from DB on daemon restart — a single redrive pass.
351
+ * All accepted deliveries are persisted to delivery_queue before their POST and
352
+ * only deleted on mind-ack, so pending rows are exactly the undelivered messages.
353
+ */
354
+ async restoreFromDb() {
355
+ await this.redrive();
356
+ }
357
+ /** Start the periodic redrive sweep (idempotent). */
358
+ startRedrive() {
359
+ if (this.redriveTimer) return;
360
+ this.redriveTimer = setInterval(() => {
361
+ this.redrive().catch((err) => dlog.warn("redrive sweep failed", logger_default.errorData(err)));
362
+ }, REDRIVE_INTERVAL_MS);
363
+ this.redriveTimer.unref();
364
+ }
365
+ /**
366
+ * Re-read pending delivery_queue rows that are eligible (past their backoff window)
367
+ * and re-deliver them through the normal path. Rows already owned in-memory
368
+ * (`inFlight`) and minds that are down are skipped so we never hot-loop or double-send.
369
+ */
370
+ async redrive() {
371
+ let rows;
372
+ try {
373
+ const db = await getDb();
374
+ rows = await db.select().from(deliveryQueue).where(
375
+ and2(
376
+ eq2(deliveryQueue.status, "pending"),
377
+ sql`(${deliveryQueue.next_attempt_at} IS NULL OR ${deliveryQueue.next_attempt_at} <= datetime('now'))`
378
+ )
379
+ ).orderBy(deliveryQueue.id).limit(REDRIVE_BATCH_LIMIT);
380
+ } catch (err) {
381
+ dlog.warn("failed to read delivery queue for redrive", logger_default.errorData(err));
382
+ return;
383
+ }
384
+ let redriven = 0;
385
+ for (const row of rows) {
386
+ if (this.inFlight.has(row.id)) continue;
387
+ if (!this.isMindRunning(row.mind)) continue;
388
+ let payload;
389
+ try {
390
+ payload = JSON.parse(row.payload);
391
+ } catch (parseErr) {
392
+ dlog.warn(
393
+ `corrupt payload in delivery queue row ${row.id}, dropping`,
394
+ logger_default.errorData(parseErr)
395
+ );
396
+ await this.deleteQueueRows([row.id]);
397
+ continue;
398
+ }
399
+ const config = getRoutingConfig(row.mind);
400
+ const sessionConfig = resolveDeliveryMode(config, row.thread);
401
+ const target = row.target_mind ?? row.mind;
402
+ if (sessionConfig.delivery.mode === "batch") {
403
+ this.inFlight.add(row.id);
404
+ this.addToBatchBuffer(target, row.thread, sessionConfig, {
405
+ payload,
406
+ channel: payload.channel,
407
+ sender: payload.sender ?? null,
408
+ createdAt: Date.now(),
409
+ queueId: row.id
410
+ });
411
+ } else {
412
+ this.deliverToMind(target, row.thread, payload, sessionConfig, row.id).catch((err) => {
413
+ dlog.warn(`failed to redrive delivery for ${target}`, logger_default.errorData(err));
414
+ });
415
+ }
416
+ redriven++;
417
+ }
418
+ if (redriven > 0) dlog.info(`redrove ${redriven} pending delivery queue rows`);
419
+ }
420
+ /**
421
+ * Re-evaluate a mind's `gated` rows against its current routes.json when the routing
422
+ * config changes. For each channel that now matches a `mind` route:
423
+ * - the newest {@link GATED_RELEASE_LIMIT_PER_CHANNEL} rows are promoted to `pending`
424
+ * and re-stamped with the freshly-resolved session (NOT the gate-time fallback), so
425
+ * they land in the correct session instead of `main` (#537 bug 1);
426
+ * - any older rows are `archived` (inert) so a long backlog can't flood the mind
427
+ * in one sweep (#537 bug 2), and the mind gets one summary rather than a flood.
428
+ * Rows resolving to a `file` destination are archived (the delivery manager doesn't
429
+ * deliver file routes). Declined channels are skipped entirely — they stay gated.
430
+ */
431
+ async releaseGated(mindName) {
432
+ const baseName = await getBaseName(mindName);
433
+ const config = getRoutingConfig(baseName);
434
+ let rows;
435
+ try {
436
+ const db = await getDb();
437
+ rows = await db.select().from(deliveryQueue).where(and2(eq2(deliveryQueue.mind, baseName), eq2(deliveryQueue.status, "gated")));
438
+ } catch (err) {
439
+ dlog.warn(`failed to read gated rows for ${baseName}`, logger_default.errorData(err));
440
+ return;
441
+ }
442
+ const byChannel = /* @__PURE__ */ new Map();
443
+ const archiveIds = [];
444
+ for (const row of rows) {
445
+ let payload;
446
+ try {
447
+ payload = JSON.parse(row.payload);
448
+ } catch {
449
+ continue;
450
+ }
451
+ const meta = {
452
+ channel: payload.channel,
453
+ sender: payload.sender ?? void 0,
454
+ isDM: payload.isDM,
455
+ participantCount: payload.participantCount
456
+ };
457
+ const route = resolveRoute(config, meta);
458
+ if (!route.matched) continue;
459
+ if (route.destination === "file") {
460
+ archiveIds.push(row.id);
461
+ continue;
462
+ }
463
+ let session = route.session;
464
+ if (session === "$new") {
465
+ session = `new-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
466
+ }
467
+ const channel = row.channel ?? payload.channel ?? "unknown";
468
+ const list = byChannel.get(channel) ?? [];
469
+ list.push({
470
+ id: row.id,
471
+ session,
472
+ channel,
473
+ sender: payload.sender ?? row.sender ?? null,
474
+ content: extractTextContent(payload.content)
475
+ });
476
+ byChannel.set(channel, list);
477
+ }
478
+ const promote = [];
479
+ const truncationNotes = [];
480
+ for (const [channel, items] of byChannel) {
481
+ if (await this.isChannelDeclined(baseName, channel)) continue;
482
+ items.sort((a, b) => b.id - a.id);
483
+ const keep = items.slice(0, GATED_RELEASE_LIMIT_PER_CHANNEL);
484
+ const drop = items.slice(GATED_RELEASE_LIMIT_PER_CHANNEL);
485
+ promote.push(...keep);
486
+ for (const d of drop) archiveIds.push(d.id);
487
+ if (drop.length > 0) {
488
+ truncationNotes.push(
489
+ `${channel}: released the ${keep.length} most recent message(s); ${drop.length} earlier message(s) were held while unrouted and remain in the channel history (volute chat read ${channel}).`
490
+ );
491
+ dlog.info(
492
+ `truncated gated release for ${baseName} on ${channel}: kept ${keep.length}, archived ${drop.length}`
493
+ );
494
+ }
495
+ }
496
+ if (archiveIds.length > 0) {
497
+ try {
498
+ const db = await getDb();
499
+ for (let i = 0; i < archiveIds.length; i += 500) {
500
+ await db.update(deliveryQueue).set({ status: "archived" }).where(inArray(deliveryQueue.id, archiveIds.slice(i, i + 500)));
501
+ }
502
+ } catch (err) {
503
+ dlog.warn(`failed to archive gated rows for ${baseName}`, logger_default.errorData(err));
504
+ }
505
+ }
506
+ if (promote.length === 0) {
507
+ if (truncationNotes.length > 0) await this.sendReleaseSummary(mindName, truncationNotes);
508
+ return;
509
+ }
510
+ const orderedPromote = [...promote].sort((a, b) => a.id - b.id);
511
+ try {
512
+ const db = await getDb();
513
+ for (const p of orderedPromote) {
514
+ await db.transaction(async (tx) => {
515
+ await tx.insert(mindHistory).values({
516
+ mind: baseName,
517
+ type: "inbound",
518
+ channel: p.channel,
519
+ sender: p.sender,
520
+ content: p.content
521
+ });
522
+ await tx.update(deliveryQueue).set({ status: "pending", thread: p.session, attempts: 0, next_attempt_at: null }).where(eq2(deliveryQueue.id, p.id));
523
+ });
524
+ }
525
+ dlog.info(`released ${promote.length} gated message(s) for ${baseName} after route change`);
526
+ } catch (err) {
527
+ dlog.error(
528
+ `failed to record+promote gated rows for ${baseName} (channels: ${[...byChannel.keys()].join(", ")})`,
529
+ logger_default.errorData(err)
530
+ );
531
+ return;
532
+ }
533
+ for (const p of orderedPromote) {
534
+ publish(baseName, {
535
+ mind: baseName,
536
+ type: "inbound",
537
+ channel: p.channel,
538
+ content: p.content ?? void 0,
539
+ sender: p.sender ?? void 0
540
+ });
541
+ }
542
+ if (truncationNotes.length > 0) await this.sendReleaseSummary(mindName, truncationNotes);
543
+ await this.redrive();
544
+ }
545
+ /**
546
+ * Whether the mind has explicitly declined a channel. A declined channel keeps
547
+ * persisting history but never notifies and is never released. #537
548
+ */
549
+ async isChannelDeclined(baseName, channel) {
550
+ if (!channel) return false;
551
+ try {
552
+ const db = await getDb();
553
+ const rows = await db.select({ state: channelGates.state }).from(channelGates).where(and2(eq2(channelGates.mind, baseName), eq2(channelGates.channel, channel)));
554
+ return rows[0]?.state === "declined";
555
+ } catch (err) {
556
+ dlog.warn(`failed to read gate state for ${baseName}/${channel}`, logger_default.errorData(err));
557
+ return false;
558
+ }
559
+ }
560
+ /**
561
+ * Record that a mind has declined an unrouted channel: future messages are still
562
+ * persisted (history is preserved) but never notify, and any currently-gated rows are
563
+ * archived so they're inert. Returns the number of held messages archived. #537
564
+ */
565
+ async declineChannel(mindName, channel) {
566
+ const baseName = await getBaseName(mindName);
567
+ const db = await getDb();
568
+ await db.insert(channelGates).values({ mind: baseName, channel, state: "declined" }).onConflictDoUpdate({
569
+ target: [channelGates.mind, channelGates.channel],
570
+ set: { state: "declined", updated_at: sql`(datetime('now'))` }
571
+ });
572
+ const archived = await db.update(deliveryQueue).set({ status: "archived" }).where(
573
+ and2(
574
+ eq2(deliveryQueue.mind, baseName),
575
+ eq2(deliveryQueue.channel, channel),
576
+ eq2(deliveryQueue.status, "gated")
577
+ )
578
+ ).returning({ id: deliveryQueue.id });
579
+ dlog.info(
580
+ `declined channel ${channel} for ${baseName}; archived ${archived.length} held row(s)`
581
+ );
582
+ return archived.length;
583
+ }
584
+ /**
585
+ * Send the mind a single summary when a routing change released a truncated backlog,
586
+ * rather than a flood of individual messages. #537
587
+ */
588
+ async sendReleaseSummary(mindName, notes) {
589
+ const body = [
590
+ `[Channel backlog released]`,
591
+ `A routing change matched channel(s) that had held messages while unrouted. To avoid flooding you, only the ${GATED_RELEASE_LIMIT_PER_CHANNEL} most recent per channel were delivered:`,
592
+ "",
593
+ ...notes.map((n) => `- ${n}`)
594
+ ].join("\n");
595
+ try {
596
+ await this.notify(mindName, body);
597
+ } catch (err) {
598
+ dlog.warn(`failed to send release summary for ${mindName}`, logger_default.errorData(err));
599
+ }
600
+ }
601
+ /**
602
+ * Get pending (gated) messages for a mind.
603
+ */
604
+ async getPending(mindName) {
605
+ const db = await getDb();
606
+ const rows = await db.select().from(deliveryQueue).where(and2(eq2(deliveryQueue.mind, mindName), eq2(deliveryQueue.status, "gated")));
607
+ const byChannel = /* @__PURE__ */ new Map();
608
+ for (const row of rows) {
609
+ const ch = row.channel ?? "unknown";
610
+ const existing = byChannel.get(ch) ?? [];
611
+ existing.push(row);
612
+ byChannel.set(ch, existing);
613
+ }
614
+ return [...byChannel.entries()].map(([channel, channelRows]) => {
615
+ const firstRow = channelRows[0];
616
+ const payload = JSON.parse(firstRow.payload);
617
+ const text = extractTextContent(payload.content);
618
+ return {
619
+ channel,
620
+ sender: firstRow.sender,
621
+ count: channelRows.length,
622
+ firstSeen: firstRow.created_at,
623
+ preview: text.length > 200 ? `${text.slice(0, 200)}...` : text
624
+ };
625
+ });
626
+ }
627
+ /**
628
+ * Check if a session is currently busy (has active deliveries).
629
+ */
630
+ isSessionBusy(mindName, session) {
631
+ const state2 = this.sessionStates.get(mindName)?.get(session);
632
+ return (state2?.activeCount ?? 0) > 0;
633
+ }
634
+ /**
635
+ * Check if any session for a mind is currently busy.
636
+ */
637
+ isMindBusy(mindName) {
638
+ const mindSessions = this.sessionStates.get(mindName);
639
+ if (!mindSessions) return false;
640
+ for (const [, state2] of mindSessions) {
641
+ if (state2.activeCount > 0) return true;
642
+ }
643
+ return false;
644
+ }
645
+ /**
646
+ * Clear all session state for a specific mind (called on mind stop/crash).
647
+ * Resets active counts, clears typing indicators, and cleans up batch buffers
648
+ * so ghost state doesn't accumulate.
649
+ */
650
+ clearMindSessions(mindName) {
651
+ this.sessionStates.delete(mindName);
652
+ clearMind(mindName);
653
+ const typingMap = getTypingMap();
654
+ publishTypingForChannels(typingMap.deleteSender(mindName), typingMap);
655
+ const toDelete = [];
656
+ for (const [bufferKey, buffer] of this.batchBuffers) {
657
+ if (bufferKey.startsWith(`${mindName}:`)) {
658
+ if (buffer.debounceTimer) clearTimeout(buffer.debounceTimer);
659
+ if (buffer.maxWaitTimer) clearTimeout(buffer.maxWaitTimer);
660
+ for (const msg of buffer.messages) {
661
+ if (msg.queueId != null) this.inFlight.delete(msg.queueId);
662
+ }
663
+ toDelete.push(bufferKey);
664
+ }
665
+ }
666
+ for (const k of toDelete) this.batchBuffers.delete(k);
667
+ }
668
+ /**
669
+ * Reset a single session's leaked active count back to zero.
670
+ *
671
+ * Used by the wedged-turn sweep: when a session's `activeCount` drifts above zero
672
+ * (deliveries outnumbering `done`s) it gates turn completion indefinitely. Once the sweep
673
+ * confirms the session is genuinely idle, this clears the stale count so the next turn
674
+ * can complete normally. Batch buffers are left intact — their own maxWait timer flushes
675
+ * any pending messages.
676
+ *
677
+ * `minIdleMs` guards against a race: if a delivery landed within that window, a fresh turn
678
+ * may legitimately be in flight (real `activeCount`), so zeroing would complete it early.
679
+ * In that case we skip — the next sweep retries if it's still wedged. Returns whether the
680
+ * count was reset.
681
+ */
682
+ clearSessionActive(mindName, session, minIdleMs) {
683
+ const state2 = this.sessionStates.get(mindName)?.get(session);
684
+ if (!state2) return false;
685
+ if (Date.now() - state2.lastDeliveredAt < minIdleMs) return false;
686
+ state2.activeCount = 0;
687
+ return true;
688
+ }
689
+ /**
690
+ * Cleanup all timers and subscriptions.
691
+ */
692
+ dispose() {
693
+ for (const [, buffer] of this.batchBuffers) {
694
+ if (buffer.debounceTimer) clearTimeout(buffer.debounceTimer);
695
+ if (buffer.maxWaitTimer) clearTimeout(buffer.maxWaitTimer);
696
+ }
697
+ this.batchBuffers.clear();
698
+ this.sessionStates.clear();
699
+ this.inFlight.clear();
700
+ this.drainChains.clear();
701
+ if (this.redriveTimer) {
702
+ clearInterval(this.redriveTimer);
703
+ this.redriveTimer = null;
704
+ }
705
+ setRoutesChangeListener(void 0);
706
+ if (instance2 === this) instance2 = void 0;
707
+ }
708
+ // --- Private ---
709
+ async resolvePort(mindName) {
710
+ const entry = await findMind(mindName);
711
+ if (!entry) return null;
712
+ const baseName = entry.parent ?? mindName;
713
+ return { baseName, port: entry.port };
714
+ }
715
+ async postToMind(port, body) {
716
+ const controller = new AbortController();
717
+ const timeout = setTimeout(() => controller.abort(), 12e4);
718
+ try {
719
+ const res = await fetch(`http://127.0.0.1:${port}/message`, {
720
+ method: "POST",
721
+ headers: { "Content-Type": "application/json" },
722
+ body,
723
+ signal: controller.signal
724
+ });
725
+ if (!res.ok) {
726
+ const text = await res.text().catch(() => "");
727
+ dlog.warn(`mind responded ${res.status}: ${text}`);
728
+ return false;
729
+ }
730
+ await res.text().catch(() => {
731
+ });
732
+ return true;
733
+ } finally {
734
+ clearTimeout(timeout);
735
+ }
736
+ }
737
+ /**
738
+ * Serialize `fn` on a per-key promise chain so calls for the same key run one at a
739
+ * time in submission order. Used to drain a `(mind, session)` sequentially.
740
+ */
741
+ runSequential(key, fn) {
742
+ const prev = this.drainChains.get(key) ?? Promise.resolve();
743
+ const run = prev.then(fn, fn);
744
+ const tail = run.then(
745
+ () => {
746
+ },
747
+ () => {
748
+ }
749
+ );
750
+ this.drainChains.set(key, tail);
751
+ tail.then(() => {
752
+ if (this.drainChains.get(key) === tail) this.drainChains.delete(key);
753
+ });
754
+ return run;
755
+ }
756
+ /** Delete delivered queue rows by their specific ids. */
757
+ async deleteQueueRows(ids) {
758
+ const valid = [...new Set(ids.filter((id) => typeof id === "number"))];
759
+ if (valid.length === 0) return;
760
+ try {
761
+ const db = await getDb();
762
+ await db.delete(deliveryQueue).where(inArray(deliveryQueue.id, valid));
763
+ } catch (err) {
764
+ dlog.warn("failed to delete delivered delivery queue rows", logger_default.errorData(err));
765
+ }
766
+ }
767
+ /** Bump attempt counters and set a backoff window so a down mind isn't hot-looped. */
768
+ async scheduleRetry(ids) {
769
+ const valid = [...new Set(ids.filter((id) => typeof id === "number"))];
770
+ if (valid.length === 0) return;
771
+ try {
772
+ const db = await getDb();
773
+ const rows = await db.select({ id: deliveryQueue.id, attempts: deliveryQueue.attempts }).from(deliveryQueue).where(inArray(deliveryQueue.id, valid));
774
+ for (const row of rows) {
775
+ const attempts = row.attempts + 1;
776
+ const backoffSec = Math.round(
777
+ Math.min(RETRY_MAX_MS, RETRY_BASE_MS * 2 ** Math.min(attempts, 20)) / 1e3
778
+ );
779
+ await db.update(deliveryQueue).set({ attempts, next_attempt_at: sql`datetime('now', ${`+${backoffSec} seconds`})` }).where(eq2(deliveryQueue.id, row.id));
780
+ }
781
+ } catch (err) {
782
+ dlog.warn("failed to schedule delivery retry", logger_default.errorData(err));
783
+ }
784
+ }
785
+ async deliverToMind(mindName, session, payload, sessionConfig, queueId) {
786
+ if (queueId != null) this.inFlight.add(queueId);
787
+ await this.runSequential(`${mindName}:${session}`, async () => {
788
+ const resolved = await this.resolvePort(mindName);
789
+ if (!resolved) {
790
+ dlog.warn(`cannot deliver to ${mindName}: mind not found`);
791
+ if (queueId != null) this.inFlight.delete(queueId);
792
+ return;
793
+ }
794
+ const { baseName, port } = resolved;
795
+ const senders = /* @__PURE__ */ new Set();
796
+ if (payload.sender) senders.add(payload.sender);
797
+ const channels = /* @__PURE__ */ new Set();
798
+ if (payload.channel) channels.add(payload.channel);
799
+ this.incrementActive(baseName, session, senders, channels);
800
+ await onDeliveredToMind(baseName, payload.conversationId);
801
+ linkInboundToActiveTurn(baseName, session, payload.channel).catch(
802
+ (err) => dlog.warn(`failed to link mid-turn inbound for ${baseName}`, logger_default.errorData(err))
803
+ );
804
+ const typingMap = getTypingMap();
805
+ if (payload.channel) {
806
+ typingMap.set(payload.channel, baseName, { persistent: true });
807
+ }
808
+ if (payload.conversationId) {
809
+ typingMap.set(payload.conversationId, baseName, { persistent: true });
810
+ publishTypingForChannels([payload.conversationId], typingMap);
811
+ }
812
+ onMindEvent(baseName, "delivery", payload.channel);
813
+ const enrichedPayload = await this.enrichWithProfiles(baseName, session, payload);
814
+ const body = JSON.stringify({
815
+ ...enrichedPayload,
816
+ session,
817
+ instructions: sessionConfig.instructions
818
+ });
819
+ try {
820
+ const ok = await this.postToMind(port, body);
821
+ if (!ok) {
822
+ this.decrementActive(baseName, session);
823
+ publishTypingForChannels(typingMap.deleteSender(baseName), typingMap);
824
+ await this.scheduleRetry([queueId]);
825
+ } else {
826
+ await this.deleteQueueRows([queueId]);
827
+ }
828
+ } catch (err) {
829
+ dlog.warn(`failed to deliver to ${mindName}`, logger_default.errorData(err));
830
+ this.decrementActive(baseName, session);
831
+ publishTypingForChannels(typingMap.deleteSender(baseName), typingMap);
832
+ await this.scheduleRetry([queueId]);
833
+ } finally {
834
+ if (queueId != null) this.inFlight.delete(queueId);
835
+ }
836
+ });
837
+ }
838
+ async deliverBatchToMind(mindName, session, messages2, sessionConfig) {
839
+ const queueIds = messages2.map((m) => m.queueId).filter((id) => typeof id === "number");
840
+ await this.runSequential(`${mindName}:${session}`, async () => {
841
+ const resolved = await this.resolvePort(mindName);
842
+ if (!resolved) {
843
+ dlog.warn(`cannot deliver batch to ${mindName}: mind not found`);
844
+ for (const id of queueIds) this.inFlight.delete(id);
845
+ return;
846
+ }
847
+ const { baseName, port } = resolved;
848
+ const firstPerChannel = /* @__PURE__ */ new Set();
849
+ const isFirstForChannel = [];
850
+ for (const msg of messages2) {
851
+ const ch = msg.channel ?? "unknown";
852
+ isFirstForChannel.push(!firstPerChannel.has(ch));
853
+ firstPerChannel.add(ch);
854
+ }
855
+ const enrichedMessages = await Promise.all(
856
+ messages2.map(async (msg, i) => {
857
+ if (!isFirstForChannel[i]) return msg;
858
+ const enrichedPayload = await this.enrichWithProfiles(baseName, session, msg.payload);
859
+ return { ...msg, payload: enrichedPayload };
860
+ })
861
+ );
862
+ const channels = {};
863
+ for (const msg of enrichedMessages) {
864
+ const ch = msg.channel ?? "unknown";
865
+ if (!channels[ch]) channels[ch] = [];
866
+ channels[ch].push(msg.payload);
867
+ }
868
+ const senders = /* @__PURE__ */ new Set();
869
+ const channelSet = /* @__PURE__ */ new Set();
870
+ for (const msg of messages2) {
871
+ if (msg.sender) senders.add(msg.sender);
872
+ if (msg.channel) channelSet.add(msg.channel);
873
+ }
874
+ this.incrementActive(baseName, session, senders, channelSet);
875
+ const convIds = /* @__PURE__ */ new Set();
876
+ for (const msg of messages2) {
877
+ if (msg.payload.conversationId) convIds.add(msg.payload.conversationId);
878
+ }
879
+ for (const convId of convIds) {
880
+ await onDeliveredToMind(baseName, convId);
881
+ }
882
+ for (const ch of channelSet) {
883
+ linkInboundToActiveTurn(baseName, session, ch).catch(
884
+ (err) => dlog.warn(`failed to link mid-turn inbound for ${baseName}`, logger_default.errorData(err))
885
+ );
886
+ }
887
+ const typingMap = getTypingMap();
888
+ for (const ch of Object.keys(channels)) {
889
+ if (ch !== "unknown") typingMap.set(ch, baseName, { persistent: true });
890
+ }
891
+ const seenConvIds = /* @__PURE__ */ new Set();
892
+ for (const msg of messages2) {
893
+ if (msg.payload.conversationId && !seenConvIds.has(msg.payload.conversationId)) {
894
+ seenConvIds.add(msg.payload.conversationId);
895
+ typingMap.set(msg.payload.conversationId, baseName, { persistent: true });
896
+ }
897
+ }
898
+ if (seenConvIds.size > 0) {
899
+ publishTypingForChannels([...seenConvIds], typingMap);
900
+ }
901
+ const body = JSON.stringify({
902
+ session,
903
+ batch: { channels },
904
+ instructions: sessionConfig.instructions
905
+ });
906
+ try {
907
+ const ok = await this.postToMind(port, body);
908
+ if (!ok) {
909
+ this.decrementActive(baseName, session);
910
+ publishTypingForChannels(typingMap.deleteSender(baseName), typingMap);
911
+ await this.scheduleRetry(queueIds);
912
+ } else {
913
+ await this.deleteQueueRows(queueIds);
914
+ }
915
+ } catch (err) {
916
+ dlog.warn(`failed to deliver batch to ${mindName}`, logger_default.errorData(err));
917
+ this.decrementActive(baseName, session);
918
+ publishTypingForChannels(typingMap.deleteSender(baseName), typingMap);
919
+ await this.scheduleRetry(queueIds);
920
+ } finally {
921
+ for (const id of queueIds) this.inFlight.delete(id);
922
+ }
923
+ });
924
+ }
925
+ async enqueueBatch(mindName, session, payload, sessionConfig) {
926
+ const delivery = sessionConfig.delivery;
927
+ const queueId = await this.persistToQueue(mindName, session, payload);
928
+ if (queueId != null) this.inFlight.add(queueId);
929
+ const msg = {
930
+ payload,
931
+ channel: payload.channel,
932
+ sender: payload.sender ?? null,
933
+ createdAt: Date.now(),
934
+ queueId
935
+ };
936
+ if (delivery.triggers?.length) {
937
+ const text = extractTextContent(payload.content);
938
+ const lower = text.toLowerCase();
939
+ if (delivery.triggers.some((t) => lower.includes(t.toLowerCase()))) {
940
+ await this.flushBatch(mindName, session, [msg]);
941
+ return;
942
+ }
943
+ }
944
+ this.addToBatchBuffer(mindName, session, sessionConfig, msg);
945
+ }
946
+ addToBatchBuffer(mindName, session, sessionConfig, msg) {
947
+ const delivery = sessionConfig.delivery;
948
+ const bufferKey = `${mindName}:${session}`;
949
+ let buffer = this.batchBuffers.get(bufferKey);
950
+ if (!buffer) {
951
+ buffer = {
952
+ messages: [],
953
+ debounceTimer: null,
954
+ maxWaitTimer: null,
955
+ delivery
956
+ };
957
+ this.batchBuffers.set(bufferKey, buffer);
958
+ }
959
+ buffer.messages.push(msg);
960
+ if (buffer.messages.length >= MAX_BATCH_SIZE) {
961
+ this.flushBatch(mindName, session);
962
+ return;
963
+ }
964
+ this.scheduleBatchTimers(mindName, session, bufferKey);
965
+ }
966
+ scheduleBatchTimers(mindName, session, bufferKey) {
967
+ const buffer = this.batchBuffers.get(bufferKey);
968
+ if (!buffer) return;
969
+ if (buffer.debounceTimer) clearTimeout(buffer.debounceTimer);
970
+ buffer.debounceTimer = setTimeout(() => {
971
+ if (!this.isSessionBusy(mindName, session)) {
972
+ this.flushBatch(mindName, session);
973
+ }
974
+ }, buffer.delivery.debounce * 1e3);
975
+ buffer.debounceTimer.unref();
976
+ if (!buffer.maxWaitTimer) {
977
+ buffer.maxWaitTimer = setTimeout(() => {
978
+ this.flushBatch(mindName, session);
979
+ }, buffer.delivery.maxWait * 1e3);
980
+ buffer.maxWaitTimer.unref();
981
+ }
982
+ }
983
+ async flushBatch(mindName, session, extra) {
984
+ const bufferKey = `${mindName}:${session}`;
985
+ const buffer = this.batchBuffers.get(bufferKey);
986
+ const messages2 = [];
987
+ if (buffer) {
988
+ if (buffer.debounceTimer) clearTimeout(buffer.debounceTimer);
989
+ if (buffer.maxWaitTimer) clearTimeout(buffer.maxWaitTimer);
990
+ buffer.debounceTimer = null;
991
+ buffer.maxWaitTimer = null;
992
+ messages2.push(...buffer.messages.splice(0));
993
+ this.batchBuffers.delete(bufferKey);
994
+ }
995
+ if (extra) messages2.push(...extra);
996
+ if (messages2.length === 0) return;
997
+ const baseName = await getBaseName(mindName);
998
+ const config = getRoutingConfig(baseName);
999
+ const sessionConfig = resolveDeliveryMode(config, session);
1000
+ dlog.info(`flushing batch for ${mindName}/${session}: ${messages2.length} messages`);
1001
+ this.deliverBatchToMind(mindName, session, messages2, sessionConfig).catch((err) => {
1002
+ dlog.warn(`failed to flush batch for ${mindName}/${session}`, logger_default.errorData(err));
1003
+ });
1004
+ }
1005
+ async gateMessage(mindName, session, payload) {
1006
+ const baseName = await getBaseName(mindName);
1007
+ const declined = await this.isChannelDeclined(baseName, payload.channel);
1008
+ await this.persistToQueue(mindName, session, payload, declined ? "archived" : "gated");
1009
+ if (declined) return;
1010
+ try {
1011
+ const db = await getDb();
1012
+ const rows = await db.select({ count: sql`count(*)` }).from(deliveryQueue).where(
1013
+ and2(
1014
+ eq2(deliveryQueue.mind, baseName),
1015
+ eq2(deliveryQueue.channel, payload.channel),
1016
+ inArray(deliveryQueue.status, ["gated", "archived"])
1017
+ )
1018
+ );
1019
+ const count = rows[0]?.count ?? 0;
1020
+ if (count === 1 || count % GATED_NOTIFY_EVERY === 0) {
1021
+ await this.sendInviteNotification(mindName, payload, count);
1022
+ }
1023
+ } catch (err) {
1024
+ dlog.warn(`failed to check gated count for ${baseName}`, logger_default.errorData(err));
1025
+ }
1026
+ }
1027
+ async sendInviteNotification(mindName, payload, gatedCount = 1) {
1028
+ const text = extractTextContent(payload.content);
1029
+ const preview = text.length > 200 ? `${text.slice(0, 200)}...` : text;
1030
+ const channel = payload.channel ?? "unknown";
1031
+ const heldLine = gatedCount > 1 ? `${gatedCount} messages from this channel are being held, unrouted \u2014 you've not routed it yet.` : `Someone new is reaching out \u2014 you don't have a route for this channel yet.`;
1032
+ const detailLines = [
1033
+ payload.platform ? `Platform: ${payload.platform}` : null,
1034
+ payload.participantCount ? `Participants: ${payload.participantCount}` : null
1035
+ ].filter((l) => l !== null);
1036
+ const details = detailLines.length > 0 ? `${detailLines.join("\n")}
1037
+
1038
+ ` : "\n";
1039
+ const { getPrompt } = await import("./prompts-QKFPX6DQ.js");
1040
+ const notification = await getPrompt("channel_invite", {
1041
+ channel,
1042
+ heldLine,
1043
+ sender: payload.sender ?? "unknown",
1044
+ details,
1045
+ preview,
1046
+ limit: String(GATED_RELEASE_LIMIT_PER_CHANNEL)
1047
+ });
1048
+ await this.notify(mindName, notification);
1049
+ }
1050
+ /**
1051
+ * Insert a delivery_queue row and return its id. The `mind` column is always keyed by
1052
+ * `baseName` so inserts under a variant name and the id-scoped cleanup use the same key
1053
+ * (fixes the variant mismatch where variant-keyed rows were never matched by the base
1054
+ * cleanup). `target_mind` records the original delivery target (`mindName`, which may be a
1055
+ * variant) so redrive resolves the port from it — a variant's stranded row is re-delivered
1056
+ * to the variant, not the parent.
1057
+ */
1058
+ async persistToQueue(mindName, session, payload, status = "pending") {
1059
+ try {
1060
+ const baseName = await getBaseName(mindName);
1061
+ const db = await getDb();
1062
+ const result = await db.insert(deliveryQueue).values({
1063
+ mind: baseName,
1064
+ target_mind: mindName,
1065
+ thread: session,
1066
+ channel: payload.channel ?? null,
1067
+ sender: payload.sender ?? null,
1068
+ status,
1069
+ payload: JSON.stringify(payload)
1070
+ }).returning({ id: deliveryQueue.id });
1071
+ return result[0]?.id;
1072
+ } catch (err) {
1073
+ dlog.warn(
1074
+ `failed to persist to delivery queue for ${mindName}/${session}`,
1075
+ logger_default.errorData(err)
1076
+ );
1077
+ return void 0;
1078
+ }
1079
+ }
1080
+ async enrichWithProfiles(mindName, session, payload) {
1081
+ if (!payload.conversationId || !payload.channel) return payload;
1082
+ const mindSessions = this.sessionStates.get(mindName);
1083
+ const state2 = mindSessions?.get(session);
1084
+ if (!state2) return payload;
1085
+ const channelKey = payload.channel;
1086
+ if (state2.seenChannelProfiles.has(channelKey)) return payload;
1087
+ try {
1088
+ const participants = await getParticipants(payload.conversationId);
1089
+ const profiles = participants.map((p) => ({
1090
+ username: p.username,
1091
+ userType: p.userType,
1092
+ displayName: p.displayName,
1093
+ description: p.description
1094
+ }));
1095
+ const avatarBlocks = await this.loadAvatarBlocks(participants);
1096
+ state2.seenChannelProfiles.add(channelKey);
1097
+ const enriched = { ...payload, participantProfiles: profiles };
1098
+ if (avatarBlocks.length > 0) {
1099
+ const existing = Array.isArray(payload.content) ? payload.content : typeof payload.content === "string" ? [{ type: "text", text: payload.content }] : [];
1100
+ enriched.content = [...avatarBlocks, ...existing];
1101
+ }
1102
+ return enriched;
1103
+ } catch (err) {
1104
+ dlog.warn(`failed to fetch participant profiles for ${mindName}`, logger_default.errorData(err));
1105
+ return payload;
1106
+ }
1107
+ }
1108
+ async loadAvatarBlocks(participants) {
1109
+ const cacheKey = participants.map((p) => `${p.username}:${p.avatar ?? ""}`).sort().join(",");
1110
+ const cached = avatarBlocksCache.get(cacheKey);
1111
+ if (cached && cached.expiresAt > Date.now()) return cached.blocks;
1112
+ const blocks = [];
1113
+ for (const p of participants) {
1114
+ if (!p.avatar) continue;
1115
+ try {
1116
+ let filePath;
1117
+ if (p.userType === "mind") {
1118
+ const dir = mindDir(p.username);
1119
+ const config = readVoluteConfig(dir);
1120
+ if (!config?.profile?.avatar) continue;
1121
+ filePath = resolve(dir, "home", config.profile.avatar);
1122
+ const homeDir = resolve(dir, "home");
1123
+ if (!filePath.startsWith(`${homeDir}/`)) {
1124
+ dlog.warn(`avatar path for ${p.username} escapes home directory, skipping`);
1125
+ continue;
1126
+ }
1127
+ try {
1128
+ const realHome = await realpath(homeDir);
1129
+ const realAvatar = await realpath(filePath);
1130
+ if (!realAvatar.startsWith(`${realHome}/`)) {
1131
+ dlog.warn(
1132
+ `avatar symlink for ${p.username} resolves outside home directory, skipping`
1133
+ );
1134
+ continue;
1135
+ }
1136
+ } catch (err) {
1137
+ if (err.code === "ENOENT") continue;
1138
+ throw err;
1139
+ }
1140
+ } else {
1141
+ filePath = resolve(voluteHome(), "avatars", p.avatar);
1142
+ }
1143
+ const rendered = await renderAvatarBlock(filePath, p.username);
1144
+ if (rendered) blocks.push(...rendered);
1145
+ } catch (err) {
1146
+ const code = err.code;
1147
+ if (code !== "ENOENT") {
1148
+ dlog.warn(`failed to load avatar for ${p.username}`, logger_default.errorData(err));
1149
+ }
1150
+ }
1151
+ }
1152
+ const now = Date.now();
1153
+ for (const [key, entry] of avatarBlocksCache) {
1154
+ if (entry.expiresAt <= now) avatarBlocksCache.delete(key);
1155
+ }
1156
+ avatarBlocksCache.set(cacheKey, { blocks, expiresAt: now + AVATAR_CACHE_TTL });
1157
+ return blocks;
1158
+ }
1159
+ incrementActive(mind, session, senders, channels) {
1160
+ let mindSessions = this.sessionStates.get(mind);
1161
+ if (!mindSessions) {
1162
+ mindSessions = /* @__PURE__ */ new Map();
1163
+ this.sessionStates.set(mind, mindSessions);
1164
+ }
1165
+ const state2 = mindSessions.get(session) ?? {
1166
+ activeCount: 0,
1167
+ lastDeliveredAt: 0,
1168
+ lastDeliverySenders: /* @__PURE__ */ new Set(),
1169
+ lastDeliveryChannels: /* @__PURE__ */ new Set(),
1170
+ seenChannelProfiles: /* @__PURE__ */ new Set()
1171
+ };
1172
+ state2.activeCount++;
1173
+ state2.lastDeliveredAt = Date.now();
1174
+ if (senders) state2.lastDeliverySenders = senders;
1175
+ if (channels) state2.lastDeliveryChannels = channels;
1176
+ mindSessions.set(session, state2);
1177
+ }
1178
+ decrementActive(mind, session) {
1179
+ const mindSessions = this.sessionStates.get(mind);
1180
+ if (!mindSessions) return;
1181
+ const state2 = mindSessions.get(session);
1182
+ if (!state2) return;
1183
+ state2.activeCount = Math.max(0, state2.activeCount - 1);
1184
+ if (state2.activeCount === 0) {
1185
+ const bufferKey = `${mind}:${session}`;
1186
+ const buffer = this.batchBuffers.get(bufferKey);
1187
+ if (buffer && buffer.messages.length > 0) {
1188
+ this.scheduleBatchTimers(mind, session, bufferKey);
1189
+ } else if (session.startsWith("new-")) {
1190
+ mindSessions.delete(session);
1191
+ if (mindSessions.size === 0) this.sessionStates.delete(mind);
1192
+ }
1193
+ }
1194
+ }
1195
+ };
1196
+ var instance2;
1197
+ function initDeliveryManager() {
1198
+ if (instance2) throw new Error("DeliveryManager already initialized");
1199
+ instance2 = new DeliveryManager();
1200
+ return instance2;
1201
+ }
1202
+ function getDeliveryManager() {
1203
+ if (!instance2) {
1204
+ throw new Error("DeliveryManager not initialized \u2014 call initDeliveryManager() first");
1205
+ }
1206
+ return instance2;
1207
+ }
1208
+ function tryGetDeliveryManager() {
1209
+ return instance2;
1210
+ }
1211
+
1212
+ export {
1213
+ getTypingMap,
1214
+ isConversationId,
1215
+ publishTypingForChannels,
1216
+ checkStaleSend,
1217
+ formatHoldNotice,
1218
+ DeliveryManager,
1219
+ initDeliveryManager,
1220
+ getDeliveryManager,
1221
+ tryGetDeliveryManager
1222
+ };