volute 0.23.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-M5CNKH4J.js → chunk-NOBRGACV.js} +7 -7
- package/dist/{chunk-ISWZ6QUK.js → chunk-OOW675I3.js} +778 -108
- package/dist/{chunk-TFS25FIM.js → chunk-P3W36ZGD.js} +1 -1
- package/dist/{chunk-JG4CCJOA.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-PI47U2LT.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-RMGOOGPE.js → daemon-restart-YMPEATQH.js} +5 -5
- package/dist/daemon.js +665 -813
- 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-FHV4NO2F.js → message-delivery-S7BCNV6Y.js} +5 -5
- 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-KMY4GA2J.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-CUBJ4PKS.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-2TMQ65E4.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-Z5JRG2M2.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-LKABEJSA.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/format-prefix.ts +18 -2
- package/templates/_base/src/lib/routing.ts +2 -1
- package/templates/_base/src/lib/types.ts +8 -0
- package/dist/chunk-G5KRTU2F.js +0 -76
- package/dist/web-assets/assets/index-CZ26vsyY.js +0 -69
- package/dist/web-assets/assets/index-DyyAvJwW.css +0 -1
- /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);
|
|
@@ -647,7 +774,75 @@ function publish2(mind, event) {
|
|
|
647
774
|
}
|
|
648
775
|
|
|
649
776
|
// src/lib/delivery/delivery-manager.ts
|
|
650
|
-
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
|
+
}
|
|
651
846
|
|
|
652
847
|
// src/lib/events/conversation-events.ts
|
|
653
848
|
var subscribers2 = /* @__PURE__ */ new Map();
|
|
@@ -677,6 +872,330 @@ function publish3(conversationId, event) {
|
|
|
677
872
|
}
|
|
678
873
|
}
|
|
679
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
|
+
});
|
|
1114
|
+
}
|
|
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
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
|
|
680
1199
|
// src/lib/typing.ts
|
|
681
1200
|
var DEFAULT_TTL_MS = 1e4;
|
|
682
1201
|
var SWEEP_INTERVAL_MS = 5e3;
|
|
@@ -826,7 +1345,7 @@ function globMatch(pattern, value) {
|
|
|
826
1345
|
return regex.test(value);
|
|
827
1346
|
}
|
|
828
1347
|
var GLOB_MATCH_KEYS = /* @__PURE__ */ new Set(["channel", "sender"]);
|
|
829
|
-
var NON_MATCH_KEYS = /* @__PURE__ */ new Set(["session", "destination", "path", "mode"]);
|
|
1348
|
+
var NON_MATCH_KEYS = /* @__PURE__ */ new Set(["session", "destination", "path", "mode", "batch"]);
|
|
830
1349
|
function ruleMatches(rule, meta) {
|
|
831
1350
|
for (const [key, pattern] of Object.entries(rule)) {
|
|
832
1351
|
if (NON_MATCH_KEYS.has(key)) continue;
|
|
@@ -871,7 +1390,8 @@ function resolveRoute(config, meta) {
|
|
|
871
1390
|
destination: "mind",
|
|
872
1391
|
session: sanitizeSessionName(expandTemplate(rule.session ?? fallback, meta)),
|
|
873
1392
|
matched: true,
|
|
874
|
-
mode: rule.mode
|
|
1393
|
+
mode: rule.mode,
|
|
1394
|
+
rule
|
|
875
1395
|
};
|
|
876
1396
|
}
|
|
877
1397
|
}
|
|
@@ -883,12 +1403,27 @@ function normalizeBatchConfig(batch) {
|
|
|
883
1403
|
if (typeof batch === "number") return { maxWait: batch * 60 };
|
|
884
1404
|
return batch;
|
|
885
1405
|
}
|
|
886
|
-
function resolveDeliveryMode(config, sessionName) {
|
|
1406
|
+
function resolveDeliveryMode(config, sessionName, rule) {
|
|
1407
|
+
const ruleBatch = rule?.batch;
|
|
887
1408
|
const defaults = {
|
|
888
1409
|
delivery: { mode: "immediate" },
|
|
889
1410
|
interrupt: true
|
|
890
1411
|
};
|
|
891
|
-
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
|
+
}
|
|
892
1427
|
for (const [pattern, sessionConfig] of Object.entries(config.sessions)) {
|
|
893
1428
|
if (globMatch(pattern, sessionName)) {
|
|
894
1429
|
let delivery;
|
|
@@ -932,6 +1467,18 @@ function resolveDeliveryMode(config, sessionName) {
|
|
|
932
1467
|
};
|
|
933
1468
|
}
|
|
934
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
|
+
}
|
|
935
1482
|
return defaults;
|
|
936
1483
|
}
|
|
937
1484
|
|
|
@@ -981,7 +1528,7 @@ var DeliveryManager = class {
|
|
|
981
1528
|
if (sessionName === "$new") {
|
|
982
1529
|
sessionName = `new-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
983
1530
|
}
|
|
984
|
-
const sessionConfig = resolveDeliveryMode(config, sessionName);
|
|
1531
|
+
const sessionConfig = resolveDeliveryMode(config, sessionName, route.rule);
|
|
985
1532
|
if (sessionConfig.delivery.mode === "batch") {
|
|
986
1533
|
dlog2.debug(`enqueueing batch message for ${mindName}/${sessionName}`);
|
|
987
1534
|
this.enqueueBatch(mindName, sessionName, payload, sessionConfig);
|
|
@@ -1013,7 +1560,7 @@ var DeliveryManager = class {
|
|
|
1013
1560
|
async restoreFromDb() {
|
|
1014
1561
|
try {
|
|
1015
1562
|
const db = await getDb();
|
|
1016
|
-
const rows = await db.select().from(deliveryQueue).where(
|
|
1563
|
+
const rows = await db.select().from(deliveryQueue).where(eq3(deliveryQueue.status, "pending"));
|
|
1017
1564
|
for (const row of rows) {
|
|
1018
1565
|
let payload;
|
|
1019
1566
|
try {
|
|
@@ -1031,7 +1578,7 @@ var DeliveryManager = class {
|
|
|
1031
1578
|
this.addToBatchBuffer(row.mind, row.session, payload, sessionConfig);
|
|
1032
1579
|
} else {
|
|
1033
1580
|
try {
|
|
1034
|
-
await db.delete(deliveryQueue).where(
|
|
1581
|
+
await db.delete(deliveryQueue).where(eq3(deliveryQueue.id, row.id));
|
|
1035
1582
|
} catch (err) {
|
|
1036
1583
|
dlog2.warn(`failed to delete queue row ${row.id} for ${row.mind}`, logger_default.errorData(err));
|
|
1037
1584
|
}
|
|
@@ -1052,7 +1599,7 @@ var DeliveryManager = class {
|
|
|
1052
1599
|
*/
|
|
1053
1600
|
async getPending(mindName) {
|
|
1054
1601
|
const db = await getDb();
|
|
1055
|
-
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")));
|
|
1056
1603
|
const byChannel = /* @__PURE__ */ new Map();
|
|
1057
1604
|
for (const row of rows) {
|
|
1058
1605
|
const ch = row.channel ?? "unknown";
|
|
@@ -1145,8 +1692,9 @@ var DeliveryManager = class {
|
|
|
1145
1692
|
if (payload.conversationId) {
|
|
1146
1693
|
typingMap.set(`volute:${payload.conversationId}`, baseName, { persistent: true });
|
|
1147
1694
|
}
|
|
1695
|
+
const enrichedPayload = await this.enrichWithProfiles(baseName, session, payload);
|
|
1148
1696
|
const body = JSON.stringify({
|
|
1149
|
-
...
|
|
1697
|
+
...enrichedPayload,
|
|
1150
1698
|
session,
|
|
1151
1699
|
interrupt: sessionConfig.interrupt,
|
|
1152
1700
|
instructions: sessionConfig.instructions
|
|
@@ -1163,22 +1711,30 @@ var DeliveryManager = class {
|
|
|
1163
1711
|
publishTypingForChannels(typingMap.deleteSender(baseName), typingMap);
|
|
1164
1712
|
}
|
|
1165
1713
|
}
|
|
1166
|
-
async deliverBatchToMind(mindName, session,
|
|
1714
|
+
async deliverBatchToMind(mindName, session, messages2, sessionConfig, interruptOverride) {
|
|
1167
1715
|
const resolved = this.resolvePort(mindName);
|
|
1168
1716
|
if (!resolved) {
|
|
1169
1717
|
dlog2.warn(`cannot deliver batch to ${mindName}: mind not found`);
|
|
1170
1718
|
return;
|
|
1171
1719
|
}
|
|
1172
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
|
+
);
|
|
1173
1729
|
const channels = {};
|
|
1174
|
-
for (const msg of
|
|
1730
|
+
for (const msg of enrichedMessages) {
|
|
1175
1731
|
const ch = msg.channel ?? "unknown";
|
|
1176
1732
|
if (!channels[ch]) channels[ch] = [];
|
|
1177
1733
|
channels[ch].push(msg.payload);
|
|
1178
1734
|
}
|
|
1179
1735
|
const senders = /* @__PURE__ */ new Set();
|
|
1180
1736
|
const channelSet = /* @__PURE__ */ new Set();
|
|
1181
|
-
for (const msg of
|
|
1737
|
+
for (const msg of messages2) {
|
|
1182
1738
|
if (msg.sender) senders.add(msg.sender);
|
|
1183
1739
|
if (msg.channel) channelSet.add(msg.channel);
|
|
1184
1740
|
}
|
|
@@ -1188,7 +1744,7 @@ var DeliveryManager = class {
|
|
|
1188
1744
|
if (ch !== "unknown") typingMap.set(ch, baseName, { persistent: true });
|
|
1189
1745
|
}
|
|
1190
1746
|
const seenConvIds = /* @__PURE__ */ new Set();
|
|
1191
|
-
for (const msg of
|
|
1747
|
+
for (const msg of messages2) {
|
|
1192
1748
|
if (msg.payload.conversationId && !seenConvIds.has(msg.payload.conversationId)) {
|
|
1193
1749
|
seenConvIds.add(msg.payload.conversationId);
|
|
1194
1750
|
typingMap.set(`volute:${msg.payload.conversationId}`, baseName, { persistent: true });
|
|
@@ -1209,10 +1765,10 @@ var DeliveryManager = class {
|
|
|
1209
1765
|
try {
|
|
1210
1766
|
const db = await getDb();
|
|
1211
1767
|
await db.delete(deliveryQueue).where(
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1768
|
+
and3(
|
|
1769
|
+
eq3(deliveryQueue.mind, baseName),
|
|
1770
|
+
eq3(deliveryQueue.session, session),
|
|
1771
|
+
eq3(deliveryQueue.status, "pending")
|
|
1216
1772
|
)
|
|
1217
1773
|
);
|
|
1218
1774
|
} catch (err) {
|
|
@@ -1310,24 +1866,24 @@ var DeliveryManager = class {
|
|
|
1310
1866
|
flushBatch(mindName, session, extra, interruptOverride) {
|
|
1311
1867
|
const bufferKey = `${mindName}:${session}`;
|
|
1312
1868
|
const buffer = this.batchBuffers.get(bufferKey);
|
|
1313
|
-
const
|
|
1869
|
+
const messages2 = [];
|
|
1314
1870
|
if (buffer) {
|
|
1315
1871
|
if (buffer.debounceTimer) clearTimeout(buffer.debounceTimer);
|
|
1316
1872
|
if (buffer.maxWaitTimer) clearTimeout(buffer.maxWaitTimer);
|
|
1317
1873
|
buffer.debounceTimer = null;
|
|
1318
1874
|
buffer.maxWaitTimer = null;
|
|
1319
|
-
|
|
1875
|
+
messages2.push(...buffer.messages.splice(0));
|
|
1320
1876
|
this.batchBuffers.delete(bufferKey);
|
|
1321
1877
|
}
|
|
1322
|
-
if (extra)
|
|
1323
|
-
if (
|
|
1878
|
+
if (extra) messages2.push(...extra);
|
|
1879
|
+
if (messages2.length === 0) return;
|
|
1324
1880
|
const [baseName] = mindName.split("@", 2);
|
|
1325
1881
|
const config = getRoutingConfig(baseName);
|
|
1326
1882
|
const sessionConfig = resolveDeliveryMode(config, session);
|
|
1327
1883
|
dlog2.info(
|
|
1328
|
-
`flushing batch for ${mindName}/${session}: ${
|
|
1884
|
+
`flushing batch for ${mindName}/${session}: ${messages2.length} messages${interruptOverride ? " (new-speaker interrupt)" : ""}`
|
|
1329
1885
|
);
|
|
1330
|
-
this.deliverBatchToMind(mindName, session,
|
|
1886
|
+
this.deliverBatchToMind(mindName, session, messages2, sessionConfig, interruptOverride).catch(
|
|
1331
1887
|
(err) => {
|
|
1332
1888
|
dlog2.warn(`failed to flush batch for ${mindName}/${session}`, logger_default.errorData(err));
|
|
1333
1889
|
}
|
|
@@ -1338,14 +1894,14 @@ var DeliveryManager = class {
|
|
|
1338
1894
|
await this.persistToQueue(baseName, session, payload, "gated");
|
|
1339
1895
|
try {
|
|
1340
1896
|
const db = await getDb();
|
|
1341
|
-
const
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
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")
|
|
1346
1902
|
)
|
|
1347
1903
|
);
|
|
1348
|
-
if ((
|
|
1904
|
+
if ((count2[0]?.count ?? 0) <= 1) {
|
|
1349
1905
|
await this.sendInviteNotification(mindName, payload);
|
|
1350
1906
|
}
|
|
1351
1907
|
} catch (err) {
|
|
@@ -1397,6 +1953,72 @@ var DeliveryManager = class {
|
|
|
1397
1953
|
);
|
|
1398
1954
|
}
|
|
1399
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
|
+
}
|
|
1400
2022
|
incrementActive(mind, session, senders, channels) {
|
|
1401
2023
|
let mindSessions = this.sessionStates.get(mind);
|
|
1402
2024
|
if (!mindSessions) {
|
|
@@ -1408,7 +2030,8 @@ var DeliveryManager = class {
|
|
|
1408
2030
|
lastDeliveredAt: 0,
|
|
1409
2031
|
lastDeliverySenders: /* @__PURE__ */ new Set(),
|
|
1410
2032
|
lastDeliveryChannels: /* @__PURE__ */ new Set(),
|
|
1411
|
-
lastInterruptAt: 0
|
|
2033
|
+
lastInterruptAt: 0,
|
|
2034
|
+
seenChannelProfiles: /* @__PURE__ */ new Set()
|
|
1412
2035
|
};
|
|
1413
2036
|
state.activeCount++;
|
|
1414
2037
|
state.lastDeliveredAt = Date.now();
|
|
@@ -1731,16 +2354,16 @@ async function ensureMailAddress(mindName) {
|
|
|
1731
2354
|
}
|
|
1732
2355
|
|
|
1733
2356
|
// src/lib/daemon/scheduler.ts
|
|
1734
|
-
import { resolve as
|
|
2357
|
+
import { resolve as resolve6 } from "path";
|
|
1735
2358
|
import { CronExpressionParser } from "cron-parser";
|
|
1736
|
-
var
|
|
2359
|
+
var slog2 = logger_default.child("scheduler");
|
|
1737
2360
|
var Scheduler = class {
|
|
1738
2361
|
schedules = /* @__PURE__ */ new Map();
|
|
1739
2362
|
interval = null;
|
|
1740
2363
|
lastFired = /* @__PURE__ */ new Map();
|
|
1741
2364
|
// "mind:scheduleId" → epoch minute
|
|
1742
2365
|
get statePath() {
|
|
1743
|
-
return
|
|
2366
|
+
return resolve6(voluteHome(), "scheduler-state.json");
|
|
1744
2367
|
}
|
|
1745
2368
|
start() {
|
|
1746
2369
|
this.loadState();
|
|
@@ -1799,7 +2422,7 @@ var Scheduler = class {
|
|
|
1799
2422
|
prevMinute = Math.floor(prev.getTime() / 6e4);
|
|
1800
2423
|
cronCache.set(schedule.cron, prevMinute);
|
|
1801
2424
|
} catch (err) {
|
|
1802
|
-
|
|
2425
|
+
slog2.warn(`invalid cron "${schedule.cron}" for ${mind}:${schedule.id}`, logger_default.errorData(err));
|
|
1803
2426
|
return false;
|
|
1804
2427
|
}
|
|
1805
2428
|
}
|
|
@@ -1813,11 +2436,11 @@ var Scheduler = class {
|
|
|
1813
2436
|
try {
|
|
1814
2437
|
let text;
|
|
1815
2438
|
if (schedule.script) {
|
|
1816
|
-
const homeDir =
|
|
2439
|
+
const homeDir = resolve6(mindDir(mindName), "home");
|
|
1817
2440
|
try {
|
|
1818
2441
|
const output = await this.runScript(schedule.script, homeDir, mindName);
|
|
1819
2442
|
if (!output.trim()) {
|
|
1820
|
-
|
|
2443
|
+
slog2.info(`fired script "${schedule.id}" for ${mindName} (no output)`);
|
|
1821
2444
|
return;
|
|
1822
2445
|
}
|
|
1823
2446
|
text = output;
|
|
@@ -1825,12 +2448,12 @@ var Scheduler = class {
|
|
|
1825
2448
|
const stderr = err.stderr ?? "";
|
|
1826
2449
|
text = `[script error] ${err.message}${stderr ? `
|
|
1827
2450
|
${stderr}` : ""}`;
|
|
1828
|
-
|
|
2451
|
+
slog2.warn(`script "${schedule.id}" failed for ${mindName}`, logger_default.errorData(err));
|
|
1829
2452
|
}
|
|
1830
2453
|
} else if (schedule.message) {
|
|
1831
2454
|
text = schedule.message;
|
|
1832
2455
|
} else {
|
|
1833
|
-
|
|
2456
|
+
slog2.warn(`schedule "${schedule.id}" for ${mindName} has no message or script`);
|
|
1834
2457
|
return;
|
|
1835
2458
|
}
|
|
1836
2459
|
await this.deliver(mindName, {
|
|
@@ -1838,9 +2461,9 @@ ${stderr}` : ""}`;
|
|
|
1838
2461
|
channel: "system:scheduler",
|
|
1839
2462
|
sender: schedule.id
|
|
1840
2463
|
});
|
|
1841
|
-
|
|
2464
|
+
slog2.info(`fired "${schedule.id}" for ${mindName}`);
|
|
1842
2465
|
} catch (err) {
|
|
1843
|
-
|
|
2466
|
+
slog2.warn(`failed to fire "${schedule.id}" for ${mindName}`, logger_default.errorData(err));
|
|
1844
2467
|
}
|
|
1845
2468
|
}
|
|
1846
2469
|
runScript(script, cwd, mindName) {
|
|
@@ -1863,7 +2486,7 @@ function getScheduler() {
|
|
|
1863
2486
|
|
|
1864
2487
|
// src/lib/daemon/token-budget.ts
|
|
1865
2488
|
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
1866
|
-
import { resolve as
|
|
2489
|
+
import { resolve as resolve7 } from "path";
|
|
1867
2490
|
var tlog = logger_default.child("token-budget");
|
|
1868
2491
|
var DEFAULT_BUDGET_PERIOD_MINUTES = 60;
|
|
1869
2492
|
var MAX_QUEUE_SIZE = 100;
|
|
@@ -1937,9 +2560,9 @@ var TokenBudget = class {
|
|
|
1937
2560
|
drain(mind) {
|
|
1938
2561
|
const state = this.budgets.get(mind);
|
|
1939
2562
|
if (!state) return [];
|
|
1940
|
-
const
|
|
2563
|
+
const messages2 = state.queue;
|
|
1941
2564
|
state.queue = [];
|
|
1942
|
-
return
|
|
2565
|
+
return messages2;
|
|
1943
2566
|
}
|
|
1944
2567
|
getUsage(mind) {
|
|
1945
2568
|
const state = this.budgets.get(mind);
|
|
@@ -1981,7 +2604,7 @@ var TokenBudget = class {
|
|
|
1981
2604
|
this.dirty.clear();
|
|
1982
2605
|
}
|
|
1983
2606
|
budgetStatePath(mind) {
|
|
1984
|
-
return
|
|
2607
|
+
return resolve7(stateDir(mind), "budget.json");
|
|
1985
2608
|
}
|
|
1986
2609
|
saveBudgetState(mind, state) {
|
|
1987
2610
|
try {
|
|
@@ -2020,8 +2643,8 @@ var TokenBudget = class {
|
|
|
2020
2643
|
return null;
|
|
2021
2644
|
}
|
|
2022
2645
|
}
|
|
2023
|
-
async replay(mindName,
|
|
2024
|
-
const summary =
|
|
2646
|
+
async replay(mindName, messages2) {
|
|
2647
|
+
const summary = messages2.map((m) => {
|
|
2025
2648
|
const from = m.sender ? `[${m.sender}]` : "";
|
|
2026
2649
|
const ch = m.channel ? `(${m.channel})` : "";
|
|
2027
2650
|
return `${from}${ch} ${m.textContent}`;
|
|
@@ -2031,7 +2654,7 @@ var TokenBudget = class {
|
|
|
2031
2654
|
content: [
|
|
2032
2655
|
{
|
|
2033
2656
|
type: "text",
|
|
2034
|
-
text: `[Budget replay] ${
|
|
2657
|
+
text: `[Budget replay] ${messages2.length} queued message(s) from the previous budget period:
|
|
2035
2658
|
|
|
2036
2659
|
${summary}`
|
|
2037
2660
|
}
|
|
@@ -2039,11 +2662,11 @@ ${summary}`
|
|
|
2039
2662
|
channel: "system:budget-replay",
|
|
2040
2663
|
sender: "system"
|
|
2041
2664
|
});
|
|
2042
|
-
tlog.info(`replayed ${
|
|
2665
|
+
tlog.info(`replayed ${messages2.length} queued message(s) for ${mindName}`);
|
|
2043
2666
|
} catch (err) {
|
|
2044
2667
|
tlog.warn(`failed to replay for ${mindName}`, logger_default.errorData(err));
|
|
2045
2668
|
const state = this.budgets.get(mindName);
|
|
2046
|
-
if (state) state.queue.push(...
|
|
2669
|
+
if (state) state.queue.push(...messages2);
|
|
2047
2670
|
}
|
|
2048
2671
|
}
|
|
2049
2672
|
};
|
|
@@ -2078,6 +2701,15 @@ async function startMindFull(name) {
|
|
|
2078
2701
|
(err) => logger_default.error(`failed to ensure mail address for ${baseName}`, logger_default.errorData(err))
|
|
2079
2702
|
);
|
|
2080
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
|
+
}
|
|
2081
2713
|
if (config?.tokenBudget) {
|
|
2082
2714
|
getTokenBudget().setBudget(
|
|
2083
2715
|
baseName,
|
|
@@ -2122,7 +2754,7 @@ async function stopMindFull(name) {
|
|
|
2122
2754
|
}
|
|
2123
2755
|
|
|
2124
2756
|
// src/lib/daemon/sleep-manager.ts
|
|
2125
|
-
var
|
|
2757
|
+
var slog3 = logger_default.child("sleep");
|
|
2126
2758
|
function defaultState() {
|
|
2127
2759
|
return {
|
|
2128
2760
|
sleeping: false,
|
|
@@ -2158,7 +2790,7 @@ var SleepManager = class {
|
|
|
2158
2790
|
unsubActivity = null;
|
|
2159
2791
|
transitioning = /* @__PURE__ */ new Set();
|
|
2160
2792
|
get statePath() {
|
|
2161
|
-
return
|
|
2793
|
+
return resolve8(voluteHome(), "sleep-state.json");
|
|
2162
2794
|
}
|
|
2163
2795
|
start() {
|
|
2164
2796
|
this.loadState();
|
|
@@ -2181,7 +2813,7 @@ var SleepManager = class {
|
|
|
2181
2813
|
}
|
|
2182
2814
|
}
|
|
2183
2815
|
} catch (err) {
|
|
2184
|
-
|
|
2816
|
+
slog3.warn("failed to load sleep state", logger_default.errorData(err));
|
|
2185
2817
|
}
|
|
2186
2818
|
}
|
|
2187
2819
|
saveState() {
|
|
@@ -2193,7 +2825,7 @@ var SleepManager = class {
|
|
|
2193
2825
|
writeFileSync3(this.statePath, `${JSON.stringify(data, null, 2)}
|
|
2194
2826
|
`);
|
|
2195
2827
|
} catch (err) {
|
|
2196
|
-
|
|
2828
|
+
slog3.error("failed to save sleep state", logger_default.errorData(err));
|
|
2197
2829
|
}
|
|
2198
2830
|
}
|
|
2199
2831
|
// --- Public API ---
|
|
@@ -2240,7 +2872,7 @@ var SleepManager = class {
|
|
|
2240
2872
|
content: preSleepMsg
|
|
2241
2873
|
});
|
|
2242
2874
|
} catch (err) {
|
|
2243
|
-
|
|
2875
|
+
slog3.error(`failed to persist pre-sleep message for ${name}`, logger_default.errorData(err));
|
|
2244
2876
|
}
|
|
2245
2877
|
try {
|
|
2246
2878
|
await fetch(`http://127.0.0.1:${entry.port}/message`, {
|
|
@@ -2252,7 +2884,7 @@ var SleepManager = class {
|
|
|
2252
2884
|
})
|
|
2253
2885
|
});
|
|
2254
2886
|
} catch (err) {
|
|
2255
|
-
|
|
2887
|
+
slog3.warn(`failed to send pre-sleep message to ${name}`, logger_default.errorData(err));
|
|
2256
2888
|
}
|
|
2257
2889
|
await this.waitForIdle(name, 12e4);
|
|
2258
2890
|
await new Promise((r) => setTimeout(r, 3e3));
|
|
@@ -2260,7 +2892,7 @@ var SleepManager = class {
|
|
|
2260
2892
|
await this.killOrphanOnPort(entry.port);
|
|
2261
2893
|
await this.archiveSessions(name);
|
|
2262
2894
|
this.markSleeping(name, opts);
|
|
2263
|
-
|
|
2895
|
+
slog3.info(`${name} is now sleeping`);
|
|
2264
2896
|
} finally {
|
|
2265
2897
|
this.transitioning.delete(name);
|
|
2266
2898
|
}
|
|
@@ -2286,7 +2918,7 @@ var SleepManager = class {
|
|
|
2286
2918
|
try {
|
|
2287
2919
|
await wakeMind(name);
|
|
2288
2920
|
} catch (err) {
|
|
2289
|
-
|
|
2921
|
+
slog3.error(`failed to wake ${name}`, logger_default.errorData(err));
|
|
2290
2922
|
return;
|
|
2291
2923
|
}
|
|
2292
2924
|
const entry = findMind(name);
|
|
@@ -2318,7 +2950,7 @@ var SleepManager = class {
|
|
|
2318
2950
|
content: summaryText
|
|
2319
2951
|
});
|
|
2320
2952
|
} catch (err) {
|
|
2321
|
-
|
|
2953
|
+
slog3.error(`failed to persist wake summary for ${name}`, logger_default.errorData(err));
|
|
2322
2954
|
}
|
|
2323
2955
|
try {
|
|
2324
2956
|
await fetch(`http://127.0.0.1:${entry.port}/message`, {
|
|
@@ -2330,16 +2962,16 @@ var SleepManager = class {
|
|
|
2330
2962
|
})
|
|
2331
2963
|
});
|
|
2332
2964
|
} catch (err) {
|
|
2333
|
-
|
|
2965
|
+
slog3.warn(`failed to deliver wake summary to ${name}`, logger_default.errorData(err));
|
|
2334
2966
|
}
|
|
2335
2967
|
const flushed = await this.flushQueuedMessages(name);
|
|
2336
2968
|
if (flushed > 0) {
|
|
2337
|
-
|
|
2969
|
+
slog3.info(`flushed ${flushed} queued message(s) for ${name}`);
|
|
2338
2970
|
}
|
|
2339
2971
|
if (!opts?.trigger) {
|
|
2340
2972
|
this.markAwake(name);
|
|
2341
2973
|
}
|
|
2342
|
-
|
|
2974
|
+
slog3.info(`${name} is now awake${opts?.trigger ? " (trigger wake)" : ""}`);
|
|
2343
2975
|
} finally {
|
|
2344
2976
|
this.transitioning.delete(name);
|
|
2345
2977
|
}
|
|
@@ -2394,20 +3026,20 @@ var SleepManager = class {
|
|
|
2394
3026
|
async flushQueuedMessages(name) {
|
|
2395
3027
|
try {
|
|
2396
3028
|
const db = await getDb();
|
|
2397
|
-
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();
|
|
2398
3030
|
if (rows.length === 0) return 0;
|
|
2399
|
-
const { deliverMessage: deliverMessage2 } = await import("./message-delivery-
|
|
3031
|
+
const { deliverMessage: deliverMessage2 } = await import("./message-delivery-S7BCNV6Y.js");
|
|
2400
3032
|
const delivered = [];
|
|
2401
3033
|
for (const row of rows) {
|
|
2402
3034
|
try {
|
|
2403
3035
|
await deliverMessage2(name, JSON.parse(row.payload));
|
|
2404
3036
|
delivered.push(row.id);
|
|
2405
3037
|
} catch (err) {
|
|
2406
|
-
|
|
3038
|
+
slog3.warn(`failed to flush queued message ${row.id} for ${name}`, logger_default.errorData(err));
|
|
2407
3039
|
}
|
|
2408
3040
|
}
|
|
2409
3041
|
if (delivered.length > 0) {
|
|
2410
|
-
await db.delete(deliveryQueue).where(
|
|
3042
|
+
await db.delete(deliveryQueue).where(inArray2(deliveryQueue.id, delivered));
|
|
2411
3043
|
}
|
|
2412
3044
|
const state = this.states.get(name);
|
|
2413
3045
|
if (state) {
|
|
@@ -2415,7 +3047,7 @@ var SleepManager = class {
|
|
|
2415
3047
|
}
|
|
2416
3048
|
return delivered.length;
|
|
2417
3049
|
} catch (err) {
|
|
2418
|
-
|
|
3050
|
+
slog3.warn(`failed to flush queued messages for ${name}`, logger_default.errorData(err));
|
|
2419
3051
|
return 0;
|
|
2420
3052
|
}
|
|
2421
3053
|
}
|
|
@@ -2443,7 +3075,7 @@ var SleepManager = class {
|
|
|
2443
3075
|
const interval = CronExpressionParser2.parse(config.schedule.wake);
|
|
2444
3076
|
return interval.next().toDate().toISOString();
|
|
2445
3077
|
} catch (err) {
|
|
2446
|
-
|
|
3078
|
+
slog3.warn(`invalid wake cron "${config.schedule.wake}"`, logger_default.errorData(err));
|
|
2447
3079
|
return null;
|
|
2448
3080
|
}
|
|
2449
3081
|
}
|
|
@@ -2460,7 +3092,7 @@ var SleepManager = class {
|
|
|
2460
3092
|
const wakeAt = new Date(state.voluntaryWakeAt);
|
|
2461
3093
|
if (now >= wakeAt) {
|
|
2462
3094
|
this.initiateWake(entry.name).catch(
|
|
2463
|
-
(err) =>
|
|
3095
|
+
(err) => slog3.error(`failed voluntary wake for ${entry.name}`, logger_default.errorData(err))
|
|
2464
3096
|
);
|
|
2465
3097
|
continue;
|
|
2466
3098
|
}
|
|
@@ -2469,7 +3101,7 @@ var SleepManager = class {
|
|
|
2469
3101
|
const wakeAt = new Date(state.scheduledWakeAt);
|
|
2470
3102
|
if (now >= wakeAt) {
|
|
2471
3103
|
this.initiateWake(entry.name).catch(
|
|
2472
|
-
(err) =>
|
|
3104
|
+
(err) => slog3.error(`failed scheduled wake for ${entry.name}`, logger_default.errorData(err))
|
|
2473
3105
|
);
|
|
2474
3106
|
continue;
|
|
2475
3107
|
}
|
|
@@ -2477,7 +3109,7 @@ var SleepManager = class {
|
|
|
2477
3109
|
if (!state?.sleeping && entry.running) {
|
|
2478
3110
|
if (this.shouldSleep(config.schedule.sleep, epochMinute)) {
|
|
2479
3111
|
this.initiateSleep(entry.name).catch(
|
|
2480
|
-
(err) =>
|
|
3112
|
+
(err) => slog3.error(`failed to initiate sleep for ${entry.name}`, logger_default.errorData(err))
|
|
2481
3113
|
);
|
|
2482
3114
|
}
|
|
2483
3115
|
}
|
|
@@ -2490,22 +3122,22 @@ var SleepManager = class {
|
|
|
2490
3122
|
const prevMinute = Math.floor(prev.getTime() / 6e4);
|
|
2491
3123
|
return prevMinute === epochMinute;
|
|
2492
3124
|
} catch (err) {
|
|
2493
|
-
|
|
3125
|
+
slog3.warn(`invalid sleep cron "${cronExpr}"`, logger_default.errorData(err));
|
|
2494
3126
|
return false;
|
|
2495
3127
|
}
|
|
2496
3128
|
}
|
|
2497
3129
|
async waitForIdle(name, timeoutMs) {
|
|
2498
|
-
return new Promise((
|
|
3130
|
+
return new Promise((resolve9) => {
|
|
2499
3131
|
const timeout = setTimeout(() => {
|
|
2500
3132
|
unsub();
|
|
2501
|
-
|
|
3133
|
+
resolve9();
|
|
2502
3134
|
}, timeoutMs);
|
|
2503
3135
|
const unsub = subscribe((event) => {
|
|
2504
3136
|
if (event.mind !== name) return;
|
|
2505
3137
|
if (event.type === "mind_done" || event.type === "mind_idle") {
|
|
2506
3138
|
clearTimeout(timeout);
|
|
2507
3139
|
unsub();
|
|
2508
|
-
|
|
3140
|
+
resolve9();
|
|
2509
3141
|
}
|
|
2510
3142
|
});
|
|
2511
3143
|
});
|
|
@@ -2513,34 +3145,34 @@ var SleepManager = class {
|
|
|
2513
3145
|
async archiveSessions(name) {
|
|
2514
3146
|
const dir = mindDir(name);
|
|
2515
3147
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 16);
|
|
2516
|
-
const sessionsDir =
|
|
3148
|
+
const sessionsDir = resolve8(dir, ".mind", "sessions");
|
|
2517
3149
|
if (existsSync5(sessionsDir)) {
|
|
2518
|
-
const archiveDir =
|
|
3150
|
+
const archiveDir = resolve8(sessionsDir, "archive");
|
|
2519
3151
|
mkdirSync3(archiveDir, { recursive: true });
|
|
2520
3152
|
for (const file of readdirSync2(sessionsDir)) {
|
|
2521
3153
|
if (file === "archive" || !file.endsWith(".json")) continue;
|
|
2522
|
-
const src =
|
|
3154
|
+
const src = resolve8(sessionsDir, file);
|
|
2523
3155
|
const base = file.replace(/\.json$/, "");
|
|
2524
|
-
const dest =
|
|
3156
|
+
const dest = resolve8(archiveDir, `${base}-${timestamp}.json`);
|
|
2525
3157
|
try {
|
|
2526
3158
|
renameSync(src, dest);
|
|
2527
3159
|
} catch (err) {
|
|
2528
|
-
|
|
3160
|
+
slog3.warn(`failed to archive session ${file} for ${name}`, logger_default.errorData(err));
|
|
2529
3161
|
}
|
|
2530
3162
|
}
|
|
2531
3163
|
}
|
|
2532
|
-
const piSessionsDir =
|
|
3164
|
+
const piSessionsDir = resolve8(dir, ".mind", "pi-sessions");
|
|
2533
3165
|
if (existsSync5(piSessionsDir)) {
|
|
2534
|
-
const archiveDir =
|
|
3166
|
+
const archiveDir = resolve8(piSessionsDir, "archive");
|
|
2535
3167
|
mkdirSync3(archiveDir, { recursive: true });
|
|
2536
3168
|
for (const entry of readdirSync2(piSessionsDir, { withFileTypes: true })) {
|
|
2537
3169
|
if (entry.name === "archive" || !entry.isDirectory()) continue;
|
|
2538
|
-
const src =
|
|
2539
|
-
const dest =
|
|
3170
|
+
const src = resolve8(piSessionsDir, entry.name);
|
|
3171
|
+
const dest = resolve8(archiveDir, `${entry.name}-${timestamp}`);
|
|
2540
3172
|
try {
|
|
2541
3173
|
renameSync(src, dest);
|
|
2542
3174
|
} catch (err) {
|
|
2543
|
-
|
|
3175
|
+
slog3.warn(`failed to archive pi-session ${entry.name} for ${name}`, logger_default.errorData(err));
|
|
2544
3176
|
}
|
|
2545
3177
|
}
|
|
2546
3178
|
}
|
|
@@ -2548,18 +3180,18 @@ var SleepManager = class {
|
|
|
2548
3180
|
async buildQueuedSummary(name) {
|
|
2549
3181
|
try {
|
|
2550
3182
|
const db = await getDb();
|
|
2551
|
-
const rows = await db.select({ channel: deliveryQueue.channel }).from(deliveryQueue).where(
|
|
2552
|
-
if (rows.length === 0) return "No messages while you slept.";
|
|
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.";
|
|
2553
3185
|
const channelCounts = /* @__PURE__ */ new Map();
|
|
2554
3186
|
for (const row of rows) {
|
|
2555
3187
|
const ch = row.channel ?? "unknown";
|
|
2556
3188
|
channelCounts.set(ch, (channelCounts.get(ch) ?? 0) + 1);
|
|
2557
3189
|
}
|
|
2558
|
-
const parts = [...channelCounts.entries()].map(([ch,
|
|
2559
|
-
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.`;
|
|
2560
3192
|
} catch (err) {
|
|
2561
|
-
|
|
2562
|
-
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.";
|
|
2563
3195
|
}
|
|
2564
3196
|
}
|
|
2565
3197
|
/**
|
|
@@ -2573,7 +3205,7 @@ var SleepManager = class {
|
|
|
2573
3205
|
} catch {
|
|
2574
3206
|
return;
|
|
2575
3207
|
}
|
|
2576
|
-
|
|
3208
|
+
slog3.warn(`orphan process found on port ${port} after sleep, killing`);
|
|
2577
3209
|
const execFileAsync = promisify(execFile);
|
|
2578
3210
|
try {
|
|
2579
3211
|
const { stdout } = await execFileAsync("lsof", ["-ti", `:${port}`, "-sTCP:LISTEN"]);
|
|
@@ -2584,7 +3216,7 @@ var SleepManager = class {
|
|
|
2584
3216
|
process.kill(pid, "SIGTERM");
|
|
2585
3217
|
} catch (err) {
|
|
2586
3218
|
if (err.code !== "ESRCH") {
|
|
2587
|
-
|
|
3219
|
+
slog3.warn(`failed to kill orphan pid ${pid}`, logger_default.errorData(err));
|
|
2588
3220
|
}
|
|
2589
3221
|
}
|
|
2590
3222
|
}
|
|
@@ -2616,7 +3248,7 @@ var SleepManager = class {
|
|
|
2616
3248
|
}
|
|
2617
3249
|
}
|
|
2618
3250
|
} catch (err) {
|
|
2619
|
-
|
|
3251
|
+
slog3.warn(`failed to kill orphan on port ${port} via /proc`, logger_default.errorData(err));
|
|
2620
3252
|
}
|
|
2621
3253
|
}
|
|
2622
3254
|
await new Promise((r) => setTimeout(r, 1e3));
|
|
@@ -2626,7 +3258,7 @@ var SleepManager = class {
|
|
|
2626
3258
|
if (!state?.sleeping || !state.wokenByTrigger) return;
|
|
2627
3259
|
if (this.transitioning.has(event.mind)) return;
|
|
2628
3260
|
if (event.type === "mind_idle") {
|
|
2629
|
-
|
|
3261
|
+
slog3.info(`${event.mind} going back to sleep after trigger wake`);
|
|
2630
3262
|
state.wokenByTrigger = false;
|
|
2631
3263
|
this.transitioning.add(event.mind);
|
|
2632
3264
|
sleepMind(event.mind).then(() => this.archiveSessions(event.mind)).then(() => {
|
|
@@ -2635,9 +3267,9 @@ var SleepManager = class {
|
|
|
2635
3267
|
const sleepConfig = this.getSleepConfig(event.mind);
|
|
2636
3268
|
state.scheduledWakeAt = this.getNextWakeTime(sleepConfig);
|
|
2637
3269
|
this.saveState();
|
|
2638
|
-
|
|
3270
|
+
slog3.info(`${event.mind} returned to sleep`);
|
|
2639
3271
|
}).catch((err) => {
|
|
2640
|
-
|
|
3272
|
+
slog3.error(`failed to return ${event.mind} to sleep`, logger_default.errorData(err));
|
|
2641
3273
|
}).finally(() => {
|
|
2642
3274
|
this.transitioning.delete(event.mind);
|
|
2643
3275
|
});
|
|
@@ -2661,6 +3293,21 @@ function getSleepManagerIfReady() {
|
|
|
2661
3293
|
export {
|
|
2662
3294
|
initConnectorManager,
|
|
2663
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,
|
|
2664
3311
|
stopAllWatchers,
|
|
2665
3312
|
getCachedSites,
|
|
2666
3313
|
getCachedRecentPages,
|
|
@@ -2677,8 +3324,31 @@ export {
|
|
|
2677
3324
|
getSleepManagerIfReady,
|
|
2678
3325
|
subscribe2 as subscribe,
|
|
2679
3326
|
publish2 as publish,
|
|
3327
|
+
getWebhookUrl,
|
|
3328
|
+
getAuthHeaders,
|
|
3329
|
+
fireWebhook,
|
|
3330
|
+
initWebhook,
|
|
2680
3331
|
subscribe3 as subscribe2,
|
|
2681
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,
|
|
2682
3352
|
getTypingMap,
|
|
2683
3353
|
publishTypingForChannels,
|
|
2684
3354
|
extractTextContent,
|