volute 0.19.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 (104) hide show
  1. package/README.md +68 -68
  2. package/dist/activity-events-3WHHCOBB.js +15 -0
  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-OTWLI7F4.js → chunk-5462YKWP.js} +12 -9
  7. package/dist/{chunk-2TJGRJ4O.js → chunk-7LPTHFIL.js} +64 -59
  8. package/dist/chunk-A4S7H6G6.js +56 -0
  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-HGCDWKSP.js +97 -0
  13. package/dist/{chunk-DYZGP3EW.js → chunk-IPJXU366.js} +1 -1
  14. package/dist/{chunk-VE4D3GOP.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-4KPUF5JD.js → chunk-L3LHXZD7.js} +18 -5
  18. package/dist/{chunk-OGXOMR65.js → chunk-NWPT4ASZ.js} +1 -1
  19. package/dist/{chunk-FGV2H4TX.js → chunk-OGZYB5GL.js} +312 -268
  20. package/dist/{chunk-SCUDS4US.js → chunk-ON3FF5JA.js} +1 -1
  21. package/dist/{chunk-EMQSAY3B.js → chunk-PC6R6UUW.js} +6 -5
  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-VQWDC6UK.js → chunk-SGPEZ32F.js} +46 -1
  26. package/dist/{chunk-RHEGSQFJ.js → chunk-WSLPZF72.js} +1 -1
  27. package/dist/cli.js +59 -111
  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-JMZM3QY4.js → daemon-restart-BH67ZOTE.js} +8 -8
  35. package/dist/daemon.js +2872 -1301
  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-GCDNQCF3.js → export-XD6PJBQP.js} +19 -8
  40. package/dist/file-X4L5TTOL.js +204 -0
  41. package/dist/{history-WNK3DFUM.js → history-HTEKRNID.js} +2 -2
  42. package/dist/{import-M63VIUJ5.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-PQ5NCPSU.js → mind-BIDOF65R.js} +27 -11
  49. package/dist/mind-activity-tracker-PGC3DBJ7.js +18 -0
  50. package/dist/{mind-manager-RVCFROAY.js → mind-manager-3V2NXX4I.js} +5 -6
  51. package/dist/{package-MYE2ZJLV.js → package-HQR52XSG.js} +1 -1
  52. package/dist/{pages-AXCOSY3P.js → pages-KQBR5TAZ.js} +6 -6
  53. package/dist/{publish-YB377JB7.js → publish-OJ4QMXVZ.js} +12 -9
  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-LMX7GAQZ.js → schedule-NLR3LZLY.js} +27 -7
  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-OH3PJUJO.js → setup-OZDYCKDI.js} +25 -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 +38 -8
  67. package/dist/{sprout-VBEX63LX.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-JCJAOXTW.js → status-LV34BG6G.js} +6 -5
  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-WG65SWJU.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/drizzle/0012_activity.sql +11 -0
  84. package/drizzle/meta/0012_snapshot.json +7 -0
  85. package/drizzle/meta/_journal.json +7 -0
  86. package/package.json +1 -1
  87. package/templates/_base/home/.config/routes.json +2 -2
  88. package/templates/_base/home/VOLUTE.md +1 -1
  89. package/templates/_base/src/lib/daemon-client.ts +22 -0
  90. package/templates/_base/src/lib/transparency.ts +1 -1
  91. package/templates/claude/.init/.config/routes.json +7 -1
  92. package/templates/pi/.init/.config/routes.json +7 -1
  93. package/templates/pi/src/agent.ts +11 -5
  94. package/templates/pi/src/lib/session-context-extension.ts +6 -4
  95. package/templates/pi/src/server.ts +2 -0
  96. package/dist/chunk-UJ6GHNR7.js +0 -675
  97. package/dist/chunk-Z524RFCJ.js +0 -36
  98. package/dist/db-5ZVC6MQF.js +0 -10
  99. package/dist/delivery-manager-ISTJMZDW.js +0 -16
  100. package/dist/down-ZY35KMHR.js +0 -14
  101. package/dist/schema-5BW7DFZI.js +0 -24
  102. package/dist/variants-JAGWGBXG.js +0 -26
  103. package/dist/web-assets/assets/index-BAbuRsVF.css +0 -1
  104. package/dist/web-assets/assets/index-CiQhSKi_.js +0 -63
