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.
- package/README.md +68 -68
- package/dist/activity-events-3WHHCOBB.js +15 -0
- package/dist/{archive-ZCFOSTKB.js → archive-4ZQYK5MN.js} +4 -2
- package/dist/auth-HM2RSPY7.js +37 -0
- package/dist/{channel-PUQKGSQM.js → channel-BOOMFULW.js} +2 -2
- package/dist/{chunk-OTWLI7F4.js → chunk-5462YKWP.js} +12 -9
- package/dist/{chunk-2TJGRJ4O.js → chunk-7LPTHFIL.js} +64 -59
- package/dist/chunk-A4S7H6G6.js +56 -0
- package/dist/chunk-AKPFNL7L.js +148 -0
- package/dist/{chunk-EBGCNDMM.js → chunk-B2CPS4QU.js} +128 -114
- package/dist/{chunk-FCDU5BFX.js → chunk-HFCBO2GL.js} +2 -2
- package/dist/chunk-HGCDWKSP.js +97 -0
- package/dist/{chunk-DYZGP3EW.js → chunk-IPJXU366.js} +1 -1
- package/dist/{chunk-VE4D3GOP.js → chunk-J5A3DF2U.js} +2 -2
- package/dist/{chunk-WC6ZHVRL.js → chunk-KFI7TQJ6.js} +2 -2
- package/dist/{chunk-AW7P4EVV.js → chunk-KTJGZ7M7.js} +55 -7
- package/dist/{chunk-4KPUF5JD.js → chunk-L3LHXZD7.js} +18 -5
- package/dist/{chunk-OGXOMR65.js → chunk-NWPT4ASZ.js} +1 -1
- package/dist/{chunk-FGV2H4TX.js → chunk-OGZYB5GL.js} +312 -268
- package/dist/{chunk-SCUDS4US.js → chunk-ON3FF5JA.js} +1 -1
- package/dist/{chunk-EMQSAY3B.js → chunk-PC6R6UUW.js} +6 -5
- package/dist/{chunk-VDWCHYTS.js → chunk-PHU4DEAJ.js} +1 -1
- package/dist/{chunk-7NO7EV5Z.js → chunk-Q7AITQ44.js} +2 -2
- package/dist/{chunk-32VR2EOH.js → chunk-QUJUKM4U.js} +2 -2
- package/dist/{chunk-VQWDC6UK.js → chunk-SGPEZ32F.js} +46 -1
- package/dist/{chunk-RHEGSQFJ.js → chunk-WSLPZF72.js} +1 -1
- package/dist/cli.js +59 -111
- package/dist/{connector-JBVNZ7VK.js → connector-PYT5UOTZ.js} +6 -6
- package/dist/connectors/discord.js +2 -2
- package/dist/connectors/slack.js +2 -2
- package/dist/connectors/telegram.js +2 -2
- package/dist/{create-HP4OVVHF.js → create-WIDA3M4C.js} +1 -1
- package/dist/{daemon-client-ITWUCNFO.js → daemon-client-ZHCDL4RS.js} +2 -2
- package/dist/{daemon-restart-JMZM3QY4.js → daemon-restart-BH67ZOTE.js} +8 -8
- package/dist/daemon.js +2872 -1301
- package/dist/{delete-BSU7K3RY.js → delete-LOIANQGD.js} +1 -1
- package/dist/down-LIOQ5JDH.js +14 -0
- package/dist/{env-A3LMO777.js → env-4PHIHTF4.js} +2 -2
- package/dist/{export-GCDNQCF3.js → export-XD6PJBQP.js} +19 -8
- package/dist/file-X4L5TTOL.js +204 -0
- package/dist/{history-WNK3DFUM.js → history-HTEKRNID.js} +2 -2
- package/dist/{import-M63VIUJ5.js → import-E433B4KG.js} +3 -3
- package/dist/{log-PPPZDVEF.js → log-SRO5Q6AD.js} +2 -2
- package/dist/{login-HNH3EUQV.js → login-UO6AOVEA.js} +4 -4
- package/dist/{logout-I5CB5UZS.js → logout-UKD5LA37.js} +2 -2
- package/dist/{logs-SF2IMJN4.js → logs-HNTNNBDW.js} +2 -2
- package/dist/{merge-33C237A4.js → merge-B6SYTGI7.js} +2 -2
- package/dist/{mind-PQ5NCPSU.js → mind-BIDOF65R.js} +27 -11
- package/dist/mind-activity-tracker-PGC3DBJ7.js +18 -0
- package/dist/{mind-manager-RVCFROAY.js → mind-manager-3V2NXX4I.js} +5 -6
- package/dist/{package-MYE2ZJLV.js → package-HQR52XSG.js} +1 -1
- package/dist/{pages-AXCOSY3P.js → pages-KQBR5TAZ.js} +6 -6
- package/dist/{publish-YB377JB7.js → publish-OJ4QMXVZ.js} +12 -9
- package/dist/{pull-XAEWQJ47.js → pull-GRQAXM2E.js} +2 -2
- package/dist/{register-VSPCMHKX.js → register-U2UO6TC4.js} +5 -5
- package/dist/registry-D2BSQ2X5.js +42 -0
- package/dist/{restart-IQKMCK5M.js → restart-CIDAKGG2.js} +3 -6
- package/dist/{schedule-LMX7GAQZ.js → schedule-NLR3LZLY.js} +27 -7
- package/dist/{seed-J43YDKXG.js → seed-3H2MRREW.js} +2 -2
- package/dist/{send-KVIZIGCE.js → send-RP2TA7SG.js} +132 -36
- package/dist/{service-LUR7WDO7.js → service-TVNEORO7.js} +31 -13
- package/dist/{setup-OH3PJUJO.js → setup-OZDYCKDI.js} +25 -34
- package/dist/{shared-KO35ZM44.js → shared-DCQ2UXOM.js} +4 -4
- package/dist/{skill-BCVNI6TV.js → skill-Q2Y6PQ3L.js} +2 -2
- package/dist/skills/orientation/SKILL.md +2 -2
- package/dist/skills/volute-mind/SKILL.md +38 -8
- package/dist/{sprout-VBEX63LX.js → sprout-6Z6C42YM.js} +34 -30
- package/dist/{start-I5JYB65M.js → start-JR6CUUWF.js} +3 -6
- package/dist/{status-D7E5HHBV.js → status-5XDGYHKP.js} +2 -2
- package/dist/{status-JCJAOXTW.js → status-LV34BG6G.js} +6 -5
- package/dist/{status-4ESFLGH4.js → status-Z7NAFMBI.js} +5 -5
- package/dist/{stop-NBVKEFQQ.js → stop-VKPGK25U.js} +2 -5
- package/dist/template-hash-BIMA4ILT.js +8 -0
- package/dist/{up-WG65SWJU.js → up-7BGDMFRT.js} +5 -5
- package/dist/{update-FJIHDJKM.js → update-4WT7VWHW.js} +5 -5
- package/dist/{update-check-MWE5AH4U.js → update-check-F5Z3ALXX.js} +2 -2
- package/dist/{upgrade-AIT24B5I.js → upgrade-ZEC2GGFO.js} +1 -1
- package/dist/{variant-63ZWO2W7.js → variant-A4I7PHXS.js} +16 -24
- package/dist/version-notify-TFS2U5CF.js +173 -0
- package/dist/web-assets/assets/index-BR3gtK3E.css +1 -0
- package/dist/web-assets/assets/index-CWmrZRQd.js +64 -0
- package/dist/web-assets/index.html +2 -2
- package/drizzle/0012_activity.sql +11 -0
- package/drizzle/meta/0012_snapshot.json +7 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +1 -1
- package/templates/_base/home/.config/routes.json +2 -2
- package/templates/_base/home/VOLUTE.md +1 -1
- package/templates/_base/src/lib/daemon-client.ts +22 -0
- package/templates/_base/src/lib/transparency.ts +1 -1
- package/templates/claude/.init/.config/routes.json +7 -1
- package/templates/pi/.init/.config/routes.json +7 -1
- package/templates/pi/src/agent.ts +11 -5
- package/templates/pi/src/lib/session-context-extension.ts +6 -4
- package/templates/pi/src/server.ts +2 -0
- package/dist/chunk-UJ6GHNR7.js +0 -675
- package/dist/chunk-Z524RFCJ.js +0 -36
- package/dist/db-5ZVC6MQF.js +0 -10
- package/dist/delivery-manager-ISTJMZDW.js +0 -16
- package/dist/down-ZY35KMHR.js +0 -14
- package/dist/schema-5BW7DFZI.js +0 -24
- package/dist/variants-JAGWGBXG.js +0 -26
- package/dist/web-assets/assets/index-BAbuRsVF.css +0 -1
- 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-
|
|
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-
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
405
|
+
dlog2.info(`restored ${rows.length} queued messages from DB`);
|
|
423
406
|
}
|
|
424
407
|
} catch (err) {
|
|
425
|
-
|
|
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
|
-
|
|
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
|
-
|
|
486
|
-
return;
|
|
487
|
-
}
|
|
488
|
-
port = variant.port;
|
|
463
|
+
if (!variant) return null;
|
|
464
|
+
return { baseName, port: variant.port };
|
|
489
465
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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
|
-
|
|
514
|
-
|
|
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
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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
|
|
529
|
-
const
|
|
530
|
-
|
|
531
|
-
|
|
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
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
566
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
587
|
+
dlog2.warn(`failed to deliver batch to ${mindName}`, logger_default.errorData(err));
|
|
599
588
|
this.decrementActive(baseName, session);
|
|
600
|
-
|
|
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
|
-
|
|
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
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) ?? {
|
|
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)
|
|
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
|
-
|
|
798
|
-
|
|
839
|
+
subscribe,
|
|
840
|
+
publish,
|
|
799
841
|
getTypingMap,
|
|
800
|
-
|
|
842
|
+
publishTypingForChannels,
|
|
843
|
+
extractTextContent,
|
|
801
844
|
initDeliveryManager,
|
|
802
|
-
getDeliveryManager
|
|
845
|
+
getDeliveryManager,
|
|
846
|
+
deliverMessage
|
|
803
847
|
};
|