volute 0.21.0 → 0.23.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 (58) hide show
  1. package/dist/api.d.ts +4294 -0
  2. package/dist/chunk-G5KRTU2F.js +76 -0
  3. package/dist/chunk-ISWZ6QUK.js +2691 -0
  4. package/dist/{chunk-J5A3DF2U.js → chunk-JG4CCJOA.js} +1 -1
  5. package/dist/{chunk-IPJXU366.js → chunk-JTDFJWI2.js} +1 -0
  6. package/dist/{chunk-7LPTHFIL.js → chunk-M5CNKH4J.js} +55 -5
  7. package/dist/{chunk-L3LHXZD7.js → chunk-PHHKNGA3.js} +1 -1
  8. package/dist/{chunk-Q7AITQ44.js → chunk-QIXPN3OO.js} +1 -1
  9. package/dist/{chunk-PC6R6UUW.js → chunk-RK627D57.js} +36 -59
  10. package/dist/{chunk-5462YKWP.js → chunk-TFS25FIM.js} +1 -1
  11. package/dist/{chunk-QUJUKM4U.js → chunk-VT5QODNE.js} +1 -1
  12. package/dist/chunk-XLC342FO.js +29 -0
  13. package/dist/cli.js +10 -10
  14. package/dist/cloud-sync-PI47U2LT.js +96 -0
  15. package/dist/{daemon-restart-BH67ZOTE.js → daemon-restart-RMGOOGPE.js} +4 -4
  16. package/dist/daemon.js +1216 -1822
  17. package/dist/{down-LIOQ5JDH.js → down-WSUASL5E.js} +3 -3
  18. package/dist/{import-E433B4KG.js → import-EAXTHHXL.js} +2 -1
  19. package/dist/message-delivery-FHV4NO2F.js +23 -0
  20. package/dist/{mind-BIDOF65R.js → mind-BTXR5B3C.js} +13 -5
  21. package/dist/{mind-manager-3V2NXX4I.js → mind-manager-KMY4GA2J.js} +1 -1
  22. package/dist/mind-sleep-FWRBIFBS.js +41 -0
  23. package/dist/mind-wake-LJK2YU5X.js +36 -0
  24. package/dist/{package-HQR52XSG.js → package-CUBJ4PKS.js} +10 -1
  25. package/dist/{pages-KQBR5TAZ.js → pages-YSTRWJR4.js} +1 -1
  26. package/dist/{publish-OJ4QMXVZ.js → publish-BZNHKUUK.js} +2 -2
  27. package/dist/{service-TVNEORO7.js → service-7BFXDI6J.js} +4 -4
  28. package/dist/{setup-OZDYCKDI.js → setup-SSIIXQMI.js} +2 -2
  29. package/dist/sleep-manager-2TMQ65E4.js +27 -0
  30. package/dist/{sprout-6Z6C42YM.js → sprout-UKCYBGHK.js} +2 -2
  31. package/dist/{status-Z7NAFMBI.js → status-H2MKDN6L.js} +2 -2
  32. package/dist/{up-7BGDMFRT.js → up-Z5JRG2M2.js} +3 -3
  33. package/dist/{update-4WT7VWHW.js → update-ELC6MEUT.js} +2 -2
  34. package/dist/{upgrade-ZEC2GGFO.js → upgrade-GXW2EQY3.js} +11 -2
  35. package/dist/{version-notify-TFS2U5CF.js → version-notify-LKABEJSA.js} +11 -3
  36. package/dist/web-assets/assets/index-CZ26vsyY.js +69 -0
  37. package/dist/web-assets/assets/index-DyyAvJwW.css +1 -0
  38. package/dist/web-assets/index.html +2 -2
  39. package/package.json +10 -1
  40. package/templates/_base/.init/.config/prompts.json +1 -0
  41. package/templates/_base/home/.config/config.json.tmpl +4 -1
  42. package/templates/_base/src/lib/file-handler.ts +6 -1
  43. package/templates/_base/src/lib/logger.ts +68 -23
  44. package/templates/_base/src/lib/startup.ts +12 -3
  45. package/templates/claude/src/agent.ts +150 -29
  46. package/templates/claude/src/lib/hooks/pre-compact.ts +18 -4
  47. package/templates/claude/src/lib/message-channel.ts +6 -0
  48. package/templates/claude/src/lib/stream-consumer.ts +17 -1
  49. package/templates/claude/src/server.ts +3 -1
  50. package/templates/pi/home/.config/config.json.tmpl +4 -1
  51. package/templates/pi/src/agent.ts +87 -0
  52. package/templates/pi/src/lib/content.ts +18 -3
  53. package/templates/pi/src/lib/event-handler.ts +22 -2
  54. package/templates/pi/src/server.ts +3 -1
  55. package/dist/chunk-OGZYB5GL.js +0 -847
  56. package/dist/web-assets/assets/index-BR3gtK3E.css +0 -1
  57. package/dist/web-assets/assets/index-CWmrZRQd.js +0 -64
  58. /package/dist/{shared-DCQ2UXOM.js → shared-2OGT3NSL.js} +0 -0
