volute 0.20.0 → 0.21.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 (92) hide show
  1. package/README.md +7 -7
  2. package/dist/{activity-events-OMXKXD5N.js → activity-events-3WHHCOBB.js} +3 -4
  3. package/dist/{archive-ZCFOSTKB.js → archive-4ZQYK5MN.js} +4 -2
  4. package/dist/auth-HM2RSPY7.js +37 -0
  5. package/dist/{channel-PUQKGSQM.js → channel-BOOMFULW.js} +2 -2
  6. package/dist/{chunk-IKMY5X76.js → chunk-5462YKWP.js} +12 -9
  7. package/dist/{chunk-PUVXOZ6T.js → chunk-7LPTHFIL.js} +63 -64
  8. package/dist/{chunk-UU7A7KLB.js → chunk-A4S7H6G6.js} +5 -7
  9. package/dist/chunk-AKPFNL7L.js +148 -0
  10. package/dist/{chunk-EBGCNDMM.js → chunk-B2CPS4QU.js} +128 -114
  11. package/dist/{chunk-FCDU5BFX.js → chunk-HFCBO2GL.js} +2 -2
  12. package/dist/{chunk-GZ7DW4YL.js → chunk-HGCDWKSP.js} +2 -2
  13. package/dist/{chunk-DYZGP3EW.js → chunk-IPJXU366.js} +1 -1
  14. package/dist/{chunk-7UFKREVW.js → chunk-J5A3DF2U.js} +2 -2
  15. package/dist/{chunk-WC6ZHVRL.js → chunk-KFI7TQJ6.js} +2 -2
  16. package/dist/{chunk-AW7P4EVV.js → chunk-KTJGZ7M7.js} +55 -7
  17. package/dist/{chunk-TIWH32HP.js → chunk-L3LHXZD7.js} +3 -3
  18. package/dist/{chunk-OGXOMR65.js → chunk-NWPT4ASZ.js} +1 -1
  19. package/dist/{chunk-FGSYHIS3.js → chunk-OGZYB5GL.js} +252 -296
  20. package/dist/{chunk-SCUDS4US.js → chunk-ON3FF5JA.js} +1 -1
  21. package/dist/{chunk-O6ASDHFO.js → chunk-PC6R6UUW.js} +4 -4
  22. package/dist/{chunk-VDWCHYTS.js → chunk-PHU4DEAJ.js} +1 -1
  23. package/dist/{chunk-7NO7EV5Z.js → chunk-Q7AITQ44.js} +2 -2
  24. package/dist/{chunk-32VR2EOH.js → chunk-QUJUKM4U.js} +2 -2
  25. package/dist/{chunk-NSE7VJQA.js → chunk-SGPEZ32F.js} +29 -1
  26. package/dist/{chunk-RHEGSQFJ.js → chunk-WSLPZF72.js} +1 -1
  27. package/dist/cli.js +57 -119
  28. package/dist/{connector-JBVNZ7VK.js → connector-PYT5UOTZ.js} +6 -6
  29. package/dist/connectors/discord.js +2 -2
  30. package/dist/connectors/slack.js +2 -2
  31. package/dist/connectors/telegram.js +2 -2
  32. package/dist/{create-HP4OVVHF.js → create-WIDA3M4C.js} +1 -1
  33. package/dist/{daemon-client-ITWUCNFO.js → daemon-client-ZHCDL4RS.js} +2 -2
  34. package/dist/{daemon-restart-KPSWNYTH.js → daemon-restart-BH67ZOTE.js} +6 -6
  35. package/dist/daemon.js +1538 -687
  36. package/dist/{delete-BSU7K3RY.js → delete-LOIANQGD.js} +1 -1
  37. package/dist/down-LIOQ5JDH.js +14 -0
  38. package/dist/{env-A3LMO777.js → env-4PHIHTF4.js} +2 -2
  39. package/dist/{export-6QBUOQGC.js → export-XD6PJBQP.js} +19 -8
  40. package/dist/{file-C57SK5DK.js → file-X4L5TTOL.js} +2 -2
  41. package/dist/{history-WNK3DFUM.js → history-HTEKRNID.js} +2 -2
  42. package/dist/{import-XEC34Y4Z.js → import-E433B4KG.js} +3 -3
  43. package/dist/{log-PPPZDVEF.js → log-SRO5Q6AD.js} +2 -2
  44. package/dist/{login-HNH3EUQV.js → login-UO6AOVEA.js} +4 -4
  45. package/dist/{logout-I5CB5UZS.js → logout-UKD5LA37.js} +2 -2
  46. package/dist/{logs-SF2IMJN4.js → logs-HNTNNBDW.js} +2 -2
  47. package/dist/{merge-33C237A4.js → merge-B6SYTGI7.js} +2 -2
  48. package/dist/{mind-Z7CKD6DG.js → mind-BIDOF65R.js} +27 -11
  49. package/dist/{mind-activity-tracker-624QLQLC.js → mind-activity-tracker-PGC3DBJ7.js} +4 -5
  50. package/dist/{mind-manager-3DMYKZPB.js → mind-manager-3V2NXX4I.js} +5 -6
  51. package/dist/{package-4NHAVUUI.js → package-HQR52XSG.js} +1 -1
  52. package/dist/{pages-4DGQT7ZA.js → pages-KQBR5TAZ.js} +6 -6
  53. package/dist/{publish-TAJUET4I.js → publish-OJ4QMXVZ.js} +6 -6
  54. package/dist/{pull-XAEWQJ47.js → pull-GRQAXM2E.js} +2 -2
  55. package/dist/{register-VSPCMHKX.js → register-U2UO6TC4.js} +5 -5
  56. package/dist/registry-D2BSQ2X5.js +42 -0
  57. package/dist/{restart-IQKMCK5M.js → restart-CIDAKGG2.js} +3 -6
  58. package/dist/{schedule-FFZG23IW.js → schedule-NLR3LZLY.js} +2 -2
  59. package/dist/{seed-J43YDKXG.js → seed-3H2MRREW.js} +2 -2
  60. package/dist/{send-KVIZIGCE.js → send-RP2TA7SG.js} +132 -36
  61. package/dist/{service-LUR7WDO7.js → service-TVNEORO7.js} +31 -13
  62. package/dist/{setup-52YRV7VP.js → setup-OZDYCKDI.js} +9 -34
  63. package/dist/{shared-KO35ZM44.js → shared-DCQ2UXOM.js} +4 -4
  64. package/dist/{skill-BCVNI6TV.js → skill-Q2Y6PQ3L.js} +2 -2
  65. package/dist/skills/orientation/SKILL.md +2 -2
  66. package/dist/skills/volute-mind/SKILL.md +5 -5
  67. package/dist/{sprout-QN7Y4VVO.js → sprout-6Z6C42YM.js} +34 -30
  68. package/dist/{start-I5JYB65M.js → start-JR6CUUWF.js} +3 -6
  69. package/dist/{status-D7E5HHBV.js → status-5XDGYHKP.js} +2 -2
  70. package/dist/{status-FU2PFVVF.js → status-LV34BG6G.js} +3 -3
  71. package/dist/{status-4ESFLGH4.js → status-Z7NAFMBI.js} +5 -5
  72. package/dist/{stop-NBVKEFQQ.js → stop-VKPGK25U.js} +2 -5
  73. package/dist/template-hash-BIMA4ILT.js +8 -0
  74. package/dist/{up-FS7CKM6V.js → up-7BGDMFRT.js} +5 -5
  75. package/dist/{update-FJIHDJKM.js → update-4WT7VWHW.js} +5 -5
  76. package/dist/{update-check-MWE5AH4U.js → update-check-F5Z3ALXX.js} +2 -2
  77. package/dist/{upgrade-AIT24B5I.js → upgrade-ZEC2GGFO.js} +1 -1
  78. package/dist/{variant-63ZWO2W7.js → variant-A4I7PHXS.js} +16 -24
  79. package/dist/version-notify-TFS2U5CF.js +173 -0
  80. package/dist/web-assets/assets/index-BR3gtK3E.css +1 -0
  81. package/dist/web-assets/assets/index-CWmrZRQd.js +64 -0
  82. package/dist/web-assets/index.html +2 -2
  83. package/package.json +1 -1
  84. package/dist/chunk-5XNT2472.js +0 -36
  85. package/dist/chunk-UJ6GHNR7.js +0 -675
  86. package/dist/db-C2CJ46ZU.js +0 -10
  87. package/dist/delivery-manager-CSG7LXA4.js +0 -16
  88. package/dist/down-ZY35KMHR.js +0 -14
  89. package/dist/schema-GFH6RV3W.js +0 -26
  90. package/dist/variants-JAGWGBXG.js +0 -26
  91. package/dist/web-assets/assets/index-CUZTZzaW.js +0 -64
  92. package/dist/web-assets/assets/index-adVuCkqy.css +0 -1
