volute 0.22.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/{activity-events-3WHHCOBB.js → activity-events-4O37J7PD.js} +2 -2
- package/dist/api.d.ts +306 -15
- package/dist/{channel-BOOMFULW.js → channel-HZOSHGNF.js} +1 -1
- package/dist/{chunk-QIXPN3OO.js → chunk-2767L2RZ.js} +5 -5
- package/dist/{chunk-SGPEZ32F.js → chunk-33XAVCS4.js} +16 -0
- package/dist/{chunk-VT5QODNE.js → chunk-3AIBT4TW.js} +4 -3
- package/dist/{chunk-RK627D57.js → chunk-4TJ72QQ3.js} +2 -2
- package/dist/{chunk-A4S7H6G6.js → chunk-BFK6SOEJ.js} +1 -1
- package/dist/{chunk-HGCDWKSP.js → chunk-E7GOKNOT.js} +1 -1
- package/dist/{chunk-VNVCRVYI.js → chunk-NOBRGACV.js} +7 -7
- package/dist/{chunk-OSFGKF2T.js → chunk-OOW675I3.js} +839 -129
- package/dist/{chunk-TFS25FIM.js → chunk-P3W36ZGD.js} +1 -1
- package/dist/{chunk-JNFRY2WU.js → chunk-TQDITGES.js} +33 -15
- package/dist/{chunk-KFI7TQJ6.js → chunk-TRQEV3CD.js} +9 -5
- package/dist/cli.js +18 -18
- package/dist/{cloud-sync-C6WRYRVR.js → cloud-sync-DIU3OCPV.js} +6 -8
- package/dist/{connector-PYT5UOTZ.js → connector-M6XFI6GM.js} +1 -1
- package/dist/{create-WIDA3M4C.js → create-VDQJER52.js} +1 -1
- package/dist/{daemon-client-ZHCDL4RS.js → daemon-client-JOVQZ52X.js} +1 -1
- package/dist/{daemon-restart-TPQ2XBRZ.js → daemon-restart-YMPEATQH.js} +5 -5
- package/dist/daemon.js +697 -865
- package/dist/{delete-LOIANQGD.js → delete-2MRR4JX5.js} +1 -1
- package/dist/{down-WSUASL5E.js → down-674SX2IZ.js} +2 -2
- package/dist/{env-4PHIHTF4.js → env-2FPOZK37.js} +1 -1
- package/dist/{export-XD6PJBQP.js → export-IKFAPRAO.js} +1 -1
- package/dist/{file-X4L5TTOL.js → file-KT3UIQM3.js} +1 -1
- package/dist/{history-HTEKRNID.js → history-46WZN5CN.js} +1 -1
- package/dist/{import-EAXTHHXL.js → import-FRDPQPJ2.js} +1 -1
- package/dist/{log-SRO5Q6AD.js → log-6SGSSR3D.js} +1 -1
- package/dist/{logs-HNTNNBDW.js → logs-HRBONI5I.js} +1 -1
- package/dist/{merge-B6SYTGI7.js → merge-KSFJKX6T.js} +1 -1
- package/dist/{message-delivery-WUS4K4ZC.js → message-delivery-S7BCNV6Y.js} +9 -7
- package/dist/{mind-BTXR5B3C.js → mind-KPLCRKQA.js} +17 -17
- package/dist/{mind-activity-tracker-PGC3DBJ7.js → mind-activity-tracker-NMDDEV3K.js} +3 -3
- package/dist/{mind-manager-P5OBDUKI.js → mind-manager-ZNRIYEK3.js} +2 -2
- package/dist/{mind-sleep-FWRBIFBS.js → mind-sleep-GHPTSAYN.js} +1 -1
- package/dist/{mind-wake-LJK2YU5X.js → mind-wake-BJDJFMDF.js} +1 -1
- package/dist/{package-A7PEYJI2.js → package-S5YF25XV.js} +1 -1
- package/dist/{pull-GRQAXM2E.js → pull-D32SPFVU.js} +1 -1
- package/dist/{restart-CIDAKGG2.js → restart-5BMNV7KU.js} +1 -1
- package/dist/{schedule-NLR3LZLY.js → schedule-YEFDLVMJ.js} +1 -1
- package/dist/{seed-3H2MRREW.js → seed-6FEKB3YC.js} +1 -1
- package/dist/{send-RP2TA7SG.js → send-IISDYFCL.js} +1 -1
- package/dist/{service-7BFXDI6J.js → service-FASYWLTC.js} +3 -3
- package/dist/{setup-SSIIXQMI.js → setup-BMLM2UTK.js} +1 -1
- package/dist/{shared-2OGT3NSL.js → shared-LWMNTTZN.js} +4 -4
- package/dist/{skill-Q2Y6PQ3L.js → skill-BQOFACEI.js} +1 -1
- package/dist/skills/volute-mind/SKILL.md +71 -1
- package/dist/{sleep-manager-3RWUX2ZR.js → sleep-manager-XXSWQQLE.js} +5 -5
- package/dist/{sprout-UKCYBGHK.js → sprout-CGSW4CF5.js} +3 -3
- package/dist/{start-JR6CUUWF.js → start-C7XITZ5O.js} +1 -1
- package/dist/{status-5XDGYHKP.js → status-LYS4NUOZ.js} +1 -1
- package/dist/{status-H2MKDN6L.js → status-SIRPLEZC.js} +4 -3
- package/dist/{stop-VKPGK25U.js → stop-CVKBSLXY.js} +1 -1
- package/dist/tailscale-AJ4VL5XK.js +49 -0
- package/dist/{up-JKGC7PPF.js → up-OMHACRJL.js} +2 -2
- package/dist/{update-ELC6MEUT.js → update-7XCZMYBT.js} +7 -7
- package/dist/{upgrade-GXW2EQY3.js → upgrade-7RUIXGOO.js} +1 -1
- package/dist/{variant-A4I7PHXS.js → variant-UGREB4G5.js} +4 -4
- package/dist/{version-notify-5FGUAVSF.js → version-notify-SZ75QRGO.js} +5 -5
- package/dist/web-assets/assets/index-Bx9WDoaQ.js +69 -0
- package/dist/web-assets/assets/index-Clz8OhmJ.css +1 -0
- package/dist/web-assets/index.html +2 -2
- package/drizzle/0013_user_profiles.sql +3 -0
- package/drizzle/0014_conversation_reads.sql +7 -0
- package/drizzle/meta/0013_snapshot.json +7 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +1 -1
- package/templates/_base/src/lib/file-handler.ts +6 -1
- package/templates/_base/src/lib/format-prefix.ts +18 -2
- package/templates/_base/src/lib/routing.ts +2 -1
- package/templates/_base/src/lib/types.ts +8 -0
- package/templates/claude/src/lib/stream-consumer.ts +10 -1
- package/templates/pi/src/lib/content.ts +18 -3
- package/templates/pi/src/lib/event-handler.ts +9 -1
- package/dist/chunk-G5KRTU2F.js +0 -76
- package/dist/web-assets/assets/index-DWBxl4LO.js +0 -69
- package/dist/web-assets/assets/index-ZqMd1mx1.css +0 -1
- /package/dist/{pages-YSTRWJR4.js → pages-TWR6U7DS.js} +0 -0
|
@@ -4,11 +4,11 @@ import {
|
|
|
4
4
|
} from "./chunk-HFCBO2GL.js";
|
|
5
5
|
import {
|
|
6
6
|
markIdle
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-E7GOKNOT.js";
|
|
8
8
|
import {
|
|
9
9
|
publish,
|
|
10
10
|
subscribe
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-BFK6SOEJ.js";
|
|
12
12
|
import {
|
|
13
13
|
RestartTracker,
|
|
14
14
|
RotatingLog,
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
getPrompt,
|
|
18
18
|
loadJsonMap,
|
|
19
19
|
saveJsonMap
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-NOBRGACV.js";
|
|
21
21
|
import {
|
|
22
22
|
readVoluteConfig
|
|
23
23
|
} from "./chunk-XLC342FO.js";
|
|
@@ -25,10 +25,15 @@ import {
|
|
|
25
25
|
loadMergedEnv
|
|
26
26
|
} from "./chunk-PHU4DEAJ.js";
|
|
27
27
|
import {
|
|
28
|
+
conversationParticipants,
|
|
29
|
+
conversationReads,
|
|
30
|
+
conversations,
|
|
28
31
|
deliveryQueue,
|
|
29
32
|
getDb,
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
messages,
|
|
34
|
+
mindHistory,
|
|
35
|
+
users
|
|
36
|
+
} from "./chunk-33XAVCS4.js";
|
|
32
37
|
import {
|
|
33
38
|
logger_default
|
|
34
39
|
} from "./chunk-YUIHSKR6.js";
|
|
@@ -61,10 +66,132 @@ import {
|
|
|
61
66
|
renameSync,
|
|
62
67
|
writeFileSync as writeFileSync3
|
|
63
68
|
} from "fs";
|
|
64
|
-
import { resolve as
|
|
69
|
+
import { resolve as resolve8 } from "path";
|
|
65
70
|
import { promisify } from "util";
|
|
66
71
|
import { CronExpressionParser as CronExpressionParser2 } from "cron-parser";
|
|
67
|
-
import { and as
|
|
72
|
+
import { and as and4, eq as eq4, inArray as inArray2 } from "drizzle-orm";
|
|
73
|
+
|
|
74
|
+
// src/lib/auth.ts
|
|
75
|
+
import { compareSync, hashSync } from "bcryptjs";
|
|
76
|
+
import { and, count, eq } from "drizzle-orm";
|
|
77
|
+
var userSelectFields = {
|
|
78
|
+
id: users.id,
|
|
79
|
+
username: users.username,
|
|
80
|
+
role: users.role,
|
|
81
|
+
user_type: users.user_type,
|
|
82
|
+
display_name: users.display_name,
|
|
83
|
+
description: users.description,
|
|
84
|
+
avatar: users.avatar,
|
|
85
|
+
created_at: users.created_at
|
|
86
|
+
};
|
|
87
|
+
async function createUser(username, password) {
|
|
88
|
+
const db = await getDb();
|
|
89
|
+
const hash = hashSync(password, 10);
|
|
90
|
+
const [{ value }] = await db.select({ value: count() }).from(users).where(eq(users.user_type, "brain"));
|
|
91
|
+
const role = value === 0 ? "admin" : "pending";
|
|
92
|
+
const [result] = await db.insert(users).values({ username, password_hash: hash, role }).returning(userSelectFields);
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
async function verifyUser(username, password) {
|
|
96
|
+
const db = await getDb();
|
|
97
|
+
const row = await db.select().from(users).where(eq(users.username, username)).get();
|
|
98
|
+
if (!row) return null;
|
|
99
|
+
if (row.user_type === "mind") return null;
|
|
100
|
+
if (!compareSync(password, row.password_hash)) return null;
|
|
101
|
+
const { password_hash: _, ...user } = row;
|
|
102
|
+
return user;
|
|
103
|
+
}
|
|
104
|
+
async function getUser(id) {
|
|
105
|
+
const db = await getDb();
|
|
106
|
+
const row = await db.select(userSelectFields).from(users).where(eq(users.id, id)).get();
|
|
107
|
+
return row ?? null;
|
|
108
|
+
}
|
|
109
|
+
async function getUserByUsername(username) {
|
|
110
|
+
const db = await getDb();
|
|
111
|
+
const row = await db.select(userSelectFields).from(users).where(eq(users.username, username)).get();
|
|
112
|
+
return row ?? null;
|
|
113
|
+
}
|
|
114
|
+
async function listUsers() {
|
|
115
|
+
const db = await getDb();
|
|
116
|
+
return db.select(userSelectFields).from(users).orderBy(users.created_at).all();
|
|
117
|
+
}
|
|
118
|
+
async function listPendingUsers() {
|
|
119
|
+
const db = await getDb();
|
|
120
|
+
return db.select(userSelectFields).from(users).where(eq(users.role, "pending")).orderBy(users.created_at).all();
|
|
121
|
+
}
|
|
122
|
+
async function listUsersByType(userType) {
|
|
123
|
+
const db = await getDb();
|
|
124
|
+
return db.select(userSelectFields).from(users).where(eq(users.user_type, userType)).orderBy(users.created_at).all();
|
|
125
|
+
}
|
|
126
|
+
async function getOrCreateMindUser(mindName) {
|
|
127
|
+
const db = await getDb();
|
|
128
|
+
const existing = await db.select(userSelectFields).from(users).where(and(eq(users.username, mindName), eq(users.user_type, "mind"))).get();
|
|
129
|
+
if (existing) return existing;
|
|
130
|
+
try {
|
|
131
|
+
const [result] = await db.insert(users).values({
|
|
132
|
+
username: mindName,
|
|
133
|
+
password_hash: "!mind",
|
|
134
|
+
role: "mind",
|
|
135
|
+
user_type: "mind"
|
|
136
|
+
}).returning(userSelectFields);
|
|
137
|
+
return result;
|
|
138
|
+
} catch (err) {
|
|
139
|
+
if (err instanceof Error && err.message.includes("UNIQUE constraint")) {
|
|
140
|
+
const retried = await db.select(userSelectFields).from(users).where(and(eq(users.username, mindName), eq(users.user_type, "mind"))).get();
|
|
141
|
+
if (retried) return retried;
|
|
142
|
+
}
|
|
143
|
+
throw err;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async function deleteMindUser(mindName) {
|
|
147
|
+
const db = await getDb();
|
|
148
|
+
await db.delete(users).where(and(eq(users.username, mindName), eq(users.user_type, "mind")));
|
|
149
|
+
}
|
|
150
|
+
async function changePassword(userId, currentPassword, newPassword) {
|
|
151
|
+
const db = await getDb();
|
|
152
|
+
const row = await db.select().from(users).where(eq(users.id, userId)).get();
|
|
153
|
+
if (!row) return false;
|
|
154
|
+
if (!compareSync(currentPassword, row.password_hash)) return false;
|
|
155
|
+
const hash = hashSync(newPassword, 10);
|
|
156
|
+
await db.update(users).set({ password_hash: hash }).where(eq(users.id, userId));
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
async function approveUser(id) {
|
|
160
|
+
const db = await getDb();
|
|
161
|
+
await db.update(users).set({ role: "user" }).where(and(eq(users.id, id), eq(users.role, "pending")));
|
|
162
|
+
}
|
|
163
|
+
async function countAdmins() {
|
|
164
|
+
const db = await getDb();
|
|
165
|
+
const [{ value }] = await db.select({ value: count() }).from(users).where(eq(users.role, "admin"));
|
|
166
|
+
return value;
|
|
167
|
+
}
|
|
168
|
+
async function setUserRole(id, role) {
|
|
169
|
+
const db = await getDb();
|
|
170
|
+
const target = await db.select({ id: users.id }).from(users).where(eq(users.id, id)).get();
|
|
171
|
+
if (!target) throw new Error("User not found");
|
|
172
|
+
await db.update(users).set({ role }).where(eq(users.id, id));
|
|
173
|
+
}
|
|
174
|
+
async function deleteUser(id) {
|
|
175
|
+
const db = await getDb();
|
|
176
|
+
const target = await db.select({ id: users.id }).from(users).where(and(eq(users.id, id), eq(users.user_type, "brain"))).get();
|
|
177
|
+
if (!target) throw new Error("User not found");
|
|
178
|
+
await db.delete(users).where(and(eq(users.id, id), eq(users.user_type, "brain")));
|
|
179
|
+
}
|
|
180
|
+
async function updateUserProfile(userId, profile) {
|
|
181
|
+
const db = await getDb();
|
|
182
|
+
const target = await db.select({ id: users.id }).from(users).where(eq(users.id, userId)).get();
|
|
183
|
+
if (!target) throw new Error("User not found");
|
|
184
|
+
await db.update(users).set(profile).where(eq(users.id, userId));
|
|
185
|
+
}
|
|
186
|
+
async function syncMindProfile(mindName, config) {
|
|
187
|
+
const user = await getOrCreateMindUser(mindName);
|
|
188
|
+
const db = await getDb();
|
|
189
|
+
await db.update(users).set({
|
|
190
|
+
display_name: config.displayName ?? null,
|
|
191
|
+
description: config.description ?? null,
|
|
192
|
+
avatar: config.avatar ?? null
|
|
193
|
+
}).where(eq(users.id, user.id));
|
|
194
|
+
}
|
|
68
195
|
|
|
69
196
|
// src/lib/pages-watcher.ts
|
|
70
197
|
import { existsSync, readdirSync, statSync, watch } from "fs";
|
|
@@ -521,19 +648,19 @@ var ConnectorManager = class {
|
|
|
521
648
|
const stopKey = `${mindName}:${type}`;
|
|
522
649
|
this.stopping.add(stopKey);
|
|
523
650
|
mindMap.delete(type);
|
|
524
|
-
await new Promise((
|
|
525
|
-
tracked.child.on("exit", () =>
|
|
651
|
+
await new Promise((resolve9) => {
|
|
652
|
+
tracked.child.on("exit", () => resolve9());
|
|
526
653
|
try {
|
|
527
654
|
process.kill(-tracked.child.pid, "SIGTERM");
|
|
528
655
|
} catch {
|
|
529
|
-
|
|
656
|
+
resolve9();
|
|
530
657
|
}
|
|
531
658
|
setTimeout(() => {
|
|
532
659
|
try {
|
|
533
660
|
process.kill(-tracked.child.pid, "SIGKILL");
|
|
534
661
|
} catch {
|
|
535
662
|
}
|
|
536
|
-
|
|
663
|
+
resolve9();
|
|
537
664
|
}, 5e3);
|
|
538
665
|
});
|
|
539
666
|
this.stopping.delete(stopKey);
|
|
@@ -618,25 +745,121 @@ function getConnectorManager() {
|
|
|
618
745
|
return instance;
|
|
619
746
|
}
|
|
620
747
|
|
|
748
|
+
// src/lib/events/mind-events.ts
|
|
749
|
+
var subscribers = /* @__PURE__ */ new Map();
|
|
750
|
+
function subscribe2(mind, callback) {
|
|
751
|
+
let set = subscribers.get(mind);
|
|
752
|
+
if (!set) {
|
|
753
|
+
set = /* @__PURE__ */ new Set();
|
|
754
|
+
subscribers.set(mind, set);
|
|
755
|
+
}
|
|
756
|
+
set.add(callback);
|
|
757
|
+
return () => {
|
|
758
|
+
set.delete(callback);
|
|
759
|
+
if (set.size === 0) subscribers.delete(mind);
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
function publish2(mind, event) {
|
|
763
|
+
const set = subscribers.get(mind);
|
|
764
|
+
if (!set) return;
|
|
765
|
+
for (const cb of set) {
|
|
766
|
+
try {
|
|
767
|
+
cb(event);
|
|
768
|
+
} catch (err) {
|
|
769
|
+
console.error("[mind-events] subscriber threw:", err);
|
|
770
|
+
set.delete(cb);
|
|
771
|
+
if (set.size === 0) subscribers.delete(mind);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
621
776
|
// src/lib/delivery/delivery-manager.ts
|
|
622
|
-
import {
|
|
777
|
+
import { readFile } from "fs/promises";
|
|
778
|
+
import { extname, resolve as resolve5 } from "path";
|
|
779
|
+
import { and as and3, eq as eq3, sql as sql2 } from "drizzle-orm";
|
|
780
|
+
|
|
781
|
+
// src/lib/events/conversations.ts
|
|
782
|
+
import { randomUUID } from "crypto";
|
|
783
|
+
import { and as and2, desc, eq as eq2, inArray, isNull, lt, sql } from "drizzle-orm";
|
|
784
|
+
|
|
785
|
+
// src/lib/webhook.ts
|
|
786
|
+
var slog = logger_default.child("webhook");
|
|
787
|
+
function getWebhookUrl() {
|
|
788
|
+
return process.env.VOLUTE_WEBHOOK_URL;
|
|
789
|
+
}
|
|
790
|
+
function getAuthHeaders() {
|
|
791
|
+
const headers = { "Content-Type": "application/json" };
|
|
792
|
+
const secret = process.env.VOLUTE_WEBHOOK_SECRET;
|
|
793
|
+
if (secret) headers.Authorization = `Bearer ${secret}`;
|
|
794
|
+
return headers;
|
|
795
|
+
}
|
|
796
|
+
function fireWebhook(event) {
|
|
797
|
+
try {
|
|
798
|
+
const url = getWebhookUrl();
|
|
799
|
+
if (!url) return;
|
|
800
|
+
const payload = { ...event, timestamp: event.timestamp ?? (/* @__PURE__ */ new Date()).toISOString() };
|
|
801
|
+
fetch(url, {
|
|
802
|
+
method: "POST",
|
|
803
|
+
headers: getAuthHeaders(),
|
|
804
|
+
body: JSON.stringify(payload)
|
|
805
|
+
}).then((res) => {
|
|
806
|
+
if (!res.ok) {
|
|
807
|
+
slog.warn(`webhook ${event.event} returned HTTP ${res.status}`);
|
|
808
|
+
}
|
|
809
|
+
}).catch((err) => {
|
|
810
|
+
slog.warn(`webhook delivery failed for ${event.event}`, logger_default.errorData(err));
|
|
811
|
+
});
|
|
812
|
+
} catch (err) {
|
|
813
|
+
slog.error(`webhook ${event.event} failed to serialize`, logger_default.errorData(err));
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
function initWebhook() {
|
|
817
|
+
const url = getWebhookUrl();
|
|
818
|
+
if (!url) return () => {
|
|
819
|
+
};
|
|
820
|
+
try {
|
|
821
|
+
const parsed = new URL(url);
|
|
822
|
+
if (!["http:", "https:"].includes(parsed.protocol)) {
|
|
823
|
+
slog.error(`VOLUTE_WEBHOOK_URL has unsupported protocol: ${parsed.protocol}`);
|
|
824
|
+
return () => {
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
} catch {
|
|
828
|
+
slog.error(`VOLUTE_WEBHOOK_URL is not a valid URL`);
|
|
829
|
+
return () => {
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
slog.info("webhook enabled");
|
|
833
|
+
return subscribe((event) => {
|
|
834
|
+
try {
|
|
835
|
+
fireWebhook({
|
|
836
|
+
event: event.type,
|
|
837
|
+
mind: event.mind,
|
|
838
|
+
data: { summary: event.summary, ...event.metadata },
|
|
839
|
+
timestamp: event.created_at
|
|
840
|
+
});
|
|
841
|
+
} catch (err) {
|
|
842
|
+
slog.error(`failed to fire webhook for ${event.type}`, logger_default.errorData(err));
|
|
843
|
+
}
|
|
844
|
+
});
|
|
845
|
+
}
|
|
623
846
|
|
|
624
847
|
// src/lib/events/conversation-events.ts
|
|
625
|
-
var
|
|
626
|
-
function
|
|
627
|
-
let set =
|
|
848
|
+
var subscribers2 = /* @__PURE__ */ new Map();
|
|
849
|
+
function subscribe3(conversationId, callback) {
|
|
850
|
+
let set = subscribers2.get(conversationId);
|
|
628
851
|
if (!set) {
|
|
629
852
|
set = /* @__PURE__ */ new Set();
|
|
630
|
-
|
|
853
|
+
subscribers2.set(conversationId, set);
|
|
631
854
|
}
|
|
632
855
|
set.add(callback);
|
|
633
856
|
return () => {
|
|
634
857
|
set.delete(callback);
|
|
635
|
-
if (set.size === 0)
|
|
858
|
+
if (set.size === 0) subscribers2.delete(conversationId);
|
|
636
859
|
};
|
|
637
860
|
}
|
|
638
|
-
function
|
|
639
|
-
const set =
|
|
861
|
+
function publish3(conversationId, event) {
|
|
862
|
+
const set = subscribers2.get(conversationId);
|
|
640
863
|
if (!set) return;
|
|
641
864
|
for (const cb of set) {
|
|
642
865
|
try {
|
|
@@ -644,9 +867,333 @@ function publish2(conversationId, event) {
|
|
|
644
867
|
} catch (err) {
|
|
645
868
|
console.error("[conversation-events] subscriber threw:", err);
|
|
646
869
|
set.delete(cb);
|
|
647
|
-
if (set.size === 0)
|
|
870
|
+
if (set.size === 0) subscribers2.delete(conversationId);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// src/lib/events/conversations.ts
|
|
876
|
+
async function createConversation(mindName, channel, opts) {
|
|
877
|
+
const db = await getDb();
|
|
878
|
+
const id = randomUUID();
|
|
879
|
+
const type = opts?.type ?? "dm";
|
|
880
|
+
const name = opts?.name ?? null;
|
|
881
|
+
await db.transaction(async (tx) => {
|
|
882
|
+
await tx.insert(conversations).values({
|
|
883
|
+
id,
|
|
884
|
+
mind_name: mindName,
|
|
885
|
+
channel,
|
|
886
|
+
type,
|
|
887
|
+
name,
|
|
888
|
+
user_id: opts?.userId ?? null,
|
|
889
|
+
title: opts?.title ?? null
|
|
890
|
+
});
|
|
891
|
+
if (opts?.participantIds && opts.participantIds.length > 0) {
|
|
892
|
+
await tx.insert(conversationParticipants).values(
|
|
893
|
+
opts.participantIds.map((uid, i) => ({
|
|
894
|
+
conversation_id: id,
|
|
895
|
+
user_id: uid,
|
|
896
|
+
role: i === 0 ? "owner" : "member"
|
|
897
|
+
}))
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
fireWebhook({
|
|
902
|
+
event: "conversation_created",
|
|
903
|
+
mind: mindName ?? "",
|
|
904
|
+
data: { id, mindName, channel, type, name, title: opts?.title ?? null }
|
|
905
|
+
});
|
|
906
|
+
return {
|
|
907
|
+
id,
|
|
908
|
+
mind_name: mindName,
|
|
909
|
+
channel,
|
|
910
|
+
type,
|
|
911
|
+
name,
|
|
912
|
+
user_id: opts?.userId ?? null,
|
|
913
|
+
title: opts?.title ?? null,
|
|
914
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
915
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
async function getConversation(id) {
|
|
919
|
+
const db = await getDb();
|
|
920
|
+
const row = await db.select().from(conversations).where(eq2(conversations.id, id)).get();
|
|
921
|
+
return row ?? null;
|
|
922
|
+
}
|
|
923
|
+
async function addParticipant(conversationId, userId, role = "member") {
|
|
924
|
+
const db = await getDb();
|
|
925
|
+
await db.insert(conversationParticipants).values({
|
|
926
|
+
conversation_id: conversationId,
|
|
927
|
+
user_id: userId,
|
|
928
|
+
role
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
async function removeParticipant(conversationId, userId) {
|
|
932
|
+
const db = await getDb();
|
|
933
|
+
await db.delete(conversationParticipants).where(
|
|
934
|
+
and2(
|
|
935
|
+
eq2(conversationParticipants.conversation_id, conversationId),
|
|
936
|
+
eq2(conversationParticipants.user_id, userId)
|
|
937
|
+
)
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
async function getParticipants(conversationId) {
|
|
941
|
+
const db = await getDb();
|
|
942
|
+
const rows = await db.select({
|
|
943
|
+
userId: conversationParticipants.user_id,
|
|
944
|
+
username: users.username,
|
|
945
|
+
userType: users.user_type,
|
|
946
|
+
role: conversationParticipants.role,
|
|
947
|
+
displayName: users.display_name,
|
|
948
|
+
description: users.description,
|
|
949
|
+
avatar: users.avatar
|
|
950
|
+
}).from(conversationParticipants).innerJoin(users, eq2(conversationParticipants.user_id, users.id)).where(eq2(conversationParticipants.conversation_id, conversationId)).all();
|
|
951
|
+
return rows;
|
|
952
|
+
}
|
|
953
|
+
async function isParticipant(conversationId, userId) {
|
|
954
|
+
const db = await getDb();
|
|
955
|
+
const row = await db.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(
|
|
956
|
+
and2(
|
|
957
|
+
eq2(conversationParticipants.conversation_id, conversationId),
|
|
958
|
+
eq2(conversationParticipants.user_id, userId)
|
|
959
|
+
)
|
|
960
|
+
).get();
|
|
961
|
+
return row != null;
|
|
962
|
+
}
|
|
963
|
+
async function listConversationsForUser(userId) {
|
|
964
|
+
const db = await getDb();
|
|
965
|
+
const participantRows = await db.select({ conversation_id: conversationParticipants.conversation_id }).from(conversationParticipants).where(eq2(conversationParticipants.user_id, userId)).all();
|
|
966
|
+
if (participantRows.length === 0) return [];
|
|
967
|
+
const convIds = participantRows.map((r) => r.conversation_id);
|
|
968
|
+
return await db.select().from(conversations).where(inArray(conversations.id, convIds)).orderBy(desc(conversations.updated_at)).all();
|
|
969
|
+
}
|
|
970
|
+
async function isParticipantOrOwner(conversationId, userId) {
|
|
971
|
+
if (await isParticipant(conversationId, userId)) return true;
|
|
972
|
+
const db = await getDb();
|
|
973
|
+
const row = await db.select().from(conversations).where(and2(eq2(conversations.id, conversationId), eq2(conversations.user_id, userId))).get();
|
|
974
|
+
return row != null;
|
|
975
|
+
}
|
|
976
|
+
async function deleteConversationForUser(id, userId) {
|
|
977
|
+
if (!await isParticipantOrOwner(id, userId)) return false;
|
|
978
|
+
await deleteConversation(id);
|
|
979
|
+
return true;
|
|
980
|
+
}
|
|
981
|
+
async function addMessage(conversationId, role, senderName, content) {
|
|
982
|
+
const db = await getDb();
|
|
983
|
+
const serialized = JSON.stringify(content);
|
|
984
|
+
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 });
|
|
985
|
+
await db.update(conversations).set({ updated_at: sql`datetime('now')` }).where(eq2(conversations.id, conversationId));
|
|
986
|
+
if (role === "user") {
|
|
987
|
+
const firstText = content.find((b) => b.type === "text");
|
|
988
|
+
const title = firstText ? firstText.text.slice(0, 80) : "";
|
|
989
|
+
if (title) {
|
|
990
|
+
await db.update(conversations).set({ title }).where(and2(eq2(conversations.id, conversationId), isNull(conversations.title)));
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
const msg = {
|
|
994
|
+
id: result.id,
|
|
995
|
+
conversation_id: conversationId,
|
|
996
|
+
role,
|
|
997
|
+
sender_name: senderName,
|
|
998
|
+
content,
|
|
999
|
+
created_at: result.created_at
|
|
1000
|
+
};
|
|
1001
|
+
publish3(conversationId, {
|
|
1002
|
+
type: "message",
|
|
1003
|
+
id: msg.id,
|
|
1004
|
+
role: msg.role,
|
|
1005
|
+
senderName: msg.sender_name,
|
|
1006
|
+
content: msg.content,
|
|
1007
|
+
createdAt: msg.created_at
|
|
1008
|
+
});
|
|
1009
|
+
const conv = await db.select({ mind_name: conversations.mind_name }).from(conversations).where(eq2(conversations.id, conversationId)).get();
|
|
1010
|
+
fireWebhook({
|
|
1011
|
+
event: "message_created",
|
|
1012
|
+
mind: conv?.mind_name ?? "",
|
|
1013
|
+
data: {
|
|
1014
|
+
conversationId,
|
|
1015
|
+
messageId: result.id,
|
|
1016
|
+
role,
|
|
1017
|
+
senderName,
|
|
1018
|
+
content: content.filter((b) => b.type !== "image"),
|
|
1019
|
+
createdAt: result.created_at
|
|
1020
|
+
}
|
|
1021
|
+
});
|
|
1022
|
+
return msg;
|
|
1023
|
+
}
|
|
1024
|
+
async function getMessages(conversationId) {
|
|
1025
|
+
const db = await getDb();
|
|
1026
|
+
const rows = await db.select().from(messages).where(eq2(messages.conversation_id, conversationId)).orderBy(messages.created_at).all();
|
|
1027
|
+
return rows.map(parseMessageRow);
|
|
1028
|
+
}
|
|
1029
|
+
async function getMessagesPaginated(conversationId, opts) {
|
|
1030
|
+
const db = await getDb();
|
|
1031
|
+
const limit = Math.min(Math.max(opts?.limit ?? 50, 1), 100);
|
|
1032
|
+
const conditions = [eq2(messages.conversation_id, conversationId)];
|
|
1033
|
+
if (opts?.before != null) {
|
|
1034
|
+
conditions.push(lt(messages.id, opts.before));
|
|
1035
|
+
}
|
|
1036
|
+
const rows = await db.select().from(messages).where(and2(...conditions)).orderBy(desc(messages.id)).limit(limit + 1).all();
|
|
1037
|
+
const hasMore = rows.length > limit;
|
|
1038
|
+
const page = rows.slice(0, limit).reverse();
|
|
1039
|
+
return {
|
|
1040
|
+
messages: page.map(parseMessageRow),
|
|
1041
|
+
hasMore
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
function parseMessageRow(row) {
|
|
1045
|
+
let content;
|
|
1046
|
+
try {
|
|
1047
|
+
const parsed = JSON.parse(row.content);
|
|
1048
|
+
content = Array.isArray(parsed) ? parsed : [{ type: "text", text: row.content }];
|
|
1049
|
+
} catch {
|
|
1050
|
+
content = [{ type: "text", text: row.content }];
|
|
1051
|
+
}
|
|
1052
|
+
return { ...row, role: row.role, content };
|
|
1053
|
+
}
|
|
1054
|
+
async function listConversationsWithParticipants(userId) {
|
|
1055
|
+
const convs = await listConversationsForUser(userId);
|
|
1056
|
+
if (convs.length === 0) return [];
|
|
1057
|
+
const db = await getDb();
|
|
1058
|
+
const convIds = convs.map((c) => c.id);
|
|
1059
|
+
const rows = await db.select({
|
|
1060
|
+
conversationId: conversationParticipants.conversation_id,
|
|
1061
|
+
userId: users.id,
|
|
1062
|
+
username: users.username,
|
|
1063
|
+
userType: users.user_type,
|
|
1064
|
+
role: conversationParticipants.role,
|
|
1065
|
+
displayName: users.display_name,
|
|
1066
|
+
description: users.description,
|
|
1067
|
+
avatar: users.avatar
|
|
1068
|
+
}).from(conversationParticipants).innerJoin(users, eq2(conversationParticipants.user_id, users.id)).where(inArray(conversationParticipants.conversation_id, convIds));
|
|
1069
|
+
const byConv = /* @__PURE__ */ new Map();
|
|
1070
|
+
for (const r of rows) {
|
|
1071
|
+
let arr = byConv.get(r.conversationId);
|
|
1072
|
+
if (!arr) {
|
|
1073
|
+
arr = [];
|
|
1074
|
+
byConv.set(r.conversationId, arr);
|
|
1075
|
+
}
|
|
1076
|
+
arr.push({
|
|
1077
|
+
userId: r.userId,
|
|
1078
|
+
username: r.username,
|
|
1079
|
+
userType: r.userType,
|
|
1080
|
+
role: r.role,
|
|
1081
|
+
displayName: r.displayName,
|
|
1082
|
+
description: r.description,
|
|
1083
|
+
avatar: r.avatar
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
const lastMsgIds = await db.select({
|
|
1087
|
+
conversationId: messages.conversation_id,
|
|
1088
|
+
maxId: sql`MAX(${messages.id})`
|
|
1089
|
+
}).from(messages).where(inArray(messages.conversation_id, convIds)).groupBy(messages.conversation_id);
|
|
1090
|
+
const byLastMsg = /* @__PURE__ */ new Map();
|
|
1091
|
+
if (lastMsgIds.length > 0) {
|
|
1092
|
+
const msgRows = await db.select().from(messages).where(
|
|
1093
|
+
inArray(
|
|
1094
|
+
messages.id,
|
|
1095
|
+
lastMsgIds.map((r) => r.maxId)
|
|
1096
|
+
)
|
|
1097
|
+
);
|
|
1098
|
+
for (const m of msgRows) {
|
|
1099
|
+
let text = "";
|
|
1100
|
+
try {
|
|
1101
|
+
const parsed = JSON.parse(m.content);
|
|
1102
|
+
const blocks = Array.isArray(parsed) ? parsed : [];
|
|
1103
|
+
const textBlock = blocks.find((b) => b.type === "text");
|
|
1104
|
+
if (textBlock && "text" in textBlock) text = textBlock.text;
|
|
1105
|
+
} catch {
|
|
1106
|
+
text = m.content;
|
|
1107
|
+
}
|
|
1108
|
+
byLastMsg.set(m.conversation_id, {
|
|
1109
|
+
role: m.role,
|
|
1110
|
+
senderName: m.sender_name,
|
|
1111
|
+
text,
|
|
1112
|
+
createdAt: m.created_at
|
|
1113
|
+
});
|
|
648
1114
|
}
|
|
649
1115
|
}
|
|
1116
|
+
return convs.map((c) => ({
|
|
1117
|
+
...c,
|
|
1118
|
+
participants: byConv.get(c.id) ?? [],
|
|
1119
|
+
lastMessage: byLastMsg.get(c.id)
|
|
1120
|
+
}));
|
|
1121
|
+
}
|
|
1122
|
+
async function findDMConversation(mindName, participantIds) {
|
|
1123
|
+
const db = await getDb();
|
|
1124
|
+
const mindConvs = await db.select({ id: conversations.id }).from(conversations).where(and2(eq2(conversations.mind_name, mindName), eq2(conversations.type, "dm"))).all();
|
|
1125
|
+
for (const conv of mindConvs) {
|
|
1126
|
+
const rows = await db.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(eq2(conversationParticipants.conversation_id, conv.id)).all();
|
|
1127
|
+
if (rows.length !== 2) continue;
|
|
1128
|
+
const ids = new Set(rows.map((r) => r.user_id));
|
|
1129
|
+
if (ids.has(participantIds[0]) && ids.has(participantIds[1])) {
|
|
1130
|
+
return conv.id;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
return null;
|
|
1134
|
+
}
|
|
1135
|
+
async function deleteConversation(id) {
|
|
1136
|
+
const db = await getDb();
|
|
1137
|
+
await db.delete(conversations).where(eq2(conversations.id, id));
|
|
1138
|
+
}
|
|
1139
|
+
async function createChannel(name, creatorId) {
|
|
1140
|
+
const participantIds = creatorId ? [creatorId] : [];
|
|
1141
|
+
return createConversation(null, "volute", {
|
|
1142
|
+
type: "channel",
|
|
1143
|
+
name,
|
|
1144
|
+
title: name,
|
|
1145
|
+
participantIds
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
async function getChannelByName(name) {
|
|
1149
|
+
const db = await getDb();
|
|
1150
|
+
const row = await db.select().from(conversations).where(and2(eq2(conversations.name, name), eq2(conversations.type, "channel"))).get();
|
|
1151
|
+
return row ?? null;
|
|
1152
|
+
}
|
|
1153
|
+
async function listChannels() {
|
|
1154
|
+
const db = await getDb();
|
|
1155
|
+
return await db.select().from(conversations).where(eq2(conversations.type, "channel")).orderBy(conversations.name).all();
|
|
1156
|
+
}
|
|
1157
|
+
async function joinChannel(conversationId, userId) {
|
|
1158
|
+
if (await isParticipant(conversationId, userId)) return;
|
|
1159
|
+
await addParticipant(conversationId, userId);
|
|
1160
|
+
}
|
|
1161
|
+
async function leaveChannel(conversationId, userId) {
|
|
1162
|
+
await removeParticipant(conversationId, userId);
|
|
1163
|
+
}
|
|
1164
|
+
async function getUnreadCounts(userId, conversationIds) {
|
|
1165
|
+
if (conversationIds.length === 0) return {};
|
|
1166
|
+
const db = await getDb();
|
|
1167
|
+
const rows = await db.select({
|
|
1168
|
+
conversationId: messages.conversation_id,
|
|
1169
|
+
count: sql`COUNT(*)`
|
|
1170
|
+
}).from(messages).leftJoin(
|
|
1171
|
+
conversationReads,
|
|
1172
|
+
and2(
|
|
1173
|
+
eq2(conversationReads.conversation_id, messages.conversation_id),
|
|
1174
|
+
eq2(conversationReads.user_id, userId)
|
|
1175
|
+
)
|
|
1176
|
+
).where(
|
|
1177
|
+
and2(
|
|
1178
|
+
inArray(messages.conversation_id, conversationIds),
|
|
1179
|
+
sql`${messages.id} > COALESCE(${conversationReads.last_read_message_id}, 0)`
|
|
1180
|
+
)
|
|
1181
|
+
).groupBy(messages.conversation_id);
|
|
1182
|
+
const result = {};
|
|
1183
|
+
for (const row of rows) {
|
|
1184
|
+
result[row.conversationId] = row.count;
|
|
1185
|
+
}
|
|
1186
|
+
return result;
|
|
1187
|
+
}
|
|
1188
|
+
async function markConversationRead(userId, conversationId) {
|
|
1189
|
+
const db = await getDb();
|
|
1190
|
+
const maxRow = await db.select({ maxId: sql`MAX(${messages.id})` }).from(messages).where(eq2(messages.conversation_id, conversationId)).get();
|
|
1191
|
+
const maxId = maxRow?.maxId ?? 0;
|
|
1192
|
+
if (maxId === 0) return;
|
|
1193
|
+
await db.insert(conversationReads).values({ user_id: userId, conversation_id: conversationId, last_read_message_id: maxId }).onConflictDoUpdate({
|
|
1194
|
+
target: [conversationReads.user_id, conversationReads.conversation_id],
|
|
1195
|
+
set: { last_read_message_id: maxId }
|
|
1196
|
+
});
|
|
650
1197
|
}
|
|
651
1198
|
|
|
652
1199
|
// src/lib/typing.ts
|
|
@@ -734,7 +1281,7 @@ function publishTypingForChannels(channels, map) {
|
|
|
734
1281
|
for (const channel of channels) {
|
|
735
1282
|
if (channel.startsWith(VOLUTE_PREFIX)) {
|
|
736
1283
|
const conversationId = channel.slice(VOLUTE_PREFIX.length);
|
|
737
|
-
|
|
1284
|
+
publish3(conversationId, { type: "typing", senders: map.get(channel) });
|
|
738
1285
|
}
|
|
739
1286
|
}
|
|
740
1287
|
}
|
|
@@ -798,7 +1345,7 @@ function globMatch(pattern, value) {
|
|
|
798
1345
|
return regex.test(value);
|
|
799
1346
|
}
|
|
800
1347
|
var GLOB_MATCH_KEYS = /* @__PURE__ */ new Set(["channel", "sender"]);
|
|
801
|
-
var NON_MATCH_KEYS = /* @__PURE__ */ new Set(["session", "destination", "path", "mode"]);
|
|
1348
|
+
var NON_MATCH_KEYS = /* @__PURE__ */ new Set(["session", "destination", "path", "mode", "batch"]);
|
|
802
1349
|
function ruleMatches(rule, meta) {
|
|
803
1350
|
for (const [key, pattern] of Object.entries(rule)) {
|
|
804
1351
|
if (NON_MATCH_KEYS.has(key)) continue;
|
|
@@ -843,7 +1390,8 @@ function resolveRoute(config, meta) {
|
|
|
843
1390
|
destination: "mind",
|
|
844
1391
|
session: sanitizeSessionName(expandTemplate(rule.session ?? fallback, meta)),
|
|
845
1392
|
matched: true,
|
|
846
|
-
mode: rule.mode
|
|
1393
|
+
mode: rule.mode,
|
|
1394
|
+
rule
|
|
847
1395
|
};
|
|
848
1396
|
}
|
|
849
1397
|
}
|
|
@@ -855,12 +1403,27 @@ function normalizeBatchConfig(batch) {
|
|
|
855
1403
|
if (typeof batch === "number") return { maxWait: batch * 60 };
|
|
856
1404
|
return batch;
|
|
857
1405
|
}
|
|
858
|
-
function resolveDeliveryMode(config, sessionName) {
|
|
1406
|
+
function resolveDeliveryMode(config, sessionName, rule) {
|
|
1407
|
+
const ruleBatch = rule?.batch;
|
|
859
1408
|
const defaults = {
|
|
860
1409
|
delivery: { mode: "immediate" },
|
|
861
1410
|
interrupt: true
|
|
862
1411
|
};
|
|
863
|
-
if (!config.sessions)
|
|
1412
|
+
if (!config.sessions) {
|
|
1413
|
+
if (ruleBatch != null) {
|
|
1414
|
+
const batch = normalizeBatchConfig(ruleBatch);
|
|
1415
|
+
return {
|
|
1416
|
+
delivery: {
|
|
1417
|
+
mode: "batch",
|
|
1418
|
+
debounce: batch.debounce ?? DEFAULT_BATCH_DEBOUNCE,
|
|
1419
|
+
maxWait: batch.maxWait ?? DEFAULT_BATCH_MAX_WAIT,
|
|
1420
|
+
triggers: batch.triggers
|
|
1421
|
+
},
|
|
1422
|
+
interrupt: true
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
return defaults;
|
|
1426
|
+
}
|
|
864
1427
|
for (const [pattern, sessionConfig] of Object.entries(config.sessions)) {
|
|
865
1428
|
if (globMatch(pattern, sessionName)) {
|
|
866
1429
|
let delivery;
|
|
@@ -904,6 +1467,18 @@ function resolveDeliveryMode(config, sessionName) {
|
|
|
904
1467
|
};
|
|
905
1468
|
}
|
|
906
1469
|
}
|
|
1470
|
+
if (ruleBatch != null) {
|
|
1471
|
+
const batch = normalizeBatchConfig(ruleBatch);
|
|
1472
|
+
return {
|
|
1473
|
+
delivery: {
|
|
1474
|
+
mode: "batch",
|
|
1475
|
+
debounce: batch.debounce ?? DEFAULT_BATCH_DEBOUNCE,
|
|
1476
|
+
maxWait: batch.maxWait ?? DEFAULT_BATCH_MAX_WAIT,
|
|
1477
|
+
triggers: batch.triggers
|
|
1478
|
+
},
|
|
1479
|
+
interrupt: true
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
907
1482
|
return defaults;
|
|
908
1483
|
}
|
|
909
1484
|
|
|
@@ -953,7 +1528,7 @@ var DeliveryManager = class {
|
|
|
953
1528
|
if (sessionName === "$new") {
|
|
954
1529
|
sessionName = `new-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
955
1530
|
}
|
|
956
|
-
const sessionConfig = resolveDeliveryMode(config, sessionName);
|
|
1531
|
+
const sessionConfig = resolveDeliveryMode(config, sessionName, route.rule);
|
|
957
1532
|
if (sessionConfig.delivery.mode === "batch") {
|
|
958
1533
|
dlog2.debug(`enqueueing batch message for ${mindName}/${sessionName}`);
|
|
959
1534
|
this.enqueueBatch(mindName, sessionName, payload, sessionConfig);
|
|
@@ -985,7 +1560,7 @@ var DeliveryManager = class {
|
|
|
985
1560
|
async restoreFromDb() {
|
|
986
1561
|
try {
|
|
987
1562
|
const db = await getDb();
|
|
988
|
-
const rows = await db.select().from(deliveryQueue).where(
|
|
1563
|
+
const rows = await db.select().from(deliveryQueue).where(eq3(deliveryQueue.status, "pending"));
|
|
989
1564
|
for (const row of rows) {
|
|
990
1565
|
let payload;
|
|
991
1566
|
try {
|
|
@@ -1003,7 +1578,7 @@ var DeliveryManager = class {
|
|
|
1003
1578
|
this.addToBatchBuffer(row.mind, row.session, payload, sessionConfig);
|
|
1004
1579
|
} else {
|
|
1005
1580
|
try {
|
|
1006
|
-
await db.delete(deliveryQueue).where(
|
|
1581
|
+
await db.delete(deliveryQueue).where(eq3(deliveryQueue.id, row.id));
|
|
1007
1582
|
} catch (err) {
|
|
1008
1583
|
dlog2.warn(`failed to delete queue row ${row.id} for ${row.mind}`, logger_default.errorData(err));
|
|
1009
1584
|
}
|
|
@@ -1024,7 +1599,7 @@ var DeliveryManager = class {
|
|
|
1024
1599
|
*/
|
|
1025
1600
|
async getPending(mindName) {
|
|
1026
1601
|
const db = await getDb();
|
|
1027
|
-
const rows = await db.select().from(deliveryQueue).where(
|
|
1602
|
+
const rows = await db.select().from(deliveryQueue).where(and3(eq3(deliveryQueue.mind, mindName), eq3(deliveryQueue.status, "gated")));
|
|
1028
1603
|
const byChannel = /* @__PURE__ */ new Map();
|
|
1029
1604
|
for (const row of rows) {
|
|
1030
1605
|
const ch = row.channel ?? "unknown";
|
|
@@ -1117,8 +1692,9 @@ var DeliveryManager = class {
|
|
|
1117
1692
|
if (payload.conversationId) {
|
|
1118
1693
|
typingMap.set(`volute:${payload.conversationId}`, baseName, { persistent: true });
|
|
1119
1694
|
}
|
|
1695
|
+
const enrichedPayload = await this.enrichWithProfiles(baseName, session, payload);
|
|
1120
1696
|
const body = JSON.stringify({
|
|
1121
|
-
...
|
|
1697
|
+
...enrichedPayload,
|
|
1122
1698
|
session,
|
|
1123
1699
|
interrupt: sessionConfig.interrupt,
|
|
1124
1700
|
instructions: sessionConfig.instructions
|
|
@@ -1135,22 +1711,30 @@ var DeliveryManager = class {
|
|
|
1135
1711
|
publishTypingForChannels(typingMap.deleteSender(baseName), typingMap);
|
|
1136
1712
|
}
|
|
1137
1713
|
}
|
|
1138
|
-
async deliverBatchToMind(mindName, session,
|
|
1714
|
+
async deliverBatchToMind(mindName, session, messages2, sessionConfig, interruptOverride) {
|
|
1139
1715
|
const resolved = this.resolvePort(mindName);
|
|
1140
1716
|
if (!resolved) {
|
|
1141
1717
|
dlog2.warn(`cannot deliver batch to ${mindName}: mind not found`);
|
|
1142
1718
|
return;
|
|
1143
1719
|
}
|
|
1144
1720
|
const { baseName, port } = resolved;
|
|
1721
|
+
const enrichedMessages = await Promise.all(
|
|
1722
|
+
messages2.map(async (msg, i) => {
|
|
1723
|
+
const isFirst = messages2.findIndex((m) => m.channel === msg.channel) === i;
|
|
1724
|
+
if (!isFirst) return msg;
|
|
1725
|
+
const enrichedPayload = await this.enrichWithProfiles(baseName, session, msg.payload);
|
|
1726
|
+
return { ...msg, payload: enrichedPayload };
|
|
1727
|
+
})
|
|
1728
|
+
);
|
|
1145
1729
|
const channels = {};
|
|
1146
|
-
for (const msg of
|
|
1730
|
+
for (const msg of enrichedMessages) {
|
|
1147
1731
|
const ch = msg.channel ?? "unknown";
|
|
1148
1732
|
if (!channels[ch]) channels[ch] = [];
|
|
1149
1733
|
channels[ch].push(msg.payload);
|
|
1150
1734
|
}
|
|
1151
1735
|
const senders = /* @__PURE__ */ new Set();
|
|
1152
1736
|
const channelSet = /* @__PURE__ */ new Set();
|
|
1153
|
-
for (const msg of
|
|
1737
|
+
for (const msg of messages2) {
|
|
1154
1738
|
if (msg.sender) senders.add(msg.sender);
|
|
1155
1739
|
if (msg.channel) channelSet.add(msg.channel);
|
|
1156
1740
|
}
|
|
@@ -1160,7 +1744,7 @@ var DeliveryManager = class {
|
|
|
1160
1744
|
if (ch !== "unknown") typingMap.set(ch, baseName, { persistent: true });
|
|
1161
1745
|
}
|
|
1162
1746
|
const seenConvIds = /* @__PURE__ */ new Set();
|
|
1163
|
-
for (const msg of
|
|
1747
|
+
for (const msg of messages2) {
|
|
1164
1748
|
if (msg.payload.conversationId && !seenConvIds.has(msg.payload.conversationId)) {
|
|
1165
1749
|
seenConvIds.add(msg.payload.conversationId);
|
|
1166
1750
|
typingMap.set(`volute:${msg.payload.conversationId}`, baseName, { persistent: true });
|
|
@@ -1181,10 +1765,10 @@ var DeliveryManager = class {
|
|
|
1181
1765
|
try {
|
|
1182
1766
|
const db = await getDb();
|
|
1183
1767
|
await db.delete(deliveryQueue).where(
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1768
|
+
and3(
|
|
1769
|
+
eq3(deliveryQueue.mind, baseName),
|
|
1770
|
+
eq3(deliveryQueue.session, session),
|
|
1771
|
+
eq3(deliveryQueue.status, "pending")
|
|
1188
1772
|
)
|
|
1189
1773
|
);
|
|
1190
1774
|
} catch (err) {
|
|
@@ -1282,24 +1866,24 @@ var DeliveryManager = class {
|
|
|
1282
1866
|
flushBatch(mindName, session, extra, interruptOverride) {
|
|
1283
1867
|
const bufferKey = `${mindName}:${session}`;
|
|
1284
1868
|
const buffer = this.batchBuffers.get(bufferKey);
|
|
1285
|
-
const
|
|
1869
|
+
const messages2 = [];
|
|
1286
1870
|
if (buffer) {
|
|
1287
1871
|
if (buffer.debounceTimer) clearTimeout(buffer.debounceTimer);
|
|
1288
1872
|
if (buffer.maxWaitTimer) clearTimeout(buffer.maxWaitTimer);
|
|
1289
1873
|
buffer.debounceTimer = null;
|
|
1290
1874
|
buffer.maxWaitTimer = null;
|
|
1291
|
-
|
|
1875
|
+
messages2.push(...buffer.messages.splice(0));
|
|
1292
1876
|
this.batchBuffers.delete(bufferKey);
|
|
1293
1877
|
}
|
|
1294
|
-
if (extra)
|
|
1295
|
-
if (
|
|
1878
|
+
if (extra) messages2.push(...extra);
|
|
1879
|
+
if (messages2.length === 0) return;
|
|
1296
1880
|
const [baseName] = mindName.split("@", 2);
|
|
1297
1881
|
const config = getRoutingConfig(baseName);
|
|
1298
1882
|
const sessionConfig = resolveDeliveryMode(config, session);
|
|
1299
1883
|
dlog2.info(
|
|
1300
|
-
`flushing batch for ${mindName}/${session}: ${
|
|
1884
|
+
`flushing batch for ${mindName}/${session}: ${messages2.length} messages${interruptOverride ? " (new-speaker interrupt)" : ""}`
|
|
1301
1885
|
);
|
|
1302
|
-
this.deliverBatchToMind(mindName, session,
|
|
1886
|
+
this.deliverBatchToMind(mindName, session, messages2, sessionConfig, interruptOverride).catch(
|
|
1303
1887
|
(err) => {
|
|
1304
1888
|
dlog2.warn(`failed to flush batch for ${mindName}/${session}`, logger_default.errorData(err));
|
|
1305
1889
|
}
|
|
@@ -1310,14 +1894,14 @@ var DeliveryManager = class {
|
|
|
1310
1894
|
await this.persistToQueue(baseName, session, payload, "gated");
|
|
1311
1895
|
try {
|
|
1312
1896
|
const db = await getDb();
|
|
1313
|
-
const
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1897
|
+
const count2 = await db.select({ count: sql2`count(*)` }).from(deliveryQueue).where(
|
|
1898
|
+
and3(
|
|
1899
|
+
eq3(deliveryQueue.mind, baseName),
|
|
1900
|
+
eq3(deliveryQueue.channel, payload.channel),
|
|
1901
|
+
eq3(deliveryQueue.status, "gated")
|
|
1318
1902
|
)
|
|
1319
1903
|
);
|
|
1320
|
-
if ((
|
|
1904
|
+
if ((count2[0]?.count ?? 0) <= 1) {
|
|
1321
1905
|
await this.sendInviteNotification(mindName, payload);
|
|
1322
1906
|
}
|
|
1323
1907
|
} catch (err) {
|
|
@@ -1369,6 +1953,72 @@ var DeliveryManager = class {
|
|
|
1369
1953
|
);
|
|
1370
1954
|
}
|
|
1371
1955
|
}
|
|
1956
|
+
async enrichWithProfiles(mindName, session, payload) {
|
|
1957
|
+
if (!payload.conversationId || !payload.channel) return payload;
|
|
1958
|
+
const mindSessions = this.sessionStates.get(mindName);
|
|
1959
|
+
const state = mindSessions?.get(session);
|
|
1960
|
+
if (!state) return payload;
|
|
1961
|
+
const channelKey = payload.channel;
|
|
1962
|
+
if (state.seenChannelProfiles.has(channelKey)) return payload;
|
|
1963
|
+
try {
|
|
1964
|
+
const participants = await getParticipants(payload.conversationId);
|
|
1965
|
+
const profiles = participants.map((p) => ({
|
|
1966
|
+
username: p.username,
|
|
1967
|
+
userType: p.userType,
|
|
1968
|
+
displayName: p.displayName,
|
|
1969
|
+
description: p.description
|
|
1970
|
+
}));
|
|
1971
|
+
const avatarBlocks = await this.loadAvatarBlocks(participants);
|
|
1972
|
+
state.seenChannelProfiles.add(channelKey);
|
|
1973
|
+
const enriched = { ...payload, participantProfiles: profiles };
|
|
1974
|
+
if (avatarBlocks.length > 0) {
|
|
1975
|
+
const existing = Array.isArray(payload.content) ? payload.content : typeof payload.content === "string" ? [{ type: "text", text: payload.content }] : [];
|
|
1976
|
+
enriched.content = [...avatarBlocks, ...existing];
|
|
1977
|
+
}
|
|
1978
|
+
return enriched;
|
|
1979
|
+
} catch (err) {
|
|
1980
|
+
dlog2.warn(`failed to fetch participant profiles for ${mindName}`, logger_default.errorData(err));
|
|
1981
|
+
return payload;
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
async loadAvatarBlocks(participants) {
|
|
1985
|
+
const blocks = [];
|
|
1986
|
+
for (const p of participants) {
|
|
1987
|
+
if (!p.avatar) continue;
|
|
1988
|
+
try {
|
|
1989
|
+
let filePath;
|
|
1990
|
+
if (p.userType === "mind") {
|
|
1991
|
+
const dir = mindDir(p.username);
|
|
1992
|
+
const config = readVoluteConfig(dir);
|
|
1993
|
+
if (!config?.avatar) continue;
|
|
1994
|
+
filePath = resolve5(dir, "home", config.avatar);
|
|
1995
|
+
} else {
|
|
1996
|
+
filePath = resolve5(voluteHome(), "avatars", p.avatar);
|
|
1997
|
+
}
|
|
1998
|
+
const ext = extname(filePath).toLowerCase();
|
|
1999
|
+
const mimeMap = {
|
|
2000
|
+
".png": "image/png",
|
|
2001
|
+
".jpg": "image/jpeg",
|
|
2002
|
+
".jpeg": "image/jpeg",
|
|
2003
|
+
".gif": "image/gif",
|
|
2004
|
+
".webp": "image/webp"
|
|
2005
|
+
};
|
|
2006
|
+
const mediaType = mimeMap[ext];
|
|
2007
|
+
if (!mediaType) continue;
|
|
2008
|
+
const data = await readFile(filePath);
|
|
2009
|
+
blocks.push(
|
|
2010
|
+
{ type: "text", text: `[Avatar for ${p.username}]` },
|
|
2011
|
+
{ type: "image", media_type: mediaType, data: data.toString("base64") }
|
|
2012
|
+
);
|
|
2013
|
+
} catch (err) {
|
|
2014
|
+
const code = err.code;
|
|
2015
|
+
if (code !== "ENOENT") {
|
|
2016
|
+
dlog2.warn(`failed to load avatar for ${p.username}`, logger_default.errorData(err));
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
return blocks;
|
|
2021
|
+
}
|
|
1372
2022
|
incrementActive(mind, session, senders, channels) {
|
|
1373
2023
|
let mindSessions = this.sessionStates.get(mind);
|
|
1374
2024
|
if (!mindSessions) {
|
|
@@ -1380,7 +2030,8 @@ var DeliveryManager = class {
|
|
|
1380
2030
|
lastDeliveredAt: 0,
|
|
1381
2031
|
lastDeliverySenders: /* @__PURE__ */ new Set(),
|
|
1382
2032
|
lastDeliveryChannels: /* @__PURE__ */ new Set(),
|
|
1383
|
-
lastInterruptAt: 0
|
|
2033
|
+
lastInterruptAt: 0,
|
|
2034
|
+
seenChannelProfiles: /* @__PURE__ */ new Set()
|
|
1384
2035
|
};
|
|
1385
2036
|
state.activeCount++;
|
|
1386
2037
|
state.lastDeliveredAt = Date.now();
|
|
@@ -1418,6 +2069,26 @@ function getDeliveryManager() {
|
|
|
1418
2069
|
|
|
1419
2070
|
// src/lib/delivery/message-delivery.ts
|
|
1420
2071
|
var dlog3 = logger_default.child("delivery");
|
|
2072
|
+
async function recordInbound(mind, channel, sender, content) {
|
|
2073
|
+
try {
|
|
2074
|
+
const db = await getDb();
|
|
2075
|
+
await db.insert(mindHistory).values({
|
|
2076
|
+
mind,
|
|
2077
|
+
type: "inbound",
|
|
2078
|
+
channel,
|
|
2079
|
+
sender,
|
|
2080
|
+
content
|
|
2081
|
+
});
|
|
2082
|
+
} catch (err) {
|
|
2083
|
+
dlog3.warn(`failed to persist inbound for ${mind}`, logger_default.errorData(err));
|
|
2084
|
+
}
|
|
2085
|
+
publish2(mind, {
|
|
2086
|
+
mind,
|
|
2087
|
+
type: "inbound",
|
|
2088
|
+
channel,
|
|
2089
|
+
content: content ?? void 0
|
|
2090
|
+
});
|
|
2091
|
+
}
|
|
1421
2092
|
async function deliverMessage(mindName, payload) {
|
|
1422
2093
|
try {
|
|
1423
2094
|
const [baseName] = mindName.split("@", 2);
|
|
@@ -1427,18 +2098,7 @@ async function deliverMessage(mindName, payload) {
|
|
|
1427
2098
|
return;
|
|
1428
2099
|
}
|
|
1429
2100
|
const textContent = extractTextContent(payload.content);
|
|
1430
|
-
|
|
1431
|
-
const db = await getDb();
|
|
1432
|
-
await db.insert(mindHistory).values({
|
|
1433
|
-
mind: baseName,
|
|
1434
|
-
type: "inbound",
|
|
1435
|
-
channel: payload.channel,
|
|
1436
|
-
sender: payload.sender ?? null,
|
|
1437
|
-
content: textContent
|
|
1438
|
-
});
|
|
1439
|
-
} catch (err) {
|
|
1440
|
-
dlog3.warn(`failed to persist message for ${baseName}`, logger_default.errorData(err));
|
|
1441
|
-
}
|
|
2101
|
+
await recordInbound(baseName, payload.channel, payload.sender ?? null, textContent);
|
|
1442
2102
|
const sleepManager = getSleepManagerIfReady();
|
|
1443
2103
|
if (sleepManager?.isSleeping(baseName)) {
|
|
1444
2104
|
if (sleepManager.checkWakeTrigger(baseName, payload)) {
|
|
@@ -1694,16 +2354,16 @@ async function ensureMailAddress(mindName) {
|
|
|
1694
2354
|
}
|
|
1695
2355
|
|
|
1696
2356
|
// src/lib/daemon/scheduler.ts
|
|
1697
|
-
import { resolve as
|
|
2357
|
+
import { resolve as resolve6 } from "path";
|
|
1698
2358
|
import { CronExpressionParser } from "cron-parser";
|
|
1699
|
-
var
|
|
2359
|
+
var slog2 = logger_default.child("scheduler");
|
|
1700
2360
|
var Scheduler = class {
|
|
1701
2361
|
schedules = /* @__PURE__ */ new Map();
|
|
1702
2362
|
interval = null;
|
|
1703
2363
|
lastFired = /* @__PURE__ */ new Map();
|
|
1704
2364
|
// "mind:scheduleId" → epoch minute
|
|
1705
2365
|
get statePath() {
|
|
1706
|
-
return
|
|
2366
|
+
return resolve6(voluteHome(), "scheduler-state.json");
|
|
1707
2367
|
}
|
|
1708
2368
|
start() {
|
|
1709
2369
|
this.loadState();
|
|
@@ -1762,7 +2422,7 @@ var Scheduler = class {
|
|
|
1762
2422
|
prevMinute = Math.floor(prev.getTime() / 6e4);
|
|
1763
2423
|
cronCache.set(schedule.cron, prevMinute);
|
|
1764
2424
|
} catch (err) {
|
|
1765
|
-
|
|
2425
|
+
slog2.warn(`invalid cron "${schedule.cron}" for ${mind}:${schedule.id}`, logger_default.errorData(err));
|
|
1766
2426
|
return false;
|
|
1767
2427
|
}
|
|
1768
2428
|
}
|
|
@@ -1776,11 +2436,11 @@ var Scheduler = class {
|
|
|
1776
2436
|
try {
|
|
1777
2437
|
let text;
|
|
1778
2438
|
if (schedule.script) {
|
|
1779
|
-
const homeDir =
|
|
2439
|
+
const homeDir = resolve6(mindDir(mindName), "home");
|
|
1780
2440
|
try {
|
|
1781
2441
|
const output = await this.runScript(schedule.script, homeDir, mindName);
|
|
1782
2442
|
if (!output.trim()) {
|
|
1783
|
-
|
|
2443
|
+
slog2.info(`fired script "${schedule.id}" for ${mindName} (no output)`);
|
|
1784
2444
|
return;
|
|
1785
2445
|
}
|
|
1786
2446
|
text = output;
|
|
@@ -1788,12 +2448,12 @@ var Scheduler = class {
|
|
|
1788
2448
|
const stderr = err.stderr ?? "";
|
|
1789
2449
|
text = `[script error] ${err.message}${stderr ? `
|
|
1790
2450
|
${stderr}` : ""}`;
|
|
1791
|
-
|
|
2451
|
+
slog2.warn(`script "${schedule.id}" failed for ${mindName}`, logger_default.errorData(err));
|
|
1792
2452
|
}
|
|
1793
2453
|
} else if (schedule.message) {
|
|
1794
2454
|
text = schedule.message;
|
|
1795
2455
|
} else {
|
|
1796
|
-
|
|
2456
|
+
slog2.warn(`schedule "${schedule.id}" for ${mindName} has no message or script`);
|
|
1797
2457
|
return;
|
|
1798
2458
|
}
|
|
1799
2459
|
await this.deliver(mindName, {
|
|
@@ -1801,9 +2461,9 @@ ${stderr}` : ""}`;
|
|
|
1801
2461
|
channel: "system:scheduler",
|
|
1802
2462
|
sender: schedule.id
|
|
1803
2463
|
});
|
|
1804
|
-
|
|
2464
|
+
slog2.info(`fired "${schedule.id}" for ${mindName}`);
|
|
1805
2465
|
} catch (err) {
|
|
1806
|
-
|
|
2466
|
+
slog2.warn(`failed to fire "${schedule.id}" for ${mindName}`, logger_default.errorData(err));
|
|
1807
2467
|
}
|
|
1808
2468
|
}
|
|
1809
2469
|
runScript(script, cwd, mindName) {
|
|
@@ -1826,7 +2486,7 @@ function getScheduler() {
|
|
|
1826
2486
|
|
|
1827
2487
|
// src/lib/daemon/token-budget.ts
|
|
1828
2488
|
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
1829
|
-
import { resolve as
|
|
2489
|
+
import { resolve as resolve7 } from "path";
|
|
1830
2490
|
var tlog = logger_default.child("token-budget");
|
|
1831
2491
|
var DEFAULT_BUDGET_PERIOD_MINUTES = 60;
|
|
1832
2492
|
var MAX_QUEUE_SIZE = 100;
|
|
@@ -1900,9 +2560,9 @@ var TokenBudget = class {
|
|
|
1900
2560
|
drain(mind) {
|
|
1901
2561
|
const state = this.budgets.get(mind);
|
|
1902
2562
|
if (!state) return [];
|
|
1903
|
-
const
|
|
2563
|
+
const messages2 = state.queue;
|
|
1904
2564
|
state.queue = [];
|
|
1905
|
-
return
|
|
2565
|
+
return messages2;
|
|
1906
2566
|
}
|
|
1907
2567
|
getUsage(mind) {
|
|
1908
2568
|
const state = this.budgets.get(mind);
|
|
@@ -1944,7 +2604,7 @@ var TokenBudget = class {
|
|
|
1944
2604
|
this.dirty.clear();
|
|
1945
2605
|
}
|
|
1946
2606
|
budgetStatePath(mind) {
|
|
1947
|
-
return
|
|
2607
|
+
return resolve7(stateDir(mind), "budget.json");
|
|
1948
2608
|
}
|
|
1949
2609
|
saveBudgetState(mind, state) {
|
|
1950
2610
|
try {
|
|
@@ -1983,8 +2643,8 @@ var TokenBudget = class {
|
|
|
1983
2643
|
return null;
|
|
1984
2644
|
}
|
|
1985
2645
|
}
|
|
1986
|
-
async replay(mindName,
|
|
1987
|
-
const summary =
|
|
2646
|
+
async replay(mindName, messages2) {
|
|
2647
|
+
const summary = messages2.map((m) => {
|
|
1988
2648
|
const from = m.sender ? `[${m.sender}]` : "";
|
|
1989
2649
|
const ch = m.channel ? `(${m.channel})` : "";
|
|
1990
2650
|
return `${from}${ch} ${m.textContent}`;
|
|
@@ -1994,7 +2654,7 @@ var TokenBudget = class {
|
|
|
1994
2654
|
content: [
|
|
1995
2655
|
{
|
|
1996
2656
|
type: "text",
|
|
1997
|
-
text: `[Budget replay] ${
|
|
2657
|
+
text: `[Budget replay] ${messages2.length} queued message(s) from the previous budget period:
|
|
1998
2658
|
|
|
1999
2659
|
${summary}`
|
|
2000
2660
|
}
|
|
@@ -2002,11 +2662,11 @@ ${summary}`
|
|
|
2002
2662
|
channel: "system:budget-replay",
|
|
2003
2663
|
sender: "system"
|
|
2004
2664
|
});
|
|
2005
|
-
tlog.info(`replayed ${
|
|
2665
|
+
tlog.info(`replayed ${messages2.length} queued message(s) for ${mindName}`);
|
|
2006
2666
|
} catch (err) {
|
|
2007
2667
|
tlog.warn(`failed to replay for ${mindName}`, logger_default.errorData(err));
|
|
2008
2668
|
const state = this.budgets.get(mindName);
|
|
2009
|
-
if (state) state.queue.push(...
|
|
2669
|
+
if (state) state.queue.push(...messages2);
|
|
2010
2670
|
}
|
|
2011
2671
|
}
|
|
2012
2672
|
};
|
|
@@ -2041,6 +2701,15 @@ async function startMindFull(name) {
|
|
|
2041
2701
|
(err) => logger_default.error(`failed to ensure mail address for ${baseName}`, logger_default.errorData(err))
|
|
2042
2702
|
);
|
|
2043
2703
|
const config = readVoluteConfig(dir);
|
|
2704
|
+
if (config) {
|
|
2705
|
+
syncMindProfile(baseName, {
|
|
2706
|
+
displayName: config.displayName,
|
|
2707
|
+
description: config.description,
|
|
2708
|
+
avatar: config.avatar
|
|
2709
|
+
}).catch(
|
|
2710
|
+
(err) => logger_default.error(`failed to sync profile for ${baseName}`, logger_default.errorData(err))
|
|
2711
|
+
);
|
|
2712
|
+
}
|
|
2044
2713
|
if (config?.tokenBudget) {
|
|
2045
2714
|
getTokenBudget().setBudget(
|
|
2046
2715
|
baseName,
|
|
@@ -2085,7 +2754,7 @@ async function stopMindFull(name) {
|
|
|
2085
2754
|
}
|
|
2086
2755
|
|
|
2087
2756
|
// src/lib/daemon/sleep-manager.ts
|
|
2088
|
-
var
|
|
2757
|
+
var slog3 = logger_default.child("sleep");
|
|
2089
2758
|
function defaultState() {
|
|
2090
2759
|
return {
|
|
2091
2760
|
sleeping: false,
|
|
@@ -2121,7 +2790,7 @@ var SleepManager = class {
|
|
|
2121
2790
|
unsubActivity = null;
|
|
2122
2791
|
transitioning = /* @__PURE__ */ new Set();
|
|
2123
2792
|
get statePath() {
|
|
2124
|
-
return
|
|
2793
|
+
return resolve8(voluteHome(), "sleep-state.json");
|
|
2125
2794
|
}
|
|
2126
2795
|
start() {
|
|
2127
2796
|
this.loadState();
|
|
@@ -2144,7 +2813,7 @@ var SleepManager = class {
|
|
|
2144
2813
|
}
|
|
2145
2814
|
}
|
|
2146
2815
|
} catch (err) {
|
|
2147
|
-
|
|
2816
|
+
slog3.warn("failed to load sleep state", logger_default.errorData(err));
|
|
2148
2817
|
}
|
|
2149
2818
|
}
|
|
2150
2819
|
saveState() {
|
|
@@ -2156,7 +2825,7 @@ var SleepManager = class {
|
|
|
2156
2825
|
writeFileSync3(this.statePath, `${JSON.stringify(data, null, 2)}
|
|
2157
2826
|
`);
|
|
2158
2827
|
} catch (err) {
|
|
2159
|
-
|
|
2828
|
+
slog3.error("failed to save sleep state", logger_default.errorData(err));
|
|
2160
2829
|
}
|
|
2161
2830
|
}
|
|
2162
2831
|
// --- Public API ---
|
|
@@ -2203,7 +2872,7 @@ var SleepManager = class {
|
|
|
2203
2872
|
content: preSleepMsg
|
|
2204
2873
|
});
|
|
2205
2874
|
} catch (err) {
|
|
2206
|
-
|
|
2875
|
+
slog3.error(`failed to persist pre-sleep message for ${name}`, logger_default.errorData(err));
|
|
2207
2876
|
}
|
|
2208
2877
|
try {
|
|
2209
2878
|
await fetch(`http://127.0.0.1:${entry.port}/message`, {
|
|
@@ -2215,7 +2884,7 @@ var SleepManager = class {
|
|
|
2215
2884
|
})
|
|
2216
2885
|
});
|
|
2217
2886
|
} catch (err) {
|
|
2218
|
-
|
|
2887
|
+
slog3.warn(`failed to send pre-sleep message to ${name}`, logger_default.errorData(err));
|
|
2219
2888
|
}
|
|
2220
2889
|
await this.waitForIdle(name, 12e4);
|
|
2221
2890
|
await new Promise((r) => setTimeout(r, 3e3));
|
|
@@ -2223,7 +2892,7 @@ var SleepManager = class {
|
|
|
2223
2892
|
await this.killOrphanOnPort(entry.port);
|
|
2224
2893
|
await this.archiveSessions(name);
|
|
2225
2894
|
this.markSleeping(name, opts);
|
|
2226
|
-
|
|
2895
|
+
slog3.info(`${name} is now sleeping`);
|
|
2227
2896
|
} finally {
|
|
2228
2897
|
this.transitioning.delete(name);
|
|
2229
2898
|
}
|
|
@@ -2249,7 +2918,7 @@ var SleepManager = class {
|
|
|
2249
2918
|
try {
|
|
2250
2919
|
await wakeMind(name);
|
|
2251
2920
|
} catch (err) {
|
|
2252
|
-
|
|
2921
|
+
slog3.error(`failed to wake ${name}`, logger_default.errorData(err));
|
|
2253
2922
|
return;
|
|
2254
2923
|
}
|
|
2255
2924
|
const entry = findMind(name);
|
|
@@ -2281,7 +2950,7 @@ var SleepManager = class {
|
|
|
2281
2950
|
content: summaryText
|
|
2282
2951
|
});
|
|
2283
2952
|
} catch (err) {
|
|
2284
|
-
|
|
2953
|
+
slog3.error(`failed to persist wake summary for ${name}`, logger_default.errorData(err));
|
|
2285
2954
|
}
|
|
2286
2955
|
try {
|
|
2287
2956
|
await fetch(`http://127.0.0.1:${entry.port}/message`, {
|
|
@@ -2293,16 +2962,16 @@ var SleepManager = class {
|
|
|
2293
2962
|
})
|
|
2294
2963
|
});
|
|
2295
2964
|
} catch (err) {
|
|
2296
|
-
|
|
2965
|
+
slog3.warn(`failed to deliver wake summary to ${name}`, logger_default.errorData(err));
|
|
2297
2966
|
}
|
|
2298
2967
|
const flushed = await this.flushQueuedMessages(name);
|
|
2299
2968
|
if (flushed > 0) {
|
|
2300
|
-
|
|
2969
|
+
slog3.info(`flushed ${flushed} queued message(s) for ${name}`);
|
|
2301
2970
|
}
|
|
2302
2971
|
if (!opts?.trigger) {
|
|
2303
2972
|
this.markAwake(name);
|
|
2304
2973
|
}
|
|
2305
|
-
|
|
2974
|
+
slog3.info(`${name} is now awake${opts?.trigger ? " (trigger wake)" : ""}`);
|
|
2306
2975
|
} finally {
|
|
2307
2976
|
this.transitioning.delete(name);
|
|
2308
2977
|
}
|
|
@@ -2357,20 +3026,20 @@ var SleepManager = class {
|
|
|
2357
3026
|
async flushQueuedMessages(name) {
|
|
2358
3027
|
try {
|
|
2359
3028
|
const db = await getDb();
|
|
2360
|
-
const rows = await db.select().from(deliveryQueue).where(
|
|
3029
|
+
const rows = await db.select().from(deliveryQueue).where(and4(eq4(deliveryQueue.mind, name), eq4(deliveryQueue.status, "sleep-queued"))).all();
|
|
2361
3030
|
if (rows.length === 0) return 0;
|
|
2362
|
-
const { deliverMessage: deliverMessage2 } = await import("./message-delivery-
|
|
3031
|
+
const { deliverMessage: deliverMessage2 } = await import("./message-delivery-S7BCNV6Y.js");
|
|
2363
3032
|
const delivered = [];
|
|
2364
3033
|
for (const row of rows) {
|
|
2365
3034
|
try {
|
|
2366
3035
|
await deliverMessage2(name, JSON.parse(row.payload));
|
|
2367
3036
|
delivered.push(row.id);
|
|
2368
3037
|
} catch (err) {
|
|
2369
|
-
|
|
3038
|
+
slog3.warn(`failed to flush queued message ${row.id} for ${name}`, logger_default.errorData(err));
|
|
2370
3039
|
}
|
|
2371
3040
|
}
|
|
2372
3041
|
if (delivered.length > 0) {
|
|
2373
|
-
await db.delete(deliveryQueue).where(
|
|
3042
|
+
await db.delete(deliveryQueue).where(inArray2(deliveryQueue.id, delivered));
|
|
2374
3043
|
}
|
|
2375
3044
|
const state = this.states.get(name);
|
|
2376
3045
|
if (state) {
|
|
@@ -2378,7 +3047,7 @@ var SleepManager = class {
|
|
|
2378
3047
|
}
|
|
2379
3048
|
return delivered.length;
|
|
2380
3049
|
} catch (err) {
|
|
2381
|
-
|
|
3050
|
+
slog3.warn(`failed to flush queued messages for ${name}`, logger_default.errorData(err));
|
|
2382
3051
|
return 0;
|
|
2383
3052
|
}
|
|
2384
3053
|
}
|
|
@@ -2406,7 +3075,7 @@ var SleepManager = class {
|
|
|
2406
3075
|
const interval = CronExpressionParser2.parse(config.schedule.wake);
|
|
2407
3076
|
return interval.next().toDate().toISOString();
|
|
2408
3077
|
} catch (err) {
|
|
2409
|
-
|
|
3078
|
+
slog3.warn(`invalid wake cron "${config.schedule.wake}"`, logger_default.errorData(err));
|
|
2410
3079
|
return null;
|
|
2411
3080
|
}
|
|
2412
3081
|
}
|
|
@@ -2423,7 +3092,7 @@ var SleepManager = class {
|
|
|
2423
3092
|
const wakeAt = new Date(state.voluntaryWakeAt);
|
|
2424
3093
|
if (now >= wakeAt) {
|
|
2425
3094
|
this.initiateWake(entry.name).catch(
|
|
2426
|
-
(err) =>
|
|
3095
|
+
(err) => slog3.error(`failed voluntary wake for ${entry.name}`, logger_default.errorData(err))
|
|
2427
3096
|
);
|
|
2428
3097
|
continue;
|
|
2429
3098
|
}
|
|
@@ -2432,7 +3101,7 @@ var SleepManager = class {
|
|
|
2432
3101
|
const wakeAt = new Date(state.scheduledWakeAt);
|
|
2433
3102
|
if (now >= wakeAt) {
|
|
2434
3103
|
this.initiateWake(entry.name).catch(
|
|
2435
|
-
(err) =>
|
|
3104
|
+
(err) => slog3.error(`failed scheduled wake for ${entry.name}`, logger_default.errorData(err))
|
|
2436
3105
|
);
|
|
2437
3106
|
continue;
|
|
2438
3107
|
}
|
|
@@ -2440,7 +3109,7 @@ var SleepManager = class {
|
|
|
2440
3109
|
if (!state?.sleeping && entry.running) {
|
|
2441
3110
|
if (this.shouldSleep(config.schedule.sleep, epochMinute)) {
|
|
2442
3111
|
this.initiateSleep(entry.name).catch(
|
|
2443
|
-
(err) =>
|
|
3112
|
+
(err) => slog3.error(`failed to initiate sleep for ${entry.name}`, logger_default.errorData(err))
|
|
2444
3113
|
);
|
|
2445
3114
|
}
|
|
2446
3115
|
}
|
|
@@ -2453,22 +3122,22 @@ var SleepManager = class {
|
|
|
2453
3122
|
const prevMinute = Math.floor(prev.getTime() / 6e4);
|
|
2454
3123
|
return prevMinute === epochMinute;
|
|
2455
3124
|
} catch (err) {
|
|
2456
|
-
|
|
3125
|
+
slog3.warn(`invalid sleep cron "${cronExpr}"`, logger_default.errorData(err));
|
|
2457
3126
|
return false;
|
|
2458
3127
|
}
|
|
2459
3128
|
}
|
|
2460
3129
|
async waitForIdle(name, timeoutMs) {
|
|
2461
|
-
return new Promise((
|
|
3130
|
+
return new Promise((resolve9) => {
|
|
2462
3131
|
const timeout = setTimeout(() => {
|
|
2463
3132
|
unsub();
|
|
2464
|
-
|
|
3133
|
+
resolve9();
|
|
2465
3134
|
}, timeoutMs);
|
|
2466
3135
|
const unsub = subscribe((event) => {
|
|
2467
3136
|
if (event.mind !== name) return;
|
|
2468
3137
|
if (event.type === "mind_done" || event.type === "mind_idle") {
|
|
2469
3138
|
clearTimeout(timeout);
|
|
2470
3139
|
unsub();
|
|
2471
|
-
|
|
3140
|
+
resolve9();
|
|
2472
3141
|
}
|
|
2473
3142
|
});
|
|
2474
3143
|
});
|
|
@@ -2476,34 +3145,34 @@ var SleepManager = class {
|
|
|
2476
3145
|
async archiveSessions(name) {
|
|
2477
3146
|
const dir = mindDir(name);
|
|
2478
3147
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 16);
|
|
2479
|
-
const sessionsDir =
|
|
3148
|
+
const sessionsDir = resolve8(dir, ".mind", "sessions");
|
|
2480
3149
|
if (existsSync5(sessionsDir)) {
|
|
2481
|
-
const archiveDir =
|
|
3150
|
+
const archiveDir = resolve8(sessionsDir, "archive");
|
|
2482
3151
|
mkdirSync3(archiveDir, { recursive: true });
|
|
2483
3152
|
for (const file of readdirSync2(sessionsDir)) {
|
|
2484
3153
|
if (file === "archive" || !file.endsWith(".json")) continue;
|
|
2485
|
-
const src =
|
|
3154
|
+
const src = resolve8(sessionsDir, file);
|
|
2486
3155
|
const base = file.replace(/\.json$/, "");
|
|
2487
|
-
const dest =
|
|
3156
|
+
const dest = resolve8(archiveDir, `${base}-${timestamp}.json`);
|
|
2488
3157
|
try {
|
|
2489
3158
|
renameSync(src, dest);
|
|
2490
3159
|
} catch (err) {
|
|
2491
|
-
|
|
3160
|
+
slog3.warn(`failed to archive session ${file} for ${name}`, logger_default.errorData(err));
|
|
2492
3161
|
}
|
|
2493
3162
|
}
|
|
2494
3163
|
}
|
|
2495
|
-
const piSessionsDir =
|
|
3164
|
+
const piSessionsDir = resolve8(dir, ".mind", "pi-sessions");
|
|
2496
3165
|
if (existsSync5(piSessionsDir)) {
|
|
2497
|
-
const archiveDir =
|
|
3166
|
+
const archiveDir = resolve8(piSessionsDir, "archive");
|
|
2498
3167
|
mkdirSync3(archiveDir, { recursive: true });
|
|
2499
3168
|
for (const entry of readdirSync2(piSessionsDir, { withFileTypes: true })) {
|
|
2500
3169
|
if (entry.name === "archive" || !entry.isDirectory()) continue;
|
|
2501
|
-
const src =
|
|
2502
|
-
const dest =
|
|
3170
|
+
const src = resolve8(piSessionsDir, entry.name);
|
|
3171
|
+
const dest = resolve8(archiveDir, `${entry.name}-${timestamp}`);
|
|
2503
3172
|
try {
|
|
2504
3173
|
renameSync(src, dest);
|
|
2505
3174
|
} catch (err) {
|
|
2506
|
-
|
|
3175
|
+
slog3.warn(`failed to archive pi-session ${entry.name} for ${name}`, logger_default.errorData(err));
|
|
2507
3176
|
}
|
|
2508
3177
|
}
|
|
2509
3178
|
}
|
|
@@ -2511,18 +3180,18 @@ var SleepManager = class {
|
|
|
2511
3180
|
async buildQueuedSummary(name) {
|
|
2512
3181
|
try {
|
|
2513
3182
|
const db = await getDb();
|
|
2514
|
-
const rows = await db.select({ channel: deliveryQueue.channel }).from(deliveryQueue).where(
|
|
2515
|
-
if (rows.length === 0) return "No messages while you slept.";
|
|
3183
|
+
const rows = await db.select({ channel: deliveryQueue.channel }).from(deliveryQueue).where(and4(eq4(deliveryQueue.mind, name), eq4(deliveryQueue.status, "sleep-queued"))).all();
|
|
3184
|
+
if (rows.length === 0) return "No messages arrived while you slept.";
|
|
2516
3185
|
const channelCounts = /* @__PURE__ */ new Map();
|
|
2517
3186
|
for (const row of rows) {
|
|
2518
3187
|
const ch = row.channel ?? "unknown";
|
|
2519
3188
|
channelCounts.set(ch, (channelCounts.get(ch) ?? 0) + 1);
|
|
2520
3189
|
}
|
|
2521
|
-
const parts = [...channelCounts.entries()].map(([ch,
|
|
2522
|
-
return `${rows.length} message${rows.length === 1 ? "" : "s"} while you slept (${parts.join(", ")}).
|
|
3190
|
+
const parts = [...channelCounts.entries()].map(([ch, count2]) => `${count2} on ${ch}`);
|
|
3191
|
+
return `${rows.length} message${rows.length === 1 ? "" : "s"} arrived while you slept (${parts.join(", ")}). They'll be delivered to your normal channels now.`;
|
|
2523
3192
|
} catch (err) {
|
|
2524
|
-
|
|
2525
|
-
return "
|
|
3193
|
+
slog3.error(`failed to build queued summary for ${name}`, logger_default.errorData(err));
|
|
3194
|
+
return "Unable to check for queued messages \u2014 there may be messages waiting.";
|
|
2526
3195
|
}
|
|
2527
3196
|
}
|
|
2528
3197
|
/**
|
|
@@ -2536,7 +3205,7 @@ var SleepManager = class {
|
|
|
2536
3205
|
} catch {
|
|
2537
3206
|
return;
|
|
2538
3207
|
}
|
|
2539
|
-
|
|
3208
|
+
slog3.warn(`orphan process found on port ${port} after sleep, killing`);
|
|
2540
3209
|
const execFileAsync = promisify(execFile);
|
|
2541
3210
|
try {
|
|
2542
3211
|
const { stdout } = await execFileAsync("lsof", ["-ti", `:${port}`, "-sTCP:LISTEN"]);
|
|
@@ -2547,7 +3216,7 @@ var SleepManager = class {
|
|
|
2547
3216
|
process.kill(pid, "SIGTERM");
|
|
2548
3217
|
} catch (err) {
|
|
2549
3218
|
if (err.code !== "ESRCH") {
|
|
2550
|
-
|
|
3219
|
+
slog3.warn(`failed to kill orphan pid ${pid}`, logger_default.errorData(err));
|
|
2551
3220
|
}
|
|
2552
3221
|
}
|
|
2553
3222
|
}
|
|
@@ -2579,7 +3248,7 @@ var SleepManager = class {
|
|
|
2579
3248
|
}
|
|
2580
3249
|
}
|
|
2581
3250
|
} catch (err) {
|
|
2582
|
-
|
|
3251
|
+
slog3.warn(`failed to kill orphan on port ${port} via /proc`, logger_default.errorData(err));
|
|
2583
3252
|
}
|
|
2584
3253
|
}
|
|
2585
3254
|
await new Promise((r) => setTimeout(r, 1e3));
|
|
@@ -2589,7 +3258,7 @@ var SleepManager = class {
|
|
|
2589
3258
|
if (!state?.sleeping || !state.wokenByTrigger) return;
|
|
2590
3259
|
if (this.transitioning.has(event.mind)) return;
|
|
2591
3260
|
if (event.type === "mind_idle") {
|
|
2592
|
-
|
|
3261
|
+
slog3.info(`${event.mind} going back to sleep after trigger wake`);
|
|
2593
3262
|
state.wokenByTrigger = false;
|
|
2594
3263
|
this.transitioning.add(event.mind);
|
|
2595
3264
|
sleepMind(event.mind).then(() => this.archiveSessions(event.mind)).then(() => {
|
|
@@ -2598,9 +3267,9 @@ var SleepManager = class {
|
|
|
2598
3267
|
const sleepConfig = this.getSleepConfig(event.mind);
|
|
2599
3268
|
state.scheduledWakeAt = this.getNextWakeTime(sleepConfig);
|
|
2600
3269
|
this.saveState();
|
|
2601
|
-
|
|
3270
|
+
slog3.info(`${event.mind} returned to sleep`);
|
|
2602
3271
|
}).catch((err) => {
|
|
2603
|
-
|
|
3272
|
+
slog3.error(`failed to return ${event.mind} to sleep`, logger_default.errorData(err));
|
|
2604
3273
|
}).finally(() => {
|
|
2605
3274
|
this.transitioning.delete(event.mind);
|
|
2606
3275
|
});
|
|
@@ -2624,6 +3293,21 @@ function getSleepManagerIfReady() {
|
|
|
2624
3293
|
export {
|
|
2625
3294
|
initConnectorManager,
|
|
2626
3295
|
getConnectorManager,
|
|
3296
|
+
createUser,
|
|
3297
|
+
verifyUser,
|
|
3298
|
+
getUser,
|
|
3299
|
+
getUserByUsername,
|
|
3300
|
+
listUsers,
|
|
3301
|
+
listPendingUsers,
|
|
3302
|
+
listUsersByType,
|
|
3303
|
+
getOrCreateMindUser,
|
|
3304
|
+
deleteMindUser,
|
|
3305
|
+
changePassword,
|
|
3306
|
+
approveUser,
|
|
3307
|
+
countAdmins,
|
|
3308
|
+
setUserRole,
|
|
3309
|
+
deleteUser,
|
|
3310
|
+
updateUserProfile,
|
|
2627
3311
|
stopAllWatchers,
|
|
2628
3312
|
getCachedSites,
|
|
2629
3313
|
getCachedRecentPages,
|
|
@@ -2640,11 +3324,37 @@ export {
|
|
|
2640
3324
|
getSleepManagerIfReady,
|
|
2641
3325
|
subscribe2 as subscribe,
|
|
2642
3326
|
publish2 as publish,
|
|
3327
|
+
getWebhookUrl,
|
|
3328
|
+
getAuthHeaders,
|
|
3329
|
+
fireWebhook,
|
|
3330
|
+
initWebhook,
|
|
3331
|
+
subscribe3 as subscribe2,
|
|
3332
|
+
publish3 as publish2,
|
|
3333
|
+
createConversation,
|
|
3334
|
+
getConversation,
|
|
3335
|
+
getParticipants,
|
|
3336
|
+
isParticipant,
|
|
3337
|
+
listConversationsForUser,
|
|
3338
|
+
isParticipantOrOwner,
|
|
3339
|
+
deleteConversationForUser,
|
|
3340
|
+
addMessage,
|
|
3341
|
+
getMessages,
|
|
3342
|
+
getMessagesPaginated,
|
|
3343
|
+
listConversationsWithParticipants,
|
|
3344
|
+
findDMConversation,
|
|
3345
|
+
createChannel,
|
|
3346
|
+
getChannelByName,
|
|
3347
|
+
listChannels,
|
|
3348
|
+
joinChannel,
|
|
3349
|
+
leaveChannel,
|
|
3350
|
+
getUnreadCounts,
|
|
3351
|
+
markConversationRead,
|
|
2643
3352
|
getTypingMap,
|
|
2644
3353
|
publishTypingForChannels,
|
|
2645
3354
|
extractTextContent,
|
|
2646
3355
|
initDeliveryManager,
|
|
2647
3356
|
getDeliveryManager,
|
|
3357
|
+
recordInbound,
|
|
2648
3358
|
deliverMessage,
|
|
2649
3359
|
initMailPoller,
|
|
2650
3360
|
getMailPoller
|