volute 0.25.0 → 0.27.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 +28 -33
- package/dist/{activity-events-4O37J7PD.js → activity-events-BBIEA2F4.js} +2 -3
- package/dist/api.d.ts +886 -220
- package/dist/{archive-4ZQYK5MN.js → archive-UA4BDFXQ.js} +2 -2
- package/dist/{auth-HM2RSPY7.js → auth-D3OT2ARB.js} +3 -3
- package/dist/bridge-FQHZL3MC.js +206 -0
- package/dist/chat-MHJ3L6JQ.js +58 -0
- package/dist/{chunk-PHU4DEAJ.js → chunk-2WPW7OT6.js} +3 -3
- package/dist/{chunk-BOTQ25QT.js → chunk-2YP2TVDT.js} +138 -56
- package/dist/{chunk-DG7TO7EE.js → chunk-4WXYUOAK.js} +5 -7
- package/dist/{chunk-JTDFJWI2.js → chunk-AW7PFDVN.js} +5 -5
- package/dist/{chunk-2767L2RZ.js → chunk-EHYDTZTF.js} +6 -6
- package/dist/{chunk-ZSH4G2P5.js → chunk-GIE6CSN5.js} +17 -17
- package/dist/chunk-H7OZRFJB.js +432 -0
- package/dist/{chunk-ON3FF5JA.js → chunk-HDN7MNGD.js} +3 -3
- package/dist/chunk-IAYBDWVG.js +477 -0
- package/dist/chunk-IKRVFPWU.js +83 -0
- package/dist/{chunk-TRQEV3CD.js → chunk-JGFVMROS.js} +32 -6
- package/dist/{chunk-PHHKNGA3.js → chunk-JKOWNZ4P.js} +3 -3
- package/dist/{chunk-E7GOKNOT.js → chunk-K5NAC55T.js} +1 -1
- package/dist/{chunk-HFCBO2GL.js → chunk-KDGS53OS.js} +4 -4
- package/dist/chunk-KTLFDYPT.js +61 -0
- package/dist/{chunk-3AIBT4TW.js → chunk-LAC664WU.js} +30 -4
- package/dist/{chunk-PMX4EIJK.js → chunk-OQZH4PBB.js} +467 -1054
- package/dist/{chunk-SHSWYG2J.js → chunk-PHSAT7YL.js} +71 -58
- package/dist/chunk-RKQEHRBB.js +177 -0
- package/dist/{chunk-RVKR2R7F.js → chunk-SSI47XP2.js} +10 -2
- package/dist/chunk-T6HKBWXZ.js +23 -0
- package/dist/chunk-USUXRNVD.js +113 -0
- package/dist/{chunk-BFK6SOEJ.js → chunk-VIVMW2H2.js} +4 -4
- package/dist/{chunk-KTJGZ7M7.js → chunk-XBLSAVJF.js} +1 -1
- package/dist/chunk-ZYGKG6VC.js +22 -0
- package/dist/cli.js +51 -32
- package/dist/{cloud-sync-PPBBJDY6.js → cloud-sync-T7M3ESC3.js} +15 -12
- package/dist/connectors/discord-bridge.js +158 -0
- package/dist/connectors/slack-bridge.js +119 -0
- package/dist/connectors/telegram-bridge.js +133 -0
- package/dist/conversations-M2K4253F.js +55 -0
- package/dist/create-D7J73A6H.js +45 -0
- package/dist/{create-VDQJER52.js → create-QWV73WXD.js} +1 -1
- package/dist/{daemon-client-JOVQZ52X.js → daemon-client-I42FK2BF.js} +2 -2
- package/dist/{daemon-restart-FDNOZEAD.js → daemon-restart-M2QTYMEG.js} +7 -6
- package/dist/daemon.js +2247 -1085
- package/dist/db-IC4J52XQ.js +8 -0
- package/dist/{delete-2MRR4JX5.js → delete-4JYGD4VN.js} +1 -1
- package/dist/down-LVBXEULC.js +14 -0
- package/dist/{env-2FPOZK37.js → env-YJMUMFIY.js} +5 -5
- package/dist/{export-IKFAPRAO.js → export-BOJQWBMA.js} +4 -4
- package/dist/{file-KT3UIQM3.js → file-CR36YUPD.js} +4 -4
- package/dist/{history-46WZN5CN.js → history-XKRTAFS2.js} +7 -7
- package/dist/{import-TH26J76F.js → import-SRTQXBGH.js} +4 -4
- package/dist/join-J4QU42DL.js +66 -0
- package/dist/list-R73GENNL.js +40 -0
- package/dist/{log-6SGSSR3D.js → log-ABYNVYJ3.js} +4 -4
- package/dist/login-3QZNR2DF.js +46 -0
- package/dist/{login-UO6AOVEA.js → login-XX37I52P.js} +3 -3
- package/dist/logout-T53VKCPU.js +39 -0
- package/dist/{logout-UKD5LA37.js → logout-W4KOOBIT.js} +2 -2
- package/dist/{logs-HRBONI5I.js → logs-U35JR2KE.js} +7 -7
- package/dist/{merge-KSFJKX6T.js → merge-LNSMSAOF.js} +4 -4
- package/dist/message-delivery-LDXLGERA.js +25 -0
- package/dist/migrate-registry-to-db-XC7T5B7P.js +110 -0
- package/dist/{mind-YVWAHL2A.js → mind-DI33C74K.js} +25 -25
- package/dist/{mind-activity-tracker-NMDDEV3K.js → mind-activity-tracker-EN6XNXPF.js} +3 -4
- package/dist/{mind-manager-4NDNAYAB.js → mind-manager-M6EMUW5I.js} +6 -5
- package/dist/{mind-sleep-GHPTSAYN.js → mind-sleep-BTSWQNAC.js} +4 -4
- package/dist/{mind-wake-BJDJFMDF.js → mind-wake-SBAKIDVP.js} +4 -4
- package/dist/notes-XCER3I7M.js +220 -0
- package/dist/{package-3HF5MXU2.js → package-7WY6VKU3.js} +2 -1
- package/dist/{pages-Y6DRWUOJ.js → pages-6EBS6CBR.js} +2 -2
- package/dist/{publish-EEKTZBHW.js → publish-66UB2ZFY.js} +5 -5
- package/dist/{pull-D32SPFVU.js → pull-XCHJTM5M.js} +4 -4
- package/dist/read-36UFXN3G.js +46 -0
- package/dist/{register-U2UO6TC4.js → register-6B2CXTYM.js} +3 -3
- package/dist/{registry-D2BSQ2X5.js → registry-NDNOOYG4.js} +15 -9
- package/dist/{restart-5BMNV7KU.js → restart-6ESL3NBO.js} +6 -6
- package/dist/sandbox-TGBX22DS.js +19 -0
- package/dist/{schedule-YEFDLVMJ.js → schedule-QTJMFATP.js} +7 -7
- package/dist/{seed-6FEKB3YC.js → seed-SSUCYYDF.js} +2 -2
- package/dist/{send-IISDYFCL.js → send-ZNCJDSRP.js} +28 -36
- package/dist/service-6LIN3F3K.js +122 -0
- package/dist/setup-JG4QAEBV.js +371 -0
- package/dist/setup-JHL5ZEST.js +17 -0
- package/dist/{shared-LWMNTTZN.js → shared-ML5I4Q2A.js} +4 -4
- package/dist/{skill-T3EMR6IR.js → skill-AUAQTSP5.js} +7 -7
- package/dist/skills/dreaming/SKILL.md +68 -0
- package/dist/skills/dreaming/references/INSTALL.md +56 -0
- package/dist/skills/dreaming/scripts/dream.ts +289 -0
- package/dist/skills/dreaming/scripts/wake-context-dreams.sh +30 -0
- package/dist/skills/notes/SKILL.md +34 -0
- package/dist/skills/orientation/SKILL.md +3 -3
- package/dist/skills/volute-mind/SKILL.md +32 -30
- package/dist/sleep-manager-MWYHM5HV.js +29 -0
- package/dist/split-TKJ5OT3P.js +63 -0
- package/dist/{sprout-QJVGJDSH.js → sprout-IJVVKSJ2.js} +6 -7
- package/dist/{start-C7XITZ5O.js → start-EUJSS5R4.js} +4 -4
- package/dist/{status-SIRPLEZC.js → status-77YEPHMW.js} +5 -5
- package/dist/{status-LYS4NUOZ.js → status-7GA4SM4Y.js} +4 -4
- package/dist/{status-LV34BG6G.js → status-THLOBLWG.js} +2 -2
- package/dist/{stop-CVKBSLXY.js → stop-3XAITBBF.js} +6 -6
- package/dist/{tailscale-AJ4VL5XK.js → tailscale-NY5MUMY3.js} +1 -1
- package/dist/up-NKSMXBWR.js +17 -0
- package/dist/{update-7XCZMYBT.js → update-PTSH22AZ.js} +11 -11
- package/dist/{update-check-F5Z3ALXX.js → update-check-64FWC4Y2.js} +2 -2
- package/dist/{upgrade-7RUIXGOO.js → upgrade-HA47CS4C.js} +12 -5
- package/dist/variant-7TGZHOU3.js +41 -0
- package/dist/{version-notify-AZQMC32A.js → version-notify-5Z4MNR6M.js} +26 -28
- package/dist/web-assets/assets/index-CI5wgghI.css +1 -0
- package/dist/web-assets/assets/index-is5CvJWH.js +75 -0
- package/dist/web-assets/favicon.png +0 -0
- package/dist/web-assets/index.html +2 -2
- package/drizzle/0015_notes.sql +23 -0
- package/drizzle/0016_note_reactions_and_replies.sql +15 -0
- package/drizzle/0017_minds.sql +16 -0
- package/drizzle/meta/_journal.json +21 -0
- package/package.json +2 -1
- package/templates/_base/.init/.config/hooks/wake-context.sh +7 -0
- package/templates/_base/.init/.config/prompts.json +2 -2
- package/templates/_base/home/VOLUTE.md +5 -5
- package/templates/_base/src/lib/startup.ts +10 -2
- package/templates/claude/src/agent.ts +51 -1
- package/templates/claude/src/server.ts +1 -0
- package/templates/pi/package.json.tmpl +1 -0
- package/templates/pi/src/agent.ts +48 -1
- package/templates/pi/src/lib/subagents.ts +150 -0
- package/templates/pi/src/server.ts +1 -0
- package/dist/channel-HZOSHGNF.js +0 -260
- package/dist/chunk-33XAVCS4.js +0 -203
- package/dist/chunk-B2CPS4QU.js +0 -283
- package/dist/chunk-NWPT4ASZ.js +0 -89
- package/dist/chunk-SIAG3QMM.js +0 -42
- package/dist/chunk-WSLPZF72.js +0 -173
- package/dist/connector-M6XFI6GM.js +0 -147
- package/dist/connectors/discord.js +0 -177
- package/dist/connectors/slack.js +0 -181
- package/dist/connectors/telegram.js +0 -187
- package/dist/down-674SX2IZ.js +0 -14
- package/dist/message-delivery-XMGV3FUM.js +0 -23
- package/dist/service-FASYWLTC.js +0 -247
- package/dist/setup-BMLM2UTK.js +0 -230
- package/dist/sleep-manager-RKTFZPD3.js +0 -27
- package/dist/up-CJ26KQLN.js +0 -15
- package/dist/variant-UGREB4G5.js +0 -207
- package/dist/web-assets/assets/index-CGPSVu19.js +0 -69
- package/dist/web-assets/assets/index-V_rNDsM8.css +0 -1
|
@@ -1,80 +1,105 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
readSystemsConfig
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-KDGS53OS.js";
|
|
5
5
|
import {
|
|
6
6
|
markIdle
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-K5NAC55T.js";
|
|
8
8
|
import {
|
|
9
|
-
broadcast,
|
|
10
|
-
publish,
|
|
11
|
-
subscribe
|
|
12
|
-
} from "./chunk-BFK6SOEJ.js";
|
|
13
|
-
import {
|
|
14
|
-
RestartTracker,
|
|
15
|
-
RotatingLog,
|
|
16
9
|
clearJsonMap,
|
|
17
10
|
getMindManager,
|
|
18
11
|
getPrompt,
|
|
19
12
|
loadJsonMap,
|
|
20
13
|
saveJsonMap
|
|
21
|
-
} from "./chunk-
|
|
22
|
-
import {
|
|
23
|
-
readVoluteConfig
|
|
24
|
-
} from "./chunk-SIAG3QMM.js";
|
|
14
|
+
} from "./chunk-PHSAT7YL.js";
|
|
25
15
|
import {
|
|
26
|
-
|
|
27
|
-
|
|
16
|
+
addMessage,
|
|
17
|
+
createChannel,
|
|
18
|
+
getChannelByName,
|
|
19
|
+
getParticipants,
|
|
20
|
+
joinChannel,
|
|
21
|
+
publish as publish2
|
|
22
|
+
} from "./chunk-IAYBDWVG.js";
|
|
28
23
|
import {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
getDb,
|
|
34
|
-
messages,
|
|
35
|
-
mindHistory,
|
|
36
|
-
users
|
|
37
|
-
} from "./chunk-33XAVCS4.js";
|
|
24
|
+
broadcast,
|
|
25
|
+
publish,
|
|
26
|
+
subscribe
|
|
27
|
+
} from "./chunk-VIVMW2H2.js";
|
|
38
28
|
import {
|
|
39
29
|
logger_default
|
|
40
30
|
} from "./chunk-YUIHSKR6.js";
|
|
41
31
|
import {
|
|
42
32
|
exec
|
|
43
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-AW7PFDVN.js";
|
|
44
34
|
import {
|
|
45
|
-
|
|
46
|
-
isIsolationEnabled,
|
|
47
|
-
wrapForIsolation
|
|
48
|
-
} from "./chunk-NWPT4ASZ.js";
|
|
49
|
-
import {
|
|
50
|
-
daemonLoopback,
|
|
35
|
+
deliveryQueue,
|
|
51
36
|
findMind,
|
|
52
|
-
|
|
37
|
+
getBaseName,
|
|
38
|
+
getDb,
|
|
53
39
|
mindDir,
|
|
40
|
+
mindHistory,
|
|
54
41
|
readRegistry,
|
|
55
42
|
stateDir,
|
|
56
|
-
|
|
57
|
-
|
|
43
|
+
users,
|
|
44
|
+
voluteHome,
|
|
45
|
+
voluteSystemDir
|
|
46
|
+
} from "./chunk-H7OZRFJB.js";
|
|
58
47
|
|
|
59
48
|
// src/lib/daemon/sleep-manager.ts
|
|
60
|
-
import { execFile } from "child_process";
|
|
49
|
+
import { execFile, spawn as spawnChild } from "child_process";
|
|
61
50
|
import {
|
|
62
51
|
existsSync as existsSync5,
|
|
63
|
-
mkdirSync as
|
|
52
|
+
mkdirSync as mkdirSync4,
|
|
64
53
|
readdirSync as readdirSync2,
|
|
65
54
|
readFileSync as readFileSync5,
|
|
66
55
|
readlinkSync,
|
|
67
56
|
renameSync,
|
|
68
|
-
writeFileSync as
|
|
57
|
+
writeFileSync as writeFileSync4
|
|
69
58
|
} from "fs";
|
|
70
59
|
import { resolve as resolve8 } from "path";
|
|
71
60
|
import { promisify } from "util";
|
|
72
61
|
import { CronExpressionParser as CronExpressionParser2 } from "cron-parser";
|
|
73
|
-
import { and as
|
|
62
|
+
import { and as and3, eq as eq3, inArray as inArray2 } from "drizzle-orm";
|
|
63
|
+
|
|
64
|
+
// src/lib/volute-config.ts
|
|
65
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
66
|
+
import { dirname, resolve } from "path";
|
|
67
|
+
function readJson(path) {
|
|
68
|
+
if (!existsSync(path)) return null;
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
71
|
+
} catch (err) {
|
|
72
|
+
console.error(`[volute-config] failed to parse ${path}: ${err}`);
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function readVoluteConfig(mindDir2) {
|
|
77
|
+
const path = resolve(mindDir2, "home/.config/volute.json");
|
|
78
|
+
const config = readJson(path);
|
|
79
|
+
if (!config) return null;
|
|
80
|
+
const legacy = config;
|
|
81
|
+
if (!config.profile && ("displayName" in config || "description" in config || "avatar" in config)) {
|
|
82
|
+
config.profile = {
|
|
83
|
+
displayName: legacy.displayName,
|
|
84
|
+
description: legacy.description,
|
|
85
|
+
avatar: legacy.avatar
|
|
86
|
+
};
|
|
87
|
+
delete legacy.displayName;
|
|
88
|
+
delete legacy.description;
|
|
89
|
+
delete legacy.avatar;
|
|
90
|
+
}
|
|
91
|
+
return config;
|
|
92
|
+
}
|
|
93
|
+
function writeVoluteConfig(mindDir2, config) {
|
|
94
|
+
const path = resolve(mindDir2, "home/.config/volute.json");
|
|
95
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
96
|
+
writeFileSync(path, `${JSON.stringify(config, null, 2)}
|
|
97
|
+
`);
|
|
98
|
+
}
|
|
74
99
|
|
|
75
100
|
// src/lib/auth.ts
|
|
76
101
|
import { compareSync, hashSync } from "bcryptjs";
|
|
77
|
-
import { and, count, eq } from "drizzle-orm";
|
|
102
|
+
import { and, count, eq, inArray } from "drizzle-orm";
|
|
78
103
|
var userSelectFields = {
|
|
79
104
|
id: users.id,
|
|
80
105
|
username: users.username,
|
|
@@ -132,7 +157,7 @@ async function getOrCreateMindUser(mindName) {
|
|
|
132
157
|
const [result] = await db.insert(users).values({
|
|
133
158
|
username: mindName,
|
|
134
159
|
password_hash: "!mind",
|
|
135
|
-
role: "
|
|
160
|
+
role: "user",
|
|
136
161
|
user_type: "mind"
|
|
137
162
|
}).returning(userSelectFields);
|
|
138
163
|
return result;
|
|
@@ -197,10 +222,14 @@ async function syncMindProfile(mindName, config) {
|
|
|
197
222
|
await db.update(users).set(newProfile).where(eq(users.id, user.id));
|
|
198
223
|
broadcast({ type: "profile_updated", mind: mindName, summary: `${mindName} profile updated` });
|
|
199
224
|
}
|
|
225
|
+
async function migrateMindRoles() {
|
|
226
|
+
const db = await getDb();
|
|
227
|
+
await db.update(users).set({ role: "user" }).where(and(eq(users.user_type, "mind"), inArray(users.role, ["mind", "agent"])));
|
|
228
|
+
}
|
|
200
229
|
|
|
201
230
|
// src/lib/pages-watcher.ts
|
|
202
|
-
import { existsSync, readdirSync, statSync, watch } from "fs";
|
|
203
|
-
import { join, resolve } from "path";
|
|
231
|
+
import { existsSync as existsSync2, readdirSync, statSync, watch } from "fs";
|
|
232
|
+
import { join, resolve as resolve2 } from "path";
|
|
204
233
|
var watchers = /* @__PURE__ */ new Map();
|
|
205
234
|
var homeWatchers = /* @__PURE__ */ new Map();
|
|
206
235
|
var debounceTimers = /* @__PURE__ */ new Map();
|
|
@@ -236,18 +265,18 @@ function startPagesWatcher(mindName, pagesDir) {
|
|
|
236
265
|
}
|
|
237
266
|
function startWatcher(mindName) {
|
|
238
267
|
if (watchers.has(mindName)) return;
|
|
239
|
-
const pagesDir =
|
|
240
|
-
if (
|
|
268
|
+
const pagesDir = resolve2(mindDir(mindName), "home", "public", "pages");
|
|
269
|
+
if (existsSync2(pagesDir)) {
|
|
241
270
|
startPagesWatcher(mindName, pagesDir);
|
|
242
271
|
return;
|
|
243
272
|
}
|
|
244
273
|
if (homeWatchers.has(mindName)) return;
|
|
245
|
-
const publicDir =
|
|
246
|
-
if (!
|
|
274
|
+
const publicDir = resolve2(mindDir(mindName), "home", "public");
|
|
275
|
+
if (!existsSync2(publicDir)) return;
|
|
247
276
|
try {
|
|
248
277
|
const hw = watch(publicDir, (_eventType, filename) => {
|
|
249
278
|
if (filename !== "pages") return;
|
|
250
|
-
if (!
|
|
279
|
+
if (!existsSync2(pagesDir)) return;
|
|
251
280
|
hw.close();
|
|
252
281
|
homeWatchers.delete(mindName);
|
|
253
282
|
invalidateCache();
|
|
@@ -305,7 +334,7 @@ function scanPagesDir(dir, urlPrefix) {
|
|
|
305
334
|
}
|
|
306
335
|
for (const item of items) {
|
|
307
336
|
if (item.startsWith(".")) continue;
|
|
308
|
-
const fullPath =
|
|
337
|
+
const fullPath = resolve2(dir, item);
|
|
309
338
|
try {
|
|
310
339
|
const s = statSync(fullPath);
|
|
311
340
|
if (s.isFile() && item.endsWith(".html")) {
|
|
@@ -315,8 +344,8 @@ function scanPagesDir(dir, urlPrefix) {
|
|
|
315
344
|
url: `${urlPrefix}/${item}`
|
|
316
345
|
});
|
|
317
346
|
} else if (s.isDirectory()) {
|
|
318
|
-
const indexPath =
|
|
319
|
-
if (
|
|
347
|
+
const indexPath = resolve2(fullPath, "index.html");
|
|
348
|
+
if (existsSync2(indexPath)) {
|
|
320
349
|
const indexStat = statSync(indexPath);
|
|
321
350
|
pages.push({
|
|
322
351
|
file: join(item, "index.html"),
|
|
@@ -331,19 +360,19 @@ function scanPagesDir(dir, urlPrefix) {
|
|
|
331
360
|
pages.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
|
|
332
361
|
return pages;
|
|
333
362
|
}
|
|
334
|
-
function buildSites() {
|
|
363
|
+
async function buildSites() {
|
|
335
364
|
const sites = [];
|
|
336
|
-
const systemPagesDir =
|
|
337
|
-
if (
|
|
365
|
+
const systemPagesDir = resolve2(voluteHome(), "shared", "pages");
|
|
366
|
+
if (existsSync2(systemPagesDir)) {
|
|
338
367
|
const systemPages = scanPagesDir(systemPagesDir, "/pages/_system");
|
|
339
368
|
if (systemPages.length > 0) {
|
|
340
369
|
sites.push({ name: "_system", label: "System", pages: systemPages });
|
|
341
370
|
}
|
|
342
371
|
}
|
|
343
|
-
const entries = readRegistry();
|
|
372
|
+
const entries = await readRegistry();
|
|
344
373
|
for (const entry of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
|
|
345
|
-
const pagesDir =
|
|
346
|
-
if (!
|
|
374
|
+
const pagesDir = resolve2(mindDir(entry.name), "home", "public", "pages");
|
|
375
|
+
if (!existsSync2(pagesDir)) continue;
|
|
347
376
|
const mindPages = scanPagesDir(pagesDir, `/pages/${entry.name}`);
|
|
348
377
|
if (mindPages.length > 0) {
|
|
349
378
|
sites.push({ name: entry.name, label: entry.name, pages: mindPages });
|
|
@@ -351,12 +380,12 @@ function buildSites() {
|
|
|
351
380
|
}
|
|
352
381
|
return sites;
|
|
353
382
|
}
|
|
354
|
-
function buildRecentPages() {
|
|
355
|
-
const entries = readRegistry();
|
|
383
|
+
async function buildRecentPages() {
|
|
384
|
+
const entries = await readRegistry();
|
|
356
385
|
const pages = [];
|
|
357
386
|
for (const entry of entries) {
|
|
358
|
-
const pagesDir =
|
|
359
|
-
if (!
|
|
387
|
+
const pagesDir = resolve2(mindDir(entry.name), "home", "public", "pages");
|
|
388
|
+
if (!existsSync2(pagesDir)) continue;
|
|
360
389
|
let items;
|
|
361
390
|
try {
|
|
362
391
|
items = readdirSync(pagesDir);
|
|
@@ -365,7 +394,7 @@ function buildRecentPages() {
|
|
|
365
394
|
}
|
|
366
395
|
for (const item of items) {
|
|
367
396
|
if (item.startsWith(".")) continue;
|
|
368
|
-
const fullPath =
|
|
397
|
+
const fullPath = resolve2(pagesDir, item);
|
|
369
398
|
try {
|
|
370
399
|
const s = statSync(fullPath);
|
|
371
400
|
if (s.isFile() && item.endsWith(".html")) {
|
|
@@ -376,8 +405,8 @@ function buildRecentPages() {
|
|
|
376
405
|
url: `/pages/${entry.name}/${item}`
|
|
377
406
|
});
|
|
378
407
|
} else if (s.isDirectory()) {
|
|
379
|
-
const indexPath =
|
|
380
|
-
if (
|
|
408
|
+
const indexPath = resolve2(fullPath, "index.html");
|
|
409
|
+
if (existsSync2(indexPath)) {
|
|
381
410
|
const indexStat = statSync(indexPath);
|
|
382
411
|
pages.push({
|
|
383
412
|
mind: entry.name,
|
|
@@ -394,360 +423,54 @@ function buildRecentPages() {
|
|
|
394
423
|
pages.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
|
|
395
424
|
return pages.slice(0, 10);
|
|
396
425
|
}
|
|
397
|
-
function getCachedSites() {
|
|
398
|
-
if (!sitesCache) sitesCache = buildSites();
|
|
426
|
+
async function getCachedSites() {
|
|
427
|
+
if (!sitesCache) sitesCache = await buildSites();
|
|
399
428
|
return sitesCache;
|
|
400
429
|
}
|
|
401
|
-
function getCachedRecentPages() {
|
|
402
|
-
if (!recentPagesCache) recentPagesCache = buildRecentPages();
|
|
430
|
+
async function getCachedRecentPages() {
|
|
431
|
+
if (!recentPagesCache) recentPagesCache = await buildRecentPages();
|
|
403
432
|
return recentPagesCache;
|
|
404
433
|
}
|
|
405
434
|
|
|
406
|
-
// src/
|
|
407
|
-
import {
|
|
408
|
-
import {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
description: "Discord server ID (optional, for slash commands)",
|
|
429
|
-
scope: "mind"
|
|
430
|
-
}
|
|
431
|
-
]
|
|
432
|
-
},
|
|
433
|
-
slack: {
|
|
434
|
-
displayName: "Slack",
|
|
435
|
-
description: "Connect to Slack via Socket Mode",
|
|
436
|
-
envVars: [
|
|
437
|
-
{
|
|
438
|
-
name: "SLACK_BOT_TOKEN",
|
|
439
|
-
required: true,
|
|
440
|
-
description: "Slack bot token (xoxb-...)",
|
|
441
|
-
scope: "mind"
|
|
442
|
-
},
|
|
443
|
-
{
|
|
444
|
-
name: "SLACK_APP_TOKEN",
|
|
445
|
-
required: true,
|
|
446
|
-
description: "Slack app-level token (xapp-...) for Socket Mode",
|
|
447
|
-
scope: "mind"
|
|
448
|
-
}
|
|
449
|
-
]
|
|
450
|
-
},
|
|
451
|
-
telegram: {
|
|
452
|
-
displayName: "Telegram",
|
|
453
|
-
description: "Connect to Telegram via long polling",
|
|
454
|
-
envVars: [
|
|
455
|
-
{
|
|
456
|
-
name: "TELEGRAM_BOT_TOKEN",
|
|
457
|
-
required: true,
|
|
458
|
-
description: "Telegram bot token from BotFather",
|
|
459
|
-
scope: "mind"
|
|
460
|
-
}
|
|
461
|
-
]
|
|
462
|
-
}
|
|
463
|
-
};
|
|
464
|
-
function getConnectorDef(type, connectorDir) {
|
|
465
|
-
if (BUILTIN_DEFS[type]) return BUILTIN_DEFS[type];
|
|
466
|
-
if (connectorDir) {
|
|
467
|
-
const jsonPath = resolve2(connectorDir, "connector.json");
|
|
468
|
-
if (existsSync2(jsonPath)) {
|
|
469
|
-
try {
|
|
470
|
-
return JSON.parse(readFileSync(jsonPath, "utf-8"));
|
|
471
|
-
} catch (err) {
|
|
472
|
-
console.warn(`Failed to parse ${jsonPath}: ${err}`);
|
|
473
|
-
return null;
|
|
474
|
-
}
|
|
475
|
-
}
|
|
435
|
+
// src/connectors/sdk.ts
|
|
436
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
437
|
+
import { join as join2, resolve as resolve3 } from "path";
|
|
438
|
+
function splitMessage(text, maxLength) {
|
|
439
|
+
const chunks = [];
|
|
440
|
+
while (text.length > maxLength) {
|
|
441
|
+
let splitAt = text.lastIndexOf("\n", maxLength);
|
|
442
|
+
if (splitAt < maxLength / 2) splitAt = maxLength;
|
|
443
|
+
chunks.push(text.slice(0, splitAt));
|
|
444
|
+
text = text.slice(splitAt).replace(/^\n/, "");
|
|
445
|
+
}
|
|
446
|
+
if (text) chunks.push(text);
|
|
447
|
+
return chunks;
|
|
448
|
+
}
|
|
449
|
+
function readChannelMap(mindName) {
|
|
450
|
+
const filePath = join2(stateDir(mindName), "channels.json");
|
|
451
|
+
if (!existsSync3(filePath)) return {};
|
|
452
|
+
try {
|
|
453
|
+
return JSON.parse(readFileSync2(filePath, "utf-8"));
|
|
454
|
+
} catch (err) {
|
|
455
|
+
console.error(`[sdk] failed to parse ${filePath}:`, err);
|
|
456
|
+
return {};
|
|
476
457
|
}
|
|
477
|
-
return null;
|
|
478
458
|
}
|
|
479
|
-
function
|
|
480
|
-
|
|
459
|
+
function writeChannelEntry(mindName, slug, entry) {
|
|
460
|
+
const dir = stateDir(mindName);
|
|
461
|
+
mkdirSync2(dir, { recursive: true });
|
|
462
|
+
const filePath = join2(dir, "channels.json");
|
|
463
|
+
const map = readChannelMap(mindName);
|
|
464
|
+
map[slug] = entry;
|
|
465
|
+
writeFileSync2(filePath, JSON.stringify(map, null, 2) + "\n");
|
|
481
466
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
let searchDir = dirname(new URL(import.meta.url).pathname);
|
|
487
|
-
for (let i = 0; i < 5; i++) {
|
|
488
|
-
const candidate = resolve3(searchDir, ...segments);
|
|
489
|
-
if (existsSync3(candidate)) return candidate;
|
|
490
|
-
searchDir = dirname(searchDir);
|
|
491
|
-
}
|
|
492
|
-
return null;
|
|
493
|
-
}
|
|
494
|
-
var ConnectorManager = class {
|
|
495
|
-
connectors = /* @__PURE__ */ new Map();
|
|
496
|
-
stopping = /* @__PURE__ */ new Set();
|
|
497
|
-
// "mind:type" keys currently being explicitly stopped
|
|
498
|
-
shuttingDown = false;
|
|
499
|
-
restartTracker = new RestartTracker();
|
|
500
|
-
async startConnectors(mindName, mindDir2, mindPort, daemonPort) {
|
|
501
|
-
const config = readVoluteConfig(mindDir2) ?? {};
|
|
502
|
-
const types = config.connectors ?? [];
|
|
503
|
-
await Promise.all(
|
|
504
|
-
types.map(
|
|
505
|
-
(type) => this.startConnector(mindName, mindDir2, mindPort, type, daemonPort).catch((err) => {
|
|
506
|
-
clog.warn(`failed to start connector ${type} for ${mindName}`, logger_default.errorData(err));
|
|
507
|
-
})
|
|
508
|
-
)
|
|
509
|
-
);
|
|
510
|
-
}
|
|
511
|
-
checkConnectorEnv(type, mindName, mindDir2) {
|
|
512
|
-
const mindConnectorDir = resolve3(mindDir2, "connectors", type);
|
|
513
|
-
const userConnectorDir = resolve3(voluteHome(), "connectors", type);
|
|
514
|
-
const connectorDir = existsSync3(mindConnectorDir) ? mindConnectorDir : existsSync3(userConnectorDir) ? userConnectorDir : void 0;
|
|
515
|
-
const def = getConnectorDef(type, connectorDir);
|
|
516
|
-
if (!def) return null;
|
|
517
|
-
const env = loadMergedEnv(mindName);
|
|
518
|
-
const missing = checkMissingEnvVars(def, env);
|
|
519
|
-
if (missing.length === 0) return null;
|
|
520
|
-
return {
|
|
521
|
-
missing: missing.map((v) => ({ name: v.name, description: v.description })),
|
|
522
|
-
connectorName: def.displayName
|
|
523
|
-
};
|
|
524
|
-
}
|
|
525
|
-
async startConnector(mindName, mindDir2, mindPort, type, daemonPort) {
|
|
526
|
-
const existing = this.connectors.get(mindName)?.get(type);
|
|
527
|
-
if (existing) {
|
|
528
|
-
await new Promise((res) => {
|
|
529
|
-
existing.child.on("exit", () => res());
|
|
530
|
-
try {
|
|
531
|
-
if (existing.child.pid) {
|
|
532
|
-
process.kill(-existing.child.pid, "SIGTERM");
|
|
533
|
-
} else {
|
|
534
|
-
existing.child.kill("SIGTERM");
|
|
535
|
-
}
|
|
536
|
-
} catch {
|
|
537
|
-
res();
|
|
538
|
-
}
|
|
539
|
-
setTimeout(() => {
|
|
540
|
-
try {
|
|
541
|
-
if (existing.child.pid) {
|
|
542
|
-
process.kill(-existing.child.pid, "SIGKILL");
|
|
543
|
-
} else {
|
|
544
|
-
existing.child.kill("SIGKILL");
|
|
545
|
-
}
|
|
546
|
-
} catch {
|
|
547
|
-
}
|
|
548
|
-
res();
|
|
549
|
-
}, 3e3);
|
|
550
|
-
});
|
|
551
|
-
this.connectors.get(mindName)?.delete(type);
|
|
552
|
-
}
|
|
553
|
-
this.killOrphanConnector(mindName, type);
|
|
554
|
-
const mindConnector = resolve3(mindDir2, "connectors", type, "index.ts");
|
|
555
|
-
const userConnector = resolve3(voluteHome(), "connectors", type, "index.ts");
|
|
556
|
-
const builtinConnector = this.resolveBuiltinConnector(type);
|
|
557
|
-
let connectorScript;
|
|
558
|
-
let runtime;
|
|
559
|
-
if (existsSync3(mindConnector)) {
|
|
560
|
-
connectorScript = mindConnector;
|
|
561
|
-
runtime = resolve3(mindDir2, "node_modules", ".bin", "tsx");
|
|
562
|
-
} else if (existsSync3(userConnector)) {
|
|
563
|
-
connectorScript = userConnector;
|
|
564
|
-
runtime = this.resolveVoluteTsx();
|
|
565
|
-
} else if (builtinConnector) {
|
|
566
|
-
connectorScript = builtinConnector;
|
|
567
|
-
runtime = process.execPath;
|
|
568
|
-
} else {
|
|
569
|
-
throw new Error(`No connector code found for type: ${type}`);
|
|
570
|
-
}
|
|
571
|
-
const mindStateDir = stateDir(mindName);
|
|
572
|
-
const logsDir = resolve3(mindStateDir, "logs");
|
|
573
|
-
mkdirSync(logsDir, { recursive: true });
|
|
574
|
-
if (isIsolationEnabled()) {
|
|
575
|
-
try {
|
|
576
|
-
const [base] = mindName.split("@", 2);
|
|
577
|
-
chownMindDir(mindStateDir, base);
|
|
578
|
-
} catch (err) {
|
|
579
|
-
throw new Error(
|
|
580
|
-
`Cannot start connector ${type} for ${mindName}: failed to set ownership on state directory ${mindStateDir}: ${err instanceof Error ? err.message : err}`
|
|
581
|
-
);
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
const logStream = new RotatingLog(resolve3(logsDir, `${type}.log`));
|
|
585
|
-
const mindEnv = loadMergedEnv(mindName);
|
|
586
|
-
const prefix = `${type.toUpperCase()}_`;
|
|
587
|
-
const connectorEnv = Object.fromEntries(
|
|
588
|
-
Object.entries(mindEnv).filter(([k]) => k.startsWith(prefix))
|
|
589
|
-
);
|
|
590
|
-
const spawnOpts = {
|
|
591
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
592
|
-
detached: true,
|
|
593
|
-
env: {
|
|
594
|
-
...process.env,
|
|
595
|
-
VOLUTE_MIND_PORT: String(mindPort),
|
|
596
|
-
VOLUTE_MIND_NAME: mindName,
|
|
597
|
-
VOLUTE_MIND_DIR: mindDir2,
|
|
598
|
-
...daemonPort ? {
|
|
599
|
-
VOLUTE_DAEMON_URL: `http://${daemonLoopback()}:${daemonPort}`,
|
|
600
|
-
VOLUTE_DAEMON_TOKEN: process.env.VOLUTE_DAEMON_TOKEN
|
|
601
|
-
} : {},
|
|
602
|
-
...connectorEnv
|
|
603
|
-
}
|
|
604
|
-
};
|
|
605
|
-
const [spawnCmd, spawnArgs] = wrapForIsolation(runtime, [connectorScript], mindName);
|
|
606
|
-
const child = spawn(spawnCmd, spawnArgs, spawnOpts);
|
|
607
|
-
let lastStderr = "";
|
|
608
|
-
child.stdout?.pipe(logStream);
|
|
609
|
-
child.stderr?.on("data", (chunk) => {
|
|
610
|
-
logStream.write(chunk);
|
|
611
|
-
lastStderr = chunk.toString().trim();
|
|
612
|
-
});
|
|
613
|
-
if (child.pid) {
|
|
614
|
-
this.saveConnectorPid(mindName, type, child.pid);
|
|
615
|
-
}
|
|
616
|
-
if (!this.connectors.has(mindName)) {
|
|
617
|
-
this.connectors.set(mindName, /* @__PURE__ */ new Map());
|
|
618
|
-
}
|
|
619
|
-
this.connectors.get(mindName).set(type, { child, type });
|
|
620
|
-
const stopKey = `${mindName}:${type}`;
|
|
621
|
-
this.restartTracker.reset(stopKey);
|
|
622
|
-
child.on("exit", (code) => {
|
|
623
|
-
const mindMap = this.connectors.get(mindName);
|
|
624
|
-
if (mindMap?.get(type)?.child === child) {
|
|
625
|
-
mindMap.delete(type);
|
|
626
|
-
}
|
|
627
|
-
if (this.shuttingDown) return;
|
|
628
|
-
if (this.stopping.has(stopKey)) return;
|
|
629
|
-
clog.error(`connector ${type} for ${mindName} exited with code ${code}`);
|
|
630
|
-
if (lastStderr) clog.warn(`connector ${type} last output: ${lastStderr}`);
|
|
631
|
-
const { shouldRestart, delay, attempt } = this.restartTracker.recordCrash(stopKey);
|
|
632
|
-
if (!shouldRestart) {
|
|
633
|
-
clog.error(`connector ${type} for ${mindName} crashed ${attempt} times \u2014 giving up`);
|
|
634
|
-
return;
|
|
635
|
-
}
|
|
636
|
-
clog.info(
|
|
637
|
-
`restarting connector ${type} for ${mindName} \u2014 attempt ${attempt}/${this.restartTracker.maxRestartAttempts}, in ${delay}ms`
|
|
638
|
-
);
|
|
639
|
-
setTimeout(() => {
|
|
640
|
-
if (this.shuttingDown || this.stopping.has(stopKey)) return;
|
|
641
|
-
this.startConnector(mindName, mindDir2, mindPort, type, daemonPort).catch((err) => {
|
|
642
|
-
clog.error(`failed to restart connector ${type} for ${mindName}`, logger_default.errorData(err));
|
|
643
|
-
});
|
|
644
|
-
}, delay);
|
|
645
|
-
});
|
|
646
|
-
clog.info(`started connector ${type} for ${mindName}`);
|
|
647
|
-
}
|
|
648
|
-
async stopConnector(mindName, type) {
|
|
649
|
-
const mindMap = this.connectors.get(mindName);
|
|
650
|
-
if (!mindMap) return;
|
|
651
|
-
const tracked = mindMap.get(type);
|
|
652
|
-
if (!tracked) return;
|
|
653
|
-
const stopKey = `${mindName}:${type}`;
|
|
654
|
-
this.stopping.add(stopKey);
|
|
655
|
-
mindMap.delete(type);
|
|
656
|
-
await new Promise((resolve9) => {
|
|
657
|
-
tracked.child.on("exit", () => resolve9());
|
|
658
|
-
try {
|
|
659
|
-
process.kill(-tracked.child.pid, "SIGTERM");
|
|
660
|
-
} catch {
|
|
661
|
-
resolve9();
|
|
662
|
-
}
|
|
663
|
-
setTimeout(() => {
|
|
664
|
-
try {
|
|
665
|
-
process.kill(-tracked.child.pid, "SIGKILL");
|
|
666
|
-
} catch {
|
|
667
|
-
}
|
|
668
|
-
resolve9();
|
|
669
|
-
}, 5e3);
|
|
670
|
-
});
|
|
671
|
-
this.stopping.delete(stopKey);
|
|
672
|
-
this.restartTracker.reset(stopKey);
|
|
673
|
-
try {
|
|
674
|
-
this.removeConnectorPid(mindName, type);
|
|
675
|
-
} catch (err) {
|
|
676
|
-
clog.warn(`failed to remove PID file for ${type}/${mindName}`, logger_default.errorData(err));
|
|
677
|
-
}
|
|
678
|
-
clog.info(`stopped connector ${type} for ${mindName}`);
|
|
679
|
-
}
|
|
680
|
-
async stopConnectors(mindName) {
|
|
681
|
-
const mindMap = this.connectors.get(mindName);
|
|
682
|
-
if (!mindMap) return;
|
|
683
|
-
const types = [...mindMap.keys()];
|
|
684
|
-
await Promise.all(types.map((type) => this.stopConnector(mindName, type)));
|
|
685
|
-
this.connectors.delete(mindName);
|
|
686
|
-
}
|
|
687
|
-
async stopAll() {
|
|
688
|
-
this.shuttingDown = true;
|
|
689
|
-
const minds = [...this.connectors.keys()];
|
|
690
|
-
await Promise.all(minds.map((name) => this.stopConnectors(name)));
|
|
691
|
-
}
|
|
692
|
-
getConnectorStatus(mindName) {
|
|
693
|
-
const mindMap = this.connectors.get(mindName);
|
|
694
|
-
if (!mindMap) return [];
|
|
695
|
-
return [...mindMap.entries()].map(([type, tracked]) => ({
|
|
696
|
-
type,
|
|
697
|
-
running: !tracked.child.killed
|
|
698
|
-
}));
|
|
699
|
-
}
|
|
700
|
-
connectorPidPath(mindName, type) {
|
|
701
|
-
return resolve3(stateDir(mindName), "connectors", `${type}.pid`);
|
|
702
|
-
}
|
|
703
|
-
saveConnectorPid(mindName, type, pid) {
|
|
704
|
-
const pidPath = this.connectorPidPath(mindName, type);
|
|
705
|
-
mkdirSync(dirname(pidPath), { recursive: true });
|
|
706
|
-
writeFileSync(pidPath, String(pid));
|
|
707
|
-
}
|
|
708
|
-
removeConnectorPid(mindName, type) {
|
|
709
|
-
try {
|
|
710
|
-
unlinkSync(this.connectorPidPath(mindName, type));
|
|
711
|
-
} catch {
|
|
712
|
-
}
|
|
467
|
+
function resolveChannelId(mindName, slug) {
|
|
468
|
+
const map = readChannelMap(mindName);
|
|
469
|
+
if (map[slug]) {
|
|
470
|
+
return map[slug].platformId;
|
|
713
471
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
if (!existsSync3(pidPath)) return;
|
|
717
|
-
try {
|
|
718
|
-
const pid = parseInt(readFileSync2(pidPath, "utf-8").trim(), 10);
|
|
719
|
-
if (pid > 0) {
|
|
720
|
-
try {
|
|
721
|
-
process.kill(-pid, "SIGTERM");
|
|
722
|
-
} catch {
|
|
723
|
-
process.kill(pid, "SIGTERM");
|
|
724
|
-
}
|
|
725
|
-
clog.warn(`killed orphan connector ${type} (pid ${pid})`);
|
|
726
|
-
}
|
|
727
|
-
} catch {
|
|
728
|
-
}
|
|
729
|
-
try {
|
|
730
|
-
unlinkSync(pidPath);
|
|
731
|
-
} catch {
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
resolveBuiltinConnector(type) {
|
|
735
|
-
return searchUpwards("connectors", `${type}.js`);
|
|
736
|
-
}
|
|
737
|
-
resolveVoluteTsx() {
|
|
738
|
-
return searchUpwards("node_modules", ".bin", "tsx") ?? "tsx";
|
|
739
|
-
}
|
|
740
|
-
};
|
|
741
|
-
var instance = null;
|
|
742
|
-
function initConnectorManager() {
|
|
743
|
-
if (instance) throw new Error("ConnectorManager already initialized");
|
|
744
|
-
instance = new ConnectorManager();
|
|
745
|
-
return instance;
|
|
746
|
-
}
|
|
747
|
-
function getConnectorManager() {
|
|
748
|
-
if (!instance)
|
|
749
|
-
throw new Error("ConnectorManager not initialized \u2014 call initConnectorManager() first");
|
|
750
|
-
return instance;
|
|
472
|
+
const colonIndex = slug.indexOf(":");
|
|
473
|
+
return colonIndex >= 0 ? slug.slice(colonIndex + 1) : slug;
|
|
751
474
|
}
|
|
752
475
|
|
|
753
476
|
// src/lib/events/mind-events.ts
|
|
@@ -764,7 +487,7 @@ function subscribe2(mind, callback) {
|
|
|
764
487
|
if (set.size === 0) subscribers.delete(mind);
|
|
765
488
|
};
|
|
766
489
|
}
|
|
767
|
-
function
|
|
490
|
+
function publish3(mind, event) {
|
|
768
491
|
const set = subscribers.get(mind);
|
|
769
492
|
if (!set) return;
|
|
770
493
|
for (const cb of set) {
|
|
@@ -781,425 +504,7 @@ function publish2(mind, event) {
|
|
|
781
504
|
// src/lib/delivery/delivery-manager.ts
|
|
782
505
|
import { readFile, realpath } from "fs/promises";
|
|
783
506
|
import { extname, resolve as resolve5 } from "path";
|
|
784
|
-
import { and as
|
|
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
|
-
}
|
|
851
|
-
|
|
852
|
-
// src/lib/events/conversation-events.ts
|
|
853
|
-
var subscribers2 = /* @__PURE__ */ new Map();
|
|
854
|
-
function subscribe3(conversationId, callback) {
|
|
855
|
-
let set = subscribers2.get(conversationId);
|
|
856
|
-
if (!set) {
|
|
857
|
-
set = /* @__PURE__ */ new Set();
|
|
858
|
-
subscribers2.set(conversationId, set);
|
|
859
|
-
}
|
|
860
|
-
set.add(callback);
|
|
861
|
-
return () => {
|
|
862
|
-
set.delete(callback);
|
|
863
|
-
if (set.size === 0) subscribers2.delete(conversationId);
|
|
864
|
-
};
|
|
865
|
-
}
|
|
866
|
-
function publish3(conversationId, event) {
|
|
867
|
-
const set = subscribers2.get(conversationId);
|
|
868
|
-
if (!set) return;
|
|
869
|
-
for (const cb of set) {
|
|
870
|
-
try {
|
|
871
|
-
cb(event);
|
|
872
|
-
} catch (err) {
|
|
873
|
-
console.error("[conversation-events] subscriber threw:", err);
|
|
874
|
-
set.delete(cb);
|
|
875
|
-
if (set.size === 0) subscribers2.delete(conversationId);
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
}
|
|
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
|
-
}
|
|
507
|
+
import { and as and2, eq as eq2, sql } from "drizzle-orm";
|
|
1203
508
|
|
|
1204
509
|
// src/lib/typing.ts
|
|
1205
510
|
var DEFAULT_TTL_MS = 1e4;
|
|
@@ -1259,7 +564,7 @@ var TypingMap = class {
|
|
|
1259
564
|
dispose() {
|
|
1260
565
|
clearInterval(this.sweepTimer);
|
|
1261
566
|
this.channels.clear();
|
|
1262
|
-
if (
|
|
567
|
+
if (instance === this) instance = void 0;
|
|
1263
568
|
}
|
|
1264
569
|
sweep() {
|
|
1265
570
|
const now = Date.now();
|
|
@@ -1275,18 +580,18 @@ var TypingMap = class {
|
|
|
1275
580
|
}
|
|
1276
581
|
}
|
|
1277
582
|
};
|
|
1278
|
-
var
|
|
583
|
+
var instance;
|
|
1279
584
|
function getTypingMap() {
|
|
1280
|
-
if (!
|
|
1281
|
-
|
|
585
|
+
if (!instance) {
|
|
586
|
+
instance = new TypingMap();
|
|
1282
587
|
}
|
|
1283
|
-
return
|
|
588
|
+
return instance;
|
|
1284
589
|
}
|
|
1285
590
|
function publishTypingForChannels(channels, map) {
|
|
1286
591
|
for (const channel of channels) {
|
|
1287
592
|
if (channel.startsWith(VOLUTE_PREFIX)) {
|
|
1288
593
|
const conversationId = channel.slice(VOLUTE_PREFIX.length);
|
|
1289
|
-
|
|
594
|
+
publish2(conversationId, { type: "typing", senders: map.get(channel) });
|
|
1290
595
|
}
|
|
1291
596
|
}
|
|
1292
597
|
}
|
|
@@ -1500,7 +805,7 @@ var DeliveryManager = class {
|
|
|
1500
805
|
* or queued for batching depending on the session's delivery mode.
|
|
1501
806
|
*/
|
|
1502
807
|
async routeAndDeliver(mindName, payload) {
|
|
1503
|
-
const
|
|
808
|
+
const baseName = await getBaseName(mindName);
|
|
1504
809
|
const config = getRoutingConfig(baseName);
|
|
1505
810
|
const meta = {
|
|
1506
811
|
channel: payload.channel,
|
|
@@ -1536,7 +841,7 @@ var DeliveryManager = class {
|
|
|
1536
841
|
const sessionConfig = resolveDeliveryMode(config, sessionName, route.rule);
|
|
1537
842
|
if (sessionConfig.delivery.mode === "batch") {
|
|
1538
843
|
dlog2.debug(`enqueueing batch message for ${mindName}/${sessionName}`);
|
|
1539
|
-
this.enqueueBatch(mindName, sessionName, payload, sessionConfig);
|
|
844
|
+
await this.enqueueBatch(mindName, sessionName, payload, sessionConfig);
|
|
1540
845
|
return { routed: true, session: sessionName, destination: "mind", mode: "batch" };
|
|
1541
846
|
}
|
|
1542
847
|
await this.deliverToMind(mindName, sessionName, payload, sessionConfig);
|
|
@@ -1546,8 +851,8 @@ var DeliveryManager = class {
|
|
|
1546
851
|
* Called when a mind's session emits a "done" event — decrements active count
|
|
1547
852
|
* and may trigger batch flush if session goes idle.
|
|
1548
853
|
*/
|
|
1549
|
-
sessionDone(mindName, session) {
|
|
1550
|
-
const
|
|
854
|
+
async sessionDone(mindName, session) {
|
|
855
|
+
const baseName = await getBaseName(mindName);
|
|
1551
856
|
if (session) {
|
|
1552
857
|
this.decrementActive(baseName, session);
|
|
1553
858
|
} else {
|
|
@@ -1565,7 +870,7 @@ var DeliveryManager = class {
|
|
|
1565
870
|
async restoreFromDb() {
|
|
1566
871
|
try {
|
|
1567
872
|
const db = await getDb();
|
|
1568
|
-
const rows = await db.select().from(deliveryQueue).where(
|
|
873
|
+
const rows = await db.select().from(deliveryQueue).where(eq2(deliveryQueue.status, "pending"));
|
|
1569
874
|
for (const row of rows) {
|
|
1570
875
|
let payload;
|
|
1571
876
|
try {
|
|
@@ -1583,7 +888,7 @@ var DeliveryManager = class {
|
|
|
1583
888
|
this.addToBatchBuffer(row.mind, row.session, payload, sessionConfig);
|
|
1584
889
|
} else {
|
|
1585
890
|
try {
|
|
1586
|
-
await db.delete(deliveryQueue).where(
|
|
891
|
+
await db.delete(deliveryQueue).where(eq2(deliveryQueue.id, row.id));
|
|
1587
892
|
} catch (err) {
|
|
1588
893
|
dlog2.warn(`failed to delete queue row ${row.id} for ${row.mind}`, logger_default.errorData(err));
|
|
1589
894
|
}
|
|
@@ -1604,7 +909,7 @@ var DeliveryManager = class {
|
|
|
1604
909
|
*/
|
|
1605
910
|
async getPending(mindName) {
|
|
1606
911
|
const db = await getDb();
|
|
1607
|
-
const rows = await db.select().from(deliveryQueue).where(
|
|
912
|
+
const rows = await db.select().from(deliveryQueue).where(and2(eq2(deliveryQueue.mind, mindName), eq2(deliveryQueue.status, "gated")));
|
|
1608
913
|
const byChannel = /* @__PURE__ */ new Map();
|
|
1609
914
|
for (const row of rows) {
|
|
1610
915
|
const ch = row.channel ?? "unknown";
|
|
@@ -1642,18 +947,13 @@ var DeliveryManager = class {
|
|
|
1642
947
|
}
|
|
1643
948
|
this.batchBuffers.clear();
|
|
1644
949
|
this.sessionStates.clear();
|
|
1645
|
-
if (
|
|
950
|
+
if (instance2 === this) instance2 = void 0;
|
|
1646
951
|
}
|
|
1647
952
|
// --- Private ---
|
|
1648
|
-
resolvePort(mindName) {
|
|
1649
|
-
const
|
|
1650
|
-
const entry = findMind(baseName);
|
|
953
|
+
async resolvePort(mindName) {
|
|
954
|
+
const entry = await findMind(mindName);
|
|
1651
955
|
if (!entry) return null;
|
|
1652
|
-
|
|
1653
|
-
const variant = findVariant(baseName, variantName);
|
|
1654
|
-
if (!variant) return null;
|
|
1655
|
-
return { baseName, port: variant.port };
|
|
1656
|
-
}
|
|
956
|
+
const baseName = entry.parent ?? mindName;
|
|
1657
957
|
return { baseName, port: entry.port };
|
|
1658
958
|
}
|
|
1659
959
|
async postToMind(port, body) {
|
|
@@ -1679,7 +979,7 @@ var DeliveryManager = class {
|
|
|
1679
979
|
}
|
|
1680
980
|
}
|
|
1681
981
|
async deliverToMind(mindName, session, payload, sessionConfig) {
|
|
1682
|
-
const resolved = this.resolvePort(mindName);
|
|
982
|
+
const resolved = await this.resolvePort(mindName);
|
|
1683
983
|
if (!resolved) {
|
|
1684
984
|
dlog2.warn(`cannot deliver to ${mindName}: mind not found`);
|
|
1685
985
|
return;
|
|
@@ -1716,16 +1016,16 @@ var DeliveryManager = class {
|
|
|
1716
1016
|
publishTypingForChannels(typingMap.deleteSender(baseName), typingMap);
|
|
1717
1017
|
}
|
|
1718
1018
|
}
|
|
1719
|
-
async deliverBatchToMind(mindName, session,
|
|
1720
|
-
const resolved = this.resolvePort(mindName);
|
|
1019
|
+
async deliverBatchToMind(mindName, session, messages, sessionConfig, interruptOverride) {
|
|
1020
|
+
const resolved = await this.resolvePort(mindName);
|
|
1721
1021
|
if (!resolved) {
|
|
1722
1022
|
dlog2.warn(`cannot deliver batch to ${mindName}: mind not found`);
|
|
1723
1023
|
return;
|
|
1724
1024
|
}
|
|
1725
1025
|
const { baseName, port } = resolved;
|
|
1726
1026
|
const enrichedMessages = await Promise.all(
|
|
1727
|
-
|
|
1728
|
-
const isFirst =
|
|
1027
|
+
messages.map(async (msg, i) => {
|
|
1028
|
+
const isFirst = messages.findIndex((m) => m.channel === msg.channel) === i;
|
|
1729
1029
|
if (!isFirst) return msg;
|
|
1730
1030
|
const enrichedPayload = await this.enrichWithProfiles(baseName, session, msg.payload);
|
|
1731
1031
|
return { ...msg, payload: enrichedPayload };
|
|
@@ -1739,7 +1039,7 @@ var DeliveryManager = class {
|
|
|
1739
1039
|
}
|
|
1740
1040
|
const senders = /* @__PURE__ */ new Set();
|
|
1741
1041
|
const channelSet = /* @__PURE__ */ new Set();
|
|
1742
|
-
for (const msg of
|
|
1042
|
+
for (const msg of messages) {
|
|
1743
1043
|
if (msg.sender) senders.add(msg.sender);
|
|
1744
1044
|
if (msg.channel) channelSet.add(msg.channel);
|
|
1745
1045
|
}
|
|
@@ -1749,7 +1049,7 @@ var DeliveryManager = class {
|
|
|
1749
1049
|
if (ch !== "unknown") typingMap.set(ch, baseName, { persistent: true });
|
|
1750
1050
|
}
|
|
1751
1051
|
const seenConvIds = /* @__PURE__ */ new Set();
|
|
1752
|
-
for (const msg of
|
|
1052
|
+
for (const msg of messages) {
|
|
1753
1053
|
if (msg.payload.conversationId && !seenConvIds.has(msg.payload.conversationId)) {
|
|
1754
1054
|
seenConvIds.add(msg.payload.conversationId);
|
|
1755
1055
|
typingMap.set(`volute:${msg.payload.conversationId}`, baseName, { persistent: true });
|
|
@@ -1770,10 +1070,10 @@ var DeliveryManager = class {
|
|
|
1770
1070
|
try {
|
|
1771
1071
|
const db = await getDb();
|
|
1772
1072
|
await db.delete(deliveryQueue).where(
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1073
|
+
and2(
|
|
1074
|
+
eq2(deliveryQueue.mind, baseName),
|
|
1075
|
+
eq2(deliveryQueue.session, session),
|
|
1076
|
+
eq2(deliveryQueue.status, "pending")
|
|
1777
1077
|
)
|
|
1778
1078
|
);
|
|
1779
1079
|
} catch (err) {
|
|
@@ -1789,13 +1089,13 @@ var DeliveryManager = class {
|
|
|
1789
1089
|
publishTypingForChannels(typingMap.deleteSender(baseName), typingMap);
|
|
1790
1090
|
}
|
|
1791
1091
|
}
|
|
1792
|
-
enqueueBatch(mindName, session, payload, sessionConfig) {
|
|
1092
|
+
async enqueueBatch(mindName, session, payload, sessionConfig) {
|
|
1793
1093
|
const delivery = sessionConfig.delivery;
|
|
1794
1094
|
if (delivery.triggers?.length) {
|
|
1795
1095
|
const text = extractTextContent(payload.content);
|
|
1796
1096
|
const lower = text.toLowerCase();
|
|
1797
1097
|
if (delivery.triggers.some((t) => lower.includes(t.toLowerCase()))) {
|
|
1798
|
-
this.flushBatch(mindName, session, [
|
|
1098
|
+
await this.flushBatch(mindName, session, [
|
|
1799
1099
|
{
|
|
1800
1100
|
payload,
|
|
1801
1101
|
channel: payload.channel,
|
|
@@ -1806,14 +1106,14 @@ var DeliveryManager = class {
|
|
|
1806
1106
|
return;
|
|
1807
1107
|
}
|
|
1808
1108
|
}
|
|
1809
|
-
const
|
|
1109
|
+
const baseName = await getBaseName(mindName);
|
|
1810
1110
|
const state = this.sessionStates.get(baseName)?.get(session);
|
|
1811
1111
|
if (state && state.activeCount > 0 && payload.sender && !state.lastDeliverySenders.has(payload.sender) && payload.channel && state.lastDeliveryChannels.has(payload.channel) && Date.now() - state.lastDeliveredAt < delivery.maxWait * 1e3 && Date.now() - state.lastInterruptAt > delivery.debounce * 1e3) {
|
|
1812
1112
|
state.lastInterruptAt = Date.now();
|
|
1813
1113
|
this.persistToQueue(mindName, session, payload).catch((err) => {
|
|
1814
1114
|
dlog2.warn(`failed to persist batch message for ${mindName}/${session}`, logger_default.errorData(err));
|
|
1815
1115
|
});
|
|
1816
|
-
this.flushBatch(
|
|
1116
|
+
await this.flushBatch(
|
|
1817
1117
|
mindName,
|
|
1818
1118
|
session,
|
|
1819
1119
|
[{ payload, channel: payload.channel, sender: payload.sender, createdAt: Date.now() }],
|
|
@@ -1868,42 +1168,42 @@ var DeliveryManager = class {
|
|
|
1868
1168
|
buffer.maxWaitTimer.unref();
|
|
1869
1169
|
}
|
|
1870
1170
|
}
|
|
1871
|
-
flushBatch(mindName, session, extra, interruptOverride) {
|
|
1171
|
+
async flushBatch(mindName, session, extra, interruptOverride) {
|
|
1872
1172
|
const bufferKey = `${mindName}:${session}`;
|
|
1873
1173
|
const buffer = this.batchBuffers.get(bufferKey);
|
|
1874
|
-
const
|
|
1174
|
+
const messages = [];
|
|
1875
1175
|
if (buffer) {
|
|
1876
1176
|
if (buffer.debounceTimer) clearTimeout(buffer.debounceTimer);
|
|
1877
1177
|
if (buffer.maxWaitTimer) clearTimeout(buffer.maxWaitTimer);
|
|
1878
1178
|
buffer.debounceTimer = null;
|
|
1879
1179
|
buffer.maxWaitTimer = null;
|
|
1880
|
-
|
|
1180
|
+
messages.push(...buffer.messages.splice(0));
|
|
1881
1181
|
this.batchBuffers.delete(bufferKey);
|
|
1882
1182
|
}
|
|
1883
|
-
if (extra)
|
|
1884
|
-
if (
|
|
1885
|
-
const
|
|
1183
|
+
if (extra) messages.push(...extra);
|
|
1184
|
+
if (messages.length === 0) return;
|
|
1185
|
+
const baseName = await getBaseName(mindName);
|
|
1886
1186
|
const config = getRoutingConfig(baseName);
|
|
1887
1187
|
const sessionConfig = resolveDeliveryMode(config, session);
|
|
1888
1188
|
dlog2.info(
|
|
1889
|
-
`flushing batch for ${mindName}/${session}: ${
|
|
1189
|
+
`flushing batch for ${mindName}/${session}: ${messages.length} messages${interruptOverride ? " (new-speaker interrupt)" : ""}`
|
|
1890
1190
|
);
|
|
1891
|
-
this.deliverBatchToMind(mindName, session,
|
|
1191
|
+
this.deliverBatchToMind(mindName, session, messages, sessionConfig, interruptOverride).catch(
|
|
1892
1192
|
(err) => {
|
|
1893
1193
|
dlog2.warn(`failed to flush batch for ${mindName}/${session}`, logger_default.errorData(err));
|
|
1894
1194
|
}
|
|
1895
1195
|
);
|
|
1896
1196
|
}
|
|
1897
1197
|
async gateMessage(mindName, session, payload) {
|
|
1898
|
-
const
|
|
1198
|
+
const baseName = await getBaseName(mindName);
|
|
1899
1199
|
await this.persistToQueue(baseName, session, payload, "gated");
|
|
1900
1200
|
try {
|
|
1901
1201
|
const db = await getDb();
|
|
1902
|
-
const count2 = await db.select({ count:
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1202
|
+
const count2 = await db.select({ count: sql`count(*)` }).from(deliveryQueue).where(
|
|
1203
|
+
and2(
|
|
1204
|
+
eq2(deliveryQueue.mind, baseName),
|
|
1205
|
+
eq2(deliveryQueue.channel, payload.channel),
|
|
1206
|
+
eq2(deliveryQueue.status, "gated")
|
|
1907
1207
|
)
|
|
1908
1208
|
);
|
|
1909
1209
|
if ((count2[0]?.count ?? 0) <= 1) {
|
|
@@ -1933,7 +1233,7 @@ var DeliveryManager = class {
|
|
|
1933
1233
|
sender: "system",
|
|
1934
1234
|
content: [{ type: "text", text: notification }]
|
|
1935
1235
|
};
|
|
1936
|
-
const config = getRoutingConfig(mindName
|
|
1236
|
+
const config = getRoutingConfig(await getBaseName(mindName));
|
|
1937
1237
|
const sessionConfig = resolveDeliveryMode(config, "main");
|
|
1938
1238
|
await this.deliverToMind(mindName, "main", invitePayload, {
|
|
1939
1239
|
...sessionConfig,
|
|
@@ -2077,17 +1377,17 @@ var DeliveryManager = class {
|
|
|
2077
1377
|
}
|
|
2078
1378
|
}
|
|
2079
1379
|
};
|
|
2080
|
-
var
|
|
1380
|
+
var instance2;
|
|
2081
1381
|
function initDeliveryManager() {
|
|
2082
|
-
if (
|
|
2083
|
-
|
|
2084
|
-
return
|
|
1382
|
+
if (instance2) throw new Error("DeliveryManager already initialized");
|
|
1383
|
+
instance2 = new DeliveryManager();
|
|
1384
|
+
return instance2;
|
|
2085
1385
|
}
|
|
2086
1386
|
function getDeliveryManager() {
|
|
2087
|
-
if (!
|
|
1387
|
+
if (!instance2) {
|
|
2088
1388
|
throw new Error("DeliveryManager not initialized \u2014 call initDeliveryManager() first");
|
|
2089
1389
|
}
|
|
2090
|
-
return
|
|
1390
|
+
return instance2;
|
|
2091
1391
|
}
|
|
2092
1392
|
|
|
2093
1393
|
// src/lib/delivery/message-delivery.ts
|
|
@@ -2105,7 +1405,7 @@ async function recordInbound(mind, channel, sender, content) {
|
|
|
2105
1405
|
} catch (err) {
|
|
2106
1406
|
dlog3.warn(`failed to persist inbound for ${mind}`, logger_default.errorData(err));
|
|
2107
1407
|
}
|
|
2108
|
-
|
|
1408
|
+
publish3(mind, {
|
|
2109
1409
|
mind,
|
|
2110
1410
|
type: "inbound",
|
|
2111
1411
|
channel,
|
|
@@ -2114,8 +1414,8 @@ async function recordInbound(mind, channel, sender, content) {
|
|
|
2114
1414
|
}
|
|
2115
1415
|
async function deliverMessage(mindName, payload) {
|
|
2116
1416
|
try {
|
|
2117
|
-
const
|
|
2118
|
-
const entry = findMind(baseName);
|
|
1417
|
+
const baseName = await getBaseName(mindName);
|
|
1418
|
+
const entry = await findMind(baseName);
|
|
2119
1419
|
if (!entry) {
|
|
2120
1420
|
dlog3.warn(`cannot deliver to ${mindName}: mind not found`);
|
|
2121
1421
|
return;
|
|
@@ -2139,6 +1439,60 @@ async function deliverMessage(mindName, payload) {
|
|
|
2139
1439
|
}
|
|
2140
1440
|
}
|
|
2141
1441
|
|
|
1442
|
+
// src/lib/system-channel.ts
|
|
1443
|
+
var SYSTEM_CHANNEL_NAME = "system";
|
|
1444
|
+
var cachedChannelId = null;
|
|
1445
|
+
async function ensureSystemChannel() {
|
|
1446
|
+
if (cachedChannelId) return cachedChannelId;
|
|
1447
|
+
const existing = await getChannelByName(SYSTEM_CHANNEL_NAME);
|
|
1448
|
+
if (existing) {
|
|
1449
|
+
cachedChannelId = existing.id;
|
|
1450
|
+
return existing.id;
|
|
1451
|
+
}
|
|
1452
|
+
const conv = await createChannel(SYSTEM_CHANNEL_NAME);
|
|
1453
|
+
cachedChannelId = conv.id;
|
|
1454
|
+
logger_default.info("created #system channel");
|
|
1455
|
+
return conv.id;
|
|
1456
|
+
}
|
|
1457
|
+
async function joinSystemChannel(userId) {
|
|
1458
|
+
const channelId = await ensureSystemChannel();
|
|
1459
|
+
await joinChannel(channelId, userId);
|
|
1460
|
+
}
|
|
1461
|
+
async function joinSystemChannelForMind(mindName) {
|
|
1462
|
+
const user = await getOrCreateMindUser(mindName);
|
|
1463
|
+
await joinSystemChannel(user.id);
|
|
1464
|
+
}
|
|
1465
|
+
async function announceToSystem(text) {
|
|
1466
|
+
const channelId = await ensureSystemChannel();
|
|
1467
|
+
await addMessage(channelId, "system", "system", [{ type: "text", text }]);
|
|
1468
|
+
const participants = await getParticipants(channelId);
|
|
1469
|
+
const mindParticipants = participants.filter((p) => p.userType === "mind");
|
|
1470
|
+
const channel = "volute:#system";
|
|
1471
|
+
for (const mind of mindParticipants) {
|
|
1472
|
+
try {
|
|
1473
|
+
writeChannelEntry(mind.username, channel, {
|
|
1474
|
+
platformId: channelId,
|
|
1475
|
+
platform: "volute",
|
|
1476
|
+
name: SYSTEM_CHANNEL_NAME,
|
|
1477
|
+
type: "group"
|
|
1478
|
+
});
|
|
1479
|
+
} catch (err) {
|
|
1480
|
+
logger_default.warn(`failed to write channel entry for ${mind.username}`, logger_default.errorData(err));
|
|
1481
|
+
}
|
|
1482
|
+
deliverMessage(mind.username, {
|
|
1483
|
+
content: [{ type: "text", text }],
|
|
1484
|
+
channel,
|
|
1485
|
+
conversationId: channelId,
|
|
1486
|
+
sender: "system",
|
|
1487
|
+
participants: participants.map((p) => p.username),
|
|
1488
|
+
participantCount: participants.length,
|
|
1489
|
+
isDM: false
|
|
1490
|
+
}).catch((err) => {
|
|
1491
|
+
logger_default.warn(`failed to deliver system announcement to ${mind.username}`, logger_default.errorData(err));
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
|
|
2142
1496
|
// src/lib/daemon/mail-poller.ts
|
|
2143
1497
|
var mlog = logger_default.child("mail");
|
|
2144
1498
|
function formatEmailContent(email) {
|
|
@@ -2325,7 +1679,7 @@ var MailPoller = class {
|
|
|
2325
1679
|
await this.deliver(mind, { ...email, mind });
|
|
2326
1680
|
}
|
|
2327
1681
|
async deliver(mind, email) {
|
|
2328
|
-
const entry = findMind(mind);
|
|
1682
|
+
const entry = await findMind(mind);
|
|
2329
1683
|
if (!entry || !entry.running) {
|
|
2330
1684
|
mlog.warn(`skipping delivery to ${mind}: ${!entry ? "not found" : "not running"}`);
|
|
2331
1685
|
return;
|
|
@@ -2345,15 +1699,11 @@ var MailPoller = class {
|
|
|
2345
1699
|
}
|
|
2346
1700
|
}
|
|
2347
1701
|
};
|
|
2348
|
-
var
|
|
1702
|
+
var instance3 = null;
|
|
2349
1703
|
function initMailPoller() {
|
|
2350
|
-
if (
|
|
2351
|
-
|
|
2352
|
-
return
|
|
2353
|
-
}
|
|
2354
|
-
function getMailPoller() {
|
|
2355
|
-
if (!instance4) throw new Error("MailPoller not initialized \u2014 call initMailPoller() first");
|
|
2356
|
-
return instance4;
|
|
1704
|
+
if (instance3) throw new Error("MailPoller already initialized");
|
|
1705
|
+
instance3 = new MailPoller();
|
|
1706
|
+
return instance3;
|
|
2357
1707
|
}
|
|
2358
1708
|
async function ensureMailAddress(mindName) {
|
|
2359
1709
|
const config = readSystemsConfig();
|
|
@@ -2379,14 +1729,14 @@ async function ensureMailAddress(mindName) {
|
|
|
2379
1729
|
// src/lib/daemon/scheduler.ts
|
|
2380
1730
|
import { resolve as resolve6 } from "path";
|
|
2381
1731
|
import { CronExpressionParser } from "cron-parser";
|
|
2382
|
-
var
|
|
1732
|
+
var slog = logger_default.child("scheduler");
|
|
2383
1733
|
var Scheduler = class {
|
|
2384
1734
|
schedules = /* @__PURE__ */ new Map();
|
|
2385
1735
|
interval = null;
|
|
2386
1736
|
lastFired = /* @__PURE__ */ new Map();
|
|
2387
1737
|
// "mind:scheduleId" → epoch minute
|
|
2388
1738
|
get statePath() {
|
|
2389
|
-
return resolve6(
|
|
1739
|
+
return resolve6(voluteSystemDir(), "scheduler-state.json");
|
|
2390
1740
|
}
|
|
2391
1741
|
start() {
|
|
2392
1742
|
this.loadState();
|
|
@@ -2445,7 +1795,7 @@ var Scheduler = class {
|
|
|
2445
1795
|
prevMinute = Math.floor(prev.getTime() / 6e4);
|
|
2446
1796
|
cronCache.set(schedule.cron, prevMinute);
|
|
2447
1797
|
} catch (err) {
|
|
2448
|
-
|
|
1798
|
+
slog.warn(`invalid cron "${schedule.cron}" for ${mind}:${schedule.id}`, logger_default.errorData(err));
|
|
2449
1799
|
return false;
|
|
2450
1800
|
}
|
|
2451
1801
|
}
|
|
@@ -2456,6 +1806,18 @@ var Scheduler = class {
|
|
|
2456
1806
|
return false;
|
|
2457
1807
|
}
|
|
2458
1808
|
async fire(mindName, schedule) {
|
|
1809
|
+
const sleepManager = getSleepManagerIfReady();
|
|
1810
|
+
const sleepState = sleepManager?.getState(mindName);
|
|
1811
|
+
if (sleepState?.sleeping) {
|
|
1812
|
+
if (schedule.skipWhenSleeping) {
|
|
1813
|
+
slog.info(`skipped "${schedule.id}" for ${mindName} (sleeping)`);
|
|
1814
|
+
return;
|
|
1815
|
+
}
|
|
1816
|
+
if (sleepState.wokenByTrigger) {
|
|
1817
|
+
slog.info(`skipped "${schedule.id}" for ${mindName} (trigger-woken)`);
|
|
1818
|
+
return;
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
2459
1821
|
try {
|
|
2460
1822
|
let text;
|
|
2461
1823
|
if (schedule.script) {
|
|
@@ -2463,7 +1825,7 @@ var Scheduler = class {
|
|
|
2463
1825
|
try {
|
|
2464
1826
|
const output = await this.runScript(schedule.script, homeDir, mindName);
|
|
2465
1827
|
if (!output.trim()) {
|
|
2466
|
-
|
|
1828
|
+
slog.info(`fired script "${schedule.id}" for ${mindName} (no output)`);
|
|
2467
1829
|
return;
|
|
2468
1830
|
}
|
|
2469
1831
|
text = output;
|
|
@@ -2471,22 +1833,22 @@ var Scheduler = class {
|
|
|
2471
1833
|
const stderr = err.stderr ?? "";
|
|
2472
1834
|
text = `[script error] ${err.message}${stderr ? `
|
|
2473
1835
|
${stderr}` : ""}`;
|
|
2474
|
-
|
|
1836
|
+
slog.warn(`script "${schedule.id}" failed for ${mindName}`, logger_default.errorData(err));
|
|
2475
1837
|
}
|
|
2476
1838
|
} else if (schedule.message) {
|
|
2477
1839
|
text = schedule.message;
|
|
2478
1840
|
} else {
|
|
2479
|
-
|
|
1841
|
+
slog.warn(`schedule "${schedule.id}" for ${mindName} has no message or script`);
|
|
2480
1842
|
return;
|
|
2481
1843
|
}
|
|
2482
1844
|
await this.deliver(mindName, {
|
|
2483
1845
|
content: [{ type: "text", text }],
|
|
2484
|
-
channel: "system:scheduler",
|
|
1846
|
+
channel: schedule.channel ?? "system:scheduler",
|
|
2485
1847
|
sender: schedule.id
|
|
2486
1848
|
});
|
|
2487
|
-
|
|
1849
|
+
slog.info(`fired "${schedule.id}" for ${mindName}`);
|
|
2488
1850
|
} catch (err) {
|
|
2489
|
-
|
|
1851
|
+
slog.warn(`failed to fire "${schedule.id}" for ${mindName}`, logger_default.errorData(err));
|
|
2490
1852
|
}
|
|
2491
1853
|
}
|
|
2492
1854
|
runScript(script, cwd, mindName) {
|
|
@@ -2496,19 +1858,19 @@ ${stderr}` : ""}`;
|
|
|
2496
1858
|
return deliverMessage(mindName, payload);
|
|
2497
1859
|
}
|
|
2498
1860
|
};
|
|
2499
|
-
var
|
|
1861
|
+
var instance4 = null;
|
|
2500
1862
|
function initScheduler() {
|
|
2501
|
-
if (
|
|
2502
|
-
|
|
2503
|
-
return
|
|
1863
|
+
if (instance4) throw new Error("Scheduler already initialized");
|
|
1864
|
+
instance4 = new Scheduler();
|
|
1865
|
+
return instance4;
|
|
2504
1866
|
}
|
|
2505
1867
|
function getScheduler() {
|
|
2506
|
-
if (!
|
|
2507
|
-
return
|
|
1868
|
+
if (!instance4) throw new Error("Scheduler not initialized \u2014 call initScheduler() first");
|
|
1869
|
+
return instance4;
|
|
2508
1870
|
}
|
|
2509
1871
|
|
|
2510
1872
|
// src/lib/daemon/token-budget.ts
|
|
2511
|
-
import { existsSync as existsSync4, mkdirSync as
|
|
1873
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
2512
1874
|
import { resolve as resolve7 } from "path";
|
|
2513
1875
|
var tlog = logger_default.child("token-budget");
|
|
2514
1876
|
var DEFAULT_BUDGET_PERIOD_MINUTES = 60;
|
|
@@ -2583,9 +1945,9 @@ var TokenBudget = class {
|
|
|
2583
1945
|
drain(mind) {
|
|
2584
1946
|
const state = this.budgets.get(mind);
|
|
2585
1947
|
if (!state) return [];
|
|
2586
|
-
const
|
|
1948
|
+
const messages = state.queue;
|
|
2587
1949
|
state.queue = [];
|
|
2588
|
-
return
|
|
1950
|
+
return messages;
|
|
2589
1951
|
}
|
|
2590
1952
|
getUsage(mind) {
|
|
2591
1953
|
const state = this.budgets.get(mind);
|
|
@@ -2632,14 +1994,14 @@ var TokenBudget = class {
|
|
|
2632
1994
|
saveBudgetState(mind, state) {
|
|
2633
1995
|
try {
|
|
2634
1996
|
const dir = stateDir(mind);
|
|
2635
|
-
|
|
1997
|
+
mkdirSync3(dir, { recursive: true });
|
|
2636
1998
|
const data = {
|
|
2637
1999
|
periodStart: state.periodStart,
|
|
2638
2000
|
tokensUsed: state.tokensUsed,
|
|
2639
2001
|
warningInjected: state.warningInjected,
|
|
2640
2002
|
queue: state.queue
|
|
2641
2003
|
};
|
|
2642
|
-
|
|
2004
|
+
writeFileSync3(this.budgetStatePath(mind), `${JSON.stringify(data)}
|
|
2643
2005
|
`);
|
|
2644
2006
|
} catch (err) {
|
|
2645
2007
|
tlog.warn(`failed to save budget state for ${mind}`, logger_default.errorData(err));
|
|
@@ -2666,8 +2028,8 @@ var TokenBudget = class {
|
|
|
2666
2028
|
return null;
|
|
2667
2029
|
}
|
|
2668
2030
|
}
|
|
2669
|
-
async replay(mindName,
|
|
2670
|
-
const summary =
|
|
2031
|
+
async replay(mindName, messages) {
|
|
2032
|
+
const summary = messages.map((m) => {
|
|
2671
2033
|
const from = m.sender ? `[${m.sender}]` : "";
|
|
2672
2034
|
const ch = m.channel ? `(${m.channel})` : "";
|
|
2673
2035
|
return `${from}${ch} ${m.textContent}`;
|
|
@@ -2677,7 +2039,7 @@ var TokenBudget = class {
|
|
|
2677
2039
|
content: [
|
|
2678
2040
|
{
|
|
2679
2041
|
type: "text",
|
|
2680
|
-
text: `[Budget replay] ${
|
|
2042
|
+
text: `[Budget replay] ${messages.length} queued message(s) from the previous budget period:
|
|
2681
2043
|
|
|
2682
2044
|
${summary}`
|
|
2683
2045
|
}
|
|
@@ -2685,40 +2047,38 @@ ${summary}`
|
|
|
2685
2047
|
channel: "system:budget-replay",
|
|
2686
2048
|
sender: "system"
|
|
2687
2049
|
});
|
|
2688
|
-
tlog.info(`replayed ${
|
|
2050
|
+
tlog.info(`replayed ${messages.length} queued message(s) for ${mindName}`);
|
|
2689
2051
|
} catch (err) {
|
|
2690
2052
|
tlog.warn(`failed to replay for ${mindName}`, logger_default.errorData(err));
|
|
2691
2053
|
const state = this.budgets.get(mindName);
|
|
2692
|
-
if (state) state.queue.push(...
|
|
2054
|
+
if (state) state.queue.push(...messages);
|
|
2693
2055
|
}
|
|
2694
2056
|
}
|
|
2695
2057
|
};
|
|
2696
|
-
var
|
|
2058
|
+
var instance5 = null;
|
|
2697
2059
|
function initTokenBudget() {
|
|
2698
|
-
if (
|
|
2699
|
-
|
|
2700
|
-
return
|
|
2060
|
+
if (instance5) throw new Error("TokenBudget already initialized");
|
|
2061
|
+
instance5 = new TokenBudget();
|
|
2062
|
+
return instance5;
|
|
2701
2063
|
}
|
|
2702
2064
|
function getTokenBudget() {
|
|
2703
|
-
if (!
|
|
2704
|
-
return
|
|
2065
|
+
if (!instance5) throw new Error("TokenBudget not initialized \u2014 call initTokenBudget() first");
|
|
2066
|
+
return instance5;
|
|
2705
2067
|
}
|
|
2706
2068
|
|
|
2707
2069
|
// src/lib/daemon/mind-service.ts
|
|
2708
2070
|
async function startMindFull(name) {
|
|
2709
|
-
const
|
|
2071
|
+
const entry = await findMind(name);
|
|
2072
|
+
const baseName = entry?.parent ?? name;
|
|
2710
2073
|
await getMindManager().startMind(name);
|
|
2711
2074
|
publish({
|
|
2712
2075
|
type: "mind_started",
|
|
2713
2076
|
mind: name,
|
|
2714
2077
|
summary: `${name} started`
|
|
2715
2078
|
}).catch((err) => logger_default.error("failed to publish mind_started activity", logger_default.errorData(err)));
|
|
2716
|
-
if (
|
|
2717
|
-
const entry = findMind(baseName);
|
|
2079
|
+
if (entry?.parent) return;
|
|
2718
2080
|
if (!entry || entry.stage === "seed") return;
|
|
2719
2081
|
const dir = mindDir(baseName);
|
|
2720
|
-
const daemonPort = process.env.VOLUTE_DAEMON_PORT ? parseInt(process.env.VOLUTE_DAEMON_PORT, 10) : void 0;
|
|
2721
|
-
await getConnectorManager().startConnectors(baseName, dir, entry.port, daemonPort);
|
|
2722
2082
|
getScheduler().loadSchedules(baseName);
|
|
2723
2083
|
ensureMailAddress(baseName).catch(
|
|
2724
2084
|
(err) => logger_default.error(`failed to ensure mail address for ${baseName}`, logger_default.errorData(err))
|
|
@@ -2729,6 +2089,9 @@ async function startMindFull(name) {
|
|
|
2729
2089
|
(err) => logger_default.error(`failed to sync profile for ${baseName}`, logger_default.errorData(err))
|
|
2730
2090
|
);
|
|
2731
2091
|
}
|
|
2092
|
+
joinSystemChannelForMind(baseName).catch(
|
|
2093
|
+
(err) => logger_default.error(`failed to join #system for ${baseName}`, logger_default.errorData(err))
|
|
2094
|
+
);
|
|
2732
2095
|
if (config?.tokenBudget) {
|
|
2733
2096
|
getTokenBudget().setBudget(
|
|
2734
2097
|
baseName,
|
|
@@ -2756,11 +2119,11 @@ async function wakeMind(name) {
|
|
|
2756
2119
|
}).catch((err) => logger_default.error("failed to publish mind_waking activity", logger_default.errorData(err)));
|
|
2757
2120
|
}
|
|
2758
2121
|
async function stopMindFull(name) {
|
|
2759
|
-
const
|
|
2760
|
-
|
|
2122
|
+
const baseName = await getBaseName(name);
|
|
2123
|
+
const isBase = baseName === name;
|
|
2124
|
+
if (isBase) {
|
|
2761
2125
|
stopWatcher(baseName);
|
|
2762
2126
|
markIdle(baseName);
|
|
2763
|
-
await getConnectorManager().stopConnectors(baseName);
|
|
2764
2127
|
getScheduler().unloadSchedules(baseName);
|
|
2765
2128
|
getTokenBudget().removeBudget(baseName);
|
|
2766
2129
|
}
|
|
@@ -2773,7 +2136,7 @@ async function stopMindFull(name) {
|
|
|
2773
2136
|
}
|
|
2774
2137
|
|
|
2775
2138
|
// src/lib/daemon/sleep-manager.ts
|
|
2776
|
-
var
|
|
2139
|
+
var slog2 = logger_default.child("sleep");
|
|
2777
2140
|
function defaultState() {
|
|
2778
2141
|
return {
|
|
2779
2142
|
sleeping: false,
|
|
@@ -2781,7 +2144,8 @@ function defaultState() {
|
|
|
2781
2144
|
scheduledWakeAt: null,
|
|
2782
2145
|
wokenByTrigger: false,
|
|
2783
2146
|
voluntaryWakeAt: null,
|
|
2784
|
-
queuedMessageCount: 0
|
|
2147
|
+
queuedMessageCount: 0,
|
|
2148
|
+
triggerWakeHistory: []
|
|
2785
2149
|
};
|
|
2786
2150
|
}
|
|
2787
2151
|
function formatCurrentDate() {
|
|
@@ -2809,7 +2173,7 @@ var SleepManager = class {
|
|
|
2809
2173
|
unsubActivity = null;
|
|
2810
2174
|
transitioning = /* @__PURE__ */ new Set();
|
|
2811
2175
|
get statePath() {
|
|
2812
|
-
return resolve8(
|
|
2176
|
+
return resolve8(voluteSystemDir(), "sleep-state.json");
|
|
2813
2177
|
}
|
|
2814
2178
|
start() {
|
|
2815
2179
|
this.loadState();
|
|
@@ -2828,11 +2192,12 @@ var SleepManager = class {
|
|
|
2828
2192
|
if (existsSync5(this.statePath)) {
|
|
2829
2193
|
const data = JSON.parse(readFileSync5(this.statePath, "utf-8"));
|
|
2830
2194
|
for (const [name, state] of Object.entries(data)) {
|
|
2195
|
+
state.triggerWakeHistory ??= [];
|
|
2831
2196
|
this.states.set(name, state);
|
|
2832
2197
|
}
|
|
2833
2198
|
}
|
|
2834
2199
|
} catch (err) {
|
|
2835
|
-
|
|
2200
|
+
slog2.warn("failed to load sleep state", logger_default.errorData(err));
|
|
2836
2201
|
}
|
|
2837
2202
|
}
|
|
2838
2203
|
saveState() {
|
|
@@ -2841,10 +2206,10 @@ var SleepManager = class {
|
|
|
2841
2206
|
if (state.sleeping) data[name] = state;
|
|
2842
2207
|
}
|
|
2843
2208
|
try {
|
|
2844
|
-
|
|
2209
|
+
writeFileSync4(this.statePath, `${JSON.stringify(data, null, 2)}
|
|
2845
2210
|
`);
|
|
2846
2211
|
} catch (err) {
|
|
2847
|
-
|
|
2212
|
+
slog2.error("failed to save sleep state", logger_default.errorData(err));
|
|
2848
2213
|
}
|
|
2849
2214
|
}
|
|
2850
2215
|
// --- Public API ---
|
|
@@ -2857,6 +2222,16 @@ var SleepManager = class {
|
|
|
2857
2222
|
getState(name) {
|
|
2858
2223
|
return this.states.get(name) ?? defaultState();
|
|
2859
2224
|
}
|
|
2225
|
+
/**
|
|
2226
|
+
* Convert a trigger-wake into a full wake. The mind is already running;
|
|
2227
|
+
* this just clears the sleep state so onActivityEvent won't return it to sleep.
|
|
2228
|
+
*/
|
|
2229
|
+
convertTriggerToFullWake(name) {
|
|
2230
|
+
const state = this.states.get(name);
|
|
2231
|
+
if (!state?.sleeping || !state.wokenByTrigger) return;
|
|
2232
|
+
this.markAwake(name);
|
|
2233
|
+
slog2.info(`${name} trigger-wake converted to full wake`);
|
|
2234
|
+
}
|
|
2860
2235
|
getSleepConfig(name) {
|
|
2861
2236
|
const dir = mindDir(name);
|
|
2862
2237
|
const config = readVoluteConfig(dir);
|
|
@@ -2876,7 +2251,7 @@ var SleepManager = class {
|
|
|
2876
2251
|
this.markSleeping(name, opts);
|
|
2877
2252
|
return;
|
|
2878
2253
|
}
|
|
2879
|
-
const entry = findMind(name);
|
|
2254
|
+
const entry = await findMind(name);
|
|
2880
2255
|
if (!entry) return;
|
|
2881
2256
|
const sleepConfig = this.getSleepConfig(name);
|
|
2882
2257
|
const wakeTime = opts?.voluntaryWakeAt ?? this.getNextWakeTime(sleepConfig) ?? "scheduled time";
|
|
@@ -2891,7 +2266,7 @@ var SleepManager = class {
|
|
|
2891
2266
|
content: preSleepMsg
|
|
2892
2267
|
});
|
|
2893
2268
|
} catch (err) {
|
|
2894
|
-
|
|
2269
|
+
slog2.error(`failed to persist pre-sleep message for ${name}`, logger_default.errorData(err));
|
|
2895
2270
|
}
|
|
2896
2271
|
try {
|
|
2897
2272
|
await fetch(`http://127.0.0.1:${entry.port}/message`, {
|
|
@@ -2903,7 +2278,7 @@ var SleepManager = class {
|
|
|
2903
2278
|
})
|
|
2904
2279
|
});
|
|
2905
2280
|
} catch (err) {
|
|
2906
|
-
|
|
2281
|
+
slog2.warn(`failed to send pre-sleep message to ${name}`, logger_default.errorData(err));
|
|
2907
2282
|
}
|
|
2908
2283
|
await this.waitForIdle(name, 12e4);
|
|
2909
2284
|
await new Promise((r) => setTimeout(r, 3e3));
|
|
@@ -2911,7 +2286,7 @@ var SleepManager = class {
|
|
|
2911
2286
|
await this.killOrphanOnPort(entry.port);
|
|
2912
2287
|
await this.archiveSessions(name);
|
|
2913
2288
|
this.markSleeping(name, opts);
|
|
2914
|
-
|
|
2289
|
+
slog2.info(`${name} is now sleeping`);
|
|
2915
2290
|
} finally {
|
|
2916
2291
|
this.transitioning.delete(name);
|
|
2917
2292
|
}
|
|
@@ -2925,72 +2300,76 @@ var SleepManager = class {
|
|
|
2925
2300
|
if (this.transitioning.has(name)) return;
|
|
2926
2301
|
this.transitioning.add(name);
|
|
2927
2302
|
try {
|
|
2928
|
-
const sleepingSince = state.sleepingSince ? new Date(state.sleepingSince) : /* @__PURE__ */ new Date();
|
|
2929
|
-
const now = /* @__PURE__ */ new Date();
|
|
2930
|
-
const duration = formatDuration(sleepingSince, now);
|
|
2931
|
-
const currentDate = formatCurrentDate();
|
|
2932
|
-
const sleepTime = sleepingSince.toLocaleTimeString("en-US", {
|
|
2933
|
-
hour: "numeric",
|
|
2934
|
-
minute: "2-digit"
|
|
2935
|
-
});
|
|
2936
|
-
const queuedSummary = await this.buildQueuedSummary(name);
|
|
2937
2303
|
try {
|
|
2938
2304
|
await wakeMind(name);
|
|
2939
2305
|
} catch (err) {
|
|
2940
|
-
|
|
2306
|
+
slog2.error(`failed to wake ${name}`, logger_default.errorData(err));
|
|
2941
2307
|
return;
|
|
2942
2308
|
}
|
|
2943
|
-
const entry = findMind(name);
|
|
2309
|
+
const entry = await findMind(name);
|
|
2944
2310
|
if (!entry) return;
|
|
2945
|
-
let summaryText;
|
|
2946
2311
|
if (opts?.trigger) {
|
|
2947
2312
|
state.wokenByTrigger = true;
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
sleepTime,
|
|
2952
|
-
duration,
|
|
2953
|
-
queuedSummary
|
|
2313
|
+
state.triggerWakeHistory.push({
|
|
2314
|
+
channel: opts.trigger.channel,
|
|
2315
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2954
2316
|
});
|
|
2317
|
+
this.saveState();
|
|
2955
2318
|
} else {
|
|
2956
|
-
|
|
2319
|
+
const sleepingSince = state.sleepingSince ? new Date(state.sleepingSince) : /* @__PURE__ */ new Date();
|
|
2320
|
+
const now = /* @__PURE__ */ new Date();
|
|
2321
|
+
const duration = formatDuration(sleepingSince, now);
|
|
2322
|
+
const currentDate = formatCurrentDate();
|
|
2323
|
+
const sleepTime = sleepingSince.toLocaleTimeString("en-US", {
|
|
2324
|
+
hour: "numeric",
|
|
2325
|
+
minute: "2-digit"
|
|
2326
|
+
});
|
|
2327
|
+
const triggerWakeSummary = this.buildTriggerWakeSummary(state);
|
|
2328
|
+
const wakeContext = await this.runWakeContextScript(
|
|
2329
|
+
name,
|
|
2330
|
+
state.sleepingSince ?? sleepingSince.toISOString(),
|
|
2331
|
+
duration
|
|
2332
|
+
);
|
|
2333
|
+
const queuedSummary = await this.buildQueuedSummary(name);
|
|
2334
|
+
const sleepActivity = [triggerWakeSummary, wakeContext, queuedSummary].filter(Boolean).join("\n\n");
|
|
2335
|
+
const summaryText = await getPrompt("wake_summary", {
|
|
2957
2336
|
currentDate,
|
|
2958
2337
|
sleepTime,
|
|
2959
2338
|
duration,
|
|
2960
|
-
|
|
2961
|
-
});
|
|
2962
|
-
}
|
|
2963
|
-
try {
|
|
2964
|
-
const db = await getDb();
|
|
2965
|
-
await db.insert(mindHistory).values({
|
|
2966
|
-
mind: name,
|
|
2967
|
-
type: "inbound",
|
|
2968
|
-
channel: "system:sleep",
|
|
2969
|
-
content: summaryText
|
|
2339
|
+
sleepActivity
|
|
2970
2340
|
});
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2341
|
+
try {
|
|
2342
|
+
const db = await getDb();
|
|
2343
|
+
await db.insert(mindHistory).values({
|
|
2344
|
+
mind: name,
|
|
2345
|
+
type: "inbound",
|
|
2346
|
+
channel: "system:sleep",
|
|
2347
|
+
content: summaryText
|
|
2348
|
+
});
|
|
2349
|
+
} catch (err) {
|
|
2350
|
+
slog2.error(`failed to persist wake summary for ${name}`, logger_default.errorData(err));
|
|
2351
|
+
}
|
|
2352
|
+
try {
|
|
2353
|
+
await fetch(`http://127.0.0.1:${entry.port}/message`, {
|
|
2354
|
+
method: "POST",
|
|
2355
|
+
headers: { "Content-Type": "application/json" },
|
|
2356
|
+
body: JSON.stringify({
|
|
2357
|
+
content: [{ type: "text", text: summaryText }],
|
|
2358
|
+
channel: "system:sleep"
|
|
2359
|
+
})
|
|
2360
|
+
});
|
|
2361
|
+
} catch (err) {
|
|
2362
|
+
slog2.warn(`failed to deliver wake summary to ${name}`, logger_default.errorData(err));
|
|
2363
|
+
}
|
|
2985
2364
|
}
|
|
2986
2365
|
const flushed = await this.flushQueuedMessages(name);
|
|
2987
2366
|
if (flushed > 0) {
|
|
2988
|
-
|
|
2367
|
+
slog2.info(`flushed ${flushed} queued message(s) for ${name}`);
|
|
2989
2368
|
}
|
|
2990
2369
|
if (!opts?.trigger) {
|
|
2991
2370
|
this.markAwake(name);
|
|
2992
2371
|
}
|
|
2993
|
-
|
|
2372
|
+
slog2.info(`${name} is now awake${opts?.trigger ? " (trigger wake)" : ""}`);
|
|
2994
2373
|
} finally {
|
|
2995
2374
|
this.transitioning.delete(name);
|
|
2996
2375
|
}
|
|
@@ -3045,16 +2424,16 @@ var SleepManager = class {
|
|
|
3045
2424
|
async flushQueuedMessages(name) {
|
|
3046
2425
|
try {
|
|
3047
2426
|
const db = await getDb();
|
|
3048
|
-
const rows = await db.select().from(deliveryQueue).where(
|
|
2427
|
+
const rows = await db.select().from(deliveryQueue).where(and3(eq3(deliveryQueue.mind, name), eq3(deliveryQueue.status, "sleep-queued"))).all();
|
|
3049
2428
|
if (rows.length === 0) return 0;
|
|
3050
|
-
const { deliverMessage: deliverMessage2 } = await import("./message-delivery-
|
|
2429
|
+
const { deliverMessage: deliverMessage2 } = await import("./message-delivery-LDXLGERA.js");
|
|
3051
2430
|
const delivered = [];
|
|
3052
2431
|
for (const row of rows) {
|
|
3053
2432
|
try {
|
|
3054
2433
|
await deliverMessage2(name, JSON.parse(row.payload));
|
|
3055
2434
|
delivered.push(row.id);
|
|
3056
2435
|
} catch (err) {
|
|
3057
|
-
|
|
2436
|
+
slog2.warn(`failed to flush queued message ${row.id} for ${name}`, logger_default.errorData(err));
|
|
3058
2437
|
}
|
|
3059
2438
|
}
|
|
3060
2439
|
if (delivered.length > 0) {
|
|
@@ -3066,7 +2445,7 @@ var SleepManager = class {
|
|
|
3066
2445
|
}
|
|
3067
2446
|
return delivered.length;
|
|
3068
2447
|
} catch (err) {
|
|
3069
|
-
|
|
2448
|
+
slog2.warn(`failed to flush queued messages for ${name}`, logger_default.errorData(err));
|
|
3070
2449
|
return 0;
|
|
3071
2450
|
}
|
|
3072
2451
|
}
|
|
@@ -3079,7 +2458,8 @@ var SleepManager = class {
|
|
|
3079
2458
|
scheduledWakeAt: this.getNextWakeTime(sleepConfig),
|
|
3080
2459
|
wokenByTrigger: false,
|
|
3081
2460
|
voluntaryWakeAt: opts?.voluntaryWakeAt ?? null,
|
|
3082
|
-
queuedMessageCount: this.states.get(name)?.queuedMessageCount ?? 0
|
|
2461
|
+
queuedMessageCount: this.states.get(name)?.queuedMessageCount ?? 0,
|
|
2462
|
+
triggerWakeHistory: []
|
|
3083
2463
|
};
|
|
3084
2464
|
this.states.set(name, state);
|
|
3085
2465
|
this.saveState();
|
|
@@ -3094,14 +2474,14 @@ var SleepManager = class {
|
|
|
3094
2474
|
const interval = CronExpressionParser2.parse(config.schedule.wake);
|
|
3095
2475
|
return interval.next().toDate().toISOString();
|
|
3096
2476
|
} catch (err) {
|
|
3097
|
-
|
|
2477
|
+
slog2.warn(`invalid wake cron "${config.schedule.wake}"`, logger_default.errorData(err));
|
|
3098
2478
|
return null;
|
|
3099
2479
|
}
|
|
3100
2480
|
}
|
|
3101
|
-
tick() {
|
|
2481
|
+
async tick() {
|
|
3102
2482
|
const now = /* @__PURE__ */ new Date();
|
|
3103
2483
|
const epochMinute = Math.floor(now.getTime() / 6e4);
|
|
3104
|
-
const registry = readRegistry();
|
|
2484
|
+
const registry = await readRegistry();
|
|
3105
2485
|
for (const entry of registry) {
|
|
3106
2486
|
if (!entry.running && !this.isSleeping(entry.name)) continue;
|
|
3107
2487
|
const config = this.getSleepConfig(entry.name);
|
|
@@ -3111,7 +2491,7 @@ var SleepManager = class {
|
|
|
3111
2491
|
const wakeAt = new Date(state.voluntaryWakeAt);
|
|
3112
2492
|
if (now >= wakeAt) {
|
|
3113
2493
|
this.initiateWake(entry.name).catch(
|
|
3114
|
-
(err) =>
|
|
2494
|
+
(err) => slog2.error(`failed voluntary wake for ${entry.name}`, logger_default.errorData(err))
|
|
3115
2495
|
);
|
|
3116
2496
|
continue;
|
|
3117
2497
|
}
|
|
@@ -3120,7 +2500,7 @@ var SleepManager = class {
|
|
|
3120
2500
|
const wakeAt = new Date(state.scheduledWakeAt);
|
|
3121
2501
|
if (now >= wakeAt) {
|
|
3122
2502
|
this.initiateWake(entry.name).catch(
|
|
3123
|
-
(err) =>
|
|
2503
|
+
(err) => slog2.error(`failed scheduled wake for ${entry.name}`, logger_default.errorData(err))
|
|
3124
2504
|
);
|
|
3125
2505
|
continue;
|
|
3126
2506
|
}
|
|
@@ -3128,7 +2508,7 @@ var SleepManager = class {
|
|
|
3128
2508
|
if (!state?.sleeping && entry.running) {
|
|
3129
2509
|
if (this.shouldSleep(config.schedule.sleep, epochMinute)) {
|
|
3130
2510
|
this.initiateSleep(entry.name).catch(
|
|
3131
|
-
(err) =>
|
|
2511
|
+
(err) => slog2.error(`failed to initiate sleep for ${entry.name}`, logger_default.errorData(err))
|
|
3132
2512
|
);
|
|
3133
2513
|
}
|
|
3134
2514
|
}
|
|
@@ -3141,7 +2521,7 @@ var SleepManager = class {
|
|
|
3141
2521
|
const prevMinute = Math.floor(prev.getTime() / 6e4);
|
|
3142
2522
|
return prevMinute === epochMinute;
|
|
3143
2523
|
} catch (err) {
|
|
3144
|
-
|
|
2524
|
+
slog2.warn(`invalid sleep cron "${cronExpr}"`, logger_default.errorData(err));
|
|
3145
2525
|
return false;
|
|
3146
2526
|
}
|
|
3147
2527
|
}
|
|
@@ -3167,7 +2547,7 @@ var SleepManager = class {
|
|
|
3167
2547
|
const sessionsDir = resolve8(dir, ".mind", "sessions");
|
|
3168
2548
|
if (existsSync5(sessionsDir)) {
|
|
3169
2549
|
const archiveDir = resolve8(sessionsDir, "archive");
|
|
3170
|
-
|
|
2550
|
+
mkdirSync4(archiveDir, { recursive: true });
|
|
3171
2551
|
for (const file of readdirSync2(sessionsDir)) {
|
|
3172
2552
|
if (file === "archive" || !file.endsWith(".json")) continue;
|
|
3173
2553
|
const src = resolve8(sessionsDir, file);
|
|
@@ -3176,14 +2556,14 @@ var SleepManager = class {
|
|
|
3176
2556
|
try {
|
|
3177
2557
|
renameSync(src, dest);
|
|
3178
2558
|
} catch (err) {
|
|
3179
|
-
|
|
2559
|
+
slog2.warn(`failed to archive session ${file} for ${name}`, logger_default.errorData(err));
|
|
3180
2560
|
}
|
|
3181
2561
|
}
|
|
3182
2562
|
}
|
|
3183
2563
|
const piSessionsDir = resolve8(dir, ".mind", "pi-sessions");
|
|
3184
2564
|
if (existsSync5(piSessionsDir)) {
|
|
3185
2565
|
const archiveDir = resolve8(piSessionsDir, "archive");
|
|
3186
|
-
|
|
2566
|
+
mkdirSync4(archiveDir, { recursive: true });
|
|
3187
2567
|
for (const entry of readdirSync2(piSessionsDir, { withFileTypes: true })) {
|
|
3188
2568
|
if (entry.name === "archive" || !entry.isDirectory()) continue;
|
|
3189
2569
|
const src = resolve8(piSessionsDir, entry.name);
|
|
@@ -3191,25 +2571,77 @@ var SleepManager = class {
|
|
|
3191
2571
|
try {
|
|
3192
2572
|
renameSync(src, dest);
|
|
3193
2573
|
} catch (err) {
|
|
3194
|
-
|
|
2574
|
+
slog2.warn(`failed to archive pi-session ${entry.name} for ${name}`, logger_default.errorData(err));
|
|
3195
2575
|
}
|
|
3196
2576
|
}
|
|
3197
2577
|
}
|
|
3198
2578
|
}
|
|
2579
|
+
async runWakeContextScript(name, sleepingSince, duration) {
|
|
2580
|
+
const scriptPath = resolve8(mindDir(name), "home", ".config", "hooks", "wake-context.sh");
|
|
2581
|
+
if (!existsSync5(scriptPath)) return "";
|
|
2582
|
+
const input = JSON.stringify({
|
|
2583
|
+
sleepingSince,
|
|
2584
|
+
duration,
|
|
2585
|
+
wakeTime: (/* @__PURE__ */ new Date()).toISOString()
|
|
2586
|
+
});
|
|
2587
|
+
try {
|
|
2588
|
+
const result = await new Promise((resolvePromise, reject) => {
|
|
2589
|
+
const child = spawnChild("bash", [scriptPath], {
|
|
2590
|
+
cwd: mindDir(name),
|
|
2591
|
+
timeout: 5e3,
|
|
2592
|
+
env: { ...process.env, VOLUTE_MIND: name },
|
|
2593
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2594
|
+
});
|
|
2595
|
+
let stdout = "";
|
|
2596
|
+
let stderr = "";
|
|
2597
|
+
child.stdout.on("data", (data) => {
|
|
2598
|
+
stdout += data.toString();
|
|
2599
|
+
});
|
|
2600
|
+
child.stderr.on("data", (data) => {
|
|
2601
|
+
stderr += data.toString();
|
|
2602
|
+
});
|
|
2603
|
+
child.on("close", (code) => {
|
|
2604
|
+
if (code === 0) resolvePromise(stdout);
|
|
2605
|
+
else
|
|
2606
|
+
reject(
|
|
2607
|
+
new Error(
|
|
2608
|
+
`wake-context script exited with code ${code}${stderr ? `: ${stderr.trim()}` : ""}`
|
|
2609
|
+
)
|
|
2610
|
+
);
|
|
2611
|
+
});
|
|
2612
|
+
child.on("error", reject);
|
|
2613
|
+
child.stdin.end(input);
|
|
2614
|
+
});
|
|
2615
|
+
return result.trim();
|
|
2616
|
+
} catch (err) {
|
|
2617
|
+
slog2.warn(`wake-context script failed for ${name}`, logger_default.errorData(err));
|
|
2618
|
+
return "";
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
buildTriggerWakeSummary(state) {
|
|
2622
|
+
const history = state.triggerWakeHistory;
|
|
2623
|
+
if (!history || history.length === 0) return "";
|
|
2624
|
+
const channels = [...new Set(history.map((h) => h.channel))];
|
|
2625
|
+
const times = history.length === 1 ? "once" : `${history.length} times`;
|
|
2626
|
+
return `You were briefly woken ${times} during sleep to handle messages on ${channels.join(", ")} (sessions were archived after each).`;
|
|
2627
|
+
}
|
|
3199
2628
|
async buildQueuedSummary(name) {
|
|
3200
2629
|
try {
|
|
3201
2630
|
const db = await getDb();
|
|
3202
|
-
const rows = await db.select({ channel: deliveryQueue.channel }).from(deliveryQueue).where(
|
|
2631
|
+
const rows = await db.select({ channel: deliveryQueue.channel, sender: deliveryQueue.sender }).from(deliveryQueue).where(and3(eq3(deliveryQueue.mind, name), eq3(deliveryQueue.status, "sleep-queued"))).all();
|
|
3203
2632
|
if (rows.length === 0) return "No messages arrived while you slept.";
|
|
3204
2633
|
const channelCounts = /* @__PURE__ */ new Map();
|
|
2634
|
+
const senders = /* @__PURE__ */ new Set();
|
|
3205
2635
|
for (const row of rows) {
|
|
3206
2636
|
const ch = row.channel ?? "unknown";
|
|
3207
2637
|
channelCounts.set(ch, (channelCounts.get(ch) ?? 0) + 1);
|
|
2638
|
+
if (row.sender) senders.add(row.sender);
|
|
3208
2639
|
}
|
|
3209
2640
|
const parts = [...channelCounts.entries()].map(([ch, count2]) => `${count2} on ${ch}`);
|
|
3210
|
-
|
|
2641
|
+
const senderNote = senders.size > 0 ? ` from ${[...senders].join(", ")}` : "";
|
|
2642
|
+
return `${rows.length} message${rows.length === 1 ? "" : "s"} arrived while you slept${senderNote} (${parts.join(", ")}). They'll be delivered to your normal channels now.`;
|
|
3211
2643
|
} catch (err) {
|
|
3212
|
-
|
|
2644
|
+
slog2.error(`failed to build queued summary for ${name}`, logger_default.errorData(err));
|
|
3213
2645
|
return "Unable to check for queued messages \u2014 there may be messages waiting.";
|
|
3214
2646
|
}
|
|
3215
2647
|
}
|
|
@@ -3224,7 +2656,7 @@ var SleepManager = class {
|
|
|
3224
2656
|
} catch {
|
|
3225
2657
|
return;
|
|
3226
2658
|
}
|
|
3227
|
-
|
|
2659
|
+
slog2.warn(`orphan process found on port ${port} after sleep, killing`);
|
|
3228
2660
|
const execFileAsync = promisify(execFile);
|
|
3229
2661
|
try {
|
|
3230
2662
|
const { stdout } = await execFileAsync("lsof", ["-ti", `:${port}`, "-sTCP:LISTEN"]);
|
|
@@ -3235,7 +2667,7 @@ var SleepManager = class {
|
|
|
3235
2667
|
process.kill(pid, "SIGTERM");
|
|
3236
2668
|
} catch (err) {
|
|
3237
2669
|
if (err.code !== "ESRCH") {
|
|
3238
|
-
|
|
2670
|
+
slog2.warn(`failed to kill orphan pid ${pid}`, logger_default.errorData(err));
|
|
3239
2671
|
}
|
|
3240
2672
|
}
|
|
3241
2673
|
}
|
|
@@ -3267,7 +2699,7 @@ var SleepManager = class {
|
|
|
3267
2699
|
}
|
|
3268
2700
|
}
|
|
3269
2701
|
} catch (err) {
|
|
3270
|
-
|
|
2702
|
+
slog2.warn(`failed to kill orphan on port ${port} via /proc`, logger_default.errorData(err));
|
|
3271
2703
|
}
|
|
3272
2704
|
}
|
|
3273
2705
|
await new Promise((r) => setTimeout(r, 1e3));
|
|
@@ -3277,7 +2709,7 @@ var SleepManager = class {
|
|
|
3277
2709
|
if (!state?.sleeping || !state.wokenByTrigger) return;
|
|
3278
2710
|
if (this.transitioning.has(event.mind)) return;
|
|
3279
2711
|
if (event.type === "mind_idle") {
|
|
3280
|
-
|
|
2712
|
+
slog2.info(`${event.mind} going back to sleep after trigger wake`);
|
|
3281
2713
|
state.wokenByTrigger = false;
|
|
3282
2714
|
this.transitioning.add(event.mind);
|
|
3283
2715
|
sleepMind(event.mind).then(() => this.archiveSessions(event.mind)).then(() => {
|
|
@@ -3286,32 +2718,30 @@ var SleepManager = class {
|
|
|
3286
2718
|
const sleepConfig = this.getSleepConfig(event.mind);
|
|
3287
2719
|
state.scheduledWakeAt = this.getNextWakeTime(sleepConfig);
|
|
3288
2720
|
this.saveState();
|
|
3289
|
-
|
|
2721
|
+
slog2.info(`${event.mind} returned to sleep`);
|
|
3290
2722
|
}).catch((err) => {
|
|
3291
|
-
|
|
2723
|
+
slog2.error(`failed to return ${event.mind} to sleep`, logger_default.errorData(err));
|
|
3292
2724
|
}).finally(() => {
|
|
3293
2725
|
this.transitioning.delete(event.mind);
|
|
3294
2726
|
});
|
|
3295
2727
|
}
|
|
3296
2728
|
}
|
|
3297
2729
|
};
|
|
3298
|
-
var
|
|
2730
|
+
var instance6 = null;
|
|
3299
2731
|
function initSleepManager() {
|
|
3300
|
-
if (
|
|
3301
|
-
|
|
3302
|
-
return
|
|
2732
|
+
if (instance6) throw new Error("SleepManager already initialized");
|
|
2733
|
+
instance6 = new SleepManager();
|
|
2734
|
+
return instance6;
|
|
3303
2735
|
}
|
|
3304
2736
|
function getSleepManager() {
|
|
3305
|
-
if (!
|
|
3306
|
-
return
|
|
2737
|
+
if (!instance6) throw new Error("SleepManager not initialized \u2014 call initSleepManager() first");
|
|
2738
|
+
return instance6;
|
|
3307
2739
|
}
|
|
3308
2740
|
function getSleepManagerIfReady() {
|
|
3309
|
-
return
|
|
2741
|
+
return instance6;
|
|
3310
2742
|
}
|
|
3311
2743
|
|
|
3312
2744
|
export {
|
|
3313
|
-
initConnectorManager,
|
|
3314
|
-
getConnectorManager,
|
|
3315
2745
|
createUser,
|
|
3316
2746
|
verifyUser,
|
|
3317
2747
|
getUser,
|
|
@@ -3327,9 +2757,18 @@ export {
|
|
|
3327
2757
|
setUserRole,
|
|
3328
2758
|
deleteUser,
|
|
3329
2759
|
updateUserProfile,
|
|
2760
|
+
migrateMindRoles,
|
|
2761
|
+
readVoluteConfig,
|
|
2762
|
+
writeVoluteConfig,
|
|
3330
2763
|
stopAllWatchers,
|
|
3331
2764
|
getCachedSites,
|
|
3332
2765
|
getCachedRecentPages,
|
|
2766
|
+
splitMessage,
|
|
2767
|
+
writeChannelEntry,
|
|
2768
|
+
resolveChannelId,
|
|
2769
|
+
ensureSystemChannel,
|
|
2770
|
+
joinSystemChannel,
|
|
2771
|
+
announceToSystem,
|
|
3333
2772
|
initScheduler,
|
|
3334
2773
|
getScheduler,
|
|
3335
2774
|
initTokenBudget,
|
|
@@ -3342,32 +2781,7 @@ export {
|
|
|
3342
2781
|
getSleepManager,
|
|
3343
2782
|
getSleepManagerIfReady,
|
|
3344
2783
|
subscribe2 as subscribe,
|
|
3345
|
-
|
|
3346
|
-
getWebhookUrl,
|
|
3347
|
-
getAuthHeaders,
|
|
3348
|
-
fireWebhook,
|
|
3349
|
-
initWebhook,
|
|
3350
|
-
subscribe3 as subscribe2,
|
|
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,
|
|
2784
|
+
publish3 as publish,
|
|
3371
2785
|
getTypingMap,
|
|
3372
2786
|
publishTypingForChannels,
|
|
3373
2787
|
extractTextContent,
|
|
@@ -3375,6 +2789,5 @@ export {
|
|
|
3375
2789
|
getDeliveryManager,
|
|
3376
2790
|
recordInbound,
|
|
3377
2791
|
deliverMessage,
|
|
3378
|
-
initMailPoller
|
|
3379
|
-
getMailPoller
|
|
2792
|
+
initMailPoller
|
|
3380
2793
|
};
|