volute 0.24.0 → 0.26.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 (114) hide show
  1. package/README.md +15 -20
  2. package/dist/{activity-events-4O37J7PD.js → activity-events-ZMBAKLUF.js} +2 -2
  3. package/dist/api.d.ts +590 -10
  4. package/dist/{auth-HM2RSPY7.js → auth-4TV573WE.js} +2 -2
  5. package/dist/{channel-HZOSHGNF.js → channel-ZVZV42UD.js} +3 -3
  6. package/dist/{chunk-NOBRGACV.js → chunk-2VO7453N.js} +56 -19
  7. package/dist/{chunk-OOW675I3.js → chunk-3CFRE2VC.js} +931 -775
  8. package/dist/{chunk-PHHKNGA3.js → chunk-3TV4GLFO.js} +2 -2
  9. package/dist/{chunk-4TJ72QQ3.js → chunk-5Y3PBKW6.js} +3 -3
  10. package/dist/{chunk-BFK6SOEJ.js → chunk-J2CO4WEV.js} +1 -1
  11. package/dist/{chunk-TQDITGES.js → chunk-LX22GRG7.js} +10 -13
  12. package/dist/{chunk-E7GOKNOT.js → chunk-NWI2425I.js} +1 -1
  13. package/dist/{chunk-2767L2RZ.js → chunk-OZFKBXD6.js} +1 -1
  14. package/dist/{chunk-XLC342FO.js → chunk-SIAG3QMM.js} +14 -1
  15. package/dist/{chunk-RVKR2R7F.js → chunk-SSI47XP2.js} +10 -2
  16. package/dist/chunk-TZKJLDQN.js +78 -0
  17. package/dist/{chunk-P3W36ZGD.js → chunk-USNBKHYG.js} +33 -5
  18. package/dist/chunk-UTL75LP6.js +113 -0
  19. package/dist/{chunk-3AIBT4TW.js → chunk-V63B7DX3.js} +24 -1
  20. package/dist/{chunk-33XAVCS4.js → chunk-WBHMQ5OZ.js} +49 -0
  21. package/dist/{chunk-TRQEV3CD.js → chunk-WGOGUMPO.js} +22 -3
  22. package/dist/chunk-XOXLRRR2.js +176 -0
  23. package/dist/{chunk-JTDFJWI2.js → chunk-YJA7P64S.js} +1 -1
  24. package/dist/chunk-ZYGKG6VC.js +22 -0
  25. package/dist/cli.js +44 -20
  26. package/dist/{cloud-sync-DIU3OCPV.js → cloud-sync-NI2K3C7G.js} +11 -9
  27. package/dist/{connector-M6XFI6GM.js → connector-G722WXAU.js} +4 -4
  28. package/dist/{create-VDQJER52.js → create-4YBRTTJS.js} +1 -1
  29. package/dist/{daemon-client-JOVQZ52X.js → daemon-client-Z7FAJ6JW.js} +1 -1
  30. package/dist/{daemon-restart-YMPEATQH.js → daemon-restart-BJZ3O4U4.js} +6 -5
  31. package/dist/daemon.js +982 -340
  32. package/dist/{delete-2MRR4JX5.js → delete-27OYNK25.js} +1 -1
  33. package/dist/{down-674SX2IZ.js → down-7UKFMJJZ.js} +4 -4
  34. package/dist/{env-2FPOZK37.js → env-M336ONDP.js} +4 -4
  35. package/dist/{export-IKFAPRAO.js → export-HP4G5DQC.js} +1 -1
  36. package/dist/{file-KT3UIQM3.js → file-HUDKTRAS.js} +3 -3
  37. package/dist/{history-46WZN5CN.js → history-B64GTFTD.js} +3 -3
  38. package/dist/{import-FRDPQPJ2.js → import-XIB7UV4S.js} +2 -2
  39. package/dist/{log-6SGSSR3D.js → log-PBFNILJ4.js} +3 -3
  40. package/dist/{login-UO6AOVEA.js → login-6U7U6BNG.js} +1 -1
  41. package/dist/login-B5E7N7MY.js +46 -0
  42. package/dist/logout-XSJRYS3U.js +39 -0
  43. package/dist/{logs-HRBONI5I.js → logs-3CART7O7.js} +3 -3
  44. package/dist/{merge-KSFJKX6T.js → merge-VK2HSKMA.js} +3 -3
  45. package/dist/{message-delivery-S7BCNV6Y.js → message-delivery-MS5JYPZX.js} +11 -9
  46. package/dist/{mind-KPLCRKQA.js → mind-HZ3QSDDJ.js} +17 -17
  47. package/dist/{mind-activity-tracker-NMDDEV3K.js → mind-activity-tracker-4G6FURY2.js} +3 -3
  48. package/dist/{mind-manager-ZNRIYEK3.js → mind-manager-VVK67AY3.js} +6 -4
  49. package/dist/{mind-sleep-GHPTSAYN.js → mind-sleep-DTV7L44D.js} +3 -3
  50. package/dist/{mind-wake-BJDJFMDF.js → mind-wake-PFN4FN3T.js} +3 -3
  51. package/dist/notes-37FW2UR2.js +230 -0
  52. package/dist/{package-S5YF25XV.js → package-VZWLXPHV.js} +3 -1
  53. package/dist/{pages-TWR6U7DS.js → pages-DIIT5HMQ.js} +1 -1
  54. package/dist/{publish-BZNHKUUK.js → publish-HQV7YREB.js} +4 -4
  55. package/dist/{pull-D32SPFVU.js → pull-2MB4SK3C.js} +3 -3
  56. package/dist/{register-U2UO6TC4.js → register-EFND67FQ.js} +1 -1
  57. package/dist/{restart-5BMNV7KU.js → restart-CCK7D6TV.js} +3 -3
  58. package/dist/sandbox-EHGFF52K.js +19 -0
  59. package/dist/{schedule-YEFDLVMJ.js → schedule-6F7ELB2M.js} +3 -3
  60. package/dist/{seed-6FEKB3YC.js → seed-E5OQGWX3.js} +1 -1
  61. package/dist/{send-IISDYFCL.js → send-IH6XZKPC.js} +6 -20
  62. package/dist/service-LLBV3R7M.js +122 -0
  63. package/dist/setup-F6TWFYGQ.js +371 -0
  64. package/dist/setup-YGAAIKKZ.js +17 -0
  65. package/dist/{shared-LWMNTTZN.js → shared-UMO4S7CC.js} +4 -4
  66. package/dist/{skill-BQOFACEI.js → skill-42LGFBQC.js} +13 -5
  67. package/dist/skills/dreaming/SKILL.md +68 -0
  68. package/dist/skills/dreaming/references/INSTALL.md +56 -0
  69. package/dist/skills/dreaming/scripts/dream.ts +289 -0
  70. package/dist/skills/dreaming/scripts/wake-context-dreams.sh +30 -0
  71. package/dist/skills/imagegen/SKILL.md +37 -0
  72. package/dist/skills/imagegen/references/INSTALL.md +13 -0
  73. package/dist/skills/imagegen/scripts/imagegen.ts +136 -0
  74. package/dist/skills/notes/SKILL.md +34 -0
  75. package/dist/skills/resonance/SKILL.md +73 -0
  76. package/dist/skills/resonance/assets/default-config.json +21 -0
  77. package/dist/skills/resonance/references/INSTALL.md +23 -0
  78. package/dist/skills/resonance/scripts/resonance.ts +1250 -0
  79. package/dist/skills/volute-mind/SKILL.md +23 -3
  80. package/dist/{sleep-manager-XXSWQQLE.js → sleep-manager-EE4NRN2Q.js} +11 -9
  81. package/dist/{sprout-CGSW4CF5.js → sprout-QL74KR2X.js} +5 -5
  82. package/dist/{start-C7XITZ5O.js → start-O5JQASRC.js} +3 -3
  83. package/dist/{status-SIRPLEZC.js → status-FZBEBM7Q.js} +3 -3
  84. package/dist/{status-LYS4NUOZ.js → status-WXD4HXRL.js} +3 -3
  85. package/dist/{stop-CVKBSLXY.js → stop-2SOG5NYF.js} +3 -3
  86. package/dist/up-SDMCSVI3.js +17 -0
  87. package/dist/{update-7XCZMYBT.js → update-5VUDAI3D.js} +6 -6
  88. package/dist/{upgrade-7RUIXGOO.js → upgrade-QCCO33BK.js} +1 -1
  89. package/dist/{variant-UGREB4G5.js → variant-WWLDY6D5.js} +4 -4
  90. package/dist/{version-notify-SZ75QRGO.js → version-notify-USFZBWMG.js} +11 -9
  91. package/dist/web-assets/assets/index-CUQ31ieL.js +69 -0
  92. package/dist/web-assets/assets/index-CW8NSl1o.css +1 -0
  93. package/dist/web-assets/favicon.png +0 -0
  94. package/dist/web-assets/index.html +5 -4
  95. package/dist/web-assets/logo.png +0 -0
  96. package/drizzle/0015_notes.sql +23 -0
  97. package/drizzle/0016_note_reactions_and_replies.sql +15 -0
  98. package/drizzle/meta/_journal.json +14 -0
  99. package/package.json +3 -1
  100. package/templates/_base/.init/.config/hooks/wake-context.sh +7 -0
  101. package/templates/_base/home/public/.gitkeep +0 -0
  102. package/templates/_base/src/lib/startup.ts +8 -0
  103. package/templates/claude/src/agent.ts +51 -1
  104. package/templates/claude/src/server.ts +1 -0
  105. package/templates/pi/package.json.tmpl +1 -0
  106. package/templates/pi/src/agent.ts +48 -1
  107. package/templates/pi/src/lib/subagents.ts +150 -0
  108. package/templates/pi/src/server.ts +1 -0
  109. package/dist/chunk-NWPT4ASZ.js +0 -89
  110. package/dist/service-FASYWLTC.js +0 -247
  111. package/dist/setup-BMLM2UTK.js +0 -230
  112. package/dist/up-OMHACRJL.js +0 -15
  113. package/dist/web-assets/assets/index-Bx9WDoaQ.js +0 -69
  114. package/dist/web-assets/assets/index-Clz8OhmJ.css +0 -1
@@ -4,26 +4,26 @@ import {
4
4
  } from "./chunk-HFCBO2GL.js";
5
5
  import {
6
6
  markIdle
7
- } from "./chunk-E7GOKNOT.js";
7
+ } from "./chunk-NWI2425I.js";
8
8
  import {
9
+ broadcast,
9
10
  publish,
10
11
  subscribe
11
- } from "./chunk-BFK6SOEJ.js";
12
+ } from "./chunk-J2CO4WEV.js";
12
13
  import {
13
14
  RestartTracker,
14
15
  RotatingLog,
15
16
  clearJsonMap,
16
17
  getMindManager,
18
+ getMindToken,
17
19
  getPrompt,
18
20
  loadJsonMap,
19
21
  saveJsonMap
20
- } from "./chunk-NOBRGACV.js";
22
+ } from "./chunk-2VO7453N.js";
21
23
  import {
22
- readVoluteConfig
23
- } from "./chunk-XLC342FO.js";
24
- import {
25
- loadMergedEnv
26
- } from "./chunk-PHU4DEAJ.js";
24
+ isSandboxEnabled,
25
+ wrapForSandbox
26
+ } from "./chunk-UTL75LP6.js";
27
27
  import {
28
28
  conversationParticipants,
29
29
  conversationReads,
@@ -33,18 +33,24 @@ import {
33
33
  messages,
34
34
  mindHistory,
35
35
  users
36
- } from "./chunk-33XAVCS4.js";
36
+ } from "./chunk-WBHMQ5OZ.js";
37
37
  import {
38
38
  logger_default
39
39
  } from "./chunk-YUIHSKR6.js";
