volute 0.25.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 (102) 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 +477 -6
  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-SHSWYG2J.js → chunk-2VO7453N.js} +56 -19
  7. package/dist/{chunk-PMX4EIJK.js → chunk-3CFRE2VC.js} +878 -741
  8. package/dist/{chunk-PHHKNGA3.js → chunk-3TV4GLFO.js} +2 -2
  9. package/dist/{chunk-BOTQ25QT.js → chunk-5Y3PBKW6.js} +2 -2
  10. package/dist/{chunk-BFK6SOEJ.js → chunk-J2CO4WEV.js} +1 -1
  11. package/dist/{chunk-ZSH4G2P5.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-RVKR2R7F.js → chunk-SSI47XP2.js} +10 -2
  15. package/dist/chunk-TZKJLDQN.js +78 -0
  16. package/dist/{chunk-DG7TO7EE.js → chunk-USNBKHYG.js} +3 -3
  17. package/dist/chunk-UTL75LP6.js +113 -0
  18. package/dist/{chunk-3AIBT4TW.js → chunk-V63B7DX3.js} +24 -1
  19. package/dist/{chunk-33XAVCS4.js → chunk-WBHMQ5OZ.js} +49 -0
  20. package/dist/{chunk-TRQEV3CD.js → chunk-WGOGUMPO.js} +22 -3
  21. package/dist/chunk-XOXLRRR2.js +176 -0
  22. package/dist/{chunk-JTDFJWI2.js → chunk-YJA7P64S.js} +1 -1
  23. package/dist/chunk-ZYGKG6VC.js +22 -0
  24. package/dist/cli.js +44 -20
  25. package/dist/{cloud-sync-PPBBJDY6.js → cloud-sync-NI2K3C7G.js} +11 -9
  26. package/dist/{connector-M6XFI6GM.js → connector-G722WXAU.js} +4 -4
  27. package/dist/{create-VDQJER52.js → create-4YBRTTJS.js} +1 -1
  28. package/dist/{daemon-client-JOVQZ52X.js → daemon-client-Z7FAJ6JW.js} +1 -1
  29. package/dist/{daemon-restart-FDNOZEAD.js → daemon-restart-BJZ3O4U4.js} +6 -5
  30. package/dist/daemon.js +693 -265
  31. package/dist/{delete-2MRR4JX5.js → delete-27OYNK25.js} +1 -1
  32. package/dist/{down-674SX2IZ.js → down-7UKFMJJZ.js} +4 -4
  33. package/dist/{env-2FPOZK37.js → env-M336ONDP.js} +4 -4
  34. package/dist/{export-IKFAPRAO.js → export-HP4G5DQC.js} +1 -1
  35. package/dist/{file-KT3UIQM3.js → file-HUDKTRAS.js} +3 -3
  36. package/dist/{history-46WZN5CN.js → history-B64GTFTD.js} +3 -3
  37. package/dist/{import-TH26J76F.js → import-XIB7UV4S.js} +1 -1
  38. package/dist/{log-6SGSSR3D.js → log-PBFNILJ4.js} +3 -3
  39. package/dist/{login-UO6AOVEA.js → login-6U7U6BNG.js} +1 -1
  40. package/dist/login-B5E7N7MY.js +46 -0
  41. package/dist/logout-XSJRYS3U.js +39 -0
  42. package/dist/{logs-HRBONI5I.js → logs-3CART7O7.js} +3 -3
  43. package/dist/{merge-KSFJKX6T.js → merge-VK2HSKMA.js} +3 -3
  44. package/dist/{message-delivery-XMGV3FUM.js → message-delivery-MS5JYPZX.js} +10 -8
  45. package/dist/{mind-YVWAHL2A.js → mind-HZ3QSDDJ.js} +17 -17
  46. package/dist/{mind-activity-tracker-NMDDEV3K.js → mind-activity-tracker-4G6FURY2.js} +3 -3
  47. package/dist/{mind-manager-4NDNAYAB.js → mind-manager-VVK67AY3.js} +6 -4
  48. package/dist/{mind-sleep-GHPTSAYN.js → mind-sleep-DTV7L44D.js} +3 -3
  49. package/dist/{mind-wake-BJDJFMDF.js → mind-wake-PFN4FN3T.js} +3 -3
  50. package/dist/notes-37FW2UR2.js +230 -0
  51. package/dist/{package-3HF5MXU2.js → package-VZWLXPHV.js} +2 -1
  52. package/dist/{pages-Y6DRWUOJ.js → pages-DIIT5HMQ.js} +1 -1
  53. package/dist/{publish-EEKTZBHW.js → publish-HQV7YREB.js} +3 -3
  54. package/dist/{pull-D32SPFVU.js → pull-2MB4SK3C.js} +3 -3
  55. package/dist/{register-U2UO6TC4.js → register-EFND67FQ.js} +1 -1
  56. package/dist/{restart-5BMNV7KU.js → restart-CCK7D6TV.js} +3 -3
  57. package/dist/sandbox-EHGFF52K.js +19 -0
  58. package/dist/{schedule-YEFDLVMJ.js → schedule-6F7ELB2M.js} +3 -3
  59. package/dist/{seed-6FEKB3YC.js → seed-E5OQGWX3.js} +1 -1
  60. package/dist/{send-IISDYFCL.js → send-IH6XZKPC.js} +6 -20
  61. package/dist/service-LLBV3R7M.js +122 -0
  62. package/dist/setup-F6TWFYGQ.js +371 -0
  63. package/dist/setup-YGAAIKKZ.js +17 -0
  64. package/dist/{shared-LWMNTTZN.js → shared-UMO4S7CC.js} +4 -4
  65. package/dist/{skill-T3EMR6IR.js → skill-42LGFBQC.js} +3 -3
  66. package/dist/skills/dreaming/SKILL.md +68 -0
  67. package/dist/skills/dreaming/references/INSTALL.md +56 -0
  68. package/dist/skills/dreaming/scripts/dream.ts +289 -0
  69. package/dist/skills/dreaming/scripts/wake-context-dreams.sh +30 -0
  70. package/dist/skills/notes/SKILL.md +34 -0
  71. package/dist/{sleep-manager-RKTFZPD3.js → sleep-manager-EE4NRN2Q.js} +10 -8
  72. package/dist/{sprout-QJVGJDSH.js → sprout-QL74KR2X.js} +5 -5
  73. package/dist/{start-C7XITZ5O.js → start-O5JQASRC.js} +3 -3
  74. package/dist/{status-SIRPLEZC.js → status-FZBEBM7Q.js} +3 -3
  75. package/dist/{status-LYS4NUOZ.js → status-WXD4HXRL.js} +3 -3
  76. package/dist/{stop-CVKBSLXY.js → stop-2SOG5NYF.js} +3 -3
  77. package/dist/up-SDMCSVI3.js +17 -0
  78. package/dist/{update-7XCZMYBT.js → update-5VUDAI3D.js} +6 -6
  79. package/dist/{upgrade-7RUIXGOO.js → upgrade-QCCO33BK.js} +1 -1
  80. package/dist/{variant-UGREB4G5.js → variant-WWLDY6D5.js} +4 -4
  81. package/dist/{version-notify-AZQMC32A.js → version-notify-USFZBWMG.js} +11 -9
  82. package/dist/web-assets/assets/index-CUQ31ieL.js +69 -0
  83. package/dist/web-assets/assets/index-CW8NSl1o.css +1 -0
  84. package/dist/web-assets/index.html +2 -2
  85. package/drizzle/0015_notes.sql +23 -0
  86. package/drizzle/0016_note_reactions_and_replies.sql +15 -0
  87. package/drizzle/meta/_journal.json +14 -0
  88. package/package.json +2 -1
  89. package/templates/_base/.init/.config/hooks/wake-context.sh +7 -0
  90. package/templates/_base/src/lib/startup.ts +8 -0
  91. package/templates/claude/src/agent.ts +51 -1
  92. package/templates/claude/src/server.ts +1 -0
  93. package/templates/pi/package.json.tmpl +1 -0
  94. package/templates/pi/src/agent.ts +48 -1
  95. package/templates/pi/src/lib/subagents.ts +150 -0
  96. package/templates/pi/src/server.ts +1 -0
  97. package/dist/chunk-NWPT4ASZ.js +0 -89
  98. package/dist/service-FASYWLTC.js +0 -247
  99. package/dist/setup-BMLM2UTK.js +0 -230
  100. package/dist/up-CJ26KQLN.js +0 -15
  101. package/dist/web-assets/assets/index-CGPSVu19.js +0 -69
  102. package/dist/web-assets/assets/index-V_rNDsM8.css +0 -1
