volute 0.23.0 → 0.25.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 +5 -5
- package/dist/{activity-events-3WHHCOBB.js → activity-events-4O37J7PD.js} +2 -2
- package/dist/api.d.ts +419 -19
- package/dist/{channel-BOOMFULW.js → channel-HZOSHGNF.js} +1 -1
- package/dist/{chunk-QIXPN3OO.js → chunk-2767L2RZ.js} +5 -5
- package/dist/{chunk-SGPEZ32F.js → chunk-33XAVCS4.js} +16 -0
- package/dist/{chunk-VT5QODNE.js → chunk-3AIBT4TW.js} +4 -3
- package/dist/{chunk-A4S7H6G6.js → chunk-BFK6SOEJ.js} +1 -1
- package/dist/{chunk-RK627D57.js → chunk-BOTQ25QT.js} +3 -3
- package/dist/{chunk-TFS25FIM.js → chunk-DG7TO7EE.js} +31 -3
- package/dist/{chunk-HGCDWKSP.js → chunk-E7GOKNOT.js} +1 -1
- package/dist/{chunk-ISWZ6QUK.js → chunk-PMX4EIJK.js} +804 -115
- package/dist/{chunk-M5CNKH4J.js → chunk-SHSWYG2J.js} +7 -7
- package/dist/{chunk-XLC342FO.js → chunk-SIAG3QMM.js} +14 -1
- package/dist/{chunk-KFI7TQJ6.js → chunk-TRQEV3CD.js} +9 -5
- package/dist/{chunk-JG4CCJOA.js → chunk-ZSH4G2P5.js} +33 -15
- package/dist/cli.js +18 -18
- package/dist/{cloud-sync-PI47U2LT.js → cloud-sync-PPBBJDY6.js} +7 -9
- package/dist/{connector-PYT5UOTZ.js → connector-M6XFI6GM.js} +1 -1
- package/dist/{create-WIDA3M4C.js → create-VDQJER52.js} +1 -1
- package/dist/{daemon-client-ZHCDL4RS.js → daemon-client-JOVQZ52X.js} +1 -1
- package/dist/{daemon-restart-RMGOOGPE.js → daemon-restart-FDNOZEAD.js} +5 -5
- package/dist/daemon.js +1047 -981
- package/dist/{delete-LOIANQGD.js → delete-2MRR4JX5.js} +1 -1
- package/dist/{down-WSUASL5E.js → down-674SX2IZ.js} +2 -2
- package/dist/{env-4PHIHTF4.js → env-2FPOZK37.js} +1 -1
- package/dist/{export-XD6PJBQP.js → export-IKFAPRAO.js} +1 -1
- package/dist/{file-X4L5TTOL.js → file-KT3UIQM3.js} +1 -1
- package/dist/{history-HTEKRNID.js → history-46WZN5CN.js} +1 -1
- package/dist/{import-EAXTHHXL.js → import-TH26J76F.js} +2 -2
- package/dist/{log-SRO5Q6AD.js → log-6SGSSR3D.js} +1 -1
- package/dist/{logs-HNTNNBDW.js → logs-HRBONI5I.js} +1 -1
- package/dist/{merge-B6SYTGI7.js → merge-KSFJKX6T.js} +1 -1
- package/dist/{message-delivery-FHV4NO2F.js → message-delivery-XMGV3FUM.js} +6 -6
- package/dist/{mind-BTXR5B3C.js → mind-YVWAHL2A.js} +17 -17
- package/dist/{mind-activity-tracker-PGC3DBJ7.js → mind-activity-tracker-NMDDEV3K.js} +3 -3
- package/dist/{mind-manager-KMY4GA2J.js → mind-manager-4NDNAYAB.js} +2 -2
- package/dist/{mind-sleep-FWRBIFBS.js → mind-sleep-GHPTSAYN.js} +1 -1
- package/dist/{mind-wake-LJK2YU5X.js → mind-wake-BJDJFMDF.js} +1 -1
- package/dist/{package-CUBJ4PKS.js → package-3HF5MXU2.js} +2 -1
- package/dist/{pages-YSTRWJR4.js → pages-Y6DRWUOJ.js} +1 -1
- package/dist/{publish-BZNHKUUK.js → publish-EEKTZBHW.js} +1 -1
- package/dist/{pull-GRQAXM2E.js → pull-D32SPFVU.js} +1 -1
- package/dist/{restart-CIDAKGG2.js → restart-5BMNV7KU.js} +1 -1
- package/dist/{schedule-NLR3LZLY.js → schedule-YEFDLVMJ.js} +1 -1
- package/dist/{seed-3H2MRREW.js → seed-6FEKB3YC.js} +1 -1
- package/dist/{send-RP2TA7SG.js → send-IISDYFCL.js} +1 -1
- package/dist/{service-7BFXDI6J.js → service-FASYWLTC.js} +3 -3
- package/dist/{setup-SSIIXQMI.js → setup-BMLM2UTK.js} +1 -1
- package/dist/{shared-2OGT3NSL.js → shared-LWMNTTZN.js} +4 -4
- package/dist/{skill-Q2Y6PQ3L.js → skill-T3EMR6IR.js} +11 -3
- package/dist/skills/imagegen/SKILL.md +37 -0
- package/dist/skills/imagegen/references/INSTALL.md +13 -0
- package/dist/skills/imagegen/scripts/imagegen.ts +136 -0
- package/dist/skills/resonance/SKILL.md +73 -0
- package/dist/skills/resonance/assets/default-config.json +21 -0
- package/dist/skills/resonance/references/INSTALL.md +23 -0
- package/dist/skills/resonance/scripts/resonance.ts +1250 -0
- package/dist/skills/volute-mind/SKILL.md +94 -4
- package/dist/{sleep-manager-2TMQ65E4.js → sleep-manager-RKTFZPD3.js} +6 -6
- package/dist/{sprout-UKCYBGHK.js → sprout-QJVGJDSH.js} +3 -3
- package/dist/{start-JR6CUUWF.js → start-C7XITZ5O.js} +1 -1
- package/dist/{status-5XDGYHKP.js → status-LYS4NUOZ.js} +1 -1
- package/dist/{status-H2MKDN6L.js → status-SIRPLEZC.js} +4 -3
- package/dist/{stop-VKPGK25U.js → stop-CVKBSLXY.js} +1 -1
- package/dist/tailscale-AJ4VL5XK.js +49 -0
- package/dist/{up-Z5JRG2M2.js → up-CJ26KQLN.js} +2 -2
- package/dist/{update-ELC6MEUT.js → update-7XCZMYBT.js} +7 -7
- package/dist/{upgrade-GXW2EQY3.js → upgrade-7RUIXGOO.js} +1 -1
- package/dist/{variant-A4I7PHXS.js → variant-UGREB4G5.js} +4 -4
- package/dist/{version-notify-LKABEJSA.js → version-notify-AZQMC32A.js} +6 -6
- package/dist/web-assets/assets/index-CGPSVu19.js +69 -0
- package/dist/web-assets/assets/index-V_rNDsM8.css +1 -0
- package/dist/web-assets/favicon.png +0 -0
- package/dist/web-assets/index.html +5 -4
- package/dist/web-assets/logo.png +0 -0
- package/drizzle/0013_user_profiles.sql +3 -0
- package/drizzle/0014_conversation_reads.sql +7 -0
- package/drizzle/meta/0013_snapshot.json +7 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +2 -1
- package/templates/_base/home/public/.gitkeep +0 -0
- package/templates/_base/src/lib/format-prefix.ts +18 -2
- package/templates/_base/src/lib/routing.ts +2 -1
- package/templates/_base/src/lib/types.ts +8 -0
- package/dist/chunk-G5KRTU2F.js +0 -76
- package/dist/web-assets/assets/index-CZ26vsyY.js +0 -69
- package/dist/web-assets/assets/index-DyyAvJwW.css +0 -1
|
@@ -4,11 +4,12 @@ import {
|
|
|
4
4
|
} from "./chunk-HFCBO2GL.js";
|
|
5
5
|
import {
|
|
6
6
|
markIdle
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-E7GOKNOT.js";
|
|
8
8
|
import {
|
|
9
|
+
broadcast,
|
|
9
10
|
publish,
|
|
10
11
|
subscribe
|
|
11
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-BFK6SOEJ.js";
|
|
12
13
|
import {
|
|
13
14
|
RestartTracker,
|
|
14
15
|
RotatingLog,
|
|
@@ -17,18 +18,23 @@ import {
|
|
|
17
18
|
getPrompt,
|
|
18
19
|
loadJsonMap,
|
|
19
20
|
saveJsonMap
|
|
20
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-SHSWYG2J.js";
|
|
21
22
|
import {
|
|
22
23
|
readVoluteConfig
|
|
23
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-SIAG3QMM.js";
|
|
24
25
|
import {
|
|
25
26
|
loadMergedEnv
|
|
26
27
|
} from "./chunk-PHU4DEAJ.js";
|
|
27
28
|
import {
|
|
29
|
+
conversationParticipants,
|
|
30
|
+
conversationReads,
|
|
31
|
+
conversations,
|
|
28
32
|
deliveryQueue,
|
|
29
33
|
getDb,
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
messages,
|
|
35
|
+
mindHistory,
|
|
36
|
+
users
|
|
37
|
+
} from "./chunk-33XAVCS4.js";
|
|
32
38
|
import {
|
|
33
39
|
logger_default
|
|
34
40
|
} from "./chunk-YUIHSKR6.js";
|
|
@@ -61,10 +67,136 @@ import {
|
|
|
61
67
|
renameSync,
|
|
62
68
|
writeFileSync as writeFileSync3
|
|
63
69
|
} from "fs";
|
|
64
|
-
import { resolve as
|
|
70
|
+
import { resolve as resolve8 } from "path";
|
|
65
71
|
import { promisify } from "util";
|
|
66
72
|
import { CronExpressionParser as CronExpressionParser2 } from "cron-parser";
|
|
67
|
-
import { and as
|
|
73
|
+
import { and as and4, eq as eq4, inArray as inArray2 } from "drizzle-orm";
|
|
74
|
+
|
|
75
|
+
// src/lib/auth.ts
|
|
76
|
+
import { compareSync, hashSync } from "bcryptjs";
|
|
77
|
+
import { and, count, eq } from "drizzle-orm";
|
|
78
|
+
var userSelectFields = {
|
|
79
|
+
id: users.id,
|
|
80
|
+
username: users.username,
|
|
81
|
+
role: users.role,
|
|
82
|
+
user_type: users.user_type,
|
|
83
|
+
display_name: users.display_name,
|
|
84
|
+
description: users.description,
|
|
85
|
+
avatar: users.avatar,
|
|
86
|
+
created_at: users.created_at
|
|
87
|
+
};
|
|
88
|
+
async function createUser(username, password) {
|
|
89
|
+
const db = await getDb();
|
|
90
|
+
const hash = hashSync(password, 10);
|
|
91
|
+
const [{ value }] = await db.select({ value: count() }).from(users).where(eq(users.user_type, "brain"));
|
|
92
|
+
const role = value === 0 ? "admin" : "pending";
|
|
93
|
+
const [result] = await db.insert(users).values({ username, password_hash: hash, role }).returning(userSelectFields);
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
async function verifyUser(username, password) {
|
|
97
|
+
const db = await getDb();
|
|
98
|
+
const row = await db.select().from(users).where(eq(users.username, username)).get();
|
|
99
|
+
if (!row) return null;
|
|
100
|
+
if (row.user_type === "mind") return null;
|
|
101
|
+
if (!compareSync(password, row.password_hash)) return null;
|
|
102
|
+
const { password_hash: _, ...user } = row;
|
|
103
|
+
return user;
|
|
104
|
+
}
|
|
105
|
+
async function getUser(id) {
|
|
106
|
+
const db = await getDb();
|
|
107
|
+
const row = await db.select(userSelectFields).from(users).where(eq(users.id, id)).get();
|
|
108
|
+
return row ?? null;
|
|
109
|
+
}
|
|
110
|
+
async function getUserByUsername(username) {
|
|
111
|
+
const db = await getDb();
|
|
112
|
+
const row = await db.select(userSelectFields).from(users).where(eq(users.username, username)).get();
|
|
113
|
+
return row ?? null;
|
|
114
|
+
}
|
|
115
|
+
async function listUsers() {
|
|
116
|
+
const db = await getDb();
|
|
117
|
+
return db.select(userSelectFields).from(users).orderBy(users.created_at).all();
|
|
118
|
+
}
|
|
119
|
+
async function listPendingUsers() {
|
|
120
|
+
const db = await getDb();
|
|
121
|
+
return db.select(userSelectFields).from(users).where(eq(users.role, "pending")).orderBy(users.created_at).all();
|
|
122
|
+
}
|
|
123
|
+
async function listUsersByType(userType) {
|
|
124
|
+
const db = await getDb();
|
|
125
|
+
return db.select(userSelectFields).from(users).where(eq(users.user_type, userType)).orderBy(users.created_at).all();
|
|
126
|
+
}
|
|
127
|
+
async function getOrCreateMindUser(mindName) {
|
|
128
|
+
const db = await getDb();
|
|
129
|
+
const existing = await db.select(userSelectFields).from(users).where(and(eq(users.username, mindName), eq(users.user_type, "mind"))).get();
|
|
130
|
+
if (existing) return existing;
|
|
131
|
+
try {
|
|
132
|
+
const [result] = await db.insert(users).values({
|
|
133
|
+
username: mindName,
|
|
134
|
+
password_hash: "!mind",
|
|
135
|
+
role: "mind",
|
|
136
|
+
user_type: "mind"
|
|
137
|
+
}).returning(userSelectFields);
|
|
138
|
+
return result;
|
|
139
|
+
} catch (err) {
|
|
140
|
+
if (err instanceof Error && err.message.includes("UNIQUE constraint")) {
|
|
141
|
+
const retried = await db.select(userSelectFields).from(users).where(and(eq(users.username, mindName), eq(users.user_type, "mind"))).get();
|
|
142
|
+
if (retried) return retried;
|
|
143
|
+
}
|
|
144
|
+
throw err;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async function deleteMindUser(mindName) {
|
|
148
|
+
const db = await getDb();
|
|
149
|
+
await db.delete(users).where(and(eq(users.username, mindName), eq(users.user_type, "mind")));
|
|
150
|
+
}
|
|
151
|
+
async function changePassword(userId, currentPassword, newPassword) {
|
|
152
|
+
const db = await getDb();
|
|
153
|
+
const row = await db.select().from(users).where(eq(users.id, userId)).get();
|
|
154
|
+
if (!row) return false;
|
|
155
|
+
if (!compareSync(currentPassword, row.password_hash)) return false;
|
|
156
|
+
const hash = hashSync(newPassword, 10);
|
|
157
|
+
await db.update(users).set({ password_hash: hash }).where(eq(users.id, userId));
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
async function approveUser(id) {
|
|
161
|
+
const db = await getDb();
|
|
162
|
+
await db.update(users).set({ role: "user" }).where(and(eq(users.id, id), eq(users.role, "pending")));
|
|
163
|
+
}
|
|
164
|
+
async function countAdmins() {
|
|
165
|
+
const db = await getDb();
|
|
166
|
+
const [{ value }] = await db.select({ value: count() }).from(users).where(eq(users.role, "admin"));
|
|
167
|
+
return value;
|
|
168
|
+
}
|
|
169
|
+
async function setUserRole(id, role) {
|
|
170
|
+
const db = await getDb();
|
|
171
|
+
const target = await db.select({ id: users.id }).from(users).where(eq(users.id, id)).get();
|
|
172
|
+
if (!target) throw new Error("User not found");
|
|
173
|
+
await db.update(users).set({ role }).where(eq(users.id, id));
|
|
174
|
+
}
|
|
175
|
+
async function deleteUser(id) {
|
|
176
|
+
const db = await getDb();
|
|
177
|
+
const target = await db.select({ id: users.id }).from(users).where(and(eq(users.id, id), eq(users.user_type, "brain"))).get();
|
|
178
|
+
if (!target) throw new Error("User not found");
|
|
179
|
+
await db.delete(users).where(and(eq(users.id, id), eq(users.user_type, "brain")));
|
|
180
|
+
}
|
|
181
|
+
async function updateUserProfile(userId, profile) {
|
|
182
|
+
const db = await getDb();
|
|
183
|
+
const target = await db.select({ id: users.id }).from(users).where(eq(users.id, userId)).get();
|
|
184
|
+
if (!target) throw new Error("User not found");
|
|
185
|
+
await db.update(users).set(profile).where(eq(users.id, userId));
|
|
186
|
+
}
|
|
187
|
+
async function syncMindProfile(mindName, config) {
|
|
188
|
+
const user = await getOrCreateMindUser(mindName);
|
|
189
|
+
const newProfile = {
|
|
190
|
+
display_name: config.displayName ?? null,
|
|
191
|
+
description: config.description ?? null,
|
|
192
|
+
avatar: config.avatar ?? null
|
|
193
|
+
};
|
|
194
|
+
const changed = user.display_name !== newProfile.display_name || user.description !== newProfile.description || user.avatar !== newProfile.avatar;
|
|
195
|
+
if (!changed) return;
|
|
196
|
+
const db = await getDb();
|
|
197
|
+
await db.update(users).set(newProfile).where(eq(users.id, user.id));
|
|
198
|
+
broadcast({ type: "profile_updated", mind: mindName, summary: `${mindName} profile updated` });
|
|
199
|
+
}
|
|
68
200
|
|
|
69
201
|
// src/lib/pages-watcher.ts
|
|
70
202
|
import { existsSync, readdirSync, statSync, watch } from "fs";
|
|
@@ -104,16 +236,16 @@ function startPagesWatcher(mindName, pagesDir) {
|
|
|
104
236
|
}
|
|
105
237
|
function startWatcher(mindName) {
|
|
106
238
|
if (watchers.has(mindName)) return;
|
|
107
|
-
const pagesDir = resolve(mindDir(mindName), "home", "pages");
|
|
239
|
+
const pagesDir = resolve(mindDir(mindName), "home", "public", "pages");
|
|
108
240
|
if (existsSync(pagesDir)) {
|
|
109
241
|
startPagesWatcher(mindName, pagesDir);
|
|
110
242
|
return;
|
|
111
243
|
}
|
|
112
244
|
if (homeWatchers.has(mindName)) return;
|
|
113
|
-
const
|
|
114
|
-
if (!existsSync(
|
|
245
|
+
const publicDir = resolve(mindDir(mindName), "home", "public");
|
|
246
|
+
if (!existsSync(publicDir)) return;
|
|
115
247
|
try {
|
|
116
|
-
const hw = watch(
|
|
248
|
+
const hw = watch(publicDir, (_eventType, filename) => {
|
|
117
249
|
if (filename !== "pages") return;
|
|
118
250
|
if (!existsSync(pagesDir)) return;
|
|
119
251
|
hw.close();
|
|
@@ -210,7 +342,7 @@ function buildSites() {
|
|
|
210
342
|
}
|
|
211
343
|
const entries = readRegistry();
|
|
212
344
|
for (const entry of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
|
|
213
|
-
const pagesDir = resolve(mindDir(entry.name), "home", "pages");
|
|
345
|
+
const pagesDir = resolve(mindDir(entry.name), "home", "public", "pages");
|
|
214
346
|
if (!existsSync(pagesDir)) continue;
|
|
215
347
|
const mindPages = scanPagesDir(pagesDir, `/pages/${entry.name}`);
|
|
216
348
|
if (mindPages.length > 0) {
|
|
@@ -223,7 +355,7 @@ function buildRecentPages() {
|
|
|
223
355
|
const entries = readRegistry();
|
|
224
356
|
const pages = [];
|
|
225
357
|
for (const entry of entries) {
|
|
226
|
-
const pagesDir = resolve(mindDir(entry.name), "home", "pages");
|
|
358
|
+
const pagesDir = resolve(mindDir(entry.name), "home", "public", "pages");
|
|
227
359
|
if (!existsSync(pagesDir)) continue;
|
|
228
360
|
let items;
|
|
229
361
|
try {
|
|
@@ -521,19 +653,19 @@ var ConnectorManager = class {
|
|
|
521
653
|
const stopKey = `${mindName}:${type}`;
|
|
522
654
|
this.stopping.add(stopKey);
|
|
523
655
|
mindMap.delete(type);
|
|
524
|
-
await new Promise((
|
|
525
|
-
tracked.child.on("exit", () =>
|
|
656
|
+
await new Promise((resolve9) => {
|
|
657
|
+
tracked.child.on("exit", () => resolve9());
|
|
526
658
|
try {
|
|
527
659
|
process.kill(-tracked.child.pid, "SIGTERM");
|
|
528
660
|
} catch {
|
|
529
|
-
|
|
661
|
+
resolve9();
|
|
530
662
|
}
|
|
531
663
|
setTimeout(() => {
|
|
532
664
|
try {
|
|
533
665
|
process.kill(-tracked.child.pid, "SIGKILL");
|
|
534
666
|
} catch {
|
|
535
667
|
}
|
|
536
|
-
|
|
668
|
+
resolve9();
|
|
537
669
|
}, 5e3);
|
|
538
670
|
});
|
|
539
671
|
this.stopping.delete(stopKey);
|
|
@@ -647,7 +779,75 @@ function publish2(mind, event) {
|
|
|
647
779
|
}
|
|
648
780
|
|
|
649
781
|
// src/lib/delivery/delivery-manager.ts
|
|
650
|
-
import {
|
|
782
|
+
import { readFile, realpath } from "fs/promises";
|
|
783
|
+
import { extname, resolve as resolve5 } from "path";
|
|
784
|
+
import { and as and3, eq as eq3, sql as sql2 } from "drizzle-orm";
|
|
785
|
+
|
|
786
|
+
// src/lib/events/conversations.ts
|
|
787
|
+
import { randomUUID } from "crypto";
|
|
788
|
+
import { and as and2, desc, eq as eq2, inArray, isNull, lt, sql } from "drizzle-orm";
|
|
789
|
+
|
|
790
|
+
// src/lib/webhook.ts
|
|
791
|
+
var slog = logger_default.child("webhook");
|
|
792
|
+
function getWebhookUrl() {
|
|
793
|
+
return process.env.VOLUTE_WEBHOOK_URL;
|
|
794
|
+
}
|
|
795
|
+
function getAuthHeaders() {
|
|
796
|
+
const headers = { "Content-Type": "application/json" };
|
|
797
|
+
const secret = process.env.VOLUTE_WEBHOOK_SECRET;
|
|
798
|
+
if (secret) headers.Authorization = `Bearer ${secret}`;
|
|
799
|
+
return headers;
|
|
800
|
+
}
|
|
801
|
+
function fireWebhook(event) {
|
|
802
|
+
try {
|
|
803
|
+
const url = getWebhookUrl();
|
|
804
|
+
if (!url) return;
|
|
805
|
+
const payload = { ...event, timestamp: event.timestamp ?? (/* @__PURE__ */ new Date()).toISOString() };
|
|
806
|
+
fetch(url, {
|
|
807
|
+
method: "POST",
|
|
808
|
+
headers: getAuthHeaders(),
|
|
809
|
+
body: JSON.stringify(payload)
|
|
810
|
+
}).then((res) => {
|
|
811
|
+
if (!res.ok) {
|
|
812
|
+
slog.warn(`webhook ${event.event} returned HTTP ${res.status}`);
|
|
813
|
+
}
|
|
814
|
+
}).catch((err) => {
|
|
815
|
+
slog.warn(`webhook delivery failed for ${event.event}`, logger_default.errorData(err));
|
|
816
|
+
});
|
|
817
|
+
} catch (err) {
|
|
818
|
+
slog.error(`webhook ${event.event} failed to serialize`, logger_default.errorData(err));
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
function initWebhook() {
|
|
822
|
+
const url = getWebhookUrl();
|
|
823
|
+
if (!url) return () => {
|
|
824
|
+
};
|
|
825
|
+
try {
|
|
826
|
+
const parsed = new URL(url);
|
|
827
|
+
if (!["http:", "https:"].includes(parsed.protocol)) {
|
|
828
|
+
slog.error(`VOLUTE_WEBHOOK_URL has unsupported protocol: ${parsed.protocol}`);
|
|
829
|
+
return () => {
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
} catch {
|
|
833
|
+
slog.error(`VOLUTE_WEBHOOK_URL is not a valid URL`);
|
|
834
|
+
return () => {
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
slog.info("webhook enabled");
|
|
838
|
+
return subscribe((event) => {
|
|
839
|
+
try {
|
|
840
|
+
fireWebhook({
|
|
841
|
+
event: event.type,
|
|
842
|
+
mind: event.mind,
|
|
843
|
+
data: { summary: event.summary, ...event.metadata },
|
|
844
|
+
timestamp: event.created_at
|
|
845
|
+
});
|
|
846
|
+
} catch (err) {
|
|
847
|
+
slog.error(`failed to fire webhook for ${event.type}`, logger_default.errorData(err));
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
}
|
|
651
851
|
|
|
652
852
|
// src/lib/events/conversation-events.ts
|
|
653
853
|
var subscribers2 = /* @__PURE__ */ new Map();
|
|
@@ -677,6 +877,330 @@ function publish3(conversationId, event) {
|
|
|
677
877
|
}
|
|
678
878
|
}
|
|
679
879
|
|
|
880
|
+
// src/lib/events/conversations.ts
|
|
881
|
+
async function createConversation(mindName, channel, opts) {
|
|
882
|
+
const db = await getDb();
|
|
883
|
+
const id = randomUUID();
|
|
884
|
+
const type = opts?.type ?? "dm";
|
|
885
|
+
const name = opts?.name ?? null;
|
|
886
|
+
await db.transaction(async (tx) => {
|
|
887
|
+
await tx.insert(conversations).values({
|
|
888
|
+
id,
|
|
889
|
+
mind_name: mindName,
|
|
890
|
+
channel,
|
|
891
|
+
type,
|
|
892
|
+
name,
|
|
893
|
+
user_id: opts?.userId ?? null,
|
|
894
|
+
title: opts?.title ?? null
|
|
895
|
+
});
|
|
896
|
+
if (opts?.participantIds && opts.participantIds.length > 0) {
|
|
897
|
+
await tx.insert(conversationParticipants).values(
|
|
898
|
+
opts.participantIds.map((uid, i) => ({
|
|
899
|
+
conversation_id: id,
|
|
900
|
+
user_id: uid,
|
|
901
|
+
role: i === 0 ? "owner" : "member"
|
|
902
|
+
}))
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
});
|
|
906
|
+
fireWebhook({
|
|
907
|
+
event: "conversation_created",
|
|
908
|
+
mind: mindName ?? "",
|
|
909
|
+
data: { id, mindName, channel, type, name, title: opts?.title ?? null }
|
|
910
|
+
});
|
|
911
|
+
return {
|
|
912
|
+
id,
|
|
913
|
+
mind_name: mindName,
|
|
914
|
+
channel,
|
|
915
|
+
type,
|
|
916
|
+
name,
|
|
917
|
+
user_id: opts?.userId ?? null,
|
|
918
|
+
title: opts?.title ?? null,
|
|
919
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
920
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
async function getConversation(id) {
|
|
924
|
+
const db = await getDb();
|
|
925
|
+
const row = await db.select().from(conversations).where(eq2(conversations.id, id)).get();
|
|
926
|
+
return row ?? null;
|
|
927
|
+
}
|
|
928
|
+
async function addParticipant(conversationId, userId, role = "member") {
|
|
929
|
+
const db = await getDb();
|
|
930
|
+
await db.insert(conversationParticipants).values({
|
|
931
|
+
conversation_id: conversationId,
|
|
932
|
+
user_id: userId,
|
|
933
|
+
role
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
async function removeParticipant(conversationId, userId) {
|
|
937
|
+
const db = await getDb();
|
|
938
|
+
await db.delete(conversationParticipants).where(
|
|
939
|
+
and2(
|
|
940
|
+
eq2(conversationParticipants.conversation_id, conversationId),
|
|
941
|
+
eq2(conversationParticipants.user_id, userId)
|
|
942
|
+
)
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
async function getParticipants(conversationId) {
|
|
946
|
+
const db = await getDb();
|
|
947
|
+
const rows = await db.select({
|
|
948
|
+
userId: conversationParticipants.user_id,
|
|
949
|
+
username: users.username,
|
|
950
|
+
userType: users.user_type,
|
|
951
|
+
role: conversationParticipants.role,
|
|
952
|
+
displayName: users.display_name,
|
|
953
|
+
description: users.description,
|
|
954
|
+
avatar: users.avatar
|
|
955
|
+
}).from(conversationParticipants).innerJoin(users, eq2(conversationParticipants.user_id, users.id)).where(eq2(conversationParticipants.conversation_id, conversationId)).all();
|
|
956
|
+
return rows;
|
|
957
|
+
}
|
|
958
|
+
async function isParticipant(conversationId, userId) {
|
|
959
|
+
const db = await getDb();
|
|
960
|
+
const row = await db.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(
|
|
961
|
+
and2(
|
|
962
|
+
eq2(conversationParticipants.conversation_id, conversationId),
|
|
963
|
+
eq2(conversationParticipants.user_id, userId)
|
|
964
|
+
)
|
|
965
|
+
).get();
|
|
966
|
+
return row != null;
|
|
967
|
+
}
|
|
968
|
+
async function listConversationsForUser(userId) {
|
|
969
|
+
const db = await getDb();
|
|
970
|
+
const participantRows = await db.select({ conversation_id: conversationParticipants.conversation_id }).from(conversationParticipants).where(eq2(conversationParticipants.user_id, userId)).all();
|
|
971
|
+
if (participantRows.length === 0) return [];
|
|
972
|
+
const convIds = participantRows.map((r) => r.conversation_id);
|
|
973
|
+
return await db.select().from(conversations).where(inArray(conversations.id, convIds)).orderBy(desc(conversations.updated_at)).all();
|
|
974
|
+
}
|
|
975
|
+
async function isParticipantOrOwner(conversationId, userId) {
|
|
976
|
+
if (await isParticipant(conversationId, userId)) return true;
|
|
977
|
+
const db = await getDb();
|
|
978
|
+
const row = await db.select().from(conversations).where(and2(eq2(conversations.id, conversationId), eq2(conversations.user_id, userId))).get();
|
|
979
|
+
return row != null;
|
|
980
|
+
}
|
|
981
|
+
async function deleteConversationForUser(id, userId) {
|
|
982
|
+
if (!await isParticipantOrOwner(id, userId)) return false;
|
|
983
|
+
await deleteConversation(id);
|
|
984
|
+
return true;
|
|
985
|
+
}
|
|
986
|
+
async function addMessage(conversationId, role, senderName, content) {
|
|
987
|
+
const db = await getDb();
|
|
988
|
+
const serialized = JSON.stringify(content);
|
|
989
|
+
const [result] = await db.insert(messages).values({ conversation_id: conversationId, role, sender_name: senderName, content: serialized }).returning({ id: messages.id, created_at: messages.created_at });
|
|
990
|
+
await db.update(conversations).set({ updated_at: sql`datetime('now')` }).where(eq2(conversations.id, conversationId));
|
|
991
|
+
if (role === "user") {
|
|
992
|
+
const firstText = content.find((b) => b.type === "text");
|
|
993
|
+
const title = firstText ? firstText.text.slice(0, 80) : "";
|
|
994
|
+
if (title) {
|
|
995
|
+
await db.update(conversations).set({ title }).where(and2(eq2(conversations.id, conversationId), isNull(conversations.title)));
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
const msg = {
|
|
999
|
+
id: result.id,
|
|
1000
|
+
conversation_id: conversationId,
|
|
1001
|
+
role,
|
|
1002
|
+
sender_name: senderName,
|
|
1003
|
+
content,
|
|
1004
|
+
created_at: result.created_at
|
|
1005
|
+
};
|
|
1006
|
+
publish3(conversationId, {
|
|
1007
|
+
type: "message",
|
|
1008
|
+
id: msg.id,
|
|
1009
|
+
role: msg.role,
|
|
1010
|
+
senderName: msg.sender_name,
|
|
1011
|
+
content: msg.content,
|
|
1012
|
+
createdAt: msg.created_at
|
|
1013
|
+
});
|
|
1014
|
+
const conv = await db.select({ mind_name: conversations.mind_name }).from(conversations).where(eq2(conversations.id, conversationId)).get();
|
|
1015
|
+
fireWebhook({
|
|
1016
|
+
event: "message_created",
|
|
1017
|
+
mind: conv?.mind_name ?? "",
|
|
1018
|
+
data: {
|
|
1019
|
+
conversationId,
|
|
1020
|
+
messageId: result.id,
|
|
1021
|
+
role,
|
|
1022
|
+
senderName,
|
|
1023
|
+
content: content.filter((b) => b.type !== "image"),
|
|
1024
|
+
createdAt: result.created_at
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
1027
|
+
return msg;
|
|
1028
|
+
}
|
|
1029
|
+
async function getMessages(conversationId) {
|
|
1030
|
+
const db = await getDb();
|
|
1031
|
+
const rows = await db.select().from(messages).where(eq2(messages.conversation_id, conversationId)).orderBy(messages.created_at).all();
|
|
1032
|
+
return rows.map(parseMessageRow);
|
|
1033
|
+
}
|
|
1034
|
+
async function getMessagesPaginated(conversationId, opts) {
|
|
1035
|
+
const db = await getDb();
|
|
1036
|
+
const limit = Math.min(Math.max(opts?.limit ?? 50, 1), 100);
|
|
1037
|
+
const conditions = [eq2(messages.conversation_id, conversationId)];
|
|
1038
|
+
if (opts?.before != null) {
|
|
1039
|
+
conditions.push(lt(messages.id, opts.before));
|
|
1040
|
+
}
|
|
1041
|
+
const rows = await db.select().from(messages).where(and2(...conditions)).orderBy(desc(messages.id)).limit(limit + 1).all();
|
|
1042
|
+
const hasMore = rows.length > limit;
|
|
1043
|
+
const page = rows.slice(0, limit).reverse();
|
|
1044
|
+
return {
|
|
1045
|
+
messages: page.map(parseMessageRow),
|
|
1046
|
+
hasMore
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
function parseMessageRow(row) {
|
|
1050
|
+
let content;
|
|
1051
|
+
try {
|
|
1052
|
+
const parsed = JSON.parse(row.content);
|
|
1053
|
+
content = Array.isArray(parsed) ? parsed : [{ type: "text", text: row.content }];
|
|
1054
|
+
} catch {
|
|
1055
|
+
content = [{ type: "text", text: row.content }];
|
|
1056
|
+
}
|
|
1057
|
+
return { ...row, role: row.role, content };
|
|
1058
|
+
}
|
|
1059
|
+
async function listConversationsWithParticipants(userId) {
|
|
1060
|
+
const convs = await listConversationsForUser(userId);
|
|
1061
|
+
if (convs.length === 0) return [];
|
|
1062
|
+
const db = await getDb();
|
|
1063
|
+
const convIds = convs.map((c) => c.id);
|
|
1064
|
+
const rows = await db.select({
|
|
1065
|
+
conversationId: conversationParticipants.conversation_id,
|
|
1066
|
+
userId: users.id,
|
|
1067
|
+
username: users.username,
|
|
1068
|
+
userType: users.user_type,
|
|
1069
|
+
role: conversationParticipants.role,
|
|
1070
|
+
displayName: users.display_name,
|
|
1071
|
+
description: users.description,
|
|
1072
|
+
avatar: users.avatar
|
|
1073
|
+
}).from(conversationParticipants).innerJoin(users, eq2(conversationParticipants.user_id, users.id)).where(inArray(conversationParticipants.conversation_id, convIds));
|
|
1074
|
+
const byConv = /* @__PURE__ */ new Map();
|
|
1075
|
+
for (const r of rows) {
|
|
1076
|
+
let arr = byConv.get(r.conversationId);
|
|
1077
|
+
if (!arr) {
|
|
1078
|
+
arr = [];
|
|
1079
|
+
byConv.set(r.conversationId, arr);
|
|
1080
|
+
}
|
|
1081
|
+
arr.push({
|
|
1082
|
+
userId: r.userId,
|
|
1083
|
+
username: r.username,
|
|
1084
|
+
userType: r.userType,
|
|
1085
|
+
role: r.role,
|
|
1086
|
+
displayName: r.displayName,
|
|
1087
|
+
description: r.description,
|
|
1088
|
+
avatar: r.avatar
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
const lastMsgIds = await db.select({
|
|
1092
|
+
conversationId: messages.conversation_id,
|
|
1093
|
+
maxId: sql`MAX(${messages.id})`
|
|
1094
|
+
}).from(messages).where(inArray(messages.conversation_id, convIds)).groupBy(messages.conversation_id);
|
|
1095
|
+
const byLastMsg = /* @__PURE__ */ new Map();
|
|
1096
|
+
if (lastMsgIds.length > 0) {
|
|
1097
|
+
const msgRows = await db.select().from(messages).where(
|
|
1098
|
+
inArray(
|
|
1099
|
+
messages.id,
|
|
1100
|
+
lastMsgIds.map((r) => r.maxId)
|
|
1101
|
+
)
|
|
1102
|
+
);
|
|
1103
|
+
for (const m of msgRows) {
|
|
1104
|
+
let text = "";
|
|
1105
|
+
try {
|
|
1106
|
+
const parsed = JSON.parse(m.content);
|
|
1107
|
+
const blocks = Array.isArray(parsed) ? parsed : [];
|
|
1108
|
+
const textBlock = blocks.find((b) => b.type === "text");
|
|
1109
|
+
if (textBlock && "text" in textBlock) text = textBlock.text;
|
|
1110
|
+
} catch {
|
|
1111
|
+
text = m.content;
|
|
1112
|
+
}
|
|
1113
|
+
byLastMsg.set(m.conversation_id, {
|
|
1114
|
+
role: m.role,
|
|
1115
|
+
senderName: m.sender_name,
|
|
1116
|
+
text,
|
|
1117
|
+
createdAt: m.created_at
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
return convs.map((c) => ({
|
|
1122
|
+
...c,
|
|
1123
|
+
participants: byConv.get(c.id) ?? [],
|
|
1124
|
+
lastMessage: byLastMsg.get(c.id)
|
|
1125
|
+
}));
|
|
1126
|
+
}
|
|
1127
|
+
async function findDMConversation(mindName, participantIds) {
|
|
1128
|
+
const db = await getDb();
|
|
1129
|
+
const mindConvs = await db.select({ id: conversations.id }).from(conversations).where(and2(eq2(conversations.mind_name, mindName), eq2(conversations.type, "dm"))).all();
|
|
1130
|
+
for (const conv of mindConvs) {
|
|
1131
|
+
const rows = await db.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(eq2(conversationParticipants.conversation_id, conv.id)).all();
|
|
1132
|
+
if (rows.length !== 2) continue;
|
|
1133
|
+
const ids = new Set(rows.map((r) => r.user_id));
|
|
1134
|
+
if (ids.has(participantIds[0]) && ids.has(participantIds[1])) {
|
|
1135
|
+
return conv.id;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
return null;
|
|
1139
|
+
}
|
|
1140
|
+
async function deleteConversation(id) {
|
|
1141
|
+
const db = await getDb();
|
|
1142
|
+
await db.delete(conversations).where(eq2(conversations.id, id));
|
|
1143
|
+
}
|
|
1144
|
+
async function createChannel(name, creatorId) {
|
|
1145
|
+
const participantIds = creatorId ? [creatorId] : [];
|
|
1146
|
+
return createConversation(null, "volute", {
|
|
1147
|
+
type: "channel",
|
|
1148
|
+
name,
|
|
1149
|
+
title: name,
|
|
1150
|
+
participantIds
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
async function getChannelByName(name) {
|
|
1154
|
+
const db = await getDb();
|
|
1155
|
+
const row = await db.select().from(conversations).where(and2(eq2(conversations.name, name), eq2(conversations.type, "channel"))).get();
|
|
1156
|
+
return row ?? null;
|
|
1157
|
+
}
|
|
1158
|
+
async function listChannels() {
|
|
1159
|
+
const db = await getDb();
|
|
1160
|
+
return await db.select().from(conversations).where(eq2(conversations.type, "channel")).orderBy(conversations.name).all();
|
|
1161
|
+
}
|
|
1162
|
+
async function joinChannel(conversationId, userId) {
|
|
1163
|
+
if (await isParticipant(conversationId, userId)) return;
|
|
1164
|
+
await addParticipant(conversationId, userId);
|
|
1165
|
+
}
|
|
1166
|
+
async function leaveChannel(conversationId, userId) {
|
|
1167
|
+
await removeParticipant(conversationId, userId);
|
|
1168
|
+
}
|
|
1169
|
+
async function getUnreadCounts(userId, conversationIds) {
|
|
1170
|
+
if (conversationIds.length === 0) return {};
|
|
1171
|
+
const db = await getDb();
|
|
1172
|
+
const rows = await db.select({
|
|
1173
|
+
conversationId: messages.conversation_id,
|
|
1174
|
+
count: sql`COUNT(*)`
|
|
1175
|
+
}).from(messages).leftJoin(
|
|
1176
|
+
conversationReads,
|
|
1177
|
+
and2(
|
|
1178
|
+
eq2(conversationReads.conversation_id, messages.conversation_id),
|
|
1179
|
+
eq2(conversationReads.user_id, userId)
|
|
1180
|
+
)
|
|
1181
|
+
).where(
|
|
1182
|
+
and2(
|
|
1183
|
+
inArray(messages.conversation_id, conversationIds),
|
|
1184
|
+
sql`${messages.id} > COALESCE(${conversationReads.last_read_message_id}, 0)`
|
|
1185
|
+
)
|
|
1186
|
+
).groupBy(messages.conversation_id);
|
|
1187
|
+
const result = {};
|
|
1188
|
+
for (const row of rows) {
|
|
1189
|
+
result[row.conversationId] = row.count;
|
|
1190
|
+
}
|
|
1191
|
+
return result;
|
|
1192
|
+
}
|
|
1193
|
+
async function markConversationRead(userId, conversationId) {
|
|
1194
|
+
const db = await getDb();
|
|
1195
|
+
const maxRow = await db.select({ maxId: sql`MAX(${messages.id})` }).from(messages).where(eq2(messages.conversation_id, conversationId)).get();
|
|
1196
|
+
const maxId = maxRow?.maxId ?? 0;
|
|
1197
|
+
if (maxId === 0) return;
|
|
1198
|
+
await db.insert(conversationReads).values({ user_id: userId, conversation_id: conversationId, last_read_message_id: maxId }).onConflictDoUpdate({
|
|
1199
|
+
target: [conversationReads.user_id, conversationReads.conversation_id],
|
|
1200
|
+
set: { last_read_message_id: maxId }
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
|
|
680
1204
|
// src/lib/typing.ts
|
|
681
1205
|
var DEFAULT_TTL_MS = 1e4;
|
|
682
1206
|
var SWEEP_INTERVAL_MS = 5e3;
|
|
@@ -826,7 +1350,7 @@ function globMatch(pattern, value) {
|
|
|
826
1350
|
return regex.test(value);
|
|
827
1351
|
}
|
|
828
1352
|
var GLOB_MATCH_KEYS = /* @__PURE__ */ new Set(["channel", "sender"]);
|
|
829
|
-
var NON_MATCH_KEYS = /* @__PURE__ */ new Set(["session", "destination", "path", "mode"]);
|
|
1353
|
+
var NON_MATCH_KEYS = /* @__PURE__ */ new Set(["session", "destination", "path", "mode", "batch"]);
|
|
830
1354
|
function ruleMatches(rule, meta) {
|
|
831
1355
|
for (const [key, pattern] of Object.entries(rule)) {
|
|
832
1356
|
if (NON_MATCH_KEYS.has(key)) continue;
|
|
@@ -871,7 +1395,8 @@ function resolveRoute(config, meta) {
|
|
|
871
1395
|
destination: "mind",
|
|
872
1396
|
session: sanitizeSessionName(expandTemplate(rule.session ?? fallback, meta)),
|
|
873
1397
|
matched: true,
|
|
874
|
-
mode: rule.mode
|
|
1398
|
+
mode: rule.mode,
|
|
1399
|
+
rule
|
|
875
1400
|
};
|
|
876
1401
|
}
|
|
877
1402
|
}
|
|
@@ -883,12 +1408,27 @@ function normalizeBatchConfig(batch) {
|
|
|
883
1408
|
if (typeof batch === "number") return { maxWait: batch * 60 };
|
|
884
1409
|
return batch;
|
|
885
1410
|
}
|
|
886
|
-
function resolveDeliveryMode(config, sessionName) {
|
|
1411
|
+
function resolveDeliveryMode(config, sessionName, rule) {
|
|
1412
|
+
const ruleBatch = rule?.batch;
|
|
887
1413
|
const defaults = {
|
|
888
1414
|
delivery: { mode: "immediate" },
|
|
889
1415
|
interrupt: true
|
|
890
1416
|
};
|
|
891
|
-
if (!config.sessions)
|
|
1417
|
+
if (!config.sessions) {
|
|
1418
|
+
if (ruleBatch != null) {
|
|
1419
|
+
const batch = normalizeBatchConfig(ruleBatch);
|
|
1420
|
+
return {
|
|
1421
|
+
delivery: {
|
|
1422
|
+
mode: "batch",
|
|
1423
|
+
debounce: batch.debounce ?? DEFAULT_BATCH_DEBOUNCE,
|
|
1424
|
+
maxWait: batch.maxWait ?? DEFAULT_BATCH_MAX_WAIT,
|
|
1425
|
+
triggers: batch.triggers
|
|
1426
|
+
},
|
|
1427
|
+
interrupt: true
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
return defaults;
|
|
1431
|
+
}
|
|
892
1432
|
for (const [pattern, sessionConfig] of Object.entries(config.sessions)) {
|
|
893
1433
|
if (globMatch(pattern, sessionName)) {
|
|
894
1434
|
let delivery;
|
|
@@ -932,6 +1472,18 @@ function resolveDeliveryMode(config, sessionName) {
|
|
|
932
1472
|
};
|
|
933
1473
|
}
|
|
934
1474
|
}
|
|
1475
|
+
if (ruleBatch != null) {
|
|
1476
|
+
const batch = normalizeBatchConfig(ruleBatch);
|
|
1477
|
+
return {
|
|
1478
|
+
delivery: {
|
|
1479
|
+
mode: "batch",
|
|
1480
|
+
debounce: batch.debounce ?? DEFAULT_BATCH_DEBOUNCE,
|
|
1481
|
+
maxWait: batch.maxWait ?? DEFAULT_BATCH_MAX_WAIT,
|
|
1482
|
+
triggers: batch.triggers
|
|
1483
|
+
},
|
|
1484
|
+
interrupt: true
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
935
1487
|
return defaults;
|
|
936
1488
|
}
|
|
937
1489
|
|
|
@@ -981,7 +1533,7 @@ var DeliveryManager = class {
|
|
|
981
1533
|
if (sessionName === "$new") {
|
|
982
1534
|
sessionName = `new-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
983
1535
|
}
|
|
984
|
-
const sessionConfig = resolveDeliveryMode(config, sessionName);
|
|
1536
|
+
const sessionConfig = resolveDeliveryMode(config, sessionName, route.rule);
|
|
985
1537
|
if (sessionConfig.delivery.mode === "batch") {
|
|
986
1538
|
dlog2.debug(`enqueueing batch message for ${mindName}/${sessionName}`);
|
|
987
1539
|
this.enqueueBatch(mindName, sessionName, payload, sessionConfig);
|
|
@@ -1013,7 +1565,7 @@ var DeliveryManager = class {
|
|
|
1013
1565
|
async restoreFromDb() {
|
|
1014
1566
|
try {
|
|
1015
1567
|
const db = await getDb();
|
|
1016
|
-
const rows = await db.select().from(deliveryQueue).where(
|
|
1568
|
+
const rows = await db.select().from(deliveryQueue).where(eq3(deliveryQueue.status, "pending"));
|
|
1017
1569
|
for (const row of rows) {
|
|
1018
1570
|
let payload;
|
|
1019
1571
|
try {
|
|
@@ -1031,7 +1583,7 @@ var DeliveryManager = class {
|
|
|
1031
1583
|
this.addToBatchBuffer(row.mind, row.session, payload, sessionConfig);
|
|
1032
1584
|
} else {
|
|
1033
1585
|
try {
|
|
1034
|
-
await db.delete(deliveryQueue).where(
|
|
1586
|
+
await db.delete(deliveryQueue).where(eq3(deliveryQueue.id, row.id));
|
|
1035
1587
|
} catch (err) {
|
|
1036
1588
|
dlog2.warn(`failed to delete queue row ${row.id} for ${row.mind}`, logger_default.errorData(err));
|
|
1037
1589
|
}
|
|
@@ -1052,7 +1604,7 @@ var DeliveryManager = class {
|
|
|
1052
1604
|
*/
|
|
1053
1605
|
async getPending(mindName) {
|
|
1054
1606
|
const db = await getDb();
|
|
1055
|
-
const rows = await db.select().from(deliveryQueue).where(
|
|
1607
|
+
const rows = await db.select().from(deliveryQueue).where(and3(eq3(deliveryQueue.mind, mindName), eq3(deliveryQueue.status, "gated")));
|
|
1056
1608
|
const byChannel = /* @__PURE__ */ new Map();
|
|
1057
1609
|
for (const row of rows) {
|
|
1058
1610
|
const ch = row.channel ?? "unknown";
|
|
@@ -1145,8 +1697,9 @@ var DeliveryManager = class {
|
|
|
1145
1697
|
if (payload.conversationId) {
|
|
1146
1698
|
typingMap.set(`volute:${payload.conversationId}`, baseName, { persistent: true });
|
|
1147
1699
|
}
|
|
1700
|
+
const enrichedPayload = await this.enrichWithProfiles(baseName, session, payload);
|
|
1148
1701
|
const body = JSON.stringify({
|
|
1149
|
-
...
|
|
1702
|
+
...enrichedPayload,
|
|
1150
1703
|
session,
|
|
1151
1704
|
interrupt: sessionConfig.interrupt,
|
|
1152
1705
|
instructions: sessionConfig.instructions
|
|
@@ -1163,22 +1716,30 @@ var DeliveryManager = class {
|
|
|
1163
1716
|
publishTypingForChannels(typingMap.deleteSender(baseName), typingMap);
|
|
1164
1717
|
}
|
|
1165
1718
|
}
|
|
1166
|
-
async deliverBatchToMind(mindName, session,
|
|
1719
|
+
async deliverBatchToMind(mindName, session, messages2, sessionConfig, interruptOverride) {
|
|
1167
1720
|
const resolved = this.resolvePort(mindName);
|
|
1168
1721
|
if (!resolved) {
|
|
1169
1722
|
dlog2.warn(`cannot deliver batch to ${mindName}: mind not found`);
|
|
1170
1723
|
return;
|
|
1171
1724
|
}
|
|
1172
1725
|
const { baseName, port } = resolved;
|
|
1726
|
+
const enrichedMessages = await Promise.all(
|
|
1727
|
+
messages2.map(async (msg, i) => {
|
|
1728
|
+
const isFirst = messages2.findIndex((m) => m.channel === msg.channel) === i;
|
|
1729
|
+
if (!isFirst) return msg;
|
|
1730
|
+
const enrichedPayload = await this.enrichWithProfiles(baseName, session, msg.payload);
|
|
1731
|
+
return { ...msg, payload: enrichedPayload };
|
|
1732
|
+
})
|
|
1733
|
+
);
|
|
1173
1734
|
const channels = {};
|
|
1174
|
-
for (const msg of
|
|
1735
|
+
for (const msg of enrichedMessages) {
|
|
1175
1736
|
const ch = msg.channel ?? "unknown";
|
|
1176
1737
|
if (!channels[ch]) channels[ch] = [];
|
|
1177
1738
|
channels[ch].push(msg.payload);
|
|
1178
1739
|
}
|
|
1179
1740
|
const senders = /* @__PURE__ */ new Set();
|
|
1180
1741
|
const channelSet = /* @__PURE__ */ new Set();
|
|
1181
|
-
for (const msg of
|
|
1742
|
+
for (const msg of messages2) {
|
|
1182
1743
|
if (msg.sender) senders.add(msg.sender);
|
|
1183
1744
|
if (msg.channel) channelSet.add(msg.channel);
|
|
1184
1745
|
}
|
|
@@ -1188,7 +1749,7 @@ var DeliveryManager = class {
|
|
|
1188
1749
|
if (ch !== "unknown") typingMap.set(ch, baseName, { persistent: true });
|
|
1189
1750
|
}
|
|
1190
1751
|
const seenConvIds = /* @__PURE__ */ new Set();
|
|
1191
|
-
for (const msg of
|
|
1752
|
+
for (const msg of messages2) {
|
|
1192
1753
|
if (msg.payload.conversationId && !seenConvIds.has(msg.payload.conversationId)) {
|
|
1193
1754
|
seenConvIds.add(msg.payload.conversationId);
|
|
1194
1755
|
typingMap.set(`volute:${msg.payload.conversationId}`, baseName, { persistent: true });
|
|
@@ -1209,10 +1770,10 @@ var DeliveryManager = class {
|
|
|
1209
1770
|
try {
|
|
1210
1771
|
const db = await getDb();
|
|
1211
1772
|
await db.delete(deliveryQueue).where(
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1773
|
+
and3(
|
|
1774
|
+
eq3(deliveryQueue.mind, baseName),
|
|
1775
|
+
eq3(deliveryQueue.session, session),
|
|
1776
|
+
eq3(deliveryQueue.status, "pending")
|
|
1216
1777
|
)
|
|
1217
1778
|
);
|
|
1218
1779
|
} catch (err) {
|
|
@@ -1310,24 +1871,24 @@ var DeliveryManager = class {
|
|
|
1310
1871
|
flushBatch(mindName, session, extra, interruptOverride) {
|
|
1311
1872
|
const bufferKey = `${mindName}:${session}`;
|
|
1312
1873
|
const buffer = this.batchBuffers.get(bufferKey);
|
|
1313
|
-
const
|
|
1874
|
+
const messages2 = [];
|
|
1314
1875
|
if (buffer) {
|
|
1315
1876
|
if (buffer.debounceTimer) clearTimeout(buffer.debounceTimer);
|
|
1316
1877
|
if (buffer.maxWaitTimer) clearTimeout(buffer.maxWaitTimer);
|
|
1317
1878
|
buffer.debounceTimer = null;
|
|
1318
1879
|
buffer.maxWaitTimer = null;
|
|
1319
|
-
|
|
1880
|
+
messages2.push(...buffer.messages.splice(0));
|
|
1320
1881
|
this.batchBuffers.delete(bufferKey);
|
|
1321
1882
|
}
|
|
1322
|
-
if (extra)
|
|
1323
|
-
if (
|
|
1883
|
+
if (extra) messages2.push(...extra);
|
|
1884
|
+
if (messages2.length === 0) return;
|
|
1324
1885
|
const [baseName] = mindName.split("@", 2);
|
|
1325
1886
|
const config = getRoutingConfig(baseName);
|
|
1326
1887
|
const sessionConfig = resolveDeliveryMode(config, session);
|
|
1327
1888
|
dlog2.info(
|
|
1328
|
-
`flushing batch for ${mindName}/${session}: ${
|
|
1889
|
+
`flushing batch for ${mindName}/${session}: ${messages2.length} messages${interruptOverride ? " (new-speaker interrupt)" : ""}`
|
|
1329
1890
|
);
|
|
1330
|
-
this.deliverBatchToMind(mindName, session,
|
|
1891
|
+
this.deliverBatchToMind(mindName, session, messages2, sessionConfig, interruptOverride).catch(
|
|
1331
1892
|
(err) => {
|
|
1332
1893
|
dlog2.warn(`failed to flush batch for ${mindName}/${session}`, logger_default.errorData(err));
|
|
1333
1894
|
}
|
|
@@ -1338,14 +1899,14 @@ var DeliveryManager = class {
|
|
|
1338
1899
|
await this.persistToQueue(baseName, session, payload, "gated");
|
|
1339
1900
|
try {
|
|
1340
1901
|
const db = await getDb();
|
|
1341
|
-
const
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1902
|
+
const count2 = await db.select({ count: sql2`count(*)` }).from(deliveryQueue).where(
|
|
1903
|
+
and3(
|
|
1904
|
+
eq3(deliveryQueue.mind, baseName),
|
|
1905
|
+
eq3(deliveryQueue.channel, payload.channel),
|
|
1906
|
+
eq3(deliveryQueue.status, "gated")
|
|
1346
1907
|
)
|
|
1347
1908
|
);
|
|
1348
|
-
if ((
|
|
1909
|
+
if ((count2[0]?.count ?? 0) <= 1) {
|
|
1349
1910
|
await this.sendInviteNotification(mindName, payload);
|
|
1350
1911
|
}
|
|
1351
1912
|
} catch (err) {
|
|
@@ -1397,6 +1958,90 @@ var DeliveryManager = class {
|
|
|
1397
1958
|
);
|
|
1398
1959
|
}
|
|
1399
1960
|
}
|
|
1961
|
+
async enrichWithProfiles(mindName, session, payload) {
|
|
1962
|
+
if (!payload.conversationId || !payload.channel) return payload;
|
|
1963
|
+
const mindSessions = this.sessionStates.get(mindName);
|
|
1964
|
+
const state = mindSessions?.get(session);
|
|
1965
|
+
if (!state) return payload;
|
|
1966
|
+
const channelKey = payload.channel;
|
|
1967
|
+
if (state.seenChannelProfiles.has(channelKey)) return payload;
|
|
1968
|
+
try {
|
|
1969
|
+
const participants = await getParticipants(payload.conversationId);
|
|
1970
|
+
const profiles = participants.map((p) => ({
|
|
1971
|
+
username: p.username,
|
|
1972
|
+
userType: p.userType,
|
|
1973
|
+
displayName: p.displayName,
|
|
1974
|
+
description: p.description
|
|
1975
|
+
}));
|
|
1976
|
+
const avatarBlocks = await this.loadAvatarBlocks(participants);
|
|
1977
|
+
state.seenChannelProfiles.add(channelKey);
|
|
1978
|
+
const enriched = { ...payload, participantProfiles: profiles };
|
|
1979
|
+
if (avatarBlocks.length > 0) {
|
|
1980
|
+
const existing = Array.isArray(payload.content) ? payload.content : typeof payload.content === "string" ? [{ type: "text", text: payload.content }] : [];
|
|
1981
|
+
enriched.content = [...avatarBlocks, ...existing];
|
|
1982
|
+
}
|
|
1983
|
+
return enriched;
|
|
1984
|
+
} catch (err) {
|
|
1985
|
+
dlog2.warn(`failed to fetch participant profiles for ${mindName}`, logger_default.errorData(err));
|
|
1986
|
+
return payload;
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
async loadAvatarBlocks(participants) {
|
|
1990
|
+
const blocks = [];
|
|
1991
|
+
for (const p of participants) {
|
|
1992
|
+
if (!p.avatar) continue;
|
|
1993
|
+
try {
|
|
1994
|
+
let filePath;
|
|
1995
|
+
if (p.userType === "mind") {
|
|
1996
|
+
const dir = mindDir(p.username);
|
|
1997
|
+
const config = readVoluteConfig(dir);
|
|
1998
|
+
if (!config?.profile?.avatar) continue;
|
|
1999
|
+
filePath = resolve5(dir, "home", config.profile.avatar);
|
|
2000
|
+
const homeDir = resolve5(dir, "home");
|
|
2001
|
+
if (!filePath.startsWith(`${homeDir}/`)) {
|
|
2002
|
+
dlog2.warn(`avatar path for ${p.username} escapes home directory, skipping`);
|
|
2003
|
+
continue;
|
|
2004
|
+
}
|
|
2005
|
+
try {
|
|
2006
|
+
const realHome = await realpath(homeDir);
|
|
2007
|
+
const realAvatar = await realpath(filePath);
|
|
2008
|
+
if (!realAvatar.startsWith(`${realHome}/`)) {
|
|
2009
|
+
dlog2.warn(
|
|
2010
|
+
`avatar symlink for ${p.username} resolves outside home directory, skipping`
|
|
2011
|
+
);
|
|
2012
|
+
continue;
|
|
2013
|
+
}
|
|
2014
|
+
} catch (err) {
|
|
2015
|
+
if (err.code === "ENOENT") continue;
|
|
2016
|
+
throw err;
|
|
2017
|
+
}
|
|
2018
|
+
} else {
|
|
2019
|
+
filePath = resolve5(voluteHome(), "avatars", p.avatar);
|
|
2020
|
+
}
|
|
2021
|
+
const ext = extname(filePath).toLowerCase();
|
|
2022
|
+
const mimeMap = {
|
|
2023
|
+
".png": "image/png",
|
|
2024
|
+
".jpg": "image/jpeg",
|
|
2025
|
+
".jpeg": "image/jpeg",
|
|
2026
|
+
".gif": "image/gif",
|
|
2027
|
+
".webp": "image/webp"
|
|
2028
|
+
};
|
|
2029
|
+
const mediaType = mimeMap[ext];
|
|
2030
|
+
if (!mediaType) continue;
|
|
2031
|
+
const data = await readFile(filePath);
|
|
2032
|
+
blocks.push(
|
|
2033
|
+
{ type: "text", text: `[Avatar for ${p.username}]` },
|
|
2034
|
+
{ type: "image", media_type: mediaType, data: data.toString("base64") }
|
|
2035
|
+
);
|
|
2036
|
+
} catch (err) {
|
|
2037
|
+
const code = err.code;
|
|
2038
|
+
if (code !== "ENOENT") {
|
|
2039
|
+
dlog2.warn(`failed to load avatar for ${p.username}`, logger_default.errorData(err));
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
return blocks;
|
|
2044
|
+
}
|
|
1400
2045
|
incrementActive(mind, session, senders, channels) {
|
|
1401
2046
|
let mindSessions = this.sessionStates.get(mind);
|
|
1402
2047
|
if (!mindSessions) {
|
|
@@ -1408,7 +2053,8 @@ var DeliveryManager = class {
|
|
|
1408
2053
|
lastDeliveredAt: 0,
|
|
1409
2054
|
lastDeliverySenders: /* @__PURE__ */ new Set(),
|
|
1410
2055
|
lastDeliveryChannels: /* @__PURE__ */ new Set(),
|
|
1411
|
-
lastInterruptAt: 0
|
|
2056
|
+
lastInterruptAt: 0,
|
|
2057
|
+
seenChannelProfiles: /* @__PURE__ */ new Set()
|
|
1412
2058
|
};
|
|
1413
2059
|
state.activeCount++;
|
|
1414
2060
|
state.lastDeliveredAt = Date.now();
|
|
@@ -1731,16 +2377,16 @@ async function ensureMailAddress(mindName) {
|
|
|
1731
2377
|
}
|
|
1732
2378
|
|
|
1733
2379
|
// src/lib/daemon/scheduler.ts
|
|
1734
|
-
import { resolve as
|
|
2380
|
+
import { resolve as resolve6 } from "path";
|
|
1735
2381
|
import { CronExpressionParser } from "cron-parser";
|
|
1736
|
-
var
|
|
2382
|
+
var slog2 = logger_default.child("scheduler");
|
|
1737
2383
|
var Scheduler = class {
|
|
1738
2384
|
schedules = /* @__PURE__ */ new Map();
|
|
1739
2385
|
interval = null;
|
|
1740
2386
|
lastFired = /* @__PURE__ */ new Map();
|
|
1741
2387
|
// "mind:scheduleId" → epoch minute
|
|
1742
2388
|
get statePath() {
|
|
1743
|
-
return
|
|
2389
|
+
return resolve6(voluteHome(), "scheduler-state.json");
|
|
1744
2390
|
}
|
|
1745
2391
|
start() {
|
|
1746
2392
|
this.loadState();
|
|
@@ -1799,7 +2445,7 @@ var Scheduler = class {
|
|
|
1799
2445
|
prevMinute = Math.floor(prev.getTime() / 6e4);
|
|
1800
2446
|
cronCache.set(schedule.cron, prevMinute);
|
|
1801
2447
|
} catch (err) {
|
|
1802
|
-
|
|
2448
|
+
slog2.warn(`invalid cron "${schedule.cron}" for ${mind}:${schedule.id}`, logger_default.errorData(err));
|
|
1803
2449
|
return false;
|
|
1804
2450
|
}
|
|
1805
2451
|
}
|
|
@@ -1813,11 +2459,11 @@ var Scheduler = class {
|
|
|
1813
2459
|
try {
|
|
1814
2460
|
let text;
|
|
1815
2461
|
if (schedule.script) {
|
|
1816
|
-
const homeDir =
|
|
2462
|
+
const homeDir = resolve6(mindDir(mindName), "home");
|
|
1817
2463
|
try {
|
|
1818
2464
|
const output = await this.runScript(schedule.script, homeDir, mindName);
|
|
1819
2465
|
if (!output.trim()) {
|
|
1820
|
-
|
|
2466
|
+
slog2.info(`fired script "${schedule.id}" for ${mindName} (no output)`);
|
|
1821
2467
|
return;
|
|
1822
2468
|
}
|
|
1823
2469
|
text = output;
|
|
@@ -1825,12 +2471,12 @@ var Scheduler = class {
|
|
|
1825
2471
|
const stderr = err.stderr ?? "";
|
|
1826
2472
|
text = `[script error] ${err.message}${stderr ? `
|
|
1827
2473
|
${stderr}` : ""}`;
|
|
1828
|
-
|
|
2474
|
+
slog2.warn(`script "${schedule.id}" failed for ${mindName}`, logger_default.errorData(err));
|
|
1829
2475
|
}
|
|
1830
2476
|
} else if (schedule.message) {
|
|
1831
2477
|
text = schedule.message;
|
|
1832
2478
|
} else {
|
|
1833
|
-
|
|
2479
|
+
slog2.warn(`schedule "${schedule.id}" for ${mindName} has no message or script`);
|
|
1834
2480
|
return;
|
|
1835
2481
|
}
|
|
1836
2482
|
await this.deliver(mindName, {
|
|
@@ -1838,9 +2484,9 @@ ${stderr}` : ""}`;
|
|
|
1838
2484
|
channel: "system:scheduler",
|
|
1839
2485
|
sender: schedule.id
|
|
1840
2486
|
});
|
|
1841
|
-
|
|
2487
|
+
slog2.info(`fired "${schedule.id}" for ${mindName}`);
|
|
1842
2488
|
} catch (err) {
|
|
1843
|
-
|
|
2489
|
+
slog2.warn(`failed to fire "${schedule.id}" for ${mindName}`, logger_default.errorData(err));
|
|
1844
2490
|
}
|
|
1845
2491
|
}
|
|
1846
2492
|
runScript(script, cwd, mindName) {
|
|
@@ -1863,7 +2509,7 @@ function getScheduler() {
|
|
|
1863
2509
|
|
|
1864
2510
|
// src/lib/daemon/token-budget.ts
|
|
1865
2511
|
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
1866
|
-
import { resolve as
|
|
2512
|
+
import { resolve as resolve7 } from "path";
|
|
1867
2513
|
var tlog = logger_default.child("token-budget");
|
|
1868
2514
|
var DEFAULT_BUDGET_PERIOD_MINUTES = 60;
|
|
1869
2515
|
var MAX_QUEUE_SIZE = 100;
|
|
@@ -1937,9 +2583,9 @@ var TokenBudget = class {
|
|
|
1937
2583
|
drain(mind) {
|
|
1938
2584
|
const state = this.budgets.get(mind);
|
|
1939
2585
|
if (!state) return [];
|
|
1940
|
-
const
|
|
2586
|
+
const messages2 = state.queue;
|
|
1941
2587
|
state.queue = [];
|
|
1942
|
-
return
|
|
2588
|
+
return messages2;
|
|
1943
2589
|
}
|
|
1944
2590
|
getUsage(mind) {
|
|
1945
2591
|
const state = this.budgets.get(mind);
|
|
@@ -1981,7 +2627,7 @@ var TokenBudget = class {
|
|
|
1981
2627
|
this.dirty.clear();
|
|
1982
2628
|
}
|
|
1983
2629
|
budgetStatePath(mind) {
|
|
1984
|
-
return
|
|
2630
|
+
return resolve7(stateDir(mind), "budget.json");
|
|
1985
2631
|
}
|
|
1986
2632
|
saveBudgetState(mind, state) {
|
|
1987
2633
|
try {
|
|
@@ -2020,8 +2666,8 @@ var TokenBudget = class {
|
|
|
2020
2666
|
return null;
|
|
2021
2667
|
}
|
|
2022
2668
|
}
|
|
2023
|
-
async replay(mindName,
|
|
2024
|
-
const summary =
|
|
2669
|
+
async replay(mindName, messages2) {
|
|
2670
|
+
const summary = messages2.map((m) => {
|
|
2025
2671
|
const from = m.sender ? `[${m.sender}]` : "";
|
|
2026
2672
|
const ch = m.channel ? `(${m.channel})` : "";
|
|
2027
2673
|
return `${from}${ch} ${m.textContent}`;
|
|
@@ -2031,7 +2677,7 @@ var TokenBudget = class {
|
|
|
2031
2677
|
content: [
|
|
2032
2678
|
{
|
|
2033
2679
|
type: "text",
|
|
2034
|
-
text: `[Budget replay] ${
|
|
2680
|
+
text: `[Budget replay] ${messages2.length} queued message(s) from the previous budget period:
|
|
2035
2681
|
|
|
2036
2682
|
${summary}`
|
|
2037
2683
|
}
|
|
@@ -2039,11 +2685,11 @@ ${summary}`
|
|
|
2039
2685
|
channel: "system:budget-replay",
|
|
2040
2686
|
sender: "system"
|
|
2041
2687
|
});
|
|
2042
|
-
tlog.info(`replayed ${
|
|
2688
|
+
tlog.info(`replayed ${messages2.length} queued message(s) for ${mindName}`);
|
|
2043
2689
|
} catch (err) {
|
|
2044
2690
|
tlog.warn(`failed to replay for ${mindName}`, logger_default.errorData(err));
|
|
2045
2691
|
const state = this.budgets.get(mindName);
|
|
2046
|
-
if (state) state.queue.push(...
|
|
2692
|
+
if (state) state.queue.push(...messages2);
|
|
2047
2693
|
}
|
|
2048
2694
|
}
|
|
2049
2695
|
};
|
|
@@ -2078,6 +2724,11 @@ async function startMindFull(name) {
|
|
|
2078
2724
|
(err) => logger_default.error(`failed to ensure mail address for ${baseName}`, logger_default.errorData(err))
|
|
2079
2725
|
);
|
|
2080
2726
|
const config = readVoluteConfig(dir);
|
|
2727
|
+
if (config) {
|
|
2728
|
+
syncMindProfile(baseName, config.profile ?? {}).catch(
|
|
2729
|
+
(err) => logger_default.error(`failed to sync profile for ${baseName}`, logger_default.errorData(err))
|
|
2730
|
+
);
|
|
2731
|
+
}
|
|
2081
2732
|
if (config?.tokenBudget) {
|
|
2082
2733
|
getTokenBudget().setBudget(
|
|
2083
2734
|
baseName,
|
|
@@ -2122,7 +2773,7 @@ async function stopMindFull(name) {
|
|
|
2122
2773
|
}
|
|
2123
2774
|
|
|
2124
2775
|
// src/lib/daemon/sleep-manager.ts
|
|
2125
|
-
var
|
|
2776
|
+
var slog3 = logger_default.child("sleep");
|
|
2126
2777
|
function defaultState() {
|
|
2127
2778
|
return {
|
|
2128
2779
|
sleeping: false,
|
|
@@ -2158,7 +2809,7 @@ var SleepManager = class {
|
|
|
2158
2809
|
unsubActivity = null;
|
|
2159
2810
|
transitioning = /* @__PURE__ */ new Set();
|
|
2160
2811
|
get statePath() {
|
|
2161
|
-
return
|
|
2812
|
+
return resolve8(voluteHome(), "sleep-state.json");
|
|
2162
2813
|
}
|
|
2163
2814
|
start() {
|
|
2164
2815
|
this.loadState();
|
|
@@ -2181,7 +2832,7 @@ var SleepManager = class {
|
|
|
2181
2832
|
}
|
|
2182
2833
|
}
|
|
2183
2834
|
} catch (err) {
|
|
2184
|
-
|
|
2835
|
+
slog3.warn("failed to load sleep state", logger_default.errorData(err));
|
|
2185
2836
|
}
|
|
2186
2837
|
}
|
|
2187
2838
|
saveState() {
|
|
@@ -2193,7 +2844,7 @@ var SleepManager = class {
|
|
|
2193
2844
|
writeFileSync3(this.statePath, `${JSON.stringify(data, null, 2)}
|
|
2194
2845
|
`);
|
|
2195
2846
|
} catch (err) {
|
|
2196
|
-
|
|
2847
|
+
slog3.error("failed to save sleep state", logger_default.errorData(err));
|
|
2197
2848
|
}
|
|
2198
2849
|
}
|
|
2199
2850
|
// --- Public API ---
|
|
@@ -2240,7 +2891,7 @@ var SleepManager = class {
|
|
|
2240
2891
|
content: preSleepMsg
|
|
2241
2892
|
});
|
|
2242
2893
|
} catch (err) {
|
|
2243
|
-
|
|
2894
|
+
slog3.error(`failed to persist pre-sleep message for ${name}`, logger_default.errorData(err));
|
|
2244
2895
|
}
|
|
2245
2896
|
try {
|
|
2246
2897
|
await fetch(`http://127.0.0.1:${entry.port}/message`, {
|
|
@@ -2252,7 +2903,7 @@ var SleepManager = class {
|
|
|
2252
2903
|
})
|
|
2253
2904
|
});
|
|
2254
2905
|
} catch (err) {
|
|
2255
|
-
|
|
2906
|
+
slog3.warn(`failed to send pre-sleep message to ${name}`, logger_default.errorData(err));
|
|
2256
2907
|
}
|
|
2257
2908
|
await this.waitForIdle(name, 12e4);
|
|
2258
2909
|
await new Promise((r) => setTimeout(r, 3e3));
|
|
@@ -2260,7 +2911,7 @@ var SleepManager = class {
|
|
|
2260
2911
|
await this.killOrphanOnPort(entry.port);
|
|
2261
2912
|
await this.archiveSessions(name);
|
|
2262
2913
|
this.markSleeping(name, opts);
|
|
2263
|
-
|
|
2914
|
+
slog3.info(`${name} is now sleeping`);
|
|
2264
2915
|
} finally {
|
|
2265
2916
|
this.transitioning.delete(name);
|
|
2266
2917
|
}
|
|
@@ -2286,7 +2937,7 @@ var SleepManager = class {
|
|
|
2286
2937
|
try {
|
|
2287
2938
|
await wakeMind(name);
|
|
2288
2939
|
} catch (err) {
|
|
2289
|
-
|
|
2940
|
+
slog3.error(`failed to wake ${name}`, logger_default.errorData(err));
|
|
2290
2941
|
return;
|
|
2291
2942
|
}
|
|
2292
2943
|
const entry = findMind(name);
|
|
@@ -2318,7 +2969,7 @@ var SleepManager = class {
|
|
|
2318
2969
|
content: summaryText
|
|
2319
2970
|
});
|
|
2320
2971
|
} catch (err) {
|
|
2321
|
-
|
|
2972
|
+
slog3.error(`failed to persist wake summary for ${name}`, logger_default.errorData(err));
|
|
2322
2973
|
}
|
|
2323
2974
|
try {
|
|
2324
2975
|
await fetch(`http://127.0.0.1:${entry.port}/message`, {
|
|
@@ -2330,16 +2981,16 @@ var SleepManager = class {
|
|
|
2330
2981
|
})
|
|
2331
2982
|
});
|
|
2332
2983
|
} catch (err) {
|
|
2333
|
-
|
|
2984
|
+
slog3.warn(`failed to deliver wake summary to ${name}`, logger_default.errorData(err));
|
|
2334
2985
|
}
|
|
2335
2986
|
const flushed = await this.flushQueuedMessages(name);
|
|
2336
2987
|
if (flushed > 0) {
|
|
2337
|
-
|
|
2988
|
+
slog3.info(`flushed ${flushed} queued message(s) for ${name}`);
|
|
2338
2989
|
}
|
|
2339
2990
|
if (!opts?.trigger) {
|
|
2340
2991
|
this.markAwake(name);
|
|
2341
2992
|
}
|
|
2342
|
-
|
|
2993
|
+
slog3.info(`${name} is now awake${opts?.trigger ? " (trigger wake)" : ""}`);
|
|
2343
2994
|
} finally {
|
|
2344
2995
|
this.transitioning.delete(name);
|
|
2345
2996
|
}
|
|
@@ -2394,20 +3045,20 @@ var SleepManager = class {
|
|
|
2394
3045
|
async flushQueuedMessages(name) {
|
|
2395
3046
|
try {
|
|
2396
3047
|
const db = await getDb();
|
|
2397
|
-
const rows = await db.select().from(deliveryQueue).where(
|
|
3048
|
+
const rows = await db.select().from(deliveryQueue).where(and4(eq4(deliveryQueue.mind, name), eq4(deliveryQueue.status, "sleep-queued"))).all();
|
|
2398
3049
|
if (rows.length === 0) return 0;
|
|
2399
|
-
const { deliverMessage: deliverMessage2 } = await import("./message-delivery-
|
|
3050
|
+
const { deliverMessage: deliverMessage2 } = await import("./message-delivery-XMGV3FUM.js");
|
|
2400
3051
|
const delivered = [];
|
|
2401
3052
|
for (const row of rows) {
|
|
2402
3053
|
try {
|
|
2403
3054
|
await deliverMessage2(name, JSON.parse(row.payload));
|
|
2404
3055
|
delivered.push(row.id);
|
|
2405
3056
|
} catch (err) {
|
|
2406
|
-
|
|
3057
|
+
slog3.warn(`failed to flush queued message ${row.id} for ${name}`, logger_default.errorData(err));
|
|
2407
3058
|
}
|
|
2408
3059
|
}
|
|
2409
3060
|
if (delivered.length > 0) {
|
|
2410
|
-
await db.delete(deliveryQueue).where(
|
|
3061
|
+
await db.delete(deliveryQueue).where(inArray2(deliveryQueue.id, delivered));
|
|
2411
3062
|
}
|
|
2412
3063
|
const state = this.states.get(name);
|
|
2413
3064
|
if (state) {
|
|
@@ -2415,7 +3066,7 @@ var SleepManager = class {
|
|
|
2415
3066
|
}
|
|
2416
3067
|
return delivered.length;
|
|
2417
3068
|
} catch (err) {
|
|
2418
|
-
|
|
3069
|
+
slog3.warn(`failed to flush queued messages for ${name}`, logger_default.errorData(err));
|
|
2419
3070
|
return 0;
|
|
2420
3071
|
}
|
|
2421
3072
|
}
|
|
@@ -2443,7 +3094,7 @@ var SleepManager = class {
|
|
|
2443
3094
|
const interval = CronExpressionParser2.parse(config.schedule.wake);
|
|
2444
3095
|
return interval.next().toDate().toISOString();
|
|
2445
3096
|
} catch (err) {
|
|
2446
|
-
|
|
3097
|
+
slog3.warn(`invalid wake cron "${config.schedule.wake}"`, logger_default.errorData(err));
|
|
2447
3098
|
return null;
|
|
2448
3099
|
}
|
|
2449
3100
|
}
|
|
@@ -2460,7 +3111,7 @@ var SleepManager = class {
|
|
|
2460
3111
|
const wakeAt = new Date(state.voluntaryWakeAt);
|
|
2461
3112
|
if (now >= wakeAt) {
|
|
2462
3113
|
this.initiateWake(entry.name).catch(
|
|
2463
|
-
(err) =>
|
|
3114
|
+
(err) => slog3.error(`failed voluntary wake for ${entry.name}`, logger_default.errorData(err))
|
|
2464
3115
|
);
|
|
2465
3116
|
continue;
|
|
2466
3117
|
}
|
|
@@ -2469,7 +3120,7 @@ var SleepManager = class {
|
|
|
2469
3120
|
const wakeAt = new Date(state.scheduledWakeAt);
|
|
2470
3121
|
if (now >= wakeAt) {
|
|
2471
3122
|
this.initiateWake(entry.name).catch(
|
|
2472
|
-
(err) =>
|
|
3123
|
+
(err) => slog3.error(`failed scheduled wake for ${entry.name}`, logger_default.errorData(err))
|
|
2473
3124
|
);
|
|
2474
3125
|
continue;
|
|
2475
3126
|
}
|
|
@@ -2477,7 +3128,7 @@ var SleepManager = class {
|
|
|
2477
3128
|
if (!state?.sleeping && entry.running) {
|
|
2478
3129
|
if (this.shouldSleep(config.schedule.sleep, epochMinute)) {
|
|
2479
3130
|
this.initiateSleep(entry.name).catch(
|
|
2480
|
-
(err) =>
|
|
3131
|
+
(err) => slog3.error(`failed to initiate sleep for ${entry.name}`, logger_default.errorData(err))
|
|
2481
3132
|
);
|
|
2482
3133
|
}
|
|
2483
3134
|
}
|
|
@@ -2490,22 +3141,22 @@ var SleepManager = class {
|
|
|
2490
3141
|
const prevMinute = Math.floor(prev.getTime() / 6e4);
|
|
2491
3142
|
return prevMinute === epochMinute;
|
|
2492
3143
|
} catch (err) {
|
|
2493
|
-
|
|
3144
|
+
slog3.warn(`invalid sleep cron "${cronExpr}"`, logger_default.errorData(err));
|
|
2494
3145
|
return false;
|
|
2495
3146
|
}
|
|
2496
3147
|
}
|
|
2497
3148
|
async waitForIdle(name, timeoutMs) {
|
|
2498
|
-
return new Promise((
|
|
3149
|
+
return new Promise((resolve9) => {
|
|
2499
3150
|
const timeout = setTimeout(() => {
|
|
2500
3151
|
unsub();
|
|
2501
|
-
|
|
3152
|
+
resolve9();
|
|
2502
3153
|
}, timeoutMs);
|
|
2503
3154
|
const unsub = subscribe((event) => {
|
|
2504
3155
|
if (event.mind !== name) return;
|
|
2505
3156
|
if (event.type === "mind_done" || event.type === "mind_idle") {
|
|
2506
3157
|
clearTimeout(timeout);
|
|
2507
3158
|
unsub();
|
|
2508
|
-
|
|
3159
|
+
resolve9();
|
|
2509
3160
|
}
|
|
2510
3161
|
});
|
|
2511
3162
|
});
|
|
@@ -2513,34 +3164,34 @@ var SleepManager = class {
|
|
|
2513
3164
|
async archiveSessions(name) {
|
|
2514
3165
|
const dir = mindDir(name);
|
|
2515
3166
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 16);
|
|
2516
|
-
const sessionsDir =
|
|
3167
|
+
const sessionsDir = resolve8(dir, ".mind", "sessions");
|
|
2517
3168
|
if (existsSync5(sessionsDir)) {
|
|
2518
|
-
const archiveDir =
|
|
3169
|
+
const archiveDir = resolve8(sessionsDir, "archive");
|
|
2519
3170
|
mkdirSync3(archiveDir, { recursive: true });
|
|
2520
3171
|
for (const file of readdirSync2(sessionsDir)) {
|
|
2521
3172
|
if (file === "archive" || !file.endsWith(".json")) continue;
|
|
2522
|
-
const src =
|
|
3173
|
+
const src = resolve8(sessionsDir, file);
|
|
2523
3174
|
const base = file.replace(/\.json$/, "");
|
|
2524
|
-
const dest =
|
|
3175
|
+
const dest = resolve8(archiveDir, `${base}-${timestamp}.json`);
|
|
2525
3176
|
try {
|
|
2526
3177
|
renameSync(src, dest);
|
|
2527
3178
|
} catch (err) {
|
|
2528
|
-
|
|
3179
|
+
slog3.warn(`failed to archive session ${file} for ${name}`, logger_default.errorData(err));
|
|
2529
3180
|
}
|
|
2530
3181
|
}
|
|
2531
3182
|
}
|
|
2532
|
-
const piSessionsDir =
|
|
3183
|
+
const piSessionsDir = resolve8(dir, ".mind", "pi-sessions");
|
|
2533
3184
|
if (existsSync5(piSessionsDir)) {
|
|
2534
|
-
const archiveDir =
|
|
3185
|
+
const archiveDir = resolve8(piSessionsDir, "archive");
|
|
2535
3186
|
mkdirSync3(archiveDir, { recursive: true });
|
|
2536
3187
|
for (const entry of readdirSync2(piSessionsDir, { withFileTypes: true })) {
|
|
2537
3188
|
if (entry.name === "archive" || !entry.isDirectory()) continue;
|
|
2538
|
-
const src =
|
|
2539
|
-
const dest =
|
|
3189
|
+
const src = resolve8(piSessionsDir, entry.name);
|
|
3190
|
+
const dest = resolve8(archiveDir, `${entry.name}-${timestamp}`);
|
|
2540
3191
|
try {
|
|
2541
3192
|
renameSync(src, dest);
|
|
2542
3193
|
} catch (err) {
|
|
2543
|
-
|
|
3194
|
+
slog3.warn(`failed to archive pi-session ${entry.name} for ${name}`, logger_default.errorData(err));
|
|
2544
3195
|
}
|
|
2545
3196
|
}
|
|
2546
3197
|
}
|
|
@@ -2548,18 +3199,18 @@ var SleepManager = class {
|
|
|
2548
3199
|
async buildQueuedSummary(name) {
|
|
2549
3200
|
try {
|
|
2550
3201
|
const db = await getDb();
|
|
2551
|
-
const rows = await db.select({ channel: deliveryQueue.channel }).from(deliveryQueue).where(
|
|
2552
|
-
if (rows.length === 0) return "No messages while you slept.";
|
|
3202
|
+
const rows = await db.select({ channel: deliveryQueue.channel }).from(deliveryQueue).where(and4(eq4(deliveryQueue.mind, name), eq4(deliveryQueue.status, "sleep-queued"))).all();
|
|
3203
|
+
if (rows.length === 0) return "No messages arrived while you slept.";
|
|
2553
3204
|
const channelCounts = /* @__PURE__ */ new Map();
|
|
2554
3205
|
for (const row of rows) {
|
|
2555
3206
|
const ch = row.channel ?? "unknown";
|
|
2556
3207
|
channelCounts.set(ch, (channelCounts.get(ch) ?? 0) + 1);
|
|
2557
3208
|
}
|
|
2558
|
-
const parts = [...channelCounts.entries()].map(([ch,
|
|
2559
|
-
return `${rows.length} message${rows.length === 1 ? "" : "s"} while you slept (${parts.join(", ")}).
|
|
3209
|
+
const parts = [...channelCounts.entries()].map(([ch, count2]) => `${count2} on ${ch}`);
|
|
3210
|
+
return `${rows.length} message${rows.length === 1 ? "" : "s"} arrived while you slept (${parts.join(", ")}). They'll be delivered to your normal channels now.`;
|
|
2560
3211
|
} catch (err) {
|
|
2561
|
-
|
|
2562
|
-
return "
|
|
3212
|
+
slog3.error(`failed to build queued summary for ${name}`, logger_default.errorData(err));
|
|
3213
|
+
return "Unable to check for queued messages \u2014 there may be messages waiting.";
|
|
2563
3214
|
}
|
|
2564
3215
|
}
|
|
2565
3216
|
/**
|
|
@@ -2573,7 +3224,7 @@ var SleepManager = class {
|
|
|
2573
3224
|
} catch {
|
|
2574
3225
|
return;
|
|
2575
3226
|
}
|
|
2576
|
-
|
|
3227
|
+
slog3.warn(`orphan process found on port ${port} after sleep, killing`);
|
|
2577
3228
|
const execFileAsync = promisify(execFile);
|
|
2578
3229
|
try {
|
|
2579
3230
|
const { stdout } = await execFileAsync("lsof", ["-ti", `:${port}`, "-sTCP:LISTEN"]);
|
|
@@ -2584,7 +3235,7 @@ var SleepManager = class {
|
|
|
2584
3235
|
process.kill(pid, "SIGTERM");
|
|
2585
3236
|
} catch (err) {
|
|
2586
3237
|
if (err.code !== "ESRCH") {
|
|
2587
|
-
|
|
3238
|
+
slog3.warn(`failed to kill orphan pid ${pid}`, logger_default.errorData(err));
|
|
2588
3239
|
}
|
|
2589
3240
|
}
|
|
2590
3241
|
}
|
|
@@ -2616,7 +3267,7 @@ var SleepManager = class {
|
|
|
2616
3267
|
}
|
|
2617
3268
|
}
|
|
2618
3269
|
} catch (err) {
|
|
2619
|
-
|
|
3270
|
+
slog3.warn(`failed to kill orphan on port ${port} via /proc`, logger_default.errorData(err));
|
|
2620
3271
|
}
|
|
2621
3272
|
}
|
|
2622
3273
|
await new Promise((r) => setTimeout(r, 1e3));
|
|
@@ -2626,7 +3277,7 @@ var SleepManager = class {
|
|
|
2626
3277
|
if (!state?.sleeping || !state.wokenByTrigger) return;
|
|
2627
3278
|
if (this.transitioning.has(event.mind)) return;
|
|
2628
3279
|
if (event.type === "mind_idle") {
|
|
2629
|
-
|
|
3280
|
+
slog3.info(`${event.mind} going back to sleep after trigger wake`);
|
|
2630
3281
|
state.wokenByTrigger = false;
|
|
2631
3282
|
this.transitioning.add(event.mind);
|
|
2632
3283
|
sleepMind(event.mind).then(() => this.archiveSessions(event.mind)).then(() => {
|
|
@@ -2635,9 +3286,9 @@ var SleepManager = class {
|
|
|
2635
3286
|
const sleepConfig = this.getSleepConfig(event.mind);
|
|
2636
3287
|
state.scheduledWakeAt = this.getNextWakeTime(sleepConfig);
|
|
2637
3288
|
this.saveState();
|
|
2638
|
-
|
|
3289
|
+
slog3.info(`${event.mind} returned to sleep`);
|
|
2639
3290
|
}).catch((err) => {
|
|
2640
|
-
|
|
3291
|
+
slog3.error(`failed to return ${event.mind} to sleep`, logger_default.errorData(err));
|
|
2641
3292
|
}).finally(() => {
|
|
2642
3293
|
this.transitioning.delete(event.mind);
|
|
2643
3294
|
});
|
|
@@ -2661,6 +3312,21 @@ function getSleepManagerIfReady() {
|
|
|
2661
3312
|
export {
|
|
2662
3313
|
initConnectorManager,
|
|
2663
3314
|
getConnectorManager,
|
|
3315
|
+
createUser,
|
|
3316
|
+
verifyUser,
|
|
3317
|
+
getUser,
|
|
3318
|
+
getUserByUsername,
|
|
3319
|
+
listUsers,
|
|
3320
|
+
listPendingUsers,
|
|
3321
|
+
listUsersByType,
|
|
3322
|
+
getOrCreateMindUser,
|
|
3323
|
+
deleteMindUser,
|
|
3324
|
+
changePassword,
|
|
3325
|
+
approveUser,
|
|
3326
|
+
countAdmins,
|
|
3327
|
+
setUserRole,
|
|
3328
|
+
deleteUser,
|
|
3329
|
+
updateUserProfile,
|
|
2664
3330
|
stopAllWatchers,
|
|
2665
3331
|
getCachedSites,
|
|
2666
3332
|
getCachedRecentPages,
|
|
@@ -2677,8 +3343,31 @@ export {
|
|
|
2677
3343
|
getSleepManagerIfReady,
|
|
2678
3344
|
subscribe2 as subscribe,
|
|
2679
3345
|
publish2 as publish,
|
|
3346
|
+
getWebhookUrl,
|
|
3347
|
+
getAuthHeaders,
|
|
3348
|
+
fireWebhook,
|
|
3349
|
+
initWebhook,
|
|
2680
3350
|
subscribe3 as subscribe2,
|
|
2681
3351
|
publish3 as publish2,
|
|
3352
|
+
createConversation,
|
|
3353
|
+
getConversation,
|
|
3354
|
+
getParticipants,
|
|
3355
|
+
isParticipant,
|
|
3356
|
+
listConversationsForUser,
|
|
3357
|
+
isParticipantOrOwner,
|
|
3358
|
+
deleteConversationForUser,
|
|
3359
|
+
addMessage,
|
|
3360
|
+
getMessages,
|
|
3361
|
+
getMessagesPaginated,
|
|
3362
|
+
listConversationsWithParticipants,
|
|
3363
|
+
findDMConversation,
|
|
3364
|
+
createChannel,
|
|
3365
|
+
getChannelByName,
|
|
3366
|
+
listChannels,
|
|
3367
|
+
joinChannel,
|
|
3368
|
+
leaveChannel,
|
|
3369
|
+
getUnreadCounts,
|
|
3370
|
+
markConversationRead,
|
|
2682
3371
|
getTypingMap,
|
|
2683
3372
|
publishTypingForChannels,
|
|
2684
3373
|
extractTextContent,
|