40
+ import {
41
+ readVoluteConfig
42
+ } from "./chunk-SIAG3QMM.js";
43
+ import {
44
+ loadMergedEnv
45
+ } from "./chunk-PHU4DEAJ.js";
40
46
  import {
41
47
  exec
42
- } from "./chunk-JTDFJWI2.js";
48
+ } from "./chunk-YJA7P64S.js";
43
49
  import {
44
50
  chownMindDir,
45
51
  isIsolationEnabled,
46
52
  wrapForIsolation
47
- } from "./chunk-NWPT4ASZ.js";
53
+ } from "./chunk-XOXLRRR2.js";
48
54
  import {
49
55
  daemonLoopback,
50
56
  findMind,
@@ -56,7 +62,7 @@ import {
56
62
  } from "./chunk-B2CPS4QU.js";
57
63
 
58
64
  // src/lib/daemon/sleep-manager.ts
59
- import { execFile } from "child_process";
65
+ import { execFile, spawn as spawnChild } from "child_process";
60
66
  import {
61
67
  existsSync as existsSync5,
62
68
  mkdirSync as mkdirSync3,
@@ -69,11 +75,11 @@ import {
69
75
  import { resolve as resolve8 } from "path";
70
76
  import { promisify } from "util";
71
77
  import { CronExpressionParser as CronExpressionParser2 } from "cron-parser";
72
- import { and as and4, eq as eq4, inArray as inArray2 } from "drizzle-orm";
78
+ import { and as and4, eq as eq4, inArray as inArray3 } from "drizzle-orm";
73
79
 
74
80
  // src/lib/auth.ts
75
81
  import { compareSync, hashSync } from "bcryptjs";
76
- import { and, count, eq } from "drizzle-orm";
82
+ import { and, count, eq, inArray } from "drizzle-orm";
77
83
  var userSelectFields = {
78
84
  id: users.id,
79
85
  username: users.username,
@@ -131,7 +137,7 @@ async function getOrCreateMindUser(mindName) {
131
137
  const [result] = await db.insert(users).values({
132
138
  username: mindName,
133
139
  password_hash: "!mind",
134
- role: "mind",
140
+ role: "user",
135
141
  user_type: "mind"
136
142
  }).returning(userSelectFields);
137
143
  return result;
@@ -185,12 +191,20 @@ async function updateUserProfile(userId, profile) {
185
191
  }
186
192
  async function syncMindProfile(mindName, config) {
187
193
  const user = await getOrCreateMindUser(mindName);
188
- const db = await getDb();
189
- await db.update(users).set({
194
+ const newProfile = {
190
195
  display_name: config.displayName ?? null,
191
196
  description: config.description ?? null,
192
197
  avatar: config.avatar ?? null
193
- }).where(eq(users.id, user.id));
198
+ };
199
+ const changed = user.display_name !== newProfile.display_name || user.description !== newProfile.description || user.avatar !== newProfile.avatar;
200
+ if (!changed) return;
201
+ const db = await getDb();
202
+ await db.update(users).set(newProfile).where(eq(users.id, user.id));
203
+ broadcast({ type: "profile_updated", mind: mindName, summary: `${mindName} profile updated` });
204
+ }
205
+ async function migrateMindRoles() {
206
+ const db = await getDb();
207
+ await db.update(users).set({ role: "user" }).where(and(eq(users.user_type, "mind"), inArray(users.role, ["mind", "agent"])));
194
208
  }
195
209
 
196
210
  // src/lib/pages-watcher.ts
@@ -231,16 +245,16 @@ function startPagesWatcher(mindName, pagesDir) {
231
245
  }
232
246
  function startWatcher(mindName) {
233
247
  if (watchers.has(mindName)) return;
234
- const pagesDir = resolve(mindDir(mindName), "home", "pages");
248
+ const pagesDir = resolve(mindDir(mindName), "home", "public", "pages");
235
249
  if (existsSync(pagesDir)) {
236
250
  startPagesWatcher(mindName, pagesDir);
237
251
  return;
238
252
  }
239
253
  if (homeWatchers.has(mindName)) return;
240
- const homeDir = resolve(mindDir(mindName), "home");
241
- if (!existsSync(homeDir)) return;
254
+ const publicDir = resolve(mindDir(mindName), "home", "public");
255
+ if (!existsSync(publicDir)) return;
242
256
  try {
243
- const hw = watch(homeDir, (_eventType, filename) => {
257
+ const hw = watch(publicDir, (_eventType, filename) => {
244
258
  if (filename !== "pages") return;
245
259
  if (!existsSync(pagesDir)) return;
246
260
  hw.close();
@@ -337,7 +351,7 @@ function buildSites() {
337
351
  }
338
352
  const entries = readRegistry();
339
353
  for (const entry of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
340
- const pagesDir = resolve(mindDir(entry.name), "home", "pages");
354
+ const pagesDir = resolve(mindDir(entry.name), "home", "public", "pages");
341
355
  if (!existsSync(pagesDir)) continue;
342
356
  const mindPages = scanPagesDir(pagesDir, `/pages/${entry.name}`);
343
357
  if (mindPages.length > 0) {
@@ -350,7 +364,7 @@ function buildRecentPages() {
350
364
  const entries = readRegistry();
351
365
  const pages = [];
352
366
  for (const entry of entries) {
353
- const pagesDir = resolve(mindDir(entry.name), "home", "pages");
367
+ const pagesDir = resolve(mindDir(entry.name), "home", "public", "pages");
354
368
  if (!existsSync(pagesDir)) continue;
355
369
  let items;
356
370
  try {
@@ -398,159 +412,605 @@ function getCachedRecentPages() {
398
412
  return recentPagesCache;
399
413
  }
400
414
 
401
- // src/lib/daemon/connector-manager.ts
402
- import { spawn } from "child_process";
403
- import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "fs";
404
- import { dirname, resolve as resolve3 } from "path";
415
+ // src/lib/events/conversations.ts
416
+ import { randomUUID } from "crypto";
417
+ import { and as and2, desc, eq as eq2, inArray as inArray2, isNull, lt, sql } from "drizzle-orm";
405
418
 
406
- // src/lib/connector-defs.ts
407
- import { existsSync as existsSync2, readFileSync } from "fs";
408
- import { resolve as resolve2 } from "path";
409
- var BUILTIN_DEFS = {
410
- discord: {
411
- displayName: "Discord",
412
- description: "Connect to Discord as a bot",
413
- envVars: [
414
- {
415
- name: "DISCORD_TOKEN",
416
- required: true,
417
- description: "Discord bot token",
418
- scope: "mind"
419
- },
420
- {
421
- name: "DISCORD_GUILD_ID",
422
- required: false,
423
- description: "Discord server ID (optional, for slash commands)",
424
- scope: "mind"
425
- }
426
- ]
427
- },
428
- slack: {
429
- displayName: "Slack",
430
- description: "Connect to Slack via Socket Mode",
431
- envVars: [
432
- {
433
- name: "SLACK_BOT_TOKEN",
434
- required: true,
435
- description: "Slack bot token (xoxb-...)",
436
- scope: "mind"
437
- },
438
- {
439
- name: "SLACK_APP_TOKEN",
440
- required: true,
441
- description: "Slack app-level token (xapp-...) for Socket Mode",
442
- scope: "mind"
443
- }
444
- ]
445
- },
446
- telegram: {
447
- displayName: "Telegram",
448
- description: "Connect to Telegram via long polling",
449
- envVars: [
450
- {
451
- name: "TELEGRAM_BOT_TOKEN",
452
- required: true,
453
- description: "Telegram bot token from BotFather",
454
- scope: "mind"
419
+ // src/lib/webhook.ts
420
+ var slog = logger_default.child("webhook");
421
+ function getWebhookUrl() {
422
+ return process.env.VOLUTE_WEBHOOK_URL;
423
+ }
424
+ function getAuthHeaders() {
425
+ const headers = { "Content-Type": "application/json" };
426
+ const secret = process.env.VOLUTE_WEBHOOK_SECRET;
427
+ if (secret) headers.Authorization = `Bearer ${secret}`;
428
+ return headers;
429
+ }
430
+ function fireWebhook(event) {
431
+ try {
432
+ const url = getWebhookUrl();
433
+ if (!url) return;
434
+ const payload = { ...event, timestamp: event.timestamp ?? (/* @__PURE__ */ new Date()).toISOString() };
435
+ fetch(url, {
436
+ method: "POST",
437
+ headers: getAuthHeaders(),
438
+ body: JSON.stringify(payload)
439
+ }).then((res) => {
440
+ if (!res.ok) {
441
+ slog.warn(`webhook ${event.event} returned HTTP ${res.status}`);
455
442
  }
456
- ]
443
+ }).catch((err) => {
444
+ slog.warn(`webhook delivery failed for ${event.event}`, logger_default.errorData(err));
445
+ });
446
+ } catch (err) {
447
+ slog.error(`webhook ${event.event} failed to serialize`, logger_default.errorData(err));
457
448
  }
458
- };
459
- function getConnectorDef(type, connectorDir) {
460
- if (BUILTIN_DEFS[type]) return BUILTIN_DEFS[type];
461
- if (connectorDir) {
462
- const jsonPath = resolve2(connectorDir, "connector.json");
463
- if (existsSync2(jsonPath)) {
464
- try {
465
- return JSON.parse(readFileSync(jsonPath, "utf-8"));
466
- } catch (err) {
467
- console.warn(`Failed to parse ${jsonPath}: ${err}`);
468
- return null;
469
- }
449
+ }
450
+ function initWebhook() {
451
+ const url = getWebhookUrl();
452
+ if (!url) return () => {
453
+ };
454
+ try {
455
+ const parsed = new URL(url);
456
+ if (!["http:", "https:"].includes(parsed.protocol)) {
457
+ slog.error(`VOLUTE_WEBHOOK_URL has unsupported protocol: ${parsed.protocol}`);
458
+ return () => {
459
+ };
470
460
  }
461
+ } catch {
462
+ slog.error(`VOLUTE_WEBHOOK_URL is not a valid URL`);
463
+ return () => {
464
+ };
471
465
  }
472
- return null;
473
- }
474
- function checkMissingEnvVars(def, env) {
475
- return def.envVars.filter((v) => v.required && !env[v.name]);
466
+ slog.info("webhook enabled");
467
+ return subscribe((event) => {
468
+ try {
469
+ fireWebhook({
470
+ event: event.type,
471
+ mind: event.mind,
472
+ data: { summary: event.summary, ...event.metadata },
473
+ timestamp: event.created_at
474
+ });
475
+ } catch (err) {
476
+ slog.error(`failed to fire webhook for ${event.type}`, logger_default.errorData(err));
477
+ }
478
+ });
476
479
  }
477
480
 
478
- // src/lib/daemon/connector-manager.ts
479
- var clog = logger_default.child("connectors");
480
- function searchUpwards(...segments) {
481
- let searchDir = dirname(new URL(import.meta.url).pathname);
482
- for (let i = 0; i < 5; i++) {
483
- const candidate = resolve3(searchDir, ...segments);
484
- if (existsSync3(candidate)) return candidate;
485
- searchDir = dirname(searchDir);
481
+ // src/lib/events/conversation-events.ts
482
+ var subscribers = /* @__PURE__ */ new Map();
483
+ function subscribe2(conversationId, callback) {
484
+ let set = subscribers.get(conversationId);
485
+ if (!set) {
486
+ set = /* @__PURE__ */ new Set();
487
+ subscribers.set(conversationId, set);
486
488
  }
487
- return null;
489
+ set.add(callback);
490
+ return () => {
491
+ set.delete(callback);
492
+ if (set.size === 0) subscribers.delete(conversationId);
493
+ };
488
494
  }
489
- var ConnectorManager = class {
490
- connectors = /* @__PURE__ */ new Map();
491
- stopping = /* @__PURE__ */ new Set();
492
- // "mind:type" keys currently being explicitly stopped
493
- shuttingDown = false;
494
- restartTracker = new RestartTracker();
495
- async startConnectors(mindName, mindDir2, mindPort, daemonPort) {
496
- const config = readVoluteConfig(mindDir2) ?? {};
497
- const types = config.connectors ?? [];
498
- await Promise.all(
499
- types.map(
500
- (type) => this.startConnector(mindName, mindDir2, mindPort, type, daemonPort).catch((err) => {
501
- clog.warn(`failed to start connector ${type} for ${mindName}`, logger_default.errorData(err));
502
- })
503
- )
504
- );
505
- }
506
- checkConnectorEnv(type, mindName, mindDir2) {
507
- const mindConnectorDir = resolve3(mindDir2, "connectors", type);
508
- const userConnectorDir = resolve3(voluteHome(), "connectors", type);
509
- const connectorDir = existsSync3(mindConnectorDir) ? mindConnectorDir : existsSync3(userConnectorDir) ? userConnectorDir : void 0;
510
- const def = getConnectorDef(type, connectorDir);
511
- if (!def) return null;
512
- const env = loadMergedEnv(mindName);
513
- const missing = checkMissingEnvVars(def, env);
514
- if (missing.length === 0) return null;
515
- return {
516
- missing: missing.map((v) => ({ name: v.name, description: v.description })),
517
- connectorName: def.displayName
518
- };
495
+ function publish2(conversationId, event) {
496
+ const set = subscribers.get(conversationId);
497
+ if (!set) return;
498
+ for (const cb of set) {
499
+ try {
500
+ cb(event);
501
+ } catch (err) {
502
+ console.error("[conversation-events] subscriber threw:", err);
503
+ set.delete(cb);
504
+ if (set.size === 0) subscribers.delete(conversationId);
505
+ }
519
506
  }
520
- async startConnector(mindName, mindDir2, mindPort, type, daemonPort) {
521
- const existing = this.connectors.get(mindName)?.get(type);
522
- if (existing) {
523
- await new Promise((res) => {
524
- existing.child.on("exit", () => res());
525
- try {
526
- if (existing.child.pid) {
527
- process.kill(-existing.child.pid, "SIGTERM");
528
- } else {
529
- existing.child.kill("SIGTERM");
530
- }
531
- } catch {
532
- res();
533
- }
534
- setTimeout(() => {
535
- try {
536
- if (existing.child.pid) {
537
- process.kill(-existing.child.pid, "SIGKILL");
538
- } else {
539
- existing.child.kill("SIGKILL");
540
- }
541
- } catch {
542
- }
543
- res();
544
- }, 3e3);
545
- });
546
- this.connectors.get(mindName)?.delete(type);
507
+ }
508
+
509
+ // src/lib/events/conversations.ts
510
+ async function createConversation(mindName, channel, opts) {
511
+ const db = await getDb();
512
+ const id = randomUUID();
513
+ const type = opts?.type ?? "dm";
514
+ const name = opts?.name ?? null;
515
+ await db.transaction(async (tx) => {
516
+ await tx.insert(conversations).values({
517
+ id,
518
+ mind_name: mindName,
519
+ channel,
520
+ type,
521
+ name,
522
+ user_id: opts?.userId ?? null,
523
+ title: opts?.title ?? null
524
+ });
525
+ if (opts?.participantIds && opts.participantIds.length > 0) {
526
+ await tx.insert(conversationParticipants).values(
527
+ opts.participantIds.map((uid, i) => ({
528
+ conversation_id: id,
529
+ user_id: uid,
530
+ role: i === 0 ? "owner" : "member"
531
+ }))
532
+ );
547
533
  }
548
- this.killOrphanConnector(mindName, type);
549
- const mindConnector = resolve3(mindDir2, "connectors", type, "index.ts");
550
- const userConnector = resolve3(voluteHome(), "connectors", type, "index.ts");
551
- const builtinConnector = this.resolveBuiltinConnector(type);
552
- let connectorScript;
553
- let runtime;
534
+ });
535
+ fireWebhook({
536
+ event: "conversation_created",
537
+ mind: mindName ?? "",
538
+ data: { id, mindName, channel, type, name, title: opts?.title ?? null }
539
+ });
540
+ return {
541
+ id,
542
+ mind_name: mindName,
543
+ channel,
544
+ type,
545
+ name,
546
+ user_id: opts?.userId ?? null,
547
+ title: opts?.title ?? null,
548
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
549
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
550
+ };
551
+ }
552
+ async function getConversation(id) {
553
+ const db = await getDb();
554
+ const row = await db.select().from(conversations).where(eq2(conversations.id, id)).get();
555
+ return row ?? null;
556
+ }
557
+ async function addParticipant(conversationId, userId, role = "member") {
558
+ const db = await getDb();
559
+ await db.insert(conversationParticipants).values({
560
+ conversation_id: conversationId,
561
+ user_id: userId,
562
+ role
563
+ });
564
+ }
565
+ async function removeParticipant(conversationId, userId) {
566
+ const db = await getDb();
567
+ await db.delete(conversationParticipants).where(
568
+ and2(
569
+ eq2(conversationParticipants.conversation_id, conversationId),
570
+ eq2(conversationParticipants.user_id, userId)
571
+ )
572
+ );
573
+ }
574
+ async function getParticipants(conversationId) {
575
+ const db = await getDb();
576
+ const rows = await db.select({
577
+ userId: conversationParticipants.user_id,
578
+ username: users.username,
579
+ userType: users.user_type,
580
+ role: conversationParticipants.role,
581
+ displayName: users.display_name,
582
+ description: users.description,
583
+ avatar: users.avatar
584
+ }).from(conversationParticipants).innerJoin(users, eq2(conversationParticipants.user_id, users.id)).where(eq2(conversationParticipants.conversation_id, conversationId)).all();
585
+ return rows;
586
+ }
587
+ async function isParticipant(conversationId, userId) {
588
+ const db = await getDb();
589
+ const row = await db.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(
590
+ and2(
591
+ eq2(conversationParticipants.conversation_id, conversationId),
592
+ eq2(conversationParticipants.user_id, userId)
593
+ )
594
+ ).get();
595
+ return row != null;
596
+ }
597
+ async function listConversationsForUser(userId) {
598
+ const db = await getDb();
599
+ const participantRows = await db.select({ conversation_id: conversationParticipants.conversation_id }).from(conversationParticipants).where(eq2(conversationParticipants.user_id, userId)).all();
600
+ if (participantRows.length === 0) return [];
601
+ const convIds = participantRows.map((r) => r.conversation_id);
602
+ return await db.select().from(conversations).where(inArray2(conversations.id, convIds)).orderBy(desc(conversations.updated_at)).all();
603
+ }
604
+ async function isParticipantOrOwner(conversationId, userId) {
605
+ if (await isParticipant(conversationId, userId)) return true;
606
+ const db = await getDb();
607
+ const row = await db.select().from(conversations).where(and2(eq2(conversations.id, conversationId), eq2(conversations.user_id, userId))).get();
608
+ return row != null;
609
+ }
610
+ async function deleteConversationForUser(id, userId) {
611
+ if (!await isParticipantOrOwner(id, userId)) return false;
612
+ await deleteConversation(id);
613
+ return true;
614
+ }
615
+ async function addMessage(conversationId, role, senderName, content) {
616
+ const db = await getDb();
617
+ const serialized = JSON.stringify(content);
618
+ 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 });
619
+ await db.update(conversations).set({ updated_at: sql`datetime('now')` }).where(eq2(conversations.id, conversationId));
620
+ if (role === "user") {
621
+ const firstText = content.find((b) => b.type === "text");
622
+ const title = firstText ? firstText.text.slice(0, 80) : "";
623
+ if (title) {
624
+ await db.update(conversations).set({ title }).where(and2(eq2(conversations.id, conversationId), isNull(conversations.title)));
625
+ }
626
+ }
627
+ const msg = {
628
+ id: result.id,
629
+ conversation_id: conversationId,
630
+ role,
631
+ sender_name: senderName,
632
+ content,
633
+ created_at: result.created_at
634
+ };
635
+ publish2(conversationId, {
636
+ type: "message",
637
+ id: msg.id,
638
+ role: msg.role,
639
+ senderName: msg.sender_name,
640
+ content: msg.content,
641
+ createdAt: msg.created_at
642
+ });
643
+ const conv = await db.select({ mind_name: conversations.mind_name }).from(conversations).where(eq2(conversations.id, conversationId)).get();
644
+ fireWebhook({
645
+ event: "message_created",
646
+ mind: conv?.mind_name ?? "",
647
+ data: {
648
+ conversationId,
649
+ messageId: result.id,
650
+ role,
651
+ senderName,
652
+ content: content.filter((b) => b.type !== "image"),
653
+ createdAt: result.created_at
654
+ }
655
+ });
656
+ return msg;
657
+ }
658
+ async function getMessages(conversationId) {
659
+ const db = await getDb();
660
+ const rows = await db.select().from(messages).where(eq2(messages.conversation_id, conversationId)).orderBy(messages.created_at).all();
661
+ return rows.map(parseMessageRow);
662
+ }
663
+ async function getMessagesPaginated(conversationId, opts) {
664
+ const db = await getDb();
665
+ const limit = Math.min(Math.max(opts?.limit ?? 50, 1), 100);
666
+ const conditions = [eq2(messages.conversation_id, conversationId)];
667
+ if (opts?.before != null) {
668
+ conditions.push(lt(messages.id, opts.before));
669
+ }
670
+ const rows = await db.select().from(messages).where(and2(...conditions)).orderBy(desc(messages.id)).limit(limit + 1).all();
671
+ const hasMore = rows.length > limit;
672
+ const page = rows.slice(0, limit).reverse();
673
+ return {
674
+ messages: page.map(parseMessageRow),
675
+ hasMore
676
+ };
677
+ }
678
+ function parseMessageRow(row) {
679
+ let content;
680
+ try {
681
+ const parsed = JSON.parse(row.content);
682
+ content = Array.isArray(parsed) ? parsed : [{ type: "text", text: row.content }];
683
+ } catch {
684
+ content = [{ type: "text", text: row.content }];
685
+ }
686
+ return { ...row, role: row.role, content };
687
+ }
688
+ async function listConversationsWithParticipants(userId) {
689
+ const convs = await listConversationsForUser(userId);
690
+ if (convs.length === 0) return [];
691
+ const db = await getDb();
692
+ const convIds = convs.map((c) => c.id);
693
+ const rows = await db.select({
694
+ conversationId: conversationParticipants.conversation_id,
695
+ userId: users.id,
696
+ username: users.username,
697
+ userType: users.user_type,
698
+ role: conversationParticipants.role,
699
+ displayName: users.display_name,
700
+ description: users.description,
701
+ avatar: users.avatar
702
+ }).from(conversationParticipants).innerJoin(users, eq2(conversationParticipants.user_id, users.id)).where(inArray2(conversationParticipants.conversation_id, convIds));
703
+ const byConv = /* @__PURE__ */ new Map();
704
+ for (const r of rows) {
705
+ let arr = byConv.get(r.conversationId);
706
+ if (!arr) {
707
+ arr = [];
708
+ byConv.set(r.conversationId, arr);
709
+ }
710
+ arr.push({
711
+ userId: r.userId,
712
+ username: r.username,
713
+ userType: r.userType,
714
+ role: r.role,
715
+ displayName: r.displayName,
716
+ description: r.description,
717
+ avatar: r.avatar
718
+ });
719
+ }
720
+ const lastMsgIds = await db.select({
721
+ conversationId: messages.conversation_id,
722
+ maxId: sql`MAX(${messages.id})`
723
+ }).from(messages).where(inArray2(messages.conversation_id, convIds)).groupBy(messages.conversation_id);
724
+ const byLastMsg = /* @__PURE__ */ new Map();
725
+ if (lastMsgIds.length > 0) {
726
+ const msgRows = await db.select().from(messages).where(
727
+ inArray2(
728
+ messages.id,
729
+ lastMsgIds.map((r) => r.maxId)
730
+ )
731
+ );
732
+ for (const m of msgRows) {
733
+ let text = "";
734
+ try {
735
+ const parsed = JSON.parse(m.content);
736
+ const blocks = Array.isArray(parsed) ? parsed : [];
737
+ const textBlock = blocks.find((b) => b.type === "text");
738
+ if (textBlock && "text" in textBlock) text = textBlock.text;
739
+ } catch {
740
+ text = m.content;
741
+ }
742
+ byLastMsg.set(m.conversation_id, {
743
+ role: m.role,
744
+ senderName: m.sender_name,
745
+ text,
746
+ createdAt: m.created_at
747
+ });
748
+ }
749
+ }
750
+ return convs.map((c) => ({
751
+ ...c,
752
+ participants: byConv.get(c.id) ?? [],
753
+ lastMessage: byLastMsg.get(c.id)
754
+ }));
755
+ }
756
+ async function findDMConversation(mindName, participantIds) {
757
+ const db = await getDb();
758
+ const mindConvs = await db.select({ id: conversations.id }).from(conversations).where(and2(eq2(conversations.mind_name, mindName), eq2(conversations.type, "dm"))).all();
759
+ for (const conv of mindConvs) {
760
+ const rows = await db.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(eq2(conversationParticipants.conversation_id, conv.id)).all();
761
+ if (rows.length !== 2) continue;
762
+ const ids = new Set(rows.map((r) => r.user_id));
763
+ if (ids.has(participantIds[0]) && ids.has(participantIds[1])) {
764
+ return conv.id;
765
+ }
766
+ }
767
+ return null;
768
+ }
769
+ async function deleteConversation(id) {
770
+ const db = await getDb();
771
+ await db.delete(conversations).where(eq2(conversations.id, id));
772
+ }
773
+ async function createChannel(name, creatorId) {
774
+ const participantIds = creatorId ? [creatorId] : [];
775
+ return createConversation(null, "volute", {
776
+ type: "channel",
777
+ name,
778
+ title: name,
779
+ participantIds
780
+ });
781
+ }
782
+ async function getChannelByName(name) {
783
+ const db = await getDb();
784
+ const row = await db.select().from(conversations).where(and2(eq2(conversations.name, name), eq2(conversations.type, "channel"))).get();
785
+ return row ?? null;
786
+ }
787
+ async function listChannels() {
788
+ const db = await getDb();
789
+ return await db.select().from(conversations).where(eq2(conversations.type, "channel")).orderBy(conversations.name).all();
790
+ }
791
+ async function joinChannel(conversationId, userId) {
792
+ if (await isParticipant(conversationId, userId)) return;
793
+ await addParticipant(conversationId, userId);
794
+ }
795
+ async function leaveChannel(conversationId, userId) {
796
+ await removeParticipant(conversationId, userId);
797
+ }
798
+ async function getUnreadCounts(userId, conversationIds) {
799
+ if (conversationIds.length === 0) return {};
800
+ const db = await getDb();
801
+ const rows = await db.select({
802
+ conversationId: messages.conversation_id,
803
+ count: sql`COUNT(*)`
804
+ }).from(messages).leftJoin(
805
+ conversationReads,
806
+ and2(
807
+ eq2(conversationReads.conversation_id, messages.conversation_id),
808
+ eq2(conversationReads.user_id, userId)
809
+ )
810
+ ).where(
811
+ and2(
812
+ inArray2(messages.conversation_id, conversationIds),
813
+ sql`${messages.id} > COALESCE(${conversationReads.last_read_message_id}, 0)`
814
+ )
815
+ ).groupBy(messages.conversation_id);
816
+ const result = {};
817
+ for (const row of rows) {
818
+ result[row.conversationId] = row.count;
819
+ }
820
+ return result;
821
+ }
822
+ async function markConversationRead(userId, conversationId) {
823
+ const db = await getDb();
824
+ const maxRow = await db.select({ maxId: sql`MAX(${messages.id})` }).from(messages).where(eq2(messages.conversation_id, conversationId)).get();
825
+ const maxId = maxRow?.maxId ?? 0;
826
+ if (maxId === 0) return;
827
+ await db.insert(conversationReads).values({ user_id: userId, conversation_id: conversationId, last_read_message_id: maxId }).onConflictDoUpdate({
828
+ target: [conversationReads.user_id, conversationReads.conversation_id],
829
+ set: { last_read_message_id: maxId }
830
+ });
831
+ }
832
+
833
+ // src/lib/system-channel.ts
834
+ var SYSTEM_CHANNEL_NAME = "system";
835
+ var cachedChannelId = null;
836
+ async function ensureSystemChannel() {
837
+ if (cachedChannelId) return cachedChannelId;
838
+ const existing = await getChannelByName(SYSTEM_CHANNEL_NAME);
839
+ if (existing) {
840
+ cachedChannelId = existing.id;
841
+ return existing.id;
842
+ }
843
+ const conv = await createChannel(SYSTEM_CHANNEL_NAME);
844
+ cachedChannelId = conv.id;
845
+ logger_default.info("created #system channel");
846
+ return conv.id;
847
+ }
848
+ async function joinSystemChannel(userId) {
849
+ const channelId = await ensureSystemChannel();
850
+ await joinChannel(channelId, userId);
851
+ }
852
+ async function joinSystemChannelForMind(mindName) {
853
+ const user = await getOrCreateMindUser(mindName);
854
+ await joinSystemChannel(user.id);
855
+ }
856
+ async function announceToSystem(text) {
857
+ const channelId = await ensureSystemChannel();
858
+ await addMessage(channelId, "system", "system", [{ type: "text", text }]);
859
+ }
860
+
861
+ // src/lib/daemon/connector-manager.ts
862
+ import { spawn } from "child_process";
863
+ import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "fs";
864
+ import { dirname, resolve as resolve3 } from "path";
865
+
866
+ // src/lib/connector-defs.ts
867
+ import { existsSync as existsSync2, readFileSync } from "fs";
868
+ import { resolve as resolve2 } from "path";
869
+ var BUILTIN_DEFS = {
870
+ discord: {
871
+ displayName: "Discord",
872
+ description: "Connect to Discord as a bot",
873
+ envVars: [
874
+ {
875
+ name: "DISCORD_TOKEN",
876
+ required: true,
877
+ description: "Discord bot token",
878
+ scope: "mind"
879
+ },
880
+ {
881
+ name: "DISCORD_GUILD_ID",
882
+ required: false,
883
+ description: "Discord server ID (optional, for slash commands)",
884
+ scope: "mind"
885
+ }
886
+ ]
887
+ },
888
+ slack: {
889
+ displayName: "Slack",
890
+ description: "Connect to Slack via Socket Mode",
891
+ envVars: [
892
+ {
893
+ name: "SLACK_BOT_TOKEN",
894
+ required: true,
895
+ description: "Slack bot token (xoxb-...)",
896
+ scope: "mind"
897
+ },
898
+ {
899
+ name: "SLACK_APP_TOKEN",
900
+ required: true,
901
+ description: "Slack app-level token (xapp-...) for Socket Mode",
902
+ scope: "mind"
903
+ }
904
+ ]
905
+ },
906
+ telegram: {
907
+ displayName: "Telegram",
908
+ description: "Connect to Telegram via long polling",
909
+ envVars: [
910
+ {
911
+ name: "TELEGRAM_BOT_TOKEN",
912
+ required: true,
913
+ description: "Telegram bot token from BotFather",
914
+ scope: "mind"
915
+ }
916
+ ]
917
+ }
918
+ };
919
+ function getConnectorDef(type, connectorDir) {
920
+ if (BUILTIN_DEFS[type]) return BUILTIN_DEFS[type];
921
+ if (connectorDir) {
922
+ const jsonPath = resolve2(connectorDir, "connector.json");
923
+ if (existsSync2(jsonPath)) {
924
+ try {
925
+ return JSON.parse(readFileSync(jsonPath, "utf-8"));
926
+ } catch (err) {
927
+ console.warn(`Failed to parse ${jsonPath}: ${err}`);
928
+ return null;
929
+ }
930
+ }
931
+ }
932
+ return null;
933
+ }
934
+ function checkMissingEnvVars(def, env) {
935
+ return def.envVars.filter((v) => v.required && !env[v.name]);
936
+ }
937
+
938
+ // src/lib/daemon/connector-manager.ts
939
+ var clog = logger_default.child("connectors");
940
+ function searchUpwards(...segments) {
941
+ let searchDir = dirname(new URL(import.meta.url).pathname);
942
+ for (let i = 0; i < 5; i++) {
943
+ const candidate = resolve3(searchDir, ...segments);
944
+ if (existsSync3(candidate)) return candidate;
945
+ searchDir = dirname(searchDir);
946
+ }
947
+ return null;
948
+ }
949
+ var ConnectorManager = class {
950
+ connectors = /* @__PURE__ */ new Map();
951
+ stopping = /* @__PURE__ */ new Set();
952
+ // "mind:type" keys currently being explicitly stopped
953
+ shuttingDown = false;
954
+ restartTracker = new RestartTracker();
955
+ async startConnectors(mindName, mindDir2, mindPort, daemonPort) {
956
+ const config = readVoluteConfig(mindDir2) ?? {};
957
+ const types = config.connectors ?? [];
958
+ await Promise.all(
959
+ types.map(
960
+ (type) => this.startConnector(mindName, mindDir2, mindPort, type, daemonPort).catch((err) => {
961
+ clog.warn(`failed to start connector ${type} for ${mindName}`, logger_default.errorData(err));
962
+ })
963
+ )
964
+ );
965
+ }
966
+ checkConnectorEnv(type, mindName, mindDir2) {
967
+ const mindConnectorDir = resolve3(mindDir2, "connectors", type);
968
+ const userConnectorDir = resolve3(voluteHome(), "connectors", type);
969
+ const connectorDir = existsSync3(mindConnectorDir) ? mindConnectorDir : existsSync3(userConnectorDir) ? userConnectorDir : void 0;
970
+ const def = getConnectorDef(type, connectorDir);
971
+ if (!def) return null;
972
+ const env = loadMergedEnv(mindName);
973
+ const missing = checkMissingEnvVars(def, env);
974
+ if (missing.length === 0) return null;
975
+ return {
976
+ missing: missing.map((v) => ({ name: v.name, description: v.description })),
977
+ connectorName: def.displayName
978
+ };
979
+ }
980
+ async startConnector(mindName, mindDir2, mindPort, type, daemonPort) {
981
+ const existing = this.connectors.get(mindName)?.get(type);
982
+ if (existing) {
983
+ await new Promise((res) => {
984
+ existing.child.on("exit", () => res());
985
+ try {
986
+ if (existing.child.pid) {
987
+ process.kill(-existing.child.pid, "SIGTERM");
988
+ } else {
989
+ existing.child.kill("SIGTERM");
990
+ }
991
+ } catch {
992
+ res();
993
+ }
994
+ setTimeout(() => {
995
+ try {
996
+ if (existing.child.pid) {
997
+ process.kill(-existing.child.pid, "SIGKILL");
998
+ } else {
999
+ existing.child.kill("SIGKILL");
1000
+ }
1001
+ } catch {
1002
+ }
1003
+ res();
1004
+ }, 3e3);
1005
+ });
1006
+ this.connectors.get(mindName)?.delete(type);
1007
+ }
1008
+ this.killOrphanConnector(mindName, type);
1009
+ const mindConnector = resolve3(mindDir2, "connectors", type, "index.ts");
1010
+ const userConnector = resolve3(voluteHome(), "connectors", type, "index.ts");
1011
+ const builtinConnector = this.resolveBuiltinConnector(type);
1012
+ let connectorScript;
1013
+ let runtime;
554
1014
  if (existsSync3(mindConnector)) {
555
1015
  connectorScript = mindConnector;
556
1016
  runtime = resolve3(mindDir2, "node_modules", ".bin", "tsx");
@@ -592,12 +1052,24 @@ var ConnectorManager = class {
592
1052
  VOLUTE_MIND_DIR: mindDir2,
593
1053
  ...daemonPort ? {
594
1054
  VOLUTE_DAEMON_URL: `http://${daemonLoopback()}:${daemonPort}`,
595
- VOLUTE_DAEMON_TOKEN: process.env.VOLUTE_DAEMON_TOKEN
1055
+ VOLUTE_DAEMON_TOKEN: getMindToken(mindName) ?? void 0
596
1056
  } : {},
597
1057
  ...connectorEnv
598
1058
  }
599
1059
  };
600
- const [spawnCmd, spawnArgs] = wrapForIsolation(runtime, [connectorScript], mindName);
1060
+ let spawnCmd;
1061
+ let spawnArgs;
1062
+ if (isIsolationEnabled()) {
1063
+ [spawnCmd, spawnArgs] = wrapForIsolation(runtime, [connectorScript], mindName);
1064
+ } else if (isSandboxEnabled()) {
1065
+ [spawnCmd, spawnArgs] = await wrapForSandbox(runtime, [connectorScript], mindDir2, mindName, [
1066
+ mindDir2,
1067
+ mindStateDir
1068
+ ]);
1069
+ } else {
1070
+ spawnCmd = runtime;
1071
+ spawnArgs = [connectorScript];
1072
+ }
601
1073
  const child = spawn(spawnCmd, spawnArgs, spawnOpts);
602
1074
  let lastStderr = "";
603
1075
  child.stdout?.pipe(logStream);
@@ -644,558 +1116,140 @@ var ConnectorManager = class {
644
1116
  const mindMap = this.connectors.get(mindName);
645
1117
  if (!mindMap) return;
646
1118
  const tracked = mindMap.get(type);
647
- if (!tracked) return;
648
- const stopKey = `${mindName}:${type}`;
649
- this.stopping.add(stopKey);
650
- mindMap.delete(type);
651
- await new Promise((resolve9) => {
652
- tracked.child.on("exit", () => resolve9());
653
- try {
654
- process.kill(-tracked.child.pid, "SIGTERM");
655
- } catch {
656
- resolve9();
657
- }
658
- setTimeout(() => {
659
- try {
660
- process.kill(-tracked.child.pid, "SIGKILL");
661
- } catch {
662
- }
663
- resolve9();
664
- }, 5e3);
665
- });
666
- this.stopping.delete(stopKey);
667
- this.restartTracker.reset(stopKey);
668
- try {
669
- this.removeConnectorPid(mindName, type);
670
- } catch (err) {
671
- clog.warn(`failed to remove PID file for ${type}/${mindName}`, logger_default.errorData(err));
672
- }
673
- clog.info(`stopped connector ${type} for ${mindName}`);
674
- }
675
- async stopConnectors(mindName) {
676
- const mindMap = this.connectors.get(mindName);
677
- if (!mindMap) return;
678
- const types = [...mindMap.keys()];
679
- await Promise.all(types.map((type) => this.stopConnector(mindName, type)));
680
- this.connectors.delete(mindName);
681
- }
682
- async stopAll() {
683
- this.shuttingDown = true;
684
- const minds = [...this.connectors.keys()];
685
- await Promise.all(minds.map((name) => this.stopConnectors(name)));
686
- }
687
- getConnectorStatus(mindName) {
688
- const mindMap = this.connectors.get(mindName);
689
- if (!mindMap) return [];
690
- return [...mindMap.entries()].map(([type, tracked]) => ({
691
- type,
692
- running: !tracked.child.killed
693
- }));
694
- }
695
- connectorPidPath(mindName, type) {
696
- return resolve3(stateDir(mindName), "connectors", `${type}.pid`);
697
- }
698
- saveConnectorPid(mindName, type, pid) {
699
- const pidPath = this.connectorPidPath(mindName, type);
700
- mkdirSync(dirname(pidPath), { recursive: true });
701
- writeFileSync(pidPath, String(pid));
702
- }
703
- removeConnectorPid(mindName, type) {
704
- try {
705
- unlinkSync(this.connectorPidPath(mindName, type));
706
- } catch {
707
- }
708
- }
709
- killOrphanConnector(mindName, type) {
710
- const pidPath = this.connectorPidPath(mindName, type);
711
- if (!existsSync3(pidPath)) return;
712
- try {
713
- const pid = parseInt(readFileSync2(pidPath, "utf-8").trim(), 10);
714
- if (pid > 0) {
715
- try {
716
- process.kill(-pid, "SIGTERM");
717
- } catch {
718
- process.kill(pid, "SIGTERM");
719
- }
720
- clog.warn(`killed orphan connector ${type} (pid ${pid})`);
721
- }
722
- } catch {
723
- }
724
- try {
725
- unlinkSync(pidPath);
726
- } catch {
727
- }
728
- }
729
- resolveBuiltinConnector(type) {
730
- return searchUpwards("connectors", `${type}.js`);
731
- }
732
- resolveVoluteTsx() {
733
- return searchUpwards("node_modules", ".bin", "tsx") ?? "tsx";
734
- }
735
- };
736
- var instance = null;
737
- function initConnectorManager() {
738
- if (instance) throw new Error("ConnectorManager already initialized");
739
- instance = new ConnectorManager();
740
- return instance;
741
- }
742
- function getConnectorManager() {
743
- if (!instance)
744
- throw new Error("ConnectorManager not initialized \u2014 call initConnectorManager() first");
745
- return instance;
746
- }
747
-
748
- // src/lib/events/mind-events.ts
749
- var subscribers = /* @__PURE__ */ new Map();
750
- function subscribe2(mind, callback) {
751
- let set = subscribers.get(mind);
752
- if (!set) {
753
- set = /* @__PURE__ */ new Set();
754
- subscribers.set(mind, set);
755
- }
756
- set.add(callback);
757
- return () => {
758
- set.delete(callback);
759
- if (set.size === 0) subscribers.delete(mind);
760
- };
761
- }
762
- function publish2(mind, event) {
763
- const set = subscribers.get(mind);
764
- if (!set) return;
765
- for (const cb of set) {
766
- try {
767
- cb(event);
768
- } catch (err) {
769
- console.error("[mind-events] subscriber threw:", err);
770
- set.delete(cb);
771
- if (set.size === 0) subscribers.delete(mind);
772
- }
773
- }
774
- }
775
-
776
- // src/lib/delivery/delivery-manager.ts
777
- import { readFile } from "fs/promises";
778
- import { extname, resolve as resolve5 } from "path";
779
- import { and as and3, eq as eq3, sql as sql2 } from "drizzle-orm";
780
-
781
- // src/lib/events/conversations.ts
782
- import { randomUUID } from "crypto";
783
- import { and as and2, desc, eq as eq2, inArray, isNull, lt, sql } from "drizzle-orm";
784
-
785
- // src/lib/webhook.ts
786
- var slog = logger_default.child("webhook");
787
- function getWebhookUrl() {
788
- return process.env.VOLUTE_WEBHOOK_URL;
789
- }
790
- function getAuthHeaders() {
791
- const headers = { "Content-Type": "application/json" };
792
- const secret = process.env.VOLUTE_WEBHOOK_SECRET;
793
- if (secret) headers.Authorization = `Bearer ${secret}`;
794
- return headers;
795
- }
796
- function fireWebhook(event) {
797
- try {
798
- const url = getWebhookUrl();
799
- if (!url) return;
800
- const payload = { ...event, timestamp: event.timestamp ?? (/* @__PURE__ */ new Date()).toISOString() };
801
- fetch(url, {
802
- method: "POST",
803
- headers: getAuthHeaders(),
804
- body: JSON.stringify(payload)
805
- }).then((res) => {
806
- if (!res.ok) {
807
- slog.warn(`webhook ${event.event} returned HTTP ${res.status}`);
808
- }
809
- }).catch((err) => {
810
- slog.warn(`webhook delivery failed for ${event.event}`, logger_default.errorData(err));
811
- });
812
- } catch (err) {
813
- slog.error(`webhook ${event.event} failed to serialize`, logger_default.errorData(err));
814
- }
815
- }
816
- function initWebhook() {
817
- const url = getWebhookUrl();
818
- if (!url) return () => {
819
- };
820
- try {
821
- const parsed = new URL(url);
822
- if (!["http:", "https:"].includes(parsed.protocol)) {
823
- slog.error(`VOLUTE_WEBHOOK_URL has unsupported protocol: ${parsed.protocol}`);
824
- return () => {
825
- };
826
- }
827
- } catch {
828
- slog.error(`VOLUTE_WEBHOOK_URL is not a valid URL`);
829
- return () => {
830
- };
831
- }
832
- slog.info("webhook enabled");
833
- return subscribe((event) => {
834
- try {
835
- fireWebhook({
836
- event: event.type,
837
- mind: event.mind,
838
- data: { summary: event.summary, ...event.metadata },
839
- timestamp: event.created_at
840
- });
841
- } catch (err) {
842
- slog.error(`failed to fire webhook for ${event.type}`, logger_default.errorData(err));
843
- }
844
- });
845
- }
846
-
847
- // src/lib/events/conversation-events.ts
848
- var subscribers2 = /* @__PURE__ */ new Map();
849
- function subscribe3(conversationId, callback) {
850
- let set = subscribers2.get(conversationId);
851
- if (!set) {
852
- set = /* @__PURE__ */ new Set();
853
- subscribers2.set(conversationId, set);
854
- }
855
- set.add(callback);
856
- return () => {
857
- set.delete(callback);
858
- if (set.size === 0) subscribers2.delete(conversationId);
859
- };
860
- }
861
- function publish3(conversationId, event) {
862
- const set = subscribers2.get(conversationId);
863
- if (!set) return;
864
- for (const cb of set) {
1119
+ if (!tracked) return;
1120
+ const stopKey = `${mindName}:${type}`;
1121
+ this.stopping.add(stopKey);
1122
+ mindMap.delete(type);
1123
+ await new Promise((resolve9) => {
1124
+ tracked.child.on("exit", () => resolve9());
1125
+ try {
1126
+ process.kill(-tracked.child.pid, "SIGTERM");
1127
+ } catch {
1128
+ resolve9();
1129
+ }
1130
+ setTimeout(() => {
1131
+ try {
1132
+ process.kill(-tracked.child.pid, "SIGKILL");
1133
+ } catch {
1134
+ }
1135
+ resolve9();
1136
+ }, 5e3);
1137
+ });
1138
+ this.stopping.delete(stopKey);
1139
+ this.restartTracker.reset(stopKey);
865
1140
  try {
866
- cb(event);
1141
+ this.removeConnectorPid(mindName, type);
867
1142
  } catch (err) {
868
- console.error("[conversation-events] subscriber threw:", err);
869
- set.delete(cb);
870
- if (set.size === 0) subscribers2.delete(conversationId);
1143
+ clog.warn(`failed to remove PID file for ${type}/${mindName}`, logger_default.errorData(err));
871
1144
  }
1145
+ clog.info(`stopped connector ${type} for ${mindName}`);
872
1146
  }
873
- }
874
-
875
- // src/lib/events/conversations.ts
876
- async function createConversation(mindName, channel, opts) {
877
- const db = await getDb();
878
- const id = randomUUID();
879
- const type = opts?.type ?? "dm";
880
- const name = opts?.name ?? null;
881
- await db.transaction(async (tx) => {
882
- await tx.insert(conversations).values({
883
- id,
884
- mind_name: mindName,
885
- channel,
1147
+ async stopConnectors(mindName) {
1148
+ const mindMap = this.connectors.get(mindName);
1149
+ if (!mindMap) return;
1150
+ const types = [...mindMap.keys()];
1151
+ await Promise.all(types.map((type) => this.stopConnector(mindName, type)));
1152
+ this.connectors.delete(mindName);
1153
+ }
1154
+ async stopAll() {
1155
+ this.shuttingDown = true;
1156
+ const minds = [...this.connectors.keys()];
1157
+ await Promise.all(minds.map((name) => this.stopConnectors(name)));
1158
+ }
1159
+ getConnectorStatus(mindName) {
1160
+ const mindMap = this.connectors.get(mindName);
1161
+ if (!mindMap) return [];
1162
+ return [...mindMap.entries()].map(([type, tracked]) => ({
886
1163
  type,
887
- name,
888
- user_id: opts?.userId ?? null,
889
- title: opts?.title ?? null
890
- });
891
- if (opts?.participantIds && opts.participantIds.length > 0) {
892
- await tx.insert(conversationParticipants).values(
893
- opts.participantIds.map((uid, i) => ({
894
- conversation_id: id,
895
- user_id: uid,
896
- role: i === 0 ? "owner" : "member"
897
- }))
898
- );
899
- }
900
- });
901
- fireWebhook({
902
- event: "conversation_created",
903
- mind: mindName ?? "",
904
- data: { id, mindName, channel, type, name, title: opts?.title ?? null }
905
- });
906
- return {
907
- id,
908
- mind_name: mindName,
909
- channel,
910
- type,
911
- name,
912
- user_id: opts?.userId ?? null,
913
- title: opts?.title ?? null,
914
- created_at: (/* @__PURE__ */ new Date()).toISOString(),
915
- updated_at: (/* @__PURE__ */ new Date()).toISOString()
916
- };
917
- }
918
- async function getConversation(id) {
919
- const db = await getDb();
920
- const row = await db.select().from(conversations).where(eq2(conversations.id, id)).get();
921
- return row ?? null;
922
- }
923
- async function addParticipant(conversationId, userId, role = "member") {
924
- const db = await getDb();
925
- await db.insert(conversationParticipants).values({
926
- conversation_id: conversationId,
927
- user_id: userId,
928
- role
929
- });
930
- }
931
- async function removeParticipant(conversationId, userId) {
932
- const db = await getDb();
933
- await db.delete(conversationParticipants).where(
934
- and2(
935
- eq2(conversationParticipants.conversation_id, conversationId),
936
- eq2(conversationParticipants.user_id, userId)
937
- )
938
- );
939
- }
940
- async function getParticipants(conversationId) {
941
- const db = await getDb();
942
- const rows = await db.select({
943
- userId: conversationParticipants.user_id,
944
- username: users.username,
945
- userType: users.user_type,
946
- role: conversationParticipants.role,
947
- displayName: users.display_name,
948
- description: users.description,
949
- avatar: users.avatar
950
- }).from(conversationParticipants).innerJoin(users, eq2(conversationParticipants.user_id, users.id)).where(eq2(conversationParticipants.conversation_id, conversationId)).all();
951
- return rows;
952
- }
953
- async function isParticipant(conversationId, userId) {
954
- const db = await getDb();
955
- const row = await db.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(
956
- and2(
957
- eq2(conversationParticipants.conversation_id, conversationId),
958
- eq2(conversationParticipants.user_id, userId)
959
- )
960
- ).get();
961
- return row != null;
962
- }
963
- async function listConversationsForUser(userId) {
964
- const db = await getDb();
965
- const participantRows = await db.select({ conversation_id: conversationParticipants.conversation_id }).from(conversationParticipants).where(eq2(conversationParticipants.user_id, userId)).all();
966
- if (participantRows.length === 0) return [];
967
- const convIds = participantRows.map((r) => r.conversation_id);
968
- return await db.select().from(conversations).where(inArray(conversations.id, convIds)).orderBy(desc(conversations.updated_at)).all();
969
- }
970
- async function isParticipantOrOwner(conversationId, userId) {
971
- if (await isParticipant(conversationId, userId)) return true;
972
- const db = await getDb();
973
- const row = await db.select().from(conversations).where(and2(eq2(conversations.id, conversationId), eq2(conversations.user_id, userId))).get();
974
- return row != null;
975
- }
976
- async function deleteConversationForUser(id, userId) {
977
- if (!await isParticipantOrOwner(id, userId)) return false;
978
- await deleteConversation(id);
979
- return true;
980
- }
981
- async function addMessage(conversationId, role, senderName, content) {
982
- const db = await getDb();
983
- const serialized = JSON.stringify(content);
984
- const [result] = await db.insert(messages).values({ conversation_id: conversationId, role, sender_name: senderName, content: serialized }).returning({ id: messages.id, created_at: messages.created_at });
985
- await db.update(conversations).set({ updated_at: sql`datetime('now')` }).where(eq2(conversations.id, conversationId));
986
- if (role === "user") {
987
- const firstText = content.find((b) => b.type === "text");
988
- const title = firstText ? firstText.text.slice(0, 80) : "";
989
- if (title) {
990
- await db.update(conversations).set({ title }).where(and2(eq2(conversations.id, conversationId), isNull(conversations.title)));
991
- }
1164
+ running: !tracked.child.killed
1165
+ }));
992
1166
  }
993
- const msg = {
994
- id: result.id,
995
- conversation_id: conversationId,
996
- role,
997
- sender_name: senderName,
998
- content,
999
- created_at: result.created_at
1000
- };
1001
- publish3(conversationId, {
1002
- type: "message",
1003
- id: msg.id,
1004
- role: msg.role,
1005
- senderName: msg.sender_name,
1006
- content: msg.content,
1007
- createdAt: msg.created_at
1008
- });
1009
- const conv = await db.select({ mind_name: conversations.mind_name }).from(conversations).where(eq2(conversations.id, conversationId)).get();
1010
- fireWebhook({
1011
- event: "message_created",
1012
- mind: conv?.mind_name ?? "",
1013
- data: {
1014
- conversationId,
1015
- messageId: result.id,
1016
- role,
1017
- senderName,
1018
- content: content.filter((b) => b.type !== "image"),
1019
- createdAt: result.created_at
1020
- }
1021
- });
1022
- return msg;
1023
- }
1024
- async function getMessages(conversationId) {
1025
- const db = await getDb();
1026
- const rows = await db.select().from(messages).where(eq2(messages.conversation_id, conversationId)).orderBy(messages.created_at).all();
1027
- return rows.map(parseMessageRow);
1028
- }
1029
- async function getMessagesPaginated(conversationId, opts) {
1030
- const db = await getDb();
1031
- const limit = Math.min(Math.max(opts?.limit ?? 50, 1), 100);
1032
- const conditions = [eq2(messages.conversation_id, conversationId)];
1033
- if (opts?.before != null) {
1034
- conditions.push(lt(messages.id, opts.before));
1167
+ connectorPidPath(mindName, type) {
1168
+ return resolve3(stateDir(mindName), "connectors", `${type}.pid`);
1035
1169
  }
1036
- const rows = await db.select().from(messages).where(and2(...conditions)).orderBy(desc(messages.id)).limit(limit + 1).all();
1037
- const hasMore = rows.length > limit;
1038
- const page = rows.slice(0, limit).reverse();
1039
- return {
1040
- messages: page.map(parseMessageRow),
1041
- hasMore
1042
- };
1043
- }
1044
- function parseMessageRow(row) {
1045
- let content;
1046
- try {
1047
- const parsed = JSON.parse(row.content);
1048
- content = Array.isArray(parsed) ? parsed : [{ type: "text", text: row.content }];
1049
- } catch {
1050
- content = [{ type: "text", text: row.content }];
1170
+ saveConnectorPid(mindName, type, pid) {
1171
+ const pidPath = this.connectorPidPath(mindName, type);
1172
+ mkdirSync(dirname(pidPath), { recursive: true });
1173
+ writeFileSync(pidPath, String(pid));
1051
1174
  }
1052
- return { ...row, role: row.role, content };
1053
- }
1054
- async function listConversationsWithParticipants(userId) {
1055
- const convs = await listConversationsForUser(userId);
1056
- if (convs.length === 0) return [];
1057
- const db = await getDb();
1058
- const convIds = convs.map((c) => c.id);
1059
- const rows = await db.select({
1060
- conversationId: conversationParticipants.conversation_id,
1061
- userId: users.id,
1062
- username: users.username,
1063
- userType: users.user_type,
1064
- role: conversationParticipants.role,
1065
- displayName: users.display_name,
1066
- description: users.description,
1067
- avatar: users.avatar
1068
- }).from(conversationParticipants).innerJoin(users, eq2(conversationParticipants.user_id, users.id)).where(inArray(conversationParticipants.conversation_id, convIds));
1069
- const byConv = /* @__PURE__ */ new Map();
1070
- for (const r of rows) {
1071
- let arr = byConv.get(r.conversationId);
1072
- if (!arr) {
1073
- arr = [];
1074
- byConv.set(r.conversationId, arr);
1075
- }
1076
- arr.push({
1077
- userId: r.userId,
1078
- username: r.username,
1079
- userType: r.userType,
1080
- role: r.role,
1081
- displayName: r.displayName,
1082
- description: r.description,
1083
- avatar: r.avatar
1084
- });
1175
+ removeConnectorPid(mindName, type) {
1176
+ try {
1177
+ unlinkSync(this.connectorPidPath(mindName, type));
1178
+ } catch {
1179
+ }
1085
1180
  }
1086
- const lastMsgIds = await db.select({
1087
- conversationId: messages.conversation_id,
1088
- maxId: sql`MAX(${messages.id})`
1089
- }).from(messages).where(inArray(messages.conversation_id, convIds)).groupBy(messages.conversation_id);
1090
- const byLastMsg = /* @__PURE__ */ new Map();
1091
- if (lastMsgIds.length > 0) {
1092
- const msgRows = await db.select().from(messages).where(
1093
- inArray(
1094
- messages.id,
1095
- lastMsgIds.map((r) => r.maxId)
1096
- )
1097
- );
1098
- for (const m of msgRows) {
1099
- let text = "";
1100
- try {
1101
- const parsed = JSON.parse(m.content);
1102
- const blocks = Array.isArray(parsed) ? parsed : [];
1103
- const textBlock = blocks.find((b) => b.type === "text");
1104
- if (textBlock && "text" in textBlock) text = textBlock.text;
1105
- } catch {
1106
- text = m.content;
1181
+ killOrphanConnector(mindName, type) {
1182
+ const pidPath = this.connectorPidPath(mindName, type);
1183
+ if (!existsSync3(pidPath)) return;
1184
+ try {
1185
+ const pid = parseInt(readFileSync2(pidPath, "utf-8").trim(), 10);
1186
+ if (pid > 0) {
1187
+ try {
1188
+ process.kill(-pid, "SIGTERM");
1189
+ } catch {
1190
+ process.kill(pid, "SIGTERM");
1191
+ }
1192
+ clog.warn(`killed orphan connector ${type} (pid ${pid})`);
1107
1193
  }
1108
- byLastMsg.set(m.conversation_id, {
1109
- role: m.role,
1110
- senderName: m.sender_name,
1111
- text,
1112
- createdAt: m.created_at
1113
- });
1194
+ } catch {
1114
1195
  }
1115
- }
1116
- return convs.map((c) => ({
1117
- ...c,
1118
- participants: byConv.get(c.id) ?? [],
1119
- lastMessage: byLastMsg.get(c.id)
1120
- }));
1121
- }
1122
- async function findDMConversation(mindName, participantIds) {
1123
- const db = await getDb();
1124
- const mindConvs = await db.select({ id: conversations.id }).from(conversations).where(and2(eq2(conversations.mind_name, mindName), eq2(conversations.type, "dm"))).all();
1125
- for (const conv of mindConvs) {
1126
- const rows = await db.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(eq2(conversationParticipants.conversation_id, conv.id)).all();
1127
- if (rows.length !== 2) continue;
1128
- const ids = new Set(rows.map((r) => r.user_id));
1129
- if (ids.has(participantIds[0]) && ids.has(participantIds[1])) {
1130
- return conv.id;
1196
+ try {
1197
+ unlinkSync(pidPath);
1198
+ } catch {
1131
1199
  }
1132
1200
  }
1133
- return null;
1134
- }
1135
- async function deleteConversation(id) {
1136
- const db = await getDb();
1137
- await db.delete(conversations).where(eq2(conversations.id, id));
1138
- }
1139
- async function createChannel(name, creatorId) {
1140
- const participantIds = creatorId ? [creatorId] : [];
1141
- return createConversation(null, "volute", {
1142
- type: "channel",
1143
- name,
1144
- title: name,
1145
- participantIds
1146
- });
1147
- }
1148
- async function getChannelByName(name) {
1149
- const db = await getDb();
1150
- const row = await db.select().from(conversations).where(and2(eq2(conversations.name, name), eq2(conversations.type, "channel"))).get();
1151
- return row ?? null;
1152
- }
1153
- async function listChannels() {
1154
- const db = await getDb();
1155
- return await db.select().from(conversations).where(eq2(conversations.type, "channel")).orderBy(conversations.name).all();
1156
- }
1157
- async function joinChannel(conversationId, userId) {
1158
- if (await isParticipant(conversationId, userId)) return;
1159
- await addParticipant(conversationId, userId);
1201
+ resolveBuiltinConnector(type) {
1202
+ return searchUpwards("connectors", `${type}.js`);
1203
+ }
1204
+ resolveVoluteTsx() {
1205
+ return searchUpwards("node_modules", ".bin", "tsx") ?? "tsx";
1206
+ }
1207
+ };
1208
+ var instance = null;
1209
+ function initConnectorManager() {
1210
+ if (instance) throw new Error("ConnectorManager already initialized");
1211
+ instance = new ConnectorManager();
1212
+ return instance;
1160
1213
  }
1161
- async function leaveChannel(conversationId, userId) {
1162
- await removeParticipant(conversationId, userId);
1214
+ function getConnectorManager() {
1215
+ if (!instance)
1216
+ throw new Error("ConnectorManager not initialized \u2014 call initConnectorManager() first");
1217
+ return instance;
1163
1218
  }
1164
- async function getUnreadCounts(userId, conversationIds) {
1165
- if (conversationIds.length === 0) return {};
1166
- const db = await getDb();
1167
- const rows = await db.select({
1168
- conversationId: messages.conversation_id,
1169
- count: sql`COUNT(*)`
1170
- }).from(messages).leftJoin(
1171
- conversationReads,
1172
- and2(
1173
- eq2(conversationReads.conversation_id, messages.conversation_id),
1174
- eq2(conversationReads.user_id, userId)
1175
- )
1176
- ).where(
1177
- and2(
1178
- inArray(messages.conversation_id, conversationIds),
1179
- sql`${messages.id} > COALESCE(${conversationReads.last_read_message_id}, 0)`
1180
- )
1181
- ).groupBy(messages.conversation_id);
1182
- const result = {};
1183
- for (const row of rows) {
1184
- result[row.conversationId] = row.count;
1219
+
1220
+ // src/lib/events/mind-events.ts
1221
+ var subscribers2 = /* @__PURE__ */ new Map();
1222
+ function subscribe3(mind, callback) {
1223
+ let set = subscribers2.get(mind);
1224
+ if (!set) {
1225
+ set = /* @__PURE__ */ new Set();
1226
+ subscribers2.set(mind, set);
1185
1227
  }
1186
- return result;
1228
+ set.add(callback);
1229
+ return () => {
1230
+ set.delete(callback);
1231
+ if (set.size === 0) subscribers2.delete(mind);
1232
+ };
1187
1233
  }
1188
- async function markConversationRead(userId, conversationId) {
1189
- const db = await getDb();
1190
- const maxRow = await db.select({ maxId: sql`MAX(${messages.id})` }).from(messages).where(eq2(messages.conversation_id, conversationId)).get();
1191
- const maxId = maxRow?.maxId ?? 0;
1192
- if (maxId === 0) return;
1193
- await db.insert(conversationReads).values({ user_id: userId, conversation_id: conversationId, last_read_message_id: maxId }).onConflictDoUpdate({
1194
- target: [conversationReads.user_id, conversationReads.conversation_id],
1195
- set: { last_read_message_id: maxId }
1196
- });
1234
+ function publish3(mind, event) {
1235
+ const set = subscribers2.get(mind);
1236
+ if (!set) return;
1237
+ for (const cb of set) {
1238
+ try {
1239
+ cb(event);
1240
+ } catch (err) {
1241
+ console.error("[mind-events] subscriber threw:", err);
1242
+ set.delete(cb);
1243
+ if (set.size === 0) subscribers2.delete(mind);
1244
+ }
1245
+ }
1197
1246
  }
1198
1247
 
1248
+ // src/lib/delivery/delivery-manager.ts
1249
+ import { readFile, realpath } from "fs/promises";
1250
+ import { extname, resolve as resolve5 } from "path";
1251
+ import { and as and3, eq as eq3, sql as sql2 } from "drizzle-orm";
1252
+
1199
1253
  // src/lib/typing.ts
1200
1254
  var DEFAULT_TTL_MS = 1e4;
1201
1255
  var SWEEP_INTERVAL_MS = 5e3;
@@ -1281,7 +1335,7 @@ function publishTypingForChannels(channels, map) {
1281
1335
  for (const channel of channels) {
1282
1336
  if (channel.startsWith(VOLUTE_PREFIX)) {
1283
1337
  const conversationId = channel.slice(VOLUTE_PREFIX.length);
1284
- publish3(conversationId, { type: "typing", senders: map.get(channel) });
1338
+ publish2(conversationId, { type: "typing", senders: map.get(channel) });
1285
1339
  }
1286
1340
  }
1287
1341
  }
@@ -1990,8 +2044,26 @@ var DeliveryManager = class {
1990
2044
  if (p.userType === "mind") {
1991
2045
  const dir = mindDir(p.username);
1992
2046
  const config = readVoluteConfig(dir);
1993
- if (!config?.avatar) continue;
1994
- filePath = resolve5(dir, "home", config.avatar);
2047
+ if (!config?.profile?.avatar) continue;
2048
+ filePath = resolve5(dir, "home", config.profile.avatar);
2049
+ const homeDir = resolve5(dir, "home");
2050
+ if (!filePath.startsWith(`${homeDir}/`)) {
2051
+ dlog2.warn(`avatar path for ${p.username} escapes home directory, skipping`);
2052
+ continue;
2053
+ }
2054
+ try {
2055
+ const realHome = await realpath(homeDir);
2056
+ const realAvatar = await realpath(filePath);
2057
+ if (!realAvatar.startsWith(`${realHome}/`)) {
2058
+ dlog2.warn(
2059
+ `avatar symlink for ${p.username} resolves outside home directory, skipping`
2060
+ );
2061
+ continue;
2062
+ }
2063
+ } catch (err) {
2064
+ if (err.code === "ENOENT") continue;
2065
+ throw err;
2066
+ }
1995
2067
  } else {
1996
2068
  filePath = resolve5(voluteHome(), "avatars", p.avatar);
1997
2069
  }
@@ -2082,7 +2154,7 @@ async function recordInbound(mind, channel, sender, content) {
2082
2154
  } catch (err) {
2083
2155
  dlog3.warn(`failed to persist inbound for ${mind}`, logger_default.errorData(err));
2084
2156
  }
2085
- publish2(mind, {
2157
+ publish3(mind, {
2086
2158
  mind,
2087
2159
  type: "inbound",
2088
2160
  channel,
@@ -2433,6 +2505,18 @@ var Scheduler = class {
2433
2505
  return false;
2434
2506
  }
2435
2507
  async fire(mindName, schedule) {
2508
+ const sleepManager = getSleepManagerIfReady();
2509
+ const sleepState = sleepManager?.getState(mindName);
2510
+ if (sleepState?.sleeping) {
2511
+ if (schedule.skipWhenSleeping) {
2512
+ slog2.info(`skipped "${schedule.id}" for ${mindName} (sleeping)`);
2513
+ return;
2514
+ }
2515
+ if (sleepState.wokenByTrigger) {
2516
+ slog2.info(`skipped "${schedule.id}" for ${mindName} (trigger-woken)`);
2517
+ return;
2518
+ }
2519
+ }
2436
2520
  try {
2437
2521
  let text;
2438
2522
  if (schedule.script) {
@@ -2458,7 +2542,7 @@ ${stderr}` : ""}`;
2458
2542
  }
2459
2543
  await this.deliver(mindName, {
2460
2544
  content: [{ type: "text", text }],
2461
- channel: "system:scheduler",
2545
+ channel: schedule.channel ?? "system:scheduler",
2462
2546
  sender: schedule.id
2463
2547
  });
2464
2548
  slog2.info(`fired "${schedule.id}" for ${mindName}`);
@@ -2702,14 +2786,13 @@ async function startMindFull(name) {
2702
2786
  );
2703
2787
  const config = readVoluteConfig(dir);
2704
2788
  if (config) {
2705
- syncMindProfile(baseName, {
2706
- displayName: config.displayName,
2707
- description: config.description,
2708
- avatar: config.avatar
2709
- }).catch(
2789
+ syncMindProfile(baseName, config.profile ?? {}).catch(
2710
2790
  (err) => logger_default.error(`failed to sync profile for ${baseName}`, logger_default.errorData(err))
2711
2791
  );
2712
2792
  }
2793
+ joinSystemChannelForMind(baseName).catch(
2794
+ (err) => logger_default.error(`failed to join #system for ${baseName}`, logger_default.errorData(err))
2795
+ );
2713
2796
  if (config?.tokenBudget) {
2714
2797
  getTokenBudget().setBudget(
2715
2798
  baseName,
@@ -2762,7 +2845,8 @@ function defaultState() {
2762
2845
  scheduledWakeAt: null,
2763
2846
  wokenByTrigger: false,
2764
2847
  voluntaryWakeAt: null,
2765
- queuedMessageCount: 0
2848
+ queuedMessageCount: 0,
2849
+ triggerWakeHistory: []
2766
2850
  };
2767
2851
  }
2768
2852
  function formatCurrentDate() {
@@ -2809,6 +2893,7 @@ var SleepManager = class {
2809
2893
  if (existsSync5(this.statePath)) {
2810
2894
  const data = JSON.parse(readFileSync5(this.statePath, "utf-8"));
2811
2895
  for (const [name, state] of Object.entries(data)) {
2896
+ state.triggerWakeHistory ??= [];
2812
2897
  this.states.set(name, state);
2813
2898
  }
2814
2899
  }
@@ -2838,6 +2923,16 @@ var SleepManager = class {
2838
2923
  getState(name) {
2839
2924
  return this.states.get(name) ?? defaultState();
2840
2925
  }
2926
+ /**
2927
+ * Convert a trigger-wake into a full wake. The mind is already running;
2928
+ * this just clears the sleep state so onActivityEvent won't return it to sleep.
2929
+ */
2930
+ convertTriggerToFullWake(name) {
2931
+ const state = this.states.get(name);
2932
+ if (!state?.sleeping || !state.wokenByTrigger) return;
2933
+ this.markAwake(name);
2934
+ slog3.info(`${name} trigger-wake converted to full wake`);
2935
+ }
2841
2936
  getSleepConfig(name) {
2842
2937
  const dir = mindDir(name);
2843
2938
  const config = readVoluteConfig(dir);
@@ -2906,15 +3001,6 @@ var SleepManager = class {
2906
3001
  if (this.transitioning.has(name)) return;
2907
3002
  this.transitioning.add(name);
2908
3003
  try {
2909
- const sleepingSince = state.sleepingSince ? new Date(state.sleepingSince) : /* @__PURE__ */ new Date();
2910
- const now = /* @__PURE__ */ new Date();
2911
- const duration = formatDuration(sleepingSince, now);
2912
- const currentDate = formatCurrentDate();
2913
- const sleepTime = sleepingSince.toLocaleTimeString("en-US", {
2914
- hour: "numeric",
2915
- minute: "2-digit"
2916
- });
2917
- const queuedSummary = await this.buildQueuedSummary(name);
2918
3004
  try {
2919
3005
  await wakeMind(name);
2920
3006
  } catch (err) {
@@ -2923,46 +3009,59 @@ var SleepManager = class {
2923
3009
  }
2924
3010
  const entry = findMind(name);
2925
3011
  if (!entry) return;
2926
- let summaryText;
2927
3012
  if (opts?.trigger) {
2928
3013
  state.wokenByTrigger = true;
2929
- summaryText = await getPrompt("wake_trigger_summary", {
2930
- currentDate,
2931
- triggerChannel: opts.trigger.channel,
2932
- sleepTime,
2933
- duration,
2934
- queuedSummary
3014
+ state.triggerWakeHistory.push({
3015
+ channel: opts.trigger.channel,
3016
+ at: (/* @__PURE__ */ new Date()).toISOString()
2935
3017
  });
3018
+ this.saveState();
2936
3019
  } else {
2937
- summaryText = await getPrompt("wake_summary", {
3020
+ const sleepingSince = state.sleepingSince ? new Date(state.sleepingSince) : /* @__PURE__ */ new Date();
3021
+ const now = /* @__PURE__ */ new Date();
3022
+ const duration = formatDuration(sleepingSince, now);
3023
+ const currentDate = formatCurrentDate();
3024
+ const sleepTime = sleepingSince.toLocaleTimeString("en-US", {
3025
+ hour: "numeric",
3026
+ minute: "2-digit"
3027
+ });
3028
+ const triggerWakeSummary = this.buildTriggerWakeSummary(state);
3029
+ const wakeContext = await this.runWakeContextScript(
3030
+ name,
3031
+ state.sleepingSince ?? sleepingSince.toISOString(),
3032
+ duration
3033
+ );
3034
+ const queuedSummary = await this.buildQueuedSummary(name);
3035
+ const sleepActivity = [triggerWakeSummary, wakeContext, queuedSummary].filter(Boolean).join("\n\n");
3036
+ const summaryText = await getPrompt("wake_summary", {
2938
3037
  currentDate,
2939
3038
  sleepTime,
2940
3039
  duration,
2941
- queuedSummary
2942
- });
2943
- }
2944
- try {
2945
- const db = await getDb();
2946
- await db.insert(mindHistory).values({
2947
- mind: name,
2948
- type: "inbound",
2949
- channel: "system:sleep",
2950
- content: summaryText
2951
- });
2952
- } catch (err) {
2953
- slog3.error(`failed to persist wake summary for ${name}`, logger_default.errorData(err));
2954
- }
2955
- try {
2956
- await fetch(`http://127.0.0.1:${entry.port}/message`, {
2957
- method: "POST",
2958
- headers: { "Content-Type": "application/json" },
2959
- body: JSON.stringify({
2960
- content: [{ type: "text", text: summaryText }],
2961
- channel: "system:sleep"
2962
- })
3040
+ sleepActivity
2963
3041
  });
2964
- } catch (err) {
2965
- slog3.warn(`failed to deliver wake summary to ${name}`, logger_default.errorData(err));
3042
+ try {
3043
+ const db = await getDb();
3044
+ await db.insert(mindHistory).values({
3045
+ mind: name,
3046
+ type: "inbound",
3047
+ channel: "system:sleep",
3048
+ content: summaryText
3049
+ });
3050
+ } catch (err) {
3051
+ slog3.error(`failed to persist wake summary for ${name}`, logger_default.errorData(err));
3052
+ }
3053
+ try {
3054
+ await fetch(`http://127.0.0.1:${entry.port}/message`, {
3055
+ method: "POST",
3056
+ headers: { "Content-Type": "application/json" },
3057
+ body: JSON.stringify({
3058
+ content: [{ type: "text", text: summaryText }],
3059
+ channel: "system:sleep"
3060
+ })
3061
+ });
3062
+ } catch (err) {
3063
+ slog3.warn(`failed to deliver wake summary to ${name}`, logger_default.errorData(err));
3064
+ }
2966
3065
  }
2967
3066
  const flushed = await this.flushQueuedMessages(name);
2968
3067
  if (flushed > 0) {
@@ -3028,7 +3127,7 @@ var SleepManager = class {
3028
3127
  const db = await getDb();
3029
3128
  const rows = await db.select().from(deliveryQueue).where(and4(eq4(deliveryQueue.mind, name), eq4(deliveryQueue.status, "sleep-queued"))).all();
3030
3129
  if (rows.length === 0) return 0;
3031
- const { deliverMessage: deliverMessage2 } = await import("./message-delivery-S7BCNV6Y.js");
3130
+ const { deliverMessage: deliverMessage2 } = await import("./message-delivery-MS5JYPZX.js");
3032
3131
  const delivered = [];
3033
3132
  for (const row of rows) {
3034
3133
  try {
@@ -3039,7 +3138,7 @@ var SleepManager = class {
3039
3138
  }
3040
3139
  }
3041
3140
  if (delivered.length > 0) {
3042
- await db.delete(deliveryQueue).where(inArray2(deliveryQueue.id, delivered));
3141
+ await db.delete(deliveryQueue).where(inArray3(deliveryQueue.id, delivered));
3043
3142
  }
3044
3143
  const state = this.states.get(name);
3045
3144
  if (state) {
@@ -3060,7 +3159,8 @@ var SleepManager = class {
3060
3159
  scheduledWakeAt: this.getNextWakeTime(sleepConfig),
3061
3160
  wokenByTrigger: false,
3062
3161
  voluntaryWakeAt: opts?.voluntaryWakeAt ?? null,
3063
- queuedMessageCount: this.states.get(name)?.queuedMessageCount ?? 0
3162
+ queuedMessageCount: this.states.get(name)?.queuedMessageCount ?? 0,
3163
+ triggerWakeHistory: []
3064
3164
  };
3065
3165
  this.states.set(name, state);
3066
3166
  this.saveState();
@@ -3177,18 +3277,70 @@ var SleepManager = class {
3177
3277
  }
3178
3278
  }
3179
3279
  }
3280
+ async runWakeContextScript(name, sleepingSince, duration) {
3281
+ const scriptPath = resolve8(mindDir(name), "home", ".config", "hooks", "wake-context.sh");
3282
+ if (!existsSync5(scriptPath)) return "";
3283
+ const input = JSON.stringify({
3284
+ sleepingSince,
3285
+ duration,
3286
+ wakeTime: (/* @__PURE__ */ new Date()).toISOString()
3287
+ });
3288
+ try {
3289
+ const result = await new Promise((resolvePromise, reject) => {
3290
+ const child = spawnChild("bash", [scriptPath], {
3291
+ cwd: mindDir(name),
3292
+ timeout: 5e3,
3293
+ env: { ...process.env, VOLUTE_MIND: name },
3294
+ stdio: ["pipe", "pipe", "pipe"]
3295
+ });
3296
+ let stdout = "";
3297
+ let stderr = "";
3298
+ child.stdout.on("data", (data) => {
3299
+ stdout += data.toString();
3300
+ });
3301
+ child.stderr.on("data", (data) => {
3302
+ stderr += data.toString();
3303
+ });
3304
+ child.on("close", (code) => {
3305
+ if (code === 0) resolvePromise(stdout);
3306
+ else
3307
+ reject(
3308
+ new Error(
3309
+ `wake-context script exited with code ${code}${stderr ? `: ${stderr.trim()}` : ""}`
3310
+ )
3311
+ );
3312
+ });
3313
+ child.on("error", reject);
3314
+ child.stdin.end(input);
3315
+ });
3316
+ return result.trim();
3317
+ } catch (err) {
3318
+ slog3.warn(`wake-context script failed for ${name}`, logger_default.errorData(err));
3319
+ return "";
3320
+ }
3321
+ }
3322
+ buildTriggerWakeSummary(state) {
3323
+ const history = state.triggerWakeHistory;
3324
+ if (!history || history.length === 0) return "";
3325
+ const channels = [...new Set(history.map((h) => h.channel))];
3326
+ const times = history.length === 1 ? "once" : `${history.length} times`;
3327
+ return `You were briefly woken ${times} during sleep to handle messages on ${channels.join(", ")} (sessions were archived after each).`;
3328
+ }
3180
3329
  async buildQueuedSummary(name) {
3181
3330
  try {
3182
3331
  const db = await getDb();
3183
- const rows = await db.select({ channel: deliveryQueue.channel }).from(deliveryQueue).where(and4(eq4(deliveryQueue.mind, name), eq4(deliveryQueue.status, "sleep-queued"))).all();
3332
+ const rows = await db.select({ channel: deliveryQueue.channel, sender: deliveryQueue.sender }).from(deliveryQueue).where(and4(eq4(deliveryQueue.mind, name), eq4(deliveryQueue.status, "sleep-queued"))).all();
3184
3333
  if (rows.length === 0) return "No messages arrived while you slept.";
3185
3334
  const channelCounts = /* @__PURE__ */ new Map();
3335
+ const senders = /* @__PURE__ */ new Set();
3186
3336
  for (const row of rows) {
3187
3337
  const ch = row.channel ?? "unknown";
3188
3338
  channelCounts.set(ch, (channelCounts.get(ch) ?? 0) + 1);
3339
+ if (row.sender) senders.add(row.sender);
3189
3340
  }
3190
3341
  const parts = [...channelCounts.entries()].map(([ch, count2]) => `${count2} on ${ch}`);
3191
- return `${rows.length} message${rows.length === 1 ? "" : "s"} arrived while you slept (${parts.join(", ")}). They'll be delivered to your normal channels now.`;
3342
+ const senderNote = senders.size > 0 ? ` from ${[...senders].join(", ")}` : "";
3343
+ return `${rows.length} message${rows.length === 1 ? "" : "s"} arrived while you slept${senderNote} (${parts.join(", ")}). They'll be delivered to your normal channels now.`;
3192
3344
  } catch (err) {
3193
3345
  slog3.error(`failed to build queued summary for ${name}`, logger_default.errorData(err));
3194
3346
  return "Unable to check for queued messages \u2014 there may be messages waiting.";
@@ -3291,8 +3443,6 @@ function getSleepManagerIfReady() {
3291
3443
  }
3292
3444
 
3293
3445
  export {
3294
- initConnectorManager,
3295
- getConnectorManager,
3296
3446
  createUser,
3297
3447
  verifyUser,
3298
3448
  getUser,
@@ -3308,28 +3458,18 @@ export {
3308
3458
  setUserRole,
3309
3459
  deleteUser,
3310
3460
  updateUserProfile,
3461
+ migrateMindRoles,
3462
+ initConnectorManager,
3463
+ getConnectorManager,
3311
3464
  stopAllWatchers,
3312
3465
  getCachedSites,
3313
3466
  getCachedRecentPages,
3314
- initScheduler,
3315
- getScheduler,
3316
- initTokenBudget,
3317
- getTokenBudget,
3318
- startMindFull,
3319
- stopMindFull,
3320
- matchesGlob,
3321
- SleepManager,
3322
- initSleepManager,
3323
- getSleepManager,
3324
- getSleepManagerIfReady,
3325
- subscribe2 as subscribe,
3326
- publish2 as publish,
3327
3467
  getWebhookUrl,
3328
3468
  getAuthHeaders,
3329
3469
  fireWebhook,
3330
3470
  initWebhook,
3331
- subscribe3 as subscribe2,
3332
- publish3 as publish2,
3471
+ subscribe2 as subscribe,
3472
+ publish2 as publish,
3333
3473
  createConversation,
3334
3474
  getConversation,
3335
3475
  getParticipants,
@@ -3349,6 +3489,22 @@ export {
3349
3489
  leaveChannel,
3350
3490
  getUnreadCounts,
3351
3491
  markConversationRead,
3492
+ ensureSystemChannel,
3493
+ joinSystemChannel,
3494
+ announceToSystem,
3495
+ initScheduler,
3496
+ getScheduler,
3497
+ initTokenBudget,
3498
+ getTokenBudget,
3499
+ startMindFull,
3500
+ stopMindFull,
3501
+ matchesGlob,
3502
+ SleepManager,
3503
+ initSleepManager,
3504
+ getSleepManager,
3505
+ getSleepManagerIfReady,
3506
+ subscribe3 as subscribe2,
3507
+ publish3 as publish2,
3352
3508
  getTypingMap,
3353
3509
  publishTypingForChannels,
3354
3510
  extractTextContent,