@@ -4,27 +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
9
  broadcast,
10
10
  publish,
11
11
  subscribe
12
- } from "./chunk-BFK6SOEJ.js";
12
+ } from "./chunk-J2CO4WEV.js";
13
13
  import {
14
14
  RestartTracker,
15
15
  RotatingLog,
16
16
  clearJsonMap,
17
17
  getMindManager,
18
+ getMindToken,
18
19
  getPrompt,
19
20
  loadJsonMap,
20
21
  saveJsonMap
21
- } from "./chunk-SHSWYG2J.js";
22
+ } from "./chunk-2VO7453N.js";
22
23
  import {
23
- readVoluteConfig
24
- } from "./chunk-SIAG3QMM.js";
25
- import {
26
- loadMergedEnv
27
- } from "./chunk-PHU4DEAJ.js";
24
+ isSandboxEnabled,
25
+ wrapForSandbox
26
+ } from "./chunk-UTL75LP6.js";
28
27
  import {
29
28
  conversationParticipants,
30
29
  conversationReads,
@@ -34,18 +33,24 @@ import {
34
33
  messages,
35
34
  mindHistory,
36
35
  users
37
- } from "./chunk-33XAVCS4.js";
36
+ } from "./chunk-WBHMQ5OZ.js";
38
37
  import {
39
38
  logger_default
40
39
  } from "./chunk-YUIHSKR6.js";