@@ -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-Z524RFCJ.js";
8
2
  import {
9
3
  deliveryQueue,
4
+ getDb,
10
5
  mindHistory
11
- } from "./chunk-VQWDC6UK.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,162 +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-ISTJMZDW.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/typing.ts
246
- var DEFAULT_TTL_MS = 1e4;
247
- var SWEEP_INTERVAL_MS = 5e3;
248
- var TypingMap = class {
249
- channels = /* @__PURE__ */ new Map();
250
- sweepTimer;
251
- constructor() {
252
- this.sweepTimer = setInterval(() => this.sweep(), SWEEP_INTERVAL_MS);
253
- this.sweepTimer.unref();
254
- }
255
- set(channel, sender, opts) {
256
- const expiresAt = opts?.persistent ? Infinity : Date.now() + (opts?.ttlMs ?? DEFAULT_TTL_MS);
257
- let senders = this.channels.get(channel);
258
- if (!senders) {
259
- senders = /* @__PURE__ */ new Map();
260
- this.channels.set(channel, senders);
261
- }
262
- senders.set(sender, { expiresAt });
263
- }
264
- delete(channel, sender) {
265
- const senders = this.channels.get(channel);
266
- if (senders) {
267
- senders.delete(sender);
268
- if (senders.size === 0) {
269
- this.channels.delete(channel);
270
- }
271
- }
272
- }
273
- /** Remove a sender from all channels (e.g. when a mind finishes processing). */
274
- deleteSender(sender) {
275
- for (const [channel, senders] of this.channels) {
276
- senders.delete(sender);
277
- if (senders.size === 0) {
278
- this.channels.delete(channel);
279
- }
280
- }
281
- }
282
- get(channel) {
283
- const senders = this.channels.get(channel);
284
- if (!senders) return [];
285
- const now = Date.now();
286
- const result = [];
287
- for (const [sender, entry] of senders) {
288
- if (entry.expiresAt > now) {
289
- result.push(sender);
290
- }
291
- }
292
- return result;
293
- }
294
- dispose() {
295
- clearInterval(this.sweepTimer);
296
- this.channels.clear();
297
- if (instance === this) instance = void 0;
298
- }
299
- sweep() {
300
- const now = Date.now();
301
- for (const [channel, senders] of this.channels) {
302
- for (const [sender, entry] of senders) {
303
- if (entry.expiresAt <= now) {
304
- senders.delete(sender);
305
- }
306
- }
307
- if (senders.size === 0) {
308
- this.channels.delete(channel);
309
- }
310
- }
311
- }
312
- };
313
- var instance;
314
- function getTypingMap() {
315
- if (!instance) {
316
- instance = new TypingMap();
317
- }
318
- return instance;
319
- }
320
-
321
- // src/lib/delivery-manager.ts
322
- var dlog3 = logger_default.child("delivery-manager");
305
+ // src/lib/delivery/delivery-manager.ts
306
+ var dlog2 = logger_default.child("delivery-manager");
323
307
  var MAX_BATCH_SIZE = 50;
324
308
  var DeliveryManager = class {
325
309
  sessionStates = /* @__PURE__ */ new Map();
@@ -396,7 +380,7 @@ var DeliveryManager = class {
396
380
  try {
397
381
  payload = JSON.parse(row.payload);
398
382
  } catch (parseErr) {
399
- dlog3.warn(
383
+ dlog2.warn(
400
384
  `corrupt payload in delivery queue row ${row.id}, skipping`,
401
385
  logger_default.errorData(parseErr)
402
386
  );
@@ -407,22 +391,21 @@ var DeliveryManager = class {
407
391
  if (sessionConfig.delivery.mode === "batch") {
408
392
  this.addToBatchBuffer(row.mind, row.session, payload, sessionConfig);
409
393
  } else {
410
- this.deliverToMind(row.mind, row.session, payload, sessionConfig).then(async () => {
411
- try {
412
- const db2 = await getDb();
413
- await db2.delete(deliveryQueue).where(eq(deliveryQueue.id, row.id));
414
- } catch {
415
- }
416
- }).catch((err) => {
417
- 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));
418
401
  });
419
402
  }
420
403
  }
421
404
  if (rows.length > 0) {
422
- dlog3.info(`restored ${rows.length} queued messages from DB`);
405
+ dlog2.info(`restored ${rows.length} queued messages from DB`);
423
406
  }
424
407
  } catch (err) {
425
- 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));
426
409
  }
427
410
  }