@@ -1,41 +1,173 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- logger_default
4
- } from "./chunk-YUIHSKR6.js";
5
- import {
6
- getDb
7
- } from "./chunk-5XNT2472.js";
8
2
  import {
9
3
  deliveryQueue,
4
+ getDb,
10
5
  mindHistory
11
- } from "./chunk-NSE7VJQA.js";
6
+ } from "./chunk-SGPEZ32F.js";
7
+ import {
8
+ logger_default
9
+ } from "./chunk-YUIHSKR6.js";
12
10
  import {
13
11
  findMind,
14
12
  findVariant,
15
13
  mindDir
16
- } from "./chunk-EBGCNDMM.js";
14
+ } from "./chunk-B2CPS4QU.js";
17
15
 
18
- // src/lib/delivery-manager.ts
16
+ // src/lib/delivery/delivery-manager.ts
19
17
  import { and, eq, sql } from "drizzle-orm";
20
18
 
21
- // src/lib/delivery-router.ts
19
+ // src/lib/events/conversation-events.ts
20
+ var subscribers = /* @__PURE__ */ new Map();
21
+ function subscribe(conversationId, callback) {
22
+ let set = subscribers.get(conversationId);
23
+ if (!set) {
24
+ set = /* @__PURE__ */ new Set();
25
+ subscribers.set(conversationId, set);
26
+ }
27
+ set.add(callback);
28
+ return () => {
29
+ set.delete(callback);
30
+ if (set.size === 0) subscribers.delete(conversationId);
31
+ };
32
+ }
33
+ function publish(conversationId, event) {
34
+ const set = subscribers.get(conversationId);
35
+ if (!set) return;
36
+ for (const cb of set) {
37
+ try {
38
+ cb(event);
39
+ } catch (err) {
40
+ console.error("[conversation-events] subscriber threw:", err);
41
+ set.delete(cb);
42
+ if (set.size === 0) subscribers.delete(conversationId);
43
+ }
44
+ }
45
+ }
46
+
47
+ // src/lib/typing.ts
48
+ var DEFAULT_TTL_MS = 1e4;
49
+ var SWEEP_INTERVAL_MS = 5e3;
50
+ var VOLUTE_PREFIX = "volute:";
51
+ var TypingMap = class {
52
+ channels = /* @__PURE__ */ new Map();
53
+ sweepTimer;
54
+ constructor() {
55
+ this.sweepTimer = setInterval(() => this.sweep(), SWEEP_INTERVAL_MS);
56
+ this.sweepTimer.unref();
57
+ }
58
+ set(channel, sender, opts) {
59
+ const expiresAt = opts?.persistent ? Infinity : Date.now() + (opts?.ttlMs ?? DEFAULT_TTL_MS);
60
+ let senders = this.channels.get(channel);
61
+ if (!senders) {
62
+ senders = /* @__PURE__ */ new Map();
63
+ this.channels.set(channel, senders);
64
+ }
65
+ senders.set(sender, { expiresAt });
66
+ }
67
+ delete(channel, sender) {
68
+ const senders = this.channels.get(channel);
69
+ if (senders) {
70
+ senders.delete(sender);
71
+ if (senders.size === 0) {
72
+ this.channels.delete(channel);
73
+ }
74
+ }
75
+ }
76
+ /** Remove a sender from all channels (e.g. when a mind finishes processing). Returns affected channel names. */
77
+ deleteSender(sender) {
78
+ const affected = [];
79
+ for (const [channel, senders] of this.channels) {
80
+ if (senders.has(sender)) {
81
+ senders.delete(sender);
82
+ affected.push(channel);
83
+ }
84
+ if (senders.size === 0) {
85
+ this.channels.delete(channel);
86
+ }
87
+ }
88
+ return affected;
89
+ }
90
+ get(channel) {
91
+ const senders = this.channels.get(channel);
92
+ if (!senders) return [];
93
+ const now = Date.now();
94
+ const result = [];
95
+ for (const [sender, entry] of senders) {
96
+ if (entry.expiresAt > now) {
97
+ result.push(sender);
98
+ }
99
+ }
100
+ return result;
101
+ }
102
+ dispose() {
103
+ clearInterval(this.sweepTimer);
104
+ this.channels.clear();
105
+ if (instance === this) instance = void 0;
106
+ }
107
+ sweep() {
108
+ const now = Date.now();
109
+ for (const [channel, senders] of this.channels) {
110
+ for (const [sender, entry] of senders) {
111
+ if (entry.expiresAt <= now) {
112
+ senders.delete(sender);
113
+ }
114
+ }
115
+ if (senders.size === 0) {
116
+ this.channels.delete(channel);
117
+ }
118
+ }
119
+ }
120
+ };
121
+ var instance;
122
+ function getTypingMap() {
123
+ if (!instance) {
124
+ instance = new TypingMap();
125
+ }
126
+ return instance;
127
+ }
128
+ function publishTypingForChannels(channels, map) {
129
+ for (const channel of channels) {
130
+ if (channel.startsWith(VOLUTE_PREFIX)) {
131
+ const conversationId = channel.slice(VOLUTE_PREFIX.length);
132
+ publish(conversationId, { type: "typing", senders: map.get(channel) });
133
+ }
134
+ }
135
+ }
136
+
137
+ // src/lib/delivery/delivery-router.ts
22
138
  import { readFileSync, statSync } from "fs";