40
+ import {
41
+ readVoluteConfig
42
+ } from "./chunk-SIAG3QMM.js";
43
+ import {
44
+ loadMergedEnv
45
+ } from "./chunk-PHU4DEAJ.js";
41
46
  import {
42
47
  exec
43
- } from "./chunk-JTDFJWI2.js";
48
+ } from "./chunk-YJA7P64S.js";
44
49
  import {
45
50
  chownMindDir,
46
51
  isIsolationEnabled,
47
52
  wrapForIsolation
48
- } from "./chunk-NWPT4ASZ.js";
53
+ } from "./chunk-XOXLRRR2.js";
49
54
  import {
50
55
  daemonLoopback,
51
56
  findMind,
@@ -57,7 +62,7 @@ import {
57
62
  } from "./chunk-B2CPS4QU.js";
58
63
 
59
64
  // src/lib/daemon/sleep-manager.ts
60
- import { execFile } from "child_process";
65
+ import { execFile, spawn as spawnChild } from "child_process";
61
66
  import {
62
67
  existsSync as existsSync5,
63
68
  mkdirSync as mkdirSync3,
@@ -70,11 +75,11 @@ import {
70
75
  import { resolve as resolve8 } from "path";
71
76
  import { promisify } from "util";
72
77
  import { CronExpressionParser as CronExpressionParser2 } from "cron-parser";
73
- 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";
74
79
 
75
80
  // src/lib/auth.ts
76
81
  import { compareSync, hashSync } from "bcryptjs";
77
- import { and, count, eq } from "drizzle-orm";
82
+ import { and, count, eq, inArray } from "drizzle-orm";
78
83
  var userSelectFields = {
79
84
  id: users.id,
80
85
  username: users.username,
@@ -132,7 +137,7 @@ async function getOrCreateMindUser(mindName) {
132
137
  const [result] = await db.insert(users).values({
133
138
  username: mindName,
134
139
  password_hash: "!mind",
135
- role: "mind",
140
+ role: "user",
136
141
  user_type: "mind"
137
142
  }).returning(userSelectFields);
138
143
  return result;
@@ -197,6 +202,10 @@ async function syncMindProfile(mindName, config) {
197
202
  await db.update(users).set(newProfile).where(eq(users.id, user.id));
198
203
  broadcast({ type: "profile_updated", mind: mindName, summary: `${mindName} profile updated` });
199
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"])));
208
+ }
200
209
 
201
210
  // src/lib/pages-watcher.ts
202
211
  import { existsSync, readdirSync, statSync, watch } from "fs";
@@ -403,160 +412,606 @@ function getCachedRecentPages() {
403
412
  return recentPagesCache;
404
413
  }
405
414
 
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";
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";
410
418
 
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"
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}`);
460
442
  }
461
- ]
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));
462
448
  }
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
- }
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
+ };
475
460
  }
461
+ } catch {
462
+ slog.error(`VOLUTE_WEBHOOK_URL is not a valid URL`);
463
+ return () => {
464
+ };
476
465
  }
477
- return null;
478
- }
479
- function checkMissingEnvVars(def, env) {
480
- 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
+ });
481
479
  }
482
480
 
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);
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);
491
488
  }
492
- return null;
489
+ set.add(callback);
490
+ return () => {
491
+ set.delete(callback);
492
+ if (set.size === 0) subscribers.delete(conversationId);
493
+ };
493
494
  }
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
- };
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
+ }
524
506
  }
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);
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
+ );
552
533
  }
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)) {
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;
1014
+ if (existsSync3(mindConnector)) {
560
1015
  connectorScript = mindConnector;
561
1016
  runtime = resolve3(mindDir2, "node_modules", ".bin", "tsx");
562
1017
  } else if (existsSync3(userConnector)) {
@@ -597,12 +1052,24 @@ var ConnectorManager = class {
597
1052
  VOLUTE_MIND_DIR: mindDir2,
598
1053
  ...daemonPort ? {
599
1054
  VOLUTE_DAEMON_URL: `http://${daemonLoopback()}:${daemonPort}`,
600
- VOLUTE_DAEMON_TOKEN: process.env.VOLUTE_DAEMON_TOKEN
1055
+ VOLUTE_DAEMON_TOKEN: getMindToken(mindName) ?? void 0
601
1056
  } : {},
602
1057
  ...connectorEnv
603
1058
  }
604
1059
  };
605
- 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
+ }
606
1073
  const child = spawn(spawnCmd, spawnArgs, spawnOpts);
