volute 0.21.0 → 0.22.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 (56) hide show
  1. package/dist/api.d.ts +4294 -0
  2. package/dist/chunk-G5KRTU2F.js +76 -0
  3. package/dist/{chunk-J5A3DF2U.js → chunk-JNFRY2WU.js} +1 -1
  4. package/dist/{chunk-IPJXU366.js → chunk-JTDFJWI2.js} +1 -0
  5. package/dist/chunk-OSFGKF2T.js +2651 -0
  6. package/dist/{chunk-L3LHXZD7.js → chunk-PHHKNGA3.js} +1 -1
  7. package/dist/{chunk-Q7AITQ44.js → chunk-QIXPN3OO.js} +1 -1
  8. package/dist/{chunk-PC6R6UUW.js → chunk-RK627D57.js} +36 -59
  9. package/dist/{chunk-5462YKWP.js → chunk-TFS25FIM.js} +1 -1
  10. package/dist/{chunk-7LPTHFIL.js → chunk-VNVCRVYI.js} +55 -5
  11. package/dist/{chunk-QUJUKM4U.js → chunk-VT5QODNE.js} +1 -1
  12. package/dist/chunk-XLC342FO.js +29 -0
  13. package/dist/cli.js +10 -10
  14. package/dist/cloud-sync-C6WRYRVR.js +96 -0
  15. package/dist/{daemon-restart-BH67ZOTE.js → daemon-restart-TPQ2XBRZ.js} +4 -4
  16. package/dist/daemon.js +1199 -1785
  17. package/dist/{down-LIOQ5JDH.js → down-WSUASL5E.js} +3 -3
  18. package/dist/{import-E433B4KG.js → import-EAXTHHXL.js} +2 -1
  19. package/dist/message-delivery-WUS4K4ZC.js +21 -0
  20. package/dist/{mind-BIDOF65R.js → mind-BTXR5B3C.js} +13 -5
  21. package/dist/{mind-manager-3V2NXX4I.js → mind-manager-P5OBDUKI.js} +1 -1
  22. package/dist/mind-sleep-FWRBIFBS.js +41 -0
  23. package/dist/mind-wake-LJK2YU5X.js +36 -0
  24. package/dist/{package-HQR52XSG.js → package-A7PEYJI2.js} +10 -1
  25. package/dist/{pages-KQBR5TAZ.js → pages-YSTRWJR4.js} +1 -1
  26. package/dist/{publish-OJ4QMXVZ.js → publish-BZNHKUUK.js} +2 -2
  27. package/dist/{service-TVNEORO7.js → service-7BFXDI6J.js} +4 -4
  28. package/dist/{setup-OZDYCKDI.js → setup-SSIIXQMI.js} +2 -2
  29. package/dist/sleep-manager-3RWUX2ZR.js +27 -0
  30. package/dist/{sprout-6Z6C42YM.js → sprout-UKCYBGHK.js} +2 -2
  31. package/dist/{status-Z7NAFMBI.js → status-H2MKDN6L.js} +2 -2
  32. package/dist/{up-7BGDMFRT.js → up-JKGC7PPF.js} +3 -3
  33. package/dist/{update-4WT7VWHW.js → update-ELC6MEUT.js} +2 -2
  34. package/dist/{upgrade-ZEC2GGFO.js → upgrade-GXW2EQY3.js} +11 -2
  35. package/dist/{version-notify-TFS2U5CF.js → version-notify-5FGUAVSF.js} +11 -3
  36. package/dist/web-assets/assets/index-DWBxl4LO.js +69 -0
  37. package/dist/web-assets/assets/index-ZqMd1mx1.css +1 -0
  38. package/dist/web-assets/index.html +2 -2
  39. package/package.json +10 -1
  40. package/templates/_base/.init/.config/prompts.json +1 -0
  41. package/templates/_base/home/.config/config.json.tmpl +4 -1
  42. package/templates/_base/src/lib/logger.ts +68 -23
  43. package/templates/_base/src/lib/startup.ts +12 -3
  44. package/templates/claude/src/agent.ts +150 -29
  45. package/templates/claude/src/lib/hooks/pre-compact.ts +18 -4
  46. package/templates/claude/src/lib/message-channel.ts +6 -0
  47. package/templates/claude/src/lib/stream-consumer.ts +7 -0
  48. package/templates/claude/src/server.ts +3 -1
  49. package/templates/pi/home/.config/config.json.tmpl +4 -1
  50. package/templates/pi/src/agent.ts +87 -0
  51. package/templates/pi/src/lib/event-handler.ts +13 -1
  52. package/templates/pi/src/server.ts +3 -1
  53. package/dist/chunk-OGZYB5GL.js +0 -847
  54. package/dist/web-assets/assets/index-BR3gtK3E.css +0 -1
  55. package/dist/web-assets/assets/index-CWmrZRQd.js +0 -64
  56. /package/dist/{shared-DCQ2UXOM.js → shared-2OGT3NSL.js} +0 -0
package/dist/daemon.js CHANGED
@@ -7,62 +7,76 @@ import {
7
7
  sharedMerge,
8
8
  sharedPull,
9
9
  sharedStatus
10
- } from "./chunk-L3LHXZD7.js";
10
+ } from "./chunk-PHHKNGA3.js";
11
11
  import {
12
- readSystemsConfig
13
- } from "./chunk-HFCBO2GL.js";
12
+ fireWebhook,
13
+ initWebhook
14
+ } from "./chunk-G5KRTU2F.js";
14
15
  import {
15
- PROMPT_DEFAULTS,
16
- PROMPT_KEYS,
17
- RestartTracker,
18
- RotatingLog,
19
- clearJsonMap,
20
- getMindManager,
21
- getMindPromptDefaults,
22
- getPrompt,
23
- getPromptIfCustom,
24
- initMindManager,
25
- loadJsonMap,
26
- saveJsonMap,
27
- substitute
28
- } from "./chunk-7LPTHFIL.js";
16
+ applyInitFiles,
17
+ composeTemplate,
18
+ computeTemplateHash,
19
+ copyTemplateToDir,
20
+ findTemplatesRoot,
21
+ listFiles
22
+ } from "./chunk-AKPFNL7L.js";
29
23
  import {
30
24
  deliverMessage,
31
25
  extractTextContent,
26
+ getCachedRecentPages,
27
+ getCachedSites,
28
+ getConnectorManager,
32
29
  getDeliveryManager,
30
+ getMailPoller,
31
+ getScheduler,
32
+ getTokenBudget,
33
33
  getTypingMap,
34
+ initConnectorManager,
34
35
  initDeliveryManager,
36
+ initMailPoller,
37
+ initScheduler,
38
+ initSleepManager,
39
+ initTokenBudget,
35
40
  publish,
36
41
  publishTypingForChannels,
37
- subscribe
38
- } from "./chunk-OGZYB5GL.js";
42
+ startMindFull,
43
+ stopAllWatchers,
44
+ stopMindFull,
45
+ subscribe as subscribe2
46
+ } from "./chunk-OSFGKF2T.js";
39
47
  import {
40
- applyInitFiles,
41
- composeTemplate,
42
- computeTemplateHash,
43
- copyTemplateToDir,
44
- findTemplatesRoot,
45
- listFiles
46
- } from "./chunk-AKPFNL7L.js";
48
+ readSystemsConfig
49
+ } from "./chunk-HFCBO2GL.js";
47
50
  import {
48
51
  getActiveMinds,
49
- markIdle,
50
52
  onMindEvent,
51
53
  stopAll
52
54
  } from "./chunk-HGCDWKSP.js";
53
55
  import {
54
56
  broadcast,
55
- publish as publish2,
56
- subscribe as subscribe2
57
+ subscribe
57
58
  } from "./chunk-A4S7H6G6.js";