23
139
  import { resolve } from "path";
140
+ function extractTextContent(content) {
141
+ if (typeof content === "string") return content;
142
+ if (Array.isArray(content)) {
143
+ return content.filter((p) => p.type === "text" && p.text).map((p) => p.text).join("\n");
144
+ }
145
+ return JSON.stringify(content);
146
+ }
24
147
  var configCache = /* @__PURE__ */ new Map();
148
+ var statCheckCache = /* @__PURE__ */ new Map();
149
+ var STAT_TTL_MS = 5e3;
25
150
  var dlog = logger_default.child("delivery-router");
26
151
  function configPath(mindName) {
27
152
  return resolve(mindDir(mindName), "home/.config/routes.json");
28
153
  }
29
154
  function getRoutingConfig(mindName) {
30
155
  const path = configPath(mindName);
156
+ const now = Date.now();
157
+ const statCached = statCheckCache.get(mindName);
158
+ const cached = configCache.get(mindName);
159
+ if (statCached && cached && now - statCached.checkedAt < STAT_TTL_MS) {
160
+ return cached.config;
161
+ }
31
162
  let mtime;
32
163
  try {
33
164
  mtime = statSync(path).mtimeMs;
34
165
  } catch {
35
166
  configCache.delete(mindName);
167
+ statCheckCache.delete(mindName);
36
168
  return {};
37
169
  }
38
- const cached = configCache.get(mindName);
170
+ statCheckCache.set(mindName, { mtime, checkedAt: now });
39
171
  if (cached && cached.mtime === mtime) {
40
172
  return cached.config;
41
173
  }
@@ -50,9 +182,15 @@ function getRoutingConfig(mindName) {
50
182
  return {};
51
183
  }
52
184
  }