@@ -1,847 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- deliveryQueue,
4
- getDb,
5
- mindHistory
6
- } from "./chunk-SGPEZ32F.js";
7
- import {
8
- logger_default
9
- } from "./chunk-YUIHSKR6.js";
10
- import {
11
- findMind,
12
- findVariant,
13
- mindDir
14
- } from "./chunk-B2CPS4QU.js";
15
-
16
- // src/lib/delivery/delivery-manager.ts
17
- import { and, eq, sql } from "drizzle-orm";
18
-
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
138
- import { readFileSync, statSync } from "fs";
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
- }
147
- var configCache = /* @__PURE__ */ new Map();
148
- var statCheckCache = /* @__PURE__ */ new Map();
149
- var STAT_TTL_MS = 5e3;
150
- var dlog = logger_default.child("delivery-router");
151
- function configPath(mindName) {
152
- return resolve(mindDir(mindName), "home/.config/routes.json");
153
- }
154
- function getRoutingConfig(mindName) {
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
- }
162
- let mtime;
163
- try {
164
- mtime = statSync(path).mtimeMs;
165
- } catch {
166
- configCache.delete(mindName);
167
- statCheckCache.delete(mindName);
168
- return {};
169
- }
170
- statCheckCache.set(mindName, { mtime, checkedAt: now });
171
- if (cached && cached.mtime === mtime) {
172
- return cached.config;
173
- }
174
- try {
175
- const parsed = JSON.parse(readFileSync(path, "utf-8"));
176
- const config = Array.isArray(parsed) ? { rules: parsed } : parsed;
177
- configCache.set(mindName, { config, mtime });
178
- return config;
179
- } catch (err) {
180
- dlog.warn(`failed to load routes.json for ${mindName}`, logger_default.errorData(err));
181
- configCache.delete(mindName);
182
- return {};
183
- }
184
- }
185
- var globRegexCache = /* @__PURE__ */ new Map();
186
- function globMatch(pattern, 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);
194
- }
195
- var GLOB_MATCH_KEYS = /* @__PURE__ */ new Set(["channel", "sender"]);
196
- var NON_MATCH_KEYS = /* @__PURE__ */ new Set(["session", "destination", "path", "mode"]);
197
- function ruleMatches(rule, meta) {
198
- for (const [key, pattern] of Object.entries(rule)) {
199
- if (NON_MATCH_KEYS.has(key)) continue;
200
- if (key === "isDM") {
201
- if (typeof pattern !== "boolean") return false;
202
- if ((meta.isDM ?? false) !== pattern) return false;
203
- continue;
204
- }
205
- if (key === "participants") {
206
- if (typeof pattern !== "number") return false;
207
- if ((meta.participantCount ?? 0) !== pattern) return false;
208
- continue;
209
- }
210
- if (typeof pattern !== "string") return false;
211
- if (!GLOB_MATCH_KEYS.has(key)) return false;
212
- const value = meta[key] ?? "";
213
- if (!globMatch(pattern, value)) return false;
214
- }
215
- return true;
216
- }
217
- function expandTemplate(template, meta) {
218
- return template.replace(/\$\{sender\}/g, meta.sender ?? "unknown").replace(/\$\{channel\}/g, meta.channel ?? "unknown");
219
- }
220
- function sanitizeSessionName(name) {
221
- return name.replace(/\0/g, "").replace(/[/\\]/g, "-").replace(/\.\./g, "-").slice(0, 100);
222
- }
223
- function resolveRoute(config, meta) {
224
- const fallback = config.default ?? "main";
225
- if (!config.rules) {
226
- return { destination: "mind", session: fallback, matched: false };
227
- }
228
- for (const rule of config.rules) {
229
- if (ruleMatches(rule, meta)) {
230
- if (rule.destination === "file") {
231
- if (!rule.path) {
232
- dlog.warn("file destination rule missing path \u2014 falling through");
233
- continue;
234
- }
235
- return { destination: "file", path: rule.path, matched: true };
236
- }
237
- return {
238
- destination: "mind",
239
- session: sanitizeSessionName(expandTemplate(rule.session ?? fallback, meta)),
240
- matched: true,
241
- mode: rule.mode
242
- };
243
- }
244
- }
245
- return { destination: "mind", session: fallback, matched: false };
246
- }
247
- var DEFAULT_BATCH_DEBOUNCE = 5;
248
- var DEFAULT_BATCH_MAX_WAIT = 120;
249
- function normalizeBatchConfig(batch) {
250
- if (typeof batch === "number") return { maxWait: batch * 60 };
251
- return batch;
252
- }
253
- function resolveDeliveryMode(config, sessionName) {
254
- const defaults = {
255
- delivery: { mode: "immediate" },
256
- interrupt: true
257
- };
258
- if (!config.sessions) return defaults;
259
- for (const [pattern, sessionConfig] of Object.entries(config.sessions)) {
260
- if (globMatch(pattern, sessionName)) {
261
- let delivery;
262
- if (sessionConfig.delivery != null) {
263
- if (sessionConfig.delivery === "immediate") {
264
- delivery = { mode: "immediate" };
265
- } else if (sessionConfig.delivery === "batch") {
266
- delivery = {
267
- mode: "batch",
268
- debounce: DEFAULT_BATCH_DEBOUNCE,
269
- maxWait: DEFAULT_BATCH_MAX_WAIT
270
- };
271
- } else {
272
- delivery = {
273
- mode: "batch",
274
- debounce: sessionConfig.delivery.debounce ?? DEFAULT_BATCH_DEBOUNCE,
275
- maxWait: sessionConfig.delivery.maxWait ?? DEFAULT_BATCH_MAX_WAIT
276
- };
277
- }
278
- } else if (sessionConfig.batch != null) {
279
- const batch = normalizeBatchConfig(sessionConfig.batch);
280
- delivery = {
281
- mode: "batch",
282
- debounce: batch.debounce ?? DEFAULT_BATCH_DEBOUNCE,
283
- maxWait: batch.maxWait ?? DEFAULT_BATCH_MAX_WAIT,
284
- triggers: batch.triggers
285
- };
286
- } else if (sessionConfig.interrupt === false) {
287
- delivery = {
288
- mode: "batch",
289
- debounce: DEFAULT_BATCH_DEBOUNCE,
290
- maxWait: DEFAULT_BATCH_MAX_WAIT
291
- };
292
- } else {
293
- delivery = { mode: "immediate" };
294
- }
295
- return {
296
- delivery,
297
- interrupt: sessionConfig.interrupt ?? true,
298
- instructions: sessionConfig.instructions
299
- };
300
- }
301
- }
302
- return defaults;
303
- }
304
-
305
- // src/lib/delivery/delivery-manager.ts
306
- var dlog2 = logger_default.child("delivery-manager");
307
- var MAX_BATCH_SIZE = 50;
308
- var DeliveryManager = class {
309
- sessionStates = /* @__PURE__ */ new Map();
310
- batchBuffers = /* @__PURE__ */ new Map();
311
- // --- Public API ---
312
- /**
313
- * Route and deliver a message to a mind. This is the main entry point.
314
- * The message is routed via the mind's routes.json, then either delivered immediately
315
- * or queued for batching depending on the session's delivery mode.
316
- */
317
- async routeAndDeliver(mindName, payload) {
318
- const [baseName] = mindName.split("@", 2);
319
- const config = getRoutingConfig(baseName);
320
- const meta = {
321
- channel: payload.channel,
322
- sender: payload.sender ?? void 0,
323
- isDM: payload.isDM,
324
- participantCount: payload.participantCount
325
- };
326
- const route = resolveRoute(config, meta);
327
- if (route.destination === "file") {
328
- return { routed: true, session: route.path, destination: "file", mode: "immediate" };
329
- }
330
- if (!route.matched && config.gateUnmatched !== false) {
331
- await this.gateMessage(mindName, route.session, payload);
332
- return { routed: true, session: route.session, destination: "mind", mode: "gated" };
333
- }
334
- if (route.mode === "mention" && payload.sender) {
335
- const text = extractTextContent(payload.content);
336
- const escaped = baseName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
337
- const pattern = new RegExp(`\\b${escaped}\\b`, "i");
338
- if (!pattern.test(text)) {
339
- return { routed: false, reason: "mention-filtered" };
340
- }
341
- }
342
- let sessionName = route.session;
343
- if (sessionName === "$new") {
344
- sessionName = `new-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
345
- }
346
- const sessionConfig = resolveDeliveryMode(config, sessionName);
347
- if (sessionConfig.delivery.mode === "batch") {
348
- this.enqueueBatch(mindName, sessionName, payload, sessionConfig);
349
- return { routed: true, session: sessionName, destination: "mind", mode: "batch" };
350
- }
351
- await this.deliverToMind(mindName, sessionName, payload, sessionConfig);
352
- return { routed: true, session: sessionName, destination: "mind", mode: "immediate" };
353
- }
354
- /**
355
- * Called when a mind's session emits a "done" event — decrements active count
356
- * and may trigger batch flush if session goes idle.
357
- */
358
- sessionDone(mindName, session) {
359
- const [baseName] = mindName.split("@", 2);
360
- if (session) {
361
- this.decrementActive(baseName, session);
362
- } else {
363
- const mindSessions = this.sessionStates.get(baseName);
364
- if (mindSessions) {
365
- for (const [sessionName] of mindSessions) {
366
- this.decrementActive(baseName, sessionName);
367
- }
368
- }
369
- }
370
- }
371
- /**
372
- * Restore queued messages from DB on daemon restart.
373
- */
374
- async restoreFromDb() {
375
- try {
376
- const db = await getDb();
377
- const rows = await db.select().from(deliveryQueue).where(eq(deliveryQueue.status, "pending"));
378
- for (const row of rows) {
379
- let payload;
380
- try {
381
- payload = JSON.parse(row.payload);
382
- } catch (parseErr) {
383
- dlog2.warn(
384
- `corrupt payload in delivery queue row ${row.id}, skipping`,
385
- logger_default.errorData(parseErr)
386
- );
387
- continue;
388
- }
389
- const config = getRoutingConfig(row.mind);
390
- const sessionConfig = resolveDeliveryMode(config, row.session);
391
- if (sessionConfig.delivery.mode === "batch") {
392
- this.addToBatchBuffer(row.mind, row.session, payload, sessionConfig);
393
- } else {
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));
401
- });
402
- }
403
- }
404
- if (rows.length > 0) {
405
- dlog2.info(`restored ${rows.length} queued messages from DB`);
406
- }
407
- } catch (err) {
408
- dlog2.warn("failed to restore delivery queue from DB", logger_default.errorData(err));
409
- }
410
- }
411
- /**
412
- * Get pending (gated) messages for a mind.
413
- */
414
- async getPending(mindName) {
415
- const db = await getDb();
416
- const rows = await db.select().from(deliveryQueue).where(and(eq(deliveryQueue.mind, mindName), eq(deliveryQueue.status, "gated")));
417
- const byChannel = /* @__PURE__ */ new Map();
418
- for (const row of rows) {
419
- const ch = row.channel ?? "unknown";
420
- const existing = byChannel.get(ch) ?? [];
421
- existing.push(row);
422
- byChannel.set(ch, existing);
423
- }
424
- return [...byChannel.entries()].map(([channel, channelRows]) => {
425
- const firstRow = channelRows[0];
426
- const payload = JSON.parse(firstRow.payload);
427
- const text = extractTextContent(payload.content);
428
- return {
429
- channel,
430
- sender: firstRow.sender,
431
- count: channelRows.length,
432
- firstSeen: firstRow.created_at,
433
- preview: text.length > 200 ? `${text.slice(0, 200)}...` : text
434
- };
435
- });
436
- }
437
- /**
438
- * Check if a session is currently busy (has active deliveries).
439
- */
440
- isSessionBusy(mindName, session) {
441
- const state = this.sessionStates.get(mindName)?.get(session);
442
- return (state?.activeCount ?? 0) > 0;
443
- }
444
- /**
445
- * Cleanup all timers and subscriptions.
446
- */
447
- dispose() {
448
- for (const [, buffer] of this.batchBuffers) {
449
- if (buffer.debounceTimer) clearTimeout(buffer.debounceTimer);
450
- if (buffer.maxWaitTimer) clearTimeout(buffer.maxWaitTimer);
451
- }
452
- this.batchBuffers.clear();
453
- this.sessionStates.clear();
454
- if (instance2 === this) instance2 = void 0;
455
- }
456
- // --- Private ---
457
- resolvePort(mindName) {
458
- const [baseName, variantName] = mindName.split("@", 2);
459
- const entry = findMind(baseName);
460
- if (!entry) return null;
461
- if (variantName) {
462
- const variant = findVariant(baseName, variantName);
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;
482
- }
483
- await res.text().catch(() => {
484
- });
485
- return true;
486
- } finally {
487
- clearTimeout(timeout);
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;
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);
520
- }
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);
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;
534
- const channels = {};
535
- for (const msg of messages) {
536
- const ch = msg.channel ?? "unknown";
537
- if (!channels[ch]) channels[ch] = [];
538
- channels[ch].push(msg.payload);
539
- }
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);
547
- const typingMap = getTypingMap();
548
- for (const ch of Object.keys(channels)) {
549
- if (ch !== "unknown") typingMap.set(ch, baseName, { persistent: true });
550
- }
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({
559
- session,
560
- batch: { channels },
561
- interrupt: interruptOverride ?? sessionConfig.interrupt,
562
- instructions: sessionConfig.instructions
563
- });
564
- try {
565
- const ok = await this.postToMind(port, body);
566
- if (!ok) {
567
- this.decrementActive(baseName, session);
568
- publishTypingForChannels(typingMap.deleteSender(baseName), typingMap);
569
- } else {
570
- try {
571
- const db = await getDb();
572
- await db.delete(deliveryQueue).where(
573
- and(
574
- eq(deliveryQueue.mind, baseName),
575
- eq(deliveryQueue.session, session),
576
- eq(deliveryQueue.status, "pending")
577
- )
578
- );
579
- } catch (err) {
580
- dlog2.warn(
581
- `failed to clean delivery queue for ${baseName}/${session}`,
582
- logger_default.errorData(err)
583
- );
584
- }
585
- }
586
- } catch (err) {
587
- dlog2.warn(`failed to deliver batch to ${mindName}`, logger_default.errorData(err));
588
- this.decrementActive(baseName, session);
589
- publishTypingForChannels(typingMap.deleteSender(baseName), typingMap);
590
- }
591
- }
592
- enqueueBatch(mindName, session, payload, sessionConfig) {
593
- const delivery = sessionConfig.delivery;
594
- if (delivery.triggers?.length) {
595
- const text = extractTextContent(payload.content);
596
- const lower = text.toLowerCase();
597
- if (delivery.triggers.some((t) => lower.includes(t.toLowerCase()))) {
598
- this.flushBatch(mindName, session, [
599
- {
600
- payload,
601
- channel: payload.channel,
602
- sender: payload.sender ?? null,
603
- createdAt: Date.now()
604
- }
605
- ]);
606
- return;
607
- }
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
- this.persistToQueue(mindName, session, payload).catch((err) => {
625
- dlog2.warn(`failed to persist batch message for ${mindName}/${session}`, logger_default.errorData(err));
626
- });
627
- this.addToBatchBuffer(mindName, session, payload, sessionConfig);
628
- }
629
- addToBatchBuffer(mindName, session, payload, sessionConfig) {
630
- const delivery = sessionConfig.delivery;
631
- const bufferKey = `${mindName}:${session}`;
632
- let buffer = this.batchBuffers.get(bufferKey);
633
- if (!buffer) {
634
- buffer = {
635
- messages: [],
636
- debounceTimer: null,
637
- maxWaitTimer: null,
638
- delivery
639
- };
640
- this.batchBuffers.set(bufferKey, buffer);
641
- }
642
- buffer.messages.push({
643
- payload,
644
- channel: payload.channel,
645
- sender: payload.sender ?? null,
646
- createdAt: Date.now()
647
- });
648
- if (buffer.messages.length >= MAX_BATCH_SIZE) {
649
- this.flushBatch(mindName, session);
650
- return;
651
- }
652
- this.scheduleBatchTimers(mindName, session, bufferKey);
653
- }
654
- scheduleBatchTimers(mindName, session, bufferKey) {
655
- const buffer = this.batchBuffers.get(bufferKey);
656
- if (!buffer) return;
657
- if (buffer.debounceTimer) clearTimeout(buffer.debounceTimer);
658
- buffer.debounceTimer = setTimeout(() => {
659
- if (!this.isSessionBusy(mindName, session)) {
660
- this.flushBatch(mindName, session);
661
- }
662
- }, buffer.delivery.debounce * 1e3);
663
- buffer.debounceTimer.unref();
664
- if (!buffer.maxWaitTimer) {
665
- buffer.maxWaitTimer = setTimeout(() => {
666
- this.flushBatch(mindName, session);
667
- }, buffer.delivery.maxWait * 1e3);
668
- buffer.maxWaitTimer.unref();
669
- }
670
- }
671
- flushBatch(mindName, session, extra, interruptOverride) {
672
- const bufferKey = `${mindName}:${session}`;
673
- const buffer = this.batchBuffers.get(bufferKey);
674
- const messages = [];
675
- if (buffer) {
676
- if (buffer.debounceTimer) clearTimeout(buffer.debounceTimer);
677
- if (buffer.maxWaitTimer) clearTimeout(buffer.maxWaitTimer);
678
- buffer.debounceTimer = null;
679
- buffer.maxWaitTimer = null;
680
- messages.push(...buffer.messages.splice(0));
681
- this.batchBuffers.delete(bufferKey);
682
- }
683
- if (extra) messages.push(...extra);
684
- if (messages.length === 0) return;
685
- const [baseName] = mindName.split("@", 2);
686
- const config = getRoutingConfig(baseName);
687
- const sessionConfig = resolveDeliveryMode(config, session);
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
- );
696
- }
697
- async gateMessage(mindName, session, payload) {
698
- const [baseName] = mindName.split("@", 2);
699
- await this.persistToQueue(baseName, session, payload, "gated");
700
- try {
701
- const db = await getDb();
702
- const count = await db.select({ count: sql`count(*)` }).from(deliveryQueue).where(
703
- and(
704
- eq(deliveryQueue.mind, baseName),
705
- eq(deliveryQueue.channel, payload.channel),
706
- eq(deliveryQueue.status, "gated")
707
- )
708
- );
709
- if ((count[0]?.count ?? 0) <= 1) {
710
- await this.sendInviteNotification(mindName, payload);
711
- }
712
- } catch (err) {
713
- dlog2.warn(`failed to check gated count for ${baseName}`, logger_default.errorData(err));
714
- }
715
- }
716
- async sendInviteNotification(mindName, payload) {
717
- const text = extractTextContent(payload.content);
718
- const preview = text.length > 200 ? `${text.slice(0, 200)}...` : text;
719
- const channel = payload.channel ?? "unknown";
720
- const notification = [
721
- `[New channel: ${channel}]`,
722
- `Sender: ${payload.sender ?? "unknown"}`,
723
- payload.platform ? `Platform: ${payload.platform}` : null,
724
- payload.participantCount ? `Participants: ${payload.participantCount}` : null,
725
- "",
726
- `Preview: ${preview}`,
727
- "",
728
- `To accept this channel, add a routing rule for "${channel}" to your routes.json.`,
729
- `Messages are being held until a route is configured.`
730
- ].filter((line) => line !== null).join("\n");
731
- const invitePayload = {
732
- channel: "system:delivery",
733
- sender: "system",
734
- content: [{ type: "text", text: notification }]
735
- };
736
- const config = getRoutingConfig(mindName.split("@", 2)[0]);
737
- const sessionConfig = resolveDeliveryMode(config, "main");
738
- await this.deliverToMind(mindName, "main", invitePayload, {
739
- ...sessionConfig,
740
- interrupt: true
741
- });
742
- }
743
- async persistToQueue(mindName, session, payload, status = "pending") {
744
- try {
745
- const db = await getDb();
746
- await db.insert(deliveryQueue).values({
747
- mind: mindName,
748
- session,
749
- channel: payload.channel ?? null,
750
- sender: payload.sender ?? null,
751
- status,
752
- payload: JSON.stringify(payload)
753
- });
754
- } catch (err) {
755
- dlog2.warn(
756
- `failed to persist to delivery queue for ${mindName}/${session}`,
757
- logger_default.errorData(err)
758
- );
759
- }
760
- }
761
- incrementActive(mind, session, senders, channels) {
762
- let mindSessions = this.sessionStates.get(mind);
763
- if (!mindSessions) {
764
- mindSessions = /* @__PURE__ */ new Map();
765
- this.sessionStates.set(mind, mindSessions);
766
- }
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
- };
774
- state.activeCount++;
775
- state.lastDeliveredAt = Date.now();
776
- if (senders) state.lastDeliverySenders = senders;
777
- if (channels) state.lastDeliveryChannels = channels;
778
- mindSessions.set(session, state);
779
- }
780
- decrementActive(mind, session) {
781
- const mindSessions = this.sessionStates.get(mind);
782
- if (!mindSessions) return;
783
- const state = mindSessions.get(session);
784
- if (!state) return;
785
- state.activeCount = Math.max(0, state.activeCount - 1);
786
- if (state.activeCount === 0) {
787
- const bufferKey = `${mind}:${session}`;
788
- const buffer = this.batchBuffers.get(bufferKey);
789
- if (buffer && buffer.messages.length > 0) {
790
- this.scheduleBatchTimers(mind, session, bufferKey);
791
- }
792
- }
793
- }
794
- };
795
- var instance2;
796
- function initDeliveryManager() {
797
- if (instance2) throw new Error("DeliveryManager already initialized");
798
- instance2 = new DeliveryManager();
799
- return instance2;
800
- }
801
- function getDeliveryManager() {
802
- if (!instance2) {
803
- throw new Error("DeliveryManager not initialized \u2014 call initDeliveryManager() first");
804
- }
805
- return instance2;
806
- }
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
-
838
- export {
839
- subscribe,
840
- publish,
841
- getTypingMap,
842
- publishTypingForChannels,
843
- extractTextContent,
844
- initDeliveryManager,
845
- getDeliveryManager,
846
- deliverMessage
847
- };