59
+ import {
60
+ PROMPT_DEFAULTS,
61
+ PROMPT_KEYS,
62
+ RotatingLog,
63
+ getMindManager,
64
+ getMindPromptDefaults,
65
+ getPrompt,
66
+ getPromptIfCustom,
67
+ initMindManager,
68
+ substitute
69
+ } from "./chunk-VNVCRVYI.js";
58
70
  import {
59
71
  findOpenClawSession,
60
72
  importOpenClawConnectors,
61
73
  importPiSession,
62
- parseNameFromIdentity,
74
+ parseNameFromIdentity
75
+ } from "./chunk-RK627D57.js";
76
+ import {
63
77
  readVoluteConfig,
64
78
  writeVoluteConfig
65
- } from "./chunk-PC6R6UUW.js";
79
+ } from "./chunk-XLC342FO.js";
66
80
  import {
67
81
  loadMergedEnv,
68
82
  mindEnvPath,
@@ -88,7 +102,7 @@ import {
88
102
  syncBuiltinSkills,
89
103
  uninstallSkill,
90
104
  updateSkill
91
- } from "./chunk-5462YKWP.js";
105
+ } from "./chunk-TFS25FIM.js";
92
106
  import {
93
107
  activity,
94
108
  conversationParticipants,
@@ -109,7 +123,7 @@ import {
109
123
  exec,
110
124
  gitExec,
111
125
  resolveVoluteBin
112
- } from "./chunk-IPJXU366.js";
126
+ } from "./chunk-JTDFJWI2.js";
113
127
  import {
114
128
  chownMindDir,
115
129
  createMindUser,
@@ -134,7 +148,6 @@ import {
134
148
  addMind,
135
149
  addVariant,
136
150
  checkHealth,
137
- daemonLoopback,
138
151
  ensureVoluteHome,
139
152
  findMind,
140
153
  findVariant,
@@ -163,1178 +176,15 @@ import {
163
176
 
164
177
  // src/daemon.ts
165
178
  import { randomBytes as randomBytes2 } from "crypto";
166
- import { mkdirSync as mkdirSync10, readFileSync as readFileSync13, unlinkSync as unlinkSync2, writeFileSync as writeFileSync10 } from "fs";
179
+ import { mkdirSync as mkdirSync8, readFileSync as readFileSync10, unlinkSync, writeFileSync as writeFileSync8 } from "fs";
167
180
  import { homedir as homedir2 } from "os";
168
- import { resolve as resolve22 } from "path";
181
+ import { resolve as resolve17 } from "path";
169
182
  import { format } from "util";
170
183
 
171
- // src/lib/daemon/connector-manager.ts
172
- import { spawn } from "child_process";
173
- import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "fs";
174
- import { dirname, resolve as resolve2 } from "path";
175
-
176
- // src/lib/connector-defs.ts
177
- import { existsSync, readFileSync } from "fs";
178
- import { resolve } from "path";
179
- var BUILTIN_DEFS = {
180
- discord: {
181
- displayName: "Discord",
182
- description: "Connect to Discord as a bot",
183
- envVars: [
184
- {
185
- name: "DISCORD_TOKEN",
186
- required: true,
187
- description: "Discord bot token",
188
- scope: "mind"
189
- },
190
- {
191
- name: "DISCORD_GUILD_ID",
192
- required: false,
193
- description: "Discord server ID (optional, for slash commands)",
194
- scope: "mind"
195
- }
196
- ]
197
- },
198
- slack: {
199
- displayName: "Slack",
200
- description: "Connect to Slack via Socket Mode",
201
- envVars: [
202
- {
203
- name: "SLACK_BOT_TOKEN",
204
- required: true,
205
- description: "Slack bot token (xoxb-...)",
206
- scope: "mind"
207
- },
208
- {
209
- name: "SLACK_APP_TOKEN",
210
- required: true,
211
- description: "Slack app-level token (xapp-...) for Socket Mode",
212
- scope: "mind"
213
- }
214
- ]
215
- },
216
- telegram: {
217
- displayName: "Telegram",
218
- description: "Connect to Telegram via long polling",
219
- envVars: [
220
- {
221
- name: "TELEGRAM_BOT_TOKEN",
222
- required: true,
223
- description: "Telegram bot token from BotFather",
224
- scope: "mind"
225
- }
226
- ]
227
- }
228
- };
229
- function getConnectorDef(type, connectorDir) {
230
- if (BUILTIN_DEFS[type]) return BUILTIN_DEFS[type];
231
- if (connectorDir) {
232
- const jsonPath = resolve(connectorDir, "connector.json");
233
- if (existsSync(jsonPath)) {
234
- try {
235
- return JSON.parse(readFileSync(jsonPath, "utf-8"));
236
- } catch (err) {
237
- console.warn(`Failed to parse ${jsonPath}: ${err}`);
238
- return null;
239
- }
240
- }
241
- }
242
- return null;
243
- }
244
- function checkMissingEnvVars(def, env) {
245
- return def.envVars.filter((v) => v.required && !env[v.name]);
246
- }
247
-
248
- // src/lib/daemon/connector-manager.ts
249
- var clog = logger_default.child("connectors");
250
- function searchUpwards(...segments) {
251
- let searchDir = dirname(new URL(import.meta.url).pathname);
252
- for (let i = 0; i < 5; i++) {
253
- const candidate = resolve2(searchDir, ...segments);
254
- if (existsSync2(candidate)) return candidate;
255
- searchDir = dirname(searchDir);
256
- }
257
- return null;
258
- }
259
- var ConnectorManager = class {
260
- connectors = /* @__PURE__ */ new Map();
261
- stopping = /* @__PURE__ */ new Set();
262
- // "mind:type" keys currently being explicitly stopped
263
- shuttingDown = false;
264
- restartTracker = new RestartTracker();
265
- async startConnectors(mindName, mindDir2, mindPort, daemonPort) {
266
- const config = readVoluteConfig(mindDir2) ?? {};
267
- const types = config.connectors ?? [];
268
- await Promise.all(
269
- types.map(
270
- (type) => this.startConnector(mindName, mindDir2, mindPort, type, daemonPort).catch((err) => {
271
- clog.warn(`failed to start connector ${type} for ${mindName}`, logger_default.errorData(err));
272
- })
273
- )
274
- );
275
- }
276
- checkConnectorEnv(type, mindName, mindDir2) {
277
- const mindConnectorDir = resolve2(mindDir2, "connectors", type);
278
- const userConnectorDir = resolve2(voluteHome(), "connectors", type);
279
- const connectorDir = existsSync2(mindConnectorDir) ? mindConnectorDir : existsSync2(userConnectorDir) ? userConnectorDir : void 0;
280
- const def = getConnectorDef(type, connectorDir);
281
- if (!def) return null;
282
- const env = loadMergedEnv(mindName);
283
- const missing = checkMissingEnvVars(def, env);
284
- if (missing.length === 0) return null;
285
- return {
286
- missing: missing.map((v) => ({ name: v.name, description: v.description })),
287
- connectorName: def.displayName
288
- };
289
- }
290
- async startConnector(mindName, mindDir2, mindPort, type, daemonPort) {
291
- const existing = this.connectors.get(mindName)?.get(type);
292
- if (existing) {
293
- await new Promise((res) => {
294
- existing.child.on("exit", () => res());
295
- try {
296
- if (existing.child.pid) {
297
- process.kill(-existing.child.pid, "SIGTERM");
298
- } else {
299
- existing.child.kill("SIGTERM");
300
- }
301
- } catch {
302
- res();
303
- }
304
- setTimeout(() => {
305
- try {
306
- if (existing.child.pid) {
307
- process.kill(-existing.child.pid, "SIGKILL");
308
- } else {
309
- existing.child.kill("SIGKILL");
310
- }
311
- } catch {
312
- }
313
- res();
314
- }, 3e3);
315
- });
316
- this.connectors.get(mindName)?.delete(type);
317
- }
318
- this.killOrphanConnector(mindName, type);
319
- const mindConnector = resolve2(mindDir2, "connectors", type, "index.ts");
320
- const userConnector = resolve2(voluteHome(), "connectors", type, "index.ts");
321
- const builtinConnector = this.resolveBuiltinConnector(type);
322
- let connectorScript;
323
- let runtime;
324
- if (existsSync2(mindConnector)) {
325
- connectorScript = mindConnector;
326
- runtime = resolve2(mindDir2, "node_modules", ".bin", "tsx");
327
- } else if (existsSync2(userConnector)) {
328
- connectorScript = userConnector;
329
- runtime = this.resolveVoluteTsx();
330
- } else if (builtinConnector) {
331
- connectorScript = builtinConnector;
332
- runtime = process.execPath;
333
- } else {
334
- throw new Error(`No connector code found for type: ${type}`);
335
- }
336
- const mindStateDir = stateDir(mindName);
337
- const logsDir = resolve2(mindStateDir, "logs");
338
- mkdirSync(logsDir, { recursive: true });
339
- if (isIsolationEnabled()) {
340
- try {
341
- const [base] = mindName.split("@", 2);
342
- chownMindDir(mindStateDir, base);
343
- } catch (err) {
344
- throw new Error(
345
- `Cannot start connector ${type} for ${mindName}: failed to set ownership on state directory ${mindStateDir}: ${err instanceof Error ? err.message : err}`
346
- );
347
- }
348
- }
349
- const logStream = new RotatingLog(resolve2(logsDir, `${type}.log`));
350
- const mindEnv = loadMergedEnv(mindName);
351
- const prefix = `${type.toUpperCase()}_`;
352
- const connectorEnv = Object.fromEntries(
353
- Object.entries(mindEnv).filter(([k]) => k.startsWith(prefix))
354
- );
355
- const spawnOpts = {
356
- stdio: ["ignore", "pipe", "pipe"],
357
- detached: true,
358
- env: {
359
- ...process.env,
360
- VOLUTE_MIND_PORT: String(mindPort),
361
- VOLUTE_MIND_NAME: mindName,
362
- VOLUTE_MIND_DIR: mindDir2,
363
- ...daemonPort ? {
364
- VOLUTE_DAEMON_URL: `http://${daemonLoopback()}:${daemonPort}`,
365
- VOLUTE_DAEMON_TOKEN: process.env.VOLUTE_DAEMON_TOKEN
366
- } : {},
367
- ...connectorEnv
368
- }
369
- };
370
- const [spawnCmd, spawnArgs] = wrapForIsolation(runtime, [connectorScript], mindName);
371
- const child = spawn(spawnCmd, spawnArgs, spawnOpts);
372
- let lastStderr = "";
373
- child.stdout?.pipe(logStream);
374
- child.stderr?.on("data", (chunk) => {
375
- logStream.write(chunk);
376
- lastStderr = chunk.toString().trim();
377
- });
378
- if (child.pid) {
379
- this.saveConnectorPid(mindName, type, child.pid);
380
- }
381
- if (!this.connectors.has(mindName)) {
382
- this.connectors.set(mindName, /* @__PURE__ */ new Map());
383
- }
384
- this.connectors.get(mindName).set(type, { child, type });
385
- const stopKey = `${mindName}:${type}`;
386
- this.restartTracker.reset(stopKey);
387
- child.on("exit", (code) => {
388
- const mindMap = this.connectors.get(mindName);
389
- if (mindMap?.get(type)?.child === child) {
390
- mindMap.delete(type);
391
- }
392
- if (this.shuttingDown) return;
393
- if (this.stopping.has(stopKey)) return;
394
- clog.error(`connector ${type} for ${mindName} exited with code ${code}`);
395
- if (lastStderr) clog.warn(`connector ${type} last output: ${lastStderr}`);
396
- const { shouldRestart, delay, attempt } = this.restartTracker.recordCrash(stopKey);
397
- if (!shouldRestart) {
398
- clog.error(`connector ${type} for ${mindName} crashed ${attempt} times \u2014 giving up`);
399
- return;
400
- }
401
- clog.info(
402
- `restarting connector ${type} for ${mindName} \u2014 attempt ${attempt}/${this.restartTracker.maxRestartAttempts}, in ${delay}ms`
403
- );
404
- setTimeout(() => {
405
- if (this.shuttingDown || this.stopping.has(stopKey)) return;
406
- this.startConnector(mindName, mindDir2, mindPort, type, daemonPort).catch((err) => {
407
- clog.error(`failed to restart connector ${type} for ${mindName}`, logger_default.errorData(err));
408
- });
409
- }, delay);
410
- });
411
- clog.info(`started connector ${type} for ${mindName}`);
412
- }
413
- async stopConnector(mindName, type) {
414
- const mindMap = this.connectors.get(mindName);
415
- if (!mindMap) return;
416
- const tracked = mindMap.get(type);
417
- if (!tracked) return;
418
- const stopKey = `${mindName}:${type}`;
419
- this.stopping.add(stopKey);
420
- mindMap.delete(type);
421
- await new Promise((resolve23) => {
422
- tracked.child.on("exit", () => resolve23());
423
- try {
424
- process.kill(-tracked.child.pid, "SIGTERM");
425
- } catch {
426
- resolve23();
427
- }
428
- setTimeout(() => {
429
- try {
430
- process.kill(-tracked.child.pid, "SIGKILL");
431
- } catch {
432
- }
433
- resolve23();
434
- }, 5e3);
435
- });
436
- this.stopping.delete(stopKey);
437
- this.restartTracker.reset(stopKey);
438
- try {
439
- this.removeConnectorPid(mindName, type);
440
- } catch (err) {
441
- clog.warn(`failed to remove PID file for ${type}/${mindName}`, logger_default.errorData(err));
442
- }
443
- clog.info(`stopped connector ${type} for ${mindName}`);
444
- }
445
- async stopConnectors(mindName) {
446
- const mindMap = this.connectors.get(mindName);
447
- if (!mindMap) return;
448
- const types = [...mindMap.keys()];
449
- await Promise.all(types.map((type) => this.stopConnector(mindName, type)));
450
- this.connectors.delete(mindName);
451
- }
452
- async stopAll() {
453
- this.shuttingDown = true;
454
- const minds = [...this.connectors.keys()];
455
- await Promise.all(minds.map((name) => this.stopConnectors(name)));
456
- }
457
- getConnectorStatus(mindName) {
458
- const mindMap = this.connectors.get(mindName);
459
- if (!mindMap) return [];
460
- return [...mindMap.entries()].map(([type, tracked]) => ({
461
- type,
462
- running: !tracked.child.killed
463
- }));
464
- }
465
- connectorPidPath(mindName, type) {
466
- return resolve2(stateDir(mindName), "connectors", `${type}.pid`);
467
- }
468
- saveConnectorPid(mindName, type, pid) {
469
- const pidPath = this.connectorPidPath(mindName, type);
470
- mkdirSync(dirname(pidPath), { recursive: true });
471
- writeFileSync(pidPath, String(pid));
472
- }
473
- removeConnectorPid(mindName, type) {
474
- try {
475
- unlinkSync(this.connectorPidPath(mindName, type));
476
- } catch {
477
- }
478
- }
479
- killOrphanConnector(mindName, type) {
480
- const pidPath = this.connectorPidPath(mindName, type);
481
- if (!existsSync2(pidPath)) return;
482
- try {
483
- const pid = parseInt(readFileSync2(pidPath, "utf-8").trim(), 10);
484
- if (pid > 0) {
485
- try {
486
- process.kill(-pid, "SIGTERM");
487
- } catch {
488
- process.kill(pid, "SIGTERM");
489
- }
490
- clog.warn(`killed orphan connector ${type} (pid ${pid})`);
491
- }
492
- } catch {
493
- }
494
- try {
495
- unlinkSync(pidPath);
496
- } catch {
497
- }
498
- }
499
- resolveBuiltinConnector(type) {
500
- return searchUpwards("connectors", `${type}.js`);
501
- }
502
- resolveVoluteTsx() {
503
- return searchUpwards("node_modules", ".bin", "tsx") ?? "tsx";
504
- }
505
- };
506
- var instance = null;
507
- function initConnectorManager() {
508
- if (instance) throw new Error("ConnectorManager already initialized");
509
- instance = new ConnectorManager();
510
- return instance;
511
- }
512
- function getConnectorManager() {
513
- if (!instance)
514
- throw new Error("ConnectorManager not initialized \u2014 call initConnectorManager() first");
515
- return instance;
516
- }
517
-
518
- // src/lib/daemon/mail-poller.ts
519
- var mlog = logger_default.child("mail");
520
- function formatEmailContent(email) {
521
- if (email.body) {
522
- return email.subject ? `Subject: ${email.subject}
523
-
524
- ${email.body}` : email.body;
525
- }
526
- if (email.html) {
527
- return email.subject ? `Subject: ${email.subject}
528
-
529
- [HTML email \u2014 plain text not available]` : "[HTML email \u2014 plain text not available]";
530
- }
531
- return email.subject ? `Subject: ${email.subject}` : "[Empty email]";
532
- }
533
- var PING_INTERVAL_MS = 3e4;
534
- var INITIAL_RECONNECT_MS = 1e3;
535
- var MAX_RECONNECT_MS = 6e4;
536
- var MailPoller = class {
537
- ws = null;
538
- running = false;
539
- pingTimer = null;
540
- reconnectTimer = null;
541
- reconnectDelay = INITIAL_RECONNECT_MS;
542
- reconnectAttempts = 0;
543
- disconnectedAt = null;
544
- config = null;
545
- start() {
546
- if (this.running) {
547
- mlog.warn("already running \u2014 ignoring duplicate start");
548
- return;
549
- }
550
- this.config = readSystemsConfig();
551
- if (!this.config) {
552
- mlog.info("no systems config \u2014 mail disabled");
553
- return;
554
- }
555
- this.running = true;
556
- this.connect();
557
- }
558
- stop() {
559
- this.running = false;
560
- this.config = null;
561
- if (this.pingTimer) clearInterval(this.pingTimer);
562
- this.pingTimer = null;
563
- if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
564
- this.reconnectTimer = null;
565
- if (this.ws) {
566
- this.ws.close();
567
- this.ws = null;
568
- }
569
- }
570
- isRunning() {
571
- return this.running;
572
- }
573
- connect() {
574
- if (!this.running) return;
575
- this.config = readSystemsConfig();
576
- if (!this.config) {
577
- mlog.info("systems config removed \u2014 stopping");
578
- this.stop();
579
- return;
580
- }
581
- const wsUrl = `${this.config.apiUrl.replace(/^http/, "ws")}/api/ws`;
582
- try {
583
- this.ws = new WebSocket(wsUrl, {
584
- headers: { Authorization: `Bearer ${this.config.apiKey}` }
585
- });
586
- } catch (err) {
587
- mlog.warn("failed to create WebSocket", logger_default.errorData(err));
588
- this.scheduleReconnect();
589
- return;
590
- }
591
- this.ws.onopen = () => {
592
- if (this.reconnectAttempts > 0) {
593
- mlog.info(`reconnected after ${this.reconnectAttempts} attempts`);
594
- }
595
- mlog.info("connected");
596
- this.reconnectAttempts = 0;
597
- this.reconnectDelay = INITIAL_RECONNECT_MS;
598
- if (this.disconnectedAt) {
599
- this.catchUp(this.disconnectedAt);
600
- this.disconnectedAt = null;
601
- }
602
- if (this.pingTimer) clearInterval(this.pingTimer);
603
- this.pingTimer = setInterval(() => {
604
- try {
605
- if (this.ws?.readyState === WebSocket.OPEN) {
606
- this.ws.send("ping");
607
- }
608
- } catch (err) {
609
- mlog.warn("ping failed", logger_default.errorData(err));
610
- }
611
- }, PING_INTERVAL_MS);
612
- };
613
- this.ws.onmessage = (event) => {
614
- this.handleMessage(String(event.data));
615
- };
616
- this.ws.onclose = () => {
617
- mlog.warn("disconnected");
618
- if (!this.disconnectedAt) {
619
- this.disconnectedAt = (/* @__PURE__ */ new Date()).toISOString();
620
- }
621
- this.cleanup();
622
- this.scheduleReconnect();
623
- };
624
- this.ws.onerror = (err) => {
625
- mlog.warn("WebSocket error", logger_default.errorData(err));
626
- };
627
- }
628
- cleanup() {
629
- if (this.pingTimer) clearInterval(this.pingTimer);
630
- this.pingTimer = null;
631
- this.ws = null;
632
- }
633
- scheduleReconnect() {
634
- if (!this.running) return;
635
- this.reconnectAttempts++;
636
- if (this.reconnectAttempts % 10 === 0) {
637
- mlog.warn(
638
- `failed to connect ${this.reconnectAttempts} times \u2014 check systems config and network`
639
- );
640
- }
641
- mlog.info(`reconnecting in ${this.reconnectDelay}ms (attempt ${this.reconnectAttempts})`);
642
- this.reconnectTimer = setTimeout(() => {
643
- this.reconnectTimer = null;
644
- this.connect();
645
- }, this.reconnectDelay);
646
- this.reconnectDelay = Math.min(this.reconnectDelay * 2, MAX_RECONNECT_MS);
647
- }
648
- /** Fetch emails that arrived while disconnected */
649
- catchUp(since) {
650
- if (!this.config) return;
651
- const url = `${this.config.apiUrl}/api/mail/system/poll?since=${encodeURIComponent(since)}`;
652
- fetch(url, {
653
- headers: { Authorization: `Bearer ${this.config.apiKey}` }
654
- }).then(async (res) => {
655
- if (!res.ok) {
656
- mlog.warn(`catch-up poll failed: HTTP ${res.status}`);
657
- return;
658
- }
659
- const data = await res.json();
660
- if (!Array.isArray(data.emails) || data.emails.length === 0) return;
661
- mlog.info(`catching up on ${data.emails.length} missed emails`);
662
- for (const email of data.emails) {
663
- await this.deliver(email.mind, email);
664
- }
665
- }).catch((err) => {
666
- mlog.warn("catch-up error", logger_default.errorData(err));
667
- });
668
- }
669
- handleMessage(data) {
670
- if (data === "pong") return;
671
- let msg;
672
- try {
673
- msg = JSON.parse(data);
674
- } catch {
675
- mlog.warn(`received unparseable message: ${data.slice(0, 200)}`);
676
- return;
677
- }
678
- if (msg.type !== "email") return;
679
- if (!msg.mind || !msg.email?.id) {
680
- mlog.warn(`received malformed email notification: ${data.slice(0, 500)}`);
681
- return;
682
- }
683
- this.fetchAndDeliver(msg.mind, msg.email).catch((err) => {
684
- mlog.warn(`failed to process email for ${msg.mind}`, logger_default.errorData(err));
685
- });
686
- }
687
- async fetchAndDeliver(mind, notification) {
688
- if (!this.config) {
689
- mlog.warn(`systems config missing \u2014 cannot fetch email ${notification.id} for ${mind}`);
690
- return;
691
- }
692
- const url = `${this.config.apiUrl}/api/mail/emails/${encodeURIComponent(mind)}/${encodeURIComponent(notification.id)}`;
693
- const res = await fetch(url, {
694
- headers: { Authorization: `Bearer ${this.config.apiKey}` }
695
- });
696
- if (!res.ok) {
697
- mlog.warn(`failed to fetch email ${notification.id}: HTTP ${res.status}`);
698
- return;
699
- }
700
- const email = await res.json();
701
- await this.deliver(mind, { ...email, mind });
702
- }
703
- async deliver(mind, email) {
704
- const entry = findMind(mind);
705
- if (!entry || !entry.running) {
706
- mlog.warn(`skipping delivery to ${mind}: ${!entry ? "not found" : "not running"}`);
707
- return;
708
- }
709
- const text = formatEmailContent(email);
710
- try {
711
- await deliverMessage(mind, {
712
- content: [{ type: "text", text }],
713
- channel: `mail:${email.from.address}`,
714
- sender: email.from.name || email.from.address,
715
- platform: "Email",
716
- isDM: true
717
- });
718
- mlog.info(`delivered email from ${email.from.address} to ${mind}`);
719
- } catch (err) {
720
- mlog.warn(`failed to deliver to ${mind}`, logger_default.errorData(err));
721
- }
722
- }
723
- };
724
- var instance2 = null;
725
- function initMailPoller() {
726
- if (instance2) throw new Error("MailPoller already initialized");
727
- instance2 = new MailPoller();
728
- return instance2;
729
- }
730
- function getMailPoller() {
731
- if (!instance2) throw new Error("MailPoller not initialized \u2014 call initMailPoller() first");
732
- return instance2;
733
- }
734
- async function ensureMailAddress(mindName) {
735
- const config = readSystemsConfig();
736
- if (!config) return;
737
- try {
738
- const res = await fetch(`${config.apiUrl}/api/mail/addresses/${encodeURIComponent(mindName)}`, {
739
- method: "PUT",
740
- headers: {
741
- Authorization: `Bearer ${config.apiKey}`,
742
- "Content-Type": "application/json"
743
- }
744
- });
745
- if (!res.ok) {
746
- mlog.warn(`failed to ensure address for ${mindName}: HTTP ${res.status}`);
747
- }
748
- await res.text().catch(() => {
749
- });
750
- } catch (err) {
751
- mlog.warn(`failed to ensure address for ${mindName}`, logger_default.errorData(err));
752
- }
753
- }
754
-
755
- // src/lib/pages-watcher.ts
756
- import { existsSync as existsSync3, readdirSync, statSync, watch } from "fs";
757
- import { join, resolve as resolve3 } from "path";
758
- var watchers = /* @__PURE__ */ new Map();
759
- var homeWatchers = /* @__PURE__ */ new Map();
760
- var debounceTimers = /* @__PURE__ */ new Map();
761
- var sitesCache = null;
762
- var recentPagesCache = null;
763
- function startPagesWatcher(mindName, pagesDir) {
764
- try {
765
- const watcher = watch(pagesDir, { recursive: true }, (_eventType, filename) => {
766
- if (!filename || !filename.endsWith(".html")) return;
767
- const key = `${mindName}:${filename}`;
768
- const existing = debounceTimers.get(key);
769
- if (existing) clearTimeout(existing);
770
- debounceTimers.set(
771
- key,
772
- setTimeout(() => {
773
- debounceTimers.delete(key);
774
- invalidateCache();
775
- publish2({
776
- type: "page_updated",
777
- mind: mindName,
778
- summary: `${mindName} updated ${filename}`,
779
- metadata: { file: filename }
780
- }).catch(
781
- (err) => logger_default.error("failed to publish page_updated activity", logger_default.errorData(err))
782
- );
783
- }, 100)
784
- );
785
- });
786
- watchers.set(mindName, watcher);
787
- } catch (err) {
788
- logger_default.warn(`failed to start pages watcher for ${mindName}`, logger_default.errorData(err));
789
- }
790
- }
791
- function startWatcher(mindName) {
792
- if (watchers.has(mindName)) return;
793
- const pagesDir = resolve3(mindDir(mindName), "home", "pages");
794
- if (existsSync3(pagesDir)) {
795
- startPagesWatcher(mindName, pagesDir);
796
- return;
797
- }
798
- if (homeWatchers.has(mindName)) return;
799
- const homeDir = resolve3(mindDir(mindName), "home");
800
- if (!existsSync3(homeDir)) return;
801
- try {
802
- const hw = watch(homeDir, (_eventType, filename) => {
803
- if (filename !== "pages") return;
804
- if (!existsSync3(pagesDir)) return;
805
- hw.close();
806
- homeWatchers.delete(mindName);
807
- invalidateCache();
808
- startPagesWatcher(mindName, pagesDir);
809
- });
810
- homeWatchers.set(mindName, hw);
811
- } catch (err) {
812
- logger_default.warn(`failed to start home watcher for ${mindName}`, logger_default.errorData(err));
813
- }
814
- }
815
- function stopWatcher(mindName) {
816
- const watcher = watchers.get(mindName);
817
- if (watcher) {
818
- watcher.close();
819
- watchers.delete(mindName);
820
- }
821
- const hw = homeWatchers.get(mindName);
822
- if (hw) {
823
- hw.close();
824
- homeWatchers.delete(mindName);
825
- }
826
- for (const [key, timer] of debounceTimers) {
827
- if (key.startsWith(`${mindName}:`)) {
828
- clearTimeout(timer);
829
- debounceTimers.delete(key);
830
- }
831
- }
832
- }
833
- function stopAllWatchers() {
834
- for (const [, watcher] of watchers) {
835
- watcher.close();
836
- }
837
- watchers.clear();
838
- for (const [, hw] of homeWatchers) {
839
- hw.close();
840
- }
841
- homeWatchers.clear();
842
- for (const [, timer] of debounceTimers) {
843
- clearTimeout(timer);
844
- }
845
- debounceTimers.clear();
846
- invalidateCache();
847
- }
848
- function invalidateCache() {
849
- sitesCache = null;
850
- recentPagesCache = null;
851
- }
852
- function scanPagesDir(dir, urlPrefix) {
853
- const pages = [];
854
- let items;
855
- try {
856
- items = readdirSync(dir);
857
- } catch {
858
- return pages;
859
- }
860
- for (const item of items) {
861
- if (item.startsWith(".")) continue;
862
- const fullPath = resolve3(dir, item);
863
- try {
864
- const s = statSync(fullPath);
865
- if (s.isFile() && item.endsWith(".html")) {
866
- pages.push({
867
- file: item,
868
- modified: s.mtime.toISOString(),
869
- url: `${urlPrefix}/${item}`
870
- });
871
- } else if (s.isDirectory()) {
872
- const indexPath = resolve3(fullPath, "index.html");
873
- if (existsSync3(indexPath)) {
874
- const indexStat = statSync(indexPath);
875
- pages.push({
876
- file: join(item, "index.html"),
877
- modified: indexStat.mtime.toISOString(),
878
- url: `${urlPrefix}/${item}/`
879
- });
880
- }
881
- }
882
- } catch {
883
- }
884
- }
885
- pages.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
886
- return pages;
887
- }
888
- function buildSites() {
889
- const sites = [];
890
- const systemPagesDir = resolve3(voluteHome(), "shared", "pages");
891
- if (existsSync3(systemPagesDir)) {
892
- const systemPages = scanPagesDir(systemPagesDir, "/pages/_system");
893
- if (systemPages.length > 0) {
894
- sites.push({ name: "_system", label: "System", pages: systemPages });
895
- }
896
- }
897
- const entries = readRegistry();
898
- for (const entry of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
899
- const pagesDir = resolve3(mindDir(entry.name), "home", "pages");
900
- if (!existsSync3(pagesDir)) continue;
901
- const mindPages = scanPagesDir(pagesDir, `/pages/${entry.name}`);
902
- if (mindPages.length > 0) {
903
- sites.push({ name: entry.name, label: entry.name, pages: mindPages });
904
- }
905
- }
906
- return sites;
907
- }
908
- function buildRecentPages() {
909
- const entries = readRegistry();
910
- const pages = [];
911
- for (const entry of entries) {
912
- const pagesDir = resolve3(mindDir(entry.name), "home", "pages");
913
- if (!existsSync3(pagesDir)) continue;
914
- let items;
915
- try {
916
- items = readdirSync(pagesDir);
917
- } catch {
918
- continue;
919
- }
920
- for (const item of items) {
921
- if (item.startsWith(".")) continue;
922
- const fullPath = resolve3(pagesDir, item);
923
- try {
924
- const s = statSync(fullPath);
925
- if (s.isFile() && item.endsWith(".html")) {
926
- pages.push({
927
- mind: entry.name,
928
- file: item,
929
- modified: s.mtime.toISOString(),
930
- url: `/pages/${entry.name}/${item}`
931
- });
932
- } else if (s.isDirectory()) {
933
- const indexPath = resolve3(fullPath, "index.html");
934
- if (existsSync3(indexPath)) {
935
- const indexStat = statSync(indexPath);
936
- pages.push({
937
- mind: entry.name,
938
- file: join(item, "index.html"),
939
- modified: indexStat.mtime.toISOString(),
940
- url: `/pages/${entry.name}/${item}/`
941
- });
942
- }
943
- }
944
- } catch {
945
- }
946
- }
947
- }
948
- pages.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
949
- return pages.slice(0, 10);
950
- }
951
- function getCachedSites() {
952
- if (!sitesCache) sitesCache = buildSites();
953
- return sitesCache;
954
- }
955
- function getCachedRecentPages() {
956
- if (!recentPagesCache) recentPagesCache = buildRecentPages();
957
- return recentPagesCache;
958
- }
959
-
960
- // src/lib/daemon/scheduler.ts
961
- import { resolve as resolve4 } from "path";
962
- import { CronExpressionParser } from "cron-parser";
963
- var slog = logger_default.child("scheduler");
964
- var Scheduler = class {
965
- schedules = /* @__PURE__ */ new Map();
966
- interval = null;
967
- lastFired = /* @__PURE__ */ new Map();
968
- // "mind:scheduleId" → epoch minute
969
- get statePath() {
970
- return resolve4(voluteHome(), "scheduler-state.json");
971
- }
972
- start() {
973
- this.loadState();
974
- this.interval = setInterval(() => this.tick(), 6e4);
975
- }
976
- stop() {
977
- if (this.interval) clearInterval(this.interval);
978
- }
979
- loadState() {
980
- this.lastFired = loadJsonMap(this.statePath);
981
- }
982
- saveState() {
983
- saveJsonMap(this.statePath, this.lastFired);
984
- }
985
- clearState() {
986
- clearJsonMap(this.statePath, this.lastFired);
987
- }
988
- loadSchedules(mindName) {
989
- const dir = mindDir(mindName);
990
- const config = readVoluteConfig(dir);
991
- if (!config) return;
992
- const schedules = config.schedules ?? [];
993
- if (schedules.length > 0) {
994
- this.schedules.set(mindName, schedules);
995
- } else {
996
- this.schedules.delete(mindName);
997
- }
998
- }
999
- unloadSchedules(mindName) {
1000
- this.schedules.delete(mindName);
1001
- }
1002
- tick() {
1003
- const now = /* @__PURE__ */ new Date();
1004
- const epochMinute = Math.floor(now.getTime() / 6e4);
1005
- const cronCache = /* @__PURE__ */ new Map();
1006
- let anyFired = false;
1007
- for (const [mind, schedules] of this.schedules) {
1008
- for (const schedule of schedules) {
1009
- if (!schedule.enabled) continue;
1010
- if (this.shouldFire(schedule, epochMinute, mind, cronCache)) {
1011
- anyFired = true;
1012
- this.fire(mind, schedule);
1013
- }
1014
- }
1015
- }
1016
- if (anyFired) this.saveState();
1017
- }
1018
- shouldFire(schedule, epochMinute, mind, cronCache) {
1019
- const key = `${mind}:${schedule.id}`;
1020
- if (this.lastFired.get(key) === epochMinute) return false;
1021
- let prevMinute = cronCache.get(schedule.cron);
1022
- if (prevMinute === void 0) {
1023
- try {
1024
- const interval = CronExpressionParser.parse(schedule.cron);
1025
- const prev = interval.prev().toDate();
1026
- prevMinute = Math.floor(prev.getTime() / 6e4);
1027
- cronCache.set(schedule.cron, prevMinute);
1028
- } catch (err) {
1029
- slog.warn(`invalid cron "${schedule.cron}" for ${mind}:${schedule.id}`, logger_default.errorData(err));
1030
- return false;
1031
- }
1032
- }
1033
- if (prevMinute === epochMinute) {
1034
- this.lastFired.set(key, epochMinute);
1035
- return true;
1036
- }
1037
- return false;
1038
- }
1039
- async fire(mindName, schedule) {
1040
- try {
1041
- let text;
1042
- if (schedule.script) {
1043
- const homeDir = resolve4(mindDir(mindName), "home");
1044
- try {
1045
- const output = await this.runScript(schedule.script, homeDir, mindName);
1046
- if (!output.trim()) {
1047
- slog.info(`fired script "${schedule.id}" for ${mindName} (no output)`);
1048
- return;
1049
- }
1050
- text = output;
1051
- } catch (err) {
1052
- const stderr = err.stderr ?? "";
1053
- text = `[script error] ${err.message}${stderr ? `
1054
- ${stderr}` : ""}`;
1055
- slog.warn(`script "${schedule.id}" failed for ${mindName}`, logger_default.errorData(err));
1056
- }
1057
- } else if (schedule.message) {
1058
- text = schedule.message;
1059
- } else {
1060
- slog.warn(`schedule "${schedule.id}" for ${mindName} has no message or script`);
1061
- return;
1062
- }
1063
- await this.deliver(mindName, {
1064
- content: [{ type: "text", text }],
1065
- channel: "system:scheduler",
1066
- sender: schedule.id
1067
- });
1068
- slog.info(`fired "${schedule.id}" for ${mindName}`);
1069
- } catch (err) {
1070
- slog.warn(`failed to fire "${schedule.id}" for ${mindName}`, logger_default.errorData(err));
1071
- }
1072
- }
1073
- runScript(script, cwd, mindName) {
1074
- return exec("bash", ["-c", script], { cwd, mindName });
1075
- }
1076
- deliver(mindName, payload) {
1077
- return deliverMessage(mindName, payload);
1078
- }
1079
- };
1080
- var instance3 = null;
1081
- function initScheduler() {
1082
- if (instance3) throw new Error("Scheduler already initialized");
1083
- instance3 = new Scheduler();
1084
- return instance3;
1085
- }
1086
- function getScheduler() {
1087
- if (!instance3) throw new Error("Scheduler not initialized \u2014 call initScheduler() first");
1088
- return instance3;
1089
- }
1090
-
1091
- // src/lib/daemon/token-budget.ts
1092
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
1093
- import { resolve as resolve5 } from "path";
1094
- var tlog = logger_default.child("token-budget");
1095
- var DEFAULT_BUDGET_PERIOD_MINUTES = 60;
1096
- var MAX_QUEUE_SIZE = 100;
1097
- var TokenBudget = class {
1098
- budgets = /* @__PURE__ */ new Map();
1099
- interval = null;
1100
- dirty = /* @__PURE__ */ new Set();
1101
- start() {
1102
- this.interval = setInterval(() => this.tick(), 6e4);
1103
- }
1104
- stop() {
1105
- this.flush();
1106
- if (this.interval) clearInterval(this.interval);
1107
- this.interval = null;
1108
- }
1109
- setBudget(mind, tokenLimit, periodMinutes) {
1110
- if (tokenLimit <= 0) return;
1111
- const existing = this.budgets.get(mind);
1112
- if (existing) {
1113
- existing.tokenLimit = tokenLimit;
1114
- existing.periodMinutes = periodMinutes;
1115
- } else {
1116
- const persisted = this.loadBudgetState(mind);
1117
- if (persisted) {
1118
- persisted.tokenLimit = tokenLimit;
1119
- persisted.periodMinutes = periodMinutes;
1120
- this.budgets.set(mind, persisted);
1121
- } else {
1122
- this.budgets.set(mind, {
1123
- tokensUsed: 0,
1124
- periodStart: Date.now(),
1125
- periodMinutes,
1126
- tokenLimit,
1127
- queue: [],
1128
- warningInjected: false
1129
- });
1130
- }
1131
- }
1132
- }
1133
- removeBudget(mind) {
1134
- this.budgets.delete(mind);
1135
- }
1136
- recordUsage(mind, inputTokens, outputTokens) {
1137
- const state = this.budgets.get(mind);
1138
- if (!state) return;
1139
- state.tokensUsed += inputTokens + outputTokens;
1140
- this.dirty.add(mind);
1141
- }
1142
- /** Returns current budget status. Does not mutate state — call acknowledgeWarning() after delivering a warning. */
1143
- checkBudget(mind) {
1144
- const state = this.budgets.get(mind);
1145
- if (!state) return "ok";
1146
- const pct = state.tokensUsed / state.tokenLimit;
1147
- if (pct >= 1) return "exceeded";
1148
- if (pct >= 0.8 && !state.warningInjected) return "warning";
1149
- return "ok";
1150
- }
1151
- /** Mark warning as delivered for this period. Call after successfully injecting the warning. */
1152
- acknowledgeWarning(mind) {
1153
- const state = this.budgets.get(mind);
1154
- if (state) state.warningInjected = true;
1155
- }
1156
- enqueue(mind, message) {
1157
- const state = this.budgets.get(mind);
1158
- if (!state) return;
1159
- if (state.queue.length >= MAX_QUEUE_SIZE) {
1160
- state.queue.shift();
1161
- }
1162
- state.queue.push(message);
1163
- }
1164
- drain(mind) {
1165
- const state = this.budgets.get(mind);
1166
- if (!state) return [];
1167
- const messages2 = state.queue;
1168
- state.queue = [];
1169
- return messages2;
1170
- }
1171
- getUsage(mind) {
1172
- const state = this.budgets.get(mind);
1173
- if (!state) return null;
1174
- return {
1175
- tokensUsed: state.tokensUsed,
1176
- tokenLimit: state.tokenLimit,
1177
- periodMinutes: state.periodMinutes,
1178
- periodStart: state.periodStart,
1179
- queueLength: state.queue.length,
1180
- percentUsed: Math.round(state.tokensUsed / state.tokenLimit * 100)
1181
- };
1182
- }
1183
- tick() {
1184
- const now = Date.now();
1185
- for (const [mind, state] of this.budgets) {
1186
- const elapsed = now - state.periodStart;
1187
- if (elapsed >= state.periodMinutes * 6e4) {
1188
- state.tokensUsed = 0;
1189
- state.periodStart = now;
1190
- state.warningInjected = false;
1191
- this.dirty.add(mind);
1192
- const queued = this.drain(mind);
1193
- if (queued.length > 0) {
1194
- this.replay(mind, queued).catch((err) => {
1195
- tlog.warn(`replay error for ${mind}`, logger_default.errorData(err));
1196
- });
1197
- }
1198
- }
1199
- }
1200
- this.flush();
1201
- }
1202
- /** Flush all dirty budget states to disk. */
1203
- flush() {
1204
- for (const mind of this.dirty) {
1205
- const state = this.budgets.get(mind);
1206
- if (state) this.saveBudgetState(mind, state);
1207
- }
1208
- this.dirty.clear();
1209
- }
1210
- budgetStatePath(mind) {
1211
- return resolve5(stateDir(mind), "budget.json");
1212
- }
1213
- saveBudgetState(mind, state) {
1214
- try {
1215
- const dir = stateDir(mind);
1216
- mkdirSync2(dir, { recursive: true });
1217
- const data = {
1218
- periodStart: state.periodStart,
1219
- tokensUsed: state.tokensUsed,
1220
- warningInjected: state.warningInjected,
1221
- queue: state.queue
1222
- };
1223
- writeFileSync2(this.budgetStatePath(mind), `${JSON.stringify(data)}
1224
- `);
1225
- } catch (err) {
1226
- tlog.warn(`failed to save budget state for ${mind}`, logger_default.errorData(err));
1227
- }
1228
- }
1229
- loadBudgetState(mind) {
1230
- try {
1231
- const path = this.budgetStatePath(mind);
1232
- if (!existsSync4(path)) return null;
1233
- const data = JSON.parse(readFileSync3(path, "utf-8"));
1234
- if (typeof data.periodStart !== "number" || typeof data.tokensUsed !== "number") return null;
1235
- return {
1236
- periodStart: data.periodStart,
1237
- tokensUsed: data.tokensUsed,
1238
- warningInjected: data.warningInjected ?? false,
1239
- queue: Array.isArray(data.queue) ? data.queue : [],
1240
- periodMinutes: 0,
1241
- // will be overwritten by caller
1242
- tokenLimit: 0
1243
- // will be overwritten by caller
1244
- };
1245
- } catch (err) {
1246
- tlog.warn(`failed to load budget state for ${mind}`, logger_default.errorData(err));
1247
- return null;
1248
- }
1249
- }
1250
- async replay(mindName, messages2) {
1251
- const summary = messages2.map((m) => {
1252
- const from = m.sender ? `[${m.sender}]` : "";
1253
- const ch = m.channel ? `(${m.channel})` : "";
1254
- return `${from}${ch} ${m.textContent}`;
1255
- }).join("\n");
1256
- try {
1257
- await deliverMessage(mindName, {
1258
- content: [
1259
- {
1260
- type: "text",
1261
- text: `[Budget replay] ${messages2.length} queued message(s) from the previous budget period:
1262
-
1263
- ${summary}`
1264
- }
1265
- ],
1266
- channel: "system:budget-replay",
1267
- sender: "system"
1268
- });
1269
- tlog.info(`replayed ${messages2.length} queued message(s) for ${mindName}`);
1270
- } catch (err) {
1271
- tlog.warn(`failed to replay for ${mindName}`, logger_default.errorData(err));
1272
- const state = this.budgets.get(mindName);
1273
- if (state) state.queue.push(...messages2);
1274
- }
1275
- }
1276
- };
1277
- var instance4 = null;
1278
- function initTokenBudget() {
1279
- if (instance4) throw new Error("TokenBudget already initialized");
1280
- instance4 = new TokenBudget();
1281
- return instance4;
1282
- }
1283
- function getTokenBudget() {
1284
- if (!instance4) throw new Error("TokenBudget not initialized \u2014 call initTokenBudget() first");
1285
- return instance4;
1286
- }
1287
-
1288
- // src/lib/daemon/mind-service.ts
1289
- async function startMindFull(name) {
1290
- const [baseName, variantName] = name.split("@", 2);
1291
- await getMindManager().startMind(name);
1292
- publish2({
1293
- type: "mind_started",
1294
- mind: name,
1295
- summary: `${name} started`
1296
- }).catch((err) => logger_default.error("failed to publish mind_started activity", logger_default.errorData(err)));
1297
- if (variantName) return;
1298
- const entry = findMind(baseName);
1299
- if (!entry || entry.stage === "seed") return;
1300
- const dir = mindDir(baseName);
1301
- const daemonPort = process.env.VOLUTE_DAEMON_PORT ? parseInt(process.env.VOLUTE_DAEMON_PORT, 10) : void 0;
1302
- await getConnectorManager().startConnectors(baseName, dir, entry.port, daemonPort);
1303
- getScheduler().loadSchedules(baseName);
1304
- ensureMailAddress(baseName).catch(
1305
- (err) => logger_default.error(`failed to ensure mail address for ${baseName}`, logger_default.errorData(err))
1306
- );
1307
- const config = readVoluteConfig(dir);
1308
- if (config?.tokenBudget) {
1309
- getTokenBudget().setBudget(
1310
- baseName,
1311
- config.tokenBudget,
1312
- config.tokenBudgetPeriodMinutes ?? DEFAULT_BUDGET_PERIOD_MINUTES
1313
- );
1314
- }
1315
- startWatcher(baseName);
1316
- }
1317
- async function stopMindFull(name) {
1318
- const [baseName, variantName] = name.split("@", 2);
1319
- if (!variantName) {
1320
- stopWatcher(baseName);
1321
- markIdle(baseName);
1322
- await getConnectorManager().stopConnectors(baseName);
1323
- getScheduler().unloadSchedules(baseName);
1324
- getTokenBudget().removeBudget(baseName);
1325
- }
1326
- await getMindManager().stopMind(name);
1327
- publish2({
1328
- type: "mind_stopped",
1329
- mind: name,
1330
- summary: `${name} stopped`
1331
- }).catch((err) => logger_default.error("failed to publish mind_stopped activity", logger_default.errorData(err)));
1332
- }
1333
-
1334
184
  // src/lib/migrate-agents-to-minds.ts
1335
185
  import { execFileSync } from "child_process";
1336
- import { existsSync as existsSync5, readFileSync as readFileSync4, renameSync, writeFileSync as writeFileSync3 } from "fs";
1337
- import { resolve as resolve6 } from "path";
186
+ import { existsSync, readFileSync, renameSync, writeFileSync } from "fs";
187
+ import { resolve } from "path";
1338
188
  var TAG = "[migrate]";
1339
189
  function log(msg) {
1340
190
  console.error(`${TAG} ${msg}`);
@@ -1355,19 +205,19 @@ function bridgeEnvVar() {
1355
205
  }
1356
206
  }
1357
207
  function migrateRegistry(home) {
1358
- const oldPath = resolve6(home, "agents.json");
1359
- const newPath = resolve6(home, "minds.json");
1360
- if (!existsSync5(oldPath) || existsSync5(newPath)) {
208
+ const oldPath = resolve(home, "agents.json");
209
+ const newPath = resolve(home, "minds.json");
210
+ if (!existsSync(oldPath) || existsSync(newPath)) {
1361
211
  return readNamesFromRegistry(newPath);
1362
212
  }
1363
- const raw = readFileSync4(oldPath, "utf-8");
213
+ const raw = readFileSync(oldPath, "utf-8");
1364
214
  const entries = JSON.parse(raw);
1365
215
  for (const entry of entries) {
1366
216
  if (entry.stage === "mind") {
1367
217
  entry.stage = "sprouted";
1368
218
  }
1369
219
  }
1370
- writeFileSync3(newPath, `${JSON.stringify(entries, null, 2)}
220
+ writeFileSync(newPath, `${JSON.stringify(entries, null, 2)}
1371
221
  `);
1372
222
  try {
1373
223
  renameSync(oldPath, `${oldPath}.bak`);
@@ -1377,9 +227,9 @@ function migrateRegistry(home) {
1377
227
  return entries.map((e) => e.name);
1378
228
  }
1379
229
  function readNamesFromRegistry(path) {
1380
- if (!existsSync5(path)) return [];
230
+ if (!existsSync(path)) return [];
1381
231
  try {
1382
- const entries = JSON.parse(readFileSync4(path, "utf-8"));
232
+ const entries = JSON.parse(readFileSync(path, "utf-8"));
1383
233
  return entries.map((e) => e.name);
1384
234
  } catch {
1385
235
  return [];
@@ -1387,9 +237,9 @@ function readNamesFromRegistry(path) {
1387
237
  }
1388
238
  function migrateMindsDirectory(home) {
1389
239
  if (process.env.VOLUTE_MINDS_DIR) return;
1390
- const oldDir = resolve6(home, "agents");
1391
- const newDir = resolve6(home, "minds");
1392
- if (existsSync5(oldDir) && !existsSync5(newDir)) {
240
+ const oldDir = resolve(home, "agents");
241
+ const newDir = resolve(home, "minds");
242
+ if (existsSync(oldDir) && !existsSync(newDir)) {
1393
243
  try {
1394
244
  renameSync(oldDir, newDir);
1395
245
  log("renamed agents/ \u2192 minds/");
@@ -1400,10 +250,10 @@ function migrateMindsDirectory(home) {
1400
250
  }
1401
251
  function migrateLogFiles(home, names) {
1402
252
  for (const name of names) {
1403
- const logsDir = resolve6(home, "state", name, "logs");
1404
- const oldLog = resolve6(logsDir, "agent.log");
1405
- const newLog = resolve6(logsDir, "mind.log");
1406
- if (existsSync5(oldLog) && !existsSync5(newLog)) {
253
+ const logsDir = resolve(home, "state", name, "logs");
254
+ const oldLog = resolve(logsDir, "agent.log");
255
+ const newLog = resolve(logsDir, "mind.log");
256
+ if (existsSync(oldLog) && !existsSync(newLog)) {
1407
257
  try {
1408
258
  renameSync(oldLog, newLog);
1409
259
  log(`renamed ${name} agent.log \u2192 mind.log`);
@@ -1453,12 +303,12 @@ function migrateLinuxUsers(names) {
1453
303
  }
1454
304
  function migrateProfileScript() {
1455
305
  const profilePath = "/etc/profile.d/volute.sh";
1456
- if (!existsSync5(profilePath)) return;
306
+ if (!existsSync(profilePath)) return;
1457
307
  try {
1458
- const content = readFileSync4(profilePath, "utf-8");
308
+ const content = readFileSync(profilePath, "utf-8");
1459
309
  if (!content.includes("VOLUTE_AGENTS_DIR")) return;
1460
310
  const updated = content.replace(/VOLUTE_AGENTS_DIR/g, "VOLUTE_MINDS_DIR");
1461
- writeFileSync3(profilePath, updated);
311
+ writeFileSync(profilePath, updated);
1462
312
  log("updated /etc/profile.d/volute.sh: VOLUTE_AGENTS_DIR \u2192 VOLUTE_MINDS_DIR");
1463
313
  } catch (err) {
1464
314
  log(`failed to update profile script: ${err}`);
@@ -1466,37 +316,37 @@ function migrateProfileScript() {
1466
316
  }
1467
317
 
1468
318
  // src/lib/migrate-state.ts
1469
- import { copyFileSync, existsSync as existsSync6, mkdirSync as mkdirSync3, readdirSync as readdirSync2, renameSync as renameSync2 } from "fs";
1470
- import { resolve as resolve7 } from "path";
319
+ import { copyFileSync, existsSync as existsSync2, mkdirSync, readdirSync, renameSync as renameSync2 } from "fs";
320
+ import { resolve as resolve2 } from "path";
1471
321
  function migrateDotVoluteDir(name) {
1472
322
  const dir = mindDir(name);
1473
- const oldDir = resolve7(dir, ".volute");
1474
- const newDir = resolve7(dir, ".mind");
1475
- if (existsSync6(oldDir) && !existsSync6(newDir)) {
323
+ const oldDir = resolve2(dir, ".volute");
324
+ const newDir = resolve2(dir, ".mind");
325
+ if (existsSync2(oldDir) && !existsSync2(newDir)) {
1476
326
  renameSync2(oldDir, newDir);
1477
- } else if (existsSync6(oldDir) && existsSync6(newDir)) {
327
+ } else if (existsSync2(oldDir) && existsSync2(newDir)) {
1478
328
  console.warn(`[migrate] both .volute/ and .mind/ exist for ${name}, skipping rename`);
1479
329
  }
1480
330
  }
1481
331
  function migrateMindState(name) {
1482
- const src = resolve7(mindDir(name), ".mind");
1483
- if (!existsSync6(src)) return;
332
+ const src = resolve2(mindDir(name), ".mind");
333
+ if (!existsSync2(src)) return;
1484
334
  const dest = stateDir(name);
1485
- mkdirSync3(dest, { recursive: true });
335
+ mkdirSync(dest, { recursive: true });
1486
336
  for (const file of ["env.json", "channels.json"]) {
1487
- const srcPath = resolve7(src, file);
1488
- const destPath = resolve7(dest, file);
1489
- if (existsSync6(srcPath) && !existsSync6(destPath)) {
337
+ const srcPath = resolve2(src, file);
338
+ const destPath = resolve2(dest, file);
339
+ if (existsSync2(srcPath) && !existsSync2(destPath)) {
1490
340
  copyFileSync(srcPath, destPath);
1491
341
  }
1492
342
  }
1493
- const srcLogs = resolve7(src, "logs");
1494
- const destLogs = resolve7(dest, "logs");
1495
- if (existsSync6(srcLogs) && !existsSync6(destLogs)) {
1496
- mkdirSync3(destLogs, { recursive: true });
1497
- for (const file of readdirSync2(srcLogs)) {
343
+ const srcLogs = resolve2(src, "logs");
344
+ const destLogs = resolve2(dest, "logs");
345
+ if (existsSync2(srcLogs) && !existsSync2(destLogs)) {
346
+ mkdirSync(destLogs, { recursive: true });
347
+ for (const file of readdirSync(srcLogs)) {
1498
348
  try {
1499
- copyFileSync(resolve7(srcLogs, file), resolve7(destLogs, file));
349
+ copyFileSync(resolve2(srcLogs, file), resolve2(destLogs, file));
1500
350
  } catch (err) {
1501
351
  console.error(`[migrate] failed to copy log ${file} for ${name}:`, err);
1502
352
  }
@@ -1722,13 +572,13 @@ var authMiddleware = createMiddleware(async (c, next) => {
1722
572
  });
1723
573
 
1724
574
  // src/web/server.ts
1725
- import { existsSync as existsSync15 } from "fs";
575
+ import { existsSync as existsSync12 } from "fs";
1726
576
  import { readFile as readFile3, stat as stat3 } from "fs/promises";
1727
- import { dirname as dirname2, extname as extname3, resolve as resolve21 } from "path";
577
+ import { dirname, extname as extname3, resolve as resolve16 } from "path";
1728
578
  import { serve } from "@hono/node-server";
1729
579
 
1730
580
  // src/web/app.ts
1731
- import { Hono as Hono25 } from "hono";
581
+ import { Hono as Hono28 } from "hono";
1732
582
  import { bodyLimit } from "hono/body-limit";
1733
583
  import { csrf } from "hono/csrf";
1734
584
  import { HTTPException } from "hono/http-exception";
@@ -1740,7 +590,7 @@ import { streamSSE } from "hono/streaming";
1740
590
 
1741
591
  // src/lib/events/conversations.ts
1742
592
  import { randomUUID } from "crypto";
1743
- import { and as and2, desc, eq as eq3, inArray, isNull, sql } from "drizzle-orm";
593
+ import { and as and2, desc, eq as eq3, inArray, isNull, lt as lt2, sql } from "drizzle-orm";
1744
594
  async function createConversation(mindName, channel, opts) {
1745
595
  const db = await getDb();
1746
596
  const id = randomUUID();
@@ -1766,6 +616,11 @@ async function createConversation(mindName, channel, opts) {
1766
616
  );
1767
617
  }
1768
618
  });
619
+ fireWebhook({
620
+ event: "conversation_created",
621
+ mind: mindName ?? "",
622
+ data: { id, mindName, channel, type, name, title: opts?.title ?? null }
623
+ });
1769
624
  return {
1770
625
  id,
1771
626
  mind_name: mindName,
@@ -1866,21 +721,50 @@ async function addMessage(conversationId, role, senderName, content) {
1866
721
  content: msg.content,
1867
722
  createdAt: msg.created_at
1868
723
  });
724
+ const conv = await db.select({ mind_name: conversations.mind_name }).from(conversations).where(eq3(conversations.id, conversationId)).get();
725
+ fireWebhook({
726
+ event: "message_created",
727
+ mind: conv?.mind_name ?? "",
728
+ data: {
729
+ conversationId,
730
+ messageId: result.id,
731
+ role,
732
+ senderName,
733
+ content: content.filter((b) => b.type !== "image"),
734
+ createdAt: result.created_at
735
+ }
736
+ });
1869
737
  return msg;
1870
738
  }
1871
739
  async function getMessages(conversationId) {
1872
740
  const db = await getDb();
1873
741
  const rows = await db.select().from(messages).where(eq3(messages.conversation_id, conversationId)).orderBy(messages.created_at).all();
1874
- return rows.map((row) => {
1875
- let content;
1876
- try {
1877
- const parsed = JSON.parse(row.content);
1878
- content = Array.isArray(parsed) ? parsed : [{ type: "text", text: row.content }];
1879
- } catch {
1880
- content = [{ type: "text", text: row.content }];
1881
- }
1882
- return { ...row, content };
1883
- });
742
+ return rows.map(parseMessageRow);
743
+ }
744
+ async function getMessagesPaginated(conversationId, opts) {
745
+ const db = await getDb();
746
+ const limit = Math.min(Math.max(opts?.limit ?? 50, 1), 100);
747
+ const conditions = [eq3(messages.conversation_id, conversationId)];
748
+ if (opts?.before != null) {
749
+ conditions.push(lt2(messages.id, opts.before));
750
+ }
751
+ const rows = await db.select().from(messages).where(and2(...conditions)).orderBy(desc(messages.id)).limit(limit + 1).all();
752
+ const hasMore = rows.length > limit;
753
+ const page = rows.slice(0, limit).reverse();
754
+ return {
755
+ messages: page.map(parseMessageRow),
756
+ hasMore
757
+ };
758
+ }
759
+ function parseMessageRow(row) {
760
+ let content;
761
+ try {
762
+ const parsed = JSON.parse(row.content);
763
+ content = Array.isArray(parsed) ? parsed : [{ type: "text", text: row.content }];
764
+ } catch {
765
+ content = [{ type: "text", text: row.content }];
766
+ }
767
+ return { ...row, role: row.role, content };
1884
768
  }
1885
769
  async function listConversationsWithParticipants(userId) {
1886
770
  const convs = await listConversationsForUser(userId);
@@ -2022,7 +906,7 @@ var app = new Hono().get("/events", async (c) => {
2022
906
  activeMinds: getActiveMinds()
2023
907
  })
2024
908
  });
2025
- const unsubActivity = subscribe2((event) => {
909
+ const unsubActivity = subscribe((event) => {
2026
910
  stream.writeSSE({
2027
911
  data: JSON.stringify({ event: "activity", ...event })
2028
912
  }).catch((err) => {
@@ -2031,7 +915,7 @@ var app = new Hono().get("/events", async (c) => {
2031
915
  });
2032
916
  cleanups.push(unsubActivity);
2033
917
  for (const conv of conversations2) {
2034
- const unsubConv = subscribe(conv.id, (event) => {
918
+ const unsubConv = subscribe2(conv.id, (event) => {
2035
919
  stream.writeSSE({
2036
920
  data: JSON.stringify({ event: "conversation", conversationId: conv.id, ...event })
2037
921
  }).catch((err) => {
@@ -2046,8 +930,8 @@ var app = new Hono().get("/events", async (c) => {
2046
930
  });
2047
931
  }, 15e3);
2048
932
  cleanups.push(() => clearInterval(keepAlive));
2049
- await new Promise((resolve23) => {
2050
- stream.onAbort(() => resolve23());
933
+ await new Promise((resolve18) => {
934
+ stream.onAbort(() => resolve18());
2051
935
  });
2052
936
  } finally {
2053
937
  for (const cleanup of cleanups) {
@@ -2609,16 +1493,16 @@ __export(volute_exports, {
2609
1493
  read: () => read4,
2610
1494
  send: () => send4
2611
1495
  });
2612
- import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
2613
- import { resolve as resolve8 } from "path";
1496
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
1497
+ import { resolve as resolve3 } from "path";
2614
1498
  function getDaemonConfig() {
2615
- const configPath2 = resolve8(voluteHome(), "daemon.json");
2616
- if (!existsSync7(configPath2)) {
1499
+ const configPath2 = resolve3(voluteHome(), "daemon.json");
1500
+ if (!existsSync3(configPath2)) {
2617
1501
  throw new Error("Volute daemon is not running");
2618
1502
  }
2619
1503
  let config;
2620
1504
  try {
2621
- config = JSON.parse(readFileSync5(configPath2, "utf-8"));
1505
+ config = JSON.parse(readFileSync2(configPath2, "utf-8"));
2622
1506
  } catch (err) {
2623
1507
  throw new Error(`Failed to parse ${configPath2}: ${err}`);
2624
1508
  }
@@ -3055,14 +1939,14 @@ var sharedEnvApp = new Hono5().get("/", (c) => {
3055
1939
  var env_default = app5;
3056
1940
 
3057
1941
  // src/web/api/file-sharing.ts
3058
- import { readFileSync as readFileSync7, statSync as statSync2 } from "fs";
3059
- import { resolve as resolve10 } from "path";
1942
+ import { readFileSync as readFileSync4, statSync } from "fs";
1943
+ import { resolve as resolve5 } from "path";
3060
1944
  import { Hono as Hono6 } from "hono";
3061
1945
 
3062
1946
  // src/lib/file-sharing.ts
3063
1947
  import { randomBytes } from "crypto";
3064
- import { existsSync as existsSync8, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync4 } from "fs";
3065
- import { basename, join as join2, normalize, resolve as resolve9 } from "path";
1948
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync2 } from "fs";
1949
+ import { basename, join, normalize, resolve as resolve4 } from "path";
3066
1950
  function validateFilePath(filePath) {
3067
1951
  if (!filePath) return "File path is required";
3068
1952
  const normalized = normalize(filePath);
@@ -3075,13 +1959,13 @@ function validateFilePath(filePath) {
3075
1959
  return null;
3076
1960
  }
3077
1961
  function configPath(dir) {
3078
- return resolve9(dir, "home", ".config", "file-sharing.json");
1962
+ return resolve4(dir, "home", ".config", "file-sharing.json");
3079
1963
  }
3080
1964
  function readFileSharingConfig(dir) {
3081
1965
  const p = configPath(dir);
3082
- if (!existsSync8(p)) return {};
1966
+ if (!existsSync4(p)) return {};
3083
1967
  try {
3084
- return JSON.parse(readFileSync6(p, "utf-8"));
1968
+ return JSON.parse(readFileSync3(p, "utf-8"));
3085
1969
  } catch (err) {
3086
1970
  console.warn(`[file-sharing] failed to parse config at ${p}:`, err);
3087
1971
  return {};
@@ -3089,8 +1973,8 @@ function readFileSharingConfig(dir) {
3089
1973
  }
3090
1974
  function writeFileSharingConfig(dir, config) {
3091
1975
  const p = configPath(dir);
3092
- mkdirSync4(resolve9(p, ".."), { recursive: true });
3093
- writeFileSync4(p, `${JSON.stringify(config, null, 2)}
1976
+ mkdirSync2(resolve4(p, ".."), { recursive: true });
1977
+ writeFileSync2(p, `${JSON.stringify(config, null, 2)}
3094
1978
  `);
3095
1979
  }
3096
1980
  function isTrustedSender(dir, sender) {
@@ -3113,7 +1997,7 @@ function removeTrust(dir, sender) {
3113
1997
  writeFileSharingConfig(dir, config);
3114
1998
  }
3115
1999
  function pendingDir(receiver) {
3116
- return resolve9(stateDir(receiver), "pending-files");
2000
+ return resolve4(stateDir(receiver), "pending-files");
3117
2001
  }
3118
2002
  function validateId(id) {
3119
2003
  if (!id || id.includes("/") || id.includes("\\") || id.includes("..")) {
@@ -3132,8 +2016,8 @@ function stageFile(receiver, sender, filename, content, originalPath) {
3132
2016
  throw new Error("Invalid sender name");
3133
2017
  }
3134
2018
  const id = generateId(sender);
3135
- const dir = resolve9(pendingDir(receiver), id);
3136
- mkdirSync4(dir, { recursive: true });
2019
+ const dir = resolve4(pendingDir(receiver), id);
2020
+ mkdirSync2(dir, { recursive: true });
3137
2021
  const metadata = {
3138
2022
  id,
3139
2023
  sender,
@@ -3142,22 +2026,22 @@ function stageFile(receiver, sender, filename, content, originalPath) {
3142
2026
  size: content.length,
3143
2027
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
3144
2028
  };
3145
- writeFileSync4(resolve9(dir, "metadata.json"), `${JSON.stringify(metadata, null, 2)}
2029
+ writeFileSync2(resolve4(dir, "metadata.json"), `${JSON.stringify(metadata, null, 2)}
3146
2030
  `);
3147
- writeFileSync4(resolve9(dir, "data"), content);
2031
+ writeFileSync2(resolve4(dir, "data"), content);
3148
2032
  return { id };
3149
2033
  }
3150
2034
  function listPending(receiver) {
3151
2035
  const dir = pendingDir(receiver);
3152
- if (!existsSync8(dir)) return [];
3153
- const entries = readdirSync3(dir, { withFileTypes: true });
2036
+ if (!existsSync4(dir)) return [];
2037
+ const entries = readdirSync2(dir, { withFileTypes: true });
3154
2038
  const result = [];
3155
2039
  for (const entry of entries) {
3156
2040
  if (!entry.isDirectory()) continue;
3157
- const metaPath = resolve9(dir, entry.name, "metadata.json");
3158
- if (!existsSync8(metaPath)) continue;
2041
+ const metaPath = resolve4(dir, entry.name, "metadata.json");
2042
+ if (!existsSync4(metaPath)) continue;
3159
2043
  try {
3160
- result.push(JSON.parse(readFileSync6(metaPath, "utf-8")));
2044
+ result.push(JSON.parse(readFileSync3(metaPath, "utf-8")));
3161
2045
  } catch (err) {
3162
2046
  console.warn(`[file-sharing] skipping malformed pending entry ${entry.name}:`, err);
3163
2047
  }
@@ -3166,10 +2050,10 @@ function listPending(receiver) {
3166
2050
  }
3167
2051
  function getPending(receiver, id) {
3168
2052
  validateId(id);
3169
- const metaPath = resolve9(pendingDir(receiver), id, "metadata.json");
3170
- if (!existsSync8(metaPath)) return null;
2053
+ const metaPath = resolve4(pendingDir(receiver), id, "metadata.json");
2054
+ if (!existsSync4(metaPath)) return null;
3171
2055
  try {
3172
- return JSON.parse(readFileSync6(metaPath, "utf-8"));
2056
+ return JSON.parse(readFileSync3(metaPath, "utf-8"));
3173
2057
  } catch (err) {
3174
2058
  console.warn(`[file-sharing] failed to read pending metadata for ${id}:`, err);
3175
2059
  return null;
@@ -3184,27 +2068,27 @@ function deliverFile(receiverDir, sender, filename, content, inboxPath) {
3184
2068
  if (sender.includes("/") || sender.includes("\\")) {
3185
2069
  throw new Error("Invalid sender name");
3186
2070
  }
3187
- const destDir = resolve9(receiverDir, "home", inbox, sender);
3188
- mkdirSync4(destDir, { recursive: true });
3189
- const destPath = resolve9(destDir, basename(filename));
3190
- writeFileSync4(destPath, content);
3191
- return join2(inbox, sender, basename(filename));
2071
+ const destDir = resolve4(receiverDir, "home", inbox, sender);
2072
+ mkdirSync2(destDir, { recursive: true });
2073
+ const destPath = resolve4(destDir, basename(filename));
2074
+ writeFileSync2(destPath, content);
2075
+ return join(inbox, sender, basename(filename));
3192
2076
  }
3193
2077
  function acceptPending(receiver, id, receiverDir) {
3194
2078
  const meta = getPending(receiver, id);
3195
2079
  if (!meta) throw new Error(`Pending file not found: ${id}`);
3196
- const dataPath = resolve9(pendingDir(receiver), id, "data");
3197
- const content = readFileSync6(dataPath);
2080
+ const dataPath = resolve4(pendingDir(receiver), id, "data");
2081
+ const content = readFileSync3(dataPath);
3198
2082
  const config = readFileSharingConfig(receiverDir);
3199
2083
  const inboxPath = config.inboxPath ?? "inbox";
3200
2084
  const destPath = deliverFile(receiverDir, meta.sender, meta.filename, content, inboxPath);
3201
- rmSync(resolve9(pendingDir(receiver), id), { recursive: true });
2085
+ rmSync(resolve4(pendingDir(receiver), id), { recursive: true });
3202
2086
  return { sender: meta.sender, filename: meta.filename, destPath };
3203
2087
  }
3204
2088
  function rejectPending(receiver, id) {
3205
2089
  const meta = getPending(receiver, id);
3206
2090
  if (!meta) throw new Error(`Pending file not found: ${id}`);
3207
- rmSync(resolve9(pendingDir(receiver), id), { recursive: true });
2091
+ rmSync(resolve4(pendingDir(receiver), id), { recursive: true });
3208
2092
  return { sender: meta.sender, filename: meta.filename };
3209
2093
  }
3210
2094
  function formatFileSize(bytes) {
@@ -3245,9 +2129,9 @@ var app6 = new Hono6().post("/:name/files/send", async (c) => {
3245
2129
  const pathErr = validateFilePath(body.filePath);
3246
2130
  if (pathErr) return c.json({ error: pathErr }, 400);
3247
2131
  const senderDir = mindDir(senderName);
3248
- const filePath = resolve10(senderDir, "home", body.filePath);
2132
+ const filePath = resolve5(senderDir, "home", body.filePath);
3249
2133
  const MAX_FILE_SIZE = 50 * 1024 * 1024;
3250
- const stat4 = statSync2(filePath, { throwIfNoEntry: false });
2134
+ const stat4 = statSync(filePath, { throwIfNoEntry: false });
3251
2135
  if (!stat4) return c.json({ error: `File not found: ${body.filePath}` }, 404);
3252
2136
  if (stat4.size > MAX_FILE_SIZE) {
3253
2137
  return c.json(
@@ -3259,7 +2143,7 @@ var app6 = new Hono6().post("/:name/files/send", async (c) => {
3259
2143
  }
3260
2144
  let content;
3261
2145
  try {
3262
- content = readFileSync7(filePath);
2146
+ content = readFileSync4(filePath);
3263
2147
  } catch {
3264
2148
  return c.json({ error: `File not found: ${body.filePath}` }, 404);
3265
2149
  }
@@ -3361,9 +2245,9 @@ var app6 = new Hono6().post("/:name/files/send", async (c) => {
3361
2245
  var file_sharing_default = app6;
3362
2246
 
3363
2247
  // src/web/api/files.ts
3364
- import { existsSync as existsSync9 } from "fs";
2248
+ import { existsSync as existsSync5 } from "fs";
3365
2249
  import { readdir, readFile, realpath, stat } from "fs/promises";
3366
- import { extname, resolve as resolve11 } from "path";
2250
+ import { extname, resolve as resolve6 } from "path";
3367
2251
  import { Hono as Hono7 } from "hono";
3368
2252
  var ALLOWED_FILES = /* @__PURE__ */ new Set(["SOUL.md", "MEMORY.md", "CLAUDE.md", "VOLUTE.md"]);
3369
2253
  var AVATAR_MIME = {
@@ -3384,8 +2268,8 @@ var app7 = new Hono7().get("/:name/avatar", async (c) => {
3384
2268
  const ext = extname(config.avatar).toLowerCase();
3385
2269
  const mime = AVATAR_MIME[ext];
3386
2270
  if (!mime) return c.json({ error: "Invalid avatar extension" }, 400);
3387
- const homeDir = resolve11(dir, "home");
3388
- const avatarPath = resolve11(homeDir, config.avatar);
2271
+ const homeDir = resolve6(dir, "home");
2272
+ const avatarPath = resolve6(homeDir, config.avatar);
3389
2273
  if (!avatarPath.startsWith(`${homeDir}/`)) return c.json({ error: "Invalid avatar path" }, 400);
3390
2274
  let realAvatarPath;
3391
2275
  try {
@@ -3414,8 +2298,8 @@ var app7 = new Hono7().get("/:name/avatar", async (c) => {
3414
2298
  const entry = findMind(name);
3415
2299
  if (!entry) return c.json({ error: "Mind not found" }, 404);
3416
2300
  const dir = mindDir(name);
3417
- const homeDir = resolve11(dir, "home");
3418
- if (!existsSync9(homeDir)) return c.json({ error: "Home directory missing" }, 404);
2301
+ const homeDir = resolve6(dir, "home");
2302
+ if (!existsSync5(homeDir)) return c.json({ error: "Home directory missing" }, 404);
3419
2303
  const allFiles = await readdir(homeDir);
3420
2304
  const files = allFiles.filter((f) => f.endsWith(".md") && ALLOWED_FILES.has(f));
3421
2305
  return c.json(files);
@@ -3428,8 +2312,8 @@ var app7 = new Hono7().get("/:name/avatar", async (c) => {
3428
2312
  const entry = findMind(name);
3429
2313
  if (!entry) return c.json({ error: "Mind not found" }, 404);
3430
2314
  const dir = mindDir(name);
3431
- const filePath = resolve11(dir, "home", filename);
3432
- if (!existsSync9(filePath)) {
2315
+ const filePath = resolve6(dir, "home", filename);
2316
+ if (!existsSync5(filePath)) {
3433
2317
  return c.json({ error: "File not found" }, 404);
3434
2318
  }
3435
2319
  const content = await readFile(filePath, "utf-8");
@@ -3442,19 +2326,19 @@ import { Hono as Hono8 } from "hono";
3442
2326
 
3443
2327
  // src/lib/identity.ts
3444
2328
  import { createHash, generateKeyPairSync, sign, verify } from "crypto";
3445
- import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
3446
- import { resolve as resolve12 } from "path";
2329
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
2330
+ import { resolve as resolve7 } from "path";
3447
2331
  function generateIdentity(mindDir2) {
3448
- const identityDir = resolve12(mindDir2, ".mind/identity");
3449
- mkdirSync5(identityDir, { recursive: true });
2332
+ const identityDir = resolve7(mindDir2, ".mind/identity");
2333
+ mkdirSync3(identityDir, { recursive: true });
3450
2334
  const { publicKey, privateKey } = generateKeyPairSync("ed25519", {
3451
2335
  publicKeyEncoding: { type: "spki", format: "pem" },
3452
2336
  privateKeyEncoding: { type: "pkcs8", format: "pem" }
3453
2337
  });
3454
- const privatePath = resolve12(identityDir, "private.pem");
3455
- const publicPath = resolve12(identityDir, "public.pem");
3456
- writeFileSync5(privatePath, privateKey, { mode: 384 });
3457
- writeFileSync5(publicPath, publicKey, { mode: 420 });
2338
+ const privatePath = resolve7(identityDir, "private.pem");
2339
+ const publicPath = resolve7(identityDir, "public.pem");
2340
+ writeFileSync3(privatePath, privateKey, { mode: 384 });
2341
+ writeFileSync3(publicPath, publicKey, { mode: 420 });
3458
2342
  const config = readVoluteConfig(mindDir2) ?? {};
3459
2343
  config.identity = {
3460
2344
  privateKey: ".mind/identity/private.pem",
@@ -3467,17 +2351,17 @@ function getPrivateKey(mindDir2) {
3467
2351
  const config = readVoluteConfig(mindDir2);
3468
2352
  const relPath = config?.identity?.privateKey;
3469
2353
  if (!relPath) return null;
3470
- const fullPath = resolve12(mindDir2, relPath);
3471
- if (!existsSync10(fullPath)) return null;
3472
- return readFileSync8(fullPath, "utf-8");
2354
+ const fullPath = resolve7(mindDir2, relPath);
2355
+ if (!existsSync6(fullPath)) return null;
2356
+ return readFileSync5(fullPath, "utf-8");
3473
2357
  }
3474
2358
  function getPublicKey(mindDir2) {
3475
2359
  const config = readVoluteConfig(mindDir2);
3476
2360
  const relPath = config?.identity?.publicKey;
3477
2361
  if (!relPath) return null;
3478
- const fullPath = resolve12(mindDir2, relPath);
3479
- if (!existsSync10(fullPath)) return null;
3480
- return readFileSync8(fullPath, "utf-8");
2362
+ const fullPath = resolve7(mindDir2, relPath);
2363
+ if (!existsSync6(fullPath)) return null;
2364
+ return readFileSync5(fullPath, "utf-8");
3481
2365
  }
3482
2366
  function getFingerprint(publicKeyPem) {
3483
2367
  return createHash("sha256").update(publicKeyPem).digest("hex");
@@ -3529,21 +2413,21 @@ var app8 = new Hono8().get("/:fingerprint", (c) => {
3529
2413
  var keys_default = app8;
3530
2414
 
3531
2415
  // src/web/api/logs.ts
3532
- import { spawn as spawn2 } from "child_process";
3533
- import { existsSync as existsSync11 } from "fs";
3534
- import { resolve as resolve13 } from "path";
2416
+ import { spawn } from "child_process";
2417
+ import { existsSync as existsSync7 } from "fs";
2418
+ import { resolve as resolve8 } from "path";
3535
2419
  import { Hono as Hono9 } from "hono";
3536
2420
  import { streamSSE as streamSSE2 } from "hono/streaming";
3537
2421
  var app9 = new Hono9().get("/:name/logs", async (c) => {
3538
2422
  const name = c.req.param("name");
3539
2423
  const entry = findMind(name);
3540
2424
  if (!entry) return c.json({ error: "Mind not found" }, 404);
3541
- const logFile = resolve13(stateDir(name), "logs", "mind.log");
3542
- if (!existsSync11(logFile)) {
2425
+ const logFile = resolve8(stateDir(name), "logs", "mind.log");
2426
+ if (!existsSync7(logFile)) {
3543
2427
  return c.json({ error: "No log file found" }, 404);
3544
2428
  }
3545
2429
  return streamSSE2(c, async (stream) => {
3546
- const tail = spawn2("tail", ["-n", "200", "-f", logFile]);
2430
+ const tail = spawn("tail", ["-n", "200", "-f", logFile]);
3547
2431
  const onData = (data) => {
3548
2432
  const lines = data.toString().split("\n");
3549
2433
  for (const line of lines) {
@@ -3557,28 +2441,28 @@ var app9 = new Hono9().get("/:name/logs", async (c) => {
3557
2441
  stream.onAbort(() => {
3558
2442
  tail.kill();
3559
2443
  });
3560
- await new Promise((resolve23) => {
3561
- tail.on("exit", resolve23);
3562
- stream.onAbort(resolve23);
2444
+ await new Promise((resolve18) => {
2445
+ tail.on("exit", resolve18);
2446
+ stream.onAbort(resolve18);
3563
2447
  });
3564
2448
  });
3565
2449
  }).get("/:name/logs/tail", async (c) => {
3566
2450
  const name = c.req.param("name");
3567
2451
  const entry = findMind(name);
3568
2452
  if (!entry) return c.json({ error: "Mind not found" }, 404);
3569
- const logFile = resolve13(stateDir(name), "logs", "mind.log");
3570
- if (!existsSync11(logFile)) {
2453
+ const logFile = resolve8(stateDir(name), "logs", "mind.log");
2454
+ if (!existsSync7(logFile)) {
3571
2455
  return c.json({ error: "No log file found" }, 404);
3572
2456
  }
3573
2457
  const nParam = parseInt(c.req.query("n") ?? "50", 10);
3574
2458
  const n = Number.isFinite(nParam) && nParam > 0 ? Math.min(nParam, 1e4) : 50;
3575
- const tail = spawn2("tail", ["-n", String(n), logFile]);
2459
+ const tail = spawn("tail", ["-n", String(n), logFile]);
3576
2460
  let output = "";
3577
2461
  tail.stdout.on("data", (data) => {
3578
2462
  output += data.toString();
3579
2463
  });
3580
- await new Promise((resolve23) => {
3581
- tail.on("exit", resolve23);
2464
+ await new Promise((resolve18) => {
2465
+ tail.on("exit", resolve18);
3582
2466
  });
3583
2467
  return c.text(output);
3584
2468
  });
@@ -3668,33 +2552,33 @@ var mind_skills_default = app10;
3668
2552
  // src/web/api/minds.ts
3669
2553
  import {
3670
2554
  cpSync,
3671
- existsSync as existsSync12,
3672
- mkdirSync as mkdirSync7,
3673
- readdirSync as readdirSync5,
3674
- readFileSync as readFileSync11,
3675
- rmSync as rmSync2,
3676
- writeFileSync as writeFileSync8
2555
+ existsSync as existsSync9,
2556
+ mkdirSync as mkdirSync5,
2557
+ readdirSync as readdirSync4,
2558
+ readFileSync as readFileSync8,
2559
+ rmSync as rmSync3,
2560
+ writeFileSync as writeFileSync6
3677
2561
  } from "fs";
3678
- import { resolve as resolve16 } from "path";
2562
+ import { resolve as resolve11 } from "path";
3679
2563
  import { zValidator as zValidator3 } from "@hono/zod-validator";
3680
2564
  import { and as and3, desc as desc3, eq as eq4, sql as sql2 } from "drizzle-orm";
3681
2565
  import { Hono as Hono11 } from "hono";
3682
2566
  import { z as z3 } from "zod";
3683
2567
 
3684
2568
  // src/lib/consolidate.ts
3685
- import { readdirSync as readdirSync4, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
3686
- import { resolve as resolve14 } from "path";
2569
+ import { readdirSync as readdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
2570
+ import { resolve as resolve9 } from "path";
3687
2571
  async function consolidateMemory(mindDir2) {
3688
- const soulPath = resolve14(mindDir2, "home/SOUL.md");
3689
- const memoryPath = resolve14(mindDir2, "home/MEMORY.md");
3690
- const memoryDir = resolve14(mindDir2, "home/memory");
3691
- const soul = readFileSync9(soulPath, "utf-8");
2572
+ const soulPath = resolve9(mindDir2, "home/SOUL.md");
2573
+ const memoryPath = resolve9(mindDir2, "home/MEMORY.md");
2574
+ const memoryDir = resolve9(mindDir2, "home/memory");
2575
+ const soul = readFileSync6(soulPath, "utf-8");
3692
2576
  const logs = [];
3693
2577
  try {
3694
- const files = readdirSync4(memoryDir).filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f)).sort();
2578
+ const files = readdirSync3(memoryDir).filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f)).sort();
3695
2579
  for (const filename of files) {
3696
2580
  const date = filename.replace(".md", "");
3697
- const content2 = readFileSync9(resolve14(memoryDir, filename), "utf-8").trim();
2581
+ const content2 = readFileSync6(resolve9(memoryDir, filename), "utf-8").trim();
3698
2582
  if (content2) {
3699
2583
  logs.push(`### ${date}
3700
2584
 
@@ -3744,7 +2628,7 @@ ${content2}`);
3744
2628
  const data = await res.json();
3745
2629
  const content = data.content.filter((b) => b.type === "text" && b.text).map((b) => b.text).join("").trim();
3746
2630
  if (content) {
3747
- writeFileSync6(memoryPath, `${content}
2631
+ writeFileSync4(memoryPath, `${content}
3748
2632
  `);
3749
2633
  console.log("MEMORY.md created successfully.");
3750
2634
  } else {
@@ -3754,11 +2638,11 @@ ${content2}`);
3754
2638
 
3755
2639
  // src/lib/convert-session.ts
3756
2640
  import { randomUUID as randomUUID2 } from "crypto";
3757
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync7 } from "fs";
2641
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
3758
2642
  import { homedir } from "os";
3759
- import { resolve as resolve15 } from "path";
2643
+ import { resolve as resolve10 } from "path";
3760
2644
  function convertSession(opts) {
3761
- const lines = readFileSync10(opts.sessionPath, "utf-8").trim().split("\n");
2645
+ const lines = readFileSync7(opts.sessionPath, "utf-8").trim().split("\n");
3762
2646
  const sessionId = randomUUID2();
3763
2647
  const idMap = /* @__PURE__ */ new Map();
3764
2648
  const messages2 = [];
@@ -3872,10 +2756,10 @@ function convertSession(opts) {
3872
2756
  }
3873
2757
  }
3874
2758
  const projectId = opts.projectDir.replace(/\//g, "-");
3875
- const sdkDir = resolve15(homedir(), ".claude", "projects", projectId);
3876
- mkdirSync6(sdkDir, { recursive: true });
3877
- const sdkPath = resolve15(sdkDir, `${sessionId}.jsonl`);
3878
- writeFileSync7(sdkPath, `${sdkEvents.join("\n")}
2759
+ const sdkDir = resolve10(homedir(), ".claude", "projects", projectId);
2760
+ mkdirSync4(sdkDir, { recursive: true });
2761
+ const sdkPath = resolve10(sdkDir, `${sessionId}.jsonl`);
2762
+ writeFileSync5(sdkPath, `${sdkEvents.join("\n")}
3879
2763
  `);
3880
2764
  console.log(`Converted ${sdkEvents.length} messages \u2192 ${sdkPath}`);
3881
2765
  return sessionId;
@@ -3940,7 +2824,7 @@ function subscribe3(mind, callback) {
3940
2824
  if (set.size === 0) subscribers.delete(mind);
3941
2825
  };
3942
2826
  }
3943
- function publish3(mind, event) {
2827
+ function publish2(mind, event) {
3944
2828
  const set = subscribers.get(mind);
3945
2829
  if (!set) return;
3946
2830
  for (const cb of set) {
@@ -3954,11 +2838,53 @@ function publish3(mind, event) {
3954
2838
  }
3955
2839
  }
3956
2840
 
2841
+ // src/lib/variant-cleanup.ts
2842
+ import { existsSync as existsSync8, rmSync as rmSync2 } from "fs";
2843
+ async function cleanupVariant(mindName, variantName, projectRoot, variantPath, opts) {
2844
+ if (opts?.stop) {
2845
+ try {
2846
+ await getMindManager().stopMind(`${mindName}@${variantName}`);
2847
+ } catch {
2848
+ }
2849
+ }
2850
+ if (existsSync8(variantPath)) {
2851
+ try {
2852
+ await gitExec(["worktree", "remove", "--force", variantPath], { cwd: projectRoot });
2853
+ } catch {
2854
+ rmSync2(variantPath, { recursive: true, force: true });
2855
+ try {
2856
+ await gitExec(["worktree", "prune"], { cwd: projectRoot });
2857
+ } catch {
2858
+ }
2859
+ }
2860
+ }
2861
+ try {
2862
+ await gitExec(["branch", "-D", variantName], { cwd: projectRoot });
2863
+ } catch {
2864
+ }
2865
+ try {
2866
+ removeVariant(mindName, variantName);
2867
+ } catch {
2868
+ }
2869
+ try {
2870
+ chownMindDir(projectRoot, mindName);
2871
+ } catch (err) {
2872
+ logger_default.error(`failed to fix ownership during variant cleanup for ${mindName}`, logger_default.errorData(err));
2873
+ }
2874
+ }
2875
+
3957
2876
  // src/web/api/minds.ts
3958
2877
  async function getMindStatus(name, port) {
3959
2878
  const manager = getMindManager();
3960
2879
  let status = "stopped";
3961
- if (manager.isRunning(name)) {
2880
+ try {
2881
+ const { getSleepManagerIfReady } = await import("./sleep-manager-3RWUX2ZR.js");
2882
+ if (getSleepManagerIfReady()?.isSleeping(name)) {
2883
+ status = "sleeping";
2884
+ }
2885
+ } catch {
2886
+ }
2887
+ if (status !== "sleeping" && manager.isRunning(name)) {
3962
2888
  const health = await checkHealth(port);
3963
2889
  status = health.ok ? "running" : "starting";
3964
2890
  }
@@ -4010,7 +2936,7 @@ async function initTemplateBranch(projectRoot, composedDir, manifest, mindName,
4010
2936
  await gitExec(["commit", "-m", "initial commit"], opts);
4011
2937
  }
4012
2938
  async function updateTemplateBranch(projectRoot, template, mindName) {
4013
- const tempWorktree = resolve16(projectRoot, ".variants", "_template_update");
2939
+ const tempWorktree = resolve11(projectRoot, ".variants", "_template_update");
4014
2940
  let branchExists = false;
4015
2941
  try {
4016
2942
  await gitExec(["rev-parse", "--verify", TEMPLATE_BRANCH], { cwd: projectRoot });
@@ -4021,8 +2947,8 @@ async function updateTemplateBranch(projectRoot, template, mindName) {
4021
2947
  await gitExec(["worktree", "remove", "--force", tempWorktree], { cwd: projectRoot });
4022
2948
  } catch {
4023
2949
  }
4024
- if (existsSync12(tempWorktree)) {
4025
- rmSync2(tempWorktree, { recursive: true, force: true });
2950
+ if (existsSync9(tempWorktree)) {
2951
+ rmSync3(tempWorktree, { recursive: true, force: true });
4026
2952
  }
4027
2953
  const templatesRoot = findTemplatesRoot();
4028
2954
  const { composedDir, manifest } = composeTemplate(templatesRoot, template);
@@ -4042,9 +2968,9 @@ async function updateTemplateBranch(projectRoot, template, mindName) {
4042
2968
  });
4043
2969
  }
4044
2970
  copyTemplateToDir(composedDir, tempWorktree, mindName, manifest);
4045
- const initDir = resolve16(tempWorktree, ".init");
4046
- if (existsSync12(initDir)) {
4047
- rmSync2(initDir, { recursive: true, force: true });
2971
+ const initDir = resolve11(tempWorktree, ".init");
2972
+ if (existsSync9(initDir)) {
2973
+ rmSync3(initDir, { recursive: true, force: true });
4048
2974
  }
4049
2975
  await gitExec(["add", "-A"], { cwd: tempWorktree });
4050
2976
  try {
@@ -4057,10 +2983,10 @@ async function updateTemplateBranch(projectRoot, template, mindName) {
4057
2983
  await gitExec(["worktree", "remove", "--force", tempWorktree], { cwd: projectRoot });
4058
2984
  } catch {
4059
2985
  }
4060
- if (existsSync12(tempWorktree)) {
4061
- rmSync2(tempWorktree, { recursive: true, force: true });
2986
+ if (existsSync9(tempWorktree)) {
2987
+ rmSync3(tempWorktree, { recursive: true, force: true });
4062
2988
  }
4063
- rmSync2(composedDir, { recursive: true, force: true });
2989
+ rmSync3(composedDir, { recursive: true, force: true });
4064
2990
  }
4065
2991
  }
4066
2992
  async function mergeTemplateBranch(worktreeDir) {
@@ -4083,14 +3009,14 @@ async function mergeTemplateBranch(worktreeDir) {
4083
3009
  async function npmInstallAsMind(cwd, mindName) {
4084
3010
  if (isIsolationEnabled()) {
4085
3011
  const [cmd, args] = wrapForIsolation("npm", ["install"], mindName);
4086
- await exec(cmd, args, { cwd, env: { ...process.env, HOME: resolve16(cwd, "home") } });
3012
+ await exec(cmd, args, { cwd, env: { ...process.env, HOME: resolve11(cwd, "home") } });
4087
3013
  } else {
4088
3014
  await exec("npm", ["install"], { cwd });
4089
3015
  }
4090
3016
  }
4091
3017
  async function importFromArchive(c, tempDir, nameOverride, manifest) {
4092
- const extractedMindDir = resolve16(tempDir, "mind");
4093
- if (!existsSync12(extractedMindDir)) {
3018
+ const extractedMindDir = resolve11(tempDir, "mind");
3019
+ if (!existsSync9(extractedMindDir)) {
4094
3020
  return c.json({ error: "Invalid archive: missing mind/ directory" }, 400);
4095
3021
  }
4096
3022
  if (!manifest?.includes || !manifest.name || !manifest.template) {
@@ -4108,21 +3034,21 @@ async function importFromFullArchive(c, tempDir, extractedMindDir, nameOverride,
4108
3034
  if (findMind(name)) return c.json({ error: `Mind already exists: ${name}` }, 409);
4109
3035
  ensureVoluteHome();
4110
3036
  const dest = mindDir(name);
4111
- if (existsSync12(dest)) return c.json({ error: "Mind directory already exists" }, 409);
3037
+ if (existsSync9(dest)) return c.json({ error: "Mind directory already exists" }, 409);
4112
3038
  try {
4113
3039
  cpSync(extractedMindDir, dest, { recursive: true });
4114
3040
  if (!manifest.includes.identity) {
4115
3041
  generateIdentity(dest);
4116
3042
  }
4117
3043
  const state = stateDir(name);
4118
- mkdirSync7(state, { recursive: true });
4119
- const channelsJson = resolve16(tempDir, "state/channels.json");
4120
- if (existsSync12(channelsJson)) {
4121
- cpSync(channelsJson, resolve16(state, "channels.json"));
3044
+ mkdirSync5(state, { recursive: true });
3045
+ const channelsJson = resolve11(tempDir, "state/channels.json");
3046
+ if (existsSync9(channelsJson)) {
3047
+ cpSync(channelsJson, resolve11(state, "channels.json"));
4122
3048
  }
4123
- const envJson = resolve16(tempDir, "state/env.json");
4124
- if (existsSync12(envJson)) {
4125
- cpSync(envJson, resolve16(state, "env.json"));
3049
+ const envJson = resolve11(tempDir, "state/env.json");
3050
+ if (existsSync9(envJson)) {
3051
+ cpSync(envJson, resolve11(state, "env.json"));
4126
3052
  }
4127
3053
  const port = nextPort();
4128
3054
  addMind(name, port, manifest.stage, manifest.template);
@@ -4131,36 +3057,36 @@ async function importFromFullArchive(c, tempDir, extractedMindDir, nameOverride,
4131
3057
  } catch (err) {
4132
3058
  logger_default.warn(`failed to set template hash for ${name}`, logger_default.errorData(err));
4133
3059
  }
4134
- const homeDir = resolve16(dest, "home");
3060
+ const homeDir = resolve11(dest, "home");
4135
3061
  ensureVoluteGroup();
4136
3062
  createMindUser(name, homeDir);
4137
3063
  chownMindDir(dest, name);
4138
3064
  await npmInstallAsMind(dest, name);
4139
3065
  await importHistoryFromArchive(name, tempDir);
4140
3066
  importSessionsFromArchive(dest, tempDir);
4141
- if (!existsSync12(resolve16(dest, ".git"))) {
3067
+ if (!existsSync9(resolve11(dest, ".git"))) {
4142
3068
  try {
4143
- const env = isIsolationEnabled() ? { ...process.env, HOME: resolve16(dest, "home") } : void 0;
3069
+ const env = isIsolationEnabled() ? { ...process.env, HOME: resolve11(dest, "home") } : void 0;
4144
3070
  await gitExec(["init"], { cwd: dest, mindName: name, env });
4145
3071
  await configureGitIdentity(name, { cwd: dest, mindName: name, env });
4146
3072
  await gitExec(["add", "-A"], { cwd: dest, mindName: name, env });
4147
3073
  await gitExec(["commit", "-m", "import from archive"], { cwd: dest, mindName: name, env });
4148
3074
  } catch (err) {
4149
3075
  logger_default.error(`git setup failed for imported mind ${name}`, logger_default.errorData(err));
4150
- rmSync2(resolve16(dest, ".git"), { recursive: true, force: true });
3076
+ rmSync3(resolve11(dest, ".git"), { recursive: true, force: true });
4151
3077
  }
4152
3078
  }
4153
3079
  chownMindDir(dest, name);
4154
- rmSync2(tempDir, { recursive: true, force: true });
3080
+ rmSync3(tempDir, { recursive: true, force: true });
4155
3081
  return c.json({ ok: true, name, port, message: `Imported mind: ${name} (port ${port})` });
4156
3082
  } catch (err) {
4157
- if (existsSync12(dest)) rmSync2(dest, { recursive: true, force: true });
3083
+ if (existsSync9(dest)) rmSync3(dest, { recursive: true, force: true });
4158
3084
  try {
4159
3085
  removeMind(name);
4160
3086
  } catch (cleanupErr) {
4161
3087
  logger_default.error(`Failed to clean up registry for ${name}`, logger_default.errorData(cleanupErr));
4162
3088
  }
4163
- rmSync2(tempDir, { recursive: true, force: true });
3089
+ rmSync3(tempDir, { recursive: true, force: true });
4164
3090
  return c.json({ error: err instanceof Error ? err.message : "Failed to import mind" }, 500);
4165
3091
  }
4166
3092
  }
@@ -4171,7 +3097,7 @@ async function importFromHomeOnlyArchive(c, tempDir, extractedMindDir, nameOverr
4171
3097
  if (findMind(name)) return c.json({ error: `Mind already exists: ${name}` }, 409);
4172
3098
  ensureVoluteHome();
4173
3099
  const dest = mindDir(name);
4174
- if (existsSync12(dest)) return c.json({ error: "Mind directory already exists" }, 409);
3100
+ if (existsSync9(dest)) return c.json({ error: "Mind directory already exists" }, 409);
4175
3101
  const templatesRoot = findTemplatesRoot();
4176
3102
  const { composedDir, manifest: templateManifest } = composeTemplate(
4177
3103
  templatesRoot,
@@ -4180,40 +3106,40 @@ async function importFromHomeOnlyArchive(c, tempDir, extractedMindDir, nameOverr
4180
3106
  try {
4181
3107
  copyTemplateToDir(composedDir, dest, name, templateManifest);
4182
3108
  applyInitFiles(dest);
4183
- const extractedHome = resolve16(extractedMindDir, "home");
4184
- if (existsSync12(extractedHome)) {
4185
- cpSync(extractedHome, resolve16(dest, "home"), { recursive: true });
3109
+ const extractedHome = resolve11(extractedMindDir, "home");
3110
+ if (existsSync9(extractedHome)) {
3111
+ cpSync(extractedHome, resolve11(dest, "home"), { recursive: true });
4186
3112
  }
4187
- const extractedMindInternal = resolve16(extractedMindDir, ".mind");
4188
- if (existsSync12(extractedMindInternal)) {
4189
- cpSync(extractedMindInternal, resolve16(dest, ".mind"), { recursive: true });
3113
+ const extractedMindInternal = resolve11(extractedMindDir, ".mind");
3114
+ if (existsSync9(extractedMindInternal)) {
3115
+ cpSync(extractedMindInternal, resolve11(dest, ".mind"), { recursive: true });
4190
3116
  }
4191
- const identityDir = resolve16(dest, ".mind/identity");
3117
+ const identityDir = resolve11(dest, ".mind/identity");
4192
3118
  let publicKeyPem;
4193
- if (!manifest.includes.identity || !existsSync12(resolve16(identityDir, "private.pem"))) {
3119
+ if (!manifest.includes.identity || !existsSync9(resolve11(identityDir, "private.pem"))) {
4194
3120
  ({ publicKeyPem } = generateIdentity(dest));
4195
3121
  } else {
4196
- publicKeyPem = readFileSync11(resolve16(identityDir, "public.pem"), "utf-8");
3122
+ publicKeyPem = readFileSync8(resolve11(identityDir, "public.pem"), "utf-8");
4197
3123
  }
4198
- const promptsPath = resolve16(dest, "home/.config/prompts.json");
4199
- if (!existsSync12(promptsPath)) {
3124
+ const promptsPath = resolve11(dest, "home/.config/prompts.json");
3125
+ if (!existsSync9(promptsPath)) {
4200
3126
  const mindPrompts = await getMindPromptDefaults();
4201
- writeFileSync8(promptsPath, `${JSON.stringify(mindPrompts, null, 2)}
3127
+ writeFileSync6(promptsPath, `${JSON.stringify(mindPrompts, null, 2)}
4202
3128
  `);
4203
3129
  }
4204
3130
  const state = stateDir(name);
4205
- mkdirSync7(state, { recursive: true });
4206
- const channelsJson = resolve16(tempDir, "state/channels.json");
4207
- if (existsSync12(channelsJson)) {
4208
- cpSync(channelsJson, resolve16(state, "channels.json"));
3131
+ mkdirSync5(state, { recursive: true });
3132
+ const channelsJson = resolve11(tempDir, "state/channels.json");
3133
+ if (existsSync9(channelsJson)) {
3134
+ cpSync(channelsJson, resolve11(state, "channels.json"));
4209
3135
  }
4210
- const envJson = resolve16(tempDir, "state/env.json");
4211
- if (existsSync12(envJson)) {
4212
- cpSync(envJson, resolve16(state, "env.json"));
3136
+ const envJson = resolve11(tempDir, "state/env.json");
3137
+ if (existsSync9(envJson)) {
3138
+ cpSync(envJson, resolve11(state, "env.json"));
4213
3139
  }
4214
3140
  const port = nextPort();
4215
3141
  addMind(name, port, manifest.stage, manifest.template);
4216
- const homeDir = resolve16(dest, "home");
3142
+ const homeDir = resolve11(dest, "home");
4217
3143
  ensureVoluteGroup();
4218
3144
  createMindUser(name, homeDir);
4219
3145
  chownMindDir(dest, name);
@@ -4226,7 +3152,7 @@ async function importFromHomeOnlyArchive(c, tempDir, extractedMindDir, nameOverr
4226
3152
  await initTemplateBranch(dest, composedDir, templateManifest, name, env);
4227
3153
  } catch (err) {
4228
3154
  logger_default.error(`git setup failed for imported mind ${name}`, logger_default.errorData(err));
4229
- rmSync2(resolve16(dest, ".git"), { recursive: true, force: true });
3155
+ rmSync3(resolve11(dest, ".git"), { recursive: true, force: true });
4230
3156
  gitWarning = "Git setup failed \u2014 variants and upgrades won't be available until git is initialized.";
4231
3157
  }
4232
3158
  try {
@@ -4250,7 +3176,7 @@ async function importFromHomeOnlyArchive(c, tempDir, extractedMindDir, nameOverr
4250
3176
  publishPublicKey(name, publicKeyPem).catch(
4251
3177
  (err) => logger_default.warn(`failed to publish key for ${name}`, { error: err.message })
4252
3178
  );
4253
- rmSync2(tempDir, { recursive: true, force: true });
3179
+ rmSync3(tempDir, { recursive: true, force: true });
4254
3180
  return c.json({
4255
3181
  ok: true,
4256
3182
  name,
@@ -4261,24 +3187,24 @@ async function importFromHomeOnlyArchive(c, tempDir, extractedMindDir, nameOverr
4261
3187
  ...skillWarnings.length > 0 && { skillWarnings }
4262
3188
  });
4263
3189
  } catch (err) {
4264
- if (existsSync12(dest)) rmSync2(dest, { recursive: true, force: true });
3190
+ if (existsSync9(dest)) rmSync3(dest, { recursive: true, force: true });
4265
3191
  try {
4266
3192
  removeMind(name);
4267
3193
  } catch (cleanupErr) {
4268
3194
  logger_default.error(`Failed to clean up registry for ${name}`, logger_default.errorData(cleanupErr));
4269
3195
  }
4270
- rmSync2(tempDir, { recursive: true, force: true });
3196
+ rmSync3(tempDir, { recursive: true, force: true });
4271
3197
  return c.json({ error: err instanceof Error ? err.message : "Failed to import mind" }, 500);
4272
3198
  } finally {
4273
- rmSync2(composedDir, { recursive: true, force: true });
3199
+ rmSync3(composedDir, { recursive: true, force: true });
4274
3200
  }
4275
3201
  }
4276
3202
  async function importHistoryFromArchive(name, tempDir) {
4277
- const historyJsonl = resolve16(tempDir, "history.jsonl");
4278
- if (!existsSync12(historyJsonl)) return;
3203
+ const historyJsonl = resolve11(tempDir, "history.jsonl");
3204
+ if (!existsSync9(historyJsonl)) return;
4279
3205
  try {
4280
3206
  const db = await getDb();
4281
- const lines = readFileSync11(historyJsonl, "utf-8").trim().split("\n");
3207
+ const lines = readFileSync8(historyJsonl, "utf-8").trim().split("\n");
4282
3208
  let imported = 0;
4283
3209
  let failed = 0;
4284
3210
  for (const line of lines) {
@@ -4314,13 +3240,13 @@ async function importHistoryFromArchive(name, tempDir) {
4314
3240
  }
4315
3241
  }
4316
3242
  function importSessionsFromArchive(dest, tempDir) {
4317
- const sessionsDir = resolve16(tempDir, "sessions");
4318
- if (!existsSync12(sessionsDir)) return;
3243
+ const sessionsDir = resolve11(tempDir, "sessions");
3244
+ if (!existsSync9(sessionsDir)) return;
4319
3245
  try {
4320
- const destSessions = resolve16(dest, ".mind/sessions");
4321
- mkdirSync7(destSessions, { recursive: true });
4322
- for (const file of readdirSync5(sessionsDir)) {
4323
- cpSync(resolve16(sessionsDir, file), resolve16(destSessions, file));
3246
+ const destSessions = resolve11(dest, ".mind/sessions");
3247
+ mkdirSync5(destSessions, { recursive: true });
3248
+ for (const file of readdirSync4(sessionsDir)) {
3249
+ cpSync(resolve11(sessionsDir, file), resolve11(destSessions, file));
4324
3250
  }
4325
3251
  } catch (err) {
4326
3252
  logger_default.error("Failed to import sessions from archive", logger_default.errorData(err));
@@ -4343,7 +3269,7 @@ var app11 = new Hono11().post("/", requireAdmin, zValidator3("json", createMindS
4343
3269
  if (findMind(name)) return c.json({ error: `Mind already exists: ${name}` }, 409);
4344
3270
  ensureVoluteHome();
4345
3271
  const dest = mindDir(name);
4346
- if (existsSync12(dest)) return c.json({ error: "Mind directory already exists" }, 409);
3272
+ if (existsSync9(dest)) return c.json({ error: "Mind directory already exists" }, 409);
4347
3273
  const templatesRoot = findTemplatesRoot();
4348
3274
  const { composedDir, manifest } = composeTemplate(templatesRoot, template);
4349
3275
  try {
@@ -4357,15 +3283,15 @@ var app11 = new Hono11().post("/", requireAdmin, zValidator3("json", createMindS
4357
3283
  writeVoluteConfig(dest, seedConfig);
4358
3284
  }
4359
3285
  if (body.model) {
4360
- const configPath2 = resolve16(dest, "home/.config/config.json");
4361
- const existing = existsSync12(configPath2) ? JSON.parse(readFileSync11(configPath2, "utf-8")) : {};
3286
+ const configPath2 = resolve11(dest, "home/.config/config.json");
3287
+ const existing = existsSync9(configPath2) ? JSON.parse(readFileSync8(configPath2, "utf-8")) : {};
4362
3288
  existing.model = body.model;
4363
- writeFileSync8(configPath2, `${JSON.stringify(existing, null, 2)}
3289
+ writeFileSync6(configPath2, `${JSON.stringify(existing, null, 2)}
4364
3290
  `);
4365
3291
  }
4366
3292
  const mindPrompts = await getMindPromptDefaults();
4367
- writeFileSync8(
4368
- resolve16(dest, "home/.config/prompts.json"),
3293
+ writeFileSync6(
3294
+ resolve11(dest, "home/.config/prompts.json"),
4369
3295
  `${JSON.stringify(mindPrompts, null, 2)}
4370
3296
  `
4371
3297
  );
@@ -4376,7 +3302,7 @@ var app11 = new Hono11().post("/", requireAdmin, zValidator3("json", createMindS
4376
3302
  } catch (err) {
4377
3303
  logger_default.warn(`failed to set template hash for ${name}`, logger_default.errorData(err));
4378
3304
  }
4379
- const homeDir = resolve16(dest, "home");
3305
+ const homeDir = resolve11(dest, "home");
4380
3306
  ensureVoluteGroup();
4381
3307
  createMindUser(name, homeDir);
4382
3308
  chownMindDir(dest, name);
@@ -4389,7 +3315,7 @@ var app11 = new Hono11().post("/", requireAdmin, zValidator3("json", createMindS
4389
3315
  await initTemplateBranch(dest, composedDir, manifest, name, env);
4390
3316
  } catch (err) {
4391
3317
  logger_default.error(`git setup failed for ${name}`, logger_default.errorData(err));
4392
- rmSync2(resolve16(dest, ".git"), { recursive: true, force: true });
3318
+ rmSync3(resolve11(dest, ".git"), { recursive: true, force: true });
4393
3319
  gitWarning = "Git setup failed \u2014 variants and upgrades won't be available until git is initialized.";
4394
3320
  }
4395
3321
  try {
@@ -4404,7 +3330,7 @@ The human who planted you described you as: "${body.description}"
4404
3330
  ` : "";
4405
3331
  const seedSoulRaw = body.seedSoul ?? await getPrompt("seed_soul", { name, description: descLine });
4406
3332
  const seedSoul = body.seedSoul ? substitute(seedSoulRaw, { name, description: descLine }) : seedSoulRaw;
4407
- writeFileSync8(resolve16(dest, "home/SOUL.md"), seedSoul);
3333
+ writeFileSync6(resolve11(dest, "home/SOUL.md"), seedSoul);
4408
3334
  }
4409
3335
  const skillSet = body.skills ?? (body.stage === "seed" ? SEED_SKILLS : STANDARD_SKILLS);
4410
3336
  const skillWarnings = [];
@@ -4419,16 +3345,27 @@ The human who planted you described you as: "${body.description}"
4419
3345
  if (body.stage !== "seed") {
4420
3346
  const customSoul = await getPromptIfCustom("default_soul");
4421
3347
  if (customSoul) {
4422
- writeFileSync8(resolve16(dest, "home/SOUL.md"), customSoul.replace(/\{\{name\}\}/g, name));
3348
+ writeFileSync6(resolve11(dest, "home/SOUL.md"), customSoul.replace(/\{\{name\}\}/g, name));
4423
3349
  }
4424
3350
  const customMemory = await getPromptIfCustom("default_memory");
4425
3351
  if (customMemory) {
4426
- writeFileSync8(resolve16(dest, "home/MEMORY.md"), customMemory);
3352
+ writeFileSync6(resolve11(dest, "home/MEMORY.md"), customMemory);
4427
3353
  }
4428
3354
  }
4429
3355
  publishPublicKey(name, publicKeyPem).catch(
4430
3356
  (err) => logger_default.warn(`failed to publish key for ${name}`, { error: err.message })
4431
3357
  );
3358
+ fireWebhook({
3359
+ event: "mind_created",
3360
+ mind: name,
3361
+ data: {
3362
+ name,
3363
+ port,
3364
+ stage: body.stage ?? "sprouted",
3365
+ template,
3366
+ description: body.description
3367
+ }
3368
+ });
4432
3369
  return c.json({
4433
3370
  ok: true,
4434
3371
  name,
@@ -4439,14 +3376,14 @@ The human who planted you described you as: "${body.description}"
4439
3376
  ...skillWarnings.length > 0 && { skillWarnings }
4440
3377
  });
4441
3378
  } catch (err) {
4442
- if (existsSync12(dest)) rmSync2(dest, { recursive: true, force: true });
3379
+ if (existsSync9(dest)) rmSync3(dest, { recursive: true, force: true });
4443
3380
  try {
4444
3381
  removeMind(name);
4445
3382
  } catch {
4446
3383
  }
4447
3384
  return c.json({ error: err instanceof Error ? err.message : "Failed to create mind" }, 500);
4448
3385
  } finally {
4449
- rmSync2(composedDir, { recursive: true, force: true });
3386
+ rmSync3(composedDir, { recursive: true, force: true });
4450
3387
  }
4451
3388
  }).post("/import", requireAdmin, async (c) => {
4452
3389
  let body;
@@ -4459,13 +3396,13 @@ The human who planted you described you as: "${body.description}"
4459
3396
  return importFromArchive(c, body.archivePath, body.name, body.manifest);
4460
3397
  }
4461
3398
  const wsDir = body.workspacePath;
4462
- if (!wsDir || !existsSync12(resolve16(wsDir, "SOUL.md")) || !existsSync12(resolve16(wsDir, "IDENTITY.md"))) {
3399
+ if (!wsDir || !existsSync9(resolve11(wsDir, "SOUL.md")) || !existsSync9(resolve11(wsDir, "IDENTITY.md"))) {
4463
3400
  return c.json({ error: "Invalid workspace: missing SOUL.md or IDENTITY.md" }, 400);
4464
3401
  }
4465
- const soul = readFileSync11(resolve16(wsDir, "SOUL.md"), "utf-8");
4466
- const identity = readFileSync11(resolve16(wsDir, "IDENTITY.md"), "utf-8");
4467
- const userPath = resolve16(wsDir, "USER.md");
4468
- const user = existsSync12(userPath) ? readFileSync11(userPath, "utf-8") : "";
3402
+ const soul = readFileSync8(resolve11(wsDir, "SOUL.md"), "utf-8");
3403
+ const identity = readFileSync8(resolve11(wsDir, "IDENTITY.md"), "utf-8");
3404
+ const userPath = resolve11(wsDir, "USER.md");
3405
+ const user = existsSync9(userPath) ? readFileSync8(userPath, "utf-8") : "";
4469
3406
  const name = body.name ?? parseNameFromIdentity(identity) ?? "imported-mind";
4470
3407
  const template = body.template ?? "claude";
4471
3408
  const nameErr = validateMindName(name);
@@ -4485,33 +3422,33 @@ ${user.trimEnd()}
4485
3422
  ` : "";
4486
3423
  ensureVoluteHome();
4487
3424
  const dest = mindDir(name);
4488
- if (existsSync12(dest)) return c.json({ error: "Mind directory already exists" }, 409);
3425
+ if (existsSync9(dest)) return c.json({ error: "Mind directory already exists" }, 409);
4489
3426
  const templatesRoot = findTemplatesRoot();
4490
3427
  const { composedDir, manifest } = composeTemplate(templatesRoot, template);
4491
3428
  try {
4492
3429
  copyTemplateToDir(composedDir, dest, name, manifest);
4493
3430
  applyInitFiles(dest);
4494
3431
  const { publicKeyPem: importPublicKey } = generateIdentity(dest);
4495
- writeFileSync8(resolve16(dest, "home/SOUL.md"), mergedSoul);
4496
- const wsMemoryPath = resolve16(wsDir, "MEMORY.md");
4497
- const hasMemory = existsSync12(wsMemoryPath);
3432
+ writeFileSync6(resolve11(dest, "home/SOUL.md"), mergedSoul);
3433
+ const wsMemoryPath = resolve11(wsDir, "MEMORY.md");
3434
+ const hasMemory = existsSync9(wsMemoryPath);
4498
3435
  if (hasMemory) {
4499
- const existingMemory = readFileSync11(wsMemoryPath, "utf-8");
4500
- writeFileSync8(
4501
- resolve16(dest, "home/MEMORY.md"),
3436
+ const existingMemory = readFileSync8(wsMemoryPath, "utf-8");
3437
+ writeFileSync6(
3438
+ resolve11(dest, "home/MEMORY.md"),
4502
3439
  `${existingMemory.trimEnd()}${mergedMemoryExtra}`
4503
3440
  );
4504
3441
  } else if (user) {
4505
- writeFileSync8(resolve16(dest, "home/MEMORY.md"), `${user.trimEnd()}
3442
+ writeFileSync6(resolve11(dest, "home/MEMORY.md"), `${user.trimEnd()}
4506
3443
  `);
4507
3444
  }
4508
- const wsMemoryDir = resolve16(wsDir, "memory");
3445
+ const wsMemoryDir = resolve11(wsDir, "memory");
4509
3446
  let dailyLogCount = 0;
4510
- if (existsSync12(wsMemoryDir)) {
4511
- const destMemoryDir = resolve16(dest, "home/memory");
4512
- const files = readdirSync5(wsMemoryDir).filter((f) => f.endsWith(".md"));
3447
+ if (existsSync9(wsMemoryDir)) {
3448
+ const destMemoryDir = resolve11(dest, "home/memory");
3449
+ const files = readdirSync4(wsMemoryDir).filter((f) => f.endsWith(".md"));
4513
3450
  for (const file of files) {
4514
- cpSync(resolve16(wsMemoryDir, file), resolve16(destMemoryDir, file));
3451
+ cpSync(resolve11(wsMemoryDir, file), resolve11(destMemoryDir, file));
4515
3452
  }
4516
3453
  dailyLogCount = files.length;
4517
3454
  }
@@ -4522,7 +3459,7 @@ ${user.trimEnd()}
4522
3459
  } catch (err) {
4523
3460
  logger_default.warn(`failed to set template hash for ${name}`, logger_default.errorData(err));
4524
3461
  }
4525
- const homeDir = resolve16(dest, "home");
3462
+ const homeDir = resolve11(dest, "home");
4526
3463
  ensureVoluteGroup();
4527
3464
  createMindUser(name, homeDir);
4528
3465
  chownMindDir(dest, name);
@@ -4530,20 +3467,20 @@ ${user.trimEnd()}
4530
3467
  if (!hasMemory && dailyLogCount > 0) {
4531
3468
  await consolidateMemory(dest);
4532
3469
  }
4533
- const env = isIsolationEnabled() ? { ...process.env, HOME: resolve16(dest, "home") } : void 0;
3470
+ const env = isIsolationEnabled() ? { ...process.env, HOME: resolve11(dest, "home") } : void 0;
4534
3471
  await gitExec(["init"], { cwd: dest, mindName: name, env });
4535
3472
  await configureGitIdentity(name, { cwd: dest, mindName: name, env });
4536
3473
  await gitExec(["add", "-A"], { cwd: dest, mindName: name, env });
4537
3474
  await gitExec(["commit", "-m", "import from OpenClaw"], { cwd: dest, mindName: name, env });
4538
- const sessionFile = body.sessionPath ? resolve16(body.sessionPath) : findOpenClawSession(wsDir);
4539
- if (sessionFile && existsSync12(sessionFile)) {
3475
+ const sessionFile = body.sessionPath ? resolve11(body.sessionPath) : findOpenClawSession(wsDir);
3476
+ if (sessionFile && existsSync9(sessionFile)) {
4540
3477
  if (template === "pi") {
4541
3478
  importPiSession(sessionFile, dest);
4542
3479
  } else if (template === "claude") {
4543
3480
  const sessionId = convertSession({ sessionPath: sessionFile, projectDir: dest });
4544
- const mindRuntimeDir = resolve16(dest, ".mind");
4545
- mkdirSync7(mindRuntimeDir, { recursive: true });
4546
- writeFileSync8(resolve16(mindRuntimeDir, "session.json"), JSON.stringify({ sessionId }));
3481
+ const mindRuntimeDir = resolve11(dest, ".mind");
3482
+ mkdirSync5(mindRuntimeDir, { recursive: true });
3483
+ writeFileSync6(resolve11(mindRuntimeDir, "session.json"), JSON.stringify({ sessionId }));
4547
3484
  }
4548
3485
  }
4549
3486
  importOpenClawConnectors(name, dest);
@@ -4558,14 +3495,14 @@ ${user.trimEnd()}
4558
3495
  );
4559
3496
  return c.json({ ok: true, name, port, message: `Imported mind: ${name} (port ${port})` });
4560
3497
  } catch (err) {
4561
- if (existsSync12(dest)) rmSync2(dest, { recursive: true, force: true });
3498
+ if (existsSync9(dest)) rmSync3(dest, { recursive: true, force: true });
4562
3499
  try {
4563
3500
  removeMind(name);
4564
3501
  } catch {
4565
3502
  }
4566
3503
  return c.json({ error: err instanceof Error ? err.message : "Failed to import mind" }, 500);
4567
3504
  } finally {
4568
- rmSync2(composedDir, { recursive: true, force: true });
3505
+ rmSync3(composedDir, { recursive: true, force: true });
4569
3506
  }
4570
3507
  }).get("/", async (c) => {
4571
3508
  const entries = readRegistry();
@@ -4582,7 +3519,7 @@ ${user.trimEnd()}
4582
3519
  const minds = await Promise.all(
4583
3520
  entries.map(async (entry) => {
4584
3521
  const mindStatus = await getMindStatus(entry.name, entry.port);
4585
- const hasPages = existsSync12(resolve16(mindDir(entry.name), "home", "pages"));
3522
+ const hasPages = existsSync9(resolve11(mindDir(entry.name), "home", "pages"));
4586
3523
  return {
4587
3524
  ...entry,
4588
3525
  ...mindStatus,
@@ -4600,7 +3537,7 @@ ${user.trimEnd()}
4600
3537
  const name = c.req.param("name");
4601
3538
  const entry = findMind(name);
4602
3539
  if (!entry) return c.json({ error: "Mind not found" }, 404);
4603
- if (!existsSync12(mindDir(name))) return c.json({ error: "Mind directory missing" }, 404);
3540
+ if (!existsSync9(mindDir(name))) return c.json({ error: "Mind directory missing" }, 404);
4604
3541
  const mindStatus = await getMindStatus(name, entry.port);
4605
3542
  const variants = readVariants(name);
4606
3543
  const manager = getMindManager();
@@ -4615,7 +3552,7 @@ ${user.trimEnd()}
4615
3552
  return { name: v.name, port: v.port, status: variantStatus };
4616
3553
  })
4617
3554
  );
4618
- const hasPages = existsSync12(resolve16(mindDir(name), "home", "pages"));
3555
+ const hasPages = existsSync9(resolve11(mindDir(name), "home", "pages"));
4619
3556
  return c.json({ ...entry, ...mindStatus, variants: variantStatuses, hasPages });
4620
3557
  }).post("/:name/start", requireAdmin, async (c) => {
4621
3558
  const name = c.req.param("name");
@@ -4629,7 +3566,7 @@ ${user.trimEnd()}
4629
3566
  targetPort = variant.port;
4630
3567
  } else {
4631
3568
  const dir = mindDir(baseName);
4632
- if (!existsSync12(dir)) return c.json({ error: "Mind directory missing" }, 404);
3569
+ if (!existsSync9(dir)) return c.json({ error: "Mind directory missing" }, 404);
4633
3570
  }
4634
3571
  if (getMindManager().isRunning(name)) {
4635
3572
  return c.json({ error: "Mind already running" }, 409);
@@ -4652,7 +3589,7 @@ ${user.trimEnd()}
4652
3589
  targetPort = variant.port;
4653
3590
  } else {
4654
3591
  const dir = mindDir(baseName);
4655
- if (!existsSync12(dir)) return c.json({ error: "Mind directory missing" }, 404);
3592
+ if (!existsSync9(dir)) return c.json({ error: "Mind directory missing" }, 404);
4656
3593
  }
4657
3594
  let context;
4658
3595
  const contentType = c.req.header("content-type");
@@ -4679,7 +3616,7 @@ ${user.trimEnd()}
4679
3616
  const variant = findVariant(baseName, mergeVariantName);
4680
3617
  if (variant) {
4681
3618
  const projectRoot = mindDir(baseName);
4682
- if (existsSync12(variant.path)) {
3619
+ if (existsSync9(variant.path)) {
4683
3620
  const status = (await gitExec(["status", "--porcelain"], { cwd: variant.path })).trim();
4684
3621
  if (status) {
4685
3622
  try {
@@ -4703,24 +3640,11 @@ ${user.trimEnd()}
4703
3640
  cwd: projectRoot
4704
3641
  });
4705
3642
  } catch (e) {
4706
- logger_default.error(`failed to auto-commit main worktree for ${baseName}`, logger_default.errorData(e));
4707
- }
4708
- }
4709
- await gitExec(["merge", variant.branch], { cwd: projectRoot });
4710
- if (existsSync12(variant.path)) {
4711
- try {
4712
- await gitExec(["worktree", "remove", "--force", variant.path], {
4713
- cwd: projectRoot
4714
- });
4715
- } catch {
4716
- }
4717
- }
4718
- try {
4719
- await gitExec(["branch", "-D", variant.branch], { cwd: projectRoot });
4720
- } catch {
3643
+ logger_default.error(`failed to auto-commit main worktree for ${baseName}`, logger_default.errorData(e));
3644
+ }
4721
3645
  }
4722
- removeVariant(baseName, mergeVariantName);
4723
- chownMindDir(projectRoot, baseName);
3646
+ await gitExec(["merge", variant.branch], { cwd: projectRoot });
3647
+ await cleanupVariant(baseName, mergeVariantName, projectRoot, variant.path);
4724
3648
  try {
4725
3649
  await npmInstallAsMind(projectRoot, baseName);
4726
3650
  } catch (e) {
@@ -4768,6 +3692,53 @@ ${user.trimEnd()}
4768
3692
  } catch (err) {
4769
3693
  return c.json({ error: err instanceof Error ? err.message : "Failed to stop mind" }, 500);
4770
3694
  }
3695
+ }).get("/:name/sleep", requireAdmin, async (c) => {
3696
+ const name = c.req.param("name");
3697
+ const entry = findMind(name);
3698
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
3699
+ const { getSleepManagerIfReady } = await import("./sleep-manager-3RWUX2ZR.js");
3700
+ const sm = getSleepManagerIfReady();
3701
+ if (!sm) return c.json({ error: "Sleep manager not initialized" }, 503);
3702
+ return c.json(sm.getState(name));
3703
+ }).post("/:name/sleep", requireAdmin, async (c) => {
3704
+ const name = c.req.param("name");
3705
+ const entry = findMind(name);
3706
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
3707
+ const { getSleepManagerIfReady } = await import("./sleep-manager-3RWUX2ZR.js");
3708
+ const sm = getSleepManagerIfReady();
3709
+ if (!sm) return c.json({ error: "Sleep manager not initialized" }, 503);
3710
+ if (sm.isSleeping(name)) return c.json({ error: "Mind is already sleeping" }, 409);
3711
+ const body = await c.req.json().catch(() => ({}));
3712
+ const wakeAt = body.wakeAt;
3713
+ if (wakeAt) {
3714
+ const wakeDate = new Date(wakeAt);
3715
+ if (Number.isNaN(wakeDate.getTime()) || wakeDate <= /* @__PURE__ */ new Date()) {
3716
+ return c.json({ error: "wakeAt must be a valid future ISO date" }, 400);
3717
+ }
3718
+ }
3719
+ sm.initiateSleep(name, wakeAt ? { voluntaryWakeAt: wakeAt } : void 0).catch(
3720
+ (err) => logger_default.error(`failed to initiate sleep for ${name}`, logger_default.errorData(err))
3721
+ );
3722
+ return c.json({ ok: true });
3723
+ }).post("/:name/wake", requireAdmin, async (c) => {
3724
+ const name = c.req.param("name");
3725
+ const entry = findMind(name);
3726
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
3727
+ const { getSleepManagerIfReady } = await import("./sleep-manager-3RWUX2ZR.js");
3728
+ const sm = getSleepManagerIfReady();
3729
+ if (!sm) return c.json({ error: "Sleep manager not initialized" }, 503);
3730
+ if (!sm.isSleeping(name)) return c.json({ error: "Mind is not sleeping" }, 409);
3731
+ sm.initiateWake(name).catch((err) => logger_default.error(`failed to wake ${name}`, logger_default.errorData(err)));
3732
+ return c.json({ ok: true });
3733
+ }).post("/:name/sleep/messages", requireAdmin, async (c) => {
3734
+ const name = c.req.param("name");
3735
+ const entry = findMind(name);
3736
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
3737
+ const { getSleepManagerIfReady } = await import("./sleep-manager-3RWUX2ZR.js");
3738
+ const sm = getSleepManagerIfReady();
3739
+ if (!sm) return c.json({ error: "Sleep manager not initialized" }, 503);
3740
+ const flushed = await sm.flushQueuedMessages(name);
3741
+ return c.json({ ok: true, flushed });
4771
3742
  }).post("/:name/sprout", requireAdmin, async (c) => {
4772
3743
  const name = c.req.param("name");
4773
3744
  const entry = findMind(name);
@@ -4796,20 +3767,25 @@ ${user.trimEnd()}
4796
3767
  removeMind(name);
4797
3768
  await deleteMindUser2(name);
4798
3769
  const state = stateDir(name);
4799
- if (existsSync12(state)) {
4800
- rmSync2(state, { recursive: true, force: true });
3770
+ if (existsSync9(state)) {
3771
+ rmSync3(state, { recursive: true, force: true });
4801
3772
  }
4802
- if (force && existsSync12(dir)) {
4803
- rmSync2(dir, { recursive: true, force: true });
3773
+ if (force && existsSync9(dir)) {
3774
+ rmSync3(dir, { recursive: true, force: true });
4804
3775
  deleteMindUser(name);
4805
3776
  }
3777
+ fireWebhook({
3778
+ event: "mind_deleted",
3779
+ mind: name,
3780
+ data: { port: entry.port, stage: entry.stage, template: entry.template }
3781
+ });
4806
3782
  return c.json({ ok: true });
4807
3783
  }).post("/:name/upgrade", requireAdmin, async (c) => {
4808
3784
  const mindName = c.req.param("name");
4809
3785
  const entry = findMind(mindName);
4810
3786
  if (!entry) return c.json({ error: "Mind not found" }, 404);
4811
3787
  const dir = mindDir(mindName);
4812
- if (!existsSync12(dir)) return c.json({ error: "Mind directory missing" }, 404);
3788
+ if (!existsSync9(dir)) return c.json({ error: "Mind directory missing" }, 404);
4813
3789
  let body = {};
4814
3790
  try {
4815
3791
  body = await c.req.json();
@@ -4817,9 +3793,32 @@ ${user.trimEnd()}
4817
3793
  }
4818
3794
  const template = body.template ?? entry.template ?? "claude";
4819
3795
  const UPGRADE_VARIANT = "upgrade";
3796
+ if (body.abort) {
3797
+ const worktreeDir2 = resolve11(dir, ".variants", UPGRADE_VARIANT);
3798
+ if (!existsSync9(worktreeDir2)) {
3799
+ return c.json({ error: "No upgrade in progress" }, 400);
3800
+ }
3801
+ try {
3802
+ try {
3803
+ const gitDirContent = readFileSync8(resolve11(worktreeDir2, ".git"), "utf-8").trim();
3804
+ const gitDir = gitDirContent.replace("gitdir: ", "");
3805
+ if (existsSync9(resolve11(gitDir, "MERGE_HEAD"))) {
3806
+ await gitExec(["merge", "--abort"], { cwd: worktreeDir2 });
3807
+ }
3808
+ } catch {
3809
+ }
3810
+ await cleanupVariant(mindName, UPGRADE_VARIANT, dir, worktreeDir2, { stop: true });
3811
+ return c.json({ ok: true });
3812
+ } catch (err) {
3813
+ return c.json(
3814
+ { error: err instanceof Error ? err.message : "Failed to abort upgrade" },
3815
+ 500
3816
+ );
3817
+ }
3818
+ }
4820
3819
  if (body.continue) {
4821
- const worktreeDir2 = resolve16(dir, ".variants", UPGRADE_VARIANT);
4822
- if (!existsSync12(worktreeDir2)) {
3820
+ const worktreeDir2 = resolve11(dir, ".variants", UPGRADE_VARIANT);
3821
+ if (!existsSync9(worktreeDir2)) {
4823
3822
  return c.json({ error: "No upgrade in progress" }, 400);
4824
3823
  }
4825
3824
  const status = await gitExec(["status", "--porcelain"], { cwd: worktreeDir2 });
@@ -4832,7 +3831,10 @@ ${user.trimEnd()}
4832
3831
  await gitExec(["commit", "-m", "merge template update"], { cwd: worktreeDir2 });
4833
3832
  } catch (e) {
4834
3833
  const msg = e instanceof Error ? e.message : String(e);
4835
- if (!msg.includes("nothing to commit")) throw e;
3834
+ const stderr = e?.stderr ?? "";
3835
+ const stdout = e?.stdout ?? "";
3836
+ if (!msg.includes("nothing to commit") && !stderr.includes("nothing to commit") && !stdout.includes("nothing to commit"))
3837
+ throw e;
4836
3838
  }
4837
3839
  chownMindDir(dir, mindName);
4838
3840
  try {
@@ -4853,49 +3855,30 @@ ${user.trimEnd()}
4853
3855
  port: variantPort
4854
3856
  });
4855
3857
  } catch (err) {
4856
- try {
4857
- removeVariant(mindName, UPGRADE_VARIANT);
4858
- } catch {
4859
- }
4860
- try {
4861
- await gitExec(["worktree", "remove", "--force", worktreeDir2], { cwd: dir });
4862
- } catch {
4863
- }
4864
- try {
4865
- await gitExec(["branch", "-D", UPGRADE_VARIANT], { cwd: dir });
4866
- } catch {
4867
- }
4868
- try {
4869
- chownMindDir(dir, mindName);
4870
- } catch (chownErr) {
4871
- logger_default.error(
4872
- `failed to fix ownership during upgrade cleanup for ${mindName}`,
4873
- logger_default.errorData(chownErr)
4874
- );
4875
- }
3858
+ await cleanupVariant(mindName, UPGRADE_VARIANT, dir, worktreeDir2);
4876
3859
  return c.json(
4877
3860
  { error: err instanceof Error ? err.message : "Failed to continue upgrade" },
4878
3861
  500
4879
3862
  );
4880
3863
  }
4881
3864
  }
4882
- const worktreeDir = resolve16(dir, ".variants", UPGRADE_VARIANT);
4883
- if (existsSync12(worktreeDir)) {
3865
+ const worktreeDir = resolve11(dir, ".variants", UPGRADE_VARIANT);
3866
+ if (existsSync9(worktreeDir)) {
4884
3867
  return c.json(
4885
3868
  { error: "Upgrade variant already exists. Use continue or delete it first." },
4886
3869
  409
4887
3870
  );
4888
3871
  }
4889
- if (!existsSync12(resolve16(dir, ".git"))) {
3872
+ if (!existsSync9(resolve11(dir, ".git"))) {
4890
3873
  try {
4891
- const env = isIsolationEnabled() ? { ...process.env, HOME: resolve16(dir, "home") } : void 0;
3874
+ const env = isIsolationEnabled() ? { ...process.env, HOME: resolve11(dir, "home") } : void 0;
4892
3875
  await gitExec(["init"], { cwd: dir, mindName, env });
4893
3876
  await configureGitIdentity(mindName, { cwd: dir, mindName, env });
4894
3877
  await gitExec(["add", "-A"], { cwd: dir, mindName, env });
4895
3878
  await gitExec(["commit", "-m", "initial commit"], { cwd: dir, mindName, env });
4896
3879
  chownMindDir(dir, mindName);
4897
3880
  } catch (err) {
4898
- rmSync2(resolve16(dir, ".git"), { recursive: true, force: true });
3881
+ rmSync3(resolve11(dir, ".git"), { recursive: true, force: true });
4899
3882
  return c.json(
4900
3883
  {
4901
3884
  error: `Git initialization failed: ${err instanceof Error ? err.message : String(err)}`
@@ -4909,7 +3892,7 @@ ${user.trimEnd()}
4909
3892
  await gitExec(["branch", "-D", UPGRADE_VARIANT], { cwd: dir });
4910
3893
  } catch {
4911
3894
  }
4912
- if (!existsSync12(resolve16(dir, "home", "shared"))) {
3895
+ if (!existsSync9(resolve11(dir, "home", "shared"))) {
4913
3896
  try {
4914
3897
  await addSharedWorktree(mindName, dir);
4915
3898
  } catch (err) {
@@ -4920,9 +3903,9 @@ ${user.trimEnd()}
4920
3903
  }
4921
3904
  }
4922
3905
  await updateTemplateBranch(dir, template, mindName);
4923
- const parentDir = resolve16(dir, ".variants");
4924
- if (!existsSync12(parentDir)) {
4925
- mkdirSync7(parentDir, { recursive: true });
3906
+ const parentDir = resolve11(dir, ".variants");
3907
+ if (!existsSync9(parentDir)) {
3908
+ mkdirSync5(parentDir, { recursive: true });
4926
3909
  }
4927
3910
  await gitExec(["worktree", "add", "-b", UPGRADE_VARIANT, worktreeDir], { cwd: dir });
4928
3911
  const hasConflicts = await mergeTemplateBranch(worktreeDir);
@@ -4953,26 +3936,7 @@ ${user.trimEnd()}
4953
3936
  port: variantPort
4954
3937
  });
4955
3938
  } catch (err) {
4956
- try {
4957
- removeVariant(mindName, UPGRADE_VARIANT);
4958
- } catch {
4959
- }
4960
- try {
4961
- await gitExec(["worktree", "remove", "--force", worktreeDir], { cwd: dir });
4962
- } catch {
4963
- }
4964
- try {
4965
- await gitExec(["branch", "-D", UPGRADE_VARIANT], { cwd: dir });
4966
- } catch {
4967
- }
4968
- try {
4969
- chownMindDir(dir, mindName);
4970
- } catch (chownErr) {
4971
- logger_default.error(
4972
- `failed to fix ownership during upgrade cleanup for ${mindName}`,
4973
- logger_default.errorData(chownErr)
4974
- );
4975
- }
3939
+ await cleanupVariant(mindName, UPGRADE_VARIANT, dir, worktreeDir);
4976
3940
  return c.json(
4977
3941
  { error: err instanceof Error ? err.message : "Failed to complete upgrade" },
4978
3942
  500
@@ -4987,6 +3951,38 @@ ${user.trimEnd()}
4987
3951
  const variant = findVariant(baseName, variantName);
4988
3952
  if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
4989
3953
  }
3954
+ try {
3955
+ const { getSleepManagerIfReady } = await import("./sleep-manager-3RWUX2ZR.js");
3956
+ const sm = getSleepManagerIfReady();
3957
+ if (sm?.isSleeping(baseName)) {
3958
+ const body2 = await c.req.text();
3959
+ let parsed2 = null;
3960
+ try {
3961
+ parsed2 = JSON.parse(body2);
3962
+ } catch {
3963
+ }
3964
+ if (parsed2) {
3965
+ const payload = {
3966
+ channel: parsed2.channel ?? "unknown",
3967
+ sender: parsed2.sender ?? null,
3968
+ content: parsed2.content,
3969
+ isDM: parsed2.isDM
3970
+ };
3971
+ if (sm.checkWakeTrigger(baseName, payload)) {
3972
+ await sm.queueSleepMessage(baseName, payload);
3973
+ sm.initiateWake(baseName, { trigger: { channel: payload.channel } }).catch(
3974
+ (err) => logger_default.error(`failed to trigger-wake ${baseName}`, logger_default.errorData(err))
3975
+ );
3976
+ return c.json({ ok: true, queued: true, triggerWake: true });
3977
+ }
3978
+ await sm.queueSleepMessage(baseName, payload);
3979
+ return c.json({ ok: true, queued: true });
3980
+ }
3981
+ return c.json({ error: "Invalid JSON" }, 400);
3982
+ }
3983
+ } catch (err) {
3984
+ logger_default.error(`failed to check sleep state for ${baseName}`, logger_default.errorData(err));
3985
+ }
4990
3986
  if (!getMindManager().isRunning(name)) {
4991
3987
  return c.json({ error: "Mind is not running" }, 409);
4992
3988
  }
@@ -5109,13 +4105,13 @@ ${user.trimEnd()}
5109
4105
  const entry = findMind(name);
5110
4106
  if (!entry) return c.json({ error: "Mind not found" }, 404);
5111
4107
  const dir = mindDir(name);
5112
- if (!existsSync12(dir)) return c.json({ error: "Mind directory missing" }, 404);
4108
+ if (!existsSync9(dir)) return c.json({ error: "Mind directory missing" }, 404);
5113
4109
  let config = readVoluteConfig(dir);
5114
4110
  if (!config && entry.template === "pi") {
5115
- const piConfigPath = resolve16(dir, "home/.config/config.json");
5116
- if (existsSync12(piConfigPath)) {
4111
+ const piConfigPath = resolve11(dir, "home/.config/config.json");
4112
+ if (existsSync9(piConfigPath)) {
5117
4113
  try {
5118
- config = JSON.parse(readFileSync11(piConfigPath, "utf-8"));
4114
+ config = JSON.parse(readFileSync8(piConfigPath, "utf-8"));
5119
4115
  } catch {
5120
4116
  }
5121
4117
  }
@@ -5152,7 +4148,7 @@ ${user.trimEnd()}
5152
4148
  const entry = findMind(name);
5153
4149
  if (!entry) return c.json({ error: "Mind not found" }, 404);
5154
4150
  const dir = mindDir(name);
5155
- if (!existsSync12(dir)) return c.json({ error: "Mind directory missing" }, 404);
4151
+ if (!existsSync9(dir)) return c.json({ error: "Mind directory missing" }, 404);
5156
4152
  const body = c.req.valid("json");
5157
4153
  const existing = readVoluteConfig(dir) ?? {};
5158
4154
  if (body.model !== void 0) existing.model = body.model;
@@ -5215,7 +4211,7 @@ ${user.trimEnd()}
5215
4211
  } catch (err) {
5216
4212
  logger_default.error(`failed to persist event for ${baseName}`, logger_default.errorData(err));
5217
4213
  }
5218
- publish3(baseName, {
4214
+ publish2(baseName, {
5219
4215
  mind: baseName,
5220
4216
  type: body.type,
5221
4217
  session: body.session,
@@ -5360,7 +4356,7 @@ var minds_default = app11;
5360
4356
 
5361
4357
  // src/web/api/pages.ts
5362
4358
  import { readFile as readFile2, stat as stat2 } from "fs/promises";
5363
- import { extname as extname2, resolve as resolve17 } from "path";
4359
+ import { extname as extname2, resolve as resolve12 } from "path";
5364
4360
  import { Hono as Hono12 } from "hono";
5365
4361
  var MIME_TYPES = {
5366
4362
  ".html": "text/html",
@@ -5382,17 +4378,17 @@ var app12 = new Hono12().get("/:name/*", async (c) => {
5382
4378
  const name = c.req.param("name");
5383
4379
  let pagesRoot;
5384
4380
  if (name === "_system") {
5385
- pagesRoot = resolve17(voluteHome(), "shared", "pages");
4381
+ pagesRoot = resolve12(voluteHome(), "shared", "pages");
5386
4382
  } else {
5387
4383
  if (!findMind(name)) return c.text("Not found", 404);
5388
- pagesRoot = resolve17(mindDir(name), "home", "pages");
4384
+ pagesRoot = resolve12(mindDir(name), "home", "pages");
5389
4385
  }
5390
4386
  const wildcard = c.req.path.replace(`/pages/${name}`, "") || "/";
5391
- const requestedPath = resolve17(pagesRoot, wildcard.slice(1));
4387
+ const requestedPath = resolve12(pagesRoot, wildcard.slice(1));
5392
4388
  if (!requestedPath.startsWith(pagesRoot)) return c.text("Forbidden", 403);
5393
4389
  let fileStat = await stat2(requestedPath).catch(() => null);
5394
4390
  if (fileStat?.isDirectory()) {
5395
- const indexPath = resolve17(requestedPath, "index.html");
4391
+ const indexPath = resolve12(requestedPath, "index.html");
5396
4392
  fileStat = await stat2(indexPath).catch(() => null);
5397
4393
  if (fileStat?.isFile()) {
5398
4394
  const body = await readFile2(indexPath);
@@ -5462,9 +4458,9 @@ var app13 = new Hono13().get("/", async (c) => {
5462
4458
  var prompts_default = app13;
5463
4459
 
5464
4460
  // src/web/api/schedules.ts
5465
- import { CronExpressionParser as CronExpressionParser2 } from "cron-parser";
4461
+ import { CronExpressionParser } from "cron-parser";
5466
4462
  import { Hono as Hono14 } from "hono";
5467
- var slog2 = logger_default.child("schedules");
4463
+ var slog = logger_default.child("schedules");
5468
4464
  function readSchedules(name) {
5469
4465
  return readVoluteConfig(mindDir(name))?.schedules ?? [];
5470
4466
  }
@@ -5474,6 +4470,11 @@ function writeSchedules(name, schedules) {
5474
4470
  config.schedules = schedules.length > 0 ? schedules : void 0;
5475
4471
  writeVoluteConfig(dir, config);
5476
4472
  getScheduler().loadSchedules(name);
4473
+ fireWebhook({
4474
+ event: "schedule_changed",
4475
+ mind: name,
4476
+ data: { schedules }
4477
+ });
5477
4478
  }
5478
4479
  var app14 = new Hono14().get("/:name/schedules", (c) => {
5479
4480
  const name = c.req.param("name");
@@ -5496,7 +4497,7 @@ var app14 = new Hono14().get("/:name/schedules", (c) => {
5496
4497
  return c.json({ error: "message and script are mutually exclusive" }, 400);
5497
4498
  }
5498
4499
  try {
5499
- CronExpressionParser2.parse(body.cron);
4500
+ CronExpressionParser.parse(body.cron);
5500
4501
  } catch {
5501
4502
  return c.json({ error: `Invalid cron expression: ${body.cron}` }, 400);
5502
4503
  }
@@ -5524,7 +4525,7 @@ var app14 = new Hono14().get("/:name/schedules", (c) => {
5524
4525
  }
5525
4526
  if (body.cron !== void 0) {
5526
4527
  try {
5527
- CronExpressionParser2.parse(body.cron);
4528
+ CronExpressionParser.parse(body.cron);
5528
4529
  } catch {
5529
4530
  return c.json({ error: `Invalid cron expression: ${body.cron}` }, 400);
5530
4531
  }
@@ -5574,7 +4575,7 @@ var app14 = new Hono14().get("/:name/schedules", (c) => {
5574
4575
  }
5575
4576
  return c.json({ ok: true });
5576
4577
  } catch (err) {
5577
- slog2.warn(`webhook delivery failed for ${name}`, logger_default.errorData(err));
4578
+ slog.warn(`webhook delivery failed for ${name}`, logger_default.errorData(err));
5578
4579
  return c.json({ error: "Failed to reach mind" }, 502);
5579
4580
  }
5580
4581
  });
@@ -5634,9 +4635,9 @@ var app15 = new Hono15().post("/:name/shared/merge", requireAdmin, async (c) =>
5634
4635
  var shared_default = app15;
5635
4636
 
5636
4637
  // src/web/api/skills.ts
5637
- import { existsSync as existsSync13, mkdtempSync, readdirSync as readdirSync6, rmSync as rmSync3 } from "fs";
4638
+ import { existsSync as existsSync10, mkdtempSync, readdirSync as readdirSync5, rmSync as rmSync4 } from "fs";
5638
4639
  import { tmpdir } from "os";
5639
- import { join as join3, resolve as resolve18 } from "path";
4640
+ import { join as join2, resolve as resolve13 } from "path";
5640
4641
  import AdmZip from "adm-zip";
5641
4642
  import { Hono as Hono16 } from "hono";
5642
4643
  var app16 = new Hono16().get("/", async (c) => {
@@ -5646,7 +4647,7 @@ var app16 = new Hono16().get("/", async (c) => {
5646
4647
  const id = c.req.param("id");
5647
4648
  const skill = await getSharedSkill(id);
5648
4649
  if (!skill) return c.json({ error: "Skill not found" }, 404);
5649
- const dir = join3(sharedSkillsDir(), id);
4650
+ const dir = join2(sharedSkillsDir(), id);
5650
4651
  const files = listFilesRecursive(dir);
5651
4652
  return c.json({ ...skill, files });
5652
4653
  }).post("/upload", requireAdmin, async (c) => {
@@ -5658,25 +4659,25 @@ var app16 = new Hono16().get("/", async (c) => {
5658
4659
  if (!file.name.endsWith(".zip")) {
5659
4660
  return c.json({ error: "Only .zip files are accepted" }, 400);
5660
4661
  }
5661
- const buffer = Buffer.from(await file.arrayBuffer());
5662
- const tmpDir = mkdtempSync(join3(tmpdir(), "volute-skill-upload-"));
4662
+ const buffer2 = Buffer.from(await file.arrayBuffer());
4663
+ const tmpDir = mkdtempSync(join2(tmpdir(), "volute-skill-upload-"));
5663
4664
  try {
5664
- const zip = new AdmZip(buffer);
4665
+ const zip = new AdmZip(buffer2);
5665
4666
  for (const entry of zip.getEntries()) {
5666
- const target = resolve18(tmpDir, entry.entryName);
4667
+ const target = resolve13(tmpDir, entry.entryName);
5667
4668
  if (!target.startsWith(tmpDir)) {
5668
4669
  return c.json({ error: "Invalid zip: paths must not escape archive" }, 400);
5669
4670
  }
5670
4671
  }
5671
4672
  zip.extractAllTo(tmpDir, true);
5672
4673
  let skillDir = null;
5673
- if (existsSync13(join3(tmpDir, "SKILL.md"))) {
4674
+ if (existsSync10(join2(tmpDir, "SKILL.md"))) {
5674
4675
  skillDir = tmpDir;
5675
4676
  } else {
5676
- const entries = readdirSync6(tmpDir, { withFileTypes: true }).filter((e) => e.isDirectory());
4677
+ const entries = readdirSync5(tmpDir, { withFileTypes: true }).filter((e) => e.isDirectory());
5677
4678
  for (const entry of entries) {
5678
- if (existsSync13(join3(tmpDir, entry.name, "SKILL.md"))) {
5679
- skillDir = join3(tmpDir, entry.name);
4679
+ if (existsSync10(join2(tmpDir, entry.name, "SKILL.md"))) {
4680
+ skillDir = join2(tmpDir, entry.name);
5680
4681
  break;
5681
4682
  }
5682
4683
  }
@@ -5692,7 +4693,7 @@ var app16 = new Hono16().get("/", async (c) => {
5692
4693
  }
5693
4694
  throw e;
5694
4695
  } finally {
5695
- rmSync3(tmpDir, { recursive: true, force: true });
4696
+ rmSync4(tmpDir, { recursive: true, force: true });
5696
4697
  }
5697
4698
  }).delete("/:id", requireAdmin, async (c) => {
5698
4699
  const id = c.req.param("id");
@@ -5717,93 +4718,487 @@ var app17 = new Hono17().post("/restart", requireAdmin, (c) => {
5717
4718
  return c.json({ ok: true });
5718
4719
  }).get("/logs", async (c) => {
5719
4720
  const user = c.get("user");
5720
- if (user.role !== "admin") return c.json({ error: "Forbidden" }, 403);
5721
- return streamSSE3(c, async (stream) => {
5722
- for (const entry of logBuffer.getEntries()) {
5723
- await stream.writeSSE({ data: JSON.stringify(entry) });
5724
- }
5725
- const unsubscribe = logBuffer.subscribe((entry) => {
5726
- stream.writeSSE({ data: JSON.stringify(entry) }).catch(() => {
4721
+ if (user.role !== "admin") return c.json({ error: "Forbidden" }, 403);
4722
+ return streamSSE3(c, async (stream) => {
4723
+ for (const entry of logBuffer.getEntries()) {
4724
+ await stream.writeSSE({ data: JSON.stringify(entry) });
4725
+ }
4726
+ const unsubscribe = logBuffer.subscribe((entry) => {
4727
+ stream.writeSSE({ data: JSON.stringify(entry) }).catch(() => {
4728
+ });
4729
+ });
4730
+ await new Promise((resolve18) => {
4731
+ stream.onAbort(() => {
4732
+ unsubscribe();
4733
+ resolve18();
4734
+ });
4735
+ });
4736
+ });
4737
+ }).get("/info", (c) => {
4738
+ const config = readSystemsConfig();
4739
+ return c.json({ system: config?.system ?? null });
4740
+ });
4741
+ var system_default = app17;
4742
+
4743
+ // src/web/api/typing.ts
4744
+ import { zValidator as zValidator5 } from "@hono/zod-validator";
4745
+ import { Hono as Hono18 } from "hono";
4746
+ import { z as z5 } from "zod";
4747
+ var typingSchema = z5.object({
4748
+ channel: z5.string().min(1),
4749
+ sender: z5.string().min(1),
4750
+ active: z5.boolean()
4751
+ });
4752
+ var app18 = new Hono18().post("/:name/typing", zValidator5("json", typingSchema), (c) => {
4753
+ const { channel, sender, active } = c.req.valid("json");
4754
+ const map = getTypingMap();
4755
+ if (active) {
4756
+ map.set(channel, sender);
4757
+ } else {
4758
+ map.delete(channel, sender);
4759
+ }
4760
+ const volutePrefix = "volute:";
4761
+ if (channel.startsWith(volutePrefix)) {
4762
+ const conversationId = channel.slice(volutePrefix.length);
4763
+ publish(conversationId, { type: "typing", senders: map.get(channel) });
4764
+ }
4765
+ return c.json({ ok: true });
4766
+ }).get("/:name/typing", (c) => {
4767
+ const channel = c.req.query("channel");
4768
+ if (!channel) {
4769
+ return c.json({ error: "channel query param is required" }, 400);
4770
+ }
4771
+ const map = getTypingMap();
4772
+ return c.json({ typing: map.get(channel) });
4773
+ });
4774
+ var typing_default = app18;
4775
+
4776
+ // src/web/api/update.ts
4777
+ import { spawn as spawn2 } from "child_process";
4778
+ import { Hono as Hono19 } from "hono";
4779
+ var bin;
4780
+ var app19 = new Hono19().get("/update", async (c) => {
4781
+ const result = await checkForUpdate();
4782
+ return c.json(result);
4783
+ }).post("/update", requireAdmin, async (c) => {
4784
+ bin ??= resolveVoluteBin();
4785
+ const child = spawn2(bin, ["update"], {
4786
+ stdio: "ignore",
4787
+ detached: true
4788
+ });
4789
+ child.on("error", (err) => {
4790
+ logger_default.error("Update process error", { error: err.message });
4791
+ });
4792
+ child.unref();
4793
+ return c.json({ ok: true, message: "Updating..." });
4794
+ });
4795
+ var update_default = app19;
4796
+
4797
+ // src/web/api/v1/chat.ts
4798
+ import { zValidator as zValidator6 } from "@hono/zod-validator";
4799
+ import { Hono as Hono20 } from "hono";
4800
+ import { streamSSE as streamSSE4 } from "hono/streaming";
4801
+ import { z as z6 } from "zod";
4802
+ async function fanOutToMinds(opts) {
4803
+ const participants = await getParticipants(opts.conversationId);
4804
+ const mindParticipants = participants.filter((p) => p.userType === "mind");
4805
+ const participantNames = participants.map((p) => p.username);
4806
+ const isDM = opts.isDM ?? participants.length === 2;
4807
+ const channelEntryType = opts.channelEntryType ?? (isDM ? "dm" : "group");
4808
+ const { getMindManager: getMindManager2 } = await import("./mind-manager-P5OBDUKI.js");
4809
+ const { getSleepManagerIfReady } = await import("./sleep-manager-3RWUX2ZR.js");
4810
+ const manager = getMindManager2();
4811
+ const sm = getSleepManagerIfReady();
4812
+ const targetMinds = mindParticipants.map((ap) => {
4813
+ const key = opts.targetName ? opts.targetName(ap.username) : ap.username;
4814
+ if (manager.isRunning(key) || sm?.isSleeping(ap.username)) return ap.username;
4815
+ return null;
4816
+ }).filter((n) => n !== null && n !== opts.senderName);
4817
+ function slugForMind(mindUsername) {
4818
+ return buildVoluteSlug({
4819
+ participants,
4820
+ mindUsername,
4821
+ convTitle: opts.convTitle,
4822
+ conversationId: opts.conversationId,
4823
+ ...opts.slugExtra
4824
+ });
4825
+ }
4826
+ const channelEntry = {
4827
+ platformId: opts.conversationId,
4828
+ platform: "volute",
4829
+ name: opts.convTitle ?? void 0,
4830
+ type: channelEntryType
4831
+ };
4832
+ for (const ap of mindParticipants) {
4833
+ try {
4834
+ writeChannelEntry(ap.username, slugForMind(ap.username), channelEntry);
4835
+ } catch (err) {
4836
+ logger_default.warn(`failed to write channel entry for ${ap.username}`, logger_default.errorData(err));
4837
+ }
4838
+ }
4839
+ for (const mindName of targetMinds) {
4840
+ const target = opts.targetName ? opts.targetName(mindName) : mindName;
4841
+ const channel = slugForMind(mindName);
4842
+ const typingMap = getTypingMap();
4843
+ const currentlyTyping = typingMap.get(channel).filter((name) => participantNames.includes(name));
4844
+ deliverMessage(target, {
4845
+ content: opts.contentBlocks,
4846
+ channel,
4847
+ conversationId: opts.conversationId,
4848
+ sender: opts.senderName,
4849
+ participants: participantNames,
4850
+ participantCount: participants.length,
4851
+ isDM,
4852
+ ...currentlyTyping.length > 0 ? { typing: currentlyTyping } : {}
4853
+ }).catch((err) => {
4854
+ logger_default.warn("[v1-chat] delivery failed", logger_default.errorData(err));
4855
+ });
4856
+ }
4857
+ }
4858
+ var mindChatSchema = z6.object({
4859
+ message: z6.string().optional(),
4860
+ conversationId: z6.string().optional(),
4861
+ sender: z6.string().optional(),
4862
+ images: z6.array(z6.object({ media_type: z6.string(), data: z6.string() })).optional()
4863
+ });
4864
+ var unifiedChatSchema = z6.object({
4865
+ message: z6.string().optional(),
4866
+ conversationId: z6.string(),
4867
+ images: z6.array(z6.object({ media_type: z6.string(), data: z6.string() })).optional()
4868
+ });
4869
+ var app20 = new Hono20().use("*", authMiddleware).post("/minds/:name/chat", zValidator6("json", mindChatSchema), async (c) => {
4870
+ const name = c.req.param("name");
4871
+ const [baseName] = name.split("@", 2);
4872
+ const entry = findMind(baseName);
4873
+ if (!entry) return c.json({ error: "Mind not found" }, 404);
4874
+ const body = c.req.valid("json");
4875
+ if (!body.message && (!body.images || body.images.length === 0)) {
4876
+ return c.json({ error: "message or images required" }, 400);
4877
+ }
4878
+ const user = c.get("user");
4879
+ const mindUser = await getOrCreateMindUser(baseName);
4880
+ const senderName = user.id === 0 && body.sender ? body.sender : user.username;
4881
+ let conversationId = body.conversationId;
4882
+ if (conversationId) {
4883
+ if (user.id !== 0 && !await isParticipantOrOwner(conversationId, user.id)) {
4884
+ return c.json({ error: "Conversation not found" }, 404);
4885
+ }
4886
+ } else {
4887
+ const participantIds = [];
4888
+ if (user.id !== 0) {
4889
+ participantIds.push(user.id);
4890
+ } else if (body.sender) {
4891
+ const senderMind = findMind(body.sender);
4892
+ if (senderMind) {
4893
+ const senderMindUser = await getOrCreateMindUser(body.sender);
4894
+ participantIds.push(senderMindUser.id);
4895
+ }
4896
+ }
4897
+ participantIds.push(mindUser.id);
4898
+ if (participantIds.length === 2) {
4899
+ const existing = await findDMConversation(baseName, participantIds);
4900
+ if (existing) conversationId = existing;
4901
+ }
4902
+ if (!conversationId) {
4903
+ const participantNames = /* @__PURE__ */ new Set([senderName, baseName]);
4904
+ const title = [...participantNames].join(", ");
4905
+ const conv2 = await createConversation(baseName, "volute", {
4906
+ userId: user.id !== 0 ? user.id : void 0,
4907
+ title,
4908
+ participantIds
4909
+ });
4910
+ conversationId = conv2.id;
4911
+ }
4912
+ }
4913
+ const conv = await getConversation(conversationId);
4914
+ const convTitle = conv?.title ?? null;
4915
+ const contentBlocks = [];
4916
+ if (body.message) contentBlocks.push({ type: "text", text: body.message });
4917
+ if (body.images) {
4918
+ for (const img of body.images) {
4919
+ contentBlocks.push({ type: "image", media_type: img.media_type, data: img.data });
4920
+ }
4921
+ }
4922
+ await addMessage(conversationId, "user", senderName, contentBlocks);
4923
+ await fanOutToMinds({
4924
+ conversationId,
4925
+ contentBlocks,
4926
+ senderName,
4927
+ convTitle,
4928
+ targetName: (username) => username === baseName ? name : username
4929
+ });
4930
+ return c.json({ ok: true, conversationId });
4931
+ }).get("/minds/:name/conversations/:id/events", async (c) => {
4932
+ const conversationId = c.req.param("id");
4933
+ const user = c.get("user");
4934
+ if (user.id !== 0 && !await isParticipantOrOwner(conversationId, user.id)) {
4935
+ return c.json({ error: "Conversation not found" }, 404);
4936
+ }
4937
+ return streamSSE4(c, async (stream) => {
4938
+ const unsubscribe = subscribe2(conversationId, (event) => {
4939
+ stream.writeSSE({ data: JSON.stringify(event) }).catch((err) => {
4940
+ if (!stream.aborted) logger_default.error("[v1-chat] SSE write error:", logger_default.errorData(err));
5727
4941
  });
5728
4942
  });
5729
- await new Promise((resolve23) => {
4943
+ const keepAlive = setInterval(() => {
4944
+ stream.writeSSE({ data: "" }).catch((err) => {
4945
+ if (!stream.aborted) logger_default.error("[v1-chat] SSE ping error:", logger_default.errorData(err));
4946
+ });
4947
+ }, 15e3);
4948
+ await new Promise((resolve18) => {
5730
4949
  stream.onAbort(() => {
5731
4950
  unsubscribe();
5732
- resolve23();
4951
+ clearInterval(keepAlive);
4952
+ resolve18();
5733
4953
  });
5734
4954
  });
5735
4955
  });
5736
- }).get("/info", (c) => {
5737
- const config = readSystemsConfig();
5738
- return c.json({ system: config?.system ?? null });
4956
+ }).post("/chat", zValidator6("json", unifiedChatSchema), async (c) => {
4957
+ const user = c.get("user");
4958
+ const body = c.req.valid("json");
4959
+ if (!body.message && (!body.images || body.images.length === 0)) {
4960
+ return c.json({ error: "message or images required" }, 400);
4961
+ }
4962
+ const conv = await getConversation(body.conversationId);
4963
+ if (!conv) return c.json({ error: "Conversation not found" }, 404);
4964
+ if (user.id !== 0 && !await isParticipantOrOwner(body.conversationId, user.id)) {
4965
+ return c.json({ error: "Conversation not found" }, 404);
4966
+ }
4967
+ const senderName = user.username;
4968
+ const contentBlocks = [];
4969
+ if (body.message) contentBlocks.push({ type: "text", text: body.message });
4970
+ if (body.images) {
4971
+ for (const img of body.images) {
4972
+ contentBlocks.push({ type: "image", media_type: img.media_type, data: img.data });
4973
+ }
4974
+ }
4975
+ await addMessage(body.conversationId, "user", senderName, contentBlocks);
4976
+ const isDM = conv.type === "dm";
4977
+ await fanOutToMinds({
4978
+ conversationId: body.conversationId,
4979
+ contentBlocks,
4980
+ senderName,
4981
+ convTitle: conv.title,
4982
+ isDM,
4983
+ channelEntryType: conv.type === "channel" ? "group" : isDM ? "dm" : "group",
4984
+ slugExtra: { convType: conv.type, convName: conv.name }
4985
+ });
4986
+ return c.json({ ok: true, conversationId: body.conversationId });
5739
4987
  });
5740
- var system_default = app17;
4988
+ var chat_default = app20;
5741
4989
 
5742
- // src/web/api/typing.ts
5743
- import { zValidator as zValidator5 } from "@hono/zod-validator";
5744
- import { Hono as Hono18 } from "hono";
5745
- import { z as z5 } from "zod";
5746
- var typingSchema = z5.object({
5747
- channel: z5.string().min(1),
5748
- sender: z5.string().min(1),
5749
- active: z5.boolean()
4990
+ // src/web/api/v1/conversations.ts
4991
+ import { zValidator as zValidator7 } from "@hono/zod-validator";
4992
+ import { Hono as Hono21 } from "hono";
4993
+ import { z as z7 } from "zod";
4994
+ var createSchema = z7.object({
4995
+ title: z7.string().optional(),
4996
+ participantNames: z7.array(z7.string()).min(1)
5750
4997
  });
5751
- var app18 = new Hono18().post("/:name/typing", zValidator5("json", typingSchema), (c) => {
5752
- const { channel, sender, active } = c.req.valid("json");
5753
- const map = getTypingMap();
5754
- if (active) {
5755
- map.set(channel, sender);
5756
- } else {
5757
- map.delete(channel, sender);
4998
+ var app21 = new Hono21().use("*", authMiddleware).get("/", async (c) => {
4999
+ const user = c.get("user");
5000
+ const convs = await listConversationsWithParticipants(user.id);
5001
+ return c.json(convs);
5002
+ }).get("/:id/messages", async (c) => {
5003
+ const id = c.req.param("id");
5004
+ const user = c.get("user");
5005
+ if (user.id !== 0 && !await isParticipantOrOwner(id, user.id)) {
5006
+ return c.json({ error: "Conversation not found" }, 404);
5758
5007
  }
5759
- const volutePrefix = "volute:";
5760
- if (channel.startsWith(volutePrefix)) {
5761
- const conversationId = channel.slice(volutePrefix.length);
5762
- publish(conversationId, { type: "typing", senders: map.get(channel) });
5008
+ const beforeStr = c.req.query("before");
5009
+ const limitStr = c.req.query("limit");
5010
+ if (!beforeStr && !limitStr) {
5011
+ const msgs = await getMessages(id);
5012
+ return c.json({ items: msgs, hasMore: false });
5763
5013
  }
5764
- return c.json({ ok: true });
5765
- }).get("/:name/typing", (c) => {
5766
- const channel = c.req.query("channel");
5767
- if (!channel) {
5768
- return c.json({ error: "channel query param is required" }, 400);
5014
+ const before = beforeStr ? parseInt(beforeStr, 10) : void 0;
5015
+ const limit = limitStr ? parseInt(limitStr, 10) : void 0;
5016
+ if (before !== void 0 && isNaN(before) || limit !== void 0 && isNaN(limit)) {
5017
+ return c.json({ error: "Invalid cursor params: before and limit must be integers" }, 400);
5769
5018
  }
5770
- const map = getTypingMap();
5771
- return c.json({ typing: map.get(channel) });
5019
+ const result = await getMessagesPaginated(id, { before, limit });
5020
+ return c.json({ items: result.messages, hasMore: result.hasMore });
5021
+ }).get("/:id/participants", async (c) => {
5022
+ const id = c.req.param("id");
5023
+ const user = c.get("user");
5024
+ if (user.id !== 0 && !await isParticipantOrOwner(id, user.id)) {
5025
+ return c.json({ error: "Conversation not found" }, 404);
5026
+ }
5027
+ const participants = await getParticipants(id);
5028
+ return c.json(participants);
5029
+ }).post("/", zValidator7("json", createSchema), async (c) => {
5030
+ const user = c.get("user");
5031
+ const body = c.req.valid("json");
5032
+ const participantIds = /* @__PURE__ */ new Set();
5033
+ if (user.id !== 0) participantIds.add(user.id);
5034
+ let firstMindName;
5035
+ for (const name of body.participantNames) {
5036
+ const existing = await getUserByUsername(name);
5037
+ if (existing) {
5038
+ participantIds.add(existing.id);
5039
+ if (!firstMindName && existing.user_type === "mind") firstMindName = name;
5040
+ continue;
5041
+ }
5042
+ if (findMind(name)) {
5043
+ const au = await getOrCreateMindUser(name);
5044
+ participantIds.add(au.id);
5045
+ if (!firstMindName) firstMindName = name;
5046
+ continue;
5047
+ }
5048
+ return c.json({ error: `User not found: ${name}` }, 400);
5049
+ }
5050
+ if (!firstMindName) {
5051
+ return c.json({ error: "At least one mind participant is required" }, 400);
5052
+ }
5053
+ const conv = await createConversation(firstMindName, "volute", {
5054
+ userId: user.id !== 0 ? user.id : void 0,
5055
+ title: body.title,
5056
+ participantIds: [...participantIds]
5057
+ });
5058
+ return c.json(conv, 201);
5059
+ }).delete("/:id", async (c) => {
5060
+ const id = c.req.param("id");
5061
+ const user = c.get("user");
5062
+ const deleted = await deleteConversationForUser(id, user.id);
5063
+ if (!deleted) return c.json({ error: "Conversation not found" }, 404);
5064
+ return c.json({ ok: true });
5772
5065
  });
5773
- var typing_default = app18;
5066
+ var conversations_default = app21;
5774
5067
 
5775
- // src/web/api/update.ts
5776
- import { spawn as spawn3 } from "child_process";
5777
- import { Hono as Hono19 } from "hono";
5778
- var bin;
5779
- var app19 = new Hono19().get("/update", async (c) => {
5780
- const result = await checkForUpdate();
5781
- return c.json(result);
5782
- }).post("/update", requireAdmin, async (c) => {
5783
- bin ??= resolveVoluteBin();
5784
- const child = spawn3(bin, ["update"], {
5785
- stdio: "ignore",
5786
- detached: true
5787
- });
5788
- child.on("error", (err) => {
5789
- logger_default.error("Update process error", { error: err.message });
5068
+ // src/web/api/v1/events.ts
5069
+ import { desc as desc4 } from "drizzle-orm";
5070
+ import { Hono as Hono22 } from "hono";
5071
+ import { streamSSE as streamSSE5 } from "hono/streaming";
5072
+
5073
+ // src/lib/events/event-sequencer.ts
5074
+ var BUFFER_SIZE = 1e3;
5075
+ var MAX_AGE_MS = 5 * 60 * 1e3;
5076
+ var nextId = 1;
5077
+ var buffer = [];
5078
+ function bufferEvent(data) {
5079
+ const id = nextId++;
5080
+ buffer.push({ id, data, timestamp: Date.now() });
5081
+ while (buffer.length > BUFFER_SIZE) {
5082
+ buffer.shift();
5083
+ }
5084
+ return id;
5085
+ }
5086
+ function getEventsSince(sinceId) {
5087
+ const now = Date.now();
5088
+ const startIdx = buffer.findIndex((e) => e.id > sinceId);
5089
+ if (startIdx === -1) return [];
5090
+ return buffer.slice(startIdx).filter((e) => now - e.timestamp < MAX_AGE_MS);
5091
+ }
5092
+
5093
+ // src/web/api/v1/events.ts
5094
+ var app22 = new Hono22().use("*", authMiddleware).get("/", async (c) => {
5095
+ const user = c.get("user");
5096
+ const since = c.req.query("since");
5097
+ const sinceId = since ? Number(since) : 0;
5098
+ return streamSSE5(c, async (stream) => {
5099
+ const cleanups = [];
5100
+ try {
5101
+ if (sinceId > 0) {
5102
+ const missed = getEventsSince(sinceId);
5103
+ for (const event of missed) {
5104
+ await stream.writeSSE({
5105
+ id: String(event.id),
5106
+ data: JSON.stringify(event.data)
5107
+ });
5108
+ }
5109
+ }
5110
+ let recentActivity = [];
5111
+ try {
5112
+ const db = await getDb();
5113
+ recentActivity = await db.select().from(activity).orderBy(desc4(activity.created_at)).limit(50);
5114
+ recentActivity = recentActivity.map((row) => ({
5115
+ ...row,
5116
+ metadata: row.metadata ? JSON.parse(row.metadata) : null
5117
+ }));
5118
+ } catch (err) {
5119
+ logger_default.error("[v1-events] failed to fetch recent activity", logger_default.errorData(err));
5120
+ }
5121
+ let conversations2 = [];
5122
+ try {
5123
+ conversations2 = await listConversationsWithParticipants(user.id);
5124
+ } catch (err) {
5125
+ logger_default.error("[v1-events] failed to fetch conversations", logger_default.errorData(err));
5126
+ }
5127
+ const sites = getCachedSites();
5128
+ const recentPages = getCachedRecentPages();
5129
+ const snapshotData = {
5130
+ event: "snapshot",
5131
+ activity: recentActivity,
5132
+ conversations: conversations2,
5133
+ sites,
5134
+ recentPages,
5135
+ activeMinds: getActiveMinds()
5136
+ };
5137
+ const snapshotId = bufferEvent(snapshotData);
5138
+ await stream.writeSSE({
5139
+ id: String(snapshotId),
5140
+ data: JSON.stringify(snapshotData)
5141
+ });
5142
+ const unsubActivity = subscribe((event) => {
5143
+ const data = {
5144
+ event: "activity",
5145
+ ...event,
5146
+ metadata: event.metadata ?? null
5147
+ };
5148
+ const eventId = bufferEvent(data);
5149
+ stream.writeSSE({
5150
+ id: String(eventId),
5151
+ data: JSON.stringify(data)
5152
+ }).catch((err) => {
5153
+ if (!stream.aborted) logger_default.error("[v1-events] write error:", logger_default.errorData(err));
5154
+ });
5155
+ });
5156
+ cleanups.push(unsubActivity);
5157
+ for (const conv of conversations2) {
5158
+ const unsubConv = subscribe2(conv.id, (event) => {
5159
+ const data = { event: "conversation", conversationId: conv.id, ...event };
5160
+ const eventId = bufferEvent(data);
5161
+ stream.writeSSE({
5162
+ id: String(eventId),
5163
+ data: JSON.stringify(data)
5164
+ }).catch((err) => {
5165
+ if (!stream.aborted) logger_default.error("[v1-events] write error:", logger_default.errorData(err));
5166
+ });
5167
+ });
5168
+ cleanups.push(unsubConv);
5169
+ }
5170
+ const keepAlive = setInterval(() => {
5171
+ stream.writeSSE({ data: "" }).catch((err) => {
5172
+ if (!stream.aborted) logger_default.error("[v1-events] ping error:", logger_default.errorData(err));
5173
+ });
5174
+ }, 15e3);
5175
+ cleanups.push(() => clearInterval(keepAlive));
5176
+ await new Promise((resolve18) => {
5177
+ stream.onAbort(() => resolve18());
5178
+ });
5179
+ } finally {
5180
+ for (const cleanup of cleanups) {
5181
+ try {
5182
+ cleanup();
5183
+ } catch {
5184
+ }
5185
+ }
5186
+ }
5790
5187
  });
5791
- child.unref();
5792
- return c.json({ ok: true, message: "Updating..." });
5793
5188
  });
5794
- var update_default = app19;
5189
+ var events_default = app22;
5795
5190
 
5796
5191
  // src/web/api/variants.ts
5797
- import { existsSync as existsSync14, mkdirSync as mkdirSync9, writeFileSync as writeFileSync9 } from "fs";
5798
- import { resolve as resolve20 } from "path";
5799
- import { Hono as Hono20 } from "hono";
5192
+ import { existsSync as existsSync11, mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "fs";
5193
+ import { resolve as resolve15 } from "path";
5194
+ import { Hono as Hono23 } from "hono";
5800
5195
 
5801
5196
  // src/lib/spawn-server.ts
5802
- import { spawn as spawn4 } from "child_process";
5803
- import { closeSync, mkdirSync as mkdirSync8, openSync, readFileSync as readFileSync12 } from "fs";
5804
- import { resolve as resolve19 } from "path";
5197
+ import { spawn as spawn3 } from "child_process";
5198
+ import { closeSync, mkdirSync as mkdirSync6, openSync, readFileSync as readFileSync9 } from "fs";
5199
+ import { resolve as resolve14 } from "path";
5805
5200
  function tsxBin(cwd) {
5806
- return resolve19(cwd, "node_modules", ".bin", "tsx");
5201
+ return resolve14(cwd, "node_modules", ".bin", "tsx");
5807
5202
  }
5808
5203
  function spawnServer(cwd, port, options) {
5809
5204
  if (options?.detached) {
@@ -5812,37 +5207,37 @@ function spawnServer(cwd, port, options) {
5812
5207
  return spawnAttached(cwd, port);
5813
5208
  }
5814
5209
  function spawnAttached(cwd, port) {
5815
- const child = spawn4(tsxBin(cwd), ["src/server.ts", "--port", String(port)], {
5210
+ const child = spawn3(tsxBin(cwd), ["src/server.ts", "--port", String(port)], {
5816
5211
  cwd,
5817
5212
  stdio: ["ignore", "pipe", "pipe"]
5818
5213
  });
5819
- return new Promise((resolve23) => {
5820
- const timeout = setTimeout(() => resolve23(null), 3e4);
5214
+ return new Promise((resolve18) => {
5215
+ const timeout = setTimeout(() => resolve18(null), 3e4);
5821
5216
  function checkOutput(data) {
5822
5217
  const match = data.toString().match(/listening on :(\d+)/);
5823
5218
  if (match) {
5824
5219
  clearTimeout(timeout);
5825
- resolve23({ child, actualPort: parseInt(match[1], 10) });
5220
+ resolve18({ child, actualPort: parseInt(match[1], 10) });
5826
5221
  }
5827
5222
  }
5828
5223
  child.stdout?.on("data", checkOutput);
5829
5224
  child.stderr?.on("data", checkOutput);
5830
5225
  child.on("error", () => {
5831
5226
  clearTimeout(timeout);
5832
- resolve23(null);
5227
+ resolve18(null);
5833
5228
  });
5834
5229
  child.on("exit", () => {
5835
5230
  clearTimeout(timeout);
5836
- resolve23(null);
5231
+ resolve18(null);
5837
5232
  });
5838
5233
  });
5839
5234
  }
5840
5235
  function spawnDetached(cwd, port, logDir) {
5841
- const logsDir = logDir ?? resolve19(cwd, ".mind", "logs");
5842
- mkdirSync8(logsDir, { recursive: true });
5843
- const logPath = resolve19(logsDir, "mind.log");
5236
+ const logsDir = logDir ?? resolve14(cwd, ".mind", "logs");
5237
+ mkdirSync6(logsDir, { recursive: true });
5238
+ const logPath = resolve14(logsDir, "mind.log");
5844
5239
  const logFd = openSync(logPath, "a");
5845
- const child = spawn4(tsxBin(cwd), ["src/server.ts", "--port", String(port)], {
5240
+ const child = spawn3(tsxBin(cwd), ["src/server.ts", "--port", String(port)], {
5846
5241
  cwd,
5847
5242
  stdio: ["ignore", logFd, logFd],
5848
5243
  detached: true
@@ -5860,7 +5255,7 @@ function spawnDetached(cwd, port, logDir) {
5860
5255
  }
5861
5256
  const interval = setInterval(() => {
5862
5257
  try {
5863
- const content = readFileSync12(logPath, "utf-8");
5258
+ const content = readFileSync9(logPath, "utf-8");
5864
5259
  const match = content.match(/listening on :(\d+)/);
5865
5260
  if (match) {
5866
5261
  finish({ child, actualPort: parseInt(match[1], 10) });
@@ -5912,7 +5307,7 @@ async function verify2(port) {
5912
5307
  }
5913
5308
 
5914
5309
  // src/web/api/variants.ts
5915
- var app20 = new Hono20().get("/:name/variants", async (c) => {
5310
+ var app23 = new Hono23().get("/:name/variants", async (c) => {
5916
5311
  const name = c.req.param("name");
5917
5312
  const entry = findMind(name);
5918
5313
  if (!entry) return c.json({ error: "Mind not found" }, 404);
@@ -5952,11 +5347,11 @@ var app20 = new Hono20().get("/:name/variants", async (c) => {
5952
5347
  const err = validateBranchName(variantName);
5953
5348
  if (err) return c.json({ error: err }, 400);
5954
5349
  const projectRoot = mindDir(mindName);
5955
- const variantDir = resolve20(projectRoot, ".variants", variantName);
5956
- if (existsSync14(variantDir)) {
5350
+ const variantDir = resolve15(projectRoot, ".variants", variantName);
5351
+ if (existsSync11(variantDir)) {
5957
5352
  return c.json({ error: `Variant directory already exists: ${variantDir}` }, 409);
5958
5353
  }
5959
- mkdirSync9(resolve20(projectRoot, ".variants"), { recursive: true });
5354
+ mkdirSync7(resolve15(projectRoot, ".variants"), { recursive: true });
5960
5355
  try {
5961
5356
  await gitExec(["worktree", "add", "-b", variantName, variantDir], { cwd: projectRoot });
5962
5357
  } catch (e) {
@@ -5969,7 +5364,7 @@ var app20 = new Hono20().get("/:name/variants", async (c) => {
5969
5364
  const [cmd, args] = wrapForIsolation("npm", ["install"], mindName);
5970
5365
  await exec(cmd, args, {
5971
5366
  cwd: variantDir,
5972
- env: { ...process.env, HOME: resolve20(variantDir, "home") }
5367
+ env: { ...process.env, HOME: resolve15(variantDir, "home") }
5973
5368
  });
5974
5369
  } else {
5975
5370
  await exec("npm", ["install"], { cwd: variantDir });
@@ -5979,7 +5374,7 @@ var app20 = new Hono20().get("/:name/variants", async (c) => {
5979
5374
  return c.json({ error: `npm install failed: ${msg}` }, 500);
5980
5375
  }
5981
5376
  if (body.soul) {
5982
- writeFileSync9(resolve20(variantDir, "home/SOUL.md"), body.soul);
5377
+ writeFileSync7(resolve15(variantDir, "home/SOUL.md"), body.soul);
5983
5378
  }
5984
5379
  const variantPort = body.port ?? nextPort();
5985
5380
  const variant = {
@@ -6017,7 +5412,7 @@ var app20 = new Hono20().get("/:name/variants", async (c) => {
6017
5412
  } catch {
6018
5413
  }
6019
5414
  const projectRoot = mindDir(mindName);
6020
- if (existsSync14(variant.path)) {
5415
+ if (existsSync11(variant.path)) {
6021
5416
  const status = (await gitExec(["status", "--porcelain"], { cwd: variant.path })).trim();
6022
5417
  if (status) {
6023
5418
  try {
@@ -6074,17 +5469,7 @@ var app20 = new Hono20().get("/:name/variants", async (c) => {
6074
5469
  } catch (e) {
6075
5470
  return c.json({ error: "Merge failed. Resolve conflicts manually." }, 500);
6076
5471
  }
6077
- if (existsSync14(variant.path)) {
6078
- try {
6079
- await gitExec(["worktree", "remove", "--force", variant.path], { cwd: projectRoot });
6080
- } catch {
6081
- }
6082
- }
6083
- try {
6084
- await gitExec(["branch", "-D", variant.branch], { cwd: projectRoot });
6085
- } catch {
6086
- }
6087
- removeVariant(mindName, variantName);
5472
+ await cleanupVariant(mindName, variantName, projectRoot, variant.path);
6088
5473
  if (variantName === "upgrade") {
6089
5474
  try {
6090
5475
  const { computeTemplateHash: computeTemplateHash2 } = await import("./template-hash-BIMA4ILT.js");
@@ -6095,13 +5480,12 @@ var app20 = new Hono20().get("/:name/variants", async (c) => {
6095
5480
  console.error(`[daemon] failed to update template hash for ${mindName}:`, err);
6096
5481
  }
6097
5482
  }
6098
- chownMindDir(projectRoot, mindName);
6099
5483
  try {
6100
5484
  if (isIsolationEnabled()) {
6101
5485
  const [cmd, args] = wrapForIsolation("npm", ["install"], mindName);
6102
5486
  await exec(cmd, args, {
6103
5487
  cwd: projectRoot,
6104
- env: { ...process.env, HOME: resolve20(projectRoot, "home") }
5488
+ env: { ...process.env, HOME: resolve15(projectRoot, "home") }
6105
5489
  });
6106
5490
  } else {
6107
5491
  await exec("npm", ["install"], { cwd: projectRoot });
@@ -6136,41 +5520,22 @@ var app20 = new Hono20().get("/:name/variants", async (c) => {
6136
5520
  const variant = findVariant(mindName, variantName);
6137
5521
  if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
6138
5522
  const projectRoot = mindDir(mindName);
6139
- const manager = getMindManager();
6140
- const compositeKey = `${mindName}@${variantName}`;
6141
- if (manager.isRunning(compositeKey)) {
6142
- try {
6143
- await manager.stopMind(compositeKey);
6144
- } catch {
6145
- }
6146
- }
6147
- if (existsSync14(variant.path)) {
6148
- try {
6149
- await gitExec(["worktree", "remove", "--force", variant.path], { cwd: projectRoot });
6150
- } catch {
6151
- }
6152
- }
6153
- try {
6154
- await gitExec(["branch", "-D", variant.branch], { cwd: projectRoot });
6155
- } catch {
6156
- }
6157
- removeVariant(mindName, variantName);
6158
- chownMindDir(projectRoot, mindName);
5523
+ await cleanupVariant(mindName, variantName, projectRoot, variant.path, { stop: true });
6159
5524
  return c.json({ ok: true });
6160
5525
  });
6161
- var variants_default = app20;
5526
+ var variants_default = app23;
6162
5527
 
6163
5528
  // src/web/api/volute/channels.ts
6164
- import { zValidator as zValidator6 } from "@hono/zod-validator";
6165
- import { Hono as Hono21 } from "hono";
6166
- import { z as z6 } from "zod";
6167
- var createSchema = z6.object({
6168
- name: z6.string().min(1).max(50).regex(/^[a-z0-9][a-z0-9-]*$/, "Channel names must be lowercase alphanumeric with hyphens")
5529
+ import { zValidator as zValidator8 } from "@hono/zod-validator";
5530
+ import { Hono as Hono24 } from "hono";
5531
+ import { z as z8 } from "zod";
5532
+ var createSchema2 = z8.object({
5533
+ name: z8.string().min(1).max(50).regex(/^[a-z0-9][a-z0-9-]*$/, "Channel names must be lowercase alphanumeric with hyphens")
6169
5534
  });
6170
- var inviteSchema = z6.object({
6171
- username: z6.string().min(1)
5535
+ var inviteSchema = z8.object({
5536
+ username: z8.string().min(1)
6172
5537
  });
6173
- var app21 = new Hono21().get("/", async (c) => {
5538
+ var app24 = new Hono24().get("/", async (c) => {
6174
5539
  const user = c.get("user");
6175
5540
  const channels = await listChannels();
6176
5541
  const results = await Promise.all(
@@ -6181,7 +5546,7 @@ var app21 = new Hono21().get("/", async (c) => {
6181
5546
  })
6182
5547
  );
6183
5548
  return c.json(results);
6184
- }).post("/", zValidator6("json", createSchema), async (c) => {
5549
+ }).post("/", zValidator8("json", createSchema2), async (c) => {
6185
5550
  const user = c.get("user");
6186
5551
  const body = c.req.valid("json");
6187
5552
  try {
@@ -6214,7 +5579,7 @@ var app21 = new Hono21().get("/", async (c) => {
6214
5579
  if (!ch) return c.json({ error: "Channel not found" }, 404);
6215
5580
  const participants = await getParticipants(ch.id);
6216
5581
  return c.json(participants);
6217
- }).post("/:name/invite", zValidator6("json", inviteSchema), async (c) => {
5582
+ }).post("/:name/invite", zValidator8("json", inviteSchema), async (c) => {
6218
5583
  const name = c.req.param("name");
6219
5584
  const inviter = c.get("user");
6220
5585
  const { username } = c.req.valid("json");
@@ -6234,24 +5599,27 @@ var app21 = new Hono21().get("/", async (c) => {
6234
5599
  ]);
6235
5600
  return c.json({ ok: true });
6236
5601
  });
6237
- var channels_default2 = app21;
5602
+ var channels_default2 = app24;
6238
5603
 
6239
5604
  // src/web/api/volute/chat.ts
6240
- import { zValidator as zValidator7 } from "@hono/zod-validator";
6241
- import { Hono as Hono22 } from "hono";
6242
- import { streamSSE as streamSSE4 } from "hono/streaming";
6243
- import { z as z7 } from "zod";
6244
- async function fanOutToMinds(opts) {
5605
+ import { zValidator as zValidator9 } from "@hono/zod-validator";
5606
+ import { Hono as Hono25 } from "hono";
5607
+ import { streamSSE as streamSSE6 } from "hono/streaming";
5608
+ import { z as z9 } from "zod";
5609
+ async function fanOutToMinds2(opts) {
6245
5610
  const participants = await getParticipants(opts.conversationId);
6246
5611
  const mindParticipants = participants.filter((p) => p.userType === "mind");
6247
5612
  const participantNames = participants.map((p) => p.username);
6248
5613
  const isDM = opts.isDM ?? participants.length === 2;
6249
5614
  const channelEntryType = opts.channelEntryType ?? (isDM ? "dm" : "group");
6250
- const { getMindManager: getMindManager2 } = await import("./mind-manager-3V2NXX4I.js");
5615
+ const { getMindManager: getMindManager2 } = await import("./mind-manager-P5OBDUKI.js");
5616
+ const { getSleepManagerIfReady } = await import("./sleep-manager-3RWUX2ZR.js");
6251
5617
  const manager = getMindManager2();
6252
- const runningMinds = mindParticipants.map((ap) => {
5618
+ const sm = getSleepManagerIfReady();
5619
+ const targetMinds = mindParticipants.map((ap) => {
6253
5620
  const key = opts.targetName ? opts.targetName(ap.username) : ap.username;
6254
- return manager.isRunning(key) ? ap.username : null;
5621
+ if (manager.isRunning(key) || sm?.isSleeping(ap.username)) return ap.username;
5622
+ return null;
6255
5623
  }).filter((n) => n !== null && n !== opts.senderName);
6256
5624
  function slugForMind(mindUsername) {
6257
5625
  return buildVoluteSlug({
@@ -6275,7 +5643,7 @@ async function fanOutToMinds(opts) {
6275
5643
  logger_default.warn(`failed to write channel entry for ${ap.username}`, logger_default.errorData(err));
6276
5644
  }
6277
5645
  }
6278
- for (const mindName of runningMinds) {
5646
+ for (const mindName of targetMinds) {
6279
5647
  const target = opts.targetName ? opts.targetName(mindName) : mindName;
6280
5648
  const channel = slugForMind(mindName);
6281
5649
  const typingMap = getTypingMap();
@@ -6293,18 +5661,18 @@ async function fanOutToMinds(opts) {
6293
5661
  });
6294
5662
  }
6295
5663
  }
6296
- var chatSchema = z7.object({
6297
- message: z7.string().optional(),
6298
- conversationId: z7.string().optional(),
6299
- sender: z7.string().optional(),
6300
- images: z7.array(
6301
- z7.object({
6302
- media_type: z7.string(),
6303
- data: z7.string()
5664
+ var chatSchema = z9.object({
5665
+ message: z9.string().optional(),
5666
+ conversationId: z9.string().optional(),
5667
+ sender: z9.string().optional(),
5668
+ images: z9.array(
5669
+ z9.object({
5670
+ media_type: z9.string(),
5671
+ data: z9.string()
6304
5672
  })
6305
5673
  ).optional()
6306
5674
  });
6307
- var app22 = new Hono22().post("/:name/chat", zValidator7("json", chatSchema), async (c) => {
5675
+ var app25 = new Hono25().post("/:name/chat", zValidator9("json", chatSchema), async (c) => {
6308
5676
  const name = c.req.param("name");
6309
5677
  const [baseName] = name.split("@", 2);
6310
5678
  const entry = findMind(baseName);
@@ -6362,7 +5730,7 @@ var app22 = new Hono22().post("/:name/chat", zValidator7("json", chatSchema), as
6362
5730
  }
6363
5731
  }
6364
5732
  await addMessage(conversationId, "user", senderName, contentBlocks);
6365
- await fanOutToMinds({
5733
+ await fanOutToMinds2({
6366
5734
  conversationId,
6367
5735
  contentBlocks,
6368
5736
  senderName,
@@ -6376,8 +5744,8 @@ var app22 = new Hono22().post("/:name/chat", zValidator7("json", chatSchema), as
6376
5744
  if (user.id !== 0 && !await isParticipantOrOwner(conversationId, user.id)) {
6377
5745
  return c.json({ error: "Conversation not found" }, 404);
6378
5746
  }
6379
- return streamSSE4(c, async (stream) => {
6380
- const unsubscribe = subscribe(conversationId, (event) => {
5747
+ return streamSSE6(c, async (stream) => {
5748
+ const unsubscribe = subscribe2(conversationId, (event) => {
6381
5749
  stream.writeSSE({ data: JSON.stringify(event) }).catch((err) => {
6382
5750
  if (!stream.aborted) console.error("[chat] SSE write error:", err);
6383
5751
  });
@@ -6387,23 +5755,23 @@ var app22 = new Hono22().post("/:name/chat", zValidator7("json", chatSchema), as
6387
5755
  if (!stream.aborted) console.error("[chat] SSE ping error:", err);
6388
5756
  });
6389
5757
  }, 15e3);
6390
- await new Promise((resolve23) => {
5758
+ await new Promise((resolve18) => {
6391
5759
  stream.onAbort(() => {
6392
5760
  unsubscribe();
6393
5761
  clearInterval(keepAlive);
6394
- resolve23();
5762
+ resolve18();
6395
5763
  });
6396
5764
  });
6397
5765
  });
6398
5766
  });
6399
- var unifiedChatSchema = z7.object({
6400
- message: z7.string().optional(),
6401
- conversationId: z7.string(),
6402
- images: z7.array(z7.object({ media_type: z7.string(), data: z7.string() })).optional()
5767
+ var unifiedChatSchema2 = z9.object({
5768
+ message: z9.string().optional(),
5769
+ conversationId: z9.string(),
5770
+ images: z9.array(z9.object({ media_type: z9.string(), data: z9.string() })).optional()
6403
5771
  });
6404
- var unifiedChatApp = new Hono22().post(
5772
+ var unifiedChatApp = new Hono25().post(
6405
5773
  "/chat",
6406
- zValidator7("json", unifiedChatSchema),
5774
+ zValidator9("json", unifiedChatSchema2),
6407
5775
  async (c) => {
6408
5776
  const user = c.get("user");
6409
5777
  const body = c.req.valid("json");
@@ -6425,7 +5793,7 @@ var unifiedChatApp = new Hono22().post(
6425
5793
  }
6426
5794
  await addMessage(body.conversationId, "user", senderName, contentBlocks);
6427
5795
  const isDM = conv.type === "dm";
6428
- await fanOutToMinds({
5796
+ await fanOutToMinds2({
6429
5797
  conversationId: body.conversationId,
6430
5798
  contentBlocks,
6431
5799
  senderName,
@@ -6437,18 +5805,18 @@ var unifiedChatApp = new Hono22().post(
6437
5805
  return c.json({ ok: true, conversationId: body.conversationId });
6438
5806
  }
6439
5807
  );
6440
- var chat_default = app22;
5808
+ var chat_default2 = app25;
6441
5809
 
6442
5810
  // src/web/api/volute/conversations.ts
6443
- import { zValidator as zValidator8 } from "@hono/zod-validator";
6444
- import { Hono as Hono23 } from "hono";
6445
- import { z as z8 } from "zod";
6446
- var createConvSchema = z8.object({
6447
- title: z8.string().optional(),
6448
- participantIds: z8.array(z8.number()).optional(),
6449
- participantNames: z8.array(z8.string()).optional()
5811
+ import { zValidator as zValidator10 } from "@hono/zod-validator";
5812
+ import { Hono as Hono26 } from "hono";
5813
+ import { z as z10 } from "zod";
5814
+ var createConvSchema = z10.object({
5815
+ title: z10.string().optional(),
5816
+ participantIds: z10.array(z10.number()).optional(),
5817
+ participantNames: z10.array(z10.string()).optional()
6450
5818
  });
6451
- var app23 = new Hono23().get("/:name/conversations", async (c) => {
5819
+ var app26 = new Hono26().get("/:name/conversations", async (c) => {
6452
5820
  const name = c.req.param("name");
6453
5821
  const user = c.get("user");
6454
5822
  let lookupId = user.id;
@@ -6459,7 +5827,7 @@ var app23 = new Hono23().get("/:name/conversations", async (c) => {
6459
5827
  const all = await listConversationsForUser(lookupId);
6460
5828
  const convs = all.filter((c2) => c2.mind_name === name || c2.type === "channel");
6461
5829
  return c.json(convs);
6462
- }).post("/:name/conversations", zValidator8("json", createConvSchema), async (c) => {
5830
+ }).post("/:name/conversations", zValidator10("json", createConvSchema), async (c) => {
6463
5831
  const name = c.req.param("name");
6464
5832
  const user = c.get("user");
6465
5833
  const body = c.req.valid("json");
@@ -6533,18 +5901,18 @@ var app23 = new Hono23().get("/:name/conversations", async (c) => {
6533
5901
  if (!deleted) return c.json({ error: "Conversation not found" }, 404);
6534
5902
  return c.json({ ok: true });
6535
5903
  });
6536
- var conversations_default = app23;
5904
+ var conversations_default2 = app26;
6537
5905
 
6538
5906
  // src/web/api/volute/user-conversations.ts
6539
- import { zValidator as zValidator9 } from "@hono/zod-validator";
6540
- import { Hono as Hono24 } from "hono";
6541
- import { streamSSE as streamSSE5 } from "hono/streaming";
6542
- import { z as z9 } from "zod";
6543
- var createSchema2 = z9.object({
6544
- title: z9.string().optional(),
6545
- participantNames: z9.array(z9.string()).min(1)
5907
+ import { zValidator as zValidator11 } from "@hono/zod-validator";
5908
+ import { Hono as Hono27 } from "hono";
5909
+ import { streamSSE as streamSSE7 } from "hono/streaming";
5910
+ import { z as z11 } from "zod";
5911
+ var createSchema3 = z11.object({
5912
+ title: z11.string().optional(),
5913
+ participantNames: z11.array(z11.string()).min(1)
6546
5914
  });
6547
- var app24 = new Hono24().use("*", authMiddleware).get("/", async (c) => {
5915
+ var app27 = new Hono27().use("*", authMiddleware).get("/", async (c) => {
6548
5916
  const user = c.get("user");
6549
5917
  const convs = await listConversationsWithParticipants(user.id);
6550
5918
  return c.json(convs);
@@ -6556,7 +5924,7 @@ var app24 = new Hono24().use("*", authMiddleware).get("/", async (c) => {
6556
5924
  }
6557
5925
  const msgs = await getMessages(id);
6558
5926
  return c.json(msgs);
6559
- }).post("/", zValidator9("json", createSchema2), async (c) => {
5927
+ }).post("/", zValidator11("json", createSchema3), async (c) => {
6560
5928
  const user = c.get("user");
6561
5929
  const body = c.req.valid("json");
6562
5930
  const participantIds = /* @__PURE__ */ new Set();
@@ -6592,8 +5960,8 @@ var app24 = new Hono24().use("*", authMiddleware).get("/", async (c) => {
6592
5960
  if (user.id !== 0 && !await isParticipantOrOwner(conversationId, user.id)) {
6593
5961
  return c.json({ error: "Conversation not found" }, 404);
6594
5962
  }
6595
- return streamSSE5(c, async (stream) => {
6596
- const unsubscribe = subscribe(conversationId, (event) => {
5963
+ return streamSSE7(c, async (stream) => {
5964
+ const unsubscribe = subscribe2(conversationId, (event) => {
6597
5965
  stream.writeSSE({ data: JSON.stringify(event) }).catch((err) => {
6598
5966
  if (!stream.aborted) console.error("[chat] SSE write error:", err);
6599
5967
  });
@@ -6603,11 +5971,11 @@ var app24 = new Hono24().use("*", authMiddleware).get("/", async (c) => {
6603
5971
  if (!stream.aborted) console.error("[chat] SSE ping error:", err);
6604
5972
  });
6605
5973
  }, 15e3);
6606
- await new Promise((resolve23) => {
5974
+ await new Promise((resolve18) => {
6607
5975
  stream.onAbort(() => {
6608
5976
  unsubscribe();
6609
5977
  clearInterval(keepAlive);
6610
- resolve23();
5978
+ resolve18();
6611
5979
  });
6612
5980
  });
6613
5981
  });
@@ -6618,12 +5986,12 @@ var app24 = new Hono24().use("*", authMiddleware).get("/", async (c) => {
6618
5986
  if (!deleted) return c.json({ error: "Conversation not found" }, 404);
6619
5987
  return c.json({ ok: true });
6620
5988
  });
6621
- var user_conversations_default = app24;
5989
+ var user_conversations_default = app27;
6622
5990
 
6623
5991
  // src/web/app.ts
6624
5992
  var httpLog = logger_default.child("http");
6625
- var app25 = new Hono25();
6626
- app25.onError((err, c) => {
5993
+ var app28 = new Hono28();
5994
+ app28.onError((err, c) => {
6627
5995
  if (err instanceof HTTPException) {
6628
5996
  return err.getResponse();
6629
5997
  }
@@ -6634,10 +6002,10 @@ app25.onError((err, c) => {
6634
6002
  });
6635
6003
  return c.json({ error: "Internal server error" }, 500);
6636
6004
  });
6637
- app25.notFound((c) => {
6005
+ app28.notFound((c) => {
6638
6006
  return c.json({ error: "Not found" }, 404);
6639
6007
  });
6640
- app25.use("*", async (c, next) => {
6008
+ app28.use("*", async (c, next) => {
6641
6009
  const start = Date.now();
6642
6010
  await next();
6643
6011
  const duration = Date.now() - start;
@@ -6648,7 +6016,7 @@ app25.use("*", async (c, next) => {
6648
6016
  httpLog.debug("request", data);
6649
6017
  }
6650
6018
  });
6651
- app25.get("/api/health", (c) => {
6019
+ app28.get("/api/health", (c) => {
6652
6020
  let version = "unknown";
6653
6021
  let cached = null;
6654
6022
  try {
@@ -6663,19 +6031,35 @@ app25.get("/api/health", (c) => {
6663
6031
  ...cached?.updateAvailable ? { updateAvailable: true, latest: cached.latest } : {}
6664
6032
  });
6665
6033
  });
6666
- app25.use("/api/*", bodyLimit({ maxSize: 10 * 1024 * 1024 }));
6667
- app25.use("/api/*", csrf());
6668
- app25.use("/api/activity/*", authMiddleware);
6669
- app25.use("/api/minds/*", authMiddleware);
6670
- app25.use("/api/conversations/*", authMiddleware);
6671
- app25.use("/api/volute/*", authMiddleware);
6672
- app25.use("/api/system/*", authMiddleware);
6673
- app25.use("/api/env/*", authMiddleware);
6674
- app25.use("/api/prompts/*", authMiddleware);
6675
- app25.use("/api/skills/*", authMiddleware);
6676
- app25.route("/pages", pages_default);
6677
- var routes = app25.route("/api/activity", activity_default).route("/api/keys", keys_default).route("/api/auth", auth_default).route("/api/system", system_default).route("/api/system", update_default).route("/api/minds", minds_default).route("/api/minds", chat_default).route("/api/minds", connectors_default).route("/api/minds", schedules_default).route("/api/minds", logs_default).route("/api/minds", typing_default).route("/api/minds", variants_default).route("/api/minds", file_sharing_default).route("/api/minds", files_default).route("/api/minds", channels_default).route("/api/minds", shared_default).route("/api/minds", env_default).route("/api/minds", mind_skills_default).route("/api/minds", conversations_default).route("/api/env", sharedEnvApp).route("/api/prompts", prompts_default).route("/api/skills", skills_default).route("/api/conversations", user_conversations_default).route("/api/volute/channels", channels_default2).route("/api/volute", unifiedChatApp);
6678
- var app_default = app25;
6034
+ app28.use("/api/*", bodyLimit({ maxSize: 10 * 1024 * 1024 }));
6035
+ app28.use("/api/*", csrf());
6036
+ app28.use("/api/activity/*", authMiddleware);
6037
+ app28.use("/api/minds/*", authMiddleware);
6038
+ app28.use("/api/conversations/*", authMiddleware);
6039
+ app28.use("/api/volute/*", authMiddleware);
6040
+ app28.use("/api/system/*", authMiddleware);
6041
+ app28.use("/api/env/*", authMiddleware);
6042
+ app28.use("/api/prompts/*", authMiddleware);
6043
+ app28.use("/api/skills/*", authMiddleware);
6044
+ app28.use("/api/v1/*", authMiddleware);
6045
+ app28.route("/pages", pages_default);
6046
+ var routes = app28.route("/api/activity", activity_default).route("/api/keys", keys_default).route("/api/auth", auth_default).route("/api/system", system_default).route("/api/system", update_default).route("/api/minds", minds_default).route("/api/minds", chat_default2).route("/api/minds", connectors_default).route("/api/minds", schedules_default).route("/api/minds", logs_default).route("/api/minds", typing_default).route("/api/minds", variants_default).route("/api/minds", file_sharing_default).route("/api/minds", files_default).route("/api/minds", channels_default).route("/api/minds", shared_default).route("/api/minds", env_default).route("/api/minds", mind_skills_default).route("/api/minds", conversations_default2).route("/api/env", sharedEnvApp).route("/api/prompts", prompts_default).route("/api/skills", skills_default).route("/api/conversations", user_conversations_default).route("/api/volute/channels", channels_default2).route("/api/volute", unifiedChatApp).route("/api/v1/conversations", conversations_default).route("/api/v1/events", events_default).route("/api/v1", chat_default);
6047
+ app28.route("/api/v1/minds", minds_default);
6048
+ app28.route("/api/v1/minds", typing_default);
6049
+ app28.route("/api/v1/minds", variants_default);
6050
+ app28.route("/api/v1/minds", files_default);
6051
+ app28.route("/api/v1/minds", env_default);
6052
+ app28.route("/api/v1/minds", mind_skills_default);
6053
+ app28.route("/api/v1/minds", connectors_default);
6054
+ app28.route("/api/v1/minds", schedules_default);
6055
+ app28.route("/api/v1/minds", logs_default);
6056
+ app28.route("/api/v1/system", system_default);
6057
+ app28.route("/api/v1/system", update_default);
6058
+ app28.route("/api/v1/prompts", prompts_default);
6059
+ app28.route("/api/v1/skills", skills_default);
6060
+ app28.route("/api/v1/env", sharedEnvApp);
6061
+ app28.route("/api/v1/channels", channels_default2);
6062
+ var app_default = app28;
6679
6063
 
6680
6064
  // src/web/server.ts
6681
6065
  var MIME_TYPES2 = {
@@ -6692,20 +6076,20 @@ async function startServer({
6692
6076
  hostname = "127.0.0.1"
6693
6077
  }) {
6694
6078
  let assetsDir = "";
6695
- let searchDir = dirname2(new URL(import.meta.url).pathname);
6079
+ let searchDir = dirname(new URL(import.meta.url).pathname);
6696
6080
  for (let i = 0; i < 5; i++) {
6697
- const candidate = resolve21(searchDir, "dist", "web-assets");
6698
- if (existsSync15(candidate)) {
6081
+ const candidate = resolve16(searchDir, "dist", "web-assets");
6082
+ if (existsSync12(candidate)) {
6699
6083
  assetsDir = candidate;
6700
6084
  break;
6701
6085
  }
6702
- searchDir = dirname2(searchDir);
6086
+ searchDir = dirname(searchDir);
6703
6087
  }
6704
6088
  if (assetsDir) {
6705
6089
  app_default.get("*", async (c) => {
6706
6090
  const urlPath = new URL(c.req.url).pathname;
6707
6091
  if (urlPath.startsWith("/api/")) return c.notFound();
6708
- const filePath = resolve21(assetsDir, urlPath.slice(1));
6092
+ const filePath = resolve16(assetsDir, urlPath.slice(1));
6709
6093
  if (!filePath.startsWith(assetsDir)) return c.text("Forbidden", 403);
6710
6094
  const s = await stat3(filePath).catch(() => null);
6711
6095
  if (s?.isFile()) {
@@ -6714,7 +6098,7 @@ async function startServer({
6714
6098
  const body = await readFile3(filePath);
6715
6099
  return c.body(body, 200, { "Content-Type": mime });
6716
6100
  }
6717
- const indexPath = resolve21(assetsDir, "index.html");
6101
+ const indexPath = resolve16(assetsDir, "index.html");
6718
6102
  const indexStat = await stat3(indexPath).catch(() => null);
6719
6103
  if (indexStat?.isFile()) {
6720
6104
  const body = await readFile3(indexPath, "utf-8");
@@ -6724,10 +6108,10 @@ async function startServer({
6724
6108
  });
6725
6109
  }
6726
6110
  const server = serve({ fetch: app_default.fetch, port, hostname });
6727
- await new Promise((resolve23, reject) => {
6111
+ await new Promise((resolve18, reject) => {
6728
6112
  server.on("listening", () => {
6729
6113
  logger_default.info("Volute UI running", { hostname, port });
6730
- resolve23();
6114
+ resolve18();
6731
6115
  });
6732
6116
  server.on("error", (err) => {
6733
6117
  reject(err);
@@ -6738,14 +6122,17 @@ async function startServer({
6738
6122
 
6739
6123
  // src/daemon.ts
6740
6124
  if (!process.env.VOLUTE_HOME) {
6741
- process.env.VOLUTE_HOME = resolve22(homedir2(), ".volute");
6125
+ process.env.VOLUTE_HOME = resolve17(homedir2(), ".volute");
6126
+ }
6127
+ if (process.env.VOLUTE_TIMEZONE && !process.env.TZ) {
6128
+ process.env.TZ = process.env.VOLUTE_TIMEZONE;
6742
6129
  }
6743
6130
  async function startDaemon(opts) {
6744
6131
  const { port, hostname } = opts;
6745
6132
  const myPid = String(process.pid);
6746
6133
  const home = voluteHome();
6747
6134
  if (!opts.foreground) {
6748
- const rotatingLog = new RotatingLog(resolve22(home, "daemon.log"));
6135
+ const rotatingLog = new RotatingLog(resolve17(home, "daemon.log"));
6749
6136
  logger_default.setOutput((line) => rotatingLog.write(`${line}
6750
6137
  `));
6751
6138
  const write = (...args) => rotatingLog.write(`${format(...args)}
@@ -6755,9 +6142,9 @@ async function startDaemon(opts) {
6755
6142
  console.warn = write;
6756
6143
  console.info = write;
6757
6144
  }
6758
- const DAEMON_PID_PATH = resolve22(home, "daemon.pid");
6759
- const DAEMON_JSON_PATH = resolve22(home, "daemon.json");
6760
- mkdirSync10(home, { recursive: true });
6145
+ const DAEMON_PID_PATH = resolve17(home, "daemon.pid");
6146
+ const DAEMON_JSON_PATH = resolve17(home, "daemon.json");
6147
+ mkdirSync8(home, { recursive: true });
6761
6148
  migrateAgentsToMinds();
6762
6149
  try {
6763
6150
  await ensureSharedRepo();
@@ -6785,8 +6172,8 @@ async function startDaemon(opts) {
6785
6172
  }
6786
6173
  throw err;
6787
6174
  }
6788
- writeFileSync10(DAEMON_PID_PATH, myPid, { mode: 420 });
6789
- writeFileSync10(DAEMON_JSON_PATH, `${JSON.stringify({ port, hostname, token }, null, 2)}
6175
+ writeFileSync8(DAEMON_PID_PATH, myPid, { mode: 420 });
6176
+ writeFileSync8(DAEMON_JSON_PATH, `${JSON.stringify({ port, hostname, token }, null, 2)}
6790
6177
  `, {
6791
6178
  mode: 420
6792
6179
  });
@@ -6800,6 +6187,9 @@ async function startDaemon(opts) {
6800
6187
  mailPoller.start();
6801
6188
  const tokenBudget = initTokenBudget();
6802
6189
  tokenBudget.start();
6190
+ const sleepManager = initSleepManager();
6191
+ sleepManager.start();
6192
+ const unsubscribeWebhook = initWebhook();
6803
6193
  const registry = readRegistry();
6804
6194
  for (const entry of registry) {
6805
6195
  try {
@@ -6815,6 +6205,20 @@ async function startDaemon(opts) {
6815
6205
  const workers = Array.from({ length: Math.min(5, queue.length) }, async () => {
6816
6206
  while (queue.length > 0) {
6817
6207
  const entry = queue.shift();
6208
+ if (sleepManager.isSleeping(entry.name)) {
6209
+ try {
6210
+ const dir = mindDir(entry.name);
6211
+ const daemonPort = process.env.VOLUTE_DAEMON_PORT ? parseInt(process.env.VOLUTE_DAEMON_PORT, 10) : void 0;
6212
+ await connectors.startConnectors(entry.name, dir, entry.port, daemonPort);
6213
+ scheduler.loadSchedules(entry.name);
6214
+ } catch (err) {
6215
+ logger_default.error(
6216
+ `failed to start connectors for sleeping mind ${entry.name}`,
6217
+ logger_default.errorData(err)
6218
+ );
6219
+ }
6220
+ continue;
6221
+ }
6818
6222
  try {
6819
6223
  await startMindFull(entry.name);
6820
6224
  } catch (err) {
@@ -6842,8 +6246,15 @@ async function startDaemon(opts) {
6842
6246
  });
6843
6247
  await Promise.all(workers);
6844
6248
  }
6249
+ import("./cloud-sync-C6WRYRVR.js").then(
6250
+ ({ consumeQueuedMessages }) => consumeQueuedMessages().catch((err) => {
6251
+ logger_default.warn("failed to consume queued cloud messages", logger_default.errorData(err));
6252
+ })
6253
+ ).catch((err) => {
6254
+ logger_default.warn("failed to load cloud-sync module", logger_default.errorData(err));
6255
+ });
6845
6256
  try {
6846
- const { backfillTemplateHashes, notifyVersionUpdate } = await import("./version-notify-TFS2U5CF.js");
6257
+ const { backfillTemplateHashes, notifyVersionUpdate } = await import("./version-notify-5FGUAVSF.js");
6847
6258
  backfillTemplateHashes();
6848
6259
  notifyVersionUpdate().catch((err) => {
6849
6260
  logger_default.warn("failed to send version update notifications", logger_default.errorData(err));
@@ -6860,15 +6271,15 @@ async function startDaemon(opts) {
6860
6271
  logger_default.info(`running on ${hostname}:${port}, pid ${myPid}`);
6861
6272
  function cleanup() {
6862
6273
  try {
6863
- if (readFileSync13(DAEMON_PID_PATH, "utf-8").trim() === myPid) {
6864
- unlinkSync2(DAEMON_PID_PATH);
6274
+ if (readFileSync10(DAEMON_PID_PATH, "utf-8").trim() === myPid) {
6275
+ unlinkSync(DAEMON_PID_PATH);
6865
6276
  }
6866
6277
  } catch {
6867
6278
  }
6868
6279
  try {
6869
- const data = JSON.parse(readFileSync13(DAEMON_JSON_PATH, "utf-8"));
6280
+ const data = JSON.parse(readFileSync10(DAEMON_JSON_PATH, "utf-8"));
6870
6281
  if (data.token === token) {
6871
- unlinkSync2(DAEMON_JSON_PATH);
6282
+ unlinkSync(DAEMON_JSON_PATH);
6872
6283
  }
6873
6284
  } catch {
6874
6285
  }
@@ -6890,6 +6301,9 @@ async function startDaemon(opts) {
6890
6301
  try {
6891
6302
  safe("stopAllWatchers", stopAllWatchers);
6892
6303
  safe("stopAllActivityTrackers", stopAll);
6304
+ safe("unsubscribeWebhook", unsubscribeWebhook);
6305
+ safe("sleepManager.stop", () => sleepManager.stop());
6306
+ safe("sleepManager.saveState", () => sleepManager.saveState());
6893
6307
  safe("scheduler.stop", () => scheduler.stop());
6894
6308
  safe("scheduler.saveState", () => scheduler.saveState());
6895
6309
  safe("mailPoller.stop", () => mailPoller.stop());