185
+ var globRegexCache = /* @__PURE__ */ new Map();
53
186
  function globMatch(pattern, value) {
54
- const regex = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
55
- return new RegExp(`^${regex}$`).test(value);
187
+ let regex = globRegexCache.get(pattern);
188
+ if (!regex) {
189
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
190
+ regex = new RegExp(`^${escaped}$`);
191
+ globRegexCache.set(pattern, regex);
192
+ }
193
+ return regex.test(value);
56
194
  }
57
195
  var GLOB_MATCH_KEYS = /* @__PURE__ */ new Set(["channel", "sender"]);
58
196
  var NON_MATCH_KEYS = /* @__PURE__ */ new Set(["session", "destination", "path", "mode"]);
@@ -164,204 +302,8 @@ function resolveDeliveryMode(config, sessionName) {
164
302
  return defaults;
165
303
  }
166
304
 
167
- // src/lib/message-delivery.ts
168
- var dlog2 = logger_default.child("delivery");
169
- function extractTextContent(content) {
170
- if (typeof content === "string") return content;
171
- if (Array.isArray(content)) {
172
- return content.filter((p) => p.type === "text" && p.text).map((p) => p.text).join("\n");
173
- }
174
- return JSON.stringify(content);
175
- }
176
- async function deliverMessage(mindName, payload) {
177
- try {
178
- const [baseName] = mindName.split("@", 2);
179
- const entry = findMind(baseName);
180
- if (!entry) {
181
- dlog2.warn(`cannot deliver to ${mindName}: mind not found`);
182
- return;
183
- }
184
- const textContent = extractTextContent(payload.content);
185
- try {
186
- const db = await getDb();
187
- await db.insert(mindHistory).values({
188
- mind: baseName,
189
- type: "inbound",
190
- channel: payload.channel,
191
- sender: payload.sender ?? null,
192
- content: textContent
193
- });
194
- } catch (err) {
195
- dlog2.warn(`failed to persist message for ${baseName}`, logger_default.errorData(err));
196
- }
197
- try {
198
- const { getDeliveryManager: getDeliveryManager2 } = await import("./delivery-manager-CSG7LXA4.js");
199
- const manager = getDeliveryManager2();
200
- await manager.routeAndDeliver(mindName, payload);
201
- return;
202
- } catch (err) {
203
- if (err instanceof Error && !err.message.includes("not initialized")) {
204
- dlog2.warn("delivery manager error, falling back to direct delivery", logger_default.errorData(err));
205
- }
206
- }
207
- const { findVariant: findVariant2 } = await import("./variants-JAGWGBXG.js");
208
- const [, variantName] = mindName.split("@", 2);
209
- let port = entry.port;
210
- if (variantName) {
211
- const variant = findVariant2(baseName, variantName);
212
- if (!variant) {
213
- dlog2.warn(`cannot deliver to ${mindName}: variant not found`);
214
- return;
215
- }
216
- port = variant.port;
217
- }
218
- const body = JSON.stringify(payload);
219
- const controller = new AbortController();
220
- const timeout = setTimeout(() => controller.abort(), 12e4);
221
- try {
222
- const res = await fetch(`http://127.0.0.1:${port}/message`, {
223
- method: "POST",
224
- headers: { "Content-Type": "application/json" },
225
- body,
226
- signal: controller.signal
227
- });
228
- if (!res.ok) {
229
- const text = await res.text().catch(() => "");
230
- dlog2.warn(`mind ${mindName} responded ${res.status}: ${text}`);
231
- } else {
232
- await res.text().catch(() => {
233
- });
234
- }
235
- } catch (err) {
236
- dlog2.warn(`failed to deliver to ${mindName}`, logger_default.errorData(err));
237
- } finally {
238
- clearTimeout(timeout);
239
- }
240
- } catch (err) {
241
- dlog2.warn(`unexpected error delivering to ${mindName}`, logger_default.errorData(err));
242
- }
243
- }
244
-
245
- // src/lib/conversation-events.ts
246
- var subscribers = /* @__PURE__ */ new Map();
247
- function subscribe(conversationId, callback) {
248
- let set = subscribers.get(conversationId);
249
- if (!set) {
250
- set = /* @__PURE__ */ new Set();
251
- subscribers.set(conversationId, set);
252
- }
253
- set.add(callback);
254
- return () => {
255
- set.delete(callback);
256
- if (set.size === 0) subscribers.delete(conversationId);
257
- };
258
- }
259
- function publish(conversationId, event) {
260
- const set = subscribers.get(conversationId);
261
- if (!set) return;
262
- for (const cb of set) {
263
- try {
264
- cb(event);
265
- } catch (err) {
266
- console.error("[conversation-events] subscriber threw:", err);
267
- set.delete(cb);
268
- if (set.size === 0) subscribers.delete(conversationId);
269
- }
270
- }
271
- }
272
-
273
- // src/lib/typing.ts
274
- var DEFAULT_TTL_MS = 1e4;
275
- var SWEEP_INTERVAL_MS = 5e3;
276
- var VOLUTE_PREFIX = "volute:";
277
- var TypingMap = class {
278
- channels = /* @__PURE__ */ new Map();
279
- sweepTimer;
280
- constructor() {
281
- this.sweepTimer = setInterval(() => this.sweep(), SWEEP_INTERVAL_MS);
282
- this.sweepTimer.unref();
283
- }
284
- set(channel, sender, opts) {
285
- const expiresAt = opts?.persistent ? Infinity : Date.now() + (opts?.ttlMs ?? DEFAULT_TTL_MS);
286
- let senders = this.channels.get(channel);
287
- if (!senders) {
288
- senders = /* @__PURE__ */ new Map();
289
- this.channels.set(channel, senders);
290
- }
291
- senders.set(sender, { expiresAt });
292
- }
293
- delete(channel, sender) {
294
- const senders = this.channels.get(channel);
295
- if (senders) {
296
- senders.delete(sender);
297
- if (senders.size === 0) {
298
- this.channels.delete(channel);
299
- }
300
- }
301
- }
302
- /** Remove a sender from all channels (e.g. when a mind finishes processing). Returns affected channel names. */
303
- deleteSender(sender) {
304
- const affected = [];
305
- for (const [channel, senders] of this.channels) {
306
- if (senders.has(sender)) {
307
- senders.delete(sender);
308
- affected.push(channel);
309
- }
310
- if (senders.size === 0) {
311
- this.channels.delete(channel);
312
- }
313
- }
314
- return affected;
315
- }
316
- get(channel) {
317
- const senders = this.channels.get(channel);
318
- if (!senders) return [];
319
- const now = Date.now();
320
- const result = [];
321
- for (const [sender, entry] of senders) {
322
- if (entry.expiresAt > now) {
323
- result.push(sender);
324
- }
325
- }
326
- return result;
327
- }
328
- dispose() {
329
- clearInterval(this.sweepTimer);
330
- this.channels.clear();
331
- if (instance === this) instance = void 0;
332
- }
333
- sweep() {
334
- const now = Date.now();
335
- for (const [channel, senders] of this.channels) {
336
- for (const [sender, entry] of senders) {
337
- if (entry.expiresAt <= now) {
338
- senders.delete(sender);
339
- }
340
- }
341
- if (senders.size === 0) {
342
- this.channels.delete(channel);
343
- }
344
- }
345
- }
346
- };
347
- var instance;
348
- function getTypingMap() {
349
- if (!instance) {
350
- instance = new TypingMap();
351
- }
352
- return instance;
353
- }
354
- function publishTypingForChannels(channels, map) {
355
- for (const channel of channels) {
356
- if (channel.startsWith(VOLUTE_PREFIX)) {
357
- const conversationId = channel.slice(VOLUTE_PREFIX.length);
358
- publish(conversationId, { type: "typing", senders: map.get(channel) });
359
- }
360
- }
361
- }
362
-
363
- // src/lib/delivery-manager.ts
364
- var dlog3 = logger_default.child("delivery-manager");
305
+ // src/lib/delivery/delivery-manager.ts
306
+ var dlog2 = logger_default.child("delivery-manager");
365
307
  var MAX_BATCH_SIZE = 50;
