volute 0.23.0 → 0.25.0

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