volute 0.18.0 → 0.19.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 +1 -1
- package/dist/archive-ZCFOSTKB.js +15 -0
- package/dist/{channel-SLURLIRV.js → channel-PUQKGSQM.js} +60 -7
- package/dist/{chunk-AYB7XAWO.js → chunk-2TJGRJ4O.js} +114 -279
- package/dist/{chunk-6BDNWYKG.js → chunk-32VR2EOH.js} +2 -2
- package/dist/chunk-4KPUF5JD.js +214 -0
- package/dist/{chunk-QJIIHU32.js → chunk-7NO7EV5Z.js} +2 -2
- package/dist/chunk-AW7P4EVV.js +159 -0
- package/dist/{chunk-2Y77MCFG.js → chunk-DYZGP3EW.js} +2 -2
- package/dist/{chunk-M77QBTEH.js → chunk-EBGCNDMM.js} +24 -14
- package/dist/{chunk-GSPWIM5E.js → chunk-EMQSAY3B.js} +77 -6
- package/dist/{chunk-37X7ECMF.js → chunk-FCDU5BFX.js} +1 -1
- package/dist/chunk-FGV2H4TX.js +803 -0
- package/dist/{chunk-ZCEYUUID.js → chunk-OGXOMR65.js} +2 -1
- package/dist/chunk-OTWLI7F4.js +375 -0
- package/dist/{chunk-GK4E7LM7.js → chunk-RHEGSQFJ.js} +1 -1
- package/dist/{chunk-MVSXRMJJ.js → chunk-SCUDS4US.js} +1 -1
- package/dist/{chunk-FW5API7X.js → chunk-UJ6GHNR7.js} +2 -2
- package/dist/{chunk-OYSZNX5I.js → chunk-VDWCHYTS.js} +1 -1
- package/dist/{chunk-6DVBMLVN.js → chunk-VE4D3GOP.js} +2 -2
- package/dist/chunk-VQWDC6UK.js +142 -0
- package/dist/{chunk-OJQ47SCA.js → chunk-WC6ZHVRL.js} +1 -1
- package/dist/chunk-YUIHSKR6.js +72 -0
- package/dist/chunk-Z524RFCJ.js +36 -0
- package/dist/cli.js +33 -25
- package/dist/{connector-3ELFMI2R.js → connector-JBVNZ7VK.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-ZWHCRT5F.js → create-HP4OVVHF.js} +6 -4
- package/dist/{daemon-client-ODKDUYDE.js → daemon-client-ITWUCNFO.js} +2 -2
- package/dist/{daemon-restart-2HVTHZAT.js → daemon-restart-JMZM3QY4.js} +8 -8
- package/dist/daemon.js +1144 -1108
- package/dist/db-5ZVC6MQF.js +10 -0
- package/dist/{delete-6G6WEX4F.js → delete-BSU7K3RY.js} +1 -1
- package/dist/delivery-manager-ISTJMZDW.js +16 -0
- package/dist/down-ZY35KMHR.js +14 -0
- package/dist/{env-6IDWGBUH.js → env-A3LMO777.js} +6 -6
- package/dist/export-GCDNQCF3.js +100 -0
- package/dist/{history-YUEKTJ2N.js → history-WNK3DFUM.js} +6 -6
- package/dist/{import-EDGRLIGO.js → import-M63VIUJ5.js} +3 -3
- package/dist/log-PPPZDVEF.js +39 -0
- package/dist/{login-ORQDXLBM.js → login-HNH3EUQV.js} +2 -2
- package/dist/{logout-XC5AUO5I.js → logout-I5CB5UZS.js} +2 -2
- package/dist/{logs-GYOR3L2L.js → logs-SF2IMJN4.js} +6 -6
- package/dist/merge-33C237A4.js +46 -0
- package/dist/{mind-OJN6RBZW.js → mind-PQ5NCPSU.js} +14 -10
- package/dist/mind-manager-RVCFROAY.js +18 -0
- package/dist/{package-OKLFO7UY.js → package-MYE2ZJLV.js} +5 -3
- package/dist/{pages-6IV4VQTU.js → pages-AXCOSY3P.js} +2 -2
- package/dist/{publish-Q4RPSJLL.js → publish-YB377JB7.js} +18 -4
- package/dist/pull-XAEWQJ47.js +39 -0
- package/dist/{register-LDE6LRXY.js → register-VSPCMHKX.js} +2 -2
- package/dist/{restart-YFAWFS5T.js → restart-IQKMCK5M.js} +6 -6
- package/dist/{schedule-AGYLDMNS.js → schedule-LMX7GAQZ.js} +6 -6
- package/dist/schema-5BW7DFZI.js +24 -0
- package/dist/{seed-AP4Q7RZ7.js → seed-J43YDKXG.js} +7 -4
- package/dist/{send-BNDTLUPM.js → send-KVIZIGCE.js} +8 -8
- package/dist/{service-U7MZ2H7F.js → service-LUR7WDO7.js} +6 -6
- package/dist/{setup-DJKIZKGW.js → setup-OH3PJUJO.js} +7 -7
- package/dist/shared-KO35ZM44.js +39 -0
- package/dist/{skill-2Y42P4JY.js → skill-BCVNI6TV.js} +6 -6
- package/{templates/_base/_skills → dist/skills}/orientation/SKILL.md +1 -1
- package/{templates/_base/_skills → dist/skills}/sessions/SKILL.md +2 -2
- package/{templates/_base/_skills → dist/skills}/volute-mind/SKILL.md +19 -1
- package/dist/{sprout-TJ3BHVOG.js → sprout-VBEX63LX.js} +38 -20
- package/dist/{start-3YYRXBKP.js → start-I5JYB65M.js} +6 -6
- package/dist/{status-VSFZYX7S.js → status-4ESFLGH4.js} +5 -5
- package/dist/status-D7E5HHBV.js +35 -0
- package/dist/{status-OKNA6AR3.js → status-JCJAOXTW.js} +2 -2
- package/dist/{stop-AA5K5LYG.js → stop-NBVKEFQQ.js} +6 -6
- package/dist/{up-7B3BWF2U.js → up-WG65SWJU.js} +5 -5
- package/dist/{update-YAGN5ODG.js → update-FJIHDJKM.js} +5 -5
- package/dist/{update-check-APLTH4IN.js → update-check-MWE5AH4U.js} +2 -2
- package/dist/{upgrade-KXZCQSZN.js → upgrade-AIT24B5I.js} +1 -1
- package/dist/{variant-X5QFG6KK.js → variant-63ZWO2W7.js} +4 -4
- package/dist/variants-JAGWGBXG.js +26 -0
- package/dist/web-assets/assets/index-BAbuRsVF.css +1 -0
- package/dist/web-assets/assets/index-CiQhSKi_.js +63 -0
- package/dist/web-assets/index.html +2 -2
- package/drizzle/0010_delivery_queue.sql +12 -0
- package/drizzle/0011_rename_human_to_brain.sql +1 -0
- package/drizzle/meta/0010_snapshot.json +7 -0
- package/drizzle/meta/0011_snapshot.json +7 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +5 -3
- package/templates/_base/.init/.config/hooks/startup-context.sh +1 -1
- package/templates/_base/.init/.config/scripts/session-reader.ts +3 -3
- package/templates/_base/home/VOLUTE.md +16 -1
- package/templates/_base/src/lib/auto-commit.ts +51 -14
- package/templates/_base/src/lib/router.ts +123 -1
- package/templates/_base/src/lib/types.ts +4 -0
- package/templates/_base/src/lib/volute-server.ts +91 -2
- package/templates/claude/src/server.ts +2 -2
- package/templates/claude/volute-template.json +1 -2
- package/templates/pi/src/agent.ts +1 -1
- package/templates/pi/src/lib/session-context-extension.ts +2 -2
- package/templates/pi/volute-template.json +1 -2
- package/dist/chunk-PO5Q2AYN.js +0 -121
- package/dist/down-A56B5JLK.js +0 -14
- package/dist/mind-manager-Z7O7PN2O.js +0 -15
- package/dist/web-assets/assets/index-CtiimdWK.css +0 -1
- package/dist/web-assets/assets/index-kt1_EcuO.js +0 -63
- /package/{templates/_base/_skills → dist/skills}/memory/SKILL.md +0 -0
|
@@ -0,0 +1,803 @@
|
|
|
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
|
+
import {
|
|
9
|
+
deliveryQueue,
|
|
10
|
+
mindHistory
|
|
11
|
+
} from "./chunk-VQWDC6UK.js";
|
|
12
|
+
import {
|
|
13
|
+
findMind,
|
|
14
|
+
findVariant,
|
|
15
|
+
mindDir
|
|
16
|
+
} from "./chunk-EBGCNDMM.js";
|
|
17
|
+
|
|
18
|
+
// src/lib/delivery-manager.ts
|
|
19
|
+
import { and, eq, sql } from "drizzle-orm";
|
|
20
|
+
|
|
21
|
+
// src/lib/delivery-router.ts
|
|
22
|
+
import { readFileSync, statSync } from "fs";
|
|
23
|
+
import { resolve } from "path";
|
|
24
|
+
var configCache = /* @__PURE__ */ new Map();
|
|
25
|
+
var dlog = logger_default.child("delivery-router");
|
|
26
|
+
function configPath(mindName) {
|
|
27
|
+
return resolve(mindDir(mindName), "home/.config/routes.json");
|
|
28
|
+
}
|
|
29
|
+
function getRoutingConfig(mindName) {
|
|
30
|
+
const path = configPath(mindName);
|
|
31
|
+
let mtime;
|
|
32
|
+
try {
|
|
33
|
+
mtime = statSync(path).mtimeMs;
|
|
34
|
+
} catch {
|
|
35
|
+
configCache.delete(mindName);
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
const cached = configCache.get(mindName);
|
|
39
|
+
if (cached && cached.mtime === mtime) {
|
|
40
|
+
return cached.config;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
44
|
+
const config = Array.isArray(parsed) ? { rules: parsed } : parsed;
|
|
45
|
+
configCache.set(mindName, { config, mtime });
|
|
46
|
+
return config;
|
|
47
|
+
} catch (err) {
|
|
48
|
+
dlog.warn(`failed to load routes.json for ${mindName}`, logger_default.errorData(err));
|
|
49
|
+
configCache.delete(mindName);
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function globMatch(pattern, value) {
|
|
54
|
+
const regex = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
55
|
+
return new RegExp(`^${regex}$`).test(value);
|
|
56
|
+
}
|
|
57
|
+
var GLOB_MATCH_KEYS = /* @__PURE__ */ new Set(["channel", "sender"]);
|
|
58
|
+
var NON_MATCH_KEYS = /* @__PURE__ */ new Set(["session", "destination", "path", "mode"]);
|
|
59
|
+
function ruleMatches(rule, meta) {
|
|
60
|
+
for (const [key, pattern] of Object.entries(rule)) {
|
|
61
|
+
if (NON_MATCH_KEYS.has(key)) continue;
|
|
62
|
+
if (key === "isDM") {
|
|
63
|
+
if (typeof pattern !== "boolean") return false;
|
|
64
|
+
if ((meta.isDM ?? false) !== pattern) return false;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (key === "participants") {
|
|
68
|
+
if (typeof pattern !== "number") return false;
|
|
69
|
+
if ((meta.participantCount ?? 0) !== pattern) return false;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (typeof pattern !== "string") return false;
|
|
73
|
+
if (!GLOB_MATCH_KEYS.has(key)) return false;
|
|
74
|
+
const value = meta[key] ?? "";
|
|
75
|
+
if (!globMatch(pattern, value)) return false;
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
function expandTemplate(template, meta) {
|
|
80
|
+
return template.replace(/\$\{sender\}/g, meta.sender ?? "unknown").replace(/\$\{channel\}/g, meta.channel ?? "unknown");
|
|
81
|
+
}
|
|
82
|
+
function sanitizeSessionName(name) {
|
|
83
|
+
return name.replace(/\0/g, "").replace(/[/\\]/g, "-").replace(/\.\./g, "-").slice(0, 100);
|
|
84
|
+
}
|
|
85
|
+
function resolveRoute(config, meta) {
|
|
86
|
+
const fallback = config.default ?? "main";
|
|
87
|
+
if (!config.rules) {
|
|
88
|
+
return { destination: "mind", session: fallback, matched: false };
|
|
89
|
+
}
|
|
90
|
+
for (const rule of config.rules) {
|
|
91
|
+
if (ruleMatches(rule, meta)) {
|
|
92
|
+
if (rule.destination === "file") {
|
|
93
|
+
if (!rule.path) {
|
|
94
|
+
dlog.warn("file destination rule missing path \u2014 falling through");
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
return { destination: "file", path: rule.path, matched: true };
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
destination: "mind",
|
|
101
|
+
session: sanitizeSessionName(expandTemplate(rule.session ?? fallback, meta)),
|
|
102
|
+
matched: true,
|
|
103
|
+
mode: rule.mode
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return { destination: "mind", session: fallback, matched: false };
|
|
108
|
+
}
|
|
109
|
+
var DEFAULT_BATCH_DEBOUNCE = 5;
|
|
110
|
+
var DEFAULT_BATCH_MAX_WAIT = 120;
|
|
111
|
+
function normalizeBatchConfig(batch) {
|
|
112
|
+
if (typeof batch === "number") return { maxWait: batch * 60 };
|
|
113
|
+
return batch;
|
|
114
|
+
}
|
|
115
|
+
function resolveDeliveryMode(config, sessionName) {
|
|
116
|
+
const defaults = {
|
|
117
|
+
delivery: { mode: "immediate" },
|
|
118
|
+
interrupt: true
|
|
119
|
+
};
|
|
120
|
+
if (!config.sessions) return defaults;
|
|
121
|
+
for (const [pattern, sessionConfig] of Object.entries(config.sessions)) {
|
|
122
|
+
if (globMatch(pattern, sessionName)) {
|
|
123
|
+
let delivery;
|
|
124
|
+
if (sessionConfig.delivery != null) {
|
|
125
|
+
if (sessionConfig.delivery === "immediate") {
|
|
126
|
+
delivery = { mode: "immediate" };
|
|
127
|
+
} else if (sessionConfig.delivery === "batch") {
|
|
128
|
+
delivery = {
|
|
129
|
+
mode: "batch",
|
|
130
|
+
debounce: DEFAULT_BATCH_DEBOUNCE,
|
|
131
|
+
maxWait: DEFAULT_BATCH_MAX_WAIT
|
|
132
|
+
};
|
|
133
|
+
} else {
|
|
134
|
+
delivery = {
|
|
135
|
+
mode: "batch",
|
|
136
|
+
debounce: sessionConfig.delivery.debounce ?? DEFAULT_BATCH_DEBOUNCE,
|
|
137
|
+
maxWait: sessionConfig.delivery.maxWait ?? DEFAULT_BATCH_MAX_WAIT
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
} else if (sessionConfig.batch != null) {
|
|
141
|
+
const batch = normalizeBatchConfig(sessionConfig.batch);
|
|
142
|
+
delivery = {
|
|
143
|
+
mode: "batch",
|
|
144
|
+
debounce: batch.debounce ?? DEFAULT_BATCH_DEBOUNCE,
|
|
145
|
+
maxWait: batch.maxWait ?? DEFAULT_BATCH_MAX_WAIT,
|
|
146
|
+
triggers: batch.triggers
|
|
147
|
+
};
|
|
148
|
+
} else if (sessionConfig.interrupt === false) {
|
|
149
|
+
delivery = {
|
|
150
|
+
mode: "batch",
|
|
151
|
+
debounce: DEFAULT_BATCH_DEBOUNCE,
|
|
152
|
+
maxWait: DEFAULT_BATCH_MAX_WAIT
|
|
153
|
+
};
|
|
154
|
+
} else {
|
|
155
|
+
delivery = { mode: "immediate" };
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
delivery,
|
|
159
|
+
interrupt: sessionConfig.interrupt ?? true,
|
|
160
|
+
instructions: sessionConfig.instructions
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return defaults;
|
|
165
|
+
}
|
|
166
|
+
|
|
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");
|
|
323
|
+
var MAX_BATCH_SIZE = 50;
|
|
324
|
+
var DeliveryManager = class {
|
|
325
|
+
sessionStates = /* @__PURE__ */ new Map();
|
|
326
|
+
batchBuffers = /* @__PURE__ */ new Map();
|
|
327
|
+
// --- Public API ---
|
|
328
|
+
/**
|
|
329
|
+
* Route and deliver a message to a mind. This is the main entry point.
|
|
330
|
+
* The message is routed via the mind's routes.json, then either delivered immediately
|
|
331
|
+
* or queued for batching depending on the session's delivery mode.
|
|
332
|
+
*/
|
|
333
|
+
async routeAndDeliver(mindName, payload) {
|
|
334
|
+
const [baseName] = mindName.split("@", 2);
|
|
335
|
+
const config = getRoutingConfig(baseName);
|
|
336
|
+
const meta = {
|
|
337
|
+
channel: payload.channel,
|
|
338
|
+
sender: payload.sender ?? void 0,
|
|
339
|
+
isDM: payload.isDM,
|
|
340
|
+
participantCount: payload.participantCount
|
|
341
|
+
};
|
|
342
|
+
const route = resolveRoute(config, meta);
|
|
343
|
+
if (route.destination === "file") {
|
|
344
|
+
return { routed: true, session: route.path, destination: "file", mode: "immediate" };
|
|
345
|
+
}
|
|
346
|
+
if (!route.matched && config.gateUnmatched !== false) {
|
|
347
|
+
await this.gateMessage(mindName, route.session, payload);
|
|
348
|
+
return { routed: true, session: route.session, destination: "mind", mode: "gated" };
|
|
349
|
+
}
|
|
350
|
+
if (route.mode === "mention" && payload.sender) {
|
|
351
|
+
const text = extractTextContent(payload.content);
|
|
352
|
+
const escaped = baseName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
353
|
+
const pattern = new RegExp(`\\b${escaped}\\b`, "i");
|
|
354
|
+
if (!pattern.test(text)) {
|
|
355
|
+
return { routed: false, reason: "mention-filtered" };
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
let sessionName = route.session;
|
|
359
|
+
if (sessionName === "$new") {
|
|
360
|
+
sessionName = `new-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
361
|
+
}
|
|
362
|
+
const sessionConfig = resolveDeliveryMode(config, sessionName);
|
|
363
|
+
if (sessionConfig.delivery.mode === "batch") {
|
|
364
|
+
this.enqueueBatch(mindName, sessionName, payload, sessionConfig);
|
|
365
|
+
return { routed: true, session: sessionName, destination: "mind", mode: "batch" };
|
|
366
|
+
}
|
|
367
|
+
await this.deliverToMind(mindName, sessionName, payload, sessionConfig);
|
|
368
|
+
return { routed: true, session: sessionName, destination: "mind", mode: "immediate" };
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Called when a mind's session emits a "done" event — decrements active count
|
|
372
|
+
* and may trigger batch flush if session goes idle.
|
|
373
|
+
*/
|
|
374
|
+
sessionDone(mindName, session) {
|
|
375
|
+
const [baseName] = mindName.split("@", 2);
|
|
376
|
+
if (session) {
|
|
377
|
+
this.decrementActive(baseName, session);
|
|
378
|
+
} else {
|
|
379
|
+
const mindSessions = this.sessionStates.get(baseName);
|
|
380
|
+
if (mindSessions) {
|
|
381
|
+
for (const [sessionName] of mindSessions) {
|
|
382
|
+
this.decrementActive(baseName, sessionName);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Restore queued messages from DB on daemon restart.
|
|
389
|
+
*/
|
|
390
|
+
async restoreFromDb() {
|
|
391
|
+
try {
|
|
392
|
+
const db = await getDb();
|
|
393
|
+
const rows = await db.select().from(deliveryQueue).where(eq(deliveryQueue.status, "pending"));
|
|
394
|
+
for (const row of rows) {
|
|
395
|
+
let payload;
|
|
396
|
+
try {
|
|
397
|
+
payload = JSON.parse(row.payload);
|
|
398
|
+
} catch (parseErr) {
|
|
399
|
+
dlog3.warn(
|
|
400
|
+
`corrupt payload in delivery queue row ${row.id}, skipping`,
|
|
401
|
+
logger_default.errorData(parseErr)
|
|
402
|
+
);
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
const config = getRoutingConfig(row.mind);
|
|
406
|
+
const sessionConfig = resolveDeliveryMode(config, row.session);
|
|
407
|
+
if (sessionConfig.delivery.mode === "batch") {
|
|
408
|
+
this.addToBatchBuffer(row.mind, row.session, payload, sessionConfig);
|
|
409
|
+
} 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));
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (rows.length > 0) {
|
|
422
|
+
dlog3.info(`restored ${rows.length} queued messages from DB`);
|
|
423
|
+
}
|
|
424
|
+
} catch (err) {
|
|
425
|
+
dlog3.warn("failed to restore delivery queue from DB", logger_default.errorData(err));
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Get pending (gated) messages for a mind.
|
|
430
|
+
*/
|
|
431
|
+
async getPending(mindName) {
|
|
432
|
+
const db = await getDb();
|
|
433
|
+
const rows = await db.select().from(deliveryQueue).where(and(eq(deliveryQueue.mind, mindName), eq(deliveryQueue.status, "gated")));
|
|
434
|
+
const byChannel = /* @__PURE__ */ new Map();
|
|
435
|
+
for (const row of rows) {
|
|
436
|
+
const ch = row.channel ?? "unknown";
|
|
437
|
+
const existing = byChannel.get(ch) ?? [];
|
|
438
|
+
existing.push(row);
|
|
439
|
+
byChannel.set(ch, existing);
|
|
440
|
+
}
|
|
441
|
+
return [...byChannel.entries()].map(([channel, channelRows]) => {
|
|
442
|
+
const firstRow = channelRows[0];
|
|
443
|
+
const payload = JSON.parse(firstRow.payload);
|
|
444
|
+
const text = extractTextContent(payload.content);
|
|
445
|
+
return {
|
|
446
|
+
channel,
|
|
447
|
+
sender: firstRow.sender,
|
|
448
|
+
count: channelRows.length,
|
|
449
|
+
firstSeen: firstRow.created_at,
|
|
450
|
+
preview: text.length > 200 ? `${text.slice(0, 200)}...` : text
|
|
451
|
+
};
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Check if a session is currently busy (has active deliveries).
|
|
456
|
+
*/
|
|
457
|
+
isSessionBusy(mindName, session) {
|
|
458
|
+
const state = this.sessionStates.get(mindName)?.get(session);
|
|
459
|
+
return (state?.activeCount ?? 0) > 0;
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Cleanup all timers and subscriptions.
|
|
463
|
+
*/
|
|
464
|
+
dispose() {
|
|
465
|
+
for (const [, buffer] of this.batchBuffers) {
|
|
466
|
+
if (buffer.debounceTimer) clearTimeout(buffer.debounceTimer);
|
|
467
|
+
if (buffer.maxWaitTimer) clearTimeout(buffer.maxWaitTimer);
|
|
468
|
+
}
|
|
469
|
+
this.batchBuffers.clear();
|
|
470
|
+
this.sessionStates.clear();
|
|
471
|
+
if (instance2 === this) instance2 = void 0;
|
|
472
|
+
}
|
|
473
|
+
// --- Private ---
|
|
474
|
+
async deliverToMind(mindName, session, payload, sessionConfig) {
|
|
475
|
+
const [baseName, variantName] = mindName.split("@", 2);
|
|
476
|
+
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;
|
|
482
|
+
if (variantName) {
|
|
483
|
+
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;
|
|
489
|
+
}
|
|
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);
|
|
502
|
+
const controller = new AbortController();
|
|
503
|
+
const timeout = setTimeout(() => controller.abort(), 12e4);
|
|
504
|
+
try {
|
|
505
|
+
const res = await fetch(`http://127.0.0.1:${port}/message`, {
|
|
506
|
+
method: "POST",
|
|
507
|
+
headers: { "Content-Type": "application/json" },
|
|
508
|
+
body,
|
|
509
|
+
signal: controller.signal
|
|
510
|
+
});
|
|
511
|
+
if (!res.ok) {
|
|
512
|
+
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
|
+
});
|
|
519
|
+
}
|
|
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);
|
|
524
|
+
} finally {
|
|
525
|
+
clearTimeout(timeout);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
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`);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
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;
|
|
541
|
+
}
|
|
542
|
+
port = variant.port;
|
|
543
|
+
}
|
|
544
|
+
const channels = {};
|
|
545
|
+
for (const msg of messages) {
|
|
546
|
+
const ch = msg.channel ?? "unknown";
|
|
547
|
+
if (!channels[ch]) channels[ch] = [];
|
|
548
|
+
channels[ch].push(msg.payload);
|
|
549
|
+
}
|
|
550
|
+
this.incrementActive(baseName, session);
|
|
551
|
+
const typingMap = getTypingMap();
|
|
552
|
+
for (const ch of Object.keys(channels)) {
|
|
553
|
+
if (ch !== "unknown") typingMap.set(ch, baseName, { persistent: true });
|
|
554
|
+
}
|
|
555
|
+
const batchBody = {
|
|
556
|
+
session,
|
|
557
|
+
batch: { channels },
|
|
558
|
+
interrupt: sessionConfig.interrupt,
|
|
559
|
+
instructions: sessionConfig.instructions
|
|
560
|
+
};
|
|
561
|
+
const body = JSON.stringify(batchBody);
|
|
562
|
+
const controller = new AbortController();
|
|
563
|
+
const timeout = setTimeout(() => controller.abort(), 12e4);
|
|
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}`);
|
|
574
|
+
this.decrementActive(baseName, session);
|
|
575
|
+
for (const ch of Object.keys(channels)) {
|
|
576
|
+
typingMap.delete(ch, baseName);
|
|
577
|
+
}
|
|
578
|
+
} else {
|
|
579
|
+
await res.text().catch(() => {
|
|
580
|
+
});
|
|
581
|
+
try {
|
|
582
|
+
const db = await getDb();
|
|
583
|
+
await db.delete(deliveryQueue).where(
|
|
584
|
+
and(
|
|
585
|
+
eq(deliveryQueue.mind, baseName),
|
|
586
|
+
eq(deliveryQueue.session, session),
|
|
587
|
+
eq(deliveryQueue.status, "pending")
|
|
588
|
+
)
|
|
589
|
+
);
|
|
590
|
+
} catch (err) {
|
|
591
|
+
dlog3.warn(
|
|
592
|
+
`failed to clean delivery queue for ${baseName}/${session}`,
|
|
593
|
+
logger_default.errorData(err)
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
} catch (err) {
|
|
598
|
+
dlog3.warn(`failed to deliver batch to ${mindName}`, logger_default.errorData(err));
|
|
599
|
+
this.decrementActive(baseName, session);
|
|
600
|
+
for (const ch of Object.keys(channels)) {
|
|
601
|
+
typingMap.delete(ch, baseName);
|
|
602
|
+
}
|
|
603
|
+
} finally {
|
|
604
|
+
clearTimeout(timeout);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
enqueueBatch(mindName, session, payload, sessionConfig) {
|
|
608
|
+
const delivery = sessionConfig.delivery;
|
|
609
|
+
if (delivery.triggers?.length) {
|
|
610
|
+
const text = extractTextContent(payload.content);
|
|
611
|
+
const lower = text.toLowerCase();
|
|
612
|
+
if (delivery.triggers.some((t) => lower.includes(t.toLowerCase()))) {
|
|
613
|
+
this.flushBatch(mindName, session, [
|
|
614
|
+
{
|
|
615
|
+
payload,
|
|
616
|
+
channel: payload.channel,
|
|
617
|
+
sender: payload.sender ?? null,
|
|
618
|
+
createdAt: Date.now()
|
|
619
|
+
}
|
|
620
|
+
]);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
this.persistToQueue(mindName, session, payload).catch((err) => {
|
|
625
|
+
dlog3.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) {
|
|
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
|
+
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
|
+
});
|
|
692
|
+
}
|
|
693
|
+
async gateMessage(mindName, session, payload) {
|
|
694
|
+
const [baseName] = mindName.split("@", 2);
|
|
695
|
+
await this.persistToQueue(baseName, session, payload, "gated");
|
|
696
|
+
try {
|
|
697
|
+
const db = await getDb();
|
|
698
|
+
const count = await db.select({ count: sql`count(*)` }).from(deliveryQueue).where(
|
|
699
|
+
and(
|
|
700
|
+
eq(deliveryQueue.mind, baseName),
|
|
701
|
+
eq(deliveryQueue.channel, payload.channel),
|
|
702
|
+
eq(deliveryQueue.status, "gated")
|
|
703
|
+
)
|
|
704
|
+
);
|
|
705
|
+
if ((count[0]?.count ?? 0) <= 1) {
|
|
706
|
+
await this.sendInviteNotification(mindName, payload);
|
|
707
|
+
}
|
|
708
|
+
} catch (err) {
|
|
709
|
+
dlog3.warn(`failed to check gated count for ${baseName}`, logger_default.errorData(err));
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
async sendInviteNotification(mindName, payload) {
|
|
713
|
+
const text = extractTextContent(payload.content);
|
|
714
|
+
const preview = text.length > 200 ? `${text.slice(0, 200)}...` : text;
|
|
715
|
+
const channel = payload.channel ?? "unknown";
|
|
716
|
+
const notification = [
|
|
717
|
+
`[New channel: ${channel}]`,
|
|
718
|
+
`Sender: ${payload.sender ?? "unknown"}`,
|
|
719
|
+
payload.platform ? `Platform: ${payload.platform}` : null,
|
|
720
|
+
payload.participantCount ? `Participants: ${payload.participantCount}` : null,
|
|
721
|
+
"",
|
|
722
|
+
`Preview: ${preview}`,
|
|
723
|
+
"",
|
|
724
|
+
`To accept this channel, add a routing rule for "${channel}" to your routes.json.`,
|
|
725
|
+
`Messages are being held until a route is configured.`
|
|
726
|
+
].filter((line) => line !== null).join("\n");
|
|
727
|
+
const invitePayload = {
|
|
728
|
+
channel: "system:delivery",
|
|
729
|
+
sender: "system",
|
|
730
|
+
content: [{ type: "text", text: notification }]
|
|
731
|
+
};
|
|
732
|
+
const config = getRoutingConfig(mindName.split("@", 2)[0]);
|
|
733
|
+
const sessionConfig = resolveDeliveryMode(config, "main");
|
|
734
|
+
await this.deliverToMind(mindName, "main", invitePayload, {
|
|
735
|
+
...sessionConfig,
|
|
736
|
+
interrupt: true
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
async persistToQueue(mindName, session, payload, status = "pending") {
|
|
740
|
+
try {
|
|
741
|
+
const db = await getDb();
|
|
742
|
+
await db.insert(deliveryQueue).values({
|
|
743
|
+
mind: mindName,
|
|
744
|
+
session,
|
|
745
|
+
channel: payload.channel ?? null,
|
|
746
|
+
sender: payload.sender ?? null,
|
|
747
|
+
status,
|
|
748
|
+
payload: JSON.stringify(payload)
|
|
749
|
+
});
|
|
750
|
+
} catch (err) {
|
|
751
|
+
dlog3.warn(
|
|
752
|
+
`failed to persist to delivery queue for ${mindName}/${session}`,
|
|
753
|
+
logger_default.errorData(err)
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
incrementActive(mind, session) {
|
|
758
|
+
let mindSessions = this.sessionStates.get(mind);
|
|
759
|
+
if (!mindSessions) {
|
|
760
|
+
mindSessions = /* @__PURE__ */ new Map();
|
|
761
|
+
this.sessionStates.set(mind, mindSessions);
|
|
762
|
+
}
|
|
763
|
+
const state = mindSessions.get(session) ?? { activeCount: 0, lastDeliveredAt: 0 };
|
|
764
|
+
state.activeCount++;
|
|
765
|
+
state.lastDeliveredAt = Date.now();
|
|
766
|
+
mindSessions.set(session, state);
|
|
767
|
+
}
|
|
768
|
+
decrementActive(mind, session) {
|
|
769
|
+
const mindSessions = this.sessionStates.get(mind);
|
|
770
|
+
if (!mindSessions) return;
|
|
771
|
+
const state = mindSessions.get(session);
|
|
772
|
+
if (!state) return;
|
|
773
|
+
state.activeCount = Math.max(0, state.activeCount - 1);
|
|
774
|
+
if (state.activeCount === 0) {
|
|
775
|
+
const bufferKey = `${mind}:${session}`;
|
|
776
|
+
const buffer = this.batchBuffers.get(bufferKey);
|
|
777
|
+
if (buffer && buffer.messages.length > 0) {
|
|
778
|
+
this.scheduleBatchTimers(mind, session, bufferKey);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
};
|
|
783
|
+
var instance2;
|
|
784
|
+
function initDeliveryManager() {
|
|
785
|
+
if (instance2) return instance2;
|
|
786
|
+
instance2 = new DeliveryManager();
|
|
787
|
+
return instance2;
|
|
788
|
+
}
|
|
789
|
+
function getDeliveryManager() {
|
|
790
|
+
if (!instance2) {
|
|
791
|
+
throw new Error("DeliveryManager not initialized \u2014 call initDeliveryManager() first");
|
|
792
|
+
}
|
|
793
|
+
return instance2;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
export {
|
|
797
|
+
extractTextContent,
|
|
798
|
+
deliverMessage,
|
|
799
|
+
getTypingMap,
|
|
800
|
+
DeliveryManager,
|
|
801
|
+
initDeliveryManager,
|
|
802
|
+
getDeliveryManager
|
|
803
|
+
};
|