366
308
  var DeliveryManager = class {
367
309
  sessionStates = /* @__PURE__ */ new Map();
@@ -438,7 +380,7 @@ var DeliveryManager = class {
438
380
  try {
439
381
  payload = JSON.parse(row.payload);
440
382
  } catch (parseErr) {
441
- dlog3.warn(
383
+ dlog2.warn(
442
384
  `corrupt payload in delivery queue row ${row.id}, skipping`,
443
385
  logger_default.errorData(parseErr)
444
386
  );
@@ -449,22 +391,21 @@ var DeliveryManager = class {
449
391
  if (sessionConfig.delivery.mode === "batch") {
450
392
  this.addToBatchBuffer(row.mind, row.session, payload, sessionConfig);
451
393
  } else {
452
- this.deliverToMind(row.mind, row.session, payload, sessionConfig).then(async () => {
453
- try {
454
- const db2 = await getDb();
455
- await db2.delete(deliveryQueue).where(eq(deliveryQueue.id, row.id));
456
- } catch {
457
- }
458
- }).catch((err) => {
459
- dlog3.warn(`failed to restore delivery for ${row.mind}`, logger_default.errorData(err));
394
+ try {
395
+ await db.delete(deliveryQueue).where(eq(deliveryQueue.id, row.id));
396
+ } catch (err) {
397
+ dlog2.warn(`failed to delete queue row ${row.id} for ${row.mind}`, logger_default.errorData(err));
398
+ }
399
+ this.deliverToMind(row.mind, row.session, payload, sessionConfig).catch((err) => {
400
+ dlog2.warn(`failed to restore delivery for ${row.mind}`, logger_default.errorData(err));
460
401
  });
461
402
  }
462
403
  }
463
404
  if (rows.length > 0) {
464
- dlog3.info(`restored ${rows.length} queued messages from DB`);
405
+ dlog2.info(`restored ${rows.length} queued messages from DB`);
465
406
  }
466
407
  } catch (err) {
467
- dlog3.warn("failed to restore delivery queue from DB", logger_default.errorData(err));
408
+ dlog2.warn("failed to restore delivery queue from DB", logger_default.errorData(err));
468
409
  }
469
410
  }
470
411
  /**
@@ -513,22 +454,46 @@ var DeliveryManager = class {
513
454
  if (instance2 === this) instance2 = void 0;
514
455
  }
515
456
  // --- Private ---
516
- async deliverToMind(mindName, session, payload, sessionConfig) {
457
+ resolvePort(mindName) {
517
458
  const [baseName, variantName] = mindName.split("@", 2);
518
459
  const entry = findMind(baseName);
519
- if (!entry) {
520
- dlog3.warn(`cannot deliver to ${mindName}: mind not found`);
521
- return;
522
- }
523
- let port = entry.port;
460
+ if (!entry) return null;
524
461
  if (variantName) {
525
462
  const variant = findVariant(baseName, variantName);
526
- if (!variant) {
527
- dlog3.warn(`cannot deliver to ${mindName}: variant not found`);
528
- return;
463
+ if (!variant) return null;
464
+ return { baseName, port: variant.port };
465
+ }
466
+ return { baseName, port: entry.port };
467
+ }
468
+ async postToMind(port, body) {
469
+ const controller = new AbortController();
470
+ const timeout = setTimeout(() => controller.abort(), 12e4);
471
+ try {
472
+ const res = await fetch(`http://127.0.0.1:${port}/message`, {
473
+ method: "POST",
474
+ headers: { "Content-Type": "application/json" },
475
+ body,
476
+ signal: controller.signal
477
+ });
478
+ if (!res.ok) {
479
+ const text = await res.text().catch(() => "");
480
+ dlog2.warn(`mind responded ${res.status}: ${text}`);
481
+ return false;
529
482
  }
530
- port = variant.port;
483
+ await res.text().catch(() => {
484
+ });
485
+ return true;
486
+ } finally {
487
+ clearTimeout(timeout);
531
488
  }
489
+ }
490
+ async deliverToMind(mindName, session, payload, sessionConfig) {
491
+ const resolved = this.resolvePort(mindName);
492
+ if (!resolved) {
493
+ dlog2.warn(`cannot deliver to ${mindName}: mind not found`);
494
+ return;
495
+ }
496
+ const { baseName, port } = resolved;
532
497
  const senders = /* @__PURE__ */ new Set();
533
498
  if (payload.sender) senders.add(payload.sender);
534
499
  const channels = /* @__PURE__ */ new Set();
@@ -541,55 +506,31 @@ var DeliveryManager = class {
541
506
  if (payload.conversationId) {
542
507
  typingMap.set(`volute:${payload.conversationId}`, baseName, { persistent: true });
543
508
  }
544
- const deliveryBody = {
509
+ const body = JSON.stringify({
545
510
  ...payload,
546
511
  session,
547
512
  interrupt: sessionConfig.interrupt,
548
513
  instructions: sessionConfig.instructions
549
- };
550
- const body = JSON.stringify(deliveryBody);
551
- const controller = new AbortController();
552
- const timeout = setTimeout(() => controller.abort(), 12e4);
514
+ });
553
515
  try {
554
- const res = await fetch(`http://127.0.0.1:${port}/message`, {
555
- method: "POST",
556
- headers: { "Content-Type": "application/json" },
557
- body,
558
- signal: controller.signal
559
- });
560
- if (!res.ok) {
561
- const text = await res.text().catch(() => "");
562
- dlog3.warn(`mind ${mindName} responded ${res.status}: ${text}`);
516
+ const ok = await this.postToMind(port, body);
517
+ if (!ok) {
563
518
  this.decrementActive(baseName, session);
564
519
  publishTypingForChannels(typingMap.deleteSender(baseName), typingMap);
565
- } else {
566
- await res.text().catch(() => {
567
- });
568
520
  }
569
521
  } catch (err) {
570
- dlog3.warn(`failed to deliver to ${mindName}`, logger_default.errorData(err));
522
+ dlog2.warn(`failed to deliver to ${mindName}`, logger_default.errorData(err));
571
523
  this.decrementActive(baseName, session);
572
524
  publishTypingForChannels(typingMap.deleteSender(baseName), typingMap);
573
- } finally {
574
- clearTimeout(timeout);
575
525
  }
576
526
  }
577
527
  async deliverBatchToMind(mindName, session, messages, sessionConfig, interruptOverride) {
578
- const [baseName, variantName] = mindName.split("@", 2);
579
- const entry = findMind(baseName);
580
- if (!entry) {
581
- dlog3.warn(`cannot deliver batch to ${mindName}: mind not found`);
528
+ const resolved = this.resolvePort(mindName);
529
+ if (!resolved) {
530
+ dlog2.warn(`cannot deliver batch to ${mindName}: mind not found`);
582
531
  return;
583
532
  }
584
- let port = entry.port;
585
- if (variantName) {
586
- const variant = findVariant(baseName, variantName);
587
- if (!variant) {
588
- dlog3.warn(`cannot deliver batch to ${mindName}: variant not found`);
589
- return;
590
- }
591
- port = variant.port;
592
- }
533
+ const { baseName, port } = resolved;
593
534
  const channels = {};
594
535
  for (const msg of messages) {
595
536
  const ch = msg.channel ?? "unknown";
@@ -614,30 +555,18 @@ var DeliveryManager = class {
614
555
  typingMap.set(`volute:${msg.payload.conversationId}`, baseName, { persistent: true });
615
556
  }
616
557
  }
617
- const batchBody = {
558
+ const body = JSON.stringify({
618
559
  session,
619
560
  batch: { channels },
620
561
  interrupt: interruptOverride ?? sessionConfig.interrupt,
621
562
  instructions: sessionConfig.instructions
622
- };
623
- const body = JSON.stringify(batchBody);
624
- const controller = new AbortController();
625
- const timeout = setTimeout(() => controller.abort(), 12e4);
563
+ });
626
564
  try {
627
- const res = await fetch(`http://127.0.0.1:${port}/message`, {
628
- method: "POST",
629
- headers: { "Content-Type": "application/json" },
630
- body,
631
- signal: controller.signal
632
- });
633
- if (!res.ok) {
634
- const text = await res.text().catch(() => "");
635
- dlog3.warn(`mind ${mindName} batch responded ${res.status}: ${text}`);
565
+ const ok = await this.postToMind(port, body);
566
+ if (!ok) {
636
567
  this.decrementActive(baseName, session);
637
568
  publishTypingForChannels(typingMap.deleteSender(baseName), typingMap);
638
569
  } else {
639
- await res.text().catch(() => {
640
- });
641
570
  try {
642
571
  const db = await getDb();
643
572
  await db.delete(deliveryQueue).where(
@@ -648,18 +577,16 @@ var DeliveryManager = class {
648
577
  )
649
578
  );
650
579
  } catch (err) {
651
- dlog3.warn(
580
+ dlog2.warn(
652
581
  `failed to clean delivery queue for ${baseName}/${session}`,
653
582
  logger_default.errorData(err)
654
583
  );
655
584
  }
656
585
  }
657
586
  } catch (err) {
658
- dlog3.warn(`failed to deliver batch to ${mindName}`, logger_default.errorData(err));
587
+ dlog2.warn(`failed to deliver batch to ${mindName}`, logger_default.errorData(err));
659
588
  this.decrementActive(baseName, session);
660
589
  publishTypingForChannels(typingMap.deleteSender(baseName), typingMap);
661
- } finally {
662
- clearTimeout(timeout);
663
590
  }
664
591
  }
665
592
  enqueueBatch(mindName, session, payload, sessionConfig) {
@@ -684,7 +611,7 @@ var DeliveryManager = class {
684
611
  if (state && state.activeCount > 0 && payload.sender && !state.lastDeliverySenders.has(payload.sender) && payload.channel && state.lastDeliveryChannels.has(payload.channel) && Date.now() - state.lastDeliveredAt < delivery.maxWait * 1e3 && Date.now() - state.lastInterruptAt > delivery.debounce * 1e3) {
685
612
  state.lastInterruptAt = Date.now();
686
613
  this.persistToQueue(mindName, session, payload).catch((err) => {
687
- dlog3.warn(`failed to persist batch message for ${mindName}/${session}`, logger_default.errorData(err));
614
+ dlog2.warn(`failed to persist batch message for ${mindName}/${session}`, logger_default.errorData(err));
688
615
  });
689
616
  this.flushBatch(
690
617
  mindName,
@@ -695,7 +622,7 @@ var DeliveryManager = class {
695
622
  return;
696
623
  }
697
624
  this.persistToQueue(mindName, session, payload).catch((err) => {
698
- dlog3.warn(`failed to persist batch message for ${mindName}/${session}`, logger_default.errorData(err));
625
+ dlog2.warn(`failed to persist batch message for ${mindName}/${session}`, logger_default.errorData(err));
699
626
  });
700
627
  this.addToBatchBuffer(mindName, session, payload, sessionConfig);
701
628
  }
@@ -758,12 +685,12 @@ var DeliveryManager = class {
758
685
  const [baseName] = mindName.split("@", 2);
759
686
  const config = getRoutingConfig(baseName);
760
687
  const sessionConfig = resolveDeliveryMode(config, session);
761
- dlog3.info(
688
+ dlog2.info(
762
689
  `flushing batch for ${mindName}/${session}: ${messages.length} messages${interruptOverride ? " (new-speaker interrupt)" : ""}`
763
690
  );
764
691
  this.deliverBatchToMind(mindName, session, messages, sessionConfig, interruptOverride).catch(
765
692
  (err) => {
766
- dlog3.warn(`failed to flush batch for ${mindName}/${session}`, logger_default.errorData(err));
693
+ dlog2.warn(`failed to flush batch for ${mindName}/${session}`, logger_default.errorData(err));
767
694
  }
768
695
  );
769
696
  }
@@ -783,7 +710,7 @@ var DeliveryManager = class {
783
710
  await this.sendInviteNotification(mindName, payload);
784
711
  }
785
712
  } catch (err) {
786
- dlog3.warn(`failed to check gated count for ${baseName}`, logger_default.errorData(err));
713
+ dlog2.warn(`failed to check gated count for ${baseName}`, logger_default.errorData(err));
787
714
  }
788
715
  }
789
716
  async sendInviteNotification(mindName, payload) {
@@ -825,7 +752,7 @@ var DeliveryManager = class {
825
752
  payload: JSON.stringify(payload)
826
753
  });
827
754
  } catch (err) {
828
- dlog3.warn(
755
+ dlog2.warn(
829
756
  `failed to persist to delivery queue for ${mindName}/${session}`,
830
757
  logger_default.errorData(err)
831
758
  );
@@ -867,7 +794,7 @@ var DeliveryManager = class {
867
794
  };
868
795
  var instance2;
869
796
  function initDeliveryManager() {
870
- if (instance2) return instance2;
797
+ if (instance2) throw new Error("DeliveryManager already initialized");
871
798
  instance2 = new DeliveryManager();
872
799
  return instance2;
873
800
  }
@@ -878,14 +805,43 @@ function getDeliveryManager() {
878
805
  return instance2;
879
806
  }
880
807
 
808
+ // src/lib/delivery/message-delivery.ts
809
+ var dlog3 = logger_default.child("delivery");
810
+ async function deliverMessage(mindName, payload) {
811
+ try {
812
+ const [baseName] = mindName.split("@", 2);
813
+ const entry = findMind(baseName);
814
+ if (!entry) {
815
+ dlog3.warn(`cannot deliver to ${mindName}: mind not found`);
816
+ return;
817
+ }
818
+ const textContent = extractTextContent(payload.content);
819
+ try {
820
+ const db = await getDb();
821
+ await db.insert(mindHistory).values({
822
+ mind: baseName,
823
+ type: "inbound",
824
+ channel: payload.channel,
825
+ sender: payload.sender ?? null,
826
+ content: textContent
827
+ });
828
+ } catch (err) {
829
+ dlog3.warn(`failed to persist message for ${baseName}`, logger_default.errorData(err));
830
+ }
831
+ const manager = getDeliveryManager();
832
+ await manager.routeAndDeliver(mindName, payload);
833
+ } catch (err) {
834
+ dlog3.warn(`unexpected error delivering to ${mindName}`, logger_default.errorData(err));
835
+ }
836
+ }
837
+
881
838
  export {
882
- extractTextContent,
883
- deliverMessage,
884
839
  subscribe,
885
840
  publish,
886
841
  getTypingMap,
887
842
  publishTypingForChannels,
888
- DeliveryManager,
843
+ extractTextContent,
889
844
  initDeliveryManager,
890
- getDeliveryManager
845
+ getDeliveryManager,
846
+ deliverMessage
891
847
  };