428
411
  /**
@@ -471,34 +454,18 @@ var DeliveryManager = class {
471
454
  if (instance2 === this) instance2 = void 0;
472
455
  }
473
456
  // --- Private ---
474
- async deliverToMind(mindName, session, payload, sessionConfig) {
457
+ resolvePort(mindName) {
475
458
  const [baseName, variantName] = mindName.split("@", 2);
476
459
  const entry = findMind(baseName);
477
- if (!entry) {
478
- dlog3.warn(`cannot deliver to ${mindName}: mind not found`);
479
- return;
480
- }
481
- let port = entry.port;
460
+ if (!entry) return null;
482
461
  if (variantName) {
483
462
  const variant = findVariant(baseName, variantName);
484
- if (!variant) {
485
- dlog3.warn(`cannot deliver to ${mindName}: variant not found`);
486
- return;
487
- }
488
- port = variant.port;
463
+ if (!variant) return null;
464
+ return { baseName, port: variant.port };
489
465
  }
490
- this.incrementActive(baseName, session);
491
- const typingMap = getTypingMap();
492
- if (payload.channel) {
493
- typingMap.set(payload.channel, baseName, { persistent: true });
494
- }
495
- const deliveryBody = {
496
- ...payload,
497
- session,
498
- interrupt: sessionConfig.interrupt,
499
- instructions: sessionConfig.instructions
500
- };
501
- const body = JSON.stringify(deliveryBody);
466
+ return { baseName, port: entry.port };
467
+ }
468
+ async postToMind(port, body) {
502
469
  const controller = new AbortController();
503
470
  const timeout = setTimeout(() => controller.abort(), 12e4);
504
471
  try {
@@ -510,74 +477,96 @@ var DeliveryManager = class {
510
477
  });
511
478
  if (!res.ok) {
512
479
  const text = await res.text().catch(() => "");
513
- dlog3.warn(`mind ${mindName} responded ${res.status}: ${text}`);
514
- this.decrementActive(baseName, session);
515
- if (payload.channel) typingMap.delete(payload.channel, baseName);
516
- } else {
517
- await res.text().catch(() => {
518
- });
480
+ dlog2.warn(`mind responded ${res.status}: ${text}`);
481
+ return false;
519
482
  }
520
- } catch (err) {
521
- dlog3.warn(`failed to deliver to ${mindName}`, logger_default.errorData(err));
522
- this.decrementActive(baseName, session);
523
- if (payload.channel) typingMap.delete(payload.channel, baseName);
483
+ await res.text().catch(() => {
484
+ });
485
+ return true;
524
486
  } finally {
525
487
  clearTimeout(timeout);
526
488
  }
527
489
  }
528
- async deliverBatchToMind(mindName, session, messages, sessionConfig) {
529
- const [baseName, variantName] = mindName.split("@", 2);
530
- const entry = findMind(baseName);
531
- if (!entry) {
532
- dlog3.warn(`cannot deliver batch to ${mindName}: mind not found`);
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`);
533
494
  return;
534
495
  }
535
- let port = entry.port;
536
- if (variantName) {
537
- const variant = findVariant(baseName, variantName);
538
- if (!variant) {
539
- dlog3.warn(`cannot deliver batch to ${mindName}: variant not found`);
540
- return;
496
+ const { baseName, port } = resolved;
497
+ const senders = /* @__PURE__ */ new Set();
498
+ if (payload.sender) senders.add(payload.sender);
499
+ const channels = /* @__PURE__ */ new Set();
500
+ if (payload.channel) channels.add(payload.channel);
501
+ this.incrementActive(baseName, session, senders, channels);
502
+ const typingMap = getTypingMap();
503
+ if (payload.channel) {
504
+ typingMap.set(payload.channel, baseName, { persistent: true });
505
+ }
506
+ if (payload.conversationId) {
507
+ typingMap.set(`volute:${payload.conversationId}`, baseName, { persistent: true });
508
+ }
509
+ const body = JSON.stringify({
510
+ ...payload,
511
+ session,
512
+ interrupt: sessionConfig.interrupt,
513
+ instructions: sessionConfig.instructions
514
+ });
515
+ try {
516
+ const ok = await this.postToMind(port, body);
517
+ if (!ok) {
518
+ this.decrementActive(baseName, session);
519
+ publishTypingForChannels(typingMap.deleteSender(baseName), typingMap);
541
520
  }
542
- port = variant.port;
521
+ } catch (err) {
522
+ dlog2.warn(`failed to deliver to ${mindName}`, logger_default.errorData(err));
523
+ this.decrementActive(baseName, session);
524
+ publishTypingForChannels(typingMap.deleteSender(baseName), typingMap);
543
525
  }