607
1074
  let lastStderr = "";
608
1075
  child.stdout?.pipe(logStream);
@@ -666,541 +1133,123 @@ var ConnectorManager = class {
666
1133
  } catch {
667
1134
  }
668
1135
  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
- }
713
- }
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;
751
- }
752
-
753
- // src/lib/events/mind-events.ts
754
- var subscribers = /* @__PURE__ */ new Map();
755
- function subscribe2(mind, callback) {
756
- let set = subscribers.get(mind);
757
- if (!set) {
758
- set = /* @__PURE__ */ new Set();
759
- subscribers.set(mind, set);
760
- }
761
- set.add(callback);
762
- return () => {
763
- set.delete(callback);
764
- if (set.size === 0) subscribers.delete(mind);
765
- };
766
- }
767
- function publish2(mind, event) {
768
- const set = subscribers.get(mind);
769
- if (!set) return;
770
- for (const cb of set) {
771
- try {
772
- cb(event);
773
- } catch (err) {
774
- console.error("[mind-events] subscriber threw:", err);
775
- set.delete(cb);
776
- if (set.size === 0) subscribers.delete(mind);
777
- }
778
- }
779
- }
780
-
781
- // src/lib/delivery/delivery-manager.ts
782
- import { readFile, realpath } from "fs/promises";
783
- import { extname, resolve as resolve5 } from "path";
784
- import { and as and3, eq as eq3, sql as sql2 } from "drizzle-orm";
785
-
786
- // src/lib/events/conversations.ts
787
- import { randomUUID } from "crypto";
788
- import { and as and2, desc, eq as eq2, inArray, isNull, lt, sql } from "drizzle-orm";
789
-
790
- // src/lib/webhook.ts
791
- var slog = logger_default.child("webhook");
792
- function getWebhookUrl() {
793
- return process.env.VOLUTE_WEBHOOK_URL;
794
- }
795
- function getAuthHeaders() {
796
- const headers = { "Content-Type": "application/json" };
797
- const secret = process.env.VOLUTE_WEBHOOK_SECRET;
798
- if (secret) headers.Authorization = `Bearer ${secret}`;
799
- return headers;
800
- }
801
- function fireWebhook(event) {
802
- try {
803
- const url = getWebhookUrl();
804
- if (!url) return;
805
- const payload = { ...event, timestamp: event.timestamp ?? (/* @__PURE__ */ new Date()).toISOString() };
806
- fetch(url, {
807
- method: "POST",
808
- headers: getAuthHeaders(),
809
- body: JSON.stringify(payload)
810
- }).then((res) => {
811
- if (!res.ok) {
812
- slog.warn(`webhook ${event.event} returned HTTP ${res.status}`);
813
- }
814
- }).catch((err) => {
815
- slog.warn(`webhook delivery failed for ${event.event}`, logger_default.errorData(err));
816
- });
817
- } catch (err) {
818
- slog.error(`webhook ${event.event} failed to serialize`, logger_default.errorData(err));
819
- }
820
- }
821
- function initWebhook() {
822
- const url = getWebhookUrl();
823
- if (!url) return () => {
824
- };
825
- try {
826
- const parsed = new URL(url);
827
- if (!["http:", "https:"].includes(parsed.protocol)) {
828
- slog.error(`VOLUTE_WEBHOOK_URL has unsupported protocol: ${parsed.protocol}`);
829
- return () => {
830
- };
831
- }
832
- } catch {
833
- slog.error(`VOLUTE_WEBHOOK_URL is not a valid URL`);
834
- return () => {
835
- };
836
- }
837
- slog.info("webhook enabled");
838
- return subscribe((event) => {
1136
+ }, 5e3);
1137
+ });
1138
+ this.stopping.delete(stopKey);
1139
+ this.restartTracker.reset(stopKey);
839
1140
  try {
840
- fireWebhook({
841
- event: event.type,
842
- mind: event.mind,
843
- data: { summary: event.summary, ...event.metadata },
844
- timestamp: event.created_at
845
- });
1141
+ this.removeConnectorPid(mindName, type);
846
1142
  } catch (err) {
847
- slog.error(`failed to fire webhook for ${event.type}`, logger_default.errorData(err));
1143
+ clog.warn(`failed to remove PID file for ${type}/${mindName}`, logger_default.errorData(err));
848
1144
  }
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);
1145
+ clog.info(`stopped connector ${type} for ${mindName}`);
859
1146
  }
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
- }
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);
877
1153
  }
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,
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]) => ({
891
1163
  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
- }
1164
+ running: !tracked.child.killed
1165
+ }));
997
1166
  }
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));
1167
+ connectorPidPath(mindName, type) {
1168
+ return resolve3(stateDir(mindName), "connectors", `${type}.pid`);
1040
1169
  }
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 }];
1170
+ saveConnectorPid(mindName, type, pid) {
1171
+ const pidPath = this.connectorPidPath(mindName, type);
1172
+ mkdirSync(dirname(pidPath), { recursive: true });
1173
+ writeFileSync(pidPath, String(pid));
1056
1174
  }
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);
1175
+ removeConnectorPid(mindName, type) {
1176
+ try {
1177
+ unlinkSync(this.connectorPidPath(mindName, type));
1178
+ } catch {
1080
1179
  }
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
1180
  }
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;
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})`);
1112
1193
  }
1113
- byLastMsg.set(m.conversation_id, {
1114
- role: m.role,
1115
- senderName: m.sender_name,
1116
- text,
1117
- createdAt: m.created_at
1118
- });
1194
+ } catch {
1119
1195
  }
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;
1196
+ try {
1197
+ unlinkSync(pidPath);
1198
+ } catch {
1136
1199
  }
1137
1200
  }
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);
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;
1165
1213
  }
1166
- async function leaveChannel(conversationId, userId) {
1167
- await removeParticipant(conversationId, userId);
1214
+ function getConnectorManager() {
1215
+ if (!instance)
1216
+ throw new Error("ConnectorManager not initialized \u2014 call initConnectorManager() first");
1217
+ return instance;
1168
1218
  }
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;
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);
1190
1227
  }
1191
- return result;
1228
+ set.add(callback);
1229
+ return () => {
1230
+ set.delete(callback);
1231
+ if (set.size === 0) subscribers2.delete(mind);
1232
+ };
1192
1233
  }
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
- });
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
+ }
1202
1246
  }
1203
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
+
1204
1253
  // src/lib/typing.ts
1205
1254
  var DEFAULT_TTL_MS = 1e4;
1206
1255
  var SWEEP_INTERVAL_MS = 5e3;
@@ -1286,7 +1335,7 @@ function publishTypingForChannels(channels, map) {
1286
1335
  for (const channel of channels) {
1287
1336
  if (channel.startsWith(VOLUTE_PREFIX)) {
1288
1337
  const conversationId = channel.slice(VOLUTE_PREFIX.length);
1289
- publish3(conversationId, { type: "typing", senders: map.get(channel) });
1338
+ publish2(conversationId, { type: "typing", senders: map.get(channel) });
1290
1339
  }
1291
1340
  }
1292
1341
  }
@@ -2105,7 +2154,7 @@ async function recordInbound(mind, channel, sender, content) {
2105
2154
  } catch (err) {
2106
2155
  dlog3.warn(`failed to persist inbound for ${mind}`, logger_default.errorData(err));
2107
2156
  }
2108
- publish2(mind, {
2157
+ publish3(mind, {
2109
2158
  mind,
2110
2159
  type: "inbound",
2111
2160
  channel,
@@ -2456,6 +2505,18 @@ var Scheduler = class {
2456
2505
  return false;
2457
2506
  }
2458
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
+ }
2459
2520
  try {
2460
2521
  let text;
2461
2522
  if (schedule.script) {
@@ -2481,7 +2542,7 @@ ${stderr}` : ""}`;
2481
2542
  }
2482
2543
  await this.deliver(mindName, {
2483
2544
  content: [{ type: "text", text }],
2484
- channel: "system:scheduler",
2545
+ channel: schedule.channel ?? "system:scheduler",
2485
2546
  sender: schedule.id
2486
2547
  });
2487
2548
  slog2.info(`fired "${schedule.id}" for ${mindName}`);
@@ -2729,6 +2790,9 @@ async function startMindFull(name) {
2729
2790
  (err) => logger_default.error(`failed to sync profile for ${baseName}`, logger_default.errorData(err))
2730
2791
  );
2731
2792
  }
2793
+ joinSystemChannelForMind(baseName).catch(
2794
+ (err) => logger_default.error(`failed to join #system for ${baseName}`, logger_default.errorData(err))
2795
+ );
2732
2796
  if (config?.tokenBudget) {
2733
2797
  getTokenBudget().setBudget(
2734
2798
  baseName,
@@ -2781,7 +2845,8 @@ function defaultState() {
2781
2845
  scheduledWakeAt: null,
2782
2846
  wokenByTrigger: false,
2783
2847
  voluntaryWakeAt: null,
2784
- queuedMessageCount: 0
2848
+ queuedMessageCount: 0,
2849
+ triggerWakeHistory: []
2785
2850
  };
2786
2851
  }
2787
2852
  function formatCurrentDate() {
@@ -2828,6 +2893,7 @@ var SleepManager = class {
2828
2893
  if (existsSync5(this.statePath)) {
2829
2894
  const data = JSON.parse(readFileSync5(this.statePath, "utf-8"));
2830
2895
  for (const [name, state] of Object.entries(data)) {
2896
+ state.triggerWakeHistory ??= [];
2831
2897
  this.states.set(name, state);
2832
2898
  }
2833
2899
  }
@@ -2857,6 +2923,16 @@ var SleepManager = class {
2857
2923
  getState(name) {
2858
2924
  return this.states.get(name) ?? defaultState();
2859
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
+ }
2860
2936
  getSleepConfig(name) {
2861
2937
  const dir = mindDir(name);
2862
2938
  const config = readVoluteConfig(dir);
@@ -2925,15 +3001,6 @@ var SleepManager = class {
2925
3001
  if (this.transitioning.has(name)) return;
2926
3002
  this.transitioning.add(name);
2927
3003
  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
3004
  try {
2938
3005
  await wakeMind(name);
2939
3006
  } catch (err) {
@@ -2942,46 +3009,59 @@ var SleepManager = class {
2942
3009
  }
2943
3010
  const entry = findMind(name);
2944
3011
  if (!entry) return;
2945
- let summaryText;
2946
3012
  if (opts?.trigger) {
2947
3013
  state.wokenByTrigger = true;
2948
- summaryText = await getPrompt("wake_trigger_summary", {
2949
- currentDate,
2950
- triggerChannel: opts.trigger.channel,
2951
- sleepTime,
2952
- duration,
2953
- queuedSummary
3014
+ state.triggerWakeHistory.push({
3015
+ channel: opts.trigger.channel,
3016
+ at: (/* @__PURE__ */ new Date()).toISOString()
2954
3017
  });
3018
+ this.saveState();
2955
3019
  } else {
2956
- 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", {
2957
3037
  currentDate,
2958
3038
  sleepTime,
2959
3039
  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
2970
- });
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
- })
3040
+ sleepActivity
2982
3041
  });
2983
- } catch (err) {
2984
- 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
+ }
2985
3065
  }
2986
3066
  const flushed = await this.flushQueuedMessages(name);
2987
3067
  if (flushed > 0) {
@@ -3047,7 +3127,7 @@ var SleepManager = class {
3047
3127
  const db = await getDb();
3048
3128
  const rows = await db.select().from(deliveryQueue).where(and4(eq4(deliveryQueue.mind, name), eq4(deliveryQueue.status, "sleep-queued"))).all();
3049
3129
  if (rows.length === 0) return 0;
3050
- const { deliverMessage: deliverMessage2 } = await import("./message-delivery-XMGV3FUM.js");
3130
+ const { deliverMessage: deliverMessage2 } = await import("./message-delivery-MS5JYPZX.js");
3051
3131
  const delivered = [];
3052
3132
  for (const row of rows) {
3053
3133
  try {
@@ -3058,7 +3138,7 @@ var SleepManager = class {
3058
3138
  }
3059
3139
  }
3060
3140
  if (delivered.length > 0) {
3061
- await db.delete(deliveryQueue).where(inArray2(deliveryQueue.id, delivered));
3141
+ await db.delete(deliveryQueue).where(inArray3(deliveryQueue.id, delivered));
3062
3142
  }
3063
3143
  const state = this.states.get(name);
3064
3144
  if (state) {
@@ -3079,7 +3159,8 @@ var SleepManager = class {
3079
3159
  scheduledWakeAt: this.getNextWakeTime(sleepConfig),
3080
3160
  wokenByTrigger: false,
3081
3161
  voluntaryWakeAt: opts?.voluntaryWakeAt ?? null,
3082
- queuedMessageCount: this.states.get(name)?.queuedMessageCount ?? 0
3162
+ queuedMessageCount: this.states.get(name)?.queuedMessageCount ?? 0,
3163
+ triggerWakeHistory: []
3083
3164
  };
3084
3165
  this.states.set(name, state);
3085
3166
  this.saveState();
@@ -3196,18 +3277,70 @@ var SleepManager = class {
3196
3277
  }
3197
3278
  }
3198
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
+ }
3199
3329
  async buildQueuedSummary(name) {
3200
3330
  try {
3201
3331
  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();
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();
3203
3333
  if (rows.length === 0) return "No messages arrived while you slept.";
3204
3334
  const channelCounts = /* @__PURE__ */ new Map();
3335
+ const senders = /* @__PURE__ */ new Set();
3205
3336
  for (const row of rows) {
3206
3337
  const ch = row.channel ?? "unknown";
3207
3338
  channelCounts.set(ch, (channelCounts.get(ch) ?? 0) + 1);
3339
+ if (row.sender) senders.add(row.sender);
3208
3340
  }
3209
3341
  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.`;
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.`;
3211
3344
  } catch (err) {
3212
3345
  slog3.error(`failed to build queued summary for ${name}`, logger_default.errorData(err));
3213
3346
  return "Unable to check for queued messages \u2014 there may be messages waiting.";
@@ -3310,8 +3443,6 @@ function getSleepManagerIfReady() {
3310
3443
  }
3311
3444
 
3312
3445
  export {
3313
- initConnectorManager,
3314
- getConnectorManager,
3315
3446
  createUser,
3316
3447
  verifyUser,
3317
3448
  getUser,
@@ -3327,28 +3458,18 @@ export {
3327
3458
  setUserRole,
3328
3459
  deleteUser,
3329
3460
  updateUserProfile,
3461
+ migrateMindRoles,
3462
+ initConnectorManager,
3463
+ getConnectorManager,
3330
3464
  stopAllWatchers,
3331
3465
  getCachedSites,
3332
3466
  getCachedRecentPages,
3333
- initScheduler,
3334
- getScheduler,
3335
- initTokenBudget,
3336
- getTokenBudget,
3337
- startMindFull,
3338
- stopMindFull,
3339
- matchesGlob,
3340
- SleepManager,
3341
- initSleepManager,
3342
- getSleepManager,
3343
- getSleepManagerIfReady,
3344
- subscribe2 as subscribe,
3345
- publish2 as publish,
3346
3467
  getWebhookUrl,
3347
3468
  getAuthHeaders,
3348
3469
  fireWebhook,
3349
3470
  initWebhook,
3350
- subscribe3 as subscribe2,
3351
- publish3 as publish2,
3471
+ subscribe2 as subscribe,
3472
+ publish2 as publish,
3352
3473
  createConversation,
3353
3474
  getConversation,
3354
3475
  getParticipants,
@@ -3368,6 +3489,22 @@ export {
3368
3489
  leaveChannel,
3369
3490
  getUnreadCounts,
3370
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,
3371
3508
  getTypingMap,
3372
3509
  publishTypingForChannels,
3373
3510
  extractTextContent,