volute 0.25.0 → 0.27.0

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