526
+ }
527
+ async deliverBatchToMind(mindName, session, messages, sessionConfig, interruptOverride) {
528
+ const resolved = this.resolvePort(mindName);
529
+ if (!resolved) {
530
+ dlog2.warn(`cannot deliver batch to ${mindName}: mind not found`);
531
+ return;
532
+ }
533
+ const { baseName, port } = resolved;
544
534
  const channels = {};
545
535
  for (const msg of messages) {
546
536
  const ch = msg.channel ?? "unknown";
547
537
  if (!channels[ch]) channels[ch] = [];
548
538
  channels[ch].push(msg.payload);
549
539
  }
550
- this.incrementActive(baseName, session);
540
+ const senders = /* @__PURE__ */ new Set();
541
+ const channelSet = /* @__PURE__ */ new Set();
542
+ for (const msg of messages) {
543
+ if (msg.sender) senders.add(msg.sender);
544
+ if (msg.channel) channelSet.add(msg.channel);
545
+ }
546
+ this.incrementActive(baseName, session, senders, channelSet);
551
547
  const typingMap = getTypingMap();
552
548
  for (const ch of Object.keys(channels)) {
553
549
  if (ch !== "unknown") typingMap.set(ch, baseName, { persistent: true });
554
550
  }
555
- const batchBody = {
551
+ const seenConvIds = /* @__PURE__ */ new Set();
552
+ for (const msg of messages) {
553
+ if (msg.payload.conversationId && !seenConvIds.has(msg.payload.conversationId)) {
554
+ seenConvIds.add(msg.payload.conversationId);
555
+ typingMap.set(`volute:${msg.payload.conversationId}`, baseName, { persistent: true });
556
+ }
557
+ }
558
+ const body = JSON.stringify({
556
559
  session,
557
560
  batch: { channels },
558
- interrupt: sessionConfig.interrupt,
561
+ interrupt: interruptOverride ?? sessionConfig.interrupt,
559
562
  instructions: sessionConfig.instructions
560
- };
561
- const body = JSON.stringify(batchBody);
562
- const controller = new AbortController();
563
- const timeout = setTimeout(() => controller.abort(), 12e4);
563
+ });
564
564
  try {
565
- const res = await fetch(`http://127.0.0.1:${port}/message`, {
566
- method: "POST",
567
- headers: { "Content-Type": "application/json" },
568
- body,
569
- signal: controller.signal
570
- });
571
- if (!res.ok) {
572
- const text = await res.text().catch(() => "");
573
- dlog3.warn(`mind ${mindName} batch responded ${res.status}: ${text}`);
565
+ const ok = await this.postToMind(port, body);
566
+ if (!ok) {
574
567
  this.decrementActive(baseName, session);
575
- for (const ch of Object.keys(channels)) {
576
- typingMap.delete(ch, baseName);
577
- }
568
+ publishTypingForChannels(typingMap.deleteSender(baseName), typingMap);
578
569
  } else {
579
- await res.text().catch(() => {
580
- });
581
570
  try {
582
571
  const db = await getDb();
583
572
  await db.delete(deliveryQueue).where(
@@ -588,20 +577,16 @@ var DeliveryManager = class {
588
577
  )
589
578
  );
590
579
  } catch (err) {
591
- dlog3.warn(
580
+ dlog2.warn(
592
581
  `failed to clean delivery queue for ${baseName}/${session}`,
593
582
  logger_default.errorData(err)
594
583
  );
595
584
  }
596
585
  }
597
586
  } catch (err) {
598
- 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));
599
588
  this.decrementActive(baseName, session);
600
- for (const ch of Object.keys(channels)) {
601
- typingMap.delete(ch, baseName);
602
- }
603
- } finally {
604
- clearTimeout(timeout);
589
+ publishTypingForChannels(typingMap.deleteSender(baseName), typingMap);
605
590
  }
606
591
  }
607
592
  enqueueBatch(mindName, session, payload, sessionConfig) {
@@ -621,8 +606,23 @@ var DeliveryManager = class {
621
606
  return;
622
607
  }
623
608
  }
609
+ const [baseName] = mindName.split("@", 2);
610
+ const state = this.sessionStates.get(baseName)?.get(session);
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) {
612
+ state.lastInterruptAt = Date.now();
613
+ this.persistToQueue(mindName, session, payload).catch((err) => {
614
+ dlog2.warn(`failed to persist batch message for ${mindName}/${session}`, logger_default.errorData(err));
615
+ });
616
+ this.flushBatch(
617
+ mindName,
618
+ session,
619
+ [{ payload, channel: payload.channel, sender: payload.sender, createdAt: Date.now() }],
620
+ true
621
+ );
622
+ return;
623
+ }
624
624
  this.persistToQueue(mindName, session, payload).catch((err) => {
625
- 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));
626
626
  });
