volute 0.22.0 → 0.24.0

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