627
627
  this.addToBatchBuffer(mindName, session, payload, sessionConfig);
628
628
  }
@@ -668,7 +668,7 @@ var DeliveryManager = class {
668
668
  buffer.maxWaitTimer.unref();
669
669
  }
670
670
  }
671
- flushBatch(mindName, session, extra) {
671
+ flushBatch(mindName, session, extra, interruptOverride) {
672
672
  const bufferKey = `${mindName}:${session}`;
673
673
  const buffer = this.batchBuffers.get(bufferKey);
674
674
  const messages = [];
@@ -685,10 +685,14 @@ var DeliveryManager = class {
685
685
  const [baseName] = mindName.split("@", 2);
686
686
  const config = getRoutingConfig(baseName);
687
687
  const sessionConfig = resolveDeliveryMode(config, session);
688
- dlog3.info(`flushing batch for ${mindName}/${session}: ${messages.length} messages`);
689
- this.deliverBatchToMind(mindName, session, messages, sessionConfig).catch((err) => {
690
- dlog3.warn(`failed to flush batch for ${mindName}/${session}`, logger_default.errorData(err));
691
- });
688
+ dlog2.info(
689
+ `flushing batch for ${mindName}/${session}: ${messages.length} messages${interruptOverride ? " (new-speaker interrupt)" : ""}`
690
+ );
691
+ this.deliverBatchToMind(mindName, session, messages, sessionConfig, interruptOverride).catch(
692
+ (err) => {
693
+ dlog2.warn(`failed to flush batch for ${mindName}/${session}`, logger_default.errorData(err));
694
+ }
695
+ );
692
696
  }
693
697
  async gateMessage(mindName, session, payload) {
694
698
  const [baseName] = mindName.split("@", 2);
@@ -706,7 +710,7 @@ var DeliveryManager = class {
706
710
  await this.sendInviteNotification(mindName, payload);
707
711
  }
708
712
  } catch (err) {
709
- 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));
710
714
  }
711
715
  }
712
716
  async sendInviteNotification(mindName, payload) {
@@ -748,21 +752,29 @@ var DeliveryManager = class {
748
752
  payload: JSON.stringify(payload)
749
753
  });
750
754
  } catch (err) {
751
- dlog3.warn(
755
+ dlog2.warn(
752
756
  `failed to persist to delivery queue for ${mindName}/${session}`,
753
757
  logger_default.errorData(err)
754
758
  );
755
759
  }
756
760
  }
757
- incrementActive(mind, session) {
761
+ incrementActive(mind, session, senders, channels) {
758
762
  let mindSessions = this.sessionStates.get(mind);
759
763
  if (!mindSessions) {
760
764
  mindSessions = /* @__PURE__ */ new Map();
761
765
  this.sessionStates.set(mind, mindSessions);
762
766
  }
763
- const state = mindSessions.get(session) ?? { activeCount: 0, lastDeliveredAt: 0 };
767
+ const state = mindSessions.get(session) ?? {
768
+ activeCount: 0,
769
+ lastDeliveredAt: 0,
770
+ lastDeliverySenders: /* @__PURE__ */ new Set(),
771
+ lastDeliveryChannels: /* @__PURE__ */ new Set(),
772
+ lastInterruptAt: 0
773
+ };
764
774
  state.activeCount++;
765
775
  state.lastDeliveredAt = Date.now();
776
+ if (senders) state.lastDeliverySenders = senders;
777
+ if (channels) state.lastDeliveryChannels = channels;
766
778
  mindSessions.set(session, state);
767
779
  }
768
780
  decrementActive(mind, session) {
@@ -782,7 +794,7 @@ var DeliveryManager = class {
782
794
  };
783
795
  var instance2;
784
796
  function initDeliveryManager() {
785
- if (instance2) return instance2;
797
+ if (instance2) throw new Error("DeliveryManager already initialized");
786
798
  instance2 = new DeliveryManager();
787
799
  return instance2;
788
800
  }
@@ -793,11 +805,43 @@ function getDeliveryManager() {
793
805
  return instance2;
794
806
  }
795
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
+
796
838
  export {
797
- extractTextContent,
798
- deliverMessage,
839
+ subscribe,
840
+ publish,
799
841
  getTypingMap,
800
- DeliveryManager,
842
+ publishTypingForChannels,
843
+ extractTextContent,
801
844
  initDeliveryManager,
802
- getDeliveryManager
845
+ getDeliveryManager,
846
+ deliverMessage
803
847
  };